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.
@@ -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 { getSkillsDir } from '../core/paths.js';
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
- async function listSkillEntries() {
15
- const registry = await readSkillRegistry();
16
- const byName = new Map((registry.skills || []).map((s) => [s.name, s]));
17
- await fs.mkdir(getSkillsDir(), { recursive: true });
18
- const entries = await fs.readdir(getSkillsDir(), { withFileTypes: true });
19
- const names = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
20
- return names.map((name) => byName.get(name) || { name, version: 'unknown', enabled: true });
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 readSkillMeta(name) {
24
- const dir = path.join(getSkillsDir(), name);
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 targetDir = path.join(getSkillsDir(), folderName);
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
- await upsertSkillRegistryEntry(undefined, {
121
- name: folderName,
122
- version: manifest?.version || '0.0.0',
123
- description: manifest?.description || '',
124
- enabled: true,
125
- source: sourcePath,
126
- entryFile,
127
- sha256: hash,
128
- installedAt: new Date().toISOString()
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 === -1) {
142
- throw new Error(`skill not found: ${name}`);
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
- await fs.mkdir(getSkillsDir(), { recursive: true });
150
- const entries = await fs.readdir(getSkillsDir(), { withFileTypes: true });
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(getSkillsDir(), name);
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
- await writeSkillRegistry(undefined, {
182
- version: 1,
183
- skills: rebuilt
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 entries = await listSkillEntries();
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 sourcePath = rest[0];
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
- await setEnabled(installedName, true);
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 name = rest[0];
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 count = await reindexSkills();
259
- console.log(`Reindexed skills: ${count}`);
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
 
@@ -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?.memory?.enabled === false || config?.memory?.auto_capture === false) return;
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
  }