@gird/ccli 1.0.2 → 2.0.0

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 (45) hide show
  1. package/dist/args.js +35 -0
  2. package/dist/args.js.map +1 -0
  3. package/dist/config.js +120 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/env.js +39 -0
  6. package/dist/env.js.map +1 -0
  7. package/dist/index.js +36 -226
  8. package/dist/index.js.map +1 -0
  9. package/dist/repl.js +59 -0
  10. package/dist/repl.js.map +1 -0
  11. package/dist/version.js +13 -0
  12. package/dist/version.js.map +1 -0
  13. package/package.json +4 -14
  14. package/.env.example +0 -23
  15. package/CCLI.md +0 -15
  16. package/dist/agent/client-openai.js +0 -166
  17. package/dist/agent/client.js +0 -63
  18. package/dist/agent/factory.js +0 -39
  19. package/dist/agent/loop.js +0 -163
  20. package/dist/agent/pricing.js +0 -28
  21. package/dist/agent/prompt.js +0 -43
  22. package/dist/agent/provider.js +0 -18
  23. package/dist/cli/headless.js +0 -50
  24. package/dist/cli/mentions.js +0 -60
  25. package/dist/cli/session.js +0 -432
  26. package/dist/commands/custom.js +0 -46
  27. package/dist/commands/slash.js +0 -148
  28. package/dist/config/config.js +0 -128
  29. package/dist/config/dotenv.js +0 -59
  30. package/dist/context/project.js +0 -155
  31. package/dist/hooks/hooks.js +0 -82
  32. package/dist/permissions/permissions.js +0 -87
  33. package/dist/session/store.js +0 -81
  34. package/dist/tools/bash.js +0 -97
  35. package/dist/tools/filesystem.js +0 -215
  36. package/dist/tools/index.js +0 -45
  37. package/dist/tools/search.js +0 -0
  38. package/dist/tools/task.js +0 -35
  39. package/dist/tools/todo.js +0 -68
  40. package/dist/tools/types.js +0 -8
  41. package/dist/tools/web.js +0 -108
  42. package/dist/ui/diff.js +0 -105
  43. package/dist/ui/markdown.js +0 -137
  44. package/dist/ui/spinner.js +0 -64
  45. package/dist/ui/theme.js +0 -106
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -1,18 +0,0 @@
1
- import { AnthropicClient } from './client.js';
2
- import { OpenAICompatClient } from './client-openai.js';
3
- export function createClient(config) {
4
- const common = {
5
- apiKey: config.apiKey,
6
- model: config.model,
7
- maxTokens: config.data.maxTokens,
8
- temperature: config.data.temperature,
9
- };
10
- if (config.provider === 'openai') {
11
- return new OpenAICompatClient({
12
- ...common,
13
- baseURL: config.data.baseURL ||
14
- 'https://dashscope.aliyuncs.com/compatible-mode/v1',
15
- });
16
- }
17
- return new AnthropicClient({ ...common, baseURL: config.data.baseURL });
18
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * 一次性(非交互)模式 —— 对应 `ccli -p "..."`。
3
- * 适合脚本与管道:自动批准只读工具,写操作默认按配置(可 -y 全自动)。
4
- */
5
- import { stdout } from 'node:process';
6
- import { createClient } from '../agent/provider.js';
7
- import { buildAgent } from '../agent/factory.js';
8
- import { PermissionManager, } from '../permissions/permissions.js';
9
- import { c, brand } from '../ui/theme.js';
10
- import { renderTodos } from '../tools/todo.js';
11
- import { expandMentions } from './mentions.js';
12
- export async function runHeadless(config, cwd, prompt) {
13
- const client = createClient(config);
14
- // 非交互:无法询问用户。未开启 autoApprove 时,对写操作拒绝并提示。
15
- const asker = {
16
- async ask(req) {
17
- if (config.data.autoApprove)
18
- return 'allow';
19
- stdout.write(c.dim(` [跳过未授权操作:${req.description}(非交互模式,加 -y 可自动批准)]\n`));
20
- return 'deny';
21
- },
22
- };
23
- const permissions = new PermissionManager(config, asker);
24
- const { agent, todos } = buildAgent({ client, cwd, permissions });
25
- const isTTY = stdout.isTTY ?? false;
26
- if (isTTY) {
27
- todos.onChange((items) => stdout.write('\n' + renderTodos(items) + '\n'));
28
- }
29
- const events = {
30
- onTurnStart: () => { },
31
- onTurnEnd: () => { },
32
- onAssistantText: (d) => stdout.write(d),
33
- onToolStart: (_t, _i, preview) => {
34
- if (isTTY)
35
- stdout.write('\n' + brand.accent('⚒ ') + preview + '\n');
36
- },
37
- onToolResult: (_t, display, isError) => {
38
- if (isTTY)
39
- stdout.write(' ' + display + '\n');
40
- },
41
- };
42
- try {
43
- await agent.run(expandMentions(prompt, cwd), events);
44
- stdout.write('\n');
45
- }
46
- catch (e) {
47
- stdout.write(brand.err('错误:') + (e?.message ?? String(e)) + '\n');
48
- process.exit(1);
49
- }
50
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * @文件引用展开 —— 把用户输入中的 @path 自动并入文件内容。
3
- */
4
- import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
5
- import { isAbsolute, resolve } from 'node:path';
6
- const MAX_PER_FILE = 4000;
7
- /**
8
- * 扫描输入中的 @path,对存在的文件/目录追加内容块。
9
- * 返回展开后的消息文本(原文 + 附加块)。
10
- */
11
- export function expandMentions(input, cwd) {
12
- const re = /@([\w./_-]+)/g;
13
- const seen = new Set();
14
- const blocks = [];
15
- let match;
16
- while ((match = re.exec(input))) {
17
- const p = match[1];
18
- if (seen.has(p))
19
- continue;
20
- seen.add(p);
21
- const full = isAbsolute(p) ? p : resolve(cwd, p);
22
- if (!existsSync(full))
23
- continue; // 不存在则当普通文字
24
- try {
25
- const st = statSync(full);
26
- if (st.isDirectory()) {
27
- const list = readdirSync(full)
28
- .filter((e) => e !== 'node_modules' && e !== '.git')
29
- .slice(0, 100)
30
- .join('\n');
31
- blocks.push(`<directory path="${p}">\n${list}\n</directory>`);
32
- }
33
- else {
34
- let text = readFileSync(full, 'utf8');
35
- if (text.length > MAX_PER_FILE) {
36
- text = text.slice(0, MAX_PER_FILE) + '\n…(已截断)';
37
- }
38
- blocks.push(`<file path="${p}">\n${text}\n</file>`);
39
- }
40
- }
41
- catch {
42
- /* 读失败则忽略 */
43
- }
44
- }
45
- if (!blocks.length)
46
- return input;
47
- return `${input}\n\n${blocks.join('\n\n')}`;
48
- }
49
- /** 返回输入中实际命中的引用路径(用于 UI 提示)。 */
50
- export function foundMentions(input, cwd) {
51
- const re = /@([\w./_-]+)/g;
52
- const out = [];
53
- let m;
54
- while ((m = re.exec(input))) {
55
- const full = isAbsolute(m[1]) ? m[1] : resolve(cwd, m[1]);
56
- if (existsSync(full))
57
- out.push(m[1]);
58
- }
59
- return out;
60
- }