codemini-cli 0.3.9 → 0.4.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 CHANGED
@@ -74,6 +74,11 @@ CodeMini CLI can optionally use `fff-mcp` as a faster backend for `grep`, `glob`
74
74
  | `codemini [prompt]` | Start an interactive coding session with an optional initial prompt |
75
75
  | `codemini chat [prompt]` | Chat mode — single-turn or multi-turn conversation |
76
76
  | `codemini run <task>` | Run a task non-interactively (e.g. `codemini run "fix the login bug"`) |
77
+ | `codemini run --harness <role> <task>` | Run a task with a specific sub-agent role (e.g. `coder`, `planner`, `reviewer`) |
78
+ | `codemini run --pipeline <task>` | Run a task through the full planning → coding → review pipeline |
79
+ | `codemini run <task> --max-steps N` | Limit the maximum number of agent steps for a run task |
80
+ | `codemini run <task> --model <name>` | Override the default model for a single run |
81
+ | `codemini [prompt] --plain` | Disable TUI and use plain terminal output |
77
82
  | `codemini config set\|get\|list <key> [value]` | Manage configuration (gateway, model, shell, UI, soul, etc.) |
78
83
  | `codemini doctor` | Run environment diagnostics and validate configuration |
79
84
  | `codemini skill list\|install\|enable\|disable\|inspect\|reindex` | Manage skills — list, install, toggle, or inspect bundled/third-party skills |
@@ -88,6 +93,23 @@ Built-in souls: `default`, `professional`, `ceo`, `playful`, `anime`, `caveman`,
88
93
  codemini config set soul.preset playful
89
94
  ```
90
95
 
96
+ ### Built-in Skills
97
+
98
+ Skills are reusable workflow patterns that guide how the agent approaches different types of tasks. They are loaded automatically when applicable.
99
+
100
+ | Skill | Trigger | Description |
101
+ |-------|---------|-------------|
102
+ | **superpowers-lite** | Default for all coding work | Lightweight operating style: prefer structured tools, keep context tight, use sub-agents, verify before claiming success |
103
+ | **brainstorm** | Multiple reasonable approaches exist | Explores options and tradeoffs before coding; asks one question at a time to resolve uncertainty |
104
+ | **writing-plans** | Non-trivial implementation task | Creates a step-by-step plan with exact file paths, code, and verification steps before touching code |
105
+
106
+ Skills are installed and managed via `codemini skill`:
107
+
108
+ ```bash
109
+ codemini skill list # List all available skills
110
+ codemini skill inspect <name> # Inspect a skill's details
111
+ ```
112
+
91
113
  ### How The Tool Model Works
92
114
 
93
115
  CodeMini CLI intentionally separates tools into two layers:
@@ -122,7 +144,7 @@ Typical flow:
122
144
  - Unified shell execution model:
123
145
  - one-off commands via `run`
124
146
  - long-running commands via `run` with `run_in_background=true`
125
- - Lightweight project index under `.codemini-project/`
147
+ - Lightweight project index under `.codemini/`
126
148
  - Tree-sitter based structured editing for function, class, and method-level changes
127
149
  - Reply language control via `ui.reply_language`
128
150
  - Safe mode enabled by default
@@ -157,7 +179,7 @@ Execution mode behavior:
157
179
 
158
180
  ### Project Index
159
181
 
160
- CodeMini CLI maintains a lightweight project index inside `.codemini-project/`:
182
+ CodeMini CLI maintains a lightweight project index inside `.codemini/`:
161
183
 
162
184
  - `project-map.json` — high-level repository facts such as languages, source roots, test roots, and entry candidates
163
185
  - `file-index.json` — per-file structure such as imports, exports, functions, classes, and lightweight symbol hints
@@ -169,7 +191,7 @@ The index is initialized when entering a project and refreshed incrementally aft
169
191
 
170
192
  - Global session state: `<base-config-dir>/sessions/`
171
193
  - Project workspace state: `.codemini/`
172
- - Lightweight project index: `.codemini-project/`
194
+ - Lightweight project index: `.codemini/`
173
195
  - Bundled repo skills: `skills/<name>/SKILL.md`
174
196
  - Project-scoped skills: `.codemini/skills/<name>/SKILL.md`
175
197
  - Global installed skills: `<base-config-dir>/skills/<name>/SKILL.md`
@@ -285,6 +307,11 @@ CodeMini CLI 可以可选地使用 `fff-mcp` 作为 `grep`、`glob` 和部分 `l
285
307
  | `codemini [prompt]` | 启动交互式编码会话,可附带初始提示 |
