codemini-cli 0.4.1 → 0.4.3
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/OPERATIONS.md +4 -2
- package/README.md +87 -7
- package/deployment.md +14 -7
- package/package.json +1 -3
- package/skills/grill-me/SKILL.md +30 -0
- package/skills/project-requirements/SKILL.md +245 -0
- package/skills/superpowers-lite/SKILL.md +5 -1
- package/src/cli.js +1 -1
- package/src/commands/run.js +5 -4
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +9 -214
- package/src/core/chat-runtime.js +520 -78
- package/src/core/command-loader.js +12 -5
- package/src/core/config-store.js +6 -3
- package/src/core/context-compact.js +2 -1
- package/src/core/dream-audit.js +12 -0
- package/src/core/dream-consolidate.js +131 -59
- package/src/core/dream-evaluator.js +86 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +145 -10
- package/src/core/provider/anthropic.js +2 -2
- package/src/core/provider/openai-compatible.js +2 -2
- package/src/core/reflect-skill.js +178 -0
- package/src/core/shell.js +1 -1
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +242 -69
- package/src/tui/chat-app.js +298 -48
- package/src/tui/tool-activity/presenters/system.js +6 -0
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
package/src/commands/skill.js
CHANGED
|
@@ -3,7 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { copyRecursive } from '../core/fs-utils.js';
|
|
6
|
-
import {
|
|
6
|
+
import { loadConfig, saveConfig } from '../core/config-store.js';
|
|
7
|
+
import { loadCommandsAndSkills } from '../core/command-loader.js';
|
|
8
|
+
import { getProjectSkillsDir, getSkillsDir } from '../core/paths.js';
|
|
7
9
|
import {
|
|
8
10
|
computeFileSha256,
|
|
9
11
|
readSkillRegistry,
|
|
@@ -11,17 +13,85 @@ import {
|
|
|
11
13
|
writeSkillRegistry
|
|
12
14
|
} from '../core/skill-registry.js';
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
function parseScopeArgs(args = [], { defaultScope = 'project', allowAll = false } = {}) {
|
|
17
|
+
let scope = defaultScope;
|
|
18
|
+
const rest = [];
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = String(args[index] || '');
|
|
21
|
+
if (arg === '--global') {
|
|
22
|
+
scope = 'global';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (arg === '--project') {
|
|
26
|
+
scope = 'project';
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === '--scope') {
|
|
30
|
+
const next = String(args[index + 1] || '').toLowerCase();
|
|
31
|
+
if (['project', 'global', ...(allowAll ? ['all', 'builtin'] : [])].includes(next)) {
|
|
32
|
+
scope = next;
|
|
33
|
+
index += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (arg.startsWith('--scope=')) {
|
|
38
|
+
const value = arg.slice('--scope='.length).toLowerCase();
|
|
39
|
+
if (['project', 'global', ...(allowAll ? ['all', 'builtin'] : [])].includes(value)) {
|
|
40
|
+
scope = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
rest.push(arg);
|
|
45
|
+
}
|
|
46
|
+
return { scope, rest };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function baseDirForScope(scope, cwd = process.cwd()) {
|
|
50
|
+
return scope === 'global' ? getSkillsDir() : getProjectSkillsDir(cwd);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scopeFromSource(source = '') {
|
|
54
|
+
if (source === 'bundled-skill') return 'builtin';
|
|
55
|
+
if (source === 'project-skill') return 'project';
|
|
56
|
+
if (source === 'global-skill' || source === 'registry-skill') return 'global';
|
|
57
|
+
return source || 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function setSkillEnabledConfig(name, enabled) {
|
|
61
|
+
const config = await loadConfig();
|
|
62
|
+
config.skills = config.skills || {};
|
|
63
|
+
config.skills.enabled = config.skills.enabled || {};
|
|
64
|
+
config.skills.enabled[name] = enabled;
|
|
65
|
+
await saveConfig(config);
|
|
21
66
|
}
|
|
22
67
|
|
|
23
|
-
async function
|
|
24
|
-
const
|
|
68
|
+
async function listSkillEntries({ scope = 'all', cwd = process.cwd() } = {}) {
|
|
69
|
+
const commands = await loadCommandsAndSkills(cwd);
|
|
70
|
+
const config = await loadConfig();
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const command of commands.values()) {
|
|
73
|
+
if (command.metadata?.type !== 'skill') continue;
|
|
74
|
+
const itemScope = scopeFromSource(command.source);
|
|
75
|
+
if (scope !== 'all' && itemScope !== scope) continue;
|
|
76
|
+
entries.push({
|
|
77
|
+
name: command.name,
|
|
78
|
+
version: command.metadata?.version || '0.0.0',
|
|
79
|
+
description: command.metadata?.description || '',
|
|
80
|
+
scope: itemScope,
|
|
81
|
+
path: command.path,
|
|
82
|
+
enabled: itemScope === 'builtin' ? true : config.skills?.enabled?.[command.name] !== false
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return entries.sort((a, b) => `${a.scope}:${a.name}`.localeCompare(`${b.scope}:${b.name}`));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readSkillMeta(name, { scope = 'all', cwd = process.cwd() } = {}) {
|
|
89
|
+
const entries = await listSkillEntries({ scope, cwd });
|
|
90
|
+
const found = entries.find((item) => item.name === name);
|
|
91
|
+
if (!found) {
|
|
92
|
+
return { exists: false, path: '', preview: '', manifest: null };
|
|
93
|
+
}
|
|
94
|
+
const dir = path.dirname(found.path);
|
|
25
95
|
const manifestPath = path.join(dir, 'manifest.json');
|
|
26
96
|
let manifest = null;
|
|
27
97
|
try {
|
|
@@ -30,13 +100,13 @@ async function readSkillMeta(name) {
|
|
|
30
100
|
manifest = null;
|
|
31
101
|
}
|
|
32
102
|
const entryFile = manifest?.entry || 'SKILL.md';
|
|
33
|
-
const skillPath = path.join(dir, entryFile);
|
|
103
|
+
const skillPath = found.path || path.join(dir, entryFile);
|
|
34
104
|
try {
|
|
35
105
|
const content = await fs.readFile(skillPath, 'utf8');
|
|
36
106
|
const firstLines = content.split('\n').slice(0, 20).join('\n');
|
|
37
|
-
return { exists: true, path: skillPath, preview: firstLines, manifest };
|
|
107
|
+
return { exists: true, path: skillPath, preview: firstLines, manifest, scope: found.scope };
|
|
38
108
|
} catch {
|
|
39
|
-
return { exists: false, path: skillPath, preview: '', manifest };
|
|
109
|
+
return { exists: false, path: skillPath, preview: '', manifest, scope: found.scope };
|
|
40
110
|
}
|
|
41
111
|
}
|
|
42
112
|
|
|
@@ -104,11 +174,15 @@ async function resolveSkillSourceDir(sourcePath) {
|
|
|
104
174
|
throw new Error('skill install supports <skill-dir>, <SKILL.md>, or <skill.tgz>');
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
async function installSkill(sourcePath) {
|
|
177
|
+
async function installSkill(sourcePath, { scope = 'project', cwd = process.cwd() } = {}) {
|
|
108
178
|
const resolved = await resolveSkillSourceDir(sourcePath);
|
|
109
179
|
const manifest = await readManifestSafe(resolved.dir);
|
|
110
180
|
const folderName = manifest?.name || path.basename(resolved.dir);
|
|
111
|
-
const
|
|
181
|
+
const bundled = (await listSkillEntries({ scope: 'builtin', cwd })).find((item) => item.name === folderName);
|
|
182
|
+
if (bundled) {
|
|
183
|
+
throw new Error(`cannot install over builtin skill: ${folderName}`);
|
|
184
|
+
}
|
|
185
|
+
const targetDir = path.join(baseDirForScope(scope, cwd), folderName);
|
|
112
186
|
await fs.rm(targetDir, { recursive: true, force: true });
|
|
113
187
|
await copyRecursive(resolved.dir, targetDir);
|
|
114
188
|
|
|
@@ -117,16 +191,19 @@ async function installSkill(sourcePath) {
|
|
|
117
191
|
await fs.access(entryPath);
|
|
118
192
|
|
|
119
193
|
const hash = await computeFileSha256(entryPath);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
194
|
+
if (scope === 'global') {
|
|
195
|
+
await upsertSkillRegistryEntry(undefined, {
|
|
196
|
+
name: folderName,
|
|
197
|
+
version: manifest?.version || '0.0.0',
|
|
198
|
+
description: manifest?.description || '',
|
|
199
|
+
enabled: true,
|
|
200
|
+
source: sourcePath,
|
|
201
|
+
entryFile,
|
|
202
|
+
sha256: hash,
|
|
203
|
+
installedAt: new Date().toISOString()
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
await setSkillEnabledConfig(folderName, true);
|
|
130
207
|
|
|
131
208
|
if (resolved.cleanupDir) {
|
|
132
209
|
await fs.rm(resolved.cleanupDir, { recursive: true, force: true });
|
|
@@ -135,19 +212,28 @@ async function installSkill(sourcePath) {
|
|
|
135
212
|
return folderName;
|
|
136
213
|
}
|
|
137
214
|
|
|
138
|
-
async function setEnabled(name, enabled) {
|
|
215
|
+
async function setEnabled(name, enabled, { cwd = process.cwd() } = {}) {
|
|
216
|
+
const entries = await listSkillEntries({ scope: 'all', cwd });
|
|
217
|
+
const found = entries.find((item) => item.name === name);
|
|
218
|
+
if (!found) {
|
|
219
|
+
throw new Error(`skill not found: ${name}`);
|
|
220
|
+
}
|
|
221
|
+
if (found.scope === 'builtin') {
|
|
222
|
+
throw new Error(`builtin skill cannot be ${enabled ? 'enabled' : 'disabled'}: ${name}`);
|
|
223
|
+
}
|
|
224
|
+
await setSkillEnabledConfig(name, enabled);
|
|
139
225
|
const registry = await readSkillRegistry();
|
|
140
226
|
const idx = registry.skills.findIndex((s) => s.name === name);
|
|
141
|
-
if (idx
|
|
142
|
-
|
|
227
|
+
if (idx !== -1) {
|
|
228
|
+
registry.skills[idx].enabled = enabled;
|
|
229
|
+
await writeSkillRegistry(undefined, registry);
|
|
143
230
|
}
|
|
144
|
-
registry.skills[idx].enabled = enabled;
|
|
145
|
-
await writeSkillRegistry(undefined, registry);
|
|
146
231
|
}
|
|
147
232
|
|
|
148
|
-
async function reindexSkills() {
|
|
149
|
-
|
|
150
|
-
|
|
233
|
+
async function reindexSkills({ scope = 'global', cwd = process.cwd() } = {}) {
|
|
234
|
+
const baseDir = baseDirForScope(scope, cwd);
|
|
235
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
236
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
151
237
|
const registry = await readSkillRegistry();
|
|
152
238
|
const byName = new Map((registry.skills || []).map((s) => [s.name, s]));
|
|
153
239
|
const rebuilt = [];
|
|
@@ -155,7 +241,7 @@ async function reindexSkills() {
|
|
|
155
241
|
for (const entry of entries) {
|
|
156
242
|
if (!entry.isDirectory()) continue;
|
|
157
243
|
const name = entry.name;
|
|
158
|
-
const dir = path.join(
|
|
244
|
+
const dir = path.join(baseDir, name);
|
|
159
245
|
const manifest = await readManifestSafe(dir);
|
|
160
246
|
const entryFile = manifest?.entry || 'SKILL.md';
|
|
161
247
|
const entryPath = path.join(dir, entryFile);
|
|
@@ -178,22 +264,24 @@ async function reindexSkills() {
|
|
|
178
264
|
});
|
|
179
265
|
}
|
|
180
266
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
267
|
+
if (scope === 'global') {
|
|
268
|
+
await writeSkillRegistry(undefined, {
|
|
269
|
+
version: 1,
|
|
270
|
+
skills: rebuilt
|
|
271
|
+
});
|
|
272
|
+
}
|
|
185
273
|
|
|
186
274
|
return rebuilt.length;
|
|
187
275
|
}
|
|
188
276
|
|
|
189
277
|
function usage() {
|
|
190
278
|
console.log(`Usage:
|
|
191
|
-
codemini skill list
|
|
192
|
-
codemini skill install <path>
|
|
279
|
+
codemini skill list [--scope=all|project|global|builtin]
|
|
280
|
+
codemini skill install [--scope=project|global] <path>
|
|
193
281
|
codemini skill enable <name>
|
|
194
282
|
codemini skill disable <name>
|
|
195
|
-
codemini skill inspect <name>
|
|
196
|
-
codemini skill reindex`);
|
|
283
|
+
codemini skill inspect [--scope=all|project|global|builtin] <name>
|
|
284
|
+
codemini skill reindex [--scope=project|global]`);
|
|
197
285
|
}
|
|
198
286
|
|
|
199
287
|
export async function handleSkill(args) {
|
|
@@ -204,26 +292,27 @@ export async function handleSkill(args) {
|
|
|
204
292
|
}
|
|
205
293
|
|
|
206
294
|
if (sub === 'list') {
|
|
207
|
-
const
|
|
295
|
+
const { scope } = parseScopeArgs(rest, { defaultScope: 'all', allowAll: true });
|
|
296
|
+
const entries = await listSkillEntries({ scope });
|
|
208
297
|
if (entries.length === 0) {
|
|
209
298
|
console.log('No installed skills');
|
|
210
299
|
return;
|
|
211
300
|
}
|
|
212
301
|
for (const item of entries) {
|
|
213
|
-
const state = item.enabled !== false ? 'enabled' : 'disabled';
|
|
214
|
-
console.log(`${item.name}@${item.version || '0.0.0'} (${state})`);
|
|
302
|
+
const state = item.scope === 'builtin' ? 'builtin/default' : (item.enabled !== false ? 'enabled' : 'disabled');
|
|
303
|
+
console.log(`${item.name}@${item.version || '0.0.0'} [${item.scope}] (${state})`);
|
|
215
304
|
}
|
|
216
305
|
return;
|
|
217
306
|
}
|
|
218
307
|
|
|
219
308
|
if (sub === 'install') {
|
|
220
|
-
const
|
|
309
|
+
const { scope, rest: positional } = parseScopeArgs(rest, { defaultScope: 'project' });
|
|
310
|
+
const sourcePath = positional[0];
|
|
221
311
|
if (!sourcePath) {
|
|
222
312
|
throw new Error('skill install requires <path>');
|
|
223
313
|
}
|
|
224
|
-
const installedName = await installSkill(sourcePath);
|
|
225
|
-
|
|
226
|
-
console.log(`Installed skill: ${installedName}`);
|
|
314
|
+
const installedName = await installSkill(sourcePath, { scope });
|
|
315
|
+
console.log(`Installed skill: ${installedName} (${scope})`);
|
|
227
316
|
return;
|
|
228
317
|
}
|
|
229
318
|
|
|
@@ -238,25 +327,28 @@ export async function handleSkill(args) {
|
|
|
238
327
|
}
|
|
239
328
|
|
|
240
329
|
if (sub === 'inspect') {
|
|
241
|
-
const
|
|
330
|
+
const { scope, rest: positional } = parseScopeArgs(rest, { defaultScope: 'all', allowAll: true });
|
|
331
|
+
const name = positional[0];
|
|
242
332
|
if (!name) {
|
|
243
333
|
throw new Error('skill inspect requires <name>');
|
|
244
334
|
}
|
|
245
|
-
const meta = await readSkillMeta(name);
|
|
335
|
+
const meta = await readSkillMeta(name, { scope });
|
|
246
336
|
if (!meta.exists) {
|
|
247
337
|
throw new Error(`skill not found: ${name}`);
|
|
248
338
|
}
|
|
249
339
|
if (meta.manifest) {
|
|
250
340
|
console.log(`Manifest: ${JSON.stringify(meta.manifest, null, 2)}\n`);
|
|
251
341
|
}
|
|
342
|
+
console.log(`Scope: ${meta.scope}\n`);
|
|
252
343
|
console.log(`Path: ${meta.path}\n`);
|
|
253
344
|
console.log(meta.preview);
|
|
254
345
|
return;
|
|
255
346
|
}
|
|
256
347
|
|
|
257
348
|
if (sub === 'reindex') {
|
|
258
|
-
const
|
|
259
|
-
|
|
349
|
+
const { scope } = parseScopeArgs(rest, { defaultScope: 'global' });
|
|
350
|
+
const count = await reindexSkills({ scope });
|
|
351
|
+
console.log(`Reindexed skills: ${count} (${scope})`);
|
|
260
352
|
return;
|
|
261
353
|
}
|
|
262
354
|
|
package/src/core/agent-loop.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import { BoundedCache } from './bounded-cache.js';
|
|
5
2
|
import { trimInline as _trimInline, normalizePath } from './string-utils.js';
|
|
6
3
|
import { captureToInbox, listInbox } from './memory-store.js';
|
|
7
4
|
import { requiresApprovalEvaluation } from './command-risk.js';
|
|
8
5
|
import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
|
|
9
6
|
import { normalizeToolArguments } from './tool-args.js';
|
|
7
|
+
import { storeResultIfNeeded, summarizeToolResult } from './tool-result-store.js';
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* 安全解析 JSON 字符串。
|
|
@@ -162,107 +160,6 @@ function compactToolResult(result, toolName, args, maxChars = 12000) {
|
|
|
162
160
|
return clipToolResult(obj, Math.min(maxChars, 4000));
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
// ─── P0: Large result disk store ─────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
const TOOL_RESULT_DISK_THRESHOLD = 6000;
|
|
168
|
-
const PREVIEW_SIZE_BYTES = 2000;
|
|
169
|
-
const TOOL_RESULTS_SUBDIR = 'tool-results';
|
|
170
|
-
|
|
171
|
-
let currentResultDir = null;
|
|
172
|
-
let resultDirReady = false;
|
|
173
|
-
const storedResults = new BoundedCache({
|
|
174
|
-
maxSize: 64,
|
|
175
|
-
ttlMs: 30 * 60 * 1000,
|
|
176
|
-
onEvict(key, value) {
|
|
177
|
-
if (value?.filePath) {
|
|
178
|
-
fs.unlink(value.filePath).catch(() => {});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}); // callId -> { filePath, summary }
|
|
182
|
-
const readCache = new BoundedCache({ maxSize: 128, ttlMs: 10 * 60 * 1000 }); // "path:startLine:endLine:mtimeMs" -> true
|
|
183
|
-
|
|
184
|
-
function generatePreview(content) {
|
|
185
|
-
if (content.length <= PREVIEW_SIZE_BYTES) {
|
|
186
|
-
return { preview: content, hasMore: false };
|
|
187
|
-
}
|
|
188
|
-
const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
|
|
189
|
-
const lastNewline = truncated.lastIndexOf('\n');
|
|
190
|
-
const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
|
|
191
|
-
return { preview: content.slice(0, cutPoint), hasMore: true };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function formatFileSize(chars) {
|
|
195
|
-
if (chars < 1024) return `${chars} B`;
|
|
196
|
-
return `${(chars / 1024).toFixed(1)} KB`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function setResultDir(dir) {
|
|
200
|
-
currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
|
|
201
|
-
resultDirReady = false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function ensureResultDir() {
|
|
205
|
-
if (!currentResultDir) return false;
|
|
206
|
-
if (!resultDirReady) {
|
|
207
|
-
await fs.mkdir(currentResultDir, { recursive: true });
|
|
208
|
-
resultDirReady = true;
|
|
209
|
-
}
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function storeResultIfNeeded(callId, formattedContent, rawResult) {
|
|
214
|
-
if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
|
|
215
|
-
return formattedContent;
|
|
216
|
-
}
|
|
217
|
-
try {
|
|
218
|
-
const ready = await ensureResultDir();
|
|
219
|
-
const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
|
|
220
|
-
if (!resultDirReady && dir === currentResultDir) {
|
|
221
|
-
await fs.mkdir(dir, { recursive: true });
|
|
222
|
-
} else if (!resultDirReady) {
|
|
223
|
-
await fs.mkdir(dir, { recursive: true });
|
|
224
|
-
}
|
|
225
|
-
const filePath = path.join(dir, `${callId}.txt`);
|
|
226
|
-
const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
|
|
227
|
-
await fs.writeFile(filePath, payload, 'utf-8');
|
|
228
|
-
const summary = summarizeToolResult(rawResult);
|
|
229
|
-
const { preview, hasMore } = generatePreview(payload);
|
|
230
|
-
storedResults.set(callId, { filePath, summary });
|
|
231
|
-
|
|
232
|
-
return `<persisted-output>
|
|
233
|
-
Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
|
|
234
|
-
|
|
235
|
-
Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
|
|
236
|
-
${preview}${hasMore ? '\n...' : ''}
|
|
237
|
-
|
|
238
|
-
Summary: ${summary}
|
|
239
|
-
</persisted-output>`;
|
|
240
|
-
} catch {
|
|
241
|
-
return formattedContent;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export function clearResultStore() {
|
|
246
|
-
const files = [];
|
|
247
|
-
for (const [, val] of storedResults.entries()) {
|
|
248
|
-
files.push(val.filePath);
|
|
249
|
-
}
|
|
250
|
-
storedResults.clear();
|
|
251
|
-
readCache.clear();
|
|
252
|
-
return Promise.allSettled(files.map((f) => fs.unlink(f).catch(() => {})));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ─── Read deduplication ─────────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
|
|
258
|
-
const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
|
|
259
|
-
if (readCache.has(key)) {
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
readCache.set(key, true);
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
163
|
// ─── P1a: Read-only tool classification ──────────────────────────────
|
|
267
164
|
|
|
268
165
|
const READ_ONLY_TOOLS = new Set([
|
|
@@ -282,6 +179,10 @@ const DREAM_AUTO_CAPTURE_TOOLS = new Set([
|
|
|
282
179
|
const DREAM_AUTO_CAPTURE_COOLDOWN_MS = 60_000;
|
|
283
180
|
const lastAutoCaptureByTool = new Map();
|
|
284
181
|
|
|
182
|
+
function isAutoCaptureEnabled(config = {}) {
|
|
183
|
+
return config?.memory?.enabled !== false && config?.memory?.auto_capture !== false;
|
|
184
|
+
}
|
|
185
|
+
|
|
285
186
|
function shouldAutoCaptureError(toolName, message) {
|
|
286
187
|
if (!DREAM_AUTO_CAPTURE_TOOLS.has(toolName)) return false;
|
|
287
188
|
const now = Date.now();
|
|
@@ -299,10 +200,6 @@ function shouldAutoCaptureError(toolName, message) {
|
|
|
299
200
|
/command not found/i,
|
|
300
201
|
/permission denied/i,
|
|
301
202
|
/args\?\s/i,
|
|
302
|
-
/Raw tool arguments/i,
|
|
303
|
-
/edit requires/i,
|
|
304
|
-
/write requires/i,
|
|
305
|
-
/requires file/i,
|
|
306
203
|
/path.*outside workspace/i,
|
|
307
204
|
/escapes workspace/i
|
|
308
205
|
];
|
|
@@ -312,7 +209,7 @@ function shouldAutoCaptureError(toolName, message) {
|
|
|
312
209
|
}
|
|
313
210
|
|
|
314
211
|
async function captureToolFailure(toolName, message, args, config = {}) {
|
|
315
|
-
if (config
|
|
212
|
+
if (!isAutoCaptureEnabled(config)) return;
|
|
316
213
|
const summary = `[${toolName}] ${String(message).slice(0, 120)}`;
|
|
317
214
|
const details = args
|
|
318
215
|
? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
|
|
@@ -366,108 +263,6 @@ function extractFileChange(toolName, result) {
|
|
|
366
263
|
return null;
|
|
367
264
|
}
|
|
368
265
|
|
|
369
|
-
export function summarizeToolResult(result) {
|
|
370
|
-
if (result === null || result === undefined) return 'no output';
|
|
371
|
-
if (typeof result === 'string') {
|
|
372
|
-
const oneLine = result.replace(/\s+/g, ' ').trim();
|
|
373
|
-
return oneLine.length > 90 ? `${oneLine.slice(0, 87)}...` : oneLine || 'empty string';
|
|
374
|
-
}
|
|
375
|
-
if (typeof result === 'object') {
|
|
376
|
-
const obj = result;
|
|
377
|
-
if (Array.isArray(obj)) return `array(${obj.length})`;
|
|
378
|
-
if ('deleted' in obj && 'path' in obj) {
|
|
379
|
-
const kind = trimInline(obj.type || 'item', 16);
|
|
380
|
-
const target = trimInline(obj.path || '', 96);
|
|
381
|
-
if (obj.deleted) return target ? `deleted ${kind} ${target}` : `deleted ${kind}`;
|
|
382
|
-
if (obj.cancelled) return target ? `cancelled delete ${target}` : 'cancelled delete';
|
|
383
|
-
}
|
|
384
|
-
if ('path' in obj && 'action' in obj) {
|
|
385
|
-
const p = String(obj.path || '');
|
|
386
|
-
const action = String(obj.action || 'write');
|
|
387
|
-
const line = Number(obj.changed_line || 1);
|
|
388
|
-
const suffix =
|
|
389
|
-
action === 'delete'
|
|
390
|
-
? 'deleted'
|
|
391
|
-
: action === 'create'
|
|
392
|
-
? 'created'
|
|
393
|
-
: action === 'patch'
|
|
394
|
-
? 'patched'
|
|
395
|
-
: action === 'replace_block' || action === 'replace_text'
|
|
396
|
-
? 'edited'
|
|
397
|
-
: action === 'append'
|
|
398
|
-
? 'appended'
|
|
399
|
-
: 'updated';
|
|
400
|
-
return p ? `${suffix} ${p}${line > 0 ? ` @L${line}` : ''}` : suffix;
|
|
401
|
-
}
|
|
402
|
-
if ('path' in obj && 'phase' in obj) {
|
|
403
|
-
const phase = String(obj.phase || '');
|
|
404
|
-
const p = String(obj.path || '');
|
|
405
|
-
const total = Number(obj.total_lines);
|
|
406
|
-
const start =
|
|
407
|
-
Number(obj.suggested_start_line || obj.start_line) > 0
|
|
408
|
-
? Number(obj.suggested_start_line || obj.start_line)
|
|
409
|
-
: 1;
|
|
410
|
-
const end =
|
|
411
|
-
Number(obj.suggested_end_line || obj.end_line) >= start
|
|
412
|
-
? Number(obj.suggested_end_line || obj.end_line)
|
|
413
|
-
: start;
|
|
414
|
-
const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
|
|
415
|
-
const totalText = total > 0 ? ` of ${total}` : '';
|
|
416
|
-
const enclosingText = obj.enclosing_symbol ? ` in ${obj.enclosing_symbol}` : '';
|
|
417
|
-
const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
|
|
418
|
-
const truncatedText = obj.truncated ? ' [truncated]' : '';
|
|
419
|
-
return phase === 'metadata'
|
|
420
|
-
? `metadata for ${p}${rangeText}${totalText}${errorText}`
|
|
421
|
-
: `content from ${p}${rangeText}${totalText}${enclosingText}${truncatedText}`;
|
|
422
|
-
}
|
|
423
|
-
if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
|
|
424
|
-
const stdout = trimInline(obj.stdout || '', 96);
|
|
425
|
-
const stderr = trimInline(obj.stderr || '', 96);
|
|
426
|
-
const command = trimInline(obj.command || '', 72);
|
|
427
|
-
const lead = command ? `${command} -> ` : '';
|
|
428
|
-
if (stdout) return `${lead}exit ${obj.code ?? 0}\nstdout: ${stdout}`;
|
|
429
|
-
if (stderr) return `${lead}exit ${obj.code ?? 0}\nstderr: ${stderr}`;
|
|
430
|
-
return `${lead}exit ${obj.code ?? 0}`;
|
|
431
|
-
}
|
|
432
|
-
if ('task_id' in obj && 'startup_confirmed' in obj) {
|
|
433
|
-
const status = trimInline(obj.status || 'unknown', 32);
|
|
434
|
-
const taskId = trimInline(obj.task_id || '', 24);
|
|
435
|
-
const source = trimInline(obj.startup_source || '', 24);
|
|
436
|
-
const outputFile = trimInline(obj.output_file || '', 72);
|
|
437
|
-
const output = Array.isArray(obj.recent_output) ? trimInline(obj.recent_output.slice(-1)[0] || '', 96) : '';
|
|
438
|
-
return `${taskId || 'task'} ${status}${source ? ` (${source})` : ''}${outputFile ? ` -> ${outputFile}` : ''}${output ? `\n${output}` : ''}`;
|
|
439
|
-
}
|
|
440
|
-
if ('tasks' in obj && Array.isArray(obj.tasks)) {
|
|
441
|
-
const count = obj.tasks.length;
|
|
442
|
-
const first = obj.tasks[0];
|
|
443
|
-
const lead = first?.task_id ? `${trimInline(first.task_id, 24)} ${trimInline(first.status || 'unknown', 24)}` : '';
|
|
444
|
-
return `tasks(${count})${lead ? `\n${lead}` : ''}`;
|
|
445
|
-
}
|
|
446
|
-
if ('files' in obj && Array.isArray(obj.files)) {
|
|
447
|
-
return `patched ${obj.files.length} file(s)`;
|
|
448
|
-
}
|
|
449
|
-
if ('diff' in obj && 'new_hash' in obj && 'path' in obj) {
|
|
450
|
-
const p = String(obj.path || '');
|
|
451
|
-
return p ? `diff preview for ${p}` : 'diff preview';
|
|
452
|
-
}
|
|
453
|
-
if ('created' in obj && Array.isArray(obj.created)) {
|
|
454
|
-
return `created ${obj.created.length} task(s)`;
|
|
455
|
-
}
|
|
456
|
-
if ('tasks' in obj && Array.isArray(obj.tasks)) {
|
|
457
|
-
return `${obj.tasks.length} task(s)`;
|
|
458
|
-
}
|
|
459
|
-
if ('newTodos' in obj && Array.isArray(obj.newTodos)) {
|
|
460
|
-
return obj.newTodos.length > 0 ? `updated ${obj.newTodos.length} todo item(s)` : 'cleared todo list';
|
|
461
|
-
}
|
|
462
|
-
if ('newPlan' in obj) {
|
|
463
|
-
return obj.newPlan ? `updated plan state (${String(obj.newPlan.status || 'draft')})` : 'cleared plan state';
|
|
464
|
-
}
|
|
465
|
-
const keys = Object.keys(obj);
|
|
466
|
-
return keys.length > 0 ? `keys: ${keys.slice(0, 5).join(',')}` : 'object';
|
|
467
|
-
}
|
|
468
|
-
return String(result);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
266
|
export const trimInline = _trimInline;
|
|
472
267
|
|
|
473
268
|
function normalizeAssistantText(value) {
|
|
@@ -1010,7 +805,7 @@ export async function runAgentLoop({
|
|
|
1010
805
|
if (onEvent) {
|
|
1011
806
|
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
|
|
1012
807
|
}
|
|
1013
|
-
if (shouldAutoCaptureError(toolName, message)) {
|
|
808
|
+
if (isAutoCaptureEnabled(config) && shouldAutoCaptureError(toolName, message)) {
|
|
1014
809
|
await captureToolFailure(toolName, message, effectiveArgs, config).catch(() => {});
|
|
1015
810
|
}
|
|
1016
811
|
return {
|
|
@@ -1033,13 +828,13 @@ export async function runAgentLoop({
|
|
|
1033
828
|
const stderr = String(toolResult.stderr || '');
|
|
1034
829
|
if (typeof exitCode === 'number' && exitCode !== 0 && stderr) {
|
|
1035
830
|
const failMsg = `exit ${exitCode}: ${stderr.slice(0, 120)}`;
|
|
1036
|
-
if (shouldAutoCaptureError(toolName, failMsg)) {
|
|
831
|
+
if (isAutoCaptureEnabled(config) && shouldAutoCaptureError(toolName, failMsg)) {
|
|
1037
832
|
await captureToolFailure(toolName, failMsg, effectiveArgs, config).catch(() => {});
|
|
1038
833
|
}
|
|
1039
834
|
}
|
|
1040
835
|
if (toolResult.error) {
|
|
1041
836
|
const errMsg = String(toolResult.error).slice(0, 120);
|
|
1042
|
-
if (shouldAutoCaptureError(toolName, errMsg)) {
|
|
837
|
+
if (isAutoCaptureEnabled(config) && shouldAutoCaptureError(toolName, errMsg)) {
|
|
1043
838
|
await captureToolFailure(toolName, errMsg, effectiveArgs, config).catch(() => {});
|
|
1044
839
|
}
|
|
1045
840
|
}
|