commit eb29eb3fa1907f12b14d3e1daefb72a01dac79c2 Author: WpyQwq <3911625973@qq.com> Date: Sat Sep 19 12:10:51 2026 +0800 Initial commit: sage:TypeScript AI Agent 框架 monorepo(llm / memory / tui 三包) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b3e3bd --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Sage — AI 编程助手 + +功能上高度对应 opencode,但 UI 风格完全不同(Tokyo Night 配色 + Ink/React TUI),并额外内置**长期记忆**系统。 + +## 启动 + +```bash +# 在终端里直接运行(需要真实 TTY) +node sage.mjs + +# 或者 +cd packages/tui && node --import=tsx/esm src/index.tsx +``` + +## 配置 `~/.sage/config.json` + +```json +{ + "defaultProvider": "anthropic", + "defaultModel": "claude-sonnet-4-6", + "theme": "tokyo-night", + "providers": { + "anthropic": { "apiKey": "sk-ant-..." }, + "openai": { "apiKey": "sk-..." }, + "google": { "apiKey": "..." }, + "openrouter":{ "apiKey": "sk-or-..." } + }, + "memory": { + "autoSummarize": true, + "maxMemories": 500, + "recallCount": 5 + } +} +``` + +也可以通过环境变量:`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `OPENROUTER_API_KEY` + +## 键位 + +| 按键 | 功能 | +|------|------| +| `^B` | 切换 session 面板焦点 | +| `↑↓` | 在 session 面板中导航 / 历史命令 | +| `Enter` | 发送消息 / 选中 session | +| `^C` | 中断当前生成 | +| `Esc` | 关闭弹窗 / 清除错误 | + +## 斜线命令 + +| 命令 | 说明 | +|------|------| +| `/remember <文字>` | 手动保存一条长期记忆 | +| `/memories` | 浏览 & 删除所有记忆 | +| `/model [provider]` | 切换模型,如 `/model gpt-4o openai` | +| `/new` | 新建 session | +| `/clear` | 清空显示 | +| `/abort` | 停止生成 | +| `/help` | 命令列表 | + +## 长期记忆 + +- 每次发消息前,自动用 BM25 召回最相关的历史记忆注入上下文 +- 底部黄色条 `◊ 3 memories recalled` 显示命中的记忆 +- 每 20 条消息自动用 LLM 摘要本次会话并写入记忆库 +- 数据存于 `~/.sage/sage.db`(SQLite + FTS5) + +## 内置工具 + +| 工具 | 说明 | +|------|------| +| `bash` | 执行 shell 命令 | +| `read` | 读取文件 | +| `write` | 写入文件 | +| `edit` | 精确字符串替换 | +| `glob` | 文件模式匹配 | +| `grep` | 正则内容搜索(ripgrep) | + +## 支持的 Provider + +- **Anthropic** — Claude Opus 4.8 / Sonnet 4.6 / Haiku 4.5 +- **OpenAI** — GPT-4o / GPT-4o-mini / o3 +- **Google** — Gemini 2.5 Pro / Flash +- **OpenRouter** — 所有主流模型 + +## 项目结构 + +``` +packages/ + core/ — DB、session、工具系统 + llm/ — 多 Provider + 流式调用循环 + memory/ — 长期记忆 (FTS5 BM25 检索) + tui/ — Ink (React) TUI,Tokyo Night 风格 +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f79f9b7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2140 @@ +{ + "name": "sage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sage", + "version": "0.1.0", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "typescript": "^5.8.0" + } + }, + "node_modules/@ai-sdk/anthropic": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-1.2.12.tgz", + "integrity": "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/google": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.22.tgz", + "integrity": "sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/openai": { + "version": "1.3.24", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.24.tgz", + "integrity": "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.0.0" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz", + "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", + "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "nanoid": "^3.3.8", + "secure-json-parse": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@ai-sdk/provider-utils/node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/@ai-sdk/react": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz", + "integrity": "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/ui-utils": "1.2.11", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/ui-utils": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz", + "integrity": "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.23.8" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@sage/core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@sage/llm": { + "resolved": "packages/llm", + "link": true + }, + "node_modules/@sage/memory": { + "resolved": "packages/memory", + "link": true + }, + "node_modules/@sage/tui": { + "resolved": "packages/tui", + "link": true + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/ai": { + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/ai/-/ai-4.3.19.tgz", + "integrity": "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "1.1.3", + "@ai-sdk/provider-utils": "2.2.8", + "@ai-sdk/react": "1.2.12", + "@ai-sdk/ui-utils": "1.2.11", + "@opentelemetry/api": "1.9.0", + "jsondiffpatch": "0.6.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "license": "Apache-2.0" + }, + "node_modules/drizzle-orm": { + "version": "0.44.7", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz", + "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-toolkit": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.48.1.tgz", + "integrity": "sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "license": "MIT", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz", + "integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/swr": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", + "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "packages/core": { + "name": "@sage/core", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^11.0.0", + "drizzle-orm": "^0.44.0", + "nanoid": "^5.0.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.0", + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + } + }, + "packages/llm": { + "name": "@sage/llm", + "version": "0.1.0", + "dependencies": { + "@ai-sdk/anthropic": "^1.0.0", + "@ai-sdk/google": "^1.0.0", + "@ai-sdk/openai": "^1.0.0", + "@sage/core": "*", + "ai": "^4.3.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + } + }, + "packages/memory": { + "name": "@sage/memory", + "version": "0.1.0", + "dependencies": { + "@sage/core": "*", + "nanoid": "^5.0.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + } + }, + "packages/tui": { + "name": "@sage/tui", + "version": "0.1.0", + "dependencies": { + "@sage/core": "*", + "@sage/llm": "*", + "@sage/memory": "*", + "chalk": "^5.4.0", + "figures": "^6.1.0", + "ink": "^5.1.0", + "ink-text-input": "^6.0.0", + "react": "^18.3.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^18.3.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..66041b7 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "sage", + "version": "0.1.0", + "private": true, + "workspaces": ["packages/*"], + "scripts": { + "dev": "npm run start --workspace=packages/tui", + "build": "npm run build --workspaces --if-present", + "typecheck": "npm run typecheck --workspaces --if-present" + }, + "devDependencies": { + "typescript": "^5.8.0" + } +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..a4678a9 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,21 @@ +{ + "name": "@sage/core", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { ".": "./src/index.ts" }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "better-sqlite3": "^11.0.0", + "drizzle-orm": "^0.44.0", + "nanoid": "^5.0.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.0", + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + } +} diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts new file mode 100644 index 0000000..0f72a5b --- /dev/null +++ b/packages/core/src/config/index.ts @@ -0,0 +1,72 @@ +import { existsSync, readFileSync, writeFileSync } from "fs" +import { join } from "path" +import { homedir } from "os" + +export interface SageConfig { + defaultProvider: string + defaultModel: string + theme: "tokyo-night" | "catppuccin" | "gruvbox" | "minimal" + providers: { + anthropic?: { apiKey?: string } + openai?: { apiKey?: string; baseUrl?: string } + google?: { apiKey?: string } + openrouter?: { apiKey?: string } + } + memory: { + autoSummarize: boolean + maxMemories: number + recallCount: number + } + editor: string +} + +const CONFIG_PATH = join(homedir(), ".sage", "config.json") + +const DEFAULTS: SageConfig = { + defaultProvider: "anthropic", + defaultModel: "claude-sonnet-4-6", + theme: "tokyo-night", + providers: {}, + memory: { + autoSummarize: true, + maxMemories: 500, + recallCount: 5, + }, + editor: process.env.EDITOR ?? "vim", +} + +let _config: SageConfig | null = null + +export function loadConfig(): SageConfig { + if (_config) return _config + if (!existsSync(CONFIG_PATH)) { + _config = { ...DEFAULTS } + return _config + } + try { + const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) + _config = deepMerge(DEFAULTS, raw) as SageConfig + } catch { + _config = { ...DEFAULTS } + } + return _config +} + +export function saveConfig(patch: Partial) { + const current = loadConfig() + _config = deepMerge(current, patch) as SageConfig + writeFileSync(CONFIG_PATH, JSON.stringify(_config, null, 2)) +} + +function deepMerge(base: Record, override: Record): Record { + const result = { ...base } + for (const key of Object.keys(override)) { + if (override[key] !== null && typeof override[key] === "object" && !Array.isArray(override[key]) && + typeof base[key] === "object" && base[key] !== null) { + result[key] = deepMerge(base[key] as Record, override[key] as Record) + } else { + result[key] = override[key] + } + } + return result +} diff --git a/packages/core/src/db/client.ts b/packages/core/src/db/client.ts new file mode 100644 index 0000000..d980595 --- /dev/null +++ b/packages/core/src/db/client.ts @@ -0,0 +1,117 @@ +import Database from "better-sqlite3" +import { drizzle } from "drizzle-orm/better-sqlite3" +import * as schema from "./schema.js" +import { existsSync, mkdirSync } from "fs" +import { join } from "path" +import { homedir } from "os" + +const DATA_DIR = join(homedir(), ".sage") +if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }) + +const DB_PATH = join(DATA_DIR, "sage.db") + +let _db: ReturnType | null = null +let _sqlite: Database.Database | null = null + +export function getDb() { + if (!_db) { + _sqlite = new Database(DB_PATH) + _sqlite.pragma("journal_mode = WAL") + _sqlite.pragma("foreign_keys = ON") + _db = drizzle(_sqlite, { schema }) + migrate(_sqlite) + } + return _db +} + +export function getRawDb(): Database.Database { + getDb() + return _sqlite! +} + +function migrate(sqlite: Database.Database) { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT 'New Session', + model TEXT NOT NULL, + provider TEXT NOT NULL, + cwd TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + metadata TEXT NOT NULL DEFAULT '{}' + ) + `) + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + role TEXT NOT NULL, + content TEXT NOT NULL, + tool_calls TEXT, + tool_call_id TEXT, + created_at INTEGER NOT NULL, + tokens_input INTEGER, + tokens_output INTEGER + ) + `) + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS tool_results ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + session_id TEXT NOT NULL, + tool_name TEXT NOT NULL, + tool_call_id TEXT NOT NULL, + input TEXT NOT NULL, + output TEXT NOT NULL, + exit_code INTEGER, + created_at INTEGER NOT NULL + ) + `) + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS memories ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + source TEXT NOT NULL DEFAULT 'manual', + session_id TEXT, + created_at INTEGER NOT NULL, + last_accessed INTEGER NOT NULL, + access_count INTEGER NOT NULL DEFAULT 0, + tags TEXT NOT NULL DEFAULT '[]' + ) + `) + + sqlite.exec(` + CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts + USING fts5(id UNINDEXED, content, tags, content='memories', content_rowid='rowid') + `) + + sqlite.exec(` + CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN + INSERT INTO memories_fts(rowid, id, content, tags) VALUES (new.rowid, new.id, new.content, new.tags); + END + `) + + sqlite.exec(` + CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN + INSERT INTO memories_fts(memories_fts, rowid, id, content, tags) VALUES ('delete', old.rowid, old.id, old.content, old.tags); + INSERT INTO memories_fts(rowid, id, content, tags) VALUES (new.rowid, new.id, new.content, new.tags); + END + `) + + sqlite.exec(` + CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN + INSERT INTO memories_fts(memories_fts, rowid, id, content, tags) VALUES ('delete', old.rowid, old.id, old.content, old.tags); + END + `) + + sqlite.exec(` + CREATE TABLE IF NOT EXISTS config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `) +} diff --git a/packages/core/src/db/schema.ts b/packages/core/src/db/schema.ts new file mode 100644 index 0000000..68f39a5 --- /dev/null +++ b/packages/core/src/db/schema.ts @@ -0,0 +1,52 @@ +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" + +export const sessions = sqliteTable("sessions", { + id: text("id").primaryKey(), + title: text("title").notNull().default("New Session"), + model: text("model").notNull(), + provider: text("provider").notNull(), + cwd: text("cwd").notNull(), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), + metadata: text("metadata").notNull().default("{}"), +}) + +export const messages = sqliteTable("messages", { + id: text("id").primaryKey(), + sessionId: text("session_id").notNull(), + role: text("role").notNull(), // user | assistant | tool + content: text("content").notNull(), + toolCalls: text("tool_calls"), // JSON + toolCallId: text("tool_call_id"), + createdAt: integer("created_at").notNull(), + tokensInput: integer("tokens_input"), + tokensOutput: integer("tokens_output"), +}) + +export const toolResults = sqliteTable("tool_results", { + id: text("id").primaryKey(), + messageId: text("message_id").notNull(), + sessionId: text("session_id").notNull(), + toolName: text("tool_name").notNull(), + toolCallId: text("tool_call_id").notNull(), + input: text("input").notNull(), + output: text("output").notNull(), + exitCode: integer("exit_code"), + createdAt: integer("created_at").notNull(), +}) + +export const memories = sqliteTable("memories", { + id: text("id").primaryKey(), + content: text("content").notNull(), + source: text("source").notNull().default("manual"), + sessionId: text("session_id"), + createdAt: integer("created_at").notNull(), + lastAccessed: integer("last_accessed").notNull(), + accessCount: integer("access_count").notNull().default(0), + tags: text("tags").notNull().default("[]"), +}) + +export const config = sqliteTable("config", { + key: text("key").primaryKey(), + value: text("value").notNull(), +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..75931d4 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,7 @@ +export * from "./db/client.js" +export * from "./db/schema.js" +export * from "./session/types.js" +export * from "./session/store.js" +export * from "./tool/types.js" +export * from "./tool/registry.js" +export * from "./config/index.js" diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts new file mode 100644 index 0000000..3e7ff59 --- /dev/null +++ b/packages/core/src/session/store.ts @@ -0,0 +1,86 @@ +import { eq, desc } from "drizzle-orm" +import { getDb } from "../db/client.js" +import { sessions, messages } from "../db/schema.js" +import type { Session, Message, CreateSessionInput, ToolCall } from "./types.js" +import { nanoid } from "nanoid" +import { cwd as getCwd } from "process" + +export async function createSession(input: CreateSessionInput): Promise { + const db = getDb() + const now = Date.now() + const id = nanoid() + const session: Session = { + id, + title: input.title ?? "New Session", + model: input.model, + provider: input.provider, + cwd: input.cwd ?? getCwd(), + createdAt: now, + updatedAt: now, + metadata: {}, + } + await db.insert(sessions).values({ + ...session, + metadata: JSON.stringify(session.metadata), + }) + return session +} + +export async function getSession(id: string): Promise { + const db = getDb() + const row = await db.select().from(sessions).where(eq(sessions.id, id)).get() + if (!row) return null + return { ...row, metadata: JSON.parse(row.metadata) } +} + +export async function listSessions(limit = 50): Promise { + const db = getDb() + const rows = await db.select().from(sessions).orderBy(desc(sessions.updatedAt)).limit(limit).all() + return rows.map(r => ({ ...r, metadata: JSON.parse(r.metadata) })) +} + +export async function updateSessionTitle(id: string, title: string) { + const db = getDb() + await db.update(sessions).set({ title, updatedAt: Date.now() }).where(eq(sessions.id, id)) +} + +export async function deleteSession(id: string) { + const db = getDb() + await db.delete(sessions).where(eq(sessions.id, id)) +} + +export async function addMessage(msg: Omit): Promise { + const db = getDb() + const id = nanoid() + const now = Date.now() + const full: Message = { ...msg, id, createdAt: now } + await db.insert(messages).values({ + ...full, + toolCalls: msg.toolCalls ? JSON.stringify(msg.toolCalls) : null, + }) + await db.update(sessions).set({ updatedAt: now }).where(eq(sessions.id, msg.sessionId)) + return full +} + +export async function getMessages(sessionId: string): Promise { + const db = getDb() + const rows = await db + .select() + .from(messages) + .where(eq(messages.sessionId, sessionId)) + .orderBy(messages.createdAt) + .all() + return rows.map(r => ({ + ...r, + role: r.role as Message["role"], + toolCalls: r.toolCalls ? (JSON.parse(r.toolCalls) as ToolCall[]) : undefined, + tokensInput: r.tokensInput ?? undefined, + tokensOutput: r.tokensOutput ?? undefined, + toolCallId: r.toolCallId ?? undefined, + })) +} + +export async function updateMessage(id: string, patch: Partial>) { + const db = getDb() + await db.update(messages).set(patch).where(eq(messages.id, id)) +} diff --git a/packages/core/src/session/types.ts b/packages/core/src/session/types.ts new file mode 100644 index 0000000..4327638 --- /dev/null +++ b/packages/core/src/session/types.ts @@ -0,0 +1,45 @@ +export type MessageRole = "user" | "assistant" | "tool" + +export interface ToolCall { + id: string + name: string + input: Record +} + +export interface ToolResult { + toolCallId: string + toolName: string + output: string + exitCode?: number + error?: boolean +} + +export interface Message { + id: string + sessionId: string + role: MessageRole + content: string + toolCalls?: ToolCall[] + toolCallId?: string + createdAt: number + tokensInput?: number + tokensOutput?: number +} + +export interface Session { + id: string + title: string + model: string + provider: string + cwd: string + createdAt: number + updatedAt: number + metadata: Record +} + +export interface CreateSessionInput { + model: string + provider: string + cwd?: string + title?: string +} diff --git a/packages/core/src/tool/bash.ts b/packages/core/src/tool/bash.ts new file mode 100644 index 0000000..e8cb46d --- /dev/null +++ b/packages/core/src/tool/bash.ts @@ -0,0 +1,70 @@ +import { z } from "zod" +import { spawn } from "child_process" +import type { ToolDef, ToolContext, ToolOutput } from "./types.js" + +const MAX_OUTPUT = 100_000 + +export const bash: ToolDef<{ command: string; description?: string; timeout?: number }> = { + name: "bash", + description: "Execute a shell command in the current working directory. Use for running tests, builds, git operations, and any shell task.", + inputSchema: z.object({ + command: z.string().describe("The shell command to execute"), + description: z.string().optional().describe("Short description of what this command does"), + timeout: z.number().optional().default(120000).describe("Timeout in milliseconds"), + }), + async execute(input, ctx: ToolContext): Promise { + const timeout = input.timeout ?? 120_000 + return new Promise((resolve) => { + const proc = spawn(process.platform === "win32" ? "cmd" : "sh", + process.platform === "win32" ? ["/c", input.command] : ["-c", input.command], + { + cwd: ctx.cwd, + env: { ...process.env }, + shell: false, + } + ) + + let stdout = "" + let stderr = "" + let killed = false + + const timer = setTimeout(() => { + killed = true + proc.kill("SIGTERM") + }, timeout) + + proc.stdout.on("data", (chunk: Buffer) => { + const text = chunk.toString() + stdout += text + ctx.onProgress?.(text) + }) + + proc.stderr.on("data", (chunk: Buffer) => { + stderr += chunk.toString() + }) + + proc.on("close", (code) => { + clearTimeout(timer) + let output = "" + if (stdout) output += stdout + if (stderr) output += (output ? "\n--- stderr ---\n" : "") + stderr + if (killed) output += "\n[Process killed: timeout exceeded]" + + const truncated = output.length > MAX_OUTPUT + if (truncated) output = output.slice(0, MAX_OUTPUT) + "\n... [output truncated]" + + resolve({ + content: output || "(no output)", + exitCode: code ?? 0, + error: (code ?? 0) !== 0, + truncated, + }) + }) + + proc.on("error", (err) => { + clearTimeout(timer) + resolve({ content: `Error: ${err.message}`, exitCode: 1, error: true }) + }) + }) + }, +} diff --git a/packages/core/src/tool/edit.ts b/packages/core/src/tool/edit.ts new file mode 100644 index 0000000..296c336 --- /dev/null +++ b/packages/core/src/tool/edit.ts @@ -0,0 +1,48 @@ +import { z } from "zod" +import { readFileSync, writeFileSync, existsSync } from "fs" +import { resolve } from "path" +import type { ToolDef, ToolContext, ToolOutput } from "./types.js" + +export const edit: ToolDef<{ file_path: string; old_string: string; new_string: string; replace_all?: boolean }> = { + name: "edit", + description: "Replace a specific string in a file with a new string. The old_string must match exactly (including whitespace). Use replace_all to replace every occurrence.", + inputSchema: z.object({ + file_path: z.string().describe("Path to the file to edit"), + old_string: z.string().describe("The exact string to find and replace"), + new_string: z.string().describe("The string to replace it with"), + replace_all: z.boolean().optional().default(false).describe("Replace all occurrences instead of just the first"), + }), + async execute(input, ctx: ToolContext): Promise { + const filePath = resolve(ctx.cwd, input.file_path) + + if (!existsSync(filePath)) { + return { content: `File not found: ${filePath}`, error: true } + } + + const original = readFileSync(filePath, "utf8") + + if (!original.includes(input.old_string)) { + // Attempt fuzzy match to give helpful error + const lines = original.split("\n") + const needle = input.old_string.trim() + const close = lines.findIndex(l => l.includes(needle.split("\n")[0]?.trim() ?? "")) + const hint = close >= 0 ? ` (closest match at line ${close + 1})` : "" + return { content: `old_string not found in file${hint}. Ensure the text matches exactly including whitespace.`, error: true } + } + + const occurrences = original.split(input.old_string).length - 1 + if (occurrences > 1 && !input.replace_all) { + return { + content: `old_string appears ${occurrences} times. Provide more context to make it unique, or set replace_all=true.`, + error: true, + } + } + + const updated = input.replace_all + ? original.split(input.old_string).join(input.new_string) + : original.replace(input.old_string, input.new_string) + + writeFileSync(filePath, updated, "utf8") + return { content: `Edited ${filePath}: replaced ${input.replace_all ? occurrences : 1} occurrence(s)` } + }, +} diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts new file mode 100644 index 0000000..06bd828 --- /dev/null +++ b/packages/core/src/tool/glob.ts @@ -0,0 +1,66 @@ +import { z } from "zod" +import { resolve } from "path" +import { spawnSync } from "child_process" +import { readdirSync, statSync, existsSync } from "fs" +import type { ToolDef, ToolContext, ToolOutput } from "./types.js" + +const MAX_RESULTS = 500 + +export const glob: ToolDef<{ pattern: string; path?: string }> = { + name: "glob", + description: "Find files matching a glob pattern. Returns file paths sorted by modification time.", + inputSchema: z.object({ + pattern: z.string().describe("Glob pattern, e.g. '**/*.ts' or 'src/**/*.tsx'"), + path: z.string().optional().describe("Directory to search in (default: cwd)"), + }), + async execute(input, ctx: ToolContext): Promise { + const searchDir = resolve(ctx.cwd, input.path ?? ".") + const results = matchGlob(input.pattern, searchDir) + const truncated = results.length > MAX_RESULTS + const listed = results.slice(0, MAX_RESULTS) + return { + content: listed.length > 0 ? listed.join("\n") : "(no matches)", + truncated, + } + }, +} + +function matchGlob(pattern: string, dir: string): string[] { + // Try ripgrep first for performance + const rg = spawnSync("rg", ["--files", "--glob", pattern, dir], { + encoding: "utf8", + maxBuffer: 10_000_000, + }) + if (rg.status === 0) { + return rg.stdout.trim().split("\n").filter(Boolean) + } + + // Fallback: simple recursive walk + return walkAndMatch(dir, dir, patternToRegex(pattern)) +} + +function walkAndMatch(base: string, dir: string, regex: RegExp): string[] { + if (!existsSync(dir)) return [] + const results: string[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name.startsWith(".")) continue + const full = `${dir}/${entry.name}` + const rel = full.slice(base.length + 1) + if (entry.isDirectory()) { + results.push(...walkAndMatch(base, full, regex)) + } else if (regex.test(rel)) { + results.push(full) + } + } + return results +} + +function patternToRegex(pattern: string): RegExp { + const escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*\*/g, "§DOUBLE§") + .replace(/\*/g, "[^/]*") + .replace(/§DOUBLE§/g, ".*") + .replace(/\?/g, "[^/]") + return new RegExp(`^${escaped}$`) +} diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts new file mode 100644 index 0000000..e367bbb --- /dev/null +++ b/packages/core/src/tool/grep.ts @@ -0,0 +1,50 @@ +import { z } from "zod" +import { resolve } from "path" +import { spawnSync } from "child_process" +import type { ToolDef, ToolContext, ToolOutput } from "./types.js" + +const MAX_OUTPUT = 50_000 + +export const grep: ToolDef<{ pattern: string; path?: string; glob?: string; output_mode?: "content" | "files" | "count"; case_insensitive?: boolean; context?: number }> = { + name: "grep", + description: "Search for a regex pattern in files using ripgrep. Returns matching content or file paths.", + inputSchema: z.object({ + pattern: z.string().describe("Regular expression pattern to search for"), + path: z.string().optional().describe("File or directory to search in"), + glob: z.string().optional().describe("Glob pattern to filter files, e.g. '*.ts'"), + output_mode: z.enum(["content", "files", "count"]).optional().default("content").describe("content: show matching lines, files: only file paths, count: match counts"), + case_insensitive: z.boolean().optional().default(false), + context: z.number().optional().describe("Lines of context around each match"), + }), + async execute(input, ctx: ToolContext): Promise { + const searchPath = resolve(ctx.cwd, input.path ?? ".") + const args = ["--no-heading", "--color=never"] + + if (input.case_insensitive) args.push("-i") + if (input.glob) args.push("--glob", input.glob) + if (input.output_mode === "files") args.push("-l") + if (input.output_mode === "count") args.push("-c") + if (input.context) args.push(`-C${input.context}`) + if (input.output_mode === "content") args.push("-n") + + args.push(input.pattern, searchPath) + + const result = spawnSync("rg", args, { + encoding: "utf8", + maxBuffer: MAX_OUTPUT * 2, + }) + + let output = (result.stdout ?? "") + (result.stderr ?? "") + if (result.error) { + // Fallback when rg not available + output = `ripgrep not found. Install it for grep support.\nError: ${result.error.message}` + return { content: output, error: true } + } + + const truncated = output.length > MAX_OUTPUT + return { + content: output.slice(0, MAX_OUTPUT) || "(no matches)", + truncated, + } + }, +} diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/read.ts new file mode 100644 index 0000000..0708c10 --- /dev/null +++ b/packages/core/src/tool/read.ts @@ -0,0 +1,54 @@ +import { z } from "zod" +import { readFileSync, existsSync, statSync, readdirSync } from "fs" +import { join, resolve } from "path" +import type { ToolDef, ToolContext, ToolOutput } from "./types.js" + +const MAX_FILE_SIZE = 1_000_000 +const MAX_LINES = 2000 + +export const read: ToolDef<{ file_path: string; offset?: number; limit?: number }> = { + name: "read", + description: "Read the contents of a file. Use offset/limit for large files.", + inputSchema: z.object({ + file_path: z.string().describe("Absolute or relative path to the file"), + offset: z.number().optional().describe("Line number to start reading from (1-based)"), + limit: z.number().optional().describe("Maximum number of lines to read"), + }), + async execute(input, ctx: ToolContext): Promise { + const filePath = resolve(ctx.cwd, input.file_path) + + if (!existsSync(filePath)) { + return { content: `File not found: ${filePath}`, error: true } + } + + const stat = statSync(filePath) + if (stat.isDirectory()) { + const entries = readdirSync(filePath, { withFileTypes: true }) + const listing = entries + .map(e => `${e.isDirectory() ? "d" : "f"} ${e.name}`) + .join("\n") + return { content: `Directory listing of ${filePath}:\n${listing}` } + } + + if (stat.size > MAX_FILE_SIZE) { + return { + content: `File too large (${Math.round(stat.size / 1024)}KB). Use offset/limit to read specific sections.`, + error: true, + } + } + + const raw = readFileSync(filePath, "utf8") + const lines = raw.split("\n") + const offset = (input.offset ?? 1) - 1 + const limit = input.limit ?? MAX_LINES + const slice = lines.slice(offset, offset + limit) + const numbered = slice.map((l, i) => `${String(offset + i + 1).padStart(4, " ")}\t${l}`).join("\n") + + const truncated = lines.length > offset + limit + + return { + content: numbered, + truncated, + } + }, +} diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts new file mode 100644 index 0000000..25174c9 --- /dev/null +++ b/packages/core/src/tool/registry.ts @@ -0,0 +1,20 @@ +import type { AnyToolDef } from "./types.js" +import { bash } from "./bash.js" +import { read } from "./read.js" +import { write } from "./write.js" +import { edit } from "./edit.js" +import { glob } from "./glob.js" +import { grep } from "./grep.js" + +export const ALL_TOOLS: AnyToolDef[] = [ + bash as AnyToolDef, + read as AnyToolDef, + write as AnyToolDef, + edit as AnyToolDef, + glob as AnyToolDef, + grep as AnyToolDef, +] + +export function getTool(name: string): AnyToolDef | undefined { + return ALL_TOOLS.find(t => t.name === name) +} diff --git a/packages/core/src/tool/types.ts b/packages/core/src/tool/types.ts new file mode 100644 index 0000000..c2f4b7e --- /dev/null +++ b/packages/core/src/tool/types.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export interface ToolDef> { + name: string + description: string + inputSchema: z.ZodType + execute: (input: TInput, context: ToolContext) => Promise +} + +export interface ToolContext { + sessionId: string + cwd: string + onProgress?: (text: string) => void +} + +export interface ToolOutput { + content: string + exitCode?: number + error?: boolean + truncated?: boolean +} + +export type AnyToolDef = ToolDef> diff --git a/packages/core/src/tool/write.ts b/packages/core/src/tool/write.ts new file mode 100644 index 0000000..ea56623 --- /dev/null +++ b/packages/core/src/tool/write.ts @@ -0,0 +1,21 @@ +import { z } from "zod" +import { writeFileSync, mkdirSync, existsSync } from "fs" +import { resolve, dirname } from "path" +import type { ToolDef, ToolContext, ToolOutput } from "./types.js" + +export const write: ToolDef<{ file_path: string; content: string }> = { + name: "write", + description: "Write content to a file, creating it and any parent directories if needed. Overwrites existing files.", + inputSchema: z.object({ + file_path: z.string().describe("Path to the file to write"), + content: z.string().describe("Content to write to the file"), + }), + async execute(input, ctx: ToolContext): Promise { + const filePath = resolve(ctx.cwd, input.file_path) + const dir = dirname(filePath) + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) + writeFileSync(filePath, input.content, "utf8") + const lines = input.content.split("\n").length + return { content: `Wrote ${lines} lines to ${filePath}` } + }, +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..a086b14 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/llm/package.json b/packages/llm/package.json new file mode 100644 index 0000000..20c804b --- /dev/null +++ b/packages/llm/package.json @@ -0,0 +1,19 @@ +{ + "name": "@sage/llm", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { ".": "./src/index.ts" }, + "dependencies": { + "@sage/core": "*", + "ai": "^4.3.0", + "@ai-sdk/anthropic": "^1.0.0", + "@ai-sdk/openai": "^1.0.0", + "@ai-sdk/google": "^1.0.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + } +} diff --git a/packages/llm/src/client.ts b/packages/llm/src/client.ts new file mode 100644 index 0000000..f37f0d5 --- /dev/null +++ b/packages/llm/src/client.ts @@ -0,0 +1,148 @@ +import { streamText, tool, type CoreMessage, type CoreTool, type TextStreamPart } from "ai" +import { z } from "zod" +import type { SageConfig, AnyToolDef, ToolContext } from "@sage/core" +import { ALL_TOOLS, getTool, addMessage, updateMessage } from "@sage/core" +import { getLanguageModel, type ProviderID } from "./providers/index.js" +import type { LLMEventEmitter } from "./types.js" + +const SYSTEM_PROMPT = `You are Sage, an expert AI coding assistant. You help users write, debug, and understand code. + +You have access to tools to read/write files, run shell commands, search code, and more. +Always prefer using tools to gather information before answering. +When editing files, use the edit tool for small changes and write for large rewrites. +Be concise but complete. Show your reasoning when working through complex problems.` + +export interface RunOptions { + sessionId: string + messages: CoreMessage[] + model: string + provider: string + cwd: string + config: SageConfig + memoryContext?: string + onEvent: LLMEventEmitter + signal?: AbortSignal +} + +export async function runLLM(opts: RunOptions): Promise { + const { sessionId, messages, model, provider, cwd, config, onEvent, signal } = opts + + const toolContext: ToolContext = { + sessionId, + cwd, + onProgress: (text) => { + /* incremental tool output — currently unused at this level */ + }, + } + + const aiTools: Record = {} + for (const t of ALL_TOOLS) { + aiTools[t.name] = tool({ + description: t.description, + parameters: t.inputSchema as z.ZodType>, + execute: async (input) => { + const result = await t.execute(input as Record, toolContext) + return result + }, + }) + } + + const systemParts: string[] = [SYSTEM_PROMPT] + if (opts.memoryContext) { + systemParts.push(`\n## Recalled Memories\n${opts.memoryContext}`) + } + systemParts.push(`\nCurrent working directory: ${cwd}`) + + const lm = getLanguageModel(provider as ProviderID, model, config) + + const currentMessages = [...messages] + let assistantMsgId: string | null = null + let accText = "" + + const stream = streamText({ + model: lm, + system: systemParts.join("\n"), + messages: currentMessages, + tools: aiTools, + maxSteps: 20, + abortSignal: signal, + }) + + for await (const part of stream.fullStream) { + if (signal?.aborted) break + + switch (part.type) { + case "text-delta": { + if (!assistantMsgId) { + const msg = await addMessage({ + sessionId, + role: "assistant", + content: "", + }) + assistantMsgId = msg.id + } + accText += part.textDelta + await updateMessage(assistantMsgId, { content: accText }) + onEvent({ type: "text-delta", delta: part.textDelta }) + break + } + + case "tool-call": { + onEvent({ + type: "tool-call", + toolCall: { + id: part.toolCallId, + name: part.toolName, + input: part.args as Record, + }, + }) + break + } + + case "tool-result": { + const output = typeof part.result === "string" + ? part.result + : JSON.stringify(part.result, null, 2) + + await addMessage({ + sessionId, + role: "tool", + content: output, + toolCallId: part.toolCallId, + }) + + onEvent({ + type: "tool-result", + toolResult: { + toolCallId: part.toolCallId, + toolName: part.toolName, + output, + }, + }) + break + } + + case "finish": { + const usage = part.usage + if (assistantMsgId && usage) { + await updateMessage(assistantMsgId, { + tokensInput: usage.promptTokens, + tokensOutput: usage.completionTokens, + }) + } + onEvent({ + type: "finish", + usage: usage + ? { inputTokens: usage.promptTokens, outputTokens: usage.completionTokens } + : undefined, + }) + break + } + + case "error": { + onEvent({ type: "error", error: part.error as Error }) + break + } + } + } +} diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts new file mode 100644 index 0000000..e55b942 --- /dev/null +++ b/packages/llm/src/index.ts @@ -0,0 +1,3 @@ +export * from "./client.js" +export * from "./providers/index.js" +export * from "./types.js" diff --git a/packages/llm/src/providers/index.ts b/packages/llm/src/providers/index.ts new file mode 100644 index 0000000..eacb2ef --- /dev/null +++ b/packages/llm/src/providers/index.ts @@ -0,0 +1,75 @@ +import { createAnthropic } from "@ai-sdk/anthropic" +import { createOpenAI } from "@ai-sdk/openai" +import { createGoogleGenerativeAI } from "@ai-sdk/google" +import type { LanguageModelV1 } from "ai" +import type { SageConfig } from "@sage/core" + +export type ProviderID = "anthropic" | "openai" | "google" | "openrouter" + +export interface ProviderModel { + id: string + name: string + contextWindow: number + supportsTools: boolean +} + +export const PROVIDER_MODELS: Record = { + anthropic: [ + { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 200_000, supportsTools: true }, + { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextWindow: 200_000, supportsTools: true }, + { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextWindow: 200_000, supportsTools: true }, + ], + openai: [ + { id: "gpt-4o", name: "GPT-4o", contextWindow: 128_000, supportsTools: true }, + { id: "gpt-4o-mini", name: "GPT-4o Mini", contextWindow: 128_000, supportsTools: true }, + { id: "o3", name: "o3", contextWindow: 200_000, supportsTools: true }, + ], + google: [ + { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", contextWindow: 1_000_000, supportsTools: true }, + { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", contextWindow: 1_000_000, supportsTools: true }, + ], + openrouter: [ + { id: "anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6 (OR)", contextWindow: 200_000, supportsTools: true }, + { id: "openai/gpt-4o", name: "GPT-4o (OR)", contextWindow: 128_000, supportsTools: true }, + { id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro (OR)", contextWindow: 1_000_000, supportsTools: true }, + { id: "deepseek/deepseek-r1", name: "DeepSeek R1 (OR)", contextWindow: 64_000, supportsTools: true }, + ], +} + +export function getLanguageModel(provider: ProviderID, modelId: string, config: SageConfig): LanguageModelV1 { + const providerCfg = config.providers[provider] ?? {} + + switch (provider) { + case "anthropic": { + const apiKey = (providerCfg as { apiKey?: string }).apiKey ?? process.env.ANTHROPIC_API_KEY + const client = createAnthropic({ apiKey }) + return client(modelId) as LanguageModelV1 + } + case "openai": { + const cfg = providerCfg as { apiKey?: string; baseUrl?: string } + const apiKey = cfg.apiKey ?? process.env.OPENAI_API_KEY + const client = createOpenAI({ apiKey, baseURL: cfg.baseUrl }) + return client(modelId) as LanguageModelV1 + } + case "google": { + const apiKey = (providerCfg as { apiKey?: string }).apiKey ?? process.env.GOOGLE_API_KEY + const client = createGoogleGenerativeAI({ apiKey }) + return client(modelId) as LanguageModelV1 + } + case "openrouter": { + const apiKey = (providerCfg as { apiKey?: string }).apiKey ?? process.env.OPENROUTER_API_KEY + const client = createOpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1", + headers: { "HTTP-Referer": "https://github.com/sage-ai", "X-Title": "Sage" }, + }) + return client(modelId) as LanguageModelV1 + } + default: + throw new Error(`Unknown provider: ${provider}`) + } +} + +export function listProviders(): ProviderID[] { + return ["anthropic", "openai", "google", "openrouter"] +} diff --git a/packages/llm/src/types.ts b/packages/llm/src/types.ts new file mode 100644 index 0000000..a774d10 --- /dev/null +++ b/packages/llm/src/types.ts @@ -0,0 +1,22 @@ +import type { CoreMessage, CoreTool, StreamTextResult } from "ai" +import type { ToolCall, ToolResult } from "@sage/core" + +export interface LLMRequest { + messages: CoreMessage[] + tools?: Record + model: string + provider: string + maxSteps?: number + systemPrompt?: string +} + +export interface LLMStreamEvent { + type: "text-delta" | "tool-call" | "tool-result" | "finish" | "error" + delta?: string + toolCall?: { id: string; name: string; input: Record } + toolResult?: ToolResult + usage?: { inputTokens: number; outputTokens: number } + error?: Error +} + +export type LLMEventEmitter = (event: LLMStreamEvent) => void diff --git a/packages/llm/tsconfig.json b/packages/llm/tsconfig.json new file mode 100644 index 0000000..a086b14 --- /dev/null +++ b/packages/llm/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/memory/package.json b/packages/memory/package.json new file mode 100644 index 0000000..f1bb374 --- /dev/null +++ b/packages/memory/package.json @@ -0,0 +1,16 @@ +{ + "name": "@sage/memory", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { ".": "./src/index.ts" }, + "dependencies": { + "@sage/core": "*", + "nanoid": "^5.0.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.8.0" + } +} diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts new file mode 100644 index 0000000..62cf5c1 --- /dev/null +++ b/packages/memory/src/index.ts @@ -0,0 +1,2 @@ +export * from "./store.js" +export * from "./summarizer.js" diff --git a/packages/memory/src/retrieval.ts b/packages/memory/src/retrieval.ts new file mode 100644 index 0000000..4b5d464 --- /dev/null +++ b/packages/memory/src/retrieval.ts @@ -0,0 +1,56 @@ +import Database from "better-sqlite3" +import type { Memory } from "./store.js" + +// Recall memories using SQLite FTS5 BM25 ranking +export function recallMemoriesRaw( + sqlite: Database.Database, + query: string, + limit: number +): Array { + const sanitized = sanitizeFtsQuery(query) + if (!sanitized) return [] + + const rows = sqlite.prepare<{ + id: string; content: string; source: string; session_id: string | null; + created_at: number; last_accessed: number; access_count: number; tags: string; rank: number + }, [string, number]>(` + SELECT m.id, m.content, m.source, m.session_id, m.created_at, + m.last_accessed, m.access_count, m.tags, mf.rank + FROM memories_fts mf + JOIN memories m ON m.id = mf.id + WHERE memories_fts MATCH ? + ORDER BY mf.rank + LIMIT ? + `).all(sanitized, limit) + + if (rows.length === 0) return [] + + // Update access stats + const now = Date.now() + const ids = rows.map(r => r.id) + const placeholders = ids.map(() => "?").join(",") + sqlite.prepare(`UPDATE memories SET last_accessed = ?, access_count = access_count + 1 WHERE id IN (${placeholders})`) + .run(now, ...ids) + + return rows.map(r => ({ + id: r.id, + content: r.content, + source: r.source as Memory["source"], + sessionId: r.session_id ?? undefined, + createdAt: r.created_at, + lastAccessed: r.last_accessed, + accessCount: r.access_count, + tags: JSON.parse(r.tags) as string[], + score: -r.rank, + })) +} + +function sanitizeFtsQuery(query: string): string { + return query + .replace(/["*^()]/g, " ") + .trim() + .split(/\s+/) + .filter(w => w.length > 2) + .map(w => `"${w}"`) + .join(" OR ") +} diff --git a/packages/memory/src/store.ts b/packages/memory/src/store.ts new file mode 100644 index 0000000..732244d --- /dev/null +++ b/packages/memory/src/store.ts @@ -0,0 +1,86 @@ +import { nanoid } from "nanoid" +import { getRawDb, getDb } from "@sage/core" +import { memories } from "@sage/core" +import { eq } from "drizzle-orm" +import { recallMemoriesRaw } from "./retrieval.js" + +export interface Memory { + id: string + content: string + source: "manual" | "auto" | "session" + sessionId?: string + createdAt: number + lastAccessed: number + accessCount: number + tags: string[] +} + +export interface RecalledMemory extends Memory { + score: number +} + +export async function addMemory( + content: string, + opts: { source?: Memory["source"]; sessionId?: string; tags?: string[] } = {} +): Promise { + const db = getDb() + const now = Date.now() + const id = nanoid() + const tags = opts.tags ?? extractTags(content) + const mem: Memory = { + id, + content, + source: opts.source ?? "manual", + sessionId: opts.sessionId, + createdAt: now, + lastAccessed: now, + accessCount: 0, + tags, + } + await db.insert(memories).values({ + ...mem, + tags: JSON.stringify(tags), + sessionId: opts.sessionId ?? null, + }) + return mem +} + +export async function recallMemories(query: string, limit = 5): Promise { + const sqlite = getRawDb() + return recallMemoriesRaw(sqlite, query, limit) +} + +export async function listMemories(limit = 100): Promise { + const db = getDb() + const rows = await db.select().from(memories).limit(limit).all() + return rows.map(r => ({ + ...r, + source: r.source as Memory["source"], + sessionId: r.sessionId ?? undefined, + tags: JSON.parse(r.tags) as string[], + })) +} + +export async function deleteMemory(id: string): Promise { + const db = getDb() + const result = await db.delete(memories).where(eq(memories.id, id)) + return ((result as unknown as { changes: number }).changes ?? 0) > 0 +} + +export function formatMemoriesForContext(mems: RecalledMemory[]): string { + if (mems.length === 0) return "" + return mems.map((m, i) => `${i + 1}. ${m.content}`).join("\n") +} + +function extractTags(content: string): string[] { + const words = content.toLowerCase().match(/\b[a-z][a-z0-9]{2,}\b/g) ?? [] + const stopWords = new Set(["the", "and", "for", "that", "this", "with", "from", "have", "been", "will", "not", "but"]) + const freq: Record = {} + for (const w of words) { + if (!stopWords.has(w)) freq[w] = (freq[w] ?? 0) + 1 + } + return Object.entries(freq) + .sort((a, b) => b[1] - a[1]) + .slice(0, 8) + .map(([w]) => w) +} diff --git a/packages/memory/src/summarizer.ts b/packages/memory/src/summarizer.ts new file mode 100644 index 0000000..d46aad3 --- /dev/null +++ b/packages/memory/src/summarizer.ts @@ -0,0 +1,51 @@ +import type { CoreMessage } from "ai" +import { addMemory } from "./store.js" + +// Auto-summarize a completed session and extract key memories +export async function summarizeSession( + sessionId: string, + messages: CoreMessage[], + summarizeFn: (prompt: string) => Promise +): Promise { + if (messages.length < 4) return [] + + const transcript = messages + .filter(m => m.role === "user" || m.role === "assistant") + .slice(-30) // last 30 messages max + .map(m => `${m.role.toUpperCase()}: ${typeof m.content === "string" ? m.content : JSON.stringify(m.content)}`) + .join("\n\n") + + const prompt = `You are extracting key facts and insights from a coding session transcript for long-term memory storage. + +Extract 3-7 important, reusable pieces of information that would be valuable in future sessions: +- Technical decisions made (architecture, libraries chosen, patterns used) +- Bugs found and their root causes +- User preferences or conventions discovered +- Project-specific knowledge (file structure, APIs, domain concepts) +- Important constraints or requirements + +Format as a JSON array of strings, each being a self-contained, specific memory. +Do NOT include generic observations. Each memory should be specific and actionable. + +TRANSCRIPT: +${transcript} + +Return ONLY a JSON array of strings.` + + try { + const response = await summarizeFn(prompt) + const extracted = JSON.parse(response.trim()) as string[] + if (!Array.isArray(extracted)) return [] + + const stored: string[] = [] + for (const fact of extracted.slice(0, 7)) { + if (typeof fact === "string" && fact.length > 10) { + await addMemory(fact, { source: "auto", sessionId }) + stored.push(fact) + } + } + return stored + } catch { + return [] + } +} diff --git a/packages/memory/tsconfig.json b/packages/memory/tsconfig.json new file mode 100644 index 0000000..a086b14 --- /dev/null +++ b/packages/memory/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/tui/package.json b/packages/tui/package.json new file mode 100644 index 0000000..92219be --- /dev/null +++ b/packages/tui/package.json @@ -0,0 +1,28 @@ +{ + "name": "@sage/tui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "node --import=tsx/esm src/index.tsx", + "dev": "node --import=tsx/esm --watch src/index.tsx", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@sage/core": "*", + "@sage/llm": "*", + "@sage/memory": "*", + "ink": "^5.1.0", + "ink-text-input": "^6.0.0", + "react": "^18.3.0", + "chalk": "^5.4.0", + "figures": "^6.1.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^18.3.0", + "tsx": "^4.19.0", + "typescript": "^5.8.0" + } +} diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx new file mode 100644 index 0000000..fbdfc87 --- /dev/null +++ b/packages/tui/src/app.tsx @@ -0,0 +1,101 @@ +import React from "react" +import { Box, Text, useInput } from "ink" +import { colors } from "./theme/index.js" +import { MessageList } from "./components/MessageList.js" +import { InputBar } from "./components/InputBar.js" +import { MemoryBar } from "./components/MemoryBar.js" +import { MemoryPanel } from "./components/MemoryPanel.js" +import { SessionOverlay } from "./components/SessionOverlay.js" +import { Footer } from "./components/Footer.js" +import { useAppState } from "./hooks/useAppState.js" +import { useState } from "react" + +export function App() { + const [sessionCursor, setSessionCursor] = useState(0) + const { state, patch, submitMessage, selectSession, newSession, removeSession, abort, config } = useAppState() + + useInput((_input, key) => { + if (state.openOverlay) return + if (key.ctrl && _input === "k") { patch({ openOverlay: "sessions" }); return } + if (key.escape && state.error) { patch({ error: null }); return } + }) + + const model = state.currentSession?.model ?? config.defaultModel + const provider = state.currentSession?.provider ?? config.defaultProvider + const cwd = state.currentSession?.cwd ?? process.cwd() + + if (state.openOverlay === "sessions") { + return ( + { selectSession(s); patch({ openOverlay: null }) }} + onNew={() => { newSession(); patch({ openOverlay: null }) }} + onDelete={id => { removeSession(id); setSessionCursor(c => Math.max(0, c - 1)) }} + onClose={() => patch({ openOverlay: null })} + /> + ) + } + + if (state.openOverlay === "memories") { + return patch({ openOverlay: null })} /> + } + + return ( + + {/* Session title */} + + sage + {state.currentSession + ? {state.currentSession.title} + : no session + } + + + {/* Welcome */} + {!state.currentSession && ( + + start typing to begin, or ^K to switch sessions + + )} + + {/* Messages */} + {state.currentSession && ( + + )} + + {/* Error/info */} + {state.error && ( + + + {state.error} + + {!state.error.startsWith("✓") && esc} + + )} + + {/* Memory recall indicator */} + + + {/* Input */} + + + {/* Footer */} +