286
308
  | `codemini chat [prompt]` | 对话模式——单轮或多轮 |
287
309
  | `codemini run <task>` | 非交互式执行任务(如 `codemini run "修复登录 bug"`) |
310
+ | `codemini run --harness <role> <task>` | 以指定 sub-agent 角色执行任务(如 `coder`、`planner`、`reviewer`) |
311
+ | `codemini run --pipeline <task>` | 通过完整计划→编码→审查流水线执行任务 |
312
+ | `codemini run <task> --max-steps N` | 限制单次执行的最大 agent 步数 |
313
+ | `codemini run <task> --model <name>` | 单次执行时覆盖默认模型 |
314
+ | `codemini [prompt] --plain` | 禁用 TUI,使用纯文本终端输出 |
288
315
  | `codemini config set\|get\|list <key> [value]` | 管理配置(网关、模型、shell、UI、soul 等) |
289
316
  | `codemini doctor` | 运行环境诊断并验证配置 |
290
317
  | `codemini skill list\|install\|enable\|disable\|inspect\|reindex` | 管理 skill——列表、安装、启用/禁用、检查 |
@@ -299,6 +326,23 @@ CodeMini CLI 支持可切换的 "soul" 人格,仅改变语气和表达风格
299
326
  codemini config set soul.preset playful
300
327
  ```
301
328
 
329
+ ### 内置 Skills
330
+
331
+ Skill 是可复用的工作流模式,指导 agent 如何处理不同类型的任务。适用时会自动加载。
332
+
333
+ | Skill | 触发条件 | 说明 |
334
+ |-------|----------|------|
335
+ | **superpowers-lite** | 所有编码工作的默认 skill | 轻量操作风格:优先结构化工具、保持上下文精简、使用 sub-agent、验证后再报告完成 |
336
+ | **brainstorm** | 存在多种合理方案时 | 在编码前探索选项和权衡;每次只问一个问题来消除不确定性 |
337
+ | **writing-plans** | 非平凡的实现任务 | 在动手之前创建包含精确文件路径、代码和验证步骤的分步计划 |
338
+
339
+ 通过 `codemini skill` 管理技能:
340
+
341
+ ```bash
342
+ codemini skill list # 列出所有可用 skill
343
+ codemini skill inspect <name> # 查看某个 skill 的详细信息
344
+ ```
345
+
302
346
  ### 工具模型怎么设计
303
347
 
304
348
  CodeMini CLI 把工具分成两层:
@@ -333,7 +377,7 @@ CodeMini CLI 把工具分成两层:
333
377
  - 统一的 shell 执行模型:
334
378
  - 一次性命令直接 `run`
335
379
  - 长运行命令通过 `run` + `run_in_background=true`
336
- - 在 `.codemini-project/` 下维护轻量项目索引,帮助模型更快理解仓库
380
+ - 在 `.codemini/` 下维护轻量项目索引,帮助模型更快理解仓库
337
381
  - 基于 Tree-sitter 的结构化编辑能力,适合函数级、类级、方法级改动
338
382
  - 支持通过 `ui.reply_language` 控制回复语言
339
383
  - safe mode 默认开启
@@ -368,7 +412,7 @@ Inbox 和持久记忆的区别:
368
412
 
369
413
  ### 项目索引
370
414
 
371
- CodeMini CLI 会在 `.codemini-project/` 下维护一份轻量项目索引:
415
+ CodeMini CLI 会在 `.codemini/` 下维护一份轻量项目索引:
372
416
 
373
417
  - `project-map.json` — 记录仓库的高层结构事实,比如语言、源码目录、测试目录、入口候选
374
418
  - `file-index.json` — 记录文件级结构信息,比如 imports、exports、functions、classes 和轻量 symbol 提示
@@ -380,7 +424,7 @@ CodeMini CLI 会在 `.codemini-project/` 下维护一份轻量项目索引:
380
424
 
381
425
  - 全局会话状态:`<base-config-dir>/sessions/`
382
426
  - 项目工作区状态:`.codemini/`
383
- - 轻量项目索引:`.codemini-project/`
427
+ - 轻量项目索引:`.codemini/`
384
428
  - 仓库内置 skill:`skills/<name>/SKILL.md`
385
429
  - 项目级 skill:`.codemini/skills/<name>/SKILL.md`
386
430
  - 全局已安装 skill:`<base-config-dir>/skills/<name>/SKILL.md`
package/deployment.md CHANGED
@@ -13,13 +13,13 @@ npm pack
13
13
  Expected output:
14
14
 
15
15
  ```text
