@wrongstack/tools 0.148.0 → 0.236.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 +21 -1
- package/dist/audit.js.map +1 -1
- package/dist/{background-indexer-C2014mH0.d.ts → background-indexer-CtbgPExj.d.ts} +1 -0
- package/dist/bash.js +116 -24
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +702 -209
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +9 -2
- package/dist/circuit-breaker.js +11 -2
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.d.ts +9 -2
- package/dist/codebase-index/index.js +50 -18
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/diff.js +1 -1
- package/dist/diff.js.map +1 -1
- package/dist/document.js +1 -1
- package/dist/document.js.map +1 -1
- package/dist/edit.js +1 -1
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +60 -11
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +21 -1
- package/dist/format.js.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/glob.js +1 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +1 -1
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +711 -217
- package/dist/index.js.map +1 -1
- package/dist/install.js +65 -14
- package/dist/install.js.map +1 -1
- package/dist/lint.js +21 -1
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +1 -1
- package/dist/logs.js.map +1 -1
- package/dist/outdated.js +1 -1
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +702 -209
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +1 -1
- package/dist/patch.js.map +1 -1
- package/dist/process-registry.d.ts +21 -16
- package/dist/process-registry.js +48 -10
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +1 -1
- package/dist/read.js.map +1 -1
- package/dist/replace.js +1 -1
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +1 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/search.js +19 -16
- package/dist/search.js.map +1 -1
- package/dist/test.js +21 -1
- package/dist/test.js.map +1 -1
- package/dist/todo.js +44 -0
- package/dist/todo.js.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +21 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +1 -1
- package/dist/write.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs4 from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { resolve, sep, dirname } from 'node:path';
|
|
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, loadPlan,
|
|
5
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
|
|
6
6
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import * as os from 'node:os';
|
|
8
8
|
import * as fs7 from 'node:fs';
|
|
@@ -12,6 +12,7 @@ import * as net from 'node:net';
|
|
|
12
12
|
import { Agent } from 'undici';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
import * as ts from 'typescript';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
15
16
|
|
|
16
17
|
// src/read.ts
|
|
17
18
|
async function detectPackageManager(cwd) {
|
|
@@ -29,7 +30,7 @@ async function detectPackageManager(cwd) {
|
|
|
29
30
|
return "npm";
|
|
30
31
|
}
|
|
31
32
|
function resolvePath(input, ctx) {
|
|
32
|
-
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
|
|
33
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
33
34
|
}
|
|
34
35
|
function ensureInsideRoot(absPath, ctx) {
|
|
35
36
|
const root = path.resolve(ctx.projectRoot);
|
|
@@ -1098,8 +1099,13 @@ var CircuitBreaker = class {
|
|
|
1098
1099
|
* Call this BEFORE spawning a bash/exec process.
|
|
1099
1100
|
* Returns true if the call is allowed; false if the breaker is open.
|
|
1100
1101
|
* When false, callers MUST NOT spawn a process.
|
|
1102
|
+
*
|
|
1103
|
+
* @param bypass - If true, skip the circuit breaker check entirely.
|
|
1104
|
+
* Use for background/fire-and-forget processes that should
|
|
1105
|
+
* not affect breaker state.
|
|
1101
1106
|
*/
|
|
1102
|
-
beforeCall() {
|
|
1107
|
+
beforeCall(bypass = false) {
|
|
1108
|
+
if (bypass) return true;
|
|
1103
1109
|
this._checkStateTransition();
|
|
1104
1110
|
if (this.state === "open") return false;
|
|
1105
1111
|
return true;
|
|
@@ -1109,8 +1115,12 @@ var CircuitBreaker = class {
|
|
|
1109
1115
|
* `durationMs` is the wall-clock time the process ran.
|
|
1110
1116
|
* `failed` is true when the process returned a non-zero exit code or
|
|
1111
1117
|
* threw an exception before spawning.
|
|
1118
|
+
*
|
|
1119
|
+
* @param bypass - If true, do not update breaker state.
|
|
1120
|
+
* Use for background/fire-and-forget processes.
|
|
1112
1121
|
*/
|
|
1113
|
-
afterCall(durationMs, failed) {
|
|
1122
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
1123
|
+
if (bypass) return;
|
|
1114
1124
|
const now = Date.now();
|
|
1115
1125
|
if (this.state === "half-open") {
|
|
1116
1126
|
if (failed) {
|
|
@@ -1209,6 +1219,17 @@ function redactCommand(cmd) {
|
|
|
1209
1219
|
return result;
|
|
1210
1220
|
}
|
|
1211
1221
|
var DEFAULT_GRACE_MS = 2e3;
|
|
1222
|
+
function killWin32Tree(pid) {
|
|
1223
|
+
try {
|
|
1224
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
1225
|
+
stdio: "ignore",
|
|
1226
|
+
windowsHide: true
|
|
1227
|
+
}).unref();
|
|
1228
|
+
return true;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1212
1233
|
var ProcessRegistryImpl = class {
|
|
1213
1234
|
processes = /* @__PURE__ */ new Map();
|
|
1214
1235
|
breaker;
|
|
@@ -1266,16 +1287,20 @@ var ProcessRegistryImpl = class {
|
|
|
1266
1287
|
/**
|
|
1267
1288
|
* Called before spawning a process. Returns true if allowed; false if
|
|
1268
1289
|
* the circuit breaker is open.
|
|
1290
|
+
*
|
|
1291
|
+
* @param bypass - If true, skip circuit breaker check (for background processes).
|
|
1269
1292
|
*/
|
|
1270
|
-
beforeCall() {
|
|
1271
|
-
return this.breaker.beforeCall();
|
|
1293
|
+
beforeCall(bypass = false) {
|
|
1294
|
+
return this.breaker.beforeCall(bypass);
|
|
1272
1295
|
}
|
|
1273
1296
|
/**
|
|
1274
1297
|
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
1275
1298
|
* `failed` is true for non-zero exit codes.
|
|
1299
|
+
*
|
|
1300
|
+
* @param bypass - If true, do not update circuit breaker state (for background processes).
|
|
1276
1301
|
*/
|
|
1277
|
-
afterCall(durationMs, failed) {
|
|
1278
|
-
this.breaker.afterCall(durationMs, failed);
|
|
1302
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
1303
|
+
this.breaker.afterCall(durationMs, failed, bypass);
|
|
1279
1304
|
}
|
|
1280
1305
|
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
1281
1306
|
forceBreakerOpen() {
|
|
@@ -1306,9 +1331,22 @@ var ProcessRegistryImpl = class {
|
|
|
1306
1331
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1307
1332
|
const isWin = os.platform() === "win32";
|
|
1308
1333
|
if (isWin) {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1334
|
+
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
1335
|
+
if (liveRealChild && killWin32Tree(pid)) {
|
|
1336
|
+
const fallback = setTimeout(() => {
|
|
1337
|
+
if (p.child.exitCode === null) {
|
|
1338
|
+
try {
|
|
1339
|
+
p.child.kill("SIGKILL");
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}, graceMs);
|
|
1344
|
+
fallback.unref?.();
|
|
1345
|
+
} else {
|
|
1346
|
+
try {
|
|
1347
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1312
1350
|
}
|
|
1313
1351
|
p.killed = true;
|
|
1314
1352
|
return true;
|
|
@@ -1387,6 +1425,7 @@ var MAX_OUTPUT = 32768;
|
|
|
1387
1425
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1388
1426
|
var STREAM_FLUSH_INTERVAL_MS = 200;
|
|
1389
1427
|
var STREAM_FLUSH_BYTES = 4 * 1024;
|
|
1428
|
+
var MAX_QUEUE_CHUNKS = 500;
|
|
1390
1429
|
var bashTool = {
|
|
1391
1430
|
name: "bash",
|
|
1392
1431
|
category: "Shell",
|
|
@@ -1434,7 +1473,8 @@ var bashTool = {
|
|
|
1434
1473
|
async *executeStream(input, ctx, opts) {
|
|
1435
1474
|
if (!input?.command) throw new Error("bash: command is required");
|
|
1436
1475
|
const registry = getProcessRegistry();
|
|
1437
|
-
|
|
1476
|
+
const bypassBreaker = !!input.background;
|
|
1477
|
+
if (!registry.beforeCall(bypassBreaker)) {
|
|
1438
1478
|
yield {
|
|
1439
1479
|
type: "final",
|
|
1440
1480
|
output: {
|
|
@@ -1447,6 +1487,17 @@ var bashTool = {
|
|
|
1447
1487
|
};
|
|
1448
1488
|
return;
|
|
1449
1489
|
}
|
|
1490
|
+
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
1491
|
+
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
1492
|
+
console.warn(JSON.stringify({
|
|
1493
|
+
level: "warn",
|
|
1494
|
+
event: "bash.pipe_to_shell_detected",
|
|
1495
|
+
message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
|
|
1496
|
+
command_prefix: input.command.slice(0, 100),
|
|
1497
|
+
// Log first 100 chars for review
|
|
1498
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1450
1501
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
1451
1502
|
const isWin = os.platform() === "win32";
|
|
1452
1503
|
const shell = (() => {
|
|
@@ -1505,7 +1556,7 @@ var bashTool = {
|
|
|
1505
1556
|
}
|
|
1506
1557
|
});
|
|
1507
1558
|
child2.on("close", () => {
|
|
1508
|
-
registry.afterCall(Date.now() - startedAt, false);
|
|
1559
|
+
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
1509
1560
|
});
|
|
1510
1561
|
if (typeof pid2 === "number") child2.unref();
|
|
1511
1562
|
yield {
|
|
@@ -1524,7 +1575,7 @@ var bashTool = {
|
|
|
1524
1575
|
env,
|
|
1525
1576
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1526
1577
|
detached,
|
|
1527
|
-
signal: opts.signal
|
|
1578
|
+
...isWin ? {} : { signal: opts.signal }
|
|
1528
1579
|
});
|
|
1529
1580
|
const pid = child.pid;
|
|
1530
1581
|
if (typeof pid === "number") {
|
|
@@ -1543,9 +1594,22 @@ var bashTool = {
|
|
|
1543
1594
|
const timers = [];
|
|
1544
1595
|
function killWithTimeout(child2, timeoutMs2) {
|
|
1545
1596
|
if (isWin) {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1597
|
+
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
1598
|
+
const fallback = setTimeout(() => {
|
|
1599
|
+
if (child2.exitCode === null) {
|
|
1600
|
+
try {
|
|
1601
|
+
child2.kill();
|
|
1602
|
+
} catch {
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}, 2e3);
|
|
1606
|
+
timers.push(fallback);
|
|
1607
|
+
fallback.unref?.();
|
|
1608
|
+
} else {
|
|
1609
|
+
try {
|
|
1610
|
+
child2.kill();
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1549
1613
|
}
|
|
1550
1614
|
return;
|
|
1551
1615
|
}
|
|
@@ -1584,6 +1648,11 @@ var bashTool = {
|
|
|
1584
1648
|
}, timeoutMs);
|
|
1585
1649
|
timers.push(timer);
|
|
1586
1650
|
timer.unref?.();
|
|
1651
|
+
const onAbort = () => killWithTimeout(child, 2e3);
|
|
1652
|
+
if (isWin) {
|
|
1653
|
+
if (opts.signal.aborted) onAbort();
|
|
1654
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
1655
|
+
}
|
|
1587
1656
|
const queue = [];
|
|
1588
1657
|
let resolveNext = null;
|
|
1589
1658
|
const push = (c) => {
|
|
@@ -1608,18 +1677,32 @@ var bashTool = {
|
|
|
1608
1677
|
lastFlush = Date.now();
|
|
1609
1678
|
return text;
|
|
1610
1679
|
};
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1680
|
+
let paused = false;
|
|
1681
|
+
const pauseIfFlooded = () => {
|
|
1682
|
+
if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
|
|
1683
|
+
paused = true;
|
|
1684
|
+
child.stdout?.pause();
|
|
1685
|
+
child.stderr?.pause();
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
const resumeIfDrained = () => {
|
|
1689
|
+
if (paused && queue.length < MAX_QUEUE_CHUNKS) {
|
|
1690
|
+
paused = false;
|
|
1691
|
+
child.stdout?.resume();
|
|
1692
|
+
child.stderr?.resume();
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
const onData = (chunk) => {
|
|
1618
1696
|
const text = chunk.toString();
|
|
1619
|
-
buf
|
|
1697
|
+
if (buf.length < MAX_OUTPUT) {
|
|
1698
|
+
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1699
|
+
}
|
|
1620
1700
|
pending += text;
|
|
1621
1701
|
push({ kind: "data", text });
|
|
1622
|
-
|
|
1702
|
+
pauseIfFlooded();
|
|
1703
|
+
};
|
|
1704
|
+
child.stdout?.on("data", onData);
|
|
1705
|
+
child.stderr?.on("data", onData);
|
|
1623
1706
|
child.on("error", (err) => {
|
|
1624
1707
|
for (const t of timers) clearTimeout(t);
|
|
1625
1708
|
registry.afterCall(Date.now() - startedAt, true);
|
|
@@ -1634,6 +1717,7 @@ var bashTool = {
|
|
|
1634
1717
|
try {
|
|
1635
1718
|
while (true) {
|
|
1636
1719
|
const c = await next();
|
|
1720
|
+
resumeIfDrained();
|
|
1637
1721
|
if (c.kind === "error") throw c.err;
|
|
1638
1722
|
if (c.kind === "end") {
|
|
1639
1723
|
const remainder = flush();
|
|
@@ -1658,6 +1742,15 @@ var bashTool = {
|
|
|
1658
1742
|
}
|
|
1659
1743
|
} finally {
|
|
1660
1744
|
for (const t of timers) clearTimeout(t);
|
|
1745
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1746
|
+
child.stdout?.off("data", onData);
|
|
1747
|
+
child.stderr?.off("data", onData);
|
|
1748
|
+
child.stdout?.destroy();
|
|
1749
|
+
child.stderr?.destroy();
|
|
1750
|
+
if (child.exitCode === null && !child.killed) {
|
|
1751
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
1752
|
+
else killWithTimeout(child, 2e3);
|
|
1753
|
+
}
|
|
1661
1754
|
}
|
|
1662
1755
|
}
|
|
1663
1756
|
};
|
|
@@ -1890,12 +1983,13 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1890
1983
|
let killed = false;
|
|
1891
1984
|
const startedAt = Date.now();
|
|
1892
1985
|
const resolved = resolveWin32Command(cmd);
|
|
1893
|
-
const
|
|
1986
|
+
const isWin = process.platform === "win32";
|
|
1987
|
+
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
1894
1988
|
const child = spawn(resolved, args, {
|
|
1895
1989
|
cwd,
|
|
1896
|
-
signal,
|
|
1897
1990
|
env: buildChildEnv(sessionId),
|
|
1898
1991
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1992
|
+
...isWin ? {} : { signal },
|
|
1899
1993
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
1900
1994
|
});
|
|
1901
1995
|
const registry = getProcessRegistry();
|
|
@@ -1909,6 +2003,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1909
2003
|
if (typeof pid === "number") registry.kill(pid);
|
|
1910
2004
|
else child.kill("SIGTERM");
|
|
1911
2005
|
}, timeout);
|
|
2006
|
+
const onAbort = () => {
|
|
2007
|
+
killed = true;
|
|
2008
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
2009
|
+
else child.kill("SIGTERM");
|
|
2010
|
+
};
|
|
2011
|
+
if (isWin) {
|
|
2012
|
+
if (signal.aborted) onAbort();
|
|
2013
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2014
|
+
}
|
|
1912
2015
|
child.stdout?.on("data", (chunk) => {
|
|
1913
2016
|
if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
|
|
1914
2017
|
});
|
|
@@ -1917,6 +2020,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1917
2020
|
});
|
|
1918
2021
|
child.on("close", (code) => {
|
|
1919
2022
|
clearTimeout(timer);
|
|
2023
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
1920
2024
|
if (typeof pid === "number") registry.unregister(pid);
|
|
1921
2025
|
const durationMs = Date.now() - startedAt;
|
|
1922
2026
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -1933,6 +2037,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1933
2037
|
});
|
|
1934
2038
|
child.on("error", (err) => {
|
|
1935
2039
|
clearTimeout(timer);
|
|
2040
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
1936
2041
|
if (typeof pid === "number") registry.unregister(pid);
|
|
1937
2042
|
registry.afterCall(Date.now() - startedAt, true);
|
|
1938
2043
|
resolve7({
|
|
@@ -2350,13 +2455,24 @@ var searchTool = {
|
|
|
2350
2455
|
async function duckduckgoSearch(query2, num, signal) {
|
|
2351
2456
|
const encoded = encodeURIComponent(query2);
|
|
2352
2457
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
results,
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2458
|
+
try {
|
|
2459
|
+
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS2);
|
|
2460
|
+
const html = await response.text();
|
|
2461
|
+
const results = parseDuckDuckGo(html, num);
|
|
2462
|
+
return {
|
|
2463
|
+
query: query2,
|
|
2464
|
+
results,
|
|
2465
|
+
source: "duckduckgo",
|
|
2466
|
+
truncated: results.length >= num
|
|
2467
|
+
};
|
|
2468
|
+
} catch {
|
|
2469
|
+
return {
|
|
2470
|
+
query: query2,
|
|
2471
|
+
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
2472
|
+
source: "duckduckgo",
|
|
2473
|
+
truncated: false
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2360
2476
|
}
|
|
2361
2477
|
function takeFrom(iter, max) {
|
|
2362
2478
|
const out = [];
|
|
@@ -2475,21 +2591,11 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
2475
2591
|
}
|
|
2476
2592
|
}
|
|
2477
2593
|
function anySignal(...signals) {
|
|
2478
|
-
|
|
2479
|
-
for (const s of signals) {
|
|
2480
|
-
if (s.aborted) {
|
|
2481
|
-
controller.abort();
|
|
2482
|
-
break;
|
|
2483
|
-
}
|
|
2484
|
-
s.addEventListener("abort", () => controller.abort());
|
|
2485
|
-
}
|
|
2486
|
-
return controller.signal;
|
|
2594
|
+
return AbortSignal.any(signals);
|
|
2487
2595
|
}
|
|
2488
2596
|
function stripTags2(html) {
|
|
2489
2597
|
return html.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
2490
2598
|
}
|
|
2491
|
-
|
|
2492
|
-
// src/todo.ts
|
|
2493
2599
|
var todoTool = {
|
|
2494
2600
|
name: "todo",
|
|
2495
2601
|
category: "Session",
|
|
@@ -2548,6 +2654,48 @@ var todoTool = {
|
|
|
2548
2654
|
}
|
|
2549
2655
|
}
|
|
2550
2656
|
ctx.state.replaceTodos(items);
|
|
2657
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
2658
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
2659
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
2660
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
2661
|
+
for (const item of items) {
|
|
2662
|
+
if (item.promotedFromPlan) {
|
|
2663
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
|
|
2664
|
+
}
|
|
2665
|
+
if (item.promotedFromTask) {
|
|
2666
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
for (const planId of completedPlanIds) {
|
|
2670
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
2671
|
+
const planPath = ctx.meta["plan.path"];
|
|
2672
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
2673
|
+
try {
|
|
2674
|
+
const plan = await loadPlan(planPath);
|
|
2675
|
+
if (plan) {
|
|
2676
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
2677
|
+
await savePlan(planPath, updated);
|
|
2678
|
+
}
|
|
2679
|
+
} catch {
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
for (const taskId of completedTaskIds) {
|
|
2683
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
2684
|
+
const taskPath = ctx.meta["task.path"];
|
|
2685
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
2686
|
+
try {
|
|
2687
|
+
const file = await loadTasks(taskPath);
|
|
2688
|
+
if (file) {
|
|
2689
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
2690
|
+
if (task && task.status !== "completed") {
|
|
2691
|
+
task.status = "completed";
|
|
2692
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2693
|
+
await saveTasks(taskPath, file);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
} catch {
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2551
2699
|
return {
|
|
2552
2700
|
count: items.length,
|
|
2553
2701
|
in_progress: items.filter((t) => t.status === "in_progress").length
|
|
@@ -2558,7 +2706,7 @@ var planTool = {
|
|
|
2558
2706
|
name: "plan",
|
|
2559
2707
|
category: "Session",
|
|
2560
2708
|
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.",
|
|
2561
|
-
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- 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.',
|
|
2709
|
+
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.',
|
|
2562
2710
|
permission: "confirm",
|
|
2563
2711
|
mutating: true,
|
|
2564
2712
|
capabilities: ["fs.write"],
|
|
@@ -2575,9 +2723,9 @@ var planTool = {
|
|
|
2575
2723
|
"done",
|
|
2576
2724
|
"remove",
|
|
2577
2725
|
"promote",
|
|
2578
|
-
"derive",
|
|
2579
2726
|
"template_use",
|
|
2580
|
-
"clear"
|
|
2727
|
+
"clear",
|
|
2728
|
+
"taskify"
|
|
2581
2729
|
],
|
|
2582
2730
|
description: "The operation to perform on the plan board."
|
|
2583
2731
|
},
|
|
@@ -2596,7 +2744,7 @@ var planTool = {
|
|
|
2596
2744
|
subtasks: {
|
|
2597
2745
|
type: "array",
|
|
2598
2746
|
items: { type: "string" },
|
|
2599
|
-
description: "List of subtask titles. Used with promote
|
|
2747
|
+
description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
|
|
2600
2748
|
},
|
|
2601
2749
|
template: {
|
|
2602
2750
|
type: "string",
|
|
@@ -2617,92 +2765,151 @@ var planTool = {
|
|
|
2617
2765
|
};
|
|
2618
2766
|
}
|
|
2619
2767
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
2620
|
-
let
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
if (!input.target) {
|
|
2636
|
-
return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2768
|
+
let early = null;
|
|
2769
|
+
const taskifyMeta = { title: "", details: "" };
|
|
2770
|
+
let didTaskify = false;
|
|
2771
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
2772
|
+
switch (input.action) {
|
|
2773
|
+
case "show":
|
|
2774
|
+
break;
|
|
2775
|
+
case "add": {
|
|
2776
|
+
const title = input.title?.trim();
|
|
2777
|
+
if (!title) {
|
|
2778
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
2779
|
+
return p;
|
|
2780
|
+
}
|
|
2781
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
2782
|
+
return updated;
|
|
2637
2783
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
input.target
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2784
|
+
case "start":
|
|
2785
|
+
case "done": {
|
|
2786
|
+
if (!input.target) {
|
|
2787
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2788
|
+
return p;
|
|
2789
|
+
}
|
|
2790
|
+
const next = setPlanItemStatus(
|
|
2791
|
+
p,
|
|
2792
|
+
input.target,
|
|
2793
|
+
input.action === "start" ? "in_progress" : "done"
|
|
2794
|
+
);
|
|
2795
|
+
if (next === p) {
|
|
2796
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2797
|
+
return p;
|
|
2798
|
+
}
|
|
2799
|
+
return next;
|
|
2645
2800
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2801
|
+
case "remove": {
|
|
2802
|
+
if (!input.target) {
|
|
2803
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
2804
|
+
return p;
|
|
2805
|
+
}
|
|
2806
|
+
const next = removePlanItem(p, input.target);
|
|
2807
|
+
if (next === p) {
|
|
2808
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2809
|
+
return p;
|
|
2810
|
+
}
|
|
2811
|
+
return next;
|
|
2653
2812
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2813
|
+
case "promote": {
|
|
2814
|
+
if (!input.target) {
|
|
2815
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
2816
|
+
return p;
|
|
2817
|
+
}
|
|
2818
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
2819
|
+
if (!derived) {
|
|
2820
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2821
|
+
return p;
|
|
2822
|
+
}
|
|
2823
|
+
ctx.state.replaceTodos(derived.todos);
|
|
2824
|
+
early = mkResult(
|
|
2825
|
+
derived.plan,
|
|
2826
|
+
true,
|
|
2827
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
2828
|
+
derived.todos
|
|
2829
|
+
);
|
|
2830
|
+
return derived.plan;
|
|
2657
2831
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2832
|
+
case "template_use": {
|
|
2833
|
+
const templateName = input.template?.trim();
|
|
2834
|
+
if (!templateName) {
|
|
2835
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
2836
|
+
return p;
|
|
2837
|
+
}
|
|
2838
|
+
const template = getPlanTemplate(templateName);
|
|
2839
|
+
if (!template) {
|
|
2840
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
2841
|
+
return p;
|
|
2842
|
+
}
|
|
2843
|
+
let updated = p;
|
|
2844
|
+
for (const item of template.items) {
|
|
2845
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
2846
|
+
}
|
|
2847
|
+
early = mkResult(
|
|
2848
|
+
updated,
|
|
2849
|
+
true,
|
|
2850
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
2851
|
+
);
|
|
2852
|
+
return updated;
|
|
2666
2853
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2854
|
+
case "clear":
|
|
2855
|
+
return clearPlan(p);
|
|
2856
|
+
case "taskify": {
|
|
2857
|
+
if (!input.target) {
|
|
2858
|
+
early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
|
|
2859
|
+
return p;
|
|
2860
|
+
}
|
|
2861
|
+
let itemIdx = -1;
|
|
2862
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
2863
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
2864
|
+
itemIdx = asNum - 1;
|
|
2865
|
+
} else {
|
|
2866
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
2867
|
+
if (itemIdx === -1) {
|
|
2868
|
+
const lower = input.target.toLowerCase();
|
|
2869
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
2873
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
2874
|
+
return p;
|
|
2875
|
+
}
|
|
2876
|
+
const item = p.items[itemIdx];
|
|
2877
|
+
taskifyMeta.title = item.title;
|
|
2878
|
+
taskifyMeta.details = item.details ?? "";
|
|
2879
|
+
didTaskify = true;
|
|
2880
|
+
break;
|
|
2670
2881
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
return mkResult(
|
|
2675
|
-
plan,
|
|
2676
|
-
true,
|
|
2677
|
-
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
2678
|
-
derived.todos
|
|
2679
|
-
);
|
|
2882
|
+
default:
|
|
2883
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
2884
|
+
return p;
|
|
2680
2885
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
return mkResult(plan, false, `Unknown template "${templateName}".`);
|
|
2689
|
-
}
|
|
2690
|
-
for (const item of template.items) {
|
|
2691
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
2692
|
-
}
|
|
2693
|
-
await savePlan(planPath, plan);
|
|
2694
|
-
return mkResult(
|
|
2695
|
-
plan,
|
|
2696
|
-
true,
|
|
2697
|
-
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
2698
|
-
);
|
|
2886
|
+
return p;
|
|
2887
|
+
});
|
|
2888
|
+
if (early) return early;
|
|
2889
|
+
if (didTaskify) {
|
|
2890
|
+
const taskPath = ctx.meta["task.path"];
|
|
2891
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
2892
|
+
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
2699
2893
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2894
|
+
const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
|
|
2895
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2896
|
+
taskFile.tasks.push({
|
|
2897
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
2898
|
+
title: taskifyMeta.title,
|
|
2899
|
+
description: taskifyMeta.details || void 0,
|
|
2900
|
+
type: "feature",
|
|
2901
|
+
priority: "medium",
|
|
2902
|
+
status: "pending",
|
|
2903
|
+
createdAt: now,
|
|
2904
|
+
updatedAt: now
|
|
2905
|
+
});
|
|
2906
|
+
await saveTasks(taskPath, taskFile);
|
|
2907
|
+
return mkResult(
|
|
2908
|
+
plan,
|
|
2909
|
+
true,
|
|
2910
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
2911
|
+
${formatTaskList(taskFile.tasks)}`
|
|
2912
|
+
);
|
|
2706
2913
|
}
|
|
2707
2914
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
2708
2915
|
}
|
|
@@ -3531,6 +3738,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
3531
3738
|
async function* spawnStream(opts) {
|
|
3532
3739
|
const max = opts.maxBytes ?? 2e5;
|
|
3533
3740
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
3741
|
+
const maxQueue = opts.maxQueueSize ?? 500;
|
|
3534
3742
|
let stdout = "";
|
|
3535
3743
|
let stderr = "";
|
|
3536
3744
|
let pending = "";
|
|
@@ -3546,6 +3754,7 @@ async function* spawnStream(opts) {
|
|
|
3546
3754
|
});
|
|
3547
3755
|
const queue = [];
|
|
3548
3756
|
let waiter;
|
|
3757
|
+
let paused = false;
|
|
3549
3758
|
const wake = () => {
|
|
3550
3759
|
if (waiter) {
|
|
3551
3760
|
const w = waiter;
|
|
@@ -3553,17 +3762,34 @@ async function* spawnStream(opts) {
|
|
|
3553
3762
|
w();
|
|
3554
3763
|
}
|
|
3555
3764
|
};
|
|
3765
|
+
const resume = () => {
|
|
3766
|
+
if (paused && queue.length < maxQueue) {
|
|
3767
|
+
paused = false;
|
|
3768
|
+
child.stdout?.resume();
|
|
3769
|
+
child.stderr?.resume();
|
|
3770
|
+
}
|
|
3771
|
+
};
|
|
3556
3772
|
child.stdout?.on("data", (c) => {
|
|
3557
3773
|
const s = c.toString();
|
|
3558
3774
|
if (stdout.length < max) stdout += s;
|
|
3559
3775
|
queue.push({ kind: "out", data: s });
|
|
3560
3776
|
wake();
|
|
3777
|
+
if (!paused && queue.length >= maxQueue) {
|
|
3778
|
+
paused = true;
|
|
3779
|
+
child.stdout?.pause();
|
|
3780
|
+
child.stderr?.pause();
|
|
3781
|
+
}
|
|
3561
3782
|
});
|
|
3562
3783
|
child.stderr?.on("data", (c) => {
|
|
3563
3784
|
const s = c.toString();
|
|
3564
3785
|
if (stderr.length < max) stderr += s;
|
|
3565
3786
|
queue.push({ kind: "err", data: s });
|
|
3566
3787
|
wake();
|
|
3788
|
+
if (!paused && queue.length >= maxQueue) {
|
|
3789
|
+
paused = true;
|
|
3790
|
+
child.stdout?.pause();
|
|
3791
|
+
child.stderr?.pause();
|
|
3792
|
+
}
|
|
3567
3793
|
});
|
|
3568
3794
|
child.on("error", (e) => {
|
|
3569
3795
|
error = e.message;
|
|
@@ -3583,6 +3809,7 @@ async function* spawnStream(opts) {
|
|
|
3583
3809
|
});
|
|
3584
3810
|
}
|
|
3585
3811
|
const chunk = queue.shift();
|
|
3812
|
+
resume();
|
|
3586
3813
|
if (chunk.kind === "close") {
|
|
3587
3814
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
3588
3815
|
break;
|
|
@@ -4035,8 +4262,6 @@ function parseResult(runner, result, duration) {
|
|
|
4035
4262
|
truncated: result.truncated
|
|
4036
4263
|
};
|
|
4037
4264
|
}
|
|
4038
|
-
|
|
4039
|
-
// src/install.ts
|
|
4040
4265
|
var installTool = {
|
|
4041
4266
|
name: "install",
|
|
4042
4267
|
category: "Package Management",
|
|
@@ -4131,18 +4356,48 @@ var installTool = {
|
|
|
4131
4356
|
signal: opts.signal,
|
|
4132
4357
|
maxBytes: 1e5
|
|
4133
4358
|
});
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
dry_run: args.includes("--dry-run"),
|
|
4141
|
-
truncated: result.truncated
|
|
4142
|
-
}
|
|
4359
|
+
const output = {
|
|
4360
|
+
packages: pkgList,
|
|
4361
|
+
exit_code: result.exitCode,
|
|
4362
|
+
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
4363
|
+
dry_run: args.includes("--dry-run"),
|
|
4364
|
+
truncated: result.truncated
|
|
4143
4365
|
};
|
|
4366
|
+
const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
|
|
4367
|
+
if (isSuccess && pkgList.length > 0) {
|
|
4368
|
+
const trackerOpts = ctx.meta?.["packageTrackerOpts"];
|
|
4369
|
+
if (trackerOpts) {
|
|
4370
|
+
const manifestPath = resolveManifestPath(cwd, pkgManager);
|
|
4371
|
+
for (const pkg of pkgList) {
|
|
4372
|
+
try {
|
|
4373
|
+
await recordPackageAction(trackerOpts, {
|
|
4374
|
+
manifestPath,
|
|
4375
|
+
packageName: pkg,
|
|
4376
|
+
versionSpec: "latest",
|
|
4377
|
+
// exact version resolved by package manager at install time
|
|
4378
|
+
ecosystem: detectPackageEcosystem(manifestPath),
|
|
4379
|
+
agentId: ctx.agentId,
|
|
4380
|
+
agentName: ctx.agentName,
|
|
4381
|
+
sessionId: ctx.session.id
|
|
4382
|
+
});
|
|
4383
|
+
} catch {
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
yield { type: "final", output };
|
|
4144
4389
|
}
|
|
4145
4390
|
};
|
|
4391
|
+
function resolveManifestPath(cwd, pkgManager) {
|
|
4392
|
+
switch (pkgManager) {
|
|
4393
|
+
case "pnpm":
|
|
4394
|
+
case "yarn":
|
|
4395
|
+
case "npm":
|
|
4396
|
+
return join(cwd, "package.json");
|
|
4397
|
+
default:
|
|
4398
|
+
return join(cwd, "package.json");
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4146
4401
|
|
|
4147
4402
|
// src/audit.ts
|
|
4148
4403
|
var auditTool = {
|
|
@@ -7193,18 +7448,19 @@ function isIndexableFile(filePath) {
|
|
|
7193
7448
|
}
|
|
7194
7449
|
async function runStartupIndex(opts) {
|
|
7195
7450
|
_indexing = true;
|
|
7196
|
-
_currentFile = 0;
|
|
7197
|
-
_totalFiles = 0;
|
|
7198
|
-
_lastError = null;
|
|
7199
7451
|
emitState();
|
|
7200
7452
|
try {
|
|
7201
|
-
const result = await withMutex(
|
|
7202
|
-
|
|
7453
|
+
const result = await withMutex(() => {
|
|
7454
|
+
_currentFile = 0;
|
|
7455
|
+
_totalFiles = 0;
|
|
7456
|
+
_lastError = null;
|
|
7457
|
+
return runIndexer(stubCtx(opts.projectRoot), {
|
|
7203
7458
|
projectRoot: opts.projectRoot,
|
|
7204
7459
|
indexDir: opts.indexDir,
|
|
7205
|
-
force: opts.force
|
|
7206
|
-
|
|
7207
|
-
|
|
7460
|
+
force: opts.force,
|
|
7461
|
+
signal: opts.signal
|
|
7462
|
+
});
|
|
7463
|
+
});
|
|
7208
7464
|
_ready = true;
|
|
7209
7465
|
return result;
|
|
7210
7466
|
} catch (err) {
|
|
@@ -7248,6 +7504,16 @@ var YIELD_EVERY_N = 50;
|
|
|
7248
7504
|
function yieldEventLoop() {
|
|
7249
7505
|
return new Promise((resolve7) => setImmediate(resolve7));
|
|
7250
7506
|
}
|
|
7507
|
+
function throwIfAborted(signal) {
|
|
7508
|
+
if (!signal?.aborted) return;
|
|
7509
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
7510
|
+
throw new Error(
|
|
7511
|
+
typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
|
|
7512
|
+
);
|
|
7513
|
+
}
|
|
7514
|
+
function isAbortError(err) {
|
|
7515
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
7516
|
+
}
|
|
7251
7517
|
var DEFAULT_IGNORE5 = [
|
|
7252
7518
|
"node_modules",
|
|
7253
7519
|
".git",
|
|
@@ -7259,7 +7525,7 @@ var DEFAULT_IGNORE5 = [
|
|
|
7259
7525
|
"__snapshots__",
|
|
7260
7526
|
".nyc_output"
|
|
7261
7527
|
];
|
|
7262
|
-
async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
|
|
7528
|
+
async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
7263
7529
|
const results = [];
|
|
7264
7530
|
const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...ignore]);
|
|
7265
7531
|
const globs = [
|
|
@@ -7274,13 +7540,20 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
|
|
|
7274
7540
|
{ ext: ".yaml", pat: compileGlob("**/*.yaml") },
|
|
7275
7541
|
{ ext: ".yml", pat: compileGlob("**/*.yml") }
|
|
7276
7542
|
];
|
|
7543
|
+
let dirCount = 0;
|
|
7277
7544
|
const walk = async (dir) => {
|
|
7545
|
+
throwIfAborted(signal);
|
|
7546
|
+
if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
|
|
7547
|
+
await yieldEventLoop();
|
|
7548
|
+
throwIfAborted(signal);
|
|
7549
|
+
}
|
|
7278
7550
|
let entries;
|
|
7279
7551
|
try {
|
|
7280
7552
|
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
7281
7553
|
} catch {
|
|
7282
7554
|
return;
|
|
7283
7555
|
}
|
|
7556
|
+
dirCount++;
|
|
7284
7557
|
for (const e of entries) {
|
|
7285
7558
|
if (ignoreSet.has(e.name)) continue;
|
|
7286
7559
|
const full = path.join(dir, e.name);
|
|
@@ -7325,8 +7598,18 @@ async function parseFile(file, content, lang) {
|
|
|
7325
7598
|
}
|
|
7326
7599
|
}
|
|
7327
7600
|
async function runIndexer(_ctx, opts) {
|
|
7328
|
-
const
|
|
7329
|
-
|
|
7601
|
+
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
7602
|
+
try {
|
|
7603
|
+
return await runIndexerWithStore(store, opts);
|
|
7604
|
+
} finally {
|
|
7605
|
+
try {
|
|
7606
|
+
store.close();
|
|
7607
|
+
} catch {
|
|
7608
|
+
}
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
async function runIndexerWithStore(store, opts) {
|
|
7612
|
+
const { projectRoot, force = false, langs, ignore = [], signal } = opts;
|
|
7330
7613
|
const startMs = Date.now();
|
|
7331
7614
|
const errors = [];
|
|
7332
7615
|
const langStats = {};
|
|
@@ -7337,7 +7620,7 @@ async function runIndexer(_ctx, opts) {
|
|
|
7337
7620
|
if (opts.files && opts.files.length > 0) {
|
|
7338
7621
|
files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
7339
7622
|
} else {
|
|
7340
|
-
files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
|
|
7623
|
+
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
7341
7624
|
}
|
|
7342
7625
|
if (langs && langs.length > 0) {
|
|
7343
7626
|
const langSet = new Set(langs);
|
|
@@ -7356,11 +7639,14 @@ async function runIndexer(_ctx, opts) {
|
|
|
7356
7639
|
_setIndexProgress(fi + 1, files.length);
|
|
7357
7640
|
if (fi > 0 && fi % YIELD_EVERY_N === 0) {
|
|
7358
7641
|
await yieldEventLoop();
|
|
7642
|
+
throwIfAborted(signal);
|
|
7359
7643
|
}
|
|
7360
7644
|
let stat10;
|
|
7361
7645
|
try {
|
|
7362
|
-
|
|
7363
|
-
|
|
7646
|
+
const statOpts = signal ? { signal } : {};
|
|
7647
|
+
stat10 = await fs4.stat(file, statOpts);
|
|
7648
|
+
} catch (e) {
|
|
7649
|
+
if (isAbortError(e)) throw e;
|
|
7364
7650
|
store.deleteFile(file);
|
|
7365
7651
|
continue;
|
|
7366
7652
|
}
|
|
@@ -7378,8 +7664,9 @@ async function runIndexer(_ctx, opts) {
|
|
|
7378
7664
|
store.deleteSymbolsForFile(file);
|
|
7379
7665
|
let content;
|
|
7380
7666
|
try {
|
|
7381
|
-
content = await fs4.readFile(file, "utf8");
|
|
7667
|
+
content = await fs4.readFile(file, { encoding: "utf8", signal });
|
|
7382
7668
|
} catch (e) {
|
|
7669
|
+
if (isAbortError(e)) throw e;
|
|
7383
7670
|
errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
|
|
7384
7671
|
continue;
|
|
7385
7672
|
}
|
|
@@ -7435,7 +7722,6 @@ async function runIndexer(_ctx, opts) {
|
|
|
7435
7722
|
}
|
|
7436
7723
|
const durationMs = Date.now() - startMs;
|
|
7437
7724
|
store.setLastIndexed(Date.now());
|
|
7438
|
-
store.close();
|
|
7439
7725
|
return {
|
|
7440
7726
|
filesIndexed,
|
|
7441
7727
|
symbolsIndexed,
|
|
@@ -7469,7 +7755,7 @@ var codebaseIndexTool = {
|
|
|
7469
7755
|
}
|
|
7470
7756
|
}
|
|
7471
7757
|
},
|
|
7472
|
-
async execute(input, ctx) {
|
|
7758
|
+
async execute(input, ctx, execOpts) {
|
|
7473
7759
|
if (isIndexing()) {
|
|
7474
7760
|
return {
|
|
7475
7761
|
filesIndexed: 0,
|
|
@@ -7484,7 +7770,8 @@ var codebaseIndexTool = {
|
|
|
7484
7770
|
projectRoot: ctx.projectRoot,
|
|
7485
7771
|
force: input.force ?? false,
|
|
7486
7772
|
langs: input.langs,
|
|
7487
|
-
indexDir: codebaseIndexDirOverride(ctx)
|
|
7773
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
7774
|
+
signal: execOpts?.signal
|
|
7488
7775
|
});
|
|
7489
7776
|
setIndexReady();
|
|
7490
7777
|
return result;
|
|
@@ -7748,21 +8035,87 @@ var codebaseStatsTool = {
|
|
|
7748
8035
|
}
|
|
7749
8036
|
}
|
|
7750
8037
|
};
|
|
8038
|
+
var setWorkingDirTool = {
|
|
8039
|
+
name: "set_working_dir",
|
|
8040
|
+
category: "Context",
|
|
8041
|
+
description: "Change the current working directory for all subsequent file operations. The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
|
|
8042
|
+
usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
|
|
8043
|
+
permission: "confirm",
|
|
8044
|
+
mutating: true,
|
|
8045
|
+
capabilities: ["fs.read"],
|
|
8046
|
+
timeoutMs: 5e3,
|
|
8047
|
+
inputSchema: {
|
|
8048
|
+
type: "object",
|
|
8049
|
+
properties: {
|
|
8050
|
+
path: {
|
|
8051
|
+
type: "string",
|
|
8052
|
+
description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
},
|
|
8056
|
+
async execute(input, ctx, _opts) {
|
|
8057
|
+
if (!input.path) {
|
|
8058
|
+
return {
|
|
8059
|
+
current: ctx.workingDir,
|
|
8060
|
+
message: `Current working directory is ${ctx.workingDir}`
|
|
8061
|
+
};
|
|
8062
|
+
}
|
|
8063
|
+
const previous = ctx.workingDir;
|
|
8064
|
+
let resolved;
|
|
8065
|
+
try {
|
|
8066
|
+
resolved = ctx.setWorkingDir(input.path);
|
|
8067
|
+
} catch (err) {
|
|
8068
|
+
return {
|
|
8069
|
+
current: ctx.workingDir,
|
|
8070
|
+
error: err instanceof Error ? err.message : String(err)
|
|
8071
|
+
};
|
|
8072
|
+
}
|
|
8073
|
+
try {
|
|
8074
|
+
await fs4.access(resolved);
|
|
8075
|
+
} catch {
|
|
8076
|
+
try {
|
|
8077
|
+
ctx.setWorkingDir(previous);
|
|
8078
|
+
} catch {
|
|
8079
|
+
}
|
|
8080
|
+
return {
|
|
8081
|
+
current: ctx.workingDir,
|
|
8082
|
+
error: `Directory does not exist: ${resolved}`
|
|
8083
|
+
};
|
|
8084
|
+
}
|
|
8085
|
+
return {
|
|
8086
|
+
current: resolved,
|
|
8087
|
+
previous,
|
|
8088
|
+
message: `Working directory changed to ${resolved}`
|
|
8089
|
+
};
|
|
8090
|
+
}
|
|
8091
|
+
};
|
|
8092
|
+
function findTaskIndex(tasks, query2) {
|
|
8093
|
+
const asNum = Number.parseInt(query2, 10);
|
|
8094
|
+
if (!Number.isNaN(asNum)) {
|
|
8095
|
+
const idx = asNum - 1;
|
|
8096
|
+
if (tasks[idx]) return idx;
|
|
8097
|
+
}
|
|
8098
|
+
const byId = tasks.findIndex((t) => t.id === query2);
|
|
8099
|
+
if (byId >= 0) return byId;
|
|
8100
|
+
const lower = query2.toLowerCase();
|
|
8101
|
+
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
8102
|
+
}
|
|
7751
8103
|
var taskTool = {
|
|
7752
8104
|
name: "task",
|
|
7753
8105
|
category: "Session",
|
|
7754
8106
|
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.",
|
|
7755
|
-
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\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',
|
|
7756
|
-
permission: "
|
|
7757
|
-
mutating:
|
|
8107
|
+
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',
|
|
8108
|
+
permission: "confirm",
|
|
8109
|
+
mutating: true,
|
|
8110
|
+
capabilities: ["fs.write"],
|
|
7758
8111
|
timeoutMs: 2e3,
|
|
7759
8112
|
inputSchema: {
|
|
7760
8113
|
type: "object",
|
|
7761
8114
|
properties: {
|
|
7762
8115
|
action: {
|
|
7763
8116
|
type: "string",
|
|
7764
|
-
enum: ["replace", "add", "status", "show"],
|
|
7765
|
-
description: "replace = set full list, add = append, status = update task status, show = view only."
|
|
8117
|
+
enum: ["replace", "add", "status", "show", "promote", "planify"],
|
|
8118
|
+
description: "replace = set full list, add = append, status = update task status, show = view only, promote = convert task to todos, planify = convert task to plan item."
|
|
7766
8119
|
},
|
|
7767
8120
|
tasks: {
|
|
7768
8121
|
type: "array",
|
|
@@ -7806,11 +8159,20 @@ var taskTool = {
|
|
|
7806
8159
|
required: ["title", "type", "priority"],
|
|
7807
8160
|
description: "Single task to append (id/createdAt/updatedAt auto-generated)."
|
|
7808
8161
|
},
|
|
7809
|
-
id: { type: "string", description: "Task id for action=status." },
|
|
8162
|
+
id: { type: "string", description: "Task id for action=status or target for action=promote." },
|
|
7810
8163
|
status: {
|
|
7811
8164
|
type: "string",
|
|
7812
8165
|
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
|
|
7813
8166
|
description: "New status for action=status."
|
|
8167
|
+
},
|
|
8168
|
+
target: {
|
|
8169
|
+
type: "string",
|
|
8170
|
+
description: "Target task identifier (id, 1-based index, or title substring) for action=promote."
|
|
8171
|
+
},
|
|
8172
|
+
subtasks: {
|
|
8173
|
+
type: "array",
|
|
8174
|
+
items: { type: "string" },
|
|
8175
|
+
description: "Optional subtask titles for action=promote. Each becomes a pending todo."
|
|
7814
8176
|
}
|
|
7815
8177
|
},
|
|
7816
8178
|
required: ["action"]
|
|
@@ -7821,65 +8183,196 @@ var taskTool = {
|
|
|
7821
8183
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
7822
8184
|
}
|
|
7823
8185
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
8186
|
+
let early = null;
|
|
8187
|
+
const promoteMeta = { count: 0, title: "" };
|
|
8188
|
+
const planifyMeta = { title: "", details: "" };
|
|
8189
|
+
let didPlanify = false;
|
|
8190
|
+
const file = await mutateTasks(taskPath, sessionId, async (f) => {
|
|
8191
|
+
switch (input.action) {
|
|
8192
|
+
case "show":
|
|
8193
|
+
break;
|
|
8194
|
+
case "replace": {
|
|
8195
|
+
if (!Array.isArray(input.tasks)) {
|
|
8196
|
+
early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
|
|
8197
|
+
return f;
|
|
8198
|
+
}
|
|
8199
|
+
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
8200
|
+
for (const t of input.tasks) {
|
|
8201
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
8202
|
+
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
8203
|
+
if (missing.length > 0) {
|
|
8204
|
+
early = {
|
|
8205
|
+
ok: false,
|
|
8206
|
+
message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
|
|
8207
|
+
count: 0,
|
|
8208
|
+
completed: 0,
|
|
8209
|
+
inProgress: 0
|
|
8210
|
+
};
|
|
8211
|
+
return f;
|
|
8212
|
+
}
|
|
8213
|
+
}
|
|
8214
|
+
}
|
|
8215
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8216
|
+
f.tasks = input.tasks.map((t) => ({
|
|
8217
|
+
...t,
|
|
8218
|
+
createdAt: t.createdAt || now,
|
|
8219
|
+
updatedAt: now
|
|
8220
|
+
}));
|
|
8221
|
+
break;
|
|
7831
8222
|
}
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
8223
|
+
case "add": {
|
|
8224
|
+
const t = input.task;
|
|
8225
|
+
if (!t || !t.title) {
|
|
8226
|
+
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
8227
|
+
return f;
|
|
8228
|
+
}
|
|
8229
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
8230
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
8231
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
8232
|
+
if (missing.length > 0) {
|
|
8233
|
+
early = {
|
|
8234
|
+
ok: false,
|
|
8235
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
8236
|
+
count: 0,
|
|
8237
|
+
completed: 0,
|
|
8238
|
+
inProgress: 0
|
|
8239
|
+
};
|
|
8240
|
+
return f;
|
|
8241
|
+
}
|
|
8242
|
+
}
|
|
8243
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8244
|
+
const newTask = {
|
|
8245
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
8246
|
+
title: t.title,
|
|
8247
|
+
description: t.description,
|
|
8248
|
+
type: t.type || "feature",
|
|
8249
|
+
priority: t.priority || "medium",
|
|
8250
|
+
status: t.status || "pending",
|
|
8251
|
+
dependsOn: t.dependsOn,
|
|
8252
|
+
assignee: t.assignee,
|
|
8253
|
+
estimateHours: t.estimateHours,
|
|
8254
|
+
tags: t.tags,
|
|
8255
|
+
createdAt: now,
|
|
8256
|
+
updatedAt: now
|
|
8257
|
+
};
|
|
8258
|
+
f.tasks.push(newTask);
|
|
8259
|
+
break;
|
|
7845
8260
|
}
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
updatedAt: now
|
|
7860
|
-
};
|
|
7861
|
-
file.tasks.push(newTask);
|
|
7862
|
-
await saveTasks(taskPath, file);
|
|
7863
|
-
break;
|
|
7864
|
-
}
|
|
7865
|
-
case "status": {
|
|
7866
|
-
if (!input.id || !input.status) {
|
|
7867
|
-
return { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
8261
|
+
case "status": {
|
|
8262
|
+
if (!input.id || !input.status) {
|
|
8263
|
+
early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
8264
|
+
return f;
|
|
8265
|
+
}
|
|
8266
|
+
const task = f.tasks.find((t) => t.id === input.id);
|
|
8267
|
+
if (!task) {
|
|
8268
|
+
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
8269
|
+
return f;
|
|
8270
|
+
}
|
|
8271
|
+
task.status = input.status;
|
|
8272
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8273
|
+
break;
|
|
7868
8274
|
}
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
8275
|
+
case "promote": {
|
|
8276
|
+
const target = input.target?.trim();
|
|
8277
|
+
if (!target) {
|
|
8278
|
+
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8279
|
+
return f;
|
|
8280
|
+
}
|
|
8281
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
8282
|
+
if (idx === -1) {
|
|
8283
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8284
|
+
return f;
|
|
8285
|
+
}
|
|
8286
|
+
const match = f.tasks[idx];
|
|
8287
|
+
if (!match) {
|
|
8288
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8289
|
+
return f;
|
|
8290
|
+
}
|
|
8291
|
+
if (match.status !== "completed" && match.status !== "failed") {
|
|
8292
|
+
match.status = "in_progress";
|
|
8293
|
+
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8294
|
+
}
|
|
8295
|
+
const todos = [];
|
|
8296
|
+
const ts2 = Date.now();
|
|
8297
|
+
todos.push({
|
|
8298
|
+
id: `todo_${ts2}_task`,
|
|
8299
|
+
content: match.title,
|
|
8300
|
+
status: "in_progress",
|
|
8301
|
+
activeForm: match.title,
|
|
8302
|
+
promotedFromTask: match.id
|
|
8303
|
+
});
|
|
8304
|
+
if (match.description) {
|
|
8305
|
+
todos.push({
|
|
8306
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
8307
|
+
content: match.description.slice(0, 200),
|
|
8308
|
+
status: "pending",
|
|
8309
|
+
promotedFromTask: match.id
|
|
8310
|
+
});
|
|
8311
|
+
}
|
|
8312
|
+
if (input.subtasks && input.subtasks.length > 0) {
|
|
8313
|
+
for (const st of input.subtasks) {
|
|
8314
|
+
todos.push({
|
|
8315
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
8316
|
+
content: st,
|
|
8317
|
+
status: "pending",
|
|
8318
|
+
promotedFromTask: match.id
|
|
8319
|
+
});
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
ctx.state.replaceTodos(todos);
|
|
8323
|
+
promoteMeta.count = todos.length;
|
|
8324
|
+
promoteMeta.title = match.title;
|
|
8325
|
+
break;
|
|
7872
8326
|
}
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
8327
|
+
case "planify": {
|
|
8328
|
+
const target = input.target?.trim();
|
|
8329
|
+
if (!target) {
|
|
8330
|
+
early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8331
|
+
return f;
|
|
8332
|
+
}
|
|
8333
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
8334
|
+
if (idx === -1) {
|
|
8335
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8336
|
+
return f;
|
|
8337
|
+
}
|
|
8338
|
+
const match = f.tasks[idx];
|
|
8339
|
+
if (!match) {
|
|
8340
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8341
|
+
return f;
|
|
8342
|
+
}
|
|
8343
|
+
planifyMeta.title = match.title;
|
|
8344
|
+
planifyMeta.details = match.description ?? "";
|
|
8345
|
+
didPlanify = true;
|
|
8346
|
+
break;
|
|
8347
|
+
}
|
|
8348
|
+
default:
|
|
8349
|
+
early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
|
|
8350
|
+
return f;
|
|
7877
8351
|
}
|
|
7878
|
-
|
|
7879
|
-
|
|
8352
|
+
return f;
|
|
8353
|
+
});
|
|
8354
|
+
if (early) return early;
|
|
8355
|
+
if (didPlanify) {
|
|
8356
|
+
const { title, details } = planifyMeta;
|
|
8357
|
+
const planPath = ctx.meta["plan.path"];
|
|
8358
|
+
if (typeof planPath === "string" && planPath) {
|
|
8359
|
+
const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
8360
|
+
const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
|
|
8361
|
+
await savePlan(planPath, updated);
|
|
8362
|
+
return {
|
|
8363
|
+
ok: true,
|
|
8364
|
+
message: `planify ok \u2014 added "${title}" to plan.
|
|
8365
|
+
${formatPlan(updated)}`,
|
|
8366
|
+
count: file.tasks.length,
|
|
8367
|
+
completed: computeTaskItemProgress(file.tasks).completed,
|
|
8368
|
+
inProgress: computeTaskItemProgress(file.tasks).inProgress
|
|
8369
|
+
};
|
|
8370
|
+
}
|
|
8371
|
+
return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
|
|
7880
8372
|
}
|
|
7881
8373
|
const p = computeTaskItemProgress(file.tasks);
|
|
7882
|
-
const summary =
|
|
8374
|
+
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
8375
|
+
${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
|
|
7883
8376
|
return {
|
|
7884
8377
|
ok: true,
|
|
7885
8378
|
message: summary,
|
|
@@ -7926,7 +8419,8 @@ var builtinTools = [
|
|
|
7926
8419
|
toolHelpTool,
|
|
7927
8420
|
codebaseIndexTool,
|
|
7928
8421
|
codebaseSearchTool,
|
|
7929
|
-
codebaseStatsTool
|
|
8422
|
+
codebaseStatsTool,
|
|
8423
|
+
setWorkingDirTool
|
|
7930
8424
|
];
|
|
7931
8425
|
|
|
7932
8426
|
// src/pack.ts
|