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 +50 -6
- package/deployment.md +6 -6
- package/package.json +3 -1
- package/src/core/agent-loop.js +103 -115
- package/src/core/chat-runtime.js +134 -6
- package/src/core/command-evaluator.js +66 -0
- package/src/core/command-policy.js +16 -0
- package/src/core/command-risk.js +148 -0
- package/src/core/config-store.js +2 -0
- package/src/core/constants.js +0 -1
- package/src/core/context-compact.js +32 -8
- package/src/core/default-system-prompt.js +15 -8
- package/src/core/dream-consolidate.js +54 -14
- package/src/core/dream-evaluator.js +99 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +3 -2
- package/src/core/paths.js +1 -1
- package/src/core/project-index.js +2 -2
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/shell-profile.js +13 -9
- package/src/core/tool-args.js +181 -0
- package/src/core/tool-output.js +184 -0
- package/src/core/tools.js +118 -315
- package/src/tui/chat-app.js +362 -45
- package/src/tui/tool-activity/presenters/misc.js +14 -0
- package/src/tui/tool-activity/presenters/system.js +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
"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"
|
package/src/core/agent-loop.js
CHANGED
|
@@ -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
|
-
|
|
174
|
-
|
|
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
|
|
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: '
|
|
320
|
+
await captureToInbox({
|
|
321
|
+
scope: 'repo',
|
|
404
322
|
type: 'failure',
|
|
405
323
|
summary,
|
|
406
324
|
details,
|
|
407
325
|
source: 'auto-capture'
|
|
408
|
-
})
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1043
|
+
await captureToolFailure(toolName, errMsg, effectiveArgs, config).catch(() => {});
|
|
1056
1044
|
}
|
|
1057
1045
|
}
|
|
1058
1046
|
}
|