diff --git a/.gitignore b/.gitignore index f8cbc27..61956b6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,11 @@ node_modules/ dist/ .env .env.* -data/*.db -data/*.db-shm -data/*.db-wal +data/* models/ *.tsbuildinfo .claude/settings.local.json *.draft.*.md .claude/agent-memory/ -docs/launch \ No newline at end of file +docs/launchgcp-tts-key.json +gcp-tts-key.json diff --git a/CLAUDE.md b/CLAUDE.md index b915ab7..a9518df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,17 @@ - CLAUDE.md のドキュメント一覧に漏れがないか - リンクが正しいパスを指しているか +### README同期ルール + +**`README.md` と `README.ja.md` は常に同期させること。** + +片方を変更したときは、必ずもう片方にも対応する変更を反映する: +- 新機能の説明を追加 → 両方に追加 +- セットアップ手順を変更 → 両方に反映 +- 構成やリンクを修正 → 両方に反映 + +どちらか片方だけ更新して放置しない。 + ### API変更時のルール **`packages/server/src/routes/` 以下のファイルを変更するときは、必ず `docs/spec/openapi.yml` も確認・更新すること。** diff --git a/README.ja.md b/README.ja.md index 5fa0925..06d59d6 100644 --- a/README.ja.md +++ b/README.ja.md @@ -126,6 +126,38 @@ docker compose up `http://localhost:5173` を開く — 最初のインタビューセッションを始めよう。 +### オプション:音声対話モードを有効化 + +音声ベースのインタビューセッション(タイピングの代わりに話す)を使いたい場合は、Google Cloud Text-to-Speechをセットアップします: + +1. **GCPプロジェクトを作成してAPIを有効化** + - [Google Cloud Console](https://console.cloud.google.com/) にアクセス + - 新しいプロジェクトを作成(または既存のものを使用) + - API ライブラリで **Cloud Text-to-Speech API** を有効化 + - **無料枠**:月間100万文字 — 通常の使用には十分 + +2. **サービスアカウントを作成して認証情報をダウンロード** + - **IAMと管理 → サービスアカウント** に移動 + - 以下のロールでサービスアカウントを作成:**Cloud Text-to-Speech Client**(開発用ならEditorでも可) + - JSONキーを生成してダウンロード + +3. **キーファイルを配置してDockerを設定** + ```bash + # キーをプロジェクトルートに配置 + mv ~/Downloads/your-key-*.json ./gcp-tts-key.json + + # .gitignoreに追加(デフォルトで含まれています) + echo "gcp-tts-key.json" >> .gitignore + ``` + +4. **Dockerを再起動** + ```bash + docker compose down + docker compose up + ``` + +インタビューセッションで音声対話ボタン(🎧)が有効になります。GCP認証情報がない場合、ボタンは無効のままでテキストのみモードが使用されます。 + --- ## 仕組み diff --git a/README.md b/README.md index a649d87..fc4592f 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,38 @@ docker compose up Open `http://localhost:5173` — start your first interview session. +### Optional: Enable Voice Dialogue Mode + +If you want to use voice-based interview sessions (speak instead of typing), set up Google Cloud Text-to-Speech: + +1. **Create a GCP project and enable the API** + - Go to [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project (or use an existing one) + - Enable **Cloud Text-to-Speech API** in API Library + - **Free tier**: 1 million characters/month — enough for typical usage + +2. **Create a service account and download credentials** + - Go to **IAM & Admin → Service Accounts** + - Create a service account with role: **Cloud Text-to-Speech Client** (or Editor for dev) + - Generate a JSON key and download it + +3. **Place the key file and configure Docker** + ```bash + # Place the key in project root + mv ~/Downloads/your-key-*.json ./gcp-tts-key.json + + # Add to .gitignore (already included by default) + echo "gcp-tts-key.json" >> .gitignore + ``` + +4. **Restart Docker** + ```bash + docker compose down + docker compose up + ``` + +The voice dialogue button (🎧) will now be enabled in interview sessions. Without GCP credentials, the button remains disabled and text-only mode is used. + --- ## How It Works diff --git a/docker-compose.yml b/docker-compose.yml index 41eb2b3..c654555 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - ./packages:/app/packages - ./data:/app/data - ./models:/app/models + - ./gcp-tts-key.json:/app/gcp-tts-key.json:ro # node_modules はコンテナ内のものを使う(ホストと共有しない) - /app/node_modules - /app/packages/server/node_modules @@ -21,6 +22,7 @@ services: - NODE_ENV=development - DB_PATH=/app/data/personal_context.db - SIMULATE_DB_PATH=/app/data/simulate.db + - GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-tts-key.json # Set WATCH=true to enable hot-reload (may cause instability on macOS due to polling) # - WATCH=true # - CHOKIDAR_USEPOLLING=true diff --git a/docs/launch/PROMO_POSTS.md b/docs/launch/PROMO_POSTS.md new file mode 100644 index 0000000..38b3f3d --- /dev/null +++ b/docs/launch/PROMO_POSTS.md @@ -0,0 +1,77 @@ +# Launch Posts for International Platforms + +Use these templates to launch your project on Hacker News and Reddit. They are written to highlight the core philosophy of "Personal Context Engine" and the pain point of LLM lock-in. + +--- + +## 1. Hacker News (Show HN) + +**Title:** Show HN: Personal Context Engine – Own your context across all LLMs + +**Body / First Comment:** + +Hi HN, + +I built the Personal Context Engine because I was tired of rewriting my background every time I switched between ChatGPT, Claude, and Cursor. Every major platform now has a "memory" feature, but our context is trapped inside each one. + +Worse, most LLMs don't truly *know* you even with memory. They store what you *tell* them, which is often a filtered, rationalized version of yourself. The gap between "what you say about yourself" (e.g., "value: freedom") and "how you actually behave" is where the real signal lives. + +So I built a local-first engine that builds your context from the outside in: +1. **Passive Interview:** It acts as an interviewer, asking behavior-based questions (e.g., "Where did your time and money go this week?" or "What made your body feel light?"). +2. **Vignette Extraction:** Instead of extracting generic labels, the local LLM extracts specific scenes (vignettes). +3. **Portable Markdown:** It exports your entire identity into plain Markdown files layered by depth (Core, Shape, State). + +You can paste these Markdown files into any system prompt or use the included **MCP (Model Context Protocol) Server** to connect it directly to Claude Desktop, Cursor, or any supported AI tool. + +No cloud lock-in, just plain text. I'd love to hear your thoughts on this approach to personal data portability and AI memory! + +Repo: https://github.com/uchidayuma/personal_context +Live Demo: https://personal-context.onrender.com/ (No install required) + +--- + +## 2. Reddit: r/LocalLLaMA + +**Title:** I got tired of LLM memory lock-in, so I built a local engine that interviews you and exports your "Context" as Markdown (and an MCP Server) + +**Body:** + +Hey everyone, + +I wanted to share an open-source project I’ve been working on called **Personal Context Engine**. + +I realized that while ChatGPT and Claude have great memory features now, that memory isn't portable. You can't take your Claude context into Cursor. + +Also, I strongly believe that how you *felt* reveals more truth than how you *thought*. Traditional AI memory just stores facts we declare about ourselves. This engine takes a different approach: +- It interviews you passively, asking about actions and physical sensations rather than abstract thoughts. +- It extracts "Vignettes" (specific behavioral scenes) instead of just bullet points. A single sentence like *"Turned down a promotion because the Monday all-hands felt wrong"* gives a local model way more signal than just *"values: freedom"*. +- **It's fully local-first.** It runs on Docker (Node.js/SQLite) and supports Ollama out of the box (as well as OpenAI/Anthropic via API). +- It exports everything as plain `.md` files. +- It includes an **MCP server**, so you can hook your context directly into Claude Desktop or Cursor seamlessly. + +It's entirely open-source (MIT). I'd love for this community to try it out, especially the Ollama integration, and let me know if this approach to "Portable AI Memory" makes sense to you. + +GitHub: https://github.com/uchidayuma/personal_context + +--- + +## 3. Reddit: r/ClaudeAI & r/Cursor + +**Title:** How I bring my exact personal context into Claude/Cursor without rewriting it every time (Open Source MCP Server) + +**Body:** + +Hi all, + +If you use multiple AI tools (Claude Desktop, Cursor, etc.), you know the pain of having your "Project Knowledge" or "Memory" fragmented across platforms. + +I built an open-source tool called **Personal Context Engine**. It’s a local app that occasionally interviews you to figure out what you are working on, what your core values are, and how you make decisions. + +But the best part for this community: **It has a built-in MCP (Model Context Protocol) server.** + +Once the engine learns about you, you can just add it to your `claude_desktop_config.json` or Cursor settings. The AI can now dynamically read your "Core" context (who you are) and your "State" context (what you are currently focusing on) directly from your local SQLite database, exported as clean Markdown. + +It stops the AI from giving generic, "safe" answers because it actually knows your specific behavioral patterns and current goals. + +Check it out if you're interested in owning your context: +https://github.com/uchidayuma/personal_context diff --git a/docs/launch/SUBSTACK_POST.md b/docs/launch/SUBSTACK_POST.md new file mode 100644 index 0000000..d6fb882 --- /dev/null +++ b/docs/launch/SUBSTACK_POST.md @@ -0,0 +1,71 @@ +# Title: AIに「本当の自分」を理解させる方法。脱ベンダーロックインのコンテキストエンジンを作った話 + +こんにちは。今日は新しく公開したオープンソースソフトウェア、**「Personal Context Engine(自分コンテキスト)」**について、なぜこれを作ろうと思ったのか、その裏側にある思想をお話ししたいと思います。 + +リポジトリはこちらです: +[https://github.com/uchidayuma/personal_context](https://github.com/uchidayuma/personal_context) + +--- + +## 終わらない「自己紹介」への疲弊 + +皆さんは今、いくつのAIツールを使っていますか? +ChatGPTでアイデアを練り、Claudeで文章を推敲し、Cursorでコードを書く。私自身、日常的に複数のLLM(大規模言語モデル)を行き来しています。 + +各プラットフォームは最近「Memory(メモリ)」や「Project(プロジェクト)」といった機能を充実させてきました。「私はこういう人間で、こういう前提で答えてほしい」と事前に登録しておく機能です。 + +しかし、ここに大きなストレスがありました。**コンテキストがプラットフォームごとに分断され、閉じ込められている**のです。 + +新しいAIサービスが出るたびに、あるいは別のツールに乗り換えるたびに、自分が何者で、何を大切にしているのかをイチから教え直さなければならない。AIがせっかく私の好みを学習しても、そのデータは特定の企業(OpenAIやAnthropicなど)のサーバーの中にロックインされてしまいます。 + +「自分自身のデータなのに、自分の手元にない。」 + +この気持ち悪さを解決するために、プラットフォームに依存しない、自分専用のコンテキスト・データベースを作ろうと思ったのが始まりです。 + +## 「どう考えたか」よりも「どう感じたか」を信じる + +開発を進める中で、もう一つの大きな課題にぶつかりました。 +それは、**「人間は、本当の自分を言語化するのが絶望的に下手である」**ということです。 + +AIに「あなたの価値観は何ですか?」と聞かれて「自由です」と答えたとします。 +しかし、その言葉はすでに社会的な見栄や自己イメージによって綺麗にフィルタリングされた「編集済みの自分」です。AIはその一般的な「自由」という言葉を受け取り、誰にでも当てはまるような無難で退屈な回答を返してくるようになります。 + +だから、Personal Context Engineではアプローチを変えました。 +システムは「あなたの価値観は?」とは絶対に聞きません。代わりにこう聞きます。 + +**「今週、一番お金と時間を使ったことは何ですか?」** +**「体がフッと軽くなった瞬間はいつですか?」** +**「うまくいったのに、なぜか心が消耗した出来事はありましたか?」** + +「どう考えたか」ではなく、「どう行動したか」「体がどう反応したか(どう感じたか)」を聞き出すのです。合理的な説明は嘘をつきますが、体の反応や行動の事実は嘘をつきません。 + +## 「ヴィネット(情景)」という最強のプロンプト + +AIのインタビューに答えていくと、システムはあなたの回答から**「ヴィネット(情景)」**を抽出します。 + +例えば「価値観:自由」という無機質なラベルの代わりに、以下のようなシーンを保存します。 + +> *「昇進を打診されたが断った。理由を聞かれて『月曜の全体会議がなんか違う気がした』と答えた。」* + +このたった一文の「情景」は、箇条書きのプロフィールよりもはるかに饒舌に「あなたという人間」をLLMに伝えます。このシーンを読み込んだAIは、「この人は頭での損得勘定よりも、身体的な違和感を判断基準にする人だ」という文脈を深く理解し、回答の解像度が劇的に上がります。 + +## あなたのコンテキストを、あなたの手元に + +システムはこれらのデータをすべて**プレーンなMarkdownファイル**としてエクスポートします。(CORE、SHAPE、STATEという3つの層に分けて出力されます)。 + +これをどう使うかはあなた次第です。 +ChatGPTのカスタム指示にコピペしてもいいですし、付属の「MCP(Model Context Protocol)サーバー」を使えば、Claude DesktopやCursorから直接、ローカルのデータベースにアクセスさせることもできます。 + +クラウドロックインはありません。データはすべてあなたのローカル環境(SQLiteとMarkdown)に保存されます。明日、もし全く新しい最強のAIが登場しても、あなたはこのMarkdownファイルを渡すだけで、一瞬にして「本当のあなた」を引き継ぐことができます。 + +## おわりに + +このツールは、単なるAIの利便性向上ツールではありません。 +「SNSや社会の期待に塗れて見失いがちな『本当の自分』を、AIとの静かな対話を通して取り戻すためのツール」です。 + +もしこの思想に共感していただけたら、ぜひGitHubリポジトリを覗いてみてください。そして、スター(★)を押して応援していただけると、開発の大きな励みになります。 + +[https://github.com/uchidayuma/personal_context](https://github.com/uchidayuma/personal_context) +(※ブラウザ上で今すぐ試せるライブデモも用意しています) + +最後まで読んでいただき、ありがとうございました。 \ No newline at end of file diff --git a/docs/launch/ZENN_POST.md b/docs/launch/ZENN_POST.md new file mode 100644 index 0000000..df35695 --- /dev/null +++ b/docs/launch/ZENN_POST.md @@ -0,0 +1,89 @@ +# もう最新のAI情報を追うのはやめませんか? + +以前、私は事業に失敗して多重債務を抱え、スーツケース1つで海外へ渡りました。 + +パソコン1台でのゼロからの再出発。藁にもすがる思いでAIに「どう再起を図ればいい?」と相談しました。 + +しかし、返ってきたのは「せどり」「アフィリエイト」といった、私の性格や前提条件を完全に無視した薄っぺらい一般論ばかり。 +「なんだかなぁ」と落胆し、逆に動けなくなってしまいました。 + +**新しいAIを試すたびに、期待しては同じようにがっかりする。** +そんな経験を持つ人は多いのではないでしょうか? + +その原因は、AIの性能不足ではありません。 +**問題は、AIに渡している「あなた自身の情報」にあります。** + +![model](https://static.zenn.studio/user-upload/d0627a1fee5f-20260602.png) + +--- + +LLMは大量のデータから確率的に最も尤もらしい平均値を出して回答します。 + +こちらのことを何も知らない状態のAIが返すのはあなたに最適な答えではなく、統計的に正しい一般的な答えです。 + +そこで多くの人は、自分の肩書きやプロフィール、大まかな価値観をシステムプロンプトに設定しようとします。 +しかし、それではAIの回答の解像度は上がりません。 + +なぜなら、頭で考えたプロフィールは、社会的な期待やこうあるべきというフィルターを通った後の編集済みの自分に過ぎないからです。 + +## 本当のコンテキストは自分の内側深くにある + +私の場合、AIにいくら「私は自由を重んじる」と伝えても無駄でした。 + +![3coremodel](https://static.zenn.studio/user-upload/bf7ab93af019-20260602.png) + +- CORE:あなた自身の変わらない核 +- SHAPE:後天的に影響を受けて形成された考え方 +- STATE:直近の目標など、日々変わるもの + +プロンプトに価値観=自由と書いても、LLMには人類が書いたテキストの平均値としての自由しか伝わりません。 + +本当のコンテキストは、抽象的な言葉ではなく、具体的な行動の痕跡(**ヴィネット**)に宿ります。 + +--- + +### ヴィネットとは? + +ヴィネットは**具体的な場面**のことです。 +頭だけで考えた価値観ではなく、具体的な場面からあなた自身を定義します。 + +価値観=自由というラベルを貼るよりも、昇進の話が出た日の夜、なぜか気が重くなって断ったという**行動**。 +理由を聞かれて月曜の全体会議がなんか違う気がしたと言った。 + +という、身体の反応を伴う具体的な場面を伝える方が、LLMにとっては遥かに純度の高いコンテキストになります。 + +小説の技法で彼女は悲しかったと説明するのではなく、彼女は電話を置いて、3分間壁を見つめたという場面を描写するのと同じです。 +要約されたデータではなく、こうした具体的な場面から人間を深く理解します。 + +しかし、私たちは日々の忙しさの中で、こうした身体の反応や行動の痕跡を忘れてしまいます。 +自分自身のコンテキストを、自分でも正しく把握できていないことが、AIの出力を凡庸にしている根本的な原因でした。 + +--- + +問いを立てる前に、自分を掘り下げる必要がありました。 + +この課題を解決するために、**自分コンテキスト**というOSSを作成しました。 + +毎日5分のインタビューに答えるだけで、日々の行動の痕跡や微細な変化を記録し、LLMが最も理解しやすいMarkdown形式のパーソナルコンテキストとして自動で整理します。 + +このツールが目指すのは、特定のAIプラットフォームへのロックインをなくすことです。 + +主要なAIサービスのメモリ機能は、それぞれの環境に閉じ閉じになっています。ClaudeのメモリをChatGPTに持ち込めないし、Cursorの履歴をGeminiに持ち込むこともできません。 + +自分コンテキストは、あなたの輪郭をプレーンなMarkdownとして手元に切り出します。 + +さらに、構築したコンテキストは**MCP(Model Context Protocol)サーバー**経由で、Claude Code、Claude Desktop、Cursor、Clineなどの使い慣れたAIツールへ裏側で直接接続できます。AIが自らあなたのコンテキストファイルを参照し、あなたに最適化された回答を導き出す環境が整います。 + +一般論を吐き出すAIを、あなたを本当に理解する専属のパートナーに変えるために、まずは自分自身の行動の痕跡を集めることから始めてみませんか? + +打田裕馬 + +--- + +デモはこちら(インストール不要) +まずはすぐに試せるデモ版で対話をしてみてください。 +→ [personal-context-demo.fly.dev](https://personal-context-demo.fly.dev/) + +長期的にデータを残したい場合はOSS版をお使いください。 +思想に共感していただけたら、Starを押してもらえると励みになります! +GitHubはこちら(OSS)→ [github.com/uchidayuma/personal_context](https://github.com/uchidayuma/personal_context) \ No newline at end of file diff --git a/packages/server/drizzle/0001_needy_madelyne_pryor.sql b/packages/server/drizzle/0001_needy_madelyne_pryor.sql new file mode 100644 index 0000000..ed05fb8 --- /dev/null +++ b/packages/server/drizzle/0001_needy_madelyne_pryor.sql @@ -0,0 +1 @@ +ALTER TABLE `user_questions` ADD `skipped_at` text; \ No newline at end of file diff --git a/packages/server/drizzle/meta/0001_snapshot.json b/packages/server/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..7f22b85 --- /dev/null +++ b/packages/server/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1007 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "836bdf63-6db4-48eb-b5e1-04f962e5642d", + "prevId": "40127417-4c4d-4110-82ed-f4b917b9d685", + "tables": { + "demo_rate_limit": { + "name": "demo_rate_limit", + "columns": { + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_count": { + "name": "session_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "demo_rate_limit_ip_date_pk": { + "columns": [ + "ip", + "date" + ], + "name": "demo_rate_limit_ip_date_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fact_evidences": { + "name": "fact_evidences", + "columns": { + "fact_id": { + "name": "fact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "log_id": { + "name": "log_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "fact_evidences_fact_id_structured_facts_id_fk": { + "name": "fact_evidences_fact_id_structured_facts_id_fk", + "tableFrom": "fact_evidences", + "tableTo": "structured_facts", + "columnsFrom": [ + "fact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fact_evidences_log_id_raw_logs_id_fk": { + "name": "fact_evidences_log_id_raw_logs_id_fk", + "tableFrom": "fact_evidences", + "tableTo": "raw_logs", + "columnsFrom": [ + "log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "fact_evidences_fact_id_log_id_pk": { + "columns": [ + "fact_id", + "log_id" + ], + "name": "fact_evidences_fact_id_log_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "life_timeline": { + "name": "life_timeline", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_year": { + "name": "event_year", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_month": { + "name": "event_month", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_day": { + "name": "event_day", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "age_at_event": { + "name": "age_at_event", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_description": { + "name": "event_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'interview'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "life_timeline_user_id_users_id_fk": { + "name": "life_timeline_user_id_users_id_fk", + "tableFrom": "life_timeline", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "professional_records": { + "name": "professional_records", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_year": { + "name": "start_year", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_month": { + "name": "start_month", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_year": { + "name": "end_year", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "end_month": { + "name": "end_month", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "skills": { + "name": "skills", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'import'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "professional_records_user_id_users_id_fk": { + "name": "professional_records_user_id_users_id_fk", + "tableFrom": "professional_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "question_translations": { + "name": "question_translations", + "columns": { + "question_id": { + "name": "question_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "question_translations_question_id_questions_id_fk": { + "name": "question_translations_question_id_questions_id_fk", + "tableFrom": "question_translations", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "question_translations_question_id_language_pk": { + "columns": [ + "question_id", + "language" + ], + "name": "question_translations_question_id_language_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "questions": { + "name": "questions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "raw_logs": { + "name": "raw_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "raw_logs_user_id_users_id_fk": { + "name": "raw_logs_user_id_users_id_fk", + "tableFrom": "raw_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "raw_logs_session_id_sessions_id_fk": { + "name": "raw_logs_session_id_sessions_id_fk", + "tableFrom": "raw_logs", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_vignettes": { + "name": "session_vignettes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scene": { + "name": "scene", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "insight": { + "name": "insight", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "self_gap": { + "name": "self_gap", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "session_vignettes_user_id_users_id_fk": { + "name": "session_vignettes_user_id_users_id_fk", + "tableFrom": "session_vignettes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_vignettes_session_id_sessions_id_fk": { + "name": "session_vignettes_session_id_sessions_id_fk", + "tableFrom": "session_vignettes", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'regular'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "questions_asked": { + "name": "questions_asked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "followup_count": { + "name": "followup_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_question_id": { + "name": "current_question_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "ended_at": { + "name": "ended_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "structured_facts": { + "name": "structured_facts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fact": { + "name": "fact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "confidence_score": { + "name": "confidence_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0.8 + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'interview'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "structured_facts_user_id_users_id_fk": { + "name": "structured_facts_user_id_users_id_fk", + "tableFrom": "structured_facts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "timeline_evidences": { + "name": "timeline_evidences", + "columns": { + "timeline_id": { + "name": "timeline_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "log_id": { + "name": "log_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "timeline_evidences_timeline_id_life_timeline_id_fk": { + "name": "timeline_evidences_timeline_id_life_timeline_id_fk", + "tableFrom": "timeline_evidences", + "tableTo": "life_timeline", + "columnsFrom": [ + "timeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "timeline_evidences_log_id_raw_logs_id_fk": { + "name": "timeline_evidences_log_id_raw_logs_id_fk", + "tableFrom": "timeline_evidences", + "tableTo": "raw_logs", + "columnsFrom": [ + "log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "timeline_evidences_timeline_id_log_id_pk": { + "columns": [ + "timeline_id", + "log_id" + ], + "name": "timeline_evidences_timeline_id_log_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_questions": { + "name": "user_questions", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_id": { + "name": "question_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answered_at": { + "name": "answered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "skipped_at": { + "name": "skipped_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_questions_user_id_users_id_fk": { + "name": "user_questions_user_id_users_id_fk", + "tableFrom": "user_questions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_questions_question_id_questions_id_fk": { + "name": "user_questions_question_id_questions_id_fk", + "tableFrom": "user_questions", + "tableTo": "questions", + "columnsFrom": [ + "question_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_questions_user_id_question_id_pk": { + "columns": [ + "user_id", + "question_id" + ], + "name": "user_questions_user_id_question_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ja'" + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/server/drizzle/meta/_journal.json b/packages/server/drizzle/meta/_journal.json index 9983b53..c932de2 100644 --- a/packages/server/drizzle/meta/_journal.json +++ b/packages/server/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1780317887462, "tag": "0000_high_eternals", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1780548326326, + "tag": "0001_needy_madelyne_pryor", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json index ce410c8..c6732f9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -16,12 +16,15 @@ "@ai-sdk/anthropic": "^1.0.0", "@ai-sdk/deepseek": "^0.2.0", "@ai-sdk/openai": "^1.0.0", + "@google-cloud/text-to-speech": "^6.4.1", "@hono/node-server": "^1.14.1", "@libsql/client": "^0.15.3", + "@types/jszip": "^3.4.1", "ai": "^4.3.16", "drizzle-orm": "^0.43.1", "groq-sdk": "^0.9.1", "hono": "^4.7.10", + "jszip": "^3.10.1", "pdf-parse": "^1.1.1", "xlsx": "^0.18.5", "zod": "^3.24.4" diff --git a/packages/server/src/db/client.ts b/packages/server/src/db/client.ts index 35ba570..cdf8dc6 100644 --- a/packages/server/src/db/client.ts +++ b/packages/server/src/db/client.ts @@ -15,8 +15,15 @@ const SIMULATE_DB_PATH = process.env.SIMULATE_DB_PATH ?? path.join(DEFAULT_DATA_ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }) -const client = createClient({ url: `file:${DB_PATH}` }) -const simulateClient = createClient({ url: `file:${SIMULATE_DB_PATH}` }) +// Disable WAL mode to prevent corruption on Docker restarts +const client = createClient({ + url: `file:${DB_PATH}`, + syncUrl: undefined, +}) +const simulateClient = createClient({ + url: `file:${SIMULATE_DB_PATH}`, + syncUrl: undefined, +}) export const db = drizzle(client, { schema }) export const simulateDb = drizzle(simulateClient, { schema }) @@ -28,6 +35,14 @@ const DRIZZLE_FOLDER = process.env.NODE_ENV === 'production' : fileURLToPath(new URL('../../drizzle', import.meta.url)) export async function initDatabase() { + // Force WAL checkpoint to prevent corruption + try { + await client.execute('PRAGMA wal_checkpoint(TRUNCATE);') + await simulateClient.execute('PRAGMA wal_checkpoint(TRUNCATE);') + } catch (e) { + console.warn('[initDatabase] WAL checkpoint failed (may be normal on first run):', e) + } + await migrate(db, { migrationsFolder: DRIZZLE_FOLDER }) await migrate(simulateDb, { migrationsFolder: DRIZZLE_FOLDER }) } diff --git a/packages/server/src/db/schema.ts b/packages/server/src/db/schema.ts index f798586..97218e1 100644 --- a/packages/server/src/db/schema.ts +++ b/packages/server/src/db/schema.ts @@ -49,6 +49,7 @@ export const userQuestions = sqliteTable('user_questions', { userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), questionId: text('question_id').notNull().references(() => questions.id, { onDelete: 'cascade' }), answeredAt: text('answered_at').notNull().default(sql`CURRENT_TIMESTAMP`), + skippedAt: text('skipped_at'), // If non-null, this question was skipped (not answered) }, (t) => [primaryKey({ columns: [t.userId, t.questionId] })]) export const STRUCTURED_FACT_CATEGORIES = [ diff --git a/packages/server/src/db/seed.ts b/packages/server/src/db/seed.ts index cbaec83..54080cb 100644 --- a/packages/server/src/db/seed.ts +++ b/packages/server/src/db/seed.ts @@ -58,6 +58,24 @@ const SEED_QUESTIONS = [ { id: 'q39', category: 'preferences', content: '仕事の進め方で「これが自分のリズムだ」と感じるやり方はありますか?', priority: 8 }, { id: 'q40', category: 'preferences', content: '他の人のプレゼンや文章で「読みやすい・伝わりやすい」と感じるのはどういうスタイルですか?', priority: 7 }, { id: 'q41', category: 'preferences', content: '一番集中できる環境・時間帯・状況を教えてください。', priority: 7 }, + + // 追加質問: 身体感覚・場面・対比の切り口 + { id: 'q45', category: 'values', content: '最近、自分らしく動けたなと感じた瞬間はありますか?そのとき体はどんな感じでしたか?', priority: 9 }, + { id: 'q46', category: 'values', content: '「これだけは妥協できない」と体が拒否反応を示したことはありますか?', priority: 8 }, + { id: 'q47', category: 'goals', content: '今やっていることの中で、時間を忘れて没頭してしまうものはありますか?', priority: 9 }, + { id: 'q48', category: 'goals', content: '誰にも頼まれていないのに、気づいたら考えたり調べたりしていることはありますか?', priority: 8 }, + { id: 'q49', category: 'career', content: '仕事中に「あ、これ好きだな」と体が前のめりになった瞬間を覚えていますか?', priority: 9 }, + { id: 'q50', category: 'career', content: '同じ成果でも、やり方によって体の重さが全然違うことがありますか?どんな違いですか?', priority: 8 }, + { id: 'q51', category: 'life_events', content: '「あのとき行動して本当によかった」と今でも思い出す決断はありますか?', priority: 8 }, + { id: 'q52', category: 'life_events', content: '環境が変わった瞬間、体が軽くなった(or 重くなった)経験はありますか?', priority: 8 }, + { id: 'q53', category: 'character', content: 'ストレスがかかったとき、体のどこに一番先に出ますか?', priority: 7 }, + { id: 'q54', category: 'character', content: '「考える前に体が動いてしまう」タイプですか?それとも「頭で納得しないと動けない」タイプですか?', priority: 8 }, + { id: 'q55', category: 'opinions', content: '周りが「効率的」と言っているやり方が、自分には逆に遠回りに感じることはありますか?', priority: 7 }, + { id: 'q56', category: 'opinions', content: '「これは本質じゃない」と感じるのに、なぜかみんなが重視していることはありますか?', priority: 8 }, + { id: 'q57', category: 'fears', content: '過去に「安全な選択」をしたのに、なぜか後悔している決断はありますか?', priority: 8 }, + { id: 'q58', category: 'patterns', content: 'お金・時間・人間関係で、同じ失敗を繰り返してしまうパターンはありますか?', priority: 8 }, + { id: 'q59', category: 'skills', content: '他の人が苦労することを、自分は自然にできてしまうことはありますか?', priority: 9 }, + { id: 'q60', category: 'education', content: '学校で習ったことより、独学で学んだことの方が役に立っていると感じますか?', priority: 7 }, ] const EN_TRANSLATIONS: { questionId: string; content: string }[] = [ @@ -105,6 +123,22 @@ const EN_TRANSLATIONS: { questionId: string; content: string }[] = [ { questionId: 'q39', content: 'Do you have a rhythm or way of working that feels distinctly yours?' }, { questionId: 'q40', content: 'When someone else\'s presentation or writing feels easy to follow, what is it about their style that works for you?' }, { questionId: 'q41', content: 'What environment, time of day, or situation helps you concentrate best?' }, + { questionId: 'q45', content: 'Can you recall a recent moment when you felt like you were truly being yourself? How did your body feel?' }, + { questionId: 'q46', content: 'Has there been something your body simply refused to compromise on?' }, + { questionId: 'q47', content: 'Is there anything you do now where you lose track of time completely?' }, + { questionId: 'q48', content: 'Is there something you find yourself thinking about or researching even though nobody asked you to?' }, + { questionId: 'q49', content: 'Do you remember a moment at work when you thought "oh, I actually like this" and leaned in?' }, + { questionId: 'q50', content: 'Have you noticed that the same outcome can feel completely different in your body depending on how you got there?' }, + { questionId: 'q51', content: 'Is there a decision you made that you still think back on and feel glad you went for it?' }, + { questionId: 'q52', content: 'Have you ever felt your body get lighter (or heavier) the moment your environment changed?' }, + { questionId: 'q53', content: 'When you\'re stressed, where in your body do you feel it first?' }, + { questionId: 'q54', content: 'Do you tend to move before thinking, or do you need to understand something in your head before you act?' }, + { questionId: 'q55', content: 'Is there a method others call "efficient" that actually feels like a detour to you?' }, + { questionId: 'q56', content: 'Is there something everyone else seems to prioritize that you feel isn\'t really the point?' }, + { questionId: 'q57', content: 'Have you ever made the "safe choice" and ended up regretting it anyway?' }, + { questionId: 'q58', content: 'Is there a pattern you keep repeating with money, time, or relationships — even though you know better?' }, + { questionId: 'q59', content: 'Is there something others struggle with that comes naturally to you?' }, + { questionId: 'q60', content: 'Do you feel that what you taught yourself has been more useful than what you learned in school?' }, ] export async function seedQuestions(targetDb: Db) { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0a290c5..055b5b8 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -3,6 +3,7 @@ import { serveStatic } from '@hono/node-server/serve-static' import { Hono } from 'hono' import { cors } from 'hono/cors' import { logger } from 'hono/logger' +import { sql } from 'drizzle-orm' import { initDatabase, ensureDefaultUser, ensureDemoUser, cleanupDemoData, clearSimulateData, DEFAULT_USER_ID, db, simulateDb } from './db/client.js' import { seedQuestions } from './db/seed.js' import type { AppVariables } from './types.js' @@ -15,6 +16,7 @@ import { contextSummaryRoute } from './routes/contextSummary.js' import { importRoute } from './routes/import.js' import { progressRoute } from './routes/progress.js' import { insightsRoute } from './routes/insights.js' +import { ttsRoute } from './routes/tts.js' async function bootstrap() { await initDatabase() @@ -23,6 +25,21 @@ async function bootstrap() { await seedQuestions(db) await seedQuestions(simulateDb) + // Graceful shutdown: flush WAL on SIGTERM/SIGINT + const gracefulShutdown = async () => { + console.log('[shutdown] Flushing database...') + try { + await db.$client.execute('PRAGMA wal_checkpoint(TRUNCATE);') + await simulateDb.$client.execute('PRAGMA wal_checkpoint(TRUNCATE);') + } catch (e) { + console.error('[shutdown] WAL flush failed:', e) + process.exit(1) + } + process.exit(0) + } + process.on('SIGTERM', gracefulShutdown) + process.on('SIGINT', gracefulShutdown) + const app = new Hono<{ Variables: AppVariables }>() app.use(logger()) @@ -57,6 +74,7 @@ async function bootstrap() { app.route('/api/import', importRoute) app.route('/api/progress', progressRoute) app.route('/api/insights', insightsRoute) + app.route('/api/tts', ttsRoute) if (process.env.DEMO_MODE === 'true') { setInterval(() => cleanupDemoData(db), 60 * 60 * 1000) diff --git a/packages/server/src/interview/questionSelector.ts b/packages/server/src/interview/questionSelector.ts index 84adc24..35ca3b4 100644 --- a/packages/server/src/interview/questionSelector.ts +++ b/packages/server/src/interview/questionSelector.ts @@ -21,6 +21,29 @@ export async function selectNextQuestion(db: Db, userId: string, language: strin .where(eq(userQuestions.userId, userId)) const answeredIds = answeredRows.map(r => r.questionId) + // Get categories skipped in the last 24 hours + const recentlySkippedRows = await db + .select({ questionId: userQuestions.questionId }) + .from(userQuestions) + .where( + and( + eq(userQuestions.userId, userId), + sql`${userQuestions.skippedAt} IS NOT NULL`, + sql`datetime(${userQuestions.skippedAt}) > datetime('now', '-1 day')`, + ) + ) + const recentlySkippedIds = recentlySkippedRows.map(r => r.questionId) + + // Get categories from recently skipped questions + const skippedCategories = new Set() + if (recentlySkippedIds.length > 0) { + const skippedQuestions = await db + .select({ category: questions.category }) + .from(questions) + .where(sql`${questions.id} IN (${sql.join(recentlySkippedIds.map(id => sql`${id}`), sql`, `)})`) + skippedQuestions.forEach(q => skippedCategories.add(q.category)) + } + const factCountRows = await db .select({ category: structuredFacts.category, cnt: count() }) .from(structuredFacts) @@ -33,7 +56,7 @@ export async function selectNextQuestion(db: Db, userId: string, language: strin ? and(eq(questions.isActive, true), notInArray(questions.id, answeredIds)) : eq(questions.isActive, true) - const candidates = await db + let candidates = await db .select({ id: questions.id, category: questions.category, @@ -50,6 +73,31 @@ export async function selectNextQuestion(db: Db, userId: string, language: strin ) .where(whereClause) + // Filter out questions from recently skipped categories + if (skippedCategories.size > 0) { + const beforeFilter = candidates.length + candidates = candidates.filter(q => !skippedCategories.has(q.category)) + // If filtering removed all candidates, fall back to the original list + if (candidates.length === 0) { + candidates = await db + .select({ + id: questions.id, + category: questions.category, + priority: questions.priority, + content: sql`COALESCE(${questionTranslations.content}, ${questions.content})`, + }) + .from(questions) + .leftJoin( + questionTranslations, + and( + eq(questionTranslations.questionId, questions.id), + eq(questionTranslations.language, language), + ), + ) + .where(whereClause) + } + } + if (candidates.length === 0) return null // 充足率(現在件数 / 閾値)が低いカテゴリの質問を優先し、同率なら priority 降順 @@ -60,7 +108,13 @@ export async function selectNextQuestion(db: Db, userId: string, language: strin return b.priority - a.priority }) - return { id: candidates[0].id, content: candidates[0].content } + // Add variety: pick randomly from top 3 candidates instead of always choosing #1 + // For deterministic testing, set DETERMINISTIC_QUESTION=true to always pick first + const topN = Math.min(3, candidates.length) + const randomIndex = process.env.DETERMINISTIC_QUESTION === 'true' + ? 0 + : Math.floor(Math.random() * topN) + return { id: candidates[randomIndex].id, content: candidates[randomIndex].content } } export async function getExistingFactsSummary(db: Db, userId: string, language = 'ja'): Promise { diff --git a/packages/server/src/interview/sessionManager.ts b/packages/server/src/interview/sessionManager.ts index 2defa13..b24b8c3 100644 --- a/packages/server/src/interview/sessionManager.ts +++ b/packages/server/src/interview/sessionManager.ts @@ -99,8 +99,11 @@ export async function skipQuestion(db: Db, sessionId: string, userId = DEFAULT_U if (session.currentQuestionId) { await db.insert(userQuestions) - .values({ userId, questionId: session.currentQuestionId }) - .onConflictDoNothing() + .values({ userId, questionId: session.currentQuestionId, skippedAt: sql`CURRENT_TIMESTAMP` }) + .onConflictDoUpdate({ + target: [userQuestions.userId, userQuestions.questionId], + set: { skippedAt: sql`CURRENT_TIMESTAMP` } + }) } const nextQuestion = await selectNextQuestion(db, userId, language) diff --git a/packages/server/src/llm/prompts.ts b/packages/server/src/llm/prompts.ts index 3d350c6..50c130e 100644 --- a/packages/server/src/llm/prompts.ts +++ b/packages/server/src/llm/prompts.ts @@ -2,8 +2,30 @@ import { buildSubcategoryPrompt } from '../export/layers.js' export function buildCoachingToneInstruction(existingContext: string, language: string): string { return language === 'en' - ? `Transform the given question into a short, natural conversational question in English. Rules: ask exactly ONE question, keep it concise (one sentence), do not add explanations or sub-questions. Known context about the user: ${existingContext || 'none'}. Output only the transformed question.` - : `与えられた質問を短く自然な日本語の質問に変換してください。ルール:質問は必ず1つだけ、1文で簡潔に、補足説明や追加質問を付けない。ユーザーの既知情報: ${existingContext || 'なし'}。変換後の質問のみを出力してください。` + ? `Transform the given question into a warm, conversational question in English. + +Rules: +- Ask exactly ONE question — never two or more +- Keep it concise (1-2 sentences) +- If the user gave a short answer, don't drill down — shift angle or add context to make answering easier +- Prioritize questions that invite scenes, sensations, or specific moments over abstract explanations +- No interviewer-voice preambles ("I see", "That's interesting") — just the question + +Known context about the user: ${existingContext || 'none'} + +Output only the transformed question.` + : `与えられた質問を温かく自然な日本語の質問に変換してください。 + +ルール: +- 質問は必ず1つだけ — 2つ以上に分割しない +- 1〜2文で簡潔に +- 相手が短く答えた場合、掘り下げずに角度を変えるか、答えやすくなる文脈を添える +- 抽象的な説明より、具体的な場面・感覚・エピソードを引き出す質問を優先する +- インタビュアー口調の前置き(「なるほど」「興味深いですね」)は不要 — 質問だけ + +ユーザーの既知情報: ${existingContext || 'なし'} + +変換後の質問のみを出力してください。` } export function buildDocumentImportSystemPrompt(language: string): string { diff --git a/packages/server/src/routes/export.test.ts b/packages/server/src/routes/export.test.ts index 49eaaab..bf40583 100644 --- a/packages/server/src/routes/export.test.ts +++ b/packages/server/src/routes/export.test.ts @@ -22,6 +22,13 @@ vi.mock('../export/markdown.js', () => ({ }), })) +vi.mock('jszip', () => ({ + default: vi.fn().mockImplementation(() => ({ + file: vi.fn(), + generateAsync: vi.fn().mockResolvedValue(new Uint8Array([0x50, 0x4B, 0x03, 0x04])), // PK signature + })), +})) + describe('GET /api/export', () => { let app: ReturnType @@ -65,3 +72,26 @@ describe('GET /api/export', () => { expect(res).toSatisfyApiSpec() }) }) + +describe('GET /api/export/download', () => { + let app: ReturnType + + beforeEach(() => { + app = createTestApp(exportRoute, '/api/export', createMockDb()) + }) + + it('returns 200 with application/zip content-type', async () => { + const res = await req(app, 'GET', '/api/export/download') + expect(res.status).toBe(200) + expect(res.headers['content-type']).toMatch(/application\/zip/) + expect(res.headers['content-disposition']).toBe('attachment; filename="personal_context.zip"') + }) + + it('generates zip with markdown files', async () => { + const JSZip = (await import('jszip')).default + const res = await req(app, 'GET', '/api/export/download') + expect(res.status).toBe(200) + // Verify JSZip was instantiated + expect(JSZip).toHaveBeenCalled() + }) +}) diff --git a/packages/server/src/routes/export.ts b/packages/server/src/routes/export.ts index e1f0b99..ff31cf7 100644 --- a/packages/server/src/routes/export.ts +++ b/packages/server/src/routes/export.ts @@ -1,4 +1,5 @@ import { Hono } from 'hono' +import JSZip from 'jszip' import { exportToMarkdown } from '../export/markdown.js' import type { AppVariables } from '../types.js' @@ -13,3 +14,50 @@ exportRoute.get('/', async (c) => { return c.json({ error: 'Failed to export' }, 500) } }) + +exportRoute.get('/download', async (c) => { + try { + const { files } = await exportToMarkdown(c.get('db'), c.get('userId')) + + // Create a new JSZip instance + const zip = new JSZip() + + // Map of ExportFiles keys to filenames + const fileMap: Record = { + index: '_index.md', + lifeChapters: 'life_chapters.md', + l01Values: 'L01_values.md', + l02Character: 'L02_character.md', + l03LifeTimeline: 'L03_life_timeline.md', + l04Professional: 'L04_professional.md', + l05Relationships: 'L05_relationships.md', + l06Opinions: 'L06_opinions.md', + l07Fears: 'L07_fears.md', + l08Patterns: 'L08_patterns.md', + l09Goals: 'L09_goals.md', + l10Preferences: 'L10_preferences.md', + } + + // Add each markdown file to the zip + for (const [key, filename] of Object.entries(fileMap)) { + const content = files[key as keyof typeof files] + if (content) { + zip.file(filename, content) + } + } + + // Generate the zip file as an ArrayBuffer + const zipBuffer = await zip.generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 9 } }) + + // Return the zip file + return new Response(zipBuffer, { + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': 'attachment; filename="personal_context.zip"', + }, + }) + } catch (err) { + console.error('[export/download] error:', err) + return c.json({ error: 'Failed to generate ZIP' }, 500) + } +}) diff --git a/packages/server/src/routes/tts.test.ts b/packages/server/src/routes/tts.test.ts new file mode 100644 index 0000000..d1eb636 --- /dev/null +++ b/packages/server/src/routes/tts.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { Hono } from 'hono' +import { ttsRoute } from './tts.js' + +describe('ttsRoute', () => { + let app: Hono + let originalEnv: string | undefined + + beforeEach(() => { + app = new Hono() + app.route('/api/tts', ttsRoute) + originalEnv = process.env.GOOGLE_APPLICATION_CREDENTIALS + }) + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.GOOGLE_APPLICATION_CREDENTIALS = originalEnv + } else { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS + } + }) + + describe('GET /api/tts/health', () => { + it('returns available: true when GOOGLE_APPLICATION_CREDENTIALS is set', async () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/key.json' + + const res = await app.request('/api/tts/health') + expect(res.status).toBe(200) + + const data = await res.json() + expect(data).toEqual({ available: true }) + }) + + it('returns available: false when GOOGLE_APPLICATION_CREDENTIALS is not set', async () => { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS + + const res = await app.request('/api/tts/health') + expect(res.status).toBe(200) + + const data = await res.json() + expect(data).toEqual({ available: false }) + }) + }) + + describe('POST /api/tts', () => { + it('returns 400 when request body is invalid JSON', async () => { + const res = await app.request('/api/tts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: 'invalid-json', + }) + + expect(res.status).toBe(400) + const data = await res.json() + expect(data).toHaveProperty('error') + }) + + it('returns 400 when text is missing', async () => { + const res = await app.request('/api/tts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ language: 'ja' }), + }) + + expect(res.status).toBe(400) + const data = await res.json() + expect(data.error).toBe('text is required') + }) + + it('returns 400 when text is empty string', async () => { + const res = await app.request('/api/tts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: ' ', language: 'ja' }), + }) + + expect(res.status).toBe(400) + const data = await res.json() + expect(data.error).toBe('text is required') + }) + }) +}) diff --git a/packages/server/src/routes/tts.ts b/packages/server/src/routes/tts.ts new file mode 100644 index 0000000..f2ee678 --- /dev/null +++ b/packages/server/src/routes/tts.ts @@ -0,0 +1,49 @@ +import { Hono } from 'hono' +import { TextToSpeechClient } from '@google-cloud/text-to-speech' + +export const ttsRoute = new Hono() + +// Health check endpoint — returns 200 if GCP credentials are available +ttsRoute.get('/health', async (c) => { + const hasCredentials = !!process.env.GOOGLE_APPLICATION_CREDENTIALS + return c.json({ available: hasCredentials }) +}) + +ttsRoute.post('/', async (c) => { + let text: string + let language: string + try { + const body = await c.req.json<{ text: string; language?: string }>() + text = body.text + language = body.language ?? 'ja' + } catch { + return c.json({ error: 'invalid JSON body' }, 400) + } + + if (!text?.trim()) return c.json({ error: 'text is required' }, 400) + + try { + const client = new TextToSpeechClient() + + const [response] = await client.synthesizeSpeech({ + input: { text }, + voice: { + languageCode: language === 'en' ? 'en-US' : 'ja-JP', + name: language === 'en' ? 'en-US-Journey-D' : 'ja-JP-Neural2-B', + }, + audioConfig: { audioEncoding: 'MP3' }, + }) + + if (!response.audioContent) { + throw new Error('No audio content returned') + } + + const buffer = Buffer.from(response.audioContent as Uint8Array) + return new Response(buffer, { + headers: { 'Content-Type': 'audio/mpeg' }, + }) + } catch (err) { + console.error('[TTS] Google Cloud TTS error:', err) + return c.json({ error: 'TTS failed' }, 500) + } +}) diff --git a/packages/web/src/App.module.css b/packages/web/src/App.module.css index dfcfea1..98a01f8 100644 --- a/packages/web/src/App.module.css +++ b/packages/web/src/App.module.css @@ -92,11 +92,49 @@ } /* Export view */ -.exportGrid { +.exportContainer { max-width: 720px; width: 100%; display: flex; flex-direction: column; +} + +.downloadSection { + margin-bottom: 24px; + display: flex; + justify-content: flex-end; +} + +.downloadBtn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: transparent; + color: #e94560; + border: 1.5px solid #e94560; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.downloadBtn:hover { + background: #e94560; + color: #fff; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(233, 69, 96, 0.3); +} + +.downloadIcon { + font-size: 16px; +} + +.exportGrid { + width: 100%; + display: flex; + flex-direction: column; gap: 20px; } .exportCard { diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 9554f03..a3f7712 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -101,6 +101,22 @@ export default function App() { setView('export') } + async function handleDownload() { + const res = await fetch('/api/export/download') + const blob = await res.blob() + const url = URL.createObjectURL(blob) + try { + const a = document.createElement('a') + a.href = url + a.download = 'personal_context.zip' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + } finally { + URL.revokeObjectURL(url) + } + } + async function copyContent(key: string, content: string) { await navigator.clipboard.writeText(content) setCopiedKey(key) @@ -161,7 +177,17 @@ export default function App() { {view === 'chat' && } {view === 'dashboard' && setView('chat')} />} {view === 'export' && exportFiles && ( -
+
+
+ +
+
{(() => { const layerMap = Object.fromEntries(exportLayers.map(l => [l.key, l])) return EXPORT_ORDER.map((item) => { @@ -236,6 +262,7 @@ export default function App() { }) })()}
+
)}
diff --git a/packages/web/src/components/Chat.tsx b/packages/web/src/components/Chat.tsx index fe21ebc..1664214 100644 --- a/packages/web/src/components/Chat.tsx +++ b/packages/web/src/components/Chat.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import { useState, useEffect } from 'react' import { useVoiceInput } from '../hooks/useVoiceInput.js' import { useProgress } from '../hooks/useProgress.js' import { useChat } from '../hooks/useChat.js' @@ -10,6 +11,7 @@ import styles from './Chat.module.css' export default function Chat() { const { t, i18n } = useTranslation() const { progress, savedNotice, refreshProgress } = useProgress() + const [ttsAvailable, setTtsAvailable] = useState(false) const { sessionId, messages, input, setInput, loading, ended, voiceMode, setVoiceMode, @@ -19,6 +21,13 @@ export default function Chat() { } = useChat(refreshProgress) const voice = useVoiceInput((text) => setInput(text)) + useEffect(() => { + fetch('/api/tts/health') + .then(res => res.json()) + .then(data => setTtsAvailable(data.available)) + .catch(() => setTtsAvailable(false)) + }, []) + if (rateLimitHit) { return (
@@ -46,7 +55,7 @@ export default function Chat() { ended={ended} onClose={() => { setVoiceMode(false); refreshProgress() }} onExchange={(userMsg, coachMsg) => { addMessages(userMsg, coachMsg); refreshProgress() }} - onEnd={() => setEnded(true)} + onEnd={endSessionNow} /> )} @@ -140,11 +149,15 @@ export default function Chat() { )} {sessionId && !ended && ( - - - + )} {voice.isSupported && ( - {voices.length > 0 && ( -
- 🔊 - -
- )} -
COACH

{coachText}

@@ -168,8 +175,16 @@ export default function VoiceMode({ )}
-
-

{PHASE_LABEL[displayPhase]}

+ {displayPhase === 'ready' ? ( + + ) : ( + <> +
+

{PHASE_LABEL[displayPhase]}

+ + )} {(voice.error || fetchError) && (

{voice.error ?? fetchError}

)} diff --git a/packages/web/src/hooks/useVoiceInput.ts b/packages/web/src/hooks/useVoiceInput.ts index 12a1260..098ee4d 100644 --- a/packages/web/src/hooks/useVoiceInput.ts +++ b/packages/web/src/hooks/useVoiceInput.ts @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react' // Silence detection thresholds — tune if needed const SILENCE_THRESHOLD = 12 // avg amplitude (0–255); raise if background noise triggers early -const SILENCE_DURATION_MS = 1500 // ms of silence after speech to trigger end +const SILENCE_DURATION_MS = 1000 // ms of silence after speech to trigger end const MIN_SPEECH_MS = 300 // ignore sounds shorter than this (avoid false positives) export function useVoiceInput(onTranscript: (text: string) => void) { @@ -12,6 +12,7 @@ export function useVoiceInput(onTranscript: (text: string) => void) { const [error, setError] = useState(null) const stopRef = useRef<(() => void) | null>(null) + const isStartingRef = useRef(false) const manualRecorderRef = useRef(null) const manualChunksRef = useRef([]) const onTranscriptRef = useRef(onTranscript) @@ -20,13 +21,19 @@ export function useVoiceInput(onTranscript: (text: string) => void) { const isSupported = typeof window !== 'undefined' && !!window.MediaRecorder async function startListening(lang: string) { - if (isListening || isTranscribing || stopRef.current) return + console.log('[VAD] startListening called', { isListening, isTranscribing, hasStopRef: !!stopRef.current, isStarting: isStartingRef.current }) + if (isListening || isTranscribing || stopRef.current || isStartingRef.current) { + console.log('[VAD] BLOCKED - already active') + return + } + isStartingRef.current = true setError(null) let stream: MediaStream try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }) } catch { + isStartingRef.current = false setError(lang === 'ja' ? 'マイクへのアクセスが拒否されました' : 'Microphone access denied') return } @@ -87,10 +94,13 @@ export function useVoiceInput(onTranscript: (text: string) => void) { const r = recorder! const mimeType = r.mimeType r.onstop = async () => { + console.log('[VAD] recorder stopped, cleaning up') stream.getTracks().forEach(t => t.stop()) await audioCtx.close() stopRef.current = null + isStartingRef.current = false setIsListening(false) + console.log('[VAD] starting transcription') await transcribe(new Blob(chunks, { type: mimeType }), mimeType, lang) } r.stop() @@ -109,6 +119,7 @@ export function useVoiceInput(onTranscript: (text: string) => void) { setIsListening(false) stopRef.current = null } + isStartingRef.current = false } function stopListening() { @@ -116,6 +127,7 @@ export function useVoiceInput(onTranscript: (text: string) => void) { } async function transcribe(blob: Blob, mimeType: string, lang: string) { + console.log('[VAD] transcribe started', { blobSize: blob.size }) setIsTranscribing(true) try { const ext = mimeType.split(';')[0].split('/')[1] ?? 'webm' @@ -127,14 +139,17 @@ export function useVoiceInput(onTranscript: (text: string) => void) { if (!res.ok) throw new Error(await res.text()) const data = await res.json() as { text?: string; error?: string } if (data.text) { + console.log('[VAD] transcription success:', data.text) onTranscriptRef.current(data.text.trim()) } else { throw new Error(data.error ?? 'empty response') } } catch (err) { const msg = err instanceof Error ? err.message : String(err) + console.error('[VAD] transcription error:', msg) setError(lang === 'ja' ? `変換エラー: ${msg}` : `Transcription error: ${msg}`) } finally { + console.log('[VAD] transcribe complete, resetting state') setIsTranscribing(false) } } diff --git a/packages/web/src/i18n/en.json b/packages/web/src/i18n/en.json index 5f40692..fc565cb 100644 --- a/packages/web/src/i18n/en.json +++ b/packages/web/src/i18n/en.json @@ -32,10 +32,12 @@ "transcribing": "Transcribing...", "voiceInput": "Voice input", "voiceMode": "Voice Mode", + "voiceModeDisabled": "Voice Mode (Requires Google Cloud TTS setup)", "modelError": "Model configuration error" }, "export": { - "copy": "Copy to clipboard" + "copy": "Copy to clipboard", + "downloadZip": "Download all as ZIP" }, "progress": { "label": "Context Richness", @@ -74,7 +76,8 @@ "listening": "Listening...", "processing": "Processing...", "speaking": "Coach is speaking", - "backToText": "✕ Back to text mode" + "backToText": "✕ Back to text mode", + "tapToStart": "Tap to start" }, "import": { "complete": "Import complete", diff --git a/packages/web/src/i18n/ja.json b/packages/web/src/i18n/ja.json index f15ab0a..e14d733 100644 --- a/packages/web/src/i18n/ja.json +++ b/packages/web/src/i18n/ja.json @@ -32,10 +32,12 @@ "transcribing": "変換中...", "voiceInput": "音声入力", "voiceMode": "音声対話モード", + "voiceModeDisabled": "音声対話モード (Google Cloud TTS の設定が必要)", "modelError": "モデル設定エラー" }, "export": { - "copy": "クリップボードにコピー" + "copy": "クリップボードにコピー", + "downloadZip": "ZIP でまとめてダウンロード" }, "progress": { "label": "コンテキスト充足度", @@ -74,7 +76,8 @@ "listening": "話してください...", "processing": "処理中...", "speaking": "コーチが話しています", - "backToText": "✕ テキストモードに戻る" + "backToText": "✕ テキストモードに戻る", + "tapToStart": "タップして開始" }, "import": { "complete": "インポート完了", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84fc11f..5a46562 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,12 +54,18 @@ importers: '@ai-sdk/openai': specifier: ^1.0.0 version: 1.3.24(zod@3.25.76) + '@google-cloud/text-to-speech': + specifier: ^6.4.1 + version: 6.4.1 '@hono/node-server': specifier: ^1.14.1 version: 1.19.14(hono@4.12.21) '@libsql/client': specifier: ^0.15.3 version: 0.15.15 + '@types/jszip': + specifier: ^3.4.1 + version: 3.4.1 ai: specifier: ^4.3.16 version: 4.3.19(react@19.2.6)(zod@3.25.76) @@ -72,6 +78,9 @@ importers: hono: specifier: ^4.7.10 version: 4.12.21 + jszip: + specifier: ^3.10.1 + version: 3.10.1 pdf-parse: specifier: ^1.1.1 version: 1.1.4 @@ -1089,6 +1098,19 @@ packages: '@fontsource-variable/inter@5.2.8': resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + '@google-cloud/text-to-speech@6.4.1': + resolution: {integrity: sha512-iF1SpBPbP019zoLYzIJXp/yDumrSNl19T7hXP4Lg8d2cnNtxoQKQuNOpiwFrxEKV3CBJpp7OY5+z7/K73zNr5w==} + engines: {node: '>=18'} + + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -1138,6 +1160,10 @@ packages: '@types/node': optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jest/types@26.6.2': resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} engines: {node: '>= 10.14.2'} @@ -1158,6 +1184,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@libsql/client@0.15.15': resolution: {integrity: sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w==} @@ -1276,6 +1305,40 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1566,6 +1629,10 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jszip@3.4.1': + resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} + deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1721,6 +1788,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1745,6 +1816,9 @@ packages: axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1760,6 +1834,9 @@ packages: better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -1770,6 +1847,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -1783,6 +1863,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1939,6 +2022,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} @@ -2150,6 +2236,15 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + eciesjs@0.4.18: resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -2166,6 +2261,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2284,6 +2382,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2346,6 +2447,10 @@ packages: debug: optional: true + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} @@ -2399,6 +2504,14 @@ packages: fuzzysort@3.1.0: resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2441,6 +2554,23 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-gax@5.0.6: + resolution: {integrity: sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2485,6 +2615,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -2522,6 +2656,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2615,6 +2752,9 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2622,6 +2762,9 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-diff@26.6.2: resolution: {integrity: sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==} engines: {node: '>= 10.14.2'} @@ -2666,6 +2809,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -2691,6 +2837,15 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -2704,6 +2859,9 @@ packages: cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -2789,6 +2947,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2796,9 +2957,15 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2860,9 +3027,17 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -2942,6 +3117,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2988,6 +3167,12 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3018,6 +3203,10 @@ packages: path-parser@6.1.0: resolution: {integrity: sha512-nAB6J73z2rFcQP+870OHhpkHFj5kO4rPLc2Ol4Y3Ale7F6Hk1/cPKp7cQ8RznKF8FOSvu+YR9Xc6Gafk7DlpYA==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3101,6 +3290,9 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + promise-limit@2.7.0: resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} @@ -3108,6 +3300,14 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proto3-json-serializer@3.0.4: + resolution: {integrity: sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==} + engines: {node: '>=18'} + + protobufjs@7.6.2: + resolution: {integrity: sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3166,6 +3366,9 @@ packages: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -3204,6 +3407,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry-request@8.0.2: + resolution: {integrity: sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==} + engines: {node: '>=18'} + rettime@0.11.11: resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} @@ -3211,6 +3418,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3227,6 +3438,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3262,6 +3476,9 @@ packages: set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -3345,6 +3562,12 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -3352,10 +3575,17 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3390,6 +3620,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -3425,6 +3658,10 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + teeny-request@10.1.2: + resolution: {integrity: sha512-Xj0ZAQ0CeuQn6UxCDPLbFRlgcSTUEyO3+wiepr2grjIjyL/lMMs1Z4OwXn8kLvn/V1OuaEP0UY7Na6UDNNsYrQ==} + engines: {node: '>=18'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3716,6 +3953,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4463,6 +4704,24 @@ snapshots: '@fontsource-variable/inter@5.2.8': {} + '@google-cloud/text-to-speech@6.4.1': + dependencies: + google-gax: 5.0.6 + transitivePeerDependencies: + - supports-color + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.2 + yargs: 17.7.2 + '@hono/node-server@1.19.14(hono@4.12.21)': dependencies: hono: 4.12.21 @@ -4526,6 +4785,15 @@ snapshots: optionalDependencies: '@types/node': 25.9.1 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jest/types@26.6.2': dependencies: '@types/istanbul-lib-coverage': 2.0.6 @@ -4553,6 +4821,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@libsql/client@0.15.15': dependencies: '@libsql/core': 0.15.15 @@ -4681,6 +4951,31 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.60.4': @@ -4892,6 +5187,10 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jszip@3.4.1': + dependencies: + jszip: 3.10.1 + '@types/ms@2.1.0': optional: true @@ -5064,6 +5363,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + any-promise@1.3.0: {} argparse@2.0.1: {} @@ -5084,10 +5385,11 @@ snapshots: transitivePeerDependencies: - debug + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} - base64-js@1.5.1: - optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.10.31: {} @@ -5097,6 +5399,8 @@ snapshots: prebuild-install: 7.1.3 optional: true + bignumber.js@9.3.1: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -5123,6 +5427,10 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -5139,6 +5447,8 @@ snapshots: node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5263,6 +5573,8 @@ snapshots: cookie@1.1.1: {} + core-util-is@1.0.3: {} + cors@2.8.6: dependencies: object-assign: 4.1.1 @@ -5356,6 +5668,19 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + eciesjs@0.4.18: dependencies: '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) @@ -5371,12 +5696,13 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: dependencies: once: 1.4.0 - optional: true enhanced-resolve@5.22.1: dependencies: @@ -5613,6 +5939,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -5678,6 +6006,11 @@ snapshots: follow-redirects@1.16.0: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} form-data@2.5.5: @@ -5735,6 +6068,22 @@ snapshots: fuzzysort@3.1.0: {} + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -5779,6 +6128,44 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-gax@5.0.6: + dependencies: + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.8.1 + duplexify: 4.1.3 + google-auth-library: 10.6.2 + google-logging-utils: 1.1.3 + node-fetch: 3.3.2 + object-hash: 3.0.0 + proto3-json-serializer: 3.0.4 + protobufjs: 7.6.2 + retry-request: 8.0.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -5828,6 +6215,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -5862,6 +6256,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -5920,10 +6316,18 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isexe@2.0.0: {} isexe@3.1.5: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jest-diff@26.6.2: dependencies: chalk: 4.1.2 @@ -5965,6 +6369,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-parse-even-better-errors@2.3.1: {} json-schema-traverse@1.0.0: {} @@ -5987,6 +6395,24 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + kleur@3.0.3: {} kleur@4.1.5: {} @@ -6006,6 +6432,10 @@ snapshots: '@libsql/linux-x64-musl': 0.5.29 '@libsql/win32-x64-msvc': 0.5.29 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -6061,6 +6491,8 @@ snapshots: load-tsconfig@0.2.5: {} + lodash.camelcase@4.3.0: {} + lodash.merge@4.6.2: {} log-symbols@6.0.0: @@ -6068,8 +6500,12 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + long@5.3.2: {} + loupe@3.2.1: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -6116,8 +6552,14 @@ snapshots: dependencies: brace-expansion: 5.0.6 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + minimist@1.2.8: {} + minipass@7.1.3: {} + mkdirp-classic@0.5.3: optional: true @@ -6228,6 +6670,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} object-treeify@1.1.33: {} @@ -6300,6 +6744,10 @@ snapshots: outvariant@1.4.3: {} + package-json-from-dist@1.0.1: {} + + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6326,6 +6774,11 @@ snapshots: search-params: 3.0.0 tslib: 1.14.1 + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} path-to-regexp@8.4.2: {} @@ -6402,6 +6855,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-nextick-args@2.0.1: {} + promise-limit@2.7.0: {} prompts@2.4.2: @@ -6409,6 +6864,25 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proto3-json-serializer@3.0.4: + dependencies: + protobufjs: 7.6.2 + + protobufjs@7.6.2: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 22.19.19 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -6464,12 +6938,21 @@ snapshots: react@19.2.6: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - optional: true readdirp@4.1.2: {} @@ -6498,10 +6981,21 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry-request@8.0.2: + dependencies: + extend: 3.0.2 + teeny-request: 10.1.2 + transitivePeerDependencies: + - supports-color + rettime@0.11.11: {} reusify@1.1.0: {} + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 @@ -6549,6 +7043,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -6591,6 +7087,8 @@ snapshots: set-cookie-parser@3.1.0: {} + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shadcn@4.8.2(@types/node@25.9.1)(typescript@5.9.3): @@ -6711,6 +7209,12 @@ snapshots: stdin-discarder@0.2.2: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + strict-event-emitter@0.5.1: {} string-width@4.2.3: @@ -6719,16 +7223,25 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - optional: true stringify-object@5.0.0: dependencies: @@ -6757,6 +7270,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + stubs@3.0.0: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -6802,6 +7317,15 @@ snapshots: readable-stream: 3.6.2 optional: true + teeny-request@10.1.2: + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + stream-events: 1.0.5 + transitivePeerDependencies: + - supports-color + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -7083,6 +7607,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} ws@8.20.1: {}