@wrongstack/tools 0.285.0 → 0.287.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +21 -2
  2. package/dist/_edit-match.d.ts.map +1 -1
  3. package/dist/batch-tool-use.d.ts +1 -1
  4. package/dist/batch-tool-use.d.ts.map +1 -1
  5. package/dist/batch-tool-use.js +26 -8
  6. package/dist/batch-tool-use.js.map +2 -2
  7. package/dist/browser/artifacts.d.ts +13 -0
  8. package/dist/browser/artifacts.d.ts.map +1 -0
  9. package/dist/browser/index.d.ts +7 -0
  10. package/dist/browser/index.d.ts.map +1 -0
  11. package/dist/browser/index.js +1264 -0
  12. package/dist/browser/index.js.map +7 -0
  13. package/dist/browser/manager.d.ts +57 -0
  14. package/dist/browser/manager.d.ts.map +1 -0
  15. package/dist/browser/network-guard-proxy.d.ts +23 -0
  16. package/dist/browser/network-guard-proxy.d.ts.map +1 -0
  17. package/dist/browser/security.d.ts +29 -0
  18. package/dist/browser/security.d.ts.map +1 -0
  19. package/dist/browser/tools.d.ts +62 -0
  20. package/dist/browser/tools.d.ts.map +1 -0
  21. package/dist/browser/types.d.ts +60 -0
  22. package/dist/browser/types.d.ts.map +1 -0
  23. package/dist/builtin.d.ts +4 -3
  24. package/dist/builtin.d.ts.map +1 -1
  25. package/dist/builtin.js +2609 -525
  26. package/dist/builtin.js.map +4 -4
  27. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  28. package/dist/codebase-index/index.js +190 -96
  29. package/dist/codebase-index/index.js.map +4 -4
  30. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  31. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  32. package/dist/codebase-index/worker.js +188 -94
  33. package/dist/codebase-index/worker.js.map +4 -4
  34. package/dist/e2e.d.ts +56 -0
  35. package/dist/e2e.d.ts.map +1 -0
  36. package/dist/edit.d.ts.map +1 -1
  37. package/dist/edit.js +36 -12
  38. package/dist/edit.js.map +3 -3
  39. package/dist/exec.js +1 -1
  40. package/dist/exec.js.map +2 -2
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2895 -511
  44. package/dist/index.js.map +4 -4
  45. package/dist/kanban.d.ts +7 -2
  46. package/dist/kanban.d.ts.map +1 -1
  47. package/dist/kanban.js +158 -19
  48. package/dist/kanban.js.map +4 -4
  49. package/dist/next-steps.d.ts +6 -2
  50. package/dist/next-steps.d.ts.map +1 -1
  51. package/dist/next-steps.js +3 -3
  52. package/dist/next-steps.js.map +2 -2
  53. package/dist/pack.js +2625 -525
  54. package/dist/pack.js.map +4 -4
  55. package/dist/plan.d.ts.map +1 -1
  56. package/dist/plan.js +154 -4
  57. package/dist/plan.js.map +4 -4
  58. package/dist/search.d.ts.map +1 -1
  59. package/dist/search.js +8 -2
  60. package/dist/search.js.map +2 -2
  61. package/dist/session-kanban.d.ts +38 -0
  62. package/dist/session-kanban.d.ts.map +1 -0
  63. package/dist/session-kanban.js +489 -0
  64. package/dist/session-kanban.js.map +7 -0
  65. package/dist/task.d.ts.map +1 -1
  66. package/dist/task.js +163 -4
  67. package/dist/task.js.map +4 -4
  68. package/dist/todo.d.ts.map +1 -1
  69. package/dist/todo.js +155 -7
  70. package/dist/todo.js.map +4 -4
  71. package/dist/tool-icon-map.d.ts +2 -1
  72. package/dist/tool-icon-map.d.ts.map +1 -1
  73. package/dist/tool-icons.d.ts.map +1 -1
  74. package/dist/tool-icons.js +7 -1
  75. package/dist/tool-icons.js.map +2 -2
  76. package/package.json +12 -3
