@wrongstack/tools 0.155.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/bash.js +116 -24
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +673 -202
- 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.js +19 -10
- 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 +4 -3
- package/dist/index.js +680 -209
- 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 +673 -202
- 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/pack.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, expectDefined, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob,
|
|
3
|
+
import { buildChildEnv, expectDefined, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import * as path2 from 'node:path';
|
|
7
|
-
import { resolve, sep, dirname } from 'node:path';
|
|
7
|
+
import { resolve, sep, dirname, join } from 'node:path';
|
|
8
8
|
import * as fs13 from 'node:fs/promises';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
@@ -12,6 +12,7 @@ import * as ts from 'typescript';
|
|
|
12
12
|
import * as dns from 'node:dns/promises';
|
|
13
13
|
import * as net from 'node:net';
|
|
14
14
|
import { Agent } from 'undici';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
15
16
|
|
|
16
17
|
// src/_spawn-stream.ts
|
|
17
18
|
function resolveWin32Command(cmd) {
|
|
@@ -39,6 +40,7 @@ function resolveWin32Command(cmd) {
|
|
|
39
40
|
async function* spawnStream(opts) {
|
|
40
41
|
const max = opts.maxBytes ?? 2e5;
|
|
41
42
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
43
|
+
const maxQueue = opts.maxQueueSize ?? 500;
|
|
42
44
|
let stdout = "";
|
|
43
45
|
let stderr = "";
|
|
44
46
|
let pending = "";
|
|
@@ -54,6 +56,7 @@ async function* spawnStream(opts) {
|
|
|
54
56
|
});
|
|
55
57
|
const queue = [];
|
|
56
58
|
let waiter;
|
|
59
|
+
let paused = false;
|
|
57
60
|
const wake = () => {
|
|
58
61
|
if (waiter) {
|
|
59
62
|
const w = waiter;
|
|
@@ -61,17 +64,34 @@ async function* spawnStream(opts) {
|
|
|
61
64
|
w();
|
|
62
65
|
}
|
|
63
66
|
};
|
|
67
|
+
const resume = () => {
|
|
68
|
+
if (paused && queue.length < maxQueue) {
|
|
69
|
+
paused = false;
|
|
70
|
+
child.stdout?.resume();
|
|
71
|
+
child.stderr?.resume();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
64
74
|
child.stdout?.on("data", (c) => {
|
|
65
75
|
const s = c.toString();
|
|
66
76
|
if (stdout.length < max) stdout += s;
|
|
67
77
|
queue.push({ kind: "out", data: s });
|
|
68
78
|
wake();
|
|
79
|
+
if (!paused && queue.length >= maxQueue) {
|
|
80
|
+
paused = true;
|
|
81
|
+
child.stdout?.pause();
|
|
82
|
+
child.stderr?.pause();
|
|
83
|
+
}
|
|
69
84
|
});
|
|
70
85
|
child.stderr?.on("data", (c) => {
|
|
71
86
|
const s = c.toString();
|
|
72
87
|
if (stderr.length < max) stderr += s;
|
|
73
88
|
queue.push({ kind: "err", data: s });
|
|
74
89
|
wake();
|
|
90
|
+
if (!paused && queue.length >= maxQueue) {
|
|
91
|
+
paused = true;
|
|
92
|
+
child.stdout?.pause();
|
|
93
|
+
child.stderr?.pause();
|
|
94
|
+
}
|
|
75
95
|
});
|
|
76
96
|
child.on("error", (e) => {
|
|
77
97
|
error = e.message;
|
|
@@ -91,6 +111,7 @@ async function* spawnStream(opts) {
|
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
const chunk = queue.shift();
|
|
114
|
+
resume();
|
|
94
115
|
if (chunk.kind === "close") {
|
|
95
116
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
96
117
|
break;
|
|
@@ -132,7 +153,7 @@ async function detectPackageManager(cwd) {
|
|
|
132
153
|
return "npm";
|
|
133
154
|
}
|
|
134
155
|
function resolvePath(input, ctx) {
|
|
135
|
-
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.cwd, input);
|
|
156
|
+
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
136
157
|
}
|
|
137
158
|
function ensureInsideRoot(absPath, ctx) {
|
|
138
159
|
const root = path2.resolve(ctx.projectRoot);
|
|
@@ -423,8 +444,13 @@ var CircuitBreaker = class {
|
|
|
423
444
|
* Call this BEFORE spawning a bash/exec process.
|
|
424
445
|
* Returns true if the call is allowed; false if the breaker is open.
|
|
425
446
|
* When false, callers MUST NOT spawn a process.
|
|
447
|
+
*
|
|
448
|
+
* @param bypass - If true, skip the circuit breaker check entirely.
|
|
449
|
+
* Use for background/fire-and-forget processes that should
|
|
450
|
+
* not affect breaker state.
|
|
426
451
|
*/
|
|
427
|
-
beforeCall() {
|
|
452
|
+
beforeCall(bypass = false) {
|
|
453
|
+
if (bypass) return true;
|
|
428
454
|
this._checkStateTransition();
|
|
429
455
|
if (this.state === "open") return false;
|
|
430
456
|
return true;
|
|
@@ -434,8 +460,12 @@ var CircuitBreaker = class {
|
|
|
434
460
|
* `durationMs` is the wall-clock time the process ran.
|
|
435
461
|
* `failed` is true when the process returned a non-zero exit code or
|
|
436
462
|
* threw an exception before spawning.
|
|
463
|
+
*
|
|
464
|
+
* @param bypass - If true, do not update breaker state.
|
|
465
|
+
* Use for background/fire-and-forget processes.
|
|
437
466
|
*/
|
|
438
|
-
afterCall(durationMs, failed) {
|
|
467
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
468
|
+
if (bypass) return;
|
|
439
469
|
const now = Date.now();
|
|
440
470
|
if (this.state === "half-open") {
|
|
441
471
|
if (failed) {
|
|
@@ -534,6 +564,17 @@ function redactCommand(cmd) {
|
|
|
534
564
|
return result;
|
|
535
565
|
}
|
|
536
566
|
var DEFAULT_GRACE_MS = 2e3;
|
|
567
|
+
function killWin32Tree(pid) {
|
|
568
|
+
try {
|
|
569
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
570
|
+
stdio: "ignore",
|
|
571
|
+
windowsHide: true
|
|
572
|
+
}).unref();
|
|
573
|
+
return true;
|
|
574
|
+
} catch {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
537
578
|
var ProcessRegistryImpl = class {
|
|
538
579
|
processes = /* @__PURE__ */ new Map();
|
|
539
580
|
breaker;
|
|
@@ -591,16 +632,20 @@ var ProcessRegistryImpl = class {
|
|
|
591
632
|
/**
|
|
592
633
|
* Called before spawning a process. Returns true if allowed; false if
|
|
593
634
|
* the circuit breaker is open.
|
|
635
|
+
*
|
|
636
|
+
* @param bypass - If true, skip circuit breaker check (for background processes).
|
|
594
637
|
*/
|
|
595
|
-
beforeCall() {
|
|
596
|
-
return this.breaker.beforeCall();
|
|
638
|
+
beforeCall(bypass = false) {
|
|
639
|
+
return this.breaker.beforeCall(bypass);
|
|
597
640
|
}
|
|
598
641
|
/**
|
|
599
642
|
* Called after a process finishes. `durationMs` is wall-clock time;
|
|
600
643
|
* `failed` is true for non-zero exit codes.
|
|
644
|
+
*
|
|
645
|
+
* @param bypass - If true, do not update circuit breaker state (for background processes).
|
|
601
646
|
*/
|
|
602
|
-
afterCall(durationMs, failed) {
|
|
603
|
-
this.breaker.afterCall(durationMs, failed);
|
|
647
|
+
afterCall(durationMs, failed, bypass = false) {
|
|
648
|
+
this.breaker.afterCall(durationMs, failed, bypass);
|
|
604
649
|
}
|
|
605
650
|
/** Force-open the circuit breaker (Ctrl+C, /kill force). */
|
|
606
651
|
forceBreakerOpen() {
|
|
@@ -631,9 +676,22 @@ var ProcessRegistryImpl = class {
|
|
|
631
676
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
632
677
|
const isWin = os.platform() === "win32";
|
|
633
678
|
if (isWin) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
679
|
+
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
680
|
+
if (liveRealChild && killWin32Tree(pid)) {
|
|
681
|
+
const fallback = setTimeout(() => {
|
|
682
|
+
if (p.child.exitCode === null) {
|
|
683
|
+
try {
|
|
684
|
+
p.child.kill("SIGKILL");
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}, graceMs);
|
|
689
|
+
fallback.unref?.();
|
|
690
|
+
} else {
|
|
691
|
+
try {
|
|
692
|
+
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
637
695
|
}
|
|
638
696
|
p.killed = true;
|
|
639
697
|
return true;
|
|
@@ -709,6 +767,7 @@ var MAX_OUTPUT = 32768;
|
|
|
709
767
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
710
768
|
var STREAM_FLUSH_INTERVAL_MS = 200;
|
|
711
769
|
var STREAM_FLUSH_BYTES = 4 * 1024;
|
|
770
|
+
var MAX_QUEUE_CHUNKS = 500;
|
|
712
771
|
var bashTool = {
|
|
713
772
|
name: "bash",
|
|
714
773
|
category: "Shell",
|
|
@@ -756,7 +815,8 @@ var bashTool = {
|
|
|
756
815
|
async *executeStream(input, ctx, opts) {
|
|
757
816
|
if (!input?.command) throw new Error("bash: command is required");
|
|
758
817
|
const registry = getProcessRegistry();
|
|
759
|
-
|
|
818
|
+
const bypassBreaker = !!input.background;
|
|
819
|
+
if (!registry.beforeCall(bypassBreaker)) {
|
|
760
820
|
yield {
|
|
761
821
|
type: "final",
|
|
762
822
|
output: {
|
|
@@ -769,6 +829,17 @@ var bashTool = {
|
|
|
769
829
|
};
|
|
770
830
|
return;
|
|
771
831
|
}
|
|
832
|
+
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
833
|
+
if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
|
|
834
|
+
console.warn(JSON.stringify({
|
|
835
|
+
level: "warn",
|
|
836
|
+
event: "bash.pipe_to_shell_detected",
|
|
837
|
+
message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
|
|
838
|
+
command_prefix: input.command.slice(0, 100),
|
|
839
|
+
// Log first 100 chars for review
|
|
840
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
772
843
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
|
|
773
844
|
const isWin = os.platform() === "win32";
|
|
774
845
|
const shell = (() => {
|
|
@@ -827,7 +898,7 @@ var bashTool = {
|
|
|
827
898
|
}
|
|
828
899
|
});
|
|
829
900
|
child2.on("close", () => {
|
|
830
|
-
registry.afterCall(Date.now() - startedAt, false);
|
|
901
|
+
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
831
902
|
});
|
|
832
903
|
if (typeof pid2 === "number") child2.unref();
|
|
833
904
|
yield {
|
|
@@ -846,7 +917,7 @@ var bashTool = {
|
|
|
846
917
|
env,
|
|
847
918
|
stdio: ["ignore", "pipe", "pipe"],
|
|
848
919
|
detached,
|
|
849
|
-
signal: opts.signal
|
|
920
|
+
...isWin ? {} : { signal: opts.signal }
|
|
850
921
|
});
|
|
851
922
|
const pid = child.pid;
|
|
852
923
|
if (typeof pid === "number") {
|
|
@@ -865,9 +936,22 @@ var bashTool = {
|
|
|
865
936
|
const timers = [];
|
|
866
937
|
function killWithTimeout(child2, timeoutMs2) {
|
|
867
938
|
if (isWin) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
939
|
+
if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
|
|
940
|
+
const fallback = setTimeout(() => {
|
|
941
|
+
if (child2.exitCode === null) {
|
|
942
|
+
try {
|
|
943
|
+
child2.kill();
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}, 2e3);
|
|
948
|
+
timers.push(fallback);
|
|
949
|
+
fallback.unref?.();
|
|
950
|
+
} else {
|
|
951
|
+
try {
|
|
952
|
+
child2.kill();
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
871
955
|
}
|
|
872
956
|
return;
|
|
873
957
|
}
|
|
@@ -906,6 +990,11 @@ var bashTool = {
|
|
|
906
990
|
}, timeoutMs);
|
|
907
991
|
timers.push(timer);
|
|
908
992
|
timer.unref?.();
|
|
993
|
+
const onAbort = () => killWithTimeout(child, 2e3);
|
|
994
|
+
if (isWin) {
|
|
995
|
+
if (opts.signal.aborted) onAbort();
|
|
996
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
997
|
+
}
|
|
909
998
|
const queue = [];
|
|
910
999
|
let resolveNext = null;
|
|
911
1000
|
const push = (c) => {
|
|
@@ -930,18 +1019,32 @@ var bashTool = {
|
|
|
930
1019
|
lastFlush = Date.now();
|
|
931
1020
|
return text;
|
|
932
1021
|
};
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1022
|
+
let paused = false;
|
|
1023
|
+
const pauseIfFlooded = () => {
|
|
1024
|
+
if (!paused && queue.length >= MAX_QUEUE_CHUNKS) {
|
|
1025
|
+
paused = true;
|
|
1026
|
+
child.stdout?.pause();
|
|
1027
|
+
child.stderr?.pause();
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
const resumeIfDrained = () => {
|
|
1031
|
+
if (paused && queue.length < MAX_QUEUE_CHUNKS) {
|
|
1032
|
+
paused = false;
|
|
1033
|
+
child.stdout?.resume();
|
|
1034
|
+
child.stderr?.resume();
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
const onData = (chunk) => {
|
|
940
1038
|
const text = chunk.toString();
|
|
941
|
-
buf
|
|
1039
|
+
if (buf.length < MAX_OUTPUT) {
|
|
1040
|
+
buf += text.slice(0, MAX_OUTPUT - buf.length);
|
|
1041
|
+
}
|
|
942
1042
|
pending += text;
|
|
943
1043
|
push({ kind: "data", text });
|
|
944
|
-
|
|
1044
|
+
pauseIfFlooded();
|
|
1045
|
+
};
|
|
1046
|
+
child.stdout?.on("data", onData);
|
|
1047
|
+
child.stderr?.on("data", onData);
|
|
945
1048
|
child.on("error", (err) => {
|
|
946
1049
|
for (const t of timers) clearTimeout(t);
|
|
947
1050
|
registry.afterCall(Date.now() - startedAt, true);
|
|
@@ -956,6 +1059,7 @@ var bashTool = {
|
|
|
956
1059
|
try {
|
|
957
1060
|
while (true) {
|
|
958
1061
|
const c = await next();
|
|
1062
|
+
resumeIfDrained();
|
|
959
1063
|
if (c.kind === "error") throw c.err;
|
|
960
1064
|
if (c.kind === "end") {
|
|
961
1065
|
const remainder = flush();
|
|
@@ -980,6 +1084,15 @@ var bashTool = {
|
|
|
980
1084
|
}
|
|
981
1085
|
} finally {
|
|
982
1086
|
for (const t of timers) clearTimeout(t);
|
|
1087
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
1088
|
+
child.stdout?.off("data", onData);
|
|
1089
|
+
child.stderr?.off("data", onData);
|
|
1090
|
+
child.stdout?.destroy();
|
|
1091
|
+
child.stderr?.destroy();
|
|
1092
|
+
if (child.exitCode === null && !child.killed) {
|
|
1093
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
1094
|
+
else killWithTimeout(child, 2e3);
|
|
1095
|
+
}
|
|
983
1096
|
}
|
|
984
1097
|
}
|
|
985
1098
|
};
|
|
@@ -2842,8 +2955,18 @@ async function parseFile(file, content, lang) {
|
|
|
2842
2955
|
}
|
|
2843
2956
|
}
|
|
2844
2957
|
async function runIndexer(_ctx, opts) {
|
|
2845
|
-
const
|
|
2846
|
-
|
|
2958
|
+
const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
|
|
2959
|
+
try {
|
|
2960
|
+
return await runIndexerWithStore(store, opts);
|
|
2961
|
+
} finally {
|
|
2962
|
+
try {
|
|
2963
|
+
store.close();
|
|
2964
|
+
} catch {
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
async function runIndexerWithStore(store, opts) {
|
|
2969
|
+
const { projectRoot, force = false, langs, ignore = [], signal } = opts;
|
|
2847
2970
|
const startMs = Date.now();
|
|
2848
2971
|
const errors = [];
|
|
2849
2972
|
const langStats = {};
|
|
@@ -2956,7 +3079,6 @@ async function runIndexer(_ctx, opts) {
|
|
|
2956
3079
|
}
|
|
2957
3080
|
const durationMs = Date.now() - startMs;
|
|
2958
3081
|
store.setLastIndexed(Date.now());
|
|
2959
|
-
store.close();
|
|
2960
3082
|
return {
|
|
2961
3083
|
filesIndexed,
|
|
2962
3084
|
symbolsIndexed,
|
|
@@ -3878,12 +4000,13 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3878
4000
|
let killed = false;
|
|
3879
4001
|
const startedAt = Date.now();
|
|
3880
4002
|
const resolved = resolveWin32Command(cmd);
|
|
3881
|
-
const
|
|
4003
|
+
const isWin = process.platform === "win32";
|
|
4004
|
+
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
3882
4005
|
const child = spawn(resolved, args, {
|
|
3883
4006
|
cwd,
|
|
3884
|
-
signal,
|
|
3885
4007
|
env: buildChildEnv(sessionId),
|
|
3886
4008
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4009
|
+
...isWin ? {} : { signal },
|
|
3887
4010
|
...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
|
|
3888
4011
|
});
|
|
3889
4012
|
const registry = getProcessRegistry();
|
|
@@ -3897,6 +4020,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3897
4020
|
if (typeof pid === "number") registry.kill(pid);
|
|
3898
4021
|
else child.kill("SIGTERM");
|
|
3899
4022
|
}, timeout);
|
|
4023
|
+
const onAbort = () => {
|
|
4024
|
+
killed = true;
|
|
4025
|
+
if (typeof pid === "number") registry.kill(pid, { force: true });
|
|
4026
|
+
else child.kill("SIGTERM");
|
|
4027
|
+
};
|
|
4028
|
+
if (isWin) {
|
|
4029
|
+
if (signal.aborted) onAbort();
|
|
4030
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4031
|
+
}
|
|
3900
4032
|
child.stdout?.on("data", (chunk) => {
|
|
3901
4033
|
if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
|
|
3902
4034
|
});
|
|
@@ -3905,6 +4037,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3905
4037
|
});
|
|
3906
4038
|
child.on("close", (code) => {
|
|
3907
4039
|
clearTimeout(timer);
|
|
4040
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
3908
4041
|
if (typeof pid === "number") registry.unregister(pid);
|
|
3909
4042
|
const durationMs = Date.now() - startedAt;
|
|
3910
4043
|
const exitCode = killed ? 124 : code ?? 1;
|
|
@@ -3921,6 +4054,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
3921
4054
|
});
|
|
3922
4055
|
child.on("error", (err) => {
|
|
3923
4056
|
clearTimeout(timer);
|
|
4057
|
+
if (isWin) signal.removeEventListener("abort", onAbort);
|
|
3924
4058
|
if (typeof pid === "number") registry.unregister(pid);
|
|
3925
4059
|
registry.afterCall(Date.now() - startedAt, true);
|
|
3926
4060
|
resolve7({
|
|
@@ -5042,8 +5176,6 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
5042
5176
|
used: "native"
|
|
5043
5177
|
};
|
|
5044
5178
|
}
|
|
5045
|
-
|
|
5046
|
-
// src/install.ts
|
|
5047
5179
|
var installTool = {
|
|
5048
5180
|
name: "install",
|
|
5049
5181
|
category: "Package Management",
|
|
@@ -5138,18 +5270,48 @@ var installTool = {
|
|
|
5138
5270
|
signal: opts.signal,
|
|
5139
5271
|
maxBytes: 1e5
|
|
5140
5272
|
});
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
dry_run: args.includes("--dry-run"),
|
|
5148
|
-
truncated: result.truncated
|
|
5149
|
-
}
|
|
5273
|
+
const output = {
|
|
5274
|
+
packages: pkgList,
|
|
5275
|
+
exit_code: result.exitCode,
|
|
5276
|
+
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
5277
|
+
dry_run: args.includes("--dry-run"),
|
|
5278
|
+
truncated: result.truncated
|
|
5150
5279
|
};
|
|
5280
|
+
const isSuccess = result.exitCode === 0 && !output.dry_run && !input.global;
|
|
5281
|
+
if (isSuccess && pkgList.length > 0) {
|
|
5282
|
+
const trackerOpts = ctx.meta?.["packageTrackerOpts"];
|
|
5283
|
+
if (trackerOpts) {
|
|
5284
|
+
const manifestPath = resolveManifestPath(cwd, pkgManager);
|
|
5285
|
+
for (const pkg of pkgList) {
|
|
5286
|
+
try {
|
|
5287
|
+
await recordPackageAction(trackerOpts, {
|
|
5288
|
+
manifestPath,
|
|
5289
|
+
packageName: pkg,
|
|
5290
|
+
versionSpec: "latest",
|
|
5291
|
+
// exact version resolved by package manager at install time
|
|
5292
|
+
ecosystem: detectPackageEcosystem(manifestPath),
|
|
5293
|
+
agentId: ctx.agentId,
|
|
5294
|
+
agentName: ctx.agentName,
|
|
5295
|
+
sessionId: ctx.session.id
|
|
5296
|
+
});
|
|
5297
|
+
} catch {
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
yield { type: "final", output };
|
|
5151
5303
|
}
|
|
5152
5304
|
};
|
|
5305
|
+
function resolveManifestPath(cwd, pkgManager) {
|
|
5306
|
+
switch (pkgManager) {
|
|
5307
|
+
case "pnpm":
|
|
5308
|
+
case "yarn":
|
|
5309
|
+
case "npm":
|
|
5310
|
+
return join(cwd, "package.json");
|
|
5311
|
+
default:
|
|
5312
|
+
return join(cwd, "package.json");
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5153
5315
|
var jsonTool = {
|
|
5154
5316
|
name: "json",
|
|
5155
5317
|
category: "Data",
|
|
@@ -5779,7 +5941,7 @@ var planTool = {
|
|
|
5779
5941
|
name: "plan",
|
|
5780
5942
|
category: "Session",
|
|
5781
5943
|
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.",
|
|
5782
|
-
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.',
|
|
5944
|
+
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.',
|
|
5783
5945
|
permission: "confirm",
|
|
5784
5946
|
mutating: true,
|
|
5785
5947
|
capabilities: ["fs.write"],
|
|
@@ -5796,9 +5958,9 @@ var planTool = {
|
|
|
5796
5958
|
"done",
|
|
5797
5959
|
"remove",
|
|
5798
5960
|
"promote",
|
|
5799
|
-
"derive",
|
|
5800
5961
|
"template_use",
|
|
5801
|
-
"clear"
|
|
5962
|
+
"clear",
|
|
5963
|
+
"taskify"
|
|
5802
5964
|
],
|
|
5803
5965
|
description: "The operation to perform on the plan board."
|
|
5804
5966
|
},
|
|
@@ -5817,7 +5979,7 @@ var planTool = {
|
|
|
5817
5979
|
subtasks: {
|
|
5818
5980
|
type: "array",
|
|
5819
5981
|
items: { type: "string" },
|
|
5820
|
-
description: "List of subtask titles. Used with promote
|
|
5982
|
+
description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
|
|
5821
5983
|
},
|
|
5822
5984
|
template: {
|
|
5823
5985
|
type: "string",
|
|
@@ -5838,92 +6000,151 @@ var planTool = {
|
|
|
5838
6000
|
};
|
|
5839
6001
|
}
|
|
5840
6002
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
5841
|
-
let
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
if (!input.target) {
|
|
5857
|
-
return mkResult(plan, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6003
|
+
let early = null;
|
|
6004
|
+
const taskifyMeta = { title: "", details: "" };
|
|
6005
|
+
let didTaskify = false;
|
|
6006
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
6007
|
+
switch (input.action) {
|
|
6008
|
+
case "show":
|
|
6009
|
+
break;
|
|
6010
|
+
case "add": {
|
|
6011
|
+
const title = input.title?.trim();
|
|
6012
|
+
if (!title) {
|
|
6013
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
6014
|
+
return p;
|
|
6015
|
+
}
|
|
6016
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
6017
|
+
return updated;
|
|
5858
6018
|
}
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
input.target
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
6019
|
+
case "start":
|
|
6020
|
+
case "done": {
|
|
6021
|
+
if (!input.target) {
|
|
6022
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6023
|
+
return p;
|
|
6024
|
+
}
|
|
6025
|
+
const next = setPlanItemStatus(
|
|
6026
|
+
p,
|
|
6027
|
+
input.target,
|
|
6028
|
+
input.action === "start" ? "in_progress" : "done"
|
|
6029
|
+
);
|
|
6030
|
+
if (next === p) {
|
|
6031
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6032
|
+
return p;
|
|
6033
|
+
}
|
|
6034
|
+
return next;
|
|
5866
6035
|
}
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6036
|
+
case "remove": {
|
|
6037
|
+
if (!input.target) {
|
|
6038
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
6039
|
+
return p;
|
|
6040
|
+
}
|
|
6041
|
+
const next = removePlanItem(p, input.target);
|
|
6042
|
+
if (next === p) {
|
|
6043
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6044
|
+
return p;
|
|
6045
|
+
}
|
|
6046
|
+
return next;
|
|
5874
6047
|
}
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
6048
|
+
case "promote": {
|
|
6049
|
+
if (!input.target) {
|
|
6050
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6051
|
+
return p;
|
|
6052
|
+
}
|
|
6053
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
6054
|
+
if (!derived) {
|
|
6055
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6056
|
+
return p;
|
|
6057
|
+
}
|
|
6058
|
+
ctx.state.replaceTodos(derived.todos);
|
|
6059
|
+
early = mkResult(
|
|
6060
|
+
derived.plan,
|
|
6061
|
+
true,
|
|
6062
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
6063
|
+
derived.todos
|
|
6064
|
+
);
|
|
6065
|
+
return derived.plan;
|
|
5878
6066
|
}
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
6067
|
+
case "template_use": {
|
|
6068
|
+
const templateName = input.template?.trim();
|
|
6069
|
+
if (!templateName) {
|
|
6070
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
6071
|
+
return p;
|
|
6072
|
+
}
|
|
6073
|
+
const template = getPlanTemplate(templateName);
|
|
6074
|
+
if (!template) {
|
|
6075
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
6076
|
+
return p;
|
|
6077
|
+
}
|
|
6078
|
+
let updated = p;
|
|
6079
|
+
for (const item of template.items) {
|
|
6080
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
6081
|
+
}
|
|
6082
|
+
early = mkResult(
|
|
6083
|
+
updated,
|
|
6084
|
+
true,
|
|
6085
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
6086
|
+
);
|
|
6087
|
+
return updated;
|
|
5887
6088
|
}
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
6089
|
+
case "clear":
|
|
6090
|
+
return clearPlan(p);
|
|
6091
|
+
case "taskify": {
|
|
6092
|
+
if (!input.target) {
|
|
6093
|
+
early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
|
|
6094
|
+
return p;
|
|
6095
|
+
}
|
|
6096
|
+
let itemIdx = -1;
|
|
6097
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
6098
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
6099
|
+
itemIdx = asNum - 1;
|
|
6100
|
+
} else {
|
|
6101
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
6102
|
+
if (itemIdx === -1) {
|
|
6103
|
+
const lower = input.target.toLowerCase();
|
|
6104
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
6108
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6109
|
+
return p;
|
|
6110
|
+
}
|
|
6111
|
+
const item = p.items[itemIdx];
|
|
6112
|
+
taskifyMeta.title = item.title;
|
|
6113
|
+
taskifyMeta.details = item.details ?? "";
|
|
6114
|
+
didTaskify = true;
|
|
6115
|
+
break;
|
|
5891
6116
|
}
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
return mkResult(
|
|
5896
|
-
plan,
|
|
5897
|
-
true,
|
|
5898
|
-
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
5899
|
-
derived.todos
|
|
5900
|
-
);
|
|
6117
|
+
default:
|
|
6118
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
6119
|
+
return p;
|
|
5901
6120
|
}
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
return mkResult(plan, false, `Unknown template "${templateName}".`);
|
|
5910
|
-
}
|
|
5911
|
-
for (const item of template.items) {
|
|
5912
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
5913
|
-
}
|
|
5914
|
-
await savePlan(planPath, plan);
|
|
5915
|
-
return mkResult(
|
|
5916
|
-
plan,
|
|
5917
|
-
true,
|
|
5918
|
-
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
5919
|
-
);
|
|
6121
|
+
return p;
|
|
6122
|
+
});
|
|
6123
|
+
if (early) return early;
|
|
6124
|
+
if (didTaskify) {
|
|
6125
|
+
const taskPath = ctx.meta["task.path"];
|
|
6126
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
6127
|
+
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
5920
6128
|
}
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
6129
|
+
const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
|
|
6130
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6131
|
+
taskFile.tasks.push({
|
|
6132
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
6133
|
+
title: taskifyMeta.title,
|
|
6134
|
+
description: taskifyMeta.details || void 0,
|
|
6135
|
+
type: "feature",
|
|
6136
|
+
priority: "medium",
|
|
6137
|
+
status: "pending",
|
|
6138
|
+
createdAt: now,
|
|
6139
|
+
updatedAt: now
|
|
6140
|
+
});
|
|
6141
|
+
await saveTasks(taskPath, taskFile);
|
|
6142
|
+
return mkResult(
|
|
6143
|
+
plan,
|
|
6144
|
+
true,
|
|
6145
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
6146
|
+
${formatTaskList(taskFile.tasks)}`
|
|
6147
|
+
);
|
|
5927
6148
|
}
|
|
5928
6149
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
5929
6150
|
}
|
|
@@ -6469,13 +6690,24 @@ var searchTool = {
|
|
|
6469
6690
|
async function duckduckgoSearch(query2, num, signal) {
|
|
6470
6691
|
const encoded = encodeURIComponent(query2);
|
|
6471
6692
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
results,
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6693
|
+
try {
|
|
6694
|
+
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
6695
|
+
const html = await response.text();
|
|
6696
|
+
const results = parseDuckDuckGo(html, num);
|
|
6697
|
+
return {
|
|
6698
|
+
query: query2,
|
|
6699
|
+
results,
|
|
6700
|
+
source: "duckduckgo",
|
|
6701
|
+
truncated: results.length >= num
|
|
6702
|
+
};
|
|
6703
|
+
} catch {
|
|
6704
|
+
return {
|
|
6705
|
+
query: query2,
|
|
6706
|
+
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
6707
|
+
source: "duckduckgo",
|
|
6708
|
+
truncated: false
|
|
6709
|
+
};
|
|
6710
|
+
}
|
|
6479
6711
|
}
|
|
6480
6712
|
function takeFrom(iter, max) {
|
|
6481
6713
|
const out = [];
|
|
@@ -6594,34 +6826,92 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
6594
6826
|
}
|
|
6595
6827
|
}
|
|
6596
6828
|
function anySignal(...signals) {
|
|
6597
|
-
|
|
6598
|
-
for (const s of signals) {
|
|
6599
|
-
if (s.aborted) {
|
|
6600
|
-
controller.abort();
|
|
6601
|
-
break;
|
|
6602
|
-
}
|
|
6603
|
-
s.addEventListener("abort", () => controller.abort());
|
|
6604
|
-
}
|
|
6605
|
-
return controller.signal;
|
|
6829
|
+
return AbortSignal.any(signals);
|
|
6606
6830
|
}
|
|
6607
6831
|
function stripTags2(html) {
|
|
6608
6832
|
return html.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
6609
6833
|
}
|
|
6834
|
+
var setWorkingDirTool = {
|
|
6835
|
+
name: "set_working_dir",
|
|
6836
|
+
category: "Context",
|
|
6837
|
+
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.",
|
|
6838
|
+
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.",
|
|
6839
|
+
permission: "confirm",
|
|
6840
|
+
mutating: true,
|
|
6841
|
+
capabilities: ["fs.read"],
|
|
6842
|
+
timeoutMs: 5e3,
|
|
6843
|
+
inputSchema: {
|
|
6844
|
+
type: "object",
|
|
6845
|
+
properties: {
|
|
6846
|
+
path: {
|
|
6847
|
+
type: "string",
|
|
6848
|
+
description: "Directory to navigate to. Can be relative (to projectRoot) or absolute. If omitted, returns the current working directory without changing it."
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
},
|
|
6852
|
+
async execute(input, ctx, _opts) {
|
|
6853
|
+
if (!input.path) {
|
|
6854
|
+
return {
|
|
6855
|
+
current: ctx.workingDir,
|
|
6856
|
+
message: `Current working directory is ${ctx.workingDir}`
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
const previous = ctx.workingDir;
|
|
6860
|
+
let resolved;
|
|
6861
|
+
try {
|
|
6862
|
+
resolved = ctx.setWorkingDir(input.path);
|
|
6863
|
+
} catch (err) {
|
|
6864
|
+
return {
|
|
6865
|
+
current: ctx.workingDir,
|
|
6866
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6867
|
+
};
|
|
6868
|
+
}
|
|
6869
|
+
try {
|
|
6870
|
+
await fs13.access(resolved);
|
|
6871
|
+
} catch {
|
|
6872
|
+
try {
|
|
6873
|
+
ctx.setWorkingDir(previous);
|
|
6874
|
+
} catch {
|
|
6875
|
+
}
|
|
6876
|
+
return {
|
|
6877
|
+
current: ctx.workingDir,
|
|
6878
|
+
error: `Directory does not exist: ${resolved}`
|
|
6879
|
+
};
|
|
6880
|
+
}
|
|
6881
|
+
return {
|
|
6882
|
+
current: resolved,
|
|
6883
|
+
previous,
|
|
6884
|
+
message: `Working directory changed to ${resolved}`
|
|
6885
|
+
};
|
|
6886
|
+
}
|
|
6887
|
+
};
|
|
6888
|
+
function findTaskIndex(tasks, query2) {
|
|
6889
|
+
const asNum = Number.parseInt(query2, 10);
|
|
6890
|
+
if (!Number.isNaN(asNum)) {
|
|
6891
|
+
const idx = asNum - 1;
|
|
6892
|
+
if (tasks[idx]) return idx;
|
|
6893
|
+
}
|
|
6894
|
+
const byId = tasks.findIndex((t) => t.id === query2);
|
|
6895
|
+
if (byId >= 0) return byId;
|
|
6896
|
+
const lower = query2.toLowerCase();
|
|
6897
|
+
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
6898
|
+
}
|
|
6610
6899
|
var taskTool = {
|
|
6611
6900
|
name: "task",
|
|
6612
6901
|
category: "Session",
|
|
6613
6902
|
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.",
|
|
6614
|
-
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',
|
|
6615
|
-
permission: "
|
|
6616
|
-
mutating:
|
|
6903
|
+
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',
|
|
6904
|
+
permission: "confirm",
|
|
6905
|
+
mutating: true,
|
|
6906
|
+
capabilities: ["fs.write"],
|
|
6617
6907
|
timeoutMs: 2e3,
|
|
6618
6908
|
inputSchema: {
|
|
6619
6909
|
type: "object",
|
|
6620
6910
|
properties: {
|
|
6621
6911
|
action: {
|
|
6622
6912
|
type: "string",
|
|
6623
|
-
enum: ["replace", "add", "status", "show"],
|
|
6624
|
-
description: "replace = set full list, add = append, status = update task status, show = view only."
|
|
6913
|
+
enum: ["replace", "add", "status", "show", "promote", "planify"],
|
|
6914
|
+
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."
|
|
6625
6915
|
},
|
|
6626
6916
|
tasks: {
|
|
6627
6917
|
type: "array",
|
|
@@ -6665,11 +6955,20 @@ var taskTool = {
|
|
|
6665
6955
|
required: ["title", "type", "priority"],
|
|
6666
6956
|
description: "Single task to append (id/createdAt/updatedAt auto-generated)."
|
|
6667
6957
|
},
|
|
6668
|
-
id: { type: "string", description: "Task id for action=status." },
|
|
6958
|
+
id: { type: "string", description: "Task id for action=status or target for action=promote." },
|
|
6669
6959
|
status: {
|
|
6670
6960
|
type: "string",
|
|
6671
6961
|
enum: ["pending", "in_progress", "blocked", "failed", "review", "completed"],
|
|
6672
6962
|
description: "New status for action=status."
|
|
6963
|
+
},
|
|
6964
|
+
target: {
|
|
6965
|
+
type: "string",
|
|
6966
|
+
description: "Target task identifier (id, 1-based index, or title substring) for action=promote."
|
|
6967
|
+
},
|
|
6968
|
+
subtasks: {
|
|
6969
|
+
type: "array",
|
|
6970
|
+
items: { type: "string" },
|
|
6971
|
+
description: "Optional subtask titles for action=promote. Each becomes a pending todo."
|
|
6673
6972
|
}
|
|
6674
6973
|
},
|
|
6675
6974
|
required: ["action"]
|
|
@@ -6680,65 +6979,196 @@ var taskTool = {
|
|
|
6680
6979
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
6681
6980
|
}
|
|
6682
6981
|
const sessionId = ctx.session?.id ?? "unknown";
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6982
|
+
let early = null;
|
|
6983
|
+
const promoteMeta = { count: 0, title: "" };
|
|
6984
|
+
const planifyMeta = { title: "", details: "" };
|
|
6985
|
+
let didPlanify = false;
|
|
6986
|
+
const file = await mutateTasks(taskPath, sessionId, async (f) => {
|
|
6987
|
+
switch (input.action) {
|
|
6988
|
+
case "show":
|
|
6989
|
+
break;
|
|
6990
|
+
case "replace": {
|
|
6991
|
+
if (!Array.isArray(input.tasks)) {
|
|
6992
|
+
early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
|
|
6993
|
+
return f;
|
|
6994
|
+
}
|
|
6995
|
+
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
6996
|
+
for (const t of input.tasks) {
|
|
6997
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
6998
|
+
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
6999
|
+
if (missing.length > 0) {
|
|
7000
|
+
early = {
|
|
7001
|
+
ok: false,
|
|
7002
|
+
message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
|
|
7003
|
+
count: 0,
|
|
7004
|
+
completed: 0,
|
|
7005
|
+
inProgress: 0
|
|
7006
|
+
};
|
|
7007
|
+
return f;
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7012
|
+
f.tasks = input.tasks.map((t) => ({
|
|
7013
|
+
...t,
|
|
7014
|
+
createdAt: t.createdAt || now,
|
|
7015
|
+
updatedAt: now
|
|
7016
|
+
}));
|
|
7017
|
+
break;
|
|
6690
7018
|
}
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
7019
|
+
case "add": {
|
|
7020
|
+
const t = input.task;
|
|
7021
|
+
if (!t || !t.title) {
|
|
7022
|
+
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
7023
|
+
return f;
|
|
7024
|
+
}
|
|
7025
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7026
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
7027
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
7028
|
+
if (missing.length > 0) {
|
|
7029
|
+
early = {
|
|
7030
|
+
ok: false,
|
|
7031
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
7032
|
+
count: 0,
|
|
7033
|
+
completed: 0,
|
|
7034
|
+
inProgress: 0
|
|
7035
|
+
};
|
|
7036
|
+
return f;
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7040
|
+
const newTask = {
|
|
7041
|
+
id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
7042
|
+
title: t.title,
|
|
7043
|
+
description: t.description,
|
|
7044
|
+
type: t.type || "feature",
|
|
7045
|
+
priority: t.priority || "medium",
|
|
7046
|
+
status: t.status || "pending",
|
|
7047
|
+
dependsOn: t.dependsOn,
|
|
7048
|
+
assignee: t.assignee,
|
|
7049
|
+
estimateHours: t.estimateHours,
|
|
7050
|
+
tags: t.tags,
|
|
7051
|
+
createdAt: now,
|
|
7052
|
+
updatedAt: now
|
|
7053
|
+
};
|
|
7054
|
+
f.tasks.push(newTask);
|
|
7055
|
+
break;
|
|
6704
7056
|
}
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
updatedAt: now
|
|
6719
|
-
};
|
|
6720
|
-
file.tasks.push(newTask);
|
|
6721
|
-
await saveTasks(taskPath, file);
|
|
6722
|
-
break;
|
|
6723
|
-
}
|
|
6724
|
-
case "status": {
|
|
6725
|
-
if (!input.id || !input.status) {
|
|
6726
|
-
return { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
7057
|
+
case "status": {
|
|
7058
|
+
if (!input.id || !input.status) {
|
|
7059
|
+
early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
7060
|
+
return f;
|
|
7061
|
+
}
|
|
7062
|
+
const task = f.tasks.find((t) => t.id === input.id);
|
|
7063
|
+
if (!task) {
|
|
7064
|
+
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
7065
|
+
return f;
|
|
7066
|
+
}
|
|
7067
|
+
task.status = input.status;
|
|
7068
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7069
|
+
break;
|
|
6727
7070
|
}
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
7071
|
+
case "promote": {
|
|
7072
|
+
const target = input.target?.trim();
|
|
7073
|
+
if (!target) {
|
|
7074
|
+
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
7075
|
+
return f;
|
|
7076
|
+
}
|
|
7077
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
7078
|
+
if (idx === -1) {
|
|
7079
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7080
|
+
return f;
|
|
7081
|
+
}
|
|
7082
|
+
const match = f.tasks[idx];
|
|
7083
|
+
if (!match) {
|
|
7084
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7085
|
+
return f;
|
|
7086
|
+
}
|
|
7087
|
+
if (match.status !== "completed" && match.status !== "failed") {
|
|
7088
|
+
match.status = "in_progress";
|
|
7089
|
+
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7090
|
+
}
|
|
7091
|
+
const todos = [];
|
|
7092
|
+
const ts2 = Date.now();
|
|
7093
|
+
todos.push({
|
|
7094
|
+
id: `todo_${ts2}_task`,
|
|
7095
|
+
content: match.title,
|
|
7096
|
+
status: "in_progress",
|
|
7097
|
+
activeForm: match.title,
|
|
7098
|
+
promotedFromTask: match.id
|
|
7099
|
+
});
|
|
7100
|
+
if (match.description) {
|
|
7101
|
+
todos.push({
|
|
7102
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
7103
|
+
content: match.description.slice(0, 200),
|
|
7104
|
+
status: "pending",
|
|
7105
|
+
promotedFromTask: match.id
|
|
7106
|
+
});
|
|
7107
|
+
}
|
|
7108
|
+
if (input.subtasks && input.subtasks.length > 0) {
|
|
7109
|
+
for (const st of input.subtasks) {
|
|
7110
|
+
todos.push({
|
|
7111
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
7112
|
+
content: st,
|
|
7113
|
+
status: "pending",
|
|
7114
|
+
promotedFromTask: match.id
|
|
7115
|
+
});
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
ctx.state.replaceTodos(todos);
|
|
7119
|
+
promoteMeta.count = todos.length;
|
|
7120
|
+
promoteMeta.title = match.title;
|
|
7121
|
+
break;
|
|
6731
7122
|
}
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
7123
|
+
case "planify": {
|
|
7124
|
+
const target = input.target?.trim();
|
|
7125
|
+
if (!target) {
|
|
7126
|
+
early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
7127
|
+
return f;
|
|
7128
|
+
}
|
|
7129
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
7130
|
+
if (idx === -1) {
|
|
7131
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7132
|
+
return f;
|
|
7133
|
+
}
|
|
7134
|
+
const match = f.tasks[idx];
|
|
7135
|
+
if (!match) {
|
|
7136
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7137
|
+
return f;
|
|
7138
|
+
}
|
|
7139
|
+
planifyMeta.title = match.title;
|
|
7140
|
+
planifyMeta.details = match.description ?? "";
|
|
7141
|
+
didPlanify = true;
|
|
7142
|
+
break;
|
|
7143
|
+
}
|
|
7144
|
+
default:
|
|
7145
|
+
early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
|
|
7146
|
+
return f;
|
|
6736
7147
|
}
|
|
6737
|
-
|
|
6738
|
-
|
|
7148
|
+
return f;
|
|
7149
|
+
});
|
|
7150
|
+
if (early) return early;
|
|
7151
|
+
if (didPlanify) {
|
|
7152
|
+
const { title, details } = planifyMeta;
|
|
7153
|
+
const planPath = ctx.meta["plan.path"];
|
|
7154
|
+
if (typeof planPath === "string" && planPath) {
|
|
7155
|
+
const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
7156
|
+
const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
|
|
7157
|
+
await savePlan(planPath, updated);
|
|
7158
|
+
return {
|
|
7159
|
+
ok: true,
|
|
7160
|
+
message: `planify ok \u2014 added "${title}" to plan.
|
|
7161
|
+
${formatPlan(updated)}`,
|
|
7162
|
+
count: file.tasks.length,
|
|
7163
|
+
completed: computeTaskItemProgress(file.tasks).completed,
|
|
7164
|
+
inProgress: computeTaskItemProgress(file.tasks).inProgress
|
|
7165
|
+
};
|
|
7166
|
+
}
|
|
7167
|
+
return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
|
|
6739
7168
|
}
|
|
6740
7169
|
const p = computeTaskItemProgress(file.tasks);
|
|
6741
|
-
const summary =
|
|
7170
|
+
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
7171
|
+
${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tasks) : "No tasks.";
|
|
6742
7172
|
return {
|
|
6743
7173
|
ok: true,
|
|
6744
7174
|
message: summary,
|
|
@@ -6896,8 +7326,6 @@ function parseResult(runner, result, duration) {
|
|
|
6896
7326
|
truncated: result.truncated
|
|
6897
7327
|
};
|
|
6898
7328
|
}
|
|
6899
|
-
|
|
6900
|
-
// src/todo.ts
|
|
6901
7329
|
var todoTool = {
|
|
6902
7330
|
name: "todo",
|
|
6903
7331
|
category: "Session",
|
|
@@ -6956,6 +7384,48 @@ var todoTool = {
|
|
|
6956
7384
|
}
|
|
6957
7385
|
}
|
|
6958
7386
|
ctx.state.replaceTodos(items);
|
|
7387
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
7388
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
7389
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
7390
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
7391
|
+
for (const item of items) {
|
|
7392
|
+
if (item.promotedFromPlan) {
|
|
7393
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
|
|
7394
|
+
}
|
|
7395
|
+
if (item.promotedFromTask) {
|
|
7396
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
for (const planId of completedPlanIds) {
|
|
7400
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
7401
|
+
const planPath = ctx.meta["plan.path"];
|
|
7402
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
7403
|
+
try {
|
|
7404
|
+
const plan = await loadPlan(planPath);
|
|
7405
|
+
if (plan) {
|
|
7406
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
7407
|
+
await savePlan(planPath, updated);
|
|
7408
|
+
}
|
|
7409
|
+
} catch {
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
for (const taskId of completedTaskIds) {
|
|
7413
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
7414
|
+
const taskPath = ctx.meta["task.path"];
|
|
7415
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
7416
|
+
try {
|
|
7417
|
+
const file = await loadTasks(taskPath);
|
|
7418
|
+
if (file) {
|
|
7419
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
7420
|
+
if (task && task.status !== "completed") {
|
|
7421
|
+
task.status = "completed";
|
|
7422
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7423
|
+
await saveTasks(taskPath, file);
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
} catch {
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
6959
7429
|
return {
|
|
6960
7430
|
count: items.length,
|
|
6961
7431
|
in_progress: items.filter((t) => t.status === "in_progress").length
|
|
@@ -7591,7 +8061,8 @@ var builtinTools = [
|
|
|
7591
8061
|
toolHelpTool,
|
|
7592
8062
|
codebaseIndexTool,
|
|
7593
8063
|
codebaseSearchTool,
|
|
7594
|
-
codebaseStatsTool
|
|
8064
|
+
codebaseStatsTool,
|
|
8065
|
+
setWorkingDirTool
|
|
7595
8066
|
];
|
|
7596
8067
|
|
|
7597
8068
|
// src/pack.ts
|