@gird/ccli 1.0.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +22 -61
  2. package/dist/args.js +35 -0
  3. package/dist/args.js.map +1 -0
  4. package/dist/config.js +120 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/env.js +39 -0
  7. package/dist/env.js.map +1 -0
  8. package/dist/index.js +36 -226
  9. package/dist/index.js.map +1 -0
  10. package/dist/repl.js +59 -0
  11. package/dist/repl.js.map +1 -0
  12. package/dist/version.js +13 -0
  13. package/dist/version.js.map +1 -0
  14. package/package.json +4 -14
  15. package/.env.example +0 -23
  16. package/CCLI.md +0 -15
  17. package/dist/agent/client-openai.js +0 -166
  18. package/dist/agent/client.js +0 -63
  19. package/dist/agent/factory.js +0 -39
  20. package/dist/agent/loop.js +0 -163
  21. package/dist/agent/pricing.js +0 -28
  22. package/dist/agent/prompt.js +0 -43
  23. package/dist/agent/provider.js +0 -18
  24. package/dist/cli/headless.js +0 -50
  25. package/dist/cli/mentions.js +0 -60
  26. package/dist/cli/session.js +0 -432
  27. package/dist/commands/custom.js +0 -46
  28. package/dist/commands/slash.js +0 -148
  29. package/dist/config/config.js +0 -128
  30. package/dist/config/dotenv.js +0 -59
  31. package/dist/context/project.js +0 -155
  32. package/dist/hooks/hooks.js +0 -82
  33. package/dist/permissions/permissions.js +0 -87
  34. package/dist/session/store.js +0 -81
  35. package/dist/tools/bash.js +0 -97
  36. package/dist/tools/filesystem.js +0 -215
  37. package/dist/tools/index.js +0 -45
  38. package/dist/tools/search.js +0 -0
  39. package/dist/tools/task.js +0 -35
  40. package/dist/tools/todo.js +0 -68
  41. package/dist/tools/types.js +0 -8
  42. package/dist/tools/web.js +0 -108
  43. package/dist/ui/diff.js +0 -105
  44. package/dist/ui/markdown.js +0 -137
  45. package/dist/ui/spinner.js +0 -64
  46. package/dist/ui/theme.js +0 -106
