diff --git a/assets/src/addie.tsx b/assets/src/addie.tsx new file mode 100644 index 00000000..e02f9a81 --- /dev/null +++ b/assets/src/addie.tsx @@ -0,0 +1,8 @@ +import '@/styles/index.css' +import 'vite/modulepreload-polyfill' + +import { render } from 'preact' + +import Addie from '@/screens/addie/Addie' + +render(, document.getElementById('app')!) diff --git a/assets/src/api/api.ts b/assets/src/api/api.ts index 11731250..35644a82 100644 --- a/assets/src/api/api.ts +++ b/assets/src/api/api.ts @@ -3,8 +3,17 @@ import { encode } from 'base64-arraybuffer' import { config, OAuthProvider } from '@/config' import { - AuthToken, AuthTokenPair, File as LNFile, FileType, OAuthToken, Period, Project, ProjectRole, - Task, Team, User + AuthToken, + AuthTokenPair, + File as LNFile, + FileType, + OAuthToken, + Period, + Project, + ProjectRole, + Task, + Team, + User, } from '@/models' import { AsyncPromise, logger } from '@/utils' @@ -428,6 +437,15 @@ class APIService { return response.data } + async generateChat( + messages: { role: string; content: string }[] + ): Promise<{ response: string; status: number }> { + const response = await this.axios.post(`${this.endpoint}/generate/addie`, { + messages, + }) + return { response: response.data, status: response.status } + } + // misc async githash(): Promise<{ hash: string }> { diff --git a/assets/src/components/journal/DailyPrompt.tsx b/assets/src/components/journal/DailyPrompt.tsx index 1a21245a..9b97627b 100644 --- a/assets/src/components/journal/DailyPrompt.tsx +++ b/assets/src/components/journal/DailyPrompt.tsx @@ -13,7 +13,6 @@ const prompts = [ ' “If there are nine rabbits on the ground, if you want to catch one, just focus on one.” – Jack Ma', '“You may delay, but time will not.” – Benjamin Franklin', '“The tragedy in life doesn’t lie in not reaching your goal. The tragedy lies in having no goal to reach.” – Benjamin E. Mays', - '“If you are interested in balancing work and pleasure, stop trying to balance them. Instead make your work more pleasurable.” – Donald Trump', '“Both good and bad days should end with productivity. Your mood affairs should never influence your work.” – Greg Evans', '“When one has much to put into them, a day has a hundred pockets.” – Friedrich Nietzsche', '“Focus on being productive instead of busy.” -Tim Ferriss', diff --git a/assets/src/config/paths.ts b/assets/src/config/paths.ts index d423d8c5..776b1ecc 100644 --- a/assets/src/config/paths.ts +++ b/assets/src/config/paths.ts @@ -40,4 +40,8 @@ export const paths = { INSIGHT_DOC: '/insight/docs', INSIGHT_SETTINGS: '/insight/settings', + + // --- addie + + ADDIE: '/addie', } diff --git a/assets/src/images/therapist.png b/assets/src/images/therapist.png new file mode 100644 index 00000000..4a1607f9 Binary files /dev/null and b/assets/src/images/therapist.png differ diff --git a/assets/src/models/index.ts b/assets/src/models/index.ts index 391c7efb..c16399da 100644 --- a/assets/src/models/index.ts +++ b/assets/src/models/index.ts @@ -1,6 +1,7 @@ export * from './daily_note' export * from './doc' export * from './file' +export * from './message' export * from './oauth_token' export * from './project' export * from './task' diff --git a/assets/src/models/message.ts b/assets/src/models/message.ts new file mode 100644 index 00000000..df416690 --- /dev/null +++ b/assets/src/models/message.ts @@ -0,0 +1,13 @@ +export enum Author { + BOT, + YOU, +} + +export class Message { + constructor(public from: Author, public text: string) {} +} + +export type GPTMessage = { + role: 'assistant' | 'user' | 'system' + content: string +} diff --git a/assets/src/screens/addie/Addie.tsx b/assets/src/screens/addie/Addie.tsx new file mode 100644 index 00000000..22b95df5 --- /dev/null +++ b/assets/src/screens/addie/Addie.tsx @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'preact/hooks' + +import Button from '@/components/core/Button' +import Loader from '@/components/core/Loader' +import { paths } from '@/config' +import ChatMain from '@/screens/addie/ChatMain' +import { authStore } from '@/stores/authStore' +import tracker from '@/stores/tracker' +import { uiStore } from '@/stores/uiStore' +import { useStore } from '@nanostores/preact' + +export default () => { + const user = useStore(authStore.loggedInUser) + + useEffect(() => { + uiStore.insightLoop = true + if (user === undefined) authStore.init() + else if (user === null) + location.href = paths.SIGNUP + '?path=' + location.pathname + location.search + else { + uiStore.initLoggedInUser(user) + tracker.openAddie() + } + }, [user]) + + if (!user) + return ( +
+ + +
+ ) + + return ( +
+ +
+ ) +} diff --git a/assets/src/screens/addie/ChatMain.tsx b/assets/src/screens/addie/ChatMain.tsx new file mode 100644 index 00000000..0547866f --- /dev/null +++ b/assets/src/screens/addie/ChatMain.tsx @@ -0,0 +1,177 @@ +import { useEffect, useRef, useState } from 'preact/hooks' + +import Input from '@/components/core/Input' +import Pressable from '@/components/core/Pressable' +import therapist from '@/images/therapist.png' +import { Author, Message } from '@/models' +import addieScript from '@/screens/addie/addieScript' +import { addieStore } from '@/stores/addieStore' +import { useStore } from '@nanostores/preact' + +export default () => { + useEffect(() => { + addieStore.resetConversation() + }, []) + + return ( +
+
+ + addieStore.resetConversation()} tooltip="Start Over"> + Start Over + +
+ + + + +
+ ) +} + +function Messages() { + const divRef = useRef(null) + const messages = useStore(addieStore.messages) + + useEffect(() => { + const div = divRef.current + if (!div) return + const lastMessage = div.children[div.children.length - 1] + if (lastMessage) lastMessage.scrollIntoView() + }, [messages]) + + return ( +
+ {messages.map((message, i) => { + const props = { message, prevMessage: messages[i - 1], key: i } + return message.from == Author.BOT ? : + })} +
+ ) +} + +function AwaitingResponse() { + const awaitingResponse = useStore(addieStore.awaitingResponse) + + if (!awaitingResponse) return null + + return ( +
+
+
+
+
+ ) +} + +type MessageProps = { message: Message; prevMessage: Message | undefined } + +function BotMessage({ message, prevMessage }: MessageProps) { + return ( +
+
{message.text}
+
+ ) +} + +function UserMessage({ message }: MessageProps) { + return ( +
+
{message.text}
+
+ ) +} + +function Response() { + const divRef = useRef(null) + const response = useStore(addieStore.response) + + useEffect(() => { + if (divRef.current) divRef.current.scrollIntoView() + }, [response]) + + if (!response) return null + + if (response.kind == 'end') { + return ( +
+ If you have additional feedback, send it to Tim (tim@daybird.app). + addieStore.resetConversation()}> + Start Over? + +
+ ) + } + + return ( + <> + {(response.kind == 'buttons' || response.kind == 'buttons_text') && ( + <> +
+ {response.buttons?.map((button, i) => ( + + ))} +
+ + )} + + {response.kind == 'buttons_text' &&
} + + {(response.kind == 'text' || response.kind == 'buttons_text') && ( +
+ +
+ )} + + ) +} + +function TextResponse() { + const response = useStore(addieStore.response) + const [textInput, setTextInput] = useState('') + + const handleSubmit = (e: Event) => { + e.preventDefault() + if (textInput.trim().length == 0) return + + addieStore.addUserMessage(textInput) + addieScript.handleInput(textInput) + setTextInput('') + } + + return ( +
+ ref && ref.focus()} + value={textInput} + placeholder={response?.placeholder || 'Type your response here'} + onChange={(e) => setTextInput((e.target as HTMLInputElement).value)} + className={`appearance-none block w-full px-3 py-2 border border-gray-300 + rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 + focus:border-blue-500 text-sm`} + /> +
+ ) +} + +function ErrorMessage() { + const error = useStore(addieStore.error) + if (!error) return null + + return ( + + ) +} diff --git a/assets/src/screens/addie/HeadlessEditor.tsx b/assets/src/screens/addie/HeadlessEditor.tsx new file mode 100644 index 00000000..55bfc5a1 --- /dev/null +++ b/assets/src/screens/addie/HeadlessEditor.tsx @@ -0,0 +1,37 @@ +import { decode } from 'base64-arraybuffer' +import * as Y from 'yjs' + +import { Project } from '@/models' +import { docStore } from '@/stores/docStore' +import { Editor } from '@tiptap/core' +import Collaboration from '@tiptap/extension-collaboration' +import StarterKit from '@tiptap/starter-kit' + +export const loadDoc = async (project: Project, id: string, ydoc: Y.Doc) => { + await docStore.loadDoc(project, id) + const contents = docStore.doc.get()?.contents + if (contents) { + const array = new Uint8Array(decode(contents)) + Y.applyUpdate(ydoc, array) + } + return ydoc +} + +export const createEditor = (ydoc: Y.Doc) => { + const editor = new Editor({ + extensions: [ + StarterKit.configure({ + // The Collaboration extension comes with its own history handling + history: false, + }), + Collaboration.configure({ + document: ydoc, + }), + ], + }) + return editor +} + +export const convertToDoc = (ydoc: Y.Doc) => { + return Y.encodeStateAsUpdate(ydoc) +} diff --git a/assets/src/screens/addie/addieScript.ts b/assets/src/screens/addie/addieScript.ts new file mode 100644 index 00000000..9a0bc397 --- /dev/null +++ b/assets/src/screens/addie/addieScript.ts @@ -0,0 +1,647 @@ +import { decode } from 'base64-arraybuffer' +import { prosemirrorToYDoc, yDocToProsemirrorJSON } from 'y-prosemirror' +import * as Y from 'yjs' + +import { API } from '@/api' +import { config } from '@/config' +import { DailyNote, dateToPeriodDateString, Doc, GPTMessage, Period } from '@/models' +import { createEditor, loadDoc } from '@/screens/addie/HeadlessEditor' +import { addieStore, UserResponse } from '@/stores/addieStore' +import { authStore } from '@/stores/authStore' +import { docStore } from '@/stores/docStore' +import { journalStore } from '@/stores/journalStore' +import { projectStore } from '@/stores/projectStore' +import tracker from '@/stores/tracker' +import { assertIsDefined, unwrapError } from '@/utils' +import { Editor } from '@tiptap/core' + +const LS_SEEN_BEFORE = 'addie-seen-before' + +type ButtonHandler = (index: number) => void +type InputHandler = (input: string) => void + +const coachDescription = `friendly ADHD coach who gives 1-paragraph answers` +const datePreamble = () => `It's ${new Date().toLocaleString()}.` + +class AddieScript { + messageHistory: GPTMessage[] = [] + + buttonHandler: ButtonHandler = () => {} + inputHandler: InputHandler = () => {} + + seenMenu = false + + setUserResponse = ( + response: UserResponse, + buttonHandler: ButtonHandler | null, + inputHandler: InputHandler | null + ) => { + addieStore.setResponse(response) + if (buttonHandler) this.buttonHandler = buttonHandler + if (inputHandler) this.inputHandler = inputHandler + } + + // --- welcome + + welcome = async () => { + this.seenMenu = false + if (!localStorage.getItem(LS_SEEN_BEFORE)) { + await addieStore.addBotMessage(`Hi! I am Addie, your personal ADHD assistant. + +Visit me any time you need help.`) + localStorage.setItem(LS_SEEN_BEFORE, 'true') + } else { + const user = authStore.loggedInUser.get()! + await addieStore.addBotMessage(user.name ? `Hi again, ${user.name}!` : `Hi again!`) + } + + this.mainMenu() + } + + mainMenu = async () => { + await addieStore.addBotMessage(`What can I help you with today?`) + + const buttons = ['What should I do?', 'Journal', 'Advice'] + if (this.seenMenu) buttons.push('All done') + else this.seenMenu = true + + this.setUserResponse( + { + kind: 'buttons', + buttons: buttons, + }, + this.handleMainMenu, + null + ) + } + + handleMainMenu = async (index: number) => { + if (index == 0) { + this.doCheckin() + // } else if (index == 1) { + // this.doRemember() + } else if (index == 1) { + this.doJournal() + } else if (index == 2) { + this.doHelp() + } else { + this.finishConversation() + } + } + + // + + askAboutTasks = async () => { + await addieStore.addBotMessage(`What tasks do you need to do today?`) + + this.setUserResponse( + { + kind: 'text', + }, + null, + this.handleTasks + ) + } + + handleTasks = async (input: string) => { + this.gptLoop(input, ['Ready to continue.']) + } + + // --- attend to self + + doCheckin = async () => { + tracker.addieEvent('checkin') + await addieStore.addBotMessage(`Let's start with an emotional check-in. + +Take a deep breath and close your eyes. How are you feeling right now?`) + + this.setUserResponse( + { + kind: 'buttons', + buttons: ['Calm & Present', 'On Autopilot', 'Unwell'], + }, + this.handleEmotionalCheckin, + null + ) + } + + handleEmotionalCheckin = async (index: number) => { + if (index == 0) { + this.timeOfDayCheck() + } else if (index == 1) { + await addieStore.addBotMessage(`Take a moment to be present to yourself and your needs.`) + await new Promise((resolve) => setTimeout(resolve, 4000)) + this.timeOfDayCheck() + } else if (index == 2) { + this.attendToSelf() + } + } + + attendToSelf = async () => { + tracker.addieEvent('attendSelf') + await addieStore.addBotMessage(`I'm sorry to hear that. Take a moment to attend to yourself. + +What do you need right now?`) + + this.setUserResponse( + { + kind: 'text', + }, + null, + this.handleAttendToSelf + ) + + this.messageHistory = [ + { + role: 'system', + content: `You are a ${coachDescription} helping a user who is not feeling well to ' + + 'feel ready for coaching. ${datePreamble()}`, + }, + { + role: 'assistant', + content: 'Take a moment to attend to yourself. What do you need right now?', + }, + ] + } + + handleAttendToSelf = async (input: string) => { + this.gptLoop(input, ['Ready to continue.']) + } + + // --- time of day check + + timeOfDayCheck = async () => { + const date = new Date() + const dayOfWeek = date.getDay() + const hour = new Date().getHours() + const isWeekend = dayOfWeek == 0 || dayOfWeek == 6 + + if (hour > 22 || hour < 5) { + this.bedtimeRoutine() + } else if (hour > 17 || hour < 8) { + this.homeRoutine() + } else if (isWeekend) { + this.weekendRoutine() + } else { + this.workRoutine() + } + } + + // --- bedtime + + bedtimeRoutine = async () => { + tracker.addieEvent('bedtime') + await addieStore.addBotMessage(`It's bedtime! Let's get ready for bed. + +Are you sleepy?`) + + this.setUserResponse( + { + kind: 'buttons', + buttons: ['Yes', 'No', "I'm not ready"], + }, + this.handleBedtime, + null + ) + } + + handleBedtime = async (index: number) => { + if (index == 0) { + await addieStore.addBotMessage(`Great! Let's get to bed.`) + + await addieStore.addBotMessage(`1. Put away your electronics (like this one). + +2. Get into bed. Turn on your white noise machine (if you have one). + +3. Close your eyes and take 3 deep breaths. + +4. I'll see you tomorrow.`) + + this.finishConversation() + } else if (index == 1) { + await addieStore.addBotMessage(`Okay, let's get you ready for bed. + +It's perfectly normal not to be sleepy yet. People with ADHD typically have a later circadian rhythm and have a harder time falling asleep as well.`) + + await addieStore.addBotMessage( + `Pick an activity that's relaxing and calming and does not involve screens.` + ) + + this.setUserResponse( + { + kind: 'text', + }, + null, + this.handleBedtimeText + ) + } else if (index == 2) { + await addieStore.addBotMessage( + `Okay, but remember that sleep is extra important for people with ADHD.` + ) + this.homeRoutine() + } else if (index == 3) { + this.workRoutine() + } + } + + handleBedtimeText = async (input: string) => { + await addieStore.addBotMessage(`That sounds like a great idea!`) + await addieStore.addBotMessage('Go do that. Then come back and we can get ready for bed.') + + this.setUserResponse( + { + kind: 'buttons', + buttons: ["I'm ready."], + }, + this.handleBedtime, + null + ) + } + + // --- work / home routines + + homeRoutine = async () => { + tracker.addieEvent('homeRoutine') + await addieStore.addBotMessage(`What are your important things to do right now?`) + + this.setUserResponse( + { + kind: 'text', + }, + this.handleHomeButton, + this.handleHomeRoutine + ) + + this.messageHistory = [ + { + role: 'system', + content: `You are a ${coachDescription} helping the user focus and get tasks done. ${datePreamble()}`, + }, + { + role: 'assistant', + content: 'What are your important things to do at home?', + }, + ] + } + + handleHomeRoutine = async (input: string) => { + this.gptLoop(input, ['All Done']) + } + + handleHomeButton = async (index: number) => { + this.finishConversation() + } + + workRoutine = async () => { + tracker.addieEvent('workRoutine') + await addieStore.addBotMessage( + `You're probably at work. First things first. Check your calendar and see when your next meeting is` + ) + + this.setUserResponse( + { + kind: 'buttons', + buttons: ['Soon', 'Less than an hour', 'In a few hours', 'No meetings left'], + }, + this.handleWorkRoutine, + null + ) + } + + handleWorkRoutine = async (index: number) => { + if (index == 0) { + await addieStore.addBotMessage(`Get prepared for it and don't be late.`) + this.finishConversation() + return + } else if (index == 1) { + await addieStore.addBotMessage( + `You don't have too much time, so let's work on a few small tasks.` + ) + this.workContext = 'I have a meeting in less than an hour.' + this.handleWorkRoutineText('') + } else { + await addieStore.addBotMessage( + `You've got a good chunk of time to do some deep work. What's important?` + ) + this.workContext = 'I have a few hours for deep work.' + this.handleWorkRoutineText('') + } + + this.workTasks = [] + + this.setUserResponse( + { + kind: 'text', + placeholder: 'What do you want to do?', + }, + this.handleWorkRoutineButton, + this.handleWorkRoutineText + ) + } + + workTasks: string[] = [] + workContext: string | undefined + + handleWorkRoutineText = async (input: string) => { + this.workTasks.push(input) + + this.setUserResponse( + { + kind: 'buttons_text', + placeholder: 'Anything else?', + buttons: ['Next'], + }, + this.handleWorkRoutineButton, + this.handleWorkRoutineText + ) + } + + handleWorkRoutineButton = async (input: number) => { + await addieStore.addBotMessage( + `That sounds like a great idea! Do you want to talk it through, ` + + `or are you ready to do it?` + ) + + this.setUserResponse( + { + kind: 'buttons_text', + buttons: ['Ready to work', 'Talk it through'], + }, + this.handleTaskTalkButtons, + this.handleTaskTalkText + ) + } + + weekendRoutine = async () => { + tracker.addieEvent('weekendRoutine') + await addieStore.addBotMessage(`It's the weekend! How can you spend this time well?`) + + this.setUserResponse( + { + kind: 'text', + }, + this.handleHomeButton, + this.handleHomeRoutine + ) + + this.messageHistory = [ + { + role: 'system', + content: `You are a ${coachDescription} helping the user recharge and improve. ${datePreamble()}`, + }, + { + role: 'assistant', + content: "It's the weekend! How can you spend this time well?", + }, + ] + } + + // --- talk it through + + handleTaskTalkButtons = async (index: number) => { + if (index == 0) { + this.finishConversation() + } else if (index == 1) { + this.messageHistory = [ + { + role: 'system', + content: `You are a ${coachDescription} helping the user focus and get tasks done. ${datePreamble()}`, + }, + { + role: 'assistant', + content: 'What are your important tasks?', + }, + ] + + this.handleTaskTalkText(`${this.workContext} I want to do: ${this.workTasks.join(',')}`) + } + } + + handleTaskTalkText = async (input: string) => { + this.gptLoop(input, ['All Done']) + } + + // --- remember + + doRemember = async () => {} + + editor: Editor | undefined + ydoc: Y.Doc | undefined + + doJournal = async () => { + tracker.addieEvent('journal') + const today = new Date() + const date = dateToPeriodDateString(Period.DAY, today) + addieStore.awaitingResponse.set(true) + const project = projectStore.currentProject.get()! + + const entries = await journalStore.loadNotes(project, Period.DAY, date, date) + this.ydoc = new Y.Doc() + + if (entries?.length) { + const journalEntry = entries[0] + loadDoc(project, journalEntry.id, this.ydoc) + await addieStore.addBotMessage(`Adding to your existing journal for today.`) + + this.setUserResponse( + { + kind: 'buttons_text', + buttons: ['Done', 'Show Journal'], + }, + this.handleJournalButtons, + this.handleJournal + ) + } else { + await addieStore.addBotMessage( + `Today is ${new Date().toLocaleDateString()}. What would you like to write?` + ) + this.setUserResponse( + { + kind: 'buttons_text', + buttons: ['Done'], + }, + this.handleJournalButtons, + this.handleJournal + ) + } + this.editor = createEditor(this.ydoc) + } + + handleJournalButtons = async (index: number) => { + if (index == 0) { + this.editor = undefined + this.ydoc = undefined + this.mainMenu() + } else if (index == 1) { + tracker.addieEvent('seeJournal') + await addieStore.addBotMessage(`Here's what you've written so far:`) + await addieStore.addBotMessage(this.editor?.getText() || '') + this.setUserResponse( + { + kind: 'buttons_text', + buttons: ['Done'], + }, + this.handleJournalButtons, + this.handleJournal + ) + } + } + + handleJournal = async (input: string) => { + tracker.addieEvent('writeJournal') + assertIsDefined(this.editor, 'editor') + assertIsDefined(this.ydoc, 'ydoc') + + const len = this.editor.state.doc.nodeSize + const transaction = this.editor.state.tr.insertText(input + '\n') + const newState = this.editor.state.apply(transaction) + this.editor.view.updateState(newState) + const date = dateToPeriodDateString(Period.DAY, new Date()) + + const contents = Y.encodeStateAsUpdate(this.ydoc) + const project = projectStore.currentProject.get()! + const text = this.editor.getText() + const snippet = text.substring(0, 252).trim() + + journalStore + .saveNote(project, Period.DAY, date, contents, snippet) + .then((note) => docStore.saveDoc(project, note.id, contents)) + + this.setUserResponse( + { + kind: 'buttons_text', + buttons: ['Done'], + }, + this.handleJournalButtons, + this.handleJournal + ) + } + + // --- help + + doHelp = async () => { + tracker.addieEvent('getHelp') + await addieStore.addBotMessage(`What would you like help with?`) + + this.setUserResponse( + { + kind: 'buttons_text', + buttons: ['Back to menu'], + }, + this.handleHelpButton, + this.handleHelp + ) + + this.messageHistory = [ + { + role: 'system', + content: `You are a ${coachDescription} helping the user inside an ADHD assistant app. ${datePreamble()}`, + }, + { + role: 'assistant', + content: 'What would you like help with?', + }, + ] + } + + handleHelp = async (input: string) => { + this.gptLoop(input, ['Back to menu', 'All done']) + } + + handleHelpButton = async (index: number) => { + if (index == 0) { + this.mainMenu() + } else if (index == 1) { + this.finishConversation() + } + } + + // --- end + + finishConversation = async () => { + await addieStore.addBotMessage(`Thanks for talking with me! How was this conversation?`) + + this.setUserResponse( + { + kind: 'buttons', + buttons: ['Great', 'OK', 'Bad'], + }, + this.handleFinishButtons, + null + ) + } + + handleFinishButtons = async (index: number) => { + const messages = addieStore.messages.get().length + if (index == 0) { + tracker.addieRating('great', messages) + } else if (index == 1) { + tracker.addieRating('ok', messages) + } else if (index == 2) { + tracker.addieRating('bad', messages) + } + addieStore.setResponse({ kind: 'end' }) + } + + // --- + + gptLoop = async (input: string, buttons: string[]) => { + tracker.addieGPTChat(input) + this.messageHistory.push({ + role: 'user', + content: input, + }) + + try { + addieStore.setError(null) + addieStore.awaitingResponse.set(true) + const { response, status } = await API.generateChat(this.messageHistory) + + this.messageHistory.push({ + role: 'assistant', + content: response, + }) + await addieStore.addBotMessage(response) + + // if the response was incomplete, get more (only one time) + if (status == 206) { + addieStore.awaitingResponse.set(true) + const { response } = await API.generateChat(this.messageHistory) + this.messageHistory.push({ + role: 'assistant', + content: response, + }) + await addieStore.addBotMessage(response) + } + + addieStore.setResponse({ + kind: 'buttons_text', + buttons, + }) + } catch (error) { + addieStore.setError(unwrapError(error)) + addieStore.setResponse({ + kind: 'buttons_text', + buttons, + }) + } finally { + addieStore.awaitingResponse.set(false) + } + } + + // --- + + handleButton = async (index: number) => { + addieStore.setResponse(null) + this.buttonHandler(index) + } + + handleInput = async (input: string) => { + addieStore.setResponse(null) + this.inputHandler(input) + } +} + +const addieScript = new AddieScript() +if (config.dev) (window as any)['addieScript'] = addieScript +export default addieScript diff --git a/assets/src/screens/auth/RegisterForm.tsx b/assets/src/screens/auth/RegisterForm.tsx index 445d03fd..27ed191f 100644 --- a/assets/src/screens/auth/RegisterForm.tsx +++ b/assets/src/screens/auth/RegisterForm.tsx @@ -1,7 +1,8 @@ import { useState } from 'preact/hooks' import GoogleServerOAuth, { - GoogleResponse, PROFILE_SCOPES + GoogleResponse, + PROFILE_SCOPES, } from '@/components/auth/GoogleServerOAuth' import ErrorMessage from '@/components/core/ErrorMessage' import Input from '@/components/core/Input' @@ -58,7 +59,11 @@ export default () => { } return ( - + {!uiStore.reactNative && ( <>
diff --git a/assets/src/stores/addieStore.ts b/assets/src/stores/addieStore.ts new file mode 100644 index 00000000..7c23ee9a --- /dev/null +++ b/assets/src/stores/addieStore.ts @@ -0,0 +1,60 @@ +import { action, atom, map } from 'nanostores' + +import { config } from '@/config' +import { Author, Message } from '@/models' +import addieScript from '@/screens/addie/addieScript' + +export type UserResponse = { + kind: 'text' | 'buttons' | 'end' | 'buttons_text' + buttons?: string[] + tooltips?: string[] + placeholder?: string +} + +class AddieStore { + messages = atom([]) + + response = atom(null) + + awaitingResponse = atom(false) + + error = atom(null) + + // --- actions + + resetConversation = () => { + if (config.dev) (window as any)['addieStore'] = addieStore + this.response.set(null) + this.error.set(null) + this.awaitingResponse.set(false) + this.messages.set([]) + addieScript.welcome() + } + + addMessage = (message: Message) => { + const messages = this.messages.get() + this.messages.set([...messages, message]) + } + + addBotMessage = async (text: string) => { + this.awaitingResponse.set(false) + + // add a little delay for realism + await new Promise((resolve) => setTimeout(resolve, 500)) + this.addMessage(new Message(Author.BOT, text)) + } + + addUserMessage = (text: string) => { + this.addMessage(new Message(Author.YOU, text)) + } + + setResponse = (response: UserResponse | null) => { + this.response.set(response) + } + + setError = (error: string | null) => { + this.error.set(error) + } +} + +export const addieStore = new AddieStore() diff --git a/assets/src/stores/authStore.ts b/assets/src/stores/authStore.ts index 7a433002..ed415996 100644 --- a/assets/src/stores/authStore.ts +++ b/assets/src/stores/authStore.ts @@ -118,7 +118,11 @@ class AuthStore { postAuth = () => { const search = new URLSearchParams(location.search) - const postRedirect = search.get('path') || uiStore.insightLoop ? paths.JOURNAL : paths.TODAY + const postRedirect = + search.get('path') || + (uiStore.insightLoop ? paths.JOURNAL : uiStore.addie ? paths.ADDIE : paths.TODAY) + + logger.info('post auth redirect', postRedirect, uiStore.insightLoop) location.href = postRedirect.match(/^\/[^\/]+/) ? postRedirect : paths.APP } diff --git a/assets/src/stores/tracker.ts b/assets/src/stores/tracker.ts index cad8a4ac..ea09adae 100644 --- a/assets/src/stores/tracker.ts +++ b/assets/src/stores/tracker.ts @@ -20,6 +20,22 @@ class Tracker { insightEntry(type: string) { amplitude.logEvent('insightEntry', { type }) } + + openAddie() { + amplitude.logEvent('openAddie') + } + + addieEvent(type: string) { + amplitude.logEvent('addieEvent', { type }) + } + + addieGPTChat(input: string) { + amplitude.logEvent('addieGPTChat', { input }) + } + + addieRating(rating: string, messages: number) { + amplitude.logEvent('addieRating', { rating, messages }) + } } const tracker = new Tracker() diff --git a/assets/src/stores/uiStore.ts b/assets/src/stores/uiStore.ts index 1c9eac2f..bf6b1fec 100644 --- a/assets/src/stores/uiStore.ts +++ b/assets/src/stores/uiStore.ts @@ -36,6 +36,8 @@ class UIStore { insightLoop = location.pathname.includes('/insight/') || location.search.includes('/insight/') + addie = location.pathname.includes('/addie') || location.search.includes('/addie') + reactNative = location.search?.includes('?app') || isSafariWebview path = atom() @@ -214,3 +216,4 @@ class UIStore { } export const uiStore = new UIStore() +if (config.dev) (window as any)['uiStore'] = uiStore diff --git a/assets/src/styles/index.css b/assets/src/styles/index.css index 5071f2e1..8e4829a1 100644 --- a/assets/src/styles/index.css +++ b/assets/src/styles/index.css @@ -94,3 +94,49 @@ ul[data-type="taskList"], .ProseMirror ul[data-type="taskList"] { transition: opacity 200ms; } } + +.dot-flashing { + position: relative; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: #9880ff; + color: #9880ff; + animation: dot-flashing 1s infinite linear alternate; + animation-delay: 0.5s; +} +.dot-flashing::before, .dot-flashing::after { + content: ""; + display: inline-block; + position: absolute; + top: 0; +} +.dot-flashing::before { + left: -15px; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: #9880ff; + color: #9880ff; + animation: dot-flashing 1s infinite alternate; + animation-delay: 0s; +} +.dot-flashing::after { + left: 15px; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: #9880ff; + color: #9880ff; + animation: dot-flashing 1s infinite alternate; + animation-delay: 1s; +} + +@keyframes dot-flashing { + 0% { + background-color: #9880ff; + } + 50%, 100% { + background-color: rgba(152, 128, 255, 0.2); + } +} \ No newline at end of file diff --git a/assets/static/images/apple-therapist.png b/assets/static/images/apple-therapist.png new file mode 100644 index 00000000..ebe84970 Binary files /dev/null and b/assets/static/images/apple-therapist.png differ diff --git a/assets/static/images/therapist.png b/assets/static/images/therapist.png new file mode 100644 index 00000000..4a1607f9 Binary files /dev/null and b/assets/static/images/therapist.png differ diff --git a/assets/static/pwa-addie.json b/assets/static/pwa-addie.json new file mode 100644 index 00000000..27f11d52 --- /dev/null +++ b/assets/static/pwa-addie.json @@ -0,0 +1,31 @@ +{ + "name": "Addie", + "short_name": "Addie", + "id": "/addie?source=pwa", + "start_url": "/addie?source=pwa", + "display": "standalone", + "background_color": "#f9fafb", + "theme_color": "#f9fafb", + "icons": [ + { + "src": "/images/therapist.png", + "type": "image/png", "sizes": "512x512" + }, + { + "src": "/images/therapist.png", + "type": "image/png", "sizes": "256x256" + }, + { + "src": "/images/therapist.png", + "type": "image/png", "sizes": "144x144" + }, + { + "src": "/images/therapist.png", + "type": "image/png", "sizes": "96x96" + }, + { + "src": "/images/therapist.png", + "type": "image/png","sizes": "48x48" + } + ] +} \ No newline at end of file diff --git a/assets/vite.config.ts b/assets/vite.config.ts index 752fc33e..c00c924a 100644 --- a/assets/vite.config.ts +++ b/assets/vite.config.ts @@ -61,6 +61,7 @@ export default defineConfig({ auth: 'src/auth.tsx', app: 'src/app.tsx', insight: 'src/insight.tsx', + addie: 'src/addie.tsx', }, output: { entryFileNames: 'js/[name]-[hash].js', diff --git a/config/config.exs b/config/config.exs index f203f8e6..192e38ce 100644 --- a/config/config.exs +++ b/config/config.exs @@ -109,6 +109,9 @@ config :mojito, timeout: 10_000, pool_opts: [size: 100, max_overflow: 500] +config :hammer, + backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, + cleanup_interval_ms: 60_000 * 10]} # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/lib/sequence/utils/openai.ex b/lib/sequence/utils/openai.ex index 871071bf..473e996f 100644 --- a/lib/sequence/utils/openai.ex +++ b/lib/sequence/utils/openai.ex @@ -40,6 +40,37 @@ defmodule Sequence.OpenAI do post("/completions", authenticated_headers(), request) end + # see https://platform.openai.com/docs/api-reference/chat + # {:ok, + # %{ + # "choices" => [ + # %{ + # "role" => "assistant", + # "content" => "\n\nThis is a test." + # } + # ], + # "created" => 1673048972, + # "id" => "cmpl-6VqkmcsLfr40Y2qodNNM56eleGnsj", + # "model" => "gpt-3.5-turbo", + # "object" => "chat.completion", + # "usage" => %{ + # "completion_tokens" => 7, + # "prompt_tokens" => 5, + # "total_tokens" => 12 + # } + # }} + def chat(user_id, messages, model \\ "gpt-3.5-turbo", max_tokens \\ 100, temperature \\ 0.5) do + request = %{ + model: model, + messages: messages, + max_tokens: max_tokens, + temperature: temperature, + user: "#{user_id}", + } + + post("/chat/completions", authenticated_headers(), request) + end + def api_key, do: Application.get_env(:sequence, :openai_api_key) def std_headers(content_type \\ "application/json") do @@ -53,7 +84,7 @@ defmodule Sequence.OpenAI do defp post(url, headers, params) do body = Jason.encode!(params) - Mojito.post(@url <> url, headers, body) + Mojito.post(@url <> url, headers, body, timeout: 20_000) |> parse_response() end diff --git a/lib/sequence_web/controllers/addie_controller.ex b/lib/sequence_web/controllers/addie_controller.ex new file mode 100644 index 00000000..8ff66048 --- /dev/null +++ b/lib/sequence_web/controllers/addie_controller.ex @@ -0,0 +1,38 @@ +defmodule SequenceWeb.AddieController do + use SequenceWeb, :controller + + require Logger + + action_fallback SequenceWeb.FallbackController + + # POST /generate/addie + def generate_chat(conn, %{ "messages" => messages }) do + with user when is_map(user) <- Guardian.Plug.current_resource(conn) do + + case Hammer.check_rate("addie:#{user.id}", 60_000, 5) do + {:allow, _count} -> + IO.inspect(messages) + with {:ok, response} <- Sequence.OpenAI.chat(user.id, messages) do + IO.inspect(response) + choice = hd(response["choices"]) + result = choice["message"]["content"] |> String.trim + finished = choice["finish_reason"] != "length" + + if !finished do + put_status(conn, 206) + |> text(result) + else + text conn, result + end + else + {:error, :openai, _status, body} -> + IO.inspect(body) + {:error, :bad_request, "Unable to chat"} + end + {:deny, _limit} -> + {:error, :too_many_requests, "Too many requests"} + end + end + end + +end diff --git a/lib/sequence_web/controllers/journal_controller.ex b/lib/sequence_web/controllers/journal_controller.ex index a5792abb..fbfd7417 100644 --- a/lib/sequence_web/controllers/journal_controller.ex +++ b/lib/sequence_web/controllers/journal_controller.ex @@ -48,15 +48,20 @@ defmodule SequenceWeb.JournalController do prompt_hash = :crypto.hash(:md5 , prompt) |> Base.encode16() case Redix.command(:redix, ["GET", "summary:" <> prompt_hash]) do {:ok, nil} -> - with {:ok, response} <- Sequence.OpenAI.completions(prompt, "text-curie-001", 150, 0.3) do - IO.inspect(response) - result = hd(response["choices"])["text"] |> String.trim - Redix.command(:redix, ["SET", "summary:" <> prompt_hash, result, "EX", "86400"]) - text conn, result - else - {:error, :openai, _status, body} -> - IO.inspect(body) - {:error, :bad_request, "Unable to generate summary"} + case Hammer.check_rate("journal:#{user.id}", 60_000, 5) do + {:allow, _count} -> + with {:ok, response} <- Sequence.OpenAI.completions(prompt, "text-curie-001", 150, 0.3) do + IO.inspect(response) + result = hd(response["choices"])["text"] |> String.trim + Redix.command(:redix, ["SET", "summary:" <> prompt_hash, result, "EX", "86400"]) + text conn, result + else + {:error, :openai, _status, body} -> + IO.inspect(body) + {:error, :bad_request, "Unable to generate summary"} + end + {:deny, _limit} -> + {:error, :too_many_requests, "Too many requests"} end {:ok, data} -> IO.puts("hit cache") diff --git a/lib/sequence_web/controllers/page_controller.ex b/lib/sequence_web/controllers/page_controller.ex index 5f763dcf..60596520 100644 --- a/lib/sequence_web/controllers/page_controller.ex +++ b/lib/sequence_web/controllers/page_controller.ex @@ -14,6 +14,11 @@ defmodule SequenceWeb.PageController do render conn, "app.html", pwa: "/pwa-insight.json", entry: "insight", title: "InsightLoop" end + def addie(conn, _params) do + render conn, "app.html", pwa: "/pwa-addie.json", entry: "addie", title: "Addie ADHD Assistant", + apple_icon: "/images/apple-therapist.png" + end + def auth(conn, _params) do render conn, "app.html", entry: "auth" end diff --git a/lib/sequence_web/endpoint.ex b/lib/sequence_web/endpoint.ex index eee571f2..255ca754 100644 --- a/lib/sequence_web/endpoint.ex +++ b/lib/sequence_web/endpoint.ex @@ -26,7 +26,7 @@ defmodule SequenceWeb.Endpoint do from: :sequence, gzip: true, only: ~w(assets js css images sounds favicon.ico robots.txt sitemap.xml pwa.json - pwa-local.json pwa-insight.json serviceworker.js) + pwa-local.json pwa-insight.json pwa-addie.json serviceworker.js) plug Plug.Static, at: "/", from: :sequence, diff --git a/lib/sequence_web/router.ex b/lib/sequence_web/router.ex index dbe78b94..453cdc2c 100644 --- a/lib/sequence_web/router.ex +++ b/lib/sequence_web/router.ex @@ -19,7 +19,7 @@ defmodule SequenceWeb.Router do at: "/", from: :sequence, gzip: true, only: ~w(assets js css sounds favicon.ico robots.txt version.json pwa.json - pwa-local.json pwa-insight.json serviceworker.js) + pwa-local.json pwa-insight.json pwa-addie.json serviceworker.js) ) end @@ -109,6 +109,7 @@ defmodule SequenceWeb.Router do get "/daily_notes", JournalController, :list_notes post "/daily_notes/:date", JournalController, :save_note post "/generate/summary", JournalController, :generate_summary + post "/generate/addie", AddieController, :generate_chat resources "/teams", TeamsController @@ -206,6 +207,8 @@ defmodule SequenceWeb.Router do get "/app/*path", PageController, :app get "/insight/*path", PageController, :insight + get "/addie", PageController, :addie + get "/signup", PageController, :auth get "/signin", PageController, :auth get "/auth/*path", PageController, :auth diff --git a/lib/sequence_web/templates/layout/app.html.heex b/lib/sequence_web/templates/layout/app.html.heex index f27bc9d4..b6bec6a4 100644 --- a/lib/sequence_web/templates/layout/app.html.heex +++ b/lib/sequence_web/templates/layout/app.html.heex @@ -15,7 +15,7 @@ title = if assigns[:title], do: "Daybird | #{assigns[:title]}", else: "Daybird" %> <%= title %> - + diff --git a/mix.exs b/mix.exs index 2efcbb85..d1e0e08f 100644 --- a/mix.exs +++ b/mix.exs @@ -83,6 +83,7 @@ defmodule Sequence.MixProject do {:kadabra, ">= 0.0.0"}, {:uuid, "~> 1.1"}, {:sweet_xml, "~> 0.0"}, + {:hammer, "~> 6.1"}, # dev dependencies {:mix_test_watch, "~> 0.6", only: [:dev, :docker], runtime: false}, diff --git a/mix.lock b/mix.lock index 2d6c20a6..f7125ba6 100644 --- a/mix.lock +++ b/mix.lock @@ -35,6 +35,7 @@ "goth": {:hex, :goth, "1.3.0", "db893be00006e4c85e6c92255a078b9eb249e1bc88349fd447f12e93d2f1253e", [:mix], [{:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "3c2c4e085bae945305132bc9bcb4d4d84d5e34e233a900164eb2529ab8bab85d"}, "guardian": {:hex, :guardian, "2.2.4", "3dafdc19665411c96b2796d184064d691bc08813a132da5119e39302a252b755", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "6f83d4309c16ec2469da8606bb2a9815512cc2fac1595ad34b79940a224eb110"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"}, "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"}, "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, diff --git a/package.json b/package.json index 9b87d4d4..71e5a4db 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "tsc": "cd assets && yarn tsc" }, "devDependencies": { + "okpush": "^0.0.6", "prettier": "^2.7.1" }, "prettier": { diff --git a/yarn.lock b/yarn.lock index 113c0c75..992c4697 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,442 @@ # yarn lockfile v1 +ansi-escapes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.0.0.tgz#68c580e87a489f6df3d761028bb93093fde6bd8a" + integrity sha512-IG23inYII3dWlU2EyiAiGj6Bwal5GzsgPMwjYGvc1HPE2dgbj4ZB5ToWBKSquKw74nB3TIuOwaI6/jSULzfgrw== + dependencies: + type-fest "^3.0.0" + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" + integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bl@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" + integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== + dependencies: + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^3.4.0" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.0.0, chalk@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" + integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + +cli-spinners@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" + integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== + +cli-width@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.0.0.tgz#a5622f6a3b0a9e3e711a25f099bf2399f608caf6" + integrity sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw== + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^9.4.1: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +figures@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-5.0.0.tgz#126cd055052dea699f8a54e8c9450e6ecfc44d5f" + integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg== + dependencies: + escape-string-regexp "^5.0.0" + is-unicode-supported "^1.2.0" + +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.1.tgz#c76ec81007875bc44d544ff7a11a55d12294102d" + integrity sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ== + +inquirer@^9.1.4: + version "9.1.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.1.4.tgz#482da8803670a64bd942bc5166a9547a19d41474" + integrity sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA== + dependencies: + ansi-escapes "^6.0.0" + chalk "^5.1.2" + cli-cursor "^4.0.0" + cli-width "^4.0.0" + external-editor "^3.0.3" + figures "^5.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^6.1.2" + run-async "^2.4.0" + rxjs "^7.5.7" + string-width "^5.1.2" + strip-ansi "^7.0.1" + through "^2.3.6" + wrap-ansi "^8.0.1" + +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + +is-unicode-supported@^1.1.0, is-unicode-supported@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-5.1.0.tgz#a20e3b9a5f53fac6aeb8e2bb22c07cf2c8f16d93" + integrity sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA== + dependencies: + chalk "^5.0.0" + is-unicode-supported "^1.1.0" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +okpush@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/okpush/-/okpush-0.0.6.tgz#fc1285a0e80ec2823d561c86b71ccc8d55360f10" + integrity sha512-WzgjT0gS/WUraYviDqmC6HvESzNDzOJmEyyteMowXHDi1W/u2ikUmjKMU2p6B4amH1jZJZ8Eb/u0PxtwaBV0GA== + dependencies: + axios "^1.3.4" + chalk "^4.1.2" + commander "^9.4.1" + ini "^3.0.1" + inquirer "^9.1.4" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +ora@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/ora/-/ora-6.1.2.tgz#7b3c1356b42fd90fb1dad043d5dbe649388a0bf5" + integrity sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw== + dependencies: + bl "^5.0.0" + chalk "^5.0.0" + cli-cursor "^4.0.0" + cli-spinners "^2.6.1" + is-interactive "^2.0.0" + is-unicode-supported "^1.1.0" + log-symbols "^5.1.0" + strip-ansi "^7.0.1" + wcwidth "^1.0.1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + prettier@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +rxjs@^7.5.7: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + dependencies: + ansi-regex "^6.0.1" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tslib@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +type-fest@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.1.tgz#cf8025edeebfd6cf48de73573a5e1423350b9993" + integrity sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +wrap-ansi@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1"