@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/pack.js CHANGED
@@ -56,7 +56,7 @@ function createOutputSpool(opts) {
56
56
  let filePath = null;
57
57
  let failed = false;
58
58
  let finalized = false;
59
- const open2 = () => {
59
+ const open3 = () => {
60
60
  if (stream || failed) return;
61
61
  try {
62
62
  const dir = toolOutputDir();
@@ -89,7 +89,7 @@ function createOutputSpool(opts) {
89
89
  return;
90
90
  }
91
91
  head += text;
92
- open2();
92
+ open3();
93
93
  head = "";
94
94
  return;
95
95
  }
@@ -907,8 +907,8 @@ async function* spawnStream(opts) {
907
907
  try {
908
908
  for (; ; ) {
909
909
  while (queue.length === 0) {
910
- await new Promise((resolve11) => {
911
- waiter = resolve11;
910
+ await new Promise((resolve14) => {
911
+ waiter = resolve14;
912
912
  });
913
913
  }
914
914
  const chunk = queue.shift();
@@ -982,14 +982,14 @@ function sha256hex(content) {
982
982
  return createHash("sha256").update(content, "utf8").digest("hex");
983
983
  }
984
984
  async function detectPackageManager(cwd) {
985
- const { stat: stat15 } = await import("node:fs/promises");
985
+ const { stat: stat17 } = await import("node:fs/promises");
986
986
  try {
987
- await stat15(`${cwd}/pnpm-lock.yaml`);
987
+ await stat17(`${cwd}/pnpm-lock.yaml`);
988
988
  return "pnpm";
989
989
  } catch {
990
990
  }
991
991
  try {
992
- await stat15(`${cwd}/yarn.lock`);
992
+ await stat17(`${cwd}/yarn.lock`);
993
993
  return "yarn";
994
994
  } catch {
995
995
  }
@@ -2558,8 +2558,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2558
2558
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2559
2559
  }
2560
2560
  }
2561
- function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
2562
- const lower = basename7.toLowerCase();
2561
+ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2562
+ const lower = basename8.toLowerCase();
2563
2563
  const extension = path4.extname(lower);
2564
2564
  for (const profile of profiles) {
2565
2565
  const detector = profile.detectors.find(
@@ -2570,7 +2570,7 @@ function collectFileEvidence(directory, fullPath, basename7, profiles, state) {
2570
2570
  candidate.evidence.push({
2571
2571
  kind: detector.kind,
2572
2572
  path: fullPath,
2573
- value: basename7,
2573
+ value: basename8,
2574
2574
  weight: detector.weight
2575
2575
  });
2576
2576
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2698,8 +2698,8 @@ function normalizeLimits(input) {
2698
2698
  async function canonicalDirectory(input) {
2699
2699
  const resolved = path4.resolve(input);
2700
2700
  const real = await fs2.realpath(resolved);
2701
- const stat15 = await fs2.stat(real);
2702
- if (!stat15.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2701
+ const stat17 = await fs2.stat(real);
2702
+ if (!stat17.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2703
2703
  return real;
2704
2704
  }
2705
2705
  async function canonicalInside(input, root, label) {
@@ -2719,12 +2719,12 @@ function resolveFrom(cwd, input) {
2719
2719
  return path4.isAbsolute(input) ? input : path4.resolve(cwd, input);
2720
2720
  }
2721
2721
  function isInside(candidate, root) {
2722
- const relative10 = path4.relative(root, candidate);
2723
- return relative10 === "" || !relative10.startsWith("..") && !path4.isAbsolute(relative10);
2722
+ const relative12 = path4.relative(root, candidate);
2723
+ return relative12 === "" || !relative12.startsWith("..") && !path4.isAbsolute(relative12);
2724
2724
  }
2725
2725
  function pathDepth(candidate, root) {
2726
- const relative10 = path4.relative(root, candidate);
2727
- return relative10 === "" ? 0 : relative10.split(path4.sep).length;
2726
+ const relative12 = path4.relative(root, candidate);
2727
+ return relative12 === "" ? 0 : relative12.split(path4.sep).length;
2728
2728
  }
2729
2729
  function dedupeEvidence(items) {
2730
2730
  const seen = /* @__PURE__ */ new Set();
@@ -3424,8 +3424,8 @@ function compareCandidates(a, b) {
3424
3424
  return b.confidence - a.confidence || a.language.localeCompare(b.language) || a.root.localeCompare(b.root);
3425
3425
  }
3426
3426
  function workspaceDepth(workspaceRoot, projectRoot) {
3427
- const relative10 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3428
- return relative10 === "" ? 0 : relative10.split(path6.sep).length;
3427
+ const relative12 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3428
+ return relative12 === "" ? 0 : relative12.split(path6.sep).length;
3429
3429
  }
3430
3430
  function validateOperationOptions(operation, packages) {
3431
3431
  if (!operation.startsWith("package-") || !packages) return;
@@ -3455,8 +3455,8 @@ async function canonicalTarget(cwd, target, projectRoot) {
3455
3455
  return real;
3456
3456
  }
3457
3457
  function isInside2(candidate, root) {
3458
- const relative10 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3459
- return relative10 === "" || !relative10.startsWith("..") && !path6.isAbsolute(relative10);
3458
+ const relative12 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3459
+ return relative12 === "" || !relative12.startsWith("..") && !path6.isAbsolute(relative12);
3460
3460
  }
3461
3461
  var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
3462
3462
  var init_plan = __esm({
@@ -3587,8 +3587,8 @@ async function executeInternal(options, startedAt) {
3587
3587
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3588
3588
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3589
3589
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3590
- const stat15 = await fs4.stat(safeTarget);
3591
- if (stat15.size > MAX_INTERNAL_SOURCE_BYTES) {
3590
+ const stat17 = await fs4.stat(safeTarget);
3591
+ if (stat17.size > MAX_INTERNAL_SOURCE_BYTES) {
3592
3592
  return unavailableResult(
3593
3593
  options,
3594
3594
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -3635,14 +3635,14 @@ async function validatePlanRealpaths(plan, projectRoot) {
3635
3635
  return void 0;
3636
3636
  }
3637
3637
  function isRealInside(candidate, root) {
3638
- const relative10 = path7.relative(root, candidate);
3639
- return relative10 === "" || !relative10.startsWith("..") && !path7.isAbsolute(relative10);
3638
+ const relative12 = path7.relative(root, candidate);
3639
+ return relative12 === "" || !relative12.startsWith("..") && !path7.isAbsolute(relative12);
3640
3640
  }
3641
3641
  async function assertContainedFile(candidate, projectRoot) {
3642
3642
  const realRoot = await fs4.realpath(projectRoot);
3643
3643
  const realTarget = await fs4.realpath(candidate);
3644
- const relative10 = path7.relative(realRoot, realTarget);
3645
- if (relative10.startsWith("..") || path7.isAbsolute(relative10)) {
3644
+ const relative12 = path7.relative(realRoot, realTarget);
3645
+ if (relative12.startsWith("..") || path7.isAbsolute(relative12)) {
3646
3646
  throw new Error(`Internal syntax target resolves outside project root: ${candidate}`);
3647
3647
  }
3648
3648
  return realTarget;
@@ -3875,8 +3875,8 @@ async function snapshotPaths(paths) {
3875
3875
  const existing = [];
3876
3876
  for (const candidate of paths) {
3877
3877
  try {
3878
- const stat15 = await fs4.stat(candidate);
3879
- if (!stat15.isFile()) continue;
3878
+ const stat17 = await fs4.stat(candidate);
3879
+ if (!stat17.isFile()) continue;
3880
3880
  existing.push(candidate);
3881
3881
  } catch {
3882
3882
  }
@@ -3887,15 +3887,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3887
3887
  const beforeSet = new Set(before);
3888
3888
  const afterSet = new Set(after);
3889
3889
  const changed = /* @__PURE__ */ new Set();
3890
- for (const path28 of after) {
3891
- if (!beforeSet.has(path28)) changed.add(path28);
3890
+ for (const path32 of after) {
3891
+ if (!beforeSet.has(path32)) changed.add(path32);
3892
3892
  }
3893
- for (const path28 of before) {
3894
- if (!afterSet.has(path28)) changed.add(path28);
3893
+ for (const path32 of before) {
3894
+ if (!afterSet.has(path32)) changed.add(path32);
3895
3895
  }
3896
3896
  if (beforeSizes && afterSizes) {
3897
- for (const path28 of after) {
3898
- if (beforeSizes.get(path28) !== afterSizes.get(path28)) changed.add(path28);
3897
+ for (const path32 of after) {
3898
+ if (beforeSizes.get(path32) !== afterSizes.get(path32)) changed.add(path32);
3899
3899
  }
3900
3900
  }
3901
3901
  return [...changed].sort();
@@ -3904,8 +3904,8 @@ async function snapshotSizes(paths) {
3904
3904
  const sizes = /* @__PURE__ */ new Map();
3905
3905
  for (const candidate of paths) {
3906
3906
  try {
3907
- const stat15 = await fs4.stat(candidate);
3908
- if (stat15.isFile()) sizes.set(candidate, stat15.size);
3907
+ const stat17 = await fs4.stat(candidate);
3908
+ if (stat17.isFile()) sizes.set(candidate, stat17.size);
3909
3909
  } catch {
3910
3910
  }
3911
3911
  }
@@ -4848,8 +4848,8 @@ var auditTool = {
4848
4848
  yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
4849
4849
  }
4850
4850
  };
4851
- function parseAuditOutput(json, exitCode) {
4852
- if (!json) {
4851
+ function parseAuditOutput(json2, exitCode) {
4852
+ if (!json2) {
4853
4853
  return {
4854
4854
  exit_code: exitCode,
4855
4855
  vulnerabilities: [],
@@ -4860,7 +4860,7 @@ function parseAuditOutput(json, exitCode) {
4860
4860
  };
4861
4861
  }
4862
4862
  try {
4863
- const data = JSON.parse(json);
4863
+ const data = JSON.parse(json2);
4864
4864
  const advisories = [];
4865
4865
  const ads = data.advisories ?? {};
4866
4866
  for (const id of Object.keys(ads)) {
@@ -4879,8 +4879,8 @@ function parseAuditOutput(json, exitCode) {
4879
4879
  vulnerabilities: advisories,
4880
4880
  total,
4881
4881
  summary,
4882
- output: json,
4883
- truncated: json.length > 1e5
4882
+ output: json2,
4883
+ truncated: json2.length > 1e5
4884
4884
  };
4885
4885
  } catch {
4886
4886
  return {
@@ -4888,7 +4888,7 @@ function parseAuditOutput(json, exitCode) {
4888
4888
  vulnerabilities: [],
4889
4889
  total: 0,
4890
4890
  summary: "Could not parse audit output",
4891
- output: json,
4891
+ output: json2,
4892
4892
  truncated: false
4893
4893
  };
4894
4894
  }
@@ -5916,10 +5916,10 @@ var bashTool = {
5916
5916
  queue.push(c);
5917
5917
  }
5918
5918
  };
5919
- const next = () => new Promise((resolve11) => {
5919
+ const next = () => new Promise((resolve14) => {
5920
5920
  const c = queue.shift();
5921
- if (c) resolve11(c);
5922
- else resolveNext = resolve11;
5921
+ if (c) resolve14(c);
5922
+ else resolveNext = resolve14;
5923
5923
  });
5924
5924
  let lastFlush = Date.now();
5925
5925
  const flush = () => {
@@ -6159,6 +6159,1238 @@ async function executeSingle(call, ctx, opts) {
6159
6159
  }
6160
6160
  }
6161
6161
 
6162
+ // src/browser/tools.ts
6163
+ import * as path12 from "node:path";
6164
+ import { ToolCapabilities } from "@wrongstack/core";
6165
+ import { resolveWstackPaths } from "@wrongstack/core/utils";
6166
+
6167
+ // src/browser/manager.ts
6168
+ import * as fs8 from "node:fs/promises";
6169
+ import * as path11 from "node:path";
6170
+ import { ulid as ulid2 } from "@wrongstack/core/utils";
6171
+
6172
+ // src/browser/artifacts.ts
6173
+ import { createHash as createHash3 } from "node:crypto";
6174
+ import { createReadStream } from "node:fs";
6175
+ import * as fs7 from "node:fs/promises";
6176
+ import * as path10 from "node:path";
6177
+ import { atomicWrite, ulid } from "@wrongstack/core/utils";
6178
+ var BrowserArtifactStore = class {
6179
+ constructor(root) {
6180
+ this.root = root;
6181
+ }
6182
+ root;
6183
+ async write(sessionId, kind, extension, mimeType, content) {
6184
+ const id = ulid();
6185
+ const dir = path10.join(this.root, safeSegment(sessionId));
6186
+ const target = path10.join(dir, `${id}.${extension.replace(/^\./, "")}`);
6187
+ await atomicWrite(target, content, { mode: 384 });
6188
+ return this.record(id, sessionId, kind, target, mimeType);
6189
+ }
6190
+ async record(id, sessionId, kind, target, mimeType) {
6191
+ await fs7.chmod(target, 384).catch(() => void 0);
6192
+ const [stat17, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
6193
+ const artifact = {
6194
+ id,
6195
+ kind,
6196
+ sensitivity: "sensitive",
6197
+ path: target,
6198
+ mimeType,
6199
+ sizeBytes: stat17.size,
6200
+ sha256,
6201
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6202
+ };
6203
+ const metadataPath = path10.join(path10.dirname(target), `${id}.metadata.json`);
6204
+ try {
6205
+ await atomicWrite(
6206
+ metadataPath,
6207
+ `${JSON.stringify({
6208
+ id: artifact.id,
6209
+ sessionId: safeSegment(sessionId),
6210
+ kind: artifact.kind,
6211
+ sensitivity: artifact.sensitivity,
6212
+ mimeType: artifact.mimeType,
6213
+ sizeBytes: artifact.sizeBytes,
6214
+ sha256: artifact.sha256,
6215
+ createdAt: artifact.createdAt
6216
+ })}
6217
+ `,
6218
+ { mode: 384 }
6219
+ );
6220
+ } catch (error) {
6221
+ await fs7.rm(target, { force: true }).catch(() => void 0);
6222
+ throw error;
6223
+ }
6224
+ return artifact;
6225
+ }
6226
+ pathFor(sessionId, extension) {
6227
+ const id = ulid();
6228
+ return {
6229
+ id,
6230
+ path: path10.join(this.root, safeSegment(sessionId), `${id}.${extension.replace(/^\./, "")}`)
6231
+ };
6232
+ }
6233
+ async describe(id, kind, target, mimeType) {
6234
+ return this.record(id, path10.basename(path10.dirname(target)), kind, target, mimeType);
6235
+ }
6236
+ };
6237
+ async function hashFile(target) {
6238
+ const hash = createHash3("sha256");
6239
+ await new Promise((resolve14, reject) => {
6240
+ const stream = createReadStream(target);
6241
+ stream.on("data", (chunk) => hash.update(chunk));
6242
+ stream.once("error", reject);
6243
+ stream.once("end", resolve14);
6244
+ });
6245
+ return hash.digest("hex");
6246
+ }
6247
+ function safeSegment(value) {
6248
+ return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
6249
+ }
6250
+
6251
+ // src/browser/network-guard-proxy.ts
6252
+ import * as http from "node:http";
6253
+ import * as net2 from "node:net";
6254
+
6255
+ // src/browser/security.ts
6256
+ import * as dns from "node:dns/promises";
6257
+ import * as net from "node:net";
6258
+ import { assertNotPrivateHost, isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
6259
+ var SAFE_SUBRESOURCE_PROTOCOLS = /* @__PURE__ */ new Set(["about:", "blob:", "data:"]);
6260
+ async function assertBrowserUrlAllowed(rawUrl, opts) {
6261
+ const url = parseBrowserUrl(rawUrl, opts.navigation ?? true);
6262
+ if (!opts.navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
6263
+ if (!opts.allowPrivateHosts && !isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins)) {
6264
+ await assertNotPrivateHost(url.hostname);
6265
+ }
6266
+ return url;
6267
+ }
6268
+ async function resolvePinnedBrowserTarget(rawUrl, opts = {}) {
6269
+ const url = parseBrowserUrl(rawUrl, true);
6270
+ const allowPrivate = opts.allowPrivateHosts === true || isAllowedPrivateOrigin(url, opts.allowedPrivateOrigins);
6271
+ const hostname2 = unbracket(url.hostname);
6272
+ const literalFamily = net.isIP(hostname2);
6273
+ if (literalFamily === 4 || literalFamily === 6) {
6274
+ if (!allowPrivate && isPrivateAddress(hostname2, literalFamily)) {
6275
+ throw new Error(`browser: blocked private/loopback address "${hostname2}"`);
6276
+ }
6277
+ return { url, address: hostname2, family: literalFamily };
6278
+ }
6279
+ if ((hostname2 === "localhost" || hostname2.endsWith(".localhost")) && !allowPrivate) {
6280
+ throw new Error("browser: blocked localhost target");
6281
+ }
6282
+ const lookup3 = opts.lookup ?? ((host) => dns.lookup(host, { all: true }));
6283
+ let records;
6284
+ try {
6285
+ records = await lookup3(hostname2);
6286
+ } catch (error) {
6287
+ throw new Error(
6288
+ `browser: DNS resolution failed for "${hostname2}": ${error instanceof Error ? error.message : String(error)}`
6289
+ );
6290
+ }
6291
+ if (records.length === 0) throw new Error(`browser: DNS returned no addresses for "${hostname2}"`);
6292
+ for (const record of records) {
6293
+ if (record.family !== 4 && record.family !== 6) {
6294
+ throw new Error(`browser: DNS returned an unsupported address family for "${hostname2}"`);
6295
+ }
6296
+ if (!allowPrivate && isPrivateAddress(record.address, record.family)) {
6297
+ throw new Error(`browser: resolved to private address ${record.address}`);
6298
+ }
6299
+ }
6300
+ const selected = records[0];
6301
+ return { url, address: selected.address, family: selected.family };
6302
+ }
6303
+ function parsePrivateOriginAllowlist(raw) {
6304
+ if (!raw?.trim()) return [];
6305
+ const origins = /* @__PURE__ */ new Set();
6306
+ for (const entry of raw.split(",")) {
6307
+ const candidate = entry.trim();
6308
+ if (!candidate) continue;
6309
+ const url = parseBrowserUrl(candidate, true);
6310
+ if (url.pathname !== "/" || url.search || url.hash) {
6311
+ throw new Error(`browser: private origin allowlist entry must be an origin: "${candidate}"`);
6312
+ }
6313
+ origins.add(url.origin);
6314
+ }
6315
+ return [...origins];
6316
+ }
6317
+ function parseBrowserUrl(rawUrl, navigation) {
6318
+ let url;
6319
+ try {
6320
+ url = new URL(rawUrl);
6321
+ } catch {
6322
+ throw new Error("browser: url must be an absolute URL");
6323
+ }
6324
+ if (!navigation && SAFE_SUBRESOURCE_PROTOCOLS.has(url.protocol)) return url;
6325
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
6326
+ throw new Error(`browser: unsupported protocol "${url.protocol}"`);
6327
+ }
6328
+ if (url.username || url.password) {
6329
+ throw new Error("browser: credentials in URLs are not allowed");
6330
+ }
6331
+ return url;
6332
+ }
6333
+ function isAllowedPrivateOrigin(url, origins) {
6334
+ return origins?.includes(url.origin) ?? false;
6335
+ }
6336
+ function isPrivateAddress(address, family) {
6337
+ return family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
6338
+ }
6339
+ function unbracket(hostname2) {
6340
+ return hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
6341
+ }
6342
+ function safeBrowserUrl(rawUrl) {
6343
+ try {
6344
+ const url = new URL(rawUrl);
6345
+ url.username = "";
6346
+ url.password = "";
6347
+ url.search = "";
6348
+ url.hash = "";
6349
+ return url.toString();
6350
+ } catch {
6351
+ return "about:blank";
6352
+ }
6353
+ }
6354
+ function redactBrowserText(text) {
6355
+ return text.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [REDACTED]").replace(
6356
+ /\b(api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|token|password|secret)\b\s*[:=]\s*[^\s,;]+/gi,
6357
+ "$1=[REDACTED]"
6358
+ );
6359
+ }
6360
+
6361
+ // src/browser/network-guard-proxy.ts
6362
+ var BrowserNetworkGuardProxy = class {
6363
+ constructor(options = {}) {
6364
+ this.options = options;
6365
+ this.server = http.createServer((request2, response) => {
6366
+ void this.forwardHttp(request2, response);
6367
+ });
6368
+ this.server.on("connect", (request2, socket, head) => {
6369
+ void this.forwardConnect(request2, socket, head);
6370
+ });
6371
+ this.server.on("upgrade", (request2, socket, head) => {
6372
+ void this.forwardUpgrade(request2, socket, head);
6373
+ });
6374
+ this.server.on("connection", (socket) => {
6375
+ this.sockets.add(socket);
6376
+ socket.once("close", () => this.sockets.delete(socket));
6377
+ });
6378
+ this.server.on("clientError", (_error, socket) => {
6379
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
6380
+ });
6381
+ }
6382
+ options;
6383
+ server;
6384
+ sockets = /* @__PURE__ */ new Set();
6385
+ startPromise;
6386
+ url;
6387
+ async start() {
6388
+ if (this.url) return this.url;
6389
+ if (this.startPromise) return this.startPromise;
6390
+ this.startPromise = new Promise((resolve14, reject) => {
6391
+ const onError = (error) => {
6392
+ this.server.off("listening", onListening);
6393
+ reject(error);
6394
+ };
6395
+ const onListening = () => {
6396
+ this.server.off("error", onError);
6397
+ const address = this.server.address();
6398
+ if (!address || typeof address === "string") {
6399
+ reject(new Error("browser: network guard proxy failed to bind"));
6400
+ return;
6401
+ }
6402
+ this.url = `http://127.0.0.1:${address.port}`;
6403
+ resolve14(this.url);
6404
+ };
6405
+ this.server.once("error", onError);
6406
+ this.server.once("listening", onListening);
6407
+ this.server.listen(0, "127.0.0.1");
6408
+ }).finally(() => {
6409
+ this.startPromise = void 0;
6410
+ });
6411
+ return this.startPromise;
6412
+ }
6413
+ async close() {
6414
+ this.url = void 0;
6415
+ for (const socket of this.sockets) socket.destroy();
6416
+ this.sockets.clear();
6417
+ if (!this.server.listening) return;
6418
+ await new Promise((resolve14) => this.server.close(() => resolve14()));
6419
+ }
6420
+ async forwardHttp(request2, response) {
6421
+ try {
6422
+ const target = await this.resolve(request2.url ?? "");
6423
+ if (target.url.protocol !== "http:") throw new Error("browser: HTTPS must use CONNECT");
6424
+ const headers = sanitizeHeaders(request2.headers);
6425
+ headers.host = target.url.host;
6426
+ const upstream = http.request({
6427
+ host: target.address,
6428
+ family: target.family,
6429
+ port: Number(target.url.port || 80),
6430
+ method: request2.method,
6431
+ path: `${target.url.pathname}${target.url.search}`,
6432
+ headers,
6433
+ agent: false
6434
+ });
6435
+ upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => {
6436
+ upstream.destroy(new Error("browser: upstream connection timed out"));
6437
+ });
6438
+ upstream.on("response", (upstreamResponse) => {
6439
+ response.writeHead(
6440
+ upstreamResponse.statusCode ?? 502,
6441
+ sanitizeHeaders(upstreamResponse.headers)
6442
+ );
6443
+ upstreamResponse.pipe(response);
6444
+ });
6445
+ upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
6446
+ request2.on("aborted", () => upstream.destroy());
6447
+ request2.pipe(upstream);
6448
+ } catch {
6449
+ writeProxyError(response, 403, "Blocked by browser network policy");
6450
+ }
6451
+ }
6452
+ async forwardConnect(request2, client, head) {
6453
+ try {
6454
+ const authority = request2.url ?? "";
6455
+ const target = await this.resolve(`https://${authority}`);
6456
+ const upstream = net2.connect({
6457
+ host: target.address,
6458
+ family: target.family,
6459
+ port: Number(target.url.port || 443)
6460
+ });
6461
+ upstream.setTimeout(this.options.connectTimeoutMs ?? 3e4, () => upstream.destroy());
6462
+ upstream.once("connect", () => {
6463
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
6464
+ if (head.length > 0) upstream.write(head);
6465
+ upstream.pipe(client);
6466
+ client.pipe(upstream);
6467
+ });
6468
+ upstream.once("error", () => {
6469
+ client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
6470
+ });
6471
+ client.once("close", () => upstream.destroy());
6472
+ } catch {
6473
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
6474
+ }
6475
+ }
6476
+ async forwardUpgrade(request2, client, head) {
6477
+ let upstream;
6478
+ try {
6479
+ const rawUrl = request2.url ?? "";
6480
+ const websocketUrl = new URL(rawUrl);
6481
+ if (websocketUrl.protocol !== "ws:") {
6482
+ throw new Error("browser: only plain ws upgrades are handled by the HTTP proxy");
6483
+ }
6484
+ websocketUrl.protocol = "http:";
6485
+ const target = await this.resolve(websocketUrl.toString());
6486
+ const headers = sanitizeHeaders(request2.headers, true);
6487
+ headers.host = target.url.host;
6488
+ upstream = http.request({
6489
+ host: target.address,
6490
+ family: target.family,
6491
+ port: Number(target.url.port || 80),
6492
+ method: request2.method,
6493
+ path: `${target.url.pathname}${target.url.search}`,
6494
+ headers,
6495
+ agent: false
6496
+ });
6497
+ upstream.once("upgrade", (response, upstreamSocket, upstreamHead) => {
6498
+ writeRawResponseHead(client, response);
6499
+ if (head.length > 0) upstreamSocket.write(head);
6500
+ if (upstreamHead.length > 0) client.write(upstreamHead);
6501
+ upstreamSocket.pipe(client);
6502
+ client.pipe(upstreamSocket);
6503
+ client.once("close", () => upstreamSocket.destroy());
6504
+ });
6505
+ upstream.once("response", (response) => {
6506
+ writeRawResponseHead(client, response);
6507
+ response.pipe(client);
6508
+ });
6509
+ upstream.once("error", () => {
6510
+ client.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
6511
+ });
6512
+ client.once("close", () => upstream?.destroy());
6513
+ upstream.end();
6514
+ } catch {
6515
+ upstream?.destroy();
6516
+ client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
6517
+ }
6518
+ }
6519
+ resolve(rawUrl) {
6520
+ return resolvePinnedBrowserTarget(rawUrl, {
6521
+ allowPrivateHosts: this.options.allowPrivateHosts,
6522
+ allowedPrivateOrigins: this.options.allowedPrivateOrigins,
6523
+ lookup: this.options.lookup
6524
+ });
6525
+ }
6526
+ };
6527
+ function sanitizeHeaders(headers, preserveUpgrade = false) {
6528
+ const sanitized = { ...headers };
6529
+ delete sanitized["proxy-authorization"];
6530
+ delete sanitized["proxy-connection"];
6531
+ if (!preserveUpgrade) delete sanitized.connection;
6532
+ return sanitized;
6533
+ }
6534
+ function writeRawResponseHead(client, response) {
6535
+ const status = response.statusCode ?? 502;
6536
+ const statusText = response.statusMessage ?? "Bad Gateway";
6537
+ const lines = [`HTTP/1.1 ${status} ${statusText}`];
6538
+ for (let index = 0; index < response.rawHeaders.length; index += 2) {
6539
+ lines.push(`${response.rawHeaders[index]}: ${response.rawHeaders[index + 1]}`);
6540
+ }
6541
+ client.write(`${lines.join("\r\n")}\r
6542
+ \r
6543
+ `);
6544
+ }
6545
+ function writeProxyError(response, status, message) {
6546
+ if (response.headersSent || response.destroyed) return;
6547
+ response.writeHead(status, { "content-type": "text/plain", connection: "close" });
6548
+ response.end(message);
6549
+ }
6550
+
6551
+ // src/browser/manager.ts
6552
+ var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
6553
+ var DEFAULT_MAX_SNAPSHOT_CHARS = 64 * 1024;
6554
+ var DEFAULT_MAX_ENTRIES = 100;
6555
+ var BrowserSessionManager = class {
6556
+ constructor(options, launcher = defaultLauncher) {
6557
+ this.launcher = launcher;
6558
+ this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6559
+ this.allowPrivateHosts = options.allowPrivateHosts ?? false;
6560
+ this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6561
+ this.networkProxy = new BrowserNetworkGuardProxy({
6562
+ allowPrivateHosts: this.allowPrivateHosts,
6563
+ allowedPrivateOrigins: this.allowedPrivateOrigins
6564
+ });
6565
+ this.headless = options.headless ?? true;
6566
+ this.operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
6567
+ this.maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
6568
+ this.maxConsoleEntries = options.maxConsoleEntries ?? DEFAULT_MAX_ENTRIES;
6569
+ this.maxNetworkEntries = options.maxNetworkEntries ?? DEFAULT_MAX_ENTRIES;
6570
+ }
6571
+ launcher;
6572
+ sessions = /* @__PURE__ */ new Map();
6573
+ artifacts;
6574
+ allowPrivateHosts;
6575
+ allowedPrivateOrigins;
6576
+ networkProxy;
6577
+ headless;
6578
+ operationTimeoutMs;
6579
+ maxSnapshotChars;
6580
+ maxConsoleEntries;
6581
+ maxNetworkEntries;
6582
+ browser;
6583
+ launching;
6584
+ async open(ownerId, input, signal) {
6585
+ signal.throwIfAborted();
6586
+ if (input.url) {
6587
+ await assertBrowserUrlAllowed(input.url, {
6588
+ allowPrivateHosts: this.allowPrivateHosts,
6589
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6590
+ navigation: true
6591
+ });
6592
+ }
6593
+ const browser = await this.ensureBrowser(signal);
6594
+ const proxyServer = await this.networkProxy.start();
6595
+ const context = await abortable(
6596
+ signal,
6597
+ async () => {
6598
+ const created = await browser.newContext({
6599
+ acceptDownloads: false,
6600
+ ignoreHTTPSErrors: false,
6601
+ serviceWorkers: "block",
6602
+ proxy: { server: proxyServer },
6603
+ viewport: input.viewport ?? { width: 1440, height: 900 }
6604
+ });
6605
+ if (signal.aborted) {
6606
+ await created.close().catch(() => void 0);
6607
+ signal.throwIfAborted();
6608
+ }
6609
+ return created;
6610
+ },
6611
+ () => void 0
6612
+ );
6613
+ const session = {
6614
+ id: ulid2(),
6615
+ ownerId,
6616
+ context,
6617
+ page: await context.newPage(),
6618
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6619
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
6620
+ tracing: input.trace ?? true,
6621
+ console: [],
6622
+ network: []
6623
+ };
6624
+ this.sessions.set(session.id, session);
6625
+ this.attachEvidenceCollectors(session);
6626
+ await context.route("**/*", async (route) => {
6627
+ try {
6628
+ await assertBrowserUrlAllowed(route.request().url(), {
6629
+ allowPrivateHosts: this.allowPrivateHosts,
6630
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6631
+ navigation: false
6632
+ });
6633
+ await route.continue();
6634
+ } catch {
6635
+ this.pushNetwork(session, {
6636
+ method: route.request().method(),
6637
+ url: safeBrowserUrl(route.request().url()),
6638
+ failed: true,
6639
+ at: (/* @__PURE__ */ new Date()).toISOString()
6640
+ });
6641
+ await route.abort("blockedbyclient");
6642
+ }
6643
+ });
6644
+ try {
6645
+ if (session.tracing) {
6646
+ await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
6647
+ }
6648
+ if (input.url) await this.navigate(session.id, ownerId, input.url, signal);
6649
+ return await this.summary(session);
6650
+ } catch (err) {
6651
+ await context.close().catch(() => void 0);
6652
+ this.sessions.delete(session.id);
6653
+ await this.closeBrowserIfIdle();
6654
+ throw err;
6655
+ }
6656
+ }
6657
+ async list(ownerId) {
6658
+ const owned = [...this.sessions.values()].filter((session) => session.ownerId === ownerId);
6659
+ return Promise.all(owned.map((session) => this.summary(session)));
6660
+ }
6661
+ async navigate(id, ownerId, rawUrl, signal) {
6662
+ await assertBrowserUrlAllowed(rawUrl, {
6663
+ allowPrivateHosts: this.allowPrivateHosts,
6664
+ allowedPrivateOrigins: this.allowedPrivateOrigins,
6665
+ navigation: true
6666
+ });
6667
+ const session = this.requireOwned(id, ownerId);
6668
+ await this.runPageOperation(
6669
+ session,
6670
+ signal,
6671
+ () => session.page.goto(rawUrl, {
6672
+ waitUntil: "domcontentloaded",
6673
+ timeout: this.operationTimeoutMs
6674
+ })
6675
+ );
6676
+ return this.summary(session);
6677
+ }
6678
+ async snapshot(id, ownerId, signal) {
6679
+ const session = this.requireOwned(id, ownerId);
6680
+ const raw = await this.runPageOperation(session, signal, async () => {
6681
+ try {
6682
+ return await session.page.locator("body").ariaSnapshot({ timeout: this.operationTimeoutMs });
6683
+ } catch {
6684
+ return await session.page.locator("body").innerText({ timeout: this.operationTimeoutMs });
6685
+ }
6686
+ });
6687
+ const aria = redactBrowserText(raw);
6688
+ const truncated = aria.length > this.maxSnapshotChars;
6689
+ return {
6690
+ session: await this.summary(session),
6691
+ aria: truncated ? `${aria.slice(0, this.maxSnapshotChars)}
6692
+ \u2026[snapshot truncated]` : aria,
6693
+ console: session.console.map((entry) => ({ ...entry })),
6694
+ network: session.network.map((entry) => ({ ...entry })),
6695
+ truncated
6696
+ };
6697
+ }
6698
+ async screenshot(id, ownerId, input, signal) {
6699
+ const session = this.requireOwned(id, ownerId);
6700
+ const bytes = await this.runPageOperation(
6701
+ session,
6702
+ signal,
6703
+ () => input.selector ? session.page.locator(input.selector).screenshot({ type: "png", timeout: this.operationTimeoutMs }) : session.page.screenshot({ type: "png", fullPage: input.fullPage ?? false })
6704
+ );
6705
+ signal.throwIfAborted();
6706
+ return this.artifacts.write(session.id, "screenshot", "png", "image/png", bytes);
6707
+ }
6708
+ async click(id, ownerId, selector, signal) {
6709
+ const session = this.requireOwned(id, ownerId);
6710
+ await this.runPageOperation(
6711
+ session,
6712
+ signal,
6713
+ () => session.page.locator(selector).click({ timeout: this.operationTimeoutMs })
6714
+ );
6715
+ }
6716
+ async type(id, ownerId, selector, text, signal) {
6717
+ const session = this.requireOwned(id, ownerId);
6718
+ await this.runPageOperation(
6719
+ session,
6720
+ signal,
6721
+ () => session.page.locator(selector).fill(text, { timeout: this.operationTimeoutMs })
6722
+ );
6723
+ }
6724
+ async select(id, ownerId, selector, value, signal) {
6725
+ const session = this.requireOwned(id, ownerId);
6726
+ await this.runPageOperation(
6727
+ session,
6728
+ signal,
6729
+ () => session.page.locator(selector).selectOption(value, { timeout: this.operationTimeoutMs })
6730
+ );
6731
+ }
6732
+ async press(id, ownerId, key, signal) {
6733
+ const session = this.requireOwned(id, ownerId);
6734
+ await this.runPageOperation(session, signal, () => session.page.keyboard.press(key));
6735
+ }
6736
+ async hover(id, ownerId, selector, signal) {
6737
+ const session = this.requireOwned(id, ownerId);
6738
+ await this.runPageOperation(
6739
+ session,
6740
+ signal,
6741
+ () => session.page.locator(selector).hover({ timeout: this.operationTimeoutMs })
6742
+ );
6743
+ }
6744
+ async wait(id, ownerId, input, signal) {
6745
+ const session = this.requireOwned(id, ownerId);
6746
+ const timeout = Math.min(
6747
+ Math.max(input.timeoutMs ?? this.operationTimeoutMs, 0),
6748
+ this.operationTimeoutMs
6749
+ );
6750
+ await this.runPageOperation(
6751
+ session,
6752
+ signal,
6753
+ () => input.selector ? session.page.locator(input.selector).waitFor({ state: "visible", timeout }) : session.page.waitForTimeout(timeout)
6754
+ );
6755
+ }
6756
+ async drag(id, ownerId, from, to, signal) {
6757
+ const session = this.requireOwned(id, ownerId);
6758
+ await this.runPageOperation(
6759
+ session,
6760
+ signal,
6761
+ () => session.page.locator(from).dragTo(session.page.locator(to), {
6762
+ timeout: this.operationTimeoutMs
6763
+ })
6764
+ );
6765
+ }
6766
+ async upload(id, ownerId, selector, files, projectRoot, signal) {
6767
+ const session = this.requireOwned(id, ownerId);
6768
+ const root = path11.resolve(projectRoot);
6769
+ const realRoot = await fs8.realpath(root);
6770
+ const resolved = [];
6771
+ for (const file of files) {
6772
+ const absolute = path11.resolve(root, file);
6773
+ const relative12 = path11.relative(root, absolute);
6774
+ if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
6775
+ throw new Error("browser: upload files must stay inside the project root");
6776
+ }
6777
+ const realFile = await fs8.realpath(absolute);
6778
+ const realRelative = path11.relative(realRoot, realFile);
6779
+ if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
6780
+ throw new Error("browser: upload files must not escape the project root through a symlink");
6781
+ }
6782
+ const stat17 = await fs8.stat(realFile);
6783
+ if (!stat17.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
6784
+ resolved.push(realFile);
6785
+ }
6786
+ await this.runPageOperation(
6787
+ session,
6788
+ signal,
6789
+ () => session.page.locator(selector).setInputFiles(resolved, { timeout: this.operationTimeoutMs })
6790
+ );
6791
+ }
6792
+ async evaluate(id, ownerId, expression, signal) {
6793
+ const session = this.requireOwned(id, ownerId);
6794
+ const result = await this.runPageOperation(
6795
+ session,
6796
+ signal,
6797
+ () => session.page.evaluate(expression)
6798
+ );
6799
+ const serialized = JSON.stringify(result);
6800
+ if (serialized && serialized.length > this.maxSnapshotChars) {
6801
+ throw new Error(`browser: evaluation result exceeds ${this.maxSnapshotChars} characters`);
6802
+ }
6803
+ return result;
6804
+ }
6805
+ async close(id, ownerId) {
6806
+ const session = this.requireOwned(id, ownerId);
6807
+ let trace;
6808
+ try {
6809
+ if (session.tracing) {
6810
+ const target = this.artifacts.pathFor(session.id, "zip");
6811
+ await fs8.mkdir(path11.dirname(target.path), { recursive: true });
6812
+ await session.context.tracing.stop({ path: target.path });
6813
+ trace = await this.artifacts.describe(target.id, "trace", target.path, "application/zip");
6814
+ }
6815
+ } finally {
6816
+ await session.context.close().catch(() => void 0);
6817
+ this.sessions.delete(id);
6818
+ await this.closeBrowserIfIdle();
6819
+ }
6820
+ return trace ? { trace } : {};
6821
+ }
6822
+ async closeOwner(ownerId) {
6823
+ const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
6824
+ for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
6825
+ }
6826
+ async dispose() {
6827
+ for (const session of [...this.sessions.values()]) {
6828
+ await session.context.close().catch(() => void 0);
6829
+ }
6830
+ this.sessions.clear();
6831
+ const browser = this.browser;
6832
+ this.browser = void 0;
6833
+ this.launching = void 0;
6834
+ await browser?.close().catch(() => void 0);
6835
+ await this.networkProxy.close();
6836
+ }
6837
+ async ensureBrowser(signal) {
6838
+ if (this.browser?.isConnected()) return this.browser;
6839
+ if (!this.launching) {
6840
+ this.launching = this.launcher(this.headless).then((browser) => {
6841
+ this.browser = browser;
6842
+ browser.on("disconnected", () => {
6843
+ if (this.browser === browser) this.browser = void 0;
6844
+ for (const [id, session] of this.sessions) {
6845
+ if (session.context.browser() === browser) this.sessions.delete(id);
6846
+ }
6847
+ });
6848
+ return browser;
6849
+ });
6850
+ }
6851
+ try {
6852
+ return await abortable(
6853
+ signal,
6854
+ () => this.launching,
6855
+ async () => {
6856
+ const browser = await this.launching?.catch(() => void 0);
6857
+ await browser?.close().catch(() => void 0);
6858
+ }
6859
+ );
6860
+ } finally {
6861
+ this.launching = void 0;
6862
+ }
6863
+ }
6864
+ requireOwned(id, ownerId) {
6865
+ const session = this.sessions.get(id);
6866
+ if (!session) throw new Error(`browser: unknown session "${id}"`);
6867
+ if (session.ownerId !== ownerId) throw new Error("browser: session belongs to another agent");
6868
+ session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
6869
+ return session;
6870
+ }
6871
+ async summary(session) {
6872
+ return {
6873
+ id: session.id,
6874
+ ownerId: session.ownerId,
6875
+ url: safeBrowserUrl(session.page.url()),
6876
+ title: redactBrowserText(await session.page.title().catch(() => "")),
6877
+ createdAt: session.createdAt,
6878
+ lastUsedAt: session.lastUsedAt,
6879
+ tracing: session.tracing
6880
+ };
6881
+ }
6882
+ attachEvidenceCollectors(session) {
6883
+ session.page.on("console", (message) => {
6884
+ pushBounded(
6885
+ session.console,
6886
+ {
6887
+ level: message.type(),
6888
+ text: redactBrowserText(message.text()).slice(0, 4096),
6889
+ at: (/* @__PURE__ */ new Date()).toISOString()
6890
+ },
6891
+ this.maxConsoleEntries
6892
+ );
6893
+ });
6894
+ session.page.on("response", (response) => {
6895
+ this.pushNetwork(session, {
6896
+ method: response.request().method(),
6897
+ url: safeBrowserUrl(response.url()),
6898
+ status: response.status(),
6899
+ at: (/* @__PURE__ */ new Date()).toISOString()
6900
+ });
6901
+ });
6902
+ session.page.on("requestfailed", (request2) => {
6903
+ this.pushNetwork(session, {
6904
+ method: request2.method(),
6905
+ url: safeBrowserUrl(request2.url()),
6906
+ failed: true,
6907
+ at: (/* @__PURE__ */ new Date()).toISOString()
6908
+ });
6909
+ });
6910
+ }
6911
+ pushNetwork(session, entry) {
6912
+ pushBounded(session.network, entry, this.maxNetworkEntries);
6913
+ }
6914
+ async runPageOperation(session, signal, operation) {
6915
+ signal.throwIfAborted();
6916
+ session.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
6917
+ return abortable(signal, operation, async () => {
6918
+ await session.context.close().catch(() => void 0);
6919
+ this.sessions.delete(session.id);
6920
+ await this.closeBrowserIfIdle();
6921
+ });
6922
+ }
6923
+ async closeBrowserIfIdle() {
6924
+ if (this.sessions.size > 0 || !this.browser) return;
6925
+ const browser = this.browser;
6926
+ this.browser = void 0;
6927
+ await browser.close().catch(() => void 0);
6928
+ }
6929
+ };
6930
+ async function defaultLauncher(headless) {
6931
+ try {
6932
+ const { chromium } = await import("@playwright/test");
6933
+ return await chromium.launch({ headless });
6934
+ } catch (err) {
6935
+ const detail = err instanceof Error ? err.message : String(err);
6936
+ throw new Error(
6937
+ `browser: Playwright Chromium is unavailable (${detail}). Run "pnpm exec playwright install chromium".`
6938
+ );
6939
+ }
6940
+ }
6941
+ async function browserInstallationDiagnostics() {
6942
+ try {
6943
+ const { chromium } = await import("@playwright/test");
6944
+ const executablePath = chromium.executablePath();
6945
+ await fs8.access(executablePath);
6946
+ return { available: true, executablePath, message: "Playwright Chromium is available." };
6947
+ } catch {
6948
+ return {
6949
+ available: false,
6950
+ message: 'Playwright Chromium is unavailable. Run "pnpm exec playwright install chromium".'
6951
+ };
6952
+ }
6953
+ }
6954
+ function pushBounded(target, value, limit) {
6955
+ target.push(value);
6956
+ if (target.length > limit) target.splice(0, target.length - limit);
6957
+ }
6958
+ async function abortable(signal, operation, onAbort) {
6959
+ signal.throwIfAborted();
6960
+ return new Promise((resolve14, reject) => {
6961
+ let settled = false;
6962
+ let aborting = false;
6963
+ const finish = (fn) => {
6964
+ if (settled) return;
6965
+ settled = true;
6966
+ signal.removeEventListener("abort", abort);
6967
+ fn();
6968
+ };
6969
+ const abort = () => {
6970
+ aborting = true;
6971
+ void Promise.resolve(onAbort()).finally(() => {
6972
+ finish(() => reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted")));
6973
+ });
6974
+ };
6975
+ signal.addEventListener("abort", abort, { once: true });
6976
+ operation().then(
6977
+ (value) => {
6978
+ if (!aborting) finish(() => resolve14(value));
6979
+ },
6980
+ (err) => {
6981
+ if (!aborting) finish(() => reject(err));
6982
+ }
6983
+ );
6984
+ });
6985
+ }
6986
+
6987
+ // src/browser/tools.ts
6988
+ var managers = /* @__PURE__ */ new Map();
6989
+ var cleanupSignalByContext = /* @__PURE__ */ new WeakMap();
6990
+ function managerFor(ctx) {
6991
+ const root = path12.resolve(ctx.projectRoot);
6992
+ let manager = managers.get(root);
6993
+ if (!manager) {
6994
+ const wpaths = resolveWstackPaths({ projectRoot: root });
6995
+ manager = new BrowserSessionManager({
6996
+ artifactRoot: path12.join(wpaths.projectDir, "browser-artifacts"),
6997
+ allowedPrivateOrigins: parsePrivateOriginAllowlist(
6998
+ process.env["WRONGSTACK_BROWSER_PRIVATE_ORIGINS"]
6999
+ )
7000
+ });
7001
+ managers.set(root, manager);
7002
+ }
7003
+ const runSignal = ctx.signal;
7004
+ if (cleanupSignalByContext.get(ctx) !== runSignal) {
7005
+ cleanupSignalByContext.set(ctx, runSignal);
7006
+ const managerForRun = manager;
7007
+ const ownerForRun = owner(ctx);
7008
+ ctx.registerAbortHook?.(async () => {
7009
+ if (cleanupSignalByContext.get(ctx) === runSignal) {
7010
+ cleanupSignalByContext.delete(ctx);
7011
+ }
7012
+ await managerForRun.closeOwner(ownerForRun);
7013
+ });
7014
+ }
7015
+ return manager;
7016
+ }
7017
+ function owner(ctx) {
7018
+ return ctx.agentId || "leader";
7019
+ }
7020
+ function json(value) {
7021
+ return JSON.stringify(value, null, 2);
7022
+ }
7023
+ var sessionIdSchema = {
7024
+ type: "string",
7025
+ minLength: 1,
7026
+ description: "Browser session id returned by browser_open."
7027
+ };
7028
+ var browserOpenTool = {
7029
+ name: "browser_open",
7030
+ description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
7031
+ usageHint: "browser_open({ url?, width?, height?, trace? })",
7032
+ permission: "confirm",
7033
+ mutating: true,
7034
+ riskTier: "standard",
7035
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7036
+ subjectKey: "url",
7037
+ inputSchema: {
7038
+ type: "object",
7039
+ properties: {
7040
+ url: { type: "string", description: "Optional absolute http(s) URL." },
7041
+ width: { type: "integer", minimum: 320, maximum: 3840 },
7042
+ height: { type: "integer", minimum: 240, maximum: 2160 },
7043
+ trace: { type: "boolean", description: "Capture a Playwright trace; defaults to true." }
7044
+ }
7045
+ },
7046
+ async execute(input, ctx, opts) {
7047
+ const viewport = input.width || input.height ? { width: input.width ?? 1440, height: input.height ?? 900 } : void 0;
7048
+ return json(
7049
+ await managerFor(ctx).open(
7050
+ owner(ctx),
7051
+ { url: input.url, viewport, trace: input.trace },
7052
+ opts.signal
7053
+ )
7054
+ );
7055
+ }
7056
+ };
7057
+ var browserListTool = {
7058
+ name: "browser_list",
7059
+ description: "List browser sessions owned by the current agent without exposing other agents sessions.",
7060
+ permission: "auto",
7061
+ mutating: false,
7062
+ riskTier: "safe",
7063
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7064
+ inputSchema: { type: "object", properties: {} },
7065
+ async execute(_input, ctx) {
7066
+ return json(await managerFor(ctx).list(owner(ctx)));
7067
+ }
7068
+ };
7069
+ var browserStatusTool = {
7070
+ name: "browser_status",
7071
+ description: "Check whether first-party Playwright Chromium is installed and ready to launch.",
7072
+ permission: "auto",
7073
+ mutating: false,
7074
+ riskTier: "safe",
7075
+ capabilities: [ToolCapabilities.TOOL_META],
7076
+ inputSchema: { type: "object", properties: {} },
7077
+ async execute() {
7078
+ return json(await browserInstallationDiagnostics());
7079
+ }
7080
+ };
7081
+ var browserNavigateTool = {
7082
+ name: "browser_navigate",
7083
+ description: "Navigate an owned browser session to an approved http(s) URL.",
7084
+ usageHint: "browser_navigate({ sessionId, url })",
7085
+ permission: "confirm",
7086
+ mutating: true,
7087
+ riskTier: "standard",
7088
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7089
+ subjectKey: "url",
7090
+ inputSchema: {
7091
+ type: "object",
7092
+ properties: { sessionId: sessionIdSchema, url: { type: "string", minLength: 1 } },
7093
+ required: ["sessionId", "url"]
7094
+ },
7095
+ async execute(input, ctx, opts) {
7096
+ return json(
7097
+ await managerFor(ctx).navigate(input.sessionId, owner(ctx), input.url, opts.signal)
7098
+ );
7099
+ }
7100
+ };
7101
+ var browserSnapshotTool = {
7102
+ name: "browser_snapshot",
7103
+ description: "Return bounded accessibility state plus redacted console and network summaries.",
7104
+ usageHint: "browser_snapshot({ sessionId })",
7105
+ permission: "auto",
7106
+ mutating: false,
7107
+ riskTier: "safe",
7108
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7109
+ inputSchema: {
7110
+ type: "object",
7111
+ properties: { sessionId: sessionIdSchema },
7112
+ required: ["sessionId"]
7113
+ },
7114
+ async execute(input, ctx, opts) {
7115
+ return json(await managerFor(ctx).snapshot(input.sessionId, owner(ctx), opts.signal));
7116
+ }
7117
+ };
7118
+ var browserScreenshotTool = {
7119
+ name: "browser_screenshot",
7120
+ description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
7121
+ usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
7122
+ permission: "confirm",
7123
+ mutating: true,
7124
+ riskTier: "safe",
7125
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7126
+ inputSchema: {
7127
+ type: "object",
7128
+ properties: {
7129
+ sessionId: sessionIdSchema,
7130
+ fullPage: { type: "boolean" },
7131
+ selector: { type: "string", minLength: 1 }
7132
+ },
7133
+ required: ["sessionId"]
7134
+ },
7135
+ async execute(input, ctx, opts) {
7136
+ return json(
7137
+ await managerFor(ctx).screenshot(
7138
+ input.sessionId,
7139
+ owner(ctx),
7140
+ { fullPage: input.fullPage, selector: input.selector },
7141
+ opts.signal
7142
+ )
7143
+ );
7144
+ }
7145
+ };
7146
+ function selectorTool(name, description, run) {
7147
+ return {
7148
+ name,
7149
+ description,
7150
+ usageHint: `${name}({ sessionId, selector })`,
7151
+ permission: "confirm",
7152
+ mutating: true,
7153
+ riskTier: "standard",
7154
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7155
+ inputSchema: {
7156
+ type: "object",
7157
+ properties: { sessionId: sessionIdSchema, selector: { type: "string", minLength: 1 } },
7158
+ required: ["sessionId", "selector"]
7159
+ },
7160
+ async execute(input, ctx, opts) {
7161
+ await run(managerFor(ctx), input, owner(ctx), opts.signal);
7162
+ return "ok";
7163
+ }
7164
+ };
7165
+ }
7166
+ var browserClickTool = selectorTool(
7167
+ "browser_click",
7168
+ "Click an element in an owned browser session.",
7169
+ (manager, input, ownerId, signal) => manager.click(input.sessionId, ownerId, input.selector, signal)
7170
+ );
7171
+ var browserHoverTool = selectorTool(
7172
+ "browser_hover",
7173
+ "Hover an element in an owned browser session.",
7174
+ (manager, input, ownerId, signal) => manager.hover(input.sessionId, ownerId, input.selector, signal)
7175
+ );
7176
+ var browserTypeTool = {
7177
+ name: "browser_type",
7178
+ 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.",
7179
+ usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
7180
+ permission: "confirm",
7181
+ mutating: true,
7182
+ riskTier: "standard",
7183
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7184
+ inputSchema: {
7185
+ type: "object",
7186
+ properties: {
7187
+ sessionId: sessionIdSchema,
7188
+ selector: { type: "string", minLength: 1 },
7189
+ text: { type: "string" },
7190
+ secretEnv: {
7191
+ type: "string",
7192
+ pattern: "^[A-Z_][A-Z0-9_]*$",
7193
+ description: "Host environment variable resolved only at execution time."
7194
+ }
7195
+ },
7196
+ required: ["sessionId", "selector"]
7197
+ },
7198
+ validate(input) {
7199
+ return input.text === void 0 === (input.secretEnv === void 0) ? ["exactly one of text or secretEnv is required"] : [];
7200
+ },
7201
+ async execute(input, ctx, opts) {
7202
+ const value = input.text ?? (input.secretEnv ? process.env[input.secretEnv] : void 0);
7203
+ if (value === void 0) {
7204
+ throw new Error(`browser: secret environment variable "${input.secretEnv ?? ""}" is not set`);
7205
+ }
7206
+ await managerFor(ctx).type(input.sessionId, owner(ctx), input.selector, value, opts.signal);
7207
+ return "ok";
7208
+ }
7209
+ };
7210
+ var browserSelectTool = {
7211
+ name: "browser_select",
7212
+ description: "Select an option in an owned browser session.",
7213
+ usageHint: "browser_select({ sessionId, selector, value })",
7214
+ permission: "confirm",
7215
+ mutating: true,
7216
+ riskTier: "standard",
7217
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7218
+ inputSchema: {
7219
+ type: "object",
7220
+ properties: {
7221
+ sessionId: sessionIdSchema,
7222
+ selector: { type: "string", minLength: 1 },
7223
+ value: { type: "string" }
7224
+ },
7225
+ required: ["sessionId", "selector", "value"]
7226
+ },
7227
+ async execute(input, ctx, opts) {
7228
+ await managerFor(ctx).select(
7229
+ input.sessionId,
7230
+ owner(ctx),
7231
+ input.selector,
7232
+ input.value,
7233
+ opts.signal
7234
+ );
7235
+ return "ok";
7236
+ }
7237
+ };
7238
+ var browserPressTool = {
7239
+ name: "browser_press",
7240
+ description: "Press a keyboard key in an owned browser session.",
7241
+ usageHint: "browser_press({ sessionId, key })",
7242
+ permission: "confirm",
7243
+ mutating: true,
7244
+ riskTier: "standard",
7245
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7246
+ inputSchema: {
7247
+ type: "object",
7248
+ properties: { sessionId: sessionIdSchema, key: { type: "string", minLength: 1 } },
7249
+ required: ["sessionId", "key"]
7250
+ },
7251
+ async execute(input, ctx, opts) {
7252
+ await managerFor(ctx).press(input.sessionId, owner(ctx), input.key, opts.signal);
7253
+ return "ok";
7254
+ }
7255
+ };
7256
+ var browserDragTool = {
7257
+ name: "browser_drag",
7258
+ description: "Drag one element to another in an owned browser session.",
7259
+ usageHint: "browser_drag({ sessionId, from, to })",
7260
+ permission: "confirm",
7261
+ mutating: true,
7262
+ riskTier: "standard",
7263
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7264
+ inputSchema: {
7265
+ type: "object",
7266
+ properties: {
7267
+ sessionId: sessionIdSchema,
7268
+ from: { type: "string", minLength: 1 },
7269
+ to: { type: "string", minLength: 1 }
7270
+ },
7271
+ required: ["sessionId", "from", "to"]
7272
+ },
7273
+ async execute(input, ctx, opts) {
7274
+ await managerFor(ctx).drag(input.sessionId, owner(ctx), input.from, input.to, opts.signal);
7275
+ return "ok";
7276
+ }
7277
+ };
7278
+ var browserWaitTool = {
7279
+ name: "browser_wait",
7280
+ description: "Wait for an element or a bounded duration in an owned browser session.",
7281
+ usageHint: "browser_wait({ sessionId, selector?, timeoutMs? })",
7282
+ permission: "auto",
7283
+ mutating: false,
7284
+ riskTier: "safe",
7285
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7286
+ inputSchema: {
7287
+ type: "object",
7288
+ properties: {
7289
+ sessionId: sessionIdSchema,
7290
+ selector: { type: "string", minLength: 1 },
7291
+ timeoutMs: { type: "integer", minimum: 0, maximum: 3e4 }
7292
+ },
7293
+ required: ["sessionId"]
7294
+ },
7295
+ async execute(input, ctx, opts) {
7296
+ await managerFor(ctx).wait(
7297
+ input.sessionId,
7298
+ owner(ctx),
7299
+ { selector: input.selector, timeoutMs: input.timeoutMs },
7300
+ opts.signal
7301
+ );
7302
+ return "ok";
7303
+ }
7304
+ };
7305
+ var browserEvaluateTool = {
7306
+ name: "browser_evaluate",
7307
+ description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
7308
+ usageHint: "browser_evaluate({ sessionId, expression })",
7309
+ permission: "confirm",
7310
+ mutating: true,
7311
+ riskTier: "destructive",
7312
+ capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
7313
+ inputSchema: {
7314
+ type: "object",
7315
+ properties: {
7316
+ sessionId: sessionIdSchema,
7317
+ expression: { type: "string", minLength: 1, maxLength: 16384 }
7318
+ },
7319
+ required: ["sessionId", "expression"]
7320
+ },
7321
+ async execute(input, ctx, opts) {
7322
+ return json(
7323
+ await managerFor(ctx).evaluate(input.sessionId, owner(ctx), input.expression, opts.signal)
7324
+ );
7325
+ }
7326
+ };
7327
+ var browserUploadTool = {
7328
+ name: "browser_upload",
7329
+ description: "Upload project-local files through a file input in an owned browser session.",
7330
+ usageHint: "browser_upload({ sessionId, selector, files })",
7331
+ permission: "confirm",
7332
+ mutating: true,
7333
+ riskTier: "destructive",
7334
+ capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
7335
+ inputSchema: {
7336
+ type: "object",
7337
+ properties: {
7338
+ sessionId: sessionIdSchema,
7339
+ selector: { type: "string", minLength: 1 },
7340
+ files: { type: "array", minItems: 1, maxItems: 20, items: { type: "string", minLength: 1 } }
7341
+ },
7342
+ required: ["sessionId", "selector", "files"]
7343
+ },
7344
+ async execute(input, ctx, opts) {
7345
+ await managerFor(ctx).upload(
7346
+ input.sessionId,
7347
+ owner(ctx),
7348
+ input.selector,
7349
+ input.files,
7350
+ ctx.projectRoot,
7351
+ opts.signal
7352
+ );
7353
+ return "ok";
7354
+ }
7355
+ };
7356
+ var browserCloseTool = {
7357
+ name: "browser_close",
7358
+ description: "Close an owned browser session, reclaim its context, and return sensitive trace metadata.",
7359
+ usageHint: "browser_close({ sessionId })",
7360
+ permission: "auto",
7361
+ mutating: false,
7362
+ riskTier: "safe",
7363
+ capabilities: [ToolCapabilities.NET_OUTBOUND],
7364
+ inputSchema: {
7365
+ type: "object",
7366
+ properties: { sessionId: sessionIdSchema },
7367
+ required: ["sessionId"]
7368
+ },
7369
+ async execute(input, ctx) {
7370
+ return json(await managerFor(ctx).close(input.sessionId, owner(ctx)));
7371
+ }
7372
+ };
7373
+ var browserTools = [
7374
+ browserOpenTool,
7375
+ browserStatusTool,
7376
+ browserListTool,
7377
+ browserNavigateTool,
7378
+ browserSnapshotTool,
7379
+ browserScreenshotTool,
7380
+ browserClickTool,
7381
+ browserTypeTool,
7382
+ browserSelectTool,
7383
+ browserPressTool,
7384
+ browserHoverTool,
7385
+ browserDragTool,
7386
+ browserWaitTool,
7387
+ browserEvaluateTool,
7388
+ browserUploadTool,
7389
+ browserCloseTool
7390
+ ];
7391
+ for (const tool of browserTools) tool.icon = "web";
7392
+ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
7393
+
6162
7394
  // src/codebase-index/writer.ts
6163
7395
  import { expectDefined as expectDefined2 } from "@wrongstack/core";
6164
7396
 
@@ -6245,9 +7477,9 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
6245
7477
  // src/codebase-index/writer.ts
6246
7478
  import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
6247
7479
  import { createRequire } from "node:module";
6248
- import * as fs7 from "node:fs";
6249
- import * as path10 from "node:path";
6250
- import { resolveWstackPaths } from "@wrongstack/core";
7480
+ import * as fs9 from "node:fs";
7481
+ import * as path13 from "node:path";
7482
+ import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
6251
7483
 
6252
7484
  // src/codebase-index/schema.ts
6253
7485
  var SCHEMA_VERSION = 2;
@@ -6376,7 +7608,7 @@ var Bm25Index = class {
6376
7608
  // src/codebase-index/writer.ts
6377
7609
  var DB_FILE = "index.db";
6378
7610
  function resolveIndexDir(projectRoot, override) {
6379
- return override ?? resolveWstackPaths({ projectRoot }).projectCodebaseIndex;
7611
+ return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
6380
7612
  }
6381
7613
  function codebaseIndexDirOverride(ctx) {
6382
7614
  const v = ctx.meta?.["codebaseIndexDir"];
@@ -6491,9 +7723,9 @@ var IndexStore = class {
6491
7723
  }
6492
7724
  constructor(projectRoot, opts = {}) {
6493
7725
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
6494
- fs7.mkdirSync(this.indexDir, { recursive: true });
7726
+ fs9.mkdirSync(this.indexDir, { recursive: true });
6495
7727
  const Database = loadDatabaseSync();
6496
- this.db = new Database(path10.join(this.indexDir, DB_FILE));
7728
+ this.db = new Database(path13.join(this.indexDir, DB_FILE));
6497
7729
  try {
6498
7730
  this.db.exec("PRAGMA journal_mode = WAL");
6499
7731
  this.db.exec("PRAGMA busy_timeout = 5000");
@@ -7072,9 +8304,9 @@ var IndexStore = class {
7072
8304
  }));
7073
8305
  }
7074
8306
  sizeBytes() {
7075
- const dbPath = path10.join(this.indexDir, DB_FILE);
8307
+ const dbPath = path13.join(this.indexDir, DB_FILE);
7076
8308
  try {
7077
- return fs7.statSync(dbPath).size;
8309
+ return fs9.statSync(dbPath).size;
7078
8310
  } catch {
7079
8311
  return 0;
7080
8312
  }
@@ -7089,14 +8321,14 @@ var IndexStore = class {
7089
8321
  };
7090
8322
 
7091
8323
  // src/codebase-index/background-indexer.ts
7092
- import * as fs13 from "node:fs";
8324
+ import * as fs15 from "node:fs";
7093
8325
  import { fileURLToPath } from "node:url";
7094
8326
  import { Worker } from "node:worker_threads";
7095
8327
 
7096
8328
  // src/codebase-index/indexer.ts
7097
8329
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
7098
- import * as fs12 from "node:fs/promises";
7099
- import * as path16 from "node:path";
8330
+ import * as fs14 from "node:fs/promises";
8331
+ import * as path19 from "node:path";
7100
8332
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
7101
8333
 
7102
8334
  // src/codebase-index/ts-parser.ts
@@ -7289,8 +8521,8 @@ function detectLang(file) {
7289
8521
  // src/codebase-index/go-parser.ts
7290
8522
  import { spawn as spawn4 } from "node:child_process";
7291
8523
  import * as os5 from "node:os";
7292
- import * as path11 from "node:path";
7293
- import * as fs8 from "node:fs/promises";
8524
+ import * as path14 from "node:path";
8525
+ import * as fs10 from "node:fs/promises";
7294
8526
  async function parseSymbols2(opts) {
7295
8527
  const { file, content, lang } = opts;
7296
8528
  try {
@@ -7591,10 +8823,10 @@ func formatType(t ast.Expr) string {
7591
8823
  }
7592
8824
  `;
7593
8825
  async function syncGoParse(filePath, content, lang) {
7594
- const tmpDir = await fs8.mkdtemp(path11.join(os5.tmpdir(), "ws-go-parse-"));
8826
+ const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
7595
8827
  try {
7596
- const scriptPath = path11.join(tmpDir, "parse.go");
7597
- await fs8.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
8828
+ const scriptPath = path14.join(tmpDir, "parse.go");
8829
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
7598
8830
  const proc = spawn4("go", ["run", scriptPath], {
7599
8831
  stdio: ["pipe", "pipe", "pipe"],
7600
8832
  windowsHide: true
@@ -7606,8 +8838,8 @@ async function syncGoParse(filePath, content, lang) {
7606
8838
  proc.stdin?.write(content);
7607
8839
  proc.stdin?.end();
7608
8840
  const { code } = await Promise.race([
7609
- new Promise((resolve11) => {
7610
- proc.on("close", (c) => resolve11({ code: c }));
8841
+ new Promise((resolve14) => {
8842
+ proc.on("close", (c) => resolve14({ code: c }));
7611
8843
  }),
7612
8844
  new Promise(
7613
8845
  (_, reject) => setTimeout(() => {
@@ -7637,15 +8869,15 @@ async function syncGoParse(filePath, content, lang) {
7637
8869
  } catch {
7638
8870
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
7639
8871
  } finally {
7640
- await fs8.rm(tmpDir, { recursive: true, force: true });
8872
+ await fs10.rm(tmpDir, { recursive: true, force: true });
7641
8873
  }
7642
8874
  }
7643
8875
 
7644
8876
  // src/codebase-index/py-parser.ts
7645
8877
  import { spawn as spawn5 } from "node:child_process";
7646
- import * as fs9 from "node:fs/promises";
8878
+ import * as fs11 from "node:fs/promises";
7647
8879
  import * as os6 from "node:os";
7648
- import * as path12 from "node:path";
8880
+ import * as path15 from "node:path";
7649
8881
  async function parseSymbols3(opts) {
7650
8882
  const { file, content, lang } = opts;
7651
8883
  try {
@@ -7861,10 +9093,10 @@ var _cachedScriptPath = null;
7861
9093
  async function syncPyParse(filePath, content, lang) {
7862
9094
  try {
7863
9095
  if (!_cachedScriptPath) {
7864
- const tmpDir = path12.join(os6.tmpdir(), "ws-py-parse");
7865
- await fs9.mkdir(tmpDir, { recursive: true });
7866
- _cachedScriptPath = path12.join(tmpDir, "parse.py");
7867
- await fs9.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9096
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9097
+ await fs11.mkdir(tmpDir, { recursive: true });
9098
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
9099
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
7868
9100
  }
7869
9101
  const proc = spawn5("python", [_cachedScriptPath, filePath], {
7870
9102
  stdio: ["pipe", "pipe", "pipe"],
@@ -7877,8 +9109,8 @@ async function syncPyParse(filePath, content, lang) {
7877
9109
  stdout += chunk.toString();
7878
9110
  });
7879
9111
  const { code } = await Promise.race([
7880
- new Promise((resolve11) => {
7881
- proc.on("close", (c) => resolve11({ code: c }));
9112
+ new Promise((resolve14) => {
9113
+ proc.on("close", (c) => resolve14({ code: c }));
7882
9114
  }),
7883
9115
  new Promise(
7884
9116
  (_, reject) => setTimeout(() => {
@@ -7913,8 +9145,8 @@ async function syncPyParse(filePath, content, lang) {
7913
9145
  // src/codebase-index/rs-parser.ts
7914
9146
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
7915
9147
  import { execFileSync, spawn as spawn6 } from "node:child_process";
7916
- import * as fs10 from "node:fs/promises";
7917
- import * as path13 from "node:path";
9148
+ import * as fs12 from "node:fs/promises";
9149
+ import * as path16 from "node:path";
7918
9150
  async function parseSymbols4(opts) {
7919
9151
  const { file, content, lang } = opts;
7920
9152
  const nativeAvailable = checkNativeParser();
@@ -7927,7 +9159,7 @@ async function parseSymbols4(opts) {
7927
9159
  function checkNativeParser() {
7928
9160
  try {
7929
9161
  execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
7930
- const toolsDir = path13.join(process.cwd(), "tools");
9162
+ const toolsDir = path16.join(process.cwd(), "tools");
7931
9163
  try {
7932
9164
  execFileSync(
7933
9165
  "cargo",
@@ -7937,7 +9169,7 @@ function checkNativeParser() {
7937
9169
  "--format-version",
7938
9170
  "1",
7939
9171
  "--manifest-path",
7940
- path13.join(toolsDir, "Cargo.toml")
9172
+ path16.join(toolsDir, "Cargo.toml")
7941
9173
  ],
7942
9174
  { stdio: "pipe", windowsHide: true }
7943
9175
  );
@@ -7951,13 +9183,13 @@ function checkNativeParser() {
7951
9183
  }
7952
9184
  async function tryNativeParse(file, content) {
7953
9185
  try {
7954
- const toolsDir = path13.join(process.cwd(), "tools");
7955
- const crateDir = path13.join(toolsDir, "syn-parser");
7956
- const tmpFile = path13.join(crateDir, "src", "input.rs");
7957
- await fs10.writeFile(tmpFile, content, "utf8");
9186
+ const toolsDir = path16.join(process.cwd(), "tools");
9187
+ const crateDir = path16.join(toolsDir, "syn-parser");
9188
+ const tmpFile = path16.join(crateDir, "src", "input.rs");
9189
+ await fs12.writeFile(tmpFile, content, "utf8");
7958
9190
  const proc = spawn6(
7959
9191
  "cargo",
7960
- ["run", "--manifest-path", path13.join(toolsDir, "Cargo.toml")],
9192
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
7961
9193
  {
7962
9194
  cwd: process.cwd(),
7963
9195
  stdio: ["pipe", "pipe", "pipe"],
@@ -7969,8 +9201,8 @@ async function tryNativeParse(file, content) {
7969
9201
  stdout += chunk.toString();
7970
9202
  });
7971
9203
  const { code } = await Promise.race([
7972
- new Promise((resolve11) => {
7973
- proc.on("close", (c) => resolve11({ code: c }));
9204
+ new Promise((resolve14) => {
9205
+ proc.on("close", (c) => resolve14({ code: c }));
7974
9206
  }),
7975
9207
  new Promise(
7976
9208
  (_, reject) => setTimeout(() => {
@@ -8061,7 +9293,7 @@ function regexParse(opts) {
8061
9293
 
8062
9294
  // src/codebase-index/json-parser.ts
8063
9295
  import { expectDefined as expectDefined4 } from "@wrongstack/core";
8064
- import * as path14 from "node:path";
9296
+ import * as path17 from "node:path";
8065
9297
  function parseSymbols5(opts) {
8066
9298
  const { file, content, lang } = opts;
8067
9299
  try {
@@ -8073,9 +9305,9 @@ function parseSymbols5(opts) {
8073
9305
  function regexParse2(opts) {
8074
9306
  const { file, content, lang } = opts;
8075
9307
  const symbols = [];
8076
- const basename7 = path14.basename(file).toLowerCase();
8077
- const isPackageJson = basename7 === "package.json";
8078
- const isTsconfig = basename7 === "tsconfig.json" || basename7 === "tsconfig.build.json";
9308
+ const basename8 = path17.basename(file).toLowerCase();
9309
+ const isPackageJson = basename8 === "package.json";
9310
+ const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
8079
9311
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
8080
9312
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
8081
9313
  const lines = content.split("\n");
@@ -8099,11 +9331,11 @@ function regexParse2(opts) {
8099
9331
  const line = lineFromOffset(offset);
8100
9332
  symbols.push(
8101
9333
  makeSymbol({
8102
- name: path14.basename(file),
9334
+ name: path17.basename(file),
8103
9335
  kind: "object",
8104
9336
  line,
8105
9337
  col: 0,
8106
- signature: `"${path14.basename(file)}" = { ... }`,
9338
+ signature: `"${path17.basename(file)}" = { ... }`,
8107
9339
  file,
8108
9340
  lang
8109
9341
  })
@@ -8414,8 +9646,8 @@ function makeSymbol2(opts) {
8414
9646
  }
8415
9647
 
8416
9648
  // src/codebase-index/gitignore.ts
8417
- import * as fs11 from "node:fs/promises";
8418
- import * as path15 from "node:path";
9649
+ import * as fs13 from "node:fs/promises";
9650
+ import * as path18 from "node:path";
8419
9651
  import { compileGlob } from "@wrongstack/core";
8420
9652
  function globBody(glob) {
8421
9653
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
@@ -8461,7 +9693,7 @@ function compileGitignore(lines) {
8461
9693
  async function loadGitignoreMatcher(projectRoot) {
8462
9694
  let lines = [];
8463
9695
  try {
8464
- const raw = await fs11.readFile(path15.join(projectRoot, ".gitignore"), "utf8");
9696
+ const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
8465
9697
  lines = raw.split("\n");
8466
9698
  } catch {
8467
9699
  }
@@ -8472,7 +9704,7 @@ async function loadGitignoreMatcher(projectRoot) {
8472
9704
  var YIELD_EVERY_N = 50;
8473
9705
  var PARALLEL_BATCH = 20;
8474
9706
  function yieldEventLoop() {
8475
- return new Promise((resolve11) => setImmediate(resolve11));
9707
+ return new Promise((resolve14) => setImmediate(resolve14));
8476
9708
  }
8477
9709
  function throwIfAborted(signal) {
8478
9710
  if (!signal?.aborted) return;
@@ -8519,21 +9751,21 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
8519
9751
  }
8520
9752
  let entries;
8521
9753
  try {
8522
- entries = await fs12.readdir(dir, { withFileTypes: true });
9754
+ entries = await fs14.readdir(dir, { withFileTypes: true });
8523
9755
  } catch {
8524
9756
  return;
8525
9757
  }
8526
9758
  dirCount++;
8527
9759
  for (const e of entries) {
8528
9760
  if (ignoreSet.has(e.name)) continue;
8529
- const full = path16.join(dir, e.name);
8530
- const rel = path16.relative(projectRoot, full).replace(/\\/g, "/");
9761
+ const full = path19.join(dir, e.name);
9762
+ const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
8531
9763
  if (e.isDirectory()) {
8532
9764
  if (isGitIgnored(rel, true)) continue;
8533
9765
  await walk(full);
8534
9766
  } else if (e.isFile()) {
8535
9767
  if (isGitIgnored(rel, false)) continue;
8536
- const ext = path16.extname(e.name);
9768
+ const ext = path19.extname(e.name);
8537
9769
  for (const { ext: extName, pat } of globs) {
8538
9770
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
8539
9771
  results.push(full);
@@ -8589,7 +9821,7 @@ async function runIndexerWithStore(store, opts) {
8589
9821
  let files;
8590
9822
  let discoveredFiles = null;
8591
9823
  if (opts.files && opts.files.length > 0) {
8592
- files = opts.files.map((f) => path16.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path16.relative(projectRoot, f).replace(/\\/g, "/"), false));
9824
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
8593
9825
  } else {
8594
9826
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
8595
9827
  discoveredFiles = new Set(files);
@@ -8617,34 +9849,34 @@ async function runIndexerWithStore(store, opts) {
8617
9849
  const statOpts = signal ? { signal } : {};
8618
9850
  const statReadParse = await Promise.allSettled(
8619
9851
  batchFiles.map(async (file) => {
8620
- let stat15;
9852
+ let stat17;
8621
9853
  try {
8622
- stat15 = await fs12.stat(file, statOpts);
9854
+ stat17 = await fs14.stat(file, statOpts);
8623
9855
  } catch (e) {
8624
9856
  if (isAbortError(e)) throw e;
8625
9857
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
8626
9858
  }
8627
- if (!stat15.isFile()) return { file, stat: stat15, lang: "", parsed: null };
9859
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
8628
9860
  const lang = detectLang(file);
8629
- if (!lang) return { file, stat: stat15, lang: "", parsed: null };
9861
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
8630
9862
  const meta = existingMeta.get(file);
8631
- if (!force && meta && meta.mtimeMs === Math.floor(stat15.mtimeMs)) {
8632
- return { file, stat: stat15, lang, parsed: null, skippedMeta: meta };
9863
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
9864
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
8633
9865
  }
8634
9866
  let content;
8635
9867
  try {
8636
- content = await fs12.readFile(file, { encoding: "utf8", signal });
9868
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
8637
9869
  } catch (e) {
8638
9870
  if (isAbortError(e)) throw e;
8639
- return { file, stat: stat15, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
9871
+ return { file, stat: stat17, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
8640
9872
  }
8641
9873
  let parsed;
8642
9874
  try {
8643
9875
  parsed = await parseFile(file, content, lang);
8644
9876
  } catch (e) {
8645
- return { file, stat: stat15, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
9877
+ return { file, stat: stat17, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
8646
9878
  }
8647
- return { file, stat: stat15, lang, parsed, content };
9879
+ return { file, stat: stat17, lang, parsed, content };
8648
9880
  })
8649
9881
  );
8650
9882
  const batchEntries = [];
@@ -8664,7 +9896,7 @@ async function runIndexerWithStore(store, opts) {
8664
9896
  if (result.error.includes("error:")) errors.push(result.error);
8665
9897
  continue;
8666
9898
  }
8667
- const { stat: stat15, lang, parsed } = result;
9899
+ const { stat: stat17, lang, parsed } = result;
8668
9900
  if (result.skippedMeta) {
8669
9901
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
8670
9902
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -8676,7 +9908,7 @@ async function runIndexerWithStore(store, opts) {
8676
9908
  store.upsertFile({
8677
9909
  file,
8678
9910
  lang,
8679
- mtimeMs: Math.floor(stat15.mtimeMs),
9911
+ mtimeMs: Math.floor(stat17.mtimeMs),
8680
9912
  symbolCount: 0,
8681
9913
  lastIndexed: Date.now()
8682
9914
  });
@@ -8688,7 +9920,7 @@ async function runIndexerWithStore(store, opts) {
8688
9920
  store.upsertFile({
8689
9921
  file,
8690
9922
  lang,
8691
- mtimeMs: Math.floor(stat15.mtimeMs),
9923
+ mtimeMs: Math.floor(stat17.mtimeMs),
8692
9924
  symbolCount: 0,
8693
9925
  lastIndexed: Date.now()
8694
9926
  });
@@ -8704,7 +9936,7 @@ async function runIndexerWithStore(store, opts) {
8704
9936
  lang,
8705
9937
  symbols: parsed.symbols,
8706
9938
  refs,
8707
- mtimeMs: Math.floor(stat15.mtimeMs),
9939
+ mtimeMs: Math.floor(stat17.mtimeMs),
8708
9940
  symbolCount: parsed.symbols.length
8709
9941
  });
8710
9942
  deleteForFiles.push(file);
@@ -8892,7 +10124,7 @@ function resolveWorkerUrl() {
8892
10124
  for (const rel of ["./worker.js", "./codebase-index/worker.js"]) {
8893
10125
  try {
8894
10126
  const url = new URL(rel, import.meta.url);
8895
- if (url.protocol === "file:" && fs13.existsSync(fileURLToPath(url))) return url;
10127
+ if (url.protocol === "file:" && fs15.existsSync(fileURLToPath(url))) return url;
8896
10128
  } catch {
8897
10129
  }
8898
10130
  }
@@ -8950,7 +10182,7 @@ function terminateWorker(reason) {
8950
10182
  function callIndexOp(op, args, opts) {
8951
10183
  const w = ensureWorker();
8952
10184
  if (!w) return callInline(op, args, opts);
8953
- return new Promise((resolve11, reject) => {
10185
+ return new Promise((resolve14, reject) => {
8954
10186
  const id = nextRpcId++;
8955
10187
  const timer = setTimeout(() => {
8956
10188
  pending.delete(id);
@@ -8973,7 +10205,7 @@ function callIndexOp(op, args, opts) {
8973
10205
  pending.set(id, {
8974
10206
  resolve: (v) => {
8975
10207
  cleanup();
8976
- resolve11(v);
10208
+ resolve14(v);
8977
10209
  },
8978
10210
  reject: (e) => {
8979
10211
  cleanup();
@@ -9312,8 +10544,8 @@ var codebaseStatsTool = {
9312
10544
  };
9313
10545
 
9314
10546
  // src/design.ts
9315
- import * as fs14 from "node:fs/promises";
9316
- import * as path17 from "node:path";
10547
+ import * as fs16 from "node:fs/promises";
10548
+ import * as path20 from "node:path";
9317
10549
  import {
9318
10550
  applyTokenOverrides,
9319
10551
  getDesignKitLoader,
@@ -9327,17 +10559,17 @@ import {
9327
10559
  setDesignOverrides
9328
10560
  } from "@wrongstack/core";
9329
10561
  async function resolveReal(p) {
9330
- const resolved = path17.resolve(p);
10562
+ const resolved = path20.resolve(p);
9331
10563
  let probe = resolved;
9332
10564
  const missing = [];
9333
10565
  for (; ; ) {
9334
10566
  try {
9335
- return path17.resolve(await fs14.realpath(probe), ...missing);
10567
+ return path20.resolve(await fs16.realpath(probe), ...missing);
9336
10568
  } catch (err) {
9337
10569
  if (err.code === "ENOENT") {
9338
- const parent = path17.dirname(probe);
10570
+ const parent = path20.dirname(probe);
9339
10571
  if (parent === probe) return resolved;
9340
- missing.unshift(path17.basename(probe));
10572
+ missing.unshift(path20.basename(probe));
9341
10573
  probe = parent;
9342
10574
  continue;
9343
10575
  }
@@ -9503,18 +10735,18 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
9503
10735
  outPath: input.out
9504
10736
  });
9505
10737
  const root = await resolveReal(ctx.projectRoot);
9506
- const absResolved = path17.isAbsolute(result.path) ? path17.resolve(result.path) : path17.resolve(path17.join(ctx.projectRoot, result.path));
9507
- const absParent = await resolveReal(path17.dirname(absResolved));
9508
- const abs = path17.join(absParent, path17.basename(absResolved));
9509
- const rel = path17.relative(root, abs);
9510
- if (rel.startsWith("..") || path17.isAbsolute(rel)) {
10738
+ const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
10739
+ const absParent = await resolveReal(path20.dirname(absResolved));
10740
+ const abs = path20.join(absParent, path20.basename(absResolved));
10741
+ const rel = path20.relative(root, abs);
10742
+ if (rel.startsWith("..") || path20.isAbsolute(rel)) {
9511
10743
  throw new Error(
9512
10744
  `design: materialize path "${result.path}" would escape the project root`
9513
10745
  );
9514
10746
  }
9515
10747
  let exists = false;
9516
10748
  try {
9517
- await fs14.access(abs);
10749
+ await fs16.access(abs);
9518
10750
  exists = true;
9519
10751
  } catch {
9520
10752
  }
@@ -9531,8 +10763,8 @@ ${result.content}
9531
10763
  \`\`\``
9532
10764
  };
9533
10765
  }
9534
- await fs14.mkdir(path17.dirname(abs), { recursive: true });
9535
- await fs14.writeFile(abs, result.content);
10766
+ await fs16.mkdir(path20.dirname(abs), { recursive: true });
10767
+ await fs16.writeFile(abs, result.content);
9536
10768
  return {
9537
10769
  action,
9538
10770
  kit: active.kit,
@@ -9582,8 +10814,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
9582
10814
  init_util();
9583
10815
  import { spawn as spawn7 } from "node:child_process";
9584
10816
  import { statSync as statSync2 } from "node:fs";
9585
- import * as fs15 from "node:fs/promises";
9586
- import * as path18 from "node:path";
10817
+ import * as fs17 from "node:fs/promises";
10818
+ import * as path21 from "node:path";
9587
10819
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
9588
10820
  var diffTool = {
9589
10821
  name: "diff",
@@ -9667,18 +10899,18 @@ function findGitDir(cwd) {
9667
10899
  let dir = cwd;
9668
10900
  for (let i = 0; i < 20; i++) {
9669
10901
  try {
9670
- const stat15 = statSync2(path18.join(dir, ".git"));
9671
- if (stat15.isDirectory()) return dir;
10902
+ const stat17 = statSync2(path21.join(dir, ".git"));
10903
+ if (stat17.isDirectory()) return dir;
9672
10904
  } catch {
9673
10905
  }
9674
- const parent = path18.dirname(dir);
10906
+ const parent = path21.dirname(dir);
9675
10907
  if (parent === dir) break;
9676
10908
  dir = parent;
9677
10909
  }
9678
10910
  return null;
9679
10911
  }
9680
10912
  function runGit(args, cwd, signal) {
9681
- return new Promise((resolve11) => {
10913
+ return new Promise((resolve14) => {
9682
10914
  let stdout = "";
9683
10915
  let stderr = "";
9684
10916
  const child = spawn7("git", args, {
@@ -9694,8 +10926,8 @@ function runGit(args, cwd, signal) {
9694
10926
  child.stderr?.on("data", (c) => {
9695
10927
  stderr += c.toString();
9696
10928
  });
9697
- child.on("close", (code) => resolve11({ stdout, stderr, exitCode: code ?? 0 }));
9698
- child.on("error", (e) => resolve11({ stdout: "", stderr: e.message, exitCode: 1 }));
10929
+ child.on("close", (code) => resolve14({ stdout, stderr, exitCode: code ?? 0 }));
10930
+ child.on("error", (e) => resolve14({ stdout: "", stderr: e.message, exitCode: 1 }));
9699
10931
  });
9700
10932
  }
9701
10933
  async function fileDiff(input, ctx, _signal) {
@@ -9712,9 +10944,9 @@ async function fileDiff(input, ctx, _signal) {
9712
10944
  const results = [];
9713
10945
  for (const file of files) {
9714
10946
  const absPath = safeResolve(file, ctx);
9715
- const stat15 = await fs15.stat(absPath).catch(() => null);
9716
- if (!stat15?.isFile()) continue;
9717
- const content = await fs15.readFile(absPath, "utf8");
10947
+ const stat17 = await fs17.stat(absPath).catch(() => null);
10948
+ if (!stat17?.isFile()) continue;
10949
+ const content = await fs17.readFile(absPath, "utf8");
9718
10950
  const lines = content.split(/\r?\n/);
9719
10951
  results.push(formatWithLineNumbers(file, lines));
9720
10952
  }
@@ -9734,7 +10966,7 @@ ${numbered}`;
9734
10966
 
9735
10967
  // src/document.ts
9736
10968
  init_util();
9737
- import * as fs16 from "node:fs/promises";
10969
+ import * as fs18 from "node:fs/promises";
9738
10970
  var documentTool = {
9739
10971
  name: "document",
9740
10972
  category: "Project",
@@ -9778,7 +11010,7 @@ var documentTool = {
9778
11010
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
9779
11011
  for (const absPath of fileList) {
9780
11012
  try {
9781
- const content = await fs16.readFile(absPath, "utf8");
11013
+ const content = await fs18.readFile(absPath, "utf8");
9782
11014
  filesProcessed++;
9783
11015
  const processed = processFile(
9784
11016
  content,
@@ -9814,8 +11046,8 @@ async function resolveFiles(filesInput, cwd) {
9814
11046
  for (const f of files) {
9815
11047
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
9816
11048
  try {
9817
- const stat15 = await fs16.stat(absPath);
9818
- if (stat15.isFile()) resolved.push(absPath);
11049
+ const stat17 = await fs18.stat(absPath);
11050
+ if (stat17.isFile()) resolved.push(absPath);
9819
11051
  } catch {
9820
11052
  }
9821
11053
  }
@@ -9882,10 +11114,426 @@ function processFile(content, absPath, _style, _overwrite, target) {
9882
11114
  return results;
9883
11115
  }
9884
11116
 
11117
+ // src/e2e.ts
11118
+ init_util();
11119
+ import { open, readdir as readdir5 } from "node:fs/promises";
11120
+ import * as path22 from "node:path";
11121
+ async function readBoundedText(filePath, maxBytes) {
11122
+ let handle;
11123
+ try {
11124
+ handle = await open(filePath, "r");
11125
+ const info = await handle.stat();
11126
+ if (!info.isFile() || info.size > maxBytes) return void 0;
11127
+ const buffer = Buffer.alloc(info.size);
11128
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
11129
+ return buffer.subarray(0, bytesRead).toString("utf8");
11130
+ } catch {
11131
+ return void 0;
11132
+ } finally {
11133
+ await handle?.close().catch(() => void 0);
11134
+ }
11135
+ }
11136
+ var CONFIG_NAMES = /* @__PURE__ */ new Map([
11137
+ ["playwright.config.ts", "playwright"],
11138
+ ["playwright.config.js", "playwright"],
11139
+ ["playwright.config.mts", "playwright"],
11140
+ ["playwright.config.mjs", "playwright"],
11141
+ ["playwright.config.cts", "playwright"],
11142
+ ["playwright.config.cjs", "playwright"],
11143
+ ["cypress.config.ts", "cypress"],
11144
+ ["cypress.config.js", "cypress"],
11145
+ ["cypress.config.mts", "cypress"],
11146
+ ["cypress.config.mjs", "cypress"],
11147
+ ["cypress.config.cts", "cypress"],
11148
+ ["cypress.config.cjs", "cypress"],
11149
+ ["cypress.json", "cypress"]
11150
+ ]);
11151
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
11152
+ ".git",
11153
+ ".hg",
11154
+ ".svn",
11155
+ ".wrongstack",
11156
+ "node_modules",
11157
+ "dist",
11158
+ "build",
11159
+ "coverage",
11160
+ "coverage-html",
11161
+ "playwright-report",
11162
+ "test-results",
11163
+ ".next"
11164
+ ]);
11165
+ var MAX_SCAN_DIRECTORIES = 4e3;
11166
+ var MAX_PACKAGE_BYTES = 512 * 1024;
11167
+ var MAX_CONFIG_BYTES = 512 * 1024;
11168
+ var MAX_SPEC_SAMPLES = 20;
11169
+ function relativePath(root, target) {
11170
+ const value = path22.relative(root, target) || ".";
11171
+ return value.split(path22.sep).join("/");
11172
+ }
11173
+ function escapeRegExp(value) {
11174
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11175
+ }
11176
+ function staticString(source, property) {
11177
+ const key = escapeRegExp(property);
11178
+ const direct = source.match(
11179
+ new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
11180
+ );
11181
+ if (direct?.[2] && !direct[2].includes("${")) return direct[2];
11182
+ const sameLineFallback = source.match(
11183
+ new RegExp(`(?:['"]?${key}['"]?)\\s*:[^\\r\\n]{0,300}?(['"\\x60])([^'"\\x60\\r\\n]*)\\1`)
11184
+ );
11185
+ return sameLineFallback?.[2] && !sameLineFallback[2].includes("${") ? sameLineFallback[2] : void 0;
11186
+ }
11187
+ function staticNumber(source, property) {
11188
+ const key = escapeRegExp(property);
11189
+ const match = source.match(new RegExp(`(?:['"]?${key}['"]?)\\s*:\\s*(\\d{2,5})\\b`));
11190
+ return match?.[1] ? Number.parseInt(match[1], 10) : void 0;
11191
+ }
11192
+ function serverHints(framework, source) {
11193
+ if (framework === "playwright" && /\bwebServer\s*:/.test(source)) {
11194
+ const command = staticString(source, "command");
11195
+ const url = staticString(source, "url");
11196
+ const port = staticNumber(source, "port");
11197
+ return [
11198
+ {
11199
+ kind: "managed",
11200
+ ...command ? { command } : {},
11201
+ ...url ? { url } : {},
11202
+ ...port ? { port } : {},
11203
+ source: "static-config",
11204
+ incomplete: !command || !url && !port
11205
+ }
11206
+ ];
11207
+ }
11208
+ const baseUrl = staticString(source, "baseURL") ?? staticString(source, "baseUrl");
11209
+ return baseUrl ? [{ kind: "attach", url: baseUrl, source: "static-config", incomplete: false }] : [];
11210
+ }
11211
+ function configuredSpecPattern(framework, source) {
11212
+ if (framework === "playwright") {
11213
+ const testDir = staticString(source, "testDir") ?? "./tests";
11214
+ return `${testDir.replace(/\/$/, "")}/**/*.{spec,test}.{js,jsx,ts,tsx,mjs,cjs}`;
11215
+ }
11216
+ return staticString(source, "specPattern") ?? "cypress/{e2e,integration}/**/*.cy.{js,jsx,ts,tsx,mjs,cjs}";
11217
+ }
11218
+ async function scanWorkspace(root, maxDepth, signal) {
11219
+ const result = {
11220
+ configs: [],
11221
+ packageFiles: [],
11222
+ scannedDirectories: 0,
11223
+ truncated: false
11224
+ };
11225
+ const queue = [{ directory: root, depth: 0 }];
11226
+ while (queue.length > 0) {
11227
+ signal.throwIfAborted();
11228
+ const current = queue.shift();
11229
+ if (!current) break;
11230
+ result.scannedDirectories += 1;
11231
+ if (result.scannedDirectories > MAX_SCAN_DIRECTORIES) {
11232
+ result.truncated = true;
11233
+ break;
11234
+ }
11235
+ let entries;
11236
+ try {
11237
+ entries = await readdir5(current.directory, { withFileTypes: true });
11238
+ } catch {
11239
+ continue;
11240
+ }
11241
+ for (const entry of entries) {
11242
+ signal.throwIfAborted();
11243
+ const absolutePath = path22.join(current.directory, entry.name);
11244
+ if (entry.isFile()) {
11245
+ if (entry.name === "package.json") result.packageFiles.push(absolutePath);
11246
+ const framework = CONFIG_NAMES.get(entry.name);
11247
+ if (framework) result.configs.push({ framework, absolutePath });
11248
+ } else if (entry.isDirectory() && current.depth < maxDepth && !SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith("coverage-") && !entry.name.startsWith("cov-")) {
11249
+ queue.push({ directory: absolutePath, depth: current.depth + 1 });
11250
+ }
11251
+ }
11252
+ }
11253
+ return result;
11254
+ }
11255
+ async function readPackageInfo(packagePath) {
11256
+ try {
11257
+ const raw = await readBoundedText(packagePath, MAX_PACKAGE_BYTES);
11258
+ if (raw === void 0) return void 0;
11259
+ const parsed = JSON.parse(raw);
11260
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? Object.fromEntries(
11261
+ Object.entries(parsed.scripts).filter(
11262
+ (entry) => typeof entry[1] === "string"
11263
+ )
11264
+ ) : {};
11265
+ const dependencies = /* @__PURE__ */ new Set();
11266
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
11267
+ const value = parsed[field];
11268
+ if (!value || typeof value !== "object") continue;
11269
+ for (const name of Object.keys(value)) dependencies.add(name);
11270
+ }
11271
+ const declared = typeof parsed.packageManager === "string" ? parsed.packageManager.split("@")[0] ?? "" : "";
11272
+ const packageManager = ["pnpm", "npm", "yarn", "bun"].includes(declared) ? declared : void 0;
11273
+ return { path: packagePath, scripts, dependencies, packageManager };
11274
+ } catch {
11275
+ return void 0;
11276
+ }
11277
+ }
11278
+ function frameworkFromPackage(info) {
11279
+ const found = /* @__PURE__ */ new Set();
11280
+ if (info.dependencies.has("@playwright/test") || info.dependencies.has("playwright")) {
11281
+ found.add("playwright");
11282
+ }
11283
+ if (info.dependencies.has("cypress")) found.add("cypress");
11284
+ for (const command of Object.values(info.scripts)) {
11285
+ if (/\bplaywright(?:\.cmd)?\b/i.test(command)) found.add("playwright");
11286
+ if (/\bcypress(?:\.cmd)?\b/i.test(command)) found.add("cypress");
11287
+ }
11288
+ return [...found];
11289
+ }
11290
+ async function detectPackageManager3(projectRoot, scanRoot, declared) {
11291
+ if (declared) return declared;
11292
+ let directory = projectRoot;
11293
+ while (true) {
11294
+ const names = /* @__PURE__ */ new Set();
11295
+ try {
11296
+ for (const entry of await readdir5(directory)) names.add(entry);
11297
+ } catch {
11298
+ }
11299
+ if (names.has("pnpm-lock.yaml")) return "pnpm";
11300
+ if (names.has("yarn.lock")) return "yarn";
11301
+ if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
11302
+ if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
11303
+ if (directory === scanRoot) break;
11304
+ const parent = path22.dirname(directory);
11305
+ const relativeParent = path22.relative(scanRoot, parent);
11306
+ if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
11307
+ break;
11308
+ }
11309
+ directory = parent;
11310
+ }
11311
+ return "npm";
11312
+ }
11313
+ function matchingScripts(info, framework) {
11314
+ if (!info) return [];
11315
+ const matcher = framework === "playwright" ? /\bplaywright(?:\.cmd)?\b/i : /\bcypress(?:\.cmd)?\b/i;
11316
+ return Object.entries(info.scripts).filter(([, command]) => matcher.test(command)).map(([name]) => name).sort((a, b) => {
11317
+ const preferred = ["test:e2e", "e2e", "e2e:test"];
11318
+ const ai = preferred.indexOf(a);
11319
+ const bi = preferred.indexOf(b);
11320
+ if (ai !== -1 || bi !== -1) return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
11321
+ return a.localeCompare(b);
11322
+ });
11323
+ }
11324
+ function executionPlan(framework, manager, root, configPath, scripts) {
11325
+ const script = scripts[0];
11326
+ if (script) {
11327
+ return {
11328
+ command: manager,
11329
+ args: ["run", script],
11330
+ cwd: root,
11331
+ source: "package-script",
11332
+ script
11333
+ };
11334
+ }
11335
+ const runnerArgs = framework === "playwright" ? ["playwright", "test"] : ["cypress", "run"];
11336
+ if (configPath) {
11337
+ runnerArgs.push(framework === "playwright" ? "--config" : "--config-file", configPath);
11338
+ }
11339
+ return {
11340
+ command: manager === "bun" ? "bunx" : manager,
11341
+ args: manager === "bun" ? runnerArgs : manager === "npm" ? ["exec", "--", ...runnerArgs] : ["exec", ...runnerArgs],
11342
+ cwd: root,
11343
+ source: "framework-default"
11344
+ };
11345
+ }
11346
+ function isSpec(framework, filename) {
11347
+ if (framework === "cypress") return /\.cy\.(?:[cm]?[jt]sx?)$/i.test(filename);
11348
+ return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
11349
+ }
11350
+ async function collectSpecs(root, framework, testDirectory, signal) {
11351
+ const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
11352
+ const samples = [];
11353
+ let count = 0;
11354
+ let scanned = 0;
11355
+ const queue = [...roots];
11356
+ while (queue.length > 0) {
11357
+ signal.throwIfAborted();
11358
+ const directory = queue.shift();
11359
+ if (!directory) break;
11360
+ scanned += 1;
11361
+ if (scanned > MAX_SCAN_DIRECTORIES) return { count, samples, truncated: true };
11362
+ let entries;
11363
+ try {
11364
+ entries = await readdir5(directory, { withFileTypes: true });
11365
+ } catch {
11366
+ continue;
11367
+ }
11368
+ for (const entry of entries) {
11369
+ signal.throwIfAborted();
11370
+ const target = path22.join(directory, entry.name);
11371
+ if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
11372
+ else if (entry.isFile() && isSpec(framework, entry.name)) {
11373
+ count += 1;
11374
+ if (samples.length < MAX_SPEC_SAMPLES) samples.push(relativePath(root, target));
11375
+ }
11376
+ }
11377
+ }
11378
+ return { count, samples, truncated: count > samples.length };
11379
+ }
11380
+ async function readConfig(configPath) {
11381
+ if (!configPath) return "";
11382
+ return await readBoundedText(configPath, MAX_CONFIG_BYTES) ?? "";
11383
+ }
11384
+ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
11385
+ let directory = projectRoot;
11386
+ while (true) {
11387
+ const found = packagesByDirectory.get(directory);
11388
+ if (found) return found;
11389
+ if (directory === scanRoot) return void 0;
11390
+ const parent = path22.dirname(directory);
11391
+ if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
11392
+ directory = parent;
11393
+ }
11394
+ }
11395
+ async function discoverE2EProjects(root, options) {
11396
+ const maxDepth = Math.max(0, Math.min(options.maxDepth ?? 5, 8));
11397
+ const scan = await scanWorkspace(root, maxDepth, options.signal);
11398
+ const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
11399
+ (info) => Boolean(info)
11400
+ );
11401
+ const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
11402
+ const candidates = /* @__PURE__ */ new Map();
11403
+ for (const config of scan.configs) {
11404
+ if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
11405
+ continue;
11406
+ }
11407
+ const projectRoot = path22.dirname(config.absolutePath);
11408
+ candidates.set(`${config.framework}:${projectRoot}`, {
11409
+ framework: config.framework,
11410
+ root: projectRoot,
11411
+ configPath: config.absolutePath
11412
+ });
11413
+ }
11414
+ for (const info of packages) {
11415
+ const projectRoot = path22.dirname(info.path);
11416
+ for (const framework of frameworkFromPackage(info)) {
11417
+ if (options.framework && options.framework !== "all" && framework !== options.framework)
11418
+ continue;
11419
+ const key = `${framework}:${projectRoot}`;
11420
+ if (!candidates.has(key)) candidates.set(key, { framework, root: projectRoot });
11421
+ }
11422
+ }
11423
+ const projects = [];
11424
+ for (const candidate of candidates.values()) {
11425
+ options.signal.throwIfAborted();
11426
+ const info = nearestPackage(candidate.root, packagesByDirectory, root);
11427
+ const source = await readConfig(candidate.configPath);
11428
+ const scripts = matchingScripts(info, candidate.framework);
11429
+ const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
11430
+ const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
11431
+ const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
11432
+ const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
11433
+ const unsafeTestDirectory = Boolean(
11434
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
11435
+ );
11436
+ const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
11437
+ const warnings = [];
11438
+ if (!candidate.configPath)
11439
+ warnings.push("Framework dependency or script found without a config file.");
11440
+ if (candidate.configPath && !source) {
11441
+ warnings.push("Config could not be read or exceeded the static inspection limit.");
11442
+ }
11443
+ if (unsafeTestDirectory) {
11444
+ warnings.push(
11445
+ "Configured testDir resolves outside the requested discovery root and was not scanned."
11446
+ );
11447
+ }
11448
+ const hints = serverHints(candidate.framework, source);
11449
+ if (hints.some((hint) => hint.incomplete)) {
11450
+ warnings.push("Configured server contains dynamic values; only static hints are shown.");
11451
+ }
11452
+ projects.push({
11453
+ framework: candidate.framework,
11454
+ root: relativePath(root, candidate.root),
11455
+ ...candidate.configPath ? { configPath: relativePath(root, candidate.configPath) } : {},
11456
+ ...info ? { packagePath: relativePath(root, info.path) } : {},
11457
+ packageManager: manager,
11458
+ evidence: [
11459
+ ...candidate.configPath ? [`config:${relativePath(root, candidate.configPath)}`] : [],
11460
+ ...info?.dependencies.has(
11461
+ candidate.framework === "playwright" ? "@playwright/test" : "cypress"
11462
+ ) ? [`dependency:${candidate.framework === "playwright" ? "@playwright/test" : "cypress"}`] : [],
11463
+ ...scripts.map((script) => `script:${script}`)
11464
+ ],
11465
+ scripts,
11466
+ serverHints: hints,
11467
+ specPattern: configuredSpecPattern(candidate.framework, source),
11468
+ specCount: specs.count,
11469
+ specSamples: specs.samples,
11470
+ specsTruncated: specs.truncated,
11471
+ execution: executionPlan(
11472
+ candidate.framework,
11473
+ manager,
11474
+ relativePath(root, candidate.root),
11475
+ candidate.configPath ? relativePath(candidate.root, candidate.configPath) : void 0,
11476
+ scripts
11477
+ ),
11478
+ warnings
11479
+ });
11480
+ }
11481
+ projects.sort((a, b) => a.root.localeCompare(b.root) || a.framework.localeCompare(b.framework));
11482
+ return {
11483
+ root,
11484
+ projects,
11485
+ scannedDirectories: scan.scannedDirectories,
11486
+ scanTruncated: scan.truncated,
11487
+ warnings: scan.truncated ? [`Discovery stopped after ${MAX_SCAN_DIRECTORIES} directories; narrow cwd or maxDepth.`] : []
11488
+ };
11489
+ }
11490
+ var e2ePlanTool = {
11491
+ name: "e2e_plan",
11492
+ category: "Code Quality",
11493
+ description: "Discover Playwright and Cypress projects and preview bounded E2E execution plans without loading configs or starting processes.",
11494
+ 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.",
11495
+ permission: "auto",
11496
+ mutating: false,
11497
+ riskTier: "safe",
11498
+ capabilities: ["fs.read"],
11499
+ icon: "test",
11500
+ timeoutMs: 3e4,
11501
+ inputSchema: {
11502
+ type: "object",
11503
+ properties: {
11504
+ cwd: { type: "string", description: "Directory inside the project to inspect." },
11505
+ framework: {
11506
+ type: "string",
11507
+ enum: ["all", "playwright", "cypress"],
11508
+ description: "Optional framework filter; defaults to all."
11509
+ },
11510
+ maxDepth: {
11511
+ type: "integer",
11512
+ minimum: 0,
11513
+ maximum: 8,
11514
+ description: "Maximum workspace discovery depth; defaults to 5."
11515
+ },
11516
+ includeSpecs: {
11517
+ type: "boolean",
11518
+ description: "Count and sample specs; defaults to true."
11519
+ }
11520
+ }
11521
+ },
11522
+ async execute(input, ctx, opts) {
11523
+ const root = await safeResolveReal(input.cwd ?? ".", ctx);
11524
+ return discoverE2EProjects(root, {
11525
+ framework: input.framework,
11526
+ maxDepth: input.maxDepth,
11527
+ includeSpecs: input.includeSpecs,
11528
+ signal: opts.signal
11529
+ });
11530
+ }
11531
+ };
11532
+
9885
11533
  // src/edit.ts
9886
- import * as fs17 from "node:fs/promises";
11534
+ import * as fs19 from "node:fs/promises";
9887
11535
  import {
9888
- atomicWrite,
11536
+ atomicWrite as atomicWrite2,
9889
11537
  detectNewlineStyle,
9890
11538
  normalizeToLf,
9891
11539
  ToolValidationError,
@@ -10098,7 +11746,7 @@ function prefixSimilarity(a, b) {
10098
11746
  }
10099
11747
 
10100
11748
  // src/_syntax-check.ts
10101
- import * as path19 from "node:path";
11749
+ import * as path23 from "node:path";
10102
11750
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
10103
11751
  var MAX_CHECK_CHARS = 15e5;
10104
11752
  var MAX_ERRORS = 5;
@@ -10122,15 +11770,15 @@ async function checkSyntax(filePath, content, previousContent) {
10122
11770
  return { errors, preExisting };
10123
11771
  }
10124
11772
  function isJsoncFile(filePath) {
10125
- const base = path19.basename(filePath).toLowerCase();
11773
+ const base = path23.basename(filePath).toLowerCase();
10126
11774
  if (base.endsWith(".jsonc")) return true;
10127
11775
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
10128
- const dir = path19.basename(path19.dirname(filePath)).toLowerCase();
11776
+ const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
10129
11777
  return dir === ".vscode";
10130
11778
  }
10131
11779
  async function parseErrors(filePath, content) {
10132
11780
  if (content.length > MAX_CHECK_CHARS) return void 0;
10133
- const ext = path19.extname(filePath).toLowerCase();
11781
+ const ext = path23.extname(filePath).toLowerCase();
10134
11782
  if (ext === ".json" || ext === ".jsonc") {
10135
11783
  try {
10136
11784
  JSON.parse(content);
@@ -10157,7 +11805,7 @@ async function parseErrors(filePath, content) {
10157
11805
  ts2.ScriptKind.JSX
10158
11806
  );
10159
11807
  const sourceFile = ts2.createSourceFile(
10160
- path19.basename(filePath),
11808
+ path23.basename(filePath),
10161
11809
  content,
10162
11810
  ts2.ScriptTarget.Latest,
10163
11811
  /* setParentNodes */
@@ -10231,7 +11879,7 @@ var editTool = {
10231
11879
  });
10232
11880
  }
10233
11881
  const absPath = await safeResolveReal(input.path, ctx);
10234
- const stat15 = await fs17.stat(absPath).catch((err) => {
11882
+ const stat17 = await fs19.stat(absPath).catch((err) => {
10235
11883
  if (err.code === "ENOENT") {
10236
11884
  throw new ToolValidationError({
10237
11885
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
@@ -10241,15 +11889,15 @@ var editTool = {
10241
11889
  }
10242
11890
  throw err;
10243
11891
  });
10244
- if (!stat15.isFile()) {
11892
+ if (!stat17.isFile()) {
10245
11893
  throw new ToolValidationError({
10246
11894
  message: `edit: "${input.path}" is not a regular file`,
10247
11895
  field: "path"
10248
11896
  });
10249
11897
  }
10250
11898
  const autoRead = !ctx.hasRead(absPath);
10251
- const original = await fs17.readFile(absPath, "utf8");
10252
- const updated = await fs17.stat(absPath);
11899
+ const original = await fs19.readFile(absPath, "utf8");
11900
+ const updated = await fs19.stat(absPath);
10253
11901
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
10254
11902
  const originalHash = sha256hex(original);
10255
11903
  const lastReadHash = ctx.lastReadHash?.(absPath);
@@ -10271,7 +11919,7 @@ var editTool = {
10271
11919
  });
10272
11920
  }
10273
11921
  }
10274
- if (autoRead && updated.mtimeMs > stat15.mtimeMs + mtimeTolerance) {
11922
+ if (autoRead && updated.mtimeMs > stat17.mtimeMs + mtimeTolerance) {
10275
11923
  throw new ToolValidationError({
10276
11924
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
10277
11925
  field: "path",
@@ -10353,8 +12001,8 @@ Compare this against your old_string and retry with the file's actual text.` : "
10353
12001
  }
10354
12002
  const newFile = toStyle(newFileLf, style);
10355
12003
  opts?.signal?.throwIfAborted();
10356
- await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
10357
- const written = await fs17.stat(absPath);
12004
+ await atomicWrite2(absPath, newFile, { mode: updated.mode & 511 });
12005
+ const written = await fs19.stat(absPath);
10358
12006
  ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
10359
12007
  ctx.session.recordFileChange({
10360
12008
  path: absPath,
@@ -11314,7 +12962,7 @@ var BLOCKED_ARG_PATTERNS = {
11314
12962
  // and glob patterns that could expand to dangerous targets.
11315
12963
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
11316
12964
  // `rm -rf /*` targets the filesystem root. All are blocked.
11317
- rm: [/^\//, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
12965
+ rm: [/^\//, /^[A-Za-z]:[\\/]/, /^~\//, /^~$/, /^\.$/, /^\.\.$/, /\*$/, /\/$/, /\/\*$/, /\.\//],
11318
12966
  // npm/pnpm subcommands are checked separately below. Matching every arg here
11319
12967
  // over-blocked normal dev flows such as `pnpm vitest run ...`.
11320
12968
  npm: [],
@@ -11482,7 +13130,7 @@ var execTool = {
11482
13130
  }
11483
13131
  };
11484
13132
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
11485
- return new Promise((resolve11) => {
13133
+ return new Promise((resolve14) => {
11486
13134
  let stdout = "";
11487
13135
  let stderr = "";
11488
13136
  let killed = false;
@@ -11490,7 +13138,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
11490
13138
  const finish = (result) => {
11491
13139
  if (resolvedOnce.value) return;
11492
13140
  resolvedOnce.value = true;
11493
- resolve11(result);
13141
+ resolve14(result);
11494
13142
  };
11495
13143
  const startedAt = Date.now();
11496
13144
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
@@ -11599,13 +13247,13 @@ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValida
11599
13247
  import TurndownService from "turndown";
11600
13248
 
11601
13249
  // src/_fetch-guard.ts
11602
- import * as dns from "node:dns/promises";
11603
- import * as net from "node:net";
13250
+ import * as dns2 from "node:dns/promises";
13251
+ import * as net3 from "node:net";
11604
13252
  import {
11605
13253
  FetchError,
11606
13254
  ToolValidationError as ToolValidationError2,
11607
- isPrivateIPv4,
11608
- isPrivateIPv6
13255
+ isPrivateIPv4 as isPrivateIPv42,
13256
+ isPrivateIPv6 as isPrivateIPv62
11609
13257
  } from "@wrongstack/core";
11610
13258
  import { Agent, fetch as undiciFetch } from "undici";
11611
13259
  var nativeGlobalFetch = globalThis.fetch;
@@ -11616,13 +13264,13 @@ if (ALLOW_PRIVATE && !process.env["CI"]) {
11616
13264
  );
11617
13265
  }
11618
13266
  function guardedLookup(hostname2, options, callback) {
11619
- dns.lookup(hostname2, { all: true }).then((records) => {
13267
+ dns2.lookup(hostname2, { all: true }).then((records) => {
11620
13268
  const family = options?.family;
11621
13269
  const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
11622
13270
  const list = byFamily.length > 0 ? byFamily : records;
11623
13271
  if (!ALLOW_PRIVATE) {
11624
13272
  for (const r of list) {
11625
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
13273
+ const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
11626
13274
  if (bad) {
11627
13275
  callback(
11628
13276
  Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
@@ -11727,16 +13375,16 @@ async function assertNotPrivate(hostname2) {
11727
13375
  field: "url"
11728
13376
  });
11729
13377
  }
11730
- const ipVersion = net.isIP(host);
13378
+ const ipVersion = net3.isIP(host);
11731
13379
  if (ipVersion === 4) {
11732
- if (isPrivateIPv4(host)) {
13380
+ if (isPrivateIPv42(host)) {
11733
13381
  throw new ToolValidationError2({
11734
13382
  message: `fetch: blocked private/loopback address "${host}"`,
11735
13383
  field: "url"
11736
13384
  });
11737
13385
  }
11738
13386
  } else if (ipVersion === 6) {
11739
- if (isPrivateIPv6(host)) {
13387
+ if (isPrivateIPv62(host)) {
11740
13388
  throw new ToolValidationError2({
11741
13389
  message: `fetch: blocked private/loopback address "${host}"`,
11742
13390
  field: "url"
@@ -11744,9 +13392,9 @@ async function assertNotPrivate(hostname2) {
11744
13392
  }
11745
13393
  } else {
11746
13394
  try {
11747
- const records = await dns.lookup(host, { all: true });
13395
+ const records = await dns2.lookup(host, { all: true });
11748
13396
  for (const r of records) {
11749
- const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
13397
+ const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
11750
13398
  if (bad) {
11751
13399
  throw new ToolValidationError2({
11752
13400
  message: `fetch: resolved to private address ${r.address}`,
@@ -12085,13 +13733,13 @@ var formatTool = {
12085
13733
  }
12086
13734
  };
12087
13735
  async function detectFixer(cwd) {
12088
- const { stat: stat15 } = await import("node:fs/promises");
13736
+ const { stat: stat17 } = await import("node:fs/promises");
12089
13737
  try {
12090
- await stat15(`${cwd}/biome.json`);
13738
+ await stat17(`${cwd}/biome.json`);
12091
13739
  return "biome";
12092
13740
  } catch {
12093
13741
  try {
12094
- await stat15(`${cwd}/.prettierrc`);
13742
+ await stat17(`${cwd}/.prettierrc`);
12095
13743
  return "prettier";
12096
13744
  } catch {
12097
13745
  return "biome";
@@ -12103,7 +13751,7 @@ async function detectFixer(cwd) {
12103
13751
  init_util();
12104
13752
  import { spawn as spawn9 } from "node:child_process";
12105
13753
  import { statSync as statSync3 } from "node:fs";
12106
- import { dirname as dirname9, resolve as resolve8, sep as sep3 } from "node:path";
13754
+ import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
12107
13755
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
12108
13756
  var TIMEOUT_MS2 = 3e4;
12109
13757
  var MAX_OUTPUT3 = 1e5;
@@ -12246,9 +13894,9 @@ function validateWorktreeInput(input, projectRoot) {
12246
13894
  return reject(`unsafe worktree path: ${input.worktreePath}`);
12247
13895
  }
12248
13896
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
12249
- const root = resolve8(projectRoot);
12250
- const abs = resolve8(root, input.worktreePath);
12251
- if (abs !== root && !abs.startsWith(root + sep3)) {
13897
+ const root = resolve11(projectRoot);
13898
+ const abs = resolve11(root, input.worktreePath);
13899
+ if (abs !== root && !abs.startsWith(root + sep4)) {
12252
13900
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
12253
13901
  }
12254
13902
  }
@@ -12259,12 +13907,12 @@ function findGitDir2(cwd, projectRoot) {
12259
13907
  let dir = cwd;
12260
13908
  for (let i = 0; i < 20; i++) {
12261
13909
  try {
12262
- const stat15 = statSync3(`${dir}/.git`);
12263
- if (stat15.isDirectory() || stat15.isFile()) return dir;
13910
+ const stat17 = statSync3(`${dir}/.git`);
13911
+ if (stat17.isDirectory() || stat17.isFile()) return dir;
12264
13912
  } catch {
12265
13913
  }
12266
13914
  if (dir === root) break;
12267
- const parent = dirname9(dir);
13915
+ const parent = dirname12(dir);
12268
13916
  if (parent === dir) break;
12269
13917
  dir = parent;
12270
13918
  }
@@ -12341,7 +13989,7 @@ function buildArgs(input) {
12341
13989
  }
12342
13990
  }
12343
13991
  function runGit2(args, cwd, signal) {
12344
- return new Promise((resolve11) => {
13992
+ return new Promise((resolve14) => {
12345
13993
  let stdout = "";
12346
13994
  let stderr = "";
12347
13995
  const child = spawn9("git", args, {
@@ -12362,7 +14010,7 @@ function runGit2(args, cwd, signal) {
12362
14010
  }
12363
14011
  });
12364
14012
  child.on("error", (err) => {
12365
- resolve11({
14013
+ resolve14({
12366
14014
  command: args[0],
12367
14015
  stdout: normalizeCommandOutput(stdout),
12368
14016
  stderr: err.message,
@@ -12371,7 +14019,7 @@ function runGit2(args, cwd, signal) {
12371
14019
  });
12372
14020
  });
12373
14021
  child.on("close", (code) => {
12374
- resolve11({
14022
+ resolve14({
12375
14023
  command: args[0],
12376
14024
  stdout: normalizeCommandOutput(stdout),
12377
14025
  stderr: normalizeCommandOutput(stderr),
@@ -12383,8 +14031,8 @@ function runGit2(args, cwd, signal) {
12383
14031
  }
12384
14032
 
12385
14033
  // src/glob.ts
12386
- import * as fs18 from "node:fs/promises";
12387
- import * as path20 from "node:path";
14034
+ import * as fs20 from "node:fs/promises";
14035
+ import * as path24 from "node:path";
12388
14036
  import { compileGlob as compileGlob3 } from "@wrongstack/core";
12389
14037
 
12390
14038
  // src/_concurrency.ts
@@ -12456,7 +14104,7 @@ var globTool = {
12456
14104
  return;
12457
14105
  }
12458
14106
  try {
12459
- const st = await fs18.stat(full);
14107
+ const st = await fs20.stat(full);
12460
14108
  if (truncated || results.length >= limit) {
12461
14109
  truncated = true;
12462
14110
  return;
@@ -12477,7 +14125,7 @@ var globTool = {
12477
14125
  }
12478
14126
  let entries;
12479
14127
  try {
12480
- entries = await fs18.readdir(dir, { withFileTypes: true });
14128
+ entries = await fs20.readdir(dir, { withFileTypes: true });
12481
14129
  } catch {
12482
14130
  return;
12483
14131
  }
@@ -12488,7 +14136,7 @@ var globTool = {
12488
14136
  if (DEFAULT_IGNORE2.includes(name)) continue;
12489
14137
  if (ignored.includes(name)) continue;
12490
14138
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
12491
- const full = path20.join(dir, name);
14139
+ const full = path24.join(dir, name);
12492
14140
  if (e.isDirectory()) {
12493
14141
  subdirs.push({ full, rel });
12494
14142
  } else if (e.isFile()) {
@@ -12501,13 +14149,13 @@ var globTool = {
12501
14149
  }
12502
14150
  } else if (e.isSymbolicLink()) {
12503
14151
  try {
12504
- const st = await fs18.stat(full);
14152
+ const st = await fs20.stat(full);
12505
14153
  if (st.isDirectory()) {
12506
- const real = await fs18.realpath(full);
14154
+ const real = await fs20.realpath(full);
12507
14155
  await assertRealInsideRoot(real, ctx);
12508
14156
  subdirs.push({ full, rel });
12509
14157
  } else if (st.isFile()) {
12510
- const real = await fs18.realpath(full);
14158
+ const real = await fs20.realpath(full);
12511
14159
  await assertRealInsideRoot(real, ctx);
12512
14160
  re.lastIndex = 0;
12513
14161
  const relMatch = re.test(rel);
@@ -12532,7 +14180,7 @@ var globTool = {
12532
14180
  };
12533
14181
  async function readGitignore(dir) {
12534
14182
  try {
12535
- const raw = await fs18.readFile(path20.join(dir, ".gitignore"), "utf8");
14183
+ const raw = await fs20.readFile(path24.join(dir, ".gitignore"), "utf8");
12536
14184
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
12537
14185
  } catch {
12538
14186
  return [];
@@ -12542,8 +14190,8 @@ async function readGitignore(dir) {
12542
14190
  // src/grep.ts
12543
14191
  import { expectDefined as expectDefined7 } from "@wrongstack/core";
12544
14192
  import { spawn as spawn10 } from "node:child_process";
12545
- import * as fs19 from "node:fs/promises";
12546
- import * as path21 from "node:path";
14193
+ import * as fs21 from "node:fs/promises";
14194
+ import * as path25 from "node:path";
12547
14195
  import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
12548
14196
 
12549
14197
  // src/_regex.ts
@@ -12688,13 +14336,13 @@ var grepTool = {
12688
14336
  }
12689
14337
  };
12690
14338
  async function detectRg(signal) {
12691
- return new Promise((resolve11) => {
14339
+ return new Promise((resolve14) => {
12692
14340
  try {
12693
14341
  const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
12694
- p.on("error", () => resolve11(false));
12695
- p.on("close", (code) => resolve11(code === 0));
14342
+ p.on("error", () => resolve14(false));
14343
+ p.on("close", (code) => resolve14(code === 0));
12696
14344
  } catch {
12697
- resolve11(false);
14345
+ resolve14(false);
12698
14346
  }
12699
14347
  });
12700
14348
  }
@@ -12845,9 +14493,9 @@ async function runNative(input, base, mode, limit, signal) {
12845
14493
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
12846
14494
  if (globRe) globRe.lastIndex = 0;
12847
14495
  try {
12848
- const stat15 = await fs19.stat(full);
12849
- if (!stat15.isFile() || stat15.size > maxBytes || stopped || signal.aborted) return;
12850
- const file = await fs19.open(full, "r");
14496
+ const stat17 = await fs21.stat(full);
14497
+ if (!stat17.isFile() || stat17.size > maxBytes || stopped || signal.aborted) return;
14498
+ const file = await fs21.open(full, "r");
12851
14499
  try {
12852
14500
  let bytesReadTotal = 0;
12853
14501
  let lineNumber = 0;
@@ -12926,7 +14574,7 @@ async function runNative(input, base, mode, limit, signal) {
12926
14574
  if (stopped || signal.aborted) return;
12927
14575
  let entries;
12928
14576
  try {
12929
- entries = await fs19.readdir(dir, { withFileTypes: true });
14577
+ entries = await fs21.readdir(dir, { withFileTypes: true });
12930
14578
  } catch {
12931
14579
  return;
12932
14580
  }
@@ -12936,7 +14584,7 @@ async function runNative(input, base, mode, limit, signal) {
12936
14584
  if (stopped) return;
12937
14585
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
12938
14586
  if (e.isSymbolicLink()) continue;
12939
- const full = path21.join(dir, e.name);
14587
+ const full = path25.join(dir, e.name);
12940
14588
  if (e.isDirectory()) {
12941
14589
  subdirs.push(full);
12942
14590
  } else if (e.isFile()) {
@@ -12959,7 +14607,7 @@ async function runNative(input, base, mode, limit, signal) {
12959
14607
  init_spawn_stream();
12960
14608
  init_util();
12961
14609
  init_legacy_bridge();
12962
- import { join as join18 } from "node:path";
14610
+ import { join as join21 } from "node:path";
12963
14611
  import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
12964
14612
  var installTool = {
12965
14613
  name: "install",
@@ -13136,16 +14784,16 @@ function resolveManifestPath(cwd, pkgManager) {
13136
14784
  case "pnpm":
13137
14785
  case "yarn":
13138
14786
  case "npm":
13139
- return join18(cwd, "package.json");
14787
+ return join21(cwd, "package.json");
13140
14788
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
13141
14789
  default:
13142
- return join18(cwd, "package.json");
14790
+ return join21(cwd, "package.json");
13143
14791
  }
13144
14792
  }
13145
14793
 
13146
14794
  // src/json.ts
13147
14795
  init_util();
13148
- import * as fs20 from "node:fs/promises";
14796
+ import * as fs22 from "node:fs/promises";
13149
14797
  import { deepMerge as deepMergeCore } from "@wrongstack/core";
13150
14798
  var jsonTool = {
13151
14799
  name: "json",
@@ -13220,7 +14868,7 @@ async function executeParse(input, ctx) {
13220
14868
  let raw;
13221
14869
  if (input.file) {
13222
14870
  try {
13223
- raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
14871
+ raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13224
14872
  } catch {
13225
14873
  return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
13226
14874
  }
@@ -13278,7 +14926,7 @@ async function executeQuery(input, ctx) {
13278
14926
  let parsed;
13279
14927
  if (input.file) {
13280
14928
  try {
13281
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
14929
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13282
14930
  parsed = JSON.parse(raw);
13283
14931
  } catch {
13284
14932
  return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
@@ -13320,7 +14968,7 @@ async function executeValidate(input, ctx) {
13320
14968
  let parsed;
13321
14969
  if (input.file) {
13322
14970
  try {
13323
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
14971
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13324
14972
  parsed = JSON.parse(raw);
13325
14973
  } catch {
13326
14974
  return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
@@ -13362,7 +15010,7 @@ async function executeTransform(input, ctx) {
13362
15010
  let parsed;
13363
15011
  if (input.file) {
13364
15012
  try {
13365
- const raw = await fs20.readFile(await safeResolveReal(input.file, ctx), "utf8");
15013
+ const raw = await fs22.readFile(await safeResolveReal(input.file, ctx), "utf8");
13366
15014
  parsed = JSON.parse(raw);
13367
15015
  } catch {
13368
15016
  return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
@@ -13524,56 +15172,56 @@ function jmespathSearch(data, query) {
13524
15172
  }
13525
15173
  function validateJsonSchema(data, schema) {
13526
15174
  const errors = [];
13527
- function check(value, s, path28) {
15175
+ function check(value, s, path32) {
13528
15176
  if (s["type"]) {
13529
15177
  const expectedType = s["type"];
13530
15178
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
13531
15179
  if (expectedType === "integer") {
13532
- if (!Number.isInteger(value)) errors.push(`${path28}: expected integer, got ${actualType}`);
15180
+ if (!Number.isInteger(value)) errors.push(`${path32}: expected integer, got ${actualType}`);
13533
15181
  } else if (expectedType !== actualType) {
13534
- errors.push(`${path28}: expected ${expectedType}, got ${actualType}`);
15182
+ errors.push(`${path32}: expected ${expectedType}, got ${actualType}`);
13535
15183
  }
13536
15184
  }
13537
15185
  if (typeof value === "string" && s["format"] === "uri" && value) {
13538
15186
  try {
13539
15187
  new URL(value);
13540
15188
  } catch {
13541
- errors.push(`${path28}: not a valid URI`);
15189
+ errors.push(`${path32}: not a valid URI`);
13542
15190
  }
13543
15191
  }
13544
15192
  if (typeof value === "string" && s["pattern"]) {
13545
15193
  const re = new RegExp(s["pattern"]);
13546
- if (!re.test(value)) errors.push(`${path28}: does not match pattern ${s["pattern"]}`);
15194
+ if (!re.test(value)) errors.push(`${path32}: does not match pattern ${s["pattern"]}`);
13547
15195
  }
13548
15196
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
13549
- errors.push(`${path28}: string too short (min ${s["minLength"]})`);
15197
+ errors.push(`${path32}: string too short (min ${s["minLength"]})`);
13550
15198
  }
13551
15199
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
13552
- errors.push(`${path28}: string too long (max ${s["maxLength"]})`);
15200
+ errors.push(`${path32}: string too long (max ${s["maxLength"]})`);
13553
15201
  }
13554
15202
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
13555
- errors.push(`${path28}: below minimum ${s["minimum"]}`);
15203
+ errors.push(`${path32}: below minimum ${s["minimum"]}`);
13556
15204
  }
13557
15205
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
13558
- errors.push(`${path28}: above maximum ${s["maximum"]}`);
15206
+ errors.push(`${path32}: above maximum ${s["maximum"]}`);
13559
15207
  }
13560
15208
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
13561
15209
  for (let i = 0; i < value.length; i++) {
13562
- check(value[i], s["items"], `${path28}[${i}]`);
15210
+ check(value[i], s["items"], `${path32}[${i}]`);
13563
15211
  }
13564
15212
  }
13565
15213
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
13566
15214
  const props = s["properties"];
13567
15215
  for (const [k, propSchema] of Object.entries(props)) {
13568
- check(value[k], propSchema, `${path28}.${k}`);
15216
+ check(value[k], propSchema, `${path32}.${k}`);
13569
15217
  }
13570
15218
  }
13571
15219
  }
13572
15220
  check(data, schema, "$");
13573
15221
  return { valid: errors.length === 0, errors };
13574
15222
  }
13575
- function simpleQuery(data, path28) {
13576
- const parts = path28.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
15223
+ function simpleQuery(data, path32) {
15224
+ const parts = path32.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
13577
15225
  let current = data;
13578
15226
  for (const part of parts) {
13579
15227
  if (current === null || current === void 0) return void 0;
@@ -14577,11 +16225,11 @@ var lintTool = {
14577
16225
  }
14578
16226
  };
14579
16227
  async function detectLinter(cwd) {
14580
- const { stat: stat15 } = await import("node:fs/promises");
16228
+ const { stat: stat17 } = await import("node:fs/promises");
14581
16229
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
14582
16230
  for (const f of checks) {
14583
16231
  try {
14584
- await stat15(`${cwd}/${f}`);
16232
+ await stat17(`${cwd}/${f}`);
14585
16233
  if (f.includes("biome")) return "biome";
14586
16234
  if (f.includes("eslint")) return "eslint";
14587
16235
  if (f.includes("tslint")) return "tslint";
@@ -14681,7 +16329,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14681
16329
  };
14682
16330
  }
14683
16331
  args.push("--timestamps", service);
14684
- return new Promise((resolve11) => {
16332
+ return new Promise((resolve14) => {
14685
16333
  let stdout = "";
14686
16334
  let stderr = "";
14687
16335
  const MAX = 2e5;
@@ -14697,7 +16345,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14697
16345
  if (settled) return;
14698
16346
  settled = true;
14699
16347
  clearTimeout(timer);
14700
- resolve11(result);
16348
+ resolve14(result);
14701
16349
  };
14702
16350
  const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
14703
16351
  const timer = setTimeout(() => {
@@ -14732,16 +16380,16 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
14732
16380
  }
14733
16381
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
14734
16382
  var MAX_TAIL_LINES = 1e5;
14735
- async function fileLogs(path28, lines, filterRe, stream) {
16383
+ async function fileLogs(path32, lines, filterRe, stream) {
14736
16384
  const { createInterface } = await import("node:readline");
14737
- const { createReadStream } = await import("node:fs");
16385
+ const { createReadStream: createReadStream2 } = await import("node:fs");
14738
16386
  const entries = [];
14739
16387
  const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
14740
16388
  const window = new Array(effLines);
14741
16389
  let writeIdx = 0;
14742
16390
  let totalLines = 0;
14743
16391
  const rl = createInterface({
14744
- input: createReadStream(path28),
16392
+ input: createReadStream2(path32),
14745
16393
  crlfDelay: Number.POSITIVE_INFINITY
14746
16394
  });
14747
16395
  for await (const line of rl) {
@@ -14762,7 +16410,7 @@ async function fileLogs(path28, lines, filterRe, stream) {
14762
16410
  if (parsed) entries.push(parsed);
14763
16411
  }
14764
16412
  return {
14765
- source: path28,
16413
+ source: path32,
14766
16414
  entries,
14767
16415
  total: entries.length,
14768
16416
  truncated: totalLines > effLines,
@@ -14916,7 +16564,7 @@ var outdatedTool = {
14916
16564
  }
14917
16565
  };
14918
16566
  function runOutdated(manager, args, cwd, signal) {
14919
- return new Promise((resolve11) => {
16567
+ return new Promise((resolve14) => {
14920
16568
  let stdout = "";
14921
16569
  let stderr = "";
14922
16570
  const MAX = 1e5;
@@ -14941,10 +16589,10 @@ function runOutdated(manager, args, cwd, signal) {
14941
16589
  });
14942
16590
  child.on("close", (code) => {
14943
16591
  const result = parseOutdatedOutput(stdout, code ?? 0);
14944
- resolve11(result);
16592
+ resolve14(result);
14945
16593
  });
14946
16594
  child.on("error", (e) => {
14947
- resolve11({
16595
+ resolve14({
14948
16596
  exit_code: 1,
14949
16597
  packages: [],
14950
16598
  total: 0,
@@ -14954,9 +16602,9 @@ function runOutdated(manager, args, cwd, signal) {
14954
16602
  });
14955
16603
  });
14956
16604
  }
14957
- function parseOutdatedOutput(json, exitCode) {
16605
+ function parseOutdatedOutput(json2, exitCode) {
14958
16606
  const packages = [];
14959
- if (!json) {
16607
+ if (!json2) {
14960
16608
  return {
14961
16609
  exit_code: exitCode,
14962
16610
  packages: [],
@@ -14966,7 +16614,7 @@ function parseOutdatedOutput(json, exitCode) {
14966
16614
  };
14967
16615
  }
14968
16616
  try {
14969
- const data = JSON.parse(json);
16617
+ const data = JSON.parse(json2);
14970
16618
  for (const name of Object.keys(data)) {
14971
16619
  const info = data[name];
14972
16620
  packages.push({
@@ -14984,17 +16632,17 @@ function parseOutdatedOutput(json, exitCode) {
14984
16632
  exit_code: exitCode,
14985
16633
  packages,
14986
16634
  total: packages.length,
14987
- output: json,
14988
- truncated: json.length >= 1e5
16635
+ output: json2,
16636
+ truncated: json2.length >= 1e5
14989
16637
  };
14990
16638
  }
14991
16639
 
14992
16640
  // src/patch.ts
14993
16641
  init_util();
14994
16642
  import { spawn as spawn13 } from "node:child_process";
14995
- import * as fs21 from "node:fs/promises";
16643
+ import * as fs23 from "node:fs/promises";
14996
16644
  import * as os7 from "node:os";
14997
- import * as path22 from "node:path";
16645
+ import * as path26 from "node:path";
14998
16646
  import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
14999
16647
  var patchTool = {
15000
16648
  name: "patch",
@@ -15030,9 +16678,9 @@ var patchTool = {
15030
16678
  for (const t of targets) {
15031
16679
  const stripped = stripPathComponents(t, strip);
15032
16680
  if (!stripped) continue;
15033
- const candidate = path22.resolve(dir, stripped);
15034
- const rel = path22.relative(ctx.projectRoot, candidate);
15035
- if (rel.startsWith("..") || path22.isAbsolute(rel)) {
16681
+ const candidate = path26.resolve(dir, stripped);
16682
+ const rel = path26.relative(ctx.projectRoot, candidate);
16683
+ if (rel.startsWith("..") || path26.isAbsolute(rel)) {
15036
16684
  return {
15037
16685
  applied: 0,
15038
16686
  rejected: 1,
@@ -15049,12 +16697,12 @@ var patchTool = {
15049
16697
  beforeContents.set(target, await readTextForTracking(target));
15050
16698
  }
15051
16699
  }
15052
- const tmpDir = await fs21.mkdtemp(path22.join(os7.tmpdir(), ".wstack_patch_"));
16700
+ const tmpDir = await fs23.mkdtemp(path26.join(os7.tmpdir(), ".wstack_patch_"));
15053
16701
  try {
15054
- await fs21.chmod(tmpDir, 448).catch(() => {
16702
+ await fs23.chmod(tmpDir, 448).catch(() => {
15055
16703
  });
15056
- const patchFile = path22.join(tmpDir, "in.diff");
15057
- await fs21.writeFile(patchFile, input.patch, { mode: 384 });
16704
+ const patchFile = path26.join(tmpDir, "in.diff");
16705
+ await fs23.writeFile(patchFile, input.patch, { mode: 384 });
15058
16706
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
15059
16707
  const result = await runPatch(args, dir, opts.signal);
15060
16708
  if (result.exitCode !== 0 && !dryRun) {
@@ -15072,8 +16720,8 @@ var patchTool = {
15072
16720
  const before = beforeContents.get(target) ?? null;
15073
16721
  const after = await readTextForTracking(target);
15074
16722
  if (after === null || after === before) continue;
15075
- const stat15 = await fs21.stat(target).catch(() => null);
15076
- if (stat15) ctx.recordRead?.(target, stat15.mtimeMs, "write", sha256hex(after));
16723
+ const stat17 = await fs23.stat(target).catch(() => null);
16724
+ if (stat17) ctx.recordRead?.(target, stat17.mtimeMs, "write", sha256hex(after));
15077
16725
  ctx.session?.recordFileChange?.({
15078
16726
  path: target,
15079
16727
  action: before === null ? "created" : "modified",
@@ -15090,7 +16738,7 @@ var patchTool = {
15090
16738
  message: result.stdout || "patch applied"
15091
16739
  };
15092
16740
  } finally {
15093
- await fs21.rm(tmpDir, { recursive: true, force: true }).catch(() => {
16741
+ await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
15094
16742
  });
15095
16743
  }
15096
16744
  }
@@ -15098,9 +16746,9 @@ var patchTool = {
15098
16746
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
15099
16747
  async function readTextForTracking(absPath) {
15100
16748
  try {
15101
- const stat15 = await fs21.stat(absPath);
15102
- if (!stat15.isFile() || stat15.size > MAX_TRACKING_BYTES) return null;
15103
- const buf = await fs21.readFile(absPath);
16749
+ const stat17 = await fs23.stat(absPath);
16750
+ if (!stat17.isFile() || stat17.size > MAX_TRACKING_BYTES) return null;
16751
+ const buf = await fs23.readFile(absPath);
15104
16752
  if (buf.includes(0)) return null;
15105
16753
  return buf.toString("utf8");
15106
16754
  } catch {
@@ -15125,7 +16773,7 @@ function stripPathComponents(p, strip) {
15125
16773
  return parts.slice(strip).join("/");
15126
16774
  }
15127
16775
  function runPatch(args, cwd, signal) {
15128
- return new Promise((resolve11) => {
16776
+ return new Promise((resolve14) => {
15129
16777
  let stdout = "";
15130
16778
  let stderr = "";
15131
16779
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
@@ -15136,8 +16784,8 @@ function runPatch(args, cwd, signal) {
15136
16784
  child.stderr?.on("data", (c) => {
15137
16785
  stderr += c.toString();
15138
16786
  });
15139
- child.on("close", (code) => resolve11({ exitCode: code ?? 1, stdout, stderr }));
15140
- child.on("error", (e) => resolve11({ exitCode: 1, stdout: "", stderr: e.message }));
16787
+ child.on("close", (code) => resolve14({ exitCode: code ?? 1, stdout, stderr }));
16788
+ child.on("error", (e) => resolve14({ exitCode: 1, stdout: "", stderr: e.message }));
15141
16789
  });
15142
16790
  }
15143
16791
  function extractPatchedFiles(output) {
@@ -15414,13 +17062,13 @@ ${formatTaskList(taskFile.tasks)}`
15414
17062
  }
15415
17063
  };
15416
17064
  function mkResult(plan, ok, message, todos) {
15417
- const open2 = plan.items.filter((i) => i.status !== "done").length;
17065
+ const open3 = plan.items.filter((i) => i.status !== "done").length;
15418
17066
  const result = {
15419
17067
  ok,
15420
17068
  message,
15421
17069
  plan: formatPlan(plan),
15422
17070
  count: plan.items.length,
15423
- open: open2
17071
+ open: open3
15424
17072
  };
15425
17073
  if (todos !== void 0) result.todos = todos;
15426
17074
  return result;
@@ -15428,7 +17076,7 @@ function mkResult(plan, ok, message, todos) {
15428
17076
 
15429
17077
  // src/read.ts
15430
17078
  init_util();
15431
- import * as fs22 from "node:fs/promises";
17079
+ import * as fs24 from "node:fs/promises";
15432
17080
  import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
15433
17081
  var MAX_BYTES2 = 5 * 1024 * 1024;
15434
17082
  var readTool = {
@@ -15477,9 +17125,9 @@ var readTool = {
15477
17125
  });
15478
17126
  }
15479
17127
  const absPath = await safeResolveReal(input.path, ctx);
15480
- let stat15;
17128
+ let stat17;
15481
17129
  try {
15482
- stat15 = await fs22.stat(absPath);
17130
+ stat17 = await fs24.stat(absPath);
15483
17131
  } catch (err) {
15484
17132
  const code = err.code;
15485
17133
  if (code === "ENOENT") {
@@ -15498,7 +17146,7 @@ var readTool = {
15498
17146
  cause: err
15499
17147
  });
15500
17148
  }
15501
- if (!stat15.isFile()) {
17149
+ if (!stat17.isFile()) {
15502
17150
  throw new FsError({
15503
17151
  message: `read: "${input.path}" is not a regular file`,
15504
17152
  code: "FS_READ_FAILED",
@@ -15506,22 +17154,22 @@ var readTool = {
15506
17154
  context: { reason: "not-a-regular-file" }
15507
17155
  });
15508
17156
  }
15509
- if (stat15.size > MAX_BYTES2) {
17157
+ if (stat17.size > MAX_BYTES2) {
15510
17158
  throw new FsError({
15511
- message: `read: file too large (${stat15.size} bytes, limit ${MAX_BYTES2})`,
17159
+ message: `read: file too large (${stat17.size} bytes, limit ${MAX_BYTES2})`,
15512
17160
  code: "FS_READ_FAILED",
15513
17161
  path: absPath,
15514
- context: { size: stat15.size, limit: MAX_BYTES2, reason: "too-large" }
17162
+ context: { size: stat17.size, limit: MAX_BYTES2, reason: "too-large" }
15515
17163
  });
15516
17164
  }
15517
17165
  const offset = Math.max(1, input.offset ?? 1);
15518
17166
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
15519
17167
  const prior = getReadRangeRecord(ctx, absPath);
15520
17168
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
15521
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat15.mtimeMs, offset, requestedEnd)) {
15522
- ctx.recordRead(absPath, stat15.mtimeMs);
17169
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat17.mtimeMs, offset, requestedEnd)) {
17170
+ ctx.recordRead(absPath, stat17.mtimeMs);
15523
17171
  return {
15524
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat15.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
17172
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat17.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
15525
17173
  total_lines: prior.totalLines,
15526
17174
  encoding: "utf8",
15527
17175
  truncated: requestedEnd < prior.totalLines,
@@ -15529,7 +17177,7 @@ var readTool = {
15529
17177
  note: "Repeated read suppressed to save tokens."
15530
17178
  };
15531
17179
  }
15532
- const buf = await fs22.readFile(absPath);
17180
+ const buf = await fs24.readFile(absPath);
15533
17181
  if (isBinaryBuffer(buf)) {
15534
17182
  throw new Error(`read: "${input.path}" appears to be binary`);
15535
17183
  }
@@ -15538,10 +17186,10 @@ var readTool = {
15538
17186
  const allLines = text.split(/\r\n|\r|\n/);
15539
17187
  const total = allLines.length;
15540
17188
  if (input.mode === "summary") {
15541
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15542
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, 1, Math.min(total, 200));
17189
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17190
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, Math.min(total, 200));
15543
17191
  return {
15544
- text: summarizeFile(input.path, stat15.size, allLines),
17192
+ text: summarizeFile(input.path, stat17.size, allLines),
15545
17193
  total_lines: total,
15546
17194
  encoding: "utf8",
15547
17195
  truncated: total > 200,
@@ -15549,13 +17197,13 @@ var readTool = {
15549
17197
  };
15550
17198
  }
15551
17199
  if (limit === 0) {
15552
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15553
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, 1, 0);
17200
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17201
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, 0);
15554
17202
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
15555
17203
  }
15556
17204
  if (offset > total) {
15557
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15558
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, total + 1, total + 1);
17205
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17206
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, total + 1, total + 1);
15559
17207
  return {
15560
17208
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
15561
17209
  total_lines: total,
@@ -15567,8 +17215,8 @@ var readTool = {
15567
17215
  const truncated = offset - 1 + slice.length < total;
15568
17216
  const width = String(offset + slice.length - 1).length;
15569
17217
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
15570
- ctx.recordRead(absPath, stat15.mtimeMs, "user", contentHash);
15571
- rememberReadRange(ctx, absPath, stat15.mtimeMs, total, offset, offset + slice.length - 1);
17218
+ ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
17219
+ rememberReadRange(ctx, absPath, stat17.mtimeMs, total, offset, offset + slice.length - 1);
15572
17220
  return {
15573
17221
  text: numbered,
15574
17222
  total_lines: total,
@@ -15637,10 +17285,10 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
15637
17285
  // src/replace.ts
15638
17286
  import { expectDefined as expectDefined8 } from "@wrongstack/core";
15639
17287
  import { spawn as spawn14 } from "node:child_process";
15640
- import * as fs23 from "node:fs/promises";
15641
- import * as path23 from "node:path";
17288
+ import * as fs25 from "node:fs/promises";
17289
+ import * as path27 from "node:path";
15642
17290
  import {
15643
- atomicWrite as atomicWrite2,
17291
+ atomicWrite as atomicWrite3,
15644
17292
  buildChildEnv as buildChildEnv9,
15645
17293
  compileGlob as compileGlob5,
15646
17294
  detectNewlineStyle as detectNewlineStyle2,
@@ -15711,11 +17359,11 @@ var replaceTool = {
15711
17359
  const dryRun = input.dry_run ?? true;
15712
17360
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
15713
17361
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
15714
- const realRoot = await fs23.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
17362
+ const realRoot = await fs25.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
15715
17363
  const results = [];
15716
17364
  let totalReplacements = 0;
15717
17365
  for (const absPath of fileList) {
15718
- const lstat2 = await fs23.lstat(absPath).catch((err) => {
17366
+ const lstat2 = await fs25.lstat(absPath).catch((err) => {
15719
17367
  if (err.code === "ENOENT") return null;
15720
17368
  throw err;
15721
17369
  });
@@ -15723,17 +17371,17 @@ var replaceTool = {
15723
17371
  if (lstat2.isSymbolicLink()) continue;
15724
17372
  let realPath;
15725
17373
  try {
15726
- realPath = await fs23.realpath(absPath);
17374
+ realPath = await fs25.realpath(absPath);
15727
17375
  } catch {
15728
17376
  continue;
15729
17377
  }
15730
- const rel = path23.relative(realRoot, realPath);
15731
- if (rel.startsWith("..") || path23.isAbsolute(rel)) continue;
15732
- const stat15 = await fs23.stat(realPath).catch(() => null);
15733
- if (!stat15?.isFile()) continue;
17378
+ const rel = path27.relative(realRoot, realPath);
17379
+ if (rel.startsWith("..") || path27.isAbsolute(rel)) continue;
17380
+ const stat17 = await fs25.stat(realPath).catch(() => null);
17381
+ if (!stat17?.isFile()) continue;
15734
17382
  let content;
15735
17383
  try {
15736
- const buf = await fs23.readFile(realPath);
17384
+ const buf = await fs25.readFile(realPath);
15737
17385
  if (isBinaryBuffer(buf)) continue;
15738
17386
  content = buf.toString("utf8");
15739
17387
  } catch {
@@ -15755,8 +17403,8 @@ var replaceTool = {
15755
17403
  totalReplacements += count;
15756
17404
  if (!dryRun) {
15757
17405
  const newContent = toStyle2(newContentLf, style);
15758
- await atomicWrite2(realPath, newContent, { mode: stat15.mode & 511 });
15759
- const written = await fs23.stat(realPath).catch(() => null);
17406
+ await atomicWrite3(realPath, newContent, { mode: stat17.mode & 511 });
17407
+ const written = await fs25.stat(realPath).catch(() => null);
15760
17408
  if (written) {
15761
17409
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
15762
17410
  }
@@ -15795,8 +17443,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
15795
17443
  const resolved = [];
15796
17444
  for (const p of parts) {
15797
17445
  const absPath = safeResolve(p, ctx);
15798
- const stat15 = await fs23.stat(absPath).catch(() => null);
15799
- if (stat15?.isFile()) {
17446
+ const stat17 = await fs25.stat(absPath).catch(() => null);
17447
+ if (stat17?.isFile()) {
15800
17448
  resolved.push(absPath);
15801
17449
  }
15802
17450
  }
@@ -15814,13 +17462,13 @@ async function globFiles(pattern, base, extraGlob) {
15814
17462
  return await globNative(pattern, base, extraGlob);
15815
17463
  }
15816
17464
  function checkRg() {
15817
- return new Promise((resolve11) => {
17465
+ return new Promise((resolve14) => {
15818
17466
  try {
15819
17467
  const p = spawn14("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
15820
- p.on("error", () => resolve11(false));
15821
- p.on("close", (code) => resolve11(code === 0));
17468
+ p.on("error", () => resolve14(false));
17469
+ p.on("close", (code) => resolve14(code === 0));
15822
17470
  } catch {
15823
- resolve11(false);
17471
+ resolve14(false);
15824
17472
  }
15825
17473
  });
15826
17474
  }
@@ -15837,10 +17485,10 @@ function spawnRgFind(pattern, base) {
15837
17485
  buf += chunk.toString();
15838
17486
  });
15839
17487
  return {
15840
- promise: new Promise((resolve11, reject) => {
17488
+ promise: new Promise((resolve14, reject) => {
15841
17489
  child.on("error", reject);
15842
17490
  child.on("close", () => {
15843
- resolve11(buf.split("\n").filter(Boolean));
17491
+ resolve14(buf.split("\n").filter(Boolean));
15844
17492
  });
15845
17493
  })
15846
17494
  };
@@ -15851,16 +17499,16 @@ async function globNative(pattern, base, extraGlob) {
15851
17499
  const walk = async (dir) => {
15852
17500
  let entries;
15853
17501
  try {
15854
- entries = await fs23.readdir(dir, { withFileTypes: true });
17502
+ entries = await fs25.readdir(dir, { withFileTypes: true });
15855
17503
  } catch {
15856
17504
  return;
15857
17505
  }
15858
17506
  for (const e of entries) {
15859
17507
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
15860
- const full = path23.join(dir, e.name);
17508
+ const full = path27.join(dir, e.name);
15861
17509
  try {
15862
- const stat15 = await fs23.lstat(full);
15863
- if (stat15.isSymbolicLink()) continue;
17510
+ const stat17 = await fs25.lstat(full);
17511
+ if (stat17.isSymbolicLink()) continue;
15864
17512
  } catch {
15865
17513
  continue;
15866
17514
  }
@@ -15883,9 +17531,9 @@ async function globNative(pattern, base, extraGlob) {
15883
17531
 
15884
17532
  // src/scaffold.ts
15885
17533
  init_util();
15886
- import * as fs24 from "node:fs/promises";
15887
- import * as path24 from "node:path";
15888
- import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
17534
+ import * as fs26 from "node:fs/promises";
17535
+ import * as path28 from "node:path";
17536
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
15889
17537
  var BUILT_IN_TEMPLATES = {
15890
17538
  "npm-package": {
15891
17539
  description: "Basic npm package with ESM",
@@ -16034,17 +17682,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
16034
17682
  let filesCreated = 0;
16035
17683
  for (const [filePath, content] of Object.entries(templateFiles)) {
16036
17684
  const resolvedPath = substituteVars(filePath, name, vars);
16037
- const joinedPath = path24.join(cwd, resolvedPath);
16038
- const root = path24.resolve(ctx.projectRoot);
16039
- const target = path24.resolve(joinedPath);
16040
- const rel = path24.relative(root, target);
16041
- if (rel.startsWith("..") || path24.isAbsolute(rel)) {
17685
+ const joinedPath = path28.join(cwd, resolvedPath);
17686
+ const root = path28.resolve(ctx.projectRoot);
17687
+ const target = path28.resolve(joinedPath);
17688
+ const rel = path28.relative(root, target);
17689
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
16042
17690
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
16043
17691
  }
16044
17692
  const fullPath = target;
16045
17693
  if (!dryRun) {
16046
- await fs24.mkdir(path24.dirname(fullPath), { recursive: true });
16047
- await atomicWrite3(fullPath, substituteVars(content, name, vars));
17694
+ await fs26.mkdir(path28.dirname(fullPath), { recursive: true });
17695
+ await atomicWrite4(fullPath, substituteVars(content, name, vars));
16048
17696
  }
16049
17697
  files.push(resolvedPath);
16050
17698
  filesCreated++;
@@ -16078,6 +17726,7 @@ var DEFAULT_NUM = 10;
16078
17726
  var MAX_RESULTS = 50;
16079
17727
  var TIMEOUT_MS3 = 15e3;
16080
17728
  var CACHE_TTL_MS = 3e5;
17729
+ var CACHE_MAX_ENTRIES = 200;
16081
17730
  var cache = /* @__PURE__ */ new Map();
16082
17731
  var searchTool = {
16083
17732
  name: "search",
@@ -16199,7 +17848,7 @@ var searchTool = {
16199
17848
  }
16200
17849
  const finalResults = ranked.slice(0, num);
16201
17850
  cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
16202
- pruneStaleCacheEntries();
17851
+ pruneCacheEntries();
16203
17852
  yield {
16204
17853
  type: "partial_output",
16205
17854
  text: `${finalResults.length} results from ${effectiveSource}`,
@@ -16221,11 +17870,16 @@ var searchTool = {
16221
17870
  };
16222
17871
  }
16223
17872
  };
16224
- function pruneStaleCacheEntries() {
17873
+ function pruneCacheEntries() {
16225
17874
  const cutoff = Date.now() - CACHE_TTL_MS * 2;
16226
17875
  for (const [key, entry] of cache.entries()) {
16227
17876
  if (entry.timestamp < cutoff) cache.delete(key);
16228
17877
  }
17878
+ while (cache.size > CACHE_MAX_ENTRIES) {
17879
+ const oldestKey = cache.keys().next().value;
17880
+ if (oldestKey === void 0) break;
17881
+ cache.delete(oldestKey);
17882
+ }
16229
17883
  }
16230
17884
  function rankSearchResults(results, query) {
16231
17885
  const seenUrls = /* @__PURE__ */ new Set();
@@ -16454,7 +18108,7 @@ function decodeHtmlEntities(text) {
16454
18108
  }
16455
18109
 
16456
18110
  // src/set-working-dir.ts
16457
- import * as fs25 from "node:fs/promises";
18111
+ import * as fs27 from "node:fs/promises";
16458
18112
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
16459
18113
  var setWorkingDirTool = {
16460
18114
  name: "set_working_dir",
@@ -16493,7 +18147,7 @@ var setWorkingDirTool = {
16493
18147
  };
16494
18148
  }
16495
18149
  try {
16496
- await fs25.access(resolved);
18150
+ await fs27.access(resolved);
16497
18151
  } catch {
16498
18152
  try {
16499
18153
  ctx.setWorkingDir(previous);
@@ -16893,7 +18547,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
16893
18547
  init_spawn_stream();
16894
18548
  init_util();
16895
18549
  init_legacy_bridge();
16896
- import * as path25 from "node:path";
18550
+ import * as path29 from "node:path";
16897
18551
  var testTool = {
16898
18552
  name: "test",
16899
18553
  category: "Code Quality",
@@ -16996,11 +18650,11 @@ var testTool = {
16996
18650
  }
16997
18651
  };
16998
18652
  async function detectRunner(cwd) {
16999
- const { stat: stat15 } = await import("node:fs/promises");
18653
+ const { stat: stat17 } = await import("node:fs/promises");
17000
18654
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
17001
18655
  for (const f of candidates) {
17002
18656
  try {
17003
- await stat15(path25.join(cwd, f));
18657
+ await stat17(path29.join(cwd, f));
17004
18658
  if (f.includes("vitest")) return "vitest";
17005
18659
  if (f.includes("jest")) return "jest";
17006
18660
  if (f.includes("mocha")) return "mocha";
@@ -17466,8 +19120,8 @@ var toolUseTool = {
17466
19120
  // src/tree.ts
17467
19121
  init_util();
17468
19122
  import { expectDefined as expectDefined10 } from "@wrongstack/core";
17469
- import * as fs26 from "node:fs/promises";
17470
- import * as path26 from "node:path";
19123
+ import * as fs28 from "node:fs/promises";
19124
+ import * as path30 from "node:path";
17471
19125
  var DEFAULT_IGNORE5 = [
17472
19126
  "node_modules",
17473
19127
  ".git",
@@ -17608,7 +19262,7 @@ var treeTool = {
17608
19262
  }
17609
19263
  };
17610
19264
  async function walkDir(dir, depth, opts) {
17611
- const entries = await fs26.readdir(dir, { withFileTypes: true }).catch(() => []);
19265
+ const entries = await fs28.readdir(dir, { withFileTypes: true }).catch(() => []);
17612
19266
  const filtered = entries.filter((e) => {
17613
19267
  if (!opts.showHidden && e.name.startsWith(".")) return false;
17614
19268
  if (opts.exclude.has(e.name)) return false;
@@ -17638,7 +19292,7 @@ async function walkDir(dir, depth, opts) {
17638
19292
  opts.lines.push(opts.prefix + branch + displayName);
17639
19293
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
17640
19294
  const childPrefix = opts.prefix + connector;
17641
- await walkDir(path26.join(dir, entry.name), depth + 1, {
19295
+ await walkDir(path30.join(dir, entry.name), depth + 1, {
17642
19296
  ...opts,
17643
19297
  prefix: childPrefix,
17644
19298
  isLast
@@ -17651,7 +19305,7 @@ async function walkDir(dir, depth, opts) {
17651
19305
  init_spawn_stream();
17652
19306
  init_util();
17653
19307
  init_legacy_bridge();
17654
- import * as path27 from "node:path";
19308
+ import * as path31 from "node:path";
17655
19309
  var typecheckTool = {
17656
19310
  name: "typecheck",
17657
19311
  category: "Code Quality",
@@ -17750,12 +19404,12 @@ var typecheckTool = {
17750
19404
  }
17751
19405
  };
17752
19406
  async function findTsConfig(cwd) {
17753
- const { stat: stat15 } = await import("node:fs/promises");
19407
+ const { stat: stat17 } = await import("node:fs/promises");
17754
19408
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
17755
19409
  for (const f of candidates) {
17756
19410
  try {
17757
- const s = await stat15(path27.join(cwd, f));
17758
- if (s.isFile()) return path27.join(cwd, f);
19411
+ const s = await stat17(path31.join(cwd, f));
19412
+ if (s.isFile()) return path31.join(cwd, f);
17759
19413
  } catch {
17760
19414
  }
17761
19415
  }
@@ -17763,8 +19417,8 @@ async function findTsConfig(cwd) {
17763
19417
  }
17764
19418
 
17765
19419
  // src/write.ts
17766
- import * as fs27 from "node:fs/promises";
17767
- import { atomicWrite as atomicWrite4, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
19420
+ import * as fs29 from "node:fs/promises";
19421
+ import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
17768
19422
  init_util();
17769
19423
  var writeTool = {
17770
19424
  name: "write",
@@ -17828,14 +19482,14 @@ async function prepareWrite(input, ctx) {
17828
19482
  let existed = false;
17829
19483
  let prev = "";
17830
19484
  try {
17831
- const stat15 = await fs27.stat(absPath);
17832
- existed = stat15.isFile();
19485
+ const stat17 = await fs29.stat(absPath);
19486
+ existed = stat17.isFile();
17833
19487
  if (existed) {
17834
19488
  if (!ctx.hasRead(absPath)) {
17835
- prev = await fs27.readFile(absPath, "utf8");
17836
- ctx.recordRead(absPath, stat15.mtimeMs, "write", sha256hex(prev));
19489
+ prev = await fs29.readFile(absPath, "utf8");
19490
+ ctx.recordRead(absPath, stat17.mtimeMs, "write", sha256hex(prev));
17837
19491
  } else {
17838
- prev = await fs27.readFile(absPath, "utf8");
19492
+ prev = await fs29.readFile(absPath, "utf8");
17839
19493
  }
17840
19494
  }
17841
19495
  } catch (err) {
@@ -17847,11 +19501,11 @@ async function prepareWrite(input, ctx) {
17847
19501
  }
17848
19502
  async function finishWrite(input, ctx, prepared, signal) {
17849
19503
  signal?.throwIfAborted();
17850
- await atomicWrite4(prepared.absPath, input.content);
19504
+ await atomicWrite5(prepared.absPath, input.content);
17851
19505
  const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
17852
19506
  + (new file, ${input.content.split("\n").length} lines)`;
17853
- const stat15 = await fs27.stat(prepared.absPath);
17854
- ctx.recordRead(prepared.absPath, stat15.mtimeMs, "write", sha256hex(input.content));
19507
+ const stat17 = await fs29.stat(prepared.absPath);
19508
+ ctx.recordRead(prepared.absPath, stat17.mtimeMs, "write", sha256hex(input.content));
17855
19509
  ctx.session.recordFileChange({
17856
19510
  path: prepared.absPath,
17857
19511
  action: prepared.existed ? "modified" : "created",
@@ -17875,7 +19529,27 @@ async function finishWrite(input, ctx, prepared, signal) {
17875
19529
  }
17876
19530
 
17877
19531
  // src/builtin.ts
19532
+ var OPTIONAL_TOOLS = [
19533
+ ...browserTools,
19534
+ e2ePlanTool,
19535
+ installTool,
19536
+ auditTool,
19537
+ outdatedTool,
19538
+ logsTool,
19539
+ documentTool,
19540
+ scaffoldTool,
19541
+ toolSearchTool,
19542
+ toolUseTool,
19543
+ batchToolUseTool,
19544
+ toolHelpTool,
19545
+ codebaseIndexTool,
19546
+ codebaseSearchTool,
19547
+ codebaseStatsTool,
19548
+ setWorkingDirTool
19549
+ ];
17878
19550
  var builtinTools = [
19551
+ ...browserTools,
19552
+ e2ePlanTool,
17879
19553
  readTool,
17880
19554
  writeTool,
17881
19555
  editTool,
@@ -17922,7 +19596,7 @@ var builtinTools = [
17922
19596
  // src/pack.ts
17923
19597
  var builtinToolsPack = {
17924
19598
  name: "builtin-tools",
17925
- 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-*).",
19599
+ 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.",
17926
19600
  tools: builtinTools
17927
19601
  };
17928
19602
  export {