@wrongstack/tools 0.313.0 → 0.316.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_edit-match.d.ts +3 -2
- package/dist/_output-spool.d.ts +4 -3
- package/dist/_shell-pick.d.ts +2 -1
- package/dist/_spawn-stream.d.ts +2 -1
- package/dist/_syntax-check.d.ts +2 -1
- package/dist/audit.js +10 -4055
- package/dist/bash-kill-guard.d.ts +3 -6
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/browser/network-guard-proxy.d.ts +2 -1
- package/dist/builtin.js +69 -33949
- package/dist/chunk-22B7GE5T.js +260 -0
- package/dist/chunk-2VSZC6QM.js +113 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-42BHI7TN.js +337 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-4UESMQDP.js +122 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-5H4GSJAI.js +223 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-7EY5DCRO.js +434 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-BSLVR56S.js +199 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DA4KNE2H.js +4412 -0
- package/dist/chunk-DR7JOQTC.js +520 -0
- package/dist/chunk-EEOFQDT6.js +898 -0
- package/dist/chunk-EJWSCQ6D.js +137 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FQZOHLG3.js +89 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HYX2TZ7O.js +1272 -0
- package/dist/chunk-I64BS3VC.js +15 -0
- package/dist/chunk-IVWV2UM6.js +955 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-IYK7XGDZ.js +892 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JNNVRHMV.js +2111 -0
- package/dist/chunk-JOHXDF3D.js +295 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-K3WNSHUF.js +1371 -0
- package/dist/chunk-L25CVUVR.js +2830 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-LWKUTMD5.js +197 -0
- package/dist/chunk-MQX2KXVW.js +54 -0
- package/dist/chunk-MZZN7VCD.js +359 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NKJW5GFO.js +366 -0
- package/dist/chunk-NKNOBUBU.js +522 -0
- package/dist/chunk-O4MBV4AX.js +134 -0
- package/dist/chunk-OUBDL3WH.js +3515 -0
- package/dist/chunk-PQKK5JH7.js +101 -0
- package/dist/chunk-PY2IOS7C.js +1000 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QZQSPUBF.js +315 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-SDLVH5RZ.js +178 -0
- package/dist/chunk-T2F4S7U4.js +1723 -0
- package/dist/chunk-TEBQ4VPZ.js +163 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UCYQTZ7F.js +113 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-VFKPWSOX.js +154 -0
- package/dist/chunk-VJNHFZ7J.js +190 -0
- package/dist/chunk-VXECAJJ7.js +1091 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZJQ4IGXK.js +474 -0
- package/dist/chunk-ZLTFLC25.js +385 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/ast-invariant-engine.d.ts +5 -4
- package/dist/codebase-index/binary-frame.d.ts +0 -2
- package/dist/codebase-index/bm25.d.ts +1 -1
- package/dist/codebase-index/circuit-breaker.d.ts +2 -1
- package/dist/codebase-index/codebase-ast-replace-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +4 -3
- package/dist/codebase-index/codebase-invariant-check-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-repo-map-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-skeleton-tool.d.ts +3 -2
- package/dist/codebase-index/codebase-targeted-test-tool.d.ts +3 -2
- package/dist/codebase-index/content-hash.d.ts +0 -2
- package/dist/codebase-index/import-extractor.d.ts +0 -13
- package/dist/codebase-index/index-service.d.ts +1 -1
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/languages.d.ts +2 -1
- package/dist/codebase-index/module-roots.d.ts +3 -2
- package/dist/codebase-index/parser-batch.d.ts +0 -5
- package/dist/codebase-index/parser-dispatch.d.ts +2 -1
- package/dist/codebase-index/parser-output.d.ts +4 -3
- package/dist/codebase-index/parser-worker-pool.d.ts +0 -2
- package/dist/codebase-index/parser-worker-script.d.ts +1 -16
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server-client-state.d.ts +2 -2
- package/dist/codebase-index/project-server-endpoint.d.ts +0 -15
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
- package/dist/codebase-index/tree-sitter-parser.d.ts +0 -2
- package/dist/codebase-index/ts-parser.d.ts +1 -1
- package/dist/codebase-index/vector-search.d.ts +2 -1
- package/dist/codebase-index/wal-maintenance.d.ts +3 -2
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/codebase-index/writer-admin.d.ts +0 -1
- package/dist/codebase-index/writer-search.d.ts +0 -9
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-JXYKJHRC.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-IBHZ4BZC.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban-contract-actions.d.ts +1 -1
- package/dist/kanban-detail-actions.d.ts +2 -1
- package/dist/kanban-presence.d.ts +2 -1
- package/dist/kanban-serializer.d.ts +0 -8
- package/dist/kanban-split-task-handler.d.ts +2 -2
- package/dist/kanban-tool-types.d.ts +2 -1
- package/dist/kanban.js +3 -2096
- package/dist/languages/diagnostics.d.ts +2 -1
- package/dist/languages/index.js +23 -4664
- package/dist/languages/legacy-bridge.d.ts +3 -2
- package/dist/languages/types.d.ts +2 -1
- package/dist/legacy-bridge-7M6QLUQN.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-QH4XPGG6.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/pwsh.d.ts +0 -1
- package/dist/py-parser-IS4HKYMF.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-XHUUO2KZ.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban-sync.d.ts +0 -6
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/spawn-background.d.ts +1 -1
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-AIERW3YI.js +15 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-RCTYKSVI.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-4SAXV3PL.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/_fetch-guard.ts
|
|
2
|
+
import * as dns from "node:dns/promises";
|
|
3
|
+
import * as net from "node:net";
|
|
4
|
+
import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
5
|
+
import { FetchError, ToolValidationError } from "@wrongstack/core/types";
|
|
6
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
7
|
+
var nativeGlobalFetch = globalThis.fetch;
|
|
8
|
+
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
9
|
+
if (ALLOW_PRIVATE && !process.env["CI"]) {
|
|
10
|
+
console.warn(
|
|
11
|
+
"[WrongStack] WARNING: WRONGSTACK_FETCH_ALLOW_PRIVATE=1 is active \u2014\n fetch tool can now access private IPs (10.x, 192.168.x, 169.254.x),\n cloud metadata endpoints, and plaintext HTTP. Use only on isolated networks."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function guardedLookup(hostname, options, callback) {
|
|
15
|
+
dns.lookup(hostname, { all: true }).then((records) => {
|
|
16
|
+
const family = options?.family;
|
|
17
|
+
const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
|
|
18
|
+
const list = byFamily.length > 0 ? byFamily : records;
|
|
19
|
+
if (!ALLOW_PRIVATE) {
|
|
20
|
+
for (const r of list) {
|
|
21
|
+
const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
|
|
22
|
+
if (bad) {
|
|
23
|
+
callback(
|
|
24
|
+
Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
|
|
25
|
+
code: "EAI_FAIL"
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (options?.all) {
|
|
33
|
+
callback(
|
|
34
|
+
null,
|
|
35
|
+
list.map((r) => ({ address: r.address, family: r.family }))
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const first = list.at(0);
|
|
40
|
+
if (!first) {
|
|
41
|
+
callback(
|
|
42
|
+
Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
callback(null, first.address, first.family);
|
|
47
|
+
}).catch((err) => callback(err));
|
|
48
|
+
}
|
|
49
|
+
var pinnedAgent;
|
|
50
|
+
function getPinnedDispatcher() {
|
|
51
|
+
if (!pinnedAgent) {
|
|
52
|
+
pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
|
|
53
|
+
}
|
|
54
|
+
return pinnedAgent;
|
|
55
|
+
}
|
|
56
|
+
function dispatcherFetch() {
|
|
57
|
+
return globalThis.fetch === nativeGlobalFetch ? undiciFetch : globalThis.fetch;
|
|
58
|
+
}
|
|
59
|
+
var _beforeExitRegistered = false;
|
|
60
|
+
if (!_beforeExitRegistered) {
|
|
61
|
+
_beforeExitRegistered = true;
|
|
62
|
+
process.on("beforeExit", () => {
|
|
63
|
+
pinnedAgent?.destroy();
|
|
64
|
+
pinnedAgent = void 0;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
68
|
+
"user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
|
|
69
|
+
accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
|
|
70
|
+
}) {
|
|
71
|
+
let redirectCount = 0;
|
|
72
|
+
let currentUrl = url;
|
|
73
|
+
for (; ; ) {
|
|
74
|
+
const parsed = new URL(currentUrl);
|
|
75
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
76
|
+
throw new ToolValidationError({
|
|
77
|
+
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
78
|
+
field: "url"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
82
|
+
throw new ToolValidationError({
|
|
83
|
+
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
84
|
+
field: "url"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await assertNotPrivate(parsed.hostname);
|
|
88
|
+
const init = {
|
|
89
|
+
redirect: "manual",
|
|
90
|
+
signal,
|
|
91
|
+
headers,
|
|
92
|
+
dispatcher: getPinnedDispatcher()
|
|
93
|
+
};
|
|
94
|
+
const res = await dispatcherFetch()(currentUrl, init);
|
|
95
|
+
if (res.status < 300 || res.status > 399) {
|
|
96
|
+
return res;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await res.body?.cancel();
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
redirectCount++;
|
|
103
|
+
if (redirectCount > maxRedirects) {
|
|
104
|
+
throw new FetchError({
|
|
105
|
+
message: `fetch: exceeded ${maxRedirects} redirects`,
|
|
106
|
+
status: res.status,
|
|
107
|
+
context: { url: currentUrl, maxRedirects, redirectCount }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const location = res.headers.get("location");
|
|
111
|
+
if (!location) {
|
|
112
|
+
throw new FetchError({
|
|
113
|
+
message: "fetch: redirect status with no location header",
|
|
114
|
+
status: res.status,
|
|
115
|
+
context: { url: currentUrl, redirectCount }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function assertNotPrivate(hostname) {
|
|
122
|
+
if (ALLOW_PRIVATE) return;
|
|
123
|
+
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
124
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
125
|
+
throw new ToolValidationError({
|
|
126
|
+
message: "fetch: blocked localhost target",
|
|
127
|
+
field: "url"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const ipVersion = net.isIP(host);
|
|
131
|
+
if (ipVersion === 4) {
|
|
132
|
+
if (isPrivateIPv4(host)) {
|
|
133
|
+
throw new ToolValidationError({
|
|
134
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
135
|
+
field: "url"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
} else if (ipVersion === 6) {
|
|
139
|
+
if (isPrivateIPv6(host)) {
|
|
140
|
+
throw new ToolValidationError({
|
|
141
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
142
|
+
field: "url"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
try {
|
|
147
|
+
const records = await dns.lookup(host, { all: true });
|
|
148
|
+
for (const r of records) {
|
|
149
|
+
const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
|
|
150
|
+
if (bad) {
|
|
151
|
+
throw new ToolValidationError({
|
|
152
|
+
message: `fetch: resolved to private address ${r.address}`,
|
|
153
|
+
field: "url"
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof ToolValidationError) throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
ALLOW_PRIVATE,
|
|
165
|
+
guardedLookup,
|
|
166
|
+
guardedFetch,
|
|
167
|
+
assertNotPrivate
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=chunk-NBDZMMG2.js.map
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
todoTool
|
|
3
|
+
} from "./chunk-7EY5DCRO.js";
|
|
4
|
+
import {
|
|
5
|
+
projectSessionPlanToKanban
|
|
6
|
+
} from "./chunk-PY2IOS7C.js";
|
|
7
|
+
|
|
8
|
+
// src/plan.ts
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
addPlanItem,
|
|
12
|
+
clearPlan,
|
|
13
|
+
deriveTodosFromPlanItem,
|
|
14
|
+
formatPlan,
|
|
15
|
+
getPlanTemplate,
|
|
16
|
+
mutatePlan,
|
|
17
|
+
mutateTasks,
|
|
18
|
+
removePlanItem,
|
|
19
|
+
setPlanItemStatus
|
|
20
|
+
} from "@wrongstack/core/storage";
|
|
21
|
+
import { formatTaskList } from "@wrongstack/core/utils";
|
|
22
|
+
var planTool = {
|
|
23
|
+
name: "plan",
|
|
24
|
+
category: "Session",
|
|
25
|
+
description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
|
|
26
|
+
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 `action: "status"` with `open`, `in_progress`, or `done` for exact status changes.\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- Unfinished plan items cannot be removed or cleared; complete them first.\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
|
|
27
|
+
permission: "confirm",
|
|
28
|
+
// WS-046: gives permission decisions something to key on.
|
|
29
|
+
// The action performed; plan has no single file or path subject.
|
|
30
|
+
subjectKey: "action",
|
|
31
|
+
mutating: true,
|
|
32
|
+
capabilities: ["fs.write"],
|
|
33
|
+
icon: "plan",
|
|
34
|
+
timeoutMs: 3e4,
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
action: {
|
|
39
|
+
type: "string",
|
|
40
|
+
enum: [
|
|
41
|
+
"show",
|
|
42
|
+
"add",
|
|
43
|
+
"status",
|
|
44
|
+
"start",
|
|
45
|
+
"done",
|
|
46
|
+
"remove",
|
|
47
|
+
"promote",
|
|
48
|
+
"template_use",
|
|
49
|
+
"clear",
|
|
50
|
+
"taskify"
|
|
51
|
+
],
|
|
52
|
+
description: "The operation to perform on the plan board."
|
|
53
|
+
},
|
|
54
|
+
title: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Title of the plan item. Required for action=add."
|
|
57
|
+
},
|
|
58
|
+
details: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Additional details or description for a new plan item (action=add)."
|
|
61
|
+
},
|
|
62
|
+
target: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Identifier for the target plan item (id, 1-based index, or partial title). Required for most actions except add/show/clear."
|
|
65
|
+
},
|
|
66
|
+
status: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["open", "in_progress", "done"],
|
|
69
|
+
description: "Exact plan item status for action=status."
|
|
70
|
+
},
|
|
71
|
+
subtasks: {
|
|
72
|
+
type: "array",
|
|
73
|
+
items: { type: "string" },
|
|
74
|
+
description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
|
|
75
|
+
},
|
|
76
|
+
template: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
|
|
79
|
+
},
|
|
80
|
+
scope: {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["session", "project"],
|
|
83
|
+
description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
required: ["action"]
|
|
87
|
+
},
|
|
88
|
+
async execute(input, ctx) {
|
|
89
|
+
const sessionPlanPath = ctx.meta["plan.path"];
|
|
90
|
+
let planPath;
|
|
91
|
+
if (input.scope === "project") {
|
|
92
|
+
if (typeof sessionPlanPath === "string") {
|
|
93
|
+
const lastSep = Math.max(
|
|
94
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
95
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
96
|
+
);
|
|
97
|
+
if (lastSep < 0) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
101
|
+
plan: "",
|
|
102
|
+
count: 0,
|
|
103
|
+
open: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
planPath = sessionPlanPath;
|
|
110
|
+
}
|
|
111
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
message: "Plan storage path is not configured for this session.",
|
|
115
|
+
plan: "",
|
|
116
|
+
count: 0,
|
|
117
|
+
open: 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const sessionId = ctx.session?.id ?? "unknown";
|
|
121
|
+
let early = null;
|
|
122
|
+
const taskifyMeta = { title: "", details: "" };
|
|
123
|
+
let didTaskify = false;
|
|
124
|
+
let todosToReplace = null;
|
|
125
|
+
let plan;
|
|
126
|
+
try {
|
|
127
|
+
plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
128
|
+
switch (input.action) {
|
|
129
|
+
case "show":
|
|
130
|
+
break;
|
|
131
|
+
case "add": {
|
|
132
|
+
const title = input.title?.trim();
|
|
133
|
+
if (!title) {
|
|
134
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
135
|
+
return p;
|
|
136
|
+
}
|
|
137
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
case "start":
|
|
141
|
+
case "done": {
|
|
142
|
+
if (!input.target) {
|
|
143
|
+
early = mkResult(
|
|
144
|
+
p,
|
|
145
|
+
false,
|
|
146
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
147
|
+
);
|
|
148
|
+
return p;
|
|
149
|
+
}
|
|
150
|
+
const next = setPlanItemStatus(
|
|
151
|
+
p,
|
|
152
|
+
input.target,
|
|
153
|
+
input.action === "start" ? "in_progress" : "done"
|
|
154
|
+
);
|
|
155
|
+
if (next === p) {
|
|
156
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
157
|
+
return p;
|
|
158
|
+
}
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
case "status": {
|
|
162
|
+
if (!input.target || !input.status) {
|
|
163
|
+
early = mkResult(
|
|
164
|
+
p,
|
|
165
|
+
false,
|
|
166
|
+
"status requires `target` (id|index|substring) and `status`."
|
|
167
|
+
);
|
|
168
|
+
return p;
|
|
169
|
+
}
|
|
170
|
+
const next = setPlanItemStatus(p, input.target, input.status);
|
|
171
|
+
if (next === p) {
|
|
172
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
173
|
+
return p;
|
|
174
|
+
}
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
case "remove": {
|
|
178
|
+
if (!input.target) {
|
|
179
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
const next = removePlanItem(p, input.target);
|
|
183
|
+
if (next === p) {
|
|
184
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
185
|
+
return p;
|
|
186
|
+
}
|
|
187
|
+
const nextIds = new Set(next.items.map((item) => item.id));
|
|
188
|
+
const removed = p.items.find((item) => !nextIds.has(item.id));
|
|
189
|
+
if (removed?.status !== "done") {
|
|
190
|
+
early = mkResult(
|
|
191
|
+
p,
|
|
192
|
+
false,
|
|
193
|
+
`Plan item "${removed?.title ?? input.target}" is not done and cannot be removed. Complete it first.`
|
|
194
|
+
);
|
|
195
|
+
return p;
|
|
196
|
+
}
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
case "promote": {
|
|
200
|
+
if (!input.target) {
|
|
201
|
+
early = mkResult(
|
|
202
|
+
p,
|
|
203
|
+
false,
|
|
204
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
205
|
+
);
|
|
206
|
+
return p;
|
|
207
|
+
}
|
|
208
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
209
|
+
if (!derived) {
|
|
210
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
211
|
+
return p;
|
|
212
|
+
}
|
|
213
|
+
todosToReplace = derived.todos;
|
|
214
|
+
early = mkResult(
|
|
215
|
+
derived.plan,
|
|
216
|
+
true,
|
|
217
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
218
|
+
derived.todos
|
|
219
|
+
);
|
|
220
|
+
return derived.plan;
|
|
221
|
+
}
|
|
222
|
+
case "template_use": {
|
|
223
|
+
const templateName = input.template?.trim();
|
|
224
|
+
if (!templateName) {
|
|
225
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
226
|
+
return p;
|
|
227
|
+
}
|
|
228
|
+
const template = getPlanTemplate(templateName);
|
|
229
|
+
if (!template) {
|
|
230
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
231
|
+
return p;
|
|
232
|
+
}
|
|
233
|
+
let updated = p;
|
|
234
|
+
for (const item of template.items) {
|
|
235
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
236
|
+
}
|
|
237
|
+
early = mkResult(
|
|
238
|
+
updated,
|
|
239
|
+
true,
|
|
240
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
241
|
+
);
|
|
242
|
+
return updated;
|
|
243
|
+
}
|
|
244
|
+
case "clear":
|
|
245
|
+
if (p.items.some((item) => item.status !== "done")) {
|
|
246
|
+
early = mkResult(
|
|
247
|
+
p,
|
|
248
|
+
false,
|
|
249
|
+
"Plan contains unfinished items and cannot be cleared. Complete them first."
|
|
250
|
+
);
|
|
251
|
+
return p;
|
|
252
|
+
}
|
|
253
|
+
return clearPlan(p);
|
|
254
|
+
case "taskify": {
|
|
255
|
+
if (!input.target) {
|
|
256
|
+
early = mkResult(
|
|
257
|
+
p,
|
|
258
|
+
false,
|
|
259
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
260
|
+
);
|
|
261
|
+
return p;
|
|
262
|
+
}
|
|
263
|
+
let itemIdx = -1;
|
|
264
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
265
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
266
|
+
itemIdx = asNum - 1;
|
|
267
|
+
} else {
|
|
268
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
269
|
+
if (itemIdx === -1) {
|
|
270
|
+
const lower = input.target.toLowerCase();
|
|
271
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
275
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
276
|
+
return p;
|
|
277
|
+
}
|
|
278
|
+
const item = p.items[itemIdx];
|
|
279
|
+
taskifyMeta.title = item.title;
|
|
280
|
+
taskifyMeta.details = item.details ?? "";
|
|
281
|
+
didTaskify = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
286
|
+
return p;
|
|
287
|
+
}
|
|
288
|
+
return p;
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
294
|
+
plan: "",
|
|
295
|
+
count: 0,
|
|
296
|
+
open: 0
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
300
|
+
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
301
|
+
if (todosToReplace) {
|
|
302
|
+
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
303
|
+
signal: AbortSignal.timeout(3e4)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (early) return early;
|
|
307
|
+
if (didTaskify) {
|
|
308
|
+
const taskPathRaw = ctx.meta["task.path"];
|
|
309
|
+
if (typeof taskPathRaw !== "string" || !taskPathRaw) {
|
|
310
|
+
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
311
|
+
}
|
|
312
|
+
let taskPath = taskPathRaw;
|
|
313
|
+
if (input.scope === "project") {
|
|
314
|
+
const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
|
|
315
|
+
taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
316
|
+
}
|
|
317
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
318
|
+
try {
|
|
319
|
+
const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
|
|
320
|
+
f.tasks.push({
|
|
321
|
+
id: `task_${randomUUID()}`,
|
|
322
|
+
title: taskifyMeta.title,
|
|
323
|
+
description: taskifyMeta.details || void 0,
|
|
324
|
+
type: "feature",
|
|
325
|
+
priority: "medium",
|
|
326
|
+
status: "pending",
|
|
327
|
+
createdAt: now,
|
|
328
|
+
updatedAt: now
|
|
329
|
+
});
|
|
330
|
+
return f;
|
|
331
|
+
});
|
|
332
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
333
|
+
return mkResult(
|
|
334
|
+
plan,
|
|
335
|
+
true,
|
|
336
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
337
|
+
${formatTaskList(taskFile.tasks)}`
|
|
338
|
+
);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return mkResult(
|
|
341
|
+
plan,
|
|
342
|
+
false,
|
|
343
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function mkResult(plan, ok, message, todos) {
|
|
351
|
+
const open = plan.items.filter((i) => i.status !== "done").length;
|
|
352
|
+
const result = {
|
|
353
|
+
ok,
|
|
354
|
+
message,
|
|
355
|
+
plan: formatPlan(plan),
|
|
356
|
+
count: plan.items.length,
|
|
357
|
+
open
|
|
358
|
+
};
|
|
359
|
+
if (todos !== void 0) result.todos = todos;
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
planTool
|
|
365
|
+
};
|
|
366
|
+
//# sourceMappingURL=chunk-NKJW5GFO.js.map
|