diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4b73671 --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# ExCLI Agent Configuration + +# OpenAI API 配置 +OPENAI_API_KEY=your-api-key-here +OPENAI_BASE_URL=https://api.openai.com/v1 # OpenAI 或兼容 API 的地址 +OPENAI_MODEL=gpt-4o-mini + +# Agent 行为配置 +AGENT_NAME=excli +AGENT_TEMPERATURE=0.7 +AGENT_MAX_TOKENS=4096 +AGENT_TRACE_MODE=false + +# Memory 配置 +MEMORY_DB_PATH=./data/memory.db +MAX_SHORT_TERM_MEMORY=20 # 短期记忆最大条目数 +MEMORY_EXTRACTION_INTERVAL=5 # 每隔多少轮提取一次长期记忆 + +# 工具配置 +TOOL_TIMEOUT_MS=30000 +TOOL_MAX_RETRIES=2 + +# 日志配置 +LOG_LEVEL=info +LOG_DIR=./logs +LOG_MAX_FILES=10 + +# CLI 配置 +CLI_PROMPT_PREFIX=excli> +CLI_WELCOME_MESSAGE=欢迎使用 ExCLI!你可以用自然语言与 AI 交互,输入 help 查看命令帮助。 +CLI_COLOR_OUTPUT=true \ No newline at end of file diff --git a/README.md b/README.md index 66faaa7..7bb1d0e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,181 @@ -# excli +# ExCLI - Agent CLI 框架 -一个简单可以使用的agent \ No newline at end of file +一个具有更好记忆能力、输出能力和工具使用能力的 Agent CLI 框架。 + +## 项目目标 + +这个 Agent CLI 解决什么问题: +- 更智能的上下文理解:通过短期和长期记忆记住用户偏好和项目背景 +- 结构化输出:根据任务类型自动选择最佳输出风格 +- 高效工具集成:通过统一的工具接口完成文件、搜索、执行等任务 + +适合场景: +- 个人 AI 助手 +- 代码开发和调试 +- 文档查询和管理 +- 自动化任务执行 + +核心能力: +- 记忆系统:自动提取和检索长期记忆 +- 输出风格:自动判断简洁/详细/技术型/写作型 +- 工具调用:智能选择和执行工具 + +## 技术选型 + +- 语言:Node.js 18+ +- CLI:原生 readline + Chalk + Ora +- 数据存储:JSON 文件存储 +- 模型调用:OpenAI API (兼容任何 OpenAI 兼容 API) +- 日志:Pino +- 配置:环境变量 + .env + +## 安装 + +```bash +# 克隆项目 +cd excli + +# 安装依赖 +npm install + +# 配置环境变量 +cp .env.example .env +# 编辑 .env 配置 OPENAI_API_KEY +``` + +## 使用 + +```bash +# 直接运行 +node bin/cli.js + +# 或使用启动脚本 +chmod +x start.sh +./start.sh +``` + +## CLI 命令 + +- `help` - 显示帮助 +- `clear` - 清屏 +- `memory` - 查看记忆状态 +- `preferences` - 查看用户偏好 +- `tools` - 查看可用工具 +- `stats` - 查看统计信息 +- `quit/exit` - 退出 + +## 项目结构 + +``` +excli/ +├── bin/ +│ └── cli.js # CLI 入口 +├── src/ +│ ├── index.js # 主程序入口 +│ ├── config/ # 配置模块 +│ ├── core/ # 核心模块 +│ │ ├── agent.js # Agent 主循环 +│ │ └── model.js # 模型适配层 +│ ├── memory/ # 记忆模块 +│ ├── prompts/ # Prompt 模板 +│ ├── tools/ # 工具模块 +│ └── utils/ # 工具函数 +├── data/ # 数据目录 +│ ├── memory/ # 长期记忆 +│ └── sessions/ # 会话数据 +├── logs/ # 日志目录 +├── bin/ # 可执行脚本 +├── package.json +└── .env.example +``` + +## 功能说明 + +### 记忆系统 + +- **短期记忆**:保存当前会话上下文,包含用户输入、助手输出、工具结果 +- **长期记忆**:JSON 文件持久化存储,区分敏感信息,自动提取 +- **记忆提取**:每隔 N 轮对话自动提取重要信息 + +### 工具系统 + +- `read_file` - 读取文件 +- `write_file` - 写入文件 +- `list_files` - 列出文件 +- `run_shell` - 执行命令 +- `search_text` - 搜索文本 + +### 输出风格 + +根据任务类型自动选择: +- 简洁模式:简短回答 +- 详细模式:完整解释 +- 技术型:代码和命令 +- 写作型:文档和说明 + +## 配置 + +主要环境变量: +- `OPENAI_API_KEY` - OpenAI API 密钥 +- `OPENAI_BASE_URL` - API 地址(默认 OpenAI) +- `OPENAI_MODEL` - 模型名称 +- `AGENT_TEMPERATURE` - 温度参数 +- `MEMORY_DB_PATH` - 记忆数据库路径 +- `LOG_LEVEL` - 日志级别 + +## 扩展 + +### 添加新工具 + +在 `src/tools/index.js` 添加新的工具类: + +```javascript +export class MyTool extends BaseTool { + constructor() { + super('my_tool', '工具描述', { + // JSON Schema + }); + } + + async execute(args) { + // 实现逻辑 + return { success: true, result: '...' }; + } +} +``` + +### 修改 System Prompt + +在 `src/prompts/index.js` 修改 `SYSTEM_PROMPT`。 + +## 示例 + +运行示例: + +``` +$ ./start.sh +欢迎使用 ExCLI!你可以用自然语言��� AI 交互,输入 help 查看命令帮助。 + +excli> 帮助 +# 显示帮助... + +excli> 列出当前目录的 js 文件 +# AI 处理并返回结果... + +excli> memory +# 显示当前记忆状态... + +excli> quit +再见! +``` + +## 注意事项 + +1. 首次运行需要配置 `OPENAI_API_KEY` +2. 工具执行可能会根据命令不同而有风险,请谨慎使用 +3. 敏感信息不会写入长期记忆 +4. 模型调用可能产生费用,请注意使用量 + +## License + +MIT \ No newline at end of file diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..1db9d1d --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +// ExCLI 入口脚本 +import { run } from '../src/index.js'; + +// 全局错误处理 +process.on('uncaughtException', (error) => { + // 忽略 readline 关闭错误(使用管道输入时会出现) + if (error.message === 'readline was closed' || error.message === 'Cannot read properties of undefined (reading \'isPaused\')') { + return; + } + console.error('未捕获的错误:', error.message); + process.exit(1); +}); + +process.on('unhandledRejection', (reason) => { + // 忽略 readline 关闭错误 + if (reason?.message === 'readline was closed') { + return; + } + console.error('未处理的 Promise 拒绝:', reason); + process.exit(1); +}); + +// 运行程序 +run().catch((error) => { + // 忽略 readline 关闭错误 + if (error?.message === 'readline was closed') { + return; + } + console.error('启动错误:', error.message); + process.exit(1); +}); \ No newline at end of file diff --git a/data/memory/memories.json b/data/memory/memories.json new file mode 100644 index 0000000..84a0a60 --- /dev/null +++ b/data/memory/memories.json @@ -0,0 +1,7 @@ +{ + "data": [], + "metadata": { + "createdAt": 1777764119795, + "lastSavedAt": 1777764474644 + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8b937d0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1106 @@ +{ + "name": "excli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "excli", + "version": "1.0.0", + "dependencies": { + "chalk": "^5.3.0", + "dotenv": "^16.4.5", + "openai": "^4.28.0", + "ora": "^7.0.1", + "pino": "^8.18.0", + "pino-pretty": "^10.3.1" + }, + "bin": { + "excli": "bin/cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "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/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/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/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "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/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/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.3.1.tgz", + "integrity": "sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "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/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/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/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "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": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "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/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5ca9f91 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "excli", + "version": "1.0.0", + "description": "一个具有更好记忆能力、输出能力和工具使用能力的 Agent CLI", + "main": "src/index.js", + "type": "module", + "bin": { + "excli": "./bin/cli.js" + }, + "scripts": { + "start": "node bin/cli.js", + "dev": "node --watch bin/cli.js", + "test": "node --test src/**/*.test.js" + }, + "dependencies": { + "openai": "^4.28.0", + "chalk": "^5.3.0", + "ora": "^7.0.1", + "dotenv": "^16.4.5", + "pino": "^8.18.0", + "pino-pretty": "^10.3.1" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..1d2f6a1 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,133 @@ +import pino from 'pino'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import dotenv from 'dotenv'; + +// 加载 .env 文件 +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 默认配置 +const DEFAULT_CONFIG = { + // OpenAI API 配置 + OPENAI_API_KEY: process.env.OPENAI_API_KEY || '', + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', + OPENAI_MODEL: process.env.OPENAI_MODEL || 'gpt-4o-mini', + + // Agent 行为配置 + AGENT_NAME: process.env.AGENT_NAME || 'excli', + AGENT_TEMPERATURE: parseFloat(process.env.AGENT_TEMPERATURE || '0.7'), + AGENT_MAX_TOKENS: parseInt(process.env.AGENT_MAX_TOKENS || '4096', 10), + AGENT_TRACE_MODE: process.env.AGENT_TRACE_MODE === 'true', + + // Memory 配置 + MEMORY_DB_PATH: process.env.MEMORY_DB_PATH || './data/memory.db', + MAX_SHORT_TERM_MEMORY: parseInt(process.env.MAX_SHORT_TERM_MEMORY || '20', 10), + MEMORY_EXTRACTION_INTERVAL: parseInt(process.env.MEMORY_EXTRACTION_INTERVAL || '5', 10), + + // 工具配置 + TOOL_TIMEOUT_MS: parseInt(process.env.TOOL_TIMEOUT_MS || '30000', 10), + TOOL_MAX_RETRIES: parseInt(process.env.TOOL_MAX_RETRIES || '2', 10), + + // 日志配置 + LOG_LEVEL: process.env.LOG_LEVEL || 'info', + LOG_DIR: process.env.LOG_DIR || './logs', + LOG_MAX_FILES: parseInt(process.env.LOG_MAX_FILES || '10', 10), + + // CLI 配置 + CLI_PROMPT_PREFIX: process.env.CLI_PROMPT_PREFIX || 'excli>', + CLI_WELCOME_MESSAGE: process.env.CLI_WELCOME_MESSAGE || '欢迎使用 ExCLI!你可以用自然语言与 AI 交互,输入 help 查看命令帮助。', + CLI_COLOR_OUTPUT: process.env.CLI_COLOR_OUTPUT !== 'false', +}; + +// 配置类 +class Config { + constructor() { + this.config = { ...DEFAULT_CONFIG }; + this.logger = null; + } + + // 初始化配置 + init() { + this.ensureDirectories(); + this.initLogger(); + return this; + } + + // 确保必要目录存在 + ensureDirectories() { + const dirs = [ + path.dirname(this.config.MEMORY_DB_PATH), + this.config.LOG_DIR, + ]; + + for (const dir of dirs) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + } + + // 初始化日志器 + initLogger() { + const logLevel = this.config.LOG_LEVEL; + const logDir = this.config.LOG_DIR; + + // 创建日志目录 + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + // 使用 pino 创建日志器 + this.logger = pino({ + level: logLevel, + transport: { + targets: [ + { + target: 'pino/file', + options: { destination: path.join(logDir, 'excli.log') }, + }, + { + target: 'pino-pretty', + options: { + colorize: this.config.CLI_COLOR_OUTPUT, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + }, + ], + }, + }); + + this.logger.info('ExCLI 配置初始化完成'); + return this.logger; + } + + // 获取配置值 + get(key) { + return this.config[key]; + } + + // 设置配置值 + set(key, value) { + this.config[key] = value; + return this; + } + + // 获取日志器 + getLogger() { + return this.logger; + } + + // 获取完整配置 + getAll() { + return { ...this.config }; + } +} + +// 导出单例 +export const config = new Config(); +export default config; \ No newline at end of file diff --git a/src/core/agent.js b/src/core/agent.js new file mode 100644 index 0000000..d8bfbe6 --- /dev/null +++ b/src/core/agent.js @@ -0,0 +1,277 @@ +// Agent 核心循环模块 +import { config } from '../config/index.js'; +import { MemoryManager } from '../memory/index.js'; +import { ToolRegistry } from '../tools/index.js'; +import { PromptBuilder } from '../prompts/index.js'; +import { ModelClient } from './model.js'; + +export class AgentLoop { + constructor(options = {}) { + this.memory = options.memory || new MemoryManager(); + this.tools = options.tools || new ToolRegistry(); + this.model = options.model || new ModelClient(); + this.promptBuilder = new PromptBuilder(); + this.logger = options.logger || null; + this.running = false; + this.sessionId = null; + } + + // 初始化 + async init() { + // 初始化日志器 + this.logger = config.getLogger(); + this.model.setLogger(this.logger); + this.tools.setLogger(this.logger); + + // 初始化组件 + this.memory.init(); + this.model.init(); + + // 创建新会话 + this.sessionId = this.generateSessionId(); + + this.log('AgentLoop 初始化完成', { sessionId: this.sessionId }); + + return this; + } + + // 主循环 - 处理用户输入 + async processInput(userInput) { + // 添加到短期记忆 + this.memory.addUserInput(userInput); + + // 搜索相关记忆 + const relatedMemories = await this.searchMemory(userInput); + + // 构造上下文 + const context = await this.buildContext(userInput, relatedMemories); + + // 判断是否需要工具 + const shouldUseTool = await this.shouldUseTool(context); + + let toolResult = null; + let toolUsed = false; + + if (shouldUseTool) { + // 使用工具 + const toolResponse = await this.decideTool(context); + if (toolResponse.use_tool && toolResponse.tool_name) { + this.log('使用工具', toolResponse); + + toolResult = await this.executeTool( + toolResponse.tool_name, + toolResponse.tool_args + ); + + toolUsed = true; + + // 添加工具结果到记忆 + if (toolResult) { + this.memory.addToolResult( + JSON.stringify(toolResult), + toolResponse.tool_name + ); + } + } + } + + // 生成最终回答 + const memories = relatedMemories.memories || []; + const finalContext = { + ...context, + toolResults: toolResult ? JSON.stringify(toolResult) : null, + relatedMemories: memories.map(m => m.content).join('\n'), + }; + + const finalAnswer = await this.generateAnswer(finalContext); + + // 添加助手输出到记忆 + this.memory.addAssistantOutput(finalAnswer); + + // 提取长期记忆 + await this.extractLongTermMemory(); + + // 增加会话计数 + this.memory.incrementConversationCount(); + + return { + answer: finalAnswer, + toolUsed, + toolResult, + relatedMemories: relatedMemories.length, + }; + } + + // 搜索相关记忆 + async searchMemory(query) { + // 搜索长期记忆 + const memories = this.memory.searchLongTerm(query, { + limit: 5, + minImportance: 3, + }); + + // 同时搜索用户偏好 + const preferences = this.memory.getAllPreferences(); + + return { + memories, + preferences, + }; + } + + // 构建上下文 + async buildContext(userInput, relatedMemories) { + const conversationHistory = this.memory.getRecentContext(10); + + return { + sessionId: this.sessionId, + userInput, + conversationHistory: conversationHistory.map(h => ({ + type: h.type, + content: h.content, + })), + relatedMemories: relatedMemories.memories, + preferences: relatedMemories.preferences, + }; + } + + // 判断是否需要使用工具 + async shouldUseTool(context) { + const prompt = this.promptBuilder.buildToolUsePrompt( + context.userInput, + [] // 不传递工具 schema 让模型判断 + ); + + const messages = [ + { role: 'system', content: prompt }, + ...this.promptBuilder.buildContext({ + ...context, + conversationHistory: context.conversationHistory.slice(-5), + }), + ]; + + const response = await this.model.completeJSON(prompt); + + // 默认不需要工具,除非明确返回需要 + if (!response.json || response.json.use_tool !== true) { + return false; + } + + return true; + } + + // 决定使用哪个工具 + async decideTool(context) { + const toolsSchema = this.tools.getAllSchemas(); + + const prompt = this.promptBuilder.buildToolUsePrompt( + context.userInput, + toolsSchema + ); + + const response = await this.model.completeJSON(prompt); + + return response.json || { use_tool: false }; + } + + // 执行工具 + async executeTool(toolName, args) { + // 处理嵌套的 JSON 字符串 + let parsedArgs = args; + if (typeof args === 'string') { + try { + parsedArgs = JSON.parse(args); + } catch (e) { + // 保持原样 + } + } + + const result = await this.tools.execute(toolName, parsedArgs, { + sessionId: this.sessionId, + }); + + return result; + } + + // 生成最终回答 + async generateAnswer(context) { + const prompt = this.promptBuilder.buildFinalAnswerPrompt(context); + + const messages = this.promptBuilder.buildContext({ + userInput: context.userInput, + conversationHistory: context.conversationHistory, + relatedMemories: context.relatedMemories, + toolResults: context.toolResults, + }); + + const response = await this.model.chat(messages, { stream: true }); + + if (response.success) { + return response.content; + } + + return `抱歉,处理您的请求时出现错误:${response.error}`; + } + + // 提取长期记忆 + async extractLongTermMemory() { + const extractionInterval = config.get('MEMORY_EXTRACTION_INTERVAL'); + const conversationCount = this.memory.conversationCount; + + // 每隔 N 轮提取一次 + if (conversationCount % extractionInterval !== 0) { + return; + } + + const conversationHistory = this.memory.getConversationHistory(); + + // 跳过太少的对话 + if (conversationHistory.length < 3) { + return; + } + + const historyText = conversationHistory + .slice(-extractionInterval) + .map(h => h.content) + .join('\n\n'); + + const prompt = this.promptBuilder.buildMemoryExtractionPrompt(historyText); + + const response = await this.model.completeJSON(prompt); + + if (response.json && response.json.should_extract) { + this.memory.extractToLongTerm(response.json); + this.log('提取长期记忆', response.json); + } + } + + // 生成会话 ID + generateSessionId() { + return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 日志 + log(message, data = null) { + if (this.logger) { + this.logger.info(message, data); + } + } + + // 获取统计信息 + getStats() { + return { + sessionId: this.sessionId, + conversationCount: this.memory.conversationCount, + memory: this.memory.getStats(), + tools: this.tools.getStats(), + }; + } + + // 关闭 + async close() { + this.memory.close(); + this.log('AgentLoop 关闭'); + } +} + +export default AgentLoop; \ No newline at end of file diff --git a/src/core/model.js b/src/core/model.js new file mode 100644 index 0000000..3f10754 --- /dev/null +++ b/src/core/model.js @@ -0,0 +1,180 @@ +// Model 适配层 - 统一管理模型调用 +import OpenAI from 'openai'; +import { config } from '../config/index.js'; + +// OpenAI 兼容 API 客户端 +export class ModelClient { + constructor(options = {}) { + this.client = null; + this.model = options.model || config.get('OPENAI_MODEL'); + this.temperature = options.temperature || config.get('AGENT_TEMPERATURE'); + this.maxTokens = options.maxTokens || config.get('AGENT_MAX_TOKENS'); + this.logger = null; + this.streamingCallback = null; + } + + // 设置日志器 + setLogger(logger) { + this.logger = logger; + return this; + } + + // 设置流式输出回调 + setStreamingCallback(callback) { + this.streamingCallback = callback; + return this; + } + + // 初始化客户端 + init() { + const apiKey = config.get('OPENAI_API_KEY'); + const baseURL = config.get('OPENAI_BASE_URL'); + + if (!apiKey) { + throw new Error('请在 .env 中配置 OPENAI_API_KEY'); + } + + this.client = new OpenAI({ + apiKey, + baseURL, + }); + + this.log('ModelClient 初始化完成', { + model: this.model, + baseURL, + }); + + return this; + } + + // 发送聊天请求 + async chat(messages, options = {}) { + if (!this.client) { + this.init(); + } + + const request = { + model: options.model || this.model, + messages, + temperature: options.temperature || this.temperature, + max_tokens: options.maxTokens || this.maxTokens, + tools: options.tools, + }; + + // 如果启用流式输出 + if (options.stream && this.streamingCallback) { + return this.chatStreaming(request); + } + + this.log('发送请求到模型', { model: request.model }); + + try { + const response = await this.client.chat.completions.create(request); + + this.log('模型响应', { + choices: response.choices?.length, + usage: response.usage, + }); + + return { + success: true, + content: response.choices?.[0]?.message?.content || '', + tool_calls: response.choices?.[0]?.message?.tool_calls, + usage: response.usage, + finish_reason: response.choices?.[0]?.finish_reason, + raw: response, + }; + } catch (error) { + this.log('模型调用错误', { error: error.message }); + + return { + success: false, + error: error.message, + }; + } + } + + // 流式聊天 + async chatStreaming(request) { + this.log('发送流式请求到模型', { model: request.model }); + + try { + const stream = await this.client.chat.completions.create({ + ...request, + stream: true, + }); + + let fullContent = ''; + let firstChunk = true; + + for await (const chunk of stream) { + const delta = chunk.choices?.[0]?.delta?.content || ''; + if (delta) { + fullContent += delta; + + // 调用流式回调 + if (this.streamingCallback) { + this.streamingCallback(delta, firstChunk); + firstChunk = false; + } + } + } + + this.log('流式响应完成', { length: fullContent.length }); + + return { + success: true, + content: fullContent, + finish_reason: 'stop', + }; + } catch (error) { + this.log('流式调用错误', { error: error.message }); + + return { + success: false, + error: error.message, + }; + } + } + + // 发送带工具的聊天请求 + async chatWithTools(messages, tools) { + return this.chat(messages, { tools }); + } + + // 简单文本补全 + async complete(prompt, options = {}) { + const messages = [{ role: 'user', content: prompt }]; + return this.chat(messages, options); + } + + // JSON 补全(强制输出 JSON) + async completeJSON(prompt, options = {}) { + const messages = [ + { role: 'system', content: '你是一个 JSON 生成器。请只输出有效的 JSON,不要有其他任何文字。' }, + { role: 'user', content: prompt }, + ]; + + const result = await this.chat(messages, options); + + if (result.success) { + try { + result.json = JSON.parse(result.content); + } catch (e) { + result.json = null; + result.parseError = e.message; + } + } + + return result; + } + + // 日志 + log(message, data = null) { + if (this.logger) { + this.logger.info(message, data); + } + } +} + +export default ModelClient; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ae66f13 --- /dev/null +++ b/src/index.js @@ -0,0 +1,258 @@ +// ExCLI 主入口 +import { config } from './config/index.js'; +import { AgentLoop } from './core/agent.js'; +import { + printWelcome, + printHelp, + printJSON, + createSpinner, + createInterface, + isExitCommand, + isHelpCommand, + isClearCommand, + clearScreen, + exit, + sleep, +} from './utils/index.js'; +import chalk from 'chalk'; + +// 主程序 +class CLI { + constructor() { + this.agent = null; + this.running = false; + this.rl = null; + } + +// 初始化 - 先显示欢迎信息,再初始化其他组件 + async init() { + // 先显示欢迎信息(此时日志还未初始化) + console.log(chalk.cyan('欢迎使用 ExCLI!你可以用自然语言与 AI 交互,输入 help 查看命令帮助。\n')); + + // 加载配置并初始化日志 + config.init(); + + // 创建 Agent + this.agent = new AgentLoop(); + await this.agent.init(); + + // 创建输入接口 + this.rl = createInterface(); + + return this; + } + + // 运行主循环 + async run() { + this.running = true; + + while (this.running) { + const input = await this.promptUser(); + + if (!input || input.trim() === '') { + continue; + } + + // 检查内置命令 + if (await this.handleCommand(input)) { + continue; + } + + // 处理用户输入 + await this.processInput(input); + } + } + + // 提示用户输入 + promptUser() { + return new Promise((resolve) => { + this.rl.question( + chalk.cyan(config.get('CLI_PROMPT_PREFIX') + ' '), + (answer) => { + resolve(answer); + } + ); + }); + } + + // 处理内置命令 + async handleCommand(input) { + const cmd = input.trim().toLowerCase(); + const parts = input.split(' '); + const arg = parts.slice(1).join(' '); + + // 退出 + if (isExitCommand(input)) { + console.log(chalk.cyan('再见!')); + await this.close(); + exit(0); + return true; + } + + // 帮助 + if (isHelpCommand(input)) { + printHelp(); + return true; + } + + // 清屏 + if (isClearCommand(input)) { + clearScreen(); + return true; + } + + // 记忆状态 + if (cmd === 'memory' || cmd === 'mem') { + const stats = this.agent.memory.getStats(); + console.log(chalk.gray('短期记忆:')); + console.log(printJSON(stats.shortTerm)); + console.log(chalk.gray('\n长期记忆:')); + console.log(printJSON(stats.longTerm)); + return true; + } + + // 偏好 + if (cmd === 'preferences' || cmd === 'pref') { + const prefs = this.agent.memory.getAllPreferences(); + console.log(chalk.gray('用户偏好:')); + if (prefs.length === 0) { + console.log(chalk.gray('暂无偏好')); + } else { + for (const p of prefs) { + console.log(` ${chalk.cyan(p.key)}: ${JSON.stringify(p.value)}`); + } + } + return true; + } + + // 工具列表 + if (cmd === 'tools') { + const schemas = this.agent.tools.getAllSchemas(); + console.log(chalk.gray('可用工具:')); + for (const tool of schemas) { + console.log(` ${chalk.cyan(tool.name)} - ${tool.description}`); + } + return true; + } + + // 统计 + if (cmd === 'stats') { + console.log(printJSON(this.agent.getStats())); + return true; + } + + return false; + } + + // 处理用户输入 + async processInput(input) { + const spinner = createSpinner('处理中...'); + spinner.start(); + + // 用于流式输出的答案片段 + let answerFragments = []; + + // 设置流式输出回调 + this.agent.model.setStreamingCallback((chunk, isFirst) => { + if (isFirst) { + spinner.stop(); + process.stdout.write(chalk.cyan('🤖 ')); + } + process.stdout.write(chunk); + answerFragments.push(chunk); + }); + + try { + const result = await this.agent.processInput(input); + spinner.stop(); + + // 如果不是流式输出,手动打印 + if (answerFragments.length === 0) { + console.log(); + this.printAnswer(result.answer, result.toolUsed); + } else { + console.log(); // 流式输出后换行 + } + + // 如果使用了工具,显示工具结果摘要 + if (result.toolUsed && result.toolResult) { + this.printToolResult(result.toolResult); + } + + // 显示相关记忆数量 + if (result.relatedMemories > 0) { + console.log(chalk.gray(`[相关记忆: ${result.relatedMemories} 条]`)); + } + + console.log(); + } catch (error) { + spinner.stop(); + console.error(chalk.red('处理错误: ' + error.message)); + } + } + + // 打印回答 + printAnswer(answer, toolUsed = false) { + if (toolUsed) { + console.log(chalk.cyan('🤖 ') + answer); + } else { + console.log(answer); + } + } + + // 打印工具结果 + printToolResult(result) { + if (result.success) { + const summary = this.summarizeToolResult(result); + if (summary) { + console.log(chalk.gray(`[工具结果: ${summary}]`)); + } + } else { + console.log(chalk.red(`[工具错误: ${result.error}]`)); + } + } + + // 总结工具结果 + summarizeToolResult(result) { + if (result.files) { + return `${result.count} 个文件`; + } + + if (result.stdout !== undefined) { + const lines = result.stdout.split('\n').length; + return `${lines} 行输出`; + } + + if (result.results) { + return `${result.count} 个匹配`; + } + + if (result.path) { + return result.path; + } + + return null; + } + + // 关闭 + async close() { + this.running = false; + + if (this.rl) { + this.rl.close(); + } + + if (this.agent) { + await this.agent.close(); + } + } +} + +// 导出运行函数 +export async function run() { + const cli = new CLI(); + await cli.init(); + await cli.run(); +} + +export default { run }; \ No newline at end of file diff --git a/src/memory/index.js b/src/memory/index.js new file mode 100644 index 0000000..964df93 --- /dev/null +++ b/src/memory/index.js @@ -0,0 +1,521 @@ +// Memory 模块核心实现 - 使用 JSON 文件存储 +import fs from 'fs'; +import path from 'path'; +import { config } from '../config/index.js'; + +// 基础存储类 +class BaseStorage { + constructor(filePath) { + this.filePath = filePath; + this.data = []; + this.metadata = { + createdAt: Date.now(), + lastSavedAt: Date.now(), + }; + } + + // 加载数据 + load() { + try { + if (fs.existsSync(this.filePath)) { + const content = fs.readFileSync(this.filePath, 'utf-8'); + const parsed = JSON.parse(content); + this.data = parsed.data || []; + this.metadata = parsed.metadata || this.metadata; + } + } catch (e) { + this.data = []; + } + return this; + } + + // 保存数据 + save() { + const dir = path.dirname(this.filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync( + this.filePath, + JSON.stringify({ + data: this.data, + metadata: { + ...this.metadata, + lastSavedAt: Date.now(), + }, + }, null, 2), + 'utf-8' + ); + + return this; + } + + // 清空数据 + clear() { + this.data = []; + return this; + } +} + +// 短期记忆管理器 +export class ShortTermMemory { + constructor(options = {}) { + this.maxItems = options.maxItems || config.get('MAX_SHORT_TERM_MEMORY'); + this.items = []; + this.metadata = { + createdAt: Date.now(), + lastAccessedAt: Date.now(), + }; + } + + // 添加记忆条目 + add(item) { + const entry = { + id: this.generateId(), + type: item.type || 'general', + content: item.content, + importance: item.importance || 0, + tokens: item.tokens || 0, + timestamp: Date.now(), + metadata: item.metadata || {}, + }; + + if (entry.importance >= 7) { + this.items.unshift(entry); + } else { + this.items.push(entry); + } + + this.prune(); + + return entry; + } + + // 修剪多余的记忆条目 + prune() { + if (this.items.length > this.maxItems) { + this.items.sort((a, b) => { + if (a.importance !== b.importance) { + return b.importance - a.importance; + } + return b.timestamp - a.timestamp; + }); + + this.items = this.items.slice(0, this.maxItems); + } + } + + // 获取最近的 N 条记忆 + getRecent(n = 10) { + this.metadata.lastAccessedAt = Date.now(); + return this.items.slice(0, n); + } + + // 根据类型获取记忆 + getByType(type) { + return this.items.filter(item => item.type === type); + } + + // 获取所有用户输入 + getUserInputs() { + return this.getByType('user_input'); + } + + // 获取所有 Assistant 输出 + getAssistantOutputs() { + return this.getByType('assistant_output'); + } + + // 获取工具执行结果 + getToolResults() { + return this.getByType('tool_result'); + } + + // 搜索记忆 + search(query) { + const lowerQuery = query.toLowerCase(); + return this.items.filter(item => + item.content.toLowerCase().includes(lowerQuery) + ); + } + + // 生成唯一 ID + generateId() { + return `stm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 清空所有记忆 + clear() { + this.items = []; + return this; + } + + // 获取统计信息 + getStats() { + return { + totalItems: this.items.length, + maxItems: this.maxItems, + byType: this.items.reduce((acc, item) => { + acc[item.type] = (acc[item.type] || 0) + 1; + return acc; + }, {}), + lastAccessedAt: this.metadata.lastAccessedAt, + }; + } + + // 导出为 JSON + toJSON() { + return { + items: this.items, + metadata: this.metadata, + }; + } + + // 从 JSON 导入 + static fromJSON(json) { + const memory = new ShortTermMemory(); + memory.items = json.items || []; + memory.metadata = json.metadata || { createdAt: Date.now(), lastAccessedAt: Date.now() }; + return memory; + } +} + +// 长期记忆管理器 - 使用 JSON 文件 +export class LongTermMemory { + constructor(options = {}) { + this.dataDir = options.dataDir || './data/memory'; + this.memoriesFile = path.join(this.dataDir, 'memories.json'); + this.preferencesFile = path.join(this.dataDir, 'preferences.json'); + this.storage = new BaseStorage(this.memoriesFile); + this.preferencesStorage = new BaseStorage(this.preferencesFile); + } + + // 初始化 + init() { + if (!fs.existsSync(this.dataDir)) { + fs.mkdirSync(this.dataDir, { recursive: true }); + } + + this.storage.load(); + this.preferencesStorage.load(); + + return this; + } + + // 添加记忆 + add(entry) { + const id = entry.id || this.generateId(); + const now = Date.now(); + + const memory = { + id, + type: entry.type || 'general', + content: entry.content, + importance: entry.importance || 5, + category: entry.category || null, + tags: entry.tags || [], + source: entry.source || 'extraction', + createdAt: now, + updatedAt: now, + accessedAt: now, + accessCount: 0, + isSensitive: entry.isSensitive || false, + isActive: true, + }; + + this.storage.data.push(memory); + this.storage.save(); + + return id; + } + + // 检索记忆 + search(query, options = {}) { + const { category, type, limit = 10, minImportance = 0 } = options; + + let results = this.storage.data.filter(m => { + if (!m.isActive) return false; + if (m.importance < minImportance) return false; + if (!m.content.toLowerCase().includes(query.toLowerCase())) return false; + if (category && m.category !== category) return false; + if (type && m.type !== type) return false; + return true; + }); + + // 更新访问信息 + for (const m of results) { + m.accessedAt = Date.now(); + m.accessCount = (m.accessCount || 0) + 1; + } + + this.storage.save(); + + return results.slice(0, limit); + } + + // 获取某类别的所有记忆 + getByCategory(category, limit = 20) { + return this.storage.data + .filter(m => m.isActive && m.category === category) + .sort((a, b) => b.importance - a.importance) + .slice(0, limit); + } + + // 获取用户偏好 + getPreference(key) { + const pref = this.preferencesStorage.data.find(p => p.key === key); + return pref ? pref.value : null; + } + + // 设置用户偏好 + setPreference(key, value, category = 'general') { + const id = this.generateId(); + const now = Date.now(); + + const existing = this.preferencesStorage.data.findIndex(p => p.key === key); + + const pref = { + id, + key, + value, + category, + createdAt: existing >= 0 ? this.preferencesStorage.data[existing].createdAt : now, + updatedAt: now, + }; + + if (existing >= 0) { + this.preferencesStorage.data[existing] = pref; + } else { + this.preferencesStorage.data.push(pref); + } + + this.preferencesStorage.save(); + + return id; + } + + // 获取所有偏好 + getAllPreferences(category = null) { + if (category) { + return this.preferencesStorage.data + .filter(p => p.category === category) + .map(p => ({ key: p.key, value: p.value, category: p.category })); + } + + return this.preferencesStorage.data.map(p => ({ + key: p.key, + value: p.value, + category: p.category, + })); + } + + // 更新记忆 + update(id, updates) { + const index = this.storage.data.findIndex(m => m.id === id); + if (index < 0) return false; + + if (updates.content !== undefined) { + this.storage.data[index].content = updates.content; + } + + if (updates.importance !== undefined) { + this.storage.data[index].importance = updates.importance; + } + + if (updates.category !== undefined) { + this.storage.data[index].category = updates.category; + } + + if (updates.tags !== undefined) { + this.storage.data[index].tags = updates.tags; + } + + this.storage.data[index].updatedAt = Date.now(); + this.storage.save(); + + return true; + } + + // 删除记忆(软删除) + delete(id) { + const index = this.storage.data.findIndex(m => m.id === id); + if (index < 0) return false; + + this.storage.data[index].isActive = false; + this.storage.data[index].updatedAt = Date.now(); + this.storage.save(); + + return true; + } + + // 生成唯一 ID + generateId() { + return `ltm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 获取统计信息 + getStats() { + const activeMemories = this.storage.data.filter(m => m.isActive); + const sensitiveMemories = activeMemories.filter(m => m.isSensitive); + const avgImportance = activeMemories.length > 0 + ? activeMemories.reduce((sum, m) => sum + m.importance, 0) / activeMemories.length + : 0; + + return { + totalMemories: this.storage.data.length, + activeMemories: activeMemories.length, + sensitiveMemories: sensitiveMemories.length, + avgImportance: Math.round(avgImportance * 10) / 10, + totalPreferences: this.preferencesStorage.data.length, + }; + } + + // 关闭 + close() { + this.storage.save(); + this.preferencesStorage.save(); + } +} + +// 统一的 Memory 管理器 +export class MemoryManager { + constructor(options = {}) { + this.shortTerm = new ShortTermMemory(options); + this.longTerm = new LongTermMemory(options); + this.conversationCount = 0; + } + + // 初始化 + init() { + this.longTerm.init(); + return this; + } + + // 添加用户输入到短期记忆 + addUserInput(content) { + return this.shortTerm.add({ + type: 'user_input', + content, + importance: 5, + }); + } + + // 添加助手输出到短期记忆 + addAssistantOutput(content) { + return this.shortTerm.add({ + type: 'assistant_output', + content, + importance: 5, + }); + } + + // 添加工具结果到短期记忆 + addToolResult(content, toolName) { + return this.shortTerm.add({ + type: 'tool_result', + content, + toolName, + importance: 3, + }); + } + + // 检索长期记忆 + searchLongTerm(query, options = {}) { + return this.longTerm.search(query, options); + } + + // 获取用户偏好 + getPreference(key) { + return this.longTerm.getPreference(key); + } + + // 设置用户偏好 + setPreference(key, value, category) { + return this.longTerm.setPreference(key, value, category); + } + + // 获取所有用户偏好 + getAllPreferences(category) { + return this.longTerm.getAllPreferences(category); + } + + // 获取最近的会话上下文 + getRecentContext(limit = 10) { + return this.shortTerm.getRecent(limit); + } + + // 获取当前会话的所有用户输入 + getConversationHistory() { + return this.shortTerm.getUserInputs(); + } + + // 提取需要存储到长期记忆的信息 + extractToLongTerm(extractionResult) { + if (!extractionResult || !extractionResult.memories) return []; + + const ids = []; + for (const item of extractionResult.memories) { + if (this.isSensitiveContent(item.content)) { + continue; + } + + const id = this.longTerm.add({ + type: item.type || 'extracted', + content: item.content, + importance: item.importance || 5, + category: item.category, + tags: item.tags, + source: 'extraction', + isSensitive: false, + }); + + ids.push(id); + } + + return ids; + } + + // 检查是否包含敏感信息 + isSensitiveContent(content) { + const sensitivePatterns = [ + /password/i, + /api[_-]?key/i, + /secret/i, + /token/i, + /credential/i, + /ssh[_-]?key/i, + /private[_-]?key/i, + ]; + + for (const pattern of sensitivePatterns) { + if (pattern.test(content)) { + return true; + } + } + + return false; + } + + // 增加会话计数 + incrementConversationCount() { + this.conversationCount++; + return this.conversationCount; + } + + // 获取统计信息 + getStats() { + return { + shortTerm: this.shortTerm.getStats(), + longTerm: this.longTerm.getStats(), + conversationCount: this.conversationCount, + }; + } + + // 关闭 + close() { + this.longTerm.close(); + } +} + +export default MemoryManager; \ No newline at end of file diff --git a/src/prompts/index.js b/src/prompts/index.js new file mode 100644 index 0000000..0e1b49f --- /dev/null +++ b/src/prompts/index.js @@ -0,0 +1,263 @@ +// Prompts 模块 - 统一管理所有 prompt 模板 +import { config } from '../config/index.js'; + +// System Prompt - Agent 的核心身份和行为定义 +export const SYSTEM_PROMPT = `你是一个强大的 AI Agent CLI,名为 ExCLI。 +你的核心能力包括: +1. 更好的记忆 - 能记住用户的偏好、常见任务、历史上下文 +2. 更好的输出 - 根据任务类型选择最佳输出风格 +3. 更好的工具使用 - 能准确调用合适的工具完成任务 + +## 基本行为准则 +- 用中文与用户交流 +- 回答要简洁、直接、少废话 +- 如果是复杂任务,先给结论,再给步骤 +- 如果是代码任务,优先给可运行结果 +- 如果信息不足,先提最关键的澄清问题 +- 不要重复用户说的话 + +## 输出风格选择 +- 简洁模式:简短回答,不超过 3 句话 +- 详细模式:完整解释,包含背景和步骤 +- 技术型:代码、配置、命令等技术支持 +- 写作型:文档、说明等文字内容 + +根据任务自动判断使用哪种风格。 + +## 记忆行为准则 +- 自动识别并记住用户的长期偏好 +- 不把一次性信息存入长期记忆 +- 能根据当前任务检索最相关的记忆 +- 用户新偏好出现时可以覆盖旧偏好 + +## 工具使用准则 +- 每次调用工具前说明目的 +- 工具执行后总结结果 +- 工具失败时给出替代方案或重试 +- 尽量减少无意义工具调用`; + +// Planner Prompt - 任务规划 +export const PLANNER_PROMPT = `你是一个任务规划专家。当用户提出复杂任务时,你需要: + +## 分析任务结构 +1. 理解任务目标 +2. 识别需要的子任务 +3. 确定任务依赖关系 +4. 评估任务的可行性和风险 + +## 输出格式 +请以如下 JSON 格式输出任务规划: +{ + "goal": "任务的最终目标", + "need_plan": true/false, // 是否需要多步骤规划 + "steps": [ + { + "id": 1, + "description": "步骤描述", + "tool_needed": true/false, // 是否需要工具 + "tool_name": "工具名称(如果有)", + "tool_args": { /* 工具参数 */ }, + "depends_on": [], // 依赖的步骤 ID + } + ], + "estimated_steps": 预期步骤数, + "risks": ["风险描述"], + "clarifications": ["需要澄清的问题"] +} + +## 判断标准 +需要规划的情况: +- 任务包含多个子任务 +- 任务需要先查询信息才能执行 +- 任务有明确的步骤顺序 +- 任务复杂度较高 + +不需要规划的情况: +- 简单问答 +- 直接可以回答的问题 +- 不需要执行操作的任务 + +请根据任务自行判断是否需要规划。`; + +// Memory Extraction Prompt - 记忆提取 +export const MEMORY_EXTRACTION_PROMPT = `你是一个记忆分析专家。需要从对话历史中提取需要长期记忆的信息。 + +## 提取原则 +需要记住的信息: +- 用户明确表达的偏好(如:喜欢的编程语言、代码风格) +- 用户的项目背景信息 +- 用户多次提到的习惯或模式 +- 重要的结论或决定 +- 常用的工作流程或命令 + +不需要记住的信息: +- 一次性问答的内容 +- 临时查询的信息 +- 不重要的闲聊 +- 明显的一次性任务 + +## 输出格式 +请以如下 JSON 格式输出记忆提取结果: +{ + "should_extract": true/false, // 是否有需要提取的信息 + "memories": [ + { + "type": "preference | background | workflow | conclusion", + "content": "记忆内容", + "importance": 1-10, // 重要性评分 + "category": "分类", + "tags": ["标签"] + } + ], + "reason": "��取理由" +} + +请分析对话历史,给出记忆提取结果。`; + +// Tool Use Prompt - 工具使用指导 +export const TOOL_USE_PROMPT = `你是一个工具使用专家。你可以根据需要调用各种工具来完成用户的任务。 + +## 可用工具 +{{TOOLS}} + +## 工具使用原则 +1. 每次调用工具前说明目的 +2. 选择最小化的工具集 +3. 先理解工具的参数要求 +4. 工具执行失败时尝试重试或替代方案 + +## 工具选择指导 +- 读取/搜索代码:使用 search_text + read_file +- 写入配置文件:使用 write_file +- 执行命令:使用 run_shell +- 查看目录:使用 list_files + +## 输出格式 +请根据需要决定是否使用工具。如果需要使用工具,请按以下格式输出: +{ + "use_tool": true/false, + "tool_name": "工具名称", + "tool_args": { /* 工具参数 */ }, + "purpose": "使用目的说明" +} + +请根据用户的任务决定是否需要使用工具。`; + +// Final Answer Prompt - 最终回答生成 +export const FINAL_ANSWER_PROMPT = `你是一个专业的 AI 助手。现在你需要根据上下文生成最终回答。 + +## 当前上下文 +对话历史:{{CONVERSATION_HISTORY}} +相关记忆:{{RELATED_MEMORIES}} +工具结果:{{TOOL_RESULTS}} +当前任务:{{USER_INPUT}} + +## 回答要求 +- 根据任务类型选择合适的输出风格 +- 如果是复杂任务,先给结论,再给步骤 +- 如果是代码任务,优先给可运行结果 +- 结合相关记忆和个人偏好 +- 保持简洁、直接、少废话 +- 用中文回答 + +## 输出风格 +- 简洁模式:简短回答,不超过 3 句话 +- 详细模式:完整解释,包含背景和步骤 +- 技术型:代码、配置、命令等技术支持 +- 写作型:文档、说明等文字内容 + +请生成最终回答。`; + +// Prompt 构建器 +export class PromptBuilder { + constructor() { + this.systemPrompt = SYSTEM_PROMPT; + this.plannerPrompt = PLANNER_PROMPT; + this.memoryExtractionPrompt = MEMORY_EXTRACTION_PROMPT; + this.toolUsePrompt = TOOL_USE_PROMPT; + this.finalAnswerPrompt = FINAL_ANSWER_PROMPT; + } + + // 构建 System Prompt + buildSystemPrompt(context = {}) { + let prompt = this.systemPrompt; + + // 添加用户偏好 + if (context.preferences) { + prompt += `\n\n## 用户偏好\n${context.preferences}`; + } + + return prompt; + } + + // 构建 Planner Prompt + buildPlannerPrompt(task) { + return `${this.plannerPrompt}\n\n## 当前任务\n${task}`; + } + + // 构建 Memory Extraction Prompt + buildMemoryExtractionPrompt(conversationHistory) { + return `${this.memoryExtractionPrompt}\n\n## 对话历史\n${conversationHistory}`; + } + + // 构建 Tool Use Prompt + buildToolUsePrompt(task, toolsSchema) { + const toolsJSON = JSON.stringify(toolsSchema, null, 2); + const prompt = this.toolUsePrompt.replace('{{TOOLS}}', toolsJSON); + return `${prompt}\n\n## 当前任务\n${task}`; + } + + // 构建 Final Answer Prompt + buildFinalAnswerPrompt(context) { + let prompt = this.finalAnswerPrompt + .replace('{{CONVERSATION_HISTORY}}', context.conversationHistory || '无') + .replace('{{RELATED_MEMORIES}}', context.relatedMemories || '无') + .replace('{{TOOL_RESULTS}}', context.toolResults || '无') + .replace('{{USER_INPUT}}', context.userInput || '无'); + + return prompt; + } + + // 构建完整的对话上下文 + buildContext(context) { + const messages = []; + + // System prompt + messages.push({ + role: 'system', + content: this.buildSystemPrompt(context), + }); + + // 对话历史 + if (context.conversationHistory && context.conversationHistory.length > 0) { + for (const item of context.conversationHistory) { + if (item.type === 'user_input') { + messages.push({ + role: 'user', + content: item.content, + }); + } else if (item.type === 'assistant_output') { + messages.push({ + role: 'assistant', + content: item.content, + }); + } else if (item.type === 'tool_result') { + messages.push({ + role: 'user', + content: `[工具执行结果]: ${item.content}`, + }); + } + } + } + + // 最终用户输入 + messages.push({ + role: 'user', + content: context.userInput, + }); + + return messages; + } +} + +export default PromptBuilder; \ No newline at end of file diff --git a/src/tools/index.js b/src/tools/index.js new file mode 100644 index 0000000..5ee23cf --- /dev/null +++ b/src/tools/index.js @@ -0,0 +1,529 @@ +// 工具模块 +import { config } from '../config/index.js'; + +// 基础工具类 +export class BaseTool { + constructor(name, description, schema) { + this.name = name; + this.description = description; + this.schema = schema; + this.usageCount = 0; + } + + async execute(args, context = {}) { + throw new Error('Tool must implement execute method'); + } + + getSchema() { + return { + name: this.name, + description: this.description, + parameters: this.schema, + }; + } +} + +// 读取文件工具 +export class ReadFileTool extends BaseTool { + constructor() { + super( + 'read_file', + '读取文件内容', + { + type: 'object', + properties: { + path: { type: 'string', description: '文件路径' }, + offset: { type: 'number', description: '起始行号 (从 1 开始)', default: 1 }, + limit: { type: 'number', description: '读取行数', default: 100 }, + encoding: { type: 'string', description: '文件编码', default: 'utf-8' }, + }, + required: ['path'], + } + ); + } + + async execute(args) { + const fs = await import('fs/promises'); + const path = await import('path'); + + const { + path: filePath, + offset = 1, + limit = 100, + encoding = 'utf-8' + } = args; + + try { + const content = await fs.readFile(filePath, encoding); + const lines = content.split('\n'); + + // 计算实际读取范围 + const start = Math.max(0, offset - 1); + const end = Math.min(lines.length, start + limit); + const selectedLines = lines.slice(start, end); + + return { + success: true, + content: selectedLines.join('\n'), + lines: { + from: start + 1, + to: end, + total: lines.length, + }, + path: filePath, + }; + } catch (error) { + return { + success: false, + error: error.message, + path: filePath, + }; + } + } +} + +// 写入文件工具 +export class WriteFileTool extends BaseTool { + constructor() { + super( + 'write_file', + '写入文件内容', + { + type: 'object', + properties: { + path: { type: 'string', description: '文件路径' }, + content: { type: 'string', description: '要写入的内容' }, + mode: { type: 'string', description: '写入模式: write | append', default: 'write' }, + }, + required: ['path', 'content'], + } + ); + } + + async execute(args) { + const fs = await import('fs/promises'); + const path = await import('path'); + + const { path: filePath, content, mode = 'write' } = args; + + try { + // 确保目录存在 + const dir = path.dirname(filePath); + await fs.mkdir(dir, { recursive: true }); + + if (mode === 'append') { + await fs.appendFile(filePath, content, 'utf-8'); + } else { + await fs.writeFile(filePath, content, 'utf-8'); + } + + return { + success: true, + path: filePath, + mode, + bytes: content.length, + }; + } catch (error) { + return { + success: false, + error: error.message, + path: filePath, + }; + } + } +} + +// 列出文件工具 +export class ListFilesTool extends BaseTool { + constructor() { + super( + 'list_files', + '列出目录中的文件', + { + type: 'object', + properties: { + path: { type: 'string', description: '目录路径', default: '.' }, + pattern: { type: 'string', description: '文件过滤模式 (glob)', default: '*' }, + recursive: { type: 'boolean', description: '是否递归查找', default: false }, + }, + } + ); + } + + async execute(args) { + const fs = await import('fs'); + const path = await import('path'); + + const { path: dirPath = '.', pattern = '*', recursive = false } = args; + + try { + const files = []; + + if (recursive) { + // 递归查找 + const walk = async (dir) => { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + } else if (entry.isFile()) { + files.push(fullPath); + } + } + }; + await walk(dirPath); + } else { + // 非递归 + const entries = await fs.promises.readdir(dirPath); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry); + const stat = await fs.promises.stat(fullPath); + if (stat.isFile()) { + files.push(fullPath); + } + } + } + + // 过滤模式 + let filtered = files; + if (pattern && pattern !== '*') { + const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.')); + filtered = files.filter(f => regex.test(path.basename(f))); + } + + return { + success: true, + files: filtered, + count: filtered.length, + }; + } catch (error) { + return { + success: false, + error: error.message, + }; + } + } +} + +// 执行 Shell 命令工具 +export class RunShellTool extends BaseTool { + constructor() { + super( + 'run_shell', + '执行 Shell 命令', + { + type: 'object', + properties: { + command: { type: 'string', description: '要执行的命令' }, + cwd: { type: 'string', description: '工作目录', default: '.' }, + timeout: { type: 'number', description: '超时时间 (毫秒)', default: 30000 }, + }, + required: ['command'], + } + ); + } + + async execute(args) { + const { exec } = await import('child_process'); + const util = await import('util'); + const execPromise = util.promisify(exec); + + const { command, cwd = '.', timeout = config.get('TOOL_TIMEOUT_MS') } = args; + + try { + const { stdout, stderr } = await execPromise(command, { + cwd, + timeout, + maxBuffer: 10 * 1024 * 1024, // 10MB + }); + + return { + success: true, + stdout: stdout || '', + stderr: stderr || '', + output: stdout || stderr, + }; + } catch (error) { + return { + success: false, + error: error.message, + stdout: error.stdout || '', + stderr: error.stderr || '', + }; + } + } +} + +// 搜索文本工具 +export class SearchTextTool extends BaseTool { + constructor() { + super( + 'search_text', + '在文件中搜索文本', + { + type: 'object', + properties: { + pattern: { type: 'string', description: '搜索的正则表达式或文本' }, + path: { type: 'string', description: '搜索路径', default: '.' }, + include: { type: 'string', description: '文件包含模式', default: '*' }, + exclude: { type: 'string', description: '文件排除模式' }, + isRegex: { type: 'boolean', description: '是否使用正则表达式', default: false }, + caseSensitive: { type: 'boolean', description: '是否区分大小写', default: true }, + maxResults: { type: 'number', description: '最大结果数', default: 100 }, + }, + required: ['pattern'], + } + ); + } + + async execute(args) { + const fs = await import('fs'); + const path = await import('path'); + + const { + pattern, + path: searchPath = '.', + include = '*', + exclude, + isRegex = false, + caseSensitive = true, + maxResults = 100, + } = args; + + try { + const results = []; + let regex; + + if (isRegex) { + regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi'); + } else { + const flags = caseSensitive ? 'g' : 'gi'; + regex = new RegExp(this.escapeRegex(pattern), flags); + } + + // 搜索文件 + const searchFile = async (filePath) => { + try { + const content = await fs.promises.readFile(filePath, 'utf-8'); + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + if (regex.test(lines[i])) { + results.push({ + file: filePath, + line: i + 1, + content: lines[i].trim(), + }); + + if (results.length >= maxResults) return true; + } + } + } catch (e) { + // 跳过无法读取的文件 + } + return false; + }; + + // 遍历文件 + const searchDir = async (dir) => { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // 跳过排除的目录 + if (exclude && new RegExp(exclude).test(entry.name)) continue; + await searchDir(fullPath); + } else if (entry.isFile()) { + // 检查包含模式 + if (include && include !== '*') { + const includeRegex = new RegExp(include.replace(/\*/g, '.*').replace(/\?/g, '.')); + if (!includeRegex.test(entry.name)) continue; + } + + if (await searchFile(fullPath)) return; + } + } + }; + + await searchDir(searchPath); + + return { + success: true, + results, + count: results.length, + pattern: pattern, + }; + } catch (error) { + return { + success: false, + error: error.message, + }; + } + } + + escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } +} + +// 工具注册表 +export class ToolRegistry { + constructor() { + this.tools = new Map(); + this.logger = null; + this.registerDefaultTools(); + } + + // 设置日志器 + setLogger(logger) { + this.logger = logger; + } + + // 注册默认工具 + registerDefaultTools() { + this.register(new ReadFileTool()); + this.register(new WriteFileTool()); + this.register(new ListFilesTool()); + this.register(new RunShellTool()); + this.register(new SearchTextTool()); + } + + // 注册工具 + register(tool) { + if (!(tool instanceof BaseTool)) { + throw new Error('Tool must extend BaseTool'); + } + + this.tools.set(tool.name, tool); + this.log(`工具已注册: ${tool.name}`); + + return this; + } + + // 获取工具 + get(name) { + return this.tools.get(name); + } + + // 获取所有工具的 schema + getAllSchemas() { + const schemas = []; + + for (const tool of this.tools.values()) { + schemas.push(tool.getSchema()); + } + + return schemas; + } + + // 执行工具 + async execute(toolName, args, context = {}) { + const tool = this.tools.get(toolName); + + if (!tool) { + return { + success: false, + error: `工具不存在: ${toolName}`, + }; + } + + this.log(`执行工具: ${toolName}`, args); + + try { + // 参数验证 + const validationError = this.validateArgs(tool.schema, args); + if (validationError) { + return { + success: false, + error: validationError, + }; + } + + // 执行工具 + const retries = config.get('TOOL_MAX_RETRIES'); + let lastError = null; + let result = null; + + for (let i = 0; i <= retries; i++) { + try { + result = await tool.execute(args, context); + break; + } catch (error) { + lastError = error; + this.log(`工具执行失败 (尝试 ${i + 1}/${retries + 1}): ${error.message}`); + + if (i < retries) { + // 等待后重试 + await this.sleep(1000 * (i + 1)); + } + } + } + + if (!result) { + result = { + success: false, + error: lastError?.message || '工具执行失败', + }; + } + + tool.usageCount++; + this.log(`工具执行完成: ${toolName}`, result); + + return result; + } catch (error) { + this.log(`工具执行错误: ${toolName}`, { error: error.message }); + + return { + success: false, + error: error.message, + }; + } + } + + // 验证参数 + validateArgs(schema, args) { + if (!schema || !schema.properties) return null; + + const required = schema.required || []; + + // 检查必需参数 + for (const param of required) { + if (args[param] === undefined) { + return `缺少必需参数: ${param}`; + } + } + + return null; + } + + // 日志 + log(message, data = null) { + if (this.logger) { + this.logger.info(message, data); + } + } + + // 等待 + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // 获取工具使用统计 + getStats() { + const stats = []; + + for (const [name, tool] of this.tools.entries()) { + stats.push({ + name, + usageCount: tool.usageCount, + }); + } + + return stats; + } +} + +export default ToolRegistry; \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..4752523 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,217 @@ +// 工具函数模块 +import readline from 'readline'; +import chalk from 'chalk'; +import ora from 'ora'; + +// 创建 CLI 输入接口 +export function createInterface() { + return readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true, + }); +} + +// 打印欢迎信息 +export function printWelcome(message) { + console.log(chalk.cyan(message)); + console.log(); +} + +// 打印帮助信息 +export function printHelp() { + console.log(` +${chalk.cyan('ExCLI 帮助')} + +${chalk.gray('命令:')} + help - 显示帮助信息 + clear - 清除屏幕 + memory - 查看记忆状态 + preferences - 查看用户偏好 + tools - 查看可用工具 + stats - 查看统计信息 + quit/exit - 退出程序 + +${chalk.gray('快捷键:')} + Ctrl+C - 退出程序 + Ctrl+L - 清除屏幕 + `); +} + +// 格式化输出 +export function formatOutput(content, style = 'default') { + switch (style) { + case 'code': + return chalk.gray('```\n') + content + '\n```'; + case 'error': + return chalk.red(content); + case 'success': + return chalk.green(content); + case 'warning': + return chalk.yellow(content); + case 'info': + return chalk.blue(content); + default: + return content; + } +} + +// 创建加载动画 +export function createSpinner(text) { + return ora({ + text, + spinner: 'dots', + color: 'cyan', + }); +} + +// 打印分隔线 +export function printDivider() { + console.log(chalk.gray('─'.repeat(50))); +} + +// 打印列表项 +export function printList(items, options = {}) { + const { numbered = false, indent = 2 } = options; + const prefix = ' '.repeat(indent); + + items.forEach((item, index) => { + const label = numbered ? `${index + 1}. ` : '• '; + console.log(prefix + label + item); + }); +} + +// 打印表格 +export function printTable(data, options = {}) { + const { headers = [], align = [] } = options; + + if (headers.length > 0) { + console.log(headers.join(' | ')); + console.log(align.map(a => '-'.repeat(a)).join('-+-')); + } + + data.forEach(row => { + console.log(row.join(' | ')); + }); +} + +// 打印 JSON +export function printJSON(data, indent = 2) { + return JSON.stringify(data, null, indent); +} + +// 打印富文本(Markdown) +export function printMarkdown(content) { + // 简单的 Markdown 渲染 + let output = content; + + // 代码块 + output = output.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => { + return chalk.gray(code.trim()); + }); + + // 行内代码 + output = output.replace(/`([^`]+)`/g, (_, code) => { + return chalk.gray(code); + }); + + // 粗体 + output = output.replace(/\*\*([^*]+)\*\*/g, (_, text) => { + return chalk.bold(text); + }); + + // 斜体 + output = output.replace(/\*([^*]+)\*/g, (_, text) => { + return chalk.italic(text); + }); + + return output; +} + +// 清屏 +export function clearScreen() { + console.clear(); +} + +// 退出 +export function exit(code = 0) { + process.exit(code); +} + +// 延迟 +export function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// 检查退出命令 +export function isExitCommand(input) { + const cmd = input.trim().toLowerCase(); + return ['quit', 'exit', 'q', '退出'].includes(cmd); +} + +// 检查帮助命令 +export function isHelpCommand(input) { + const cmd = input.trim().toLowerCase(); + return ['help', 'h', '帮助', '?'].includes(cmd); +} + +// 检查清屏命令 +export function isClearCommand(input) { + const cmd = input.trim().toLowerCase(); + return ['clear', 'cls', '清屏'].includes(cmd); +} + +// 解析命令行参数 +export function parseArgs(args) { + const result = { + flags: {}, + options: {}, + rest: [], + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg.startsWith('--')) { + const key = arg.slice(2); + const next = args[i + 1]; + + if (next && !next.startsWith('-')) { + result.options[key] = next; + i++; + } else { + result.flags[key] = true; + } + } else if (arg.startsWith('-')) { + const flags = arg.slice(1).split(''); + + for (const flag of flags) { + result.flags[flag] = true; + } + } else { + result.rest.push(arg); + } + } + + return result; +} + +export default { + createInterface, + printWelcome, + printHelp, + formatOutput, + createSpinner, + printDivider, + printList, + printTable, + printJSON, + printMarkdown, + clearScreen, + exit, + sleep, + isExitCommand, + isHelpCommand, + isClearCommand, + parseArgs, +}; \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..9e6c8b0 --- /dev/null +++ b/start.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# ExCLI 启动脚本 + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +cd "$PROJECT_DIR" + +# 检查 Node.js 版本 +NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt 18 ]; then + echo "错误: 需要 Node.js 18 或更高版本" + exit 1 +fi + +# 检查依赖 +if [ ! -d "node_modules" ]; then + echo "正在安装依赖..." + npm install + if [ $? -ne 0 ]; then + echo "安装依赖失败" + exit 1 + fi +fi + +# 复制环境配置 +if [ ! -f ".env" ]; then + if [ -f ".env.example" ]; then + cp .env.example .env + echo "已创建 .env 配置文件,请配置 OPENAI_API_KEY" + fi +fi + +# 启动程序 +exec node bin/cli.js "$@" \ No newline at end of file