codemini-cli 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -1
- package/deployment.md +6 -6
- package/package.json +6 -1
- package/skills/brainstorm/SKILL.md +49 -29
- package/skills/superpowers-lite/SKILL.md +82 -90
- package/skills/writing-plans/SKILL.md +67 -0
- package/src/commands/chat.js +51 -47
- package/src/commands/doctor.js +27 -7
- package/src/commands/run.js +36 -28
- package/src/core/agent-loop.js +191 -10
- package/src/core/chat-runtime.js +170 -11
- 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 +7 -0
- package/src/core/constants.js +0 -1
- package/src/core/default-system-prompt.js +27 -0
- package/src/core/dream-audit.js +93 -0
- package/src/core/dream-consolidate.js +157 -0
- package/src/core/dream-evaluator.js +99 -0
- package/src/core/fff-adapter.js +386 -0
- package/src/core/memory-prompt.js +23 -0
- package/src/core/memory-store.js +228 -1
- package/src/core/paths.js +13 -1
- package/src/core/project-index.js +2 -2
- package/src/core/shell-profile.js +5 -1
- package/src/core/tool-output.js +184 -0
- package/src/core/tools.js +425 -110
- package/src/tui/chat-app.js +376 -47
- package/src/tui/tool-activity/presenters/system.js +1 -1
package/src/commands/chat.js
CHANGED
|
@@ -78,57 +78,61 @@ export async function handleChat(args) {
|
|
|
78
78
|
systemPrompt
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
try {
|
|
82
|
+
if (parsed.prompt) {
|
|
83
|
+
const result = await runtime.submit(parsed.prompt);
|
|
84
|
+
if (result.text) console.log(result.text);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
if (parsed.plain || !process.stdout.isTTY) {
|
|
89
|
+
await runPlainLoop(runtime);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const React = (await import('react')).default;
|
|
94
|
+
const { render } = await import('ink');
|
|
95
|
+
const { ChatApp } = await import('../tui/chat-app.js');
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
const instance = render(
|
|
98
|
+
React.createElement(ChatApp, {
|
|
99
|
+
runtime,
|
|
100
|
+
sessionId: session.id,
|
|
101
|
+
model: parsed.model || config.model.name,
|
|
102
|
+
sdkProvider: config.sdk?.provider || 'openai-compatible',
|
|
103
|
+
language: config.ui?.language || 'zh',
|
|
104
|
+
shellName: config.shell?.default || 'powershell',
|
|
105
|
+
safeMode: config.policy?.safe_mode !== false,
|
|
106
|
+
version: pkg.version
|
|
107
|
+
})
|
|
108
|
+
);
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
// Patch Ink's renderInteractiveFrame to never use clearTerminal.
|
|
111
|
+
// Ink calls clearTerminal (ESC[2J + ESC[H]) when the output frame exceeds
|
|
112
|
+
// the terminal viewport height, which resets the scroll position to the top
|
|
113
|
+
// and prevents the user from scrolling freely during streaming.
|
|
114
|
+
// By always using incremental logUpdate updates instead, old content scrolls
|
|
115
|
+
// into the terminal's scrollback naturally and the user can scroll freely.
|
|
116
|
+
const origRenderFrame = instance.renderInteractiveFrame;
|
|
117
|
+
instance.renderInteractiveFrame = function (output, outputHeight, staticOutput) {
|
|
118
|
+
const hasStaticOutput = staticOutput !== '';
|
|
119
|
+
const outputToRender = output + '\n';
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
if (hasStaticOutput) {
|
|
122
|
+
this.fullStaticOutput += staticOutput;
|
|
123
|
+
this.log.clear();
|
|
124
|
+
this.options.stdout.write(staticOutput);
|
|
125
|
+
this.log(outputToRender);
|
|
126
|
+
} else if (output !== this.lastOutput || this.log.isCursorDirty()) {
|
|
127
|
+
this.throttledLog(outputToRender);
|
|
128
|
+
}
|
|
129
|
+
this.lastOutput = output;
|
|
130
|
+
this.lastOutputToRender = outputToRender;
|
|
131
|
+
this.lastOutputHeight = outputHeight;
|
|
132
|
+
};
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
await instance.waitUntilExit();
|
|
135
|
+
} finally {
|
|
136
|
+
await runtime.dispose?.();
|
|
137
|
+
}
|
|
134
138
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { getConfigFilePath, getSessionsDir, getSkillsDir } from '../core/paths.js';
|
|
4
5
|
import { loadConfig } from '../core/config-store.js';
|
|
5
6
|
|
|
@@ -34,8 +35,20 @@ async function checkGateway(config) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
const
|
|
38
|
+
async function commandExists(command) {
|
|
39
|
+
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
40
|
+
const result = spawnSync(probe, [command], { stdio: 'ignore' });
|
|
41
|
+
return result.status === 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function handleDoctor({
|
|
45
|
+
loadConfigFn = loadConfig,
|
|
46
|
+
checkPathWritableFn = checkPathWritable,
|
|
47
|
+
checkGatewayFn = checkGateway,
|
|
48
|
+
commandExistsFn = commandExists,
|
|
49
|
+
writeLine = (line) => console.log(line)
|
|
50
|
+
} = {}) {
|
|
51
|
+
const config = await loadConfigFn();
|
|
39
52
|
const checks = [];
|
|
40
53
|
|
|
41
54
|
checks.push({
|
|
@@ -52,32 +65,39 @@ export async function handleDoctor() {
|
|
|
52
65
|
|
|
53
66
|
checks.push({
|
|
54
67
|
name: 'Config file writable',
|
|
55
|
-
ok: await
|
|
68
|
+
ok: await checkPathWritableFn(path.dirname(getConfigFilePath())),
|
|
56
69
|
detail: getConfigFilePath()
|
|
57
70
|
});
|
|
58
71
|
|
|
59
72
|
checks.push({
|
|
60
73
|
name: 'Sessions dir writable',
|
|
61
|
-
ok: await
|
|
74
|
+
ok: await checkPathWritableFn(getSessionsDir()),
|
|
62
75
|
detail: getSessionsDir()
|
|
63
76
|
});
|
|
64
77
|
|
|
65
78
|
checks.push({
|
|
66
79
|
name: 'Skills dir writable',
|
|
67
|
-
ok: await
|
|
80
|
+
ok: await checkPathWritableFn(getSkillsDir()),
|
|
68
81
|
detail: getSkillsDir()
|
|
69
82
|
});
|
|
70
83
|
|
|
71
|
-
const gateway = await
|
|
84
|
+
const gateway = await checkGatewayFn(config);
|
|
72
85
|
checks.push({
|
|
73
86
|
name: 'Gateway connectivity',
|
|
74
87
|
ok: gateway.ok,
|
|
75
88
|
detail: gateway.reason
|
|
76
89
|
});
|
|
77
90
|
|
|
91
|
+
const hasFff = await commandExistsFn('fff-mcp');
|
|
92
|
+
checks.push({
|
|
93
|
+
name: 'FFF MCP availability',
|
|
94
|
+
ok: hasFff,
|
|
95
|
+
detail: hasFff ? 'found fff-mcp' : 'fff-mcp not found in PATH'
|
|
96
|
+
});
|
|
97
|
+
|
|
78
98
|
for (const check of checks) {
|
|
79
99
|
const mark = check.ok ? 'OK' : 'FAIL';
|
|
80
|
-
|
|
100
|
+
writeLine(`[${mark}] ${check.name}: ${check.detail}`);
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
const failed = checks.filter((c) => !c.ok).length;
|
package/src/commands/run.js
CHANGED
|
@@ -85,25 +85,29 @@ async function runHarness({ role, task, config, systemPrompt, model, maxSteps })
|
|
|
85
85
|
if (!HARNESS_ROLES.includes(role)) {
|
|
86
86
|
throw new Error(`Unknown harness role: ${role}. Available: ${HARNESS_ROLES.join(', ')}`);
|
|
87
87
|
}
|
|
88
|
-
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
88
|
+
const { definitions, handlers, formatters, deferredDefinitions, dispose } = getBuiltinTools({
|
|
89
89
|
workspaceRoot: process.cwd(),
|
|
90
90
|
config
|
|
91
91
|
});
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
try {
|
|
93
|
+
const filtered = filterToolsForRole(definitions, handlers, deferredDefinitions, role);
|
|
94
|
+
const rolePrompt = getSubAgentRolePrompt(role);
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
const result = await runAgentLoop({
|
|
97
|
+
systemPrompt: `${systemPrompt}\n${rolePrompt}`,
|
|
98
|
+
userPrompt: task,
|
|
99
|
+
model: model || config.model.name,
|
|
100
|
+
toolDefinitions: filtered.definitions,
|
|
101
|
+
toolHandlers: filtered.handlers,
|
|
102
|
+
toolFormatters: formatters,
|
|
103
|
+
deferredDefinitions: filtered.deferredDefinitions,
|
|
104
|
+
maxSteps,
|
|
105
|
+
requestCompletion: makeCompletionFn(config)
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
} finally {
|
|
109
|
+
await dispose?.();
|
|
110
|
+
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
function extractJsonBlock(text) {
|
|
@@ -263,21 +267,25 @@ export async function handleRun(args) {
|
|
|
263
267
|
return;
|
|
264
268
|
}
|
|
265
269
|
|
|
266
|
-
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
270
|
+
const { definitions, handlers, formatters, deferredDefinitions, dispose } = getBuiltinTools({
|
|
267
271
|
workspaceRoot: process.cwd(),
|
|
268
272
|
config
|
|
269
273
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
274
|
+
try {
|
|
275
|
+
const result = await runAgentLoop({
|
|
276
|
+
systemPrompt,
|
|
277
|
+
userPrompt: parsed.task,
|
|
278
|
+
model: parsed.model || config.model.name,
|
|
279
|
+
toolDefinitions: definitions,
|
|
280
|
+
toolHandlers: handlers,
|
|
281
|
+
toolFormatters: formatters,
|
|
282
|
+
deferredDefinitions,
|
|
283
|
+
maxSteps: parsed.maxSteps,
|
|
284
|
+
requestCompletion: makeCompletionFn(config)
|
|
285
|
+
});
|
|
281
286
|
|
|
282
|
-
|
|
287
|
+
console.log(result.text);
|
|
288
|
+
} finally {
|
|
289
|
+
await dispose?.();
|
|
290
|
+
}
|
|
283
291
|
}
|
package/src/core/agent-loop.js
CHANGED
|
@@ -3,6 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
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
|
+
import { captureToInbox, listInbox } from './memory-store.js';
|
|
7
|
+
import { requiresApprovalEvaluation } from './command-risk.js';
|
|
8
|
+
import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* 安全解析 JSON 字符串。
|
|
@@ -161,7 +164,7 @@ function emptyToolResultMarker(toolName) {
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
function clipToolResult(result, maxChars = 12000) {
|
|
164
|
-
const raw = typeof result === 'string' ? result : JSON.stringify(result);
|
|
167
|
+
const raw = sanitizeTextForModel(typeof result === 'string' ? result : JSON.stringify(result));
|
|
165
168
|
if (!maxChars || raw.length <= maxChars) return raw;
|
|
166
169
|
return `${raw.slice(0, maxChars)}\n... [tool result truncated ${raw.length - maxChars} chars]`;
|
|
167
170
|
}
|
|
@@ -169,8 +172,9 @@ function clipToolResult(result, maxChars = 12000) {
|
|
|
169
172
|
function compactToolResult(result, toolName, args, maxChars = 12000) {
|
|
170
173
|
if (result === null || result === undefined) return 'no output';
|
|
171
174
|
if (typeof result === 'string') {
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
const sanitized = sanitizeTextForModel(result);
|
|
176
|
+
if (sanitized.length <= maxChars) return sanitized;
|
|
177
|
+
return `${sanitized.slice(0, maxChars)}\n... [tool result truncated ${sanitized.length - maxChars} chars, original: ${sanitized.length}]`;
|
|
174
178
|
}
|
|
175
179
|
if (typeof result !== 'object') return String(result);
|
|
176
180
|
|
|
@@ -361,12 +365,103 @@ export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
|
|
|
361
365
|
const READ_ONLY_TOOLS = new Set([
|
|
362
366
|
'read', 'grep', 'glob', 'list',
|
|
363
367
|
'ast_query', 'read_ast_node',
|
|
368
|
+
'web_fetch', 'web_search',
|
|
364
369
|
'list_background_tasks', 'get_background_task',
|
|
365
370
|
'read_plan'
|
|
366
371
|
]);
|
|
367
372
|
|
|
373
|
+
// ─── Auto-capture tool errors to dream loop inbox ────────────────────
|
|
374
|
+
|
|
375
|
+
const DREAM_AUTO_CAPTURE_TOOLS = new Set([
|
|
376
|
+
'edit', 'write', 'run', 'delete'
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const DREAM_AUTO_CAPTURE_COOLDOWN_MS = 60_000;
|
|
380
|
+
const lastAutoCaptureByTool = new Map();
|
|
381
|
+
|
|
382
|
+
function shouldAutoCaptureError(toolName, message) {
|
|
383
|
+
if (!DREAM_AUTO_CAPTURE_TOOLS.has(toolName)) return false;
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
const lastTime = lastAutoCaptureByTool.get(toolName) || 0;
|
|
386
|
+
if (now - lastTime < DREAM_AUTO_CAPTURE_COOLDOWN_MS) return false;
|
|
387
|
+
const noisePatterns = [
|
|
388
|
+
/file already exists/i,
|
|
389
|
+
/no such file/i,
|
|
390
|
+
/not found$/i,
|
|
391
|
+
/already exists$/i,
|
|
392
|
+
/cancelled/i,
|
|
393
|
+
/aborted/i,
|
|
394
|
+
/blocked by (?:safe mode|policy|dangerous command)/i,
|
|
395
|
+
/exit 127/i,
|
|
396
|
+
/command not found/i,
|
|
397
|
+
/permission denied/i,
|
|
398
|
+
/args\?\s/i,
|
|
399
|
+
/Raw tool arguments/i,
|
|
400
|
+
/edit requires/i,
|
|
401
|
+
/write requires/i,
|
|
402
|
+
/requires file/i,
|
|
403
|
+
/path.*outside workspace/i,
|
|
404
|
+
/escapes workspace/i
|
|
405
|
+
];
|
|
406
|
+
if (noisePatterns.some((p) => p.test(message))) return false;
|
|
407
|
+
lastAutoCaptureByTool.set(toolName, now);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function fireAndForgetCapture(toolName, message, args) {
|
|
412
|
+
const summary = `[${toolName}] ${String(message).slice(0, 120)}`;
|
|
413
|
+
const details = args
|
|
414
|
+
? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
|
|
415
|
+
: `Tool: ${toolName}\nError: ${message}`;
|
|
416
|
+
captureToInbox({
|
|
417
|
+
scope: 'auto',
|
|
418
|
+
type: 'failure',
|
|
419
|
+
summary,
|
|
420
|
+
details,
|
|
421
|
+
source: 'auto-capture'
|
|
422
|
+
}).catch(() => {});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function checkAutoDreamThreshold(config) {
|
|
426
|
+
const threshold = Number(config?.memory?.auto_dream_threshold || 10);
|
|
427
|
+
if (threshold <= 0) return false;
|
|
428
|
+
try {
|
|
429
|
+
const entries = await listInbox();
|
|
430
|
+
return entries.length >= threshold;
|
|
431
|
+
} catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
368
436
|
// ─── Exported helpers ────────────────────────────────────────────────
|
|
369
437
|
|
|
438
|
+
function extractFileChange(toolName, result) {
|
|
439
|
+
if (!result || typeof result !== 'object') return null;
|
|
440
|
+
const FILE_TOOLS = new Set(['edit', 'write', 'delete']);
|
|
441
|
+
if (!FILE_TOOLS.has(toolName)) return null;
|
|
442
|
+
|
|
443
|
+
/* delete */
|
|
444
|
+
if ('deleted' in result && result.deleted) {
|
|
445
|
+
return { path: String(result.path || ''), action: 'delete', linesAdded: 0, linesRemoved: 0 };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* edit / write */
|
|
449
|
+
if ('path' in result && 'action' in result) {
|
|
450
|
+
const action = String(result.action || '');
|
|
451
|
+
const isCreate = action === 'create';
|
|
452
|
+
const added = Number(result.lines_added || 0);
|
|
453
|
+
const removed = Number(result.lines_removed || 0);
|
|
454
|
+
return {
|
|
455
|
+
path: String(result.path || ''),
|
|
456
|
+
action: isCreate ? 'create' : 'edit',
|
|
457
|
+
linesAdded: added,
|
|
458
|
+
linesRemoved: removed
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
370
465
|
export function summarizeToolResult(result) {
|
|
371
466
|
if (result === null || result === undefined) return 'no output';
|
|
372
467
|
if (typeof result === 'string') {
|
|
@@ -586,7 +681,7 @@ function blockedExplorationReason(toolName, args, state) {
|
|
|
586
681
|
const top = topLevelPath(target);
|
|
587
682
|
if (!top) return '';
|
|
588
683
|
|
|
589
|
-
if (['skills', 'souls', 'templates', '.codemini', '.codemini-
|
|
684
|
+
if (['skills', 'souls', 'templates', '.codemini', '.codemini-global'].includes(top)) {
|
|
590
685
|
return `Skip ${top}/ for broad repository analysis unless the user explicitly asks for it. Inspect relevant source files first.`;
|
|
591
686
|
}
|
|
592
687
|
return '';
|
|
@@ -682,14 +777,17 @@ function formatToolDisplayName(name, args) {
|
|
|
682
777
|
// ─── Format a single tool result using per-tool formatter or fallback ──
|
|
683
778
|
|
|
684
779
|
function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
|
|
780
|
+
const sanitizeOptions = getToolOutputSanitizeOptions(toolName);
|
|
685
781
|
if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
|
|
686
782
|
const formatted = toolFormatters[toolName](toolResult, args);
|
|
687
783
|
if (typeof formatted === 'string') {
|
|
688
|
-
|
|
784
|
+
const sanitized = sanitizeTextForModel(formatted, sanitizeOptions);
|
|
785
|
+
return sanitized.trim() ? sanitized : emptyToolResultMarker(toolName);
|
|
689
786
|
}
|
|
690
787
|
}
|
|
691
788
|
const fallback = compactToolResult(toolResult, toolName, args, toolResultMaxChars);
|
|
692
|
-
|
|
789
|
+
const sanitizedFallback = sanitizeTextForModel(fallback, sanitizeOptions);
|
|
790
|
+
return String(sanitizedFallback || '').trim() ? sanitizedFallback : emptyToolResultMarker(toolName);
|
|
693
791
|
}
|
|
694
792
|
|
|
695
793
|
// ─── Main agent loop ────────────────────────────────────────────────
|
|
@@ -711,7 +809,8 @@ export async function runAgentLoop({
|
|
|
711
809
|
toolFormatters = {},
|
|
712
810
|
deferredDefinitions = {},
|
|
713
811
|
signal,
|
|
714
|
-
skipAnalysisNudge = false
|
|
812
|
+
skipAnalysisNudge = false,
|
|
813
|
+
config = {}
|
|
715
814
|
}) {
|
|
716
815
|
const messages = [];
|
|
717
816
|
if (systemPrompt) {
|
|
@@ -729,10 +828,36 @@ export async function runAgentLoop({
|
|
|
729
828
|
let pendingSummaryNudges = 0;
|
|
730
829
|
const analysisGuard = createAnalysisGuardState(userPrompt);
|
|
731
830
|
const alwaysAllowSet = new Set((Array.isArray(alwaysAllowTools) ? alwaysAllowTools : []).map((t) => String(t)));
|
|
831
|
+
let autoDreamChecked = false;
|
|
732
832
|
|
|
733
833
|
// Mutable tool list — grows as tool_search loads deferred tools
|
|
734
834
|
const activeTools = [...toolDefinitions];
|
|
735
835
|
|
|
836
|
+
async function maybeRunAutoDream() {
|
|
837
|
+
if (autoDreamChecked) return;
|
|
838
|
+
autoDreamChecked = true;
|
|
839
|
+
if (executionMode === 'plan') return;
|
|
840
|
+
const autoDreamResult = await checkAutoDreamThreshold(config);
|
|
841
|
+
if (!autoDreamResult) return;
|
|
842
|
+
const dreamTool = toolHandlers['dream_consolidate'];
|
|
843
|
+
if (typeof dreamTool !== 'function') return;
|
|
844
|
+
if (onEvent) onEvent({ type: 'dream:auto', message: 'inbox threshold reached' });
|
|
845
|
+
try {
|
|
846
|
+
const report = await dreamTool({});
|
|
847
|
+
if (onEvent) {
|
|
848
|
+
onEvent({ type: 'dream:complete', report });
|
|
849
|
+
}
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (onEvent) {
|
|
852
|
+
onEvent({
|
|
853
|
+
type: 'dream:complete',
|
|
854
|
+
report: { ok: false, error: String(error?.message || error || 'unknown dream error') }
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
// Auto-dream is best-effort; don't block the loop
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
736
861
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
737
862
|
// 检查是否已被用户中止
|
|
738
863
|
if (signal?.aborted) {
|
|
@@ -806,6 +931,7 @@ export async function runAgentLoop({
|
|
|
806
931
|
continue;
|
|
807
932
|
}
|
|
808
933
|
finalText = assistantText;
|
|
934
|
+
await maybeRunAutoDream();
|
|
809
935
|
return { text: finalText, messages, steps: step + 1 };
|
|
810
936
|
}
|
|
811
937
|
|
|
@@ -822,6 +948,7 @@ export async function runAgentLoop({
|
|
|
822
948
|
]
|
|
823
949
|
.filter(Boolean)
|
|
824
950
|
.join('\n');
|
|
951
|
+
await maybeRunAutoDream();
|
|
825
952
|
return { text: finalText.trim(), messages, steps: step + 1 };
|
|
826
953
|
}
|
|
827
954
|
|
|
@@ -841,7 +968,11 @@ export async function runAgentLoop({
|
|
|
841
968
|
let approved = true;
|
|
842
969
|
let approvalArgs = args;
|
|
843
970
|
let preflightErrorContent = '';
|
|
844
|
-
const
|
|
971
|
+
const isSafeModeRun = toolName === 'run'
|
|
972
|
+
&& config?.policy?.safe_mode !== false
|
|
973
|
+
&& requiresApprovalEvaluation(args?.command || '', config?.shell?.default);
|
|
974
|
+
const needsApproval = toolName === 'delete' || isSafeModeRun
|
|
975
|
+
|| (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
|
|
845
976
|
if (needsApproval) {
|
|
846
977
|
approved = false;
|
|
847
978
|
const handler = toolHandlers[toolName];
|
|
@@ -857,6 +988,31 @@ export async function runAgentLoop({
|
|
|
857
988
|
preflightErrorContent = clipToolResult({ error: message }, toolResultMaxChars);
|
|
858
989
|
}
|
|
859
990
|
}
|
|
991
|
+
/* Run tool: safe mode LLM-based command evaluation */
|
|
992
|
+
if (toolName === 'run' && isSafeModeRun && !preflightErrorContent) {
|
|
993
|
+
try {
|
|
994
|
+
const { evaluateCommandWithLLM } = await import('./command-evaluator.js');
|
|
995
|
+
const evaluation = await evaluateCommandWithLLM({
|
|
996
|
+
command: args?.command || '',
|
|
997
|
+
config,
|
|
998
|
+
workspaceRoot: config?.workspaceRoot || process.cwd()
|
|
999
|
+
});
|
|
1000
|
+
approvalArgs = { ...args, _risk: evaluation.risk, _evaluation: evaluation };
|
|
1001
|
+
/* LLM says low-risk + allow → auto-approve, skip confirmation panel */
|
|
1002
|
+
if (evaluation.risk === 'low' && evaluation.recommendation === 'allow') {
|
|
1003
|
+
approvalResults.set(call.id, { approved: true, args: approvalArgs });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
} catch (_) {
|
|
1007
|
+
approvalArgs = { ...args, _risk: 'high', _evaluation: null };
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof handler?.prepareApproval === 'function') {
|
|
1010
|
+
try {
|
|
1011
|
+
const approval = await handler.prepareApproval(approvalArgs);
|
|
1012
|
+
approvalArgs = { ...approvalArgs, approval };
|
|
1013
|
+
} catch (_) { /* skip */ }
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
860
1016
|
if (preflightErrorContent) {
|
|
861
1017
|
approvalResults.set(call.id, {
|
|
862
1018
|
approved: false,
|
|
@@ -871,7 +1027,8 @@ export async function runAgentLoop({
|
|
|
871
1027
|
name: toolName,
|
|
872
1028
|
displayName,
|
|
873
1029
|
arguments: approvalArgs,
|
|
874
|
-
approvalDetails: toolName === 'delete' ? approvalArgs.approval
|
|
1030
|
+
approvalDetails: toolName === 'delete' ? approvalArgs.approval
|
|
1031
|
+
: (toolName === 'run' ? approvalArgs.approval : undefined)
|
|
875
1032
|
});
|
|
876
1033
|
approved = Boolean(decision?.approved);
|
|
877
1034
|
}
|
|
@@ -941,6 +1098,9 @@ export async function runAgentLoop({
|
|
|
941
1098
|
if (onEvent) {
|
|
942
1099
|
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
|
|
943
1100
|
}
|
|
1101
|
+
if (shouldAutoCaptureError(toolName, message)) {
|
|
1102
|
+
fireAndForgetCapture(toolName, message, effectiveArgs);
|
|
1103
|
+
}
|
|
944
1104
|
return {
|
|
945
1105
|
callId: call.id,
|
|
946
1106
|
content: clipToolResult({ error: message }, toolResultMaxChars),
|
|
@@ -949,8 +1109,28 @@ export async function runAgentLoop({
|
|
|
949
1109
|
}
|
|
950
1110
|
|
|
951
1111
|
const durationMs = Date.now() - startedAt;
|
|
1112
|
+
/* 提取文件改动统计 */
|
|
1113
|
+
const fileChange = extractFileChange(toolName, toolResult);
|
|
952
1114
|
if (onEvent) {
|
|
953
|
-
onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult) });
|
|
1115
|
+
onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult), fileChange });
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Auto-capture non-throwing tool failures (e.g. shell non-zero exit)
|
|
1119
|
+
if (toolResult && typeof toolResult === 'object') {
|
|
1120
|
+
const exitCode = toolResult.code ?? toolResult.exitCode;
|
|
1121
|
+
const stderr = String(toolResult.stderr || '');
|
|
1122
|
+
if (typeof exitCode === 'number' && exitCode !== 0 && stderr) {
|
|
1123
|
+
const failMsg = `exit ${exitCode}: ${stderr.slice(0, 120)}`;
|
|
1124
|
+
if (shouldAutoCaptureError(toolName, failMsg)) {
|
|
1125
|
+
fireAndForgetCapture(toolName, failMsg, effectiveArgs);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
if (toolResult.error) {
|
|
1129
|
+
const errMsg = String(toolResult.error).slice(0, 120);
|
|
1130
|
+
if (shouldAutoCaptureError(toolName, errMsg)) {
|
|
1131
|
+
fireAndForgetCapture(toolName, errMsg, effectiveArgs);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
954
1134
|
}
|
|
955
1135
|
|
|
956
1136
|
// P1b: Use per-tool formatter if available, else fallback
|
|
@@ -1031,6 +1211,7 @@ export async function runAgentLoop({
|
|
|
1031
1211
|
}
|
|
1032
1212
|
|
|
1033
1213
|
const fallback = lastAssistantText || 'Stopped before final response.';
|
|
1214
|
+
await maybeRunAutoDream();
|
|
1034
1215
|
return {
|
|
1035
1216
|
text: `${fallback}\n\n[stopped] Reached max tool steps (${maxSteps}). Try a narrower prompt or increase execution.max_steps.`,
|
|
1036
1217
|
messages,
|