@wrongstack/tools 0.260.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/audit.js +154 -10
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +138 -2
  4. package/dist/bash.js.map +1 -1
  5. package/dist/batch-tool-use.js +1 -0
  6. package/dist/batch-tool-use.js.map +1 -1
  7. package/dist/builtin.d.ts +20 -1
  8. package/dist/builtin.js +674 -333
  9. package/dist/builtin.js.map +1 -1
  10. package/dist/circuit-breaker.d.ts +20 -0
  11. package/dist/circuit-breaker.js +40 -2
  12. package/dist/circuit-breaker.js.map +1 -1
  13. package/dist/codebase-index/index.d.ts +16 -0
  14. package/dist/codebase-index/index.js +62 -27
  15. package/dist/codebase-index/index.js.map +1 -1
  16. package/dist/codebase-index/worker.js +59 -27
  17. package/dist/codebase-index/worker.js.map +1 -1
  18. package/dist/diff.js +14 -6
  19. package/dist/diff.js.map +1 -1
  20. package/dist/document.js +18 -11
  21. package/dist/document.js.map +1 -1
  22. package/dist/edit.js +22 -14
  23. package/dist/edit.js.map +1 -1
  24. package/dist/exec.js +140 -3
  25. package/dist/exec.js.map +1 -1
  26. package/dist/fetch.d.ts +19 -1
  27. package/dist/fetch.js +2 -1
  28. package/dist/fetch.js.map +1 -1
  29. package/dist/format.js +153 -10
  30. package/dist/format.js.map +1 -1
  31. package/dist/git.js +1 -0
  32. package/dist/git.js.map +1 -1
  33. package/dist/glob.js +14 -6
  34. package/dist/glob.js.map +1 -1
  35. package/dist/grep.js +14 -6
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.d.ts +55 -3
  38. package/dist/index.js +833 -336
  39. package/dist/index.js.map +1 -1
  40. package/dist/install.js +154 -10
  41. package/dist/install.js.map +1 -1
  42. package/dist/json.js +2 -0
  43. package/dist/json.js.map +1 -1
  44. package/dist/lint.js +153 -10
  45. package/dist/lint.js.map +1 -1
  46. package/dist/logs.js +14 -6
  47. package/dist/logs.js.map +1 -1
  48. package/dist/memory.js +1 -0
  49. package/dist/memory.js.map +1 -1
  50. package/dist/mode.js +1 -0
  51. package/dist/mode.js.map +1 -1
  52. package/dist/outdated.js +16 -7
  53. package/dist/outdated.js.map +1 -1
  54. package/dist/pack.js +643 -332
  55. package/dist/pack.js.map +1 -1
  56. package/dist/patch.js +14 -6
  57. package/dist/patch.js.map +1 -1
  58. package/dist/process-registry.d.ts +56 -2
  59. package/dist/process-registry.js +138 -3
  60. package/dist/process-registry.js.map +1 -1
  61. package/dist/read.js +24 -18
  62. package/dist/read.js.map +1 -1
  63. package/dist/replace.js +14 -6
  64. package/dist/replace.js.map +1 -1
  65. package/dist/scaffold.js +14 -6
  66. package/dist/scaffold.js.map +1 -1
  67. package/dist/search.js +3 -3
  68. package/dist/search.js.map +1 -1
  69. package/dist/test.js +153 -10
  70. package/dist/test.js.map +1 -1
  71. package/dist/todo.js +1 -0
  72. package/dist/todo.js.map +1 -1
  73. package/dist/tool-help.js +1 -0
  74. package/dist/tool-help.js.map +1 -1
  75. package/dist/tool-icons.d.ts +20 -0
  76. package/dist/tool-icons.js +130 -0
  77. package/dist/tool-icons.js.map +1 -0
  78. package/dist/tool-search.js +1 -0
  79. package/dist/tool-search.js.map +1 -1
  80. package/dist/tool-use.js +1 -0
  81. package/dist/tool-use.js.map +1 -1
  82. package/dist/tree.js +14 -6
  83. package/dist/tree.js.map +1 -1
  84. package/dist/typecheck.js +153 -10
  85. package/dist/typecheck.js.map +1 -1
  86. package/dist/write.js +22 -14
  87. package/dist/write.js.map +1 -1
  88. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@ import * as fs4 from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { resolve, sep, dirname, join } from 'node:path';
4
4
  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, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } 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, mutateTasks, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
6
+ import { toErrorMessage } from '@wrongstack/core/utils';
6
7
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
7
8
  import * as os from 'node:os';
8
9
  import * as fs7 from 'node:fs';
@@ -11,11 +12,11 @@ import * as dns from 'node:dns/promises';
11
12
  import * as net from 'node:net';
12
13
  import { Agent } from 'undici';
13
14
  import TurndownService from 'turndown';
15
+ import { randomUUID } from 'node:crypto';
14
16
  import { createRequire } from 'node:module';
15
17
  import { fileURLToPath } from 'node:url';
16
18
  import { Worker } from 'node:worker_threads';
17
19
  import * as ts from 'typescript';
18
- import { randomUUID } from 'node:crypto';
19
20
 
20
21
  // src/read.ts
21
22
  async function detectPackageManager(cwd) {
@@ -35,20 +36,29 @@ async function detectPackageManager(cwd) {
35
36
  function resolvePath(input, ctx) {
36
37
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
37
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
+ }
38
48
  function ensureInsideRoot(absPath, ctx) {
39
- const root = path.resolve(ctx.projectRoot);
40
49
  const target = path.resolve(absPath);
41
- const rel = path.relative(root, target);
42
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
43
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
44
- }
45
- 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)}"`);
46
53
  }
47
54
  function safeResolve(input, ctx) {
48
55
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
49
56
  }
50
57
  async function assertRealInsideRoot(absPath, ctx) {
51
- const realRoot = await fs4.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
58
+ if (ctx.allowOutsideProjectRoot) return;
59
+ const realRoots = await Promise.all(
60
+ allowedRoots(ctx).map((r) => fs4.realpath(r).catch(() => path.resolve(r)))
61
+ );
52
62
  let probe = absPath;
53
63
  for (; ; ) {
54
64
  let real;
@@ -63,13 +73,10 @@ async function assertRealInsideRoot(absPath, ctx) {
63
73
  }
64
74
  throw err;
65
75
  }
66
- const rel = path.relative(realRoot, real);
67
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
68
- throw new Error(
69
- `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
70
- );
71
- }
72
- return;
76
+ if (isInsideAny(real, realRoots)) return;
77
+ throw new Error(
78
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
79
+ );
73
80
  }
74
81
  }
75
82
  async function safeResolveReal(input, ctx) {
@@ -161,8 +168,6 @@ function normalizeCommandOutput(raw, opts = {}) {
161
168
  text = text.replace(/\n{3,}/g, "\n\n");
162
169
  return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
163
170
  }
164
-
165
- // src/read.ts
166
171
  var MAX_BYTES = 5 * 1024 * 1024;
167
172
  var readTool = {
168
173
  name: "read",
@@ -172,6 +177,7 @@ var readTool = {
172
177
  permission: "auto",
173
178
  mutating: false,
174
179
  capabilities: ["fs.read"],
180
+ icon: "file",
175
181
  maxOutputBytes: 262144,
176
182
  timeoutMs: 5e3,
177
183
  inputSchema: {
@@ -202,7 +208,7 @@ var readTool = {
202
208
  const code = err.code;
203
209
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
204
210
  throw new Error(
205
- `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
211
+ `read: failed to stat "${input.path}": ${toErrorMessage(err)}`
206
212
  );
207
213
  }
