@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.
- package/README.md +22 -61
- package/dist/args.js +35 -0
- package/dist/args.js.map +1 -0
- package/dist/config.js +120 -0
- package/dist/config.js.map +1 -0
- package/dist/env.js +39 -0
- package/dist/env.js.map +1 -0
- package/dist/index.js +36 -226
- package/dist/index.js.map +1 -0
- package/dist/repl.js +59 -0
- package/dist/repl.js.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +4 -14
- package/.env.example +0 -23
- package/CCLI.md +0 -15
- package/dist/agent/client-openai.js +0 -166
- package/dist/agent/client.js +0 -63
- package/dist/agent/factory.js +0 -39
- package/dist/agent/loop.js +0 -163
- package/dist/agent/pricing.js +0 -28
- package/dist/agent/prompt.js +0 -43
- package/dist/agent/provider.js +0 -18
- package/dist/cli/headless.js +0 -50
- package/dist/cli/mentions.js +0 -60
- package/dist/cli/session.js +0 -432
- package/dist/commands/custom.js +0 -46
- package/dist/commands/slash.js +0 -148
- package/dist/config/config.js +0 -128
- package/dist/config/dotenv.js +0 -59
- package/dist/context/project.js +0 -155
- package/dist/hooks/hooks.js +0 -82
- package/dist/permissions/permissions.js +0 -87
- package/dist/session/store.js +0 -81
- package/dist/tools/bash.js +0 -97
- package/dist/tools/filesystem.js +0 -215
- package/dist/tools/index.js +0 -45
- package/dist/tools/search.js +0 -0
- package/dist/tools/task.js +0 -35
- package/dist/tools/todo.js +0 -68
- package/dist/tools/types.js +0 -8
- package/dist/tools/web.js +0 -108
- package/dist/ui/diff.js +0 -105
- package/dist/ui/markdown.js +0 -137
- package/dist/ui/spinner.js +0 -64
- package/dist/ui/theme.js +0 -106
package/dist/repl.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交互式会话循环(REPL:Read-Eval-Print-Loop)。
|
|
3
|
+
*
|
|
4
|
+
* 本步只搭骨架,不接 AI:
|
|
5
|
+
* 读一行用户输入 → 处理(暂时只回显)→ 打印提示符,再读下一行。
|
|
6
|
+
* 支持优雅退出:/exit、/quit,或按 Ctrl-D(EOF)、Ctrl-C。
|
|
7
|
+
*
|
|
8
|
+
* 用 Node 内置 readline,零依赖。等骨架稳定后,下一步把「处理」那一环
|
|
9
|
+
* 换成「调用 AI」即可,循环结构不用动。
|
|
10
|
+
*/
|
|
11
|
+
import { createInterface } from 'node:readline';
|
|
12
|
+
import { NAME, VERSION } from './version.js';
|
|
13
|
+
import { loadConfig, maskKey } from './config.js';
|
|
14
|
+
const PROMPT = '› ';
|
|
15
|
+
/** 用户输入这些即退出。 */
|
|
16
|
+
const EXIT_COMMANDS = new Set(['/exit', '/quit']);
|
|
17
|
+
export function startRepl() {
|
|
18
|
+
// 启动即解析配置:缺失就明确报错退出,不进入循环
|
|
19
|
+
const result = loadConfig();
|
|
20
|
+
if (!result.ok) {
|
|
21
|
+
console.error(result.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const { config } = result;
|
|
25
|
+
const rl = createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout,
|
|
28
|
+
prompt: PROMPT,
|
|
29
|
+
});
|
|
30
|
+
console.log(`${NAME} v${VERSION}`);
|
|
31
|
+
console.log(`后端: ${config.provider} 模型: ${config.model} 密钥: ${maskKey(config.apiKey)}`);
|
|
32
|
+
console.log('进入交互式会话。输入 /exit 退出,或按 Ctrl-C。\n');
|
|
33
|
+
rl.prompt();
|
|
34
|
+
rl.on('line', (line) => {
|
|
35
|
+
const input = line.trim();
|
|
36
|
+
if (input.length === 0) {
|
|
37
|
+
// 空行:不处理,直接再次提示
|
|
38
|
+
rl.prompt();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (EXIT_COMMANDS.has(input)) {
|
|
42
|
+
rl.close();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// 暂时的「处理」:回显。下一步换成调用 AI。
|
|
46
|
+
console.log(`你说:${input}`);
|
|
47
|
+
rl.prompt();
|
|
48
|
+
});
|
|
49
|
+
// Ctrl-D(EOF)或 rl.close() 触发
|
|
50
|
+
rl.on('close', () => {
|
|
51
|
+
console.log('\n再见 👋');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
// Ctrl-C:关闭循环(进而触发 close)
|
|
55
|
+
rl.on('SIGINT', () => {
|
|
56
|
+
rl.close();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=repl.js.map
|
package/dist/repl.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repl.js","sourceRoot":"","sources":["../src/repl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,GAAG,IAAI,CAAC;AAEpB,iBAAiB;AACjB,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAElD,MAAM,UAAU,SAAS;IACrB,0BAA0B;IAC1B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAE1B,MAAM,EAAE,GAAG,eAAe,CAAC;QACvB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,MAAM;KACjB,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CACP,OAAO,MAAM,CAAC,QAAQ,SAAS,MAAM,CAAC,KAAK,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAC/E,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAChD,EAAE,CAAC,MAAM,EAAE,CAAC;IAEZ,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAE1B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,gBAAgB;YAChB,EAAE,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO;QACX,CAAC;QAED,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO;QACX,CAAC;QAED,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC;QAE3B,EAAE,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,6BAA6B;IAC7B,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAChB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,0BAA0B;IAC1B,EAAE,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACjB,EAAE,CAAC,KAAK,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读取自身版本号。
|
|
3
|
+
*
|
|
4
|
+
* 版本号的唯一真相是 package.json,运行时从那里读,避免手写常量导致不同步。
|
|
5
|
+
* 编译后 dist/version.js 在 dist/ 下,package.json 在其上一级,故用 ../package.json。
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
10
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
11
|
+
export const NAME = pkg.name;
|
|
12
|
+
export const VERSION = pkg.version;
|
|
13
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAGnD,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;AAC7B,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gird/ccli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CCLI ——
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "CCLI —— 一步一步从零实现的代理型(Agentic)终端 AI 编程助手",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ccli": "./dist/index.js"
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"README.md"
|
|
13
|
-
"CCLI.md",
|
|
14
|
-
".env.example"
|
|
12
|
+
"README.md"
|
|
15
13
|
],
|
|
16
14
|
"scripts": {
|
|
17
15
|
"build": "tsc -p tsconfig.json",
|
|
@@ -27,10 +25,7 @@
|
|
|
27
25
|
"ai",
|
|
28
26
|
"cli",
|
|
29
27
|
"agent",
|
|
30
|
-
"coding-assistant"
|
|
31
|
-
"anthropic",
|
|
32
|
-
"qwen",
|
|
33
|
-
"openai"
|
|
28
|
+
"coding-assistant"
|
|
34
29
|
],
|
|
35
30
|
"repository": {
|
|
36
31
|
"type": "git",
|
|
@@ -40,15 +35,10 @@
|
|
|
40
35
|
"url": "https://github.com/GridGit/ai-cli/issues"
|
|
41
36
|
},
|
|
42
37
|
"homepage": "https://github.com/GridGit/ai-cli#readme",
|
|
43
|
-
"author": "",
|
|
44
38
|
"license": "MIT",
|
|
45
39
|
"publishConfig": {
|
|
46
40
|
"access": "public"
|
|
47
41
|
},
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"@anthropic-ai/sdk": "^0.104.1",
|
|
50
|
-
"openai": "^6.42.0"
|
|
51
|
-
},
|
|
52
42
|
"devDependencies": {
|
|
53
43
|
"@types/node": "^25.9.3",
|
|
54
44
|
"tsx": "^4.22.4",
|
package/.env.example
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# CCLI 本地开发默认配置
|
|
2
|
-
# 用法:复制本文件为 .env,填入 API Key 即可。
|
|
3
|
-
# cp .env.example .env
|
|
4
|
-
# .env 已被 .gitignore 忽略,不会提交。启动时自动加载(真实环境变量优先)。
|
|
5
|
-
|
|
6
|
-
# ───────────────────────────────────────────────
|
|
7
|
-
# 后端二选一(不设 ANTHROPIC_API_KEY 时自动用 OpenAI 兼容后端)
|
|
8
|
-
# ───────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
# 【方式 A】通义千问 Qwen(OpenAI 兼容,推荐用于本地开发)
|
|
11
|
-
QWEN_API_KEY=sk-在这里填入你的千问key
|
|
12
|
-
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
13
|
-
QWEN_MODEL=qwen-plus
|
|
14
|
-
|
|
15
|
-
# 【方式 B】Anthropic 官方(若有 Claude key,取消注释即可,会优先生效)
|
|
16
|
-
# ANTHROPIC_API_KEY=sk-ant-...
|
|
17
|
-
# ANTHROPIC_BASE_URL=https://api.anthropic.com
|
|
18
|
-
|
|
19
|
-
# 【方式 C】其他 OpenAI 兼容后端(DeepSeek / GLM / 自建)
|
|
20
|
-
# OPENAI_API_KEY=...
|
|
21
|
-
# OPENAI_BASE_URL=https://your-endpoint/v1
|
|
22
|
-
# CCLI_MODEL=your-model
|
|
23
|
-
# CCLI_PROVIDER=openai
|
package/CCLI.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# CCLI 项目说明(供 AI 助手阅读)
|
|
2
|
-
|
|
3
|
-
这是一个用 TypeScript 实现的、复刻 Claude Code 的代理型终端 AI 编程助手。
|
|
4
|
-
|
|
5
|
-
## 关键约定
|
|
6
|
-
- 纯 ESM(`"type": "module"`),import 语句需带 `.js` 扩展名(指向编译产物)。
|
|
7
|
-
- 运行时依赖仅 `@anthropic-ai/sdk`;其余为 Node 内置模块。
|
|
8
|
-
- UI 不使用 Ink/React,全部用 `src/ui/theme.ts` 的 ANSI 工具手写。
|
|
9
|
-
- 工具新增方式:实现 `src/tools/types.ts` 的 `Tool` 接口,在 `src/tools/index.ts` 注册。
|
|
10
|
-
- 修改后用 `npm run build` 或 `npm run typecheck` 验证。
|
|
11
|
-
|
|
12
|
-
## 开发命令
|
|
13
|
-
- `npm run build` —— 编译到 `dist/`
|
|
14
|
-
- `npm run typecheck` —— 仅类型检查
|
|
15
|
-
- `npm run dev -- <args>` —— 用 tsx 直接运行源码
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAI 兼容客户端 —— 对接 OpenAI Chat Completions 协议的后端,
|
|
3
|
-
* 例如通义千问(DashScope compatible-mode)、DeepSeek、GLM 等。
|
|
4
|
-
*
|
|
5
|
-
* 它在边界处把代理循环内部使用的 Anthropic 消息格式(含 tool_use / tool_result 块)
|
|
6
|
-
* 与 OpenAI 的 messages / tool_calls 格式互相转换,从而让上层循环逻辑保持不变。
|
|
7
|
-
*/
|
|
8
|
-
import OpenAI from 'openai';
|
|
9
|
-
export class OpenAICompatClient {
|
|
10
|
-
client;
|
|
11
|
-
model;
|
|
12
|
-
maxTokens;
|
|
13
|
-
temperature;
|
|
14
|
-
constructor(opts) {
|
|
15
|
-
this.client = new OpenAI({ apiKey: opts.apiKey, baseURL: opts.baseURL });
|
|
16
|
-
this.model = opts.model;
|
|
17
|
-
this.maxTokens = opts.maxTokens;
|
|
18
|
-
this.temperature = opts.temperature;
|
|
19
|
-
}
|
|
20
|
-
setModel(m) {
|
|
21
|
-
this.model = m;
|
|
22
|
-
}
|
|
23
|
-
getModel() {
|
|
24
|
-
return this.model;
|
|
25
|
-
}
|
|
26
|
-
async stream(system, messages, tools, callbacks, signal) {
|
|
27
|
-
const oaMessages = this.toOpenAIMessages(system, messages);
|
|
28
|
-
const oaTools = tools.map((t) => ({
|
|
29
|
-
type: 'function',
|
|
30
|
-
function: {
|
|
31
|
-
name: t.name,
|
|
32
|
-
description: t.description,
|
|
33
|
-
parameters: t.input_schema ?? { type: 'object', properties: {} },
|
|
34
|
-
},
|
|
35
|
-
}));
|
|
36
|
-
const stream = await this.client.chat.completions.create({
|
|
37
|
-
model: this.model,
|
|
38
|
-
max_tokens: this.maxTokens,
|
|
39
|
-
temperature: this.temperature,
|
|
40
|
-
messages: oaMessages,
|
|
41
|
-
tools: oaTools.length ? oaTools : undefined,
|
|
42
|
-
tool_choice: oaTools.length ? 'auto' : undefined,
|
|
43
|
-
stream: true,
|
|
44
|
-
stream_options: { include_usage: true },
|
|
45
|
-
}, { signal });
|
|
46
|
-
let text = '';
|
|
47
|
-
let finishReason = null;
|
|
48
|
-
const usage = { input: 0, output: 0 };
|
|
49
|
-
// 按 index 累积 tool_calls
|
|
50
|
-
const toolAcc = new Map();
|
|
51
|
-
for await (const chunk of stream) {
|
|
52
|
-
const choice = chunk.choices?.[0];
|
|
53
|
-
if (choice) {
|
|
54
|
-
const delta = choice.delta;
|
|
55
|
-
if (delta?.content) {
|
|
56
|
-
text += delta.content;
|
|
57
|
-
callbacks.onText?.(delta.content);
|
|
58
|
-
}
|
|
59
|
-
if (delta?.tool_calls) {
|
|
60
|
-
for (const tc of delta.tool_calls) {
|
|
61
|
-
const idx = tc.index ?? 0;
|
|
62
|
-
const cur = toolAcc.get(idx) ?? { id: '', name: '', args: '' };
|
|
63
|
-
if (tc.id)
|
|
64
|
-
cur.id = tc.id;
|
|
65
|
-
if (tc.function?.name)
|
|
66
|
-
cur.name = tc.function.name;
|
|
67
|
-
if (tc.function?.arguments)
|
|
68
|
-
cur.args += tc.function.arguments;
|
|
69
|
-
toolAcc.set(idx, cur);
|
|
70
|
-
if (tc.function?.name)
|
|
71
|
-
callbacks.onToolUseStart?.(tc.function.name, cur.id);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (choice.finish_reason)
|
|
75
|
-
finishReason = choice.finish_reason;
|
|
76
|
-
}
|
|
77
|
-
if (chunk.usage) {
|
|
78
|
-
usage.input = chunk.usage.prompt_tokens ?? 0;
|
|
79
|
-
usage.output = chunk.usage.completion_tokens ?? 0;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// 组装 Anthropic 内部格式的 content 块
|
|
83
|
-
const content = [];
|
|
84
|
-
if (text)
|
|
85
|
-
content.push({ type: 'text', text });
|
|
86
|
-
let genId = 0;
|
|
87
|
-
for (const [, tc] of [...toolAcc.entries()].sort((a, b) => a[0] - b[0])) {
|
|
88
|
-
let input = {};
|
|
89
|
-
try {
|
|
90
|
-
input = tc.args ? JSON.parse(tc.args) : {};
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
input = {};
|
|
94
|
-
}
|
|
95
|
-
content.push({
|
|
96
|
-
type: 'tool_use',
|
|
97
|
-
id: tc.id || `call_${Date.now()}_${genId++}`,
|
|
98
|
-
name: tc.name,
|
|
99
|
-
input,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
const stopReason = finishReason === 'tool_calls'
|
|
103
|
-
? 'tool_use'
|
|
104
|
-
: finishReason === 'length'
|
|
105
|
-
? 'max_tokens'
|
|
106
|
-
: 'end_turn';
|
|
107
|
-
return { content, stopReason, usage };
|
|
108
|
-
}
|
|
109
|
-
/** Anthropic 内部消息 → OpenAI messages。 */
|
|
110
|
-
toOpenAIMessages(system, messages) {
|
|
111
|
-
const out = [
|
|
112
|
-
{ role: 'system', content: system },
|
|
113
|
-
];
|
|
114
|
-
for (const m of messages) {
|
|
115
|
-
if (typeof m.content === 'string') {
|
|
116
|
-
out.push({ role: m.role, content: m.content });
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const blocks = m.content;
|
|
120
|
-
if (m.role === 'assistant') {
|
|
121
|
-
let textContent = '';
|
|
122
|
-
const toolCalls = [];
|
|
123
|
-
for (const b of blocks) {
|
|
124
|
-
if (b.type === 'text')
|
|
125
|
-
textContent += b.text;
|
|
126
|
-
else if (b.type === 'tool_use') {
|
|
127
|
-
toolCalls.push({
|
|
128
|
-
id: b.id,
|
|
129
|
-
type: 'function',
|
|
130
|
-
function: {
|
|
131
|
-
name: b.name,
|
|
132
|
-
arguments: JSON.stringify(b.input ?? {}),
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const msg = { role: 'assistant' };
|
|
138
|
-
msg.content = textContent || null;
|
|
139
|
-
if (toolCalls.length)
|
|
140
|
-
msg.tool_calls = toolCalls;
|
|
141
|
-
out.push(msg);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
// user 消息:可能含 tool_result 与 text
|
|
145
|
-
const userTexts = [];
|
|
146
|
-
for (const b of blocks) {
|
|
147
|
-
if (b.type === 'tool_result') {
|
|
148
|
-
out.push({
|
|
149
|
-
role: 'tool',
|
|
150
|
-
tool_call_id: b.tool_use_id,
|
|
151
|
-
content: typeof b.content === 'string'
|
|
152
|
-
? b.content
|
|
153
|
-
: JSON.stringify(b.content),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
else if (b.type === 'text') {
|
|
157
|
-
userTexts.push(b.text);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (userTexts.length) {
|
|
161
|
-
out.push({ role: 'user', content: userTexts.join('\n') });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
|
-
}
|
package/dist/agent/client.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic Messages API 客户端封装 —— HTTP/2 + SSE 流式传输。
|
|
3
|
-
*/
|
|
4
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
-
export class AnthropicClient {
|
|
6
|
-
client;
|
|
7
|
-
model;
|
|
8
|
-
maxTokens;
|
|
9
|
-
temperature;
|
|
10
|
-
constructor(opts) {
|
|
11
|
-
this.client = new Anthropic({
|
|
12
|
-
apiKey: opts.apiKey,
|
|
13
|
-
baseURL: opts.baseURL,
|
|
14
|
-
});
|
|
15
|
-
this.model = opts.model;
|
|
16
|
-
this.maxTokens = opts.maxTokens;
|
|
17
|
-
this.temperature = opts.temperature;
|
|
18
|
-
}
|
|
19
|
-
setModel(m) {
|
|
20
|
-
this.model = m;
|
|
21
|
-
}
|
|
22
|
-
getModel() {
|
|
23
|
-
return this.model;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* 发起一次带流式输出的对话请求。
|
|
27
|
-
* 返回完整的 assistant 回合(含文本与 tool_use 块)。
|
|
28
|
-
*/
|
|
29
|
-
async stream(system, messages, tools, callbacks, signal) {
|
|
30
|
-
const stream = this.client.messages.stream({
|
|
31
|
-
model: this.model,
|
|
32
|
-
max_tokens: this.maxTokens,
|
|
33
|
-
temperature: this.temperature,
|
|
34
|
-
system: [
|
|
35
|
-
{
|
|
36
|
-
type: 'text',
|
|
37
|
-
text: system,
|
|
38
|
-
// 系统提示缓存(对应「分层上下文压缩」中的缓存策略)
|
|
39
|
-
cache_control: { type: 'ephemeral' },
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
messages,
|
|
43
|
-
tools: tools.length ? tools : undefined,
|
|
44
|
-
}, { signal });
|
|
45
|
-
stream.on('text', (delta) => callbacks.onText?.(delta));
|
|
46
|
-
stream.on('contentBlock', (block) => {
|
|
47
|
-
if (block.type === 'tool_use') {
|
|
48
|
-
callbacks.onToolUseStart?.(block.name, block.id);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
// thinking(若启用扩展思考)
|
|
52
|
-
stream.on('thinking', (delta) => callbacks.onThinking?.(delta));
|
|
53
|
-
const final = await stream.finalMessage();
|
|
54
|
-
return {
|
|
55
|
-
content: final.content,
|
|
56
|
-
stopReason: final.stop_reason,
|
|
57
|
-
usage: {
|
|
58
|
-
input: final.usage.input_tokens,
|
|
59
|
-
output: final.usage.output_tokens,
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
package/dist/agent/factory.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Agent } from './loop.js';
|
|
2
|
-
import { buildSystemPrompt } from './prompt.js';
|
|
3
|
-
import { createDefaultRegistry, createReadOnlyRegistry, } from '../tools/index.js';
|
|
4
|
-
import { TodoStore } from '../tools/todo.js';
|
|
5
|
-
import { createTaskTool, SUBAGENT_SYSTEM, } from '../tools/task.js';
|
|
6
|
-
import { HookRunner } from '../hooks/hooks.js';
|
|
7
|
-
import { gatherContext } from '../context/project.js';
|
|
8
|
-
export function buildAgent(opts) {
|
|
9
|
-
const { client, cwd, permissions } = opts;
|
|
10
|
-
const todos = new TodoStore();
|
|
11
|
-
const hooks = new HookRunner(cwd);
|
|
12
|
-
const registry = createDefaultRegistry();
|
|
13
|
-
// 子代理运行器:用只读工具集跑一个独立 Agent,返回最终文本。
|
|
14
|
-
const runner = async (description, prompt, subCwd) => {
|
|
15
|
-
const subRegistry = createReadOnlyRegistry();
|
|
16
|
-
const subAgent = new Agent(client, subRegistry, permissions, SUBAGENT_SYSTEM, { cwd: subCwd }, hooks);
|
|
17
|
-
let text = '';
|
|
18
|
-
let isError = false;
|
|
19
|
-
try {
|
|
20
|
-
await subAgent.run(prompt, {
|
|
21
|
-
onAssistantText: (d) => {
|
|
22
|
-
text += d;
|
|
23
|
-
},
|
|
24
|
-
onTurnStart: () => { },
|
|
25
|
-
onTurnEnd: () => { },
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
isError = true;
|
|
30
|
-
text = `子代理出错:${e?.message ?? e}`;
|
|
31
|
-
}
|
|
32
|
-
return { text: text.trim(), isError };
|
|
33
|
-
};
|
|
34
|
-
registry.register(createTaskTool(runner));
|
|
35
|
-
const ctx = gatherContext(cwd);
|
|
36
|
-
const system = buildSystemPrompt(ctx);
|
|
37
|
-
const agent = new Agent(client, registry, permissions, system, { cwd, todos }, hooks);
|
|
38
|
-
return { agent, todos, hooks, client };
|
|
39
|
-
}
|
package/dist/agent/loop.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { toAnthropicTool } from '../tools/index.js';
|
|
2
|
-
const MAX_ITERATIONS = 50;
|
|
3
|
-
export class Agent {
|
|
4
|
-
client;
|
|
5
|
-
registry;
|
|
6
|
-
permissions;
|
|
7
|
-
system;
|
|
8
|
-
toolCtx;
|
|
9
|
-
hooks;
|
|
10
|
-
history = [];
|
|
11
|
-
totalUsage = { input: 0, output: 0 };
|
|
12
|
-
constructor(client, registry, permissions, system, toolCtx, hooks) {
|
|
13
|
-
this.client = client;
|
|
14
|
-
this.registry = registry;
|
|
15
|
-
this.permissions = permissions;
|
|
16
|
-
this.system = system;
|
|
17
|
-
this.toolCtx = toolCtx;
|
|
18
|
-
this.hooks = hooks;
|
|
19
|
-
}
|
|
20
|
-
setSystem(s) {
|
|
21
|
-
this.system = s;
|
|
22
|
-
}
|
|
23
|
-
/** 处理一条用户输入,驱动完整的代理循环直到回合结束。 */
|
|
24
|
-
async run(userInput, events, signal) {
|
|
25
|
-
this.history.push({ role: 'user', content: userInput });
|
|
26
|
-
await this.loop(events, signal);
|
|
27
|
-
}
|
|
28
|
-
/** 注入工具结果后继续循环(用于外部恢复,一般内部调用)。 */
|
|
29
|
-
async loop(events, signal) {
|
|
30
|
-
const tools = this.registry.list().map(toAnthropicTool);
|
|
31
|
-
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
32
|
-
if (signal?.aborted)
|
|
33
|
-
return;
|
|
34
|
-
events.onTurnStart?.();
|
|
35
|
-
let turn;
|
|
36
|
-
try {
|
|
37
|
-
turn = await this.client.stream(this.system, this.history, tools, {
|
|
38
|
-
onText: (d) => events.onAssistantText?.(d),
|
|
39
|
-
onThinking: (d) => events.onThinking?.(d),
|
|
40
|
-
}, signal);
|
|
41
|
-
}
|
|
42
|
-
catch (e) {
|
|
43
|
-
events.onTurnEnd?.();
|
|
44
|
-
if (signal?.aborted)
|
|
45
|
-
return;
|
|
46
|
-
throw e;
|
|
47
|
-
}
|
|
48
|
-
events.onTurnEnd?.();
|
|
49
|
-
this.totalUsage.input += turn.usage.input;
|
|
50
|
-
this.totalUsage.output += turn.usage.output;
|
|
51
|
-
events.onUsage?.(turn.usage);
|
|
52
|
-
// 记录 assistant 回合
|
|
53
|
-
this.history.push({ role: 'assistant', content: turn.content });
|
|
54
|
-
// 收集 tool_use 块
|
|
55
|
-
const toolUses = turn.content.filter((b) => b.type === 'tool_use');
|
|
56
|
-
if (toolUses.length === 0) {
|
|
57
|
-
// 没有工具调用 —— 回合结束
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
// 依次执行工具,收集 tool_result
|
|
61
|
-
const toolResults = [];
|
|
62
|
-
for (const tu of toolUses) {
|
|
63
|
-
if (signal?.aborted) {
|
|
64
|
-
toolResults.push({
|
|
65
|
-
type: 'tool_result',
|
|
66
|
-
tool_use_id: tu.id,
|
|
67
|
-
content: '用户中断了操作。',
|
|
68
|
-
is_error: true,
|
|
69
|
-
});
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const result = await this.executeTool(tu, events);
|
|
73
|
-
toolResults.push(result);
|
|
74
|
-
}
|
|
75
|
-
// 把工具结果作为新的 user 消息回传
|
|
76
|
-
this.history.push({ role: 'user', content: toolResults });
|
|
77
|
-
// 继续循环
|
|
78
|
-
}
|
|
79
|
-
// 达到迭代上限
|
|
80
|
-
this.history.push({
|
|
81
|
-
role: 'user',
|
|
82
|
-
content: '已达到工具调用上限,请总结当前进展。',
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
async executeTool(tu, events) {
|
|
86
|
-
const tool = this.registry.get(tu.name);
|
|
87
|
-
if (!tool) {
|
|
88
|
-
return {
|
|
89
|
-
type: 'tool_result',
|
|
90
|
-
tool_use_id: tu.id,
|
|
91
|
-
content: `未知工具:${tu.name}`,
|
|
92
|
-
is_error: true,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
const preview = tool.preview ? tool.preview(tu.input, this.toolCtx) : tu.name;
|
|
96
|
-
events.onToolStart?.(tu.name, tu.input, preview);
|
|
97
|
-
// 权限检查
|
|
98
|
-
if (tool.permission) {
|
|
99
|
-
const req = tool.permission(tu.input, this.toolCtx);
|
|
100
|
-
if (req) {
|
|
101
|
-
const decision = await this.permissions.check(req);
|
|
102
|
-
if (decision === 'deny') {
|
|
103
|
-
events.onToolDenied?.(tu.name, '用户拒绝了该操作');
|
|
104
|
-
return {
|
|
105
|
-
type: 'tool_result',
|
|
106
|
-
tool_use_id: tu.id,
|
|
107
|
-
content: '用户拒绝执行此操作。请不要重试相同操作,可以询问用户希望如何继续。',
|
|
108
|
-
is_error: true,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// PreToolUse 钩子:可阻止工具
|
|
114
|
-
if (this.hooks) {
|
|
115
|
-
const pre = this.hooks.runPre(tu.name, tu.input);
|
|
116
|
-
if (pre.blocked) {
|
|
117
|
-
events.onToolDenied?.(tu.name, `钩子拦截:${pre.reason ?? ''}`);
|
|
118
|
-
return {
|
|
119
|
-
type: 'tool_result',
|
|
120
|
-
tool_use_id: tu.id,
|
|
121
|
-
content: `操作被 PreToolUse 钩子阻止:${pre.reason ?? '(无理由)'}`,
|
|
122
|
-
is_error: true,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// 执行
|
|
127
|
-
try {
|
|
128
|
-
const res = await tool.run(tu.input, this.toolCtx);
|
|
129
|
-
this.hooks?.runPost(tu.name, tu.input, res.content);
|
|
130
|
-
events.onToolResult?.(tu.name, res.display ?? res.content.slice(0, 200), res.isError ?? false);
|
|
131
|
-
return {
|
|
132
|
-
type: 'tool_result',
|
|
133
|
-
tool_use_id: tu.id,
|
|
134
|
-
content: res.content,
|
|
135
|
-
is_error: res.isError ?? false,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
events.onToolResult?.(tu.name, `执行异常:${e.message}`, true);
|
|
140
|
-
return {
|
|
141
|
-
type: 'tool_result',
|
|
142
|
-
tool_use_id: tu.id,
|
|
143
|
-
content: `工具执行异常:${e.message}`,
|
|
144
|
-
is_error: true,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/** 清空对话历史(/clear)。 */
|
|
149
|
-
clear() {
|
|
150
|
-
this.history = [];
|
|
151
|
-
this.totalUsage = { input: 0, output: 0 };
|
|
152
|
-
}
|
|
153
|
-
/** 压缩历史(/compact)—— 简化实现:保留最近若干轮 + 摘要占位。 */
|
|
154
|
-
compactSummary(summary) {
|
|
155
|
-
this.history = [
|
|
156
|
-
{ role: 'user', content: `[此前对话的摘要]\n${summary}` },
|
|
157
|
-
{
|
|
158
|
-
role: 'assistant',
|
|
159
|
-
content: '好的,我已了解此前的上下文,请继续。',
|
|
160
|
-
},
|
|
161
|
-
];
|
|
162
|
-
}
|
|
163
|
-
}
|
package/dist/agent/pricing.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const PRICES = [
|
|
2
|
-
{ match: /opus/i, price: { input: 15, output: 75 } },
|
|
3
|
-
{ match: /sonnet/i, price: { input: 3, output: 15 } },
|
|
4
|
-
{ match: /haiku/i, price: { input: 0.8, output: 4 } },
|
|
5
|
-
// OpenAI 兼容后端(近似值,仅供参考)
|
|
6
|
-
{ match: /qwen-max/i, price: { input: 1.6, output: 6.4 } },
|
|
7
|
-
{ match: /qwen-plus/i, price: { input: 0.4, output: 1.2 } },
|
|
8
|
-
{ match: /qwen-turbo|qwen-flash/i, price: { input: 0.05, output: 0.2 } },
|
|
9
|
-
{ match: /qwen/i, price: { input: 0.4, output: 1.2 } },
|
|
10
|
-
{ match: /deepseek/i, price: { input: 0.27, output: 1.1 } },
|
|
11
|
-
{ match: /glm/i, price: { input: 0.5, output: 1.5 } },
|
|
12
|
-
];
|
|
13
|
-
const FALLBACK = { input: 3, output: 15 };
|
|
14
|
-
export function priceFor(model) {
|
|
15
|
-
return PRICES.find((p) => p.match.test(model))?.price ?? FALLBACK;
|
|
16
|
-
}
|
|
17
|
-
/** 计算给定用量的美元成本。 */
|
|
18
|
-
export function estimateCost(model, usage) {
|
|
19
|
-
const p = priceFor(model);
|
|
20
|
-
const input = (usage.input / 1_000_000) * p.input;
|
|
21
|
-
const output = (usage.output / 1_000_000) * p.output;
|
|
22
|
-
return { input, output, total: input + output };
|
|
23
|
-
}
|
|
24
|
-
export function formatUsd(n) {
|
|
25
|
-
if (n < 0.01)
|
|
26
|
-
return `$${n.toFixed(4)}`;
|
|
27
|
-
return `$${n.toFixed(4)}`;
|
|
28
|
-
}
|
package/dist/agent/prompt.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 系统提示词 —— 定义 Agent 的人格、工作流与约束。
|
|
3
|
-
* 复刻 Claude Code 「AI 软件工程师」的代理型行为。
|
|
4
|
-
*/
|
|
5
|
-
import { renderEnvironment } from '../context/project.js';
|
|
6
|
-
const BASE_PROMPT = `你是 CCLI,一个运行在用户终端中的 AI 软件工程师,是 Claude Code 的开源复刻。
|
|
7
|
-
你不仅能编写代码,更能理解整个代码库的架构与逻辑,并自主完成多步骤的开发任务。
|
|
8
|
-
|
|
9
|
-
# 工作方式(代理循环 Agentic Loop)
|
|
10
|
-
你的每一次响应都遵循「感知 → 推理 → 行动 → 验证」的循环:
|
|
11
|
-
1. 感知:通过工具读取文件、运行命令、检索代码来收集事实,而不是猜测。
|
|
12
|
-
2. 推理:基于事实规划要做的步骤。
|
|
13
|
-
3. 行动:调用工具执行——读写文件、运行命令、修改代码。
|
|
14
|
-
4. 验证:运行测试 / 类型检查 / 构建命令来确认改动正确,必要时修复。
|
|
15
|
-
循环持续进行,直到用户的目标真正完成。
|
|
16
|
-
|
|
17
|
-
# 行为准则
|
|
18
|
-
- 务实、直接、简洁。这是 CLI 终端环境,避免冗长的寒暄和不必要的总结。
|
|
19
|
-
- 默认用中文与用户交流(除非用户使用其他语言)。
|
|
20
|
-
- 动手之前先用工具了解现状:阅读相关文件、搜索相关代码,理解现有约定(命名、风格、依赖)后再修改。
|
|
21
|
-
- 修改代码时严格匹配周边代码的风格与缩进。不要主动添加未被要求的注释。
|
|
22
|
-
- 不要假设某个库存在或可用——先检查(package.json、import 语句、已有用法)。
|
|
23
|
-
- 一次只做用户要求的事,不擅自扩大范围。完成后简要说明做了什么。
|
|
24
|
-
- 编辑文件后,主动运行可用的验证手段(测试、lint、typecheck、build),并修复发现的问题。
|
|
25
|
-
- 涉及破坏性操作(删除文件、git push、覆盖重要文件)时保持谨慎,遵循权限机制。
|
|
26
|
-
|
|
27
|
-
# 工具使用
|
|
28
|
-
- 优先使用专用工具(read_file / edit_file / write_file / grep / glob / list_dir)而非 shell 等价命令。
|
|
29
|
-
- 需要并行且互相独立的工具调用时,可在一次响应中同时发起。
|
|
30
|
-
- bash 工具用于运行测试、构建、git 等命令;不要用它来读写文件(用专用工具更安全)。
|
|
31
|
-
- 不要在没有读取文件内容的情况下编辑它。edit_file 要求 old_text 精确匹配(含缩进)。
|
|
32
|
-
|
|
33
|
-
# 输出格式
|
|
34
|
-
- 输出会以 GitHub 风格 Markdown 在终端渲染。可使用代码块、列表、粗体。
|
|
35
|
-
- 引用代码位置时使用 \`文件路径:行号\` 格式,便于用户跳转。
|
|
36
|
-
- 不要用表情包堆砌,保持专业。
|
|
37
|
-
|
|
38
|
-
# 安全
|
|
39
|
-
- 协助授权范围内的安全测试、防御性安全与教学场景。
|
|
40
|
-
- 拒绝明显恶意的破坏性请求。`;
|
|
41
|
-
export function buildSystemPrompt(ctx) {
|
|
42
|
-
return `${BASE_PROMPT}\n\n# 当前环境\n${renderEnvironment(ctx)}`;
|
|
43
|
-
}
|