@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/session/store.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话持久化 —— 把对话历史落盘,支持 --continue / --resume。
|
|
3
|
-
* 存储路径:~/.ccli/sessions/<cwd 哈希>/<sessionId>.json
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
9
|
-
function sessionsRoot() {
|
|
10
|
-
return join(homedir(), '.ccli', 'sessions');
|
|
11
|
-
}
|
|
12
|
-
function cwdDir(cwd) {
|
|
13
|
-
const hash = createHash('sha1').update(cwd).digest('hex').slice(0, 12);
|
|
14
|
-
return join(sessionsRoot(), hash);
|
|
15
|
-
}
|
|
16
|
-
export class SessionStore {
|
|
17
|
-
id;
|
|
18
|
-
cwd;
|
|
19
|
-
dir;
|
|
20
|
-
createdAt;
|
|
21
|
-
constructor(cwd, id) {
|
|
22
|
-
this.cwd = cwd;
|
|
23
|
-
this.id = id ?? randomUUID().slice(0, 8);
|
|
24
|
-
this.dir = cwdDir(cwd);
|
|
25
|
-
this.createdAt = new Date().toISOString();
|
|
26
|
-
}
|
|
27
|
-
path() {
|
|
28
|
-
return join(this.dir, `${this.id}.json`);
|
|
29
|
-
}
|
|
30
|
-
save(model, messages) {
|
|
31
|
-
try {
|
|
32
|
-
if (!existsSync(this.dir))
|
|
33
|
-
mkdirSync(this.dir, { recursive: true });
|
|
34
|
-
const data = {
|
|
35
|
-
id: this.id,
|
|
36
|
-
cwd: this.cwd,
|
|
37
|
-
model,
|
|
38
|
-
createdAt: this.createdAt,
|
|
39
|
-
updatedAt: new Date().toISOString(),
|
|
40
|
-
messages,
|
|
41
|
-
};
|
|
42
|
-
writeFileSync(this.path(), JSON.stringify(data), 'utf8');
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
/* 落盘失败不影响主流程 */
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/** 列出该 cwd 下的会话,按更新时间倒序。 */
|
|
49
|
-
static list(cwd) {
|
|
50
|
-
const dir = cwdDir(cwd);
|
|
51
|
-
if (!existsSync(dir))
|
|
52
|
-
return [];
|
|
53
|
-
const out = [];
|
|
54
|
-
for (const f of readdirSync(dir)) {
|
|
55
|
-
if (!f.endsWith('.json'))
|
|
56
|
-
continue;
|
|
57
|
-
try {
|
|
58
|
-
out.push(JSON.parse(readFileSync(join(dir, f), 'utf8')));
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
/* skip */
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
65
|
-
}
|
|
66
|
-
/** 加载指定会话(id 可省略 → 最新一条)。 */
|
|
67
|
-
static load(cwd, id) {
|
|
68
|
-
const all = SessionStore.list(cwd);
|
|
69
|
-
if (!all.length)
|
|
70
|
-
return null;
|
|
71
|
-
if (!id)
|
|
72
|
-
return all[0];
|
|
73
|
-
return all.find((s) => s.id === id) ?? null;
|
|
74
|
-
}
|
|
75
|
-
/** 用已有 SessionData 构造一个续写的 store(保留原 id 与创建时间)。 */
|
|
76
|
-
static resume(data) {
|
|
77
|
-
const s = new SessionStore(data.cwd, data.id);
|
|
78
|
-
s.createdAt = data.createdAt;
|
|
79
|
-
return s;
|
|
80
|
-
}
|
|
81
|
-
}
|
package/dist/tools/bash.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bash 工具 —— 通过 child_process 执行 shell 命令。
|
|
3
|
-
* 用于运行测试、构建、git 等命令。受权限机制约束,高危命令需二次确认。
|
|
4
|
-
*/
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { c, brand } from '../ui/theme.js';
|
|
7
|
-
import { commandPrefix, isDangerousCommand, } from '../permissions/permissions.js';
|
|
8
|
-
const DEFAULT_TIMEOUT = 120_000;
|
|
9
|
-
const MAX_OUTPUT = 30_000;
|
|
10
|
-
export const bashTool = {
|
|
11
|
-
name: 'bash',
|
|
12
|
-
description: '在持久化的工作目录中执行 shell 命令(zsh/bash)。用于运行测试、构建、git、安装依赖等。不要用它来读写文件——请用 read_file/write_file/edit_file。避免交互式命令。',
|
|
13
|
-
inputSchema: {
|
|
14
|
-
type: 'object',
|
|
15
|
-
properties: {
|
|
16
|
-
command: { type: 'string', description: '要执行的 shell 命令' },
|
|
17
|
-
timeout: {
|
|
18
|
-
type: 'number',
|
|
19
|
-
description: '超时毫秒数(默认 120000,最大 600000)',
|
|
20
|
-
},
|
|
21
|
-
description: {
|
|
22
|
-
type: 'string',
|
|
23
|
-
description: '对该命令用途的一句话说明(5-10 字)',
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
required: ['command'],
|
|
27
|
-
},
|
|
28
|
-
permission(input) {
|
|
29
|
-
const cmd = String(input.command || '');
|
|
30
|
-
const prefix = commandPrefix(cmd);
|
|
31
|
-
const dangerous = isDangerousCommand(cmd);
|
|
32
|
-
return {
|
|
33
|
-
tool: 'bash',
|
|
34
|
-
rule: `Bash(${prefix})`,
|
|
35
|
-
description: input.description
|
|
36
|
-
? `${input.description}:${cmd}`
|
|
37
|
-
: `执行:${cmd}`,
|
|
38
|
-
dangerous,
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
preview(input) {
|
|
42
|
-
return `${brand.accent('$')} ${input.command}`;
|
|
43
|
-
},
|
|
44
|
-
async run(input, ctx) {
|
|
45
|
-
const cmd = String(input.command || '');
|
|
46
|
-
const timeout = Math.min(input.timeout ?? DEFAULT_TIMEOUT, 600_000);
|
|
47
|
-
return new Promise((resolveP) => {
|
|
48
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
49
|
-
const child = spawn(shell, ['-c', cmd], {
|
|
50
|
-
cwd: ctx.cwd,
|
|
51
|
-
env: process.env,
|
|
52
|
-
});
|
|
53
|
-
let stdout = '';
|
|
54
|
-
let stderr = '';
|
|
55
|
-
let killed = false;
|
|
56
|
-
const timer = setTimeout(() => {
|
|
57
|
-
killed = true;
|
|
58
|
-
child.kill('SIGKILL');
|
|
59
|
-
}, timeout);
|
|
60
|
-
child.stdout.on('data', (d) => {
|
|
61
|
-
stdout += d.toString();
|
|
62
|
-
if (stdout.length > MAX_OUTPUT)
|
|
63
|
-
stdout = stdout.slice(0, MAX_OUTPUT);
|
|
64
|
-
});
|
|
65
|
-
child.stderr.on('data', (d) => {
|
|
66
|
-
stderr += d.toString();
|
|
67
|
-
if (stderr.length > MAX_OUTPUT)
|
|
68
|
-
stderr = stderr.slice(0, MAX_OUTPUT);
|
|
69
|
-
});
|
|
70
|
-
child.on('error', (e) => {
|
|
71
|
-
clearTimeout(timer);
|
|
72
|
-
resolveP({ content: `命令启动失败:${e.message}`, isError: true });
|
|
73
|
-
});
|
|
74
|
-
child.on('close', (code) => {
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
const combined = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
77
|
-
if (killed) {
|
|
78
|
-
resolveP({
|
|
79
|
-
content: `命令超时(${timeout}ms)被终止。\n部分输出:\n${combined.slice(0, 4000)}`,
|
|
80
|
-
isError: true,
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const exitNote = code === 0 ? '' : `\n[退出码 ${code}]`;
|
|
85
|
-
const body = combined || '(无输出)';
|
|
86
|
-
const truncated = body.length >= MAX_OUTPUT ? '\n…(输出已截断)' : '';
|
|
87
|
-
resolveP({
|
|
88
|
-
content: `${body}${truncated}${exitNote}`,
|
|
89
|
-
isError: code !== 0,
|
|
90
|
-
display: code === 0
|
|
91
|
-
? `${brand.ok('✓')} ${c.dim(cmd)}`
|
|
92
|
-
: `${brand.err('✗')} ${c.dim(cmd)} ${c.dim(`(退出码 ${code})`)}`,
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
},
|
|
97
|
-
};
|
package/dist/tools/filesystem.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 文件系统工具:read_file / write_file / edit_file / list_dir。
|
|
3
|
-
* 复刻 Claude Code 通过 fs 模块操作文件的本地能力。
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, } from 'node:fs';
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
7
|
-
import { c, brand } from '../ui/theme.js';
|
|
8
|
-
import { renderDiff, diffStat } from '../ui/diff.js';
|
|
9
|
-
function resolvePath(p, ctx) {
|
|
10
|
-
return isAbsolute(p) ? p : resolve(ctx.cwd, p);
|
|
11
|
-
}
|
|
12
|
-
function rel(p, ctx) {
|
|
13
|
-
const r = relative(ctx.cwd, p);
|
|
14
|
-
return r.startsWith('..') ? p : r || '.';
|
|
15
|
-
}
|
|
16
|
-
export const readFileTool = {
|
|
17
|
-
name: 'read_file',
|
|
18
|
-
description: '读取文件内容。返回带行号的内容。可指定 offset/limit 读取大文件的部分。优先用此工具而非 cat/head。',
|
|
19
|
-
inputSchema: {
|
|
20
|
-
type: 'object',
|
|
21
|
-
properties: {
|
|
22
|
-
path: { type: 'string', description: '文件路径(相对或绝对)' },
|
|
23
|
-
offset: { type: 'number', description: '起始行号(从 1 开始,可选)' },
|
|
24
|
-
limit: { type: 'number', description: '读取行数(可选,默认 2000)' },
|
|
25
|
-
},
|
|
26
|
-
required: ['path'],
|
|
27
|
-
},
|
|
28
|
-
preview(input, ctx) {
|
|
29
|
-
return `读取 ${brand.accent(rel(resolvePath(input.path, ctx), ctx))}`;
|
|
30
|
-
},
|
|
31
|
-
async run(input, ctx) {
|
|
32
|
-
const full = resolvePath(input.path, ctx);
|
|
33
|
-
if (!existsSync(full)) {
|
|
34
|
-
return { content: `错误:文件不存在 ${input.path}`, isError: true };
|
|
35
|
-
}
|
|
36
|
-
const st = statSync(full);
|
|
37
|
-
if (st.isDirectory()) {
|
|
38
|
-
return { content: `错误:${input.path} 是目录,请用 list_dir`, isError: true };
|
|
39
|
-
}
|
|
40
|
-
let text;
|
|
41
|
-
try {
|
|
42
|
-
text = readFileSync(full, 'utf8');
|
|
43
|
-
}
|
|
44
|
-
catch (e) {
|
|
45
|
-
return { content: `读取失败:${e.message}`, isError: true };
|
|
46
|
-
}
|
|
47
|
-
const lines = text.split('\n');
|
|
48
|
-
const offset = Math.max(1, input.offset ?? 1);
|
|
49
|
-
const limit = input.limit ?? 2000;
|
|
50
|
-
const slice = lines.slice(offset - 1, offset - 1 + limit);
|
|
51
|
-
const numbered = slice
|
|
52
|
-
.map((l, i) => `${String(offset + i).padStart(6)}\t${l}`)
|
|
53
|
-
.join('\n');
|
|
54
|
-
const more = lines.length > offset - 1 + limit
|
|
55
|
-
? `\n…(共 ${lines.length} 行,已显示 ${offset}-${offset + slice.length - 1})`
|
|
56
|
-
: '';
|
|
57
|
-
return {
|
|
58
|
-
content: numbered + more || '(空文件)',
|
|
59
|
-
display: `${c.dim('读取')} ${rel(full, ctx)} ${c.dim(`(${slice.length} 行)`)}`,
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
export const writeFileTool = {
|
|
64
|
-
name: 'write_file',
|
|
65
|
-
description: '写入文件(覆盖已存在的内容,自动创建父目录)。用于创建新文件或完整重写文件。部分修改请用 edit_file。',
|
|
66
|
-
inputSchema: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: {
|
|
69
|
-
path: { type: 'string', description: '文件路径' },
|
|
70
|
-
content: { type: 'string', description: '完整文件内容' },
|
|
71
|
-
},
|
|
72
|
-
required: ['path', 'content'],
|
|
73
|
-
},
|
|
74
|
-
permission(input, ctx) {
|
|
75
|
-
const r = rel(resolvePath(input.path, ctx), ctx);
|
|
76
|
-
return {
|
|
77
|
-
tool: 'write_file',
|
|
78
|
-
rule: `Write(${r})`,
|
|
79
|
-
description: `写入文件 ${r}`,
|
|
80
|
-
dangerous: false,
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
preview(input, ctx) {
|
|
84
|
-
const r = rel(resolvePath(input.path, ctx), ctx);
|
|
85
|
-
const exists = existsSync(resolvePath(input.path, ctx));
|
|
86
|
-
const lines = String(input.content ?? '').split('\n').length;
|
|
87
|
-
return `${exists ? '覆盖' : '创建'} ${brand.accent(r)} ${c.dim(`(${lines} 行)`)}`;
|
|
88
|
-
},
|
|
89
|
-
async run(input, ctx) {
|
|
90
|
-
const full = resolvePath(input.path, ctx);
|
|
91
|
-
const existed = existsSync(full);
|
|
92
|
-
const before = existed ? readFileSync(full, 'utf8') : '';
|
|
93
|
-
try {
|
|
94
|
-
const dir = dirname(full);
|
|
95
|
-
if (!existsSync(dir))
|
|
96
|
-
mkdirSync(dir, { recursive: true });
|
|
97
|
-
writeFileSync(full, input.content, 'utf8');
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
return { content: `写入失败:${e.message}`, isError: true };
|
|
101
|
-
}
|
|
102
|
-
const lines = String(input.content).split('\n').length;
|
|
103
|
-
let display = `${existed ? c.yellow('覆盖') : brand.ok('创建')} ${rel(full, ctx)} ${c.dim(`(${lines} 行)`)}`;
|
|
104
|
-
if (existed && before !== input.content) {
|
|
105
|
-
const stat = diffStat(before, input.content);
|
|
106
|
-
display += ` ${c.dim(`(+${stat.add} -${stat.del})`)}\n${renderDiff(before, input.content)}`;
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
content: `${existed ? '已覆盖' : '已创建'} ${rel(full, ctx)}(${lines} 行)`,
|
|
110
|
-
display,
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
export const editFileTool = {
|
|
115
|
-
name: 'edit_file',
|
|
116
|
-
description: '对文件做精确替换。old_text 必须在文件中唯一且精确匹配(含缩进/空白)。用于局部修改。如需替换全部出现,设 replace_all=true。',
|
|
117
|
-
inputSchema: {
|
|
118
|
-
type: 'object',
|
|
119
|
-
properties: {
|
|
120
|
-
path: { type: 'string', description: '文件路径' },
|
|
121
|
-
old_text: { type: 'string', description: '要替换的原文(精确匹配)' },
|
|
122
|
-
new_text: { type: 'string', description: '替换后的新文本' },
|
|
123
|
-
replace_all: {
|
|
124
|
-
type: 'boolean',
|
|
125
|
-
description: '是否替换所有出现(默认 false)',
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
required: ['path', 'old_text', 'new_text'],
|
|
129
|
-
},
|
|
130
|
-
permission(input, ctx) {
|
|
131
|
-
const r = rel(resolvePath(input.path, ctx), ctx);
|
|
132
|
-
return {
|
|
133
|
-
tool: 'edit_file',
|
|
134
|
-
rule: `Edit(${r})`,
|
|
135
|
-
description: `编辑文件 ${r}`,
|
|
136
|
-
dangerous: false,
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
preview(input, ctx) {
|
|
140
|
-
const r = rel(resolvePath(input.path, ctx), ctx);
|
|
141
|
-
return `编辑 ${brand.accent(r)}`;
|
|
142
|
-
},
|
|
143
|
-
async run(input, ctx) {
|
|
144
|
-
const full = resolvePath(input.path, ctx);
|
|
145
|
-
if (!existsSync(full)) {
|
|
146
|
-
return { content: `错误:文件不存在 ${input.path}`, isError: true };
|
|
147
|
-
}
|
|
148
|
-
let text = readFileSync(full, 'utf8');
|
|
149
|
-
const before = text;
|
|
150
|
-
const { old_text, new_text, replace_all } = input;
|
|
151
|
-
if (old_text === new_text) {
|
|
152
|
-
return { content: '错误:old_text 与 new_text 相同', isError: true };
|
|
153
|
-
}
|
|
154
|
-
const count = text.split(old_text).length - 1;
|
|
155
|
-
if (count === 0) {
|
|
156
|
-
return {
|
|
157
|
-
content: `错误:在文件中未找到 old_text。请先 read_file 确认精确内容(含缩进)。`,
|
|
158
|
-
isError: true,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
if (count > 1 && !replace_all) {
|
|
162
|
-
return {
|
|
163
|
-
content: `错误:old_text 出现了 ${count} 次,不唯一。请提供更多上下文,或设 replace_all=true。`,
|
|
164
|
-
isError: true,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
text = replace_all
|
|
168
|
-
? text.split(old_text).join(new_text)
|
|
169
|
-
: text.replace(old_text, new_text);
|
|
170
|
-
writeFileSync(full, text, 'utf8');
|
|
171
|
-
const stat = diffStat(before, text);
|
|
172
|
-
const diff = renderDiff(before, text);
|
|
173
|
-
return {
|
|
174
|
-
content: `已编辑 ${rel(full, ctx)}(替换 ${replace_all ? count : 1} 处)`,
|
|
175
|
-
display: `${brand.ok('编辑')} ${rel(full, ctx)} ${c.dim(`(+${stat.add} -${stat.del})`)}\n${diff}`,
|
|
176
|
-
};
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
export const listDirTool = {
|
|
180
|
-
name: 'list_dir',
|
|
181
|
-
description: '列出目录内容(文件与子目录)。优先用此工具而非 ls。',
|
|
182
|
-
inputSchema: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
properties: {
|
|
185
|
-
path: { type: 'string', description: '目录路径(默认当前工作目录)' },
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
preview(input, ctx) {
|
|
189
|
-
return `列出 ${brand.accent(input.path || '.')}`;
|
|
190
|
-
},
|
|
191
|
-
async run(input, ctx) {
|
|
192
|
-
const full = resolvePath(input.path || '.', ctx);
|
|
193
|
-
if (!existsSync(full)) {
|
|
194
|
-
return { content: `错误:路径不存在 ${input.path}`, isError: true };
|
|
195
|
-
}
|
|
196
|
-
let entries;
|
|
197
|
-
try {
|
|
198
|
-
entries = readdirSync(full);
|
|
199
|
-
}
|
|
200
|
-
catch (e) {
|
|
201
|
-
return { content: `列目录失败:${e.message}`, isError: true };
|
|
202
|
-
}
|
|
203
|
-
const rows = entries
|
|
204
|
-
.filter((e) => e !== 'node_modules' && e !== '.git')
|
|
205
|
-
.sort()
|
|
206
|
-
.map((e) => {
|
|
207
|
-
const st = statSync(join(full, e));
|
|
208
|
-
return st.isDirectory() ? `${e}/` : e;
|
|
209
|
-
});
|
|
210
|
-
return {
|
|
211
|
-
content: rows.length ? rows.join('\n') : '(空目录)',
|
|
212
|
-
display: `${c.dim('列目录')} ${rel(full, ctx)} ${c.dim(`(${rows.length} 项)`)}`,
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
};
|
package/dist/tools/index.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { readFileTool, writeFileTool, editFileTool, listDirTool, } from './filesystem.js';
|
|
2
|
-
import { grepTool, globTool } from './search.js';
|
|
3
|
-
import { bashTool } from './bash.js';
|
|
4
|
-
import { todoWriteTool } from './todo.js';
|
|
5
|
-
import { webFetchTool } from './web.js';
|
|
6
|
-
export class ToolRegistry {
|
|
7
|
-
tools = new Map();
|
|
8
|
-
register(tool) {
|
|
9
|
-
this.tools.set(tool.name, tool);
|
|
10
|
-
}
|
|
11
|
-
get(name) {
|
|
12
|
-
return this.tools.get(name);
|
|
13
|
-
}
|
|
14
|
-
list() {
|
|
15
|
-
return [...this.tools.values()];
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/** 完整工具集(主 Agent 使用)。 */
|
|
19
|
-
export function createDefaultRegistry() {
|
|
20
|
-
const reg = new ToolRegistry();
|
|
21
|
-
// 感知类
|
|
22
|
-
reg.register(readFileTool);
|
|
23
|
-
reg.register(listDirTool);
|
|
24
|
-
reg.register(grepTool);
|
|
25
|
-
reg.register(globTool);
|
|
26
|
-
reg.register(webFetchTool);
|
|
27
|
-
// 行动类
|
|
28
|
-
reg.register(writeFileTool);
|
|
29
|
-
reg.register(editFileTool);
|
|
30
|
-
reg.register(bashTool);
|
|
31
|
-
// 协调类
|
|
32
|
-
reg.register(todoWriteTool);
|
|
33
|
-
return reg;
|
|
34
|
-
}
|
|
35
|
-
/** 只读工具集(子代理 / Task 使用,防止破坏与递归)。 */
|
|
36
|
-
export function createReadOnlyRegistry() {
|
|
37
|
-
const reg = new ToolRegistry();
|
|
38
|
-
reg.register(readFileTool);
|
|
39
|
-
reg.register(listDirTool);
|
|
40
|
-
reg.register(grepTool);
|
|
41
|
-
reg.register(globTool);
|
|
42
|
-
reg.register(webFetchTool);
|
|
43
|
-
return reg;
|
|
44
|
-
}
|
|
45
|
-
export * from './types.js';
|
package/dist/tools/search.js
DELETED
|
Binary file
|
package/dist/tools/task.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { brand, c } from '../ui/theme.js';
|
|
2
|
-
export const SUBAGENT_SYSTEM = `你是一个被主 Agent 派生的调研子代理。
|
|
3
|
-
你只拥有只读工具(read_file / list_dir / grep / glob / web_fetch)。
|
|
4
|
-
请自主、彻底地完成被交付的调研任务,然后用简洁的中文输出**最终结论**:
|
|
5
|
-
- 直接给出发现的事实、相关文件路径(用 路径:行号)与结论。
|
|
6
|
-
- 不要返回中间过程的啰嗦叙述,主 Agent 只需要你的结论。`;
|
|
7
|
-
export function createTaskTool(runner) {
|
|
8
|
-
return {
|
|
9
|
-
name: 'task',
|
|
10
|
-
description: '派生一个只读子代理来完成聚焦的调研类任务(如「在整个代码库中找出所有使用 X 的位置并总结」)。适合需要多步搜索/阅读但你不想让中间过程占满上下文的场景。子代理只能读,不能修改文件或执行命令。返回子代理的最终结论。',
|
|
11
|
-
inputSchema: {
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {
|
|
14
|
-
description: { type: 'string', description: '任务简短标题(3-6 字)' },
|
|
15
|
-
prompt: {
|
|
16
|
-
type: 'string',
|
|
17
|
-
description: '交给子代理的完整、自洽的任务说明',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
required: ['description', 'prompt'],
|
|
21
|
-
},
|
|
22
|
-
preview(input) {
|
|
23
|
-
return `${brand.accent('task')}: ${input.description}`;
|
|
24
|
-
},
|
|
25
|
-
async run(input, ctx) {
|
|
26
|
-
const { description, prompt } = input;
|
|
27
|
-
const res = await runner(String(description ?? '子任务'), String(prompt ?? ''), ctx.cwd);
|
|
28
|
-
return {
|
|
29
|
-
content: res.text || '(子代理无输出)',
|
|
30
|
-
isError: res.isError,
|
|
31
|
-
display: `${brand.ok('✓')} 子代理「${description}」完成 ${c.dim(`(${res.text.length} 字结论)`)}`,
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
package/dist/tools/todo.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { c, brand } from '../ui/theme.js';
|
|
2
|
-
/** 会话级 todo 状态(整表覆盖式更新)。 */
|
|
3
|
-
export class TodoStore {
|
|
4
|
-
items = [];
|
|
5
|
-
listeners = [];
|
|
6
|
-
set(items) {
|
|
7
|
-
this.items = items;
|
|
8
|
-
for (const l of this.listeners)
|
|
9
|
-
l(items);
|
|
10
|
-
}
|
|
11
|
-
onChange(fn) {
|
|
12
|
-
this.listeners.push(fn);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export function renderTodos(items) {
|
|
16
|
-
if (!items.length)
|
|
17
|
-
return c.dim('(清单为空)');
|
|
18
|
-
const done = items.filter((t) => t.status === 'completed').length;
|
|
19
|
-
const lines = items.map((t) => {
|
|
20
|
-
if (t.status === 'completed')
|
|
21
|
-
return ` ${brand.ok('✔')} ${c.dim(c.strike(t.content))}`;
|
|
22
|
-
if (t.status === 'in_progress')
|
|
23
|
-
return ` ${brand.accent('▸')} ${c.bold(t.content)}`;
|
|
24
|
-
return ` ${c.dim('○')} ${t.content}`;
|
|
25
|
-
});
|
|
26
|
-
return (`${brand.primary('任务清单')} ${c.dim(`(${done}/${items.length})`)}\n` +
|
|
27
|
-
lines.join('\n'));
|
|
28
|
-
}
|
|
29
|
-
export const todoWriteTool = {
|
|
30
|
-
name: 'todo_write',
|
|
31
|
-
description: '维护任务清单以追踪多步骤工作的进度。每次传入完整的清单(整表覆盖)。对于包含 3 步以上的复杂任务,应在开始时创建清单,并随进展把每项标记为 in_progress / completed。同一时刻只应有一项 in_progress。',
|
|
32
|
-
inputSchema: {
|
|
33
|
-
type: 'object',
|
|
34
|
-
properties: {
|
|
35
|
-
todos: {
|
|
36
|
-
type: 'array',
|
|
37
|
-
description: '完整的任务清单',
|
|
38
|
-
items: {
|
|
39
|
-
type: 'object',
|
|
40
|
-
properties: {
|
|
41
|
-
content: { type: 'string', description: '任务描述(祈使式)' },
|
|
42
|
-
status: {
|
|
43
|
-
type: 'string',
|
|
44
|
-
enum: ['pending', 'in_progress', 'completed'],
|
|
45
|
-
},
|
|
46
|
-
activeForm: { type: 'string', description: '进行时描述(可选)' },
|
|
47
|
-
},
|
|
48
|
-
required: ['content', 'status'],
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
required: ['todos'],
|
|
53
|
-
},
|
|
54
|
-
async run(input, ctx) {
|
|
55
|
-
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
56
|
-
const inProgress = todos.filter((t) => t.status === 'in_progress').length;
|
|
57
|
-
ctx.todos?.set(todos);
|
|
58
|
-
const warn = inProgress > 1
|
|
59
|
-
? '\n(提醒:同一时刻应只有一项 in_progress)'
|
|
60
|
-
: '';
|
|
61
|
-
const summary = `已更新任务清单:共 ${todos.length} 项,` +
|
|
62
|
-
`完成 ${todos.filter((t) => t.status === 'completed').length} 项。${warn}`;
|
|
63
|
-
return {
|
|
64
|
-
content: summary,
|
|
65
|
-
display: renderTodos(todos),
|
|
66
|
-
};
|
|
67
|
-
},
|
|
68
|
-
};
|
package/dist/tools/types.js
DELETED
package/dist/tools/web.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { c, brand } from '../ui/theme.js';
|
|
2
|
-
const MAX_CHARS = 12_000;
|
|
3
|
-
const TIMEOUT = 15_000;
|
|
4
|
-
/** 极简 HTML → 文本:去脚本/样式/标签,解码常见实体,折叠空白。 */
|
|
5
|
-
export function htmlToText(html) {
|
|
6
|
-
let s = html;
|
|
7
|
-
s = s.replace(/<script[\s\S]*?<\/script>/gi, ' ');
|
|
8
|
-
s = s.replace(/<style[\s\S]*?<\/style>/gi, ' ');
|
|
9
|
-
s = s.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
10
|
-
// 块级标签转换行
|
|
11
|
-
s = s.replace(/<\/(p|div|section|article|h[1-6]|li|tr|br)>/gi, '\n');
|
|
12
|
-
s = s.replace(/<br\s*\/?>/gi, '\n');
|
|
13
|
-
// 去掉剩余标签
|
|
14
|
-
s = s.replace(/<[^>]+>/g, ' ');
|
|
15
|
-
// 实体
|
|
16
|
-
const entities = {
|
|
17
|
-
' ': ' ',
|
|
18
|
-
'&': '&',
|
|
19
|
-
'<': '<',
|
|
20
|
-
'>': '>',
|
|
21
|
-
'"': '"',
|
|
22
|
-
''': "'",
|
|
23
|
-
''': "'",
|
|
24
|
-
};
|
|
25
|
-
s = s.replace(/&[a-z#0-9]+;/gi, (m) => entities[m.toLowerCase()] ?? m);
|
|
26
|
-
// 折叠空白
|
|
27
|
-
s = s
|
|
28
|
-
.split('\n')
|
|
29
|
-
.map((l) => l.replace(/[ \t]+/g, ' ').trim())
|
|
30
|
-
.filter((l) => l.length > 0)
|
|
31
|
-
.join('\n');
|
|
32
|
-
return s;
|
|
33
|
-
}
|
|
34
|
-
export const webFetchTool = {
|
|
35
|
-
name: 'web_fetch',
|
|
36
|
-
description: '抓取一个 http/https 网址的内容并转为精简文本,用于查阅在线文档或网页。返回正文文本(已截断)。',
|
|
37
|
-
inputSchema: {
|
|
38
|
-
type: 'object',
|
|
39
|
-
properties: {
|
|
40
|
-
url: { type: 'string', description: 'http/https 网址' },
|
|
41
|
-
prompt: {
|
|
42
|
-
type: 'string',
|
|
43
|
-
description: '你关注的问题(可选,会附在内容前作为提示)',
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
required: ['url'],
|
|
47
|
-
},
|
|
48
|
-
permission(input) {
|
|
49
|
-
return {
|
|
50
|
-
tool: 'web_fetch',
|
|
51
|
-
rule: `WebFetch(${safeHost(input.url)})`,
|
|
52
|
-
description: `抓取网页 ${input.url}`,
|
|
53
|
-
dangerous: false,
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
preview(input) {
|
|
57
|
-
return `抓取 ${brand.accent(input.url)}`;
|
|
58
|
-
},
|
|
59
|
-
async run(input, _ctx) {
|
|
60
|
-
const url = String(input.url || '');
|
|
61
|
-
if (!/^https?:\/\//i.test(url)) {
|
|
62
|
-
return { content: '错误:仅支持 http/https 协议。', isError: true };
|
|
63
|
-
}
|
|
64
|
-
const controller = new AbortController();
|
|
65
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT);
|
|
66
|
-
try {
|
|
67
|
-
const res = await fetch(url, {
|
|
68
|
-
signal: controller.signal,
|
|
69
|
-
headers: { 'User-Agent': 'CCLI/1.0 (+web_fetch)' },
|
|
70
|
-
redirect: 'follow',
|
|
71
|
-
});
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
if (!res.ok) {
|
|
74
|
-
return { content: `请求失败:HTTP ${res.status}`, isError: true };
|
|
75
|
-
}
|
|
76
|
-
const ctype = res.headers.get('content-type') || '';
|
|
77
|
-
const raw = await res.text();
|
|
78
|
-
let text;
|
|
79
|
-
if (ctype.includes('html'))
|
|
80
|
-
text = htmlToText(raw);
|
|
81
|
-
else if (ctype.includes('json'))
|
|
82
|
-
text = raw;
|
|
83
|
-
else
|
|
84
|
-
text = raw;
|
|
85
|
-
if (text.length > MAX_CHARS) {
|
|
86
|
-
text = text.slice(0, MAX_CHARS) + '\n…(内容已截断)';
|
|
87
|
-
}
|
|
88
|
-
const header = input.prompt ? `[关注点] ${input.prompt}\n\n` : '';
|
|
89
|
-
return {
|
|
90
|
-
content: `${header}来源:${url}\n\n${text}`,
|
|
91
|
-
display: `${brand.ok('✓')} 抓取 ${url} ${c.dim(`(${text.length} 字)`)}`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
const msg = e?.name === 'AbortError' ? '请求超时' : e?.message ?? String(e);
|
|
97
|
-
return { content: `抓取失败:${msg}`, isError: true };
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
function safeHost(url) {
|
|
102
|
-
try {
|
|
103
|
-
return new URL(url).host;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return url;
|
|
107
|
-
}
|
|
108
|
-
}
|