16
- codemini-cli-0.1.0.tgz
16
+ codemini-cli-0.4.1.tgz
17
17
  ```
18
18
 
19
19
  If you want to verify the package contents:
20
20
 
21
21
  ```bash
22
- tar -tf codemini-cli-0.1.0.tgz
22
+ tar -tf codemini-cli-0.4.1.tgz
23
23
  ```
24
24
 
25
25
  ## 2. Copy To The Target Machine
@@ -34,7 +34,7 @@ Copy the generated `.tgz` file to the Win10 machine by one of these methods:
34
34
  Recommended target path:
35
35
 
36
36
  ```powershell
37
- C:\temp\codemini-cli-0.1.0.tgz
37
+ C:\temp\codemini-cli-0.4.1.tgz
38
38
  ```
39
39
 
40
40
  ## 3. Environment Requirements
@@ -42,7 +42,7 @@ C:\temp\codemini-cli-0.1.0.tgz
42
42
  Target machine requirements:
43
43
 
44
44
  - Windows 10
45
- - Node.js 20 or newer
45
+ - Node.js 22 or newer
46
46
  - npm available
47
47
  - PowerShell available
48
48
 
@@ -58,7 +58,7 @@ npm -v
58
58
  Global install:
59
59
 
60
60
  ```powershell
61
- npm install -g C:\temp\codemini-cli-0.1.0.tgz
61
+ npm install -g C:\temp\codemini-cli-0.4.1.tgz
62
62
  ```
63
63
 
64
64
  If global install is blocked by company policy, install in a working directory instead:
@@ -66,7 +66,7 @@ If global install is blocked by company policy, install in a working directory i
66
66
  ```powershell
67
67
  mkdir C:\temp\coder-test
68
68
  cd C:\temp\coder-test
