@wrongstack/tools 0.285.0 → 0.286.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -123,7 +123,7 @@ function createOutputSpool(opts) {
123
123
  let filePath = null;
124
124
  let failed = false;
125
125
  let finalized = false;
126
- const open2 = () => {
126
+ const open3 = () => {
127
127
  if (stream || failed) return;
128
128
  try {
129
129
  const dir = toolOutputDir();
@@ -156,7 +156,7 @@ function createOutputSpool(opts) {
156
156
  return;
157
157
  }
158
158
  head += text;
159
- open2();
159
+ open3();
160
160
  head = "";
161
161
  return;
162
162
  }
@@ -910,8 +910,8 @@ async function* spawnStream(opts) {
910
910
  try {
911
911
  for (; ; ) {
912
912
  while (queue.length === 0) {
913
- await new Promise((resolve12) => {
914
- waiter = resolve12;
913
+ await new Promise((resolve15) => {
914
+ waiter = resolve15;
915
915
  });
916
916
  }
917
917
  const chunk = queue.shift();
@@ -985,14 +985,14 @@ function sha256hex(content) {
985
985
  return createHash("sha256").update(content, "utf8").digest("hex");
986
986
  }
987
987
  async function detectPackageManager(cwd) {
988
- const { stat: stat16 } = await import("node:fs/promises");
988
+ const { stat: stat18 } = await import("node:fs/promises");
989
989
  try {
990
- await stat16(`${cwd}/pnpm-lock.yaml`);
990
+ await stat18(`${cwd}/pnpm-lock.yaml`);
991
991
  return "pnpm";
992
992
  } catch {
993
993
  }
994
994
  try {
995
- await stat16(`${cwd}/yarn.lock`);
995
+ await stat18(`${cwd}/yarn.lock`);
996
996
  return "yarn";
997
997
  } catch {
998
998
  }
@@ -2561,8 +2561,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2561
2561
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2562
2562
  }
2563
2563
  }
2564
- function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
2565
- const lower = basename7.toLowerCase();
2564
+ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2565
+ const lower = basename8.toLowerCase();
2566
2566
  const extension = path4.extname(lower);
2567
2567
  for (const profile of profiles) {
2568
2568
  const detector = profile.detectors.find(
@@ -2573,7 +2573,7 @@ function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
2573
2573
  candidate.evidence.push({
2574
2574
  kind: detector.kind,
2575
2575
  path: fullPath,
2576
- value: basename7,
2576
+ value: basename8,
2577
2577
  weight: detector.weight
2578
2578
  });
2579
2579
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2701,8 +2701,8 @@ function normalizeLimits(input) {
2701
2701
  async function canonicalDirectory(input) {
2702
2702
  const resolved = path4.resolve(input);
2703
2703
  const real = await fs2.realpath(resolved);
2704
- const stat16 = await fs2.stat(real);
2705
- if (!stat16.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2704
+ const stat18 = await fs2.stat(real);
2705
+ if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2706
2706
  return real;
2707
2707
  }
2708
2708
  async function canonicalInside(input, root, label) {
@@ -2722,12 +2722,12 @@ function resolveFrom(cwd, input) {
2722
2722
  return path4.isAbsolute(input) ? input : path4.resolve(cwd, input);
2723
2723
  }
2724
2724
  function isInside(candidate, root) {
2725
- const relative11 = path4.relative(root, candidate);
2726
- return relative11 === "" || !relative11.startsWith("..") && !path4.isAbsolute(relative11);
2725
+ const relative13 = path4.relative(root, candidate);
2726
+ return relative13 === "" || !relative13.startsWith("..") && !path4.isAbsolute(relative13);
2727
2727
  }
2728
2728
  function pathDepth(candidate, root) {
2729
- const relative11 = path4.relative(root, candidate);
2730
- return relative11 === "" ? 0 : relative11.split(path4.sep).length;
2729
+ const relative13 = path4.relative(root, candidate);
2730
+ return relative13 === "" ? 0 : relative13.split(path4.sep).length;
2731
2731
  }
2732
2732
  function dedupeEvidence(items) {
2733
2733
  const seen = /* @__PURE__ */ new Set();
@@ -3427,8 +3427,8 @@ function compareCandidates(a, b) {
3427
3427
  return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root);
3428
3428
  }
3429
3429
  function workspaceDepth(workspaceRoot, projectRoot) {
3430
- const relative11 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3431
- return relative11 === "" ? 0 : relative11.split(path6.sep).length;
3430
+ const relative13 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3431
+ return relative13 === "" ? 0 : relative13.split(path6.sep).length;
3432
3432
  }
3433
3433
  function validateOperationOptions(operation, packages) {
3434
3434
  if (!operation.startsWith("package-") || !packages) return;
@@ -3458,8 +3458,8 @@ async function canonicalTarget(cwd, target, projectRoot) {
3458
3458
  return real;
3459
3459
  }
3460
3460
  function isInside2(candidate, root) {
3461
- const relative11 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3462
- return relative11 === "" || !relative11.startsWith("..") && !path6.isAbsolute(relative11);
3461
+ const relative13 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3462
+ return relative13 === "" || !relative13.startsWith("..") && !path6.isAbsolute(relative13);
3463
3463
  }
3464
3464
  var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
3465
3465
  var init_plan = __esm({
@@ -3590,8 +3590,8 @@ async function executeInternal(options, startedAt) {
3590
3590
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3591
3591
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3592
3592
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3593
- const stat16 = await fs4.stat(safeTarget);
3594
- if (stat16.size > MAX_INTERNAL_SOURCE_BYTES) {
3593
+ const stat18 = await fs4.stat(safeTarget);
3594
+ if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
3595
3595
  return unavailableResult(
3596
3596
  options,
3597
3597
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -3638,14 +3638,14 @@ async function validatePlanRealpaths(plan, projectRoot) {
3638
3638
  return void 0;
3639
3639
  }
3640
3640
  function isRealInside(candidate, root) {
3641
- const relative11 = path7.relative(root, candidate);
3642
- return relative11 === "" || !relative11.startsWith("..") && !path7.isAbsolute(relative11);
3641
+ const relative13 = path7.relative(root, candidate);
3642
+ return relative13 === "" || !relative13.startsWith("..") && !path7.isAbsolute(relative13);
3643
3643
  }
3644
3644
  async function assertContainedFile(candidate, projectRoot) {
3645
3645
  const realRoot = await fs4.realpath(projectRoot);
3646
3646
  const realTarget = await fs4.realpath(candidate);
3647
- const relative11 = path7.relative(realRoot, realTarget);
3648
- if (relative11.startsWith("..") || path7.isAbsolute(relative11)) {
3647
+ const relative13 = path7.relative(realRoot, realTarget);
3648
+ if (relative13.startsWith("..") || path7.isAbsolute(relative13)) {
3649
3649
  throw new Error(`Internal syntax target resolves outside project root: ${candidate}`);
3650
3650
  }
3651
3651
  return realTarget;
@@ -3878,8 +3878,8 @@ async function snapshotPaths(paths) {
3878
3878
  const existing = [];
3879
3879
  for (const candidate of paths) {
3880
3880
  try {
3881
- const stat16 = await fs4.stat(candidate);
3882
- if (!stat16.isFile()) continue;
3881
+ const stat18 = await fs4.stat(candidate);
3882
+ if (!stat18.isFile()) continue;
3883
3883
  existing.push(candidate);
3884
3884
  } catch {
3885
3885
  }
@@ -3890,15 +3890,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3890
3890
  const beforeSet = new Set(before);
3891
3891
  const afterSet = new Set(after);
3892
3892
  const changed = /* @__PURE__ */ new Set();
3893
- for (const path29 of after) {
3894
- if (!beforeSet.has(path29)) changed.add(path29);
3893
+ for (const path33 of after) {
3894
+ if (!beforeSet.has(path33)) changed.add(path33);
3895
3895
  }
3896
- for (const path29 of before) {
3897
- if (!afterSet.has(path29)) changed.add(path29);
3896
+ for (const path33 of before) {
3897
+ if (!afterSet.has(path33)) changed.add(path33);
3898
3898
  }
3899
3899
  if (beforeSizes && afterSizes) {
3900
- for (const path29 of after) {
3901
- if (beforeSizes.get(path29) !== afterSizes.get(path29)) changed.add(path29);
3900
+ for (const path33 of after) {
3901
+ if (beforeSizes.get(path33) !== afterSizes.get(path33)) changed.add(path33);
3902
3902
  }
3903
3903
  }
3904
3904
  return [...changed].sort();
@@ -3907,8 +3907,8 @@ async function snapshotSizes(paths) {
3907
3907
  const sizes = /* @__PURE__ */ new Map();
3908
3908
  for (const candidate of paths) {
3909
3909
  try {
3910
- const stat16 = await fs4.stat(candidate);
3911
- if (stat16.isFile()) sizes.set(candidate, stat16.size);
3910
+ const stat18 = await fs4.stat(candidate);
3911
+ if (stat18.isFile()) sizes.set(candidate, stat18.size);
3912
3912
  } catch {
3913
3913
  }
3914
3914
  }
@@ -5161,8 +5161,8 @@ var auditTool = {
5161
5161
  yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
5162
5162
  }
5163
5163
  };
5164
- function parseAuditOutput(json, exitCode) {
5165
- if (!json) {
5164
+ function parseAuditOutput(json2, exitCode) {
5165
+ if (!json2) {
5166
5166
  return {
5167
5167
  exit_code: exitCode,
5168
5168
  vulnerabilities: [],
@@ -5173,7 +5173,7 @@ function parseAuditOutput(json, exitCode) {
5173
5173
  };
5174
5174
  }
5175
5175
  try {
5176
- const data = JSON.parse(json);
5176
+ const data = JSON.parse(json2);
5177
5177
  const advisories = [];
5178
5178
  const ads = data.advisories ?? {};
5179
5179
  for (const id of Object.keys(ads)) {
@@ -5192,8 +5192,8 @@ function parseAuditOutput(json, exitCode) {
5192
5192
  vulnerabilities: advisories,
5193
5193
  total,
5194
5194
  summary,
5195
- output: json,
5196
- truncated: json.length > 1e5
5195
+ output: json2,
5196
+ truncated: json2.length > 1e5
5197
5197
  };
5198
5198
  } catch {
5199
5199
  return {
@@ -5201,7 +5201,7 @@ function parseAuditOutput(json, exitCode) {
5201
5201
  vulnerabilities: [],
5202
5202
  total: 0,
5203
5203
  summary: "Could not parse audit output",
5204
- output: json,
5204
+ output: json2,
5205
5205
  truncated: false
5206
5206
  };
5207
5207
  }
@@ -6235,10 +6235,10 @@ var bashTool = {
6235
6235
  queue.push(c);
6236
6236
  }
6237
6237
  };
6238
- const next = () => new Promise((resolve12) => {
6238
+ const next = () => new Promise((resolve15) => {
6239
6239
  const c = queue.shift();
6240
- if (c) resolve12(c);
6241
- else resolveNext = resolve12;
6240
+ if (c) resolve15(c);
6241
+ else resolveNext = resolve15;
6242
6242
  });
6243
6243
  let lastFlush = Date.now();
6244
6244
  const flush = () => {
@@ -6478,6 +6478,1241 @@ async function executeSingle(call, ctx, opts) {
6478
6478
  }
6479
6479
  }
6480
6480
 
6481
+ // src/browser/artifacts.ts
6482
+ import { createHash as createHash3 } from "node:crypto";
6483
+ import { createReadStream } from "node:fs";
6484
+ import * as fs7 from "node:fs/promises";
6485
+ import * as path10 from "node:path";
6486
+ import { atomicWrite, ulid } from "@wrongstack/core/utils";
6487
+ var BrowserArtifactStore = class {
6488
+ constructor(root) {
6489
+ this.root = root;
6490
+ }
6491
+ root;
6492
+ async write(sessionId, kind, extension, mimeType, content) {
6493
+ const id = ulid();
6494
+ const dir = path10.join(this.root, safeSegment(sessionId));
6495
+ const target = path10.join(dir, `${id}.${extension.replace(/^\./, "")}`);
6496
+ await atomicWrite(target, content, { mode: 384 });
6497
+ return this.record(id, sessionId, kind, target, mimeType);
6498
+ }
6499
+ async record(id, sessionId, kind, target, mimeType) {
6500
+ await fs7.chmod(target, 384).catch(() => void 0);
6501
+ const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
6502
+ const artifact = {
6503
+ id,
6504
+ kind,
6505
+ sensitivity: "sensitive",
6506
+ path: target,
6507
+ mimeType,
6508
+ sizeBytes: stat18.size,
6509
+ sha256,
6510
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6511
+ };
6512
+ const metadataPath = path10.join(path10.dirname(target), `${id}.metadata.json`);
6513
+ try {
6514
+ await atomicWrite(
6515
+ metadataPath,
6516
+ `${JSON.stringify({
6517
+ id: artifact.id,
6518
+ sessionId: safeSegment(sessionId),
6519
+ kind: artifact.kind,
6520
+ sensitivity: artifact.sensitivity,
6521
+ mimeType: artifact.mimeType,
6522
+ sizeBytes: artifact.sizeBytes,
6523
+ sha256: artifact.sha256,
6524
+ createdAt: artifact.createdAt
6525
+ })}
6526
+ `,
6527
+ { mode: 384 }
6528
+ );
6529
+ } catch (error) {
6530
+ await fs7.rm(target, { force: true }).catch(() => void 0);
6531
+ throw error;
6532
+ }
6533
+ return artifact;
6534
+ }
6535
+ pathFor(sessionId, extension) {
6536
+ const id = ulid();
6537
+ return {
6538
+ id,
6539
+ path: path10.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
6540
+ };
6541
+ }
6542
+ async describe(id, kind, target, mimeType) {
6543
+ return this.record(id, path10.basename(path10.dirname(target)), kind, target, mimeType);
6544
+ }
6545
+ };
6546
+ async function hashFile(target) {
6547
+ const hash = createHash3("sha256");
6548
+ await new Promise((resolve15, reject) => {
6549
+ const stream = createReadStream(target);
6550
+ stream.on("data", (chunk) => hash.update(chunk));
6551
+ stream.once("error", reject);
6552
+ stream.once("end", resolve15);
6553
+ });
6554
+ return hash.digest("hex");
6555
+ }
6556
+ function safeSegment(value) {
6557
+ return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
6558
+ }
6559
+
6560
+ // src/browser/manager.ts
6561
+ import * as fs8 from "node:fs/promises";
6562
+ import * as path11 from "node:path";
6563
+ import { ulid as ulid2 } from "@wrongstack/core/utils";
6564
+
6565
+ // src/browser/network-guard-proxy.ts
6566
+ import * as http from "node:http";
6567
+ import * as net2 from "node:net";
6568
+
6569
+ // src/browser/security.ts
6570
+ import * as dns from "node:dns/promises";
6571
+ import * as net from "node:net";
6572
+ import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
6573
+ var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
6574
+ async function assertBrowserUrlAllowed(rawUrl, opts) {
6575
+ const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
6576
+ if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
6577
+ if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
6578
+ await assertNotPrivateHost(url.hostname);
6579
+ }
6580
+ return url;
6581
+ }
6582
+ async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
6583
+ const url = parseBrowserUrl(rawUrl, true);
6584
+ const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
6585
+ const hostname4 = unbracket(url.hostname);
6586
+ const literalFamily = net.isIP(hostname4);
6587
+ if (literalFamily === 4 || literalFamily === 6) {
6588
+ if (!allowPrivate && isPrivateAddress(hostname4, literalFamily)) {
6589
+ throw new Error(`browser: blocked private/loopback address "${hostname4}"`);
6590
+ }
6591
+ return { url, address: hostname4, family: literalFamily };
6592
+ }
6593
+ if ((hostname4 === "localhost" || hostname4.endsWith(".localhost")) && !allowPrivate) {
6594
+ throw new Error("browser: blocked localhost target");
6595
+ }
6596
+ const lookup3 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
6597
+ let records;
6598
+ try {
6599
+ records = await lookup3(hostname4);
6600
+ } catch (error) {
6601
+ throw new Error(
6602
+ `browser: DNS resolution failed for "${hostname4}": ${error instanceof Error ? error.message : String(error)}`
6603
+ );
6604
+ }
6605
+ if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname4}"`);
6606
+ for (const record of records) {
6607
+ if (record.family !== 4 && record.family !== 6) {
6608
+ throw new Error(`browser: DNS returned an unsupported address family for "${hostname4}"`);
6609
+ }
6610
+ if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
6611
+ throw new Error(`browser: resolved to private address ${record.address}`);
6612
+ }
6613
+ }
6614
+ const selected = records[0];
6615
+ return { url, address: selected.address, family: selected.family };
6616
+ }
6617
+ function parsePrivateOriginAllowlist(raw) {
6618
+ if (!raw?.trim()) return [];
6619
+ const origins = /* @__PURE__ */ new Set();
6620
+ for (const entry of raw.split(",")) {
6621
+ const candidate = entry.trim();
6622
+ if (!candidate) continue;
6623
+ const url = parseBrowserUrl(candidate, true);
6624
+ if (url.pathname !== "/" || url.search || url.hash) {
6625
+ throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
6626
+ }
6627
+ origins.add(url.origin);
6628
+ }
6629
+ return [...origins];
6630
+ }
6631
+ function parseBrowserUrl(rawUrl, navigation) {
6632
+ let url;
6633
+ try {
6634
+ url = new URL(rawUrl);
6635
+ } catch {
6636
+ throw new Error("browser: url must be an absolute URL");
6637
+ }
6638
+ if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
6639
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
6640
+ throw new Error(`browser: unsupported protocol "${url.protocol}"`);
6641
+ }
6642
+ if (url.username || url.password) {
6643
+ throw new Error("browser: credentials in URLs are not allowed");
6644
+ }
6645
+ return url;
6646
+ }
6647
+ function isAllowedPrivateOrigin(url, origins) {
6648
+ return origins?.includes(url.origin) ?? false;
6649
+ }
6650
+ function isPrivateAddress(address, family) {
6651
+ return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
6652
+ }
6653
+ function unbracket(hostname4) {
6654
+ return hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
6655
+ }
6656
+ function safeBrowserUrl(rawUrl) {
6657
+ try {
6658
+ const url = new URL(rawUrl);
6659
+ url.username = "";
6660
+ url.password = "";
6661
+ url.search = "";
6662
+ url.hash = "";
6663
+ return url.toString();
6664
+ } catch {
6665
+ return "about:blank";
6666
+ }
6667
+ }
6668
+ function redactBrowserText(text) {
6669
+ return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
6670
+ /\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
6671
+ "$1=[REDACTED]"
6672
+ );
6673
+ }
6674
+
6675
+ // src/browser/network-guard-proxy.ts
6676
+ var BrowserNetworkGuardProxy = class {
6677
+ constructor(options = {}) {
6678
+ this.options = options;
6679
+ this.server = http.createServer((request2, response) => {
6680
+ void this.forwardHttp(request2, response);
6681
+ });
6682
+ this.server.on("connect", (request2, socket, head) => {
6683
+ void this.forwardConnect(request2, socket, head);
6684
+ });
6685
+ this.server.on("upgrade", (request2, socket, head) => {
6686
+ void this.forwardUpgrade(request2, socket, head);
6687
+ });
6688
+ this.server.on("connection", (socket) => {
6689
+ this.sockets.add(socket);
6690
+ socket.once("close", () => this.sockets.delete(socket));
6691
+ });
6692
+ this.server.on("clientError", (_error, socket) => {
6693
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
6694
+ });
6695
+ }
6696
+ options;
6697
+ server;
6698
+ sockets = /* @__PURE__ */ new Set();
6699
+ startPromise;
6700
+ url;
6701
+ async start() {
6702
+ if (this.url) return this.url;
6703
+ if (this.startPromise) return this.startPromise;
6704
+ this.startPromise = new Promise((resolve15, reject) => {
6705
+ const onError = (error) => {
6706
+ this.server.off("listening", onListening);
6707
+ reject(error);
6708
+ };
6709
+ const onListening = () => {
6710
+ this.server.off("error", onError);
6711
+ const address = this.server.address();
6712
+ if (!address || typeof address === "string") {
6713
+ reject(new Error("browser: network guard proxy failed to bind"));
6714
+ return;
6715
+ }
6716
+ this.url = `http://127.0.0.1:${address.port}`;
6717
+ resolve15(this.url);
6718
+ };
6719
+ this.server.once("error", onError);
6720
+ this.server.once("listening", onListening);
6721
+ this.server.listen(0, "127.0.0.1");
6722
+ }).finally(() => {
6723
+ this.startPromise = void 0;
6724
+ });
6725
+ return this.startPromise;
6726
+ }
6727
+ async close() {
6728
+ this.url = void 0;
6729
+ for (const socket of this.sockets) socket.destroy();
6730
+ this.sockets.clear();
6731
+ if (!this.server.listening) return;
6732
+ await new Promise((resolve15) => this.server.close(() => resolve15()));
6733
+ }
6734
+ async forwardHttp(request2, response) {
6735
+ try {
6736
+ const target = await this.resolve(request2.url ?? "");
6737
+ if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
6738
+ const headers = sanitizeHeaders(request2.headers);
6739
+ headers.host = target.url.host;
6740
+ const upstream = http.request({
6741
+ host: target.address,
6742
+ family: target.family,
6743
+ port: Number(target.url.port || 80),
6744
+ method: request2.method,
6745
+ path: `${target.url.pathname}${target.url.search}`,
6746
+ headers,
6747
+ agent: false
6748
+ });
6749
+ upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
6750
+ upstream.destroy(new Error("browser: upstream connection timed out"));
6751
+ });
6752
+ upstream.on("response", (upstreamResponse) => {
6753
+ response.writeHead(
6754
+ upstreamResponse.statusCode ?? 502,
6755
+ sanitizeHeaders(upstreamResponse.headers)
6756
+ );
6757
+ upstreamResponse.pipe(response);
6758
+ });
6759
+ upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
6760
+ request2.on("aborted", () => upstream.destroy());
6761
+ request2.pipe(upstream);
6762
+ } catch {
6763
+ writeProxyError(response, 403, "Blocked by browser network policy");
6764
+ }
6765
+ }
6766
+ async forwardConnect(request2, client, head) {
6767
+ try {
6768
+ const authority = request2.url ?? "";
6769
+ const target = await this.resolve(`https://${authority}`);
6770
+ const upstream = net2.connect({
6771
+ host: target.address,
6772
+ family: target.family,
6773
+ port: Number(target.url.port || 443)
6774
+ });
6775
+ upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
6776
+ upstream.once("connect", () => {
6777
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
6778
+ if (head.length > 0) upstream.write(head);
6779
+ upstream.pipe(client);
6780
+ client.pipe(upstream);
6781
+ });
6782
+ upstream.once("error", () => {
6783
+ client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
6784
+ });
6785
+ client.once("close", () => upstream.destroy());
6786
+ } catch {
6787
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
6788
+ }
6789
+ }
6790
+ async forwardUpgrade(request2, client, head) {
6791
+ let upstream;
6792
+ try {
6793
+ const rawUrl = request2.url ?? "";
6794
+ const websocketUrl = new URL(rawUrl);
6795
+ if (websocketUrl.protocol !== "ws:") {
6796
+ throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
6797
+ }
6798
+ websocketUrl.protocol = "http:";
6799
+ const target = await this.resolve(websocketUrl.toString());
6800
+ const headers = sanitizeHeaders(request2.headers, true);
6801
+ headers.host = target.url.host;
6802
+ upstream = http.request({
6803
+ host: target.address,
6804
+ family: target.family,
6805
+ port: Number(target.url.port || 80),
6806
+ method: request2.method,
6807
+ path: `${target.url.pathname}${target.url.search}`,
6808
+ headers,
6809
+ agent: false
6810
+ });
6811
+ upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
6812
+ writeRawResponseHead(client, response);
6813
+ if (head.length > 0) upstreamSocket.write(head);
6814
+ if (upstreamHead.length > 0) client.write(upstreamHead);
6815
+ upstreamSocket.pipe(client);
6816
+ client.pipe(upstreamSocket);
6817
+ client.once("close", () => upstreamSocket.destroy());
6818
+ });
6819
+ upstream.once("response", (response) => {
6820
+ writeRawResponseHead(client, response);
6821
+ response.pipe(client);
6822
+ });
6823
+ upstream.once("error", () => {
6824
+ client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
6825
+ });
6826
+ client.once("close", () => upstream?.destroy());
6827
+ upstream.end();
6828
+ } catch {
6829
+ upstream?.destroy();
6830
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
6831
+ }
6832
+ }
6833
+ resolve(rawUrl) {
6834
+ return resolvePinnedBrowserTarget(rawUrl, {
6835
+ allowPrivateHosts: this.options.allowPrivateHosts,
6836
+ allowedPrivateOrigins: this.options.allowedPrivateOrigins,
6837
+ lookup: this.options.lookup
6838
+ });
6839
+ }
6840
+ };
6841
+ function sanitizeHeaders(headers, preserveUpgrade = false) {
6842
+ const sanitized = { ...headers };
6843
+ delete sanitized["proxy-authorization"];
6844
+ delete sanitized["proxy-connection"];
6845
+ if (!preserveUpgrade) delete sanitized.connection;
6846
+ return sanitized;
6847
+ }
6848
+ function writeRawResponseHead(client, response) {
6849
+ const status = response.statusCode ?? 502;
6850
+ const statusText = response.statusMessage ?? "Bad Gateway";
6851
+ const lines = [`HTTP/1.1 ${status} ${statusText}`];
6852
+ for (let index = 0; index < response.rawHeaders.length; index += 2) {
6853
+ lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
6854
+ }
6855
+ client.write(`${lines.join("\r\n")}\r
6856
+ \r
6857
+ `);
6858
+ }
6859
+ function writeProxyError(response, status, message) {
6860
+ if (response.headersSent || response.destroyed) return;
6861
+ response.writeHead(status, { "content-type": "text/plain", connection: "close" });
6862
+ response.end(message);
6863
+ }
6864
+
6865
+ // src/browser/manager.ts
6866
+ var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
6867
+ var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
6868
+ var DEFAULT_MAX_ENTRIES = 100;
6869
+ var BrowserSessionManager = class {
6870
+ constructor(options, launcher = defaultLauncher) {
6871
+ this.launcher = launcher;
6872
+ this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6873
+ this.allowPrivateHosts = options.allowPrivateHosts ?? false;
6874
+ this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6875
+ this.networkProxy = new BrowserNetworkGuardProxy({
6876
+ allowPrivateHosts: this.allowPrivateHosts,
6877
+ allowedPrivateOrigins: this.allowedPrivateOrigins
6878
+ });
6879
+ this.headless = options.headless ?? true;
6880
+ this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
6881
+ this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
6882
+ this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
6883
+ this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
6884
+ }
6885
+ launcher;
6886
+ sessions = /* @__PURE__ */ new Map();
6887
+ artifacts;
6888
+ allowPrivateHosts;
6889
+ allowedPrivateOrigins;
6890
+ networkProxy;
6891
+ headless;
6892
+ operationTimeoutMs;
6893
+ maxSnapshotChars;
6894
+ maxConsoleEntries;
6895
+ maxNetworkEntries;
6896
+ browser;
6897
+ launching;
6898
+ async open(ownerId, input, signal) {
6899
+ signal.throwIfAborted();
6900
+ if (input.url) {
6901
+ await assertBrowserUrlAllowed(input.url, {
6902
+ allowPrivateHosts: this.allowPrivateHosts,
6903
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6904
+ navigation: true
6905
+ });
6906
+ }
6907
+ const browser = await this.ensureBrowser(signal);
6908
+ const proxyServer = await this.networkProxy.start();
6909
+ const context = await abortable(
6910
+ signal,
6911
+ async () => {
6912
+ const created = await browser.newContext({
6913
+ acceptDownloads: false,
6914
+ ignoreHTTPSErrors: false,
6915
+ serviceWorkers: "block",
6916
+ proxy: { server: proxyServer },
6917
+ viewport: input.viewport ?? { width: 1440, height: 900 }
6918
+ });
6919
+ if (signal.aborted) {
6920
+ await created.close().catch(() => void 0);
6921
+ signal.throwIfAborted();
6922
+ }
6923
+ return created;
6924
+ },
6925
+ () => void 0
6926
+ );
6927
+ const session = {
6928
+ id: ulid2(),
6929
+ ownerId,
6930
+ context,
6931
+ page: await context.newPage(),
6932
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6933
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
6934
+ tracing: input.trace ?? true,
6935
+ console: [],
6936
+ network: []
6937
+ };
6938
+ this.sessions.set(session.id, session);
6939
+ this.attachEvidenceCollectors(session);
6940
+ await context.route("**/*", async (route) => {
6941
+ try {
6942
+ await assertBrowserUrlAllowed(route.request().url(), {
6943
+ allowPrivateHosts: this.allowPrivateHosts,
6944
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6945
+ navigation: false
6946
+ });
6947
+ await route.continue();
6948
+ } catch {
6949
+ this.pushNetwork(session, {
6950
+ method: route.request().method(),
6951
+ url: safeBrowserUrl(route.request().url()),
6952
+ failed: true,
6953
+ at: (/* @__PURE__ */ new Date()).toISOString()
6954
+ });
6955
+ await route.abort("blockedbyclient");
6956
+ }
6957
+ });
6958
+ try {
6959
+ if (session.tracing) {
6960
+ await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
6961
+ }
6962
+ if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
6963
+ return await this.summary(session);
6964
+ } catch (err) {
6965
+ await context.close().catch(() => void 0);
6966
+ this.sessions.delete(session.id);
6967
+ await this.closeBrowserIfIdle();
6968
+ throw err;
6969
+ }
6970
+ }
6971
+ async list(ownerId) {
6972
+ const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
6973
+ return Promise.all(owned.map((session) => this.summary(session)));
6974
+ }
6975
+ async navigate(id, ownerId, rawUrl, signal) {
6976
+ await assertBrowserUrlAllowed(rawUrl, {
6977
+ allowPrivateHosts: this.allowPrivateHosts,
6978
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6979
+ navigation: true
6980
+ });
6981
+ const session = this.requireOwned(id, ownerId);
6982
+ await this.runPageOperation(
6983
+ session,
6984
+ signal,
6985
+ () => session.page.goto(rawUrl, {
6986
+ waitUntil: "domcontentloaded",
6987
+ timeout: this.operationTimeoutMs
6988
+ })
6989
+ );
6990
+ return this.summary(session);
6991
+ }
6992
+ async snapshot(id, ownerId, signal) {
6993
+ const session = this.requireOwned(id, ownerId);
6994
+ const raw = await this.runPageOperation(session, signal, async () => {
6995
+ try {
6996
+ return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
6997
+ } catch {
6998
+ return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
6999
+ }
7000
+ });
7001
+ const aria = redactBrowserText(raw);
7002
+ const truncated = aria.length > this.maxSnapshotChars;
7003
+ return {
7004
+ session: await this.summary(session),
7005
+ aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
7006
+ \u2026[snapshot truncated]` : aria,
7007
+ console: session.console.map((entry) => ({ ...entry })),
7008
+ network: session.network.map((entry) => ({ ...entry })),
7009
+ truncated
7010
+ };
7011
+ }
7012
+ async screenshot(id, ownerId, input, signal) {
7013
+ const session = this.requireOwned(id, ownerId);
7014
+ const bytes = await this.runPageOperation(
7015
+ session,
7016
+ signal,
7017
+ () => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
7018
+ );
7019
+ signal.throwIfAborted();
7020
+ return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
7021
+ }
7022
+ async click(id, ownerId, selector, signal) {
7023
+ const session = this.requireOwned(id, ownerId);
7024
+ await this.runPageOperation(
7025
+ session,
7026
+ signal,
7027
+ () => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
7028
+ );
7029
+ }
7030
+ async type(id, ownerId, selector, text, signal) {
7031
+ const session = this.requireOwned(id, ownerId);
7032
+ await this.runPageOperation(
7033
+ session,
7034
+ signal,
7035
+ () => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
7036
+ );
7037
+ }
7038
+ async select(id, ownerId, selector, value, signal) {
7039
+ const session = this.requireOwned(id, ownerId);
7040
+ await this.runPageOperation(
7041
+ session,
7042
+ signal,
7043
+ () => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
7044
+ );
7045
+ }
7046
+ async press(id, ownerId, key, signal) {
7047
+ const session = this.requireOwned(id, ownerId);
7048
+ await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
7049
+ }
7050
+ async hover(id, ownerId, selector, signal) {
7051
+ const session = this.requireOwned(id, ownerId);
7052
+ await this.runPageOperation(
7053
+ session,
7054
+ signal,
7055
+ () => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
7056
+ );
7057
+ }
7058
+ async wait(id, ownerId, input, signal) {
7059
+ const session = this.requireOwned(id, ownerId);
7060
+ const timeout = Math.min(
7061
+ Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
7062
+ this.operationTimeoutMs
7063
+ );
7064
+ await this.runPageOperation(
7065
+ session,
7066
+ signal,
7067
+ () => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
7068
+ );
7069
+ }
7070
+ async drag(id, ownerId, from, to, signal) {
7071
+ const session = this.requireOwned(id, ownerId);
7072
+ await this.runPageOperation(
7073
+ session,
7074
+ signal,
7075
+ () => session.page.locator(from).dragTo(session.page.locator(to), {
7076
+ timeout: this.operationTimeoutMs
7077
+ })
7078
+ );
7079
+ }
7080
+ async upload(id, ownerId, selector, files, projectRoot, signal) {
7081
+ const session = this.requireOwned(id, ownerId);
7082
+ const root = path11.resolve(projectRoot);
7083
+ const realRoot = await fs8.realpath(root);
7084
+ const resolved = [];
7085
+ for (const file of files) {
7086
+ const absolute = path11.resolve(root, file);
7087
+ const relative13 = path11.relative(root, absolute);
7088
+ if (relative13.startsWith("..") || path11.isAbsolute(relative13)) {
7089
+ throw new Error("browser: upload files must stay inside the project root");
7090
+ }
7091
+ const realFile = await fs8.realpath(absolute);
7092
+ const realRelative = path11.relative(realRoot, realFile);
7093
+ if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
7094
+ throw new Error("browser: upload files must not escape the project root through a symlink");
7095
+ }
7096
+ const stat18 = await fs8.stat(realFile);
7097
+ if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
7098
+ resolved.push(realFile);
7099
+ }
7100
+ await this.runPageOperation(
7101
+ session,
7102
+ signal,
7103
+ () => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
7104
+ );
7105
+ }
7106
+ async evaluate(id, ownerId, expression, signal) {
7107
+ const session = this.requireOwned(id, ownerId);
7108
+ const result = await this.runPageOperation(
7109
+ session,
7110
+ signal,
7111
+ () => session.page.evaluate(expression)
7112
+ );
7113
+ const serialized = JSON.stringify(result);
7114
+ if (serialized && serialized.length > this.maxSnapshotChars) {
7115
+ throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
7116
+ }
7117
+ return result;
7118
+ }
7119
+ async close(id, ownerId) {
7120
+ const session = this.requireOwned(id, ownerId);
7121
+ let trace;
7122
+ try {
7123
+ if (session.tracing) {
7124
+ const target = this.artifacts.pathFor(session.id, "zip");
7125
+ await fs8.mkdir(path11.dirname(target.path), { recursive: true });
7126
+ await session.context.tracing.stop({ path: target.path });
7127
+ trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
7128
+ }
7129
+ } finally {
7130
+ await session.context.close().catch(() => void 0);
7131
+ this.sessions.delete(id);
7132
+ await this.closeBrowserIfIdle();
7133
+ }
7134
+ return trace ? { trace } : {};
7135
+ }
7136
+ async closeOwner(ownerId) {
7137
+ const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
7138
+ for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
7139
+ }
7140
+ async dispose() {
7141
+ for (const session of [...this.sessions.values()]) {
7142
+ await session.context.close().catch(() => void 0);
7143
+ }
7144
+ this.sessions.clear();
7145
+ const browser = this.browser;
7146
+ this.browser = void 0;
7147
+ this.launching = void 0;
7148
+ await browser?.close().catch(() => void 0);
7149
+ await this.networkProxy.close();
7150
+ }
7151
+ async ensureBrowser(signal) {
7152
+ if (this.browser?.isConnected()) return this.browser;
7153
+ if (!this.launching) {
7154
+ this.launching = this.launcher(this.headless).then((browser) => {
7155
+ this.browser = browser;
7156
+ browser.on("disconnected", () => {
7157
+ if (this.browser === browser) this.browser = void 0;
7158
+ for (const [id, session] of this.sessions) {
7159
+ if (session.context.browser() === browser) this.sessions.delete(id);
7160
+ }
7161
+ });
7162
+ return browser;
7163
+ });
7164
+ }
7165
+ try {
7166
+ return await abortable(
7167
+ signal,
7168
+ () => this.launching,
7169
+ async () => {
7170
+ const browser = await this.launching?.catch(() => void 0);
7171
+ await browser?.close().catch(() => void 0);
7172
+ }
7173
+ );
7174
+ } finally {
7175
+ this.launching = void 0;
7176
+ }
7177
+ }
7178
+ requireOwned(id, ownerId) {
7179
+ const session = this.sessions.get(id);
7180
+ if (!session) throw new Error(`browser: unknown session "${id}"`);
7181
+ if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
7182
+ session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
7183
+ return session;
7184
+ }
7185
+ async summary(session) {
7186
+ return {
7187
+ id: session.id,
7188
+ ownerId: session.ownerId,
7189
+ url: safeBrowserUrl(session.page.url()),
7190
+ title: redactBrowserText(await session.page.title().catch(() => "")),
7191
+ createdAt: session.createdAt,
7192
+ lastUsedAt: session.lastUsedAt,
7193
+ tracing: session.tracing
7194
+ };
7195
+ }
7196
+ attachEvidenceCollectors(session) {
7197
+ session.page.on("console", (message) => {
7198
+ pushBounded(
7199
+ session.console,
7200
+ {
7201
+ level: message.type(),
7202
+ text: redactBrowserText(message.text()).slice(0, 4096),
7203
+ at: (/* @__PURE__ */ new Date()).toISOString()
7204
+ },
7205
+ this.maxConsoleEntries
7206
+ );
7207
+ });
7208
+ session.page.on("response", (response) => {
7209
+ this.pushNetwork(session, {
7210
+ method: response.request().method(),
7211
+ url: safeBrowserUrl(response.url()),
7212
+ status: response.status(),
7213
+ at: (/* @__PURE__ */ new Date()).toISOString()
7214
+ });
7215
+ });
7216
+ session.page.on("requestfailed", (request2) => {
7217
+ this.pushNetwork(session, {
7218
+ method: request2.method(),
7219
+ url: safeBrowserUrl(request2.url()),
7220
+ failed: true,
7221
+ at: (/* @__PURE__ */ new Date()).toISOString()
7222
+ });
7223
+ });
7224
+ }
7225
+ pushNetwork(session, entry) {
7226
+ pushBounded(session.network, entry, this.maxNetworkEntries);
7227
+ }
7228
+ async runPageOperation(session, signal, operation) {
7229
+ signal.throwIfAborted();
7230
+ session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
7231
+ return abortable(signal, operation, async () => {
7232
+ await session.context.close().catch(() => void 0);
7233
+ this.sessions.delete(session.id);
7234
+ await this.closeBrowserIfIdle();
7235
+ });
7236
+ }
7237
+ async closeBrowserIfIdle() {
7238
+ if (this.sessions.size > 0 || !this.browser) return;
7239
+ const browser = this.browser;
7240
+ this.browser = void 0;
7241
+ await browser.close().catch(() => void 0);
7242
+ }
7243
+ };
7244
+ async function defaultLauncher(headless) {
7245
+ try {
7246
+ const { chromium } = await import("@playwright/test");
7247
+ return await chromium.launch({ headless });
7248
+ } catch (err) {
7249
+ const detail = err instanceof Error ? err.message : String(err);
7250
+ throw new Error(
7251
+ `browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
7252
+ );
7253
+ }
7254
+ }
7255
+ async function browserInstallationDiagnostics() {
7256
+ try {
7257
+ const { chromium } = await import("@playwright/test");
7258
+ const executablePath = chromium.executablePath();
7259
+ await fs8.access(executablePath);
7260
+ return { available: true, executablePath, message: "Playwright Chromium is available." };
7261
+ } catch {
7262
+ return {
7263
+ available: false,
7264
+ message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
7265
+ };
7266
+ }
7267
+ }
7268
+ function pushBounded(target, value, limit) {
7269
+ target.push(value);
7270
+ if (target.length > limit) target.splice(0, target.length - limit);
7271
+ }
7272
+ async function abortable(signal, operation, onAbort) {
7273
+ signal.throwIfAborted();
7274
+ return new Promise((resolve15, reject) => {
7275
+ let settled = false;
7276
+ let aborting = false;
7277
+ const finish = (fn) => {
7278
+ if (settled) return;
7279
+ settled = true;
7280
+ signal.removeEventListener("abort", abort);
7281
+ fn();
7282
+ };
7283
+ const abort = () => {
7284
+ aborting = true;
7285
+ void Promise.resolve(onAbort()).finally(() => {
7286
+ finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
7287
+ });
7288
+ };
7289
+ signal.addEventListener("abort", abort, { once: true });
7290
+ operation().then(
7291
+ (value) => {
7292
+ if (!aborting) finish(() => resolve15(value));
7293
+ },
7294
+ (err) => {
7295
+ if (!aborting) finish(() => reject(err));
7296
+ }
7297
+ );
7298
+ });
7299
+ }
7300
+
7301
+ // src/browser/tools.ts
7302
+ import * as path12 from "node:path";
7303
+ import { ToolCapabilities } from "@wrongstack/core";
7304
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
7305
+ var managers = /* @__PURE__ */ new Map();
7306
+ var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
7307
+ function managerFor(ctx) {
7308
+ const root = path12.resolve(ctx.projectRoot);
7309
+ let manager = managers.get(root);
7310
+ if (!manager) {
7311
+ const wpaths = resolveWstackPaths({ projectRoot: root });
7312
+ manager = new BrowserSessionManager({
7313
+ artifactRoot: path12.join(wpaths.projectDir, "browser-artifacts"),
7314
+ allowedPrivateOrigins: parsePrivateOriginAllowlist(
7315
+ process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
7316
+ )
7317
+ });
7318
+ managers.set(root, manager);
7319
+ }
7320
+ const runSignal = ctx.signal;
7321
+ if (cleanupSignalByContext.get(ctx) !== runSignal) {
7322
+ cleanupSignalByContext.set(ctx, runSignal);
7323
+ const managerForRun = manager;
7324
+ const ownerForRun = owner(ctx);
7325
+ ctx.registerAbortHook?.(async () => {
7326
+ if (cleanupSignalByContext.get(ctx) === runSignal) {
7327
+ cleanupSignalByContext.delete(ctx);
7328
+ }
7329
+ await managerForRun.closeOwner(ownerForRun);
7330
+ });
7331
+ }
7332
+ return manager;
7333
+ }
7334
+ function owner(ctx) {
7335
+ return ctx.agentId || "leader";
7336
+ }
7337
+ function json(value) {
7338
+ return JSON.stringify(value, null, 2);
7339
+ }
7340
+ var sessionIdSchema = {
7341
+ type: "string",
7342
+ minLength: 1,
7343
+ description: "Browser session id returned by browser_open."
7344
+ };
7345
+ var browserOpenTool = {
7346
+ name: "browser_open",
7347
+ description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
7348
+ usageHint: "browser_open({ url?, width?, height?, trace? })",
7349
+ permission: "confirm",
7350
+ mutating: true,
7351
+ riskTier: "standard",
7352
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7353
+ subjectKey: "url",
7354
+ inputSchema: {
7355
+ type: "object",
7356
+ properties: {
7357
+ url: { type: "string", description: "Optional absolute http(s) URL." },
7358
+ width: { type: "integer", minimum: 320, maximum: 3840 },
7359
+ height: { type: "integer", minimum: 240, maximum: 2160 },
7360
+ trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
7361
+ }
7362
+ },
7363
+ async execute(input, ctx, opts) {
7364
+ const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
7365
+ return json(
7366
+ await managerFor(ctx).open(
7367
+ owner(ctx),
7368
+ { url: input.url, viewport, trace: input.trace },
7369
+ opts.signal
7370
+ )
7371
+ );
7372
+ }
7373
+ };
7374
+ var browserListTool = {
7375
+ name: "browser_list",
7376
+ description: "List browser sessions owned by the current agent without exposing other agents sessions.",
7377
+ permission: "auto",
7378
+ mutating: false,
7379
+ riskTier: "safe",
7380
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7381
+ inputSchema: { type: "object", properties: {} },
7382
+ async execute(_input, ctx) {
7383
+ return json(await managerFor(ctx).list(owner(ctx)));
7384
+ }
7385
+ };
7386
+ var browserStatusTool = {
7387
+ name: "browser_status",
7388
+ description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
7389
+ permission: "auto",
7390
+ mutating: false,
7391
+ riskTier: "safe",
7392
+ capabilities: [ToolCapabilities.TOOL_META],
7393
+ inputSchema: { type: "object", properties: {} },
7394
+ async execute() {
7395
+ return json(await browserInstallationDiagnostics());
7396
+ }
7397
+ };
7398
+ var browserNavigateTool = {
7399
+ name: "browser_navigate",
7400
+ description: "Navigate an owned browser session to an approved http(s) URL.",
7401
+ usageHint: "browser_navigate({ sessionId, url })",
7402
+ permission: "confirm",
7403
+ mutating: true,
7404
+ riskTier: "standard",
7405
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7406
+ subjectKey: "url",
7407
+ inputSchema: {
7408
+ type: "object",
7409
+ properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
7410
+ required: ["sessionId", "url"]
7411
+ },
7412
+ async execute(input, ctx, opts) {
7413
+ return json(
7414
+ await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
7415
+ );
7416
+ }
7417
+ };
7418
+ var browserSnapshotTool = {
7419
+ name: "browser_snapshot",
7420
+ description: "Return bounded accessibility state plus redacted console and network summaries.",
7421
+ usageHint: "browser_snapshot({ sessionId })",
7422
+ permission: "auto",
7423
+ mutating: false,
7424
+ riskTier: "safe",
7425
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7426
+ inputSchema: {
7427
+ type: "object",
7428
+ properties: { sessionId: sessionIdSchema },
7429
+ required: ["sessionId"]
7430
+ },
7431
+ async execute(input, ctx, opts) {
7432
+ return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
7433
+ }
7434
+ };
7435
+ var browserScreenshotTool = {
7436
+ name: "browser_screenshot",
7437
+ description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
7438
+ usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
7439
+ permission: "confirm",
7440
+ mutating: true,
7441
+ riskTier: "safe",
7442
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7443
+ inputSchema: {
7444
+ type: "object",
7445
+ properties: {
7446
+ sessionId: sessionIdSchema,
7447
+ fullPage: { type: "boolean" },
7448
+ selector: { type: "string", minLength: 1 }
7449
+ },
7450
+ required: ["sessionId"]
7451
+ },
7452
+ async execute(input, ctx, opts) {
7453
+ return json(
7454
+ await managerFor(ctx).screenshot(
7455
+ input.sessionId,
7456
+ owner(ctx),
7457
+ { fullPage: input.fullPage, selector: input.selector },
7458
+ opts.signal
7459
+ )
7460
+ );
7461
+ }
7462
+ };
7463
+ function selectorTool(name, description, run) {
7464
+ return {
7465
+ name,
7466
+ description,
7467
+ usageHint: `${name}({ sessionId, selector })`,
7468
+ permission: "confirm",
7469
+ mutating: true,
7470
+ riskTier: "standard",
7471
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7472
+ inputSchema: {
7473
+ type: "object",
7474
+ properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
7475
+ required: ["sessionId", "selector"]
7476
+ },
7477
+ async execute(input, ctx, opts) {
7478
+ await run(managerFor(ctx), input, owner(ctx), opts.signal);
7479
+ return "ok";
7480
+ }
7481
+ };
7482
+ }
7483
+ var browserClickTool = selectorTool(
7484
+ "browser_click",
7485
+ "Click an element in an owned browser session.",
7486
+ (manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
7487
+ );
7488
+ var browserHoverTool = selectorTool(
7489
+ "browser_hover",
7490
+ "Hover an element in an owned browser session.",
7491
+ (manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
7492
+ );
7493
+ var browserTypeTool = {
7494
+ name: "browser_type",
7495
+ 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.",
7496
+ usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
7497
+ permission: "confirm",
7498
+ mutating: true,
7499
+ riskTier: "standard",
7500
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7501
+ inputSchema: {
7502
+ type: "object",
7503
+ properties: {
7504
+ sessionId: sessionIdSchema,
7505
+ selector: { type: "string", minLength: 1 },
7506
+ text: { type: "string" },
7507
+ secretEnv: {
7508
+ type: "string",
7509
+ pattern: "^[A-Z_][A-Z0-9_]*$",
7510
+ description: "Host environment variable resolved only at execution time."
7511
+ }
7512
+ },
7513
+ required: ["sessionId", "selector"]
7514
+ },
7515
+ validate(input) {
7516
+ return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
7517
+ },
7518
+ async execute(input, ctx, opts) {
7519
+ const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
7520
+ if (value === void 0) {
7521
+ throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
7522
+ }
7523
+ await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
7524
+ return "ok";
7525
+ }
7526
+ };
7527
+ var browserSelectTool = {
7528
+ name: "browser_select",
7529
+ description: "Select an option in an owned browser session.",
7530
+ usageHint: "browser_select({ sessionId, selector, value })",
7531
+ permission: "confirm",
7532
+ mutating: true,
7533
+ riskTier: "standard",
7534
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7535
+ inputSchema: {
7536
+ type: "object",
7537
+ properties: {
7538
+ sessionId: sessionIdSchema,
7539
+ selector: { type: "string", minLength: 1 },
7540
+ value: { type: "string" }
7541
+ },
7542
+ required: ["sessionId", "selector", "value"]
7543
+ },
7544
+ async execute(input, ctx, opts) {
7545
+ await managerFor(ctx).select(
7546
+ input.sessionId,
7547
+ owner(ctx),
7548
+ input.selector,
7549
+ input.value,
7550
+ opts.signal
7551
+ );
7552
+ return "ok";
7553
+ }
7554
+ };
7555
+ var browserPressTool = {
7556
+ name: "browser_press",
7557
+ description: "Press a keyboard key in an owned browser session.",
7558
+ usageHint: "browser_press({ sessionId, key })",
7559
+ permission: "confirm",
7560
+ mutating: true,
7561
+ riskTier: "standard",
7562
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7563
+ inputSchema: {
7564
+ type: "object",
7565
+ properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
7566
+ required: ["sessionId", "key"]
7567
+ },
7568
+ async execute(input, ctx, opts) {
7569
+ await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
7570
+ return "ok";
7571
+ }
7572
+ };
7573
+ var browserDragTool = {
7574
+ name: "browser_drag",
7575
+ description: "Drag one element to another in an owned browser session.",
7576
+ usageHint: "browser_drag({ sessionId, from, to })",
7577
+ permission: "confirm",
7578
+ mutating: true,
7579
+ riskTier: "standard",
7580
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7581
+ inputSchema: {
7582
+ type: "object",
7583
+ properties: {
7584
+ sessionId: sessionIdSchema,
7585
+ from: { type: "string", minLength: 1 },
7586
+ to: { type: "string", minLength: 1 }
7587
+ },
7588
+ required: ["sessionId", "from", "to"]
7589
+ },
7590
+ async execute(input, ctx, opts) {
7591
+ await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
7592
+ return "ok";
7593
+ }
7594
+ };
7595
+ var browserWaitTool = {
7596
+ name: "browser_wait",
7597
+ description: "Wait for an element or a bounded duration in an owned browser session.",
7598
+ usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
7599
+ permission: "auto",
7600
+ mutating: false,
7601
+ riskTier: "safe",
7602
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7603
+ inputSchema: {
7604
+ type: "object",
7605
+ properties: {
7606
+ sessionId: sessionIdSchema,
7607
+ selector: { type: "string", minLength: 1 },
7608
+ timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
7609
+ },
7610
+ required: ["sessionId"]
7611
+ },
7612
+ async execute(input, ctx, opts) {
7613
+ await managerFor(ctx).wait(
7614
+ input.sessionId,
7615
+ owner(ctx),
7616
+ { selector: input.selector, timeoutMs: input.timeoutMs },
7617
+ opts.signal
7618
+ );
7619
+ return "ok";
7620
+ }
7621
+ };
7622
+ var browserEvaluateTool = {
7623
+ name: "browser_evaluate",
7624
+ description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
7625
+ usageHint: "browser_evaluate({ sessionId, expression })",
7626
+ permission: "confirm",
7627
+ mutating: true,
7628
+ riskTier: "destructive",
7629
+ capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
7630
+ inputSchema: {
7631
+ type: "object",
7632
+ properties: {
7633
+ sessionId: sessionIdSchema,
7634
+ expression: { type: "string", minLength: 1, maxLength: 16384 }
7635
+ },
7636
+ required: ["sessionId", "expression"]
7637
+ },
7638
+ async execute(input, ctx, opts) {
7639
+ return json(
7640
+ await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
7641
+ );
7642
+ }
7643
+ };
7644
+ var browserUploadTool = {
7645
+ name: "browser_upload",
7646
+ description: "Upload project-local files through a file input in an owned browser session.",
7647
+ usageHint: "browser_upload({ sessionId, selector, files })",
7648
+ permission: "confirm",
7649
+ mutating: true,
7650
+ riskTier: "destructive",
7651
+ capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
7652
+ inputSchema: {
7653
+ type: "object",
7654
+ properties: {
7655
+ sessionId: sessionIdSchema,
7656
+ selector: { type: "string", minLength: 1 },
7657
+ files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
7658
+ },
7659
+ required: ["sessionId", "selector", "files"]
7660
+ },
7661
+ async execute(input, ctx, opts) {
7662
+ await managerFor(ctx).upload(
7663
+ input.sessionId,
7664
+ owner(ctx),
7665
+ input.selector,
7666
+ input.files,
7667
+ ctx.projectRoot,
7668
+ opts.signal
7669
+ );
7670
+ return "ok";
7671
+ }
7672
+ };
7673
+ var browserCloseTool = {
7674
+ name: "browser_close",
7675
+ description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
7676
+ usageHint: "browser_close({ sessionId })",
7677
+ permission: "auto",
7678
+ mutating: false,
7679
+ riskTier: "safe",
7680
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7681
+ inputSchema: {
7682
+ type: "object",
7683
+ properties: { sessionId: sessionIdSchema },
7684
+ required: ["sessionId"]
7685
+ },
7686
+ async execute(input, ctx) {
7687
+ return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
7688
+ }
7689
+ };
7690
+ var browserTools = [
7691
+ browserOpenTool,
7692
+ browserStatusTool,
7693
+ browserListTool,
7694
+ browserNavigateTool,
7695
+ browserSnapshotTool,
7696
+ browserScreenshotTool,
7697
+ browserClickTool,
7698
+ browserTypeTool,
7699
+ browserSelectTool,
7700
+ browserPressTool,
7701
+ browserHoverTool,
7702
+ browserDragTool,
7703
+ browserWaitTool,
7704
+ browserEvaluateTool,
7705
+ browserUploadTool,
7706
+ browserCloseTool
7707
+ ];
7708
+ for (const tool of browserTools) tool.icon = "web";
7709
+ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
7710
+ async function shutdownBrowserTools() {
7711
+ const active = [...managers.values()];
7712
+ managers.clear();
7713
+ await Promise.all(active.map((manager) => manager.dispose()));
7714
+ }
7715
+
6481
7716
  // src/codebase-index/writer.ts
6482
7717
  import { expectDefined as expectDefined2 } from "@wrongstack/core";
6483
7718
 
@@ -6567,9 +7802,9 @@ function resetIndexCircuitBreaker() {
6567
7802
  // src/codebase-index/writer.ts
6568
7803
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
6569
7804
  import { createRequire } from "node:module";
6570
- import * as fs7 from "node:fs";
6571
- import * as path10 from "node:path";
6572
- import { resolveWstackPaths } from "@wrongstack/core";
7805
+ import * as fs9 from "node:fs";
7806
+ import * as path13 from "node:path";
7807
+ import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
6573
7808
 
6574
7809
  // src/codebase-index/schema.ts
6575
7810
  var SCHEMA_VERSION = 2;
@@ -6698,7 +7933,7 @@ var Bm25Index = class {
6698
7933
  // src/codebase-index/writer.ts
6699
7934
  var DB_FILE = "index.db";
6700
7935
  function resolveIndexDir(projectRoot, override) {
6701
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
7936
+ return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
6702
7937
  }
6703
7938
  function codebaseIndexDirOverride(ctx) {
6704
7939
  const v = ctx.meta?.["codebaseIndexDir"];
@@ -6813,9 +8048,9 @@ var IndexStore = class {
6813
8048
  }
6814
8049
  constructor(projectRoot, opts = {}) {
6815
8050
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6816
- fs7.mkdirSync(this.indexDir, { recursive: true });
8051
+ fs9.mkdirSync(this.indexDir, { recursive: true });
6817
8052
  const Database = loadDatabaseSync();
6818
- this.db = new Database(path10.join(this.indexDir, DB_FILE));
8053
+ this.db = new Database(path13.join(this.indexDir, DB_FILE));
6819
8054
  try {
6820
8055
  this.db.exec("PRAGMA journal_mode = WAL");
6821
8056
  this.db.exec("PRAGMA busy_timeout = 5000");
@@ -7394,9 +8629,9 @@ var IndexStore = class {
7394
8629
  }));
7395
8630
  }
7396
8631
  sizeBytes() {
7397
- const dbPath = path10.join(this.indexDir, DB_FILE);
8632
+ const dbPath = path13.join(this.indexDir, DB_FILE);
7398
8633
  try {
7399
- return fs7.statSync(dbPath).size;
8634
+ return fs9.statSync(dbPath).size;
7400
8635
  } catch {
7401
8636
  return 0;
7402
8637
  }
@@ -7411,14 +8646,14 @@ var IndexStore = class {
7411
8646
  };
7412
8647
 
7413
8648
  // src/codebase-index/background-indexer.ts
7414
- import * as fs13 from "node:fs";
8649
+ import * as fs15 from "node:fs";
7415
8650
  import { fileURLToPath } from "node:url";
7416
8651
  import { Worker } from "node:worker_threads";
7417
8652
 
7418
8653
  // src/codebase-index/indexer.ts
7419
8654
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
7420
- import * as fs12 from "node:fs/promises";
7421
- import * as path16 from "node:path";
8655
+ import * as fs14 from "node:fs/promises";
8656
+ import * as path19 from "node:path";
7422
8657
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
7423
8658
 
7424
8659
  // src/codebase-index/ts-parser.ts
@@ -7611,8 +8846,8 @@ function detectLang(file) {
7611
8846
  // src/codebase-index/go-parser.ts
7612
8847
  import { spawn as spawn4 } from "node:child_process";
7613
8848
  import * as os5 from "node:os";
7614
- import * as path11 from "node:path";
7615
- import * as fs8 from "node:fs/promises";
8849
+ import * as path14 from "node:path";
8850
+ import * as fs10 from "node:fs/promises";
7616
8851
  async function parseSymbols2(opts) {
7617
8852
  const { file, content, lang } = opts;
7618
8853
  try {
@@ -7913,10 +9148,10 @@ func formatType(t ast.Expr) string {
7913
9148
  }
7914
9149
  `;
7915
9150
  async function syncGoParse(filePath, content, lang) {
7916
- const tmpDir = await fs8.mkdtemp(path11.join(os5.tmpdir(), "ws-go-parse-"));
9151
+ const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
7917
9152
  try {
7918
- const scriptPath = path11.join(tmpDir, "parse.go");
7919
- await fs8.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
9153
+ const scriptPath = path14.join(tmpDir, "parse.go");
9154
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
7920
9155
  const proc = spawn4("go", ["run", scriptPath], {
7921
9156
  stdio: ["pipe", "pipe", "pipe"],
7922
9157
  windowsHide: true
@@ -7928,8 +9163,8 @@ async function syncGoParse(filePath, content, lang) {
7928
9163
  proc.stdin?.write(content);
7929
9164
  proc.stdin?.end();
7930
9165
  const { code } = await Promise.race([
7931
- new Promise((resolve12) => {
7932
- proc.on("close", (c) => resolve12({ code: c }));
9166
+ new Promise((resolve15) => {
9167
+ proc.on("close", (c) => resolve15({ code: c }));
7933
9168
  }),
7934
9169
  new Promise(
7935
9170
  (_, reject) => setTimeout(() => {
@@ -7959,15 +9194,15 @@ async function syncGoParse(filePath, content, lang) {
7959
9194
  } catch {
7960
9195
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7961
9196
  } finally {
7962
- await fs8.rm(tmpDir, { recursive: true, force: true });
9197
+ await fs10.rm(tmpDir, { recursive: true, force: true });
7963
9198
  }
7964
9199
  }
7965
9200
 
7966
9201
  // src/codebase-index/py-parser.ts
7967
9202
  import { spawn as spawn5 } from "node:child_process";
7968
- import * as fs9 from "node:fs/promises";
9203
+ import * as fs11 from "node:fs/promises";
7969
9204
  import * as os6 from "node:os";
7970
- import * as path12 from "node:path";
9205
+ import * as path15 from "node:path";
7971
9206
  async function parseSymbols3(opts) {
7972
9207
  const { file, content, lang } = opts;
7973
9208
  try {
@@ -8183,10 +9418,10 @@ var _cachedScriptPath = null;
8183
9418
  async function syncPyParse(filePath, content, lang) {
8184
9419
  try {
8185
9420
  if (!_cachedScriptPath) {
8186
- const tmpDir = path12.join(os6.tmpdir(), "ws-py-parse");
8187
- await fs9.mkdir(tmpDir, { recursive: true });
8188
- _cachedScriptPath = path12.join(tmpDir, "parse.py");
8189
- await fs9.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9421
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9422
+ await fs11.mkdir(tmpDir, { recursive: true });
9423
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
9424
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
8190
9425
  }
8191
9426
  const proc = spawn5("python", [_cachedScriptPath, filePath], {
8192
9427
  stdio: ["pipe", "pipe", "pipe"],
@@ -8199,8 +9434,8 @@ async function syncPyParse(filePath, content, lang) {
8199
9434
  stdout += chunk.toString();
8200
9435
  });
8201
9436
  const { code } = await Promise.race([
8202
- new Promise((resolve12) => {
8203
- proc.on("close", (c) => resolve12({ code: c }));
9437
+ new Promise((resolve15) => {
9438
+ proc.on("close", (c) => resolve15({ code: c }));
8204
9439
  }),
8205
9440
  new Promise(
8206
9441
  (_, reject) => setTimeout(() => {
@@ -8235,8 +9470,8 @@ async function syncPyParse(filePath, content, lang) {
8235
9470
  // src/codebase-index/rs-parser.ts
8236
9471
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
8237
9472
  import { execFileSync, spawn as spawn6 } from "node:child_process";
8238
- import * as fs10 from "node:fs/promises";
8239
- import * as path13 from "node:path";
9473
+ import * as fs12 from "node:fs/promises";
9474
+ import * as path16 from "node:path";
8240
9475
  async function parseSymbols4(opts) {
8241
9476
  const { file, content, lang } = opts;
8242
9477
  const nativeAvailable = checkNativeParser();
@@ -8249,7 +9484,7 @@ async function parseSymbols4(opts) {
8249
9484
  function checkNativeParser() {
8250
9485
  try {
8251
9486
  execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
8252
- const toolsDir = path13.join(process.cwd(), "tools");
9487
+ const toolsDir = path16.join(process.cwd(), "tools");
8253
9488
  try {
8254
9489
  execFileSync(
8255
9490
  "cargo",
@@ -8259,7 +9494,7 @@ function checkNativeParser() {
8259
9494
  "--format-version",
8260
9495
  "1",
8261
9496
  "--manifest-path",
8262
- path13.join(toolsDir, "Cargo.toml")
9497
+ path16.join(toolsDir, "Cargo.toml")
8263
9498
  ],
8264
9499
  { stdio: "pipe", windowsHide: true }
8265
9500
  );
@@ -8273,13 +9508,13 @@ function checkNativeParser() {
8273
9508
  }
8274
9509
  async function tryNativeParse(file, content) {
8275
9510
  try {
8276
- const toolsDir = path13.join(process.cwd(), "tools");
8277
- const crateDir = path13.join(toolsDir, "syn-parser");
8278
- const tmpFile = path13.join(crateDir, "src", "input.rs");
8279
- await fs10.writeFile(tmpFile, content, "utf8");
9511
+ const toolsDir = path16.join(process.cwd(), "tools");
9512
+ const crateDir = path16.join(toolsDir, "syn-parser");
9513
+ const tmpFile = path16.join(crateDir, "src", "input.rs");
9514
+ await fs12.writeFile(tmpFile, content, "utf8");
8280
9515
  const proc = spawn6(
8281
9516
  "cargo",
8282
- ["run", "--manifest-path", path13.join(toolsDir, "Cargo.toml")],
9517
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
8283
9518
  {
8284
9519
  cwd: process.cwd(),
8285
9520
  stdio: ["pipe", "pipe", "pipe"],
@@ -8291,8 +9526,8 @@ async function tryNativeParse(file, content) {
8291
9526
  stdout += chunk.toString();
8292
9527
  });
8293
9528
  const { code } = await Promise.race([
8294
- new Promise((resolve12) => {
8295
- proc.on("close", (c) => resolve12({ code: c }));
9529
+ new Promise((resolve15) => {
9530
+ proc.on("close", (c) => resolve15({ code: c }));
8296
9531
  }),
8297
9532
  new Promise(
8298
9533
  (_, reject) => setTimeout(() => {
@@ -8383,7 +9618,7 @@ function regexParse(opts) {
8383
9618
 
8384
9619
  // src/codebase-index/json-parser.ts
8385
9620
  import { expectDefined as expectDefined4 } from "@wrongstack/core";
8386
- import * as path14 from "node:path";
9621
+ import * as path17 from "node:path";
8387
9622
  function parseSymbols5(opts) {
8388
9623
  const { file, content, lang } = opts;
8389
9624
  try {
@@ -8395,9 +9630,9 @@ function parseSymbols5(opts) {
8395
9630
  function regexParse2(opts) {
8396
9631
  const { file, content, lang } = opts;
8397
9632
  const symbols = [];
8398
- const basename7 = path14.basename(file).toLowerCase();
8399
- const isPackageJson = basename7 === "package.json";
8400
- const isTsconfig = basename7 === "tsconfig.json" || basename7 === "tsconfig.build.json";
9633
+ const basename8 = path17.basename(file).toLowerCase();
9634
+ const isPackageJson = basename8 === "package.json";
9635
+ const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
8401
9636
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
8402
9637
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
8403
9638
  const lines = content.split("\n");
@@ -8421,11 +9656,11 @@ function regexParse2(opts) {
8421
9656
  const line = lineFromOffset(offset);
8422
9657
  symbols.push(
8423
9658
  makeSymbol({
8424
- name: path14.basename(file),
9659
+ name: path17.basename(file),
8425
9660
  kind: "object",
8426
9661
  line,
8427
9662
  col: 0,
8428
- signature: `"${path14.basename(file)}" = { ... }`,
9663
+ signature: `"${path17.basename(file)}" = { ... }`,
8429
9664
  file,
8430
9665
  lang
8431
9666
  })
@@ -8736,8 +9971,8 @@ function makeSymbol2(opts) {
8736
9971
  }
8737
9972
 
8738
9973
  // src/codebase-index/gitignore.ts
8739
- import * as fs11 from "node:fs/promises";
8740
- import * as path15 from "node:path";
9974
+ import * as fs13 from "node:fs/promises";
9975
+ import * as path18 from "node:path";
8741
9976
  import { compileGlob } from "@wrongstack/core";
8742
9977
  function globBody(glob) {
8743
9978
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -8783,7 +10018,7 @@ function compileGitignore(lines) {
8783
10018
  async function loadGitignoreMatcher(projectRoot) {
8784
10019
  let lines = [];
8785
10020
  try {
8786
- const raw = await fs11.readFile(path15.join(projectRoot, ".gitignore"), "utf8");
10021
+ const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
8787
10022
  lines = raw.split("\n");
8788
10023
  } catch {
8789
10024
  }
@@ -8794,7 +10029,7 @@ async function loadGitignoreMatcher(projectRoot) {
8794
10029
  var YIELD_EVERY_N = 50;
8795
10030
  var PARALLEL_BATCH = 20;
8796
10031
  function yieldEventLoop() {
8797
- return new Promise((resolve12) => setImmediate(resolve12));
10032
+ return new Promise((resolve15) => setImmediate(resolve15));
8798
10033
  }
8799
10034
  function throwIfAborted(signal) {
8800
10035
  if (!signal?.aborted) return;
@@ -8841,21 +10076,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
8841
10076
  }
8842
10077
  let entries;
8843
10078
  try {
8844
- entries = await fs12.readdir(dir, { withFileTypes: true });
10079
+ entries = await fs14.readdir(dir, { withFileTypes: true });
8845
10080
  } catch {
8846
10081
  return;
8847
10082
  }
8848
10083
  dirCount++;
8849
10084
  for (const e of entries) {
8850
10085
  if (ignoreSet.has(e.name)) continue;
8851
- const full = path16.join(dir, e.name);
8852
- const rel = path16.relative(projectRoot, full).replace(/\\/g, "/");
10086
+ const full = path19.join(dir, e.name);
10087
+ const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
8853
10088
  if (e.isDirectory()) {
8854
10089
  if (isGitIgnored(rel, true)) continue;
8855
10090
  await walk2(full);
8856
10091
  } else if (e.isFile()) {
8857
10092
  if (isGitIgnored(rel, false)) continue;
8858
- const ext = path16.extname(e.name);
10093
+ const ext = path19.extname(e.name);
8859
10094
  for (const { ext: extName, pat } of globs) {
8860
10095
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
8861
10096
  results.push(full);
@@ -8911,7 +10146,7 @@ async function runIndexerWithStore(store, opts) {
8911
10146
  let files;
8912
10147
  let discoveredFiles = null;
8913
10148
  if (opts.files && opts.files.length > 0) {
8914
- files = opts.files.map((f) => path16.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path16.relative(projectRoot, f).replace(/\\/g, "/"), false));
10149
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
8915
10150
  } else {
8916
10151
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
8917
10152
  discoveredFiles = new Set(files);
@@ -8939,34 +10174,34 @@ async function runIndexerWithStore(store, opts) {
8939
10174
  const statOpts = signal ? { signal } : {};
8940
10175
  const statReadParse = await Promise.allSettled(
8941
10176
  batchFiles.map(async (file) => {
8942
- let stat16;
10177
+ let stat18;
8943
10178
  try {
8944
- stat16 = await fs12.stat(file, statOpts);
10179
+ stat18 = await fs14.stat(file, statOpts);
8945
10180
  } catch (e) {
8946
10181
  if (isAbortError(e)) throw e;
8947
10182
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
8948
10183
  }
8949
- if (!stat16.isFile()) return { file, stat: stat16, lang: "", parsed: null };
10184
+ if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
8950
10185
  const lang = detectLang(file);
8951
- if (!lang) return { file, stat: stat16, lang: "", parsed: null };
10186
+ if (!lang) return { file, stat: stat18, lang: "", parsed: null };
8952
10187
  const meta = existingMeta.get(file);
8953
- if (!force && meta && meta.mtimeMs === Math.floor(stat16.mtimeMs)) {
8954
- return { file, stat: stat16, lang, parsed: null, skippedMeta: meta };
10188
+ if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10189
+ return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
8955
10190
  }
8956
10191
  let content;
8957
10192
  try {
8958
- content = await fs12.readFile(file, { encoding: "utf8", signal });
10193
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
8959
10194
  } catch (e) {
8960
10195
  if (isAbortError(e)) throw e;
8961
- return { file, stat: stat16, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
10196
+ return { file, stat: stat18, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
8962
10197
  }
8963
10198
  let parsed;
8964
10199
  try {
8965
10200
  parsed = await parseFile(file, content, lang);
8966
10201
  } catch (e) {
8967
- return { file, stat: stat16, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
10202
+ return { file, stat: stat18, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
8968
10203
  }
8969
- return { file, stat: stat16, lang, parsed, content };
10204
+ return { file, stat: stat18, lang, parsed, content };
8970
10205
  })
8971
10206
  );
8972
10207
  const batchEntries = [];
@@ -8986,7 +10221,7 @@ async function runIndexerWithStore(store, opts) {
8986
10221
  if (result.error.includes("error:")) errors.push(result.error);
8987
10222
  continue;
8988
10223
  }
8989
- const { stat: stat16, lang, parsed } = result;
10224
+ const { stat: stat18, lang, parsed } = result;
8990
10225
  if (result.skippedMeta) {
8991
10226
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
8992
10227
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -8998,7 +10233,7 @@ async function runIndexerWithStore(store, opts) {
8998
10233
  store.upsertFile({
8999
10234
  file,
9000
10235
  lang,
9001
- mtimeMs: Math.floor(stat16.mtimeMs),
10236
+ mtimeMs: Math.floor(stat18.mtimeMs),
9002
10237
  symbolCount: 0,
9003
10238
  lastIndexed: Date.now()
9004
10239
  });
@@ -9010,7 +10245,7 @@ async function runIndexerWithStore(store, opts) {
9010
10245
  store.upsertFile({
9011
10246
  file,
9012
10247
  lang,
9013
- mtimeMs: Math.floor(stat16.mtimeMs),
10248
+ mtimeMs: Math.floor(stat18.mtimeMs),
9014
10249
  symbolCount: 0,
9015
10250
  lastIndexed: Date.now()
9016
10251
  });
@@ -9026,7 +10261,7 @@ async function runIndexerWithStore(store, opts) {
9026
10261
  lang,
9027
10262
  symbols: parsed.symbols,
9028
10263
  refs,
9029
- mtimeMs: Math.floor(stat16.mtimeMs),
10264
+ mtimeMs: Math.floor(stat18.mtimeMs),
9030
10265
  symbolCount: parsed.symbols.length
9031
10266
  });
9032
10267
  deleteForFiles.push(file);
@@ -9224,7 +10459,7 @@ function resolveWorkerUrl() {
9224
10459
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
9225
10460
  try {
9226
10461
  const url = new URL(rel, import.meta.url);
9227
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath(url))) return url;
10462
+ if (url.protocol === "file:" && fs15.existsSync(fileURLToPath(url))) return url;
9228
10463
  } catch {
9229
10464
  }
9230
10465
  }
@@ -9287,7 +10522,7 @@ function shutdownCodebaseIndexHost() {
9287
10522
  function callIndexOp(op, args, opts) {
9288
10523
  const w = ensureWorker();
9289
10524
  if (!w) return callInline(op, args, opts);
9290
- return new Promise((resolve12, reject) => {
10525
+ return new Promise((resolve15, reject) => {
9291
10526
  const id = nextRpcId++;
9292
10527
  const timer = setTimeout(() => {
9293
10528
  pending.delete(id);
@@ -9310,7 +10545,7 @@ function callIndexOp(op, args, opts) {
9310
10545
  pending.set(id, {
9311
10546
  resolve: (v) => {
9312
10547
  cleanup();
9313
- resolve12(v);
10548
+ resolve15(v);
9314
10549
  },
9315
10550
  reject: (e) => {
9316
10551
  cleanup();
@@ -9693,8 +10928,8 @@ var codebaseStatsTool = {
9693
10928
  };
9694
10929
 
9695
10930
  // src/design.ts
9696
- import * as fs14 from "node:fs/promises";
9697
- import * as path17 from "node:path";
10931
+ import * as fs16 from "node:fs/promises";
10932
+ import * as path20 from "node:path";
9698
10933
  import {
9699
10934
  applyTokenOverrides,
9700
10935
  getDesignKitLoader,
@@ -9708,17 +10943,17 @@ import {
9708
10943
  setDesignOverrides
9709
10944
  } from "@wrongstack/core";
9710
10945
  async function resolveReal(p) {
9711
- const resolved = path17.resolve(p);
10946
+ const resolved = path20.resolve(p);
9712
10947
  let probe = resolved;
9713
10948
  const missing = [];
9714
10949
  for (; ; ) {
9715
10950
  try {
9716
- return path17.resolve(await fs14.realpath(probe), ...missing);
10951
+ return path20.resolve(await fs16.realpath(probe), ...missing);
9717
10952
  } catch (err) {
9718
10953
  if (err.code === "ENOENT") {
9719
- const parent = path17.dirname(probe);
10954
+ const parent = path20.dirname(probe);
9720
10955
  if (parent === probe) return resolved;
9721
- missing.unshift(path17.basename(probe));
10956
+ missing.unshift(path20.basename(probe));
9722
10957
  probe = parent;
9723
10958
  continue;
9724
10959
  }
@@ -9884,18 +11119,18 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
9884
11119
  outPath: input.out
9885
11120
  });
9886
11121
  const root = await resolveReal(ctx.projectRoot);
9887
- const absResolved = path17.isAbsolute(result.path) ? path17.resolve(result.path) : path17.resolve(path17.join(ctx.projectRoot, result.path));
9888
- const absParent = await resolveReal(path17.dirname(absResolved));
9889
- const abs = path17.join(absParent, path17.basename(absResolved));
9890
- const rel = path17.relative(root, abs);
9891
- if (rel.startsWith("..") || path17.isAbsolute(rel)) {
11122
+ const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
11123
+ const absParent = await resolveReal(path20.dirname(absResolved));
11124
+ const abs = path20.join(absParent, path20.basename(absResolved));
11125
+ const rel = path20.relative(root, abs);
11126
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) {
9892
11127
  throw new Error(
9893
11128
  `design: materialize path "${result.path}" would escape the project root`
9894
11129
  );
9895
11130
  }
9896
11131
  let exists = false;
9897
11132
  try {
9898
- await fs14.access(abs);
11133
+ await fs16.access(abs);
9899
11134
  exists = true;
9900
11135
  } catch {
9901
11136
  }
@@ -9912,8 +11147,8 @@ ${result.content}
9912
11147
  \`\`\``
9913
11148
  };
9914
11149
  }
9915
- await fs14.mkdir(path17.dirname(abs), { recursive: true });
9916
- await fs14.writeFile(abs, result.content);
11150
+ await fs16.mkdir(path20.dirname(abs), { recursive: true });
11151
+ await fs16.writeFile(abs, result.content);
9917
11152
  return {
9918
11153
  action,
9919
11154
  kit: active.kit,
@@ -9963,8 +11198,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
9963
11198
  init_util();
9964
11199
  import { spawn as spawn7 } from "node:child_process";
9965
11200
  import { statSync as statSync2 } from "node:fs";
9966
- import * as fs15 from "node:fs/promises";
9967
- import * as path18 from "node:path";
11201
+ import * as fs17 from "node:fs/promises";
11202
+ import * as path21 from "node:path";
9968
11203
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
9969
11204
  var diffTool = {
9970
11205
  name: "diff",
@@ -10048,18 +11283,18 @@ function findGitDir(cwd) {
10048
11283
  let dir = cwd;
10049
11284
  for (let i = 0; i < 20; i++) {
10050
11285
  try {
10051
- const stat16 = statSync2(path18.join(dir, ".git"));
10052
- if (stat16.isDirectory()) return dir;
11286
+ const stat18 = statSync2(path21.join(dir, ".git"));
11287
+ if (stat18.isDirectory()) return dir;
10053
11288
  } catch {
10054
11289
  }
10055
- const parent = path18.dirname(dir);
11290
+ const parent = path21.dirname(dir);
10056
11291
  if (parent === dir) break;
10057
11292
  dir = parent;
10058
11293
  }
10059
11294
  return null;
10060
11295
  }
10061
11296
  function runGit(args, cwd, signal) {
10062
- return new Promise((resolve12) => {
11297
+ return new Promise((resolve15) => {
10063
11298
  let stdout = "";
10064
11299
  let stderr = "";
10065
11300
  const child = spawn7("git", args, {
@@ -10075,8 +11310,8 @@ function runGit(args, cwd, signal) {
10075
11310
  child.stderr?.on("data", (c) => {
10076
11311
  stderr += c.toString();
10077
11312
  });
10078
- child.on("close", (code) => resolve12({ stdout, stderr, exitCode: code ?? 0 }));
10079
- child.on("error", (e) => resolve12({ stdout: "", stderr: e.message, exitCode: 1 }));
11313
+ child.on("close", (code) => resolve15({ stdout, stderr, exitCode: code ?? 0 }));
11314
+ child.on("error", (e) => resolve15({ stdout: "", stderr: e.message, exitCode: 1 }));
10080
11315
  });
10081
11316
  }
10082
11317
  async function fileDiff(input, ctx, _signal) {
@@ -10093,9 +11328,9 @@ async function fileDiff(input, ctx, _signal) {
10093
11328
  const results = [];
10094
11329
  for (const file of files) {
10095
11330
  const absPath = safeResolve(file, ctx);
10096
- const stat16 = await fs15.stat(absPath).catch(() => null);
10097
- if (!stat16?.isFile()) continue;
10098
- const content = await fs15.readFile(absPath, "utf8");
11331
+ const stat18 = await fs17.stat(absPath).catch(() => null);
11332
+ if (!stat18?.isFile()) continue;
11333
+ const content = await fs17.readFile(absPath, "utf8");
10099
11334
  const lines = content.split(/\r?\n/);
10100
11335
  results.push(formatWithLineNumbers(file, lines));
10101
11336
  }
@@ -10115,7 +11350,7 @@ ${numbered}`;
10115
11350
 
10116
11351
  // src/document.ts
10117
11352
  init_util();
10118
- import * as fs16 from "node:fs/promises";
11353
+ import * as fs18 from "node:fs/promises";
10119
11354
  var documentTool = {
10120
11355
  name: "document",
10121
11356
  category: "Project",
@@ -10159,7 +11394,7 @@ var documentTool = {
10159
11394
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
10160
11395
  for (const absPath of fileList) {
10161
11396
  try {
10162
- const content = await fs16.readFile(absPath, "utf8");
11397
+ const content = await fs18.readFile(absPath, "utf8");
10163
11398
  filesProcessed++;
10164
11399
  const processed = processFile(
10165
11400
  content,
@@ -10195,8 +11430,8 @@ async function resolveFiles(filesInput, cwd) {
10195
11430
  for (const f of files) {
10196
11431
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
10197
11432
  try {
10198
- const stat16 = await fs16.stat(absPath);
10199
- if (stat16.isFile()) resolved.push(absPath);
11433
+ const stat18 = await fs18.stat(absPath);
11434
+ if (stat18.isFile()) resolved.push(absPath);
10200
11435
  } catch {
10201
11436
  }
10202
11437
  }
@@ -10263,10 +11498,426 @@ function processFile(content, absPath, _style, _overwrite, target) {
10263
11498
  return results;
10264
11499
  }
10265
11500
 
11501
+ // src/e2e.ts
11502
+ init_util();
11503
+ import { open, readdir as readdir5 } from "node:fs/promises";
11504
+ import * as path22 from "node:path";
11505
+ async function readBoundedText(filePath, maxBytes) {
11506
+ let handle;
11507
+ try {
11508
+ handle = await open(filePath, "r");
11509
+ const info = await handle.stat();
11510
+ if (!info.isFile() || info.size > maxBytes) return void 0;
11511
+ const buffer = Buffer.alloc(info.size);
11512
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
11513
+ return buffer.subarray(0, bytesRead).toString("utf8");
11514
+ } catch {
11515
+ return void 0;
11516
+ } finally {
11517
+ await handle?.close().catch(() => void 0);
11518
+ }
11519
+ }
11520
+ var CONFIG_NAMES = /* @__PURE__ */ new Map([
11521
+ ["playwright.config.ts", "playwright"],
11522
+ ["playwright.config.js", "playwright"],
11523
+ ["playwright.config.mts", "playwright"],
11524
+ ["playwright.config.mjs", "playwright"],
11525
+ ["playwright.config.cts", "playwright"],
11526
+ ["playwright.config.cjs", "playwright"],
11527
+ ["cypress.config.ts", "cypress"],
11528
+ ["cypress.config.js", "cypress"],
11529
+ ["cypress.config.mts", "cypress"],
11530
+ ["cypress.config.mjs", "cypress"],
11531
+ ["cypress.config.cts", "cypress"],
11532
+ ["cypress.config.cjs", "cypress"],
11533
+ ["cypress.json", "cypress"]
11534
+ ]);
11535
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
11536
+ ".git",
11537
+ ".hg",
11538
+ ".svn",
11539
+ ".wrongstack",
11540
+ "node_modules",
11541
+ "dist",
11542
+ "build",
11543
+ "coverage",
11544
+ "coverage-html",
11545
+ "playwright-report",
11546
+ "test-results",
11547
+ ".next"
11548
+ ]);
11549
+ var MAX_SCAN_DIRECTORIES = 4e3;
11550
+ var MAX_PACKAGE_BYTES = 512 * 1024;
11551
+ var MAX_CONFIG_BYTES = 512 * 1024;
11552
+ var MAX_SPEC_SAMPLES = 20;
11553
+ function relativePath(root, target) {
11554
+ const value = path22.relative(root, target) || ".";
11555
+ return value.split(path22.sep).join("/");
11556
+ }
11557
+ function escapeRegExp(value) {
11558
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11559
+ }
11560
+ function staticString(source, property) {
11561
+ const key = escapeRegExp(property);
11562
+ const direct = source.match(
11563
+ new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
11564
+ );
11565
+ if (direct?.[2] && !direct[2].includes("${")) return direct[2];
11566
+ const sameLineFallback = source.match(
11567
+ new RegExp(`(?:['"]?${key}['"]?)\\s*:[^\\r\\n]{0,300}?(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
11568
+ );
11569
+ return sameLineFallback?.[2] && !sameLineFallback[2].includes("${") ? sameLineFallback[2] : void 0;
11570
+ }
11571
+ function staticNumber(source, property) {
11572
+ const key = escapeRegExp(property);
11573
+ const match = source.match(new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(\\d{2,5})\\b`));
11574
+ return match?.[1] ? Number.parseInt(match[1], 10) : void 0;
11575
+ }
11576
+ function serverHints(framework, source) {
11577
+ if (framework === "playwright" && /\bwebServer\s*:/.test(source)) {
11578
+ const command = staticString(source, "command");
11579
+ const url = staticString(source, "url");
11580
+ const port = staticNumber(source, "port");
11581
+ return [
11582
+ {
11583
+ kind: "managed",
11584
+ ...command ? { command } : {},
11585
+ ...url ? { url } : {},
11586
+ ...port ? { port } : {},
11587
+ source: "static-config",
11588
+ incomplete: !command || !url && !port
11589
+ }
11590
+ ];
11591
+ }
11592
+ const baseUrl = staticString(source, "baseURL") ?? staticString(source, "baseUrl");
11593
+ return baseUrl ? [{ kind: "attach", url: baseUrl, source: "static-config", incomplete: false }] : [];
11594
+ }
11595
+ function configuredSpecPattern(framework, source) {
11596
+ if (framework === "playwright") {
11597
+ const testDir = staticString(source, "testDir") ?? "./tests";
11598
+ return `${testDir.replace(/\/$/, "")}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;
11599
+ }
11600
+ return staticString(source, "specPattern") ?? "cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}";
11601
+ }
11602
+ async function scanWorkspace(root, maxDepth, signal) {
11603
+ const result = {
11604
+ configs: [],
11605
+ packageFiles: [],
11606
+ scannedDirectories: 0,
11607
+ truncated: false
11608
+ };
11609
+ const queue = [{ directory: root, depth: 0 }];
11610
+ while (queue.length > 0) {
11611
+ signal.throwIfAborted();
11612
+ const current = queue.shift();
11613
+ if (!current) break;
11614
+ result.scannedDirectories += 1;
11615
+ if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {
11616
+ result.truncated = true;
11617
+ break;
11618
+ }
11619
+ let entries;
11620
+ try {
11621
+ entries = await readdir5(current.directory, { withFileTypes: true });
11622
+ } catch {
11623
+ continue;
11624
+ }
11625
+ for (const entry of entries) {
11626
+ signal.throwIfAborted();
11627
+ const absolutePath = path22.join(current.directory, entry.name);
11628
+ if (entry.isFile()) {
11629
+ if (entry.name === "package.json") result.packageFiles.push(absolutePath);
11630
+ const framework = CONFIG_NAMES.get(entry.name);
11631
+ if (framework) result.configs.push({ framework, absolutePath });
11632
+ } else if (entry.isDirectory() && current.depth < maxDepth && !SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith("coverage-") && !entry.name.startsWith("cov-")) {
11633
+ queue.push({ directory: absolutePath, depth: current.depth + 1 });
11634
+ }
11635
+ }
11636
+ }
11637
+ return result;
11638
+ }
11639
+ async function readPackageInfo(packagePath) {
11640
+ try {
11641
+ const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);
11642
+ if (raw === void 0) return void 0;
11643
+ const parsed = JSON.parse(raw);
11644
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? Object.fromEntries(
11645
+ Object.entries(parsed.scripts).filter(
11646
+ (entry) => typeof entry[1] === "string"
11647
+ )
11648
+ ) : {};
11649
+ const dependencies = /* @__PURE__ */ new Set();
11650
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
11651
+ const value = parsed[field];
11652
+ if (!value || typeof value !== "object") continue;
11653
+ for (const name of Object.keys(value)) dependencies.add(name);
11654
+ }
11655
+ const declared = typeof parsed.packageManager === "string" ? parsed.packageManager.split("@")[0] ?? "" : "";
11656
+ const packageManager = ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : void 0;
11657
+ return { path: packagePath, scripts, dependencies, packageManager };
11658
+ } catch {
11659
+ return void 0;
11660
+ }
11661
+ }
11662
+ function frameworkFromPackage(info) {
11663
+ const found = /* @__PURE__ */ new Set();
11664
+ if (info.dependencies.has("@playwright/test") || info.dependencies.has("playwright")) {
11665
+ found.add("playwright");
11666
+ }
11667
+ if (info.dependencies.has("cypress")) found.add("cypress");
11668
+ for (const command of Object.values(info.scripts)) {
11669
+ if (/\bplaywright(?:\.cmd)?\b/i.test(command)) found.add("playwright");
11670
+ if (/\bcypress(?:\.cmd)?\b/i.test(command)) found.add("cypress");
11671
+ }
11672
+ return [...found];
11673
+ }
11674
+ async function detectPackageManager3(projectRoot, scanRoot, declared) {
11675
+ if (declared) return declared;
11676
+ let directory = projectRoot;
11677
+ while (true) {
11678
+ const names = /* @__PURE__ */ new Set();
11679
+ try {
11680
+ for (const entry of await readdir5(directory)) names.add(entry);
11681
+ } catch {
11682
+ }
11683
+ if (names.has("pnpm-lock.yaml")) return "pnpm";
11684
+ if (names.has("yarn.lock")) return "yarn";
11685
+ if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
11686
+ if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
11687
+ if (directory === scanRoot) break;
11688
+ const parent = path22.dirname(directory);
11689
+ const relativeParent = path22.relative(scanRoot, parent);
11690
+ if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
11691
+ break;
11692
+ }
11693
+ directory = parent;
11694
+ }
11695
+ return "npm";
11696
+ }
11697
+ function matchingScripts(info, framework) {
11698
+ if (!info) return [];
11699
+ const matcher = framework === "playwright" ? /\bplaywright(?:\.cmd)?\b/i : /\bcypress(?:\.cmd)?\b/i;
11700
+ return Object.entries(info.scripts).filter(([, command]) => matcher.test(command)).map(([name]) => name).sort((a, b) => {
11701
+ const preferred = ["test:e2e", "e2e", "e2e:test"];
11702
+ const ai = preferred.indexOf(a);
11703
+ const bi = preferred.indexOf(b);
11704
+ if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
11705
+ return a.localeCompare(b);
11706
+ });
11707
+ }
11708
+ function executionPlan(framework, manager, root, configPath, scripts) {
11709
+ const script = scripts[0];
11710
+ if (script) {
11711
+ return {
11712
+ command: manager,
11713
+ args: ["run", script],
11714
+ cwd: root,
11715
+ source: "package-script",
11716
+ script
11717
+ };
11718
+ }
11719
+ const runnerArgs = framework === "playwright" ? ["playwright", "test"] : ["cypress", "run"];
11720
+ if (configPath) {
11721
+ runnerArgs.push(framework === "playwright" ? "--config" : "--config-file", configPath);
11722
+ }
11723
+ return {
11724
+ command: manager === "bun" ? "bunx" : manager,
11725
+ args: manager === "bun" ? runnerArgs : manager === "npm" ? ["exec", "--", ...runnerArgs] : ["exec", ...runnerArgs],
11726
+ cwd: root,
11727
+ source: "framework-default"
11728
+ };
11729
+ }
11730
+ function isSpec(framework, filename) {
11731
+ if (framework === "cypress") return /\.cy\.(?:[cm]?[jt]sx?)$/i.test(filename);
11732
+ return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
11733
+ }
11734
+ async function collectSpecs(root, framework, testDirectory, signal) {
11735
+ const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
11736
+ const samples = [];
11737
+ let count = 0;
11738
+ let scanned = 0;
11739
+ const queue = [...roots];
11740
+ while (queue.length > 0) {
11741
+ signal.throwIfAborted();
11742
+ const directory = queue.shift();
11743
+ if (!directory) break;
11744
+ scanned += 1;
11745
+ if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
11746
+ let entries;
11747
+ try {
11748
+ entries = await readdir5(directory, { withFileTypes: true });
11749
+ } catch {
11750
+ continue;
11751
+ }
11752
+ for (const entry of entries) {
11753
+ signal.throwIfAborted();
11754
+ const target = path22.join(directory, entry.name);
11755
+ if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
11756
+ else if (entry.isFile() && isSpec(framework, entry.name)) {
11757
+ count += 1;
11758
+ if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));
11759
+ }
11760
+ }
11761
+ }
11762
+ return { count, samples, truncated: count > samples.length };
11763
+ }
11764
+ async function readConfig(configPath) {
11765
+ if (!configPath) return "";
11766
+ return await readBoundedText(configPath, MAX_CONFIG_BYTES) ?? "";
11767
+ }
11768
+ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
11769
+ let directory = projectRoot;
11770
+ while (true) {
11771
+ const found = packagesByDirectory.get(directory);
11772
+ if (found) return found;
11773
+ if (directory === scanRoot) return void 0;
11774
+ const parent = path22.dirname(directory);
11775
+ if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
11776
+ directory = parent;
11777
+ }
11778
+ }
11779
+ async function discoverE2EProjects(root, options) {
11780
+ const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));
11781
+ const scan = await scanWorkspace(root, maxDepth, options.signal);
11782
+ const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
11783
+ (info) => Boolean(info)
11784
+ );
11785
+ const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
11786
+ const candidates = /* @__PURE__ */ new Map();
11787
+ for (const config of scan.configs) {
11788
+ if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
11789
+ continue;
11790
+ }
11791
+ const projectRoot = path22.dirname(config.absolutePath);
11792
+ candidates.set(`${config.framework}:${projectRoot}`, {
11793
+ framework: config.framework,
11794
+ root: projectRoot,
11795
+ configPath: config.absolutePath
11796
+ });
11797
+ }
11798
+ for (const info of packages) {
11799
+ const projectRoot = path22.dirname(info.path);
11800
+ for (const framework of frameworkFromPackage(info)) {
11801
+ if (options.framework && options.framework !== "all" && framework !== options.framework)
11802
+ continue;
11803
+ const key = `${framework}:${projectRoot}`;
11804
+ if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });
11805
+ }
11806
+ }
11807
+ const projects = [];
11808
+ for (const candidate of candidates.values()) {
11809
+ options.signal.throwIfAborted();
11810
+ const info = nearestPackage(candidate.root, packagesByDirectory, root);
11811
+ const source = await readConfig(candidate.configPath);
11812
+ const scripts = matchingScripts(info, candidate.framework);
11813
+ const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
11814
+ const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
11815
+ const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
11816
+ const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
11817
+ const unsafeTestDirectory = Boolean(
11818
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
11819
+ );
11820
+ const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
11821
+ const warnings = [];
11822
+ if (!candidate.configPath)
11823
+ warnings.push("Framework dependency or script found without a config file.");
11824
+ if (candidate.configPath && !source) {
11825
+ warnings.push("Config could not be read or exceeded the static inspection limit.");
11826
+ }
11827
+ if (unsafeTestDirectory) {
11828
+ warnings.push(
11829
+ "Configured testDir resolves outside the requested discovery root and was not scanned."
11830
+ );
11831
+ }
11832
+ const hints = serverHints(candidate.framework, source);
11833
+ if (hints.some((hint) => hint.incomplete)) {
11834
+ warnings.push("Configured server contains dynamic values; only static hints are shown.");
11835
+ }
11836
+ projects.push({
11837
+ framework: candidate.framework,
11838
+ root: relativePath(root, candidate.root),
11839
+ ...candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {},
11840
+ ...info ? { packagePath: relativePath(root, info.path) } : {},
11841
+ packageManager: manager,
11842
+ evidence: [
11843
+ ...candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : [],
11844
+ ...info?.dependencies.has(
11845
+ candidate.framework === "playwright" ? "@playwright/test" : "cypress"
11846
+ ) ? [`dependency:${candidate.framework === "playwright" ? "@playwright/test" : "cypress"}`] : [],
11847
+ ...scripts.map((script) => `script:${script}`)
11848
+ ],
11849
+ scripts,
11850
+ serverHints: hints,
11851
+ specPattern: configuredSpecPattern(candidate.framework, source),
11852
+ specCount: specs.count,
11853
+ specSamples: specs.samples,
11854
+ specsTruncated: specs.truncated,
11855
+ execution: executionPlan(
11856
+ candidate.framework,
11857
+ manager,
11858
+ relativePath(root, candidate.root),
11859
+ candidate.configPath ? relativePath(candidate.root, candidate.configPath) : void 0,
11860
+ scripts
11861
+ ),
11862
+ warnings
11863
+ });
11864
+ }
11865
+ projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));
11866
+ return {
11867
+ root,
11868
+ projects,
11869
+ scannedDirectories: scan.scannedDirectories,
11870
+ scanTruncated: scan.truncated,
11871
+ warnings: scan.truncated ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`] : []
11872
+ };
11873
+ }
11874
+ var e2ePlanTool = {
11875
+ name: "e2e_plan",
11876
+ category: "Code Quality",
11877
+ description: "Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.",
11878
+ 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.",
11879
+ permission: "auto",
11880
+ mutating: false,
11881
+ riskTier: "safe",
11882
+ capabilities: ["fs.read"],
11883
+ icon: "test",
11884
+ timeoutMs: 3e4,
11885
+ inputSchema: {
11886
+ type: "object",
11887
+ properties: {
11888
+ cwd: { type: "string", description: "Directory inside the project to inspect." },
11889
+ framework: {
11890
+ type: "string",
11891
+ enum: ["all", "playwright", "cypress"],
11892
+ description: "Optional framework filter; defaults to all."
11893
+ },
11894
+ maxDepth: {
11895
+ type: "integer",
11896
+ minimum: 0,
11897
+ maximum: 8,
11898
+ description: "Maximum workspace discovery depth; defaults to 5."
11899
+ },
11900
+ includeSpecs: {
11901
+ type: "boolean",
11902
+ description: "Count and sample specs; defaults to true."
11903
+ }
11904
+ }
11905
+ },
11906
+ async execute(input, ctx, opts) {
11907
+ const root = await safeResolveReal(input.cwd ?? ".", ctx);
11908
+ return discoverE2EProjects(root, {
11909
+ framework: input.framework,
11910
+ maxDepth: input.maxDepth,
11911
+ includeSpecs: input.includeSpecs,
11912
+ signal: opts.signal
11913
+ });
11914
+ }
11915
+ };
11916
+
10266
11917
  // src/edit.ts
10267
- import * as fs17 from "node:fs/promises";
11918
+ import * as fs19 from "node:fs/promises";
10268
11919
  import {
10269
- atomicWrite,
11920
+ atomicWrite as atomicWrite2,
10270
11921
  detectNewlineStyle,
10271
11922
  normalizeToLf,
10272
11923
  ToolValidationError,
@@ -10479,7 +12130,7 @@ function prefixSimilarity(a, b) {
10479
12130
  }
10480
12131
 
10481
12132
  // src/_syntax-check.ts
10482
- import * as path19 from "node:path";
12133
+ import * as path23 from "node:path";
10483
12134
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
10484
12135
  var MAX_CHECK_CHARS = 15e5;
10485
12136
  var MAX_ERRORS = 5;
@@ -10503,15 +12154,15 @@ async function checkSyntax(filePath, content, previousContent) {
10503
12154
  return { errors, preExisting };
10504
12155
  }
10505
12156
  function isJsoncFile(filePath) {
10506
- const base = path19.basename(filePath).toLowerCase();
12157
+ const base = path23.basename(filePath).toLowerCase();
10507
12158
  if (base.endsWith(".jsonc")) return true;
10508
12159
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
10509
- const dir = path19.basename(path19.dirname(filePath)).toLowerCase();
12160
+ const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
10510
12161
  return dir === ".vscode";
10511
12162
  }
10512
12163
  async function parseErrors(filePath, content) {
10513
12164
  if (content.length > MAX_CHECK_CHARS) return void 0;
10514
- const ext = path19.extname(filePath).toLowerCase();
12165
+ const ext = path23.extname(filePath).toLowerCase();
10515
12166
  if (ext === ".json" || ext === ".jsonc") {
10516
12167
  try {
10517
12168
  JSON.parse(content);
@@ -10538,7 +12189,7 @@ async function parseErrors(filePath, content) {
10538
12189
  ts2.ScriptKind.JSX
10539
12190
  );
10540
12191
  const sourceFile = ts2.createSourceFile(
10541
- path19.basename(filePath),
12192
+ path23.basename(filePath),
10542
12193
  content,
10543
12194
  ts2.ScriptTarget.Latest,
10544
12195
  /* setParentNodes */
@@ -10612,7 +12263,7 @@ var editTool = {
10612
12263
  });
10613
12264
  }
10614
12265
  const absPath = await safeResolveReal(input.path, ctx);
10615
- const stat16 = await fs17.stat(absPath).catch((err) => {
12266
+ const stat18 = await fs19.stat(absPath).catch((err) => {
10616
12267
  if (err.code === "ENOENT") {
10617
12268
  throw new ToolValidationError({
10618
12269
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
@@ -10622,15 +12273,15 @@ var editTool = {
10622
12273
  }
10623
12274
  throw err;
10624
12275
  });
10625
- if (!stat16.isFile()) {
12276
+ if (!stat18.isFile()) {
10626
12277
  throw new ToolValidationError({
10627
12278
  message: `edit: "${input.path}" is not a regular file`,
10628
12279
  field: "path"
10629
12280
  });
10630
12281
  }
10631
12282
  const autoRead = !ctx.hasRead(absPath);
10632
- const original = await fs17.readFile(absPath, "utf8");
10633
- const updated = await fs17.stat(absPath);
12283
+ const original = await fs19.readFile(absPath, "utf8");
12284
+ const updated = await fs19.stat(absPath);
10634
12285
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
10635
12286
  const originalHash = sha256hex(original);
10636
12287
  const lastReadHash = ctx.lastReadHash?.(absPath);
@@ -10652,7 +12303,7 @@ var editTool = {
10652
12303
  });
10653
12304
  }
10654
12305
  }
10655
- if (autoRead && updated.mtimeMs > stat16.mtimeMs + mtimeTolerance) {
12306
+ if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
10656
12307
  throw new ToolValidationError({
10657
12308
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
10658
12309
  field: "path",
@@ -10734,8 +12385,8 @@ Compare this against your old_string and retry with the file's actual text.` : "
10734
12385
  }
10735
12386
  const newFile = toStyle(newFileLf, style);
10736
12387
  opts?.signal?.throwIfAborted();
10737
- await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
10738
- const written = await fs17.stat(absPath);
12388
+ await atomicWrite2(absPath, newFile, { mode: updated.mode & 511 });
12389
+ const written = await fs19.stat(absPath);
10739
12390
  ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
10740
12391
  ctx.session.recordFileChange({
10741
12392
  path: absPath,
@@ -11441,7 +13092,7 @@ var BLOCKED_ARG_PATTERNS = {
11441
13092
  // and glob patterns that could expand to dangerous targets.
11442
13093
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
11443
13094
  // `rm -rf /*` targets the filesystem root. All are blocked.
11444
- rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
13095
+ rm: [/^\//, /^[A-Za-z]:[\\/]/, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
11445
13096
  // npm/pnpm subcommands are checked separately below. Matching every arg here
11446
13097
  // over-blocked normal dev flows such as `pnpm vitest run ...`.
11447
13098
  npm: [],
@@ -11609,7 +13260,7 @@ var execTool = {
11609
13260
  }
11610
13261
  };
11611
13262
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
11612
- return new Promise((resolve12) => {
13263
+ return new Promise((resolve15) => {
11613
13264
  let stdout = "";
11614
13265
  let stderr = "";
11615
13266
  let killed = false;
@@ -11617,7 +13268,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
11617
13268
  const finish = (result) => {
11618
13269
  if (resolvedOnce.value) return;
11619
13270
  resolvedOnce.value = true;
11620
- resolve12(result);
13271
+ resolve15(result);
11621
13272
  };
11622
13273
  const startedAt = Date.now();
11623
13274
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
@@ -11726,13 +13377,13 @@ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValida
11726
13377
  import TurndownService from "turndown";
11727
13378
 
11728
13379
  // src/_fetch-guard.ts
11729
- import * as dns from "node:dns/promises";
11730
- import * as net from "node:net";
13380
+ import * as dns2 from "node:dns/promises";
13381
+ import * as net3 from "node:net";
11731
13382
  import {
11732
13383
  FetchError,
11733
13384
  ToolValidationError as ToolValidationError2,
11734
- isPrivateIPv4,
11735
- isPrivateIPv6
13385
+ isPrivateIPv4 as isPrivateIPv42,
13386
+ isPrivateIPv6 as isPrivateIPv62
11736
13387
  } from "@wrongstack/core";
11737
13388
  import { Agent, fetch as undiciFetch } from "undici";
11738
13389
  var nativeGlobalFetch = globalThis.fetch;
@@ -11743,13 +13394,13 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
11743
13394
  );
11744
13395
  }
11745
13396
  function guardedLookup(hostname4, options, callback) {
11746
- dns.lookup(hostname4, { all: true }).then((records) => {
13397
+ dns2.lookup(hostname4, { all: true }).then((records) => {
11747
13398
  const family = options?.family;
11748
13399
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
11749
13400
  const list = byFamily.length > 0 ? byFamily : records;
11750
13401
  if (!ALLOW_PRIVATE) {
11751
13402
  for (const r of list) {
11752
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
13403
+ const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
11753
13404
  if (bad) {
11754
13405
  callback(
11755
13406
  Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
@@ -11854,16 +13505,16 @@ async function assertNotPrivate(hostname4) {
11854
13505
  field: "url"
11855
13506
  });
11856
13507
  }
11857
- const ipVersion = net.isIP(host);
13508
+ const ipVersion = net3.isIP(host);
11858
13509
  if (ipVersion === 4) {
11859
- if (isPrivateIPv4(host)) {
13510
+ if (isPrivateIPv42(host)) {
11860
13511
  throw new ToolValidationError2({
11861
13512
  message: `fetch: blocked private/loopback address "${host}"`,
11862
13513
  field: "url"
11863
13514
  });
11864
13515
  }
11865
13516
  } else if (ipVersion === 6) {
11866
- if (isPrivateIPv6(host)) {
13517
+ if (isPrivateIPv62(host)) {
11867
13518
  throw new ToolValidationError2({
11868
13519
  message: `fetch: blocked private/loopback address "${host}"`,
11869
13520
  field: "url"
@@ -11871,9 +13522,9 @@ async function assertNotPrivate(hostname4) {
11871
13522
  }
11872
13523
  } else {
11873
13524
  try {
11874
- const records = await dns.lookup(host, { all: true });
13525
+ const records = await dns2.lookup(host, { all: true });
11875
13526
  for (const r of records) {
11876
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
13527
+ const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
11877
13528
  if (bad) {
11878
13529
  throw new ToolValidationError2({
11879
13530
  message: `fetch: resolved to private address ${r.address}`,
@@ -12212,13 +13863,13 @@ var formatTool = {
12212
13863
  }
12213
13864
  };
12214
13865
  async function detectFixer(cwd) {
12215
- const { stat: stat16 } = await import("node:fs/promises");
13866
+ const { stat: stat18 } = await import("node:fs/promises");
12216
13867
  try {
12217
- await stat16(`${cwd}/biome.json`);
13868
+ await stat18(`${cwd}/biome.json`);
12218
13869
  return "biome";
12219
13870
  } catch {
12220
13871
  try {
12221
- await stat16(`${cwd}/.prettierrc`);
13872
+ await stat18(`${cwd}/.prettierrc`);
12222
13873
  return "prettier";
12223
13874
  } catch {
12224
13875
  return "biome";
@@ -12230,7 +13881,7 @@ async function detectFixer(cwd) {
12230
13881
  init_util();
12231
13882
  import { spawn as spawn9 } from "node:child_process";
12232
13883
  import { statSync as statSync3 } from "node:fs";
12233
- import { dirname as dirname9, resolve as resolve8, sep as sep3 } from "node:path";
13884
+ import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
12234
13885
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
12235
13886
  var TIMEOUT_MS2 = 3e4;
12236
13887
  var MAX_OUTPUT3 = 1e5;
@@ -12373,9 +14024,9 @@ function validateWorktreeInput(input, projectRoot) {
12373
14024
  return reject(`unsafe worktree path: ${input.worktreePath}`);
12374
14025
  }
12375
14026
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
12376
- const root = resolve8(projectRoot);
12377
- const abs = resolve8(root, input.worktreePath);
12378
- if (abs !== root && !abs.startsWith(root + sep3)) {
14027
+ const root = resolve11(projectRoot);
14028
+ const abs = resolve11(root, input.worktreePath);
14029
+ if (abs !== root && !abs.startsWith(root + sep4)) {
12379
14030
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
12380
14031
  }
12381
14032
  }
@@ -12386,12 +14037,12 @@ function findGitDir2(cwd, projectRoot) {
12386
14037
  let dir = cwd;
12387
14038
  for (let i = 0; i < 20; i++) {
12388
14039
  try {
12389
- const stat16 = statSync3(`${dir}/.git`);
12390
- if (stat16.isDirectory() || stat16.isFile()) return dir;
14040
+ const stat18 = statSync3(`${dir}/.git`);
14041
+ if (stat18.isDirectory() || stat18.isFile()) return dir;
12391
14042
  } catch {
12392
14043
  }
12393
14044
  if (dir === root) break;
12394
- const parent = dirname9(dir);
14045
+ const parent = dirname12(dir);
12395
14046
  if (parent === dir) break;
12396
14047
  dir = parent;
12397
14048
  }
@@ -12468,7 +14119,7 @@ function buildArgs(input) {
12468
14119
  }
12469
14120
  }
12470
14121
  function runGit2(args, cwd, signal) {
12471
- return new Promise((resolve12) => {
14122
+ return new Promise((resolve15) => {
12472
14123
  let stdout = "";
12473
14124
  let stderr = "";
12474
14125
  const child = spawn9("git", args, {
@@ -12489,7 +14140,7 @@ function runGit2(args, cwd, signal) {
12489
14140
  }
12490
14141
  });
12491
14142
  child.on("error", (err) => {
12492
- resolve12({
14143
+ resolve15({
12493
14144
  command: args[0],
12494
14145
  stdout: normalizeCommandOutput(stdout),
12495
14146
  stderr: err.message,
@@ -12498,7 +14149,7 @@ function runGit2(args, cwd, signal) {
12498
14149
  });
12499
14150
  });
12500
14151
  child.on("close", (code) => {
12501
- resolve12({
14152
+ resolve15({
12502
14153
  command: args[0],
12503
14154
  stdout: normalizeCommandOutput(stdout),
12504
14155
  stderr: normalizeCommandOutput(stderr),
@@ -12510,8 +14161,8 @@ function runGit2(args, cwd, signal) {
12510
14161
  }
12511
14162
 
12512
14163
  // src/glob.ts
12513
- import * as fs18 from "node:fs/promises";
12514
- import * as path20 from "node:path";
14164
+ import * as fs20 from "node:fs/promises";
14165
+ import * as path24 from "node:path";
12515
14166
  import { compileGlob as compileGlob3 } from "@wrongstack/core";
12516
14167
 
12517
14168
  // src/_concurrency.ts
@@ -12583,7 +14234,7 @@ var globTool = {
12583
14234
  return;
12584
14235
  }
12585
14236
  try {
12586
- const st = await fs18.stat(full);
14237
+ const st = await fs20.stat(full);
12587
14238
  if (truncated || results.length >= limit) {
12588
14239
  truncated = true;
12589
14240
  return;
@@ -12604,7 +14255,7 @@ var globTool = {
12604
14255
  }
12605
14256
  let entries;
12606
14257
  try {
12607
- entries = await fs18.readdir(dir, { withFileTypes: true });
14258
+ entries = await fs20.readdir(dir, { withFileTypes: true });
12608
14259
  } catch {
12609
14260
  return;
12610
14261
  }
@@ -12615,7 +14266,7 @@ var globTool = {
12615
14266
  if (DEFAULT_IGNORE2.includes(name)) continue;
12616
14267
  if (ignored.includes(name)) continue;
12617
14268
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
12618
- const full = path20.join(dir, name);
14269
+ const full = path24.join(dir, name);
12619
14270
  if (e.isDirectory()) {
12620
14271
  subdirs.push({ full, rel });
12621
14272
  } else if (e.isFile()) {
@@ -12628,13 +14279,13 @@ var globTool = {
12628
14279
  }
12629
14280
  } else if (e.isSymbolicLink()) {
12630
14281
  try {
12631
- const st = await fs18.stat(full);
14282
+ const st = await fs20.stat(full);
12632
14283
  if (st.isDirectory()) {
12633
- const real = await fs18.realpath(full);
14284
+ const real = await fs20.realpath(full);
12634
14285
  await assertRealInsideRoot(real, ctx);
12635
14286
  subdirs.push({ full, rel });
12636
14287
  } else if (st.isFile()) {
12637
- const real = await fs18.realpath(full);
14288
+ const real = await fs20.realpath(full);
12638
14289
  await assertRealInsideRoot(real, ctx);
12639
14290
  re.lastIndex = 0;
12640
14291
  const relMatch = re.test(rel);
@@ -12659,7 +14310,7 @@ var globTool = {
12659
14310
  };
12660
14311
  async function readGitignore(dir) {
12661
14312
  try {
12662
- const raw = await fs18.readFile(path20.join(dir, ".gitignore"), "utf8");
14313
+ const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
12663
14314
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
12664
14315
  } catch {
12665
14316
  return [];
@@ -12669,8 +14320,8 @@ async function readGitignore(dir) {
12669
14320
  // src/grep.ts
12670
14321
  import { expectDefined as expectDefined7 } from "@wrongstack/core";
12671
14322
  import { spawn as spawn10 } from "node:child_process";
12672
- import * as fs19 from "node:fs/promises";
12673
- import * as path21 from "node:path";
14323
+ import * as fs21 from "node:fs/promises";
14324
+ import * as path25 from "node:path";
12674
14325
  import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
12675
14326
 
12676
14327
  // src/_regex.ts
@@ -12815,13 +14466,13 @@ var grepTool = {
12815
14466
  }
12816
14467
  };
12817
14468
  async function detectRg(signal) {
12818
- return new Promise((resolve12) => {
14469
+ return new Promise((resolve15) => {
12819
14470
  try {
12820
14471
  const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
12821
- p.on("error", () => resolve12(false));
12822
- p.on("close", (code) => resolve12(code === 0));
14472
+ p.on("error", () => resolve15(false));
14473
+ p.on("close", (code) => resolve15(code === 0));
12823
14474
  } catch {
12824
- resolve12(false);
14475
+ resolve15(false);
12825
14476
  }
12826
14477
  });
12827
14478
  }
@@ -12972,9 +14623,9 @@ async function runNative(input, base, mode, limit, signal) {
12972
14623
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
12973
14624
  if (globRe) globRe.lastIndex = 0;
12974
14625
  try {
12975
- const stat16 = await fs19.stat(full);
12976
- if (!stat16.isFile() || stat16.size > maxBytes || stopped || signal.aborted) return;
12977
- const file = await fs19.open(full, "r");
14626
+ const stat18 = await fs21.stat(full);
14627
+ if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
14628
+ const file = await fs21.open(full, "r");
12978
14629
  try {
12979
14630
  let bytesReadTotal = 0;
12980
14631
  let lineNumber = 0;
@@ -13053,7 +14704,7 @@ async function runNative(input, base, mode, limit, signal) {
13053
14704
  if (stopped || signal.aborted) return;
13054
14705
  let entries;
13055
14706
  try {
13056
- entries = await fs19.readdir(dir, { withFileTypes: true });
14707
+ entries = await fs21.readdir(dir, { withFileTypes: true });
13057
14708
  } catch {
13058
14709
  return;
13059
14710
  }
@@ -13063,7 +14714,7 @@ async function runNative(input, base, mode, limit, signal) {
13063
14714
  if (stopped) return;
13064
14715
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
13065
14716
  if (e.isSymbolicLink()) continue;
13066
- const full = path21.join(dir, e.name);
14717
+ const full = path25.join(dir, e.name);
13067
14718
  if (e.isDirectory()) {
13068
14719
  subdirs.push(full);
13069
14720
  } else if (e.isFile()) {
@@ -13086,7 +14737,7 @@ async function runNative(input, base, mode, limit, signal) {
13086
14737
  init_spawn_stream();
13087
14738
  init_util();
13088
14739
  init_legacy_bridge();
13089
- import { join as join18 } from "node:path";
14740
+ import { join as join21 } from "node:path";
13090
14741
  import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
13091
14742
  var installTool = {
13092
14743
  name: "install",
@@ -13263,16 +14914,16 @@ function resolveManifestPath(cwd, pkgManager) {
13263
14914
  case "pnpm":
13264
14915
  case "yarn":
13265
14916
  case "npm":
13266
- return join18(cwd, "package.json");
14917
+ return join21(cwd, "package.json");
13267
14918
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
13268
14919
  default:
13269
- return join18(cwd, "package.json");
14920
+ return join21(cwd, "package.json");
13270
14921
  }
13271
14922
  }
13272
14923
 
13273
14924
  // src/json.ts
13274
14925
  init_util();
13275
- import * as fs20 from "node:fs/promises";
14926
+ import * as fs22 from "node:fs/promises";
13276
14927
  import { deepMerge as deepMergeCore } from "@wrongstack/core";
13277
14928
  var jsonTool = {
13278
14929
  name: "json",
@@ -13347,7 +14998,7 @@ async function executeParse(input, ctx) {
13347
14998
  let raw;
13348
14999
  if (input.file) {
13349
15000
  try {
13350
- raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15001
+ raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13351
15002
  } catch {
13352
15003
  return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
13353
15004
  }
@@ -13405,7 +15056,7 @@ async function executeQuery(input, ctx) {
13405
15056
  let parsed;
13406
15057
  if (input.file) {
13407
15058
  try {
13408
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15059
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13409
15060
  parsed = JSON.parse(raw);
13410
15061
  } catch {
13411
15062
  return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
@@ -13447,7 +15098,7 @@ async function executeValidate(input, ctx) {
13447
15098
  let parsed;
13448
15099
  if (input.file) {
13449
15100
  try {
13450
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15101
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13451
15102
  parsed = JSON.parse(raw);
13452
15103
  } catch {
13453
15104
  return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
@@ -13489,7 +15140,7 @@ async function executeTransform(input, ctx) {
13489
15140
  let parsed;
13490
15141
  if (input.file) {
13491
15142
  try {
13492
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15143
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13493
15144
  parsed = JSON.parse(raw);
13494
15145
  } catch {
13495
15146
  return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
@@ -13651,56 +15302,56 @@ function jmespathSearch(data, query) {
13651
15302
  }
13652
15303
  function validateJsonSchema(data, schema) {
13653
15304
  const errors = [];
13654
- function check(value, s, path29) {
15305
+ function check(value, s, path33) {
13655
15306
  if (s["type"]) {
13656
15307
  const expectedType = s["type"];
13657
15308
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
13658
15309
  if (expectedType === "integer") {
13659
- if (!Number.isInteger(value)) errors.push(`${path29}: expected integer, got ${actualType}`);
15310
+ if (!Number.isInteger(value)) errors.push(`${path33}: expected integer, got ${actualType}`);
13660
15311
  } else if (expectedType !== actualType) {
13661
- errors.push(`${path29}: expected ${expectedType}, got ${actualType}`);
15312
+ errors.push(`${path33}: expected ${expectedType}, got ${actualType}`);
13662
15313
  }
13663
15314
  }
13664
15315
  if (typeof value === "string" && s["format"] === "uri" && value) {
13665
15316
  try {
13666
15317
  new URL(value);
13667
15318
  } catch {
13668
- errors.push(`${path29}: not a valid URI`);
15319
+ errors.push(`${path33}: not a valid URI`);
13669
15320
  }
13670
15321
  }
13671
15322
  if (typeof value === "string" && s["pattern"]) {
13672
15323
  const re = new RegExp(s["pattern"]);
13673
- if (!re.test(value)) errors.push(`${path29}: does not match pattern ${s["pattern"]}`);
15324
+ if (!re.test(value)) errors.push(`${path33}: does not match pattern ${s["pattern"]}`);
13674
15325
  }
13675
15326
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
13676
- errors.push(`${path29}: string too short (min ${s["minLength"]})`);
15327
+ errors.push(`${path33}: string too short (min ${s["minLength"]})`);
13677
15328
  }
13678
15329
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
13679
- errors.push(`${path29}: string too long (max ${s["maxLength"]})`);
15330
+ errors.push(`${path33}: string too long (max ${s["maxLength"]})`);
13680
15331
  }
13681
15332
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
13682
- errors.push(`${path29}: below minimum ${s["minimum"]}`);
15333
+ errors.push(`${path33}: below minimum ${s["minimum"]}`);
13683
15334
  }
13684
15335
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
13685
- errors.push(`${path29}: above maximum ${s["maximum"]}`);
15336
+ errors.push(`${path33}: above maximum ${s["maximum"]}`);
13686
15337
  }
13687
15338
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
13688
15339
  for (let i = 0; i < value.length; i++) {
13689
- check(value[i], s["items"], `${path29}[${i}]`);
15340
+ check(value[i], s["items"], `${path33}[${i}]`);
13690
15341
  }
13691
15342
  }
13692
15343
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
13693
15344
  const props = s["properties"];
13694
15345
  for (const [k, propSchema] of Object.entries(props)) {
13695
- check(value[k], propSchema, `${path29}.${k}`);
15346
+ check(value[k], propSchema, `${path33}.${k}`);
13696
15347
  }
13697
15348
  }
13698
15349
  }
13699
15350
  check(data, schema, "$");
13700
15351
  return { valid: errors.length === 0, errors };
13701
15352
  }
13702
- function simpleQuery(data, path29) {
13703
- const parts = path29.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
15353
+ function simpleQuery(data, path33) {
15354
+ const parts = path33.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
13704
15355
  let current = data;
13705
15356
  for (const part of parts) {
13706
15357
  if (current === null || current === void 0) return void 0;
@@ -14704,11 +16355,11 @@ var lintTool = {
14704
16355
  }
14705
16356
  };
14706
16357
  async function detectLinter(cwd) {
14707
- const { stat: stat16 } = await import("node:fs/promises");
16358
+ const { stat: stat18 } = await import("node:fs/promises");
14708
16359
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
14709
16360
  for (const f of checks) {
14710
16361
  try {
14711
- await stat16(`${cwd}/${f}`);
16362
+ await stat18(`${cwd}/${f}`);
14712
16363
  if (f.includes("biome")) return "biome";
14713
16364
  if (f.includes("eslint")) return "eslint";
14714
16365
  if (f.includes("tslint")) return "tslint";
@@ -14808,7 +16459,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14808
16459
  };
14809
16460
  }
14810
16461
  args.push("--timestamps", service);
14811
- return new Promise((resolve12) => {
16462
+ return new Promise((resolve15) => {
14812
16463
  let stdout = "";
14813
16464
  let stderr = "";
14814
16465
  const MAX = 2e5;
@@ -14824,7 +16475,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14824
16475
  if (settled) return;
14825
16476
  settled = true;
14826
16477
  clearTimeout(timer);
14827
- resolve12(result);
16478
+ resolve15(result);
14828
16479
  };
14829
16480
  const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
14830
16481
  const timer = setTimeout(() => {
@@ -14859,16 +16510,16 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14859
16510
  }
14860
16511
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
14861
16512
  var MAX_TAIL_LINES = 1e5;
14862
- async function fileLogs(path29, lines, filterRe, stream) {
16513
+ async function fileLogs(path33, lines, filterRe, stream) {
14863
16514
  const { createInterface } = await import("node:readline");
14864
- const { createReadStream } = await import("node:fs");
16515
+ const { createReadStream: createReadStream2 } = await import("node:fs");
14865
16516
  const entries = [];
14866
16517
  const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
14867
16518
  const window = new Array(effLines);
14868
16519
  let writeIdx = 0;
14869
16520
  let totalLines = 0;
14870
16521
  const rl = createInterface({
14871
- input: createReadStream(path29),
16522
+ input: createReadStream2(path33),
14872
16523
  crlfDelay: Number.POSITIVE_INFINITY
14873
16524
  });
14874
16525
  for await (const line of rl) {
@@ -14889,7 +16540,7 @@ async function fileLogs(path29, lines, filterRe, stream) {
14889
16540
  if (parsed) entries.push(parsed);
14890
16541
  }
14891
16542
  return {
14892
- source: path29,
16543
+ source: path33,
14893
16544
  entries,
14894
16545
  total: entries.length,
14895
16546
  truncated: totalLines > effLines,
@@ -15043,7 +16694,7 @@ var outdatedTool = {
15043
16694
  }
15044
16695
  };
15045
16696
  function runOutdated(manager, args, cwd, signal) {
15046
- return new Promise((resolve12) => {
16697
+ return new Promise((resolve15) => {
15047
16698
  let stdout = "";
15048
16699
  let stderr = "";
15049
16700
  const MAX = 1e5;
@@ -15068,10 +16719,10 @@ function runOutdated(manager, args, cwd, signal) {
15068
16719
  });
15069
16720
  child.on("close", (code) => {
15070
16721
  const result = parseOutdatedOutput(stdout, code ?? 0);
15071
- resolve12(result);
16722
+ resolve15(result);
15072
16723
  });
15073
16724
  child.on("error", (e) => {
15074
- resolve12({
16725
+ resolve15({
15075
16726
  exit_code: 1,
15076
16727
  packages: [],
15077
16728
  total: 0,
@@ -15081,9 +16732,9 @@ function runOutdated(manager, args, cwd, signal) {
15081
16732
  });
15082
16733
  });
15083
16734
  }
15084
- function parseOutdatedOutput(json, exitCode) {
16735
+ function parseOutdatedOutput(json2, exitCode) {
15085
16736
  const packages = [];
15086
- if (!json) {
16737
+ if (!json2) {
15087
16738
  return {
15088
16739
  exit_code: exitCode,
15089
16740
  packages: [],
@@ -15093,7 +16744,7 @@ function parseOutdatedOutput(json, exitCode) {
15093
16744
  };
15094
16745
  }
15095
16746
  try {
15096
- const data = JSON.parse(json);
16747
+ const data = JSON.parse(json2);
15097
16748
  for (const name of Object.keys(data)) {
15098
16749
  const info = data[name];
15099
16750
  packages.push({
@@ -15111,17 +16762,17 @@ function parseOutdatedOutput(json, exitCode) {
15111
16762
  exit_code: exitCode,
15112
16763
  packages,
15113
16764
  total: packages.length,
15114
- output: json,
15115
- truncated: json.length >= 1e5
16765
+ output: json2,
16766
+ truncated: json2.length >= 1e5
15116
16767
  };
15117
16768
  }
15118
16769
 
15119
16770
  // src/patch.ts
15120
16771
  init_util();
15121
16772
  import { spawn as spawn13 } from "node:child_process";
15122
- import * as fs21 from "node:fs/promises";
16773
+ import * as fs23 from "node:fs/promises";
15123
16774
  import * as os7 from "node:os";
15124
- import * as path22 from "node:path";
16775
+ import * as path26 from "node:path";
15125
16776
  import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
15126
16777
  var patchTool = {
15127
16778
  name: "patch",
@@ -15157,9 +16808,9 @@ var patchTool = {
15157
16808
  for (const t of targets) {
15158
16809
  const stripped = stripPathComponents(t, strip);
15159
16810
  if (!stripped) continue;
15160
- const candidate = path22.resolve(dir, stripped);
15161
- const rel = path22.relative(ctx.projectRoot, candidate);
15162
- if (rel.startsWith("..") || path22.isAbsolute(rel)) {
16811
+ const candidate = path26.resolve(dir, stripped);
16812
+ const rel = path26.relative(ctx.projectRoot, candidate);
16813
+ if (rel.startsWith("..") || path26.isAbsolute(rel)) {
15163
16814
  return {
15164
16815
  applied: 0,
15165
16816
  rejected: 1,
@@ -15176,12 +16827,12 @@ var patchTool = {
15176
16827
  beforeContents.set(target, await readTextForTracking(target));
15177
16828
  }
15178
16829
  }
15179
- const tmpDir = await fs21.mkdtemp(path22.join(os7.tmpdir(), ".wstack_patch_"));
16830
+ const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
15180
16831
  try {
15181
- await fs21.chmod(tmpDir, 448).catch(() => {
16832
+ await fs23.chmod(tmpDir, 448).catch(() => {
15182
16833
  });
15183
- const patchFile = path22.join(tmpDir, "in.diff");
15184
- await fs21.writeFile(patchFile, input.patch, { mode: 384 });
16834
+ const patchFile = path26.join(tmpDir, "in.diff");
16835
+ await fs23.writeFile(patchFile, input.patch, { mode: 384 });
15185
16836
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
15186
16837
  const result = await runPatch(args, dir, opts.signal);
15187
16838
  if (result.exitCode !== 0 && !dryRun) {
@@ -15199,8 +16850,8 @@ var patchTool = {
15199
16850
  const before = beforeContents.get(target) ?? null;
15200
16851
  const after = await readTextForTracking(target);
15201
16852
  if (after === null || after === before) continue;
15202
- const stat16 = await fs21.stat(target).catch(() => null);
15203
- if (stat16) ctx.recordRead?.(target, stat16.mtimeMs, "write", sha256hex(after));
16853
+ const stat18 = await fs23.stat(target).catch(() => null);
16854
+ if (stat18) ctx.recordRead?.(target, stat18.mtimeMs, "write", sha256hex(after));
15204
16855
  ctx.session?.recordFileChange?.({
15205
16856
  path: target,
15206
16857
  action: before === null ? "created" : "modified",
@@ -15217,7 +16868,7 @@ var patchTool = {
15217
16868
  message: result.stdout || "patch applied"
15218
16869
  };
15219
16870
  } finally {
15220
- await fs21.rm(tmpDir, { recursive: true, force: true }).catch(() => {
16871
+ await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
15221
16872
  });
15222
16873
  }
15223
16874
  }
@@ -15225,9 +16876,9 @@ var patchTool = {
15225
16876
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
15226
16877
  async function readTextForTracking(absPath) {
15227
16878
  try {
15228
- const stat16 = await fs21.stat(absPath);
15229
- if (!stat16.isFile() || stat16.size > MAX_TRACKING_BYTES) return null;
15230
- const buf = await fs21.readFile(absPath);
16879
+ const stat18 = await fs23.stat(absPath);
16880
+ if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
16881
+ const buf = await fs23.readFile(absPath);
15231
16882
  if (buf.includes(0)) return null;
15232
16883
  return buf.toString("utf8");
15233
16884
  } catch {
@@ -15252,7 +16903,7 @@ function stripPathComponents(p, strip) {
15252
16903
  return parts.slice(strip).join("/");
15253
16904
  }
15254
16905
  function runPatch(args, cwd, signal) {
15255
- return new Promise((resolve12) => {
16906
+ return new Promise((resolve15) => {
15256
16907
  let stdout = "";
15257
16908
  let stderr = "";
15258
16909
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
@@ -15263,8 +16914,8 @@ function runPatch(args, cwd, signal) {
15263
16914
  child.stderr?.on("data", (c) => {
15264
16915
  stderr += c.toString();
15265
16916
  });
15266
- child.on("close", (code) => resolve12({ exitCode: code ?? 1, stdout, stderr }));
15267
- child.on("error", (e) => resolve12({ exitCode: 1, stdout: "", stderr: e.message }));
16917
+ child.on("close", (code) => resolve15({ exitCode: code ?? 1, stdout, stderr }));
16918
+ child.on("error", (e) => resolve15({ exitCode: 1, stdout: "", stderr: e.message }));
15268
16919
  });
15269
16920
  }
15270
16921
  function extractPatchedFiles(output) {
@@ -15541,13 +17192,13 @@ ${formatTaskList(taskFile.tasks)}`
15541
17192
  }
15542
17193
  };
15543
17194
  function mkResult(plan, ok, message, todos) {
15544
- const open2 = plan.items.filter((i) => i.status !== "done").length;
17195
+ const open3 = plan.items.filter((i) => i.status !== "done").length;
15545
17196
  const result = {
15546
17197
  ok,
15547
17198
  message,
15548
17199
  plan: formatPlan(plan),
15549
17200
  count: plan.items.length,
15550
- open: open2
17201
+ open: open3
15551
17202
  };
15552
17203
  if (todos !== void 0) result.todos = todos;
15553
17204
  return result;
@@ -15555,7 +17206,7 @@ function mkResult(plan, ok, message, todos) {
15555
17206
 
15556
17207
  // src/read.ts
15557
17208
  init_util();
15558
- import * as fs22 from "node:fs/promises";
17209
+ import * as fs24 from "node:fs/promises";
15559
17210
  import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
15560
17211
  var MAX_BYTES2 = 5 * 1024 * 1024;
15561
17212
  var readTool = {
@@ -15604,9 +17255,9 @@ var readTool = {
15604
17255
  });
15605
17256
  }
15606
17257
  const absPath = await safeResolveReal(input.path, ctx);
15607
- let stat16;
17258
+ let stat18;
15608
17259
  try {
15609
- stat16 = await fs22.stat(absPath);
17260
+ stat18 = await fs24.stat(absPath);
15610
17261
  } catch (err) {
15611
17262
  const code = err.code;
15612
17263
  if (code === "ENOENT") {
@@ -15625,7 +17276,7 @@ var readTool = {
15625
17276
  cause: err
15626
17277
  });
15627
17278
  }
15628
- if (!stat16.isFile()) {
17279
+ if (!stat18.isFile()) {
15629
17280
  throw new FsError({
15630
17281
  message: `read: "${input.path}" is not a regular file`,
15631
17282
  code: "FS_READ_FAILED",
@@ -15633,22 +17284,22 @@ var readTool = {
15633
17284
  context: { reason: "not-a-regular-file" }
15634
17285
  });
15635
17286
  }
15636
- if (stat16.size > MAX_BYTES2) {
17287
+ if (stat18.size > MAX_BYTES2) {
15637
17288
  throw new FsError({
15638
- message: `read: file too large (${stat16.size} bytes, limit ${MAX_BYTES2})`,
17289
+ message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
15639
17290
  code: "FS_READ_FAILED",
15640
17291
  path: absPath,
15641
- context: { size: stat16.size, limit: MAX_BYTES2, reason: "too-large" }
17292
+ context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
15642
17293
  });
15643
17294
  }
15644
17295
  const offset = Math.max(1, input.offset ?? 1);
15645
17296
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
15646
17297
  const prior = getReadRangeRecord(ctx, absPath);
15647
17298
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
15648
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat16.mtimeMs, offset, requestedEnd)) {
15649
- ctx.recordRead(absPath, stat16.mtimeMs);
17299
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
17300
+ ctx.recordRead(absPath, stat18.mtimeMs);
15650
17301
  return {
15651
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat16.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
17302
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
15652
17303
  total_lines: prior.totalLines,
15653
17304
  encoding: "utf8",
15654
17305
  truncated: requestedEnd < prior.totalLines,
@@ -15656,7 +17307,7 @@ var readTool = {
15656
17307
  note: "Repeated read suppressed to save tokens."
15657
17308
  };
15658
17309
  }
15659
- const buf = await fs22.readFile(absPath);
17310
+ const buf = await fs24.readFile(absPath);
15660
17311
  if (isBinaryBuffer(buf)) {
15661
17312
  throw new Error(`read: "${input.path}" appears to be binary`);
15662
17313
  }
@@ -15665,10 +17316,10 @@ var readTool = {
15665
17316
  const allLines = text.split(/\r\n|\r|\n/);
15666
17317
  const total = allLines.length;
15667
17318
  if (input.mode === "summary") {
15668
- ctx.recordRead(absPath, stat16.mtimeMs, "user", contentHash);
15669
- rememberReadRange(ctx, absPath, stat16.mtimeMs, total, 1, Math.min(total, 200));
17319
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
17320
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
15670
17321
  return {
15671
- text: summarizeFile(input.path, stat16.size, allLines),
17322
+ text: summarizeFile(input.path, stat18.size, allLines),
15672
17323
  total_lines: total,
15673
17324
  encoding: "utf8",
15674
17325
  truncated: total > 200,
@@ -15676,13 +17327,13 @@ var readTool = {
15676
17327
  };
15677
17328
  }
15678
17329
  if (limit === 0) {
15679
- ctx.recordRead(absPath, stat16.mtimeMs, "user", contentHash);
15680
- rememberReadRange(ctx, absPath, stat16.mtimeMs, total, 1, 0);
17330
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
17331
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
15681
17332
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
15682
17333
  }
15683
17334
  if (offset > total) {
15684
- ctx.recordRead(absPath, stat16.mtimeMs, "user", contentHash);
15685
- rememberReadRange(ctx, absPath, stat16.mtimeMs, total, total + 1, total + 1);
17335
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
17336
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
15686
17337
  return {
15687
17338
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
15688
17339
  total_lines: total,
@@ -15694,8 +17345,8 @@ var readTool = {
15694
17345
  const truncated = offset - 1 + slice.length < total;
15695
17346
  const width = String(offset + slice.length - 1).length;
15696
17347
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
15697
- ctx.recordRead(absPath, stat16.mtimeMs, "user", contentHash);
15698
- rememberReadRange(ctx, absPath, stat16.mtimeMs, total, offset, offset + slice.length - 1);
17348
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
17349
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
15699
17350
  return {
15700
17351
  text: numbered,
15701
17352
  total_lines: total,
@@ -15764,10 +17415,10 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
15764
17415
  // src/replace.ts
15765
17416
  import { expectDefined as expectDefined8 } from "@wrongstack/core";
15766
17417
  import { spawn as spawn14 } from "node:child_process";
15767
- import * as fs23 from "node:fs/promises";
15768
- import * as path23 from "node:path";
17418
+ import * as fs25 from "node:fs/promises";
17419
+ import * as path27 from "node:path";
15769
17420
  import {
15770
- atomicWrite as atomicWrite2,
17421
+ atomicWrite as atomicWrite3,
15771
17422
  buildChildEnv as buildChildEnv9,
15772
17423
  compileGlob as compileGlob5,
15773
17424
  detectNewlineStyle as detectNewlineStyle2,
@@ -15838,11 +17489,11 @@ var replaceTool = {
15838
17489
  const dryRun = input.dry_run ?? true;
15839
17490
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
15840
17491
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
15841
- const realRoot = await fs23.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
17492
+ const realRoot = await fs25.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
15842
17493
  const results = [];
15843
17494
  let totalReplacements = 0;
15844
17495
  for (const absPath of fileList) {
15845
- const lstat2 = await fs23.lstat(absPath).catch((err) => {
17496
+ const lstat2 = await fs25.lstat(absPath).catch((err) => {
15846
17497
  if (err.code === "ENOENT") return null;
15847
17498
  throw err;
15848
17499
  });
@@ -15850,17 +17501,17 @@ var replaceTool = {
15850
17501
  if (lstat2.isSymbolicLink()) continue;
15851
17502
  let realPath;
15852
17503
  try {
15853
- realPath = await fs23.realpath(absPath);
17504
+ realPath = await fs25.realpath(absPath);
15854
17505
  } catch {
15855
17506
  continue;
15856
17507
  }
15857
- const rel = path23.relative(realRoot, realPath);
15858
- if (rel.startsWith("..") || path23.isAbsolute(rel)) continue;
15859
- const stat16 = await fs23.stat(realPath).catch(() => null);
15860
- if (!stat16?.isFile()) continue;
17508
+ const rel = path27.relative(realRoot, realPath);
17509
+ if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
17510
+ const stat18 = await fs25.stat(realPath).catch(() => null);
17511
+ if (!stat18?.isFile()) continue;
15861
17512
  let content;
15862
17513
  try {
15863
- const buf = await fs23.readFile(realPath);
17514
+ const buf = await fs25.readFile(realPath);
15864
17515
  if (isBinaryBuffer(buf)) continue;
15865
17516
  content = buf.toString("utf8");
15866
17517
  } catch {
@@ -15882,8 +17533,8 @@ var replaceTool = {
15882
17533
  totalReplacements += count;
15883
17534
  if (!dryRun) {
15884
17535
  const newContent = toStyle2(newContentLf, style);
15885
- await atomicWrite2(realPath, newContent, { mode: stat16.mode & 511 });
15886
- const written = await fs23.stat(realPath).catch(() => null);
17536
+ await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
17537
+ const written = await fs25.stat(realPath).catch(() => null);
15887
17538
  if (written) {
15888
17539
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
15889
17540
  }
@@ -15922,8 +17573,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
15922
17573
  const resolved = [];
15923
17574
  for (const p of parts) {
15924
17575
  const absPath = safeResolve(p, ctx);
15925
- const stat16 = await fs23.stat(absPath).catch(() => null);
15926
- if (stat16?.isFile()) {
17576
+ const stat18 = await fs25.stat(absPath).catch(() => null);
17577
+ if (stat18?.isFile()) {
15927
17578
  resolved.push(absPath);
15928
17579
  }
15929
17580
  }
@@ -15941,13 +17592,13 @@ async function globFiles(pattern, base, extraGlob) {
15941
17592
  return await globNative(pattern, base, extraGlob);
15942
17593
  }
15943
17594
  function checkRg() {
15944
- return new Promise((resolve12) => {
17595
+ return new Promise((resolve15) => {
15945
17596
  try {
15946
17597
  const p = spawn14("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
15947
- p.on("error", () => resolve12(false));
15948
- p.on("close", (code) => resolve12(code === 0));
17598
+ p.on("error", () => resolve15(false));
17599
+ p.on("close", (code) => resolve15(code === 0));
15949
17600
  } catch {
15950
- resolve12(false);
17601
+ resolve15(false);
15951
17602
  }
15952
17603
  });
15953
17604
  }
@@ -15964,10 +17615,10 @@ function spawnRgFind(pattern, base) {
15964
17615
  buf += chunk.toString();
15965
17616
  });
15966
17617
  return {
15967
- promise: new Promise((resolve12, reject) => {
17618
+ promise: new Promise((resolve15, reject) => {
15968
17619
  child.on("error", reject);
15969
17620
  child.on("close", () => {
15970
- resolve12(buf.split("\n").filter(Boolean));
17621
+ resolve15(buf.split("\n").filter(Boolean));
15971
17622
  });
15972
17623
  })
15973
17624
  };
@@ -15978,16 +17629,16 @@ async function globNative(pattern, base, extraGlob) {
15978
17629
  const walk2 = async (dir) => {
15979
17630
  let entries;
15980
17631
  try {
15981
- entries = await fs23.readdir(dir, { withFileTypes: true });
17632
+ entries = await fs25.readdir(dir, { withFileTypes: true });
15982
17633
  } catch {
15983
17634
  return;
15984
17635
  }
15985
17636
  for (const e of entries) {
15986
17637
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
15987
- const full = path23.join(dir, e.name);
17638
+ const full = path27.join(dir, e.name);
15988
17639
  try {
15989
- const stat16 = await fs23.lstat(full);
15990
- if (stat16.isSymbolicLink()) continue;
17640
+ const stat18 = await fs25.lstat(full);
17641
+ if (stat18.isSymbolicLink()) continue;
15991
17642
  } catch {
15992
17643
  continue;
15993
17644
  }
@@ -16010,9 +17661,9 @@ async function globNative(pattern, base, extraGlob) {
16010
17661
 
16011
17662
  // src/scaffold.ts
16012
17663
  init_util();
16013
- import * as fs24 from "node:fs/promises";
16014
- import * as path24 from "node:path";
16015
- import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
17664
+ import * as fs26 from "node:fs/promises";
17665
+ import * as path28 from "node:path";
17666
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
16016
17667
  var BUILT_IN_TEMPLATES = {
16017
17668
  "npm-package": {
16018
17669
  description: "Basic npm package with ESM",
@@ -16161,17 +17812,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
16161
17812
  let filesCreated = 0;
16162
17813
  for (const [filePath, content] of Object.entries(templateFiles)) {
16163
17814
  const resolvedPath = substituteVars(filePath, name, vars);
16164
- const joinedPath = path24.join(cwd, resolvedPath);
16165
- const root = path24.resolve(ctx.projectRoot);
16166
- const target = path24.resolve(joinedPath);
16167
- const rel = path24.relative(root, target);
16168
- if (rel.startsWith("..") || path24.isAbsolute(rel)) {
17815
+ const joinedPath = path28.join(cwd, resolvedPath);
17816
+ const root = path28.resolve(ctx.projectRoot);
17817
+ const target = path28.resolve(joinedPath);
17818
+ const rel = path28.relative(root, target);
17819
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
16169
17820
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
16170
17821
  }
16171
17822
  const fullPath = target;
16172
17823
  if (!dryRun) {
16173
- await fs24.mkdir(path24.dirname(fullPath), { recursive: true });
16174
- await atomicWrite3(fullPath, substituteVars(content, name, vars));
17824
+ await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
17825
+ await atomicWrite4(fullPath, substituteVars(content, name, vars));
16175
17826
  }
16176
17827
  files.push(resolvedPath);
16177
17828
  filesCreated++;
@@ -16205,6 +17856,7 @@ var DEFAULT_NUM = 10;
16205
17856
  var MAX_RESULTS = 50;
16206
17857
  var TIMEOUT_MS3 = 15e3;
16207
17858
  var CACHE_TTL_MS = 3e5;
17859
+ var CACHE_MAX_ENTRIES = 200;
16208
17860
  var cache = /* @__PURE__ */ new Map();
16209
17861
  var searchTool = {
16210
17862
  name: "search",
@@ -16326,7 +17978,7 @@ var searchTool = {
16326
17978
  }
16327
17979
  const finalResults = ranked.slice(0, num);
16328
17980
  cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
16329
- pruneStaleCacheEntries();
17981
+ pruneCacheEntries();
16330
17982
  yield {
16331
17983
  type: "partial_output",
16332
17984
  text: `${finalResults.length} results from ${effectiveSource}`,
@@ -16348,11 +18000,16 @@ var searchTool = {
16348
18000
  };
16349
18001
  }
16350
18002
  };
16351
- function pruneStaleCacheEntries() {
18003
+ function pruneCacheEntries() {
16352
18004
  const cutoff = Date.now() - CACHE_TTL_MS * 2;
16353
18005
  for (const [key, entry] of cache.entries()) {
16354
18006
  if (entry.timestamp < cutoff) cache.delete(key);
16355
18007
  }
18008
+ while (cache.size > CACHE_MAX_ENTRIES) {
18009
+ const oldestKey = cache.keys().next().value;
18010
+ if (oldestKey === void 0) break;
18011
+ cache.delete(oldestKey);
18012
+ }
16356
18013
  }
16357
18014
  function rankSearchResults(results, query) {
16358
18015
  const seenUrls = /* @__PURE__ */ new Set();
@@ -16581,7 +18238,7 @@ function decodeHtmlEntities(text) {
16581
18238
  }
16582
18239
 
16583
18240
  // src/set-working-dir.ts
16584
- import * as fs25 from "node:fs/promises";
18241
+ import * as fs27 from "node:fs/promises";
16585
18242
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
16586
18243
  var setWorkingDirTool = {
16587
18244
  name: "set_working_dir",
@@ -16620,7 +18277,7 @@ var setWorkingDirTool = {
16620
18277
  };
16621
18278
  }
16622
18279
  try {
16623
- await fs25.access(resolved);
18280
+ await fs27.access(resolved);
16624
18281
  } catch {
16625
18282
  try {
16626
18283
  ctx.setWorkingDir(previous);
@@ -17020,7 +18677,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
17020
18677
  init_spawn_stream();
17021
18678
  init_util();
17022
18679
  init_legacy_bridge();
17023
- import * as path25 from "node:path";
18680
+ import * as path29 from "node:path";
17024
18681
  var testTool = {
17025
18682
  name: "test",
17026
18683
  category: "Code Quality",
@@ -17123,11 +18780,11 @@ var testTool = {
17123
18780
  }
17124
18781
  };
17125
18782
  async function detectRunner(cwd) {
17126
- const { stat: stat16 } = await import("node:fs/promises");
18783
+ const { stat: stat18 } = await import("node:fs/promises");
17127
18784
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
17128
18785
  for (const f of candidates) {
17129
18786
  try {
17130
- await stat16(path25.join(cwd, f));
18787
+ await stat18(path29.join(cwd, f));
17131
18788
  if (f.includes("vitest")) return "vitest";
17132
18789
  if (f.includes("jest")) return "jest";
17133
18790
  if (f.includes("mocha")) return "mocha";
@@ -17593,8 +19250,8 @@ var toolUseTool = {
17593
19250
  // src/tree.ts
17594
19251
  init_util();
17595
19252
  import { expectDefined as expectDefined10 } from "@wrongstack/core";
17596
- import * as fs26 from "node:fs/promises";
17597
- import * as path26 from "node:path";
19253
+ import * as fs28 from "node:fs/promises";
19254
+ import * as path30 from "node:path";
17598
19255
  var DEFAULT_IGNORE5 = [
17599
19256
  "node_modules",
17600
19257
  ".git",
@@ -17735,7 +19392,7 @@ var treeTool = {
17735
19392
  }
17736
19393
  };
17737
19394
  async function walkDir(dir, depth, opts) {
17738
- const entries = await fs26.readdir(dir, { withFileTypes: true }).catch(() => []);
19395
+ const entries = await fs28.readdir(dir, { withFileTypes: true }).catch(() => []);
17739
19396
  const filtered = entries.filter((e) => {
17740
19397
  if (!opts.showHidden && e.name.startsWith(".")) return false;
17741
19398
  if (opts.exclude.has(e.name)) return false;
@@ -17765,7 +19422,7 @@ async function walkDir(dir, depth, opts) {
17765
19422
  opts.lines.push(opts.prefix + branch + displayName);
17766
19423
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
17767
19424
  const childPrefix = opts.prefix + connector;
17768
- await walkDir(path26.join(dir, entry.name), depth + 1, {
19425
+ await walkDir(path30.join(dir, entry.name), depth + 1, {
17769
19426
  ...opts,
17770
19427
  prefix: childPrefix,
17771
19428
  isLast
@@ -17778,7 +19435,7 @@ async function walkDir(dir, depth, opts) {
17778
19435
  init_spawn_stream();
17779
19436
  init_util();
17780
19437
  init_legacy_bridge();
17781
- import * as path27 from "node:path";
19438
+ import * as path31 from "node:path";
17782
19439
  var typecheckTool = {
17783
19440
  name: "typecheck",
17784
19441
  category: "Code Quality",
@@ -17877,12 +19534,12 @@ var typecheckTool = {
17877
19534
  }
17878
19535
  };
17879
19536
  async function findTsConfig(cwd) {
17880
- const { stat: stat16 } = await import("node:fs/promises");
19537
+ const { stat: stat18 } = await import("node:fs/promises");
17881
19538
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
17882
19539
  for (const f of candidates) {
17883
19540
  try {
17884
- const s = await stat16(path27.join(cwd, f));
17885
- if (s.isFile()) return path27.join(cwd, f);
19541
+ const s = await stat18(path31.join(cwd, f));
19542
+ if (s.isFile()) return path31.join(cwd, f);
17886
19543
  } catch {
17887
19544
  }
17888
19545
  }
@@ -17890,8 +19547,8 @@ async function findTsConfig(cwd) {
17890
19547
  }
17891
19548
 
17892
19549
  // src/write.ts
17893
- import * as fs27 from "node:fs/promises";
17894
- import { atomicWrite as atomicWrite4, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
19550
+ import * as fs29 from "node:fs/promises";
19551
+ import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
17895
19552
  init_util();
17896
19553
  var writeTool = {
17897
19554
  name: "write",
@@ -17955,14 +19612,14 @@ async function prepareWrite(input, ctx) {
17955
19612
  let existed = false;
17956
19613
  let prev = "";
17957
19614
  try {
17958
- const stat16 = await fs27.stat(absPath);
17959
- existed = stat16.isFile();
19615
+ const stat18 = await fs29.stat(absPath);
19616
+ existed = stat18.isFile();
17960
19617
  if (existed) {
17961
19618
  if (!ctx.hasRead(absPath)) {
17962
- prev = await fs27.readFile(absPath, "utf8");
17963
- ctx.recordRead(absPath, stat16.mtimeMs, "write", sha256hex(prev));
19619
+ prev = await fs29.readFile(absPath, "utf8");
19620
+ ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
17964
19621
  } else {
17965
- prev = await fs27.readFile(absPath, "utf8");
19622
+ prev = await fs29.readFile(absPath, "utf8");
17966
19623
  }
17967
19624
  }
17968
19625
  } catch (err) {
@@ -17974,11 +19631,11 @@ async function prepareWrite(input, ctx) {
17974
19631
  }
17975
19632
  async function finishWrite(input, ctx, prepared, signal) {
17976
19633
  signal?.throwIfAborted();
17977
- await atomicWrite4(prepared.absPath, input.content);
19634
+ await atomicWrite5(prepared.absPath, input.content);
17978
19635
  const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
17979
19636
  + (new file, ${input.content.split("\n").length} lines)`;
17980
- const stat16 = await fs27.stat(prepared.absPath);
17981
- ctx.recordRead(prepared.absPath, stat16.mtimeMs, "write", sha256hex(input.content));
19637
+ const stat18 = await fs29.stat(prepared.absPath);
19638
+ ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
17982
19639
  ctx.session.recordFileChange({
17983
19640
  path: prepared.absPath,
17984
19641
  action: prepared.existed ? "modified" : "created",
@@ -18003,6 +19660,8 @@ async function finishWrite(input, ctx, prepared, signal) {
18003
19660
 
18004
19661
  // src/builtin.ts
18005
19662
  var OPTIONAL_TOOLS = [
19663
+ ...browserTools,
19664
+ e2ePlanTool,
18006
19665
  installTool,
18007
19666
  auditTool,
18008
19667
  outdatedTool,
@@ -18066,6 +19725,8 @@ var TIER3_TOOLS = [
18066
19725
  setWorkingDirTool
18067
19726
  ];
18068
19727
  var builtinTools = [
19728
+ ...browserTools,
19729
+ e2ePlanTool,
18069
19730
  readTool,
18070
19731
  writeTool,
18071
19732
  editTool,
@@ -18409,7 +20070,7 @@ ${mode.description}`
18409
20070
  // src/pack.ts
18410
20071
  var builtinToolsPack = {
18411
20072
  name: "builtin-tools",
18412
- description: "The complete set of built-in tools that ship with WrongStack. Covers filesystem (read/write/edit/replace/glob/grep/tree), execution (bash/exec/git/install), networking (fetch/search), code quality (lint/test/typecheck/format), planning (todo/plan/memory), and meta tools (tool-search/tool-help/batch-tool-use/codebase-*).",
20073
+ description: "The complete set of built-in tools that ship with WrongStack. Covers filesystem, execution, first-party browser automation, read-only E2E planning, networking, code quality, planning, and meta tools.",
18413
20074
  tools: builtinTools
18414
20075
  };
18415
20076
 
@@ -18985,8 +20646,8 @@ function createGlobalPsSlashCommand() {
18985
20646
  }
18986
20647
 
18987
20648
  // src/skill.ts
18988
- import * as fs28 from "node:fs/promises";
18989
- import * as path28 from "node:path";
20649
+ import * as fs30 from "node:fs/promises";
20650
+ import * as path32 from "node:path";
18990
20651
  import {
18991
20652
  SKILL_LIMITS,
18992
20653
  stripFrontmatter,
@@ -19033,7 +20694,7 @@ function makeSkillTool(skillLoader) {
19033
20694
  field: "name"
19034
20695
  });
19035
20696
  }
19036
- const dir = path28.dirname(manifest.path);
20697
+ const dir = path32.dirname(manifest.path);
19037
20698
  let loadedResource;
19038
20699
  if (input.resource?.trim()) {
19039
20700
  loadedResource = await loadResource(dir, input.resource.trim());
@@ -19083,15 +20744,15 @@ ${listing}`;
19083
20744
  }
19084
20745
  async function loadResource(skillDir, rel) {
19085
20746
  const norm = rel.replace(/\\/g, "/");
19086
- if (path28.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
20747
+ if (path32.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
19087
20748
  throw new ToolValidationError10({
19088
20749
  message: `skill: invalid resource path "${rel}"`,
19089
20750
  field: "resource"
19090
20751
  });
19091
20752
  }
19092
- const absPath = path28.resolve(skillDir, rel);
19093
- const root = path28.resolve(skillDir);
19094
- if (absPath !== root && !absPath.startsWith(root + path28.sep)) {
20753
+ const absPath = path32.resolve(skillDir, rel);
20754
+ const root = path32.resolve(skillDir);
20755
+ if (absPath !== root && !absPath.startsWith(root + path32.sep)) {
19095
20756
  throw new ToolValidationError10({
19096
20757
  message: `skill: resource "${rel}" escapes the skill directory`,
19097
20758
  field: "resource"
@@ -19099,7 +20760,7 @@ async function loadResource(skillDir, rel) {
19099
20760
  }
19100
20761
  let buf;
19101
20762
  try {
19102
- buf = await fs28.readFile(absPath);
20763
+ buf = await fs30.readFile(absPath);
19103
20764
  } catch {
19104
20765
  throw new ToolValidationError10({
19105
20766
  message: `skill: resource "${rel}" not readable`,
@@ -19126,17 +20787,17 @@ async function walk(root, dir, out) {
19126
20787
  if (out.length >= MAX_LISTED_RESOURCES) return;
19127
20788
  let entries;
19128
20789
  try {
19129
- entries = await fs28.readdir(dir, { withFileTypes: true });
20790
+ entries = await fs30.readdir(dir, { withFileTypes: true });
19130
20791
  } catch {
19131
20792
  return;
19132
20793
  }
19133
20794
  for (const e of entries) {
19134
20795
  if (out.length >= MAX_LISTED_RESOURCES) return;
19135
- const fullPath = path28.join(dir, e.name);
20796
+ const fullPath = path32.join(dir, e.name);
19136
20797
  let isDir = e.isDirectory();
19137
20798
  if (e.isSymbolicLink()) {
19138
20799
  try {
19139
- isDir = (await fs28.stat(fullPath)).isDirectory();
20800
+ isDir = (await fs30.stat(fullPath)).isDirectory();
19140
20801
  } catch {
19141
20802
  continue;
19142
20803
  }
@@ -19147,9 +20808,9 @@ async function walk(root, dir, out) {
19147
20808
  } else if (e.isFile()) {
19148
20809
  if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
19149
20810
  try {
19150
- const stat16 = await fs28.stat(fullPath);
19151
- const rel = path28.relative(root, fullPath).split(path28.sep).join("/");
19152
- out.push({ path: rel, bytes: stat16.size });
20811
+ const stat18 = await fs30.stat(fullPath);
20812
+ const rel = path32.relative(root, fullPath).split(path32.sep).join("/");
20813
+ out.push({ path: rel, bytes: stat18.size });
19153
20814
  } catch {
19154
20815
  }
19155
20816
  }
@@ -19198,6 +20859,7 @@ var TOOL_ICON_MAP = {
19198
20859
  // Testing
19199
20860
  test: "test",
19200
20861
  tests: "test",
20862
+ e2e_plan: "test",
19201
20863
  // Package management
19202
20864
  install: "package",
19203
20865
  uninstall: "package",
@@ -19318,6 +20980,9 @@ var TOOL_ICON_CONFIG = {
19318
20980
  var FALLBACK_ICON = "fallback";
19319
20981
  export {
19320
20982
  ADDITIONAL_LANGUAGE_PROFILES,
20983
+ BrowserArtifactStore,
20984
+ BrowserNetworkGuardProxy,
20985
+ BrowserSessionManager,
19321
20986
  CircuitBreaker,
19322
20987
  CircuitOpenError,
19323
20988
  FALLBACK_ICON,
@@ -19332,9 +20997,28 @@ export {
19332
20997
  TOOL_ICON_CONFIG,
19333
20998
  TOOL_ICON_MAP,
19334
20999
  _resetProcessRegistry,
21000
+ assertBrowserUrlAllowed,
19335
21001
  auditTool,
19336
21002
  bashTool,
19337
21003
  batchToolUseTool,
21004
+ browserClickTool,
21005
+ browserCloseTool,
21006
+ browserDragTool,
21007
+ browserEvaluateTool,
21008
+ browserHoverTool,
21009
+ browserInstallationDiagnostics,
21010
+ browserListTool,
21011
+ browserNavigateTool,
21012
+ browserOpenTool,
21013
+ browserPressTool,
21014
+ browserScreenshotTool,
21015
+ browserSelectTool,
21016
+ browserSnapshotTool,
21017
+ browserStatusTool,
21018
+ browserTools,
21019
+ browserTypeTool,
21020
+ browserUploadTool,
21021
+ browserWaitTool,
19338
21022
  builtinTools,
19339
21023
  builtinToolsPack,
19340
21024
  cancelPendingReindexes,
@@ -19351,7 +21035,9 @@ export {
19351
21035
  detectLanguageWorkspaces,
19352
21036
  diagnosticsForInternalSyntax,
19353
21037
  diffTool,
21038
+ discoverE2EProjects,
19354
21039
  documentTool,
21040
+ e2ePlanTool,
19355
21041
  editTool,
19356
21042
  enqueueReindex,
19357
21043
  ensureSessionShell,
@@ -19396,10 +21082,12 @@ export {
19396
21082
  outdatedTool,
19397
21083
  parseLanguageDiagnostics,
19398
21084
  parsePackageReports,
21085
+ parsePrivateOriginAllowlist,
19399
21086
  patchTool,
19400
21087
  planLanguageOperation,
19401
21088
  planTool,
19402
21089
  readTool,
21090
+ redactBrowserText,
19403
21091
  relatedMemoryTool,
19404
21092
  rememberTool,
19405
21093
  replaceTool,
@@ -19407,12 +21095,15 @@ export {
19407
21095
  resetExecPolicy,
19408
21096
  resetIndexCircuitBreaker,
19409
21097
  resetPersistentProcessRegistry,
21098
+ resolvePinnedBrowserTarget,
19410
21099
  resolveSessionShell,
19411
21100
  runStartupIndex,
21101
+ safeBrowserUrl,
19412
21102
  scaffoldTool,
19413
21103
  searchCodebaseIndex,
19414
21104
  searchMemoryTool,
19415
21105
  searchTool,
21106
+ shutdownBrowserTools,
19416
21107
  shutdownCodebaseIndexHost,
19417
21108
  startProcessGuardian,
19418
21109
  stopProcessGuardian,