@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,432 +0,0 @@
1
- /**
2
- * 交互式会话(REPL)—— 自定义终端 UI,串联输入、流式输出、权限确认与斜杠命令。
3
- */
4
- import * as readline from 'node:readline';
5
- import { stdin, stdout } from 'node:process';
6
- import { spawnSync } from 'node:child_process';
7
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
- import { join } from 'node:path';
9
- import { createClient } from '../agent/provider.js';
10
- import { Agent } from '../agent/loop.js';
11
- import { buildAgent } from '../agent/factory.js';
12
- import { PermissionManager, } from '../permissions/permissions.js';
13
- import { c, brand } from '../ui/theme.js';
14
- import { renderMarkdown, box, MarkdownStream } from '../ui/markdown.js';
15
- import { renderTodos } from '../tools/todo.js';
16
- import { Spinner } from '../ui/spinner.js';
17
- import { findCommand } from '../commands/slash.js';
18
- import { loadCustomCommands, expandTemplate, } from '../commands/custom.js';
19
- import { expandMentions, foundMentions } from './mentions.js';
20
- import { SessionStore } from '../session/store.js';
21
- import { createReadOnlyRegistry } from '../tools/index.js';
22
- const INIT_PROMPT = `请分析当前项目并生成一份 CCLI.md(供 AI 助手阅读的项目说明)。
23
- 步骤:
24
- 1. 用 list_dir / glob / read_file 了解项目结构、技术栈、关键约定(构建/测试/lint 命令、代码风格、目录职责)。
25
- 2. 阅读 package.json / README / 配置文件等以提取真实信息,不要臆测。
26
- 3. 用 write_file 写入 CCLI.md,内容简洁实用:项目概述、常用命令、关键约定、目录结构要点。
27
- 如果已存在 CCLI.md 或 CLAUDE.md,请先读取并在其基础上补充完善,而非覆盖有用内容。`;
28
- function commitPrompt(extra) {
29
- return `请帮我创建一个 git 提交:
30
- 1. 用 bash 运行 \`git status\` 和 \`git diff --staged\`(以及必要时 \`git diff\`)了解改动。
31
- 2. 若没有已暂存的改动,先 \`git add -A\` 暂存相关文件。
32
- 3. 依据改动总结一个清晰的提交信息(中文,首行简明,必要时附要点)。
33
- 4. 用 \`git commit\` 提交。不要包含 Co-Authored-By 行。
34
- ${extra ? `\n补充要求:${extra}` : ''}`;
35
- }
36
- export class Session {
37
- config;
38
- client;
39
- agent;
40
- permissions;
41
- rl;
42
- spinner = new Spinner();
43
- cwd;
44
- exiting = false;
45
- currentAbort = null;
46
- streaming = false;
47
- streamedAny = false;
48
- todos;
49
- customCommands;
50
- store;
51
- constructor(config, cwd, resumeFrom) {
52
- this.config = config;
53
- this.cwd = cwd;
54
- this.client = createClient(config);
55
- const asker = {
56
- ask: (req) => this.askPermission(req),
57
- };
58
- this.permissions = new PermissionManager(config, asker);
59
- const built = buildAgent({
60
- client: this.client,
61
- cwd,
62
- permissions: this.permissions,
63
- });
64
- this.agent = built.agent;
65
- this.todos = built.todos;
66
- // 渲染 todo 变更
67
- this.todos.onChange((items) => {
68
- stdout.write('\n' + this.indent(renderTodos(items)) + '\n');
69
- });
70
- // 自定义命令
71
- this.customCommands = loadCustomCommands(cwd);
72
- // 会话持久化
73
- if (resumeFrom) {
74
- this.agent.history = resumeFrom.messages;
75
- this.store = SessionStore.resume(resumeFrom);
76
- }
77
- else {
78
- this.store = new SessionStore(cwd);
79
- }
80
- this.rl = readline.createInterface({
81
- input: stdin,
82
- output: stdout,
83
- prompt: this.promptStr(),
84
- historySize: 200,
85
- });
86
- }
87
- promptStr() {
88
- return brand.primary('❯ ');
89
- }
90
- print(s) {
91
- stdout.write(s + '\n');
92
- }
93
- // ---------- 启动 ----------
94
- async start() {
95
- this.printBanner();
96
- this.setupSignals();
97
- this.rl.prompt();
98
- this.rl.on('line', async (line) => {
99
- const input = line.trim();
100
- if (this.streaming)
101
- return; // 流式中忽略
102
- await this.handleLine(input);
103
- if (!this.exiting) {
104
- this.rl.setPrompt(this.promptStr());
105
- this.rl.prompt();
106
- }
107
- });
108
- this.rl.on('close', () => {
109
- if (!this.exiting) {
110
- this.print('\n' + c.dim('再见 👋'));
111
- }
112
- process.exit(0);
113
- });
114
- }
115
- printBanner() {
116
- const lines = [
117
- brand.primary(c.bold('✶ CCLI')) + c.dim(' —— AI 软件工程师(Claude Code 复刻)'),
118
- '',
119
- `${c.dim('模型')} ${brand.accent(this.config.model)}`,
120
- `${c.dim('目录')} ${this.cwd}`,
121
- '',
122
- c.dim('输入你的目标,我会自主规划并完成。/help 查看命令。'),
123
- ];
124
- this.print(box(lines));
125
- this.print('');
126
- }
127
- setupSignals() {
128
- let lastSigint = 0;
129
- process.on('SIGINT', () => {
130
- const now = Date.now();
131
- if (this.streaming && this.currentAbort) {
132
- // 中断当前生成
133
- this.currentAbort.abort();
134
- this.spinner.stop();
135
- this.streaming = false;
136
- this.print('\n' + c.yellow('⏹ 已中断。'));
137
- this.rl.prompt();
138
- return;
139
- }
140
- if (now - lastSigint < 1000) {
141
- this.exiting = true;
142
- this.print('\n' + c.dim('再见 👋'));
143
- process.exit(0);
144
- }
145
- lastSigint = now;
146
- this.print('\n' + c.dim('(再次按 Ctrl+C 退出)'));
147
- this.rl.prompt();
148
- });
149
- }
150
- // ---------- 输入分发 ----------
151
- async handleLine(input) {
152
- if (!input)
153
- return;
154
- // shell 直通
155
- if (input.startsWith('!')) {
156
- this.runShell(input.slice(1).trim());
157
- return;
158
- }
159
- // # 写记忆:追加到 CCLI.md
160
- if (input.startsWith('#')) {
161
- this.appendMemory(input.slice(1).trim());
162
- return;
163
- }
164
- // 斜杠命令
165
- if (input.startsWith('/')) {
166
- await this.runSlash(input.slice(1));
167
- return;
168
- }
169
- // 普通对话 —— 进入代理循环
170
- await this.converse(input);
171
- }
172
- /**
173
- * 把一行规则追加到项目记忆文件(# 写记忆)。
174
- * 默认写入 CCLI.md;若项目已有 CLAUDE.md 而无 CCLI.md,则尊重现状写入 CLAUDE.md。
175
- */
176
- appendMemory(text) {
177
- if (!text) {
178
- this.print(c.dim('用法:# 你想让我长期记住的规则'));
179
- return;
180
- }
181
- const ccli = join(this.cwd, 'CCLI.md');
182
- const claude = join(this.cwd, 'CLAUDE.md');
183
- const path = !existsSync(ccli) && existsSync(claude) ? claude : ccli;
184
- const name = path === claude ? 'CLAUDE.md' : 'CCLI.md';
185
- try {
186
- let content = existsSync(path) ? readFileSync(path, 'utf8') : '';
187
- if (content && !content.endsWith('\n'))
188
- content += '\n';
189
- if (!content.includes('## 用户记忆')) {
190
- content += (content ? '\n' : '') + '## 用户记忆\n';
191
- }
192
- content += `- ${text}\n`;
193
- writeFileSync(path, content, 'utf8');
194
- this.print(brand.ok(`✓ 已记入 ${name}:`) + ' ' + c.dim(text));
195
- }
196
- catch (e) {
197
- this.print(brand.err('写入失败:' + e.message));
198
- }
199
- }
200
- runShell(cmd) {
201
- if (!cmd)
202
- return;
203
- this.print(c.dim(`$ ${cmd}`));
204
- const res = spawnSync(process.env.SHELL || '/bin/bash', ['-c', cmd], {
205
- cwd: this.cwd,
206
- stdio: 'inherit',
207
- });
208
- if (res.status !== 0) {
209
- this.print(c.dim(`[退出码 ${res.status}]`));
210
- }
211
- }
212
- async runSlash(raw) {
213
- const [name, ...rest] = raw.split(/\s+/);
214
- // /resume:列出会话
215
- if (name === 'resume') {
216
- this.listSessions();
217
- return;
218
- }
219
- // /init:让 agent 分析项目并生成 CCLI.md
220
- if (name === 'init') {
221
- await this.converse(INIT_PROMPT);
222
- return;
223
- }
224
- // /commit:让 agent 生成提交信息并提交
225
- if (name === 'commit') {
226
- await this.converse(commitPrompt(rest.join(' ')));
227
- return;
228
- }
229
- const cmd = findCommand(name);
230
- if (!cmd) {
231
- // 自定义命令兜底
232
- const custom = this.customCommands.get(name);
233
- if (custom) {
234
- const msg = expandTemplate(custom.template, rest.join(' '));
235
- this.print(c.dim(`(自定义命令 /${name})`));
236
- await this.converse(msg);
237
- return;
238
- }
239
- this.print(c.yellow(`未知命令 /${name},输入 /help 查看可用命令。`));
240
- return;
241
- }
242
- const sctx = {
243
- print: (s) => this.print(s),
244
- clearHistory: () => this.agent.clear(),
245
- setModel: (m) => {
246
- this.config.model = m;
247
- this.client.setModel(m);
248
- this.config.saveProject();
249
- },
250
- getModel: () => this.client.getModel(),
251
- getUsage: () => this.agent.totalUsage,
252
- toggleAutoApprove: () => {
253
- this.config.data.autoApprove = !this.config.data.autoApprove;
254
- return this.config.data.autoApprove;
255
- },
256
- listAllowed: () => this.config.data.allowedTools,
257
- cwd: this.cwd,
258
- requestExit: () => {
259
- this.exiting = true;
260
- this.print(c.dim('再见 👋'));
261
- process.exit(0);
262
- },
263
- requestCompact: () => this.compact(),
264
- getHistoryLength: () => this.agent.history.length,
265
- };
266
- await cmd.run(rest.join(' '), sctx);
267
- }
268
- listSessions() {
269
- const all = SessionStore.list(this.cwd);
270
- if (!all.length) {
271
- this.print(c.dim('(该目录暂无历史会话)'));
272
- return;
273
- }
274
- const lines = all.slice(0, 10).map((s, i) => {
275
- const when = s.updatedAt.replace('T', ' ').slice(0, 16);
276
- const turns = s.messages.length;
277
- return ` ${brand.accent(s.id)} ${c.dim(when)} ${c.dim(`${turns} 条`)}`;
278
- });
279
- this.print([
280
- brand.primary('该目录的历史会话'),
281
- ...lines,
282
- '',
283
- c.dim('用 ccli --resume <id> 恢复某次会话。'),
284
- ].join('\n'));
285
- }
286
- // ---------- 对话(代理循环)----------
287
- async converse(rawInput) {
288
- // @文件引用展开
289
- const mentioned = foundMentions(rawInput, this.cwd);
290
- if (mentioned.length) {
291
- this.print(c.dim(` ↳ 已引用:${mentioned.join('、')}`));
292
- }
293
- const input = expandMentions(rawInput, this.cwd);
294
- this.streaming = true;
295
- this.streamedAny = false;
296
- this.currentAbort = new AbortController();
297
- const signal = this.currentAbort.signal;
298
- // 流式 Markdown 渲染器(每个 assistant 文本段一个)
299
- let md = null;
300
- const endTextSegment = () => {
301
- if (md) {
302
- md.flush();
303
- md = null;
304
- }
305
- };
306
- const events = {
307
- onTurnStart: () => this.spinner.start(),
308
- onTurnEnd: () => this.spinner.stop(),
309
- onAssistantText: (delta) => {
310
- if (!md) {
311
- this.spinner.stop();
312
- stdout.write('\n' + brand.primary('●') + '\n');
313
- md = new MarkdownStream((s) => stdout.write(s));
314
- this.streamedAny = true;
315
- }
316
- md.feed(delta);
317
- },
318
- onThinking: (delta) => {
319
- // 思考内容以暗色显示
320
- this.spinner.stop();
321
- stdout.write(c.dim(delta));
322
- },
323
- onToolStart: (tool, _input, preview) => {
324
- endTextSegment();
325
- this.spinner.stop();
326
- stdout.write('\n' + brand.accent(' ⚒ ') + preview + '\n');
327
- },
328
- onToolResult: (_tool, display, isError) => {
329
- stdout.write(' ' + this.indent(display) + '\n');
330
- },
331
- onToolDenied: (_tool, reason) => {
332
- stdout.write(' ' + c.yellow('✗ ' + reason) + '\n');
333
- },
334
- };
335
- try {
336
- await this.agent.run(input, events, signal);
337
- endTextSegment();
338
- this.store.save(this.client.getModel(), this.agent.history);
339
- }
340
- catch (e) {
341
- endTextSegment();
342
- this.spinner.stop();
343
- if (!signal.aborted) {
344
- this.print('\n' + brand.err('✗ 出错:') + (e?.message ?? String(e)));
345
- if (e?.status === 401) {
346
- this.print(c.dim(' API Key 无效,请检查 ANTHROPIC_API_KEY。'));
347
- }
348
- }
349
- }
350
- finally {
351
- this.streaming = false;
352
- this.currentAbort = null;
353
- }
354
- }
355
- indent(s) {
356
- return s.split('\n').join('\n ');
357
- }
358
- // ---------- 权限确认 UI ----------
359
- askPermission(req) {
360
- this.spinner.stop();
361
- return new Promise((resolvePromise) => {
362
- const title = req.dangerous
363
- ? brand.err('⚠ 高危操作需要确认')
364
- : brand.warn('需要你的授权');
365
- const lines = [
366
- '',
367
- box([
368
- title,
369
- '',
370
- `${c.dim('操作')} ${req.description}`,
371
- req.dangerous ? c.red(' 这是一个可能造成破坏的命令,请谨慎。') : '',
372
- ].filter(Boolean), { color: req.dangerous ? c.red : brand.warn }),
373
- '',
374
- ` ${brand.ok('[y]')} 允许一次 ${brand.accent('[a]')} 始终允许(加入白名单) ${c.red('[n]')} 拒绝`,
375
- '',
376
- ];
377
- this.print(lines.join('\n'));
378
- // 复用主 readline 的 question —— Node 会在此期间抑制 'line' 事件,
379
- // 避免双重消费按键。递归直到得到合法答案。
380
- const askOnce = () => {
381
- this.rl.question(brand.primary(' > '), (raw) => {
382
- const key = raw.trim().toLowerCase();
383
- let answer = null;
384
- if (key === 'y' || key === '' || key === 'yes')
385
- answer = 'allow';
386
- else if (key === 'a' || key === 'always')
387
- answer = 'always';
388
- else if (key === 'n' || key === 'no')
389
- answer = 'deny';
390
- if (!answer) {
391
- stdout.write(c.dim(' 请输入 y / a / n。\n'));
392
- askOnce();
393
- return;
394
- }
395
- resolvePromise(answer);
396
- });
397
- };
398
- askOnce();
399
- });
400
- }
401
- // ---------- compact ----------
402
- async compact() {
403
- if (this.agent.history.length === 0) {
404
- this.print(c.dim('(无可压缩的历史)'));
405
- return;
406
- }
407
- this.print(c.dim('正在压缩对话历史…'));
408
- this.spinner.start('压缩中');
409
- try {
410
- const summaryAgent = this.agent;
411
- // 用一个轻量请求生成摘要
412
- const tmp = new Agent(this.client, createReadOnlyRegistry(), this.permissions, '你是一个对话摘要助手。请用简洁中文总结此前对话的关键事实、已完成的改动和待办,供后续继续工作使用。', { cwd: this.cwd });
413
- tmp.history = [...summaryAgent.history];
414
- let summary = '';
415
- await tmp.run('请总结以上对话,输出一段供继续工作的上下文摘要。', {
416
- onAssistantText: (d) => {
417
- summary += d;
418
- },
419
- onTurnStart: () => { },
420
- onTurnEnd: () => { },
421
- });
422
- this.spinner.stop();
423
- this.agent.compactSummary(summary.trim());
424
- this.print(brand.ok('✓ 已压缩历史。'));
425
- this.print(c.dim(renderMarkdown(summary.trim()).split('\n').slice(0, 8).join('\n')));
426
- }
427
- catch (e) {
428
- this.spinner.stop();
429
- this.print(brand.err('压缩失败:' + e.message));
430
- }
431
- }
432
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * 自定义斜杠命令 —— 从 .ccli/commands/*.md 加载用户定义的提示词模板。
3
- * 文件名即命令名,$ARGUMENTS 会被替换为调用参数。
4
- */
5
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
- import { join, basename } from 'node:path';
7
- import { homedir } from 'node:os';
8
- function scanDir(dir) {
9
- if (!existsSync(dir))
10
- return [];
11
- const out = [];
12
- for (const f of readdirSync(dir)) {
13
- if (!f.endsWith('.md'))
14
- continue;
15
- try {
16
- const template = readFileSync(join(dir, f), 'utf8');
17
- out.push({
18
- name: basename(f, '.md'),
19
- template,
20
- source: join(dir, f),
21
- });
22
- }
23
- catch {
24
- /* skip */
25
- }
26
- }
27
- return out;
28
- }
29
- /** 加载项目级 + 全局自定义命令(项目级优先)。 */
30
- export function loadCustomCommands(cwd) {
31
- const map = new Map();
32
- for (const cmd of scanDir(join(homedir(), '.ccli', 'commands'))) {
33
- map.set(cmd.name, cmd);
34
- }
35
- for (const cmd of scanDir(join(cwd, '.ccli', 'commands'))) {
36
- map.set(cmd.name, cmd); // 项目级覆盖全局
37
- }
38
- return map;
39
- }
40
- /** 把模板里的 $ARGUMENTS 展开为参数文本。 */
41
- export function expandTemplate(template, args) {
42
- if (template.includes('$ARGUMENTS')) {
43
- return template.split('$ARGUMENTS').join(args);
44
- }
45
- return args ? `${template}\n\n${args}` : template;
46
- }
@@ -1,148 +0,0 @@
1
- /**
2
- * 斜杠命令(Slash Commands)—— 复刻 Claude Code 的 /help、/clear、/model 等。
3
- */
4
- import { c, brand } from '../ui/theme.js';
5
- import { box } from '../ui/markdown.js';
6
- import { estimateCost, formatUsd } from '../agent/pricing.js';
7
- export const COMMANDS = [
8
- {
9
- name: 'help',
10
- description: '显示所有可用命令',
11
- run: (_args, ctx) => {
12
- const lines = COMMANDS.map((cmd) => ` ${brand.accent('/' + cmd.name.padEnd(12))} ${c.dim(cmd.description)}`);
13
- ctx.print([
14
- brand.primary(c.bold('CCLI 命令')),
15
- ...lines,
16
- '',
17
- c.dim(' 直接输入文字即可与 AI 对话。'),
18
- c.dim(' Ctrl+C 中断当前操作,再次 Ctrl+C 或 /exit 退出。'),
19
- c.dim(' shell 直通:以 ! 开头的输入会作为 shell 命令执行。'),
20
- c.dim(' 文件引用:在消息中用 @路径 自动并入文件内容。'),
21
- c.dim(' 写记忆:以 # 开头的输入会追加到 CCLI.md。'),
22
- c.dim(' 自定义命令:在 .ccli/commands/<名>.md 定义 /<名>。'),
23
- ].join('\n'));
24
- },
25
- },
26
- {
27
- name: 'clear',
28
- description: '清空对话历史,重置上下文',
29
- run: (_args, ctx) => {
30
- ctx.clearHistory();
31
- ctx.print(brand.ok('✓ 已清空对话历史。'));
32
- },
33
- },
34
- {
35
- name: 'compact',
36
- description: '压缩对话历史以节省上下文',
37
- run: async (_args, ctx) => {
38
- await ctx.requestCompact();
39
- },
40
- },
41
- {
42
- name: 'model',
43
- description: '查看或切换模型,如 /model claude-sonnet-4-6',
44
- run: (args, ctx) => {
45
- const m = args.trim();
46
- if (!m) {
47
- ctx.print([
48
- `当前模型:${brand.accent(ctx.getModel())}`,
49
- c.dim('可选:claude-opus-4-8 / claude-sonnet-4-6 / claude-haiku-4-5-20251001'),
50
- c.dim('用法:/model <模型ID>'),
51
- ].join('\n'));
52
- return;
53
- }
54
- ctx.setModel(m);
55
- ctx.print(brand.ok(`✓ 已切换模型为 ${m}`));
56
- },
57
- },
58
- {
59
- name: 'cost',
60
- description: '显示本次会话的 token 用量与估算成本',
61
- run: (_args, ctx) => {
62
- const u = ctx.getUsage();
63
- const model = ctx.getModel();
64
- const cost = estimateCost(model, u);
65
- ctx.print([
66
- brand.primary('本次会话用量'),
67
- ` 模型:${brand.accent(model)}`,
68
- ` 输入 tokens:${c.bold(u.input.toLocaleString())} (${formatUsd(cost.input)})`,
69
- ` 输出 tokens:${c.bold(u.output.toLocaleString())} (${formatUsd(cost.output)})`,
70
- ` 合计:${c.bold((u.input + u.output).toLocaleString())} tokens ${brand.ok(formatUsd(cost.total))}`,
71
- c.dim(' (按公开近似单价估算,仅供参考)'),
72
- ].join('\n'));
73
- },
74
- },
75
- {
76
- name: 'auto',
77
- description: '切换自动批准模式(危险:跳过所有确认)',
78
- run: (_args, ctx) => {
79
- const on = ctx.toggleAutoApprove();
80
- ctx.print(on
81
- ? c.yellow('⚠ 已开启自动批准模式 —— 所有工具调用将不再询问。')
82
- : brand.ok('✓ 已关闭自动批准模式。'));
83
- },
84
- },
85
- {
86
- name: 'permissions',
87
- description: '查看已批准的权限白名单',
88
- run: (_args, ctx) => {
89
- const allowed = ctx.listAllowed();
90
- if (!allowed.length) {
91
- ctx.print(c.dim('(暂无永久批准的权限)'));
92
- return;
93
- }
94
- ctx.print([brand.primary('已批准的权限'), ...allowed.map((r) => ` ${c.green('✓')} ${r}`)].join('\n'));
95
- },
96
- },
97
- {
98
- name: 'init',
99
- description: '分析项目并生成 CCLI.md',
100
- run: () => {
101
- /* 由 Session.runSlash 拦截处理 */
102
- },
103
- },
104
- {
105
- name: 'commit',
106
- description: '生成提交信息并 git 提交',
107
- run: () => {
108
- /* 由 Session.runSlash 拦截处理 */
109
- },
110
- },
111
- {
112
- name: 'resume',
113
- description: '列出本目录的历史会话',
114
- run: () => {
115
- /* 由 Session.runSlash 拦截处理 */
116
- },
117
- },
118
- {
119
- name: 'status',
120
- description: '显示当前会话状态',
121
- run: (_args, ctx) => {
122
- const u = ctx.getUsage();
123
- ctx.print(box([
124
- `${c.dim('工作目录')} ${ctx.cwd}`,
125
- `${c.dim('模型')} ${brand.accent(ctx.getModel())}`,
126
- `${c.dim('历史消息')} ${ctx.getHistoryLength()} 条`,
127
- `${c.dim('Token')} ${(u.input + u.output).toLocaleString()}`,
128
- ], { color: brand.accent }));
129
- },
130
- },
131
- {
132
- name: 'exit',
133
- description: '退出 CCLI',
134
- run: (_args, ctx) => {
135
- ctx.requestExit();
136
- },
137
- },
138
- {
139
- name: 'quit',
140
- description: '退出 CCLI(同 /exit)',
141
- run: (_args, ctx) => {
142
- ctx.requestExit();
143
- },
144
- },
145
- ];
146
- export function findCommand(name) {
147
- return COMMANDS.find((c) => c.name === name);
148
- }