@wrongstack/tools 0.319.0 → 0.320.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/_edit-match.d.ts +1 -3
  2. package/dist/_util.d.ts +7 -0
  3. package/dist/audit.js +8 -8
  4. package/dist/bash.js +6 -6
  5. package/dist/browser/index.js +1 -1
  6. package/dist/builtin.d.ts +10 -0
  7. package/dist/builtin.js +54 -52
  8. package/dist/{chunk-65EMYN5G.js → chunk-23EX5WDO.js} +6 -3
  9. package/dist/{chunk-2472NT76.js → chunk-2OXHS3KZ.js} +17 -5
  10. package/dist/{chunk-7WV3D4S3.js → chunk-34VLZ35Z.js} +75 -36
  11. package/dist/{chunk-MYRMD6PZ.js → chunk-3GIKYHLA.js} +32 -8
  12. package/dist/{chunk-X5K3U6GI.js → chunk-3M7LLN7W.js} +71 -7
  13. package/dist/{chunk-VALQFAY2.js → chunk-4BABEJC2.js} +158 -45
  14. package/dist/{chunk-42BHI7TN.js → chunk-4CQWNZ62.js} +2 -2
  15. package/dist/{chunk-WNUJR3H4.js → chunk-5UFVCIUB.js} +40 -16
  16. package/dist/{chunk-UMXVT7ST.js → chunk-6AAOT4PW.js} +58 -10
  17. package/dist/{chunk-H6AWAZ2Y.js → chunk-B4HTSV6Z.js} +57 -6
  18. package/dist/{chunk-5ZYEGP2P.js → chunk-BK732J7I.js} +72 -27
  19. package/dist/{chunk-KD5EHYA2.js → chunk-C2DQKVLO.js} +61 -28
  20. package/dist/{chunk-Z4VBTVUM.js → chunk-DFQDJT22.js} +99 -12
  21. package/dist/{chunk-6RNIFLG4.js → chunk-ELRRTWNP.js} +38 -6
  22. package/dist/{chunk-3PVYQFRQ.js → chunk-ESLPBT5A.js} +2 -1
  23. package/dist/{chunk-YZHK4TLF.js → chunk-FQNXYA6W.js} +45 -6
  24. package/dist/{chunk-OYVPBPCL.js → chunk-HR67OWEP.js} +72 -33
  25. package/dist/{chunk-ZDUGI5BC.js → chunk-I7FBZOEP.js} +2 -2
  26. package/dist/{chunk-HBM4QNXU.js → chunk-IU4DV2PA.js} +18 -8
  27. package/dist/{chunk-TFMVHYJI.js → chunk-J43O3BOX.js} +9 -15
  28. package/dist/{chunk-ODFVG54U.js → chunk-J74TMK4M.js} +37 -13
  29. package/dist/{chunk-COUNXG4X.js → chunk-JLYA4X7K.js} +75 -8
  30. package/dist/{chunk-FQZOHLG3.js → chunk-JSNSMG5R.js} +3 -2
  31. package/dist/{chunk-SYXW3Y5Z.js → chunk-JVRLTEO7.js} +2 -2
  32. package/dist/{chunk-V43EQ6VR.js → chunk-KE7A76VC.js} +3 -2
  33. package/dist/{chunk-ZKSBHXVB.js → chunk-KWXIKGE3.js} +64 -15
  34. package/dist/{chunk-RK47IWO3.js → chunk-LRDIRJJL.js} +62 -30
  35. package/dist/{chunk-UC3RKRIV.js → chunk-NDIT3SBZ.js} +3 -3
  36. package/dist/{chunk-C3W3V54D.js → chunk-NSPNFOJ7.js} +57 -27
  37. package/dist/{chunk-TX3R5NVV.js → chunk-NU2IZ72J.js} +7 -7
  38. package/dist/{chunk-WHIIIREL.js → chunk-NYRLWDT5.js} +7 -3
  39. package/dist/{chunk-PMHVSHMU.js → chunk-OTPTQBYE.js} +80 -19
  40. package/dist/{chunk-ZHH3BEWU.js → chunk-OYP27E3I.js} +4 -4
  41. package/dist/{chunk-W4GAAI3M.js → chunk-PV6HIZQD.js} +2 -2
  42. package/dist/{chunk-NKNOBUBU.js → chunk-QGF2C7TF.js} +2 -2
  43. package/dist/{chunk-PQTONQ4P.js → chunk-QPNYDH5B.js} +19 -5
  44. package/dist/{chunk-VFF3Q5Z3.js → chunk-QSNVDHOZ.js} +37 -9
  45. package/dist/{chunk-45JPRMC2.js → chunk-QWKSKFKZ.js} +7 -3
  46. package/dist/{chunk-2VSZC6QM.js → chunk-SCMRM6NL.js} +6 -4
  47. package/dist/{chunk-RJZQWEMS.js → chunk-T77XOX7E.js} +3 -2
  48. package/dist/{chunk-QZDOQRF7.js → chunk-UZFY3CKD.js} +35 -14
  49. package/dist/{chunk-D4CXUZIF.js → chunk-VD2LV5WO.js} +82 -13
  50. package/dist/{chunk-DPKTXJQS.js → chunk-VPALDFYM.js} +2 -2
  51. package/dist/{chunk-IIF5HIVF.js → chunk-VVZPQPTY.js} +2 -2
  52. package/dist/{chunk-HWDB2G7J.js → chunk-W232EFEM.js} +49 -36
  53. package/dist/{chunk-FJEQZHZN.js → chunk-W2CECGTG.js} +24 -14
  54. package/dist/{chunk-4Q7LFU4T.js → chunk-WAQASTXB.js} +20 -9
  55. package/dist/{chunk-CWALNI6K.js → chunk-X6KXNHUO.js} +1 -1
  56. package/dist/{chunk-OCUXN6Y3.js → chunk-XRNN44QY.js} +1 -1
  57. package/dist/{chunk-N5O3FK5Q.js → chunk-XSSFBS5I.js} +70 -29
  58. package/dist/{chunk-Y7QXSE7N.js → chunk-YTKSNTT4.js} +44 -10
  59. package/dist/{chunk-MRKWZCHZ.js → chunk-Z5Q3TXRY.js} +3 -3
  60. package/dist/{chunk-PRXZOIEW.js → chunk-Z6IHMJMC.js} +104 -59
  61. package/dist/{chunk-NWZ6XGBJ.js → chunk-ZBMANLYH.js} +5 -3
  62. package/dist/{chunk-OJ4FIBAN.js → chunk-ZMW6WJFT.js} +30 -23
  63. package/dist/{chunk-QLF74MSV.js → chunk-ZUFHAHTE.js} +12 -6
  64. package/dist/codebase-index/index.js +12 -12
  65. package/dist/codebase-index/parser-worker-script.js +3 -3
  66. package/dist/codebase-index/project-server.js +5 -5
  67. package/dist/codebase-index/worker.js +4 -4
  68. package/dist/design.d.ts +1 -1
  69. package/dist/design.js +1 -1
  70. package/dist/diff.js +3 -2
  71. package/dist/document.js +2 -2
  72. package/dist/e2e.js +2 -2
  73. package/dist/edit.js +9 -9
  74. package/dist/exec.js +6 -6
  75. package/dist/fetch.js +2 -2
  76. package/dist/format.js +8 -8
  77. package/dist/git.js +2 -2
  78. package/dist/glob.js +3 -3
  79. package/dist/{go-parser-IKHW67WE.js → go-parser-D7ELWQ7F.js} +2 -2
  80. package/dist/grep.d.ts +2 -0
  81. package/dist/grep.js +5 -3
  82. package/dist/index.js +56 -56
  83. package/dist/install.js +8 -8
  84. package/dist/{json-parser-2EOVP5MR.js → json-parser-34DHYCPW.js} +1 -18
  85. package/dist/json.js +2 -2
  86. package/dist/kanban-tool-types.d.ts +2 -0
  87. package/dist/kanban.js +2 -2
  88. package/dist/languages/index.js +6 -6
  89. package/dist/legacy-bridge-FOV5ODDI.js +18 -0
  90. package/dist/lint.js +8 -8
  91. package/dist/logs.js +2 -2
  92. package/dist/memory.js +1 -1
  93. package/dist/next-steps.d.ts +7 -0
  94. package/dist/next-steps.js +82 -38
  95. package/dist/outdated.js +5 -3
  96. package/dist/pack.js +53 -53
  97. package/dist/{parser-dispatch-3PDBKIQ7.js → parser-dispatch-BDVCJXTV.js} +4 -4
  98. package/dist/patch.js +8 -8
  99. package/dist/plan.js +4 -4
  100. package/dist/process-registry.js +1 -1
  101. package/dist/ps-slash.js +3 -3
  102. package/dist/{py-parser-RFWG2HYI.js → py-parser-QC6MR4GB.js} +3 -3
  103. package/dist/read.js +8 -8
  104. package/dist/replace.js +9 -8
  105. package/dist/scaffold.js +2 -2
  106. package/dist/search.js +1 -1
  107. package/dist/session-kanban.js +1 -1
  108. package/dist/task.js +4 -4
  109. package/dist/test.js +8 -8
  110. package/dist/todo.js +3 -3
  111. package/dist/tool-search.js +1 -1
  112. package/dist/tool-tier.js +54 -54
  113. package/dist/{tree-sitter-parser-FGKHJGSX.js → tree-sitter-parser-GILYQLS4.js} +2 -2
  114. package/dist/tree.js +2 -2
  115. package/dist/typecheck.js +8 -8
  116. package/dist/win32.js +1 -1
  117. package/dist/write.js +9 -9
  118. package/package.json +5 -5
  119. package/dist/legacy-bridge-EC7GM2IW.js +0 -18
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  COMMAND_OUTPUT_MAX_BYTES,
3
3
  normalizeCommandOutput
