@wrongstack/tools 0.264.0 → 0.267.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/dist/audit.js +154 -11
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +138 -2
- package/dist/bash.js.map +1 -1
- package/dist/batch-tool-use.js +1 -0
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/builtin.d.ts +20 -1
- package/dist/builtin.js +796 -340
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +20 -0
- package/dist/circuit-breaker.js +40 -2
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.d.ts +16 -0
- package/dist/codebase-index/index.js +59 -25
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +56 -25
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/diff.js +14 -7
- package/dist/diff.js.map +1 -1
- package/dist/document.js +14 -8
- package/dist/document.js.map +1 -1
- package/dist/edit.d.ts +1 -0
- package/dist/edit.js +33 -22
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +140 -3
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +1 -0
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +153 -11
- package/dist/format.js.map +1 -1
- package/dist/git.d.ts +7 -0
- package/dist/git.js +20 -2
- package/dist/git.js.map +1 -1
- package/dist/glob.js +14 -7
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +14 -7
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +55 -3
- package/dist/index.js +957 -341
- package/dist/index.js.map +1 -1
- package/dist/install.js +153 -11
- package/dist/install.js.map +1 -1
- package/dist/json.js +1 -0
- package/dist/json.js.map +1 -1
- package/dist/lint.js +153 -11
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +14 -7
- package/dist/logs.js.map +1 -1
- package/dist/memory.js +1 -0
- package/dist/memory.js.map +1 -1
- package/dist/mode.js +1 -0
- package/dist/mode.js.map +1 -1
- package/dist/outdated.js +21 -10
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +765 -339
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +14 -7
- package/dist/patch.js.map +1 -1
- package/dist/process-registry.d.ts +56 -2
- package/dist/process-registry.js +138 -3
- package/dist/process-registry.js.map +1 -1
- package/dist/read.d.ts +3 -0
- package/dist/read.js +124 -22
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -7
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +14 -7
- package/dist/scaffold.js.map +1 -1
- package/dist/search.js +1 -0
- package/dist/search.js.map +1 -1
- package/dist/test.js +153 -11
- package/dist/test.js.map +1 -1
- package/dist/todo.js +1 -0
- package/dist/todo.js.map +1 -1
- package/dist/tool-help.js +1 -0
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-icons.d.ts +20 -0
- package/dist/tool-icons.js +130 -0
- package/dist/tool-icons.js.map +1 -0
- package/dist/tool-search.js +1 -0
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-use.js +1 -0
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.js +14 -7
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +153 -11
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +21 -15
- package/dist/write.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs4 from 'node:fs/promises';
|
|
2
|
+
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
4
5
|
import * as Core from '@wrongstack/core';
|
|
5
|
-
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem,
|
|
6
|
-
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
6
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
7
7
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as fs7 from 'node:fs';
|
|
@@ -36,22 +36,29 @@ async function detectPackageManager(cwd) {
|
|
|
36
36
|
function resolvePath(input, ctx) {
|
|
37
37
|
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
38
38
|
}
|
|
39
|
+
function allowedRoots(ctx) {
|
|
40
|
+
return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];
|
|
41
|
+
}
|
|
42
|
+
function isInsideAny(target, roots) {
|
|
43
|
+
return roots.some((root) => {
|
|
44
|
+
const rel = path.relative(root, target);
|
|
45
|
+
return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
39
48
|
function ensureInsideRoot(absPath, ctx) {
|
|
40
|
-
if (ctx.allowOutsideProjectRoot) return path.resolve(absPath);
|
|
41
|
-
const root = path.resolve(ctx.projectRoot);
|
|
42
49
|
const target = path.resolve(absPath);
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return target;
|
|
50
|
+
if (ctx.allowOutsideProjectRoot) return target;
|
|
51
|
+
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
52
|
+
throw new Error(`Path "${absPath}" is outside project root "${path.resolve(ctx.projectRoot)}"`);
|
|
48
53
|
}
|
|
49
54
|
function safeResolve(input, ctx) {
|
|
50
55
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
51
56
|
}
|
|
52
57
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
53
58
|
if (ctx.allowOutsideProjectRoot) return;
|
|
54
|
-
const
|
|
59
|
+
const realRoots = await Promise.all(
|
|
60
|
+
allowedRoots(ctx).map((r) => fs4.realpath(r).catch(() => path.resolve(r)))
|
|
61
|
+
);
|
|
55
62
|
let probe = absPath;
|
|
56
63
|
for (; ; ) {
|
|
57
64
|
let real;
|
|
@@ -66,13 +73,10 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
66
73
|
}
|
|
67
74
|
throw err;
|
|
68
75
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return;
|
|
76
|
+
if (isInsideAny(real, realRoots)) return;
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
79
|
+
);
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
async function safeResolveReal(input, ctx) {
|
|
@@ -164,6 +168,8 @@ function normalizeCommandOutput(raw, opts = {}) {
|
|
|
164
168
|
text = text.replace(/\n{3,}/g, "\n\n");
|
|
165
169
|
return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
|
|
166
170
|
}
|
|
171
|
+
|
|
172
|
+
// src/read.ts
|
|
167
173
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
168
174
|
var readTool = {
|
|
169
175
|
name: "read",
|
|
@@ -173,6 +179,7 @@ var readTool = {
|
|
|
173
179
|
permission: "auto",
|
|
174
180
|
mutating: false,
|
|
175
181
|
capabilities: ["fs.read"],
|
|
182
|
+
icon: "file",
|
|
176
183
|
maxOutputBytes: 262144,
|
|
177
184
|
timeoutMs: 5e3,
|
|
178
185
|
inputSchema: {
|
|
@@ -189,6 +196,11 @@ var readTool = {
|
|
|
189
196
|
limit: {
|
|
190
197
|
type: "integer",
|
|
191
198
|
description: "Maximum number of lines to return (default is 2000)."
|
|
199
|
+
},
|
|
200
|
+
mode: {
|
|
201
|
+
type: "string",
|
|
202
|
+
enum: ["content", "summary"],
|
|
203
|
+
description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
|
|
192
204
|
}
|
|
193
205
|
},
|
|
194
206
|
required: ["path"]
|
|
@@ -202,14 +214,27 @@ var readTool = {
|
|
|
202
214
|
} catch (err) {
|
|
203
215
|
const code = err.code;
|
|
204
216
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
205
|
-
throw new Error(
|
|
206
|
-
`read: failed to stat "${input.path}": ${toErrorMessage(err)}`
|
|
207
|
-
);
|
|
217
|
+
throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
|
|
208
218
|
}
|
|
209
219
|
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
210
220
|
if (stat11.size > MAX_BYTES) {
|
|
211
221
|
throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
|
|
212
222
|
}
|
|
223
|
+
const offset = Math.max(1, input.offset ?? 1);
|
|
224
|
+
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
225
|
+
const prior = getReadRangeRecord(ctx, absPath);
|
|
226
|
+
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
227
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat11.mtimeMs, offset, requestedEnd)) {
|
|
228
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
229
|
+
return {
|
|
230
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat11.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
231
|
+
total_lines: prior.totalLines,
|
|
232
|
+
encoding: "utf8",
|
|
233
|
+
truncated: requestedEnd < prior.totalLines,
|
|
234
|
+
cached: true,
|
|
235
|
+
note: "Repeated read suppressed to save tokens."
|
|
236
|
+
};
|
|
237
|
+
}
|
|
213
238
|
const buf = await fs4.readFile(absPath);
|
|
214
239
|
if (isBinaryBuffer(buf)) {
|
|
215
240
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
@@ -217,17 +242,38 @@ var readTool = {
|
|
|
217
242
|
const text = buf.toString("utf8");
|
|
218
243
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
219
244
|
const total = allLines.length;
|
|
220
|
-
|
|
221
|
-
|
|
245
|
+
if (input.mode === "summary") {
|
|
246
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
247
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, Math.min(total, 200));
|
|
248
|
+
return {
|
|
249
|
+
text: summarizeFile(input.path, stat11.size, allLines),
|
|
250
|
+
total_lines: total,
|
|
251
|
+
encoding: "utf8",
|
|
252
|
+
truncated: total > 200,
|
|
253
|
+
note: "Summary mode returned compact structure instead of full file content."
|
|
254
|
+
};
|
|
255
|
+
}
|
|
222
256
|
if (limit === 0) {
|
|
223
257
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
258
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, 0);
|
|
224
259
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
225
260
|
}
|
|
261
|
+
if (offset > total) {
|
|
262
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
263
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, total + 1, total + 1);
|
|
264
|
+
return {
|
|
265
|
+
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
266
|
+
total_lines: total,
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
truncated: false
|
|
269
|
+
};
|
|
270
|
+
}
|
|
226
271
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
227
272
|
const truncated = offset - 1 + slice.length < total;
|
|
228
273
|
const width = String(offset + slice.length - 1).length;
|
|
229
274
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
230
275
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
276
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, offset, offset + slice.length - 1);
|
|
231
277
|
return {
|
|
232
278
|
text: numbered,
|
|
233
279
|
total_lines: total,
|
|
@@ -236,6 +282,62 @@ var readTool = {
|
|
|
236
282
|
};
|
|
237
283
|
}
|
|
238
284
|
};
|
|
285
|
+
var READ_RANGES_META_KEY = "tools.read.ranges.v1";
|
|
286
|
+
function getReadRanges(ctx) {
|
|
287
|
+
const existing = ctx.meta[READ_RANGES_META_KEY];
|
|
288
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
289
|
+
return existing;
|
|
290
|
+
}
|
|
291
|
+
const next = {};
|
|
292
|
+
ctx.meta[READ_RANGES_META_KEY] = next;
|
|
293
|
+
return next;
|
|
294
|
+
}
|
|
295
|
+
function getReadRangeRecord(ctx, absPath) {
|
|
296
|
+
return getReadRanges(ctx)[absPath];
|
|
297
|
+
}
|
|
298
|
+
function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
|
|
299
|
+
if (end < start) return;
|
|
300
|
+
const ranges = getReadRanges(ctx);
|
|
301
|
+
const prior = ranges[absPath];
|
|
302
|
+
const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
|
|
303
|
+
nextRanges.push({ start, end });
|
|
304
|
+
ranges[absPath] = {
|
|
305
|
+
mtimeMs,
|
|
306
|
+
totalLines,
|
|
307
|
+
ranges: mergeRanges(nextRanges)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function coversRange(record, mtimeMs, start, end) {
|
|
311
|
+
if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
|
|
312
|
+
return record.ranges.some((range) => range.start <= start && range.end >= end);
|
|
313
|
+
}
|
|
314
|
+
function mergeRanges(ranges) {
|
|
315
|
+
const sorted = ranges.slice().sort((a, b) => a.start - b.start);
|
|
316
|
+
const merged = [];
|
|
317
|
+
for (const range of sorted) {
|
|
318
|
+
const last = merged[merged.length - 1];
|
|
319
|
+
if (!last || range.start > last.end + 1) {
|
|
320
|
+
merged.push({ ...range });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
last.end = Math.max(last.end, range.end);
|
|
324
|
+
}
|
|
325
|
+
return merged;
|
|
326
|
+
}
|
|
327
|
+
function summarizeFile(filePath, bytes, lines) {
|
|
328
|
+
const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
|
|
329
|
+
({ line }) => /^(import\s|export\s|class\s|interface\s|type\s|function\s|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|def\s+|async\s+function\s)/.test(
|
|
330
|
+
line
|
|
331
|
+
)
|
|
332
|
+
).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
|
|
333
|
+
return [
|
|
334
|
+
`summary: ${filePath}`,
|
|
335
|
+
`bytes=${bytes}`,
|
|
336
|
+
`total_lines=${lines.length}`,
|
|
337
|
+
interesting.length > 0 ? `symbols/imports:
|
|
338
|
+
${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
239
341
|
var writeTool = {
|
|
240
342
|
name: "write",
|
|
241
343
|
category: "Filesystem",
|
|
@@ -245,6 +347,7 @@ var writeTool = {
|
|
|
245
347
|
mutating: true,
|
|
246
348
|
timeoutMs: 5e3,
|
|
247
349
|
capabilities: ["fs.write"],
|
|
350
|
+
icon: "file",
|
|
248
351
|
inputSchema: {
|
|
249
352
|
type: "object",
|
|
250
353
|
properties: {
|
|
@@ -303,11 +406,12 @@ var writeTool = {
|
|
|
303
406
|
var editTool = {
|
|
304
407
|
name: "edit",
|
|
305
408
|
category: "Filesystem",
|
|
306
|
-
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It
|
|
307
|
-
usageHint: "
|
|
409
|
+
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
|
|
410
|
+
usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
|
|
308
411
|
permission: "confirm",
|
|
309
412
|
mutating: true,
|
|
310
413
|
capabilities: ["fs.write"],
|
|
414
|
+
icon: "edit",
|
|
311
415
|
timeoutMs: 5e3,
|
|
312
416
|
inputSchema: {
|
|
313
417
|
type: "object",
|
|
@@ -332,9 +436,7 @@ var editTool = {
|
|
|
332
436
|
throw err;
|
|
333
437
|
});
|
|
334
438
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
335
|
-
|
|
336
|
-
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
337
|
-
}
|
|
439
|
+
const autoRead = !ctx.hasRead(absPath);
|
|
338
440
|
const original = await fs4.readFile(absPath, "utf8");
|
|
339
441
|
const updated = await fs4.stat(absPath);
|
|
340
442
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
@@ -342,15 +444,21 @@ var editTool = {
|
|
|
342
444
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
343
445
|
throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
|
|
344
446
|
}
|
|
447
|
+
if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
|
|
448
|
+
throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
|
|
449
|
+
}
|
|
450
|
+
const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
|
|
345
451
|
const style = detectNewlineStyle(original);
|
|
346
452
|
const fileLf = normalizeToLf(original);
|
|
347
453
|
const oldLf = normalizeToLf(input.old_string);
|
|
348
454
|
const newLf = normalizeToLf(input.new_string);
|
|
349
455
|
if (oldLf === newLf) {
|
|
456
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
350
457
|
return {
|
|
351
458
|
path: absPath,
|
|
352
459
|
replacements: 0,
|
|
353
|
-
diff: "(no-op: old and new are identical)"
|
|
460
|
+
diff: "(no-op: old and new are identical)",
|
|
461
|
+
note: autoReadNote
|
|
354
462
|
};
|
|
355
463
|
}
|
|
356
464
|
let count = 0;
|
|
@@ -391,7 +499,8 @@ var editTool = {
|
|
|
391
499
|
return {
|
|
392
500
|
path: absPath,
|
|
393
501
|
replacements: input.replace_all ? count : 1,
|
|
394
|
-
diff
|
|
502
|
+
diff,
|
|
503
|
+
note: autoReadNote
|
|
395
504
|
};
|
|
396
505
|
}
|
|
397
506
|
};
|
|
@@ -475,6 +584,7 @@ var replaceTool = {
|
|
|
475
584
|
permission: "confirm",
|
|
476
585
|
mutating: true,
|
|
477
586
|
capabilities: ["fs.write"],
|
|
587
|
+
icon: "edit",
|
|
478
588
|
timeoutMs: 3e4,
|
|
479
589
|
inputSchema: {
|
|
480
590
|
type: "object",
|
|
@@ -676,6 +786,7 @@ var globTool = {
|
|
|
676
786
|
permission: "auto",
|
|
677
787
|
mutating: false,
|
|
678
788
|
capabilities: ["fs.read"],
|
|
789
|
+
icon: "folder",
|
|
679
790
|
maxOutputBytes: 65536,
|
|
680
791
|
timeoutMs: 5e3,
|
|
681
792
|
inputSchema: {
|
|
@@ -761,6 +872,7 @@ var grepTool = {
|
|
|
761
872
|
permission: "auto",
|
|
762
873
|
mutating: false,
|
|
763
874
|
capabilities: ["fs.read"],
|
|
875
|
+
icon: "search",
|
|
764
876
|
maxOutputBytes: 131072,
|
|
765
877
|
timeoutMs: 1e4,
|
|
766
878
|
inputSchema: {
|
|
@@ -1162,6 +1274,23 @@ var CircuitBreaker = class {
|
|
|
1162
1274
|
lastSlowAt = null;
|
|
1163
1275
|
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
1164
1276
|
openedAt = null;
|
|
1277
|
+
/**
|
|
1278
|
+
* Master enable flag. When false the breaker is bypassed: `beforeCall`
|
|
1279
|
+
* always returns true and `afterCall` records nothing. The class itself
|
|
1280
|
+
* defaults to enabled (so the standalone unit tests exercise tripping); the
|
|
1281
|
+
* ProcessRegistry flips this off until the user opts in via `/settings`.
|
|
1282
|
+
*/
|
|
1283
|
+
enabled = true;
|
|
1284
|
+
/**
|
|
1285
|
+
* Fired (best-effort) when the breaker transitions into the `open` state.
|
|
1286
|
+
* The registry uses this to arm its auto kill/reset countdown.
|
|
1287
|
+
*/
|
|
1288
|
+
onTrip;
|
|
1289
|
+
/**
|
|
1290
|
+
* Fired (best-effort) when the breaker returns to `closed` after having been
|
|
1291
|
+
* open/half-open. The registry uses this to cancel a pending kill/reset.
|
|
1292
|
+
*/
|
|
1293
|
+
onReset;
|
|
1165
1294
|
constructor(config = {}) {
|
|
1166
1295
|
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
1167
1296
|
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
@@ -1170,12 +1299,22 @@ var CircuitBreaker = class {
|
|
|
1170
1299
|
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
1171
1300
|
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
1172
1301
|
}
|
|
1302
|
+
/** Toggle the master enable. Disabling resets to a clean `closed` state. */
|
|
1303
|
+
setEnabled(enabled) {
|
|
1304
|
+
if (this.enabled === enabled) return;
|
|
1305
|
+
this.enabled = enabled;
|
|
1306
|
+
if (!enabled) this._reset();
|
|
1307
|
+
}
|
|
1308
|
+
get isEnabled() {
|
|
1309
|
+
return this.enabled;
|
|
1310
|
+
}
|
|
1173
1311
|
/**
|
|
1174
1312
|
* Returns true if the circuit allows a new call to proceed.
|
|
1175
1313
|
* When false, callers should abort the tool call and return a
|
|
1176
1314
|
* circuit-breaker error instead of spawning a process.
|
|
1177
1315
|
*/
|
|
1178
1316
|
get canProceed() {
|
|
1317
|
+
if (!this.enabled) return true;
|
|
1179
1318
|
this._checkStateTransition();
|
|
1180
1319
|
return this.state !== "open";
|
|
1181
1320
|
}
|
|
@@ -1211,7 +1350,7 @@ var CircuitBreaker = class {
|
|
|
1211
1350
|
* not affect breaker state.
|
|
1212
1351
|
*/
|
|
1213
1352
|
beforeCall(bypass = false) {
|
|
1214
|
-
if (bypass) return true;
|
|
1353
|
+
if (bypass || !this.enabled) return true;
|
|
1215
1354
|
this._checkStateTransition();
|
|
1216
1355
|
if (this.state === "open") return false;
|
|
1217
1356
|
return true;
|
|
@@ -1226,7 +1365,7 @@ var CircuitBreaker = class {
|
|
|
1226
1365
|
* Use for background/fire-and-forget processes.
|
|
1227
1366
|
*/
|
|
1228
1367
|
afterCall(durationMs, failed, bypass = false) {
|
|
1229
|
-
if (bypass) return;
|
|
1368
|
+
if (bypass || !this.enabled) return;
|
|
1230
1369
|
const now = Date.now();
|
|
1231
1370
|
if (this.state === "half-open") {
|
|
1232
1371
|
if (failed) {
|
|
@@ -1272,12 +1411,23 @@ var CircuitBreaker = class {
|
|
|
1272
1411
|
if (this.state === "open") return;
|
|
1273
1412
|
this.state = "open";
|
|
1274
1413
|
this.openedAt = Date.now();
|
|
1414
|
+
try {
|
|
1415
|
+
this.onTrip?.();
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1275
1418
|
}
|
|
1276
1419
|
_reset() {
|
|
1420
|
+
const wasRecovering = this.state !== "closed";
|
|
1277
1421
|
this.state = "closed";
|
|
1278
1422
|
this.consecutiveFailures = 0;
|
|
1279
1423
|
this.window = [];
|
|
1280
1424
|
this.openedAt = null;
|
|
1425
|
+
if (wasRecovering) {
|
|
1426
|
+
try {
|
|
1427
|
+
this.onReset?.();
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1281
1431
|
}
|
|
1282
1432
|
/** Transition from open → half-open when cooldown elapses. */
|
|
1283
1433
|
_checkStateTransition() {
|
|
@@ -1339,8 +1489,21 @@ function killWin32Tree(pid) {
|
|
|
1339
1489
|
var ProcessRegistryImpl = class {
|
|
1340
1490
|
processes = /* @__PURE__ */ new Map();
|
|
1341
1491
|
breaker;
|
|
1492
|
+
/**
|
|
1493
|
+
* Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
|
|
1494
|
+
* a countdown is armed; on expiry all tracked processes are killed and the
|
|
1495
|
+
* breaker is reset to closed (forced recovery). Zero means manual recovery
|
|
1496
|
+
* only (`/kill reset`).
|
|
1497
|
+
*/
|
|
1498
|
+
autoKillResetMs = 0;
|
|
1499
|
+
autoKillTimer = null;
|
|
1500
|
+
autoKillArmedAt = null;
|
|
1501
|
+
breakerCountdownListeners = [];
|
|
1342
1502
|
constructor(breakerConfig) {
|
|
1343
1503
|
this.breaker = new CircuitBreaker(breakerConfig);
|
|
1504
|
+
this.breaker.onTrip = () => this._armAutoKillReset();
|
|
1505
|
+
this.breaker.onReset = () => this._cancelAutoKillReset();
|
|
1506
|
+
this.breaker.setEnabled(false);
|
|
1344
1507
|
}
|
|
1345
1508
|
register(info) {
|
|
1346
1509
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
@@ -1416,6 +1579,90 @@ var ProcessRegistryImpl = class {
|
|
|
1416
1579
|
forceBreakerReset() {
|
|
1417
1580
|
this.breaker.forceReset();
|
|
1418
1581
|
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Configure circuit-breaker protection at runtime. Called from `/settings`
|
|
1584
|
+
* (instant, all modes) and on TUI mount (applies persisted config).
|
|
1585
|
+
*
|
|
1586
|
+
* - `enabled` toggles whether the breaker gates `bash`/`exec`.
|
|
1587
|
+
* - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
|
|
1588
|
+
* trips (0 = manual recovery only).
|
|
1589
|
+
*
|
|
1590
|
+
* Re-applies cleanly on every call: cancels a pending countdown when the
|
|
1591
|
+
* timeout is cleared or protection disabled, and re-arms if the breaker is
|
|
1592
|
+
* currently open under the new settings.
|
|
1593
|
+
*/
|
|
1594
|
+
setBreakerConfig(cfg) {
|
|
1595
|
+
if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
|
|
1596
|
+
if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
|
|
1597
|
+
if (this.autoKillResetMs <= 0) {
|
|
1598
|
+
this._cancelAutoKillReset();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
|
|
1602
|
+
this._armAutoKillReset();
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Live countdown to the next auto kill/reset, or null when nothing is armed.
|
|
1607
|
+
* The TUI polls this on a 1s tick while armed so the statusline decrements.
|
|
1608
|
+
*/
|
|
1609
|
+
getBreakerCountdown() {
|
|
1610
|
+
if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
|
|
1611
|
+
const elapsed = Date.now() - this.autoKillArmedAt;
|
|
1612
|
+
return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
|
|
1616
|
+
* Use {@link getBreakerCountdown} for the live ticking value between events.
|
|
1617
|
+
*/
|
|
1618
|
+
onBreakerCountdownChange(listener) {
|
|
1619
|
+
this.breakerCountdownListeners.push(listener);
|
|
1620
|
+
return () => {
|
|
1621
|
+
this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
_emitBreakerCountdown() {
|
|
1625
|
+
const snap = this.getBreakerCountdown();
|
|
1626
|
+
for (const l of this.breakerCountdownListeners) {
|
|
1627
|
+
try {
|
|
1628
|
+
l(snap);
|
|
1629
|
+
} catch {
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
|
|
1635
|
+
* (a fresh trip after a failed half-open probe restarts the clock). No-op
|
|
1636
|
+
* when protection is off or no timeout is configured.
|
|
1637
|
+
*/
|
|
1638
|
+
_armAutoKillReset() {
|
|
1639
|
+
if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
|
|
1640
|
+
this._clearAutoKillTimer();
|
|
1641
|
+
this.autoKillArmedAt = Date.now();
|
|
1642
|
+
this.autoKillTimer = setTimeout(() => {
|
|
1643
|
+
this.autoKillTimer = null;
|
|
1644
|
+
this.autoKillArmedAt = null;
|
|
1645
|
+
this.killAll({ force: false });
|
|
1646
|
+
this.breaker.forceReset();
|
|
1647
|
+
this._emitBreakerCountdown();
|
|
1648
|
+
}, this.autoKillResetMs);
|
|
1649
|
+
this.autoKillTimer.unref?.();
|
|
1650
|
+
this._emitBreakerCountdown();
|
|
1651
|
+
}
|
|
1652
|
+
_cancelAutoKillReset() {
|
|
1653
|
+
const wasArmed = this.autoKillArmedAt !== null;
|
|
1654
|
+
this._clearAutoKillTimer();
|
|
1655
|
+
if (wasArmed) {
|
|
1656
|
+
this.autoKillArmedAt = null;
|
|
1657
|
+
this._emitBreakerCountdown();
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
_clearAutoKillTimer() {
|
|
1661
|
+
if (this.autoKillTimer !== null) {
|
|
1662
|
+
clearTimeout(this.autoKillTimer);
|
|
1663
|
+
this.autoKillTimer = null;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1419
1666
|
/** Kill a single process by PID.
|
|
1420
1667
|
*
|
|
1421
1668
|
* On POSIX: sends SIGTERM to the *process group* (-pid) so that
|
|
@@ -1540,6 +1787,7 @@ var bashTool = {
|
|
|
1540
1787
|
permission: "confirm",
|
|
1541
1788
|
mutating: true,
|
|
1542
1789
|
riskTier: "destructive",
|
|
1790
|
+
icon: "terminal",
|
|
1543
1791
|
// Trust rules match on the literal `command` string. Without subjectKey
|
|
1544
1792
|
// the policy heuristic would have done the same here, but declaring it
|
|
1545
1793
|
// explicitly removes the implicit cross-tool aliasing.
|
|
@@ -2002,6 +2250,7 @@ var execTool = {
|
|
|
2002
2250
|
riskTier: "standard",
|
|
2003
2251
|
timeoutMs: DEFAULT_TIMEOUT_MS2,
|
|
2004
2252
|
capabilities: ["shell.restricted"],
|
|
2253
|
+
icon: "terminal",
|
|
2005
2254
|
inputSchema: {
|
|
2006
2255
|
type: "object",
|
|
2007
2256
|
properties: {
|
|
@@ -2101,7 +2350,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
2101
2350
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
2102
2351
|
const resolved = resolveWin32Command(cmd);
|
|
2103
2352
|
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
2104
|
-
const
|
|
2353
|
+
const spawnCmd = needsShell ? cmd : resolved;
|
|
2354
|
+
const child = spawn(spawnCmd, args, {
|
|
2105
2355
|
cwd,
|
|
2106
2356
|
env: buildChildEnv(sessionId),
|
|
2107
2357
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -2288,6 +2538,7 @@ var fetchTool = {
|
|
|
2288
2538
|
permission: "confirm",
|
|
2289
2539
|
mutating: false,
|
|
2290
2540
|
capabilities: ["net.outbound"],
|
|
2541
|
+
icon: "web",
|
|
2291
2542
|
// Trust rules for fetch match on the literal URL — declare it explicitly
|
|
2292
2543
|
// so a user can trust `https://api.example.com/*` without accidentally
|
|
2293
2544
|
// matching that pattern on any other tool that happens to have a `url`
|
|
@@ -2438,6 +2689,7 @@ var searchTool = {
|
|
|
2438
2689
|
permission: "confirm",
|
|
2439
2690
|
mutating: false,
|
|
2440
2691
|
capabilities: ["net.outbound"],
|
|
2692
|
+
icon: "search",
|
|
2441
2693
|
timeoutMs: TIMEOUT_MS2,
|
|
2442
2694
|
inputSchema: {
|
|
2443
2695
|
type: "object",
|
|
@@ -2653,6 +2905,7 @@ var todoTool = {
|
|
|
2653
2905
|
// mutates only conversation state (ctx.todos), not external state — no confirmation needed
|
|
2654
2906
|
timeoutMs: 1e3,
|
|
2655
2907
|
capabilities: ["session.todo"],
|
|
2908
|
+
icon: "todo",
|
|
2656
2909
|
inputSchema: {
|
|
2657
2910
|
type: "object",
|
|
2658
2911
|
properties: {
|
|
@@ -2753,11 +3006,12 @@ var todoTool = {
|
|
|
2753
3006
|
var planTool = {
|
|
2754
3007
|
name: "plan",
|
|
2755
3008
|
category: "Session",
|
|
2756
|
-
description:
|
|
2757
|
-
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns
|
|
3009
|
+
description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
|
|
3010
|
+
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
|
|
2758
3011
|
permission: "confirm",
|
|
2759
3012
|
mutating: true,
|
|
2760
3013
|
capabilities: ["fs.write"],
|
|
3014
|
+
icon: "plan",
|
|
2761
3015
|
timeoutMs: 2e3,
|
|
2762
3016
|
inputSchema: {
|
|
2763
3017
|
type: "object",
|
|
@@ -2797,12 +3051,26 @@ var planTool = {
|
|
|
2797
3051
|
template: {
|
|
2798
3052
|
type: "string",
|
|
2799
3053
|
description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
|
|
3054
|
+
},
|
|
3055
|
+
scope: {
|
|
3056
|
+
type: "string",
|
|
3057
|
+
enum: ["session", "project"],
|
|
3058
|
+
description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
|
|
2800
3059
|
}
|
|
2801
3060
|
},
|
|
2802
3061
|
required: ["action"]
|
|
2803
3062
|
},
|
|
2804
3063
|
async execute(input, ctx) {
|
|
2805
|
-
const
|
|
3064
|
+
const sessionPlanPath = ctx.meta["plan.path"];
|
|
3065
|
+
let planPath;
|
|
3066
|
+
if (input.scope === "project") {
|
|
3067
|
+
if (typeof sessionPlanPath === "string") {
|
|
3068
|
+
const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
|
|
3069
|
+
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
3070
|
+
}
|
|
3071
|
+
} else {
|
|
3072
|
+
planPath = sessionPlanPath;
|
|
3073
|
+
}
|
|
2806
3074
|
if (typeof planPath !== "string" || !planPath) {
|
|
2807
3075
|
return {
|
|
2808
3076
|
ok: false,
|
|
@@ -2816,148 +3084,169 @@ var planTool = {
|
|
|
2816
3084
|
let early = null;
|
|
2817
3085
|
const taskifyMeta = { title: "", details: "" };
|
|
2818
3086
|
let didTaskify = false;
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
case "done": {
|
|
2834
|
-
if (!input.target) {
|
|
2835
|
-
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2836
|
-
return p;
|
|
2837
|
-
}
|
|
2838
|
-
const next = setPlanItemStatus(
|
|
2839
|
-
p,
|
|
2840
|
-
input.target,
|
|
2841
|
-
input.action === "start" ? "in_progress" : "done"
|
|
2842
|
-
);
|
|
2843
|
-
if (next === p) {
|
|
2844
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2845
|
-
return p;
|
|
2846
|
-
}
|
|
2847
|
-
return next;
|
|
2848
|
-
}
|
|
2849
|
-
case "remove": {
|
|
2850
|
-
if (!input.target) {
|
|
2851
|
-
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
2852
|
-
return p;
|
|
2853
|
-
}
|
|
2854
|
-
const next = removePlanItem(p, input.target);
|
|
2855
|
-
if (next === p) {
|
|
2856
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2857
|
-
return p;
|
|
2858
|
-
}
|
|
2859
|
-
return next;
|
|
2860
|
-
}
|
|
2861
|
-
case "promote": {
|
|
2862
|
-
if (!input.target) {
|
|
2863
|
-
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2864
|
-
return p;
|
|
2865
|
-
}
|
|
2866
|
-
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
2867
|
-
if (!derived) {
|
|
2868
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2869
|
-
return p;
|
|
3087
|
+
let plan;
|
|
3088
|
+
try {
|
|
3089
|
+
plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
3090
|
+
switch (input.action) {
|
|
3091
|
+
case "show":
|
|
3092
|
+
break;
|
|
3093
|
+
case "add": {
|
|
3094
|
+
const title = input.title?.trim();
|
|
3095
|
+
if (!title) {
|
|
3096
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
3097
|
+
return p;
|
|
3098
|
+
}
|
|
3099
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
3100
|
+
return updated;
|
|
2870
3101
|
}
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3102
|
+
case "start":
|
|
3103
|
+
case "done": {
|
|
3104
|
+
if (!input.target) {
|
|
3105
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
3106
|
+
return p;
|
|
3107
|
+
}
|
|
3108
|
+
const next = setPlanItemStatus(
|
|
3109
|
+
p,
|
|
3110
|
+
input.target,
|
|
3111
|
+
input.action === "start" ? "in_progress" : "done"
|
|
3112
|
+
);
|
|
3113
|
+
if (next === p) {
|
|
3114
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
3115
|
+
return p;
|
|
3116
|
+
}
|
|
3117
|
+
return next;
|
|
2885
3118
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3119
|
+
case "remove": {
|
|
3120
|
+
if (!input.target) {
|
|
3121
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
3122
|
+
return p;
|
|
3123
|
+
}
|
|
3124
|
+
const next = removePlanItem(p, input.target);
|
|
3125
|
+
if (next === p) {
|
|
3126
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
3127
|
+
return p;
|
|
3128
|
+
}
|
|
3129
|
+
return next;
|
|
2890
3130
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3131
|
+
case "promote": {
|
|
3132
|
+
if (!input.target) {
|
|
3133
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
3134
|
+
return p;
|
|
3135
|
+
}
|
|
3136
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
3137
|
+
if (!derived) {
|
|
3138
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
3139
|
+
return p;
|
|
3140
|
+
}
|
|
3141
|
+
ctx.state.replaceTodos(derived.todos);
|
|
3142
|
+
early = mkResult(
|
|
3143
|
+
derived.plan,
|
|
3144
|
+
true,
|
|
3145
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
3146
|
+
derived.todos
|
|
3147
|
+
);
|
|
3148
|
+
return derived.plan;
|
|
2894
3149
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3150
|
+
case "template_use": {
|
|
3151
|
+
const templateName = input.template?.trim();
|
|
3152
|
+
if (!templateName) {
|
|
3153
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
3154
|
+
return p;
|
|
3155
|
+
}
|
|
3156
|
+
const template = getPlanTemplate(templateName);
|
|
3157
|
+
if (!template) {
|
|
3158
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
3159
|
+
return p;
|
|
3160
|
+
}
|
|
3161
|
+
let updated = p;
|
|
3162
|
+
for (const item of template.items) {
|
|
3163
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
3164
|
+
}
|
|
3165
|
+
early = mkResult(
|
|
3166
|
+
updated,
|
|
3167
|
+
true,
|
|
3168
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
3169
|
+
);
|
|
3170
|
+
return updated;
|
|
2908
3171
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3172
|
+
case "clear":
|
|
3173
|
+
return clearPlan(p);
|
|
3174
|
+
case "taskify": {
|
|
3175
|
+
if (!input.target) {
|
|
3176
|
+
early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
|
|
3177
|
+
return p;
|
|
3178
|
+
}
|
|
3179
|
+
let itemIdx = -1;
|
|
3180
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
3181
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
3182
|
+
itemIdx = asNum - 1;
|
|
3183
|
+
} else {
|
|
3184
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
3185
|
+
if (itemIdx === -1) {
|
|
3186
|
+
const lower = input.target.toLowerCase();
|
|
3187
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
3188
|
+
}
|
|
2918
3189
|
}
|
|
3190
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
3191
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
3192
|
+
return p;
|
|
3193
|
+
}
|
|
3194
|
+
const item = p.items[itemIdx];
|
|
3195
|
+
taskifyMeta.title = item.title;
|
|
3196
|
+
taskifyMeta.details = item.details ?? "";
|
|
3197
|
+
didTaskify = true;
|
|
3198
|
+
break;
|
|
2919
3199
|
}
|
|
2920
|
-
|
|
2921
|
-
early = mkResult(p, false, `
|
|
3200
|
+
default:
|
|
3201
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
2922
3202
|
return p;
|
|
2923
|
-
}
|
|
2924
|
-
const item = p.items[itemIdx];
|
|
2925
|
-
taskifyMeta.title = item.title;
|
|
2926
|
-
taskifyMeta.details = item.details ?? "";
|
|
2927
|
-
didTaskify = true;
|
|
2928
|
-
break;
|
|
2929
3203
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3204
|
+
return p;
|
|
3205
|
+
});
|
|
3206
|
+
} catch (err) {
|
|
3207
|
+
return {
|
|
3208
|
+
ok: false,
|
|
3209
|
+
message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
3210
|
+
plan: "",
|
|
3211
|
+
count: 0,
|
|
3212
|
+
open: 0
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
2936
3215
|
if (early) return early;
|
|
2937
3216
|
if (didTaskify) {
|
|
2938
|
-
const
|
|
2939
|
-
if (typeof
|
|
3217
|
+
const taskPathRaw = ctx.meta["task.path"];
|
|
3218
|
+
if (typeof taskPathRaw !== "string" || !taskPathRaw) {
|
|
2940
3219
|
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
2941
3220
|
}
|
|
2942
|
-
|
|
3221
|
+
let taskPath = taskPathRaw;
|
|
3222
|
+
if (input.scope === "project") {
|
|
3223
|
+
const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
|
|
3224
|
+
taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
3225
|
+
}
|
|
2943
3226
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3227
|
+
try {
|
|
3228
|
+
const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
|
|
3229
|
+
f.tasks.push({
|
|
3230
|
+
id: `task_${randomUUID()}`,
|
|
3231
|
+
title: taskifyMeta.title,
|
|
3232
|
+
description: taskifyMeta.details || void 0,
|
|
3233
|
+
type: "feature",
|
|
3234
|
+
priority: "medium",
|
|
3235
|
+
status: "pending",
|
|
3236
|
+
createdAt: now,
|
|
3237
|
+
updatedAt: now
|
|
3238
|
+
});
|
|
3239
|
+
return f;
|
|
3240
|
+
});
|
|
3241
|
+
return mkResult(
|
|
3242
|
+
plan,
|
|
3243
|
+
true,
|
|
3244
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
2959
3245
|
${formatTaskList(taskFile.tasks)}`
|
|
2960
|
-
|
|
3246
|
+
);
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
3249
|
+
}
|
|
2961
3250
|
}
|
|
2962
3251
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
2963
3252
|
}
|
|
@@ -2980,8 +3269,9 @@ var gitTool = {
|
|
|
2980
3269
|
name: "git",
|
|
2981
3270
|
category: "Git",
|
|
2982
3271
|
description: "Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.",
|
|
2983
|
-
usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
|
|
3272
|
+
usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\n- For `commit` in a possibly-shared working tree, pass an explicit `files` list scoped to what YOU changed. A bare commit (no `files`) includes ALL staged changes and may capture another agent's half-done work. Heed the `warning` field on the result.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
|
|
2984
3273
|
permission: "confirm",
|
|
3274
|
+
icon: "git",
|
|
2985
3275
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
2986
3276
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
2987
3277
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
@@ -3068,6 +3358,22 @@ var gitTool = {
|
|
|
3068
3358
|
};
|
|
3069
3359
|
}
|
|
3070
3360
|
const args = buildArgs(input);
|
|
3361
|
+
let safetyWarning;
|
|
3362
|
+
if (input.command === "commit") {
|
|
3363
|
+
try {
|
|
3364
|
+
const report = await assessCommitSafety({
|
|
3365
|
+
cwd: ctx.cwd,
|
|
3366
|
+
projectRoot: ctx.projectRoot,
|
|
3367
|
+
sessionId: ctx.session?.id,
|
|
3368
|
+
signal: opts.signal
|
|
3369
|
+
});
|
|
3370
|
+
if (report.warning) {
|
|
3371
|
+
const scopeNote = input.files ? "" : "\nNote: this commit has no explicit `files` list, so it will include ALL staged changes. Pass `files` to scope the commit to only what you changed.";
|
|
3372
|
+
safetyWarning = report.warning + scopeNote;
|
|
3373
|
+
}
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3071
3377
|
let stagedDiff;
|
|
3072
3378
|
if (input.command === "commit" && !input.dry_run) {
|
|
3073
3379
|
try {
|
|
@@ -3081,6 +3387,7 @@ var gitTool = {
|
|
|
3081
3387
|
}
|
|
3082
3388
|
const result = await runGit(args, gitDir, opts.signal);
|
|
3083
3389
|
if (stagedDiff !== void 0) result.diff = stagedDiff;
|
|
3390
|
+
if (safetyWarning !== void 0) result.warning = safetyWarning;
|
|
3084
3391
|
return result;
|
|
3085
3392
|
}
|
|
3086
3393
|
};
|
|
@@ -3240,6 +3547,7 @@ var patchTool = {
|
|
|
3240
3547
|
permission: "confirm",
|
|
3241
3548
|
mutating: true,
|
|
3242
3549
|
capabilities: ["fs.write"],
|
|
3550
|
+
icon: "edit",
|
|
3243
3551
|
timeoutMs: 3e4,
|
|
3244
3552
|
inputSchema: {
|
|
3245
3553
|
type: "object",
|
|
@@ -3353,6 +3661,7 @@ var jsonTool = {
|
|
|
3353
3661
|
mutating: false,
|
|
3354
3662
|
timeoutMs: 5e3,
|
|
3355
3663
|
capabilities: ["fs.read"],
|
|
3664
|
+
icon: "json",
|
|
3356
3665
|
inputSchema: {
|
|
3357
3666
|
type: "object",
|
|
3358
3667
|
properties: {
|
|
@@ -3475,6 +3784,7 @@ var diffTool = {
|
|
|
3475
3784
|
permission: "auto",
|
|
3476
3785
|
mutating: false,
|
|
3477
3786
|
capabilities: ["fs.read"],
|
|
3787
|
+
icon: "diff",
|
|
3478
3788
|
timeoutMs: 1e4,
|
|
3479
3789
|
inputSchema: {
|
|
3480
3790
|
type: "object",
|
|
@@ -3633,6 +3943,7 @@ var treeTool = {
|
|
|
3633
3943
|
permission: "auto",
|
|
3634
3944
|
mutating: false,
|
|
3635
3945
|
capabilities: ["fs.read"],
|
|
3946
|
+
icon: "tree",
|
|
3636
3947
|
timeoutMs: 15e3,
|
|
3637
3948
|
inputSchema: {
|
|
3638
3949
|
type: "object",
|
|
@@ -3799,8 +4110,9 @@ async function* spawnStream(opts) {
|
|
|
3799
4110
|
let pending2 = "";
|
|
3800
4111
|
let error;
|
|
3801
4112
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
3802
|
-
const
|
|
3803
|
-
const needsShell = isWin2 && (
|
|
4113
|
+
const resolved = resolveWin32Command(opts.cmd);
|
|
4114
|
+
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4115
|
+
const cmd = needsShell ? opts.cmd : resolved;
|
|
3804
4116
|
const child = spawn(cmd, opts.args, {
|
|
3805
4117
|
cwd: opts.cwd,
|
|
3806
4118
|
env: buildChildEnv(),
|
|
@@ -3958,6 +4270,7 @@ var lintTool = {
|
|
|
3958
4270
|
mutating: false,
|
|
3959
4271
|
timeoutMs: 6e4,
|
|
3960
4272
|
capabilities: ["shell.restricted"],
|
|
4273
|
+
icon: "code",
|
|
3961
4274
|
inputSchema: {
|
|
3962
4275
|
type: "object",
|
|
3963
4276
|
properties: {
|
|
@@ -4052,6 +4365,7 @@ var formatTool = {
|
|
|
4052
4365
|
permission: "confirm",
|
|
4053
4366
|
mutating: true,
|
|
4054
4367
|
capabilities: ["fs.write", "shell.exec"],
|
|
4368
|
+
icon: "code",
|
|
4055
4369
|
timeoutMs: 6e4,
|
|
4056
4370
|
inputSchema: {
|
|
4057
4371
|
type: "object",
|
|
@@ -4153,6 +4467,7 @@ var typecheckTool = {
|
|
|
4153
4467
|
mutating: false,
|
|
4154
4468
|
timeoutMs: 12e4,
|
|
4155
4469
|
capabilities: ["shell.restricted"],
|
|
4470
|
+
icon: "code",
|
|
4156
4471
|
inputSchema: {
|
|
4157
4472
|
type: "object",
|
|
4158
4473
|
properties: {
|
|
@@ -4239,6 +4554,7 @@ var testTool = {
|
|
|
4239
4554
|
usageHint: "ESSENTIAL BEFORE CONSIDERING WORK DONE:\n\n- Use `files` or `grep` to run only relevant tests during development.\n- `coverage: true` is useful when working on critical paths.\nRun tests frequently. A clean test run is usually required before the task can be considered complete.",
|
|
4240
4555
|
permission: "confirm",
|
|
4241
4556
|
mutating: false,
|
|
4557
|
+
icon: "test",
|
|
4242
4558
|
timeoutMs: 12e4,
|
|
4243
4559
|
capabilities: ["shell.restricted"],
|
|
4244
4560
|
inputSchema: {
|
|
@@ -4396,6 +4712,7 @@ var installTool = {
|
|
|
4396
4712
|
permission: "confirm",
|
|
4397
4713
|
mutating: true,
|
|
4398
4714
|
riskTier: "standard",
|
|
4715
|
+
icon: "package",
|
|
4399
4716
|
timeoutMs: 12e4,
|
|
4400
4717
|
capabilities: ["package.install", "shell.restricted"],
|
|
4401
4718
|
inputSchema: {
|
|
@@ -4535,6 +4852,7 @@ var auditTool = {
|
|
|
4535
4852
|
permission: "confirm",
|
|
4536
4853
|
mutating: false,
|
|
4537
4854
|
capabilities: ["shell.restricted"],
|
|
4855
|
+
icon: "package",
|
|
4538
4856
|
timeoutMs: 6e4,
|
|
4539
4857
|
inputSchema: {
|
|
4540
4858
|
type: "object",
|
|
@@ -4630,6 +4948,7 @@ var outdatedTool = {
|
|
|
4630
4948
|
description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
|
|
4631
4949
|
usageHint: "MAINTENANCE & SECURITY TOOL:\n\n- Run periodically or before dependency-related work.\n- Helps surface packages that may need updates for security or features.\n- Hits the package registry over HTTP, so it is NOT purely local \u2014 flagged as mutating for the confirmation gate.\nUse the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.",
|
|
4632
4950
|
permission: "confirm",
|
|
4951
|
+
icon: "package",
|
|
4633
4952
|
// Network side-effecting (registry HTTP). Pairs with `mutating: true`
|
|
4634
4953
|
// so the H7 invariant test (`no auto-permission tool declares
|
|
4635
4954
|
// mutating: true`) passes — a tool claiming `'auto'` must be purely
|
|
@@ -4639,12 +4958,15 @@ var outdatedTool = {
|
|
|
4639
4958
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
|
|
4640
4959
|
// web_search) but missed this one; applying the same contract here.
|
|
4641
4960
|
mutating: true,
|
|
4642
|
-
// Capability is
|
|
4961
|
+
// Capability is outbound network — the tool only hits the package
|
|
4643
4962
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
4963
|
+
// Use the canonical `net.outbound` capability (not the non-existent
|
|
4964
|
+
// `network` string) so the subagent allowlist recognises it and
|
|
4965
|
+
// permits read-only registry lookups under a director.
|
|
4644
4966
|
// The H7 invariant test requires this array to be non-empty for
|
|
4645
4967
|
// any mutating:true tool (meta-tools whitelisted). See
|
|
4646
4968
|
// tests/permission-mutating-invariant.test.ts:92.
|
|
4647
|
-
capabilities: ["
|
|
4969
|
+
capabilities: ["net.outbound"],
|
|
4648
4970
|
timeoutMs: 6e4,
|
|
4649
4971
|
inputSchema: {
|
|
4650
4972
|
type: "object",
|
|
@@ -4681,7 +5003,8 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
4681
5003
|
const MAX = 1e5;
|
|
4682
5004
|
const resolved = resolveWin32Command(manager);
|
|
4683
5005
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4684
|
-
const
|
|
5006
|
+
const spawnCmd = needsShell ? manager : resolved;
|
|
5007
|
+
const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
4685
5008
|
child.stdout?.on("data", (c) => {
|
|
4686
5009
|
if (stdout.length < MAX) stdout += c.toString();
|
|
4687
5010
|
});
|
|
@@ -4746,6 +5069,7 @@ var logsTool = {
|
|
|
4746
5069
|
mutating: false,
|
|
4747
5070
|
timeoutMs: 3e4,
|
|
4748
5071
|
capabilities: ["shell.restricted"],
|
|
5072
|
+
icon: "logs",
|
|
4749
5073
|
inputSchema: {
|
|
4750
5074
|
type: "object",
|
|
4751
5075
|
properties: {
|
|
@@ -4950,6 +5274,7 @@ var documentTool = {
|
|
|
4950
5274
|
mutating: false,
|
|
4951
5275
|
timeoutMs: 3e4,
|
|
4952
5276
|
capabilities: ["fs.read"],
|
|
5277
|
+
icon: "document",
|
|
4953
5278
|
inputSchema: {
|
|
4954
5279
|
type: "object",
|
|
4955
5280
|
properties: {
|
|
@@ -5185,6 +5510,7 @@ var scaffoldTool = {
|
|
|
5185
5510
|
permission: "confirm",
|
|
5186
5511
|
mutating: true,
|
|
5187
5512
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
5513
|
+
icon: "scaffold",
|
|
5188
5514
|
timeoutMs: 3e4,
|
|
5189
5515
|
inputSchema: {
|
|
5190
5516
|
type: "object",
|
|
@@ -5280,6 +5606,7 @@ var toolSearchTool = {
|
|
|
5280
5606
|
mutating: false,
|
|
5281
5607
|
timeoutMs: 1e3,
|
|
5282
5608
|
capabilities: ["tool.meta"],
|
|
5609
|
+
icon: "meta",
|
|
5283
5610
|
inputSchema: {
|
|
5284
5611
|
type: "object",
|
|
5285
5612
|
properties: {
|
|
@@ -5359,6 +5686,7 @@ var toolUseTool = {
|
|
|
5359
5686
|
mutating: true,
|
|
5360
5687
|
timeoutMs: 6e4,
|
|
5361
5688
|
capabilities: ["tool.mutate.any"],
|
|
5689
|
+
icon: "meta",
|
|
5362
5690
|
inputSchema: {
|
|
5363
5691
|
type: "object",
|
|
5364
5692
|
properties: {
|
|
@@ -5429,6 +5757,7 @@ var batchToolUseTool = {
|
|
|
5429
5757
|
mutating: true,
|
|
5430
5758
|
timeoutMs: 12e4,
|
|
5431
5759
|
capabilities: ["tool.mutate.any"],
|
|
5760
|
+
icon: "meta",
|
|
5432
5761
|
inputSchema: {
|
|
5433
5762
|
type: "object",
|
|
5434
5763
|
properties: {
|
|
@@ -5534,6 +5863,7 @@ var toolHelpTool = {
|
|
|
5534
5863
|
mutating: false,
|
|
5535
5864
|
timeoutMs: 5e3,
|
|
5536
5865
|
capabilities: ["tool.meta"],
|
|
5866
|
+
icon: "meta",
|
|
5537
5867
|
inputSchema: {
|
|
5538
5868
|
type: "object",
|
|
5539
5869
|
properties: {
|
|
@@ -5658,6 +5988,7 @@ function rememberTool(memory) {
|
|
|
5658
5988
|
mutating: true,
|
|
5659
5989
|
timeoutMs: 2e3,
|
|
5660
5990
|
capabilities: ["memory.write"],
|
|
5991
|
+
icon: "settings",
|
|
5661
5992
|
inputSchema: {
|
|
5662
5993
|
type: "object",
|
|
5663
5994
|
properties: {
|
|
@@ -5832,6 +6163,7 @@ function createModeTool(modeStore) {
|
|
|
5832
6163
|
mutating: true,
|
|
5833
6164
|
timeoutMs: 5e3,
|
|
5834
6165
|
capabilities: ["session.mode"],
|
|
6166
|
+
icon: "settings",
|
|
5835
6167
|
inputSchema: {
|
|
5836
6168
|
type: "object",
|
|
5837
6169
|
properties: {
|
|
@@ -6588,39 +6920,57 @@ var IndexStore = class {
|
|
|
6588
6920
|
}
|
|
6589
6921
|
});
|
|
6590
6922
|
}
|
|
6923
|
+
/**
|
|
6924
|
+
* Bulk-insert refs for many source symbols in a single transaction.
|
|
6925
|
+
*
|
|
6926
|
+
* Unlike {@link insertRefs} this does NOT delete per source id — the caller
|
|
6927
|
+
* (the indexer) has already cleared stale refs for the file via
|
|
6928
|
+
* {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
|
|
6929
|
+
* repeated once per symbol. One transaction for the whole file instead of one
|
|
6930
|
+
* per symbol turns an O(symbols) transaction count into O(1).
|
|
6931
|
+
*
|
|
6932
|
+
* Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
|
|
6933
|
+
*/
|
|
6934
|
+
insertRefsBatch(refs) {
|
|
6935
|
+
if (refs.length === 0) return;
|
|
6936
|
+
this.runWithRetry(() => {
|
|
6937
|
+
const stmt = this.db.prepare(
|
|
6938
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
6939
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
6940
|
+
);
|
|
6941
|
+
for (const ref of refs) {
|
|
6942
|
+
stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
6943
|
+
}
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6591
6946
|
/**
|
|
6592
6947
|
* Delete all refs whose source symbols are in a given file.
|
|
6593
6948
|
* Used when re-indexing a file to clear stale refs.
|
|
6594
6949
|
*/
|
|
6595
6950
|
deleteRefsForFile(file) {
|
|
6596
6951
|
this.runWithRetry(() => {
|
|
6597
|
-
|
|
6598
|
-
"SELECT id FROM symbols WHERE file = ?"
|
|
6599
|
-
).
|
|
6600
|
-
if (!ids.length) return;
|
|
6601
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
6602
|
-
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
6952
|
+
this.db.prepare(
|
|
6953
|
+
"DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
6954
|
+
).run(file);
|
|
6603
6955
|
});
|
|
6604
6956
|
}
|
|
6605
6957
|
/**
|
|
6606
6958
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
6607
6959
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
6960
|
+
*
|
|
6961
|
+
* Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
|
|
6962
|
+
* the UPDATE to refs that will actually resolve, so `.changes` counts only refs
|
|
6963
|
+
* that found a target — matching the previous per-row loop's return value.
|
|
6608
6964
|
*/
|
|
6609
6965
|
resolveRefs() {
|
|
6610
6966
|
return this.runWithRetry(() => {
|
|
6611
|
-
const
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
if (first) {
|
|
6619
|
-
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
6620
|
-
resolved++;
|
|
6621
|
-
}
|
|
6622
|
-
}
|
|
6623
|
-
return resolved;
|
|
6967
|
+
const result = this.db.prepare(
|
|
6968
|
+
`UPDATE refs SET to_id = (
|
|
6969
|
+
SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
|
|
6970
|
+
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
6971
|
+
AND to_name IN (SELECT name FROM symbols)`
|
|
6972
|
+
).run();
|
|
6973
|
+
return result.changes ?? 0;
|
|
6624
6974
|
});
|
|
6625
6975
|
}
|
|
6626
6976
|
/**
|
|
@@ -8095,13 +8445,26 @@ async function runIndexerWithStore(store, opts) {
|
|
|
8095
8445
|
symbolsIndexed += count;
|
|
8096
8446
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
8097
8447
|
if (parsed.refs && parsed.refs.length > 0) {
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
if (
|
|
8102
|
-
|
|
8103
|
-
|
|
8448
|
+
const refsByLine = /* @__PURE__ */ new Map();
|
|
8449
|
+
for (const r of parsed.refs) {
|
|
8450
|
+
let arr = refsByLine.get(r.line);
|
|
8451
|
+
if (!arr) {
|
|
8452
|
+
arr = [];
|
|
8453
|
+
refsByLine.set(r.line, arr);
|
|
8104
8454
|
}
|
|
8455
|
+
arr.push(r);
|
|
8456
|
+
}
|
|
8457
|
+
const batch = [];
|
|
8458
|
+
for (const sym of symbolsWithIds) {
|
|
8459
|
+
const symRefs = refsByLine.get(sym.line);
|
|
8460
|
+
if (symRefs) {
|
|
8461
|
+
for (const r of symRefs) {
|
|
8462
|
+
batch.push({ ...r, fromId: sym.id });
|
|
8463
|
+
}
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
if (batch.length > 0) {
|
|
8467
|
+
store.insertRefsBatch(batch);
|
|
8105
8468
|
}
|
|
8106
8469
|
}
|
|
8107
8470
|
store.upsertFile({
|
|
@@ -8494,6 +8857,7 @@ async function codebaseIndexStats(args, opts = {}) {
|
|
|
8494
8857
|
var codebaseIndexTool = {
|
|
8495
8858
|
name: "codebase-index",
|
|
8496
8859
|
category: "Project",
|
|
8860
|
+
icon: "index",
|
|
8497
8861
|
description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
|
|
8498
8862
|
usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
|
|
8499
8863
|
permission: "confirm",
|
|
@@ -8550,6 +8914,7 @@ var codebaseIndexTool = {
|
|
|
8550
8914
|
var codebaseSearchTool = {
|
|
8551
8915
|
name: "codebase-search",
|
|
8552
8916
|
category: "Project",
|
|
8917
|
+
icon: "index",
|
|
8553
8918
|
description: "Semantic/keyword search over the indexed codebase symbols (functions, classes, interfaces, etc.). Uses BM25 ranking. Much more powerful and structured than raw `grep` for finding code by name or concept.",
|
|
8554
8919
|
usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
|
|
8555
8920
|
permission: "auto",
|
|
@@ -8638,6 +9003,7 @@ var codebaseSearchTool = {
|
|
|
8638
9003
|
var codebaseStatsTool = {
|
|
8639
9004
|
name: "codebase-stats",
|
|
8640
9005
|
category: "Project",
|
|
9006
|
+
icon: "index",
|
|
8641
9007
|
description: "Return health and statistics about the current symbol index (total symbols, files, language/kind breakdown, size, last update). Useful to decide whether to re-index.",
|
|
8642
9008
|
usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
|
|
8643
9009
|
permission: "auto",
|
|
@@ -8698,6 +9064,7 @@ var setWorkingDirTool = {
|
|
|
8698
9064
|
permission: "confirm",
|
|
8699
9065
|
mutating: true,
|
|
8700
9066
|
capabilities: ["fs.read"],
|
|
9067
|
+
icon: "settings",
|
|
8701
9068
|
timeoutMs: 5e3,
|
|
8702
9069
|
inputSchema: {
|
|
8703
9070
|
type: "object",
|
|
@@ -8758,11 +9125,12 @@ function findTaskIndex(tasks, query2) {
|
|
|
8758
9125
|
var taskTool = {
|
|
8759
9126
|
name: "task",
|
|
8760
9127
|
category: "Session",
|
|
8761
|
-
description:
|
|
8762
|
-
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
|
|
9128
|
+
description: 'Manage session-persistent structured work items with dependencies, types, and priorities. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. Tasks are written to disk and survive session resumes. By default they are isolated to this session; use `scope: "project"` to store tasks in a shared project-level file visible to all sessions.',
|
|
9129
|
+
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate\n- `scope`: "session" (default, isolated) or "project" (shared across sessions)',
|
|
8763
9130
|
permission: "confirm",
|
|
8764
9131
|
mutating: true,
|
|
8765
9132
|
capabilities: ["fs.write"],
|
|
9133
|
+
icon: "task",
|
|
8766
9134
|
timeoutMs: 2e3,
|
|
8767
9135
|
inputSchema: {
|
|
8768
9136
|
type: "object",
|
|
@@ -8828,12 +9196,26 @@ var taskTool = {
|
|
|
8828
9196
|
type: "array",
|
|
8829
9197
|
items: { type: "string" },
|
|
8830
9198
|
description: "Optional subtask titles for action=promote. Each becomes a pending todo."
|
|
9199
|
+
},
|
|
9200
|
+
scope: {
|
|
9201
|
+
type: "string",
|
|
9202
|
+
enum: ["session", "project"],
|
|
9203
|
+
description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
|
|
8831
9204
|
}
|
|
8832
9205
|
},
|
|
8833
9206
|
required: ["action"]
|
|
8834
9207
|
},
|
|
8835
9208
|
async execute(input, ctx) {
|
|
8836
|
-
const
|
|
9209
|
+
const sessionTaskPath = ctx.meta["task.path"];
|
|
9210
|
+
let taskPath;
|
|
9211
|
+
if (input.scope === "project") {
|
|
9212
|
+
if (typeof sessionTaskPath === "string") {
|
|
9213
|
+
const lastSep = Math.max(sessionTaskPath.lastIndexOf("/"), sessionTaskPath.lastIndexOf("\\"));
|
|
9214
|
+
taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
9215
|
+
}
|
|
9216
|
+
} else {
|
|
9217
|
+
taskPath = sessionTaskPath;
|
|
9218
|
+
}
|
|
8837
9219
|
if (typeof taskPath !== "string" || !taskPath) {
|
|
8838
9220
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
8839
9221
|
}
|
|
@@ -8843,23 +9225,66 @@ var taskTool = {
|
|
|
8843
9225
|
const planifyMeta = { title: "", details: "" };
|
|
8844
9226
|
let didPlanify = false;
|
|
8845
9227
|
let todosToReplace = null;
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
9228
|
+
let file;
|
|
9229
|
+
try {
|
|
9230
|
+
file = await mutateTasks(taskPath, sessionId, async (f) => {
|
|
9231
|
+
switch (input.action) {
|
|
9232
|
+
case "show":
|
|
9233
|
+
break;
|
|
9234
|
+
case "replace": {
|
|
9235
|
+
if (!Array.isArray(input.tasks)) {
|
|
9236
|
+
early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
|
|
9237
|
+
return f;
|
|
9238
|
+
}
|
|
9239
|
+
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
9240
|
+
if (newIds.size !== input.tasks.length) {
|
|
9241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9242
|
+
const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
|
|
9243
|
+
early = {
|
|
9244
|
+
ok: false,
|
|
9245
|
+
message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
|
|
9246
|
+
count: 0,
|
|
9247
|
+
completed: 0,
|
|
9248
|
+
inProgress: 0
|
|
9249
|
+
};
|
|
9250
|
+
return f;
|
|
9251
|
+
}
|
|
9252
|
+
for (const t of input.tasks) {
|
|
9253
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
9254
|
+
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
9255
|
+
if (missing.length > 0) {
|
|
9256
|
+
early = {
|
|
9257
|
+
ok: false,
|
|
9258
|
+
message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
|
|
9259
|
+
count: 0,
|
|
9260
|
+
completed: 0,
|
|
9261
|
+
inProgress: 0
|
|
9262
|
+
};
|
|
9263
|
+
return f;
|
|
9264
|
+
}
|
|
9265
|
+
}
|
|
9266
|
+
}
|
|
9267
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9268
|
+
f.tasks = input.tasks.map((t) => ({
|
|
9269
|
+
...t,
|
|
9270
|
+
createdAt: t.createdAt || now,
|
|
9271
|
+
updatedAt: now
|
|
9272
|
+
}));
|
|
9273
|
+
break;
|
|
8854
9274
|
}
|
|
8855
|
-
|
|
8856
|
-
|
|
9275
|
+
case "add": {
|
|
9276
|
+
const t = input.task;
|
|
9277
|
+
if (!t || !t.title) {
|
|
9278
|
+
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
9279
|
+
return f;
|
|
9280
|
+
}
|
|
8857
9281
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
8858
|
-
const
|
|
9282
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
9283
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
8859
9284
|
if (missing.length > 0) {
|
|
8860
9285
|
early = {
|
|
8861
9286
|
ok: false,
|
|
8862
|
-
message: `dependsOn validation failed: task
|
|
9287
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
8863
9288
|
count: 0,
|
|
8864
9289
|
completed: 0,
|
|
8865
9290
|
inProgress: 0
|
|
@@ -8867,165 +9292,170 @@ var taskTool = {
|
|
|
8867
9292
|
return f;
|
|
8868
9293
|
}
|
|
8869
9294
|
}
|
|
9295
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9296
|
+
const newTask = {
|
|
9297
|
+
id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
9298
|
+
title: t.title,
|
|
9299
|
+
description: t.description,
|
|
9300
|
+
type: t.type || "feature",
|
|
9301
|
+
priority: t.priority || "medium",
|
|
9302
|
+
status: t.status || "pending",
|
|
9303
|
+
dependsOn: t.dependsOn,
|
|
9304
|
+
assignee: t.assignee,
|
|
9305
|
+
estimateHours: t.estimateHours,
|
|
9306
|
+
tags: t.tags,
|
|
9307
|
+
createdAt: now,
|
|
9308
|
+
updatedAt: now
|
|
9309
|
+
};
|
|
9310
|
+
f.tasks.push(newTask);
|
|
9311
|
+
break;
|
|
8870
9312
|
}
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
createdAt: t.createdAt || now,
|
|
8875
|
-
updatedAt: now
|
|
8876
|
-
}));
|
|
8877
|
-
break;
|
|
8878
|
-
}
|
|
8879
|
-
case "add": {
|
|
8880
|
-
const t = input.task;
|
|
8881
|
-
if (!t || !t.title) {
|
|
8882
|
-
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
8883
|
-
return f;
|
|
8884
|
-
}
|
|
8885
|
-
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
8886
|
-
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
8887
|
-
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
8888
|
-
if (missing.length > 0) {
|
|
8889
|
-
early = {
|
|
8890
|
-
ok: false,
|
|
8891
|
-
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
8892
|
-
count: 0,
|
|
8893
|
-
completed: 0,
|
|
8894
|
-
inProgress: 0
|
|
8895
|
-
};
|
|
9313
|
+
case "status": {
|
|
9314
|
+
if (!input.id || !input.status) {
|
|
9315
|
+
early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
8896
9316
|
return f;
|
|
8897
9317
|
}
|
|
9318
|
+
const task = f.tasks.find((t) => t.id === input.id);
|
|
9319
|
+
if (!task) {
|
|
9320
|
+
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
9321
|
+
return f;
|
|
9322
|
+
}
|
|
9323
|
+
task.status = input.status;
|
|
9324
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9325
|
+
break;
|
|
8898
9326
|
}
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
}
|
|
8922
|
-
const task = f.tasks.find((t) => t.id === input.id);
|
|
8923
|
-
if (!task) {
|
|
8924
|
-
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
8925
|
-
return f;
|
|
8926
|
-
}
|
|
8927
|
-
task.status = input.status;
|
|
8928
|
-
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8929
|
-
break;
|
|
8930
|
-
}
|
|
8931
|
-
case "promote": {
|
|
8932
|
-
const target = input.target?.trim();
|
|
8933
|
-
if (!target) {
|
|
8934
|
-
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8935
|
-
return f;
|
|
8936
|
-
}
|
|
8937
|
-
const idx = findTaskIndex(f.tasks, target);
|
|
8938
|
-
if (idx === -1) {
|
|
8939
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8940
|
-
return f;
|
|
8941
|
-
}
|
|
8942
|
-
const match = f.tasks[idx];
|
|
8943
|
-
if (!match) {
|
|
8944
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8945
|
-
return f;
|
|
8946
|
-
}
|
|
8947
|
-
if (match.status !== "completed" && match.status !== "failed") {
|
|
8948
|
-
match.status = "in_progress";
|
|
8949
|
-
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8950
|
-
}
|
|
8951
|
-
const todos = [];
|
|
8952
|
-
const ts2 = Date.now();
|
|
8953
|
-
todos.push({
|
|
8954
|
-
id: `todo_${ts2}_task`,
|
|
8955
|
-
content: match.title,
|
|
8956
|
-
status: "in_progress",
|
|
8957
|
-
activeForm: match.title,
|
|
8958
|
-
promotedFromTask: match.id
|
|
8959
|
-
});
|
|
8960
|
-
if (match.description) {
|
|
9327
|
+
case "promote": {
|
|
9328
|
+
const target = input.target?.trim();
|
|
9329
|
+
if (!target) {
|
|
9330
|
+
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
9331
|
+
return f;
|
|
9332
|
+
}
|
|
9333
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
9334
|
+
if (idx === -1) {
|
|
9335
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
9336
|
+
return f;
|
|
9337
|
+
}
|
|
9338
|
+
const match = f.tasks[idx];
|
|
9339
|
+
if (!match) {
|
|
9340
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
9341
|
+
return f;
|
|
9342
|
+
}
|
|
9343
|
+
if (match.status !== "completed" && match.status !== "failed") {
|
|
9344
|
+
match.status = "in_progress";
|
|
9345
|
+
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9346
|
+
}
|
|
9347
|
+
const todos = [];
|
|
9348
|
+
const ts2 = Date.now();
|
|
8961
9349
|
todos.push({
|
|
8962
|
-
id: `todo_${ts2}
|
|
8963
|
-
content: match.
|
|
8964
|
-
status: "
|
|
9350
|
+
id: `todo_${ts2}_task`,
|
|
9351
|
+
content: match.title,
|
|
9352
|
+
status: "in_progress",
|
|
9353
|
+
activeForm: match.title,
|
|
8965
9354
|
promotedFromTask: match.id
|
|
8966
9355
|
});
|
|
8967
|
-
|
|
8968
|
-
if (input.subtasks && input.subtasks.length > 0) {
|
|
8969
|
-
for (const st of input.subtasks) {
|
|
9356
|
+
if (match.description) {
|
|
8970
9357
|
todos.push({
|
|
8971
9358
|
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
8972
|
-
content:
|
|
9359
|
+
content: match.description.slice(0, 200),
|
|
8973
9360
|
status: "pending",
|
|
8974
9361
|
promotedFromTask: match.id
|
|
8975
9362
|
});
|
|
8976
9363
|
}
|
|
9364
|
+
if (input.subtasks && input.subtasks.length > 0) {
|
|
9365
|
+
for (const st of input.subtasks) {
|
|
9366
|
+
todos.push({
|
|
9367
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
9368
|
+
content: st,
|
|
9369
|
+
status: "pending",
|
|
9370
|
+
promotedFromTask: match.id
|
|
9371
|
+
});
|
|
9372
|
+
}
|
|
9373
|
+
}
|
|
9374
|
+
todosToReplace = todos;
|
|
9375
|
+
promoteMeta.count = todos.length;
|
|
9376
|
+
promoteMeta.title = match.title;
|
|
9377
|
+
break;
|
|
8977
9378
|
}
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
9379
|
+
case "planify": {
|
|
9380
|
+
const target = input.target?.trim();
|
|
9381
|
+
if (!target) {
|
|
9382
|
+
early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
9383
|
+
return f;
|
|
9384
|
+
}
|
|
9385
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
9386
|
+
if (idx === -1) {
|
|
9387
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
9388
|
+
return f;
|
|
9389
|
+
}
|
|
9390
|
+
const match = f.tasks[idx];
|
|
9391
|
+
if (!match) {
|
|
9392
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
9393
|
+
return f;
|
|
9394
|
+
}
|
|
9395
|
+
planifyMeta.title = match.title;
|
|
9396
|
+
planifyMeta.details = match.description ?? "";
|
|
9397
|
+
didPlanify = true;
|
|
9398
|
+
break;
|
|
8993
9399
|
}
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
9400
|
+
default:
|
|
9401
|
+
early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
|
|
8997
9402
|
return f;
|
|
8998
|
-
}
|
|
8999
|
-
planifyMeta.title = match.title;
|
|
9000
|
-
planifyMeta.details = match.description ?? "";
|
|
9001
|
-
didPlanify = true;
|
|
9002
|
-
break;
|
|
9003
9403
|
}
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9404
|
+
return f;
|
|
9405
|
+
});
|
|
9406
|
+
} catch (err) {
|
|
9407
|
+
return {
|
|
9408
|
+
ok: false,
|
|
9409
|
+
message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
9410
|
+
count: 0,
|
|
9411
|
+
completed: 0,
|
|
9412
|
+
inProgress: 0
|
|
9413
|
+
};
|
|
9414
|
+
}
|
|
9010
9415
|
if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
|
|
9011
9416
|
if (early) return early;
|
|
9012
9417
|
if (didPlanify) {
|
|
9013
9418
|
const { title, details } = planifyMeta;
|
|
9014
|
-
const
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9419
|
+
const planPathRaw = ctx.meta["plan.path"];
|
|
9420
|
+
const prog = computeTaskItemProgress(file.tasks);
|
|
9421
|
+
if (typeof planPathRaw === "string" && planPathRaw) {
|
|
9422
|
+
let planPath = planPathRaw;
|
|
9423
|
+
if (input.scope === "project") {
|
|
9424
|
+
const lastSep = Math.max(planPath.lastIndexOf("/"), planPath.lastIndexOf("\\"));
|
|
9425
|
+
planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
9426
|
+
}
|
|
9427
|
+
let formatted = "";
|
|
9428
|
+
try {
|
|
9429
|
+
await mutatePlan(planPath, sessionId, (pf) => {
|
|
9430
|
+
const { plan: updated } = addPlanItem(pf, title, details || void 0);
|
|
9431
|
+
formatted = formatPlan(updated);
|
|
9432
|
+
return updated;
|
|
9433
|
+
});
|
|
9434
|
+
} catch (err) {
|
|
9435
|
+
return {
|
|
9436
|
+
ok: false,
|
|
9437
|
+
message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
9438
|
+
count: file.tasks.length,
|
|
9439
|
+
completed: prog.completed,
|
|
9440
|
+
inProgress: prog.inProgress
|
|
9441
|
+
};
|
|
9442
|
+
}
|
|
9019
9443
|
return {
|
|
9020
9444
|
ok: true,
|
|
9021
9445
|
message: `planify ok \u2014 added "${title}" to plan.
|
|
9022
|
-
${
|
|
9446
|
+
${formatted}`,
|
|
9023
9447
|
count: file.tasks.length,
|
|
9024
|
-
completed:
|
|
9025
|
-
inProgress:
|
|
9448
|
+
completed: prog.completed,
|
|
9449
|
+
inProgress: prog.inProgress
|
|
9026
9450
|
};
|
|
9027
9451
|
}
|
|
9028
|
-
return {
|
|
9452
|
+
return {
|
|
9453
|
+
ok: false,
|
|
9454
|
+
message: "Plan storage path not configured \u2014 cannot planify.",
|
|
9455
|
+
count: file.tasks.length,
|
|
9456
|
+
completed: prog.completed,
|
|
9457
|
+
inProgress: prog.inProgress
|
|
9458
|
+
};
|
|
9029
9459
|
}
|
|
9030
9460
|
const p = computeTaskItemProgress(file.tasks);
|
|
9031
9461
|
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
@@ -9069,6 +9499,36 @@ var TIER1_TOOLS = [
|
|
|
9069
9499
|
jsonTool,
|
|
9070
9500
|
searchTool
|
|
9071
9501
|
];
|
|
9502
|
+
var TIER2_TOOLS = [
|
|
9503
|
+
replaceTool,
|
|
9504
|
+
execTool,
|
|
9505
|
+
fetchTool,
|
|
9506
|
+
gitTool,
|
|
9507
|
+
treeTool,
|
|
9508
|
+
lintTool,
|
|
9509
|
+
formatTool,
|
|
9510
|
+
typecheckTool,
|
|
9511
|
+
testTool,
|
|
9512
|
+
todoTool,
|
|
9513
|
+
planTool,
|
|
9514
|
+
taskTool,
|
|
9515
|
+
installTool,
|
|
9516
|
+
auditTool
|
|
9517
|
+
];
|
|
9518
|
+
var TIER3_TOOLS = [
|
|
9519
|
+
outdatedTool,
|
|
9520
|
+
logsTool,
|
|
9521
|
+
documentTool,
|
|
9522
|
+
scaffoldTool,
|
|
9523
|
+
toolSearchTool,
|
|
9524
|
+
toolUseTool,
|
|
9525
|
+
batchToolUseTool,
|
|
9526
|
+
toolHelpTool,
|
|
9527
|
+
codebaseIndexTool,
|
|
9528
|
+
codebaseSearchTool,
|
|
9529
|
+
codebaseStatsTool,
|
|
9530
|
+
setWorkingDirTool
|
|
9531
|
+
];
|
|
9072
9532
|
var builtinTools = [
|
|
9073
9533
|
readTool,
|
|
9074
9534
|
writeTool,
|
|
@@ -9115,6 +9575,162 @@ var builtinToolsPack = {
|
|
|
9115
9575
|
tools: builtinTools
|
|
9116
9576
|
};
|
|
9117
9577
|
|
|
9118
|
-
|
|
9578
|
+
// src/tool-icon-map.ts
|
|
9579
|
+
var TOOL_ICON_MAP = {
|
|
9580
|
+
// File operations
|
|
9581
|
+
read: "file",
|
|
9582
|
+
write: "file",
|
|
9583
|
+
create: "file",
|
|
9584
|
+
// File modification
|
|
9585
|
+
edit: "edit",
|
|
9586
|
+
patch: "edit",
|
|
9587
|
+
replace: "edit",
|
|
9588
|
+
// Content search
|
|
9589
|
+
grep: "search",
|
|
9590
|
+
search: "search",
|
|
9591
|
+
// File discovery
|
|
9592
|
+
glob: "folder",
|
|
9593
|
+
// Shell/command execution
|
|
9594
|
+
bash: "terminal",
|
|
9595
|
+
exec: "terminal",
|
|
9596
|
+
run: "terminal",
|
|
9597
|
+
command: "terminal",
|
|
9598
|
+
shell: "terminal",
|
|
9599
|
+
// Network
|
|
9600
|
+
fetch: "web",
|
|
9601
|
+
curl: "web",
|
|
9602
|
+
http: "web",
|
|
9603
|
+
request: "web",
|
|
9604
|
+
// Version control
|
|
9605
|
+
git: "git",
|
|
9606
|
+
// Directory structure
|
|
9607
|
+
tree: "tree",
|
|
9608
|
+
ls: "tree",
|
|
9609
|
+
list: "tree",
|
|
9610
|
+
// Code quality
|
|
9611
|
+
lint: "code",
|
|
9612
|
+
format: "code",
|
|
9613
|
+
typecheck: "code",
|
|
9614
|
+
// Testing
|
|
9615
|
+
test: "test",
|
|
9616
|
+
tests: "test",
|
|
9617
|
+
// Package management
|
|
9618
|
+
install: "package",
|
|
9619
|
+
uninstall: "package",
|
|
9620
|
+
audit: "package",
|
|
9621
|
+
outdated: "package",
|
|
9622
|
+
npm: "package",
|
|
9623
|
+
pnpm: "package",
|
|
9624
|
+
yarn: "package",
|
|
9625
|
+
// Documentation
|
|
9626
|
+
document: "document",
|
|
9627
|
+
doc: "document",
|
|
9628
|
+
jsdoc: "document",
|
|
9629
|
+
// Project scaffolding
|
|
9630
|
+
scaffold: "scaffold",
|
|
9631
|
+
generate: "scaffold",
|
|
9632
|
+
template: "scaffold",
|
|
9633
|
+
// Task management
|
|
9634
|
+
todo: "todo",
|
|
9635
|
+
todos: "todo",
|
|
9636
|
+
// Planning
|
|
9637
|
+
plan: "plan",
|
|
9638
|
+
planning: "plan",
|
|
9639
|
+
// Structured tasks
|
|
9640
|
+
task: "task",
|
|
9641
|
+
tasks: "task",
|
|
9642
|
+
// Meta/tools
|
|
9643
|
+
"tool-use": "meta",
|
|
9644
|
+
"batch-tool-use": "meta",
|
|
9645
|
+
"tool-search": "meta",
|
|
9646
|
+
"tool-help": "meta",
|
|
9647
|
+
tool_use: "meta",
|
|
9648
|
+
batch_tool_use: "meta",
|
|
9649
|
+
tool_search: "meta",
|
|
9650
|
+
tool_help: "meta",
|
|
9651
|
+
// Code indexing
|
|
9652
|
+
"codebase-index": "index",
|
|
9653
|
+
"codebase-search": "index",
|
|
9654
|
+
"codebase-stats": "index",
|
|
9655
|
+
"codebase_index": "index",
|
|
9656
|
+
"codebase_search": "index",
|
|
9657
|
+
"codebase_stats": "index",
|
|
9658
|
+
// Data
|
|
9659
|
+
json: "json",
|
|
9660
|
+
parse: "json",
|
|
9661
|
+
query: "json",
|
|
9662
|
+
// Comparison
|
|
9663
|
+
diff: "diff",
|
|
9664
|
+
compare: "diff",
|
|
9665
|
+
// Logs
|
|
9666
|
+
logs: "logs",
|
|
9667
|
+
log: "logs",
|
|
9668
|
+
// Configuration
|
|
9669
|
+
"set-working-dir": "settings",
|
|
9670
|
+
set_working_dir: "settings",
|
|
9671
|
+
cwd: "settings",
|
|
9672
|
+
cd: "settings",
|
|
9673
|
+
// AI/Agent
|
|
9674
|
+
think: "brain",
|
|
9675
|
+
reason: "brain",
|
|
9676
|
+
analyze: "brain",
|
|
9677
|
+
reasoning: "brain"
|
|
9678
|
+
};
|
|
9679
|
+
function getToolIcon(toolName) {
|
|
9680
|
+
return TOOL_ICON_MAP[toolName.toLowerCase()] ?? "fallback";
|
|
9681
|
+
}
|
|
9682
|
+
var TOOL_ICON_CONFIG = {
|
|
9683
|
+
file: { icon: "file", color: "#6366f1" },
|
|
9684
|
+
// indigo
|
|
9685
|
+
edit: { icon: "edit", color: "#f59e0b" },
|
|
9686
|
+
// amber
|
|
9687
|
+
search: { icon: "search", color: "#10b981" },
|
|
9688
|
+
// emerald
|
|
9689
|
+
folder: { icon: "folder", color: "#8b5cf6" },
|
|
9690
|
+
// violet
|
|
9691
|
+
terminal: { icon: "terminal", color: "#fb923c" },
|
|
9692
|
+
// orange
|
|
9693
|
+
web: { icon: "web", color: "#06b6d4" },
|
|
9694
|
+
// cyan
|
|
9695
|
+
git: { icon: "git", color: "#f97316" },
|
|
9696
|
+
// orange
|
|
9697
|
+
tree: { icon: "tree", color: "#22c55e" },
|
|
9698
|
+
// green
|
|
9699
|
+
code: { icon: "code", color: "#3b82f6" },
|
|
9700
|
+
// blue
|
|
9701
|
+
test: { icon: "test", color: "#84cc16" },
|
|
9702
|
+
// lime
|
|
9703
|
+
package: { icon: "package", color: "#ec4899" },
|
|
9704
|
+
// pink
|
|
9705
|
+
document: { icon: "document", color: "#14b8a6" },
|
|
9706
|
+
// teal
|
|
9707
|
+
scaffold: { icon: "scaffold", color: "#f43f5e" },
|
|
9708
|
+
// rose
|
|
9709
|
+
todo: { icon: "todo", color: "#a855f7" },
|
|
9710
|
+
// purple
|
|
9711
|
+
plan: { icon: "plan", color: "#7c3aed" },
|
|
9712
|
+
// violet-dark
|
|
9713
|
+
task: { icon: "task", color: "#db2777" },
|
|
9714
|
+
// pink-dark
|
|
9715
|
+
meta: { icon: "meta", color: "#6b7280" },
|
|
9716
|
+
// gray
|
|
9717
|
+
index: { icon: "index", color: "#0ea5e9" },
|
|
9718
|
+
// sky
|
|
9719
|
+
json: { icon: "json", color: "#fbbf24" },
|
|
9720
|
+
// yellow
|
|
9721
|
+
diff: { icon: "diff", color: "#a3e635" },
|
|
9722
|
+
// lime-light
|
|
9723
|
+
logs: { icon: "logs", color: "#78716c" },
|
|
9724
|
+
// stone
|
|
9725
|
+
settings: { icon: "settings", color: "#64748b" },
|
|
9726
|
+
// slate
|
|
9727
|
+
brain: { icon: "brain", color: "#d946ef" },
|
|
9728
|
+
// fuchsia
|
|
9729
|
+
fallback: { icon: "fallback", color: "#9ca3af" }
|
|
9730
|
+
// gray-light
|
|
9731
|
+
};
|
|
9732
|
+
var FALLBACK_ICON = "fallback";
|
|
9733
|
+
|
|
9734
|
+
export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
|
|
9119
9735
|
//# sourceMappingURL=index.js.map
|
|
9120
9736
|
//# sourceMappingURL=index.js.map
|