69
- npm install C:\temp\codemini-cli-0.1.0.tgz
69
+ npm install C:\temp\codemini-cli-0.4.1.tgz
70
70
  ```
71
71
 
72
72
  ## 5. Confirm Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -48,10 +48,12 @@
48
48
  "dependencies": {
49
49
  "@cursorless/tree-sitter-wasms": "^0.8.1",
50
50
  "cheerio": "^1.1.2",
51
+ "cli-truncate": "^6.0.0",
51
52
  "duck-duck-scrape": "^2.2.7",
52
53
  "ink": "^7.0.0",
53
54
  "playwright": "^1.54.2",
54
55
  "react": "^19.2.5",
56
+ "strip-ansi": "^7.2.0",
55
57
  "web-tree-sitter": "^0.26.8"
56
58
  },
57
59
  "license": "MIT"
@@ -4,6 +4,9 @@ import fs from 'node:fs/promises';
4
4
  import { BoundedCache } from './bounded-cache.js';
5
5
  import { trimInline as _trimInline, normalizePath } from './string-utils.js';
6
6
  import { captureToInbox, listInbox } from './memory-store.js';
7
+ import { requiresApprovalEvaluation } from './command-risk.js';
8
+ import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
9
+ import { normalizeToolArguments } from './tool-args.js';
7
10
 
8
11
  /**
9
12
  * 安全解析 JSON 字符串。
@@ -23,20 +26,6 @@ function safeJsonParse(raw) {
23
26
  }
24
27
  }
25
28
 
26
- function parseInlineRangePath(value) {
27
- const text = String(value || '').trim();
28
- if (!text) return null;
29
- const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
30
- if (!match) return null;
31
- const [, maybePath, startRaw, endRaw] = match;
32
- if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
33
- const start = Number(startRaw);
34
- const end = Number(endRaw || startRaw);
35
- if (!Number.isFinite(start) || start <= 0) return null;
36
- if (!Number.isFinite(end) || end < start) return null;
37
- return { path: maybePath, start_line: start, end_line: end };
38
- }
39
-
40
29
  function buildDeleteApprovalDetails(source, rawPath) {
41
30
  const existing =
42
31
  source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
@@ -72,97 +61,13 @@ function buildDeleteCancellationResult(args) {
72
61
  };
73
62
  }
74
63
 
75
- function normalizeToolArguments(toolName, args, rawArguments) {
76
- const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
77
- const primitive =
78
- args == null || Array.isArray(args) || typeof args !== 'object'
79
- ? args
80
- : null;
81
- const source =
82
- args && typeof args === 'object' && !Array.isArray(args)
83
- ? { ...args }
84
- : {};
85
-
86
- if (primitive != null && typeof primitive !== 'object') {
87
- source._raw = rawText || String(primitive);
88
- } else if (!source._raw && rawText && source._invalid_json) {
89
- source._raw = rawText;
90
- }
91
-
92
- const stringValue =
93
- typeof primitive === 'string'
94
- ? primitive.trim()
95
- : String(source._raw || '').trim();
96
-
97
- if (toolName === 'read') {
98
- const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
99
- if (value) source.path = value;
100
- if (source.offset != null && source.start_line == null) source.start_line = source.offset;
101
- if (source.limit != null && source.end_line == null && Number(source.start_line) > 0) {
102
- source.end_line = Number(source.start_line) + Number(source.limit) - 1;
103
- }
104
- const range = parseInlineRangePath(source.path);
105
- if (range) {
106
- source.path = range.path;
107
- if (source.start_line == null) source.start_line = range.start_line;
108
- if (source.end_line == null) source.end_line = range.end_line;
109
- }
110
- return source;
111
- }
112
-
113
- if (toolName === 'list') {
114
- const value = String(source.path || source.dir || source.directory || stringValue || '.').trim();
115
- return { ...source, path: value || '.' };
116
- }
117
-
118
- if (toolName === 'glob') {
119
- const pattern = String(source.pattern || source.glob || source.query || stringValue || '').trim();
120
- if (pattern) source.pattern = pattern;
121
- if (!source.path && source.directory) source.path = source.directory;
122
- return source;
123
- }
124
-
125
- if (toolName === 'grep') {
126
- const pattern = String(source.pattern || source.query || source.symbol || source.q || stringValue || '').trim();
127
- if (pattern) source.pattern = pattern;
128
- if (!source.path && (source.directory || source.dir || source.cwd)) {
129
- source.path = source.directory || source.dir || source.cwd;
130
- }
131
- return source;
132
- }
133
-
134
- if (toolName === 'write') {
135
- const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
136
- if (value) source.path = value;
137
- if (source.content == null && source.text != null) source.content = source.text;
138
- if (source.content == null && source.new_content != null) source.content = source.new_content;
139
- return source;
140
- }
141
-
142
- if (toolName === 'edit') {
143
- const value = String(source.path || source.file || source.file_path || '').trim();
144
- if (value && !source.path) source.path = value;
145
- return source;
146
- }
147
-
148
- if (toolName === 'delete') {
149
- const value = String(source.path || source.file_path || source.file || source.target || source.directory || source.dir || stringValue || '').trim();
150
- if (value) source.path = value;
151
- const approval = buildDeleteApprovalDetails(source, source.path);
152
- if (approval) source.approval = approval;
153
- return source;
154
- }
155
-
156
- return source;
157
- }
158
-
159
64
  function emptyToolResultMarker(toolName) {
160
65
  const name = String(toolName || 'tool').trim() || 'tool';
161
66
  return `(${name} completed with no output)`;
162
67
  }
163
68
 
164
69
  function clipToolResult(result, maxChars = 12000) {
165
- const raw = typeof result === 'string' ? result : JSON.stringify(result);
70
+ const raw = sanitizeTextForModel(typeof result === 'string' ? result : JSON.stringify(result));
166
71
  if (!maxChars || raw.length <= maxChars) return raw;
167
72
  return `${raw.slice(0, maxChars)}\n... [tool result truncated ${raw.length - maxChars} chars]`;
168
73
  }
@@ -170,8 +75,9 @@ function clipToolResult(result, maxChars = 12000) {
170
75
  function compactToolResult(result, toolName, args, maxChars = 12000) {
171
76
  if (result === null || result === undefined) return 'no output';
172
77
  if (typeof result === 'string') {
173
- if (result.length <= maxChars) return result;
174
- return `${result.slice(0, maxChars)}\n... [tool result truncated ${result.length - maxChars} chars, original: ${result.length}]`;
78
+ const sanitized = sanitizeTextForModel(result);
79
+ if (sanitized.length <= maxChars) return sanitized;
80
+ return `${sanitized.slice(0, maxChars)}\n... [tool result truncated ${sanitized.length - maxChars} chars, original: ${sanitized.length}]`;
175
81
  }
176
82
  if (typeof result !== 'object') return String(result);
177
83
 
@@ -387,25 +293,37 @@ function shouldAutoCaptureError(toolName, message) {
387
293
  /not found$/i,
388
294
  /already exists$/i,
389
295
  /cancelled/i,
390
- /aborted/i
296
+ /aborted/i,
297
+ /blocked by (?:safe mode|policy|dangerous command)/i,
298
+ /exit 127/i,
299
+ /command not found/i,
300
+ /permission denied/i,
301
+ /args\?\s/i,
302
+ /Raw tool arguments/i,
303
+ /edit requires/i,
304
+ /write requires/i,
305
+ /requires file/i,
306
+ /path.*outside workspace/i,
307
+ /escapes workspace/i
391
308
  ];
392
309
  if (noisePatterns.some((p) => p.test(message))) return false;
393
310
  lastAutoCaptureByTool.set(toolName, now);
394
311
  return true;
395
312
  }
396
313
 
397
- function fireAndForgetCapture(toolName, message, args) {
314
+ async function captureToolFailure(toolName, message, args, config = {}) {
315
+ if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return;
398
316
  const summary = `[${toolName}] ${String(message).slice(0, 120)}`;
399
317
  const details = args
400
318
  ? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
401
319
  : `Tool: ${toolName}\nError: ${message}`;
402
- captureToInbox({
403
- scope: 'global',
320
+ await captureToInbox({
321
+ scope: 'repo',
404
322
  type: 'failure',
405
323
  summary,
406
324
  details,
407
325
  source: 'auto-capture'
408
- }).catch(() => {});
326
+ });
409
327
  }
410
328
 
411
329
  async function checkAutoDreamThreshold(config) {
@@ -421,6 +339,33 @@ async function checkAutoDreamThreshold(config) {
421
339
 
422
340
  // ─── Exported helpers ────────────────────────────────────────────────
423
341
 
342
+ function extractFileChange(toolName, result) {
343
+ if (!result || typeof result !== 'object') return null;
344
+ const FILE_TOOLS = new Set(['edit', 'write', 'delete']);
345
+ if (!FILE_TOOLS.has(toolName)) return null;
346
+
347
+ /* delete */
348
+ if ('deleted' in result && result.deleted) {
349
+ return { path: String(result.path || ''), action: 'delete', linesAdded: 0, linesRemoved: 0 };
350
+ }
351
+
352
+ /* edit / write */
353
+ if ('path' in result && 'action' in result) {
354
+ const action = String(result.action || '');
355
+ const isCreate = action === 'create';
356
+ const added = Number(result.lines_added || 0);
357
+ const removed = Number(result.lines_removed || 0);
358
+ return {
359
+ path: String(result.path || ''),
360
+ action: isCreate ? 'create' : 'edit',
361
+ linesAdded: added,
362
+ linesRemoved: removed
363
+ };
364
+ }
365
+
366
+ return null;
367
+ }
368
+
424
369
  export function summarizeToolResult(result) {
425
370
  if (result === null || result === undefined) return 'no output';
426
371
  if (typeof result === 'string') {
@@ -640,7 +585,7 @@ function blockedExplorationReason(toolName, args, state) {
640
585
  const top = topLevelPath(target);
641
586
  if (!top) return '';
642
587
 
643
- if (['skills', 'souls', 'templates', '.codemini', '.codemini-project'].includes(top)) {
588
+ if (['skills', 'souls', 'templates', '.codemini', '.codemini-global'].includes(top)) {
644
589
  return `Skip ${top}/ for broad repository analysis unless the user explicitly asks for it. Inspect relevant source files first.`;
645
590
  }
646
591
  return '';
@@ -709,6 +654,14 @@ function formatToolDisplayName(name, args) {
709
654
  const command = trimInline(args?.command || '', 96);
710
655
  return command ? `run(${command})` : name;
711
656
  }
657
+ if (name === 'web_fetch') {
658
+ const url = trimInline(args?.url || args?.href || '', 96);
659
+ return url ? `web_fetch(${url})` : name;
660
+ }
661
+ if (name === 'web_search') {
662
+ const query = trimInline(args?.query || args?.q || '', 96);
663
+ return query ? `web_search(${query})` : name;
664
+ }
712
665
  if (name === 'edit') {
713
666
  const target = trimInline(args?.path || args?.file || '.', 96) || '.';
714
667
  return `edit(${target})`;
@@ -736,14 +689,17 @@ function formatToolDisplayName(name, args) {
736
689
  // ─── Format a single tool result using per-tool formatter or fallback ──
737
690
 
738
691
  function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
692
+ const sanitizeOptions = getToolOutputSanitizeOptions(toolName);
739
693
  if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
740
694
  const formatted = toolFormatters[toolName](toolResult, args);
741
695
  if (typeof formatted === 'string') {
742
- return formatted.trim() ? formatted : emptyToolResultMarker(toolName);
696
+ const sanitized = sanitizeTextForModel(formatted, sanitizeOptions);
697
+ return sanitized.trim() ? sanitized : emptyToolResultMarker(toolName);
743
698
  }
744
699
  }
745
700
  const fallback = compactToolResult(toolResult, toolName, args, toolResultMaxChars);
746
- return String(fallback || '').trim() ? fallback : emptyToolResultMarker(toolName);
701
+ const sanitizedFallback = sanitizeTextForModel(fallback, sanitizeOptions);
702
+ return String(sanitizedFallback || '').trim() ? sanitizedFallback : emptyToolResultMarker(toolName);
747
703
  }
748
704
 
749
705
  // ─── Main agent loop ────────────────────────────────────────────────
@@ -924,7 +880,11 @@ export async function runAgentLoop({
924
880
  let approved = true;
925
881
  let approvalArgs = args;
926
882
  let preflightErrorContent = '';
927
- const needsApproval = toolName === 'delete' || (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
883
+ const isSafeModeRun = toolName === 'run'
884
+ && config?.policy?.safe_mode !== false
885
+ && requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
886
+ const needsApproval = toolName === 'delete' || isSafeModeRun
887
+ || (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
928
888
  if (needsApproval) {
929
889
  approved = false;
930
890
  const handler = toolHandlers[toolName];
@@ -940,6 +900,31 @@ export async function runAgentLoop({
940
900
  preflightErrorContent = clipToolResult({ error: message }, toolResultMaxChars);
941
901
  }
942
902
  }
903
+ /* Run tool: safe mode LLM-based command evaluation */
904
+ if (toolName === 'run' && isSafeModeRun && !preflightErrorContent) {
905
+ try {
906
+ const { evaluateCommandWithLLM } = await import('./command-evaluator.js');
907
+ const evaluation = await evaluateCommandWithLLM({
908
+ command: args?.command || '',
909
+ config,
910
+ workspaceRoot: config?.workspaceRoot || process.cwd()
911
+ });
912
+ approvalArgs = { ...args, _risk: evaluation.risk, _evaluation: evaluation };
913
+ /* LLM says low-risk + allow → auto-approve, skip confirmation panel */
914
+ if (evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
915
+ approvalResults.set(call.id, { approved: true, args: approvalArgs });
916
+ continue;
917
+ }
918
+ } catch (_) {
919
+ approvalArgs = { ...args, _risk: 'high', _evaluation: null };
920
+ }
921
+ if (typeof handler?.prepareApproval === 'function') {
922
+ try {
923
+ const approval = await handler.prepareApproval(approvalArgs);
924
+ approvalArgs = { ...approvalArgs, approval };
925
+ } catch (_) { /* skip */ }
926
+ }
927
+ }
943
928
  if (preflightErrorContent) {
944
929
  approvalResults.set(call.id, {
945
930
  approved: false,
@@ -954,7 +939,8 @@ export async function runAgentLoop({
954
939
  name: toolName,
955
940
  displayName,
956
941
  arguments: approvalArgs,
957
- approvalDetails: toolName === 'delete' ? approvalArgs.approval : undefined
942
+ approvalDetails: toolName === 'delete' ? approvalArgs.approval
943
+ : (toolName === 'run' ? approvalArgs.approval : undefined)
958
944
  });
959
945
  approved = Boolean(decision?.approved);
960
946
  }
@@ -1025,7 +1011,7 @@ export async function runAgentLoop({
1025
1011
  onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
1026
1012
  }
1027
1013
  if (shouldAutoCaptureError(toolName, message)) {
1028
- fireAndForgetCapture(toolName, message, effectiveArgs);
1014
+ await captureToolFailure(toolName, message, effectiveArgs, config).catch(() => {});
1029
1015
  }
1030
1016
  return {
1031
1017
  callId: call.id,
@@ -1035,8 +1021,10 @@ export async function runAgentLoop({
1035
1021
  }
1036
1022
 
1037
1023
  const durationMs = Date.now() - startedAt;
1024
+ /* 提取文件改动统计 */
1025
+ const fileChange = extractFileChange(toolName, toolResult);
1038
1026
  if (onEvent) {
1039
- onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult) });
1027
+ onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult), fileChange });
1040
1028
  }
1041
1029
 
1042
1030
  // Auto-capture non-throwing tool failures (e.g. shell non-zero exit)
@@ -1046,13 +1034,13 @@ export async function runAgentLoop({
1046
1034
  if (typeof exitCode === 'number' && exitCode !== 0 && stderr) {
1047
1035
  const failMsg = `exit ${exitCode}: ${stderr.slice(0, 120)}`;
1048
1036
  if (shouldAutoCaptureError(toolName, failMsg)) {
1049
- fireAndForgetCapture(toolName, failMsg, effectiveArgs);
1037
+ await captureToolFailure(toolName, failMsg, effectiveArgs, config).catch(() => {});
1050
1038
  }
1051
1039
  }
1052
1040
  if (toolResult.error) {
1053
1041
  const errMsg = String(toolResult.error).slice(0, 120);
1054
1042
  if (shouldAutoCaptureError(toolName, errMsg)) {
1055
- fireAndForgetCapture(toolName, errMsg, effectiveArgs);
1043
+ await captureToolFailure(toolName, errMsg, effectiveArgs, config).catch(() => {});
1056
1044
  }
1057
1045
  }
1058
1046
  }