@wrongstack/tools 0.310.1 → 0.313.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/_shell-pick.d.ts +5 -10
- package/dist/audit.js +10 -4055
- package/dist/bash.js +9 -2332
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33985
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- 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-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35423
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.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 -33933
- package/dist/parser-dispatch-VPOFDO2U.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/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- 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 -34020
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.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-GFAOKV53.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALLOW_PRIVATE,
|
|
3
|
+
assertNotPrivate,
|
|
4
|
+
guardedFetch
|
|
5
|
+
} from "./chunk-NBDZMMG2.js";
|
|
6
|
+
import {
|
|
7
|
+
truncateMiddle
|
|
8
|
+
} from "./chunk-D33S4RQG.js";
|
|
9
|
+
|
|
10
|
+
// src/fetch.ts
|
|
11
|
+
import { FetchError, ToolError, ToolValidationError } from "@wrongstack/core/types";
|
|
12
|
+
import TurndownService from "turndown";
|
|
13
|
+
var TD = new TurndownService({
|
|
14
|
+
// Use `# Title` for headings, not setext underline style (`Title\n=====`).
|
|
15
|
+
headingStyle: "atx",
|
|
16
|
+
// Don't wrap code blocks in <pre> — render them as triple-backtick blocks.
|
|
17
|
+
codeBlockStyle: "fenced"
|
|
18
|
+
});
|
|
19
|
+
TD.addRule("stripDangerousElements", {
|
|
20
|
+
filter: ["script", "style", "noscript"],
|
|
21
|
+
replacement: () => ""
|
|
22
|
+
});
|
|
23
|
+
var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
|
|
24
|
+
TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
|
|
25
|
+
var MAX_BYTES = 131072;
|
|
26
|
+
var TIMEOUT_MS = 2e4;
|
|
27
|
+
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
28
|
+
var fetchTool = {
|
|
29
|
+
name: "fetch",
|
|
30
|
+
category: "Network",
|
|
31
|
+
description: "Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).",
|
|
32
|
+
usageHint: "Use this when you need external information (documentation, API responses, web pages, etc.).\n\nSecurity notes:\n- Only HTTPS is allowed by default.\n- Internal/private networks are blocked unless explicitly enabled via environment variable.\n- Redirects are followed but re-validated at each hop.\n- Output is capped (128KB by default) to avoid flooding context.\nPrefer this over raw `bash curl` or `bash wget`.",
|
|
33
|
+
permission: "auto",
|
|
34
|
+
mutating: false,
|
|
35
|
+
capabilities: ["net.outbound"],
|
|
36
|
+
icon: "web",
|
|
37
|
+
// Trust rules for fetch match on the literal URL — declare it explicitly
|
|
38
|
+
// so a user can trust `https://api.example.com/*` without accidentally
|
|
39
|
+
// matching that pattern on any other tool that happens to have a `url`
|
|
40
|
+
// input field.
|
|
41
|
+
subjectKey: "url",
|
|
42
|
+
timeoutMs: TIMEOUT_MS,
|
|
43
|
+
maxOutputBytes: MAX_BYTES,
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
url: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "The target URL (must use https://)."
|
|
50
|
+
},
|
|
51
|
+
format: {
|
|
52
|
+
type: "string",
|
|
53
|
+
enum: ["markdown", "text", "raw"],
|
|
54
|
+
description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
required: ["url"]
|
|
58
|
+
},
|
|
59
|
+
async execute(input, ctx, opts) {
|
|
60
|
+
let final;
|
|
61
|
+
const executeStream = fetchTool.executeStream;
|
|
62
|
+
if (!executeStream) {
|
|
63
|
+
throw new ToolError({
|
|
64
|
+
message: "fetchTool: stream execution unavailable",
|
|
65
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
66
|
+
toolName: "fetch"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
for await (const ev of executeStream(input, ctx, opts)) {
|
|
70
|
+
if (ev.type === "final") final = ev.output;
|
|
71
|
+
}
|
|
72
|
+
if (!final) {
|
|
73
|
+
throw new ToolError({
|
|
74
|
+
message: "fetch: stream ended without final event",
|
|
75
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
76
|
+
toolName: "fetch"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return final;
|
|
80
|
+
},
|
|
81
|
+
async *executeStream(input, ctx, opts) {
|
|
82
|
+
if (!input?.url) {
|
|
83
|
+
throw new ToolValidationError({
|
|
84
|
+
message: "fetch: url is required",
|
|
85
|
+
field: "url"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const u = new URL(input.url);
|
|
89
|
+
if (u.username || u.password) {
|
|
90
|
+
throw new ToolValidationError({
|
|
91
|
+
message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
|
|
92
|
+
field: "url"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
96
|
+
throw new ToolValidationError({
|
|
97
|
+
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
98
|
+
field: "url"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
102
|
+
throw new ToolValidationError({
|
|
103
|
+
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
104
|
+
field: "url"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
await assertNotPrivate(u.hostname);
|
|
108
|
+
yield { type: "log", text: `GET ${input.url}` };
|
|
109
|
+
const ctrl = new AbortController();
|
|
110
|
+
const timer = setTimeout(
|
|
111
|
+
() => ctrl.abort(
|
|
112
|
+
new ToolError({
|
|
113
|
+
message: "fetch timeout",
|
|
114
|
+
code: "TOOL_TIMEOUT",
|
|
115
|
+
toolName: "fetch"
|
|
116
|
+
})
|
|
117
|
+
),
|
|
118
|
+
TIMEOUT_MS
|
|
119
|
+
);
|
|
120
|
+
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
121
|
+
try {
|
|
122
|
+
let res;
|
|
123
|
+
try {
|
|
124
|
+
res = await guardedFetch(input.url, 5, combined);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (opts.signal.aborted) throw err;
|
|
127
|
+
throw describeFetchError(err, input.url, ctrl.signal.aborted);
|
|
128
|
+
}
|
|
129
|
+
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
130
|
+
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
131
|
+
throw new FetchError({
|
|
132
|
+
message: `fetch: refusing to read binary content-type "${ct}"`,
|
|
133
|
+
status: res.status,
|
|
134
|
+
context: { url: res.url, contentType: ct }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
yield {
|
|
138
|
+
type: "log",
|
|
139
|
+
text: `HTTP ${res.status} ${ct}`,
|
|
140
|
+
data: { status: res.status, contentType: ct }
|
|
141
|
+
};
|
|
142
|
+
const reader = res.body?.getReader();
|
|
143
|
+
let received = 0;
|
|
144
|
+
const chunks = [];
|
|
145
|
+
let pendingBytes = 0;
|
|
146
|
+
const FLUSH_AT = 4 * 1024;
|
|
147
|
+
if (reader) {
|
|
148
|
+
for (; ; ) {
|
|
149
|
+
const { value, done } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
if (!value) continue;
|
|
152
|
+
received += value.byteLength;
|
|
153
|
+
pendingBytes += value.byteLength;
|
|
154
|
+
chunks.push(value);
|
|
155
|
+
if (pendingBytes >= FLUSH_AT) {
|
|
156
|
+
const recent = Buffer.from(value).toString("utf-8");
|
|
157
|
+
yield {
|
|
158
|
+
type: "partial_output",
|
|
159
|
+
text: recent,
|
|
160
|
+
data: { received }
|
|
161
|
+
};
|
|
162
|
+
pendingBytes = 0;
|
|
163
|
+
}
|
|
164
|
+
if (received > MAX_BYTES) break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
|
|
168
|
+
const format = input.format ?? (ct.includes("text/html") ? "markdown" : "text");
|
|
169
|
+
let content;
|
|
170
|
+
if (format === "raw") content = text;
|
|
171
|
+
else if (format === "markdown" && ct.includes("text/html")) content = TD.turndown(text);
|
|
172
|
+
else if (ct.includes("application/json")) content = prettyJson(text);
|
|
173
|
+
else content = text;
|
|
174
|
+
yield {
|
|
175
|
+
type: "final",
|
|
176
|
+
output: {
|
|
177
|
+
content: truncateMiddle(content, MAX_BYTES),
|
|
178
|
+
status: res.status,
|
|
179
|
+
content_type: ct,
|
|
180
|
+
url: res.url
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
ctx.recordSideEffect?.({
|
|
184
|
+
toolUseId: `fetch-${Date.now()}`,
|
|
185
|
+
toolName: "fetch",
|
|
186
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
187
|
+
input: { url: input.url, format: input.format },
|
|
188
|
+
outcome: `HTTP ${res.status} (${ct})`,
|
|
189
|
+
risk: "network"
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function describeFetchError(err, url, timedOut) {
|
|
197
|
+
if (timedOut) {
|
|
198
|
+
return new ToolError({
|
|
199
|
+
message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,
|
|
200
|
+
code: "TOOL_TIMEOUT",
|
|
201
|
+
toolName: "fetch",
|
|
202
|
+
context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },
|
|
203
|
+
cause: err
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const parts = [];
|
|
207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
208
|
+
let cur = err;
|
|
209
|
+
while (cur instanceof Error && !seen.has(cur)) {
|
|
210
|
+
seen.add(cur);
|
|
211
|
+
const code = cur.code;
|
|
212
|
+
const label = code ? `${code}: ${cur.message}` : cur.message;
|
|
213
|
+
if (label && label !== "fetch failed" && !parts.includes(label)) parts.push(label);
|
|
214
|
+
cur = cur.cause;
|
|
215
|
+
}
|
|
216
|
+
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
217
|
+
return new FetchError({
|
|
218
|
+
message: `fetch: GET ${url} failed \u2014 ${detail}`,
|
|
219
|
+
status: 502,
|
|
220
|
+
context: { url, timedOut: false, transportErrors: parts },
|
|
221
|
+
// Preserve the original undici / DNS / TLS chain so callers can inspect
|
|
222
|
+
// it via `err.cause` and structured `instanceof` checks. The flattened
|
|
223
|
+
// text version stays in the message for human readability.
|
|
224
|
+
cause: err
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function prettyJson(s) {
|
|
228
|
+
try {
|
|
229
|
+
return JSON.stringify(JSON.parse(s), null, 2);
|
|
230
|
+
} catch {
|
|
231
|
+
return s;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
fetchTool
|
|
237
|
+
};
|
|
238
|
+
//# sourceMappingURL=chunk-IXMNTNW5.js.map
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kanbanTool
|
|
3
|
+
} from "./chunk-55KF77OK.js";
|
|
4
|
+
import {
|
|
5
|
+
applyManagedKanbanBoardToTodos,
|
|
6
|
+
blockingTitles,
|
|
7
|
+
mirrorSessionTodosToKanban,
|
|
8
|
+
takeSessionMirrorFailure
|
|
9
|
+
} from "./chunk-QNJ7AQ4B.js";
|
|
10
|
+
|
|
11
|
+
// src/todo.ts
|
|
12
|
+
import {
|
|
13
|
+
loadPlan,
|
|
14
|
+
loadTasks,
|
|
15
|
+
savePlan,
|
|
16
|
+
saveTasks,
|
|
17
|
+
setPlanItemStatus
|
|
18
|
+
} from "@wrongstack/core/storage";
|
|
19
|
+
import { addTask, getBoard } from "@wrongstack/kanban";
|
|
20
|
+
function normalizedTitle(value) {
|
|
21
|
+
return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
|
|
22
|
+
}
|
|
23
|
+
function activeBoardId(items, ctx) {
|
|
24
|
+
const metaKanban = ctx.meta?.["kanban"];
|
|
25
|
+
const metaBoardId = metaKanban && typeof metaKanban === "object" ? metaKanban["boardId"] : void 0;
|
|
26
|
+
return ctx.currentKanbanBoardId ?? (typeof metaBoardId === "string" ? metaBoardId : void 0) ?? items.find((item) => item.kanbanBoardId)?.kanbanBoardId ?? "";
|
|
27
|
+
}
|
|
28
|
+
function bindTodosToBoard(items, previous, board) {
|
|
29
|
+
const previousById = new Map(previous.map((item) => [item.id, item]));
|
|
30
|
+
const available = board.tasks.filter(
|
|
31
|
+
(task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
|
|
32
|
+
).sort(
|
|
33
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.order - right.order
|
|
34
|
+
);
|
|
35
|
+
const used = /* @__PURE__ */ new Set();
|
|
36
|
+
return items.map((item) => {
|
|
37
|
+
const previousItem = previousById.get(item.id);
|
|
38
|
+
const requestedTaskId = item.kanbanBoardId === board.id ? item.kanbanTaskId : previousItem?.kanbanBoardId === board.id ? previousItem.kanbanTaskId : void 0;
|
|
39
|
+
const title = normalizedTitle(item.content);
|
|
40
|
+
const candidates = [
|
|
41
|
+
requestedTaskId ? board.tasks.find((task2) => task2.id === requestedTaskId) : void 0,
|
|
42
|
+
board.tasks.find((task2) => task2.id === item.id),
|
|
43
|
+
board.tasks.find((task2) => task2.origin?.taskId === item.id),
|
|
44
|
+
available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
|
|
45
|
+
];
|
|
46
|
+
const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
|
|
47
|
+
if (!task) {
|
|
48
|
+
const { blockedBy: _discarded, ...rest } = item;
|
|
49
|
+
return { ...rest };
|
|
50
|
+
}
|
|
51
|
+
used.add(task.id);
|
|
52
|
+
const blockedBy = blockingTitles(board, task);
|
|
53
|
+
return {
|
|
54
|
+
...item,
|
|
55
|
+
kanbanBoardId: board.id,
|
|
56
|
+
kanbanTaskId: task.id,
|
|
57
|
+
...blockedBy.length ? { blockedBy } : { blockedBy: void 0 }
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function demoteBlockedInProgress(items, warnings) {
|
|
62
|
+
return items.map((item) => {
|
|
63
|
+
if (item.status !== "in_progress" || !item.blockedBy?.length) return item;
|
|
64
|
+
warnings.push(
|
|
65
|
+
`"${item.content}" cannot start yet \u2014 it waits on: ${item.blockedBy.join("; ")}. Kept as pending; complete the blocking work first.`
|
|
66
|
+
);
|
|
67
|
+
return { ...item, status: "pending" };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function createMissingManagedCards(items, board, ctx, warnings) {
|
|
71
|
+
const created = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (item.kanbanBoardId === board.id && item.kanbanTaskId) continue;
|
|
74
|
+
try {
|
|
75
|
+
const result = await addTask(ctx.projectRoot, board.id, {
|
|
76
|
+
title: item.content,
|
|
77
|
+
description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
|
|
78
|
+
});
|
|
79
|
+
if (!result) {
|
|
80
|
+
warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
created.set(item.id, result.task.id);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
warnings.push(
|
|
86
|
+
`Could not open a Kanban card for "${item.content}": ${error instanceof Error ? error.message : String(error)}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return created;
|
|
91
|
+
}
|
|
92
|
+
async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
93
|
+
let synced = 0;
|
|
94
|
+
const warnings = [];
|
|
95
|
+
const actor = ctx.agentId?.trim() || ctx.agentName?.trim() || "kanban-agent";
|
|
96
|
+
const execute = async (input) => {
|
|
97
|
+
const result = await kanbanTool.execute(input, ctx, { signal });
|
|
98
|
+
if (!result.ok) warnings.push(result.message);
|
|
99
|
+
else {
|
|
100
|
+
synced++;
|
|
101
|
+
if (result.message.includes("Warning:")) warnings.push(result.message);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
if (item.status !== "pending" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
110
|
+
if (task?.lifecycle?.currentStage !== "running") continue;
|
|
111
|
+
const released = await execute({
|
|
112
|
+
action: "mark_assignment",
|
|
113
|
+
boardId: board.id,
|
|
114
|
+
taskId: task.id,
|
|
115
|
+
assignmentStatus: "assigned",
|
|
116
|
+
agentId: actor,
|
|
117
|
+
lastResult: `Todo returned to queue: ${item.content}`
|
|
118
|
+
});
|
|
119
|
+
if (!released.ok) continue;
|
|
120
|
+
await execute({
|
|
121
|
+
action: "transition_task",
|
|
122
|
+
boardId: board.id,
|
|
123
|
+
taskId: task.id,
|
|
124
|
+
lifecycleStage: "todo",
|
|
125
|
+
author: actor,
|
|
126
|
+
transitionComment: `Todo returned to queue: ${item.content}`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
for (const item of items) {
|
|
130
|
+
if (item.status === "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
134
|
+
if (task?.status !== "completed") continue;
|
|
135
|
+
warnings.push(
|
|
136
|
+
`"${item.content}" is already Done on the Kanban board and a completed card cannot be reopened; the row stays completed. Create a follow-up card for any remaining work.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
if (item.status !== "completed" || item.kanbanBoardId !== board.id || !item.kanbanTaskId) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
144
|
+
if (!task || task.status === "completed") continue;
|
|
145
|
+
const stage = task.lifecycle?.currentStage;
|
|
146
|
+
if (stage === "backlog" || stage === "todo") {
|
|
147
|
+
const started = await execute({
|
|
148
|
+
action: "start_task",
|
|
149
|
+
boardId: board.id,
|
|
150
|
+
taskId: task.id,
|
|
151
|
+
author: actor,
|
|
152
|
+
agentId: actor,
|
|
153
|
+
transitionComment: `Auto-started for completion: ${item.content}`
|
|
154
|
+
});
|
|
155
|
+
if (!started.ok) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await execute({
|
|
160
|
+
action: "mark_assignment",
|
|
161
|
+
boardId: board.id,
|
|
162
|
+
taskId: task.id,
|
|
163
|
+
assignmentStatus: "completed",
|
|
164
|
+
agentId: actor,
|
|
165
|
+
lastResult: `Todo completed: ${item.content}`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const attemptedParents = /* @__PURE__ */ new Set();
|
|
169
|
+
let afterCompletions = await getBoard(ctx.projectRoot, board.id);
|
|
170
|
+
while (afterCompletions) {
|
|
171
|
+
const parent = afterCompletions.tasks.find(
|
|
172
|
+
(task) => task.atomic === true && task.status !== "completed" && Boolean(task.childTaskIds?.length) && !attemptedParents.has(task.id) && task.childTaskIds?.every(
|
|
173
|
+
(childId) => afterCompletions?.tasks.find((candidate) => candidate.id === childId)?.status === "completed"
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
if (!parent) break;
|
|
177
|
+
attemptedParents.add(parent.id);
|
|
178
|
+
const started = await execute({
|
|
179
|
+
action: "start_task",
|
|
180
|
+
boardId: board.id,
|
|
181
|
+
taskId: parent.id,
|
|
182
|
+
author: actor,
|
|
183
|
+
agentId: actor,
|
|
184
|
+
transitionComment: "All child tasks completed; validating composite parent."
|
|
185
|
+
});
|
|
186
|
+
if (started.ok) {
|
|
187
|
+
await execute({
|
|
188
|
+
action: "mark_assignment",
|
|
189
|
+
boardId: board.id,
|
|
190
|
+
taskId: parent.id,
|
|
191
|
+
assignmentStatus: "completed",
|
|
192
|
+
agentId: actor,
|
|
193
|
+
lastResult: "All child tasks completed; composite result ready for verification."
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
afterCompletions = await getBoard(ctx.projectRoot, board.id);
|
|
197
|
+
}
|
|
198
|
+
const completionPending = items.some(
|
|
199
|
+
(item) => item.status === "completed" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId) && afterCompletions?.tasks.find((task) => task.id === item.kanbanTaskId)?.status !== "completed"
|
|
200
|
+
);
|
|
201
|
+
const active = items.find(
|
|
202
|
+
(item) => item.status === "in_progress" && item.kanbanBoardId === board.id && Boolean(item.kanbanTaskId)
|
|
203
|
+
);
|
|
204
|
+
const activeStage = active?.kanbanTaskId ? afterCompletions?.tasks.find((task) => task.id === active.kanbanTaskId)?.lifecycle?.currentStage : void 0;
|
|
205
|
+
if (active?.kanbanTaskId) {
|
|
206
|
+
if (activeStage === "review" || activeStage === "done") {
|
|
207
|
+
warnings.push(
|
|
208
|
+
`"${active.content}" is in ${activeStage === "review" ? "Review" : "Done"} awaiting acceptance; not re-activating it from the todo list. ` + (activeStage === "review" ? "Call kanban start_task explicitly to reopen it as a repair." : "Done is terminal; reopen only by creating a follow-up card.")
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
await execute({
|
|
212
|
+
action: "start_task",
|
|
213
|
+
boardId: board.id,
|
|
214
|
+
taskId: active.kanbanTaskId,
|
|
215
|
+
author: actor,
|
|
216
|
+
agentId: actor,
|
|
217
|
+
transitionComment: `Todo activated: ${active.content}`
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (active?.kanbanTaskId && activeStage === "review") {
|
|
222
|
+
} else if (active?.kanbanTaskId && completionPending) {
|
|
223
|
+
warnings.push(
|
|
224
|
+
"A completed todo is still awaiting acceptance; the next independent Kanban task was started."
|
|
225
|
+
);
|
|
226
|
+
} else if (!active && ctx.currentKanbanBoardId === board.id) {
|
|
227
|
+
ctx.setCurrentKanbanTask(void 0, board.id);
|
|
228
|
+
}
|
|
229
|
+
return { synced, warnings };
|
|
230
|
+
}
|
|
231
|
+
var todoTool = {
|
|
232
|
+
name: "todo",
|
|
233
|
+
category: "Session",
|
|
234
|
+
description: "Manage the compact active-work list. With a managed Kanban binding, every row resolves to a real card and status changes advance that card; without Kanban it remains session-only state. Each call replaces ordering and supplied fields, but unfinished omitted rows are retained until completed.",
|
|
235
|
+
usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- **Re-order items** to reflect current priorities. Omission is not cancellation: unfinished rows are retained until completed.\n- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
|
|
236
|
+
permission: "confirm",
|
|
237
|
+
mutating: true,
|
|
238
|
+
timeoutMs: 3e4,
|
|
239
|
+
capabilities: ["session.todo", "fs.write"],
|
|
240
|
+
subjectKey: "todos",
|
|
241
|
+
icon: "todo",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
todos: {
|
|
246
|
+
type: "array",
|
|
247
|
+
items: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
id: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: 'Unique identifier for the todo item (e.g. "1", "auth-flow").'
|
|
253
|
+
},
|
|
254
|
+
content: {
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "Clear, actionable description of the task."
|
|
257
|
+
},
|
|
258
|
+
status: {
|
|
259
|
+
type: "string",
|
|
260
|
+
enum: ["pending", "in_progress", "completed"],
|
|
261
|
+
description: 'Current status. Only one item should be "in_progress" at a time.'
|
|
262
|
+
},
|
|
263
|
+
activeForm: {
|
|
264
|
+
type: "string",
|
|
265
|
+
description: 'Optional present-tense form shown while the task is active (e.g. "Fixing auth bug").'
|
|
266
|
+
},
|
|
267
|
+
kanbanBoardId: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Kanban board that owns this UI row when Kanban is active."
|
|
270
|
+
},
|
|
271
|
+
kanbanTaskId: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "Real Kanban task represented by this UI row."
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
required: ["id", "content", "status"]
|
|
277
|
+
},
|
|
278
|
+
description: "The desired todo list. Supplied rows are replaced/reordered; unfinished omitted rows are retained."
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
required: ["todos"]
|
|
282
|
+
},
|
|
283
|
+
async execute(input, ctx, call) {
|
|
284
|
+
if (!Array.isArray(input?.todos)) {
|
|
285
|
+
throw new Error("todo: todos must be an array");
|
|
286
|
+
}
|
|
287
|
+
const items = input.todos.filter((t) => Boolean(t?.id && t.content));
|
|
288
|
+
const todoIdentity = (item) => item.kanbanBoardId && item.kanbanTaskId ? `kanban:${item.kanbanBoardId}:${item.kanbanTaskId}` : item.promotedFromTask ? `task:${item.promotedFromTask}` : item.promotedFromPlan ? `plan:${item.promotedFromPlan}` : `todo:${item.id}`;
|
|
289
|
+
const requestedIdentities = new Set(items.map(todoIdentity));
|
|
290
|
+
for (const previous of ctx.todos ?? []) {
|
|
291
|
+
const identity = todoIdentity(previous);
|
|
292
|
+
if (previous.status === "completed" || requestedIdentities.has(identity)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
items.push({ ...previous });
|
|
296
|
+
requestedIdentities.add(identity);
|
|
297
|
+
}
|
|
298
|
+
const inProgress = items.filter((t) => t.status === "in_progress");
|
|
299
|
+
if (inProgress.length > 1) {
|
|
300
|
+
let seenInProgress = false;
|
|
301
|
+
for (const item of items) {
|
|
302
|
+
if (item.status === "in_progress") {
|
|
303
|
+
if (seenInProgress) item.status = "pending";
|
|
304
|
+
seenInProgress = true;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const boardId = activeBoardId(items, ctx);
|
|
309
|
+
let board = boardId ? await getBoard(ctx.projectRoot, boardId) : null;
|
|
310
|
+
const managed = board?.lifecycle?.mode === "managed";
|
|
311
|
+
let boundItems = managed && board ? bindTodosToBoard(items, ctx.todos ?? [], board) : items;
|
|
312
|
+
const creationWarnings = [];
|
|
313
|
+
if (managed && board) {
|
|
314
|
+
const managedBoardId = board.id;
|
|
315
|
+
const created = await createMissingManagedCards(boundItems, board, ctx, creationWarnings);
|
|
316
|
+
if (created.size > 0) {
|
|
317
|
+
boundItems = boundItems.map((item) => {
|
|
318
|
+
const taskId = created.get(item.id);
|
|
319
|
+
return taskId ? { ...item, kanbanBoardId: managedBoardId, kanbanTaskId: taskId } : item;
|
|
320
|
+
});
|
|
321
|
+
board = await getBoard(ctx.projectRoot, managedBoardId) ?? board;
|
|
322
|
+
boundItems = bindTodosToBoard(boundItems, ctx.todos ?? [], board);
|
|
323
|
+
}
|
|
324
|
+
boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
|
|
325
|
+
}
|
|
326
|
+
ctx.state.replaceTodos(boundItems);
|
|
327
|
+
const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
|
|
328
|
+
kanbanSync.warnings.unshift(...creationWarnings);
|
|
329
|
+
if (managed && board) {
|
|
330
|
+
const unresolved = boundItems.filter(
|
|
331
|
+
(item) => item.kanbanBoardId !== board.id || !item.kanbanTaskId
|
|
332
|
+
);
|
|
333
|
+
if (unresolved.length > 0) {
|
|
334
|
+
kanbanSync.warnings.push(
|
|
335
|
+
`${unresolved.length} Todo row(s) could not be bound to a Kanban task and were not applied. Preserve kanbanBoardId/kanbanTaskId when updating the projection.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const mirrorFailure = takeSessionMirrorFailure(ctx.projectRoot, ctx.session?.id ?? "");
|
|
340
|
+
if (mirrorFailure) kanbanSync.warnings.push(mirrorFailure);
|
|
341
|
+
let projectedBoard = board;
|
|
342
|
+
if (managed && board) {
|
|
343
|
+
const refreshed = await getBoard(ctx.projectRoot, board.id);
|
|
344
|
+
if (refreshed) {
|
|
345
|
+
projectedBoard = refreshed;
|
|
346
|
+
applyManagedKanbanBoardToTodos(ctx, refreshed);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!managed) {
|
|
350
|
+
mirrorSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
351
|
+
}
|
|
352
|
+
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
353
|
+
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
354
|
+
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
355
|
+
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const item of items) {
|
|
357
|
+
if (item.promotedFromPlan) {
|
|
358
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
359
|
+
item.promotedFromPlan
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (item.promotedFromTask) {
|
|
363
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
364
|
+
item.promotedFromTask
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const planId of completedPlanIds) {
|
|
369
|
+
if (pendingPlanIds.has(planId)) continue;
|
|
370
|
+
const meta = ctx.meta;
|
|
371
|
+
const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
|
|
372
|
+
if (typeof planPath !== "string" || !planPath) continue;
|
|
373
|
+
try {
|
|
374
|
+
const plan = await loadPlan(planPath);
|
|
375
|
+
if (plan) {
|
|
376
|
+
const updated = setPlanItemStatus(plan, planId, "done");
|
|
377
|
+
await savePlan(planPath, updated);
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const taskId of completedTaskIds) {
|
|
383
|
+
if (pendingTaskIds.has(taskId)) continue;
|
|
384
|
+
const meta = ctx.meta;
|
|
385
|
+
const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
|
|
386
|
+
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
387
|
+
try {
|
|
388
|
+
const file = await loadTasks(taskPath);
|
|
389
|
+
if (file) {
|
|
390
|
+
const task = file.tasks.find((t) => t.id === taskId);
|
|
391
|
+
if (task && task.status !== "completed") {
|
|
392
|
+
task.status = "completed";
|
|
393
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
394
|
+
await saveTasks(taskPath, file);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
count: items.length,
|
|
402
|
+
in_progress: (ctx.todos ?? boundItems).filter((t) => t.status === "in_progress").length,
|
|
403
|
+
...kanbanSync.synced > 0 ? { kanban_synced: kanbanSync.synced } : {},
|
|
404
|
+
...kanbanSync.warnings.length > 0 ? { kanban_warnings: kanbanSync.warnings } : {},
|
|
405
|
+
...projectedBoard?.lifecycle?.mode === "managed" ? {
|
|
406
|
+
kanban_bindings: boundItems.flatMap((item) => {
|
|
407
|
+
if (item.kanbanBoardId !== projectedBoard.id || !item.kanbanTaskId) return [];
|
|
408
|
+
const task = projectedBoard.tasks.find(
|
|
409
|
+
(candidate) => candidate.id === item.kanbanTaskId
|
|
410
|
+
);
|
|
411
|
+
return task ? [
|
|
412
|
+
{
|
|
413
|
+
todoId: item.id,
|
|
414
|
+
boardId: projectedBoard.id,
|
|
415
|
+
taskId: task.id,
|
|
416
|
+
taskStatus: task.status
|
|
417
|
+
}
|
|
418
|
+
] : [];
|
|
419
|
+
})
|
|
420
|
+
} : {}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export {
|
|
426
|
+
todoTool
|
|
427
|
+
};
|
|
428
|
+
//# sourceMappingURL=chunk-J4SRJKBW.js.map
|