codemini-cli 0.4.0 → 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 +6 -6
- package/deployment.md +5 -5
- package/package.json +1 -1
- package/src/core/agent-loop.js +17 -105
- package/src/core/chat-runtime.js +84 -1
- package/src/core/config-store.js +2 -0
- package/src/core/context-compact.js +32 -8
- package/src/core/default-system-prompt.js +5 -5
- package/src/core/fff-adapter.js +1 -1
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/shell-profile.js +8 -8
- package/src/core/tool-args.js +181 -0
- package/src/core/tools.js +18 -160
- package/src/tui/chat-app.js +23 -1
- package/src/tui/tool-activity/presenters/misc.js +14 -0
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ Typical flow:
|
|
|
144
144
|
- Unified shell execution model:
|
|
145
145
|
- one-off commands via `run`
|
|
146
146
|
- long-running commands via `run` with `run_in_background=true`
|
|
147
|
-
- Lightweight project index under `.codemini
|
|
147
|
+
- Lightweight project index under `.codemini/`
|
|
148
148
|
- Tree-sitter based structured editing for function, class, and method-level changes
|
|
149
149
|
- Reply language control via `ui.reply_language`
|
|
150
150
|
- Safe mode enabled by default
|
|
@@ -179,7 +179,7 @@ Execution mode behavior:
|
|
|
179
179
|
|
|
180
180
|
### Project Index
|
|
181
181
|
|
|
182
|
-
CodeMini CLI maintains a lightweight project index inside `.codemini
|
|
182
|
+
CodeMini CLI maintains a lightweight project index inside `.codemini/`:
|
|
183
183
|
|
|
184
184
|
- `project-map.json` — high-level repository facts such as languages, source roots, test roots, and entry candidates
|
|
185
185
|
- `file-index.json` — per-file structure such as imports, exports, functions, classes, and lightweight symbol hints
|
|
@@ -191,7 +191,7 @@ The index is initialized when entering a project and refreshed incrementally aft
|
|
|
191
191
|
|
|
192
192
|
- Global session state: `<base-config-dir>/sessions/`
|
|
193
193
|
- Project workspace state: `.codemini/`
|
|
194
|
-
- Lightweight project index: `.codemini
|
|
194
|
+
- Lightweight project index: `.codemini/`
|
|
195
195
|
- Bundled repo skills: `skills/<name>/SKILL.md`
|
|
196
196
|
- Project-scoped skills: `.codemini/skills/<name>/SKILL.md`
|
|
197
197
|
- Global installed skills: `<base-config-dir>/skills/<name>/SKILL.md`
|
|
@@ -377,7 +377,7 @@ CodeMini CLI 把工具分成两层:
|
|
|
377
377
|
- 统一的 shell 执行模型:
|
|
378
378
|
- 一次性命令直接 `run`
|
|
379
379
|
- 长运行命令通过 `run` + `run_in_background=true`
|
|
380
|
-
- 在 `.codemini
|
|
380
|
+
- 在 `.codemini/` 下维护轻量项目索引,帮助模型更快理解仓库
|
|
381
381
|
- 基于 Tree-sitter 的结构化编辑能力,适合函数级、类级、方法级改动
|
|
382
382
|
- 支持通过 `ui.reply_language` 控制回复语言
|
|
383
383
|
- safe mode 默认开启
|
|
@@ -412,7 +412,7 @@ Inbox 和持久记忆的区别:
|
|
|
412
412
|
|
|
413
413
|
### 项目索引
|
|
414
414
|
|
|
415
|
-
CodeMini CLI 会在 `.codemini
|
|
415
|
+
CodeMini CLI 会在 `.codemini/` 下维护一份轻量项目索引:
|
|
416
416
|
|
|
417
417
|
- `project-map.json` — 记录仓库的高层结构事实,比如语言、源码目录、测试目录、入口候选
|
|
418
418
|
- `file-index.json` — 记录文件级结构信息,比如 imports、exports、functions、classes 和轻量 symbol 提示
|
|
@@ -424,7 +424,7 @@ CodeMini CLI 会在 `.codemini-project/` 下维护一份轻量项目索引:
|
|
|
424
424
|
|
|
425
425
|
- 全局会话状态:`<base-config-dir>/sessions/`
|
|
426
426
|
- 项目工作区状态:`.codemini/`
|
|
427
|
-
- 轻量项目索引:`.codemini
|
|
427
|
+
- 轻量项目索引:`.codemini/`
|
|
428
428
|
- 仓库内置 skill:`skills/<name>/SKILL.md`
|
|
429
429
|
- 项目级 skill:`.codemini/skills/<name>/SKILL.md`
|
|
430
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.4.
|
|
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.4.
|
|
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.4.
|
|
37
|
+
C:\temp\codemini-cli-0.4.1.tgz
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## 3. Environment Requirements
|
|
@@ -58,7 +58,7 @@ npm -v
|
|
|
58
58
|
Global install:
|
|
59
59
|
|
|
60
60
|
```powershell
|
|
61
|
-
npm install -g C:\temp\codemini-cli-0.4.
|
|
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.4.
|
|
69
|
+
npm install C:\temp\codemini-cli-0.4.1.tgz
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## 5. Confirm Installation
|
package/package.json
CHANGED
package/src/core/agent-loop.js
CHANGED
|
@@ -6,6 +6,7 @@ import { trimInline as _trimInline, normalizePath } from './string-utils.js';
|
|
|
6
6
|
import { captureToInbox, listInbox } from './memory-store.js';
|
|
7
7
|
import { requiresApprovalEvaluation } from './command-risk.js';
|
|
8
8
|
import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
|
|
9
|
+
import { normalizeToolArguments } from './tool-args.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 安全解析 JSON 字符串。
|
|
@@ -25,20 +26,6 @@ function safeJsonParse(raw) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function parseInlineRangePath(value) {
|
|
29
|
-
const text = String(value || '').trim();
|
|
30
|
-
if (!text) return null;
|
|
31
|
-
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
32
|
-
if (!match) return null;
|
|
33
|
-
const [, maybePath, startRaw, endRaw] = match;
|
|
34
|
-
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
35
|
-
const start = Number(startRaw);
|
|
36
|
-
const end = Number(endRaw || startRaw);
|
|
37
|
-
if (!Number.isFinite(start) || start <= 0) return null;
|
|
38
|
-
if (!Number.isFinite(end) || end < start) return null;
|
|
39
|
-
return { path: maybePath, start_line: start, end_line: end };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
29
|
function buildDeleteApprovalDetails(source, rawPath) {
|
|
43
30
|
const existing =
|
|
44
31
|
source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
|
|
@@ -74,90 +61,6 @@ function buildDeleteCancellationResult(args) {
|
|
|
74
61
|
};
|
|
75
62
|
}
|
|
76
63
|
|
|
77
|
-
function normalizeToolArguments(toolName, args, rawArguments) {
|
|
78
|
-
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
79
|
-
const primitive =
|
|
80
|
-
args == null || Array.isArray(args) || typeof args !== 'object'
|
|
81
|
-
? args
|
|
82
|
-
: null;
|
|
83
|
-
const source =
|
|
84
|
-
args && typeof args === 'object' && !Array.isArray(args)
|
|
85
|
-
? { ...args }
|
|
86
|
-
: {};
|
|
87
|
-
|
|
88
|
-
if (primitive != null && typeof primitive !== 'object') {
|
|
89
|
-
source._raw = rawText || String(primitive);
|
|
90
|
-
} else if (!source._raw && rawText && source._invalid_json) {
|
|
91
|
-
source._raw = rawText;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const stringValue =
|
|
95
|
-
typeof primitive === 'string'
|
|
96
|
-
? primitive.trim()
|
|
97
|
-
: String(source._raw || '').trim();
|
|
98
|
-
|
|
99
|
-
if (toolName === 'read') {
|
|
100
|
-
const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
|
|
101
|
-
if (value) source.path = value;
|
|
102
|
-
if (source.offset != null && source.start_line == null) source.start_line = source.offset;
|
|
103
|
-
if (source.limit != null && source.end_line == null && Number(source.start_line) > 0) {
|
|
104
|
-
source.end_line = Number(source.start_line) + Number(source.limit) - 1;
|
|
105
|
-
}
|
|
106
|
-
const range = parseInlineRangePath(source.path);
|
|
107
|
-
if (range) {
|
|
108
|
-
source.path = range.path;
|
|
109
|
-
if (source.start_line == null) source.start_line = range.start_line;
|
|
110
|
-
if (source.end_line == null) source.end_line = range.end_line;
|
|
111
|
-
}
|
|
112
|
-
return source;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (toolName === 'list') {
|
|
116
|
-
const value = String(source.path || source.dir || source.directory || stringValue || '.').trim();
|
|
117
|
-
return { ...source, path: value || '.' };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (toolName === 'glob') {
|
|
121
|
-
const pattern = String(source.pattern || source.glob || source.query || stringValue || '').trim();
|
|
122
|
-
if (pattern) source.pattern = pattern;
|
|
123
|
-
if (!source.path && source.directory) source.path = source.directory;
|
|
124
|
-
return source;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (toolName === 'grep') {
|
|
128
|
-
const pattern = String(source.pattern || source.query || source.symbol || source.q || stringValue || '').trim();
|
|
129
|
-
if (pattern) source.pattern = pattern;
|
|
130
|
-
if (!source.path && (source.directory || source.dir || source.cwd)) {
|
|
131
|
-
source.path = source.directory || source.dir || source.cwd;
|
|
132
|
-
}
|
|
133
|
-
return source;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (toolName === 'write') {
|
|
137
|
-
const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
|
|
138
|
-
if (value) source.path = value;
|
|
139
|
-
if (source.content == null && source.text != null) source.content = source.text;
|
|
140
|
-
if (source.content == null && source.new_content != null) source.content = source.new_content;
|
|
141
|
-
return source;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (toolName === 'edit') {
|
|
145
|
-
const value = String(source.path || source.file || source.file_path || '').trim();
|
|
146
|
-
if (value && !source.path) source.path = value;
|
|
147
|
-
return source;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (toolName === 'delete') {
|
|
151
|
-
const value = String(source.path || source.file_path || source.file || source.target || source.directory || source.dir || stringValue || '').trim();
|
|
152
|
-
if (value) source.path = value;
|
|
153
|
-
const approval = buildDeleteApprovalDetails(source, source.path);
|
|
154
|
-
if (approval) source.approval = approval;
|
|
155
|
-
return source;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return source;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
64
|
function emptyToolResultMarker(toolName) {
|
|
162
65
|
const name = String(toolName || 'tool').trim() || 'tool';
|
|
163
66
|
return `(${name} completed with no output)`;
|
|
@@ -408,18 +311,19 @@ function shouldAutoCaptureError(toolName, message) {
|
|
|
408
311
|
return true;
|
|
409
312
|
}
|
|
410
313
|
|
|
411
|
-
function
|
|
314
|
+
async function captureToolFailure(toolName, message, args, config = {}) {
|
|
315
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return;
|
|
412
316
|
const summary = `[${toolName}] ${String(message).slice(0, 120)}`;
|
|
413
317
|
const details = args
|
|
414
318
|
? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
|
|
415
319
|
: `Tool: ${toolName}\nError: ${message}`;
|
|
416
|
-
captureToInbox({
|
|
417
|
-
scope: '
|
|
320
|
+
await captureToInbox({
|
|
321
|
+
scope: 'repo',
|
|
418
322
|
type: 'failure',
|
|
419
323
|
summary,
|
|
420
324
|
details,
|
|
421
325
|
source: 'auto-capture'
|
|
422
|
-
})
|
|
326
|
+
});
|
|
423
327
|
}
|
|
424
328
|
|
|
425
329
|
async function checkAutoDreamThreshold(config) {
|
|
@@ -750,6 +654,14 @@ function formatToolDisplayName(name, args) {
|
|
|
750
654
|
const command = trimInline(args?.command || '', 96);
|
|
751
655
|
return command ? `run(${command})` : name;
|
|
752
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
|
+
}
|
|
753
665
|
if (name === 'edit') {
|
|
754
666
|
const target = trimInline(args?.path || args?.file || '.', 96) || '.';
|
|
755
667
|
return `edit(${target})`;
|
|
@@ -1099,7 +1011,7 @@ export async function runAgentLoop({
|
|
|
1099
1011
|
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
|
|
1100
1012
|
}
|
|
1101
1013
|
if (shouldAutoCaptureError(toolName, message)) {
|
|
1102
|
-
|
|
1014
|
+
await captureToolFailure(toolName, message, effectiveArgs, config).catch(() => {});
|
|
1103
1015
|
}
|
|
1104
1016
|
return {
|
|
1105
1017
|
callId: call.id,
|
|
@@ -1122,13 +1034,13 @@ export async function runAgentLoop({
|
|
|
1122
1034
|
if (typeof exitCode === 'number' && exitCode !== 0 && stderr) {
|
|
1123
1035
|
const failMsg = `exit ${exitCode}: ${stderr.slice(0, 120)}`;
|
|
1124
1036
|
if (shouldAutoCaptureError(toolName, failMsg)) {
|
|
1125
|
-
|
|
1037
|
+
await captureToolFailure(toolName, failMsg, effectiveArgs, config).catch(() => {});
|
|
1126
1038
|
}
|
|
1127
1039
|
}
|
|
1128
1040
|
if (toolResult.error) {
|
|
1129
1041
|
const errMsg = String(toolResult.error).slice(0, 120);
|
|
1130
1042
|
if (shouldAutoCaptureError(toolName, errMsg)) {
|
|
1131
|
-
|
|
1043
|
+
await captureToolFailure(toolName, errMsg, effectiveArgs, config).catch(() => {});
|
|
1132
1044
|
}
|
|
1133
1045
|
}
|
|
1134
1046
|
}
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -25,7 +25,7 @@ import { buildSystemPromptWithSoul } from './soul.js';
|
|
|
25
25
|
import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
|
|
26
26
|
import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
|
|
27
27
|
import { buildMemorySnapshot } from './memory-prompt.js';
|
|
28
|
-
import { forgetMemory, listMemories, searchMemories, captureToInbox, listInbox } from './memory-store.js';
|
|
28
|
+
import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToInbox, listInbox } from './memory-store.js';
|
|
29
29
|
import { runDreamConsolidation } from './dream-consolidate.js';
|
|
30
30
|
import { normalizePlanState } from './plan-state.js';
|
|
31
31
|
import { countActiveTodos, normalizeTodos } from './todo-state.js';
|
|
@@ -3312,6 +3312,75 @@ export async function createChatRuntime({
|
|
|
3312
3312
|
await saveSession(currentSession);
|
|
3313
3313
|
};
|
|
3314
3314
|
|
|
3315
|
+
const captureCompactSummary = async ({ summary, mode, beforeTokens, afterTokens }) => {
|
|
3316
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return null;
|
|
3317
|
+
const normalizedSummary = String(summary || '').trim();
|
|
3318
|
+
if (!normalizedSummary) return null;
|
|
3319
|
+
const entrySummary = `Context compacted (${mode}): ${beforeTokens} -> ${afterTokens} tokens`;
|
|
3320
|
+
return captureToInbox({
|
|
3321
|
+
scope: 'repo',
|
|
3322
|
+
type: 'observation',
|
|
3323
|
+
summary: entrySummary,
|
|
3324
|
+
details: normalizedSummary,
|
|
3325
|
+
tags: ['compact', 'context-summary'],
|
|
3326
|
+
source: 'auto-compact'
|
|
3327
|
+
}).catch(() => null);
|
|
3328
|
+
};
|
|
3329
|
+
|
|
3330
|
+
const shouldAutoCaptureUserPrompt = (text) => {
|
|
3331
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return false;
|
|
3332
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
3333
|
+
if (value.length < 12) return false;
|
|
3334
|
+
const actionPattern =
|
|
3335
|
+
/\b(add|build|fix|implement|change|update|refactor|test|debug|remember|capture|continue|review)\b|实现|增加|添加|修复|修改|更新|重构|测试|调试|记住|继续|检查|沉淀|捕获/i;
|
|
3336
|
+
return actionPattern.test(value);
|
|
3337
|
+
};
|
|
3338
|
+
|
|
3339
|
+
const classifyDirectMemoryPrompt = (text) => {
|
|
3340
|
+
if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return null;
|
|
3341
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
3342
|
+
if (value.length < 6) return null;
|
|
3343
|
+
const userPreferencePattern =
|
|
3344
|
+
/(?:记住|请记住|以后|后续|我偏好|我的偏好|我喜欢|我习惯|不要再|别再|always remember|remember that|i prefer|my preference|don't|do not)/i;
|
|
3345
|
+
if (!userPreferencePattern.test(value)) return null;
|
|
3346
|
+
const projectPattern = /(?:本项目|这个项目|当前项目|这个仓库|当前仓库|repo|repository|project)/i;
|
|
3347
|
+
const isProject = projectPattern.test(value);
|
|
3348
|
+
return {
|
|
3349
|
+
scope: isProject ? 'project' : 'user',
|
|
3350
|
+
kind: isProject ? 'workflow' : 'preference',
|
|
3351
|
+
content: value
|
|
3352
|
+
};
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
const saveDirectMemoryPrompt = async (text) => {
|
|
3356
|
+
const direct = classifyDirectMemoryPrompt(text);
|
|
3357
|
+
if (!direct) return null;
|
|
3358
|
+
return rememberMemory({
|
|
3359
|
+
scope: direct.scope,
|
|
3360
|
+
content: direct.content,
|
|
3361
|
+
kind: direct.kind,
|
|
3362
|
+
summary: direct.content.slice(0, 80),
|
|
3363
|
+
source: 'auto-user-directive',
|
|
3364
|
+
replaceSimilar: true,
|
|
3365
|
+
workspaceRoot: process.cwd(),
|
|
3366
|
+
config
|
|
3367
|
+
}).catch(() => null);
|
|
3368
|
+
};
|
|
3369
|
+
|
|
3370
|
+
const captureUserPromptForDream = async (text) => {
|
|
3371
|
+
if (classifyDirectMemoryPrompt(text)) return null;
|
|
3372
|
+
if (!shouldAutoCaptureUserPrompt(text)) return null;
|
|
3373
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
3374
|
+
return captureToInbox({
|
|
3375
|
+
scope: 'repo',
|
|
3376
|
+
type: 'observation',
|
|
3377
|
+
summary: `User task: ${value.slice(0, 120)}`,
|
|
3378
|
+
details: value,
|
|
3379
|
+
tags: ['user-prompt'],
|
|
3380
|
+
source: 'auto-user-prompt'
|
|
3381
|
+
}).catch(() => null);
|
|
3382
|
+
};
|
|
3383
|
+
|
|
3315
3384
|
const buildActiveSystemPrompt = async () => {
|
|
3316
3385
|
const soulPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
|
|
3317
3386
|
const memorySnapshot = await buildMemorySnapshot({
|
|
@@ -4005,6 +4074,12 @@ export async function createChatRuntime({
|
|
|
4005
4074
|
compactState.backupMessages = structuredClone(currentSession.messages);
|
|
4006
4075
|
currentSession.messages = result.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
|
|
4007
4076
|
await saveSession(currentSession);
|
|
4077
|
+
await captureCompactSummary({
|
|
4078
|
+
summary: result.summary,
|
|
4079
|
+
mode: compactState.mode,
|
|
4080
|
+
beforeTokens,
|
|
4081
|
+
afterTokens
|
|
4082
|
+
});
|
|
4008
4083
|
await persistLocalExchange(line, report, { includeUser: false });
|
|
4009
4084
|
return { type: 'system', text: report };
|
|
4010
4085
|
}
|
|
@@ -4125,6 +4200,12 @@ export async function createChatRuntime({
|
|
|
4125
4200
|
at: new Date().toISOString()
|
|
4126
4201
|
}));
|
|
4127
4202
|
await saveSession(currentSession);
|
|
4203
|
+
await captureCompactSummary({
|
|
4204
|
+
summary: autoResult.summary,
|
|
4205
|
+
mode: compactState.mode,
|
|
4206
|
+
beforeTokens: currentTokens,
|
|
4207
|
+
afterTokens: estimateMessagesTokens(currentSession.messages)
|
|
4208
|
+
});
|
|
4128
4209
|
if (onAgentEvent) {
|
|
4129
4210
|
onAgentEvent({
|
|
4130
4211
|
type: 'compact:auto',
|
|
@@ -4137,6 +4218,7 @@ export async function createChatRuntime({
|
|
|
4137
4218
|
}
|
|
4138
4219
|
|
|
4139
4220
|
const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
|
|
4221
|
+
await saveDirectMemoryPrompt(expandedText);
|
|
4140
4222
|
const autoRoute = classifyAutoRoute(expandedText);
|
|
4141
4223
|
if (autoRoute.autoPlan) {
|
|
4142
4224
|
await maybeAutoDreamFromRuntime();
|
|
@@ -4188,6 +4270,7 @@ export async function createChatRuntime({
|
|
|
4188
4270
|
executionMode,
|
|
4189
4271
|
signal
|
|
4190
4272
|
});
|
|
4273
|
+
await captureUserPromptForDream(expandedText);
|
|
4191
4274
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
4192
4275
|
};
|
|
4193
4276
|
|
package/src/core/config-store.js
CHANGED
|
@@ -65,6 +65,7 @@ const DEFAULT_CONFIG = {
|
|
|
65
65
|
memory: {
|
|
66
66
|
enabled: true,
|
|
67
67
|
auto_write: true,
|
|
68
|
+
auto_capture: true,
|
|
68
69
|
inject_on_session_start: true,
|
|
69
70
|
auto_dream_threshold: 10,
|
|
70
71
|
max_items_per_scope: 12,
|
|
@@ -165,6 +166,7 @@ function normalizePolicyLists(config) {
|
|
|
165
166
|
next.memory = next.memory || {};
|
|
166
167
|
next.memory.enabled = next.memory.enabled !== false;
|
|
167
168
|
next.memory.auto_write = next.memory.auto_write !== false;
|
|
169
|
+
next.memory.auto_capture = next.memory.auto_capture !== false;
|
|
168
170
|
next.memory.inject_on_session_start = next.memory.inject_on_session_start !== false;
|
|
169
171
|
next.memory.max_items_per_scope = Math.max(1, Number(next.memory.max_items_per_scope || 12));
|
|
170
172
|
next.memory.auto_dream_threshold = Number(next.memory.auto_dream_threshold ?? 10);
|
|
@@ -37,20 +37,30 @@ function modeToKeepRecent(mode) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function buildLocalSummary(messages) {
|
|
40
|
-
const
|
|
40
|
+
const goal = [];
|
|
41
|
+
const constraints = [];
|
|
42
|
+
const changedFiles = new Set();
|
|
43
|
+
const verification = [];
|
|
44
|
+
const openThreads = [];
|
|
41
45
|
const limit = 16;
|
|
42
46
|
for (const msg of messages.slice(-limit)) {
|
|
43
47
|
if (msg.role === 'tool') {
|
|
44
|
-
// Try to parse tool result as JSON for semantic summary
|
|
45
48
|
const text = textFromContent(msg.content);
|
|
46
49
|
let parsed;
|
|
47
50
|
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
|
48
51
|
if (parsed && typeof parsed === 'object') {
|
|
49
52
|
const summary = summarizeToolResult(parsed);
|
|
50
|
-
|
|
53
|
+
if (parsed.path) changedFiles.add(String(parsed.path));
|
|
54
|
+
if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
|
|
55
|
+
verification.push(summary);
|
|
56
|
+
} else {
|
|
57
|
+
openThreads.push(`tool_result: ${summary}`);
|
|
58
|
+
}
|
|
51
59
|
} else {
|
|
52
60
|
const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
53
|
-
|
|
61
|
+
const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
|
|
62
|
+
if (match) changedFiles.add(match[1]);
|
|
63
|
+
openThreads.push(`tool_result: ${clipped}`);
|
|
54
64
|
}
|
|
55
65
|
continue;
|
|
56
66
|
}
|
|
@@ -59,21 +69,35 @@ function buildLocalSummary(messages) {
|
|
|
59
69
|
const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
|
|
60
70
|
const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
|
|
61
71
|
const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
62
|
-
|
|
72
|
+
if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
|
|
63
73
|
continue;
|
|
64
74
|
}
|
|
65
75
|
if (msg.role === 'user') {
|
|
66
76
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
67
77
|
const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
68
|
-
|
|
78
|
+
if (goal.length === 0) goal.push(clipped);
|
|
79
|
+
else constraints.push(clipped);
|
|
69
80
|
continue;
|
|
70
81
|
}
|
|
71
82
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
72
83
|
if (!text) continue;
|
|
73
84
|
const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
74
|
-
|
|
85
|
+
openThreads.push(`${msg.role}: ${clipped}`);
|
|
75
86
|
}
|
|
76
|
-
|
|
87
|
+
const lines = [
|
|
88
|
+
'Context Summary',
|
|
89
|
+
'Goal:',
|
|
90
|
+
goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
|
|
91
|
+
'Key Constraints:',
|
|
92
|
+
...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
|
|
93
|
+
'Changed Files:',
|
|
94
|
+
...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
|
|
95
|
+
'Verification:',
|
|
96
|
+
...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
|
|
97
|
+
'Open Threads:',
|
|
98
|
+
...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
|
|
99
|
+
];
|
|
100
|
+
return lines.join('\n').trim();
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
export function compactMessagesLocally(messages, { mode = 'default' } = {}) {
|
|
@@ -9,14 +9,14 @@ function getToolFewShotBlock() {
|
|
|
9
9
|
Use these as style examples for tool calls:
|
|
10
10
|
|
|
11
11
|
Current working directory: ${cwd}
|
|
12
|
-
When a tool takes
|
|
12
|
+
When a tool takes path, build it from the current working directory and prefer absolute paths.
|
|
13
13
|
If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
|
|
14
14
|
|
|
15
15
|
1. File discovery then read
|
|
16
16
|
User: compare the auth flow
|
|
17
17
|
Assistant: first narrow the search with the project index
|
|
18
18
|
Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
|
|
19
|
-
Tool: read({"
|
|
19
|
+
Tool: read({"path":"${cwd}/src/auth/service.ts"})
|
|
20
20
|
|
|
21
21
|
If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
|
|
22
22
|
Example:
|
|
@@ -27,7 +27,7 @@ Tool: glob({"pattern":"src/**/*.ts"})
|
|
|
27
27
|
User: rename loginUser to signInUser
|
|
28
28
|
Assistant: first find the exact occurrences
|
|
29
29
|
Tool: grep({"pattern":"loginUser","path":"src"})
|
|
30
|
-
Tool: edit({"
|
|
30
|
+
Tool: edit({"path":"${cwd}/src/auth/service.ts","old_text":"loginUser","new_text":"signInUser"})
|
|
31
31
|
|
|
32
32
|
3. Read a specific range
|
|
33
33
|
User: inspect the reducer around line 120
|
|
@@ -43,7 +43,7 @@ Assistant: keep the checklist updated as each phase finishes, and do not give a
|
|
|
43
43
|
5. Create a new file
|
|
44
44
|
User: add a notes file
|
|
45
45
|
Assistant: create the file directly
|
|
46
|
-
Tool: write({"
|
|
46
|
+
Tool: write({"path":"${cwd}/notes.txt","content":"todo\\n"})
|
|
47
47
|
|
|
48
48
|
6. Save a high-signal observation to memory
|
|
49
49
|
When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference — save it to persistent memory. Choose scope carefully:
|
|
@@ -73,7 +73,7 @@ Tool: tool_search({"query":"web_search"})
|
|
|
73
73
|
Tool: web_search({"query":"latest pnpm release","max_results":5})
|
|
74
74
|
|
|
75
75
|
Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
|
|
76
|
-
Prefer explicit absolute
|
|
76
|
+
Prefer explicit absolute path values when the current working directory is known.`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function getEnvBlock() {
|
package/src/core/fff-adapter.js
CHANGED
|
@@ -54,6 +54,36 @@ async function parseJsonResponse(response) {
|
|
|
54
54
|
return response.json();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function isRetryableStatus(status) {
|
|
58
|
+
return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isRetryableError(error) {
|
|
62
|
+
const name = String(error?.name || '');
|
|
63
|
+
if (name === 'AbortError' || name === 'TimeoutError') return false;
|
|
64
|
+
const message = String(error?.message || error || '');
|
|
65
|
+
return /fetch failed|network|socket|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchWithRetry(url, init, { maxRetries = 0 } = {}) {
|
|
69
|
+
const attempts = Math.max(0, Number(maxRetries) || 0) + 1;
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, init);
|
|
74
|
+
if (response.ok || !isRetryableStatus(response.status) || attempt === attempts - 1) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
await response.arrayBuffer().catch(() => null);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
if (!isRetryableError(error) || attempt === attempts - 1) throw error;
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
83
|
+
}
|
|
84
|
+
throw lastError || new Error('Gateway request failed');
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
async function* iterateSseEvents(stream) {
|
|
58
88
|
const decoder = new TextDecoder();
|
|
59
89
|
let buffer = '';
|
|
@@ -318,12 +348,12 @@ export async function createChatCompletion({
|
|
|
318
348
|
maxRetries = 2
|
|
319
349
|
}) {
|
|
320
350
|
const payload = buildPayload({ model, temperature, messages, tools });
|
|
321
|
-
const response = await
|
|
351
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
322
352
|
method: 'POST',
|
|
323
353
|
headers: createHeaders(apiKey),
|
|
324
354
|
body: JSON.stringify(payload),
|
|
325
355
|
signal: AbortSignal.timeout(timeoutMs)
|
|
326
|
-
});
|
|
356
|
+
}, { maxRetries });
|
|
327
357
|
const data = await parseJsonResponse(response);
|
|
328
358
|
const message = data?.choices?.[0]?.message || {};
|
|
329
359
|
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
@@ -386,12 +416,12 @@ export async function createChatCompletionStream({
|
|
|
386
416
|
}
|
|
387
417
|
}
|
|
388
418
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
389
|
-
const response = await
|
|
419
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
390
420
|
method: 'POST',
|
|
391
421
|
headers: createHeaders(apiKey),
|
|
392
422
|
body: JSON.stringify(payload),
|
|
393
423
|
signal: controller.signal
|
|
394
|
-
});
|
|
424
|
+
}, { maxRetries });
|
|
395
425
|
if (!response.ok || !response.body) {
|
|
396
426
|
const text = await response.text().catch(() => '');
|
|
397
427
|
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
@@ -402,7 +432,8 @@ export async function createChatCompletionStream({
|
|
|
402
432
|
let usage = null;
|
|
403
433
|
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
404
434
|
|
|
405
|
-
|
|
435
|
+
try {
|
|
436
|
+
for await (const chunk of iterateSseEvents(response.body)) {
|
|
406
437
|
usage = chunk?.usage || usage;
|
|
407
438
|
const choice0 = chunk?.choices?.[0] || {};
|
|
408
439
|
const delta = choice0?.delta || {};
|
|
@@ -452,6 +483,10 @@ export async function createChatCompletionStream({
|
|
|
452
483
|
if (choice0?.finish_reason) {
|
|
453
484
|
break;
|
|
454
485
|
}
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
timeoutSignal.removeEventListener('abort', onAbort);
|
|
489
|
+
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
|
455
490
|
}
|
|
456
491
|
|
|
457
492
|
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
@@ -146,11 +146,11 @@ export function getShellSystemPrompt(value) {
|
|
|
146
146
|
ALWAYS prefer dedicated tools over raw shell commands:
|
|
147
147
|
- The visible default tool list is intentionally small. If a needed capability is not currently listed, do not assume it is unavailable — call tool_search to load additional tools first
|
|
148
148
|
- Use query_project_index first for broad repository understanding. It combines project-map metadata with indexed file symbols so you can narrow candidates before reading source files
|
|
149
|
-
- Use read to inspect files — NEVER use cat, head, or tail via run.
|
|
149
|
+
- Use read to inspect files — NEVER use cat, head, or tail via run. Use canonical shapes like {path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {path:"src/app.ts", start_line:10, end_line:40}
|
|
150
150
|
- Use grep to search file contents — NEVER use grep or rg via run
|
|
151
151
|
- Use list for directory-by-directory filesystem discovery. If you specifically need pattern-based file lookup like src/**/*.ts, load glob with tool_search instead of falling back to run
|
|
152
|
-
- Use edit to modify existing files — this is the DEFAULT path for code changes.
|
|
153
|
-
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files).
|
|
152
|
+
- Use edit to modify existing files — this is the DEFAULT path for code changes. Prefer {path:"src/app.ts", old_text:"foo", new_text:"bar"}
|
|
153
|
+
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Prefer {path:"notes.txt", content:"..."}
|
|
154
154
|
- Use update_todos to manage the session todo checklist for complex work. Provide the full current list each time and usually keep exactly one item in_progress
|
|
155
155
|
- Use read_plan and update_plan to recover or sync structured plan state when plan progress was interrupted (for example by transient gateway/model errors)
|
|
156
156
|
- Use run for shell commands. For long-running processes (dev servers, watchers), set run_in_background=true when you know you do not need the final result immediately. Long-running commands may also be backgrounded automatically
|
|
@@ -181,15 +181,15 @@ For background commands: use run to launch. If you need management tools that ar
|
|
|
181
181
|
Common tool call patterns:
|
|
182
182
|
- Query the project index first: {query:"login auth flow", path:"src", max_results:5}
|
|
183
183
|
- Load a deferred tool when needed: {query:"glob"} or {query:"all"}
|
|
184
|
-
- Read a file: {path:"src/app.ts"} or {
|
|
184
|
+
- Read a file: {path:"src/app.ts"} or {path:"src/app.ts", start_line:20, end_line:60}
|
|
185
185
|
- Read a specific range inline: {path:"src/app.ts:20-60"}
|
|
186
186
|
- Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
|
|
187
187
|
- List a directory first: {path:"src"}
|
|
188
188
|
- After loading glob, find files by pattern: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
|
|
189
|
-
- Edit exact text: {
|
|
189
|
+
- Edit exact text: {path:"src/app.ts", old_text:"foo", new_text:"bar"}
|
|
190
190
|
- Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
|
|
191
|
-
- Write a new file: {
|
|
192
|
-
- When the environment provides a Working directory, prefer absolute
|
|
191
|
+
- Write a new file: {path:"notes.txt", content:"..."} or {path:"src/page.tsx", content:"..."}
|
|
192
|
+
- When the environment provides a Working directory, prefer absolute path values rooted there instead of guessing prefixes
|
|
193
193
|
- If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
|
|
194
194
|
|
|
195
195
|
# Doing tasks
|
|
@@ -218,7 +218,7 @@ Common tool call patterns:
|
|
|
218
218
|
- Keep answers compact and easy to scan
|
|
219
219
|
- Lead with the answer or next action, not scene-setting
|
|
220
220
|
- Do not restate the user's request unless a brief restatement prevents ambiguity
|
|
221
|
-
- When referencing code, use
|
|
221
|
+
- When referencing code, use path:line_number format
|
|
222
222
|
- Keep technical wording, commands, paths, and error details exact
|
|
223
223
|
- Only use emojis if the user explicitly requests it`;
|
|
224
224
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function parseInlineRangePath(value) {
|
|
4
|
+
const text = String(value || '').trim();
|
|
5
|
+
if (!text) return null;
|
|
6
|
+
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const [, maybePath, startRaw, endRaw] = match;
|
|
9
|
+
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
10
|
+
const startLine = Number(startRaw);
|
|
11
|
+
const endLine = Number(endRaw || startRaw);
|
|
12
|
+
if (!Number.isFinite(startLine) || startLine <= 0) return null;
|
|
13
|
+
if (!Number.isFinite(endLine) || endLine < startLine) return null;
|
|
14
|
+
return {
|
|
15
|
+
path: maybePath,
|
|
16
|
+
start_line: startLine,
|
|
17
|
+
end_line: endLine
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeReadArgs(rawArgs) {
|
|
22
|
+
const source =
|
|
23
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
24
|
+
? { ...rawArgs }
|
|
25
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
26
|
+
|
|
27
|
+
const normalized = { ...source };
|
|
28
|
+
const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
|
|
29
|
+
if (aliasPath) normalized.path = aliasPath;
|
|
30
|
+
|
|
31
|
+
if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
|
|
32
|
+
normalized.start_line = Number(source.offset);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
|
|
36
|
+
const startLine = Number(normalized.start_line);
|
|
37
|
+
const limit = Number(source.limit);
|
|
38
|
+
if (startLine > 0 && limit > 0) {
|
|
39
|
+
normalized.end_line = startLine + limit - 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const inlineRange = parseInlineRangePath(normalized.path);
|
|
44
|
+
if (inlineRange) {
|
|
45
|
+
normalized.path = inlineRange.path;
|
|
46
|
+
if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
|
|
47
|
+
if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizePathArgs(rawArgs, aliases = []) {
|
|
54
|
+
const source =
|
|
55
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
56
|
+
? { ...rawArgs }
|
|
57
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
58
|
+
const normalized = { ...source };
|
|
59
|
+
const keys = ['path', ...aliases];
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const value = String(source?.[key] || '').trim();
|
|
62
|
+
if (value) {
|
|
63
|
+
normalized.path = value;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
|
|
71
|
+
const source =
|
|
72
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
73
|
+
? { ...rawArgs }
|
|
74
|
+
: { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
75
|
+
const normalized = { ...source };
|
|
76
|
+
for (const key of ['pattern', ...aliases]) {
|
|
77
|
+
const value = String(source?.[key] || '').trim();
|
|
78
|
+
if (value) {
|
|
79
|
+
normalized.pattern = value;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const key of ['path', ...defaultPathAliases]) {
|
|
84
|
+
const value = String(source?.[key] || '').trim();
|
|
85
|
+
if (value) {
|
|
86
|
+
normalized.path = value;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeWriteArgs(rawArgs) {
|
|
94
|
+
const source =
|
|
95
|
+
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
96
|
+
? { ...rawArgs }
|
|
97
|
+
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
98
|
+
const normalized = { ...source };
|
|
99
|
+
const filePath = String(source.path || source.file_path || source.file || '').trim();
|
|
100
|
+
if (filePath) normalized.path = filePath;
|
|
101
|
+
if (normalized.content == null) {
|
|
102
|
+
if (source.text != null) normalized.content = source.text;
|
|
103
|
+
if (source.new_content != null) normalized.content = source.new_content;
|
|
104
|
+
}
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeWebFetchArgs(rawArgs) {
|
|
109
|
+
const normalized = normalizePathArgs(rawArgs, ['url', 'href', 'link', 'target']);
|
|
110
|
+
const url = String(normalized.url || normalized.path || '').trim();
|
|
111
|
+
return { ...normalized, url };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeWebSearchArgs(rawArgs) {
|
|
115
|
+
const normalized = normalizePatternArgs(rawArgs, ['query', 'q', 'keyword']);
|
|
116
|
+
const query = String(normalized.query || normalized.pattern || '').trim();
|
|
117
|
+
return { ...normalized, query };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildDeleteApprovalDetails(source, rawPath) {
|
|
121
|
+
const existing =
|
|
122
|
+
source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
|
|
123
|
+
? source.approval
|
|
124
|
+
: {};
|
|
125
|
+
const approvalPath = String(existing.path || rawPath || '').trim();
|
|
126
|
+
const approvalName = String(existing.name || (approvalPath ? path.basename(approvalPath) : '') || '').trim();
|
|
127
|
+
const approvalType = String(existing.type || '').trim();
|
|
128
|
+
|
|
129
|
+
const approval = {};
|
|
130
|
+
if (approvalPath) approval.path = approvalPath;
|
|
131
|
+
if (approvalName) approval.name = approvalName;
|
|
132
|
+
if (approvalType) approval.type = approvalType;
|
|
133
|
+
return Object.keys(approval).length > 0 ? approval : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function normalizeToolArguments(toolName, args, rawArguments) {
|
|
137
|
+
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
138
|
+
const primitive =
|
|
139
|
+
args == null || Array.isArray(args) || typeof args !== 'object'
|
|
140
|
+
? args
|
|
141
|
+
: null;
|
|
142
|
+
const source =
|
|
143
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
144
|
+
? { ...args }
|
|
145
|
+
: {};
|
|
146
|
+
|
|
147
|
+
if (primitive != null && typeof primitive !== 'object') {
|
|
148
|
+
source._raw = rawText || String(primitive);
|
|
149
|
+
} else if (!source._raw && rawText && source._invalid_json) {
|
|
150
|
+
source._raw = rawText;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const stringValue =
|
|
154
|
+
typeof primitive === 'string'
|
|
155
|
+
? primitive.trim()
|
|
156
|
+
: String(source._raw || '').trim();
|
|
157
|
+
|
|
158
|
+
if (toolName === 'read') return normalizeReadArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
|
|
159
|
+
if (toolName === 'list') return normalizePathArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) }, ['dir', 'directory']);
|
|
160
|
+
if (toolName === 'glob') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['glob', 'query'], ['directory']);
|
|
161
|
+
if (toolName === 'grep') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
|
|
162
|
+
if (toolName === 'write') return normalizeWriteArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
|
|
163
|
+
|
|
164
|
+
if (toolName === 'edit') {
|
|
165
|
+
const value = String(source.path || source.file || source.file_path || '').trim();
|
|
166
|
+
if (value && !source.path) source.path = value;
|
|
167
|
+
return source;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toolName === 'delete') {
|
|
171
|
+
const normalized = normalizePathArgs(
|
|
172
|
+
{ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) },
|
|
173
|
+
['file_path', 'file', 'target', 'directory', 'dir']
|
|
174
|
+
);
|
|
175
|
+
const approval = buildDeleteApprovalDetails(normalized, normalized.path);
|
|
176
|
+
if (approval) normalized.approval = approval;
|
|
177
|
+
return normalized;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return source;
|
|
181
|
+
}
|
package/src/core/tools.js
CHANGED
|
@@ -29,6 +29,14 @@ import {
|
|
|
29
29
|
sanitizeTextForModel,
|
|
30
30
|
summarizeRunOutput
|
|
31
31
|
} from './tool-output.js';
|
|
32
|
+
import {
|
|
33
|
+
normalizePathArgs,
|
|
34
|
+
normalizePatternArgs,
|
|
35
|
+
normalizeReadArgs,
|
|
36
|
+
normalizeWebFetchArgs,
|
|
37
|
+
normalizeWebSearchArgs,
|
|
38
|
+
normalizeWriteArgs
|
|
39
|
+
} from './tool-args.js';
|
|
32
40
|
const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
|
|
33
41
|
const BACKGROUND_TASK_POLL_MS = 150;
|
|
34
42
|
const MAX_AST_ENCLOSING_BYTES = 300_000;
|
|
@@ -100,133 +108,6 @@ function splitLines(text) {
|
|
|
100
108
|
return String(text || '').split('\n');
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
function parseInlineReadRange(value) {
|
|
104
|
-
const text = String(value || '').trim();
|
|
105
|
-
if (!text) return null;
|
|
106
|
-
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
107
|
-
if (!match) return null;
|
|
108
|
-
const [, maybePath, startRaw, endRaw] = match;
|
|
109
|
-
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
110
|
-
const startLine = Number(startRaw);
|
|
111
|
-
const endLine = Number(endRaw || startRaw);
|
|
112
|
-
if (!Number.isFinite(startLine) || startLine <= 0) return null;
|
|
113
|
-
if (!Number.isFinite(endLine) || endLine < startLine) return null;
|
|
114
|
-
return {
|
|
115
|
-
path: maybePath,
|
|
116
|
-
start_line: startLine,
|
|
117
|
-
end_line: endLine
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function normalizeReadArgs(rawArgs) {
|
|
122
|
-
const source =
|
|
123
|
-
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
124
|
-
? { ...rawArgs }
|
|
125
|
-
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
126
|
-
|
|
127
|
-
const normalized = { ...source };
|
|
128
|
-
const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
|
|
129
|
-
if (aliasPath) normalized.path = aliasPath;
|
|
130
|
-
|
|
131
|
-
if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
|
|
132
|
-
normalized.start_line = Number(source.offset);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
|
|
136
|
-
const startLine = Number(normalized.start_line);
|
|
137
|
-
const limit = Number(source.limit);
|
|
138
|
-
if (startLine > 0 && limit > 0) {
|
|
139
|
-
normalized.end_line = startLine + limit - 1;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const inlineRange = parseInlineReadRange(normalized.path);
|
|
144
|
-
if (inlineRange) {
|
|
145
|
-
normalized.path = inlineRange.path;
|
|
146
|
-
if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
|
|
147
|
-
if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return normalized;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function normalizePathArgs(rawArgs, aliases = []) {
|
|
154
|
-
const source =
|
|
155
|
-
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
156
|
-
? { ...rawArgs }
|
|
157
|
-
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
158
|
-
const normalized = { ...source };
|
|
159
|
-
const keys = ['path', ...aliases];
|
|
160
|
-
for (const key of keys) {
|
|
161
|
-
const value = String(source?.[key] || '').trim();
|
|
162
|
-
if (value) {
|
|
163
|
-
normalized.path = value;
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return normalized;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
|
|
171
|
-
const source =
|
|
172
|
-
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
173
|
-
? { ...rawArgs }
|
|
174
|
-
: { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
175
|
-
const normalized = { ...source };
|
|
176
|
-
for (const key of ['pattern', ...aliases]) {
|
|
177
|
-
const value = String(source?.[key] || '').trim();
|
|
178
|
-
if (value) {
|
|
179
|
-
normalized.pattern = value;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
for (const key of ['path', ...defaultPathAliases]) {
|
|
184
|
-
const value = String(source?.[key] || '').trim();
|
|
185
|
-
if (value) {
|
|
186
|
-
normalized.path = value;
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return normalized;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function normalizeWriteArgs(rawArgs) {
|
|
194
|
-
const source =
|
|
195
|
-
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
196
|
-
? { ...rawArgs }
|
|
197
|
-
: { path: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
198
|
-
const normalized = { ...source };
|
|
199
|
-
const filePath = String(source.path || source.file_path || source.file || '').trim();
|
|
200
|
-
if (filePath) normalized.path = filePath;
|
|
201
|
-
if (normalized.content == null) {
|
|
202
|
-
if (source.text != null) normalized.content = source.text;
|
|
203
|
-
if (source.new_content != null) normalized.content = source.new_content;
|
|
204
|
-
}
|
|
205
|
-
return normalized;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function normalizeWebFetchArgs(rawArgs) {
|
|
209
|
-
const source =
|
|
210
|
-
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
211
|
-
? { ...rawArgs }
|
|
212
|
-
: { url: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
213
|
-
const normalized = { ...source };
|
|
214
|
-
const url = String(source.url || source.href || source.link || source.target || '').trim();
|
|
215
|
-
if (url) normalized.url = url;
|
|
216
|
-
return normalized;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function normalizeWebSearchArgs(rawArgs) {
|
|
220
|
-
const source =
|
|
221
|
-
rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
|
|
222
|
-
? { ...rawArgs }
|
|
223
|
-
: { query: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
224
|
-
const normalized = { ...source };
|
|
225
|
-
const query = String(source.query || source.q || source.keyword || '').trim();
|
|
226
|
-
if (query) normalized.query = query;
|
|
227
|
-
return normalized;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
111
|
function clampNumber(value, min, max, fallback) {
|
|
231
112
|
const num = Number(value);
|
|
232
113
|
if (!Number.isFinite(num)) return fallback;
|
|
@@ -1819,20 +1700,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1819
1700
|
function: {
|
|
1820
1701
|
name: 'read',
|
|
1821
1702
|
description:
|
|
1822
|
-
'Inspect code or text files. Use read(path) for normal file or line-window reads
|
|
1703
|
+
'Inspect code or text files. Use read(path) for normal file or line-window reads. Use start_line and end_line for ranges, or path:"src/app.ts:10-40" for inline ranges. Prefer this over run with cat, head, or tail.',
|
|
1823
1704
|
parameters: {
|
|
1824
1705
|
type: 'object',
|
|
1825
1706
|
properties: {
|
|
1826
1707
|
path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
|
|
1827
|
-
file_path: { type: 'string', description: 'Alias for path' },
|
|
1828
1708
|
start_line: { type: 'number', description: '1-based start line' },
|
|
1829
1709
|
end_line: { type: 'number', description: 'Inclusive end line' },
|
|
1830
|
-
offset: { type: 'number', description: 'Alias for start_line' },
|
|
1831
|
-
limit: { type: 'number', description: 'Number of lines to read starting from offset/start_line' },
|
|
1832
1710
|
max_chars: { type: 'number', description: 'Max chars to return' },
|
|
1833
|
-
include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
|
|
1834
|
-
read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
|
|
1835
|
-
metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' },
|
|
1836
1711
|
ast_target: { type: 'object', description: 'AST target from ast_query or a prior AST selection. When provided, read returns that node instead of a line window.' },
|
|
1837
1712
|
query: { type: 'string', description: 'Optional Tree-sitter query to run inline before reading the first matched AST node. Use with path for one-shot function/class/method reads.' },
|
|
1838
1713
|
capture_name: { type: 'string', description: 'Optional capture name to select when query is provided.' },
|
|
@@ -1847,14 +1722,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1847
1722
|
function: {
|
|
1848
1723
|
name: 'grep',
|
|
1849
1724
|
description:
|
|
1850
|
-
'Search file contents. Use this for code search before read or edit.
|
|
1725
|
+
'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
|
|
1851
1726
|
parameters: {
|
|
1852
1727
|
type: 'object',
|
|
1853
1728
|
properties: {
|
|
1854
1729
|
pattern: { type: 'string', description: 'Search pattern' },
|
|
1855
|
-
query: { type: 'string', description: 'Alias for pattern' },
|
|
1856
1730
|
path: { type: 'string', description: 'Directory or file to search' },
|
|
1857
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
1858
1731
|
regex: { type: 'boolean', description: 'Treat pattern as regex' },
|
|
1859
1732
|
case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
|
|
1860
1733
|
max_results: { type: 'number', description: 'Max matches to return' },
|
|
@@ -1869,12 +1742,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1869
1742
|
type: 'function',
|
|
1870
1743
|
function: {
|
|
1871
1744
|
name: 'list',
|
|
1872
|
-
description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.
|
|
1745
|
+
description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
|
|
1873
1746
|
parameters: {
|
|
1874
1747
|
type: 'object',
|
|
1875
1748
|
properties: {
|
|
1876
1749
|
path: { type: 'string', description: 'Directory path to list' },
|
|
1877
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
1878
1750
|
include_hidden: { type: 'boolean', description: 'Include dotfiles' }
|
|
1879
1751
|
}
|
|
1880
1752
|
}
|
|
@@ -1885,14 +1757,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1885
1757
|
function: {
|
|
1886
1758
|
name: 'glob',
|
|
1887
1759
|
description:
|
|
1888
|
-
'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts.
|
|
1760
|
+
'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts.',
|
|
1889
1761
|
parameters: {
|
|
1890
1762
|
type: 'object',
|
|
1891
1763
|
properties: {
|
|
1892
1764
|
pattern: { type: 'string', description: 'Glob pattern' },
|
|
1893
1765
|
path: { type: 'string', description: 'Directory to search' },
|
|
1894
|
-
query: { type: 'string', description: 'Alias for pattern' },
|
|
1895
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
1896
1766
|
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
1897
1767
|
max_results: { type: 'number', description: 'Max results' }
|
|
1898
1768
|
},
|
|
@@ -1923,18 +1793,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1923
1793
|
function: {
|
|
1924
1794
|
name: 'edit',
|
|
1925
1795
|
description:
|
|
1926
|
-
'Edit existing files. Prefer one of these shapes: 1) {
|
|
1796
|
+
'Edit existing files. Prefer one of these shapes: 1) {path, old_text, new_text} for exact text replacement, 2) {path, symbol, edit:{kind:"replace_block", new_content:"..."}} for block replacement, 3) {path, anchor_text, position:"before"|"after", content:"..."} for inserts. Read first unless the exact target is already known. Prefer this over write for existing code changes.',
|
|
1927
1797
|
parameters: {
|
|
1928
1798
|
type: 'object',
|
|
1929
1799
|
properties: {
|
|
1930
|
-
|
|
1931
|
-
path: { type: 'string', description: 'Alias for file' },
|
|
1932
|
-
file_path: { type: 'string', description: 'Alias for file, compatible with simpler demo-style tool calls' },
|
|
1800
|
+
path: { type: 'string', description: 'File path to edit' },
|
|
1933
1801
|
new_content: { type: 'string', description: 'Replacement content' },
|
|
1934
1802
|
old_text: { type: 'string', description: 'Exact text to replace' },
|
|
1935
1803
|
new_text: { type: 'string', description: 'Replacement text' },
|
|
1936
|
-
old_string: { type: 'string', description: 'Alias for old_text' },
|
|
1937
|
-
new_string: { type: 'string', description: 'Alias for new_text' },
|
|
1938
1804
|
anchor_text: { type: 'string', description: 'Anchor text for inserts' },
|
|
1939
1805
|
content: { type: 'string', description: 'Content to insert or append' },
|
|
1940
1806
|
position: { type: 'string', description: 'before or after' },
|
|
@@ -1945,7 +1811,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1945
1811
|
line: { type: 'number', description: 'Line to target' },
|
|
1946
1812
|
edit: { type: 'object', description: 'Structured edit input' }
|
|
1947
1813
|
},
|
|
1948
|
-
required: ['
|
|
1814
|
+
required: ['path']
|
|
1949
1815
|
}
|
|
1950
1816
|
}
|
|
1951
1817
|
},
|
|
@@ -1954,16 +1820,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1954
1820
|
function: {
|
|
1955
1821
|
name: 'write',
|
|
1956
1822
|
description:
|
|
1957
|
-
'Create a new file or overwrite a file. Always include path and content.
|
|
1823
|
+
'Create a new file or overwrite a file. Always include path and content. Use this for new files or explicit full rewrites only. Example: {path:"src/page.html", content:"..."} . If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
|
|
1958
1824
|
parameters: {
|
|
1959
1825
|
type: 'object',
|
|
1960
1826
|
properties: {
|
|
1961
1827
|
path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
|
|
1962
|
-
file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
|
|
1963
|
-
file: { type: 'string', description: 'Alias for path' },
|
|
1964
1828
|
content: { type: 'string', description: 'Content to write' },
|
|
1965
|
-
text: { type: 'string', description: 'Alias for content' },
|
|
1966
|
-
new_content: { type: 'string', description: 'Alias for content' },
|
|
1967
1829
|
append: { type: 'boolean', description: 'Append instead of overwrite' },
|
|
1968
1830
|
full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
|
|
1969
1831
|
},
|
|
@@ -1976,15 +1838,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1976
1838
|
function: {
|
|
1977
1839
|
name: 'delete',
|
|
1978
1840
|
description:
|
|
1979
|
-
'Delete a file or directory inside the workspace.
|
|
1841
|
+
'Delete a file or directory inside the workspace. Missing targets fail. Workspace escape attempts are rejected.',
|
|
1980
1842
|
parameters: {
|
|
1981
1843
|
type: 'object',
|
|
1982
1844
|
properties: {
|
|
1983
|
-
path: { type: 'string', description: 'File or directory path to delete' }
|
|
1984
|
-
file: { type: 'string', description: 'Alias for path' },
|
|
1985
|
-
file_path: { type: 'string', description: 'Alias for path' },
|
|
1986
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
1987
|
-
dir: { type: 'string', description: 'Alias for path' }
|
|
1845
|
+
path: { type: 'string', description: 'File or directory path to delete' }
|
|
1988
1846
|
},
|
|
1989
1847
|
required: ['path']
|
|
1990
1848
|
}
|
package/src/tui/chat-app.js
CHANGED
|
@@ -180,6 +180,10 @@ const TUI_COPY = {
|
|
|
180
180
|
doingGlob: '正在按模式查找文件',
|
|
181
181
|
doneGrep: '已搜索关键词',
|
|
182
182
|
doingGrep: '正在搜索关键词',
|
|
183
|
+
doneWebFetch: '已抓取网页',
|
|
184
|
+
doingWebFetch: '正在抓取网页',
|
|
185
|
+
doneWebSearch: '已搜索网页',
|
|
186
|
+
doingWebSearch: '正在搜索网页',
|
|
183
187
|
doneCommand: '已执行命令',
|
|
184
188
|
doingCommand: '正在执行命令',
|
|
185
189
|
doneUpdateTodos: '已更新待办',
|
|
@@ -388,6 +392,10 @@ const TUI_COPY = {
|
|
|
388
392
|
doingGlob: 'Matching files by pattern',
|
|
389
393
|
doneGrep: 'Searched keywords',
|
|
390
394
|
doingGrep: 'Searching keywords',
|
|
395
|
+
doneWebFetch: 'Fetched page',
|
|
396
|
+
doingWebFetch: 'Fetching page',
|
|
397
|
+
doneWebSearch: 'Searched web',
|
|
398
|
+
doingWebSearch: 'Searching web',
|
|
391
399
|
doneCommand: 'Ran command',
|
|
392
400
|
doingCommand: 'Running command',
|
|
393
401
|
doneUpdateTodos: 'Updated todos',
|
|
@@ -1175,6 +1183,8 @@ function getActivityDisplayParts(activity) {
|
|
|
1175
1183
|
patch: 'Patch',
|
|
1176
1184
|
run: 'Run',
|
|
1177
1185
|
grep: 'Search',
|
|
1186
|
+
web_fetch: 'Fetch',
|
|
1187
|
+
web_search: 'Web Search',
|
|
1178
1188
|
glob: 'Glob',
|
|
1179
1189
|
list: 'List',
|
|
1180
1190
|
list_background_tasks: 'Tasks',
|
|
@@ -1193,6 +1203,8 @@ function getActivityDisplayParts(activity) {
|
|
|
1193
1203
|
patch: '🩹',
|
|
1194
1204
|
run: '⚙️',
|
|
1195
1205
|
grep: '🔍',
|
|
1206
|
+
web_fetch: '🌐',
|
|
1207
|
+
web_search: '🌐',
|
|
1196
1208
|
glob: '🧭',
|
|
1197
1209
|
list: '📂',
|
|
1198
1210
|
list_background_tasks: '🗃️',
|
|
@@ -2401,7 +2413,17 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
|
|
|
2401
2413
|
} else {
|
|
2402
2414
|
pushTextRows(msg?.text || '');
|
|
2403
2415
|
const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
|
|
2404
|
-
|
|
2416
|
+
const pendingToolCalls = Array.isArray(msg?.pendingToolCalls) ? msg.pendingToolCalls : [];
|
|
2417
|
+
const visibleCalls = [
|
|
2418
|
+
...toolCalls,
|
|
2419
|
+
...pendingToolCalls.filter((pending) => {
|
|
2420
|
+
if (!pending) return false;
|
|
2421
|
+
if (pending.id && toolCalls.some((tool) => tool?.id && tool.id === pending.id)) return false;
|
|
2422
|
+
const pendingBase = parseToolDisplayName(pending.name).base;
|
|
2423
|
+
return !toolCalls.some((tool) => parseToolDisplayName(tool?.name).base === pendingBase && tool?.status === 'running');
|
|
2424
|
+
})
|
|
2425
|
+
];
|
|
2426
|
+
visibleCalls.forEach((tool, idx) => pushActivityRows(tool, idx, visibleCalls.length));
|
|
2405
2427
|
}
|
|
2406
2428
|
|
|
2407
2429
|
const codeGenerationRows = getCodeGenerationActivityRows(msg);
|
|
@@ -12,5 +12,19 @@ export function describeMiscToolActivity(copy, parsed, rawName, { done = false,
|
|
|
12
12
|
if (parsed.base === 'update_todos') {
|
|
13
13
|
return blocked ? makeBlocked(copy, 'update_todos') : done ? copy.toolActivity.doneUpdateTodos : copy.toolActivity.doingUpdateTodos;
|
|
14
14
|
}
|
|
15
|
+
if (parsed.base === 'web_fetch') {
|
|
16
|
+
const target = parsed.target || parsed.raw;
|
|
17
|
+
const label = done
|
|
18
|
+
? (copy.toolActivity.doneWebFetch || copy.toolActivity.doneGeneric)
|
|
19
|
+
: (copy.toolActivity.doingWebFetch || copy.toolActivity.doingGeneric);
|
|
20
|
+
return blocked ? makeBlocked(copy, target) : `${label}: ${target}`;
|
|
21
|
+
}
|
|
22
|
+
if (parsed.base === 'web_search') {
|
|
23
|
+
const target = parsed.target || parsed.raw;
|
|
24
|
+
const label = done
|
|
25
|
+
? (copy.toolActivity.doneWebSearch || copy.toolActivity.doneGeneric)
|
|
26
|
+
: (copy.toolActivity.doingWebSearch || copy.toolActivity.doingGeneric);
|
|
27
|
+
return blocked ? makeBlocked(copy, target) : `${label}: ${target}`;
|
|
28
|
+
}
|
|
15
29
|
return blocked ? `${copy.toolActivity.blocked}: ${parsed.raw}` : done ? `${copy.toolActivity.doneGeneric}: ${parsed.raw}` : `${copy.toolActivity.doingGeneric}: ${parsed.raw}`;
|
|
16
30
|
}
|