package/README.md CHANGED
@@ -4,26 +4,17 @@
4
4
  [![license](https://img.shields.io/npm/l/@gird/ccli.svg)](./LICENSE)
5
5
  [![node](https://img.shields.io/node/v/@gird/ccli.svg)](https://nodejs.org)
6
6
 
7
- **CCLI** 是一个运行在终端里的**代理型(AgenticAI 编程助手**。你给出高层目标,它自主地阅读代码、检索、执行命令、修改文件并验证结果,直到任务完成——而不只是回答一个问题。
7
+ **CCLI** —— 一个**从零开始、一步一步**实现的代理型(Agentic)终端 AI 编程助手。
8
8
 
9
- 它支持 **Anthropic Claude** **OpenAI 兼容**后端(通义千问 Qwen、DeepSeek、GLM 等),完整的工具调用与多轮代理循环在两类后端上均可用。
9
+ > ⚠️ **重建中(work in progress)**:本项目正在从零逐步重写,力求每一步都简单、可读、可运行。
10
+ > 当前进度见下方「实现进度」。每一步都配有说明文档,放在 [`docs/steps/`](docs/steps/)。
10
11
 
11
- ```bash
12
- npm install -g @gird/ccli
13
- ccli # 首次运行会引导你配置模型与密钥
14
- ```
15
-
16
- ---
12
+ ## 为什么重写
17
13
 
18
- ## 特性
19
-
20
- - **代理循环** —— 感知(读文件 / 跑命令 / 检索)→ 推理 → 行动 → 验证,自动多轮直到目标完成。
21
- - **多后端** —— 一套接口适配 Anthropic Messages API 与 OpenAI Chat Completions(千问 / DeepSeek / GLM…),自动识别。
22
- - **丰富工具集** —— 读写与精确编辑文件、grep / glob 检索、shell 执行、网页抓取、子代理调研、任务清单。
23
- - **安全可控** —— 只读操作放行;写文件 / 执行命令前显式授权;高危命令(`rm -rf`、`git push`…)告警;支持钩子拦截。
24
- - **上下文工程** —— 自动索引项目结构、注入 `CCLI.md` 项目记忆、`/compact` 压缩、`/cost` 成本统计。
25
- - **顺手的交互** —— 流式 Markdown 渲染、`@文件` 引用、`!` 直通 shell、`#` 写长期记忆、会话恢复、自定义斜杠命令。
26
- - **零重型依赖** —— 纯 TypeScript 实现,运行时仅依赖官方 SDK。
14
+ 上一版实现一次性堆了太多功能,代码复杂到难以通读。现在推倒重来,遵循一个原则:
15
+ **从最小可运行的骨架开始,每次只加一个能独立讲清楚的功能,并为它写一份文档**
16
+ (这一步做什么、怎么做的、遇到什么问题、下一步是什么)。目标是过程可追、代码好懂,
17
+ 最终长成一个可发布的工具。
27
18
 
28
19
  ## 安装
29
20
 
@@ -33,18 +24,17 @@ npm install -g @gird/ccli
33
24
 
34
25
  要求 Node.js ≥ 18。
35
26
 
36
- ## 快速开始
27
+ ## 用法
37
28
 
38
29
  ```bash
39
- ccli # 进入交互式会话,首次运行引导配置
40
- ccli "为这个项目添加单元测试" # 直接给出目标
41
- ccli -p "总结 src 的结构" # 一次性模式(适合脚本 / 管道)
30
+ ccli # 进入交互式会话
31
+ ccli --version # 打印版本号
32
+ ccli --help # 打印帮助
42
33
  ```
43
34
 
44
35
  ## 配置
45
36
 
46
- CCLI 自动识别后端协议。密钥来源优先级:**环境变量 > 项目 `.env` > 全局 `~/.ccli/config.json`**。
47
- 首次运行 `ccli` 会弹出引导菜单帮你完成配置,也可手动设置:
37
+ 密钥来源优先级:**环境变量 > 项目 `.env` > 全局 `~/.ccli/config.json`**。
48
38
 
49
39
  **Anthropic Claude**
50
40
 
@@ -58,47 +48,18 @@ export ANTHROPIC_API_KEY=sk-ant-...
58
48
  export QWEN_API_KEY=sk-...
59
49
  export QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
60
50
  export QWEN_MODEL=qwen-plus
61
- # 或通用:OPENAI_API_KEY / OPENAI_BASE_URL / CCLI_MODEL / CCLI_PROVIDER=openai
51
+ # 或通用:OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL
62
52
  ```
63
53
 
64
- ## 命令行用法
65
-
66
- ```bash
67
- ccli [目标] 进入交互式会话(可附带初始目标)
68
- ccli -p, --print <文本> 一次性模式,输出后退出
69
- ccli -m, --model <id> 指定模型,如 -m qwen-max
70
- ccli -c, --continue 恢复本目录最近一次会话
71
- ccli -r, --resume [id] 列出 / 恢复历史会话
72
- ccli -y, --yes 自动批准所有工具(慎用)
73
- ccli config 配置密钥
74
- ccli -h, --help 查看帮助
75
- ```
76
-
77
- ## 交互
78
-
79
- 进入会话后,输入框支持:
80
-
81
- | 输入 | 作用 |
82
- | --- | --- |
83
- | 直接输入 | 与 AI 对话,触发代理循环 |
84
- | `@<路径>` | 将文件 / 目录内容并入消息 |
85
- | `!<命令>` | 直接执行 shell 命令 |
86
- | `#<文字>` | 将一条规则写入 `CCLI.md`(长期记忆) |
87
-
88
- 常用斜杠命令:`/help` `/model` `/cost` `/init` `/commit` `/resume` `/compact` `/clear` `/auto` `/exit`。
89
- 在 `.ccli/commands/<名>.md` 放置提示词模板即可定义自定义命令 `/<名>`。
90
-
91
- ## 工具
54
+ ## 实现进度
92
55
 
93
- | 工具 | 类型 | 说明 |
56
+ | 步骤 | 内容 | 文档 |
94
57
  | --- | --- | --- |
95
- | `read_file` / `list_dir` | 感知 | 读取文件(带行号)、列目录 |
96
- | `grep` / `glob` | 感知 | 正则检索内容、按模式匹配文件 |
97
- | `web_fetch` | 感知 | 抓取网页并转为文本 |
98
- | `write_file` / `edit_file` | 行动 | 创建 / 覆盖、精确替换编辑(带行级 diff) |
99
- | `bash` | 行动 | 执行 shell 命令(受权限约束,高危告警) |
100
- | `todo_write` | 协调 | 维护多步任务清单并实时显示进度 |
101
- | `task` | 协调 | 派生只读子代理执行聚焦调研 |
58
+ | 01 | 最小可执行入口 | [docs/steps/01-入口.md](docs/steps/01-入口.md) |
59
+ | 02 | 命令行参数解析(`--version` / `--help`) | [docs/steps/02-参数解析.md](docs/steps/02-参数解析.md) |
60
+ | 03 | 交互式会话循环(REPL) | [docs/steps/03-交互循环.md](docs/steps/03-交互循环.md) |
61
+ | 04 | 配置与密钥读取 | [docs/steps/04-配置读取.md](docs/steps/04-配置读取.md) |
62
+ | | 接入 AI、工具调用、代理循环… | 进行中 |
102
63
 
103
64
  ## 本地开发
104
65
 
@@ -107,7 +68,7 @@ git clone https://github.com/GridGit/ai-cli.git
107
68
  cd ai-cli
108
69
  npm install
109
70
  cp .env.example .env # 填入密钥
110
- npm run dev -- "你的目标" # 用 tsx 直接运行源码
71
+ npm run dev # 用 tsx 直接运行源码(免编译)
111
72
  npm run build # 编译到 dist/
112
73
  ```
113
74
 
package/dist/args.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 极简命令行参数解析。
3
+ *
4
+ * 现在只需要认识几个标志(flag),所以手写解析即可,不引第三方库。
5
+ * 把「原始 argv」翻译成一个结构化的 ParsedArgs,交给 index.ts 决定做什么。
6
+ */
7
+ export function parseArgs(argv) {
8
+ const result = {
9
+ version: false,
10
+ help: false,
11
+ unknown: [],
12
+ positionals: [],
13
+ };
14
+ for (const arg of argv) {
15
+ switch (arg) {
16
+ case '-v':
17
+ case '--version':
18
+ result.version = true;
19
+ break;
20
+ case '-h':
21
+ case '--help':
22
+ result.help = true;
23
+ break;
24
+ default:
25
+ if (arg.startsWith('-')) {
26
+ result.unknown.push(arg);
27
+ }
28
+ else {
29
+ result.positionals.push(arg);
30
+ }
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,MAAM,UAAU,SAAS,CAAC,IAAc;IACpC,MAAM,MAAM,GAAe;QACvB,OAAO,EAAE,KAAK;QACd,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,EAAE;QACX,WAAW,EAAE,EAAE;KAClB,CAAC;IAEF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,QAAQ,GAAG,EAAE,CAAC;YACV,KAAK,IAAI,CAAC;YACV,KAAK,WAAW;gBACZ,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;gBACtB,MAAM;YACV,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACT,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;gBACnB,MAAM;YACV;gBACI,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC7B,CAAC;qBAAM,CAAC;oBACJ,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACjC,CAAC;QACT,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 配置与密钥解析。
3
+ *
4
+ * 优先级(高覆盖低):
5
+ * 环境变量 > 项目 .env 文件 > 全局 ~/.ccli/config.json
6
+ *
7
+ * 支持的后端:
8
+ * - Anthropic (Claude) —— ANTHROPIC_API_KEY [+ ANTHROPIC_BASE_URL / ANTHROPIC_MODEL]
9
+ * - OpenAI 兼容(通义千问等) —— QWEN_API_KEY / OPENAI_API_KEY [+ *_BASE_URL / *_MODEL]
10
+ *
11
+ * 本步只负责「读取并解析成一个清晰的 Config」,不发起任何网络请求。
12
+ */
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { readFileSync } from 'node:fs';
16
+ import { loadEnvFile } from './env.js';
17
+ /** 各后端在未显式指定 model 时的兜底默认值。 */
18
+ const DEFAULT_MODEL = {
19
+ anthropic: 'claude-sonnet-4-5',
20
+ openai: 'qwen-plus',
21
+ };
22
+ const GLOBAL_CONFIG_PATH = join(homedir(), '.ccli', 'config.json');
23
+ /** 读取全局 ~/.ccli/config.json(优先级最低);不存在或损坏则返回空。 */
24
+ function loadGlobalConfig() {
25
+ try {
26
+ const raw = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, 'utf8'));
27
+ return {
28
+ provider: raw.provider,
29
+ apiKey: raw.apiKey,
30
+ baseURL: raw.baseURL,
31
+ model: raw.model,
32
+ };
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
38
+ /**
39
+ * 解析出最终生效的配置。
40
+ * @param cwd 项目根目录,用于定位 .env
41
+ */
42
+ export function loadConfig(cwd = process.cwd()) {
43
+ const dotenv = loadEnvFile(join(cwd, '.env'));
44
+ const global = loadGlobalConfig();
45
+ // 按优先级取一个环境风格的键:环境变量 > .env
46
+ const get = (key) => process.env[key] ?? dotenv[key];
47
+ // —— 后端识别(按 Key 出现顺序)——
48
+ // 1) Anthropic
49
+ const anthropicKey = get('ANTHROPIC_API_KEY');
50
+ if (anthropicKey) {
51
+ return {
52
+ ok: true,
53
+ config: {
54
+ provider: 'anthropic',
55
+ apiKey: anthropicKey,
56
+ baseURL: get('ANTHROPIC_BASE_URL'),
57
+ model: get('ANTHROPIC_MODEL') ??
58
+ get('CCLI_MODEL') ??
59
+ DEFAULT_MODEL.anthropic,
60
+ },
61
+ };
62
+ }
63
+ // 2) 通义千问 Qwen(OpenAI 兼容的便捷前缀)
64
+ const qwenKey = get('QWEN_API_KEY');
65
+ if (qwenKey) {
66
+ return {
67
+ ok: true,
68
+ config: {
69
+ provider: 'openai',
70
+ apiKey: qwenKey,
71
+ baseURL: get('QWEN_BASE_URL'),
72
+ model: get('QWEN_MODEL') ?? DEFAULT_MODEL.openai,
73
+ },
74
+ };
75
+ }
76
+ // 3) 通用 OpenAI 兼容后端(DeepSeek / GLM / 自建)
77
+ const openaiKey = get('OPENAI_API_KEY');
78
+ if (openaiKey) {
79
+ return {
80
+ ok: true,
81
+ config: {
82
+ provider: 'openai',
83
+ apiKey: openaiKey,
84
+ baseURL: get('OPENAI_BASE_URL'),
85
+ model: get('OPENAI_MODEL') ??
86
+ get('CCLI_MODEL') ??
87
+ DEFAULT_MODEL.openai,
88
+ },
89
+ };
90
+ }
91
+ // 4) 回退到全局配置
92
+ if (global.apiKey && global.provider) {
93
+ return {
94
+ ok: true,
95
+ config: {
96
+ provider: global.provider,
97
+ apiKey: global.apiKey,
98
+ baseURL: global.baseURL,
99
+ model: global.model ?? DEFAULT_MODEL[global.provider],
100
+ },
101
+ };
102
+ }
103
+ // 都没有 —— 给出可操作的提示,而不是抛异常
104
+ return {
105
+ ok: false,
106
+ message: [
107
+ '未找到可用的 API 配置。请任选其一:',
108
+ ' • 设置环境变量 ANTHROPIC_API_KEY=sk-ant-...',
109
+ ' • 或在项目 .env 写入 QWEN_API_KEY / QWEN_BASE_URL / QWEN_MODEL',
110
+ ' • 或在 ~/.ccli/config.json 配置 { provider, apiKey, baseURL, model }',
111
+ ].join('\n'),
112
+ };
113
+ }
114
+ /** 打码显示密钥,用于安全地回显「用了哪个 key」。 */
115
+ export function maskKey(key) {
116
+ if (key.length <= 8)
117
+ return '****';
118
+ return `${key.slice(0, 4)}…${key.slice(-4)}`;
119
+ }
120
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/env.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * 极简 .env 文件解析器(零依赖,不引 dotenv)。
3
+ *
4
+ * 只做最常见的事:逐行读 KEY=VALUE,忽略空行和 # 注释,去掉值两侧引号。
5
+ * 解析结果作为一个普通对象返回,由 config.ts 决定它和真实环境变量谁优先。
6
+ * 特意「不写回 process.env」,避免隐式副作用——优先级由调用方显式控制。
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ export function parseEnv(text) {
10
+ const result = {};
11
+ for (const raw of text.split('\n')) {
12
+ const line = raw.trim();
13
+ if (line.length === 0 || line.startsWith('#'))
14
+ continue;
15
+ const eq = line.indexOf('=');
16
+ if (eq === -1)
17
+ continue; // 没有 = 的行跳过
18
+ const key = line.slice(0, eq).trim();
19
+ let value = line.slice(eq + 1).trim();
20
+ // 去掉成对的首尾引号
21
+ if ((value.startsWith('"') && value.endsWith('"')) ||
22
+ (value.startsWith("'") && value.endsWith("'"))) {
23
+ value = value.slice(1, -1);
24
+ }
25
+ if (key)
26
+ result[key] = value;
27
+ }
28
+ return result;
29
+ }
30
+ /** 读取并解析一个 .env 文件;文件不存在则返回空对象。 */
31
+ export function loadEnvFile(path) {
32
+ try {
33
+ return parseEnv(readFileSync(path, 'utf8'));
34
+ }
35
+ catch {
36
+ return {}; // 文件不存在等情况:静默当作没有配置
37
+ }
38
+ }
39
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,UAAU,QAAQ,CAAC,IAAY;IACjC,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAExD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,SAAS,CAAC,YAAY;QAErC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEtC,YAAY;QACZ,IACI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC9C,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAChD,CAAC;YACC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;QAED,IAAI,GAAG;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACjC,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,WAAW,CAAC,IAAY;IACpC,IAAI,CAAC;QACD,OAAO,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC,CAAC,oBAAoB;IACnC,CAAC;AACL,CAAC"}
package/dist/index.js CHANGED
@@ -1,237 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CCLI 入口 —— 命令行参数解析与启动。
3
+ * CCLI 入口。
4
4
  *
5
- * 用法:
6
- * ccli 进入交互式会话(REPL)
7
- * ccli -p "提示词" 一次性(非交互)模式,输出后退出
8
- * ccli --model <id> 指定模型
9
- * ccli config 配置 API Key
10
- * ccli --version / --help
5
+ * 第二步:解析命令行参数。
6
+ * ccli --version / -v 打印版本号
7
+ * ccli --help / -h 打印帮助
8
+ * ccli (暂时)打印占位问候,后续接入交互式会话
9
+ * ccli --xxx 未知标志 报错并给出提示,退出码 1
11
10
  */
12
- import * as readline from 'node:readline/promises';
13
- import { stdin, stdout } from 'node:process';
14
- import { Config, DEFAULT_MODEL } from './config/config.js';
15
- import { Session } from './cli/session.js';
16
- import { c, brand } from './ui/theme.js';
17
- import { box } from './ui/markdown.js';
18
- import { runHeadless } from './cli/headless.js';
19
- import { SessionStore } from './session/store.js';
20
- import { loadDotenv } from './config/dotenv.js';
21
- const VERSION = '1.0.0';
22
- function parseArgs(argv) {
23
- const args = { cwd: process.cwd() };
24
- for (let i = 0; i < argv.length; i++) {
25
- const a = argv[i];
26
- switch (a) {
27
- case '-p':
28
- case '--print':
29
- args.prompt = argv[++i];
30
- break;
31
- case '--model':
32
- case '-m':
33
- args.model = argv[++i];
34
- break;
35
- case '--cwd':
36
- args.cwd = argv[++i];
37
- break;
38
- case '--dangerously-skip-permissions':
39
- case '--yes':
40
- case '-y':
41
- args.autoApprove = true;
42
- break;
43
- case '--continue':
44
- case '-c':
45
- args.continueSession = true;
46
- break;
47
- case '--resume':
48
- case '-r': {
49
- args.resume = true;
50
- const next = argv[i + 1];
51
- if (next && !next.startsWith('-')) {
52
- args.resumeId = next;
53
- i++;
54
- }
55
- break;
56
- }
57
- case '--version':
58
- case '-v':
59
- args.command = 'version';
60
- break;
61
- case '--help':
62
- case '-h':
63
- args.command = 'help';
64
- break;
65
- case 'config':
66
- args.command = 'config';
67
- break;
68
- default:
69
- // 把裸参数当作 prompt
70
- if (!a.startsWith('-') && !args.prompt) {
71
- args.prompt = argv.slice(i).join(' ');
72
- i = argv.length;
73
- }
74
- }
75
- }
76
- return args;
77
- }
78
- function printHelp() {
79
- const lines = [
80
- brand.primary(c.bold('CCLI')) + c.dim(` v${VERSION} —— AI 软件工程师(Claude Code 复刻)`),
81
- '',
82
- c.bold('用法'),
83
- ` ${brand.accent('ccli')} 进入交互式会话`,
84
- ` ${brand.accent('ccli "重构 utils 模块"')} 直接给出目标并进入会话`,
85
- ` ${brand.accent('ccli -p "..."')} 一次性模式(输出后退出,适合脚本/管道)`,
86
- ` ${brand.accent('ccli config')} 配置 API Key`,
87
- '',
88
- c.bold('选项'),
89
- ` ${c.cyan('-p, --print <文本>')} 非交互模式`,
90
- ` ${c.cyan('-m, --model <id>')} 指定模型(默认 claude-opus-4-8)`,
91
- ` ${c.cyan('-y, --yes')} 自动批准所有工具调用`,
92
- ` ${c.cyan('-c, --continue')} 恢复本目录最近一次会话`,
93
- ` ${c.cyan('-r, --resume [id]')} 列出/恢复历史会话`,
94
- ` ${c.cyan('--cwd <目录>')} 指定工作目录`,
95
- ` ${c.cyan('-v, --version')} 版本号`,
96
- ` ${c.cyan('-h, --help')} 帮助`,
97
- '',
98
- c.bold('环境变量'),
99
- ` ${c.cyan('ANTHROPIC_API_KEY')} Anthropic API 密钥`,
100
- ` ${c.cyan('ANTHROPIC_BASE_URL')} 自定义 API 端点(可选)`,
101
- ];
102
- stdout.write(box(lines) + '\n');
103
- }
104
- async function onboardApiKey(config) {
105
- stdout.write(box([
106
- brand.primary(c.bold('欢迎使用 CCLI')),
107
- '',
108
- '首次使用,先配置一个模型后端(将保存到 ~/.ccli/config.json)。',
109
- '',
110
- ` ${brand.accent('1')} 通义千问 Qwen ${c.dim('(OpenAI 兼容,推荐)')}`,
111
- ` ${brand.accent('2')} Anthropic Claude`,
112
- ` ${brand.accent('3')} 其他 OpenAI 兼容后端 ${c.dim('(DeepSeek / GLM / 自建)')}`,
113
- ], { color: brand.warn }) + '\n');
114
- const rl = readline.createInterface({ input: stdin, output: stdout });
115
- try {
116
- const choice = (await rl.question(brand.primary('选择后端 [1] ❯ '))).trim() || '1';
117
- if (choice === '2') {
118
- config.data.provider = 'anthropic';
119
- const key = (await rl.question(brand.primary('Anthropic API Key ❯ '))).trim();
120
- if (!key)
121
- return false;
122
- config.data.apiKey = key;
123
- config.data.baseURL = undefined;
124
- config.data.model = DEFAULT_MODEL;
125
- }
126
- else if (choice === '3') {
127
- config.data.provider = 'openai';
128
- const base = (await rl.question(brand.primary('Base URL ❯ '))).trim();
129
- const model = (await rl.question(brand.primary('模型 ID ❯ '))).trim();
130
- const key = (await rl.question(brand.primary('API Key ❯ '))).trim();
131
- if (!key || !base || !model)
132
- return false;
133
- config.data.apiKey = key;
134
- config.data.baseURL = base;
135
- config.data.model = model;
136
- }
137
- else {
138
- // 默认:千问
139
- config.data.provider = 'openai';
140
- const key = (await rl.question(brand.primary('千问 API Key ') + c.dim('(sk-...) ❯ '))).trim();
141
- if (!key)
142
- return false;
143
- config.data.apiKey = key;
144
- config.data.baseURL =
145
- 'https://dashscope.aliyuncs.com/compatible-mode/v1';
146
- const model = (await rl.question(brand.primary('模型 ') + c.dim('[qwen-plus] ❯ '))).trim();
147
- config.data.model = model || 'qwen-plus';
148
- }
149
- }
150
- finally {
151
- rl.close();
152
- }
153
- config.saveGlobal();
154
- stdout.write(brand.ok('✓ 已保存配置。') +
155
- c.dim(` 后端 ${config.data.provider} · 模型 ${config.data.model}\n\n`));
156
- return true;
157
- }
158
- async function main() {
11
+ import { parseArgs } from './args.js';
12
+ import { NAME, VERSION } from './version.js';
13
+ import { startRepl } from './repl.js';
14
+ const HELP = `${NAME} v${VERSION}
15
+
16
+ 用法:
17
+ ccli [选项]
18
+
19
+ 选项:
20
+ -v, --version 打印版本号
21
+ -h, --help 打印本帮助
22
+
23
+ 示例:
24
+ ccli 启动 CCLI
25
+ ccli --version 查看版本
26
+ `;
27
+ function main() {
28
+ // argv 前两个是 node 路径和脚本路径,从第 3 个起才是用户输入
159
29
  const args = parseArgs(process.argv.slice(2));
160
- // 本地开发:自动加载 .env(不覆盖真实环境变量)
161
- loadDotenv(args.cwd);
162
- if (args.command === 'version') {
163
- stdout.write(`ccli v${VERSION}\n`);
164
- return;
30
+ if (args.unknown.length > 0) {
31
+ console.error(`未知选项: ${args.unknown.join(', ')}`);
32
+ console.error(`运行 "ccli --help" 查看可用选项。`);
33
+ process.exit(1);
165
34
  }
166
- if (args.command === 'help') {
167
- printHelp();
35
+ if (args.help) {
36
+ console.log(HELP);
168
37
  return;
169
38
  }
170
- const config = new Config(args.cwd);
171
- if (args.model) {
172
- config.model = args.model;
173
- }
174
- if (args.autoApprove) {
175
- config.data.autoApprove = true;
176
- }
177
- if (args.command === 'config') {
178
- await onboardApiKey(config);
179
- stdout.write([
180
- `当前模型:${brand.accent(config.model)}`,
181
- `配置文件:~/.ccli/config.json`,
182
- ].join('\n') + '\n');
39
+ if (args.version) {
40
+ console.log(VERSION);
183
41
  return;
184
42
  }
185
- // 检查 API Key
186
- if (!config.apiKey) {
187
- if (args.prompt) {
188
- stdout.write(brand.err('错误:') +
189
- '未设置 API Key。请先运行 ccli(无参数,引导配置),或 export ANTHROPIC_API_KEY / QWEN_API_KEY。\n');
190
- process.exit(1);
191
- }
192
- const ok = await onboardApiKey(config);
193
- if (!ok || !config.apiKey) {
194
- stdout.write(c.dim('已退出。设置 API Key 后再运行 ccli。\n'));
195
- process.exit(0);
196
- }
197
- }
198
- // 一次性模式
199
- if (args.prompt) {
200
- await runHeadless(config, args.cwd, args.prompt);
201
- return;
202
- }
203
- // 会话恢复
204
- let resumeFrom;
205
- if (args.resume && !args.resumeId) {
206
- // 列出供选择
207
- const all = SessionStore.list(args.cwd);
208
- if (!all.length) {
209
- stdout.write(c.dim('该目录暂无历史会话,将开启新会话。\n'));
210
- }
211
- else {
212
- stdout.write(brand.primary('该目录的历史会话:\n'));
213
- for (const s of all.slice(0, 10)) {
214
- stdout.write(` ${brand.accent(s.id)} ${c.dim(s.updatedAt.replace('T', ' ').slice(0, 16))} ${c.dim(`${s.messages.length} 条`)}\n`);
215
- }
216
- stdout.write(c.dim('用 ccli --resume <id> 恢复指定会话。本次将开启新会话。\n\n'));
217
- }
218
- }
219
- else if ((args.continueSession || args.resumeId) && config.apiKey) {
220
- const data = SessionStore.load(args.cwd, args.resumeId);
221
- if (data) {
222
- resumeFrom = data;
223
- config.model = data.model;
224
- stdout.write(c.dim(`已恢复会话 ${data.id}(${data.messages.length} 条历史)。\n\n`));
225
- }
226
- else {
227
- stdout.write(c.dim('未找到可恢复的会话,将开启新会话。\n\n'));
228
- }
229
- }
230
- // 交互式会话
231
- const session = new Session(config, args.cwd, resumeFrom);
232
- await session.start();
43
+ // 无参数:进入交互式会话循环。
44
+ startRepl();
233
45
  }
234
- main().catch((e) => {
235
- stdout.write(brand.err('\n致命错误:') + (e?.stack ?? e?.message ?? String(e)) + '\n');
236
- process.exit(1);
237
- });
46
+ main();
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,IAAI,GAAG,GAAG,IAAI,KAAK,OAAO;;;;;;;;;;;;CAY/B,CAAC;AAEF,SAAS,IAAI;IACT,uCAAuC;IACvC,MAAM,IAAI,GACV,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClD,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO;IACX,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO;IACX,CAAC;IAED,iBAAiB;IACjB,SAAS,EAAE,CAAC;AAChB,CAAC;AAED,IAAI,EAAE,CAAC"}