package/dist/builtin.js CHANGED
@@ -56,7 +56,7 @@ function createOutputSpool(opts) {
56
56
  let filePath = null;
57
57
  let failed = false;
58
58
  let finalized = false;
59
- const open2 = () => {
59
+ const open3 = () => {
60
60
  if (stream || failed) return;
61
61
  try {
62
62
  const dir = toolOutputDir();
@@ -89,7 +89,7 @@ function createOutputSpool(opts) {
89
89
  return;
90
90
  }
91
91
  head += text;
92
- open2();
92
+ open3();
93
93
  head = "";
94
94
  return;
95
95
  }
@@ -907,8 +907,8 @@ async function* spawnStream(opts) {
907
907
  try {
908
908
  for (; ; ) {
909
909
  while (queue.length === 0) {
910
- await new Promise((resolve11) => {
911
- waiter = resolve11;
910
+ await new Promise((resolve14) => {
911
+ waiter = resolve14;
912
912
  });
913
913
  }
914
914
  const chunk = queue.shift();
@@ -982,14 +982,14 @@ function sha256hex(content) {
982
982
  return createHash("sha256").update(content, "utf8").digest("hex");
983
983
  }
984
984
  async function detectPackageManager(cwd) {
985
- const { stat: stat15 } = await import("node:fs/promises");
985
+ const { stat: stat17 } = await import("node:fs/promises");
986
986
  try {
987
- await stat15(`${cwd}/pnpm-lock.yaml`);
987
+ await stat17(`${cwd}/pnpm-lock.yaml`);
988
988
  return "pnpm";
989
989
  } catch {
990
990
  }
991
991
  try {
992
- await stat15(`${cwd}/yarn.lock`);
992
+ await stat17(`${cwd}/yarn.lock`);
993
993
  return "yarn";
994
994
  } catch {
995
995
  }
@@ -2558,8 +2558,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2558
2558
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2559
2559
  }
2560
2560
  }
2561
- function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
2562
- const lower = basename7.toLowerCase();
2561
+ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2562
+ const lower = basename8.toLowerCase();
2563
2563
  const extension = path4.extname(lower);
2564
2564
  for (const profile of profiles) {
2565
2565
  const detector = profile.detectors.find(
@@ -2570,7 +2570,7 @@ function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
2570
2570
  candidate.evidence.push({
2571
2571
  kind: detector.kind,
2572
2572
  path: fullPath,
2573
- value: basename7,
2573
+ value: basename8,
2574
2574
  weight: detector.weight
2575
2575
  });
2576
2576
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2698,8 +2698,8 @@ function normalizeLimits(input) {
2698
2698
  async function canonicalDirectory(input) {
2699
2699
  const resolved = path4.resolve(input);
2700
2700
  const real = await fs2.realpath(resolved);
2701
- const stat15 = await fs2.stat(real);
2702
- if (!stat15.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2701
+ const stat17 = await fs2.stat(real);
2702
+ if (!stat17.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2703
2703
  return real;
2704
2704
  }
2705
2705
  async function canonicalInside(input, root, label) {
@@ -2719,12 +2719,12 @@ function resolveFrom(cwd, input) {
2719
2719
  return path4.isAbsolute(input) ? input : path4.resolve(cwd, input);
2720
2720
  }
2721
2721
  function isInside(candidate, root) {
2722
- const relative10 = path4.relative(root, candidate);
2723
- return relative10 === "" || !relative10.startsWith("..") && !path4.isAbsolute(relative10);
2722
+ const relative12 = path4.relative(root, candidate);
2723
+ return relative12 === "" || !relative12.startsWith("..") && !path4.isAbsolute(relative12);
2724
2724
  }
2725
2725
  function pathDepth(candidate, root) {
2726
- const relative10 = path4.relative(root, candidate);
2727
- return relative10 === "" ? 0 : relative10.split(path4.sep).length;
2726
+ const relative12 = path4.relative(root, candidate);
2727
+ return relative12 === "" ? 0 : relative12.split(path4.sep).length;
2728
2728
  }
2729
2729
  function dedupeEvidence(items) {
2730
2730
  const seen = /* @__PURE__ */ new Set();
@@ -3424,8 +3424,8 @@ function compareCandidates(a, b) {
3424
3424
  return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root);
3425
3425
  }
3426
3426
  function workspaceDepth(workspaceRoot, projectRoot) {
3427
- const relative10 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3428
- return relative10 === "" ? 0 : relative10.split(path6.sep).length;
3427
+ const relative12 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3428
+ return relative12 === "" ? 0 : relative12.split(path6.sep).length;
3429
3429
  }
3430
3430
  function validateOperationOptions(operation, packages) {
3431
3431
  if (!operation.startsWith("package-") || !packages) return;
@@ -3455,8 +3455,8 @@ async function canonicalTarget(cwd, target, projectRoot) {
3455
3455
  return real;
3456
3456
  }
3457
3457
  function isInside2(candidate, root) {
3458
- const relative10 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3459
- return relative10 === "" || !relative10.startsWith("..") && !path6.isAbsolute(relative10);
3458
+ const relative12 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3459
+ return relative12 === "" || !relative12.startsWith("..") && !path6.isAbsolute(relative12);
3460
3460
  }
3461
3461
  var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
3462
3462
  var init_plan = __esm({
@@ -3587,8 +3587,8 @@ async function executeInternal(options, startedAt) {
3587
3587
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3588
3588
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3589
3589
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3590
- const stat15 = await fs4.stat(safeTarget);
3591
- if (stat15.size > MAX_INTERNAL_SOURCE_BYTES) {
3590
+ const stat17 = await fs4.stat(safeTarget);
3591
+ if (stat17.size > MAX_INTERNAL_SOURCE_BYTES) {
3592
3592
  return unavailableResult(
3593
3593
  options,
3594
3594
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -3635,14 +3635,14 @@ async function validatePlanRealpaths(plan, projectRoot) {
3635
3635
  return void 0;
3636
3636
  }
3637
3637
  function isRealInside(candidate, root) {
3638
- const relative10 = path7.relative(root, candidate);
3639
- return relative10 === "" || !relative10.startsWith("..") && !path7.isAbsolute(relative10);
3638
+ const relative12 = path7.relative(root, candidate);
3639
+ return relative12 === "" || !relative12.startsWith("..") && !path7.isAbsolute(relative12);
3640
3640
  }
3641
3641
  async function assertContainedFile(candidate, projectRoot) {
3642
3642
  const realRoot = await fs4.realpath(projectRoot);
3643
3643
  const realTarget = await fs4.realpath(candidate);
3644
- const relative10 = path7.relative(realRoot, realTarget);
3645
- if (relative10.startsWith("..") || path7.isAbsolute(relative10)) {
3644
+ const relative12 = path7.relative(realRoot, realTarget);
3645
+ if (relative12.startsWith("..") || path7.isAbsolute(relative12)) {
3646
3646
  throw new Error(`Internal syntax target resolves outside project root: ${candidate}`);
3647
3647
  }
3648
3648
  return realTarget;
@@ -3875,8 +3875,8 @@ async function snapshotPaths(paths) {
3875
3875
  const existing = [];
3876
3876
  for (const candidate of paths) {
3877
3877
  try {
3878
- const stat15 = await fs4.stat(candidate);
3879
- if (!stat15.isFile()) continue;
3878
+ const stat17 = await fs4.stat(candidate);
3879
+ if (!stat17.isFile()) continue;
3880
3880
  existing.push(candidate);
3881
3881
  } catch {
3882
3882
  }
@@ -3887,15 +3887,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3887
3887
  const beforeSet = new Set(before);
3888
3888
  const afterSet = new Set(after);
3889
3889
  const changed = /* @__PURE__ */ new Set();
3890
- for (const path28 of after) {
3891
- if (!beforeSet.has(path28)) changed.add(path28);
3890
+ for (const path32 of after) {
3891
+ if (!beforeSet.has(path32)) changed.add(path32);
3892
3892
  }
3893
- for (const path28 of before) {
3894
- if (!afterSet.has(path28)) changed.add(path28);
3893
+ for (const path32 of before) {
3894
+ if (!afterSet.has(path32)) changed.add(path32);
3895
3895
  }
3896
3896
  if (beforeSizes && afterSizes) {
3897
- for (const path28 of after) {
3898
- if (beforeSizes.get(path28) !== afterSizes.get(path28)) changed.add(path28);
3897
+ for (const path32 of after) {
3898
+ if (beforeSizes.get(path32) !== afterSizes.get(path32)) changed.add(path32);
3899
3899
  }
3900
3900
  }
3901
3901
  return [...changed].sort();
@@ -3904,8 +3904,8 @@ async function snapshotSizes(paths) {
3904
3904
  const sizes = /* @__PURE__ */ new Map();
3905
3905
  for (const candidate of paths) {
3906
3906
  try {
3907
- const stat15 = await fs4.stat(candidate);
3908
- if (stat15.isFile()) sizes.set(candidate, stat15.size);
3907
+ const stat17 = await fs4.stat(candidate);
3908
+ if (stat17.isFile()) sizes.set(candidate, stat17.size);
3909
3909
  } catch {
3910
3910
  }
3911
3911
  }
@@ -4848,8 +4848,8 @@ var auditTool = {
4848
4848
  yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
4849
4849
  }
4850
4850
  };
4851
- function parseAuditOutput(json, exitCode) {
4852
- if (!json) {
4851
+ function parseAuditOutput(json2, exitCode) {
4852
+ if (!json2) {
4853
4853
  return {
4854
4854
  exit_code: exitCode,
4855
4855
  vulnerabilities: [],
@@ -4860,7 +4860,7 @@ function parseAuditOutput(json, exitCode) {
4860
4860
  };
4861
4861
  }
4862
4862
  try {
4863
- const data = JSON.parse(json);
4863
+ const data = JSON.parse(json2);
4864
4864
  const advisories = [];
4865
4865
  const ads = data.advisories ?? {};
4866
4866
  for (const id of Object.keys(ads)) {
@@ -4879,8 +4879,8 @@ function parseAuditOutput(json, exitCode) {
4879
4879
  vulnerabilities: advisories,
4880
4880
  total,
4881
4881
  summary,
4882
- output: json,
4883
- truncated: json.length > 1e5
4882
+ output: json2,
4883
+ truncated: json2.length > 1e5
4884
4884
  };
4885
4885
  } catch {
4886
4886
  return {
@@ -4888,7 +4888,7 @@ function parseAuditOutput(json, exitCode) {
4888
4888
  vulnerabilities: [],
4889
4889
  total: 0,
4890
4890
  summary: "Could not parse audit output",
4891
- output: json,
4891
+ output: json2,
4892
4892
  truncated: false
4893
4893
  };
4894
4894
  }
@@ -5916,10 +5916,10 @@ var bashTool = {
5916
5916
  queue.push(c);
5917
5917
  }
5918
5918
  };
5919
- const next = () => new Promise((resolve11) => {
5919
+ const next = () => new Promise((resolve14) => {
5920
5920
  const c = queue.shift();
5921
- if (c) resolve11(c);
5922
- else resolveNext = resolve11;
5921
+ if (c) resolve14(c);
5922
+ else resolveNext = resolve14;
5923
5923
  });
5924
5924
  let lastFlush = Date.now();
5925
5925
  const flush = () => {
@@ -6054,6 +6054,9 @@ ${hint}` : ""),
6054
6054
  };
6055
6055
 
6056
6056
  // src/batch-tool-use.ts
6057
+ import {
6058
+ GOVERNED_TOOL_EXECUTOR_META_KEY
6059
+ } from "@wrongstack/core";
6057
6060
  var batchToolUseTool = {
6058
6061
  name: "batch_tool_use",
6059
6062
  category: "Meta",
@@ -6090,7 +6093,7 @@ var batchToolUseTool = {
6090
6093
  },
6091
6094
  required: ["calls"]
6092
6095
  },
6093
- async execute(input, ctx, opts) {
6096
+ async execute(input, ctx, _opts) {
6094
6097
  if (!input?.calls || input.calls.length === 0) {
6095
6098
  return {
6096
6099
  results: [],
@@ -6100,18 +6103,33 @@ var batchToolUseTool = {
6100
6103
  stop_on_error: false
6101
6104
  };
6102
6105
  }
6106
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
6107
+ if (typeof governedExecute !== "function") {
6108
+ return {
6109
+ results: input.calls.map((call) => ({
6110
+ tool: call.tool,
6111
+ success: false,
6112
+ error: "governed nested execution is unavailable; call the tool directly",
6113
+ executionMs: 0
6114
+ })),
6115
+ total: input.calls.length,
6116
+ succeeded: 0,
6117
+ failed: input.calls.length,
6118
+ stop_on_error: input.stop_on_error ?? false
6119
+ };
6120
+ }
6103
6121
  const results = [];
6104
6122
  let succeeded = 0;
6105
6123
  let failed = 0;
6106
6124
  if (input.parallel !== false) {
6107
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, opts));
6125
+ const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
6108
6126
  const allResults = await Promise.all(promises);
6109
6127
  results.push(...allResults);
6110
6128
  succeeded = allResults.filter((r) => r.success).length;
6111
6129
  failed = allResults.filter((r) => !r.success).length;
6112
6130
  } else {
6113
6131
  for (const call of input.calls) {
6114
- const result = await executeSingle(call, ctx, opts ?? { signal: void 0 });
6132
+ const result = await executeSingle(call, ctx, governedExecute);
6115
6133
  results.push(result);
6116
6134
  if (result.success) {
6117
6135
  succeeded++;
@@ -6130,9 +6148,9 @@ var batchToolUseTool = {
6130
6148
  };
6131
6149
  }
6132
6150
  };
6133
- async function executeSingle(call, ctx, opts) {
6151
+ async function executeSingle(call, ctx, governedExecute) {
6134
6152
  const start = Date.now();
6135
- const tool = ctx.tools.find((t) => t.name === call.tool);
6153
+ const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
6136
6154
  if (!tool) {
6137
6155
  return {
6138
6156
  tool: call.tool,
@@ -6141,23 +6159,1255 @@ async function executeSingle(call, ctx, opts) {
6141
6159
  executionMs: Date.now() - start
6142
6160
  };
6143
6161
  }
6144
- try {
6145
- const result = await tool.execute(call.input, ctx, opts);
6146
- return {
6147
- tool: call.tool,
6148
- success: true,
6149
- result,
6150
- executionMs: Date.now() - start
6151
- };
6152
- } catch (e) {
6153
- return {
6154
- tool: call.tool,
6155
- success: false,
6156
- error: e instanceof Error ? e.message : String(e),
6157
- executionMs: Date.now() - start
6158
- };
6162
+ try {
6163
+ const result = await governedExecute(call.tool, call.input);
6164
+ return {
6165
+ tool: call.tool,
6166
+ success: result.success,
6167
+ ...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
6168
+ executionMs: Date.now() - start
6169
+ };
6170
+ } catch (e) {
6171
+ return {
6172
+ tool: call.tool,
6173
+ success: false,
6174
+ error: e instanceof Error ? e.message : String(e),
6175
+ executionMs: Date.now() - start
6176
+ };
6177
+ }
6178
+ }
6179
+
6180
+ // src/browser/tools.ts
6181
+ import * as path12 from "node:path";
6182
+ import { ToolCapabilities } from "@wrongstack/core";
6183
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
6184
+
6185
+ // src/browser/manager.ts
6186
+ import * as fs8 from "node:fs/promises";
6187
+ import * as path11 from "node:path";
6188
+ import { ulid as ulid2 } from "@wrongstack/core/utils";
6189
+
6190
+ // src/browser/artifacts.ts
6191
+ import { createHash as createHash3 } from "node:crypto";
6192
+ import { createReadStream } from "node:fs";
6193
+ import * as fs7 from "node:fs/promises";
6194
+ import * as path10 from "node:path";
6195
+ import { atomicWrite, ulid } from "@wrongstack/core/utils";
6196
+ var BrowserArtifactStore = class {
6197
+ constructor(root) {
6198
+ this.root = root;
6199
+ }
6200
+ root;
6201
+ async write(sessionId, kind, extension, mimeType, content) {
6202
+ const id = ulid();
6203
+ const dir = path10.join(this.root, safeSegment(sessionId));
6204
+ const target = path10.join(dir, `${id}.${extension.replace(/^\./, "")}`);
6205
+ await atomicWrite(target, content, { mode: 384 });
6206
+ return this.record(id, sessionId, kind, target, mimeType);
6207
+ }
6208
+ async record(id, sessionId, kind, target, mimeType) {
6209
+ await fs7.chmod(target, 384).catch(() => void 0);
6210
+ const [stat17, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
6211
+ const artifact = {
6212
+ id,
6213
+ kind,
6214
+ sensitivity: "sensitive",
6215
+ path: target,
6216
+ mimeType,
6217
+ sizeBytes: stat17.size,
6218
+ sha256,
6219
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6220
+ };
6221
+ const metadataPath = path10.join(path10.dirname(target), `${id}.metadata.json`);
6222
+ try {
6223
+ await atomicWrite(
6224
+ metadataPath,
6225
+ `${JSON.stringify({
6226
+ id: artifact.id,
6227
+ sessionId: safeSegment(sessionId),
6228
+ kind: artifact.kind,
6229
+ sensitivity: artifact.sensitivity,
6230
+ mimeType: artifact.mimeType,
6231
+ sizeBytes: artifact.sizeBytes,
6232
+ sha256: artifact.sha256,
6233
+ createdAt: artifact.createdAt
6234
+ })}
6235
+ `,
6236
+ { mode: 384 }
6237
+ );
6238
+ } catch (error) {
6239
+ await fs7.rm(target, { force: true }).catch(() => void 0);
6240
+ throw error;
6241
+ }
6242
+ return artifact;
6243
+ }
6244
+ pathFor(sessionId, extension) {
6245
+ const id = ulid();
6246
+ return {
6247
+ id,
6248
+ path: path10.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
6249
+ };
6250
+ }
6251
+ async describe(id, kind, target, mimeType) {
6252
+ return this.record(id, path10.basename(path10.dirname(target)), kind, target, mimeType);
6253
+ }
6254
+ };
6255
+ async function hashFile(target) {
6256
+ const hash = createHash3("sha256");
6257
+ await new Promise((resolve14, reject) => {
6258
+ const stream = createReadStream(target);
6259
+ stream.on("data", (chunk) => hash.update(chunk));
6260
+ stream.once("error", reject);
6261
+ stream.once("end", resolve14);
6262
+ });
6263
+ return hash.digest("hex");
6264
+ }
6265
+ function safeSegment(value) {
6266
+ return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
6267
+ }
6268
+
6269
+ // src/browser/network-guard-proxy.ts
6270
+ import * as http from "node:http";
6271
+ import * as net2 from "node:net";
6272
+
6273
+ // src/browser/security.ts
6274
+ import * as dns from "node:dns/promises";
6275
+ import * as net from "node:net";
6276
+ import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
6277
+ var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
6278
+ async function assertBrowserUrlAllowed(rawUrl, opts) {
6279
+ const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
6280
+ if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
6281
+ if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
6282
+ await assertNotPrivateHost(url.hostname);
6283
+ }
6284
+ return url;
6285
+ }
6286
+ async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
6287
+ const url = parseBrowserUrl(rawUrl, true);
6288
+ const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
6289
+ const hostname2 = unbracket(url.hostname);
6290
+ const literalFamily = net.isIP(hostname2);
6291
+ if (literalFamily === 4 || literalFamily === 6) {
6292
+ if (!allowPrivate && isPrivateAddress(hostname2, literalFamily)) {
6293
+ throw new Error(`browser: blocked private/loopback address "${hostname2}"`);
6294
+ }
6295
+ return { url, address: hostname2, family: literalFamily };
6296
+ }
6297
+ if ((hostname2 === "localhost" || hostname2.endsWith(".localhost")) && !allowPrivate) {
6298
+ throw new Error("browser: blocked localhost target");
6299
+ }
6300
+ const lookup3 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
6301
+ let records;
6302
+ try {
6303
+ records = await lookup3(hostname2);
6304
+ } catch (error) {
6305
+ throw new Error(
6306
+ `browser: DNS resolution failed for "${hostname2}": ${error instanceof Error ? error.message : String(error)}`
6307
+ );
6308
+ }
6309
+ if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname2}"`);
6310
+ for (const record of records) {
6311
+ if (record.family !== 4 && record.family !== 6) {
6312
+ throw new Error(`browser: DNS returned an unsupported address family for "${hostname2}"`);
6313
+ }
6314
+ if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
6315
+ throw new Error(`browser: resolved to private address ${record.address}`);
6316
+ }
6317
+ }
6318
+ const selected = records[0];
6319
+ return { url, address: selected.address, family: selected.family };
6320
+ }
6321
+ function parsePrivateOriginAllowlist(raw) {
6322
+ if (!raw?.trim()) return [];
6323
+ const origins = /* @__PURE__ */ new Set();
6324
+ for (const entry of raw.split(",")) {
6325
+ const candidate = entry.trim();
6326
+ if (!candidate) continue;
6327
+ const url = parseBrowserUrl(candidate, true);
6328
+ if (url.pathname !== "/" || url.search || url.hash) {
6329
+ throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
6330
+ }
6331
+ origins.add(url.origin);
6332
+ }
6333
+ return [...origins];
6334
+ }
6335
+ function parseBrowserUrl(rawUrl, navigation) {
6336
+ let url;
6337
+ try {
6338
+ url = new URL(rawUrl);
6339
+ } catch {
6340
+ throw new Error("browser: url must be an absolute URL");
6341
+ }
6342
+ if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
6343
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
6344
+ throw new Error(`browser: unsupported protocol "${url.protocol}"`);
6345
+ }
6346
+ if (url.username || url.password) {
6347
+ throw new Error("browser: credentials in URLs are not allowed");
6348
+ }
6349
+ return url;
6350
+ }
6351
+ function isAllowedPrivateOrigin(url, origins) {
6352
+ return origins?.includes(url.origin) ?? false;
6353
+ }
6354
+ function isPrivateAddress(address, family) {
6355
+ return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
6356
+ }
6357
+ function unbracket(hostname2) {
6358
+ return hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
6359
+ }
6360
+ function safeBrowserUrl(rawUrl) {
6361
+ try {
6362
+ const url = new URL(rawUrl);
6363
+ url.username = "";
6364
+ url.password = "";
6365
+ url.search = "";
6366
+ url.hash = "";
6367
+ return url.toString();
6368
+ } catch {
6369
+ return "about:blank";
6370
+ }
6371
+ }
6372
+ function redactBrowserText(text) {
6373
+ return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
6374
+ /\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
6375
+ "$1=[REDACTED]"
6376
+ );
6377
+ }
6378
+
6379
+ // src/browser/network-guard-proxy.ts
6380
+ var BrowserNetworkGuardProxy = class {
6381
+ constructor(options = {}) {
6382
+ this.options = options;
6383
+ this.server = http.createServer((request2, response) => {
6384
+ void this.forwardHttp(request2, response);
6385
+ });
6386
+ this.server.on("connect", (request2, socket, head) => {
6387
+ void this.forwardConnect(request2, socket, head);
6388
+ });
6389
+ this.server.on("upgrade", (request2, socket, head) => {
6390
+ void this.forwardUpgrade(request2, socket, head);
6391
+ });
6392
+ this.server.on("connection", (socket) => {
6393
+ this.sockets.add(socket);
6394
+ socket.once("close", () => this.sockets.delete(socket));
6395
+ });
6396
+ this.server.on("clientError", (_error, socket) => {
6397
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
6398
+ });
6399
+ }
6400
+ options;
6401
+ server;
6402
+ sockets = /* @__PURE__ */ new Set();
6403
+ startPromise;
6404
+ url;
6405
+ async start() {
6406
+ if (this.url) return this.url;
6407
+ if (this.startPromise) return this.startPromise;
6408
+ this.startPromise = new Promise((resolve14, reject) => {
6409
+ const onError = (error) => {
6410
+ this.server.off("listening", onListening);
6411
+ reject(error);
6412
+ };
6413
+ const onListening = () => {
6414
+ this.server.off("error", onError);
6415
+ const address = this.server.address();
6416
+ if (!address || typeof address === "string") {
6417
+ reject(new Error("browser: network guard proxy failed to bind"));
6418
+ return;
6419
+ }
6420
+ this.url = `http://127.0.0.1:${address.port}`;
6421
+ resolve14(this.url);
6422
+ };
6423
+ this.server.once("error", onError);
6424
+ this.server.once("listening", onListening);
6425
+ this.server.listen(0, "127.0.0.1");
6426
+ }).finally(() => {
6427
+ this.startPromise = void 0;
6428
+ });
6429
+ return this.startPromise;
6430
+ }
6431
+ async close() {
6432
+ this.url = void 0;
6433
+ for (const socket of this.sockets) socket.destroy();
6434
+ this.sockets.clear();
6435
+ if (!this.server.listening) return;
6436
+ await new Promise((resolve14) => this.server.close(() => resolve14()));
6437
+ }
6438
+ async forwardHttp(request2, response) {
6439
+ try {
6440
+ const target = await this.resolve(request2.url ?? "");
6441
+ if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
6442
+ const headers = sanitizeHeaders(request2.headers);
6443
+ headers.host = target.url.host;
6444
+ const upstream = http.request({
6445
+ host: target.address,
6446
+ family: target.family,
6447
+ port: Number(target.url.port || 80),
6448
+ method: request2.method,
6449
+ path: `${target.url.pathname}${target.url.search}`,
6450
+ headers,
6451
+ agent: false
6452
+ });
6453
+ upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
6454
+ upstream.destroy(new Error("browser: upstream connection timed out"));
6455
+ });
6456
+ upstream.on("response", (upstreamResponse) => {
6457
+ response.writeHead(
6458
+ upstreamResponse.statusCode ?? 502,
6459
+ sanitizeHeaders(upstreamResponse.headers)
6460
+ );
6461
+ upstreamResponse.pipe(response);
6462
+ });
6463
+ upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
6464
+ request2.on("aborted", () => upstream.destroy());
6465
+ request2.pipe(upstream);
6466
+ } catch {
6467
+ writeProxyError(response, 403, "Blocked by browser network policy");
6468
+ }
6469
+ }
6470
+ async forwardConnect(request2, client, head) {
6471
+ try {
6472
+ const authority = request2.url ?? "";
6473
+ const target = await this.resolve(`https://${authority}`);
6474
+ const upstream = net2.connect({
6475
+ host: target.address,
6476
+ family: target.family,
6477
+ port: Number(target.url.port || 443)
6478
+ });
6479
+ upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
6480
+ upstream.once("connect", () => {
6481
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
6482
+ if (head.length > 0) upstream.write(head);
6483
+ upstream.pipe(client);
6484
+ client.pipe(upstream);
6485
+ });
6486
+ upstream.once("error", () => {
6487
+ client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
6488
+ });
6489
+ client.once("close", () => upstream.destroy());
6490
+ } catch {
6491
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
6492
+ }
6493
+ }
6494
+ async forwardUpgrade(request2, client, head) {
6495
+ let upstream;
6496
+ try {
6497
+ const rawUrl = request2.url ?? "";
6498
+ const websocketUrl = new URL(rawUrl);
6499
+ if (websocketUrl.protocol !== "ws:") {
6500
+ throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
6501
+ }
6502
+ websocketUrl.protocol = "http:";
6503
+ const target = await this.resolve(websocketUrl.toString());
6504
+ const headers = sanitizeHeaders(request2.headers, true);
6505
+ headers.host = target.url.host;
6506
+ upstream = http.request({
6507
+ host: target.address,
6508
+ family: target.family,
6509
+ port: Number(target.url.port || 80),
6510
+ method: request2.method,
6511
+ path: `${target.url.pathname}${target.url.search}`,
6512
+ headers,
6513
+ agent: false
6514
+ });
6515
+ upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
6516
+ writeRawResponseHead(client, response);
6517
+ if (head.length > 0) upstreamSocket.write(head);
6518
+ if (upstreamHead.length > 0) client.write(upstreamHead);
6519
+ upstreamSocket.pipe(client);
6520
+ client.pipe(upstreamSocket);
6521
+ client.once("close", () => upstreamSocket.destroy());
6522
+ });
6523
+ upstream.once("response", (response) => {
6524
+ writeRawResponseHead(client, response);
6525
+ response.pipe(client);
6526
+ });
6527
+ upstream.once("error", () => {
6528
+ client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
6529
+ });
6530
+ client.once("close", () => upstream?.destroy());
6531
+ upstream.end();
6532
+ } catch {
6533
+ upstream?.destroy();
6534
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
6535
+ }
6536
+ }
6537
+ resolve(rawUrl) {
6538
+ return resolvePinnedBrowserTarget(rawUrl, {
6539
+ allowPrivateHosts: this.options.allowPrivateHosts,
6540
+ allowedPrivateOrigins: this.options.allowedPrivateOrigins,
6541
+ lookup: this.options.lookup
6542
+ });
6543
+ }
6544
+ };
6545
+ function sanitizeHeaders(headers, preserveUpgrade = false) {
6546
+ const sanitized = { ...headers };
6547
+ delete sanitized["proxy-authorization"];
6548
+ delete sanitized["proxy-connection"];
6549
+ if (!preserveUpgrade) delete sanitized.connection;
6550
+ return sanitized;
6551
+ }
6552
+ function writeRawResponseHead(client, response) {
6553
+ const status = response.statusCode ?? 502;
6554
+ const statusText = response.statusMessage ?? "Bad Gateway";
6555
+ const lines = [`HTTP/1.1 ${status} ${statusText}`];
6556
+ for (let index = 0; index < response.rawHeaders.length; index += 2) {
6557
+ lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
6558
+ }
6559
+ client.write(`${lines.join("\r\n")}\r
6560
+ \r
6561
+ `);
6562
+ }
6563
+ function writeProxyError(response, status, message) {
6564
+ if (response.headersSent || response.destroyed) return;
6565
+ response.writeHead(status, { "content-type": "text/plain", connection: "close" });
6566
+ response.end(message);
6567
+ }
6568
+
6569
+ // src/browser/manager.ts
6570
+ var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
6571
+ var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
6572
+ var DEFAULT_MAX_ENTRIES = 100;
6573
+ var BrowserSessionManager = class {
6574
+ constructor(options, launcher = defaultLauncher) {
6575
+ this.launcher = launcher;
6576
+ this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6577
+ this.allowPrivateHosts = options.allowPrivateHosts ?? true;
6578
+ this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6579
+ this.networkProxy = new BrowserNetworkGuardProxy({
6580
+ allowPrivateHosts: this.allowPrivateHosts,
6581
+ allowedPrivateOrigins: this.allowedPrivateOrigins
6582
+ });
6583
+ this.headless = options.headless ?? true;
6584
+ this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
6585
+ this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
6586
+ this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
6587
+ this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
6588
+ }
6589
+ launcher;
6590
+ sessions = /* @__PURE__ */ new Map();
6591
+ artifacts;
6592
+ allowPrivateHosts;
6593
+ allowedPrivateOrigins;
6594
+ networkProxy;
6595
+ headless;
6596
+ operationTimeoutMs;
6597
+ maxSnapshotChars;
6598
+ maxConsoleEntries;
6599
+ maxNetworkEntries;
6600
+ browser;
6601
+ launching;
6602
+ async open(ownerId, input, signal) {
6603
+ signal.throwIfAborted();
6604
+ if (input.url) {
6605
+ await assertBrowserUrlAllowed(input.url, {
6606
+ allowPrivateHosts: this.allowPrivateHosts,
6607
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6608
+ navigation: true
6609
+ });
6610
+ }
6611
+ const browser = await this.ensureBrowser(signal);
6612
+ const proxyServer = await this.networkProxy.start();
6613
+ const context = await abortable(
6614
+ signal,
6615
+ async () => {
6616
+ const created = await browser.newContext({
6617
+ acceptDownloads: false,
6618
+ ignoreHTTPSErrors: false,
6619
+ serviceWorkers: "block",
6620
+ proxy: { server: proxyServer },
6621
+ viewport: input.viewport ?? { width: 1440, height: 900 }
6622
+ });
6623
+ if (signal.aborted) {
6624
+ await created.close().catch(() => void 0);
6625
+ signal.throwIfAborted();
6626
+ }
6627
+ return created;
6628
+ },
6629
+ () => void 0
6630
+ );
6631
+ const session = {
6632
+ id: ulid2(),
6633
+ ownerId,
6634
+ context,
6635
+ page: await context.newPage(),
6636
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6637
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
6638
+ tracing: input.trace ?? true,
6639
+ console: [],
6640
+ network: []
6641
+ };
6642
+ this.sessions.set(session.id, session);
6643
+ this.attachEvidenceCollectors(session);
6644
+ await context.route("**/*", async (route) => {
6645
+ try {
6646
+ await assertBrowserUrlAllowed(route.request().url(), {
6647
+ allowPrivateHosts: this.allowPrivateHosts,
6648
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6649
+ navigation: false
6650
+ });
6651
+ await route.continue();
6652
+ } catch {
6653
+ this.pushNetwork(session, {
6654
+ method: route.request().method(),
6655
+ url: safeBrowserUrl(route.request().url()),
6656
+ failed: true,
6657
+ at: (/* @__PURE__ */ new Date()).toISOString()
6658
+ });
6659
+ await route.abort("blockedbyclient");
6660
+ }
6661
+ });
6662
+ try {
6663
+ if (session.tracing) {
6664
+ await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
6665
+ }
6666
+ if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
6667
+ return await this.summary(session);
6668
+ } catch (err) {
6669
+ await context.close().catch(() => void 0);
6670
+ this.sessions.delete(session.id);
6671
+ await this.closeBrowserIfIdle();
6672
+ throw err;
6673
+ }
6674
+ }
6675
+ async list(ownerId) {
6676
+ const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
6677
+ return Promise.all(owned.map((session) => this.summary(session)));
6678
+ }
6679
+ async navigate(id, ownerId, rawUrl, signal) {
6680
+ await assertBrowserUrlAllowed(rawUrl, {
6681
+ allowPrivateHosts: this.allowPrivateHosts,
6682
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6683
+ navigation: true
6684
+ });
6685
+ const session = this.requireOwned(id, ownerId);
6686
+ await this.runPageOperation(
6687
+ session,
6688
+ signal,
6689
+ () => session.page.goto(rawUrl, {
6690
+ waitUntil: "domcontentloaded",
6691
+ timeout: this.operationTimeoutMs
6692
+ })
6693
+ );
6694
+ return this.summary(session);
6695
+ }
6696
+ async snapshot(id, ownerId, signal) {
6697
+ const session = this.requireOwned(id, ownerId);
6698
+ const raw = await this.runPageOperation(session, signal, async () => {
6699
+ try {
6700
+ return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
6701
+ } catch {
6702
+ return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
6703
+ }
6704
+ });
6705
+ const aria = redactBrowserText(raw);
6706
+ const truncated = aria.length > this.maxSnapshotChars;
6707
+ return {
6708
+ session: await this.summary(session),
6709
+ aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
6710
+ \u2026[snapshot truncated]` : aria,
6711
+ console: session.console.map((entry) => ({ ...entry })),
6712
+ network: session.network.map((entry) => ({ ...entry })),
6713
+ truncated
6714
+ };
6715
+ }
6716
+ async screenshot(id, ownerId, input, signal) {
6717
+ const session = this.requireOwned(id, ownerId);
6718
+ const bytes = await this.runPageOperation(
6719
+ session,
6720
+ signal,
6721
+ () => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
6722
+ );
6723
+ signal.throwIfAborted();
6724
+ return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
6725
+ }
6726
+ async click(id, ownerId, selector, signal) {
6727
+ const session = this.requireOwned(id, ownerId);
6728
+ await this.runPageOperation(
6729
+ session,
6730
+ signal,
6731
+ () => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
6732
+ );
6733
+ }
6734
+ async type(id, ownerId, selector, text, signal) {
6735
+ const session = this.requireOwned(id, ownerId);
6736
+ await this.runPageOperation(
6737
+ session,
6738
+ signal,
6739
+ () => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
6740
+ );
6741
+ }
6742
+ async select(id, ownerId, selector, value, signal) {
6743
+ const session = this.requireOwned(id, ownerId);
6744
+ await this.runPageOperation(
6745
+ session,
6746
+ signal,
6747
+ () => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
6748
+ );
6749
+ }
6750
+ async press(id, ownerId, key, signal) {
6751
+ const session = this.requireOwned(id, ownerId);
6752
+ await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
6753
+ }
6754
+ async hover(id, ownerId, selector, signal) {
6755
+ const session = this.requireOwned(id, ownerId);
6756
+ await this.runPageOperation(
6757
+ session,
6758
+ signal,
6759
+ () => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
6760
+ );
6761
+ }
6762
+ async wait(id, ownerId, input, signal) {
6763
+ const session = this.requireOwned(id, ownerId);
6764
+ const timeout = Math.min(
6765
+ Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
6766
+ this.operationTimeoutMs
6767
+ );
6768
+ await this.runPageOperation(
6769
+ session,
6770
+ signal,
6771
+ () => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
6772
+ );
6773
+ }
6774
+ async drag(id, ownerId, from, to, signal) {
6775
+ const session = this.requireOwned(id, ownerId);
6776
+ await this.runPageOperation(
6777
+ session,
6778
+ signal,
6779
+ () => session.page.locator(from).dragTo(session.page.locator(to), {
6780
+ timeout: this.operationTimeoutMs
6781
+ })
6782
+ );
6783
+ }
6784
+ async upload(id, ownerId, selector, files, projectRoot, signal) {
6785
+ const session = this.requireOwned(id, ownerId);
6786
+ const root = path11.resolve(projectRoot);
6787
+ const realRoot = await fs8.realpath(root);
6788
+ const resolved = [];
6789
+ for (const file of files) {
6790
+ const absolute = path11.resolve(root, file);
6791
+ const relative12 = path11.relative(root, absolute);
6792
+ if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
6793
+ throw new Error("browser: upload files must stay inside the project root");
6794
+ }
6795
+ const realFile = await fs8.realpath(absolute);
6796
+ const realRelative = path11.relative(realRoot, realFile);
6797
+ if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
6798
+ throw new Error("browser: upload files must not escape the project root through a symlink");
6799
+ }
6800
+ const stat17 = await fs8.stat(realFile);
6801
+ if (!stat17.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
6802
+ resolved.push(realFile);
6803
+ }
6804
+ await this.runPageOperation(
6805
+ session,
6806
+ signal,
6807
+ () => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
6808
+ );
6809
+ }
6810
+ async evaluate(id, ownerId, expression, signal) {
6811
+ const session = this.requireOwned(id, ownerId);
6812
+ const result = await this.runPageOperation(
6813
+ session,
6814
+ signal,
6815
+ () => session.page.evaluate(expression)
6816
+ );
6817
+ const serialized = JSON.stringify(result);
6818
+ if (serialized && serialized.length > this.maxSnapshotChars) {
6819
+ throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
6820
+ }
6821
+ return result;
6822
+ }
6823
+ async close(id, ownerId) {
6824
+ const session = this.requireOwned(id, ownerId);
6825
+ let trace;
6826
+ try {
6827
+ if (session.tracing) {
6828
+ const target = this.artifacts.pathFor(session.id, "zip");
6829
+ await fs8.mkdir(path11.dirname(target.path), { recursive: true });
6830
+ await session.context.tracing.stop({ path: target.path });
6831
+ trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
6832
+ }
6833
+ } finally {
6834
+ await session.context.close().catch(() => void 0);
6835
+ this.sessions.delete(id);
6836
+ await this.closeBrowserIfIdle();
6837
+ }
6838
+ return trace ? { trace } : {};
6839
+ }
6840
+ async closeOwner(ownerId) {
6841
+ const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
6842
+ for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
6843
+ }
6844
+ async dispose() {
6845
+ for (const session of [...this.sessions.values()]) {
6846
+ await session.context.close().catch(() => void 0);
6847
+ }
6848
+ this.sessions.clear();
6849
+ const browser = this.browser;
6850
+ this.browser = void 0;
6851
+ this.launching = void 0;
6852
+ await browser?.close().catch(() => void 0);
6853
+ await this.networkProxy.close();
6854
+ }
6855
+ async ensureBrowser(signal) {
6856
+ if (this.browser?.isConnected()) return this.browser;
6857
+ if (!this.launching) {
6858
+ this.launching = this.launcher(this.headless).then((browser) => {
6859
+ this.browser = browser;
6860
+ browser.on("disconnected", () => {
6861
+ if (this.browser === browser) this.browser = void 0;
6862
+ for (const [id, session] of this.sessions) {
6863
+ if (session.context.browser() === browser) this.sessions.delete(id);
6864
+ }
6865
+ });
6866
+ return browser;
6867
+ });
6868
+ }
6869
+ try {
6870
+ return await abortable(
6871
+ signal,
6872
+ () => this.launching,
6873
+ async () => {
6874
+ const browser = await this.launching?.catch(() => void 0);
6875
+ await browser?.close().catch(() => void 0);
6876
+ }
6877
+ );
6878
+ } finally {
6879
+ this.launching = void 0;
6880
+ }
6881
+ }
6882
+ requireOwned(id, ownerId) {
6883
+ const session = this.sessions.get(id);
6884
+ if (!session) throw new Error(`browser: unknown session "${id}"`);
6885
+ if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
6886
+ session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
6887
+ return session;
6888
+ }
6889
+ async summary(session) {
6890
+ return {
6891
+ id: session.id,
6892
+ ownerId: session.ownerId,
6893
+ url: safeBrowserUrl(session.page.url()),
6894
+ title: redactBrowserText(await session.page.title().catch(() => "")),
6895
+ createdAt: session.createdAt,
6896
+ lastUsedAt: session.lastUsedAt,
6897
+ tracing: session.tracing
6898
+ };
6899
+ }
6900
+ attachEvidenceCollectors(session) {
6901
+ session.page.on("console", (message) => {
6902
+ pushBounded(
6903
+ session.console,
6904
+ {
6905
+ level: message.type(),
6906
+ text: redactBrowserText(message.text()).slice(0, 4096),
6907
+ at: (/* @__PURE__ */ new Date()).toISOString()
6908
+ },
6909
+ this.maxConsoleEntries
6910
+ );
6911
+ });
6912
+ session.page.on("response", (response) => {
6913
+ this.pushNetwork(session, {
6914
+ method: response.request().method(),
6915
+ url: safeBrowserUrl(response.url()),
6916
+ status: response.status(),
6917
+ at: (/* @__PURE__ */ new Date()).toISOString()
6918
+ });
6919
+ });
6920
+ session.page.on("requestfailed", (request2) => {
6921
+ this.pushNetwork(session, {
6922
+ method: request2.method(),
6923
+ url: safeBrowserUrl(request2.url()),
6924
+ failed: true,
6925
+ at: (/* @__PURE__ */ new Date()).toISOString()
6926
+ });
6927
+ });
6928
+ }
6929
+ pushNetwork(session, entry) {
6930
+ pushBounded(session.network, entry, this.maxNetworkEntries);
6931
+ }
6932
+ async runPageOperation(session, signal, operation) {
6933
+ signal.throwIfAborted();
6934
+ session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
6935
+ return abortable(signal, operation, async () => {
6936
+ await session.context.close().catch(() => void 0);
6937
+ this.sessions.delete(session.id);
6938
+ await this.closeBrowserIfIdle();
6939
+ });
6940
+ }
6941
+ async closeBrowserIfIdle() {
6942
+ if (this.sessions.size > 0 || !this.browser) return;
6943
+ const browser = this.browser;
6944
+ this.browser = void 0;
6945
+ await browser.close().catch(() => void 0);
6946
+ }
6947
+ };
6948
+ async function defaultLauncher(headless) {
6949
+ try {
6950
+ const { chromium } = await import("@playwright/test");
6951
+ return await chromium.launch({ headless });
6952
+ } catch (err) {
6953
+ const detail = err instanceof Error ? err.message : String(err);
6954
+ throw new Error(
6955
+ `browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
6956
+ );
6957
+ }
6958
+ }
6959
+ async function browserInstallationDiagnostics() {
6960
+ try {
6961
+ const { chromium } = await import("@playwright/test");
6962
+ const executablePath = chromium.executablePath();
6963
+ await fs8.access(executablePath);
6964
+ return { available: true, executablePath, message: "Playwright Chromium is available." };
6965
+ } catch {
6966
+ return {
6967
+ available: false,
6968
+ message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
6969
+ };
6970
+ }
6971
+ }
6972
+ function pushBounded(target, value, limit) {
6973
+ target.push(value);
6974
+ if (target.length > limit) target.splice(0, target.length - limit);
6975
+ }
6976
+ async function abortable(signal, operation, onAbort) {
6977
+ signal.throwIfAborted();
6978
+ return new Promise((resolve14, reject) => {
6979
+ let settled = false;
6980
+ let aborting = false;
6981
+ const finish = (fn) => {
6982
+ if (settled) return;
6983
+ settled = true;
6984
+ signal.removeEventListener("abort", abort);
6985
+ fn();
6986
+ };
6987
+ const abort = () => {
6988
+ aborting = true;
6989
+ void Promise.resolve(onAbort()).finally(() => {
6990
+ finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
6991
+ });
6992
+ };
6993
+ signal.addEventListener("abort", abort, { once: true });
6994
+ operation().then(
6995
+ (value) => {
6996
+ if (!aborting) finish(() => resolve14(value));
6997
+ },
6998
+ (err) => {
6999
+ if (!aborting) finish(() => reject(err));
7000
+ }
7001
+ );
7002
+ });
7003
+ }
7004
+
7005
+ // src/browser/tools.ts
7006
+ var managers = /* @__PURE__ */ new Map();
7007
+ var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
7008
+ function managerFor(ctx) {
7009
+ const root = path12.resolve(ctx.projectRoot);
7010
+ let manager = managers.get(root);
7011
+ if (!manager) {
7012
+ const wpaths = resolveWstackPaths({ projectRoot: root });
7013
+ manager = new BrowserSessionManager({
7014
+ artifactRoot: path12.join(wpaths.projectDir, "browser-artifacts"),
7015
+ allowedPrivateOrigins: parsePrivateOriginAllowlist(
7016
+ process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
7017
+ )
7018
+ });
7019
+ managers.set(root, manager);
7020
+ }
7021
+ const runSignal = ctx.signal;
7022
+ if (cleanupSignalByContext.get(ctx) !== runSignal) {
7023
+ cleanupSignalByContext.set(ctx, runSignal);
7024
+ const managerForRun = manager;
7025
+ const ownerForRun = owner(ctx);
7026
+ ctx.registerAbortHook?.(async () => {
7027
+ if (cleanupSignalByContext.get(ctx) === runSignal) {
7028
+ cleanupSignalByContext.delete(ctx);
7029
+ }
7030
+ await managerForRun.closeOwner(ownerForRun);
7031
+ });
7032
+ }
7033
+ return manager;
7034
+ }
7035
+ function owner(ctx) {
7036
+ return ctx.agentId || "leader";
7037
+ }
7038
+ function json(value) {
7039
+ return JSON.stringify(value, null, 2);
7040
+ }
7041
+ var sessionIdSchema = {
7042
+ type: "string",
7043
+ minLength: 1,
7044
+ description: "Browser session id returned by browser_open."
7045
+ };
7046
+ var browserOpenTool = {
7047
+ name: "browser_open",
7048
+ description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
7049
+ usageHint: "browser_open({ url?, width?, height?, trace? })",
7050
+ permission: "confirm",
7051
+ mutating: true,
7052
+ riskTier: "standard",
7053
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7054
+ subjectKey: "url",
7055
+ inputSchema: {
7056
+ type: "object",
7057
+ properties: {
7058
+ url: { type: "string", description: "Optional absolute http(s) URL." },
7059
+ width: { type: "integer", minimum: 320, maximum: 3840 },
7060
+ height: { type: "integer", minimum: 240, maximum: 2160 },
7061
+ trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
7062
+ }
7063
+ },
7064
+ async execute(input, ctx, opts) {
7065
+ const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
7066
+ return json(
7067
+ await managerFor(ctx).open(
7068
+ owner(ctx),
7069
+ { url: input.url, viewport, trace: input.trace },
7070
+ opts.signal
7071
+ )
7072
+ );
7073
+ }
7074
+ };
7075
+ var browserListTool = {
7076
+ name: "browser_list",
7077
+ description: "List browser sessions owned by the current agent without exposing other agents sessions.",
7078
+ permission: "auto",
7079
+ mutating: false,
7080
+ riskTier: "safe",
7081
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7082
+ inputSchema: { type: "object", properties: {} },
7083
+ async execute(_input, ctx) {
7084
+ return json(await managerFor(ctx).list(owner(ctx)));
7085
+ }
7086
+ };
7087
+ var browserStatusTool = {
7088
+ name: "browser_status",
7089
+ description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
7090
+ permission: "auto",
7091
+ mutating: false,
7092
+ riskTier: "safe",
7093
+ capabilities: [ToolCapabilities.TOOL_META],
7094
+ inputSchema: { type: "object", properties: {} },
7095
+ async execute() {
7096
+ return json(await browserInstallationDiagnostics());
7097
+ }
7098
+ };
7099
+ var browserNavigateTool = {
7100
+ name: "browser_navigate",
7101
+ description: "Navigate an owned browser session to an approved http(s) URL.",
7102
+ usageHint: "browser_navigate({ sessionId, url })",
7103
+ permission: "confirm",
7104
+ mutating: true,
7105
+ riskTier: "standard",
7106
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7107
+ subjectKey: "url",
7108
+ inputSchema: {
7109
+ type: "object",
7110
+ properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
7111
+ required: ["sessionId", "url"]
7112
+ },
7113
+ async execute(input, ctx, opts) {
7114
+ return json(
7115
+ await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
7116
+ );
7117
+ }
7118
+ };
7119
+ var browserSnapshotTool = {
7120
+ name: "browser_snapshot",
7121
+ description: "Return bounded accessibility state plus redacted console and network summaries.",
7122
+ usageHint: "browser_snapshot({ sessionId })",
7123
+ permission: "auto",
7124
+ mutating: false,
7125
+ riskTier: "safe",
7126
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7127
+ inputSchema: {
7128
+ type: "object",
7129
+ properties: { sessionId: sessionIdSchema },
7130
+ required: ["sessionId"]
7131
+ },
7132
+ async execute(input, ctx, opts) {
7133
+ return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
7134
+ }
7135
+ };
7136
+ var browserScreenshotTool = {
7137
+ name: "browser_screenshot",
7138
+ description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
7139
+ usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
7140
+ permission: "confirm",
7141
+ mutating: true,
7142
+ riskTier: "safe",
7143
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7144
+ inputSchema: {
7145
+ type: "object",
7146
+ properties: {
7147
+ sessionId: sessionIdSchema,
7148
+ fullPage: { type: "boolean" },
7149
+ selector: { type: "string", minLength: 1 }
7150
+ },
7151
+ required: ["sessionId"]
7152
+ },
7153
+ async execute(input, ctx, opts) {
7154
+ return json(
7155
+ await managerFor(ctx).screenshot(
7156
+ input.sessionId,
7157
+ owner(ctx),
7158
+ { fullPage: input.fullPage, selector: input.selector },
7159
+ opts.signal
7160
+ )
7161
+ );
7162
+ }
7163
+ };
7164
+ function selectorTool(name, description, run) {
7165
+ return {
7166
+ name,
7167
+ description,
7168
+ usageHint: `${name}({ sessionId, selector })`,
7169
+ permission: "confirm",
7170
+ mutating: true,
7171
+ riskTier: "standard",
7172
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7173
+ inputSchema: {
7174
+ type: "object",
7175
+ properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
7176
+ required: ["sessionId", "selector"]
7177
+ },
7178
+ async execute(input, ctx, opts) {
7179
+ await run(managerFor(ctx), input, owner(ctx), opts.signal);
7180
+ return "ok";
7181
+ }
7182
+ };
7183
+ }
7184
+ var browserClickTool = selectorTool(
7185
+ "browser_click",
7186
+ "Click an element in an owned browser session.",
7187
+ (manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
7188
+ );
7189
+ var browserHoverTool = selectorTool(
7190
+ "browser_hover",
7191
+ "Hover an element in an owned browser session.",
7192
+ (manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
7193
+ );
7194
+ var browserTypeTool = {
7195
+ name: "browser_type",
7196
+ description: "Fill an input in an owned browser session. Use secretEnv instead of text for credentials so values stay out of tool arguments and session audit.",
7197
+ usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
7198
+ permission: "confirm",
7199
+ mutating: true,
7200
+ riskTier: "standard",
7201
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7202
+ inputSchema: {
7203
+ type: "object",
7204
+ properties: {
7205
+ sessionId: sessionIdSchema,
7206
+ selector: { type: "string", minLength: 1 },
7207
+ text: { type: "string" },
7208
+ secretEnv: {
7209
+ type: "string",
7210
+ pattern: "^[A-Z_][A-Z0-9_]*$",
7211
+ description: "Host environment variable resolved only at execution time."
7212
+ }
7213
+ },
7214
+ required: ["sessionId", "selector"]
7215
+ },
7216
+ validate(input) {
7217
+ return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
7218
+ },
7219
+ async execute(input, ctx, opts) {
7220
+ const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
7221
+ if (value === void 0) {
7222
+ throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
7223
+ }
7224
+ await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
7225
+ return "ok";
7226
+ }
7227
+ };
7228
+ var browserSelectTool = {
7229
+ name: "browser_select",
7230
+ description: "Select an option in an owned browser session.",
7231
+ usageHint: "browser_select({ sessionId, selector, value })",
7232
+ permission: "confirm",
7233
+ mutating: true,
7234
+ riskTier: "standard",
7235
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7236
+ inputSchema: {
7237
+ type: "object",
7238
+ properties: {
7239
+ sessionId: sessionIdSchema,
7240
+ selector: { type: "string", minLength: 1 },
7241
+ value: { type: "string" }
7242
+ },
7243
+ required: ["sessionId", "selector", "value"]
7244
+ },
7245
+ async execute(input, ctx, opts) {
7246
+ await managerFor(ctx).select(
7247
+ input.sessionId,
7248
+ owner(ctx),
7249
+ input.selector,
7250
+ input.value,
7251
+ opts.signal
7252
+ );
7253
+ return "ok";
7254
+ }
7255
+ };
7256
+ var browserPressTool = {
7257
+ name: "browser_press",
7258
+ description: "Press a keyboard key in an owned browser session.",
7259
+ usageHint: "browser_press({ sessionId, key })",
7260
+ permission: "confirm",
7261
+ mutating: true,
7262
+ riskTier: "standard",
7263
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7264
+ inputSchema: {
7265
+ type: "object",
7266
+ properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
7267
+ required: ["sessionId", "key"]
7268
+ },
7269
+ async execute(input, ctx, opts) {
7270
+ await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
7271
+ return "ok";
7272
+ }
7273
+ };
7274
+ var browserDragTool = {
7275
+ name: "browser_drag",
7276
+ description: "Drag one element to another in an owned browser session.",
7277
+ usageHint: "browser_drag({ sessionId, from, to })",
7278
+ permission: "confirm",
7279
+ mutating: true,
7280
+ riskTier: "standard",
7281
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7282
+ inputSchema: {
7283
+ type: "object",
7284
+ properties: {
7285
+ sessionId: sessionIdSchema,
7286
+ from: { type: "string", minLength: 1 },
7287
+ to: { type: "string", minLength: 1 }
7288
+ },
7289
+ required: ["sessionId", "from", "to"]
7290
+ },
7291
+ async execute(input, ctx, opts) {
7292
+ await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
7293
+ return "ok";
7294
+ }
7295
+ };
7296
+ var browserWaitTool = {
7297
+ name: "browser_wait",
7298
+ description: "Wait for an element or a bounded duration in an owned browser session.",
7299
+ usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
7300
+ permission: "auto",
7301
+ mutating: false,
7302
+ riskTier: "safe",
7303
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7304
+ inputSchema: {
7305
+ type: "object",
7306
+ properties: {
7307
+ sessionId: sessionIdSchema,
7308
+ selector: { type: "string", minLength: 1 },
7309
+ timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
7310
+ },
7311
+ required: ["sessionId"]
7312
+ },
7313
+ async execute(input, ctx, opts) {
7314
+ await managerFor(ctx).wait(
7315
+ input.sessionId,
7316
+ owner(ctx),
7317
+ { selector: input.selector, timeoutMs: input.timeoutMs },
7318
+ opts.signal
7319
+ );
7320
+ return "ok";
7321
+ }
7322
+ };
7323
+ var browserEvaluateTool = {
7324
+ name: "browser_evaluate",
7325
+ description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
7326
+ usageHint: "browser_evaluate({ sessionId, expression })",
7327
+ permission: "confirm",
7328
+ mutating: true,
7329
+ riskTier: "destructive",
7330
+ capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
7331
+ inputSchema: {
7332
+ type: "object",
7333
+ properties: {
7334
+ sessionId: sessionIdSchema,
7335
+ expression: { type: "string", minLength: 1, maxLength: 16384 }
7336
+ },
7337
+ required: ["sessionId", "expression"]
7338
+ },
7339
+ async execute(input, ctx, opts) {
7340
+ return json(
7341
+ await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
7342
+ );
7343
+ }
7344
+ };
7345
+ var browserUploadTool = {
7346
+ name: "browser_upload",
7347
+ description: "Upload project-local files through a file input in an owned browser session.",
7348
+ usageHint: "browser_upload({ sessionId, selector, files })",
7349
+ permission: "confirm",
7350
+ mutating: true,
7351
+ riskTier: "destructive",
7352
+ capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
7353
+ inputSchema: {
7354
+ type: "object",
7355
+ properties: {
7356
+ sessionId: sessionIdSchema,
7357
+ selector: { type: "string", minLength: 1 },
7358
+ files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
7359
+ },
7360
+ required: ["sessionId", "selector", "files"]
7361
+ },
7362
+ async execute(input, ctx, opts) {
7363
+ await managerFor(ctx).upload(
7364
+ input.sessionId,
7365
+ owner(ctx),
7366
+ input.selector,
7367
+ input.files,
7368
+ ctx.projectRoot,
7369
+ opts.signal
7370
+ );
7371
+ return "ok";
7372
+ }
7373
+ };
7374
+ var browserCloseTool = {
7375
+ name: "browser_close",
7376
+ description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
7377
+ usageHint: "browser_close({ sessionId })",
7378
+ permission: "auto",
7379
+ mutating: false,
7380
+ riskTier: "safe",
7381
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7382
+ inputSchema: {
7383
+ type: "object",
7384
+ properties: { sessionId: sessionIdSchema },
7385
+ required: ["sessionId"]
7386
+ },
7387
+ async execute(input, ctx) {
7388
+ return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
6159
7389
  }
6160
- }
7390
+ };
7391
+ var browserTools = [
7392
+ browserOpenTool,
7393
+ browserStatusTool,
7394
+ browserListTool,
7395
+ browserNavigateTool,
7396
+ browserSnapshotTool,
7397
+ browserScreenshotTool,
7398
+ browserClickTool,
7399
+ browserTypeTool,
7400
+ browserSelectTool,
7401
+ browserPressTool,
7402
+ browserHoverTool,
7403
+ browserDragTool,
7404
+ browserWaitTool,
7405
+ browserEvaluateTool,
7406
+ browserUploadTool,
7407
+ browserCloseTool
7408
+ ];
7409
+ for (const tool of browserTools) tool.icon = "web";
7410
+ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
6161
7411
 
6162
7412
  // src/codebase-index/writer.ts
6163
7413
  import { expectDefined as expectDefined2 } from "@wrongstack/core";
@@ -6245,9 +7495,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
6245
7495
  // src/codebase-index/writer.ts
6246
7496
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
6247
7497
  import { createRequire } from "node:module";
6248
- import * as fs7 from "node:fs";
6249
- import * as path10 from "node:path";
6250
- import { resolveWstackPaths } from "@wrongstack/core";
7498
+ import * as fs9 from "node:fs";
7499
+ import * as path13 from "node:path";
7500
+ import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
6251
7501
 
6252
7502
  // src/codebase-index/schema.ts
6253
7503
  var SCHEMA_VERSION = 2;
@@ -6376,7 +7626,7 @@ var Bm25Index = class {
6376
7626
  // src/codebase-index/writer.ts
6377
7627
  var DB_FILE = "index.db";
6378
7628
  function resolveIndexDir(projectRoot, override) {
6379
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
7629
+ return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
6380
7630
  }
6381
7631
  function codebaseIndexDirOverride(ctx) {
6382
7632
  const v = ctx.meta?.["codebaseIndexDir"];
@@ -6491,9 +7741,9 @@ var IndexStore = class {
6491
7741
  }
6492
7742
  constructor(projectRoot, opts = {}) {
6493
7743
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6494
- fs7.mkdirSync(this.indexDir, { recursive: true });
7744
+ fs9.mkdirSync(this.indexDir, { recursive: true });
6495
7745
  const Database = loadDatabaseSync();
6496
- this.db = new Database(path10.join(this.indexDir, DB_FILE));
7746
+ this.db = new Database(path13.join(this.indexDir, DB_FILE));
6497
7747
  try {
6498
7748
  this.db.exec("PRAGMA journal_mode = WAL");
6499
7749
  this.db.exec("PRAGMA busy_timeout = 5000");
@@ -7072,9 +8322,9 @@ var IndexStore = class {
7072
8322
  }));
7073
8323
  }
7074
8324
  sizeBytes() {
7075
- const dbPath = path10.join(this.indexDir, DB_FILE);
8325
+ const dbPath = path13.join(this.indexDir, DB_FILE);
7076
8326
  try {
7077
- return fs7.statSync(dbPath).size;
8327
+ return fs9.statSync(dbPath).size;
7078
8328
  } catch {
7079
8329
  return 0;
7080
8330
  }
@@ -7089,14 +8339,14 @@ var IndexStore = class {
7089
8339
  };
7090
8340
 
7091
8341
  // src/codebase-index/background-indexer.ts
7092
- import * as fs13 from "node:fs";
8342
+ import * as fs15 from "node:fs";
7093
8343
  import { fileURLToPath } from "node:url";
7094
8344
  import { Worker } from "node:worker_threads";
7095
8345
 
7096
8346
  // src/codebase-index/indexer.ts
7097
8347
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
7098
- import * as fs12 from "node:fs/promises";
7099
- import * as path16 from "node:path";
8348
+ import * as fs14 from "node:fs/promises";
8349
+ import * as path19 from "node:path";
7100
8350
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
7101
8351
 
7102
8352
  // src/codebase-index/ts-parser.ts
@@ -7287,10 +8537,11 @@ function detectLang(file) {
7287
8537
  }
7288
8538
 
7289
8539
  // src/codebase-index/go-parser.ts
8540
+ init_win32_resolve();
7290
8541
  import { spawn as spawn4 } from "node:child_process";
7291
8542
  import * as os5 from "node:os";
7292
- import * as path11 from "node:path";
7293
- import * as fs8 from "node:fs/promises";
8543
+ import * as path14 from "node:path";
8544
+ import * as fs10 from "node:fs/promises";
7294
8545
  async function parseSymbols2(opts) {
7295
8546
  const { file, content, lang } = opts;
7296
8547
  try {
@@ -7591,31 +8842,46 @@ func formatType(t ast.Expr) string {
7591
8842
  }
7592
8843
  `;
7593
8844
  async function syncGoParse(filePath, content, lang) {
7594
- const tmpDir = await fs8.mkdtemp(path11.join(os5.tmpdir(), "ws-go-parse-"));
8845
+ const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
7595
8846
  try {
7596
- const scriptPath = path11.join(tmpDir, "parse.go");
7597
- await fs8.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
7598
- const proc = spawn4("go", ["run", scriptPath], {
7599
- stdio: ["pipe", "pipe", "pipe"],
7600
- windowsHide: true
7601
- });
7602
- let stdout = "";
7603
- proc.stdout?.on("data", (chunk) => {
7604
- stdout += chunk.toString();
7605
- });
7606
- proc.stdin?.write(content);
7607
- proc.stdin?.end();
7608
- const { code } = await Promise.race([
7609
- new Promise((resolve11) => {
7610
- proc.on("close", (c) => resolve11({ code: c }));
7611
- }),
7612
- new Promise(
7613
- (_, reject) => setTimeout(() => {
8847
+ const scriptPath = path14.join(tmpDir, "parse.go");
8848
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
8849
+ const goBinary = resolveWin32Command("go");
8850
+ const goResult = await new Promise(
8851
+ (resolve14, reject) => {
8852
+ let settled = false;
8853
+ const proc = spawn4(goBinary, ["run", scriptPath], {
8854
+ stdio: ["pipe", "pipe", "pipe"],
8855
+ windowsHide: true
8856
+ });
8857
+ proc.on("error", (err) => {
8858
+ if (settled) return;
8859
+ settled = true;
8860
+ reject(err);
8861
+ });
8862
+ let stdout2 = "";
8863
+ proc.stdout?.on("data", (chunk) => {
8864
+ stdout2 += chunk.toString();
8865
+ });
8866
+ proc.stderr?.resume();
8867
+ proc.stdin?.write(content);
8868
+ proc.stdin?.end();
8869
+ const timer = setTimeout(() => {
8870
+ if (settled) return;
8871
+ settled = true;
7614
8872
  proc.kill("SIGKILL");
7615
8873
  reject(new Error("timeout"));
7616
- }, 15e3)
7617
- )
7618
- ]).catch(() => ({ code: -1 }));
8874
+ }, 15e3);
8875
+ timer.unref?.();
8876
+ proc.on("close", (code2) => {
8877
+ if (settled) return;
8878
+ settled = true;
8879
+ clearTimeout(timer);
8880
+ resolve14({ code: code2, stdout: stdout2 });
8881
+ });
8882
+ }
8883
+ );
8884
+ const { code, stdout } = goResult;
7619
8885
  if (code !== 0 || !stdout.trim()) {
7620
8886
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7621
8887
  }
@@ -7637,15 +8903,16 @@ async function syncGoParse(filePath, content, lang) {
7637
8903
  } catch {
7638
8904
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7639
8905
  } finally {
7640
- await fs8.rm(tmpDir, { recursive: true, force: true });
8906
+ await fs10.rm(tmpDir, { recursive: true, force: true });
7641
8907
  }
7642
8908
  }
7643
8909
 
7644
8910
  // src/codebase-index/py-parser.ts
7645
- import { spawn as spawn5 } from "node:child_process";
7646
- import * as fs9 from "node:fs/promises";
8911
+ init_win32_resolve();
8912
+ import { spawn as spawn5, spawnSync } from "node:child_process";
8913
+ import * as fs11 from "node:fs/promises";
7647
8914
  import * as os6 from "node:os";
7648
- import * as path12 from "node:path";
8915
+ import * as path15 from "node:path";
7649
8916
  async function parseSymbols3(opts) {
7650
8917
  const { file, content, lang } = opts;
7651
8918
  try {
@@ -7857,36 +9124,74 @@ visitor.visit(tree)
7857
9124
 
7858
9125
  print(json.dumps([s.to_dict() for s in syms]))
7859
9126
  `;
7860
- var _cachedScriptPath = null;
7861
- async function syncPyParse(filePath, content, lang) {
7862
- try {
7863
- if (!_cachedScriptPath) {
7864
- const tmpDir = path12.join(os6.tmpdir(), "ws-py-parse");
7865
- await fs9.mkdir(tmpDir, { recursive: true });
7866
- _cachedScriptPath = path12.join(tmpDir, "parse.py");
7867
- await fs9.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
7868
- }
7869
- const proc = spawn5("python", [_cachedScriptPath, filePath], {
9127
+ function resolvePython() {
9128
+ const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9129
+ for (const name of candidates) {
9130
+ const resolved = resolveWin32Command(name);
9131
+ const result = spawnSync(resolved, ["--version"], {
9132
+ stdio: "pipe",
9133
+ timeout: 5e3
9134
+ });
9135
+ if (result.error) continue;
9136
+ if (result.status !== 0) continue;
9137
+ return resolved;
9138
+ }
9139
+ return null;
9140
+ }
9141
+ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9142
+ return new Promise((resolve14, reject) => {
9143
+ let settled = false;
9144
+ const proc = spawn5(pyBinary, [scriptPath, filePath], {
7870
9145
  stdio: ["pipe", "pipe", "pipe"],
7871
9146
  windowsHide: true
7872
9147
  });
9148
+ proc.on("error", (err) => {
9149
+ if (settled) return;
9150
+ settled = true;
9151
+ reject(err);
9152
+ });
7873
9153
  proc.stdin?.write(content);
7874
9154
  proc.stdin?.end();
7875
9155
  let stdout = "";
7876
9156
  proc.stdout?.on("data", (chunk) => {
7877
9157
  stdout += chunk.toString();
7878
9158
  });
7879
- const { code } = await Promise.race([
7880
- new Promise((resolve11) => {
7881
- proc.on("close", (c) => resolve11({ code: c }));
7882
- }),
7883
- new Promise(
7884
- (_, reject) => setTimeout(() => {
7885
- proc.kill("SIGKILL");
7886
- reject(new Error("timeout"));
7887
- }, 15e3)
7888
- )
7889
- ]).catch(() => ({ code: -1 }));
9159
+ proc.stderr?.resume();
9160
+ const timer = setTimeout(() => {
9161
+ if (settled) return;
9162
+ settled = true;
9163
+ proc.kill("SIGKILL");
9164
+ reject(new Error("timeout"));
9165
+ }, 15e3);
9166
+ timer.unref?.();
9167
+ proc.on("close", (code) => {
9168
+ if (settled) return;
9169
+ settled = true;
9170
+ clearTimeout(timer);
9171
+ resolve14({ code, stdout });
9172
+ });
9173
+ });
9174
+ }
9175
+ var _cachedScriptPath = null;
9176
+ var _cachedPyBinary = null;
9177
+ async function syncPyParse(filePath, content, lang) {
9178
+ try {
9179
+ if (!_cachedScriptPath) {
9180
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9181
+ await fs11.mkdir(tmpDir, { recursive: true });
9182
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
9183
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9184
+ }
9185
+ if (!_cachedPyBinary) {
9186
+ _cachedPyBinary = resolvePython();
9187
+ if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9188
+ }
9189
+ const { code, stdout } = await spawnPyParser(
9190
+ _cachedPyBinary,
9191
+ _cachedScriptPath,
9192
+ filePath,
9193
+ content
9194
+ );
7890
9195
  if (code !== 0 || !stdout.trim()) {
7891
9196
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7892
9197
  }
@@ -7911,10 +9216,11 @@ async function syncPyParse(filePath, content, lang) {
7911
9216
  }
7912
9217
 
7913
9218
  // src/codebase-index/rs-parser.ts
9219
+ init_win32_resolve();
7914
9220
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
7915
9221
  import { execFileSync, spawn as spawn6 } from "node:child_process";
7916
- import * as fs10 from "node:fs/promises";
7917
- import * as path13 from "node:path";
9222
+ import * as fs12 from "node:fs/promises";
9223
+ import * as path16 from "node:path";
7918
9224
  async function parseSymbols4(opts) {
7919
9225
  const { file, content, lang } = opts;
7920
9226
  const nativeAvailable = checkNativeParser();
@@ -7927,7 +9233,7 @@ async function parseSymbols4(opts) {
7927
9233
  function checkNativeParser() {
7928
9234
  try {
7929
9235
  execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
7930
- const toolsDir = path13.join(process.cwd(), "tools");
9236
+ const toolsDir = path16.join(process.cwd(), "tools");
7931
9237
  try {
7932
9238
  execFileSync(
7933
9239
  "cargo",
@@ -7937,7 +9243,7 @@ function checkNativeParser() {
7937
9243
  "--format-version",
7938
9244
  "1",
7939
9245
  "--manifest-path",
7940
- path13.join(toolsDir, "Cargo.toml")
9246
+ path16.join(toolsDir, "Cargo.toml")
7941
9247
  ],
7942
9248
  { stdio: "pipe", windowsHide: true }
7943
9249
  );
@@ -7951,34 +9257,49 @@ function checkNativeParser() {
7951
9257
  }
7952
9258
  async function tryNativeParse(file, content) {
7953
9259
  try {
7954
- const toolsDir = path13.join(process.cwd(), "tools");
7955
- const crateDir = path13.join(toolsDir, "syn-parser");
7956
- const tmpFile = path13.join(crateDir, "src", "input.rs");
7957
- await fs10.writeFile(tmpFile, content, "utf8");
7958
- const proc = spawn6(
7959
- "cargo",
7960
- ["run", "--manifest-path", path13.join(toolsDir, "Cargo.toml")],
7961
- {
7962
- cwd: process.cwd(),
7963
- stdio: ["pipe", "pipe", "pipe"],
7964
- windowsHide: true
7965
- }
7966
- );
7967
- let stdout = "";
7968
- proc.stdout?.on("data", (chunk) => {
7969
- stdout += chunk.toString();
7970
- });
7971
- const { code } = await Promise.race([
7972
- new Promise((resolve11) => {
7973
- proc.on("close", (c) => resolve11({ code: c }));
7974
- }),
7975
- new Promise(
7976
- (_, reject) => setTimeout(() => {
9260
+ const toolsDir = path16.join(process.cwd(), "tools");
9261
+ const crateDir = path16.join(toolsDir, "syn-parser");
9262
+ const tmpFile = path16.join(crateDir, "src", "input.rs");
9263
+ await fs12.writeFile(tmpFile, content, "utf8");
9264
+ const cargoBinary = resolveWin32Command("cargo");
9265
+ const result = await new Promise(
9266
+ (resolve14, reject) => {
9267
+ let settled = false;
9268
+ const proc = spawn6(
9269
+ cargoBinary,
9270
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9271
+ {
9272
+ cwd: process.cwd(),
9273
+ stdio: ["pipe", "pipe", "pipe"],
9274
+ windowsHide: true
9275
+ }
9276
+ );
9277
+ proc.on("error", (err) => {
9278
+ if (settled) return;
9279
+ settled = true;
9280
+ reject(err);
9281
+ });
9282
+ let stdout2 = "";
9283
+ proc.stdout?.on("data", (chunk) => {
9284
+ stdout2 += chunk.toString();
9285
+ });
9286
+ proc.stderr?.resume();
9287
+ const timer = setTimeout(() => {
9288
+ if (settled) return;
9289
+ settled = true;
7977
9290
  proc.kill("SIGKILL");
7978
9291
  reject(new Error("timeout"));
7979
- }, 15e3)
7980
- )
7981
- ]).catch(() => ({ code: -1 }));
9292
+ }, 15e3);
9293
+ timer.unref?.();
9294
+ proc.on("close", (c) => {
9295
+ if (settled) return;
9296
+ settled = true;
9297
+ clearTimeout(timer);
9298
+ resolve14({ code: c, stdout: stdout2 });
9299
+ });
9300
+ }
9301
+ );
9302
+ const { code, stdout } = result;
7982
9303
  if (code === 0 && stdout.trim()) {
7983
9304
  const symbols = JSON.parse(stdout.trim());
7984
9305
  return {
@@ -8061,7 +9382,7 @@ function regexParse(opts) {
8061
9382
 
8062
9383
  // src/codebase-index/json-parser.ts
8063
9384
  import { expectDefined as expectDefined4 } from "@wrongstack/core";
8064
- import * as path14 from "node:path";
9385
+ import * as path17 from "node:path";
8065
9386
  function parseSymbols5(opts) {
8066
9387
  const { file, content, lang } = opts;
8067
9388
  try {
@@ -8073,9 +9394,9 @@ function parseSymbols5(opts) {
8073
9394
  function regexParse2(opts) {
8074
9395
  const { file, content, lang } = opts;
8075
9396
  const symbols = [];
8076
- const basename7 = path14.basename(file).toLowerCase();
8077
- const isPackageJson = basename7 === "package.json";
8078
- const isTsconfig = basename7 === "tsconfig.json" || basename7 === "tsconfig.build.json";
9397
+ const basename8 = path17.basename(file).toLowerCase();
9398
+ const isPackageJson = basename8 === "package.json";
9399
+ const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
8079
9400
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
8080
9401
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
8081
9402
  const lines = content.split("\n");
@@ -8099,11 +9420,11 @@ function regexParse2(opts) {
8099
9420
  const line = lineFromOffset(offset);
8100
9421
  symbols.push(
8101
9422
  makeSymbol({
8102
- name: path14.basename(file),
9423
+ name: path17.basename(file),
8103
9424
  kind: "object",
8104
9425
  line,
8105
9426
  col: 0,
8106
- signature: `"${path14.basename(file)}" = { ... }`,
9427
+ signature: `"${path17.basename(file)}" = { ... }`,
8107
9428
  file,
8108
9429
  lang
8109
9430
  })
@@ -8414,8 +9735,8 @@ function makeSymbol2(opts) {
8414
9735
  }
8415
9736
 
8416
9737
  // src/codebase-index/gitignore.ts
8417
- import * as fs11 from "node:fs/promises";
8418
- import * as path15 from "node:path";
9738
+ import * as fs13 from "node:fs/promises";
9739
+ import * as path18 from "node:path";
8419
9740
  import { compileGlob } from "@wrongstack/core";
8420
9741
  function globBody(glob) {
8421
9742
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -8461,7 +9782,7 @@ function compileGitignore(lines) {
8461
9782
  async function loadGitignoreMatcher(projectRoot) {
8462
9783
  let lines = [];
8463
9784
  try {
8464
- const raw = await fs11.readFile(path15.join(projectRoot, ".gitignore"), "utf8");
9785
+ const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
8465
9786
  lines = raw.split("\n");
8466
9787
  } catch {
8467
9788
  }
@@ -8472,7 +9793,7 @@ async function loadGitignoreMatcher(projectRoot) {
8472
9793
  var YIELD_EVERY_N = 50;
8473
9794
  var PARALLEL_BATCH = 20;
8474
9795
  function yieldEventLoop() {
8475
- return new Promise((resolve11) => setImmediate(resolve11));
9796
+ return new Promise((resolve14) => setImmediate(resolve14));
8476
9797
  }
8477
9798
  function throwIfAborted(signal) {
8478
9799
  if (!signal?.aborted) return;
@@ -8519,21 +9840,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
8519
9840
  }
8520
9841
  let entries;
8521
9842
  try {
8522
- entries = await fs12.readdir(dir, { withFileTypes: true });
9843
+ entries = await fs14.readdir(dir, { withFileTypes: true });
8523
9844
  } catch {
8524
9845
  return;
8525
9846
  }
8526
9847
  dirCount++;
8527
9848
  for (const e of entries) {
8528
9849
  if (ignoreSet.has(e.name)) continue;
8529
- const full = path16.join(dir, e.name);
8530
- const rel = path16.relative(projectRoot, full).replace(/\\/g, "/");
9850
+ const full = path19.join(dir, e.name);
9851
+ const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
8531
9852
  if (e.isDirectory()) {
8532
9853
  if (isGitIgnored(rel, true)) continue;
8533
9854
  await walk(full);
8534
9855
  } else if (e.isFile()) {
8535
9856
  if (isGitIgnored(rel, false)) continue;
8536
- const ext = path16.extname(e.name);
9857
+ const ext = path19.extname(e.name);
8537
9858
  for (const { ext: extName, pat } of globs) {
8538
9859
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
8539
9860
  results.push(full);
@@ -8589,7 +9910,7 @@ async function runIndexerWithStore(store, opts) {
8589
9910
  let files;
8590
9911
  let discoveredFiles = null;
8591
9912
  if (opts.files && opts.files.length > 0) {
8592
- files = opts.files.map((f) => path16.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path16.relative(projectRoot, f).replace(/\\/g, "/"), false));
9913
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
8593
9914
  } else {
8594
9915
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
8595
9916
  discoveredFiles = new Set(files);
@@ -8617,34 +9938,34 @@ async function runIndexerWithStore(store, opts) {
8617
9938
  const statOpts = signal ? { signal } : {};
8618
9939
  const statReadParse = await Promise.allSettled(
8619
9940
  batchFiles.map(async (file) => {
8620
- let stat15;
9941
+ let stat17;
8621
9942
  try {
8622
- stat15 = await fs12.stat(file, statOpts);
9943
+ stat17 = await fs14.stat(file, statOpts);
8623
9944
  } catch (e) {
8624
9945
  if (isAbortError(e)) throw e;
8625
9946
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
8626
9947
  }
8627
- if (!stat15.isFile()) return { file, stat: stat15, lang: "", parsed: null };
9948
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
8628
9949
  const lang = detectLang(file);
8629
- if (!lang) return { file, stat: stat15, lang: "", parsed: null };
9950
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
8630
9951
  const meta = existingMeta.get(file);
8631
- if (!force && meta && meta.mtimeMs === Math.floor(stat15.mtimeMs)) {
8632
- return { file, stat: stat15, lang, parsed: null, skippedMeta: meta };
9952
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
9953
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
8633
9954
  }
8634
9955
  let content;
8635
9956
  try {
8636
- content = await fs12.readFile(file, { encoding: "utf8", signal });
9957
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
8637
9958
  } catch (e) {
8638
9959
  if (isAbortError(e)) throw e;
8639
- return { file, stat: stat15, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
9960
+ return { file, stat: stat17, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
8640
9961
  }
8641
9962
  let parsed;
8642
9963
  try {
8643
9964
  parsed = await parseFile(file, content, lang);
8644
9965
  } catch (e) {
8645
- return { file, stat: stat15, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
9966
+ return { file, stat: stat17, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
8646
9967
  }
8647
- return { file, stat: stat15, lang, parsed, content };
9968
+ return { file, stat: stat17, lang, parsed, content };
8648
9969
  })
8649
9970
  );
8650
9971
  const batchEntries = [];
@@ -8664,7 +9985,7 @@ async function runIndexerWithStore(store, opts) {
8664
9985
  if (result.error.includes("error:")) errors.push(result.error);
8665
9986
  continue;
8666
9987
  }
8667
- const { stat: stat15, lang, parsed } = result;
9988
+ const { stat: stat17, lang, parsed } = result;
8668
9989
  if (result.skippedMeta) {
8669
9990
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
8670
9991
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -8676,7 +9997,7 @@ async function runIndexerWithStore(store, opts) {
8676
9997
  store.upsertFile({
8677
9998
  file,
8678
9999
  lang,
8679
- mtimeMs: Math.floor(stat15.mtimeMs),
10000
+ mtimeMs: Math.floor(stat17.mtimeMs),
8680
10001
  symbolCount: 0,
8681
10002
  lastIndexed: Date.now()
8682
10003
  });
@@ -8688,7 +10009,7 @@ async function runIndexerWithStore(store, opts) {
8688
10009
  store.upsertFile({
8689
10010
  file,
8690
10011
  lang,
8691
- mtimeMs: Math.floor(stat15.mtimeMs),
10012
+ mtimeMs: Math.floor(stat17.mtimeMs),
8692
10013
  symbolCount: 0,
8693
10014
  lastIndexed: Date.now()
8694
10015
  });
@@ -8704,7 +10025,7 @@ async function runIndexerWithStore(store, opts) {
8704
10025
  lang,
8705
10026
  symbols: parsed.symbols,
8706
10027
  refs,
8707
- mtimeMs: Math.floor(stat15.mtimeMs),
10028
+ mtimeMs: Math.floor(stat17.mtimeMs),
8708
10029
  symbolCount: parsed.symbols.length
8709
10030
  });
8710
10031
  deleteForFiles.push(file);
@@ -8892,7 +10213,7 @@ function resolveWorkerUrl() {
8892
10213
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8893
10214
  try {
8894
10215
  const url = new URL(rel, import.meta.url);
8895
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath(url))) return url;
10216
+ if (url.protocol === "file:" && fs15.existsSync(fileURLToPath(url))) return url;
8896
10217
  } catch {
8897
10218
  }
8898
10219
  }
@@ -8950,7 +10271,7 @@ function terminateWorker(reason) {
8950
10271
  function callIndexOp(op, args, opts) {
8951
10272
  const w = ensureWorker();
8952
10273
  if (!w) return callInline(op, args, opts);
8953
- return new Promise((resolve11, reject) => {
10274
+ return new Promise((resolve14, reject) => {
8954
10275
  const id = nextRpcId++;
8955
10276
  const timer = setTimeout(() => {
8956
10277
  pending.delete(id);
@@ -8973,7 +10294,7 @@ function callIndexOp(op, args, opts) {
8973
10294
  pending.set(id, {
8974
10295
  resolve: (v) => {
8975
10296
  cleanup();
8976
- resolve11(v);
10297
+ resolve14(v);
8977
10298
  },
8978
10299
  reject: (e) => {
8979
10300
  cleanup();
@@ -9312,8 +10633,8 @@ var codebaseStatsTool = {
9312
10633
  };
9313
10634
 
9314
10635
  // src/design.ts
9315
- import * as fs14 from "node:fs/promises";
9316
- import * as path17 from "node:path";
10636
+ import * as fs16 from "node:fs/promises";
10637
+ import * as path20 from "node:path";
9317
10638
  import {
9318
10639
  applyTokenOverrides,
9319
10640
  getDesignKitLoader,
@@ -9327,17 +10648,17 @@ import {
9327
10648
  setDesignOverrides
9328
10649
  } from "@wrongstack/core";
9329
10650
  async function resolveReal(p) {
9330
- const resolved = path17.resolve(p);
10651
+ const resolved = path20.resolve(p);
9331
10652
  let probe = resolved;
9332
10653
  const missing = [];
9333
10654
  for (; ; ) {
9334
10655
  try {
9335
- return path17.resolve(await fs14.realpath(probe), ...missing);
10656
+ return path20.resolve(await fs16.realpath(probe), ...missing);
9336
10657
  } catch (err) {
9337
10658
  if (err.code === "ENOENT") {
9338
- const parent = path17.dirname(probe);
10659
+ const parent = path20.dirname(probe);
9339
10660
  if (parent === probe) return resolved;
9340
- missing.unshift(path17.basename(probe));
10661
+ missing.unshift(path20.basename(probe));
9341
10662
  probe = parent;
9342
10663
  continue;
9343
10664
  }
@@ -9503,18 +10824,18 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
9503
10824
  outPath: input.out
9504
10825
  });
9505
10826
  const root = await resolveReal(ctx.projectRoot);
9506
- const absResolved = path17.isAbsolute(result.path) ? path17.resolve(result.path) : path17.resolve(path17.join(ctx.projectRoot, result.path));
9507
- const absParent = await resolveReal(path17.dirname(absResolved));
9508
- const abs = path17.join(absParent, path17.basename(absResolved));
9509
- const rel = path17.relative(root, abs);
9510
- if (rel.startsWith("..") || path17.isAbsolute(rel)) {
10827
+ const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
10828
+ const absParent = await resolveReal(path20.dirname(absResolved));
10829
+ const abs = path20.join(absParent, path20.basename(absResolved));
10830
+ const rel = path20.relative(root, abs);
10831
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) {
9511
10832
  throw new Error(
9512
10833
  `design: materialize path "${result.path}" would escape the project root`
9513
10834
  );
9514
10835
  }
9515
10836
  let exists = false;
9516
10837
  try {
9517
- await fs14.access(abs);
10838
+ await fs16.access(abs);
9518
10839
  exists = true;
9519
10840
  } catch {
9520
10841
  }
@@ -9531,8 +10852,8 @@ ${result.content}
9531
10852
  \`\`\``
9532
10853
  };
9533
10854
  }
9534
- await fs14.mkdir(path17.dirname(abs), { recursive: true });
9535
- await fs14.writeFile(abs, result.content);
10855
+ await fs16.mkdir(path20.dirname(abs), { recursive: true });
10856
+ await fs16.writeFile(abs, result.content);
9536
10857
  return {
9537
10858
  action,
9538
10859
  kit: active.kit,
@@ -9582,8 +10903,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
9582
10903
  init_util();
9583
10904
  import { spawn as spawn7 } from "node:child_process";
9584
10905
  import { statSync as statSync2 } from "node:fs";
9585
- import * as fs15 from "node:fs/promises";
9586
- import * as path18 from "node:path";
10906
+ import * as fs17 from "node:fs/promises";
10907
+ import * as path21 from "node:path";
9587
10908
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
9588
10909
  var diffTool = {
9589
10910
  name: "diff",
@@ -9667,18 +10988,18 @@ function findGitDir(cwd) {
9667
10988
  let dir = cwd;
9668
10989
  for (let i = 0; i < 20; i++) {
9669
10990
  try {
9670
- const stat15 = statSync2(path18.join(dir, ".git"));
9671
- if (stat15.isDirectory()) return dir;
10991
+ const stat17 = statSync2(path21.join(dir, ".git"));
10992
+ if (stat17.isDirectory()) return dir;
9672
10993
  } catch {
9673
10994
  }
9674
- const parent = path18.dirname(dir);
10995
+ const parent = path21.dirname(dir);
9675
10996
  if (parent === dir) break;
9676
10997
  dir = parent;
9677
10998
  }
9678
10999
  return null;
9679
11000
  }
9680
11001
  function runGit(args, cwd, signal) {
9681
- return new Promise((resolve11) => {
11002
+ return new Promise((resolve14) => {
9682
11003
  let stdout = "";
9683
11004
  let stderr = "";
9684
11005
  const child = spawn7("git", args, {
@@ -9694,8 +11015,8 @@ function runGit(args, cwd, signal) {
9694
11015
  child.stderr?.on("data", (c) => {
9695
11016
  stderr += c.toString();
9696
11017
  });
9697
- child.on("close", (code) => resolve11({ stdout, stderr, exitCode: code ?? 0 }));
9698
- child.on("error", (e) => resolve11({ stdout: "", stderr: e.message, exitCode: 1 }));
11018
+ child.on("close", (code) => resolve14({ stdout, stderr, exitCode: code ?? 0 }));
11019
+ child.on("error", (e) => resolve14({ stdout: "", stderr: e.message, exitCode: 1 }));
9699
11020
  });
9700
11021
  }
9701
11022
  async function fileDiff(input, ctx, _signal) {
@@ -9712,9 +11033,9 @@ async function fileDiff(input, ctx, _signal) {
9712
11033
  const results = [];
9713
11034
  for (const file of files) {
9714
11035
  const absPath = safeResolve(file, ctx);
9715
- const stat15 = await fs15.stat(absPath).catch(() => null);
9716
- if (!stat15?.isFile()) continue;
9717
- const content = await fs15.readFile(absPath, "utf8");
11036
+ const stat17 = await fs17.stat(absPath).catch(() => null);
11037
+ if (!stat17?.isFile()) continue;
11038
+ const content = await fs17.readFile(absPath, "utf8");
9718
11039
  const lines = content.split(/\r?\n/);
9719
11040
  results.push(formatWithLineNumbers(file, lines));
9720
11041
  }
@@ -9734,7 +11055,7 @@ ${numbered}`;
9734
11055
 
9735
11056
  // src/document.ts
9736
11057
  init_util();
9737
- import * as fs16 from "node:fs/promises";
11058
+ import * as fs18 from "node:fs/promises";
9738
11059
  var documentTool = {
9739
11060
  name: "document",
9740
11061
  category: "Project",
@@ -9778,7 +11099,7 @@ var documentTool = {
9778
11099
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
9779
11100
  for (const absPath of fileList) {
9780
11101
  try {
9781
- const content = await fs16.readFile(absPath, "utf8");
11102
+ const content = await fs18.readFile(absPath, "utf8");
9782
11103
  filesProcessed++;
9783
11104
  const processed = processFile(
9784
11105
  content,
@@ -9801,91 +11122,507 @@ var documentTool = {
9801
11122
  }
9802
11123
  }
9803
11124
  return {
9804
- files_processed: filesProcessed,
9805
- items_documented: itemsDocumented,
9806
- results,
9807
- style
11125
+ files_processed: filesProcessed,
11126
+ items_documented: itemsDocumented,
11127
+ results,
11128
+ style
11129
+ };
11130
+ }
11131
+ };
11132
+ async function resolveFiles(filesInput, cwd) {
11133
+ const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
11134
+ const resolved = [];
11135
+ for (const f of files) {
11136
+ const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
11137
+ try {
11138
+ const stat17 = await fs18.stat(absPath);
11139
+ if (stat17.isFile()) resolved.push(absPath);
11140
+ } catch {
11141
+ }
11142
+ }
11143
+ return resolved;
11144
+ }
11145
+ function processFile(content, absPath, _style, _overwrite, target) {
11146
+ const results = [];
11147
+ const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
11148
+ const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
11149
+ const classRegex = /class\s+(\w+)/g;
11150
+ const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
11151
+ const allMatches = [];
11152
+ if (target === "all" || target === "function") {
11153
+ for (const m of content.matchAll(functionRegex)) {
11154
+ if (!m[1]) continue;
11155
+ allMatches.push({
11156
+ name: m[1],
11157
+ sig: m[2] ?? "",
11158
+ type: "function",
11159
+ line: content.slice(0, m.index).split("\n").length
11160
+ });
11161
+ }
11162
+ for (const m of content.matchAll(arrowRegex)) {
11163
+ if (!m[1]) continue;
11164
+ allMatches.push({
11165
+ name: m[1],
11166
+ sig: m[2] ?? "",
11167
+ type: "arrow",
11168
+ line: content.slice(0, m.index).split("\n").length
11169
+ });
11170
+ }
11171
+ }
11172
+ if (target === "all" || target === "class") {
11173
+ for (const m of content.matchAll(classRegex)) {
11174
+ if (!m[1]) continue;
11175
+ allMatches.push({
11176
+ name: m[1],
11177
+ sig: "",
11178
+ type: "class",
11179
+ line: content.slice(0, m.index).split("\n").length
11180
+ });
11181
+ }
11182
+ }
11183
+ if (target === "all" || target === "type") {
11184
+ for (const m of content.matchAll(typeRegex)) {
11185
+ if (!m[1]) continue;
11186
+ allMatches.push({
11187
+ name: m[1],
11188
+ sig: m[0] ?? "",
11189
+ type: "type",
11190
+ line: content.slice(0, m.index).split("\n").length
11191
+ });
11192
+ }
11193
+ }
11194
+ for (const m of allMatches) {
11195
+ results.push({
11196
+ path: absPath,
11197
+ name: m.name,
11198
+ signature: m.sig,
11199
+ docstring: `/** ${m.name} - documented at line ${m.line} */`,
11200
+ status: "skipped"
11201
+ });
11202
+ }
11203
+ return results;
11204
+ }
11205
+
11206
+ // src/e2e.ts
11207
+ init_util();
11208
+ import { open, readdir as readdir5 } from "node:fs/promises";
11209
+ import * as path22 from "node:path";
11210
+ async function readBoundedText(filePath, maxBytes) {
11211
+ let handle;
11212
+ try {
11213
+ handle = await open(filePath, "r");
11214
+ const info = await handle.stat();
11215
+ if (!info.isFile() || info.size > maxBytes) return void 0;
11216
+ const buffer = Buffer.alloc(info.size);
11217
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
11218
+ return buffer.subarray(0, bytesRead).toString("utf8");
11219
+ } catch {
11220
+ return void 0;
11221
+ } finally {
11222
+ await handle?.close().catch(() => void 0);
11223
+ }
11224
+ }
11225
+ var CONFIG_NAMES = /* @__PURE__ */ new Map([
11226
+ ["playwright.config.ts", "playwright"],
11227
+ ["playwright.config.js", "playwright"],
11228
+ ["playwright.config.mts", "playwright"],
11229
+ ["playwright.config.mjs", "playwright"],
11230
+ ["playwright.config.cts", "playwright"],
11231
+ ["playwright.config.cjs", "playwright"],
11232
+ ["cypress.config.ts", "cypress"],
11233
+ ["cypress.config.js", "cypress"],
11234
+ ["cypress.config.mts", "cypress"],
11235
+ ["cypress.config.mjs", "cypress"],
11236
+ ["cypress.config.cts", "cypress"],
11237
+ ["cypress.config.cjs", "cypress"],
11238
+ ["cypress.json", "cypress"]
11239
+ ]);
11240
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
11241
+ ".git",
11242
+ ".hg",
11243
+ ".svn",
11244
+ ".wrongstack",
11245
+ "node_modules",
11246
+ "dist",
11247
+ "build",
11248
+ "coverage",
11249
+ "coverage-html",
11250
+ "playwright-report",
11251
+ "test-results",
11252
+ ".next"
11253
+ ]);
11254
+ var MAX_SCAN_DIRECTORIES = 4e3;
11255
+ var MAX_PACKAGE_BYTES = 512 * 1024;
11256
+ var MAX_CONFIG_BYTES = 512 * 1024;
11257
+ var MAX_SPEC_SAMPLES = 20;
11258
+ function relativePath(root, target) {
11259
+ const value = path22.relative(root, target) || ".";
11260
+ return value.split(path22.sep).join("/");
11261
+ }
11262
+ function escapeRegExp(value) {
11263
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11264
+ }
11265
+ function staticString(source, property) {
11266
+ const key = escapeRegExp(property);
11267
+ const direct = source.match(
11268
+ new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
11269
+ );
11270
+ if (direct?.[2] && !direct[2].includes("${")) return direct[2];
11271
+ const sameLineFallback = source.match(
11272
+ new RegExp(`(?:['"]?${key}['"]?)\\s*:[^\\r\\n]{0,300}?(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
11273
+ );
11274
+ return sameLineFallback?.[2] && !sameLineFallback[2].includes("${") ? sameLineFallback[2] : void 0;
11275
+ }
11276
+ function staticNumber(source, property) {
11277
+ const key = escapeRegExp(property);
11278
+ const match = source.match(new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(\\d{2,5})\\b`));
11279
+ return match?.[1] ? Number.parseInt(match[1], 10) : void 0;
11280
+ }
11281
+ function serverHints(framework, source) {
11282
+ if (framework === "playwright" && /\bwebServer\s*:/.test(source)) {
11283
+ const command = staticString(source, "command");
11284
+ const url = staticString(source, "url");
11285
+ const port = staticNumber(source, "port");
11286
+ return [
11287
+ {
11288
+ kind: "managed",
11289
+ ...command ? { command } : {},
11290
+ ...url ? { url } : {},
11291
+ ...port ? { port } : {},
11292
+ source: "static-config",
11293
+ incomplete: !command || !url && !port
11294
+ }
11295
+ ];
11296
+ }
11297
+ const baseUrl = staticString(source, "baseURL") ?? staticString(source, "baseUrl");
11298
+ return baseUrl ? [{ kind: "attach", url: baseUrl, source: "static-config", incomplete: false }] : [];
11299
+ }
11300
+ function configuredSpecPattern(framework, source) {
11301
+ if (framework === "playwright") {
11302
+ const testDir = staticString(source, "testDir") ?? "./tests";
11303
+ return `${testDir.replace(/\/$/, "")}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;
11304
+ }
11305
+ return staticString(source, "specPattern") ?? "cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}";
11306
+ }
11307
+ async function scanWorkspace(root, maxDepth, signal) {
11308
+ const result = {
11309
+ configs: [],
11310
+ packageFiles: [],
11311
+ scannedDirectories: 0,
11312
+ truncated: false
11313
+ };
11314
+ const queue = [{ directory: root, depth: 0 }];
11315
+ while (queue.length > 0) {
11316
+ signal.throwIfAborted();
11317
+ const current = queue.shift();
11318
+ if (!current) break;
11319
+ result.scannedDirectories += 1;
11320
+ if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {
11321
+ result.truncated = true;
11322
+ break;
11323
+ }
11324
+ let entries;
11325
+ try {
11326
+ entries = await readdir5(current.directory, { withFileTypes: true });
11327
+ } catch {
11328
+ continue;
11329
+ }
11330
+ for (const entry of entries) {
11331
+ signal.throwIfAborted();
11332
+ const absolutePath = path22.join(current.directory, entry.name);
11333
+ if (entry.isFile()) {
11334
+ if (entry.name === "package.json") result.packageFiles.push(absolutePath);
11335
+ const framework = CONFIG_NAMES.get(entry.name);
11336
+ if (framework) result.configs.push({ framework, absolutePath });
11337
+ } else if (entry.isDirectory() && current.depth < maxDepth && !SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith("coverage-") && !entry.name.startsWith("cov-")) {
11338
+ queue.push({ directory: absolutePath, depth: current.depth + 1 });
11339
+ }
11340
+ }
11341
+ }
11342
+ return result;
11343
+ }
11344
+ async function readPackageInfo(packagePath) {
11345
+ try {
11346
+ const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);
11347
+ if (raw === void 0) return void 0;
11348
+ const parsed = JSON.parse(raw);
11349
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? Object.fromEntries(
11350
+ Object.entries(parsed.scripts).filter(
11351
+ (entry) => typeof entry[1] === "string"
11352
+ )
11353
+ ) : {};
11354
+ const dependencies = /* @__PURE__ */ new Set();
11355
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
11356
+ const value = parsed[field];
11357
+ if (!value || typeof value !== "object") continue;
11358
+ for (const name of Object.keys(value)) dependencies.add(name);
11359
+ }
11360
+ const declared = typeof parsed.packageManager === "string" ? parsed.packageManager.split("@")[0] ?? "" : "";
11361
+ const packageManager = ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : void 0;
11362
+ return { path: packagePath, scripts, dependencies, packageManager };
11363
+ } catch {
11364
+ return void 0;
11365
+ }
11366
+ }
11367
+ function frameworkFromPackage(info) {
11368
+ const found = /* @__PURE__ */ new Set();
11369
+ if (info.dependencies.has("@playwright/test") || info.dependencies.has("playwright")) {
11370
+ found.add("playwright");
11371
+ }
11372
+ if (info.dependencies.has("cypress")) found.add("cypress");
11373
+ for (const command of Object.values(info.scripts)) {
11374
+ if (/\bplaywright(?:\.cmd)?\b/i.test(command)) found.add("playwright");
11375
+ if (/\bcypress(?:\.cmd)?\b/i.test(command)) found.add("cypress");
11376
+ }
11377
+ return [...found];
11378
+ }
11379
+ async function detectPackageManager3(projectRoot, scanRoot, declared) {
11380
+ if (declared) return declared;
11381
+ let directory = projectRoot;
11382
+ while (true) {
11383
+ const names = /* @__PURE__ */ new Set();
11384
+ try {
11385
+ for (const entry of await readdir5(directory)) names.add(entry);
11386
+ } catch {
11387
+ }
11388
+ if (names.has("pnpm-lock.yaml")) return "pnpm";
11389
+ if (names.has("yarn.lock")) return "yarn";
11390
+ if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
11391
+ if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
11392
+ if (directory === scanRoot) break;
11393
+ const parent = path22.dirname(directory);
11394
+ const relativeParent = path22.relative(scanRoot, parent);
11395
+ if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
11396
+ break;
11397
+ }
11398
+ directory = parent;
11399
+ }
11400
+ return "npm";
11401
+ }
11402
+ function matchingScripts(info, framework) {
11403
+ if (!info) return [];
11404
+ const matcher = framework === "playwright" ? /\bplaywright(?:\.cmd)?\b/i : /\bcypress(?:\.cmd)?\b/i;
11405
+ return Object.entries(info.scripts).filter(([, command]) => matcher.test(command)).map(([name]) => name).sort((a, b) => {
11406
+ const preferred = ["test:e2e", "e2e", "e2e:test"];
11407
+ const ai = preferred.indexOf(a);
11408
+ const bi = preferred.indexOf(b);
11409
+ if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
11410
+ return a.localeCompare(b);
11411
+ });
11412
+ }
11413
+ function executionPlan(framework, manager, root, configPath, scripts) {
11414
+ const script = scripts[0];
11415
+ if (script) {
11416
+ return {
11417
+ command: manager,
11418
+ args: ["run", script],
11419
+ cwd: root,
11420
+ source: "package-script",
11421
+ script
9808
11422
  };
9809
11423
  }
9810
- };
9811
- async function resolveFiles(filesInput, cwd) {
9812
- const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
9813
- const resolved = [];
9814
- for (const f of files) {
9815
- const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
11424
+ const runnerArgs = framework === "playwright" ? ["playwright", "test"] : ["cypress", "run"];
11425
+ if (configPath) {
11426
+ runnerArgs.push(framework === "playwright" ? "--config" : "--config-file", configPath);
11427
+ }
11428
+ return {
11429
+ command: manager === "bun" ? "bunx" : manager,
11430
+ args: manager === "bun" ? runnerArgs : manager === "npm" ? ["exec", "--", ...runnerArgs] : ["exec", ...runnerArgs],
11431
+ cwd: root,
11432
+ source: "framework-default"
11433
+ };
11434
+ }
11435
+ function isSpec(framework, filename) {
11436
+ if (framework === "cypress") return /\.cy\.(?:[cm]?[jt]sx?)$/i.test(filename);
11437
+ return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
11438
+ }
11439
+ async function collectSpecs(root, framework, testDirectory, signal) {
11440
+ const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
11441
+ const samples = [];
11442
+ let count = 0;
11443
+ let scanned = 0;
11444
+ const queue = [...roots];
11445
+ while (queue.length > 0) {
11446
+ signal.throwIfAborted();
11447
+ const directory = queue.shift();
11448
+ if (!directory) break;
11449
+ scanned += 1;
11450
+ if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
11451
+ let entries;
9816
11452
  try {
9817
- const stat15 = await fs16.stat(absPath);
9818
- if (stat15.isFile()) resolved.push(absPath);
11453
+ entries = await readdir5(directory, { withFileTypes: true });
9819
11454
  } catch {
11455
+ continue;
11456
+ }
11457
+ for (const entry of entries) {
11458
+ signal.throwIfAborted();
11459
+ const target = path22.join(directory, entry.name);
11460
+ if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
11461
+ else if (entry.isFile() && isSpec(framework, entry.name)) {
11462
+ count += 1;
11463
+ if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));
11464
+ }
9820
11465
  }
9821
11466
  }
9822
- return resolved;
11467
+ return { count, samples, truncated: count > samples.length };
9823
11468
  }
9824
- function processFile(content, absPath, _style, _overwrite, target) {
9825
- const results = [];
9826
- const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
9827
- const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
9828
- const classRegex = /class\s+(\w+)/g;
9829
- const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
9830
- const allMatches = [];
9831
- if (target === "all" || target === "function") {
9832
- for (const m of content.matchAll(functionRegex)) {
9833
- if (!m[1]) continue;
9834
- allMatches.push({
9835
- name: m[1],
9836
- sig: m[2] ?? "",
9837
- type: "function",
9838
- line: content.slice(0, m.index).split("\n").length
9839
- });
9840
- }
9841
- for (const m of content.matchAll(arrowRegex)) {
9842
- if (!m[1]) continue;
9843
- allMatches.push({
9844
- name: m[1],
9845
- sig: m[2] ?? "",
9846
- type: "arrow",
9847
- line: content.slice(0, m.index).split("\n").length
9848
- });
9849
- }
11469
+ async function readConfig(configPath) {
11470
+ if (!configPath) return "";
11471
+ return await readBoundedText(configPath, MAX_CONFIG_BYTES) ?? "";
11472
+ }
11473
+ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
11474
+ let directory = projectRoot;
11475
+ while (true) {
11476
+ const found = packagesByDirectory.get(directory);
11477
+ if (found) return found;
11478
+ if (directory === scanRoot) return void 0;
11479
+ const parent = path22.dirname(directory);
11480
+ if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
11481
+ directory = parent;
9850
11482
  }
9851
- if (target === "all" || target === "class") {
9852
- for (const m of content.matchAll(classRegex)) {
9853
- if (!m[1]) continue;
9854
- allMatches.push({
9855
- name: m[1],
9856
- sig: "",
9857
- type: "class",
9858
- line: content.slice(0, m.index).split("\n").length
9859
- });
11483
+ }
11484
+ async function discoverE2EProjects(root, options) {
11485
+ const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));
11486
+ const scan = await scanWorkspace(root, maxDepth, options.signal);
11487
+ const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
11488
+ (info) => Boolean(info)
11489
+ );
11490
+ const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
11491
+ const candidates = /* @__PURE__ */ new Map();
11492
+ for (const config of scan.configs) {
11493
+ if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
11494
+ continue;
9860
11495
  }
11496
+ const projectRoot = path22.dirname(config.absolutePath);
11497
+ candidates.set(`${config.framework}:${projectRoot}`, {
11498
+ framework: config.framework,
11499
+ root: projectRoot,
11500
+ configPath: config.absolutePath
11501
+ });
9861
11502
  }
9862
- if (target === "all" || target === "type") {
9863
- for (const m of content.matchAll(typeRegex)) {
9864
- if (!m[1]) continue;
9865
- allMatches.push({
9866
- name: m[1],
9867
- sig: m[0] ?? "",
9868
- type: "type",
9869
- line: content.slice(0, m.index).split("\n").length
9870
- });
11503
+ for (const info of packages) {
11504
+ const projectRoot = path22.dirname(info.path);
11505
+ for (const framework of frameworkFromPackage(info)) {
11506
+ if (options.framework && options.framework !== "all" && framework !== options.framework)
11507
+ continue;
11508
+ const key = `${framework}:${projectRoot}`;
11509
+ if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });
11510
+ }
11511
+ }
11512
+ const projects = [];
11513
+ for (const candidate of candidates.values()) {
11514
+ options.signal.throwIfAborted();
11515
+ const info = nearestPackage(candidate.root, packagesByDirectory, root);
11516
+ const source = await readConfig(candidate.configPath);
11517
+ const scripts = matchingScripts(info, candidate.framework);
11518
+ const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
11519
+ const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
11520
+ const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
11521
+ const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
11522
+ const unsafeTestDirectory = Boolean(
11523
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
11524
+ );
11525
+ const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
11526
+ const warnings = [];
11527
+ if (!candidate.configPath)
11528
+ warnings.push("Framework dependency or script found without a config file.");
11529
+ if (candidate.configPath && !source) {
11530
+ warnings.push("Config could not be read or exceeded the static inspection limit.");
11531
+ }
11532
+ if (unsafeTestDirectory) {
11533
+ warnings.push(
11534
+ "Configured testDir resolves outside the requested discovery root and was not scanned."
11535
+ );
9871
11536
  }
9872
- }
9873
- for (const m of allMatches) {
9874
- results.push({
9875
- path: absPath,
9876
- name: m.name,
9877
- signature: m.sig,
9878
- docstring: `/** ${m.name} - documented at line ${m.line} */`,
9879
- status: "skipped"
11537
+ const hints = serverHints(candidate.framework, source);
11538
+ if (hints.some((hint) => hint.incomplete)) {
11539
+ warnings.push("Configured server contains dynamic values; only static hints are shown.");
11540
+ }
11541
+ projects.push({
11542
+ framework: candidate.framework,
11543
+ root: relativePath(root, candidate.root),
11544
+ ...candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {},
11545
+ ...info ? { packagePath: relativePath(root, info.path) } : {},
11546
+ packageManager: manager,
11547
+ evidence: [
11548
+ ...candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : [],
11549
+ ...info?.dependencies.has(
11550
+ candidate.framework === "playwright" ? "@playwright/test" : "cypress"
11551
+ ) ? [`dependency:${candidate.framework === "playwright" ? "@playwright/test" : "cypress"}`] : [],
11552
+ ...scripts.map((script) => `script:${script}`)
11553
+ ],
11554
+ scripts,
11555
+ serverHints: hints,
11556
+ specPattern: configuredSpecPattern(candidate.framework, source),
11557
+ specCount: specs.count,
11558
+ specSamples: specs.samples,
11559
+ specsTruncated: specs.truncated,
11560
+ execution: executionPlan(
11561
+ candidate.framework,
11562
+ manager,
11563
+ relativePath(root, candidate.root),
11564
+ candidate.configPath ? relativePath(candidate.root, candidate.configPath) : void 0,
11565
+ scripts
11566
+ ),
11567
+ warnings
9880
11568
  });
9881
11569
  }
9882
- return results;
11570
+ projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));
11571
+ return {
11572
+ root,
11573
+ projects,
11574
+ scannedDirectories: scan.scannedDirectories,
11575
+ scanTruncated: scan.truncated,
11576
+ warnings: scan.truncated ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`] : []
11577
+ };
9883
11578
  }
11579
+ var e2ePlanTool = {
11580
+ name: "e2e_plan",
11581
+ category: "Code Quality",
11582
+ description: "Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.",
11583
+ usageHint: "Use before running browser E2E tests. The result identifies authoritative configs, package scripts, static server hints, specs, and exact argv. This tool never imports project config or executes commands.",
11584
+ permission: "auto",
11585
+ mutating: false,
11586
+ riskTier: "safe",
11587
+ capabilities: ["fs.read"],
11588
+ icon: "test",
11589
+ timeoutMs: 3e4,
11590
+ inputSchema: {
11591
+ type: "object",
11592
+ properties: {
11593
+ cwd: { type: "string", description: "Directory inside the project to inspect." },
11594
+ framework: {
11595
+ type: "string",
11596
+ enum: ["all", "playwright", "cypress"],
11597
+ description: "Optional framework filter; defaults to all."
11598
+ },
11599
+ maxDepth: {
11600
+ type: "integer",
11601
+ minimum: 0,
11602
+ maximum: 8,
11603
+ description: "Maximum workspace discovery depth; defaults to 5."
11604
+ },
11605
+ includeSpecs: {
11606
+ type: "boolean",
11607
+ description: "Count and sample specs; defaults to true."
11608
+ }
11609
+ }
11610
+ },
11611
+ async execute(input, ctx, opts) {
11612
+ const root = await safeResolveReal(input.cwd ?? ".", ctx);
11613
+ return discoverE2EProjects(root, {
11614
+ framework: input.framework,
11615
+ maxDepth: input.maxDepth,
11616
+ includeSpecs: input.includeSpecs,
11617
+ signal: opts.signal
11618
+ });
11619
+ }
11620
+ };
9884
11621
 
9885
11622
  // src/edit.ts
9886
- import * as fs17 from "node:fs/promises";
11623
+ import * as fs19 from "node:fs/promises";
9887
11624
  import {
9888
- atomicWrite,
11625
+ atomicWrite as atomicWrite2,
9889
11626
  detectNewlineStyle,
9890
11627
  normalizeToLf,
9891
11628
  ToolValidationError,
@@ -9922,25 +11659,46 @@ function findLadderMatches(fileLf, oldLf) {
9922
11659
  const needleLines = oldLf.split("\n");
9923
11660
  if (needleLines.length > fileLines.length) return void 0;
9924
11661
  const offsets = lineOffsets(fileLines);
11662
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
11663
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
9925
11664
  const trailing = windowScan(
9926
- fileLines,
9927
- needleLines,
11665
+ fileTrimEnd,
11666
+ needleTrimEnd,
9928
11667
  offsets,
9929
- (a, b) => a.trimEnd() === b.trimEnd()
11668
+ fileLines,
11669
+ (a, b) => a === b
9930
11670
  );
9931
11671
  if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
9932
- const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
11672
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
9933
11673
  if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
9934
- const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
11674
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
11675
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
11676
+ const normalized = windowScan(
11677
+ fileTrimmed,
11678
+ needleTrimmed,
11679
+ offsets,
11680
+ fileLines,
11681
+ (a, b) => a === b
11682
+ );
9935
11683
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
9936
11684
  return fuzzyScan(fileLines, needleLines, offsets);
9937
11685
  }
9938
11686
  function lineAt(text, pos) {
11687
+ if (pos < 512) {
11688
+ let line2 = 1;
11689
+ for (let i = 0; i < pos; i++) {
11690
+ if (text.charCodeAt(i) === 10) line2++;
11691
+ }
11692
+ return line2;
11693
+ }
9939
11694
  let line = 1;
9940
- for (let i = 0; i < pos; i++) {
9941
- if (text.charCodeAt(i) === 10) line++;
11695
+ let search = 0;
11696
+ while (true) {
11697
+ const idx = text.indexOf("\n", search);
11698
+ if (idx === -1 || idx >= pos) return line;
11699
+ search = idx + 1;
11700
+ line++;
9942
11701
  }
9943
- return line;
9944
11702
  }
9945
11703
  function lineOffsets(lines) {
9946
11704
  const out = new Array(lines.length);
@@ -9959,19 +11717,19 @@ function windowToMatch(fileLines, offsets, start, windowLen) {
9959
11717
  startLine: start + 1
9960
11718
  };
9961
11719
  }
9962
- function windowScan(fileLines, needleLines, offsets, eq) {
11720
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
9963
11721
  const n = needleLines.length;
9964
11722
  const out = [];
9965
- for (let i = 0; i + n <= fileLines.length; i++) {
11723
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
9966
11724
  let all = true;
9967
11725
  for (let j = 0; j < n; j++) {
9968
- if (!eq(fileLines[i + j], needleLines[j])) {
11726
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
9969
11727
  all = false;
9970
11728
  break;
9971
11729
  }
9972
11730
  }
9973
11731
  if (all) {
9974
- out.push(windowToMatch(fileLines, offsets, i, n));
11732
+ out.push(windowToMatch(originalLines, offsets, i, n));
9975
11733
  i += n - 1;
9976
11734
  }
9977
11735
  }
@@ -10098,7 +11856,7 @@ function prefixSimilarity(a, b) {
10098
11856
  }
10099
11857
 
10100
11858
  // src/_syntax-check.ts
10101
- import * as path19 from "node:path";
11859
+ import * as path23 from "node:path";
10102
11860
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
10103
11861
  var MAX_CHECK_CHARS = 15e5;
10104
11862
  var MAX_ERRORS = 5;
@@ -10122,15 +11880,15 @@ async function checkSyntax(filePath, content, previousContent) {
10122
11880
  return { errors, preExisting };
10123
11881
  }
10124
11882
  function isJsoncFile(filePath) {
10125
- const base = path19.basename(filePath).toLowerCase();
11883
+ const base = path23.basename(filePath).toLowerCase();
10126
11884
  if (base.endsWith(".jsonc")) return true;
10127
11885
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
10128
- const dir = path19.basename(path19.dirname(filePath)).toLowerCase();
11886
+ const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
10129
11887
  return dir === ".vscode";
10130
11888
  }
10131
11889
  async function parseErrors(filePath, content) {
10132
11890
  if (content.length > MAX_CHECK_CHARS) return void 0;
10133
- const ext = path19.extname(filePath).toLowerCase();
11891
+ const ext = path23.extname(filePath).toLowerCase();
10134
11892
  if (ext === ".json" || ext === ".jsonc") {
10135
11893
  try {
10136
11894
  JSON.parse(content);
@@ -10157,7 +11915,7 @@ async function parseErrors(filePath, content) {
10157
11915
  ts2.ScriptKind.JSX
10158
11916
  );
10159
11917
  const sourceFile = ts2.createSourceFile(
10160
- path19.basename(filePath),
11918
+ path23.basename(filePath),
10161
11919
  content,
10162
11920
  ts2.ScriptTarget.Latest,
10163
11921
  /* setParentNodes */
@@ -10231,7 +11989,7 @@ var editTool = {
10231
11989
  });
10232
11990
  }
10233
11991
  const absPath = await safeResolveReal(input.path, ctx);
10234
- const stat15 = await fs17.stat(absPath).catch((err) => {
11992
+ const stat17 = await fs19.stat(absPath).catch((err) => {
10235
11993
  if (err.code === "ENOENT") {
10236
11994
  throw new ToolValidationError({
10237
11995
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
@@ -10241,15 +11999,15 @@ var editTool = {
10241
11999
  }
10242
12000
  throw err;
10243
12001
  });
10244
- if (!stat15.isFile()) {
12002
+ if (!stat17.isFile()) {
10245
12003
  throw new ToolValidationError({
10246
12004
  message: `edit: "${input.path}" is not a regular file`,
10247
12005
  field: "path"
10248
12006
  });
10249
12007
  }
10250
12008
  const autoRead = !ctx.hasRead(absPath);
10251
- const original = await fs17.readFile(absPath, "utf8");
10252
- const updated = await fs17.stat(absPath);
12009
+ const original = await fs19.readFile(absPath, "utf8");
12010
+ const updated = await fs19.stat(absPath);
10253
12011
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
10254
12012
  const originalHash = sha256hex(original);
10255
12013
  const lastReadHash = ctx.lastReadHash?.(absPath);
@@ -10271,7 +12029,7 @@ var editTool = {
10271
12029
  });
10272
12030
  }
10273
12031
  }
10274
- if (autoRead && updated.mtimeMs > stat15.mtimeMs + mtimeTolerance) {
12032
+ if (autoRead && updated.mtimeMs > stat17.mtimeMs + mtimeTolerance) {
10275
12033
  throw new ToolValidationError({
10276
12034
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
10277
12035
  field: "path",
@@ -10293,8 +12051,10 @@ var editTool = {
10293
12051
  note: autoReadNote
10294
12052
  };
10295
12053
  }
12054
+ opts?.signal?.throwIfAborted();
10296
12055
  const ladder = findLadderMatches(fileLf, oldLf);
10297
12056
  if (!ladder) {
12057
+ opts?.signal?.throwIfAborted();
10298
12058
  const hint = nearestMatchHint(fileLf, oldLf);
10299
12059
  throw new ToolValidationError({
10300
12060
  message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
@@ -10353,8 +12113,8 @@ Compare this against your old_string and retry with the file's actual text.` : "
10353
12113
  }
10354
12114
  const newFile = toStyle(newFileLf, style);
10355
12115
  opts?.signal?.throwIfAborted();
10356
- await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
10357
- const written = await fs17.stat(absPath);
12116
+ await atomicWrite2(absPath, newFile, { mode: updated.mode & 511 });
12117
+ const written = await fs19.stat(absPath);
10358
12118
  ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
10359
12119
  ctx.session.recordFileChange({
10360
12120
  path: absPath,
@@ -10362,6 +12122,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
10362
12122
  before: original,
10363
12123
  after: newFile
10364
12124
  });
12125
+ opts?.signal?.throwIfAborted();
10365
12126
  const diff = unifiedDiff(original, newFile, {
10366
12127
  fromFile: input.path,
10367
12128
  toFile: input.path
@@ -11314,7 +13075,7 @@ var BLOCKED_ARG_PATTERNS = {
11314
13075
  // and glob patterns that could expand to dangerous targets.
11315
13076
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
11316
13077
  // `rm -rf /*` targets the filesystem root. All are blocked.
11317
- rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
13078
+ rm: [/^\//, /^[A-Za-z]:[\\/]/, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
11318
13079
  // npm/pnpm subcommands are checked separately below. Matching every arg here
11319
13080
  // over-blocked normal dev flows such as `pnpm vitest run ...`.
11320
13081
  npm: [],
@@ -11482,7 +13243,7 @@ var execTool = {
11482
13243
  }
11483
13244
  };
11484
13245
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
11485
- return new Promise((resolve11) => {
13246
+ return new Promise((resolve14) => {
11486
13247
  let stdout = "";
11487
13248
  let stderr = "";
11488
13249
  let killed = false;
@@ -11490,7 +13251,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
11490
13251
  const finish = (result) => {
11491
13252
  if (resolvedOnce.value) return;
11492
13253
  resolvedOnce.value = true;
11493
- resolve11(result);
13254
+ resolve14(result);
11494
13255
  };
11495
13256
  const startedAt = Date.now();
11496
13257
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
@@ -11599,13 +13360,13 @@ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValida
11599
13360
  import TurndownService from "turndown";
11600
13361
 
11601
13362
  // src/_fetch-guard.ts
11602
- import * as dns from "node:dns/promises";
11603
- import * as net from "node:net";
13363
+ import * as dns2 from "node:dns/promises";
13364
+ import * as net3 from "node:net";
11604
13365
  import {
11605
13366
  FetchError,
11606
13367
  ToolValidationError as ToolValidationError2,
11607
- isPrivateIPv4,
11608
- isPrivateIPv6
13368
+ isPrivateIPv4 as isPrivateIPv42,
13369
+ isPrivateIPv6 as isPrivateIPv62
11609
13370
  } from "@wrongstack/core";
11610
13371
  import { Agent, fetch as undiciFetch } from "undici";
11611
13372
  var nativeGlobalFetch = globalThis.fetch;
@@ -11616,13 +13377,13 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
11616
13377
  );
11617
13378
  }
11618
13379
  function guardedLookup(hostname2, options, callback) {
11619
- dns.lookup(hostname2, { all: true }).then((records) => {
13380
+ dns2.lookup(hostname2, { all: true }).then((records) => {
11620
13381
  const family = options?.family;
11621
13382
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
11622
13383
  const list = byFamily.length > 0 ? byFamily : records;
11623
13384
  if (!ALLOW_PRIVATE) {
11624
13385
  for (const r of list) {
11625
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
13386
+ const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
11626
13387
  if (bad) {
11627
13388
  callback(
11628
13389
  Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
@@ -11727,16 +13488,16 @@ async function assertNotPrivate(hostname2) {
11727
13488
  field: "url"
11728
13489
  });
11729
13490
  }
11730
- const ipVersion = net.isIP(host);
13491
+ const ipVersion = net3.isIP(host);
11731
13492
  if (ipVersion === 4) {
11732
- if (isPrivateIPv4(host)) {
13493
+ if (isPrivateIPv42(host)) {
11733
13494
  throw new ToolValidationError2({
11734
13495
  message: `fetch: blocked private/loopback address "${host}"`,
11735
13496
  field: "url"
11736
13497
  });
11737
13498
  }
11738
13499
  } else if (ipVersion === 6) {
11739
- if (isPrivateIPv6(host)) {
13500
+ if (isPrivateIPv62(host)) {
11740
13501
  throw new ToolValidationError2({
11741
13502
  message: `fetch: blocked private/loopback address "${host}"`,
11742
13503
  field: "url"
@@ -11744,9 +13505,9 @@ async function assertNotPrivate(hostname2) {
11744
13505
  }
11745
13506
  } else {
11746
13507
  try {
11747
- const records = await dns.lookup(host, { all: true });
13508
+ const records = await dns2.lookup(host, { all: true });
11748
13509
  for (const r of records) {
11749
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
13510
+ const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
11750
13511
  if (bad) {
11751
13512
  throw new ToolValidationError2({
11752
13513
  message: `fetch: resolved to private address ${r.address}`,
@@ -12085,13 +13846,13 @@ var formatTool = {
12085
13846
  }
12086
13847
  };
12087
13848
  async function detectFixer(cwd) {
12088
- const { stat: stat15 } = await import("node:fs/promises");
13849
+ const { stat: stat17 } = await import("node:fs/promises");
12089
13850
  try {
12090
- await stat15(`${cwd}/biome.json`);
13851
+ await stat17(`${cwd}/biome.json`);
12091
13852
  return "biome";
12092
13853
  } catch {
12093
13854
  try {
12094
- await stat15(`${cwd}/.prettierrc`);
13855
+ await stat17(`${cwd}/.prettierrc`);
12095
13856
  return "prettier";
12096
13857
  } catch {
12097
13858
  return "biome";
@@ -12103,7 +13864,7 @@ async function detectFixer(cwd) {
12103
13864
  init_util();
12104
13865
  import { spawn as spawn9 } from "node:child_process";
12105
13866
  import { statSync as statSync3 } from "node:fs";
12106
- import { dirname as dirname9, resolve as resolve8, sep as sep3 } from "node:path";
13867
+ import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
12107
13868
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
12108
13869
  var TIMEOUT_MS2 = 3e4;
12109
13870
  var MAX_OUTPUT3 = 1e5;
@@ -12246,9 +14007,9 @@ function validateWorktreeInput(input, projectRoot) {
12246
14007
  return reject(`unsafe worktree path: ${input.worktreePath}`);
12247
14008
  }
12248
14009
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
12249
- const root = resolve8(projectRoot);
12250
- const abs = resolve8(root, input.worktreePath);
12251
- if (abs !== root && !abs.startsWith(root + sep3)) {
14010
+ const root = resolve11(projectRoot);
14011
+ const abs = resolve11(root, input.worktreePath);
14012
+ if (abs !== root && !abs.startsWith(root + sep4)) {
12252
14013
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
12253
14014
  }
12254
14015
  }
@@ -12259,12 +14020,12 @@ function findGitDir2(cwd, projectRoot) {
12259
14020
  let dir = cwd;
12260
14021
  for (let i = 0; i < 20; i++) {
12261
14022
  try {
12262
- const stat15 = statSync3(`${dir}/.git`);
12263
- if (stat15.isDirectory() || stat15.isFile()) return dir;
14023
+ const stat17 = statSync3(`${dir}/.git`);
14024
+ if (stat17.isDirectory() || stat17.isFile()) return dir;
12264
14025
  } catch {
12265
14026
  }
12266
14027
  if (dir === root) break;
12267
- const parent = dirname9(dir);
14028
+ const parent = dirname12(dir);
12268
14029
  if (parent === dir) break;
12269
14030
  dir = parent;
12270
14031
  }
@@ -12341,7 +14102,7 @@ function buildArgs(input) {
12341
14102
  }
12342
14103
  }
12343
14104
  function runGit2(args, cwd, signal) {
12344
- return new Promise((resolve11) => {
14105
+ return new Promise((resolve14) => {
12345
14106
  let stdout = "";
12346
14107
  let stderr = "";
12347
14108
  const child = spawn9("git", args, {
@@ -12362,7 +14123,7 @@ function runGit2(args, cwd, signal) {
12362
14123
  }
12363
14124
  });
12364
14125
  child.on("error", (err) => {
12365
- resolve11({
14126
+ resolve14({
12366
14127
  command: args[0],
12367
14128
  stdout: normalizeCommandOutput(stdout),
12368
14129
  stderr: err.message,
@@ -12371,7 +14132,7 @@ function runGit2(args, cwd, signal) {
12371
14132
  });
12372
14133
  });
12373
14134
  child.on("close", (code) => {
12374
- resolve11({
14135
+ resolve14({
12375
14136
  command: args[0],
12376
14137
  stdout: normalizeCommandOutput(stdout),
12377
14138
  stderr: normalizeCommandOutput(stderr),
@@ -12383,8 +14144,8 @@ function runGit2(args, cwd, signal) {
12383
14144
  }
12384
14145
 
12385
14146
  // src/glob.ts
12386
- import * as fs18 from "node:fs/promises";
12387
- import * as path20 from "node:path";
14147
+ import * as fs20 from "node:fs/promises";
14148
+ import * as path24 from "node:path";
12388
14149
  import { compileGlob as compileGlob3 } from "@wrongstack/core";
12389
14150
 
12390
14151
  // src/_concurrency.ts
@@ -12456,7 +14217,7 @@ var globTool = {
12456
14217
  return;
12457
14218
  }
12458
14219
  try {
12459
- const st = await fs18.stat(full);
14220
+ const st = await fs20.stat(full);
12460
14221
  if (truncated || results.length >= limit) {
12461
14222
  truncated = true;
12462
14223
  return;
@@ -12477,7 +14238,7 @@ var globTool = {
12477
14238
  }
12478
14239
  let entries;
12479
14240
  try {
12480
- entries = await fs18.readdir(dir, { withFileTypes: true });
14241
+ entries = await fs20.readdir(dir, { withFileTypes: true });
12481
14242
  } catch {
12482
14243
  return;
12483
14244
  }
@@ -12488,7 +14249,7 @@ var globTool = {
12488
14249
  if (DEFAULT_IGNORE2.includes(name)) continue;
12489
14250
  if (ignored.includes(name)) continue;
12490
14251
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
12491
- const full = path20.join(dir, name);
14252
+ const full = path24.join(dir, name);
12492
14253
  if (e.isDirectory()) {
12493
14254
  subdirs.push({ full, rel });
12494
14255
  } else if (e.isFile()) {
@@ -12501,13 +14262,13 @@ var globTool = {
12501
14262
  }
12502
14263
  } else if (e.isSymbolicLink()) {
12503
14264
  try {
12504
- const st = await fs18.stat(full);
14265
+ const st = await fs20.stat(full);
12505
14266
  if (st.isDirectory()) {
12506
- const real = await fs18.realpath(full);
14267
+ const real = await fs20.realpath(full);
12507
14268
  await assertRealInsideRoot(real, ctx);
12508
14269
  subdirs.push({ full, rel });
12509
14270
  } else if (st.isFile()) {
12510
- const real = await fs18.realpath(full);
14271
+ const real = await fs20.realpath(full);
12511
14272
  await assertRealInsideRoot(real, ctx);
12512
14273
  re.lastIndex = 0;
12513
14274
  const relMatch = re.test(rel);
@@ -12532,7 +14293,7 @@ var globTool = {
12532
14293
  };
12533
14294
  async function readGitignore(dir) {
12534
14295
  try {
12535
- const raw = await fs18.readFile(path20.join(dir, ".gitignore"), "utf8");
14296
+ const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
12536
14297
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
12537
14298
  } catch {
12538
14299
  return [];
@@ -12542,8 +14303,8 @@ async function readGitignore(dir) {
12542
14303
  // src/grep.ts
12543
14304
  import { expectDefined as expectDefined7 } from "@wrongstack/core";
12544
14305
  import { spawn as spawn10 } from "node:child_process";
12545
- import * as fs19 from "node:fs/promises";
12546
- import * as path21 from "node:path";
14306
+ import * as fs21 from "node:fs/promises";
14307
+ import * as path25 from "node:path";
12547
14308
  import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
12548
14309
 
12549
14310
  // src/_regex.ts
@@ -12688,13 +14449,13 @@ var grepTool = {
12688
14449
  }
12689
14450
  };
12690
14451
  async function detectRg(signal) {
12691
- return new Promise((resolve11) => {
14452
+ return new Promise((resolve14) => {
12692
14453
  try {
12693
14454
  const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
12694
- p.on("error", () => resolve11(false));
12695
- p.on("close", (code) => resolve11(code === 0));
14455
+ p.on("error", () => resolve14(false));
14456
+ p.on("close", (code) => resolve14(code === 0));
12696
14457
  } catch {
12697
- resolve11(false);
14458
+ resolve14(false);
12698
14459
  }
12699
14460
  });
12700
14461
  }
@@ -12845,9 +14606,9 @@ async function runNative(input, base, mode, limit, signal) {
12845
14606
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
12846
14607
  if (globRe) globRe.lastIndex = 0;
12847
14608
  try {
12848
- const stat15 = await fs19.stat(full);
12849
- if (!stat15.isFile() || stat15.size > maxBytes || stopped || signal.aborted) return;
12850
- const file = await fs19.open(full, "r");
14609
+ const stat17 = await fs21.stat(full);
14610
+ if (!stat17.isFile() || stat17.size > maxBytes || stopped || signal.aborted) return;
14611
+ const file = await fs21.open(full, "r");
12851
14612
  try {
12852
14613
  let bytesReadTotal = 0;
12853
14614
  let lineNumber = 0;
@@ -12926,7 +14687,7 @@ async function runNative(input, base, mode, limit, signal) {
12926
14687
  if (stopped || signal.aborted) return;
12927
14688
  let entries;
12928
14689
  try {
12929
- entries = await fs19.readdir(dir, { withFileTypes: true });
14690
+ entries = await fs21.readdir(dir, { withFileTypes: true });
12930
14691
  } catch {
12931
14692
  return;
12932
14693
  }
@@ -12936,7 +14697,7 @@ async function runNative(input, base, mode, limit, signal) {
12936
14697
  if (stopped) return;
12937
14698
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
12938
14699
  if (e.isSymbolicLink()) continue;
12939
- const full = path21.join(dir, e.name);
14700
+ const full = path25.join(dir, e.name);
12940
14701
  if (e.isDirectory()) {
12941
14702
  subdirs.push(full);
12942
14703
  } else if (e.isFile()) {
@@ -12959,7 +14720,7 @@ async function runNative(input, base, mode, limit, signal) {
12959
14720
  init_spawn_stream();
12960
14721
  init_util();
12961
14722
  init_legacy_bridge();
12962
- import { join as join18 } from "node:path";
14723
+ import { join as join21 } from "node:path";
12963
14724
  import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
12964
14725
  var installTool = {
12965
14726
  name: "install",
@@ -13136,16 +14897,16 @@ function resolveManifestPath(cwd, pkgManager) {
13136
14897
  case "pnpm":
13137
14898
  case "yarn":
13138
14899
  case "npm":
13139
- return join18(cwd, "package.json");
14900
+ return join21(cwd, "package.json");
13140
14901
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
13141
14902
  default:
13142
- return join18(cwd, "package.json");
14903
+ return join21(cwd, "package.json");
13143
14904
  }
13144
14905
  }
13145
14906
 
13146
14907
  // src/json.ts
13147
14908
  init_util();
13148
- import * as fs20 from "node:fs/promises";
14909
+ import * as fs22 from "node:fs/promises";
13149
14910
  import { deepMerge as deepMergeCore } from "@wrongstack/core";
13150
14911
  var jsonTool = {
13151
14912
  name: "json",
@@ -13220,7 +14981,7 @@ async function executeParse(input, ctx) {
13220
14981
  let raw;
13221
14982
  if (input.file) {
13222
14983
  try {
13223
- raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
14984
+ raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13224
14985
  } catch {
13225
14986
  return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
13226
14987
  }
@@ -13278,7 +15039,7 @@ async function executeQuery(input, ctx) {
13278
15039
  let parsed;
13279
15040
  if (input.file) {
13280
15041
  try {
13281
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15042
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13282
15043
  parsed = JSON.parse(raw);
13283
15044
  } catch {
13284
15045
  return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
@@ -13320,7 +15081,7 @@ async function executeValidate(input, ctx) {
13320
15081
  let parsed;
13321
15082
  if (input.file) {
13322
15083
  try {
13323
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15084
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13324
15085
  parsed = JSON.parse(raw);
13325
15086
  } catch {
13326
15087
  return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
@@ -13362,7 +15123,7 @@ async function executeTransform(input, ctx) {
13362
15123
  let parsed;
13363
15124
  if (input.file) {
13364
15125
  try {
13365
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15126
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13366
15127
  parsed = JSON.parse(raw);
13367
15128
  } catch {
13368
15129
  return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
@@ -13524,56 +15285,56 @@ function jmespathSearch(data, query) {
13524
15285
  }
13525
15286
  function validateJsonSchema(data, schema) {
13526
15287
  const errors = [];
13527
- function check(value, s, path28) {
15288
+ function check(value, s, path32) {
13528
15289
  if (s["type"]) {
13529
15290
  const expectedType = s["type"];
13530
15291
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
13531
15292
  if (expectedType === "integer") {
13532
- if (!Number.isInteger(value)) errors.push(`${path28}: expected integer, got ${actualType}`);
15293
+ if (!Number.isInteger(value)) errors.push(`${path32}: expected integer, got ${actualType}`);
13533
15294
  } else if (expectedType !== actualType) {
13534
- errors.push(`${path28}: expected ${expectedType}, got ${actualType}`);
15295
+ errors.push(`${path32}: expected ${expectedType}, got ${actualType}`);
13535
15296
  }
13536
15297
  }
13537
15298
  if (typeof value === "string" && s["format"] === "uri" && value) {
13538
15299
  try {
13539
15300
  new URL(value);
13540
15301
  } catch {
13541
- errors.push(`${path28}: not a valid URI`);
15302
+ errors.push(`${path32}: not a valid URI`);
13542
15303
  }
13543
15304
  }
13544
15305
  if (typeof value === "string" && s["pattern"]) {
13545
15306
  const re = new RegExp(s["pattern"]);
13546
- if (!re.test(value)) errors.push(`${path28}: does not match pattern ${s["pattern"]}`);
15307
+ if (!re.test(value)) errors.push(`${path32}: does not match pattern ${s["pattern"]}`);
13547
15308
  }
13548
15309
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
13549
- errors.push(`${path28}: string too short (min ${s["minLength"]})`);
15310
+ errors.push(`${path32}: string too short (min ${s["minLength"]})`);
13550
15311
  }
13551
15312
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
13552
- errors.push(`${path28}: string too long (max ${s["maxLength"]})`);
15313
+ errors.push(`${path32}: string too long (max ${s["maxLength"]})`);
13553
15314
  }
13554
15315
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
13555
- errors.push(`${path28}: below minimum ${s["minimum"]}`);
15316
+ errors.push(`${path32}: below minimum ${s["minimum"]}`);
13556
15317
  }
13557
15318
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
13558
- errors.push(`${path28}: above maximum ${s["maximum"]}`);
15319
+ errors.push(`${path32}: above maximum ${s["maximum"]}`);
13559
15320
  }
13560
15321
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
13561
15322
  for (let i = 0; i < value.length; i++) {
13562
- check(value[i], s["items"], `${path28}[${i}]`);
15323
+ check(value[i], s["items"], `${path32}[${i}]`);
13563
15324
  }
13564
15325
  }
13565
15326
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
13566
15327
  const props = s["properties"];
13567
15328
  for (const [k, propSchema] of Object.entries(props)) {
13568
- check(value[k], propSchema, `${path28}.${k}`);
15329
+ check(value[k], propSchema, `${path32}.${k}`);
13569
15330
  }
13570
15331
  }
13571
15332
  }
13572
15333
  check(data, schema, "$");
13573
15334
  return { valid: errors.length === 0, errors };
13574
15335
  }
13575
- function simpleQuery(data, path28) {
13576
- const parts = path28.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
15336
+ function simpleQuery(data, path32) {
15337
+ const parts = path32.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
13577
15338
  let current = data;
13578
15339
  for (const part of parts) {
13579
15340
  if (current === null || current === void 0) return void 0;
@@ -13623,7 +15384,7 @@ function toYaml(data, indent = 0) {
13623
15384
  }
13624
15385
 
13625
15386
  // src/kanban.ts
13626
- import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core";
15387
+ import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
13627
15388
  import {
13628
15389
  addCheckToTask,
13629
15390
  addColumn,
@@ -13635,18 +15396,19 @@ import {
13635
15396
  assignTask,
13636
15397
  claimReadyTask,
13637
15398
  copyTaskToBoard,
13638
- createBoard,
15399
+ createBoard as createBoard2,
15400
+ createBoardFromTaskGraph,
13639
15401
  duplicateBoard,
13640
15402
  exportBoardAsMarkdown,
13641
15403
  exportBoardToTaskGraph,
13642
15404
  generateBoardFromDescription,
13643
- getBoard,
15405
+ getBoard as getBoard2,
13644
15406
  getKanbanOrchestrationSnapshot,
13645
15407
  getKanbanQueueHealth,
13646
15408
  getTask,
13647
15409
  getTaskChain,
13648
15410
  heartbeatTaskAssignment,
13649
- listBoards,
15411
+ listBoards as listBoards2,
13650
15412
  listKanbanEvents,
13651
15413
  listReadyTasks,
13652
15414
  mergeTasks,
@@ -13654,21 +15416,244 @@ import {
13654
15416
  parseLinesIntoTasks,
13655
15417
  recoverStaleTaskAssignments,
13656
15418
  releaseTaskClaim,
13657
- removeBoard,
15419
+ removeBoard as removeBoard2,
13658
15420
  removeColumn,
13659
15421
  removeTask,
13660
15422
  searchKanban,
13661
15423
  setTaskChain,
13662
15424
  splitTask,
13663
- syncBoardFromTaskGraph,
15425
+ syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
13664
15426
  transferTaskToBoard,
13665
- updateBoard,
15427
+ updateBoard as updateBoard2,
13666
15428
  updateCheckOnTask,
13667
15429
  updateColumn,
13668
15430
  updateGoalMetricOnTask,
13669
15431
  updateTask,
13670
15432
  updateTaskAssignment
13671
15433
  } from "@wrongstack/kanban";
15434
+
15435
+ // src/session-kanban.ts
15436
+ import {
15437
+ deserializeTaskGraph,
15438
+ loadPlan,
15439
+ loadTasks,
15440
+ mutatePlan,
15441
+ mutateTasks
15442
+ } from "@wrongstack/core";
15443
+ import {
15444
+ createBoard,
15445
+ getBoard,
15446
+ listBoards,
15447
+ removeBoard,
15448
+ syncBoardFromTaskGraph,
15449
+ updateBoard
15450
+ } from "@wrongstack/kanban";
15451
+ var SESSION_BOARD_TAG = "session-work";
15452
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
15453
+ var SESSION_KANBAN_COLUMNS = [
15454
+ { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
15455
+ { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
15456
+ { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
15457
+ { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
15458
+ ];
15459
+ var boardQueue = /* @__PURE__ */ new Map();
15460
+ var boardEnsures = /* @__PURE__ */ new Map();
15461
+ function boardKey(projectRoot, sessionId) {
15462
+ return `${projectRoot}\0${sessionId}`;
15463
+ }
15464
+ function sessionTag(sessionId) {
15465
+ return `session:${sessionId}`;
15466
+ }
15467
+ function sessionBoardTitle(sessionId) {
15468
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
15469
+ return `Session ${leaf.slice(0, 12)}`;
15470
+ }
15471
+ function sessionBoardTags(sessionId) {
15472
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
15473
+ }
15474
+ function sameColumns(columns) {
15475
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
15476
+ }
15477
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
15478
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
15479
+ const key = boardKey(projectRoot, sessionId);
15480
+ const inFlight = boardEnsures.get(key);
15481
+ if (inFlight) return inFlight;
15482
+ const promise = (async () => {
15483
+ const summary = (await listBoards(projectRoot)).find(
15484
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
15485
+ );
15486
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
15487
+ if (!board) {
15488
+ return createBoard(projectRoot, {
15489
+ title: sessionBoardTitle(sessionId),
15490
+ description: "Live session work: todos, tasks, and plan items.",
15491
+ tags: sessionBoardTags(sessionId),
15492
+ columns: SESSION_KANBAN_COLUMNS,
15493
+ generatedBy: `session-kanban:${sessionId}`
15494
+ });
15495
+ }
15496
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
15497
+ board = await updateBoard(projectRoot, board.id, {
15498
+ title: sessionBoardTitle(sessionId),
15499
+ description: "Live session work: todos, tasks, and plan items.",
15500
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
15501
+ columns: SESSION_KANBAN_COLUMNS
15502
+ }) ?? board;
15503
+ }
15504
+ return board;
15505
+ })();
15506
+ boardEnsures.set(key, promise);
15507
+ try {
15508
+ return await promise;
15509
+ } finally {
15510
+ boardEnsures.delete(key);
15511
+ }
15512
+ }
15513
+ function enqueueBoardWork(projectRoot, sessionId, work) {
15514
+ const key = boardKey(projectRoot, sessionId);
15515
+ const previous = boardQueue.get(key) ?? Promise.resolve();
15516
+ const result = previous.catch(() => void 0).then(work);
15517
+ const tail = result.then(
15518
+ () => void 0,
15519
+ () => void 0
15520
+ );
15521
+ boardQueue.set(key, tail);
15522
+ void tail.then(() => {
15523
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
15524
+ });
15525
+ return result;
15526
+ }
15527
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
15528
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
15529
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
15530
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
15531
+ if (!board) return null;
15532
+ const result = await syncBoardFromTaskGraph(
15533
+ projectRoot,
15534
+ board.id,
15535
+ deserializeTaskGraph(graph),
15536
+ {
15537
+ sourceSystem,
15538
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
15539
+ archiveMissingTasks: true,
15540
+ includeCompletedTasks: true
15541
+ }
15542
+ );
15543
+ return result?.board ?? null;
15544
+ });
15545
+ }
15546
+ function todoListToSerializedGraph(todos, sessionId) {
15547
+ const nodes = todos.map((todo, index) => ({
15548
+ id: todo.id,
15549
+ title: todo.content,
15550
+ description: todo.activeForm ?? "",
15551
+ type: "chore",
15552
+ priority: "medium",
15553
+ status: todo.status,
15554
+ createdAt: index,
15555
+ updatedAt: index
15556
+ }));
15557
+ return {
15558
+ id: `todo:${sessionId}`,
15559
+ specId: `todo:${sessionId}`,
15560
+ title: "Session todos",
15561
+ nodes,
15562
+ edges: [],
15563
+ rootNodes: nodes.map((node) => node.id),
15564
+ createdAt: 0,
15565
+ updatedAt: 0
15566
+ };
15567
+ }
15568
+ function taskFileToSerializedGraph(tasks, sessionId) {
15569
+ const ids = new Set(tasks.map((task) => task.id));
15570
+ const nodes = tasks.map((task, index) => ({
15571
+ id: task.id,
15572
+ title: task.title,
15573
+ description: task.description ?? "",
15574
+ type: task.type,
15575
+ priority: task.priority,
15576
+ status: task.status,
15577
+ ...task.assignee ? { assignee: task.assignee } : {},
15578
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
15579
+ createdAt: index,
15580
+ updatedAt: index
15581
+ }));
15582
+ const edges = tasks.flatMap(
15583
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
15584
+ id: `${dependency}->${task.id}`,
15585
+ from: dependency,
15586
+ to: task.id,
15587
+ type: "depends_on"
15588
+ }))
15589
+ );
15590
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
15591
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
15592
+ return {
15593
+ // Keep the historical graph id so existing mirrored task cards are reused.
15594
+ id: `session:${sessionId}`,
15595
+ specId: `session:${sessionId}`,
15596
+ title: "Session tasks",
15597
+ nodes,
15598
+ edges,
15599
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
15600
+ createdAt: 0,
15601
+ updatedAt: 0
15602
+ };
15603
+ }
15604
+ var PLAN_STATUS_TO_TASK = {
15605
+ open: "pending",
15606
+ in_progress: "in_progress",
15607
+ done: "completed"
15608
+ };
15609
+ function planFileToSerializedGraph(items, sessionId) {
15610
+ const nodes = items.map((item, index) => ({
15611
+ id: item.id,
15612
+ title: item.title,
15613
+ description: item.details ?? "",
15614
+ type: "chore",
15615
+ priority: "medium",
15616
+ status: PLAN_STATUS_TO_TASK[item.status],
15617
+ createdAt: index,
15618
+ updatedAt: index
15619
+ }));
15620
+ return {
15621
+ id: `plan:${sessionId}`,
15622
+ specId: `plan:${sessionId}`,
15623
+ title: "Session plan",
15624
+ nodes,
15625
+ edges: [],
15626
+ rootNodes: nodes.map((node) => node.id),
15627
+ createdAt: 0,
15628
+ updatedAt: 0
15629
+ };
15630
+ }
15631
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
15632
+ return projectGraph(
15633
+ projectRoot,
15634
+ sessionId,
15635
+ todoListToSerializedGraph(todos, sessionId),
15636
+ "session-todo"
15637
+ );
15638
+ }
15639
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
15640
+ return projectGraph(
15641
+ projectRoot,
15642
+ sessionId,
15643
+ taskFileToSerializedGraph(tasks, sessionId),
15644
+ "session-task"
15645
+ );
15646
+ }
15647
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
15648
+ return projectGraph(
15649
+ projectRoot,
15650
+ sessionId,
15651
+ planFileToSerializedGraph(items, sessionId),
15652
+ "session-plan"
15653
+ );
15654
+ }
15655
+
15656
+ // src/kanban.ts
13672
15657
  var kanbanTool = {
13673
15658
  name: "kanban",
13674
15659
  category: "Project",
@@ -13695,6 +15680,8 @@ var kanbanTool = {
13695
15680
  "export_markdown",
13696
15681
  "export_task_graph",
13697
15682
  "sync_task_graph",
15683
+ "create_from_graph",
15684
+ "import_session_tasks",
13698
15685
  "search_tasks",
13699
15686
  "ready_tasks",
13700
15687
  "snapshot",
@@ -13741,6 +15728,10 @@ var kanbanTool = {
13741
15728
  tags: { type: "array", items: { type: "string" } },
13742
15729
  labels: { type: "array", items: { type: "string" } },
13743
15730
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
15731
+ taskType: {
15732
+ type: "string",
15733
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
15734
+ },
13744
15735
  status: {
13745
15736
  type: "string",
13746
15737
  enum: [
@@ -13783,8 +15774,21 @@ var kanbanTool = {
13783
15774
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
13784
15775
  releaseReason: { type: "string" },
13785
15776
  clearAssignee: { type: "boolean" },
13786
- recoveryMode: { type: "string", enum: ["release", "retry", "fail"] },
15777
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
13787
15778
  recoveryNow: { type: "string" },
15779
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
15780
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
15781
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
15782
+ recoveryPolicyRetryPolicyOverride: {
15783
+ type: "string",
15784
+ enum: ["off", "incremental", "exponential"]
15785
+ },
15786
+ assignee: { type: "string" },
15787
+ costCeilingUsd: { type: "number" },
15788
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
15789
+ lastFailureKind: { type: "string" },
15790
+ dependsOn: { type: "array", items: { type: "string" } },
15791
+ estimatedHours: { type: "number" },
13788
15792
  taskGraph: { type: "object" },
13789
15793
  graphId: { type: "string" },
13790
15794
  specId: { type: "string" },
@@ -13840,7 +15844,7 @@ var kanbanTool = {
13840
15844
  try {
13841
15845
  switch (input.action) {
13842
15846
  case "list_boards": {
13843
- const boards = await listBoards(projectRoot);
15847
+ const boards = await listBoards2(projectRoot);
13844
15848
  return { ok: true, message: `${boards.length} board(s).`, boards };
13845
15849
  }
13846
15850
  case "get_board": {
@@ -13849,7 +15853,7 @@ var kanbanTool = {
13849
15853
  }
13850
15854
  case "create_board": {
13851
15855
  if (!input.title) return fail("create_board requires title.");
13852
- const board = await createBoard(projectRoot, {
15856
+ const board = await createBoard2(projectRoot, {
13853
15857
  title: input.title,
13854
15858
  ...input.description !== void 0 ? { description: input.description } : {},
13855
15859
  ...input.tags !== void 0 ? { tags: input.tags } : {},
@@ -13859,7 +15863,7 @@ var kanbanTool = {
13859
15863
  }
13860
15864
  case "update_board": {
13861
15865
  if (!input.boardId) return fail("update_board requires boardId.");
13862
- const board = await updateBoard(projectRoot, input.boardId, {
15866
+ const board = await updateBoard2(projectRoot, input.boardId, {
13863
15867
  ...input.title !== void 0 ? { title: input.title } : {},
13864
15868
  ...input.description !== void 0 ? { description: input.description } : {},
13865
15869
  ...input.tags !== void 0 ? { tags: input.tags } : {}
@@ -13879,7 +15883,7 @@ var kanbanTool = {
13879
15883
  }
13880
15884
  case "delete_board": {
13881
15885
  if (!input.boardId) return fail("delete_board requires boardId.");
13882
- const removed = await removeBoard(projectRoot, input.boardId);
15886
+ const removed = await removeBoard2(projectRoot, input.boardId);
13883
15887
  return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
13884
15888
  }
13885
15889
  case "generate_board": {
@@ -13890,14 +15894,14 @@ var kanbanTool = {
13890
15894
  ...input.context !== void 0 ? { context: input.context } : {},
13891
15895
  ...input.columns !== void 0 ? { columns: input.columns } : {}
13892
15896
  });
13893
- const board = await createBoard(projectRoot, boardInput);
15897
+ const board = await createBoard2(projectRoot, boardInput);
13894
15898
  for (const taskInput2 of parseLinesIntoTasks(
13895
15899
  input.description,
13896
15900
  board.columns[0]?.id ?? "backlog"
13897
15901
  )) {
13898
15902
  await addTask(projectRoot, board.id, taskInput2);
13899
15903
  }
13900
- return okBoard(await getBoard(projectRoot, board.id) ?? board, "Board generated.");
15904
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
13901
15905
  }
13902
15906
  case "export_markdown": {
13903
15907
  const board = await requireBoard(projectRoot, input.boardId);
@@ -13930,8 +15934,8 @@ var kanbanTool = {
13930
15934
  if (!input.boardId || !input.taskGraph) {
13931
15935
  return fail("sync_task_graph requires boardId and taskGraph.");
13932
15936
  }
13933
- const graph = deserializeTaskGraph(input.taskGraph);
13934
- const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
15937
+ const graph = deserializeTaskGraph2(input.taskGraph);
15938
+ const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
13935
15939
  ...input.title !== void 0 ? { title: input.title } : {},
13936
15940
  ...input.description !== void 0 ? { description: input.description } : {},
13937
15941
  ...input.tags !== void 0 ? { tags: input.tags } : {},
@@ -13948,6 +15952,59 @@ var kanbanTool = {
13948
15952
  board: result.board
13949
15953
  } : fail("Board not found.");
13950
15954
  }
15955
+ case "create_from_graph": {
15956
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
15957
+ const graph = deserializeTaskGraph2(input.taskGraph);
15958
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
15959
+ ...input.title !== void 0 ? { title: input.title } : {},
15960
+ ...input.description !== void 0 ? { description: input.description } : {},
15961
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
15962
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15963
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
15964
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
15965
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
15966
+ });
15967
+ return {
15968
+ ok: true,
15969
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
15970
+ board
15971
+ };
15972
+ }
15973
+ case "import_session_tasks": {
15974
+ const taskPath = ctx.meta?.["task.path"];
15975
+ if (!taskPath) return fail("No session task file for this session.");
15976
+ const file = await loadTasks2(taskPath);
15977
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
15978
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
15979
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
15980
+ const tags = ["session", `session:${sessionId}`];
15981
+ const existing = (await listBoards2(projectRoot)).find(
15982
+ (b) => b.tags?.includes(`session:${sessionId}`)
15983
+ );
15984
+ if (existing) {
15985
+ const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
15986
+ sourceSystem: "session",
15987
+ tags,
15988
+ archiveMissingTasks: true,
15989
+ includeCompletedTasks: true
15990
+ });
15991
+ return result ? {
15992
+ ok: true,
15993
+ message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
15994
+ board: result.board
15995
+ } : fail("Session board vanished mid-sync.");
15996
+ }
15997
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
15998
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
15999
+ sourceSystem: "session",
16000
+ tags
16001
+ });
16002
+ return {
16003
+ ok: true,
16004
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16005
+ board
16006
+ };
16007
+ }
13951
16008
  case "search_tasks": {
13952
16009
  const tasks = await searchKanban(projectRoot, {
13953
16010
  query: input.query,
@@ -14384,7 +16441,7 @@ function okTask(board, task, message) {
14384
16441
  return { ok: true, message, board, task };
14385
16442
  }
14386
16443
  async function requireBoard(projectRoot, boardId) {
14387
- return boardId ? getBoard(projectRoot, boardId) : null;
16444
+ return boardId ? getBoard2(projectRoot, boardId) : null;
14388
16445
  }
14389
16446
  function taskInput(input) {
14390
16447
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
@@ -14393,14 +16450,23 @@ function taskInput(input) {
14393
16450
  columnId: input.columnId,
14394
16451
  description: input.description,
14395
16452
  priority: input.priority,
16453
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
14396
16454
  status: input.status,
14397
16455
  labels: input.labels,
14398
16456
  ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
14399
16457
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
14400
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {},
16458
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16459
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
14401
16460
  ...assignment ? { assignment } : {}
14402
16461
  };
14403
16462
  }
16463
+ function mergedDependsOn(input) {
16464
+ const ids = [
16465
+ ...input.dependsOn ?? [],
16466
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
16467
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
16468
+ return ids.length > 0 ? ids : void 0;
16469
+ }
14404
16470
  function taskPatch(input) {
14405
16471
  return {
14406
16472
  title: input.title,
@@ -14408,10 +16474,12 @@ function taskPatch(input) {
14408
16474
  columnId: input.columnId,
14409
16475
  order: input.order,
14410
16476
  priority: input.priority,
16477
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
14411
16478
  status: input.status,
14412
16479
  labels: input.labels,
14413
16480
  assignedAgent: input.agentId,
14414
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {}
16481
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16482
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
14415
16483
  };
14416
16484
  }
14417
16485
  function assignmentInput(input) {
@@ -14577,11 +16645,11 @@ var lintTool = {
14577
16645
  }
14578
16646
  };
14579
16647
  async function detectLinter(cwd) {
14580
- const { stat: stat15 } = await import("node:fs/promises");
16648
+ const { stat: stat17 } = await import("node:fs/promises");
14581
16649
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
14582
16650
  for (const f of checks) {
14583
16651
  try {
14584
- await stat15(`${cwd}/${f}`);
16652
+ await stat17(`${cwd}/${f}`);
14585
16653
  if (f.includes("biome")) return "biome";
14586
16654
  if (f.includes("eslint")) return "eslint";
14587
16655
  if (f.includes("tslint")) return "tslint";
@@ -14681,7 +16749,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14681
16749
  };
14682
16750
  }
14683
16751
  args.push("--timestamps", service);
14684
- return new Promise((resolve11) => {
16752
+ return new Promise((resolve14) => {
14685
16753
  let stdout = "";
14686
16754
  let stderr = "";
14687
16755
  const MAX = 2e5;
@@ -14697,7 +16765,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14697
16765
  if (settled) return;
14698
16766
  settled = true;
14699
16767
  clearTimeout(timer);
14700
- resolve11(result);
16768
+ resolve14(result);
14701
16769
  };
14702
16770
  const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
14703
16771
  const timer = setTimeout(() => {
@@ -14732,16 +16800,16 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14732
16800
  }
14733
16801
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
14734
16802
  var MAX_TAIL_LINES = 1e5;
14735
- async function fileLogs(path28, lines, filterRe, stream) {
16803
+ async function fileLogs(path32, lines, filterRe, stream) {
14736
16804
  const { createInterface } = await import("node:readline");
14737
- const { createReadStream } = await import("node:fs");
16805
+ const { createReadStream: createReadStream2 } = await import("node:fs");
14738
16806
  const entries = [];
14739
16807
  const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
14740
16808
  const window = new Array(effLines);
14741
16809
  let writeIdx = 0;
14742
16810
  let totalLines = 0;
14743
16811
  const rl = createInterface({
14744
- input: createReadStream(path28),
16812
+ input: createReadStream2(path32),
14745
16813
  crlfDelay: Number.POSITIVE_INFINITY
14746
16814
  });
14747
16815
  for await (const line of rl) {
@@ -14762,7 +16830,7 @@ async function fileLogs(path28, lines, filterRe, stream) {
14762
16830
  if (parsed) entries.push(parsed);
14763
16831
  }
14764
16832
  return {
14765
- source: path28,
16833
+ source: path32,
14766
16834
  entries,
14767
16835
  total: entries.length,
14768
16836
  truncated: totalLines > effLines,
@@ -14916,7 +16984,7 @@ var outdatedTool = {
14916
16984
  }
14917
16985
  };
14918
16986
  function runOutdated(manager, args, cwd, signal) {
14919
- return new Promise((resolve11) => {
16987
+ return new Promise((resolve14) => {
14920
16988
  let stdout = "";
14921
16989
  let stderr = "";
14922
16990
  const MAX = 1e5;
@@ -14941,10 +17009,10 @@ function runOutdated(manager, args, cwd, signal) {
14941
17009
  });
14942
17010
  child.on("close", (code) => {
14943
17011
  const result = parseOutdatedOutput(stdout, code ?? 0);
14944
- resolve11(result);
17012
+ resolve14(result);
14945
17013
  });
14946
17014
  child.on("error", (e) => {
14947
- resolve11({
17015
+ resolve14({
14948
17016
  exit_code: 1,
14949
17017
  packages: [],
14950
17018
  total: 0,
@@ -14954,9 +17022,9 @@ function runOutdated(manager, args, cwd, signal) {
14954
17022
  });
14955
17023
  });
14956
17024
  }
14957
- function parseOutdatedOutput(json, exitCode) {
17025
+ function parseOutdatedOutput(json2, exitCode) {
14958
17026
  const packages = [];
14959
- if (!json) {
17027
+ if (!json2) {
14960
17028
  return {
14961
17029
  exit_code: exitCode,
14962
17030
  packages: [],
@@ -14966,7 +17034,7 @@ function parseOutdatedOutput(json, exitCode) {
14966
17034
  };
14967
17035
  }
14968
17036
  try {
14969
- const data = JSON.parse(json);
17037
+ const data = JSON.parse(json2);
14970
17038
  for (const name of Object.keys(data)) {
14971
17039
  const info = data[name];
14972
17040
  packages.push({
@@ -14984,17 +17052,17 @@ function parseOutdatedOutput(json, exitCode) {
14984
17052
  exit_code: exitCode,
14985
17053
  packages,
14986
17054
  total: packages.length,
14987
- output: json,
14988
- truncated: json.length >= 1e5
17055
+ output: json2,
17056
+ truncated: json2.length >= 1e5
14989
17057
  };
14990
17058
  }
14991
17059
 
14992
17060
  // src/patch.ts
14993
17061
  init_util();
14994
17062
  import { spawn as spawn13 } from "node:child_process";
14995
- import * as fs21 from "node:fs/promises";
17063
+ import * as fs23 from "node:fs/promises";
14996
17064
  import * as os7 from "node:os";
14997
- import * as path22 from "node:path";
17065
+ import * as path26 from "node:path";
14998
17066
  import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
14999
17067
  var patchTool = {
15000
17068
  name: "patch",
@@ -15030,9 +17098,9 @@ var patchTool = {
15030
17098
  for (const t of targets) {
15031
17099
  const stripped = stripPathComponents(t, strip);
15032
17100
  if (!stripped) continue;
15033
- const candidate = path22.resolve(dir, stripped);
15034
- const rel = path22.relative(ctx.projectRoot, candidate);
15035
- if (rel.startsWith("..") || path22.isAbsolute(rel)) {
17101
+ const candidate = path26.resolve(dir, stripped);
17102
+ const rel = path26.relative(ctx.projectRoot, candidate);
17103
+ if (rel.startsWith("..") || path26.isAbsolute(rel)) {
15036
17104
  return {
15037
17105
  applied: 0,
15038
17106
  rejected: 1,
@@ -15049,12 +17117,12 @@ var patchTool = {
15049
17117
  beforeContents.set(target, await readTextForTracking(target));
15050
17118
  }
15051
17119
  }
15052
- const tmpDir = await fs21.mkdtemp(path22.join(os7.tmpdir(), ".wstack_patch_"));
17120
+ const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
15053
17121
  try {
15054
- await fs21.chmod(tmpDir, 448).catch(() => {
17122
+ await fs23.chmod(tmpDir, 448).catch(() => {
15055
17123
  });
15056
- const patchFile = path22.join(tmpDir, "in.diff");
15057
- await fs21.writeFile(patchFile, input.patch, { mode: 384 });
17124
+ const patchFile = path26.join(tmpDir, "in.diff");
17125
+ await fs23.writeFile(patchFile, input.patch, { mode: 384 });
15058
17126
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
15059
17127
  const result = await runPatch(args, dir, opts.signal);
15060
17128
  if (result.exitCode !== 0 && !dryRun) {
@@ -15072,8 +17140,8 @@ var patchTool = {
15072
17140
  const before = beforeContents.get(target) ?? null;
15073
17141
  const after = await readTextForTracking(target);
15074
17142
  if (after === null || after === before) continue;
15075
- const stat15 = await fs21.stat(target).catch(() => null);
15076
- if (stat15) ctx.recordRead?.(target, stat15.mtimeMs, "write", sha256hex(after));
17143
+ const stat17 = await fs23.stat(target).catch(() => null);
17144
+ if (stat17) ctx.recordRead?.(target, stat17.mtimeMs, "write", sha256hex(after));
15077
17145
  ctx.session?.recordFileChange?.({
15078
17146
  path: target,
15079
17147
  action: before === null ? "created" : "modified",
@@ -15090,7 +17158,7 @@ var patchTool = {
15090
17158
  message: result.stdout || "patch applied"
15091
17159
  };
15092
17160
  } finally {
15093
- await fs21.rm(tmpDir, { recursive: true, force: true }).catch(() => {
17161
+ await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
15094
17162
  });
15095
17163
  }
15096
17164
  }
@@ -15098,9 +17166,9 @@ var patchTool = {
15098
17166
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
15099
17167
  async function readTextForTracking(absPath) {
15100
17168
  try {
15101
- const stat15 = await fs21.stat(absPath);
15102
- if (!stat15.isFile() || stat15.size > MAX_TRACKING_BYTES) return null;
15103
- const buf = await fs21.readFile(absPath);
17169
+ const stat17 = await fs23.stat(absPath);
17170
+ if (!stat17.isFile() || stat17.size > MAX_TRACKING_BYTES) return null;
17171
+ const buf = await fs23.readFile(absPath);
15104
17172
  if (buf.includes(0)) return null;
15105
17173
  return buf.toString("utf8");
15106
17174
  } catch {
@@ -15125,7 +17193,7 @@ function stripPathComponents(p, strip) {
15125
17193
  return parts.slice(strip).join("/");
15126
17194
  }
15127
17195
  function runPatch(args, cwd, signal) {
15128
- return new Promise((resolve11) => {
17196
+ return new Promise((resolve14) => {
15129
17197
  let stdout = "";
15130
17198
  let stderr = "";
15131
17199
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
@@ -15136,8 +17204,8 @@ function runPatch(args, cwd, signal) {
15136
17204
  child.stderr?.on("data", (c) => {
15137
17205
  stderr += c.toString();
15138
17206
  });
15139
- child.on("close", (code) => resolve11({ exitCode: code ?? 1, stdout, stderr }));
15140
- child.on("error", (e) => resolve11({ exitCode: 1, stdout: "", stderr: e.message }));
17207
+ child.on("close", (code) => resolve14({ exitCode: code ?? 1, stdout, stderr }));
17208
+ child.on("error", (e) => resolve14({ exitCode: 1, stdout: "", stderr: e.message }));
15141
17209
  });
15142
17210
  }
15143
17211
  function extractPatchedFiles(output) {
@@ -15156,12 +17224,12 @@ import {
15156
17224
  deriveTodosFromPlanItem,
15157
17225
  formatPlan,
15158
17226
  getPlanTemplate,
15159
- mutatePlan,
17227
+ mutatePlan as mutatePlan2,
15160
17228
  removePlanItem,
15161
17229
  setPlanItemStatus
15162
17230
  } from "@wrongstack/core";
15163
17231
  import {
15164
- mutateTasks,
17232
+ mutateTasks as mutateTasks2,
15165
17233
  formatTaskList
15166
17234
  } from "@wrongstack/core";
15167
17235
  import { randomUUID } from "node:crypto";
@@ -15248,7 +17316,7 @@ var planTool = {
15248
17316
  let didTaskify = false;
15249
17317
  let plan;
15250
17318
  try {
15251
- plan = await mutatePlan(planPath, sessionId, async (p) => {
17319
+ plan = await mutatePlan2(planPath, sessionId, async (p) => {
15252
17320
  switch (input.action) {
15253
17321
  case "show":
15254
17322
  break;
@@ -15374,6 +17442,7 @@ var planTool = {
15374
17442
  open: 0
15375
17443
  };
15376
17444
  }
17445
+ await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
15377
17446
  if (early) return early;
15378
17447
  if (didTaskify) {
15379
17448
  const taskPathRaw = ctx.meta["task.path"];
@@ -15387,7 +17456,7 @@ var planTool = {
15387
17456
  }
15388
17457
  const now = (/* @__PURE__ */ new Date()).toISOString();
15389
17458
  try {
15390
- const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
17459
+ const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
15391
17460
  f.tasks.push({
15392
17461
  id: `task_${randomUUID()}`,
15393
17462
  title: taskifyMeta.title,
@@ -15414,13 +17483,13 @@ ${formatTaskList(taskFile.tasks)}`
15414
17483
  }
15415
17484
  };
15416
17485
  function mkResult(plan, ok, message, todos) {
15417
- const open2 = plan.items.filter((i) => i.status !== "done").length;
17486
+ const open3 = plan.items.filter((i) => i.status !== "done").length;
15418
17487
  const result = {
15419
17488
  ok,
15420
17489
  message,
15421
17490
  plan: formatPlan(plan),
15422
17491
  count: plan.items.length,
15423
- open: open2
17492
+ open: open3
15424
17493
  };
15425
17494
  if (todos !== void 0) result.todos = todos;
15426
17495
  return result;
@@ -15428,7 +17497,7 @@ function mkResult(plan, ok, message, todos) {
15428
17497
 
15429
17498
  // src/read.ts
15430
17499
  init_util();
15431
- import * as fs22 from "node:fs/promises";
17500
+ import * as fs24 from "node:fs/promises";
15432
17501
  import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
15433
17502
  var MAX_BYTES2 = 5 * 1024 * 1024;
15434
17503
  var readTool = {
@@ -15477,9 +17546,9 @@ var readTool = {
15477
17546
  });
15478
17547
  }
15479
17548
  const absPath = await safeResolveReal(input.path, ctx);
15480
- let stat15;
17549
+ let stat17;
15481
17550
  try {
15482
- stat15 = await fs22.stat(absPath);
17551
+ stat17 = await fs24.stat(absPath);
15483
17552
  } catch (err) {
15484
17553
  const code = err.code;
15485
17554
  if (code === "ENOENT") {
@@ -15498,7 +17567,7 @@ var readTool = {
15498
17567
  cause: err
15499
17568
  });
15500
17569
  }
15501
- if (!stat15.isFile()) {
17570
+ if (!stat17.isFile()) {
15502
17571
  throw new FsError({
15503
17572
  message: `read: "${input.path}" is not a regular file`,
15504
17573
  code: "FS_READ_FAILED",
@@ -15506,22 +17575,22 @@ var readTool = {
15506
17575
  context: { reason: "not-a-regular-file" }
15507
17576
  });
15508
17577
  }
15509
- if (stat15.size > MAX_BYTES2) {
17578
+ if (stat17.size > MAX_BYTES2) {
15510
17579
  throw new FsError({
15511
- message: `read: file too large (${stat15.size} bytes, limit ${MAX_BYTES2})`,
17580
+ message: `read: file too large (${stat17.size} bytes, limit ${MAX_BYTES2})`,
15512
17581
  code: "FS_READ_FAILED",
15513
17582
  path: absPath,
15514
- context: { size: stat15.size, limit: MAX_BYTES2, reason: "too-large" }
17583
+ context: { size: stat17.size, limit: MAX_BYTES2, reason: "too-large" }
15515
17584
  });
15516
17585
  }
15517
17586
  const offset = Math.max(1, input.offset ?? 1);
15518
17587
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
15519
17588
  const prior = getReadRangeRecord(ctx, absPath);
15520
17589
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
15521
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat15.mtimeMs, offset, requestedEnd)) {
15522
- ctx.recordRead(absPath, stat15.mtimeMs);
17590
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat17.mtimeMs, offset, requestedEnd)) {
17591
+ ctx.recordRead(absPath, stat17.mtimeMs);
15523
17592
  return {
15524
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat15.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
17593
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat17.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
15525
17594
  total_lines: prior.totalLines,
15526
17595
  encoding: "utf8",
15527
17596
  truncated: requestedEnd < prior.totalLines,
@@ -15529,7 +17598,7 @@ var readTool = {
15529
17598
  note: "Repeated read suppressed to save tokens."
15530
17599
  };
15531
17600
  }
15532
- const buf = await fs22.readFile(absPath);
17601
+ const buf = await fs24.readFile(absPath);
15533
17602
  if (isBinaryBuffer(buf)) {
15534
17603
  throw new Error(`read: "${input.path}" appears to be binary`);
15535
17604
  }
@@ -15538,10 +17607,10 @@ var readTool = {
15538
17607
  const allLines = text.split(/\r\n|\r|\n/);
15539
17608
  const total = allLines.length;
15540
17609
  if (input.mode === "summary") {
15541
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15542
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, 1, Math.min(total, 200));
17610
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17611
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, Math.min(total, 200));
15543
17612
  return {
15544
- text: summarizeFile(input.path, stat15.size, allLines),
17613
+ text: summarizeFile(input.path, stat17.size, allLines),
15545
17614
  total_lines: total,
15546
17615
  encoding: "utf8",
15547
17616
  truncated: total > 200,
@@ -15549,13 +17618,13 @@ var readTool = {
15549
17618
  };
15550
17619
  }
15551
17620
  if (limit === 0) {
15552
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15553
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, 1, 0);
17621
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17622
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, 0);
15554
17623
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
15555
17624
  }
15556
17625
  if (offset > total) {
15557
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15558
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, total + 1, total + 1);
17626
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17627
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, total + 1, total + 1);
15559
17628
  return {
15560
17629
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
15561
17630
  total_lines: total,
@@ -15567,8 +17636,8 @@ var readTool = {
15567
17636
  const truncated = offset - 1 + slice.length < total;
15568
17637
  const width = String(offset + slice.length - 1).length;
15569
17638
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
15570
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15571
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, offset, offset + slice.length - 1);
17639
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17640
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, offset, offset + slice.length - 1);
15572
17641
  return {
15573
17642
  text: numbered,
15574
17643
  total_lines: total,
@@ -15637,10 +17706,10 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
15637
17706
  // src/replace.ts
15638
17707
  import { expectDefined as expectDefined8 } from "@wrongstack/core";
15639
17708
  import { spawn as spawn14 } from "node:child_process";
15640
- import * as fs23 from "node:fs/promises";
15641
- import * as path23 from "node:path";
17709
+ import * as fs25 from "node:fs/promises";
17710
+ import * as path27 from "node:path";
15642
17711
  import {
15643
- atomicWrite as atomicWrite2,
17712
+ atomicWrite as atomicWrite3,
15644
17713
  buildChildEnv as buildChildEnv9,
15645
17714
  compileGlob as compileGlob5,
15646
17715
  detectNewlineStyle as detectNewlineStyle2,
@@ -15711,11 +17780,11 @@ var replaceTool = {
15711
17780
  const dryRun = input.dry_run ?? true;
15712
17781
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
15713
17782
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
15714
- const realRoot = await fs23.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
17783
+ const realRoot = await fs25.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
15715
17784
  const results = [];
15716
17785
  let totalReplacements = 0;
15717
17786
  for (const absPath of fileList) {
15718
- const lstat2 = await fs23.lstat(absPath).catch((err) => {
17787
+ const lstat2 = await fs25.lstat(absPath).catch((err) => {
15719
17788
  if (err.code === "ENOENT") return null;
15720
17789
  throw err;
15721
17790
  });
@@ -15723,17 +17792,17 @@ var replaceTool = {
15723
17792
  if (lstat2.isSymbolicLink()) continue;
15724
17793
  let realPath;
15725
17794
  try {
15726
- realPath = await fs23.realpath(absPath);
17795
+ realPath = await fs25.realpath(absPath);
15727
17796
  } catch {
15728
17797
  continue;
15729
17798
  }
15730
- const rel = path23.relative(realRoot, realPath);
15731
- if (rel.startsWith("..") || path23.isAbsolute(rel)) continue;
15732
- const stat15 = await fs23.stat(realPath).catch(() => null);
15733
- if (!stat15?.isFile()) continue;
17799
+ const rel = path27.relative(realRoot, realPath);
17800
+ if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
17801
+ const stat17 = await fs25.stat(realPath).catch(() => null);
17802
+ if (!stat17?.isFile()) continue;
15734
17803
  let content;
15735
17804
  try {
15736
- const buf = await fs23.readFile(realPath);
17805
+ const buf = await fs25.readFile(realPath);
15737
17806
  if (isBinaryBuffer(buf)) continue;
15738
17807
  content = buf.toString("utf8");
15739
17808
  } catch {
@@ -15755,8 +17824,8 @@ var replaceTool = {
15755
17824
  totalReplacements += count;
15756
17825
  if (!dryRun) {
15757
17826
  const newContent = toStyle2(newContentLf, style);
15758
- await atomicWrite2(realPath, newContent, { mode: stat15.mode & 511 });
15759
- const written = await fs23.stat(realPath).catch(() => null);
17827
+ await atomicWrite3(realPath, newContent, { mode: stat17.mode & 511 });
17828
+ const written = await fs25.stat(realPath).catch(() => null);
15760
17829
  if (written) {
15761
17830
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
15762
17831
  }
@@ -15795,8 +17864,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
15795
17864
  const resolved = [];
15796
17865
  for (const p of parts) {
15797
17866
  const absPath = safeResolve(p, ctx);
15798
- const stat15 = await fs23.stat(absPath).catch(() => null);
15799
- if (stat15?.isFile()) {
17867
+ const stat17 = await fs25.stat(absPath).catch(() => null);
17868
+ if (stat17?.isFile()) {
15800
17869
  resolved.push(absPath);
15801
17870
  }
15802
17871
  }
@@ -15814,13 +17883,13 @@ async function globFiles(pattern, base, extraGlob) {
15814
17883
  return await globNative(pattern, base, extraGlob);
15815
17884
  }
15816
17885
  function checkRg() {
15817
- return new Promise((resolve11) => {
17886
+ return new Promise((resolve14) => {
15818
17887
  try {
15819
17888
  const p = spawn14("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
15820
- p.on("error", () => resolve11(false));
15821
- p.on("close", (code) => resolve11(code === 0));
17889
+ p.on("error", () => resolve14(false));
17890
+ p.on("close", (code) => resolve14(code === 0));
15822
17891
  } catch {
15823
- resolve11(false);
17892
+ resolve14(false);
15824
17893
  }
15825
17894
  });
15826
17895
  }
@@ -15837,10 +17906,10 @@ function spawnRgFind(pattern, base) {
15837
17906
  buf += chunk.toString();
15838
17907
  });
15839
17908
  return {
15840
- promise: new Promise((resolve11, reject) => {
17909
+ promise: new Promise((resolve14, reject) => {
15841
17910
  child.on("error", reject);
15842
17911
  child.on("close", () => {
15843
- resolve11(buf.split("\n").filter(Boolean));
17912
+ resolve14(buf.split("\n").filter(Boolean));
15844
17913
  });
15845
17914
  })
15846
17915
  };
@@ -15851,16 +17920,16 @@ async function globNative(pattern, base, extraGlob) {
15851
17920
  const walk = async (dir) => {
15852
17921
  let entries;
15853
17922
  try {
15854
- entries = await fs23.readdir(dir, { withFileTypes: true });
17923
+ entries = await fs25.readdir(dir, { withFileTypes: true });
15855
17924
  } catch {
15856
17925
  return;
15857
17926
  }
15858
17927
  for (const e of entries) {
15859
17928
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
15860
- const full = path23.join(dir, e.name);
17929
+ const full = path27.join(dir, e.name);
15861
17930
  try {
15862
- const stat15 = await fs23.lstat(full);
15863
- if (stat15.isSymbolicLink()) continue;
17931
+ const stat17 = await fs25.lstat(full);
17932
+ if (stat17.isSymbolicLink()) continue;
15864
17933
  } catch {
15865
17934
  continue;
15866
17935
  }
@@ -15883,9 +17952,9 @@ async function globNative(pattern, base, extraGlob) {
15883
17952
 
15884
17953
  // src/scaffold.ts
15885
17954
  init_util();
15886
- import * as fs24 from "node:fs/promises";
15887
- import * as path24 from "node:path";
15888
- import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
17955
+ import * as fs26 from "node:fs/promises";
17956
+ import * as path28 from "node:path";
17957
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
15889
17958
  var BUILT_IN_TEMPLATES = {
15890
17959
  "npm-package": {
15891
17960
  description: "Basic npm package with ESM",
@@ -16034,17 +18103,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
16034
18103
  let filesCreated = 0;
16035
18104
  for (const [filePath, content] of Object.entries(templateFiles)) {
16036
18105
  const resolvedPath = substituteVars(filePath, name, vars);
16037
- const joinedPath = path24.join(cwd, resolvedPath);
16038
- const root = path24.resolve(ctx.projectRoot);
16039
- const target = path24.resolve(joinedPath);
16040
- const rel = path24.relative(root, target);
16041
- if (rel.startsWith("..") || path24.isAbsolute(rel)) {
18106
+ const joinedPath = path28.join(cwd, resolvedPath);
18107
+ const root = path28.resolve(ctx.projectRoot);
18108
+ const target = path28.resolve(joinedPath);
18109
+ const rel = path28.relative(root, target);
18110
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
16042
18111
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
16043
18112
  }
16044
18113
  const fullPath = target;
16045
18114
  if (!dryRun) {
16046
- await fs24.mkdir(path24.dirname(fullPath), { recursive: true });
16047
- await atomicWrite3(fullPath, substituteVars(content, name, vars));
18115
+ await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
18116
+ await atomicWrite4(fullPath, substituteVars(content, name, vars));
16048
18117
  }
16049
18118
  files.push(resolvedPath);
16050
18119
  filesCreated++;
@@ -16078,6 +18147,7 @@ var DEFAULT_NUM = 10;
16078
18147
  var MAX_RESULTS = 50;
16079
18148
  var TIMEOUT_MS3 = 15e3;
16080
18149
  var CACHE_TTL_MS = 3e5;
18150
+ var CACHE_MAX_ENTRIES = 200;
16081
18151
  var cache = /* @__PURE__ */ new Map();
16082
18152
  var searchTool = {
16083
18153
  name: "search",
@@ -16199,7 +18269,7 @@ var searchTool = {
16199
18269
  }
16200
18270
  const finalResults = ranked.slice(0, num);
16201
18271
  cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
16202
- pruneStaleCacheEntries();
18272
+ pruneCacheEntries();
16203
18273
  yield {
16204
18274
  type: "partial_output",
16205
18275
  text: `${finalResults.length} results from ${effectiveSource}`,
@@ -16221,11 +18291,16 @@ var searchTool = {
16221
18291
  };
16222
18292
  }
16223
18293
  };
16224
- function pruneStaleCacheEntries() {
18294
+ function pruneCacheEntries() {
16225
18295
  const cutoff = Date.now() - CACHE_TTL_MS * 2;
16226
18296
  for (const [key, entry] of cache.entries()) {
16227
18297
  if (entry.timestamp < cutoff) cache.delete(key);
16228
18298
  }
18299
+ while (cache.size > CACHE_MAX_ENTRIES) {
18300
+ const oldestKey = cache.keys().next().value;
18301
+ if (oldestKey === void 0) break;
18302
+ cache.delete(oldestKey);
18303
+ }
16229
18304
  }
16230
18305
  function rankSearchResults(results, query) {
16231
18306
  const seenUrls = /* @__PURE__ */ new Set();
@@ -16454,7 +18529,7 @@ function decodeHtmlEntities(text) {
16454
18529
  }
16455
18530
 
16456
18531
  // src/set-working-dir.ts
16457
- import * as fs25 from "node:fs/promises";
18532
+ import * as fs27 from "node:fs/promises";
16458
18533
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
16459
18534
  var setWorkingDirTool = {
16460
18535
  name: "set_working_dir",
@@ -16493,7 +18568,7 @@ var setWorkingDirTool = {
16493
18568
  };
16494
18569
  }
16495
18570
  try {
16496
- await fs25.access(resolved);
18571
+ await fs27.access(resolved);
16497
18572
  } catch {
16498
18573
  try {
16499
18574
  ctx.setWorkingDir(previous);
@@ -16518,11 +18593,11 @@ import {
16518
18593
  formatTaskList as formatTaskList2
16519
18594
  } from "@wrongstack/core";
16520
18595
  import {
16521
- mutateTasks as mutateTasks2
18596
+ mutateTasks as mutateTasks3
16522
18597
  } from "@wrongstack/core";
16523
18598
  import {
16524
18599
  addPlanItem as addPlanItem2,
16525
- mutatePlan as mutatePlan2,
18600
+ mutatePlan as mutatePlan3,
16526
18601
  formatPlan as formatPlan2
16527
18602
  } from "@wrongstack/core";
16528
18603
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -16642,7 +18717,7 @@ var taskTool = {
16642
18717
  let todosToReplace = null;
16643
18718
  let file;
16644
18719
  try {
16645
- file = await mutateTasks2(taskPath, sessionId, async (f) => {
18720
+ file = await mutateTasks3(taskPath, sessionId, async (f) => {
16646
18721
  switch (input.action) {
16647
18722
  case "show":
16648
18723
  break;
@@ -16832,6 +18907,7 @@ var taskTool = {
16832
18907
  };
16833
18908
  }
16834
18909
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
18910
+ await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
16835
18911
  if (early) return early;
16836
18912
  if (didPlanify) {
16837
18913
  const { title, details } = planifyMeta;
@@ -16845,7 +18921,7 @@ var taskTool = {
16845
18921
  }
16846
18922
  let formatted = "";
16847
18923
  try {
16848
- await mutatePlan2(planPath, sessionId, (pf) => {
18924
+ await mutatePlan3(planPath, sessionId, (pf) => {
16849
18925
  const { plan: updated } = addPlanItem2(pf, title, details || void 0);
16850
18926
  formatted = formatPlan2(updated);
16851
18927
  return updated;
@@ -16893,7 +18969,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
16893
18969
  init_spawn_stream();
16894
18970
  init_util();
16895
18971
  init_legacy_bridge();
16896
- import * as path25 from "node:path";
18972
+ import * as path29 from "node:path";
16897
18973
  var testTool = {
16898
18974
  name: "test",
16899
18975
  category: "Code Quality",
@@ -16996,11 +19072,11 @@ var testTool = {
16996
19072
  }
16997
19073
  };
16998
19074
  async function detectRunner(cwd) {
16999
- const { stat: stat15 } = await import("node:fs/promises");
19075
+ const { stat: stat17 } = await import("node:fs/promises");
17000
19076
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
17001
19077
  for (const f of candidates) {
17002
19078
  try {
17003
- await stat15(path25.join(cwd, f));
19079
+ await stat17(path29.join(cwd, f));
17004
19080
  if (f.includes("vitest")) return "vitest";
17005
19081
  if (f.includes("jest")) return "jest";
17006
19082
  if (f.includes("mocha")) return "mocha";
@@ -17077,8 +19153,7 @@ function parseResult(runner, result, duration) {
17077
19153
  }
17078
19154
 
17079
19155
  // src/todo.ts
17080
- import { loadPlan, savePlan, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
17081
- import { loadTasks, saveTasks } from "@wrongstack/core";
19156
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
17082
19157
  var todoTool = {
17083
19158
  name: "todo",
17084
19159
  category: "Session",
@@ -17087,7 +19162,7 @@ var todoTool = {
17087
19162
  permission: "auto",
17088
19163
  mutating: false,
17089
19164
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
17090
- timeoutMs: 1e3,
19165
+ timeoutMs: 5e3,
17091
19166
  capabilities: ["session.todo"],
17092
19167
  icon: "todo",
17093
19168
  inputSchema: {
@@ -17139,16 +19214,21 @@ var todoTool = {
17139
19214
  }
17140
19215
  }
17141
19216
  ctx.state.replaceTodos(items);
19217
+ await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
17142
19218
  const completedPlanIds = /* @__PURE__ */ new Set();
17143
19219
  const completedTaskIds = /* @__PURE__ */ new Set();
17144
19220
  const pendingPlanIds = /* @__PURE__ */ new Set();
17145
19221
  const pendingTaskIds = /* @__PURE__ */ new Set();
17146
19222
  for (const item of items) {
17147
19223
  if (item.promotedFromPlan) {
17148
- (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
19224
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
19225
+ item.promotedFromPlan
19226
+ );
17149
19227
  }
17150
19228
  if (item.promotedFromTask) {
17151
- (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
19229
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
19230
+ item.promotedFromTask
19231
+ );
17152
19232
  }
17153
19233
  }
17154
19234
  for (const planId of completedPlanIds) {
@@ -17156,7 +19236,7 @@ var todoTool = {
17156
19236
  const planPath = ctx.meta["plan.path"];
17157
19237
  if (typeof planPath !== "string" || !planPath) continue;
17158
19238
  try {
17159
- const plan = await loadPlan(planPath);
19239
+ const plan = await loadPlan2(planPath);
17160
19240
  if (plan) {
17161
19241
  const updated = setPlanItemStatus2(plan, planId, "done");
17162
19242
  await savePlan(planPath, updated);
@@ -17169,7 +19249,7 @@ var todoTool = {
17169
19249
  const taskPath = ctx.meta["task.path"];
17170
19250
  if (typeof taskPath !== "string" || !taskPath) continue;
17171
19251
  try {
17172
- const file = await loadTasks(taskPath);
19252
+ const file = await loadTasks3(taskPath);
17173
19253
  if (file) {
17174
19254
  const task = file.tasks.find((t) => t.id === taskId);
17175
19255
  if (task && task.status !== "completed") {
@@ -17466,8 +19546,8 @@ var toolUseTool = {
17466
19546
  // src/tree.ts
17467
19547
  init_util();
17468
19548
  import { expectDefined as expectDefined10 } from "@wrongstack/core";
17469
- import * as fs26 from "node:fs/promises";
17470
- import * as path26 from "node:path";
19549
+ import * as fs28 from "node:fs/promises";
19550
+ import * as path30 from "node:path";
17471
19551
  var DEFAULT_IGNORE5 = [
17472
19552
  "node_modules",
17473
19553
  ".git",
@@ -17608,7 +19688,7 @@ var treeTool = {
17608
19688
  }
17609
19689
  };
17610
19690
  async function walkDir(dir, depth, opts) {
17611
- const entries = await fs26.readdir(dir, { withFileTypes: true }).catch(() => []);
19691
+ const entries = await fs28.readdir(dir, { withFileTypes: true }).catch(() => []);
17612
19692
  const filtered = entries.filter((e) => {
17613
19693
  if (!opts.showHidden && e.name.startsWith(".")) return false;
17614
19694
  if (opts.exclude.has(e.name)) return false;
@@ -17638,7 +19718,7 @@ async function walkDir(dir, depth, opts) {
17638
19718
  opts.lines.push(opts.prefix + branch + displayName);
17639
19719
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
17640
19720
  const childPrefix = opts.prefix + connector;
17641
- await walkDir(path26.join(dir, entry.name), depth + 1, {
19721
+ await walkDir(path30.join(dir, entry.name), depth + 1, {
17642
19722
  ...opts,
17643
19723
  prefix: childPrefix,
17644
19724
  isLast
@@ -17651,7 +19731,7 @@ async function walkDir(dir, depth, opts) {
17651
19731
  init_spawn_stream();
17652
19732
  init_util();
17653
19733
  init_legacy_bridge();
17654
- import * as path27 from "node:path";
19734
+ import * as path31 from "node:path";
17655
19735
  var typecheckTool = {
17656
19736
  name: "typecheck",
17657
19737
  category: "Code Quality",
@@ -17750,12 +19830,12 @@ var typecheckTool = {
17750
19830
  }
17751
19831
  };
17752
19832
  async function findTsConfig(cwd) {
17753
- const { stat: stat15 } = await import("node:fs/promises");
19833
+ const { stat: stat17 } = await import("node:fs/promises");
17754
19834
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
17755
19835
  for (const f of candidates) {
17756
19836
  try {
17757
- const s = await stat15(path27.join(cwd, f));
17758
- if (s.isFile()) return path27.join(cwd, f);
19837
+ const s = await stat17(path31.join(cwd, f));
19838
+ if (s.isFile()) return path31.join(cwd, f);
17759
19839
  } catch {
17760
19840
  }
17761
19841
  }
@@ -17763,8 +19843,8 @@ async function findTsConfig(cwd) {
17763
19843
  }
17764
19844
 
17765
19845
  // src/write.ts
17766
- import * as fs27 from "node:fs/promises";
17767
- import { atomicWrite as atomicWrite4, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
19846
+ import * as fs29 from "node:fs/promises";
19847
+ import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
17768
19848
  init_util();
17769
19849
  var writeTool = {
17770
19850
  name: "write",
@@ -17828,14 +19908,14 @@ async function prepareWrite(input, ctx) {
17828
19908
  let existed = false;
17829
19909
  let prev = "";
17830
19910
  try {
17831
- const stat15 = await fs27.stat(absPath);
17832
- existed = stat15.isFile();
19911
+ const stat17 = await fs29.stat(absPath);
19912
+ existed = stat17.isFile();
17833
19913
  if (existed) {
17834
19914
  if (!ctx.hasRead(absPath)) {
17835
- prev = await fs27.readFile(absPath, "utf8");
17836
- ctx.recordRead(absPath, stat15.mtimeMs, "write", sha256hex(prev));
19915
+ prev = await fs29.readFile(absPath, "utf8");
19916
+ ctx.recordRead(absPath, stat17.mtimeMs, "write", sha256hex(prev));
17837
19917
  } else {
17838
- prev = await fs27.readFile(absPath, "utf8");
19918
+ prev = await fs29.readFile(absPath, "utf8");
17839
19919
  }
17840
19920
  }
17841
19921
  } catch (err) {
@@ -17847,11 +19927,11 @@ async function prepareWrite(input, ctx) {
17847
19927
  }
17848
19928
  async function finishWrite(input, ctx, prepared, signal) {
17849
19929
  signal?.throwIfAborted();
17850
- await atomicWrite4(prepared.absPath, input.content);
19930
+ await atomicWrite5(prepared.absPath, input.content);
17851
19931
  const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
17852
19932
  + (new file, ${input.content.split("\n").length} lines)`;
17853
- const stat15 = await fs27.stat(prepared.absPath);
17854
- ctx.recordRead(prepared.absPath, stat15.mtimeMs, "write", sha256hex(input.content));
19933
+ const stat17 = await fs29.stat(prepared.absPath);
19934
+ ctx.recordRead(prepared.absPath, stat17.mtimeMs, "write", sha256hex(input.content));
17855
19935
  ctx.session.recordFileChange({
17856
19936
  path: prepared.absPath,
17857
19937
  action: prepared.existed ? "modified" : "created",
@@ -17876,6 +19956,8 @@ async function finishWrite(input, ctx, prepared, signal) {
17876
19956
 
17877
19957
  // src/builtin.ts
17878
19958
  var OPTIONAL_TOOLS = [
19959
+ ...browserTools,
19960
+ e2ePlanTool,
17879
19961
  installTool,
17880
19962
  auditTool,
17881
19963
  outdatedTool,
@@ -17939,6 +20021,8 @@ var TIER3_TOOLS = [
17939
20021
  setWorkingDirTool
17940
20022
  ];
17941
20023
  var builtinTools = [
20024
+ ...browserTools,
20025
+ e2ePlanTool,
17942
20026
  readTool,
17943
20027
  writeTool,
17944
20028
  editTool,