4
- } from "./chunk-QLF74MSV.js";
4
+ } from "./chunk-ZUFHAHTE.js";
5
5
 
6
6
  // src/git.ts
7
7
  import { spawn } from "node:child_process";
8
8
  import { statSync } from "node:fs";
9
9
  import { dirname, resolve, sep } from "node:path";
10
+ import { StringDecoder } from "node:string_decoder";
10
11
  import { assessCommitSafety } from "@wrongstack/core/coordination";
12
+ import { ToolValidationError } from "@wrongstack/core/types";
11
13
  import { buildChildEnv } from "@wrongstack/core/utils";
12
14
  var TIMEOUT_MS = 3e4;
13
15
  var MAX_OUTPUT = 1e5;
@@ -89,7 +91,18 @@ var gitTool = {
89
91
  required: ["command"]
90
92
  },
91
93
  async execute(input, ctx, opts) {
92
- if (!input?.command) throw new Error("git: command is required");
94
+ if (!input?.command || typeof input.command !== "string" || !input.command.trim()) {
95
+ throw new ToolValidationError({
96
+ message: "git: command is required and cannot be empty",
97
+ field: "command"
98
+ });
99
+ }
100
+ if (input.command.startsWith("-") || input.command.includes(" --")) {
101
+ throw new ToolValidationError({
102
+ message: `git: unsafe subcommand name "${input.command}"`,
103
+ field: "command"
104
+ });
105
+ }
93
106
  if (input.command === "commit" && !input.message) {
94
107
  return {
95
108
  command: "commit",
@@ -187,7 +200,7 @@ function findGitDir(cwd, projectRoot) {
187
200
  let dir = cwd;
188
201
  for (let i = 0; i < 20; i++) {
189
202
  try {
190
- const stat = statSync(`${dir}/.git`);
203
+ const stat = statSync(resolve(dir, ".git"));
191
204
  if (stat.isDirectory() || stat.isFile()) return dir;
192
205
  } catch {
193
206
  }
@@ -199,8 +212,9 @@ function findGitDir(cwd, projectRoot) {
199
212
  return null;
200
213
  }
201
214
  function buildArgs(input) {
202
- const limit = input.limit ?? 20;
203
- const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim()).filter(Boolean) : [];
215
+ const rawLimit = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0 ? Math.floor(input.limit) : 20;
216
+ const limit = Math.max(1, rawLimit);
217
+ const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim().replace(/\\/g, "/")).filter(Boolean) : [];
204
218
  switch (input.command) {
205
219
  case "status":
206
220
  return ["status", ...files.length ? ["--", ...files] : []];
@@ -269,9 +283,22 @@ function buildArgs(input) {
269
283
  }
270
284
  }
271
285
  function runGit(args, cwd, signal) {
286
+ if (signal.aborted) {
287
+ return Promise.resolve({
288
+ command: args[0],
289
+ stdout: "",
290
+ stderr: "Aborted",
291
+ exitCode: 124,
292
+ truncated: false
293
+ });
294
+ }
272
295
  return new Promise((resolve2) => {
273
296
  let stdout = "";
274
297
  let stderr = "";
298
+ let stdoutBytes = 0;
299
+ let stderrBytes = 0;
300
+ const stdoutDecoder = new StringDecoder("utf8");
301
+ const stderrDecoder = new StringDecoder("utf8");
275
302
  const child = spawn("git", args, {
276
303
  cwd,
277
304
  signal,
@@ -280,31 +307,52 @@ function runGit(args, cwd, signal) {
280
307
  windowsHide: true
281
308
  });
282
309
  child.stdout?.on("data", (chunk) => {
310
+ stdoutBytes += chunk.byteLength;
283
311
  if (stdout.length < MAX_OUTPUT) {
284
- stdout += chunk.toString();
312
+ const text = stdoutDecoder.write(chunk);
313
+ if (text) stdout += text.slice(0, MAX_OUTPUT - stdout.length);
285
314
  }
286
315
  });
287
316
  child.stderr?.on("data", (chunk) => {
317
+ stderrBytes += chunk.byteLength;
288
318
  if (stderr.length < MAX_OUTPUT) {
289
- stderr += chunk.toString();
319
+ const text = stderrDecoder.write(chunk);
320
+ if (text) stderr += text.slice(0, MAX_OUTPUT - stderr.length);
290
321
  }
291
322
  });
292
323
  child.on("error", (err) => {
324
+ const stdoutTail = stdoutDecoder.end();
325
+ if (stdoutTail && stdout.length < MAX_OUTPUT) {
326
+ stdout += stdoutTail.slice(0, MAX_OUTPUT - stdout.length);
327
+ }
328
+ const stderrTail = stderrDecoder.end();
329
+ if (stderrTail && stderr.length < MAX_OUTPUT) {
330
+ stderr += stderrTail.slice(0, MAX_OUTPUT - stderr.length);
331
+ }
293
332
  resolve2({
294
333
  command: args[0],
295
334
  stdout: normalizeCommandOutput(stdout),
296
335
  stderr: err.message,
297
336
  exitCode: 1,
298
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES
337
+ truncated: stdoutBytes > MAX_OUTPUT || Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES
299
338
  });
300
339
  });
301
340
  child.on("close", (code) => {
341
+ const stdoutTail = stdoutDecoder.end();
342
+ if (stdoutTail && stdout.length < MAX_OUTPUT) {
343
+ stdout += stdoutTail.slice(0, MAX_OUTPUT - stdout.length);
344
+ }
345
+ const stderrTail = stderrDecoder.end();
346
+ if (stderrTail && stderr.length < MAX_OUTPUT) {
347
+ stderr += stderrTail.slice(0, MAX_OUTPUT - stderr.length);
348
+ }
349
+ const isTruncated = stdoutBytes > MAX_OUTPUT || stderrBytes > MAX_OUTPUT || Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
302
350
  resolve2({
303
351
  command: args[0],
304
352
  stdout: normalizeCommandOutput(stdout),
305
353
  stderr: normalizeCommandOutput(stderr),
306
354
  exitCode: code ?? 1,
307
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES
355
+ truncated: isTruncated
308
356
  });
309
357
  });
310
358
  });
@@ -313,4 +361,4 @@ function runGit(args, cwd, signal) {
313
361
  export {
314
362
  gitTool
315
363
  };
316
- //# sourceMappingURL=chunk-UMXVT7ST.js.map
364
+ //# sourceMappingURL=chunk-6AAOT4PW.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  todoTool
3
- } from "./chunk-5ZYEGP2P.js";
3
+ } from "./chunk-BK732J7I.js";
4
4
  import {
5
5
  projectSessionPlanToKanban
6
- } from "./chunk-CWALNI6K.js";
6
+ } from "./chunk-X6KXNHUO.js";
7
7
 
8
8
  // src/plan.ts
9
9
  import { randomUUID } from "node:crypto";
@@ -13,11 +13,13 @@ import {
13
13
  deriveTodosFromPlanItem,
14
14
  formatPlan,
15
15
  getPlanTemplate,
16
+ loadPlan,
16
17
  mutatePlan,
17
18
  mutateTasks,
18
19
  removePlanItem,
19
20
  setPlanItemStatus
20
21
  } from "@wrongstack/core/storage";
22
+ import { ToolValidationError } from "@wrongstack/core/types";
21
23
  import { formatTaskList } from "@wrongstack/core/utils";
22
24
  var planTool = {
23
25
  name: "plan",
@@ -85,8 +87,35 @@ var planTool = {
85
87
  },
86
88
  required: ["action"]
87
89
  },
88
- async execute(input, ctx) {
89
- const sessionPlanPath = ctx.meta["plan.path"];
90
+ async execute(input, ctx, _opts) {
91
+ const signal = _opts?.signal ?? ctx?.signal;
92
+ signal?.throwIfAborted();
93
+ const VALID_ACTIONS = /* @__PURE__ */ new Set([
94
+ "show",
95
+ "add",
96
+ "status",
97
+ "start",
98
+ "done",
99
+ "remove",
100
+ "promote",
101
+ "template_use",
102
+ "clear",
103
+ "taskify"
104
+ ]);
105
+ if (!input?.action || !VALID_ACTIONS.has(input.action)) {
106
+ throw new ToolValidationError({
107
+ message: `plan: unknown or missing action "${input?.action}". Allowed actions: ${[...VALID_ACTIONS].join(", ")}`,
108
+ field: "action"
109
+ });
110
+ }
111
+ if (input.scope !== void 0 && input.scope !== "session" && input.scope !== "project") {
112
+ throw new ToolValidationError({
113
+ message: `plan: invalid scope "${input.scope}". Allowed: session, project`,
114
+ field: "scope"
115
+ });
116
+ }
117
+ const meta = ctx.meta ??= {};
118
+ const sessionPlanPath = meta["plan.path"];
90
119
  let planPath;
91
120
  if (input.scope === "project") {
92
121
  if (typeof sessionPlanPath === "string") {
@@ -118,6 +147,16 @@ var planTool = {
118
147
  };
119
148
  }
120
149
  const sessionId = ctx.session?.id ?? "unknown";
150
+ if (input.action === "show") {
151
+ meta["plan.path.resolved"] = planPath;
152
+ const plan2 = await loadPlan(planPath) ?? {
153
+ version: 1,
154
+ sessionId,
155
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
156
+ items: []
157
+ };
158
+ return mkResult(plan2, true, "Plan show ok.");
159
+ }
121
160
  let early = null;
122
161
  const taskifyMeta = { title: "", details: "" };
123
162
  let didTaskify = false;
@@ -159,6 +198,7 @@ var planTool = {
159
198
  return next;
160
199
  }
161
200
  case "status": {
201
+ const VALID_PLAN_STATUSES = /* @__PURE__ */ new Set(["open", "in_progress", "done"]);
162
202
  if (!input.target || !input.status) {
163
203
  early = mkResult(
164
204
  p,
@@ -167,6 +207,14 @@ var planTool = {
167
207
  );
168
208
  return p;
169
209
  }
210
+ if (!VALID_PLAN_STATUSES.has(input.status)) {
211
+ early = mkResult(
212
+ p,
213
+ false,
214
+ `status requires valid status ('open' | 'in_progress' | 'done'), got "${input.status}".`
215
+ );
216
+ return p;
217
+ }
170
218
  const next = setPlanItemStatus(p, input.target, input.status);
171
219
  if (next === p) {
172
220
  early = mkResult(p, false, `No plan item matched "${input.target}".`);
@@ -348,7 +396,10 @@ ${formatTaskList(taskFile.tasks)}`
348
396
  }
349
397
  };
350
398
  function mkResult(plan, ok, message, todos) {
351
- const open = plan.items.filter((i) => i.status !== "done").length;
399
+ let open = 0;
400
+ for (let i = 0; i < plan.items.length; i++) {
401
+ if (plan.items[i].status !== "done") open++;
402
+ }
352
403
  const result = {
353
404
  ok,
354
405
  message,
@@ -363,4 +414,4 @@ function mkResult(plan, ok, message, todos) {
363
414
  export {
364
415
  planTool
365
416
  };
366
- //# sourceMappingURL=chunk-H6AWAZ2Y.js.map
417
+ //# sourceMappingURL=chunk-B4HTSV6Z.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  kanbanTool
3
- } from "./chunk-FJEQZHZN.js";
3
+ } from "./chunk-W2CECGTG.js";
4
4
  import {
5
5
  applyManagedKanbanBoardToTodos,
6
6
  blockingTitles,
7
7
  mirrorSessionTodosToKanban,
8
8
  takeSessionMirrorFailure
9
- } from "./chunk-CWALNI6K.js";
9
+ } from "./chunk-X6KXNHUO.js";
10
10
 
11
11
  // src/todo.ts
12
12
  import {
@@ -16,6 +16,7 @@ import {
16
16
  saveTasks,
17
17
  setPlanItemStatus
18
18
  } from "@wrongstack/core/storage";
19
+ import { ToolValidationError } from "@wrongstack/core/types";
19
20
  import { addTask, getBoard } from "@wrongstack/kanban";
20
21
  function normalizedTitle(value) {
21
22
  return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
@@ -27,6 +28,10 @@ function activeBoardId(items, ctx) {
27
28
  }
28
29
  function bindTodosToBoard(items, previous, board) {
29
30
  const previousById = new Map(previous.map((item) => [item.id, item]));
31
+ const taskById = new Map(board.tasks.map((task) => [task.id, task]));
32
+ const taskByOriginId = new Map(
33
+ board.tasks.filter((task) => task.origin?.taskId).map((task) => [task.origin.taskId, task])
34
+ );
30
35
  const available = board.tasks.filter(
31
36
  (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
32
37
  ).sort(
@@ -38,9 +43,9 @@ function bindTodosToBoard(items, previous, board) {
38
43
  const requestedTaskId = item.kanbanBoardId === board.id ? item.kanbanTaskId : previousItem?.kanbanBoardId === board.id ? previousItem.kanbanTaskId : void 0;
39
44
  const title = normalizedTitle(item.content);
40
45
  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),
46
+ requestedTaskId ? taskById.get(requestedTaskId) : void 0,
47
+ taskById.get(item.id),
48
+ taskByOriginId.get(item.id),
44
49
  available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
45
50
  ];
46
51
  const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
@@ -80,7 +85,10 @@ async function createMissingManagedCards(items, board, ctx, warnings) {
80
85
  description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
81
86
  },
82
87
  // The card mirrors this session's todo row, so the session owns it.
83
- { sessionId: ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session", actor: "todo" }
88
+ {
89
+ sessionId: ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session",
90
+ actor: "todo"
91
+ }
84
92
  );
85
93
  if (!result) {
86
94
  warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
@@ -287,8 +295,40 @@ var todoTool = {
287
295
  required: ["todos"]
288
296
  },
289
297
  async execute(input, ctx, call) {
298
+ const signal = call?.signal ?? ctx.signal ?? new AbortController().signal;
299
+ signal.throwIfAborted();
290
300
  if (!Array.isArray(input?.todos)) {
291
- throw new Error("todo: todos must be an array");
301
+ throw new ToolValidationError({
302
+ message: "todo: todos must be an array",
303
+ field: "todos"
304
+ });
305
+ }
306
+ const VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
307
+ for (const item of input.todos) {
308
+ if (!item || typeof item !== "object") {
309
+ throw new ToolValidationError({
310
+ message: "todo: each item in todos must be an object",
311
+ field: "todos"
312
+ });
313
+ }
314
+ if (!item.id || typeof item.id !== "string" || !item.id.trim()) {
315
+ throw new ToolValidationError({
316
+ message: 'todo: each item must have a non-empty string "id"',
317
+ field: "todos"
318
+ });
319
+ }
320
+ if (item.content === void 0 || typeof item.content !== "string") {
321
+ throw new ToolValidationError({
322
+ message: `todo: item "${item.id}" must have a string "content"`,
323
+ field: "todos"
324
+ });
325
+ }
326
+ if (item.status !== void 0 && !VALID_STATUSES.has(item.status)) {
327
+ throw new ToolValidationError({
328
+ message: `todo: item "${item.id}" has invalid status "${item.status}". Allowed: pending, in_progress, completed`,
329
+ field: "todos"
330
+ });
331
+ }
292
332
  }
293
333
  const items = input.todos.filter((t) => Boolean(t?.id && t.content));
294
334
  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}`;
@@ -330,7 +370,7 @@ var todoTool = {
330
370
  boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
331
371
  }
332
372
  ctx.state.replaceTodos(boundItems);
333
- const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
373
+ const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, signal) : { synced: 0, warnings: [] };
334
374
  kanbanSync.warnings.unshift(...creationWarnings);
335
375
  if (managed && board) {
336
376
  const unresolved = boundItems.filter(
@@ -371,34 +411,39 @@ var todoTool = {
371
411
  );
372
412
  }
373
413
  }
374
- for (const planId of completedPlanIds) {
375
- if (pendingPlanIds.has(planId)) continue;
376
- const meta = ctx.meta;
377
- const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
378
- if (typeof planPath !== "string" || !planPath) continue;
414
+ const meta = ctx.meta;
415
+ const planPath = meta?.["plan.path.resolved"] ?? meta?.["plan.path"];
416
+ if (typeof planPath === "string" && planPath && completedPlanIds.size > 0) {
379
417
  try {
380
- const plan = await loadPlan(planPath);
418
+ let plan = await loadPlan(planPath);
381
419
  if (plan) {
382
- const updated = setPlanItemStatus(plan, planId, "done");
383
- await savePlan(planPath, updated);
420
+ let modified = false;
421
+ for (const planId of completedPlanIds) {
422
+ if (pendingPlanIds.has(planId)) continue;
423
+ plan = setPlanItemStatus(plan, planId, "done");
424
+ modified = true;
425
+ }
426
+ if (modified) await savePlan(planPath, plan);
384
427
  }
385
428
  } catch {
386
429
  }
387
430
  }
388
- for (const taskId of completedTaskIds) {
389
- if (pendingTaskIds.has(taskId)) continue;
390
- const meta = ctx.meta;
391
- const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
392
- if (typeof taskPath !== "string" || !taskPath) continue;
431
+ const taskPath = meta?.["task.path.resolved"] ?? meta?.["task.path"];
432
+ if (typeof taskPath === "string" && taskPath && completedTaskIds.size > 0) {
393
433
  try {
394
434
  const file = await loadTasks(taskPath);
395
435
  if (file) {
396
- const task = file.tasks.find((t) => t.id === taskId);
397
- if (task && task.status !== "completed") {
398
- task.status = "completed";
399
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
400
- await saveTasks(taskPath, file);
436
+ let modified = false;
437
+ for (const taskId of completedTaskIds) {
438
+ if (pendingTaskIds.has(taskId)) continue;
439
+ const task = file.tasks.find((t) => t.id === taskId);
440
+ if (task && task.status !== "completed") {
441
+ task.status = "completed";
442
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
443
+ modified = true;
444
+ }
401
445
  }
446
+ if (modified) await saveTasks(taskPath, file);
402
447
  }
403
448
  } catch {
404
449
  }
@@ -431,4 +476,4 @@ var todoTool = {
431
476
  export {
432
477
  todoTool
433
478
  };
434
- //# sourceMappingURL=chunk-5ZYEGP2P.js.map
479
+ //# sourceMappingURL=chunk-BK732J7I.js.map
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-NBDZMMG2.js";
6
6
  import {
7
7
  truncateMiddle
8
- } from "./chunk-QLF74MSV.js";
8
+ } from "./chunk-ZUFHAHTE.js";
9
9
 
10
10
  // src/fetch.ts
11
11
  import { FetchError, ToolError, ToolValidationError } from "@wrongstack/core/types";
@@ -79,13 +79,29 @@ var fetchTool = {
79
79
  return final;
80
80
  },
81
81
  async *executeStream(input, ctx, opts) {
82
- if (!input?.url) {
82
+ if (!input?.url || typeof input.url !== "string" || !input.url.trim()) {
83
83
  throw new ToolValidationError({
84
- message: "fetch: url is required",
84
+ message: "fetch: url is required and cannot be empty",
85
85
  field: "url"
86
86
  });
87
87
  }
88
- const u = new URL(input.url);
88
+ const trimmedUrl = input.url.trim();
89
+ let u;
90
+ try {
91
+ u = new URL(trimmedUrl);
92
+ } catch {
93
+ throw new ToolValidationError({
94
+ message: `fetch: invalid URL "${input.url}"`,
95
+ field: "url"
96
+ });
97
+ }
98
+ const ALLOWED_FORMATS = /* @__PURE__ */ new Set(["markdown", "text", "raw"]);
99
+ if (input.format !== void 0 && !ALLOWED_FORMATS.has(input.format)) {
100
+ throw new ToolValidationError({
101
+ message: `fetch: unsupported format "${input.format}". Allowed formats: markdown, text, raw`,
102
+ field: "format"
103
+ });
104
+ }
89
105
  if (u.username || u.password) {
90
106
  throw new ToolValidationError({
91
107
  message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
@@ -105,8 +121,9 @@ var fetchTool = {
105
121
  });
106
122
  }
107
123
  await assertNotPrivate(u.hostname);
108
- yield { type: "log", text: `GET ${input.url}` };
124
+ yield { type: "log", text: `GET ${trimmedUrl}` };
109
125
  const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
126
+ signal.throwIfAborted();
110
127
  const ctrl = new AbortController();
111
128
  const timer = setTimeout(
112
129
  () => ctrl.abort(
@@ -122,10 +139,10 @@ var fetchTool = {
122
139
  try {
123
140
  let res;
124
141
  try {
125
- res = await guardedFetch(input.url, 5, combined);
142
+ res = await guardedFetch(trimmedUrl, 5, combined);
126
143
  } catch (err) {
127
144
  if (signal.aborted) throw err;
128
- throw describeFetchError(err, input.url, ctrl.signal.aborted);
145
+ throw describeFetchError(err, trimmedUrl, ctrl.signal.aborted);
129
146
  }
130
147
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
131
148
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
@@ -146,31 +163,46 @@ var fetchTool = {
146
163
  let pendingBytes = 0;
147
164
  const FLUSH_AT = 4 * 1024;
148
165
  if (reader) {
149
- for (; ; ) {
150
- const { value, done } = await reader.read();
151
- if (done) break;
152
- if (!value) continue;
153
- received += value.byteLength;
154
- pendingBytes += value.byteLength;
155
- chunks.push(value);
156
- if (pendingBytes >= FLUSH_AT) {
157
- const recent = Buffer.from(value).toString("utf-8");
158
- yield {
159
- type: "partial_output",
160
- text: recent,
161
- data: { received }
162
- };
163
- pendingBytes = 0;
166
+ try {
167
+ for (; ; ) {
168
+ const { value, done } = await reader.read();
169
+ if (done) break;
170
+ if (!value) continue;
171
+ received += value.byteLength;
172
+ pendingBytes += value.byteLength;
173
+ chunks.push(value);
174
+ if (pendingBytes >= FLUSH_AT) {
175
+ const recent = Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString(
176
+ "utf8"
177
+ );
178
+ yield {
179
+ type: "partial_output",
180
+ text: recent,
181
+ data: { received }
182
+ };
183
+ pendingBytes = 0;
184
+ }
185
+ if (received > MAX_BYTES) {
186
+ await reader.cancel().catch(() => {
187
+ });
188
+ break;
189
+ }
164
190
  }
165
- if (received > MAX_BYTES) break;
191
+ } finally {
192
+ reader.releaseLock();
166
193
  }
167
194
  }
168
- const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
169
- const format = input.format ?? (ct.includes("text/html") ? "markdown" : "text");
195
+ const text = Buffer.concat(chunks).toString("utf8");
196
+ const isHtml = ct.includes("text/html");
197
+ const isJson = /[\/+]json(;|$)/i.test(ct);
198
+ const format = input.format ?? (isHtml ? "markdown" : "text");
170
199
  let content;
171
200
  if (format === "raw") content = text;
172
- else if (format === "markdown" && ct.includes("text/html")) content = TD.turndown(text);
173
- else if (ct.includes("application/json")) content = prettyJson(text);
201
+ else if (format === "markdown" && isHtml) content = TD.turndown(text);
202
+ else if (format === "markdown" && isJson) content = `\`\`\`json
203
+ ${prettyJson(text)}
204
+ \`\`\``;
205
+ else if (isJson) content = prettyJson(text);
174
206
  else content = text;
175
207
  yield {
176
208
  type: "final",
@@ -226,6 +258,7 @@ function describeFetchError(err, url, timedOut) {
226
258
  });
227
259
  }
228
260
  function prettyJson(s) {
261
+ if (!s || s.length > 5e5) return s;
229
262
  try {
230
263
  return JSON.stringify(JSON.parse(s), null, 2);
231
264
  } catch {
@@ -236,4 +269,4 @@ function prettyJson(s) {
236
269
  export {
237
270
  fetchTool
238
271
  };
239
- //# sourceMappingURL=chunk-KD5EHYA2.js.map
272
+ //# sourceMappingURL=chunk-C2DQKVLO.js.map