208
214
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
@@ -244,6 +250,7 @@ var writeTool = {
244
250
  mutating: true,
245
251
  timeoutMs: 5e3,
246
252
  capabilities: ["fs.write"],
253
+ icon: "file",
247
254
  inputSchema: {
248
255
  type: "object",
249
256
  properties: {
@@ -307,6 +314,7 @@ var editTool = {
307
314
  permission: "confirm",
308
315
  mutating: true,
309
316
  capabilities: ["fs.write"],
317
+ icon: "edit",
310
318
  timeoutMs: 5e3,
311
319
  inputSchema: {
312
320
  type: "object",
@@ -474,6 +482,7 @@ var replaceTool = {
474
482
  permission: "confirm",
475
483
  mutating: true,
476
484
  capabilities: ["fs.write"],
485
+ icon: "edit",
477
486
  timeoutMs: 3e4,
478
487
  inputSchema: {
479
488
  type: "object",
@@ -675,6 +684,7 @@ var globTool = {
675
684
  permission: "auto",
676
685
  mutating: false,
677
686
  capabilities: ["fs.read"],
687
+ icon: "folder",
678
688
  maxOutputBytes: 65536,
679
689
  timeoutMs: 5e3,
680
690
  inputSchema: {
@@ -760,6 +770,7 @@ var grepTool = {
760
770
  permission: "auto",
761
771
  mutating: false,
762
772
  capabilities: ["fs.read"],
773
+ icon: "search",
763
774
  maxOutputBytes: 131072,
764
775
  timeoutMs: 1e4,
765
776
  inputSchema: {
@@ -1161,6 +1172,23 @@ var CircuitBreaker = class {
1161
1172
  lastSlowAt = null;
1162
1173
  /** Timestamp when the breaker was opened (for cooldown calculation). */
1163
1174
  openedAt = null;
1175
+ /**
1176
+ * Master enable flag. When false the breaker is bypassed: `beforeCall`
1177
+ * always returns true and `afterCall` records nothing. The class itself
1178
+ * defaults to enabled (so the standalone unit tests exercise tripping); the
1179
+ * ProcessRegistry flips this off until the user opts in via `/settings`.
1180
+ */
1181
+ enabled = true;
1182
+ /**
1183
+ * Fired (best-effort) when the breaker transitions into the `open` state.
1184
+ * The registry uses this to arm its auto kill/reset countdown.
1185
+ */
1186
+ onTrip;
1187
+ /**
1188
+ * Fired (best-effort) when the breaker returns to `closed` after having been
1189
+ * open/half-open. The registry uses this to cancel a pending kill/reset.
1190
+ */
1191
+ onReset;
1164
1192
  constructor(config = {}) {
1165
1193
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
1166
1194
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -1169,12 +1197,22 @@ var CircuitBreaker = class {
1169
1197
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
1170
1198
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
1171
1199
  }
1200
+ /** Toggle the master enable. Disabling resets to a clean `closed` state. */
1201
+ setEnabled(enabled) {
1202
+ if (this.enabled === enabled) return;
1203
+ this.enabled = enabled;
1204
+ if (!enabled) this._reset();
1205
+ }
1206
+ get isEnabled() {
1207
+ return this.enabled;
1208
+ }
1172
1209
  /**
1173
1210
  * Returns true if the circuit allows a new call to proceed.
1174
1211
  * When false, callers should abort the tool call and return a
1175
1212
  * circuit-breaker error instead of spawning a process.
1176
1213
  */
1177
1214
  get canProceed() {
1215
+ if (!this.enabled) return true;
1178
1216
  this._checkStateTransition();
1179
1217
  return this.state !== "open";
1180
1218
  }
@@ -1210,7 +1248,7 @@ var CircuitBreaker = class {
1210
1248
  * not affect breaker state.
1211
1249
  */
1212
1250
  beforeCall(bypass = false) {
1213
- if (bypass) return true;
1251
+ if (bypass || !this.enabled) return true;
1214
1252
  this._checkStateTransition();
1215
1253
  if (this.state === "open") return false;
1216
1254
  return true;
@@ -1225,7 +1263,7 @@ var CircuitBreaker = class {
1225
1263
  * Use for background/fire-and-forget processes.
1226
1264
  */
1227
1265
  afterCall(durationMs, failed, bypass = false) {
1228
- if (bypass) return;
1266
+ if (bypass || !this.enabled) return;
1229
1267
  const now = Date.now();
1230
1268
  if (this.state === "half-open") {
1231
1269
  if (failed) {
@@ -1271,12 +1309,23 @@ var CircuitBreaker = class {
1271
1309
  if (this.state === "open") return;
1272
1310
  this.state = "open";
1273
1311
  this.openedAt = Date.now();
1312
+ try {
1313
+ this.onTrip?.();
1314
+ } catch {
1315
+ }
1274
1316
  }
1275
1317
  _reset() {
1318
+ const wasRecovering = this.state !== "closed";
1276
1319
  this.state = "closed";
1277
1320
  this.consecutiveFailures = 0;
1278
1321
  this.window = [];
1279
1322
  this.openedAt = null;
1323
+ if (wasRecovering) {
1324
+ try {
1325
+ this.onReset?.();
1326
+ } catch {
1327
+ }
1328
+ }
1280
1329
  }
1281
1330
  /** Transition from open → half-open when cooldown elapses. */
1282
1331
  _checkStateTransition() {
@@ -1338,8 +1387,21 @@ function killWin32Tree(pid) {
1338
1387
  var ProcessRegistryImpl = class {
1339
1388
  processes = /* @__PURE__ */ new Map();
1340
1389
  breaker;
1390
+ /**
1391
+ * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
1392
+ * a countdown is armed; on expiry all tracked processes are killed and the
1393
+ * breaker is reset to closed (forced recovery). Zero means manual recovery
1394
+ * only (`/kill reset`).
1395
+ */
1396
+ autoKillResetMs = 0;
1397
+ autoKillTimer = null;
1398
+ autoKillArmedAt = null;
1399
+ breakerCountdownListeners = [];
1341
1400
  constructor(breakerConfig) {
1342
1401
  this.breaker = new CircuitBreaker(breakerConfig);
1402
+ this.breaker.onTrip = () => this._armAutoKillReset();
1403
+ this.breaker.onReset = () => this._cancelAutoKillReset();
1404
+ this.breaker.setEnabled(false);
1343
1405
  }
1344
1406
  register(info) {
1345
1407
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -1415,6 +1477,90 @@ var ProcessRegistryImpl = class {
1415
1477
  forceBreakerReset() {
1416
1478
  this.breaker.forceReset();
1417
1479
  }
1480
+ /**
1481
+ * Configure circuit-breaker protection at runtime. Called from `/settings`
1482
+ * (instant, all modes) and on TUI mount (applies persisted config).
1483
+ *
1484
+ * - `enabled` toggles whether the breaker gates `bash`/`exec`.
1485
+ * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
1486
+ * trips (0 = manual recovery only).
1487
+ *
1488
+ * Re-applies cleanly on every call: cancels a pending countdown when the
1489
+ * timeout is cleared or protection disabled, and re-arms if the breaker is
1490
+ * currently open under the new settings.
1491
+ */
1492
+ setBreakerConfig(cfg) {
1493
+ if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
1494
+ if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
1495
+ if (this.autoKillResetMs <= 0) {
1496
+ this._cancelAutoKillReset();
1497
+ return;
1498
+ }
1499
+ if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
1500
+ this._armAutoKillReset();
1501
+ }
1502
+ }
1503
+ /**
1504
+ * Live countdown to the next auto kill/reset, or null when nothing is armed.
1505
+ * The TUI polls this on a 1s tick while armed so the statusline decrements.
1506
+ */
1507
+ getBreakerCountdown() {
1508
+ if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
1509
+ const elapsed = Date.now() - this.autoKillArmedAt;
1510
+ return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
1511
+ }
1512
+ /**
1513
+ * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
1514
+ * Use {@link getBreakerCountdown} for the live ticking value between events.
1515
+ */
1516
+ onBreakerCountdownChange(listener) {
1517
+ this.breakerCountdownListeners.push(listener);
1518
+ return () => {
1519
+ this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
1520
+ };
1521
+ }
1522
+ _emitBreakerCountdown() {
1523
+ const snap = this.getBreakerCountdown();
1524
+ for (const l of this.breakerCountdownListeners) {
1525
+ try {
1526
+ l(snap);
1527
+ } catch {
1528
+ }
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
1533
+ * (a fresh trip after a failed half-open probe restarts the clock). No-op
1534
+ * when protection is off or no timeout is configured.
1535
+ */
1536
+ _armAutoKillReset() {
1537
+ if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
1538
+ this._clearAutoKillTimer();
1539
+ this.autoKillArmedAt = Date.now();
1540
+ this.autoKillTimer = setTimeout(() => {
1541
+ this.autoKillTimer = null;
1542
+ this.autoKillArmedAt = null;
1543
+ this.killAll({ force: false });
1544
+ this.breaker.forceReset();
1545
+ this._emitBreakerCountdown();
1546
+ }, this.autoKillResetMs);
1547
+ this.autoKillTimer.unref?.();
1548
+ this._emitBreakerCountdown();
1549
+ }
1550
+ _cancelAutoKillReset() {
1551
+ const wasArmed = this.autoKillArmedAt !== null;
1552
+ this._clearAutoKillTimer();
1553
+ if (wasArmed) {
1554
+ this.autoKillArmedAt = null;
1555
+ this._emitBreakerCountdown();
1556
+ }
1557
+ }
1558
+ _clearAutoKillTimer() {
1559
+ if (this.autoKillTimer !== null) {
1560
+ clearTimeout(this.autoKillTimer);
1561
+ this.autoKillTimer = null;
1562
+ }
1563
+ }
1418
1564
  /** Kill a single process by PID.
1419
1565
  *
1420
1566
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -1539,6 +1685,7 @@ var bashTool = {
1539
1685
  permission: "confirm",
1540
1686
  mutating: true,
1541
1687
  riskTier: "destructive",
1688
+ icon: "terminal",
1542
1689
  // Trust rules match on the literal `command` string. Without subjectKey
1543
1690
  // the policy heuristic would have done the same here, but declaring it
1544
1691
  // explicitly removes the implicit cross-tool aliasing.
@@ -2001,6 +2148,7 @@ var execTool = {
2001
2148
  riskTier: "standard",
2002
2149
  timeoutMs: DEFAULT_TIMEOUT_MS2,
2003
2150
  capabilities: ["shell.restricted"],
2151
+ icon: "terminal",
2004
2152
  inputSchema: {
2005
2153
  type: "object",
2006
2154
  properties: {
@@ -2100,7 +2248,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2100
2248
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
2101
2249
  const resolved = resolveWin32Command(cmd);
2102
2250
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2103
- const child = spawn(resolved, args, {
2251
+ const spawnCmd = needsShell ? cmd : resolved;
2252
+ const child = spawn(spawnCmd, args, {
2104
2253
  cwd,
2105
2254
  env: buildChildEnv(sessionId),
2106
2255
  stdio: ["ignore", "pipe", "pipe"],
@@ -2287,6 +2436,7 @@ var fetchTool = {
2287
2436
  permission: "confirm",
2288
2437
  mutating: false,
2289
2438
  capabilities: ["net.outbound"],
2439
+ icon: "web",
2290
2440
  // Trust rules for fetch match on the literal URL — declare it explicitly
2291
2441
  // so a user can trust `https://api.example.com/*` without accidentally
2292
2442
  // matching that pattern on any other tool that happens to have a `url`
@@ -2437,6 +2587,7 @@ var searchTool = {
2437
2587
  permission: "confirm",
2438
2588
  mutating: false,
2439
2589
  capabilities: ["net.outbound"],
2590
+ icon: "search",
2440
2591
  timeoutMs: TIMEOUT_MS2,
2441
2592
  inputSchema: {
2442
2593
  type: "object",
@@ -2511,7 +2662,7 @@ async function duckduckgoSearch(query2, num, signal) {
2511
2662
  truncated: results.length >= num
2512
2663
  };
2513
2664
  } catch (err) {
2514
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: err instanceof Error ? err.message : String(err) }));
2665
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
2515
2666
  return {
2516
2667
  query: query2,
2517
2668
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -2652,6 +2803,7 @@ var todoTool = {
2652
2803
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
2653
2804
  timeoutMs: 1e3,
2654
2805
  capabilities: ["session.todo"],
2806
+ icon: "todo",
2655
2807
  inputSchema: {
2656
2808
  type: "object",
2657
2809
  properties: {
@@ -2752,11 +2904,12 @@ var todoTool = {
2752
2904
  var planTool = {
2753
2905
  name: "plan",
2754
2906
  category: "Session",
2755
- description: "Manage a persistent strategic plan for the current session. Unlike todos, plans are meant for higher-level, multi-phase approaches and survive across conversation resumptions. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute.",
2756
- 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 or even multiple sessions.',
2907
+ 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.',
2908
+ 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.',
2757
2909
  permission: "confirm",
2758
2910
  mutating: true,
2759
2911
  capabilities: ["fs.write"],
2912
+ icon: "plan",
2760
2913
  timeoutMs: 2e3,
2761
2914
  inputSchema: {
2762
2915
  type: "object",
@@ -2796,12 +2949,26 @@ var planTool = {
2796
2949
  template: {
2797
2950
  type: "string",
2798
2951
  description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
2952
+ },
2953
+ scope: {
2954
+ type: "string",
2955
+ enum: ["session", "project"],
2956
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
2799
2957
  }
2800
2958
  },
2801
2959
  required: ["action"]
2802
2960
  },
2803
2961
  async execute(input, ctx) {
2804
- const planPath = ctx.meta["plan.path"];
2962
+ const sessionPlanPath = ctx.meta["plan.path"];
2963
+ let planPath;
2964
+ if (input.scope === "project") {
2965
+ if (typeof sessionPlanPath === "string") {
2966
+ const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
2967
+ planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
2968
+ }
2969
+ } else {
2970
+ planPath = sessionPlanPath;
2971
+ }
2805
2972
  if (typeof planPath !== "string" || !planPath) {
2806
2973
  return {
2807
2974
  ok: false,
@@ -2815,148 +2982,169 @@ var planTool = {
2815
2982
  let early = null;
2816
2983
  const taskifyMeta = { title: "", details: "" };
2817
2984
  let didTaskify = false;
2818
- const plan = await mutatePlan(planPath, sessionId, async (p) => {
2819
- switch (input.action) {
2820
- case "show":
2821
- break;
2822
- case "add": {
2823
- const title = input.title?.trim();
2824
- if (!title) {
2825
- early = mkResult(p, false, "add requires `title`.");
2826
- return p;
2827
- }
2828
- const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
2829
- return updated;
2830
- }
2831
- case "start":
2832
- case "done": {
2833
- if (!input.target) {
2834
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
2835
- return p;
2836
- }
2837
- const next = setPlanItemStatus(
2838
- p,
2839
- input.target,
2840
- input.action === "start" ? "in_progress" : "done"
2841
- );
2842
- if (next === p) {
2843
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
2844
- return p;
2845
- }
2846
- return next;
2847
- }
2848
- case "remove": {
2849
- if (!input.target) {
2850
- early = mkResult(p, false, "remove requires `target` (id|index|substring).");
2851
- return p;
2852
- }
2853
- const next = removePlanItem(p, input.target);
2854
- if (next === p) {
2855
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
2856
- return p;
2857
- }
2858
- return next;
2859
- }
2860
- case "promote": {
2861
- if (!input.target) {
2862
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
2863
- return p;
2864
- }
2865
- const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
2866
- if (!derived) {
2867
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
2868
- return p;
2985
+ let plan;
2986
+ try {
2987
+ plan = await mutatePlan(planPath, sessionId, async (p) => {
2988
+ switch (input.action) {
2989
+ case "show":
2990
+ break;
2991
+ case "add": {
2992
+ const title = input.title?.trim();
2993
+ if (!title) {
2994
+ early = mkResult(p, false, "add requires `title`.");
2995
+ return p;
2996
+ }
2997
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
2998
+ return updated;
2869
2999
  }
2870
- ctx.state.replaceTodos(derived.todos);
2871
- early = mkResult(
2872
- derived.plan,
2873
- true,
2874
- `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
2875
- derived.todos
2876
- );
2877
- return derived.plan;
2878
- }
2879
- case "template_use": {
2880
- const templateName = input.template?.trim();
2881
- if (!templateName) {
2882
- early = mkResult(p, false, "template_use requires `template` name.");
2883
- return p;
3000
+ case "start":
3001
+ case "done": {
3002
+ if (!input.target) {
3003
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
3004
+ return p;
3005
+ }
3006
+ const next = setPlanItemStatus(
3007
+ p,
3008
+ input.target,
3009
+ input.action === "start" ? "in_progress" : "done"
3010
+ );
3011
+ if (next === p) {
3012
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3013
+ return p;
3014
+ }
3015
+ return next;
2884
3016
  }
2885
- const template = getPlanTemplate(templateName);
2886
- if (!template) {
2887
- early = mkResult(p, false, `Unknown template "${templateName}".`);
2888
- return p;
3017
+ case "remove": {
3018
+ if (!input.target) {
3019
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
3020
+ return p;
3021
+ }
3022
+ const next = removePlanItem(p, input.target);
3023
+ if (next === p) {
3024
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3025
+ return p;
3026
+ }
3027
+ return next;
2889
3028
  }
2890
- let updated = p;
2891
- for (const item of template.items) {
2892
- ({ plan: updated } = addPlanItem(updated, item.title, item.details));
3029
+ case "promote": {
3030
+ if (!input.target) {
3031
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
3032
+ return p;
3033
+ }
3034
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
3035
+ if (!derived) {
3036
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3037
+ return p;
3038
+ }
3039
+ ctx.state.replaceTodos(derived.todos);
3040
+ early = mkResult(
3041
+ derived.plan,
3042
+ true,
3043
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
3044
+ derived.todos
3045
+ );
3046
+ return derived.plan;
2893
3047
  }
2894
- early = mkResult(
2895
- updated,
2896
- true,
2897
- `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
2898
- );
2899
- return updated;
2900
- }
2901
- case "clear":
2902
- return clearPlan(p);
2903
- case "taskify": {
2904
- if (!input.target) {
2905
- early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
2906
- return p;
3048
+ case "template_use": {
3049
+ const templateName = input.template?.trim();
3050
+ if (!templateName) {
3051
+ early = mkResult(p, false, "template_use requires `template` name.");
3052
+ return p;
3053
+ }
3054
+ const template = getPlanTemplate(templateName);
3055
+ if (!template) {
3056
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
3057
+ return p;
3058
+ }
3059
+ let updated = p;
3060
+ for (const item of template.items) {
3061
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
3062
+ }
3063
+ early = mkResult(
3064
+ updated,
3065
+ true,
3066
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
3067
+ );
3068
+ return updated;
2907
3069
  }
2908
- let itemIdx = -1;
2909
- const asNum = Number.parseInt(input.target, 10);
2910
- if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
2911
- itemIdx = asNum - 1;
2912
- } else {
2913
- itemIdx = p.items.findIndex((it) => it.id === input.target);
2914
- if (itemIdx === -1) {
2915
- const lower = input.target.toLowerCase();
2916
- itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
3070
+ case "clear":
3071
+ return clearPlan(p);
3072
+ case "taskify": {
3073
+ if (!input.target) {
3074
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
3075
+ return p;
3076
+ }
3077
+ let itemIdx = -1;
3078
+ const asNum = Number.parseInt(input.target, 10);
3079
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
3080
+ itemIdx = asNum - 1;
3081
+ } else {
3082
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
3083
+ if (itemIdx === -1) {
3084
+ const lower = input.target.toLowerCase();
3085
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
3086
+ }
2917
3087
  }
3088
+ if (itemIdx === -1 || !p.items[itemIdx]) {
3089
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3090
+ return p;
3091
+ }
3092
+ const item = p.items[itemIdx];
3093
+ taskifyMeta.title = item.title;
3094
+ taskifyMeta.details = item.details ?? "";
3095
+ didTaskify = true;
3096
+ break;
2918
3097
  }
2919
- if (itemIdx === -1 || !p.items[itemIdx]) {
2920
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
3098
+ default:
3099
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
2921
3100
  return p;
2922
- }
2923
- const item = p.items[itemIdx];
2924
- taskifyMeta.title = item.title;
2925
- taskifyMeta.details = item.details ?? "";
2926
- didTaskify = true;
2927
- break;
2928
3101
  }
2929
- default:
2930
- early = mkResult(p, false, `Unknown action "${input.action}".`);
2931
- return p;
2932
- }
2933
- return p;
2934
- });
3102
+ return p;
3103
+ });
3104
+ } catch (err) {
3105
+ return {
3106
+ ok: false,
3107
+ message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
3108
+ plan: "",
3109
+ count: 0,
3110
+ open: 0
3111
+ };
3112
+ }
2935
3113
  if (early) return early;
2936
3114
  if (didTaskify) {
2937
- const taskPath = ctx.meta["task.path"];
2938
- if (typeof taskPath !== "string" || !taskPath) {
3115
+ const taskPathRaw = ctx.meta["task.path"];
3116
+ if (typeof taskPathRaw !== "string" || !taskPathRaw) {
2939
3117
  return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
2940
3118
  }
2941
- const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
3119
+ let taskPath = taskPathRaw;
3120
+ if (input.scope === "project") {
3121
+ const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
3122
+ taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
3123
+ }
2942
3124
  const now = (/* @__PURE__ */ new Date()).toISOString();
2943
- taskFile.tasks.push({
2944
- id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
2945
- title: taskifyMeta.title,
2946
- description: taskifyMeta.details || void 0,
2947
- type: "feature",
2948
- priority: "medium",
2949
- status: "pending",
2950
- createdAt: now,
2951
- updatedAt: now
2952
- });
2953
- await saveTasks(taskPath, taskFile);
2954
- return mkResult(
2955
- plan,
2956
- true,
2957
- `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
3125
+ try {
3126
+ const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
3127
+ f.tasks.push({
3128
+ id: `task_${randomUUID()}`,
3129
+ title: taskifyMeta.title,
3130
+ description: taskifyMeta.details || void 0,
3131
+ type: "feature",
3132
+ priority: "medium",
3133
+ status: "pending",
3134
+ createdAt: now,
3135
+ updatedAt: now
3136
+ });
3137
+ return f;
3138
+ });
3139
+ return mkResult(
3140
+ plan,
3141
+ true,
3142
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
2958
3143
  ${formatTaskList(taskFile.tasks)}`
2959
- );
3144
+ );
3145
+ } catch (err) {
3146
+ return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
3147
+ }
2960
3148
  }
2961
3149
  return mkResult(plan, true, `Plan ${input.action} ok.`);
2962
3150
  }
@@ -2981,6 +3169,7 @@ var gitTool = {
2981
3169
  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.",
2982
3170
  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.",
2983
3171
  permission: "confirm",
3172
+ icon: "git",
2984
3173
  // Conservative: any of these may mutate. The non-mutating commands
2985
3174
  // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
2986
3175
  // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
@@ -3239,6 +3428,7 @@ var patchTool = {
3239
3428
  permission: "confirm",
3240
3429
  mutating: true,
3241
3430
  capabilities: ["fs.write"],
3431
+ icon: "edit",
3242
3432
  timeoutMs: 3e4,
3243
3433
  inputSchema: {
3244
3434
  type: "object",
@@ -3352,6 +3542,7 @@ var jsonTool = {
3352
3542
  mutating: false,
3353
3543
  timeoutMs: 5e3,
3354
3544
  capabilities: ["fs.read"],
3545
+ icon: "json",
3355
3546
  inputSchema: {
3356
3547
  type: "object",
3357
3548
  properties: {
@@ -3394,6 +3585,7 @@ var jsonTool = {
3394
3585
  data: null,
3395
3586
  formatted: "",
3396
3587
  type: "unknown",
3588
+ /* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
3397
3589
  error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
3398
3590
  };
3399
3591
  }
@@ -3473,6 +3665,7 @@ var diffTool = {
3473
3665
  permission: "auto",
3474
3666
  mutating: false,
3475
3667
  capabilities: ["fs.read"],
3668
+ icon: "diff",
3476
3669
  timeoutMs: 1e4,
3477
3670
  inputSchema: {
3478
3671
  type: "object",
@@ -3631,6 +3824,7 @@ var treeTool = {
3631
3824
  permission: "auto",
3632
3825
  mutating: false,
3633
3826
  capabilities: ["fs.read"],
3827
+ icon: "tree",
3634
3828
  timeoutMs: 15e3,
3635
3829
  inputSchema: {
3636
3830
  type: "object",
@@ -3797,8 +3991,9 @@ async function* spawnStream(opts) {
3797
3991
  let pending2 = "";
3798
3992
  let error;
3799
3993
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
3800
- const cmd = resolveWin32Command(opts.cmd);
3801
- const needsShell = isWin2 && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
3994
+ const resolved = resolveWin32Command(opts.cmd);
3995
+ const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3996
+ const cmd = needsShell ? opts.cmd : resolved;
3802
3997
  const child = spawn(cmd, opts.args, {
3803
3998
  cwd: opts.cwd,
3804
3999
  env: buildChildEnv(),
@@ -3956,6 +4151,7 @@ var lintTool = {
3956
4151
  mutating: false,
3957
4152
  timeoutMs: 6e4,
3958
4153
  capabilities: ["shell.restricted"],
4154
+ icon: "code",
3959
4155
  inputSchema: {
3960
4156
  type: "object",
3961
4157
  properties: {
@@ -4050,6 +4246,7 @@ var formatTool = {
4050
4246
  permission: "confirm",
4051
4247
  mutating: true,
4052
4248
  capabilities: ["fs.write", "shell.exec"],
4249
+ icon: "code",
4053
4250
  timeoutMs: 6e4,
4054
4251
  inputSchema: {
4055
4252
  type: "object",
@@ -4151,6 +4348,7 @@ var typecheckTool = {
4151
4348
  mutating: false,
4152
4349
  timeoutMs: 12e4,
4153
4350
  capabilities: ["shell.restricted"],
4351
+ icon: "code",
4154
4352
  inputSchema: {
4155
4353
  type: "object",
4156
4354
  properties: {
@@ -4237,6 +4435,7 @@ var testTool = {
4237
4435
  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.",
4238
4436
  permission: "confirm",
4239
4437
  mutating: false,
4438
+ icon: "test",
4240
4439
  timeoutMs: 12e4,
4241
4440
  capabilities: ["shell.restricted"],
4242
4441
  inputSchema: {
@@ -4394,6 +4593,7 @@ var installTool = {
4394
4593
  permission: "confirm",
4395
4594
  mutating: true,
4396
4595
  riskTier: "standard",
4596
+ icon: "package",
4397
4597
  timeoutMs: 12e4,
4398
4598
  capabilities: ["package.install", "shell.restricted"],
4399
4599
  inputSchema: {
@@ -4518,6 +4718,7 @@ function resolveManifestPath(cwd, pkgManager) {
4518
4718
  case "yarn":
4519
4719
  case "npm":
4520
4720
  return join(cwd, "package.json");
4721
+ /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
4521
4722
  default:
4522
4723
  return join(cwd, "package.json");
4523
4724
  }
@@ -4532,6 +4733,7 @@ var auditTool = {
4532
4733
  permission: "confirm",
4533
4734
  mutating: false,
4534
4735
  capabilities: ["shell.restricted"],
4736
+ icon: "package",
4535
4737
  timeoutMs: 6e4,
4536
4738
  inputSchema: {
4537
4739
  type: "object",
@@ -4627,6 +4829,7 @@ var outdatedTool = {
4627
4829
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
4628
4830
  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.",
4629
4831
  permission: "confirm",
4832
+ icon: "package",
4630
4833
  // Network side-effecting (registry HTTP). Pairs with `mutating: true`
4631
4834
  // so the H7 invariant test (`no auto-permission tool declares
4632
4835
  // mutating: true`) passes — a tool claiming `'auto'` must be purely
@@ -4678,7 +4881,8 @@ function runOutdated(manager, args, cwd, signal) {
4678
4881
  const MAX = 1e5;
4679
4882
  const resolved = resolveWin32Command(manager);
4680
4883
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4681
- const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4884
+ const spawnCmd = needsShell ? manager : resolved;
4885
+ const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4682
4886
  child.stdout?.on("data", (c) => {
4683
4887
  if (stdout.length < MAX) stdout += c.toString();
4684
4888
  });
@@ -4743,6 +4947,7 @@ var logsTool = {
4743
4947
  mutating: false,
4744
4948
  timeoutMs: 3e4,
4745
4949
  capabilities: ["shell.restricted"],
4950
+ icon: "logs",
4746
4951
  inputSchema: {
4747
4952
  type: "object",
4748
4953
  properties: {
@@ -4947,6 +5152,7 @@ var documentTool = {
4947
5152
  mutating: false,
4948
5153
  timeoutMs: 3e4,
4949
5154
  capabilities: ["fs.read"],
5155
+ icon: "document",
4950
5156
  inputSchema: {
4951
5157
  type: "object",
4952
5158
  properties: {
@@ -5025,10 +5231,10 @@ async function resolveFiles2(filesInput, cwd) {
5025
5231
  }
5026
5232
  function processFile(content, absPath, _style, _overwrite, target) {
5027
5233
  const results = [];
5028
- const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/;
5029
- const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/;
5030
- const classRegex = /class\s+(\w+)/;
5031
- const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/;
5234
+ const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
5235
+ const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
5236
+ const classRegex = /class\s+(\w+)/g;
5237
+ const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
5032
5238
  const allMatches = [];
5033
5239
  if (target === "all" || target === "function") {
5034
5240
  for (const m of content.matchAll(functionRegex)) {
@@ -5182,6 +5388,7 @@ var scaffoldTool = {
5182
5388
  permission: "confirm",
5183
5389
  mutating: true,
5184
5390
  capabilities: ["fs.write.outside-project", "fs.write"],
5391
+ icon: "scaffold",
5185
5392
  timeoutMs: 3e4,
5186
5393
  inputSchema: {
5187
5394
  type: "object",
@@ -5277,6 +5484,7 @@ var toolSearchTool = {
5277
5484
  mutating: false,
5278
5485
  timeoutMs: 1e3,
5279
5486
  capabilities: ["tool.meta"],
5487
+ icon: "meta",
5280
5488
  inputSchema: {
5281
5489
  type: "object",
5282
5490
  properties: {
@@ -5356,6 +5564,7 @@ var toolUseTool = {
5356
5564
  mutating: true,
5357
5565
  timeoutMs: 6e4,
5358
5566
  capabilities: ["tool.mutate.any"],
5567
+ icon: "meta",
5359
5568
  inputSchema: {
5360
5569
  type: "object",
5361
5570
  properties: {
@@ -5426,6 +5635,7 @@ var batchToolUseTool = {
5426
5635
  mutating: true,
5427
5636
  timeoutMs: 12e4,
5428
5637
  capabilities: ["tool.mutate.any"],
5638
+ icon: "meta",
5429
5639
  inputSchema: {
5430
5640
  type: "object",
5431
5641
  properties: {
@@ -5531,6 +5741,7 @@ var toolHelpTool = {
5531
5741
  mutating: false,
5532
5742
  timeoutMs: 5e3,
5533
5743
  capabilities: ["tool.meta"],
5744
+ icon: "meta",
5534
5745
  inputSchema: {
5535
5746
  type: "object",
5536
5747
  properties: {
@@ -5655,6 +5866,7 @@ function rememberTool(memory) {
5655
5866
  mutating: true,
5656
5867
  timeoutMs: 2e3,
5657
5868
  capabilities: ["memory.write"],
5869
+ icon: "settings",
5658
5870
  inputSchema: {
5659
5871
  type: "object",
5660
5872
  properties: {
@@ -5829,6 +6041,7 @@ function createModeTool(modeStore) {
5829
6041
  mutating: true,
5830
6042
  timeoutMs: 5e3,
5831
6043
  capabilities: ["session.mode"],
6044
+ icon: "settings",
5832
6045
  inputSchema: {
5833
6046
  type: "object",
5834
6047
  properties: {
@@ -6139,7 +6352,7 @@ function loadDatabaseSync() {
6139
6352
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
6140
6353
  } catch (err) {
6141
6354
  throw new Error(
6142
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${err instanceof Error ? err.message : String(err)}`
6355
+ `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
6143
6356
  );
6144
6357
  }
6145
6358
  return DatabaseSyncCtor;
@@ -6585,39 +6798,57 @@ var IndexStore = class {
6585
6798
  }
6586
6799
  });
6587
6800
  }
6801
+ /**
6802
+ * Bulk-insert refs for many source symbols in a single transaction.
6803
+ *
6804
+ * Unlike {@link insertRefs} this does NOT delete per source id — the caller
6805
+ * (the indexer) has already cleared stale refs for the file via
6806
+ * {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
6807
+ * repeated once per symbol. One transaction for the whole file instead of one
6808
+ * per symbol turns an O(symbols) transaction count into O(1).
6809
+ *
6810
+ * Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
6811
+ */
6812
+ insertRefsBatch(refs) {
6813
+ if (refs.length === 0) return;
6814
+ this.runWithRetry(() => {
6815
+ const stmt = this.db.prepare(
6816
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
6817
+ VALUES (?, ?, ?, ?, ?)`
6818
+ );
6819
+ for (const ref of refs) {
6820
+ stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
6821
+ }
6822
+ });
6823
+ }
6588
6824
  /**
6589
6825
  * Delete all refs whose source symbols are in a given file.
6590
6826
  * Used when re-indexing a file to clear stale refs.
6591
6827
  */
6592
6828
  deleteRefsForFile(file) {
6593
6829
  this.runWithRetry(() => {
6594
- const ids = this.db.prepare(
6595
- "SELECT id FROM symbols WHERE file = ?"
6596
- ).all(file);
6597
- if (!ids.length) return;
6598
- const placeholders = ids.map(() => "?").join(",");
6599
- this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
6830
+ this.db.prepare(
6831
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
6832
+ ).run(file);
6600
6833
  });
6601
6834
  }
6602
6835
  /**
6603
6836
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
6604
6837
  * Call this after all symbols have been inserted to fill in cross-references.
6838
+ *
6839
+ * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
6840
+ * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
6841
+ * that found a target — matching the previous per-row loop's return value.
6605
6842
  */
6606
6843
  resolveRefs() {
6607
6844
  return this.runWithRetry(() => {
6608
- const unresolved = this.db.prepare(
6609
- "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
6610
- ).all();
6611
- let resolved = 0;
6612
- for (const row of unresolved) {
6613
- const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
6614
- const first = target[0];
6615
- if (first) {
6616
- this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
6617
- resolved++;
6618
- }
6619
- }
6620
- return resolved;
6845
+ const result = this.db.prepare(
6846
+ `UPDATE refs SET to_id = (
6847
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
6848
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
6849
+ AND to_name IN (SELECT name FROM symbols)`
6850
+ ).run();
6851
+ return result.changes ?? 0;
6621
6852
  });
6622
6853
  }
6623
6854
  /**
@@ -6724,7 +6955,7 @@ function getSignature(node, sourceFile) {
6724
6955
  }
6725
6956
  function getJsDoc(node, sourceFile) {
6726
6957
  const fullText = sourceFile.getFullText();
6727
- const nodePos = node.getFullWidth();
6958
+ const nodePos = node.getFullStart();
6728
6959
  const comments = ts.getLeadingCommentRanges(fullText, nodePos);
6729
6960
  if (!comments) return "";
6730
6961
  for (const range of comments) {
@@ -8092,14 +8323,27 @@ async function runIndexerWithStore(store, opts) {
8092
8323
  symbolsIndexed += count;
8093
8324
  langStats[lang] = (langStats[lang] ?? 0) + count;
8094
8325
  if (parsed.refs && parsed.refs.length > 0) {
8095
- for (let i = 0; i < symbolsWithIds.length; i++) {
8096
- const sym = expectDefined(symbolsWithIds[i]);
8097
- const symRefs = parsed.refs.filter((r) => r.line === sym.line);
8098
- if (symRefs.length > 0) {
8099
- const refsWithFromId = symRefs.map((r) => ({ ...r, fromId: sym.id }));
8100
- store.insertRefs(sym.id, refsWithFromId);
8326
+ const refsByLine = /* @__PURE__ */ new Map();
8327
+ for (const r of parsed.refs) {
8328
+ let arr = refsByLine.get(r.line);
8329
+ if (!arr) {
8330
+ arr = [];
8331
+ refsByLine.set(r.line, arr);
8332
+ }
8333
+ arr.push(r);
8334
+ }
8335
+ const batch = [];
8336
+ for (const sym of symbolsWithIds) {
8337
+ const symRefs = refsByLine.get(sym.line);
8338
+ if (symRefs) {
8339
+ for (const r of symRefs) {
8340
+ batch.push({ ...r, fromId: sym.id });
8341
+ }
8101
8342
  }
8102
8343
  }
8344
+ if (batch.length > 0) {
8345
+ store.insertRefsBatch(batch);
8346
+ }
8103
8347
  }
8104
8348
  store.upsertFile({
8105
8349
  file,
@@ -8491,6 +8735,7 @@ async function codebaseIndexStats(args, opts = {}) {
8491
8735
  var codebaseIndexTool = {
8492
8736
  name: "codebase-index",
8493
8737
  category: "Project",
8738
+ icon: "index",
8494
8739
  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.",
8495
8740
  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.",
8496
8741
  permission: "confirm",
@@ -8547,6 +8792,7 @@ var codebaseIndexTool = {
8547
8792
  var codebaseSearchTool = {
8548
8793
  name: "codebase-search",
8549
8794
  category: "Project",
8795
+ icon: "index",
8550
8796
  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.",
8551
8797
  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.",
8552
8798
  permission: "auto",
@@ -8635,6 +8881,7 @@ var codebaseSearchTool = {
8635
8881
  var codebaseStatsTool = {
8636
8882
  name: "codebase-stats",
8637
8883
  category: "Project",
8884
+ icon: "index",
8638
8885
  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.",
8639
8886
  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.",
8640
8887
  permission: "auto",
@@ -8695,6 +8942,7 @@ var setWorkingDirTool = {
8695
8942
  permission: "confirm",
8696
8943
  mutating: true,
8697
8944
  capabilities: ["fs.read"],
8945
+ icon: "settings",
8698
8946
  timeoutMs: 5e3,
8699
8947
  inputSchema: {
8700
8948
  type: "object",
@@ -8719,7 +8967,7 @@ var setWorkingDirTool = {
8719
8967
  } catch (err) {
8720
8968
  return {
8721
8969
  current: ctx.workingDir,
8722
- error: err instanceof Error ? err.message : String(err)
8970
+ error: toErrorMessage(err)
8723
8971
  };
8724
8972
  }
8725
8973
  try {
@@ -8755,11 +9003,12 @@ function findTaskIndex(tasks, query2) {
8755
9003
  var taskTool = {
8756
9004
  name: "task",
8757
9005
  category: "Session",
8758
- description: "Manage structured work items with dependencies, types, and priorities. Use this for complex, multi-step work where tasks have ordering constraints. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. The task list persists across session resumes.",
8759
- 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',
9006
+ 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.',
9007
+ 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)',
8760
9008
  permission: "confirm",
8761
9009
  mutating: true,
8762
9010
  capabilities: ["fs.write"],
9011
+ icon: "task",
8763
9012
  timeoutMs: 2e3,
8764
9013
  inputSchema: {
8765
9014
  type: "object",
@@ -8825,12 +9074,26 @@ var taskTool = {
8825
9074
  type: "array",
8826
9075
  items: { type: "string" },
8827
9076
  description: "Optional subtask titles for action=promote. Each becomes a pending todo."
9077
+ },
9078
+ scope: {
9079
+ type: "string",
9080
+ enum: ["session", "project"],
9081
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
8828
9082
  }
8829
9083
  },
8830
9084
  required: ["action"]
8831
9085
  },
8832
9086
  async execute(input, ctx) {
8833
- const taskPath = ctx.meta["task.path"];
9087
+ const sessionTaskPath = ctx.meta["task.path"];
9088
+ let taskPath;
9089
+ if (input.scope === "project") {
9090
+ if (typeof sessionTaskPath === "string") {
9091
+ const lastSep = Math.max(sessionTaskPath.lastIndexOf("/"), sessionTaskPath.lastIndexOf("\\"));
9092
+ taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
9093
+ }
9094
+ } else {
9095
+ taskPath = sessionTaskPath;
9096
+ }
8834
9097
  if (typeof taskPath !== "string" || !taskPath) {
8835
9098
  return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
8836
9099
  }
@@ -8840,23 +9103,66 @@ var taskTool = {
8840
9103
  const planifyMeta = { title: "", details: "" };
8841
9104
  let didPlanify = false;
8842
9105
  let todosToReplace = null;
8843
- const file = await mutateTasks(taskPath, sessionId, async (f) => {
8844
- switch (input.action) {
8845
- case "show":
8846
- break;
8847
- case "replace": {
8848
- if (!Array.isArray(input.tasks)) {
8849
- early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
8850
- return f;
9106
+ let file;
9107
+ try {
9108
+ file = await mutateTasks(taskPath, sessionId, async (f) => {
9109
+ switch (input.action) {
9110
+ case "show":
9111
+ break;
9112
+ case "replace": {
9113
+ if (!Array.isArray(input.tasks)) {
9114
+ early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
9115
+ return f;
9116
+ }
9117
+ const newIds = new Set(input.tasks.map((t) => t.id));
9118
+ if (newIds.size !== input.tasks.length) {
9119
+ const seen = /* @__PURE__ */ new Set();
9120
+ const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
9121
+ early = {
9122
+ ok: false,
9123
+ message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
9124
+ count: 0,
9125
+ completed: 0,
9126
+ inProgress: 0
9127
+ };
9128
+ return f;
9129
+ }
9130
+ for (const t of input.tasks) {
9131
+ if (t.dependsOn && t.dependsOn.length > 0) {
9132
+ const missing = t.dependsOn.filter((d) => !newIds.has(d));
9133
+ if (missing.length > 0) {
9134
+ early = {
9135
+ ok: false,
9136
+ message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
9137
+ count: 0,
9138
+ completed: 0,
9139
+ inProgress: 0
9140
+ };
9141
+ return f;
9142
+ }
9143
+ }
9144
+ }
9145
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9146
+ f.tasks = input.tasks.map((t) => ({
9147
+ ...t,
9148
+ createdAt: t.createdAt || now,
9149
+ updatedAt: now
9150
+ }));
9151
+ break;
8851
9152
  }
8852
- const newIds = new Set(input.tasks.map((t) => t.id));
8853
- for (const t of input.tasks) {
9153
+ case "add": {
9154
+ const t = input.task;
9155
+ if (!t || !t.title) {
9156
+ early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
9157
+ return f;
9158
+ }
8854
9159
  if (t.dependsOn && t.dependsOn.length > 0) {
8855
- const missing = t.dependsOn.filter((d) => !newIds.has(d));
9160
+ const existingIds = new Set(f.tasks.map((e) => e.id));
9161
+ const missing = t.dependsOn.filter((d) => !existingIds.has(d));
8856
9162
  if (missing.length > 0) {
8857
9163
  early = {
8858
9164
  ok: false,
8859
- message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
9165
+ message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
8860
9166
  count: 0,
8861
9167
  completed: 0,
8862
9168
  inProgress: 0
@@ -8864,165 +9170,170 @@ var taskTool = {
8864
9170
  return f;
8865
9171
  }
8866
9172
  }
9173
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9174
+ const newTask = {
9175
+ id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
9176
+ title: t.title,
9177
+ description: t.description,
9178
+ type: t.type || "feature",
9179
+ priority: t.priority || "medium",
9180
+ status: t.status || "pending",
9181
+ dependsOn: t.dependsOn,
9182
+ assignee: t.assignee,
9183
+ estimateHours: t.estimateHours,
9184
+ tags: t.tags,
9185
+ createdAt: now,
9186
+ updatedAt: now
9187
+ };
9188
+ f.tasks.push(newTask);
9189
+ break;
8867
9190
  }
8868
- const now = (/* @__PURE__ */ new Date()).toISOString();
8869
- f.tasks = input.tasks.map((t) => ({
8870
- ...t,
8871
- createdAt: t.createdAt || now,
8872
- updatedAt: now
8873
- }));
8874
- break;
8875
- }
8876
- case "add": {
8877
- const t = input.task;
8878
- if (!t || !t.title) {
8879
- early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
8880
- return f;
8881
- }
8882
- if (t.dependsOn && t.dependsOn.length > 0) {
8883
- const existingIds = new Set(f.tasks.map((e) => e.id));
8884
- const missing = t.dependsOn.filter((d) => !existingIds.has(d));
8885
- if (missing.length > 0) {
8886
- early = {
8887
- ok: false,
8888
- message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
8889
- count: 0,
8890
- completed: 0,
8891
- inProgress: 0
8892
- };
9191
+ case "status": {
9192
+ if (!input.id || !input.status) {
9193
+ early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8893
9194
  return f;
8894
9195
  }
9196
+ const task = f.tasks.find((t) => t.id === input.id);
9197
+ if (!task) {
9198
+ early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
9199
+ return f;
9200
+ }
9201
+ task.status = input.status;
9202
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9203
+ break;
8895
9204
  }
8896
- const now = (/* @__PURE__ */ new Date()).toISOString();
8897
- const newTask = {
8898
- id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
8899
- title: t.title,
8900
- description: t.description,
8901
- type: t.type || "feature",
8902
- priority: t.priority || "medium",
8903
- status: t.status || "pending",
8904
- dependsOn: t.dependsOn,
8905
- assignee: t.assignee,
8906
- estimateHours: t.estimateHours,
8907
- tags: t.tags,
8908
- createdAt: now,
8909
- updatedAt: now
8910
- };
8911
- f.tasks.push(newTask);
8912
- break;
8913
- }
8914
- case "status": {
8915
- if (!input.id || !input.status) {
8916
- early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8917
- return f;
8918
- }
8919
- const task = f.tasks.find((t) => t.id === input.id);
8920
- if (!task) {
8921
- early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
8922
- return f;
8923
- }
8924
- task.status = input.status;
8925
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8926
- break;
8927
- }
8928
- case "promote": {
8929
- const target = input.target?.trim();
8930
- if (!target) {
8931
- early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8932
- return f;
8933
- }
8934
- const idx = findTaskIndex(f.tasks, target);
8935
- if (idx === -1) {
8936
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8937
- return f;
8938
- }
8939
- const match = f.tasks[idx];
8940
- if (!match) {
8941
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8942
- return f;
8943
- }
8944
- if (match.status !== "completed" && match.status !== "failed") {
8945
- match.status = "in_progress";
8946
- match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8947
- }
8948
- const todos = [];
8949
- const ts2 = Date.now();
8950
- todos.push({
8951
- id: `todo_${ts2}_task`,
8952
- content: match.title,
8953
- status: "in_progress",
8954
- activeForm: match.title,
8955
- promotedFromTask: match.id
8956
- });
8957
- if (match.description) {
9205
+ case "promote": {
9206
+ const target = input.target?.trim();
9207
+ if (!target) {
9208
+ early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
9209
+ return f;
9210
+ }
9211
+ const idx = findTaskIndex(f.tasks, target);
9212
+ if (idx === -1) {
9213
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9214
+ return f;
9215
+ }
9216
+ const match = f.tasks[idx];
9217
+ if (!match) {
9218
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9219
+ return f;
9220
+ }
9221
+ if (match.status !== "completed" && match.status !== "failed") {
9222
+ match.status = "in_progress";
9223
+ match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9224
+ }
9225
+ const todos = [];
9226
+ const ts2 = Date.now();
8958
9227
  todos.push({
8959
- id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8960
- content: match.description.slice(0, 200),
8961
- status: "pending",
9228
+ id: `todo_${ts2}_task`,
9229
+ content: match.title,
9230
+ status: "in_progress",
9231
+ activeForm: match.title,
8962
9232
  promotedFromTask: match.id
8963
9233
  });
8964
- }
8965
- if (input.subtasks && input.subtasks.length > 0) {
8966
- for (const st of input.subtasks) {
9234
+ if (match.description) {
8967
9235
  todos.push({
8968
9236
  id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8969
- content: st,
9237
+ content: match.description.slice(0, 200),
8970
9238
  status: "pending",
8971
9239
  promotedFromTask: match.id
8972
9240
  });
8973
9241
  }
9242
+ if (input.subtasks && input.subtasks.length > 0) {
9243
+ for (const st of input.subtasks) {
9244
+ todos.push({
9245
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
9246
+ content: st,
9247
+ status: "pending",
9248
+ promotedFromTask: match.id
9249
+ });
9250
+ }
9251
+ }
9252
+ todosToReplace = todos;
9253
+ promoteMeta.count = todos.length;
9254
+ promoteMeta.title = match.title;
9255
+ break;
8974
9256
  }
8975
- todosToReplace = todos;
8976
- promoteMeta.count = todos.length;
8977
- promoteMeta.title = match.title;
8978
- break;
8979
- }
8980
- case "planify": {
8981
- const target = input.target?.trim();
8982
- if (!target) {
8983
- early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8984
- return f;
8985
- }
8986
- const idx = findTaskIndex(f.tasks, target);
8987
- if (idx === -1) {
8988
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8989
- return f;
9257
+ case "planify": {
9258
+ const target = input.target?.trim();
9259
+ if (!target) {
9260
+ early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
9261
+ return f;
9262
+ }
9263
+ const idx = findTaskIndex(f.tasks, target);
9264
+ if (idx === -1) {
9265
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9266
+ return f;
9267
+ }
9268
+ const match = f.tasks[idx];
9269
+ if (!match) {
9270
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9271
+ return f;
9272
+ }
9273
+ planifyMeta.title = match.title;
9274
+ planifyMeta.details = match.description ?? "";
9275
+ didPlanify = true;
9276
+ break;
8990
9277
  }
8991
- const match = f.tasks[idx];
8992
- if (!match) {
8993
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9278
+ default:
9279
+ early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
8994
9280
  return f;
8995
- }
8996
- planifyMeta.title = match.title;
8997
- planifyMeta.details = match.description ?? "";
8998
- didPlanify = true;
8999
- break;
9000
9281
  }
9001
- default:
9002
- early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
9003
- return f;
9004
- }
9005
- return f;
9006
- });
9282
+ return f;
9283
+ });
9284
+ } catch (err) {
9285
+ return {
9286
+ ok: false,
9287
+ message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
9288
+ count: 0,
9289
+ completed: 0,
9290
+ inProgress: 0
9291
+ };
9292
+ }
9007
9293
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
9008
9294
  if (early) return early;
9009
9295
  if (didPlanify) {
9010
9296
  const { title, details } = planifyMeta;
9011
- const planPath = ctx.meta["plan.path"];
9012
- if (typeof planPath === "string" && planPath) {
9013
- const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
9014
- const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
9015
- await savePlan(planPath, updated);
9297
+ const planPathRaw = ctx.meta["plan.path"];
9298
+ const prog = computeTaskItemProgress(file.tasks);
9299
+ if (typeof planPathRaw === "string" && planPathRaw) {
9300
+ let planPath = planPathRaw;
9301
+ if (input.scope === "project") {
9302
+ const lastSep = Math.max(planPath.lastIndexOf("/"), planPath.lastIndexOf("\\"));
9303
+ planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
9304
+ }
9305
+ let formatted = "";
9306
+ try {
9307
+ await mutatePlan(planPath, sessionId, (pf) => {
9308
+ const { plan: updated } = addPlanItem(pf, title, details || void 0);
9309
+ formatted = formatPlan(updated);
9310
+ return updated;
9311
+ });
9312
+ } catch (err) {
9313
+ return {
9314
+ ok: false,
9315
+ message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
9316
+ count: file.tasks.length,
9317
+ completed: prog.completed,
9318
+ inProgress: prog.inProgress
9319
+ };
9320
+ }
9016
9321
  return {
9017
9322
  ok: true,
9018
9323
  message: `planify ok \u2014 added "${title}" to plan.
9019
- ${formatPlan(updated)}`,
9324
+ ${formatted}`,
9020
9325
  count: file.tasks.length,
9021
- completed: computeTaskItemProgress(file.tasks).completed,
9022
- inProgress: computeTaskItemProgress(file.tasks).inProgress
9326
+ completed: prog.completed,
9327
+ inProgress: prog.inProgress
9023
9328
  };
9024
9329
  }
9025
- return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
9330
+ return {
9331
+ ok: false,
9332
+ message: "Plan storage path not configured \u2014 cannot planify.",
9333
+ count: file.tasks.length,
9334
+ completed: prog.completed,
9335
+ inProgress: prog.inProgress
9336
+ };
9026
9337
  }
9027
9338
  const p = computeTaskItemProgress(file.tasks);
9028
9339
  const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
@@ -9066,6 +9377,36 @@ var TIER1_TOOLS = [
9066
9377
  jsonTool,
9067
9378
  searchTool
9068
9379
  ];
9380
+ var TIER2_TOOLS = [
9381
+ replaceTool,
9382
+ execTool,
9383
+ fetchTool,
9384
+ gitTool,
9385
+ treeTool,
9386
+ lintTool,
9387
+ formatTool,
9388
+ typecheckTool,
9389
+ testTool,
9390
+ todoTool,
9391
+ planTool,
9392
+ taskTool,
9393
+ installTool,
9394
+ auditTool
9395
+ ];
9396
+ var TIER3_TOOLS = [
9397
+ outdatedTool,
9398
+ logsTool,
9399
+ documentTool,
9400
+ scaffoldTool,
9401
+ toolSearchTool,
9402
+ toolUseTool,
9403
+ batchToolUseTool,
9404
+ toolHelpTool,
9405
+ codebaseIndexTool,
9406
+ codebaseSearchTool,
9407
+ codebaseStatsTool,
9408
+ setWorkingDirTool
9409
+ ];
9069
9410
  var builtinTools = [
9070
9411
  readTool,
9071
9412
  writeTool,
@@ -9112,6 +9453,162 @@ var builtinToolsPack = {
9112
9453
  tools: builtinTools
9113
9454
  };
9114
9455
 
9115
- export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, 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 };
9456
+ // src/tool-icon-map.ts
9457
+ var TOOL_ICON_MAP = {
9458
+ // File operations
9459
+ read: "file",
9460
+ write: "file",
9461
+ create: "file",
9462
+ // File modification
9463
+ edit: "edit",
9464
+ patch: "edit",
9465
+ replace: "edit",
9466
+ // Content search
9467
+ grep: "search",
9468
+ search: "search",
9469
+ // File discovery
9470
+ glob: "folder",
9471
+ // Shell/command execution
9472
+ bash: "terminal",
9473
+ exec: "terminal",
9474
+ run: "terminal",
9475
+ command: "terminal",
9476
+ shell: "terminal",
9477
+ // Network
9478
+ fetch: "web",
9479
+ curl: "web",
9480
+ http: "web",
9481
+ request: "web",
9482
+ // Version control
9483
+ git: "git",
9484
+ // Directory structure
9485
+ tree: "tree",
9486
+ ls: "tree",
9487
+ list: "tree",
9488
+ // Code quality
9489
+ lint: "code",
9490
+ format: "code",
9491
+ typecheck: "code",
9492
+ // Testing
9493
+ test: "test",
9494
+ tests: "test",
9495
+ // Package management
9496
+ install: "package",
9497
+ uninstall: "package",
9498
+ audit: "package",
9499
+ outdated: "package",
9500
+ npm: "package",
9501
+ pnpm: "package",
9502
+ yarn: "package",
9503
+ // Documentation
9504
+ document: "document",
9505
+ doc: "document",
9506
+ jsdoc: "document",
9507
+ // Project scaffolding
9508
+ scaffold: "scaffold",
9509
+ generate: "scaffold",
9510
+ template: "scaffold",
9511
+ // Task management
9512
+ todo: "todo",
9513
+ todos: "todo",
9514
+ // Planning
9515
+ plan: "plan",
9516
+ planning: "plan",
9517
+ // Structured tasks
9518
+ task: "task",
9519
+ tasks: "task",
9520
+ // Meta/tools
9521
+ "tool-use": "meta",
9522
+ "batch-tool-use": "meta",
9523
+ "tool-search": "meta",
9524
+ "tool-help": "meta",
9525
+ tool_use: "meta",
9526
+ batch_tool_use: "meta",
9527
+ tool_search: "meta",
9528
+ tool_help: "meta",
9529
+ // Code indexing
9530
+ "codebase-index": "index",
9531
+ "codebase-search": "index",
9532
+ "codebase-stats": "index",
9533
+ "codebase_index": "index",
9534
+ "codebase_search": "index",
9535
+ "codebase_stats": "index",
9536
+ // Data
9537
+ json: "json",
9538
+ parse: "json",
9539
+ query: "json",
9540
+ // Comparison
9541
+ diff: "diff",
9542
+ compare: "diff",
9543
+ // Logs
9544
+ logs: "logs",
9545
+ log: "logs",
9546
+ // Configuration
9547
+ "set-working-dir": "settings",
9548
+ set_working_dir: "settings",
9549
+ cwd: "settings",
9550
+ cd: "settings",
9551
+ // AI/Agent
9552
+ think: "brain",
9553
+ reason: "brain",
9554
+ analyze: "brain",
9555
+ reasoning: "brain"
9556
+ };
9557
+ function getToolIcon(toolName) {
9558
+ return TOOL_ICON_MAP[toolName.toLowerCase()] ?? "fallback";
9559
+ }
9560
+ var TOOL_ICON_CONFIG = {
9561
+ file: { icon: "file", color: "#6366f1" },
9562
+ // indigo
9563
+ edit: { icon: "edit", color: "#f59e0b" },
9564
+ // amber
9565
+ search: { icon: "search", color: "#10b981" },
9566
+ // emerald
9567
+ folder: { icon: "folder", color: "#8b5cf6" },
9568
+ // violet
9569
+ terminal: { icon: "terminal", color: "#ef4444" },
9570
+ // red
9571
+ web: { icon: "web", color: "#06b6d4" },
9572
+ // cyan
9573
+ git: { icon: "git", color: "#f97316" },
9574
+ // orange
9575
+ tree: { icon: "tree", color: "#22c55e" },
9576
+ // green
9577
+ code: { icon: "code", color: "#3b82f6" },
9578
+ // blue
9579
+ test: { icon: "test", color: "#84cc16" },
9580
+ // lime
9581
+ package: { icon: "package", color: "#ec4899" },
9582
+ // pink
9583
+ document: { icon: "document", color: "#14b8a6" },
9584
+ // teal
9585
+ scaffold: { icon: "scaffold", color: "#f43f5e" },
9586
+ // rose
9587
+ todo: { icon: "todo", color: "#a855f7" },
9588
+ // purple
9589
+ plan: { icon: "plan", color: "#7c3aed" },
9590
+ // violet-dark
9591
+ task: { icon: "task", color: "#db2777" },
9592
+ // pink-dark
9593
+ meta: { icon: "meta", color: "#6b7280" },
9594
+ // gray
9595
+ index: { icon: "index", color: "#0ea5e9" },
9596
+ // sky
9597
+ json: { icon: "json", color: "#fbbf24" },
9598
+ // yellow
9599
+ diff: { icon: "diff", color: "#a3e635" },
9600
+ // lime-light
9601
+ logs: { icon: "logs", color: "#78716c" },
9602
+ // stone
9603
+ settings: { icon: "settings", color: "#64748b" },
9604
+ // slate
9605
+ brain: { icon: "brain", color: "#d946ef" },
9606
+ // fuchsia
9607
+ fallback: { icon: "fallback", color: "#9ca3af" }
9608
+ // gray-light
9609
+ };
9610
+ var FALLBACK_ICON = "fallback";
9611
+
9612
+ 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 };
9116
9613
  //# sourceMappingURL=index.js.map
9117
9614
  //# sourceMappingURL=index.js.map