@lincoln504/pi-research 1.7.7 → 1.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/agent-skill/pi-research/README.md +3 -3
  2. package/agent-skill/pi-research/SKILL.md +1 -1
  3. package/dist/cli-bin.mjs +10 -1
  4. package/dist/cli.mjs +692 -387
  5. package/dist/pdf-extract-worker.mjs +12 -3
  6. package/dist/thread-worker.mjs +41 -12
  7. package/docs/AGENT-SKILL.md +3 -3
  8. package/docs/ARCHITECTURE.md +64 -50
  9. package/docs/CHANGELOG.md +31 -0
  10. package/docs/CONFIGURATION.md +2 -2
  11. package/docs/SDK.md +7 -6
  12. package/docs/THIRD-PARTY-NOTICES.md +11 -7
  13. package/package.json +10 -8
  14. package/scripts/camoufox-layout.cjs +455 -0
  15. package/scripts/cleanup.cjs +72 -29
  16. package/scripts/setup.cjs +72 -111
  17. package/src/cli-bin.mjs +10 -1
  18. package/src/cli.ts +27 -15
  19. package/src/healthcheck/index.ts +1 -1
  20. package/src/infrastructure/browser/browser-cleanup.ts +1 -1
  21. package/src/infrastructure/browser/browser-error-utils.ts +5 -4
  22. package/src/infrastructure/browser/camoufox-layout.ts +450 -0
  23. package/src/infrastructure/browser/config.ts +46 -103
  24. package/src/infrastructure/browser/ensure-browser.ts +63 -49
  25. package/src/infrastructure/browser/thread-worker-browser.ts +38 -42
  26. package/src/infrastructure/browser/thread-worker.mjs +41 -12
  27. package/src/infrastructure/knowledge-store-service.ts +6 -6
  28. package/src/knowledge/store.ts +1 -1
  29. package/src/orchestration/researcher-executor.ts +47 -1
  30. package/src/tui/research-health.ts +1 -1
  31. package/src/utils/log-utils.ts +19 -4
  32. package/src/utils/research-export.ts +1 -1
  33. package/src/web-research/browser-search.ts +8 -10
  34. package/src/web-research/pdf-extract-worker.mjs +12 -3
  35. package/src/web-research/scraper-utils.ts +42 -10
  36. package/src/web-research/web-scraper.ts +1 -1
package/dist/cli.mjs CHANGED
@@ -230,7 +230,12 @@ function redactSecrets(message) {
230
230
  out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
231
231
  out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
232
232
  out = out.replace(COOKIE_HEADER_PATTERN, (_m, key, sep3) => `${key}${sep3}[REDACTED]`);
233
- out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep3) => `${key}${sep3}[REDACTED]`);
233
+ out = out.replace(AUTH_SCHEME_PATTERN, (_m, key, sep3, scheme) => `${key}${sep3}${scheme} [REDACTED]`);
234
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep3, val) => {
235
+ const q = val[0] === '"' || val[0] === "'" ? val[0] : "";
236
+ const closed = q !== "" && val.length > 1 && val.endsWith(q);
237
+ return `${key}${sep3}${q}[REDACTED]${closed ? q : ""}`;
238
+ });
234
239
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
235
240
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
236
241
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
@@ -249,7 +254,7 @@ function redactSecrets(message) {
249
254
  function stripTerminalEscapes(message) {
250
255
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
251
256
  }
252
- var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
257
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, AUTH_SCHEME_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
253
258
  var init_log_utils = __esm({
254
259
  "src/utils/log-utils.ts"() {
255
260
  "use strict";
@@ -264,11 +269,15 @@ var init_log_utils = __esm({
264
269
  "gi"
265
270
  );
266
271
  SENSITIVE_KV_PATTERN = new RegExp(
267
- `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
272
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|token|key)${B_RIGHT}(["']?\\s*[:=]\\s*)("[^"\\r\\n]{0,1024}"|'[^'\\r\\n]{0,1024}'|["']?[^\\s"',&)]+)`,
273
+ "gi"
274
+ );
275
+ AUTH_SCHEME_PATTERN = new RegExp(
276
+ `${B_LEFT}(authorization)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([A-Za-z][A-Za-z0-9_-]{0,31})[ \\t]+[^\\s"',&)]+`,
268
277
  "gi"
269
278
  );
270
279
  KNOWN_TOKEN_PATTERN = new RegExp(
271
- `${B_LEFT}(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})${B_RIGHT}`,
280
+ `${B_LEFT}(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|npm_[A-Za-z0-9]{36}|hf_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{10,})${B_RIGHT}`,
272
281
  "g"
273
282
  );
274
283
  JWT_PATTERN = new RegExp(
@@ -4554,12 +4563,12 @@ async function isModelCached(model) {
4554
4563
  const cacheDir = env.cacheDir;
4555
4564
  if (!cacheDir) return false;
4556
4565
  const { access: access3, readdir: readdir6, readFile: readFile7, stat: stat6, rm: rm3 } = await import("node:fs/promises");
4557
- const path20 = await import("node:path");
4558
- const modelDir = path20.default.join(cacheDir, model);
4559
- const onnxDir = path20.default.join(modelDir, "onnx");
4560
- await access3(path20.default.join(onnxDir, "model.onnx"));
4566
+ const path21 = await import("node:path");
4567
+ const modelDir = path21.default.join(cacheDir, model);
4568
+ const onnxDir = path21.default.join(modelDir, "onnx");
4569
+ await access3(path21.default.join(onnxDir, "model.onnx"));
4561
4570
  try {
4562
- const cfgRaw = await readFile7(path20.default.join(modelDir, "config.json"), "utf-8");
4571
+ const cfgRaw = await readFile7(path21.default.join(modelDir, "config.json"), "utf-8");
4563
4572
  JSON.parse(cfgRaw);
4564
4573
  } catch {
4565
4574
  return false;
@@ -4567,7 +4576,7 @@ async function isModelCached(model) {
4567
4576
  const tokenizerFiles = ["tokenizer.json", "tokenizer.model", "vocab.txt"];
4568
4577
  let hasTokenizer = false;
4569
4578
  for (const f of tokenizerFiles) {
4570
- const st = await stat6(path20.default.join(modelDir, f)).catch(() => null);
4579
+ const st = await stat6(path21.default.join(modelDir, f)).catch(() => null);
4571
4580
  if (st && st.size > 0) {
4572
4581
  hasTokenizer = true;
4573
4582
  break;
@@ -4575,7 +4584,7 @@ async function isModelCached(model) {
4575
4584
  }
4576
4585
  if (!hasTokenizer) return false;
4577
4586
  const entries = await readdir6(onnxDir).catch(() => []);
4578
- const onnxPath = path20.default.join(onnxDir, "model.onnx");
4587
+ const onnxPath = path21.default.join(onnxDir, "model.onnx");
4579
4588
  const onnxStat = await stat6(onnxPath).catch(() => null);
4580
4589
  if (onnxStat && onnxStat.size > 0 && onnxStat.size <= 1024 * 1024) {
4581
4590
  try {
@@ -4595,7 +4604,7 @@ async function isModelCached(model) {
4595
4604
  }
4596
4605
  const hasExternalWeights = entries.includes("model.onnx_data");
4597
4606
  if (hasExternalWeights) {
4598
- const dataStat = await stat6(path20.default.join(onnxDir, "model.onnx_data")).catch(() => null);
4607
+ const dataStat = await stat6(path21.default.join(onnxDir, "model.onnx_data")).catch(() => null);
4599
4608
  if (!dataStat || dataStat.size === 0) return false;
4600
4609
  }
4601
4610
  const partialArtifacts = entries.filter(
@@ -4604,7 +4613,7 @@ async function isModelCached(model) {
4604
4613
  if (partialArtifacts.length > 0) {
4605
4614
  if (hasExternalWeights) {
4606
4615
  for (const f of partialArtifacts) {
4607
- await rm3(path20.default.join(onnxDir, f), { force: true }).catch(() => {
4616
+ await rm3(path21.default.join(onnxDir, f), { force: true }).catch(() => {
4608
4617
  });
4609
4618
  }
4610
4619
  } else {
@@ -4630,10 +4639,10 @@ async function purgeModelCache(model) {
4630
4639
  const cacheDir = env.cacheDir;
4631
4640
  if (!cacheDir) return;
4632
4641
  const { rm: rm3 } = await import("node:fs/promises");
4633
- const path20 = await import("node:path");
4634
- const resolvedCache = path20.default.resolve(cacheDir);
4635
- const modelDir = path20.default.resolve(resolvedCache, model);
4636
- if (!model || path20.default.isAbsolute(model) || modelDir === resolvedCache || !modelDir.startsWith(resolvedCache + path20.default.sep)) {
4642
+ const path21 = await import("node:path");
4643
+ const resolvedCache = path21.default.resolve(cacheDir);
4644
+ const modelDir = path21.default.resolve(resolvedCache, model);
4645
+ if (!model || path21.default.isAbsolute(model) || modelDir === resolvedCache || !modelDir.startsWith(resolvedCache + path21.default.sep)) {
4637
4646
  logger.warn(`[embedder] Refusing to purge unsafe model-cache path for model id ${JSON.stringify(model)}`);
4638
4647
  return;
4639
4648
  }
@@ -6210,7 +6219,7 @@ var init_embedding_client = __esm({
6210
6219
  }
6211
6220
  }
6212
6221
  // ---- Internal HTTP helpers ----
6213
- getRequest(path20, timeoutMs = 12e4) {
6222
+ getRequest(path21, timeoutMs = 12e4) {
6214
6223
  return new Promise((resolve9, reject) => {
6215
6224
  let resolved = false;
6216
6225
  let req;
@@ -6218,7 +6227,7 @@ var init_embedding_client = __esm({
6218
6227
  if (!resolved) {
6219
6228
  resolved = true;
6220
6229
  req?.destroy();
6221
- reject(new Error(`[EmbeddingClient] GET ${path20} timed out after ${timeoutMs}ms`));
6230
+ reject(new Error(`[EmbeddingClient] GET ${path21} timed out after ${timeoutMs}ms`));
6222
6231
  }
6223
6232
  }, timeoutMs);
6224
6233
  if (timer.unref) timer.unref();
@@ -6226,7 +6235,7 @@ var init_embedding_client = __esm({
6226
6235
  {
6227
6236
  hostname: "127.0.0.1",
6228
6237
  port: this.port,
6229
- path: path20,
6238
+ path: path21,
6230
6239
  method: "GET",
6231
6240
  headers: this.authHeaders()
6232
6241
  },
@@ -6242,19 +6251,19 @@ var init_embedding_client = __esm({
6242
6251
  try {
6243
6252
  const parsed = JSON.parse(collected.toString());
6244
6253
  if (res.statusCode !== 200) {
6245
- reject(new Error(`[EmbeddingClient] GET ${path20} returned HTTP ${res.statusCode}`));
6254
+ reject(new Error(`[EmbeddingClient] GET ${path21} returned HTTP ${res.statusCode}`));
6246
6255
  } else {
6247
6256
  resolve9(parsed);
6248
6257
  }
6249
6258
  } catch (_e) {
6250
- reject(new Error(`[EmbeddingClient] Failed to parse GET ${path20} response: ${collected.toString()}`));
6259
+ reject(new Error(`[EmbeddingClient] Failed to parse GET ${path21} response: ${collected.toString()}`));
6251
6260
  }
6252
6261
  });
6253
6262
  res.on("error", (err) => {
6254
6263
  if (resolved) return;
6255
6264
  resolved = true;
6256
6265
  clearTimeout(timer);
6257
- reject(new Error(`[EmbeddingClient] GET ${path20} response error: ${err.message}`));
6266
+ reject(new Error(`[EmbeddingClient] GET ${path21} response error: ${err.message}`));
6258
6267
  });
6259
6268
  }
6260
6269
  );
@@ -6262,12 +6271,12 @@ var init_embedding_client = __esm({
6262
6271
  if (resolved) return;
6263
6272
  resolved = true;
6264
6273
  clearTimeout(timer);
6265
- reject(new Error(`[EmbeddingClient] GET ${path20} request error: ${err.message}`));
6274
+ reject(new Error(`[EmbeddingClient] GET ${path21} request error: ${err.message}`));
6266
6275
  });
6267
6276
  req.end();
6268
6277
  });
6269
6278
  }
6270
- request(path20, data) {
6279
+ request(path21, data) {
6271
6280
  return new Promise((resolve9, reject) => {
6272
6281
  const timeoutMs = 12e4;
6273
6282
  let resolved = false;
@@ -6276,7 +6285,7 @@ var init_embedding_client = __esm({
6276
6285
  if (!resolved) {
6277
6286
  resolved = true;
6278
6287
  req?.destroy();
6279
- reject(new Error(`[EmbeddingClient] POST ${path20} timed out after ${timeoutMs}ms`));
6288
+ reject(new Error(`[EmbeddingClient] POST ${path21} timed out after ${timeoutMs}ms`));
6280
6289
  }
6281
6290
  }, timeoutMs);
6282
6291
  if (timer.unref) timer.unref();
@@ -6285,7 +6294,7 @@ var init_embedding_client = __esm({
6285
6294
  {
6286
6295
  hostname: "127.0.0.1",
6287
6296
  port: this.port,
6288
- path: path20,
6297
+ path: path21,
6289
6298
  method: "POST",
6290
6299
  headers: {
6291
6300
  "Content-Type": "application/json",
@@ -6311,14 +6320,14 @@ var init_embedding_client = __esm({
6311
6320
  resolve9(parsed);
6312
6321
  }
6313
6322
  } catch (_e) {
6314
- reject(new Error(`[EmbeddingClient] Failed to parse POST ${path20} response: ${collected.toString()}`));
6323
+ reject(new Error(`[EmbeddingClient] Failed to parse POST ${path21} response: ${collected.toString()}`));
6315
6324
  }
6316
6325
  });
6317
6326
  res.on("error", (err) => {
6318
6327
  if (resolved) return;
6319
6328
  resolved = true;
6320
6329
  clearTimeout(timer);
6321
- reject(new Error(`[EmbeddingClient] POST ${path20} response error: ${err.message}`));
6330
+ reject(new Error(`[EmbeddingClient] POST ${path21} response error: ${err.message}`));
6322
6331
  });
6323
6332
  }
6324
6333
  );
@@ -6326,7 +6335,7 @@ var init_embedding_client = __esm({
6326
6335
  if (resolved) return;
6327
6336
  resolved = true;
6328
6337
  clearTimeout(timer);
6329
- reject(new Error(`[EmbeddingClient] POST ${path20} request error: ${err.message}`));
6338
+ reject(new Error(`[EmbeddingClient] POST ${path21} request error: ${err.message}`));
6330
6339
  });
6331
6340
  req.write(body);
6332
6341
  req.end();
@@ -7779,9 +7788,9 @@ var init_store_operations = __esm({
7779
7788
  });
7780
7789
 
7781
7790
  // src/knowledge/store.ts
7782
- import * as fs13 from "node:fs";
7791
+ import * as fs14 from "node:fs";
7783
7792
  import * as fsPromises from "node:fs/promises";
7784
- import * as path15 from "node:path";
7793
+ import * as path16 from "node:path";
7785
7794
  function isLanceCommitConflict(err) {
7786
7795
  const msg2 = err instanceof Error ? err.message : String(err);
7787
7796
  return /commit conflict/i.test(msg2) || /too many concurrent writes/i.test(msg2) || msg2.includes("Version mismatch") || msg2.includes("Lock error") || msg2.includes("Commit error");
@@ -7843,7 +7852,7 @@ var init_store = __esm({
7843
7852
  constructor(options) {
7844
7853
  this.options = options;
7845
7854
  this.tableName = this.retrieval === "bm25" ? BM25_TABLE_NAME : "knowledge";
7846
- this.manifestPath = path15.join(this.options.dbDir, "store-manifest.json");
7855
+ this.manifestPath = path16.join(this.options.dbDir, "store-manifest.json");
7847
7856
  }
7848
7857
  /** Normalized retrieval strategy: an unset option means the historical
7849
7858
  * 'vector' (hybrid) behavior, not bm25. Every mode check goes through this. */
@@ -7881,7 +7890,7 @@ var init_store = __esm({
7881
7890
  }
7882
7891
  async loadManifest() {
7883
7892
  try {
7884
- if (fs13.existsSync(this.manifestPath)) {
7893
+ if (fs14.existsSync(this.manifestPath)) {
7885
7894
  const content = await fsPromises.readFile(this.manifestPath, "utf-8");
7886
7895
  const manifest = JSON.parse(content);
7887
7896
  const savedVectorTable = typeof manifest.activeTableName === "string" ? manifest.activeTableName : null;
@@ -7926,7 +7935,7 @@ var init_store = __esm({
7926
7935
  return true;
7927
7936
  } catch (err) {
7928
7937
  logger.warn("[store] Failed to save manifest:", err);
7929
- if (fs13.existsSync(tempPath)) {
7938
+ if (fs14.existsSync(tempPath)) {
7930
7939
  try {
7931
7940
  await fsPromises.unlink(tempPath);
7932
7941
  } catch {
@@ -7958,8 +7967,8 @@ var init_store = __esm({
7958
7967
  async openInternal() {
7959
7968
  if (this.db) return;
7960
7969
  try {
7961
- if (!fs13.existsSync(this.options.dbDir)) {
7962
- fs13.mkdirSync(this.options.dbDir, { recursive: true });
7970
+ if (!fs14.existsSync(this.options.dbDir)) {
7971
+ fs14.mkdirSync(this.options.dbDir, { recursive: true, mode: 448 });
7963
7972
  }
7964
7973
  this.db = await (await getLancedb()).connect(this.options.dbDir);
7965
7974
  const tableNames = await this.db.tableNames();
@@ -8082,10 +8091,10 @@ var init_store = __esm({
8082
8091
  logger.warn(`[store] countRows failed before backup (continuing): ${err instanceof Error ? err.message : String(err)}`);
8083
8092
  }
8084
8093
  this.table = null;
8085
- const oldDir = path15.join(this.options.dbDir, `${this.tableName}.lance`);
8086
- const backupDir = path15.join(this.options.dbDir, `${backupTableName}.lance`);
8094
+ const oldDir = path16.join(this.options.dbDir, `${this.tableName}.lance`);
8095
+ const backupDir = path16.join(this.options.dbDir, `${backupTableName}.lance`);
8087
8096
  try {
8088
- if (fs13.existsSync(oldDir)) {
8097
+ if (fs14.existsSync(oldDir)) {
8089
8098
  await fsPromises.rename(oldDir, backupDir);
8090
8099
  logger.info(`[store] Successfully backed up database directory to ${backupDir}`);
8091
8100
  }
@@ -8175,8 +8184,8 @@ var init_store = __esm({
8175
8184
  await this.db.dropTable(oldTableName);
8176
8185
  logger.info(`[store] Renaming ${tempTableName} to ${canonicalName}...`);
8177
8186
  try {
8178
- const tempDir = path15.join(this.options.dbDir, `${tempTableName}.lance`);
8179
- const canonicalDir = path15.join(this.options.dbDir, `${canonicalName}.lance`);
8187
+ const tempDir = path16.join(this.options.dbDir, `${tempTableName}.lance`);
8188
+ const canonicalDir = path16.join(this.options.dbDir, `${canonicalName}.lance`);
8180
8189
  await fsPromises.rename(tempDir, canonicalDir);
8181
8190
  this.tableName = canonicalName;
8182
8191
  if (await this.saveManifest()) {
@@ -8550,9 +8559,9 @@ var init_store = __esm({
8550
8559
  };
8551
8560
  });
8552
8561
  try {
8553
- const dir = path15.dirname(outputPath);
8554
- await fs13.promises.mkdir(dir, { recursive: true });
8555
- await fs13.promises.writeFile(outputPath, JSON.stringify(exportData), "utf-8");
8562
+ const dir = path16.dirname(outputPath);
8563
+ await fs14.promises.mkdir(dir, { recursive: true });
8564
+ await fs14.promises.writeFile(outputPath, JSON.stringify(exportData), "utf-8");
8556
8565
  logger.info(`[store] Successfully exported ${exportData.length} entries to ${outputPath}`);
8557
8566
  } catch (err) {
8558
8567
  logger.error(`[store] Failed to export knowledge store for web:`, err);
@@ -8751,7 +8760,7 @@ var init_store = __esm({
8751
8760
  else if (name.includes("_migration_")) migrationDirs.push(name);
8752
8761
  }
8753
8762
  for (const name of migrationDirs) {
8754
- const fullPath = path15.join(this.options.dbDir, name);
8763
+ const fullPath = path16.join(this.options.dbDir, name);
8755
8764
  logger.info(`[store] Removing orphaned migration temp directory: ${name}`);
8756
8765
  await fsPromises.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8757
8766
  }
@@ -8764,7 +8773,7 @@ var init_store = __esm({
8764
8773
  const backupCutoff = Math.trunc(Date.now() - ttlDays * 24 * 60 * 60 * 1e3);
8765
8774
  for (const { name, ts } of ranked.slice(0, -1)) {
8766
8775
  if (ts >= backupCutoff) continue;
8767
- const fullPath = path15.join(this.options.dbDir, name);
8776
+ const fullPath = path16.join(this.options.dbDir, name);
8768
8777
  logger.info(`[store] Removing superseded backup directory: ${name}`);
8769
8778
  await fsPromises.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8770
8779
  }
@@ -8787,7 +8796,7 @@ var init_store = __esm({
8787
8796
  if (!Number.isNaN(parsed)) return parsed;
8788
8797
  }
8789
8798
  try {
8790
- const stat6 = await fsPromises.stat(path15.join(this.options.dbDir, name));
8799
+ const stat6 = await fsPromises.stat(path16.join(this.options.dbDir, name));
8791
8800
  return stat6.mtimeMs;
8792
8801
  } catch {
8793
8802
  return 0;
@@ -9301,8 +9310,8 @@ var init_chunker = __esm({
9301
9310
  });
9302
9311
 
9303
9312
  // src/knowledge/index.ts
9304
- import * as fs14 from "node:fs";
9305
- import * as path16 from "node:path";
9313
+ import * as fs15 from "node:fs";
9314
+ import * as path17 from "node:path";
9306
9315
  function getMigrationStrategy(config) {
9307
9316
  const strategy = config.MIGRATION_STRATEGY;
9308
9317
  if (!strategy) return void 0;
@@ -9369,12 +9378,12 @@ async function createKnowledgeStoreComponents(embedderFactory, reconnectFactory,
9369
9378
  }
9370
9379
  async function forceDeleteKnowledgeStore(config, workspace) {
9371
9380
  const dbDir = getDbDir(config, workspace);
9372
- if (!fs14.existsSync(dbDir)) return;
9381
+ if (!fs15.existsSync(dbDir)) return;
9373
9382
  let tableName = "knowledge";
9374
- const manifestPath = path16.join(dbDir, "store-manifest.json");
9383
+ const manifestPath = path17.join(dbDir, "store-manifest.json");
9375
9384
  try {
9376
- if (fs14.existsSync(manifestPath)) {
9377
- const manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf-8"));
9385
+ if (fs15.existsSync(manifestPath)) {
9386
+ const manifest = JSON.parse(fs15.readFileSync(manifestPath, "utf-8"));
9378
9387
  const key = config?.KNOWLEDGE_STORE_RETRIEVAL === "bm25" ? "activeBm25TableName" : "activeTableName";
9379
9388
  const saved = key === "activeBm25TableName" ? manifest.activeBm25TableName : manifest.activeTableName;
9380
9389
  if (saved) tableName = saved;
@@ -9382,10 +9391,10 @@ async function forceDeleteKnowledgeStore(config, workspace) {
9382
9391
  } catch (err) {
9383
9392
  logger.warn(`[knowledge] Failed to read manifest before forced delete, defaulting to table '${tableName}':`, err);
9384
9393
  }
9385
- const tableDir = path16.join(dbDir, `${tableName}.lance`);
9386
- if (!fs14.existsSync(tableDir)) return;
9394
+ const tableDir = path17.join(dbDir, `${tableName}.lance`);
9395
+ if (!fs15.existsSync(tableDir)) return;
9387
9396
  try {
9388
- fs14.rmSync(tableDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
9397
+ fs15.rmSync(tableDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
9389
9398
  logger.info(`[knowledge] Corrupted table directory ${tableDir} forcefully deleted; will be recreated on next init.`);
9390
9399
  } catch (err) {
9391
9400
  logger.error(`[knowledge] Failed to forcefully delete table directory ${tableDir}:`, err);
@@ -9415,7 +9424,7 @@ var knowledge_store_service_exports = {};
9415
9424
  __export(knowledge_store_service_exports, {
9416
9425
  KnowledgeStoreService: () => KnowledgeStoreService
9417
9426
  });
9418
- import * as path17 from "node:path";
9427
+ import * as path18 from "node:path";
9419
9428
  var KnowledgeStoreService;
9420
9429
  var init_knowledge_store_service = __esm({
9421
9430
  "src/infrastructure/knowledge-store-service.ts"() {
@@ -9518,7 +9527,7 @@ var init_knowledge_store_service = __esm({
9518
9527
  };
9519
9528
  const container = tryGetServiceContainerFromCtx(ctx);
9520
9529
  const pathConfig = await getService(ServiceNames.STATE_PATH_CONFIGURATION, void 0, container);
9521
- const lockPath = path17.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
9530
+ const lockPath = path18.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
9522
9531
  if (!this._initLock) {
9523
9532
  const processLifecycle = await getService(ServiceNames.PROCESS_LIFECYCLE, void 0, container);
9524
9533
  this._initLock = new FileLockService({
@@ -9529,12 +9538,12 @@ var init_knowledge_store_service = __esm({
9529
9538
  // liveOwnerStaleThreshold (120s default) that actually governs when a
9530
9539
  // provably-alive holder's lock may be physically reclaimed. This lock
9531
9540
  // protects a post-run FTS rebuild plus optimize with no fixed duration; if
9532
- // that holder has a stretch of non-yielding synchronous work (a single
9533
- // large better-sqlite3 call blocks the event loop for its full duration,
9534
- // during which the heartbeat literally cannot fire) longer than 20s, a
9535
- // waiting caller would abandon and throw "no sign of progress from its
9536
- // holder" against a holder that was never actually wedged — up to 100s
9537
- // before the lock would even become reclaim-eligible. Raised to match
9541
+ // that holder has a stretch of non-yielding synchronous work longer than
9542
+ // 20s (a single large native call — the FTS rebuild or optimize — blocks the
9543
+ // event loop for its full duration, during which the heartbeat literally
9544
+ // cannot fire), a waiting caller would abandon and throw "no sign of
9545
+ // progress from its holder" against a holder that was never actually
9546
+ // wedged — up to 100s before the lock would even become reclaim-eligible. Raised to match
9538
9547
  // liveOwnerStaleThreshold's own default so a live holder is given the same
9539
9548
  // grace period here that the reclaim logic itself already grants it.
9540
9549
  lockTimeout: 12e4,
@@ -10672,20 +10681,20 @@ __export(skill_installer_exports, {
10672
10681
  uninstallSkill: () => uninstallSkill
10673
10682
  });
10674
10683
  import * as crypto10 from "node:crypto";
10675
- import * as fs16 from "node:fs";
10676
- import * as os6 from "node:os";
10677
- import * as path18 from "node:path";
10684
+ import * as fs17 from "node:fs";
10685
+ import * as os7 from "node:os";
10686
+ import * as path19 from "node:path";
10678
10687
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10679
10688
  function readSkillVersion(sourceDir) {
10680
10689
  try {
10681
- const md = fs16.readFileSync(path18.join(sourceDir, "SKILL.md"), "utf-8");
10690
+ const md = fs17.readFileSync(path19.join(sourceDir, "SKILL.md"), "utf-8");
10682
10691
  return /"version"\s*:\s*"([^"]+)"/.exec(md)?.[1] ?? null;
10683
10692
  } catch {
10684
10693
  return null;
10685
10694
  }
10686
10695
  }
10687
10696
  function homeDir(opts) {
10688
- return opts?.home ?? os6.homedir();
10697
+ return opts?.home ?? os7.homedir();
10689
10698
  }
10690
10699
  function resolveSkillSourceDir(explicit) {
10691
10700
  const candidates = [];
@@ -10693,7 +10702,7 @@ function resolveSkillSourceDir(explicit) {
10693
10702
  if (process.env["PI_RESEARCH_SKILL_DIR"]) candidates.push(process.env["PI_RESEARCH_SKILL_DIR"]);
10694
10703
  const fromHere = (() => {
10695
10704
  try {
10696
- return path18.dirname(fileURLToPath5(import.meta.url));
10705
+ return path19.dirname(fileURLToPath5(import.meta.url));
10697
10706
  } catch {
10698
10707
  return void 0;
10699
10708
  }
@@ -10702,29 +10711,29 @@ function resolveSkillSourceDir(explicit) {
10702
10711
  if (!start) continue;
10703
10712
  let dir = start;
10704
10713
  for (let i = 0; i < 8; i++) {
10705
- candidates.push(path18.join(dir, SKILL_SOURCE_DIR, SKILL_NAME));
10706
- candidates.push(path18.join(dir, LEGACY_SKILL_SOURCE_DIR, SKILL_NAME));
10707
- const parent = path18.dirname(dir);
10714
+ candidates.push(path19.join(dir, SKILL_SOURCE_DIR, SKILL_NAME));
10715
+ candidates.push(path19.join(dir, LEGACY_SKILL_SOURCE_DIR, SKILL_NAME));
10716
+ const parent = path19.dirname(dir);
10708
10717
  if (parent === dir) break;
10709
10718
  dir = parent;
10710
10719
  }
10711
10720
  }
10712
10721
  for (const c of candidates) {
10713
10722
  try {
10714
- if (fs16.existsSync(path18.join(c, "SKILL.md"))) return path18.resolve(c);
10723
+ if (fs17.existsSync(path19.join(c, "SKILL.md"))) return path19.resolve(c);
10715
10724
  } catch {
10716
10725
  }
10717
10726
  }
10718
10727
  throw new Error("Could not locate the bundled research skill (agent-skill/pi-research/SKILL.md). Set PI_RESEARCH_SKILL_DIR to its path.");
10719
10728
  }
10720
10729
  function getManifestPath(opts) {
10721
- return path18.join(homeDir(opts), ".pi", "research", "installed-skills.json");
10730
+ return path19.join(homeDir(opts), ".pi", "research", "installed-skills.json");
10722
10731
  }
10723
10732
  function readManifest(opts) {
10724
10733
  const p = getManifestPath(opts);
10725
10734
  try {
10726
- if (fs16.existsSync(p)) {
10727
- const parsed = JSON.parse(fs16.readFileSync(p, "utf-8"));
10735
+ if (fs17.existsSync(p)) {
10736
+ const parsed = JSON.parse(fs17.readFileSync(p, "utf-8"));
10728
10737
  if (parsed && Array.isArray(parsed.entries)) {
10729
10738
  return { version: 1, package: PACKAGE_NAME, entries: parsed.entries };
10730
10739
  }
@@ -10735,14 +10744,14 @@ function readManifest(opts) {
10735
10744
  }
10736
10745
  function writeManifest(manifest, opts) {
10737
10746
  const p = getManifestPath(opts);
10738
- fs16.mkdirSync(path18.dirname(p), { recursive: true });
10747
+ fs17.mkdirSync(path19.dirname(p), { recursive: true });
10739
10748
  const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${manifestTmpSeq++}`;
10740
- fs16.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
10749
+ fs17.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
10741
10750
  try {
10742
- fs16.renameSync(tmp, p);
10751
+ fs17.renameSync(tmp, p);
10743
10752
  } catch (err) {
10744
10753
  try {
10745
- fs16.unlinkSync(tmp);
10754
+ fs17.unlinkSync(tmp);
10746
10755
  } catch {
10747
10756
  }
10748
10757
  throw err;
@@ -10755,10 +10764,10 @@ function upsertEntry(manifest, entry) {
10755
10764
  }
10756
10765
  function isOwnedSymlink(targetPath, expectedSource) {
10757
10766
  try {
10758
- const st = fs16.lstatSync(targetPath);
10767
+ const st = fs17.lstatSync(targetPath);
10759
10768
  if (!st.isSymbolicLink()) return false;
10760
- const dest = path18.resolve(path18.dirname(targetPath), fs16.readlinkSync(targetPath));
10761
- if (expectedSource && path18.resolve(expectedSource) === dest) return true;
10769
+ const dest = path19.resolve(path19.dirname(targetPath), fs17.readlinkSync(targetPath));
10770
+ if (expectedSource && path19.resolve(expectedSource) === dest) return true;
10762
10771
  return OWNED_SYMLINK_DEST.test(dest);
10763
10772
  } catch {
10764
10773
  return false;
@@ -10766,7 +10775,7 @@ function isOwnedSymlink(targetPath, expectedSource) {
10766
10775
  }
10767
10776
  function isOwnedCopy(targetPath) {
10768
10777
  try {
10769
- const md = fs16.readFileSync(path18.join(targetPath, "SKILL.md"), "utf-8");
10778
+ const md = fs17.readFileSync(path19.join(targetPath, "SKILL.md"), "utf-8");
10770
10779
  return md.includes(PACKAGE_NAME);
10771
10780
  } catch {
10772
10781
  return false;
@@ -10775,16 +10784,16 @@ function isOwnedCopy(targetPath) {
10775
10784
  function detectHarnesses(opts) {
10776
10785
  const home = homeDir(opts);
10777
10786
  return HARNESSES.map((h) => {
10778
- const absSkillsDir = path18.join(home, h.skillsDir);
10779
- const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
10787
+ const absSkillsDir = path19.join(home, h.skillsDir);
10788
+ const absSkillPath = path19.join(absSkillsDir, SKILL_NAME);
10780
10789
  let present = false;
10781
10790
  try {
10782
- present = fs16.existsSync(path18.join(home, h.baseDir));
10791
+ present = fs17.existsSync(path19.join(home, h.baseDir));
10783
10792
  } catch {
10784
10793
  }
10785
10794
  let installed = "none";
10786
10795
  try {
10787
- if (fs16.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10796
+ if (fs17.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10788
10797
  if (isOwnedSymlink(absSkillPath)) installed = "owned-symlink";
10789
10798
  else if (isOwnedCopy(absSkillPath)) installed = "owned-copy";
10790
10799
  else installed = "foreign";
@@ -10797,14 +10806,14 @@ function detectHarnesses(opts) {
10797
10806
  }
10798
10807
  function isUsableSkillDir(p) {
10799
10808
  try {
10800
- return fs16.existsSync(path18.join(p, "SKILL.md")) && fs16.existsSync(path18.join(p, "scripts", "run.mjs"));
10809
+ return fs17.existsSync(path19.join(p, "SKILL.md")) && fs17.existsSync(path19.join(p, "scripts", "run.mjs"));
10801
10810
  } catch {
10802
10811
  return false;
10803
10812
  }
10804
10813
  }
10805
10814
  function isSymlinkPresent(p) {
10806
10815
  try {
10807
- return fs16.lstatSync(p).isSymbolicLink();
10816
+ return fs17.lstatSync(p).isSymbolicLink();
10808
10817
  } catch {
10809
10818
  return false;
10810
10819
  }
@@ -10828,22 +10837,22 @@ function installSkill(toolIds, opts = {}) {
10828
10837
  results.push({ tool: id, path: "", type: "symlink", status: "error", message: `unknown target "${id}"` });
10829
10838
  continue;
10830
10839
  }
10831
- const absSkillsDir = path18.join(home, def.skillsDir);
10832
- const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
10840
+ const absSkillsDir = path19.join(home, def.skillsDir);
10841
+ const absSkillPath = path19.join(absSkillsDir, SKILL_NAME);
10833
10842
  const type = opts.copy ? "copy" : "symlink";
10834
10843
  if (isOwnedSymlink(absSkillPath, source) || isOwnedCopy(absSkillPath)) {
10835
10844
  try {
10836
- const lst = fs16.lstatSync(absSkillPath);
10845
+ const lst = fs17.lstatSync(absSkillPath);
10837
10846
  if (lst.isSymbolicLink()) {
10838
- const dest = path18.resolve(path18.dirname(absSkillPath), fs16.readlinkSync(absSkillPath));
10839
- if (dest !== path18.resolve(source)) {
10847
+ const dest = path19.resolve(path19.dirname(absSkillPath), fs17.readlinkSync(absSkillPath));
10848
+ if (dest !== path19.resolve(source)) {
10840
10849
  if (opts.dryRun) {
10841
10850
  results.push({ tool: id, path: absSkillPath, type: "symlink", status: "planned", message: "would re-point stale symlink to current source" });
10842
10851
  continue;
10843
10852
  }
10844
- fs16.unlinkSync(absSkillPath);
10853
+ fs17.unlinkSync(absSkillPath);
10845
10854
  try {
10846
- fs16.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10855
+ fs17.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10847
10856
  upsertEntry(manifest, { tool: id, path: absSkillPath, type: "symlink", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
10848
10857
  results.push({ tool: id, path: absSkillPath, type: "symlink", status: "installed", message: "re-pointed stale symlink to current source" });
10849
10858
  } catch (relinkErr) {
@@ -10869,7 +10878,7 @@ function installSkill(toolIds, opts = {}) {
10869
10878
  }
10870
10879
  continue;
10871
10880
  }
10872
- if (fs16.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10881
+ if (fs17.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10873
10882
  results.push({ tool: id, path: absSkillPath, type, status: "skipped-foreign", message: 'a non-pi-research "research" skill already exists here; left untouched' });
10874
10883
  continue;
10875
10884
  }
@@ -10878,10 +10887,10 @@ function installSkill(toolIds, opts = {}) {
10878
10887
  continue;
10879
10888
  }
10880
10889
  try {
10881
- fs16.mkdirSync(absSkillsDir, { recursive: true });
10890
+ fs17.mkdirSync(absSkillsDir, { recursive: true });
10882
10891
  if (type === "symlink") {
10883
10892
  try {
10884
- fs16.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10893
+ fs17.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10885
10894
  } catch (err) {
10886
10895
  copyDir(source, absSkillPath);
10887
10896
  upsertEntry(manifest, { tool: id, path: absSkillPath, type: "copy", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
@@ -10918,7 +10927,7 @@ function uninstallSkill(toolIds, opts = {}) {
10918
10927
  continue;
10919
10928
  }
10920
10929
  const owned = isOwnedSymlink(skillPath) || isOwnedCopy(skillPath);
10921
- const present = fs16.existsSync(skillPath) || isSymlinkPresent(skillPath);
10930
+ const present = fs17.existsSync(skillPath) || isSymlinkPresent(skillPath);
10922
10931
  if (!present) {
10923
10932
  results.push({ tool, path: skillPath, status: "not-present" });
10924
10933
  continue;
@@ -10936,8 +10945,8 @@ function uninstallSkill(toolIds, opts = {}) {
10936
10945
  continue;
10937
10946
  }
10938
10947
  try {
10939
- if (isSymlinkPresent(skillPath)) fs16.unlinkSync(skillPath);
10940
- else fs16.rmSync(skillPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
10948
+ if (isSymlinkPresent(skillPath)) fs17.unlinkSync(skillPath);
10949
+ else fs17.rmSync(skillPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
10941
10950
  results.push({ tool, path: skillPath, status: "removed" });
10942
10951
  } catch (err) {
10943
10952
  results.push({ tool, path: skillPath, status: "error", message: err?.message ?? String(err) });
@@ -10950,7 +10959,7 @@ function uninstallSkill(toolIds, opts = {}) {
10950
10959
  writeManifest({ version: 1, package: PACKAGE_NAME, entries: keep }, opts);
10951
10960
  } else {
10952
10961
  try {
10953
- fs16.rmSync(getManifestPath(opts));
10962
+ fs17.rmSync(getManifestPath(opts));
10954
10963
  } catch {
10955
10964
  }
10956
10965
  }
@@ -10972,56 +10981,56 @@ function reconcileSkillInstalls(opts = {}) {
10972
10981
  for (const e of manifest.entries) {
10973
10982
  if (e.type !== "symlink") {
10974
10983
  const stale = currentVersion !== null && e.version !== currentVersion;
10975
- if (stale && source !== null && fs16.existsSync(e.path) && isOwnedCopy(e.path)) {
10976
- const dir2 = path18.dirname(e.path);
10977
- const base2 = path18.basename(e.path);
10984
+ if (stale && source !== null && fs17.existsSync(e.path) && isOwnedCopy(e.path)) {
10985
+ const dir2 = path19.dirname(e.path);
10986
+ const base2 = path19.basename(e.path);
10978
10987
  try {
10979
- for (const name of fs16.readdirSync(dir2)) {
10988
+ for (const name of fs17.readdirSync(dir2)) {
10980
10989
  if (name.startsWith(`.${base2}.new-`) || name.startsWith(`.${base2}.old-`)) {
10981
- fs16.rmSync(path18.join(dir2, name), { recursive: true, force: true });
10990
+ fs17.rmSync(path19.join(dir2, name), { recursive: true, force: true });
10982
10991
  }
10983
10992
  }
10984
10993
  } catch {
10985
10994
  }
10986
10995
  const suffix = crypto10.randomBytes(4).toString("hex");
10987
- const fresh = path18.join(dir2, `.${base2}.new-${suffix}`);
10988
- const retired = path18.join(dir2, `.${base2}.old-${suffix}`);
10996
+ const fresh = path19.join(dir2, `.${base2}.new-${suffix}`);
10997
+ const retired = path19.join(dir2, `.${base2}.old-${suffix}`);
10989
10998
  try {
10990
10999
  copyDir(source, fresh);
10991
11000
  } catch {
10992
11001
  try {
10993
- fs16.rmSync(fresh, { recursive: true, force: true });
11002
+ fs17.rmSync(fresh, { recursive: true, force: true });
10994
11003
  } catch {
10995
11004
  }
10996
11005
  keep.push(e);
10997
11006
  continue;
10998
11007
  }
10999
11008
  try {
11000
- fs16.renameSync(e.path, retired);
11009
+ fs17.renameSync(e.path, retired);
11001
11010
  } catch {
11002
11011
  try {
11003
- fs16.rmSync(fresh, { recursive: true, force: true });
11012
+ fs17.rmSync(fresh, { recursive: true, force: true });
11004
11013
  } catch {
11005
11014
  }
11006
11015
  keep.push(e);
11007
11016
  continue;
11008
11017
  }
11009
11018
  try {
11010
- fs16.renameSync(fresh, e.path);
11019
+ fs17.renameSync(fresh, e.path);
11011
11020
  } catch {
11012
11021
  try {
11013
- fs16.renameSync(retired, e.path);
11022
+ fs17.renameSync(retired, e.path);
11014
11023
  } catch {
11015
11024
  }
11016
11025
  try {
11017
- fs16.rmSync(fresh, { recursive: true, force: true });
11026
+ fs17.rmSync(fresh, { recursive: true, force: true });
11018
11027
  } catch {
11019
11028
  }
11020
11029
  keep.push(e);
11021
11030
  continue;
11022
11031
  }
11023
11032
  try {
11024
- fs16.rmSync(retired, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
11033
+ fs17.rmSync(retired, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
11025
11034
  } catch {
11026
11035
  }
11027
11036
  keep.push({ ...e, source, version: currentVersion });
@@ -11031,12 +11040,12 @@ function reconcileSkillInstalls(opts = {}) {
11031
11040
  }
11032
11041
  continue;
11033
11042
  }
11034
- const dir = path18.dirname(e.path);
11035
- const base = path18.basename(e.path);
11043
+ const dir = path19.dirname(e.path);
11044
+ const base = path19.basename(e.path);
11036
11045
  try {
11037
- for (const name of fs16.readdirSync(dir)) {
11046
+ for (const name of fs17.readdirSync(dir)) {
11038
11047
  if (name.startsWith(`.${base}.new-`)) {
11039
- fs16.rmSync(path18.join(dir, name), { recursive: true, force: true });
11048
+ fs17.rmSync(path19.join(dir, name), { recursive: true, force: true });
11040
11049
  }
11041
11050
  }
11042
11051
  } catch {
@@ -11049,30 +11058,30 @@ function reconcileSkillInstalls(opts = {}) {
11049
11058
  keep.push(e);
11050
11059
  continue;
11051
11060
  }
11052
- const targetLive = fs16.existsSync(e.path) && isUsableSkillDir(e.path);
11061
+ const targetLive = fs17.existsSync(e.path) && isUsableSkillDir(e.path);
11053
11062
  if (!targetLive) {
11054
11063
  if (source !== null) {
11055
11064
  try {
11056
- fs16.unlinkSync(e.path);
11065
+ fs17.unlinkSync(e.path);
11057
11066
  } catch {
11058
11067
  keep.push(e);
11059
11068
  continue;
11060
11069
  }
11061
11070
  try {
11062
- fs16.symlinkSync(source, e.path, process.platform === "win32" ? "junction" : "dir");
11071
+ fs17.symlinkSync(source, e.path, process.platform === "win32" ? "junction" : "dir");
11063
11072
  keep.push({ ...e, type: "symlink", source });
11064
11073
  result.repointed.push(e.path);
11065
11074
  } catch {
11066
11075
  const suffix = crypto10.randomBytes(4).toString("hex");
11067
- const fresh = path18.join(dir, `.${base}.new-${suffix}`);
11076
+ const fresh = path19.join(dir, `.${base}.new-${suffix}`);
11068
11077
  try {
11069
11078
  copyDir(source, fresh);
11070
- fs16.renameSync(fresh, e.path);
11079
+ fs17.renameSync(fresh, e.path);
11071
11080
  keep.push({ ...e, type: "copy", source });
11072
11081
  result.repointed.push(e.path);
11073
11082
  } catch {
11074
11083
  try {
11075
- fs16.rmSync(fresh, { recursive: true, force: true });
11084
+ fs17.rmSync(fresh, { recursive: true, force: true });
11076
11085
  } catch {
11077
11086
  }
11078
11087
  result.pruned.push(e.path);
@@ -11080,7 +11089,7 @@ function reconcileSkillInstalls(opts = {}) {
11080
11089
  }
11081
11090
  } else if (!targetLive) {
11082
11091
  try {
11083
- fs16.unlinkSync(e.path);
11092
+ fs17.unlinkSync(e.path);
11084
11093
  result.pruned.push(e.path);
11085
11094
  } catch {
11086
11095
  keep.push(e);
@@ -11095,21 +11104,21 @@ function reconcileSkillInstalls(opts = {}) {
11095
11104
  if (keep.length > 0) writeManifest({ version: 1, package: PACKAGE_NAME, entries: keep }, opts);
11096
11105
  else {
11097
11106
  try {
11098
- fs16.rmSync(getManifestPath(opts));
11107
+ fs17.rmSync(getManifestPath(opts));
11099
11108
  } catch {
11100
11109
  }
11101
11110
  }
11102
11111
  return result;
11103
11112
  }
11104
11113
  function copyDir(src, dest) {
11105
- const st = fs16.statSync(src);
11114
+ const st = fs17.statSync(src);
11106
11115
  if (st.isDirectory()) {
11107
- fs16.mkdirSync(dest, { recursive: true });
11108
- for (const name of fs16.readdirSync(src)) {
11109
- copyDir(path18.join(src, name), path18.join(dest, name));
11116
+ fs17.mkdirSync(dest, { recursive: true });
11117
+ for (const name of fs17.readdirSync(src)) {
11118
+ copyDir(path19.join(src, name), path19.join(dest, name));
11110
11119
  }
11111
11120
  } else {
11112
- fs16.copyFileSync(src, dest);
11121
+ fs17.copyFileSync(src, dest);
11113
11122
  }
11114
11123
  }
11115
11124
  var HARNESSES, SKILL_NAME, PACKAGE_NAME, SKILL_SOURCE_DIR, LEGACY_SKILL_SOURCE_DIR, OWNED_SYMLINK_DEST, SKILL_AGENT_TARGETS, manifestTmpSeq, isTarget;
@@ -11117,15 +11126,15 @@ var init_skill_installer = __esm({
11117
11126
  "src/skill-install/skill-installer.ts"() {
11118
11127
  "use strict";
11119
11128
  HARNESSES = [
11120
- { id: "claude", label: "Claude", baseDir: ".claude", skillsDir: path18.join(".claude", "skills"), confidence: "confirmed" },
11121
- { id: "openclaw", label: "OpenClaw", baseDir: ".openclaw", skillsDir: path18.join(".openclaw", "skills"), confidence: "confirmed", note: "OpenClaw reads ~/.openclaw/skills as a managed skill root and accepts symlinked skill folders (docs.openclaw.ai/tools/skills)." },
11129
+ { id: "claude", label: "Claude", baseDir: ".claude", skillsDir: path19.join(".claude", "skills"), confidence: "confirmed" },
11130
+ { id: "openclaw", label: "OpenClaw", baseDir: ".openclaw", skillsDir: path19.join(".openclaw", "skills"), confidence: "confirmed", note: "OpenClaw reads ~/.openclaw/skills as a managed skill root and accepts symlinked skill folders (docs.openclaw.ai/tools/skills)." },
11122
11131
  // pi's own skill root is under its agent dir (~/.pi/agent/skills), NOT ~/.pi/skills.
11123
11132
  // Listed for DETECTION only — pi is never an install target (see SKILL_AGENT_TARGETS):
11124
11133
  // the host already exposes research as a native tool, so a skill there would just
11125
11134
  // duplicate it through a slower subprocess.
11126
- { id: "pi", label: "pi", baseDir: ".pi", skillsDir: path18.join(".pi", "agent", "skills"), confidence: "confirmed" },
11127
- { id: "codex", label: "OpenAI Codex CLI", baseDir: ".codex", skillsDir: path18.join(".codex", "skills"), confidence: "unverified", note: "Codex skills support is emerging; path not confirmed by official docs." },
11128
- { id: "agents", label: "Cross-tool (~/.agents/skills)", baseDir: ".agents", skillsDir: path18.join(".agents", "skills"), confidence: "unverified", note: "Proposed Universal Agents convention; not yet a ratified standard." }
11135
+ { id: "pi", label: "pi", baseDir: ".pi", skillsDir: path19.join(".pi", "agent", "skills"), confidence: "confirmed" },
11136
+ { id: "codex", label: "OpenAI Codex CLI", baseDir: ".codex", skillsDir: path19.join(".codex", "skills"), confidence: "unverified", note: "Codex skills support is emerging; path not confirmed by official docs." },
11137
+ { id: "agents", label: "Cross-tool (~/.agents/skills)", baseDir: ".agents", skillsDir: path19.join(".agents", "skills"), confidence: "unverified", note: "Proposed Universal Agents convention; not yet a ratified standard." }
11129
11138
  ];
11130
11139
  SKILL_NAME = "pi-research";
11131
11140
  PACKAGE_NAME = "@lincoln504/pi-research";
@@ -11141,9 +11150,9 @@ var init_skill_installer = __esm({
11141
11150
  });
11142
11151
 
11143
11152
  // src/cli.ts
11144
- import * as fs17 from "node:fs";
11145
- import * as os7 from "node:os";
11146
- import * as path19 from "node:path";
11153
+ import * as fs18 from "node:fs";
11154
+ import * as os8 from "node:os";
11155
+ import * as path20 from "node:path";
11147
11156
  import { spawnSync } from "node:child_process";
11148
11157
  import { fileURLToPath as fileURLToPath6 } from "node:url";
11149
11158
 
@@ -12902,47 +12911,315 @@ init_service_interfaces();
12902
12911
  init_file_lock_service();
12903
12912
  import * as crypto6 from "node:crypto";
12904
12913
  import * as net2 from "node:net";
12905
- import * as path10 from "node:path";
12914
+ import * as path11 from "node:path";
12906
12915
 
12907
12916
  // src/infrastructure/browser/config.ts
12908
- init_config();
12909
- init_logger();
12910
12917
  import * as crypto4 from "node:crypto";
12911
- import { join as join8 } from "node:path";
12912
- import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
12913
- import { platform, homedir as homedir3 } from "node:os";
12918
+ import { join as join9 } from "node:path";
12919
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
12920
+ import { platform, homedir as homedir4 } from "node:os";
12921
+
12922
+ // src/infrastructure/browser/camoufox-layout.ts
12923
+ import * as fs8 from "node:fs";
12924
+ import * as os4 from "node:os";
12925
+ import * as path10 from "node:path";
12914
12926
  import { createRequire as createRequire4 } from "node:module";
12915
- function getWindowsCamoufoxDir() {
12916
- return join8(homedir3(), "AppData", "Local", "camoufox", "camoufox", "Cache");
12927
+ var COMPAT_FLAG = ".0.5_FLAG";
12928
+ var LEGACY_MARKERS = ["camoufox-bin", "camoufox", "camoufox.exe", "application.ini", "Camoufox.app", "properties.json"];
12929
+ function isDir(p) {
12930
+ try {
12931
+ return fs8.statSync(p).isDirectory();
12932
+ } catch {
12933
+ return false;
12934
+ }
12917
12935
  }
12918
- function getCustomCamoufoxDir() {
12919
- return process.env["CAMOUFOX_INSTALL_DIR"] || process.env["PLAYWRIGHT_BROWSERS_PATH"] || void 0;
12936
+ function isFile(p) {
12937
+ try {
12938
+ return fs8.statSync(p).isFile();
12939
+ } catch {
12940
+ return false;
12941
+ }
12920
12942
  }
12921
- function getBrowserCacheDir() {
12922
- const custom = getCustomCamoufoxDir();
12923
- if (custom) {
12924
- return custom;
12943
+ function customDir(env = process.env) {
12944
+ return env["CAMOUFOX_INSTALL_DIR"] || env["PLAYWRIGHT_BROWSERS_PATH"] || null;
12945
+ }
12946
+ function defaultInstallDir(env = process.env, platform5 = process.platform, home = os4.homedir()) {
12947
+ if (platform5 === "win32") {
12948
+ const base = env["LOCALAPPDATA"] && path10.win32.isAbsolute(env["LOCALAPPDATA"]) ? env["LOCALAPPDATA"] : path10.join(home, "AppData", "Local");
12949
+ return path10.join(base, "camoufox", "camoufox", "Cache");
12925
12950
  }
12926
- const osPlatform = platform();
12927
- if (osPlatform === "win32") {
12928
- return getWindowsCamoufoxDir();
12929
- } else if (osPlatform === "darwin") {
12930
- return join8(homedir3(), "Library", "Caches", "camoufox");
12951
+ if (platform5 === "darwin") return path10.join(home, "Library", "Caches", "camoufox");
12952
+ const xdg = (env["XDG_CACHE_HOME"] || "").trim();
12953
+ return path10.join(xdg ? xdg : path10.join(home, ".cache"), "camoufox");
12954
+ }
12955
+ function resolveInstall(env = process.env, platform5 = process.platform, home = os4.homedir()) {
12956
+ const custom = customDir(env);
12957
+ if (!custom) return { dir: defaultInstallDir(env, platform5, home), overrides: {}, custom: null, ignoredCustom: false };
12958
+ if (platform5 === "darwin") {
12959
+ return { dir: defaultInstallDir(env, platform5, home), overrides: {}, custom, ignoredCustom: true };
12960
+ }
12961
+ const resolved = (platform5 === "win32" ? path10.win32 : path10).resolve(custom);
12962
+ if (platform5 === "win32") {
12963
+ const overrides2 = { LOCALAPPDATA: resolved };
12964
+ return { dir: defaultInstallDir({ ...env, ...overrides2 }, platform5, home), overrides: overrides2, custom, ignoredCustom: false };
12965
+ }
12966
+ const endsInCamoufox = path10.basename(resolved).toLowerCase() === "camoufox";
12967
+ const cacheHome = endsInCamoufox ? path10.dirname(resolved) : resolved;
12968
+ const overrides = { XDG_CACHE_HOME: cacheHome };
12969
+ return { dir: defaultInstallDir({ ...env, ...overrides }, platform5, home), overrides, custom, ignoredCustom: false };
12970
+ }
12971
+ function listInstalls(dir) {
12972
+ const out = [];
12973
+ const browsers = path10.join(dir, "browsers");
12974
+ let repos;
12975
+ try {
12976
+ repos = fs8.readdirSync(browsers, { withFileTypes: true });
12977
+ } catch {
12978
+ return out;
12979
+ }
12980
+ for (const repo of repos) {
12981
+ if (!repo.isDirectory() || repo.name.startsWith(".")) continue;
12982
+ const repoDir = path10.join(browsers, repo.name);
12983
+ let versions;
12984
+ try {
12985
+ versions = fs8.readdirSync(repoDir, { withFileTypes: true });
12986
+ } catch {
12987
+ continue;
12988
+ }
12989
+ for (const v of versions) {
12990
+ if (!v.isDirectory()) continue;
12991
+ const vdir = path10.join(repoDir, v.name);
12992
+ let meta;
12993
+ try {
12994
+ meta = JSON.parse(fs8.readFileSync(path10.join(vdir, "version.json"), "utf8"));
12995
+ } catch {
12996
+ continue;
12997
+ }
12998
+ out.push({
12999
+ repo: repo.name,
13000
+ name: v.name,
13001
+ path: vdir,
13002
+ rel: `browsers/${repo.name}/${v.name}`,
13003
+ version: meta && typeof meta.version === "string" ? meta.version : "",
13004
+ build: meta && typeof meta.build === "string" ? meta.build : ""
13005
+ });
13006
+ }
13007
+ }
13008
+ return out;
13009
+ }
13010
+ function hasInstall(dir) {
13011
+ return listInstalls(dir).length > 0;
13012
+ }
13013
+ function isLegacyFlat(dir) {
13014
+ if (!isDir(dir) || isFile(path10.join(dir, COMPAT_FLAG))) return false;
13015
+ let meta;
13016
+ try {
13017
+ meta = JSON.parse(fs8.readFileSync(path10.join(dir, "version.json"), "utf8"));
13018
+ } catch {
13019
+ return false;
13020
+ }
13021
+ if (!meta || typeof meta.version !== "string" || typeof meta.release !== "string") return false;
13022
+ return LEGACY_MARKERS.some((m) => fs8.existsSync(path10.join(dir, m)));
13023
+ }
13024
+ function hasExplicitChoice(dir) {
13025
+ try {
13026
+ const cfg = JSON.parse(fs8.readFileSync(path10.join(dir, "config.json"), "utf8"));
13027
+ return Boolean(cfg && (cfg.pinned || cfg.channel));
13028
+ } catch {
13029
+ return false;
13030
+ }
13031
+ }
13032
+ function loadPin(packageRoot) {
13033
+ try {
13034
+ const data = JSON.parse(fs8.readFileSync(path10.join(packageRoot, "dist", "data-files", "browser-pin.json"), "utf8"));
13035
+ if (!data || !data.tag) return null;
13036
+ return { repoName: String(data.repo_name).toLowerCase(), version: String(data.version), build: String(data.build) };
13037
+ } catch {
13038
+ return null;
13039
+ }
13040
+ }
13041
+ function findPackageRoot(fromUrl) {
13042
+ try {
13043
+ const require2 = createRequire4(fromUrl);
13044
+ return path10.dirname(require2.resolve("@camoufox/camoufox/package.json"));
13045
+ } catch {
13046
+ return null;
13047
+ }
13048
+ }
13049
+ function matchesPin(build, pin) {
13050
+ return build.repo.toLowerCase() === pin.repoName && build.version === pin.version && build.build === pin.build;
13051
+ }
13052
+ function pruneSuperseded(dir, log = () => {
13053
+ }, pin = null) {
13054
+ if (hasExplicitChoice(dir)) return [];
13055
+ const installs = listInstalls(dir);
13056
+ let keep = null;
13057
+ if (pin) {
13058
+ const paired = installs.find((i) => matchesPin(i, pin));
13059
+ keep = paired ? paired.rel : null;
12931
13060
  } else {
12932
- const cacheHome = process.env["XDG_CACHE_HOME"] || join8(homedir3(), ".cache");
12933
- return join8(cacheHome, "camoufox");
13061
+ try {
13062
+ keep = JSON.parse(fs8.readFileSync(path10.join(dir, "config.json"), "utf8")).active_version || null;
13063
+ } catch {
13064
+ return [];
13065
+ }
13066
+ }
13067
+ if (!keep || !installs.some((i) => i.rel === keep)) return [];
13068
+ const removed = [];
13069
+ for (const i of installs) {
13070
+ if (i.rel === keep) continue;
13071
+ try {
13072
+ fs8.rmSync(i.path, { recursive: true, force: true });
13073
+ removed.push(i.rel);
13074
+ log(`pi-research: removed superseded Camoufox build ${i.rel}`);
13075
+ } catch (e) {
13076
+ log(`pi-research: could not remove superseded Camoufox build ${i.rel}: ${e instanceof Error ? e.message : String(e)}`);
13077
+ }
13078
+ }
13079
+ try {
13080
+ for (const r of fs8.readdirSync(path10.join(dir, "browsers"))) {
13081
+ const rd = path10.join(dir, "browsers", r);
13082
+ if (isDir(rd) && fs8.readdirSync(rd).length === 0) fs8.rmdirSync(rd);
13083
+ }
13084
+ } catch {
13085
+ }
13086
+ return removed;
13087
+ }
13088
+ function stashLegacy(dir) {
13089
+ if (!isLegacyFlat(dir)) return null;
13090
+ const stash = `${dir}.legacy-${process.pid}-${Date.now()}`;
13091
+ fs8.renameSync(dir, stash);
13092
+ return stash;
13093
+ }
13094
+ function restoreLegacy(dir, stash) {
13095
+ if (!stash || !fs8.existsSync(stash)) return;
13096
+ try {
13097
+ fs8.rmSync(dir, { recursive: true, force: true });
13098
+ } catch {
13099
+ }
13100
+ fs8.renameSync(stash, dir);
13101
+ }
13102
+ function discardLegacy(stash) {
13103
+ if (!stash) return;
13104
+ fs8.rmSync(stash, { recursive: true, force: true });
13105
+ }
13106
+ function sweepStaleStashes(dir, log = () => {
13107
+ }) {
13108
+ if (!hasInstall(dir)) return [];
13109
+ const parent = path10.dirname(dir);
13110
+ const prefix = `${path10.basename(dir)}.legacy-`;
13111
+ const removed = [];
13112
+ let entries;
13113
+ try {
13114
+ entries = fs8.readdirSync(parent);
13115
+ } catch {
13116
+ return removed;
13117
+ }
13118
+ for (const e of entries) {
13119
+ if (!e.startsWith(prefix)) continue;
13120
+ const p = path10.join(parent, e);
13121
+ if (!isLegacyFlat(p)) continue;
13122
+ try {
13123
+ fs8.rmSync(p, { recursive: true, force: true });
13124
+ removed.push(p);
13125
+ log(`pi-research: removed leftover legacy Camoufox stash ${p}`);
13126
+ } catch {
13127
+ }
13128
+ }
13129
+ return removed;
13130
+ }
13131
+ function isUsable(dir, pin = null) {
13132
+ if (!isFile(path10.join(dir, COMPAT_FLAG))) return false;
13133
+ const installs = listInstalls(dir);
13134
+ if (installs.length === 0) return false;
13135
+ if (pin && !hasExplicitChoice(dir)) return installs.some((i) => matchesPin(i, pin));
13136
+ return true;
13137
+ }
13138
+ function listStashes(dir) {
13139
+ const parent = path10.dirname(dir);
13140
+ const prefix = `${path10.basename(dir)}.legacy-`;
13141
+ let entries;
13142
+ try {
13143
+ entries = fs8.readdirSync(parent);
13144
+ } catch {
13145
+ return [];
13146
+ }
13147
+ return entries.filter((e) => e.startsWith(prefix)).map((e) => path10.join(parent, e)).filter((p) => isLegacyFlat(p)).sort().reverse();
13148
+ }
13149
+ function recoverStash(dir, log = () => {
13150
+ }) {
13151
+ let empty = !fs8.existsSync(dir);
13152
+ if (!empty) {
13153
+ try {
13154
+ empty = isDir(dir) && fs8.readdirSync(dir).length === 0;
13155
+ } catch {
13156
+ return null;
13157
+ }
13158
+ }
13159
+ if (!empty) return null;
13160
+ const stash = listStashes(dir)[0];
13161
+ if (!stash) return null;
13162
+ try {
13163
+ if (fs8.existsSync(dir)) fs8.rmdirSync(dir);
13164
+ fs8.renameSync(stash, dir);
13165
+ log(`pi-research: restored the previous Camoufox install from ${stash}`);
13166
+ return stash;
13167
+ } catch {
13168
+ return null;
13169
+ }
13170
+ }
13171
+ async function provisionBrowser(dir, fetch2, log = () => {
13172
+ }, pin = null) {
13173
+ recoverStash(dir, log);
13174
+ if (isUsable(dir, pin)) {
13175
+ sweepStaleStashes(dir, log);
13176
+ return { status: "present", pruned: pruneSuperseded(dir, log, pin) };
13177
+ }
13178
+ let stash;
13179
+ try {
13180
+ stash = stashLegacy(dir);
13181
+ } catch (e) {
13182
+ return { status: "failed", pruned: [], reason: `could not move the existing legacy install aside (${e instanceof Error ? e.message : String(e)}); left untouched` };
13183
+ }
13184
+ if (stash) log(`pi-research: legacy Camoufox install moved aside to ${stash} until the new browser is verified`);
13185
+ let ok = false;
13186
+ let reason;
13187
+ try {
13188
+ ok = await fetch2() === true;
13189
+ if (!ok) reason = "camoufox fetch did not exit 0";
13190
+ } catch (e) {
13191
+ reason = e instanceof Error ? e.message : String(e);
13192
+ }
13193
+ if (isUsable(dir, pin)) {
13194
+ discardLegacy(stash);
13195
+ if (stash) log("pi-research: removed the legacy Camoufox install (replaced by the new build)");
13196
+ if (!ok) log(`pi-research: camoufox fetch reported a failure after the browser was installed (${reason}); keeping it. Run "npx camoufox fetch" to retry the companion download (e.g. the geoip database).`);
13197
+ sweepStaleStashes(dir, log);
13198
+ return { status: "installed", pruned: pruneSuperseded(dir, log, pin) };
12934
13199
  }
13200
+ if (ok) reason = "camoufox fetch exited 0 but no usable build was found";
13201
+ restoreLegacy(dir, stash);
13202
+ if (stash) log("pi-research: restored the previous Camoufox install after a failed upgrade");
13203
+ return { status: "failed", pruned: [], reason };
13204
+ }
13205
+
13206
+ // src/infrastructure/browser/config.ts
13207
+ init_config();
13208
+ init_logger();
13209
+ import { createRequire as createRequire5 } from "node:module";
13210
+ function getBrowserCacheDir() {
13211
+ return resolveInstall(process.env, platform(), homedir4()).dir;
12935
13212
  }
12936
13213
  function getBrowserProfileDir(config) {
12937
13214
  const configured = (config || getConfig()).TMP_DIR;
12938
13215
  let dir;
12939
13216
  if (configured && configured.trim().length > 0) {
12940
- dir = join8(configured, "pi-research", "profiles");
13217
+ dir = join9(configured, "pi-research", "profiles");
12941
13218
  } else {
12942
- const cacheHome = process.env["XDG_CACHE_HOME"] || join8(homedir3(), ".cache");
12943
- dir = join8(cacheHome, "pi-research", "profiles");
13219
+ const cacheHome = process.env["XDG_CACHE_HOME"] || join9(homedir4(), ".cache");
13220
+ dir = join9(cacheHome, "pi-research", "profiles");
12944
13221
  }
12945
- if (!existsSync6(dir)) {
13222
+ if (!existsSync7(dir)) {
12946
13223
  try {
12947
13224
  mkdirSync3(dir, { recursive: true });
12948
13225
  } catch (_e) {
@@ -12952,14 +13229,10 @@ function getBrowserProfileDir(config) {
12952
13229
  }
12953
13230
  function getBrowserEnv(config) {
12954
13231
  const env = { ...process.env };
12955
- const customPath = getCustomCamoufoxDir();
12956
- if (customPath) {
12957
- env["PLAYWRIGHT_BROWSERS_PATH"] = customPath;
12958
- env["CAMOUFOX_INSTALL_DIR"] = customPath;
12959
- } else {
12960
- delete env["PLAYWRIGHT_BROWSERS_PATH"];
12961
- delete env["CAMOUFOX_INSTALL_DIR"];
12962
- }
13232
+ const install = resolveInstall(process.env, platform(), homedir4());
13233
+ Object.assign(env, install.overrides);
13234
+ delete env["PLAYWRIGHT_BROWSERS_PATH"];
13235
+ delete env["CAMOUFOX_INSTALL_DIR"];
12963
13236
  if (!env["PI_RESEARCH_LOG_FILE"]) {
12964
13237
  const logFilePath = getLogger().getLogFilePath();
12965
13238
  if (logFilePath) {
@@ -12979,7 +13252,7 @@ function getBrowserEnv(config) {
12979
13252
  }
12980
13253
  function ensureBrowserCacheDir() {
12981
13254
  const cacheDir = getBrowserCacheDir();
12982
- if (!existsSync6(cacheDir)) {
13255
+ if (!existsSync7(cacheDir)) {
12983
13256
  try {
12984
13257
  mkdirSync3(cacheDir, { recursive: true });
12985
13258
  } catch (_e) {
@@ -12988,19 +13261,7 @@ function ensureBrowserCacheDir() {
12988
13261
  return cacheDir;
12989
13262
  }
12990
13263
  function getCamoufoxBinaryPath() {
12991
- const customPath = getCustomCamoufoxDir();
12992
- if (customPath) {
12993
- return customPath;
12994
- }
12995
- const osPlatform = platform();
12996
- if (osPlatform === "win32") {
12997
- return getWindowsCamoufoxDir();
12998
- } else if (osPlatform === "darwin") {
12999
- return join8(homedir3(), "Library", "Caches", "camoufox");
13000
- } else {
13001
- const cacheHome = process.env["XDG_CACHE_HOME"] || join8(homedir3(), ".cache");
13002
- return join8(cacheHome, "camoufox");
13003
- }
13264
+ return getBrowserCacheDir();
13004
13265
  }
13005
13266
  function generateSchedulerVersion(config) {
13006
13267
  const c = config || getConfig();
@@ -13030,16 +13291,15 @@ function probeBrowserNativeDeps(load = defaultNativeDepLoad) {
13030
13291
  }
13031
13292
  }
13032
13293
  function defaultNativeDepLoad() {
13033
- const require2 = createRequire4(import.meta.url);
13034
- const Database = require2("better-sqlite3");
13035
- const db = new Database(":memory:");
13036
- db.close();
13294
+ const require2 = createRequire5(import.meta.url);
13295
+ require2("impit");
13037
13296
  }
13038
13297
  function isBrowserAvailable() {
13039
13298
  if (isFullMockMode()) return false;
13040
13299
  try {
13041
- import.meta.resolve("camoufox-js");
13042
- return existsSync6(getCamoufoxBinaryPath());
13300
+ import.meta.resolve("@camoufox/camoufox");
13301
+ const root = findPackageRoot(import.meta.url);
13302
+ return isUsable(getCamoufoxBinaryPath(), root ? loadPin(root) : null);
13043
13303
  } catch {
13044
13304
  return false;
13045
13305
  }
@@ -13211,9 +13471,9 @@ var BrowserClient = class {
13211
13471
  get targetPort() {
13212
13472
  return this.port;
13213
13473
  }
13214
- async request(path20, data, signal, config) {
13474
+ async request(path21, data, signal, config) {
13215
13475
  const start = Date.now();
13216
- const operation = path20.includes("/search") ? "search" : path20.includes("/scrape") ? "browser-task" : path20.includes("/healthcheck") ? "healthcheck" : "network";
13476
+ const operation = path21.includes("/search") ? "search" : path21.includes("/scrape") ? "browser-task" : path21.includes("/healthcheck") ? "healthcheck" : "network";
13217
13477
  return new Promise((resolve9, reject) => {
13218
13478
  const agent = getClientAgent();
13219
13479
  const timeoutMs = resolveClientRequestTimeoutMs(operation, config);
@@ -13238,7 +13498,7 @@ var BrowserClient = class {
13238
13498
  resolved = true;
13239
13499
  abortCleanup?.();
13240
13500
  controller.abort();
13241
- const error = new Error(`[BrowserClient] Request to ${path20} timed out after ${timeoutMs}ms (Shared queue may be deep)`);
13501
+ const error = new Error(`[BrowserClient] Request to ${path21} timed out after ${timeoutMs}ms (Shared queue may be deep)`);
13242
13502
  errorTracker.trackError(error, {
13243
13503
  component: "browser-manager",
13244
13504
  operation,
@@ -13251,7 +13511,7 @@ var BrowserClient = class {
13251
13511
  const req = http4.request({
13252
13512
  hostname: "127.0.0.1",
13253
13513
  port: this.port,
13254
- path: path20,
13514
+ path: path21,
13255
13515
  method: "POST",
13256
13516
  agent,
13257
13517
  signal: controller.signal,
@@ -13275,7 +13535,7 @@ var BrowserClient = class {
13275
13535
  const error = new Error(
13276
13536
  `Browser pool leader is draining (${parsed.reason || "leadership-lost"}) \u2014 re-elect and retry`
13277
13537
  );
13278
- logger.debug(`[BrowserClient] Leader draining on ${path20}; re-election required.`);
13538
+ logger.debug(`[BrowserClient] Leader draining on ${path21}; re-election required.`);
13279
13539
  errorTracker.trackError(error, {
13280
13540
  component: "browser-manager",
13281
13541
  operation,
@@ -13291,7 +13551,7 @@ var BrowserClient = class {
13291
13551
  });
13292
13552
  reject(error);
13293
13553
  } else {
13294
- logger.debug(`[BrowserClient] Request ${path20} completed in ${duration}ms`);
13554
+ logger.debug(`[BrowserClient] Request ${path21} completed in ${duration}ms`);
13295
13555
  resolve9(parsed);
13296
13556
  }
13297
13557
  } catch (_e) {
@@ -13311,7 +13571,7 @@ var BrowserClient = class {
13311
13571
  resolved = true;
13312
13572
  clearTimeout(timer);
13313
13573
  abortCleanup?.();
13314
- const error = new Error(`[BrowserClient] Response stream error on ${path20}: ${err.message}`);
13574
+ const error = new Error(`[BrowserClient] Response stream error on ${path21}: ${err.message}`);
13315
13575
  errorTracker.trackError(error, {
13316
13576
  component: "browser-manager",
13317
13577
  operation,
@@ -13330,23 +13590,23 @@ var BrowserClient = class {
13330
13590
  let errorMsg;
13331
13591
  let errorType;
13332
13592
  if (nodeErr.code === "ECONNRESET" || nodeErr.code === "EPIPE") {
13333
- errorMsg = `Browser pool socket ${path20} closed (pool likely busy or restarting) - ${err.message}`;
13593
+ errorMsg = `Browser pool socket ${path21} closed (pool likely busy or restarting) - ${err.message}`;
13334
13594
  errorType = "connection_reset";
13335
13595
  } else if (nodeErr.code === "ECONNREFUSED") {
13336
- errorMsg = `Browser pool ${path20} unreachable (server restarting or torn down) - ${err.message}`;
13596
+ errorMsg = `Browser pool ${path21} unreachable (server restarting or torn down) - ${err.message}`;
13337
13597
  errorType = "connection_refused";
13338
13598
  } else if (nodeErr.code === "ETIMEDOUT") {
13339
- errorMsg = `Browser pool ${path20} timed out (slow browser response) - ${err.message}`;
13599
+ errorMsg = `Browser pool ${path21} timed out (slow browser response) - ${err.message}`;
13340
13600
  errorType = "timeout";
13341
13601
  } else {
13342
- errorMsg = `Browser pool ${path20} error: ${err.message}`;
13602
+ errorMsg = `Browser pool ${path21} error: ${err.message}`;
13343
13603
  errorType = "unknown";
13344
13604
  }
13345
13605
  const error = new Error(errorMsg);
13346
13606
  if (isTransientSocketError(error)) {
13347
- logger.debug(`[BrowserClient] Transient failure talking to http://127.0.0.1:${this.port}${path20} (caller will retry): ${errorMsg}`);
13607
+ logger.debug(`[BrowserClient] Transient failure talking to http://127.0.0.1:${this.port}${path21} (caller will retry): ${errorMsg}`);
13348
13608
  } else {
13349
- logger.error(`[BrowserClient] Request to http://127.0.0.1:${this.port}${path20} failed:`, errorMsg);
13609
+ logger.error(`[BrowserClient] Request to http://127.0.0.1:${this.port}${path21} failed:`, errorMsg);
13350
13610
  errorTracker.trackError(error, {
13351
13611
  component: "browser-manager",
13352
13612
  operation,
@@ -13598,17 +13858,17 @@ var BrowserServer = class {
13598
13858
  init_logger();
13599
13859
  import { exec } from "node:child_process";
13600
13860
  import { promisify } from "node:util";
13601
- import * as os4 from "node:os";
13861
+ import * as os5 from "node:os";
13602
13862
  var execAsync = promisify(exec);
13603
13863
  async function cleanupOrphanedCamoufoxProcesses() {
13604
- const platform4 = os4.platform();
13864
+ const platform5 = os5.platform();
13605
13865
  try {
13606
- if (platform4 === "darwin" || platform4 === "linux") {
13866
+ if (platform5 === "darwin" || platform5 === "linux") {
13607
13867
  await cleanupOrphanedProcessesUnix();
13608
- } else if (platform4 === "win32") {
13868
+ } else if (platform5 === "win32") {
13609
13869
  await cleanupOrphanedProcessesWindows();
13610
13870
  } else {
13611
- logger.warn(`[BrowserCleanup] Platform ${platform4} not supported for orphan cleanup`);
13871
+ logger.warn(`[BrowserCleanup] Platform ${platform5} not supported for orphan cleanup`);
13612
13872
  }
13613
13873
  } catch (error) {
13614
13874
  const msg2 = error instanceof Error ? error.message : String(error);
@@ -13735,11 +13995,11 @@ async function cleanupOrphanedProcessesWindows() {
13735
13995
  }
13736
13996
  }
13737
13997
  async function getBrowserPidsForWorkers(workerPids) {
13738
- const platform4 = os4.platform();
13998
+ const platform5 = os5.platform();
13739
13999
  const pids = [];
13740
14000
  if (!workerPids || workerPids.length === 0) return pids;
13741
14001
  try {
13742
- if (platform4 === "darwin" || platform4 === "linux") {
14002
+ if (platform5 === "darwin" || platform5 === "linux") {
13743
14003
  const { stdout } = await execAsync('ps -eo pid,ppid,comm | grep -E "(firefox|camoufox)" | grep -v grep').catch(() => ({ stdout: "" }));
13744
14004
  const lines = stdout.trim().split("\n");
13745
14005
  for (const line of lines) {
@@ -13750,7 +14010,7 @@ async function getBrowserPidsForWorkers(workerPids) {
13750
14010
  if (workerPids.includes(ppid)) pids.push(pid);
13751
14011
  }
13752
14012
  }
13753
- } else if (platform4 === "win32") {
14013
+ } else if (platform5 === "win32") {
13754
14014
  const procs = await queryWindowsProcesses();
13755
14015
  for (const proc of procs) {
13756
14016
  if (!PI_BROWSER_MARKER.test(proc.commandLine)) continue;
@@ -13765,10 +14025,10 @@ async function getBrowserPidsForWorkers(workerPids) {
13765
14025
  }
13766
14026
  async function killBrowserProcesses(pids) {
13767
14027
  if (!pids || pids.length === 0) return;
13768
- const platform4 = os4.platform();
14028
+ const platform5 = os5.platform();
13769
14029
  await Promise.all(pids.map(async (pid) => {
13770
14030
  try {
13771
- if (platform4 === "win32") {
14031
+ if (platform5 === "win32") {
13772
14032
  await execAsync(`taskkill /PID ${pid} /F /T`);
13773
14033
  logger.debug(`[BrowserCleanup] Terminated worker browser process: PID ${pid}`);
13774
14034
  } else {
@@ -14499,7 +14759,7 @@ async function getBrowserInitLock(container) {
14499
14759
  if (lock) return lock;
14500
14760
  const pathConfig = await getService(ServiceNames.STATE_PATH_CONFIGURATION, void 0, container);
14501
14761
  const processLifecycle = await getService(ServiceNames.PROCESS_LIFECYCLE, void 0, container);
14502
- const lockFilePath = path10.join(pathConfig.getLockDirPath(), "browser-init.lock");
14762
+ const lockFilePath = path11.join(pathConfig.getLockDirPath(), "browser-init.lock");
14503
14763
  lock = new FileLockService({
14504
14764
  lockFilePath,
14505
14765
  lockTimeout: 6e4,
@@ -14800,8 +15060,8 @@ init_config();
14800
15060
  init_logger();
14801
15061
  init_atomic_replace();
14802
15062
  init_metrics();
14803
- import * as fs8 from "node:fs/promises";
14804
- import * as path11 from "node:path";
15063
+ import * as fs9 from "node:fs/promises";
15064
+ import * as path12 from "node:path";
14805
15065
  import * as crypto7 from "node:crypto";
14806
15066
  import process2 from "node:process";
14807
15067
 
@@ -15039,12 +15299,12 @@ var StateManager = class {
15039
15299
  } = options;
15040
15300
  let stateDir = providedStateDir;
15041
15301
  if (!stateDir) {
15042
- stateDir = path11.join(getGlobalConfigDir(), "state");
15302
+ stateDir = path12.join(getGlobalConfigDir(), "state");
15043
15303
  }
15044
- this.stateFilePath = path11.join(stateDir, "research-state.json");
15045
- this.lockDirPath = path11.join(stateDir, ".locks");
15046
- this.backupDirPath = path11.join(stateDir, "backups");
15047
- this.lockFilePath = path11.join(this.lockDirPath, "research-state.lock");
15304
+ this.stateFilePath = path12.join(stateDir, "research-state.json");
15305
+ this.lockDirPath = path12.join(stateDir, ".locks");
15306
+ this.backupDirPath = path12.join(stateDir, "backups");
15307
+ this.lockFilePath = path12.join(this.lockDirPath, "research-state.lock");
15048
15308
  this.fileLockService = fileLockService;
15049
15309
  this.processLifecycle = processLifecycle;
15050
15310
  this.gpuResourceService = gpuResourceService;
@@ -15059,13 +15319,13 @@ var StateManager = class {
15059
15319
  */
15060
15320
  async ensureDirectories() {
15061
15321
  const dirs = [
15062
- path11.dirname(this.stateFilePath),
15322
+ path12.dirname(this.stateFilePath),
15063
15323
  this.lockDirPath,
15064
15324
  this.backupDirPath
15065
15325
  ];
15066
15326
  for (const dir of dirs) {
15067
15327
  try {
15068
- await fs8.mkdir(dir, { recursive: true, mode: 448 });
15328
+ await fs9.mkdir(dir, { recursive: true, mode: 448 });
15069
15329
  } catch (err) {
15070
15330
  logger.error(`[StateManager] Failed to create directory ${dir}:`, err);
15071
15331
  throw err;
@@ -15092,17 +15352,17 @@ var StateManager = class {
15092
15352
  if (this.orphanTmpSwept) return;
15093
15353
  this.orphanTmpSwept = true;
15094
15354
  const STALE_TMP_MS = 5 * 60 * 1e3;
15095
- const stateDir = path11.dirname(this.stateFilePath);
15355
+ const stateDir = path12.dirname(this.stateFilePath);
15096
15356
  const cutoff = Date.now() - STALE_TMP_MS;
15097
15357
  try {
15098
- const entries = await fs8.readdir(stateDir);
15358
+ const entries = await fs9.readdir(stateDir);
15099
15359
  for (const entry of entries) {
15100
15360
  if (!entry.startsWith("research-state-") || !entry.endsWith(".tmp")) continue;
15101
- const filePath = path11.join(stateDir, entry);
15361
+ const filePath = path12.join(stateDir, entry);
15102
15362
  try {
15103
- const stats = await fs8.stat(filePath);
15363
+ const stats = await fs9.stat(filePath);
15104
15364
  if (!stats.isFile() || stats.mtimeMs >= cutoff) continue;
15105
- await fs8.unlink(filePath);
15365
+ await fs9.unlink(filePath);
15106
15366
  logger.debug(`[StateManager] Removed orphaned state temp file: ${entry}`);
15107
15367
  } catch {
15108
15368
  }
@@ -15147,7 +15407,7 @@ var StateManager = class {
15147
15407
  return this.getDefaultState();
15148
15408
  }
15149
15409
  try {
15150
- const content = await fs8.readFile(this.stateFilePath, "utf-8");
15410
+ const content = await fs9.readFile(this.stateFilePath, "utf-8");
15151
15411
  const state = JSON.parse(content);
15152
15412
  return this.validator.migrateAndValidate(state);
15153
15413
  } catch (error) {
@@ -15176,7 +15436,7 @@ var StateManager = class {
15176
15436
  );
15177
15437
  try {
15178
15438
  await this.recoverFromCorruptionDirect();
15179
- const recovered = await fs8.readFile(this.stateFilePath, "utf-8");
15439
+ const recovered = await fs9.readFile(this.stateFilePath, "utf-8");
15180
15440
  return this.validator.migrateAndValidate(JSON.parse(recovered));
15181
15441
  } catch (recoveryError) {
15182
15442
  logger.error(
@@ -15235,9 +15495,9 @@ var StateManager = class {
15235
15495
  logger.warn(`[StateManager] Backup before write failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
15236
15496
  });
15237
15497
  const tempFileName = `research-state-${crypto7.randomBytes(16).toString("hex")}.tmp`;
15238
- tempFilePath = path11.join(path11.dirname(this.stateFilePath), tempFileName);
15498
+ tempFilePath = path12.join(path12.dirname(this.stateFilePath), tempFileName);
15239
15499
  const content = JSON.stringify(state, null, 2);
15240
- const fh = await fs8.open(tempFilePath, "w", 384);
15500
+ const fh = await fs9.open(tempFilePath, "w", 384);
15241
15501
  try {
15242
15502
  await fh.writeFile(content, { encoding: "utf-8" });
15243
15503
  await fh.sync();
@@ -15252,7 +15512,7 @@ var StateManager = class {
15252
15512
  metrics.increment("state_write_non_atomic_total", 1);
15253
15513
  const tmp = tempFilePath;
15254
15514
  tempFilePath = null;
15255
- await fs8.unlink(tmp).catch((err) => {
15515
+ await fs9.unlink(tmp).catch((err) => {
15256
15516
  logger.warn("[StateManager] Failed to remove temp file after copy fallback (write succeeded):", err);
15257
15517
  });
15258
15518
  }
@@ -15262,7 +15522,7 @@ var StateManager = class {
15262
15522
  throw new Error(`Failed to write state: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
15263
15523
  } finally {
15264
15524
  if (tempFilePath) {
15265
- await fs8.unlink(tempFilePath).catch((err) => {
15525
+ await fs9.unlink(tempFilePath).catch((err) => {
15266
15526
  logger.warn("[StateManager] Failed to clean up temp file:", err);
15267
15527
  });
15268
15528
  }
@@ -15681,7 +15941,7 @@ var MetricsService = class {
15681
15941
  // src/infrastructure/process-lifecycle-service.ts
15682
15942
  init_service_registry();
15683
15943
  init_service_names();
15684
- import * as fs9 from "node:fs/promises";
15944
+ import * as fs10 from "node:fs/promises";
15685
15945
  import { execFile } from "node:child_process";
15686
15946
  function parseEtime(value) {
15687
15947
  const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(value.trim());
@@ -15728,7 +15988,7 @@ var ProcessLifecycleService = class _ProcessLifecycleService {
15728
15988
  async getProcessStartTime(pid) {
15729
15989
  if (process.platform === "linux") {
15730
15990
  try {
15731
- const stat6 = await fs9.readFile(`/proc/${pid}/stat`, "utf8");
15991
+ const stat6 = await fs10.readFile(`/proc/${pid}/stat`, "utf8");
15732
15992
  const lastParenIndex = stat6.lastIndexOf(")");
15733
15993
  if (lastParenIndex === -1) return null;
15734
15994
  const partsAfterName = stat6.substring(lastParenIndex + 2).trim().split(/\s+/);
@@ -16307,8 +16567,8 @@ var StateValidator = class {
16307
16567
  if (!Value4.Check(SingletonStateSchema, coerced)) {
16308
16568
  const errors = [...Value4.Errors(SingletonStateSchema, coerced)];
16309
16569
  const errorMsg = errors.map((e) => {
16310
- const path20 = String(e.path || "root");
16311
- return `${path20}: ${e.message} (${JSON.stringify(e.value)})`;
16570
+ const path21 = String(e.path || "root");
16571
+ return `${path21}: ${e.message} (${JSON.stringify(e.value)})`;
16312
16572
  }).join(", ");
16313
16573
  throw new Error(`Invalid state: ${errorMsg}`);
16314
16574
  }
@@ -16355,45 +16615,45 @@ var StateValidator = class {
16355
16615
  init_logger();
16356
16616
  init_metrics();
16357
16617
  init_config();
16358
- import { dirname as dirname9, join as join13 } from "node:path";
16359
- import { existsSync as existsSync8 } from "node:fs";
16618
+ import { dirname as dirname10, join as join14 } from "node:path";
16619
+ import { existsSync as existsSync9 } from "node:fs";
16360
16620
  import { fileURLToPath as fileURLToPath3 } from "node:url";
16361
16621
  import { FixedClusterPool, WorkerChoiceStrategies } from "poolifier";
16362
16622
 
16363
16623
  // src/infrastructure/browser/ensure-browser.ts
16364
16624
  init_logger();
16365
16625
  import { spawn as spawn2 } from "node:child_process";
16366
- import { createRequire as createRequire5 } from "node:module";
16626
+ import { createRequire as createRequire6 } from "node:module";
16367
16627
  import { createHash as createHash2 } from "node:crypto";
16368
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3, fstatSync, openSync as openSync4, closeSync as closeSync4, rmSync } from "node:fs";
16369
- import { join as join11, dirname as dirname8 } from "node:path";
16370
- import { tmpdir as tmpdir2 } from "node:os";
16628
+ import { existsSync as existsSync8, fstatSync, openSync as openSync4, closeSync as closeSync4, rmSync as rmSync2, readFileSync as readFileSync5 } from "node:fs";
16629
+ import { join as join12, dirname as dirname9 } from "node:path";
16630
+ import { tmpdir as tmpdir2, platform as platform3, homedir as homedir5 } from "node:os";
16371
16631
  var FETCH_TIMEOUT_MS = 45 * 60 * 1e3;
16372
16632
  var STALE_LOCK_MS = 55 * 60 * 1e3;
16373
16633
  var WAIT_FOR_PEER_MS = 50 * 60 * 1e3;
16374
16634
  var POLL_INTERVAL_MS = 2e3;
16375
16635
  var inFlight = null;
16636
+ function currentPin() {
16637
+ const root = findPackageRoot(import.meta.url);
16638
+ return root ? loadPin(root) : null;
16639
+ }
16376
16640
  function isBrowserBinaryPresent() {
16377
16641
  try {
16378
- const cacheDir = getCamoufoxBinaryPath();
16379
- if (!existsSync7(cacheDir)) return false;
16380
- return readdirSync3(cacheDir).some((entry) => {
16381
- try {
16382
- return statSync3(join11(cacheDir, entry)).isDirectory();
16383
- } catch {
16384
- return false;
16385
- }
16386
- });
16642
+ return isUsable(getCamoufoxBinaryPath(), currentPin());
16387
16643
  } catch {
16388
16644
  return false;
16389
16645
  }
16390
16646
  }
16391
- function resolveCamoufoxBin() {
16647
+ function resolveCamoufoxCli() {
16392
16648
  try {
16393
- const require2 = createRequire5(import.meta.url);
16394
- const pkgJson = require2.resolve("camoufox-js/package.json");
16395
- const bin = join11(dirname8(pkgJson), "..", ".bin", process.platform === "win32" ? "camoufox-js.cmd" : "camoufox-js");
16396
- if (existsSync7(bin)) return bin;
16649
+ const require2 = createRequire6(import.meta.url);
16650
+ const pkgJson = require2.resolve("@camoufox/camoufox/package.json");
16651
+ const pkg = JSON.parse(readFileSync5(pkgJson, "utf8"));
16652
+ const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["camoufox"];
16653
+ if (rel) {
16654
+ const entry = join12(dirname9(pkgJson), rel);
16655
+ if (existsSync8(entry)) return entry;
16656
+ }
16397
16657
  } catch {
16398
16658
  }
16399
16659
  return null;
@@ -16401,11 +16661,17 @@ function resolveCamoufoxBin() {
16401
16661
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
16402
16662
  function runFetch() {
16403
16663
  return new Promise((resolve9, reject) => {
16404
- const bin = resolveCamoufoxBin();
16405
- const [cmd, args] = bin ? [bin, ["fetch"]] : ["npx", ["camoufox-js", "fetch"]];
16406
- logger.info(`[ensure-browser] Camoufox not found; fetching the browser (this runs once, ~500MB): ${cmd} ${args.join(" ")}`);
16407
- const useShell = process.platform === "win32";
16408
- const child = spawn2(useShell ? `"${cmd}"` : cmd, args, { stdio: "inherit", shell: useShell });
16664
+ const cli = resolveCamoufoxCli();
16665
+ if (!cli) {
16666
+ reject(new Error("@camoufox/camoufox is not installed (cannot find its CLI); run npm install"));
16667
+ return;
16668
+ }
16669
+ const install = resolveInstall(process.env, platform3(), homedir5());
16670
+ const env = { ...process.env, ...install.overrides };
16671
+ delete env["CAMOUFOX_INSTALL_DIR"];
16672
+ delete env["PLAYWRIGHT_BROWSERS_PATH"];
16673
+ logger.info(`[ensure-browser] Camoufox not found; fetching the browser (this runs once, ~1.3GB): ${process.execPath} ${cli} fetch`);
16674
+ const child = spawn2(process.execPath, [cli, "fetch"], { stdio: "inherit", env });
16409
16675
  const timer = setTimeout(() => {
16410
16676
  child.kill("SIGKILL");
16411
16677
  reject(new Error(`camoufox fetch timed out after ${FETCH_TIMEOUT_MS}ms`));
@@ -16416,8 +16682,7 @@ function runFetch() {
16416
16682
  });
16417
16683
  child.on("exit", (code) => {
16418
16684
  clearTimeout(timer);
16419
- if (code === 0) resolve9();
16420
- else reject(new Error(`camoufox fetch exited with code ${code}`));
16685
+ resolve9(code === 0);
16421
16686
  });
16422
16687
  });
16423
16688
  }
@@ -16436,7 +16701,7 @@ async function provision() {
16436
16701
  return;
16437
16702
  }
16438
16703
  const cacheKey = createHash2("sha256").update(getCamoufoxBinaryPath()).digest("hex").slice(0, 16);
16439
- const lockPath = join11(tmpdir2(), `pi-research-camoufox-fetch-${cacheKey}.lock`);
16704
+ const lockPath = join12(tmpdir2(), `pi-research-camoufox-fetch-${cacheKey}.lock`);
16440
16705
  let haveLock = false;
16441
16706
  try {
16442
16707
  closeSync4(openSync4(lockPath, "wx", 384));
@@ -16451,7 +16716,7 @@ async function provision() {
16451
16716
  closeSync4(fd);
16452
16717
  }
16453
16718
  if (Date.now() - mtimeMs > STALE_LOCK_MS) {
16454
- rmSync(lockPath, { force: true });
16719
+ rmSync2(lockPath, { force: true });
16455
16720
  closeSync4(openSync4(lockPath, "wx", 384));
16456
16721
  haveLock = true;
16457
16722
  }
@@ -16464,16 +16729,15 @@ async function provision() {
16464
16729
  }
16465
16730
  try {
16466
16731
  if (isBrowserBinaryPresent()) return;
16467
- await runFetch();
16468
- if (isBrowserBinaryPresent()) {
16469
- logger.info("[ensure-browser] Camoufox browser is ready.");
16470
- } else {
16471
- logger.warn("[ensure-browser] camoufox fetch reported success but no binary was found.");
16732
+ const result = await provisionBrowser(getCamoufoxBinaryPath(), runFetch, (m) => logger.info(`[ensure-browser] ${m}`), currentPin());
16733
+ if (result.status === "failed") {
16734
+ throw new Error(result.reason ?? "camoufox fetch failed");
16472
16735
  }
16736
+ logger.info("[ensure-browser] Camoufox browser is ready.");
16473
16737
  } finally {
16474
16738
  if (haveLock) {
16475
16739
  try {
16476
- rmSync(lockPath, { force: true });
16740
+ rmSync2(lockPath, { force: true });
16477
16741
  } catch {
16478
16742
  }
16479
16743
  }
@@ -16491,16 +16755,16 @@ function ensureBrowserInstalled() {
16491
16755
 
16492
16756
  // src/infrastructure/browser/cleanup-utils.ts
16493
16757
  init_logger();
16494
- import * as fs10 from "node:fs/promises";
16495
- import * as path12 from "node:path";
16496
- import * as os5 from "node:os";
16758
+ import * as fs11 from "node:fs/promises";
16759
+ import * as path13 from "node:path";
16760
+ import * as os6 from "node:os";
16497
16761
  var DEFAULT_STALE_MS = 30 * 24 * 60 * 60 * 1e3;
16498
16762
  var LOCK_FILE_NAMES = ["lock", ".parentlock", "parent.lock"];
16499
16763
  var LOCK_FRESHNESS_MS = 2 * 60 * 1e3;
16500
16764
  async function isWindowsParentLockHeld(lockPath) {
16501
16765
  let handle;
16502
16766
  try {
16503
- handle = await fs10.open(lockPath, "r");
16767
+ handle = await fs11.open(lockPath, "r");
16504
16768
  } catch (e) {
16505
16769
  const code = e?.code;
16506
16770
  if (code === "ENOENT") return false;
@@ -16527,16 +16791,16 @@ function isPidAlive(pid) {
16527
16791
  }
16528
16792
  async function isProfileLive(profileDir) {
16529
16793
  for (const name of LOCK_FILE_NAMES) {
16530
- const lockPath = path12.join(profileDir, name);
16794
+ const lockPath = path13.join(profileDir, name);
16531
16795
  let lst;
16532
16796
  try {
16533
- lst = await fs10.lstat(lockPath);
16797
+ lst = await fs11.lstat(lockPath);
16534
16798
  } catch {
16535
16799
  continue;
16536
16800
  }
16537
16801
  if (lst.isSymbolicLink()) {
16538
16802
  try {
16539
- const pid = parsePidFromLockTarget(await fs10.readlink(lockPath));
16803
+ const pid = parsePidFromLockTarget(await fs11.readlink(lockPath));
16540
16804
  if (pid !== null) return isPidAlive(pid);
16541
16805
  } catch {
16542
16806
  }
@@ -16553,42 +16817,42 @@ async function isProfileLive(profileDir) {
16553
16817
  async function canonicalizeForCompare(p) {
16554
16818
  let resolved;
16555
16819
  try {
16556
- resolved = await fs10.realpath(p);
16820
+ resolved = await fs11.realpath(p);
16557
16821
  } catch {
16558
- resolved = path12.resolve(p);
16822
+ resolved = path13.resolve(p);
16559
16823
  }
16560
16824
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
16561
16825
  }
16562
16826
  async function isPiOwnedSweepDir(dir) {
16563
16827
  const [realDir, realTmp] = await Promise.all([
16564
16828
  canonicalizeForCompare(dir),
16565
- canonicalizeForCompare(os5.tmpdir())
16829
+ canonicalizeForCompare(os6.tmpdir())
16566
16830
  ]);
16567
16831
  if (realDir === realTmp) return false;
16568
16832
  const hasPiSegment = (p) => p.split(/[\\/]+/).some((segment) => segment.toLowerCase().startsWith("pi-research"));
16569
16833
  return hasPiSegment(realDir);
16570
16834
  }
16571
16835
  async function cleanupStaleProfiles(baseDir, staleMs = DEFAULT_STALE_MS) {
16572
- const scanDir = baseDir ?? os5.tmpdir();
16836
+ const scanDir = baseDir ?? os6.tmpdir();
16573
16837
  const usePrefix = baseDir === void 0;
16574
16838
  const prefix = "playwright_firefoxdev_profile-";
16575
16839
  let removed = 0;
16576
16840
  let errors = 0;
16577
16841
  const sweepUnrecognized = usePrefix ? false : await isPiOwnedSweepDir(scanDir);
16578
16842
  try {
16579
- const entries = await fs10.readdir(scanDir);
16843
+ const entries = await fs11.readdir(scanDir);
16580
16844
  const now = Date.now();
16581
16845
  for (const entry of entries) {
16582
16846
  if (usePrefix && !entry.startsWith(prefix)) continue;
16583
- const fullPath = path12.join(scanDir, entry);
16847
+ const fullPath = path13.join(scanDir, entry);
16584
16848
  try {
16585
- const stats = await fs10.stat(fullPath);
16849
+ const stats = await fs11.stat(fullPath);
16586
16850
  if (!stats.isDirectory()) continue;
16587
16851
  const isProfileLike = entry.startsWith(prefix) || entry.startsWith("playwright-artifacts-");
16588
16852
  if (isProfileLike) {
16589
- const inner = await fs10.readdir(fullPath);
16853
+ const inner = await fs11.readdir(fullPath);
16590
16854
  if (inner.length === 0) {
16591
- await fs10.rmdir(fullPath);
16855
+ await fs11.rmdir(fullPath);
16592
16856
  removed++;
16593
16857
  continue;
16594
16858
  }
@@ -16596,12 +16860,12 @@ async function cleanupStaleProfiles(baseDir, staleMs = DEFAULT_STALE_MS) {
16596
16860
  logger.debug(`[Cleanup] Skipping profile ${entry} \u2014 owned by a live browser`);
16597
16861
  continue;
16598
16862
  }
16599
- await fs10.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
16863
+ await fs11.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
16600
16864
  removed++;
16601
16865
  continue;
16602
16866
  }
16603
16867
  if (sweepUnrecognized && now - stats.mtimeMs > staleMs && !await isProfileLive(fullPath)) {
16604
- await fs10.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
16868
+ await fs11.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
16605
16869
  removed++;
16606
16870
  }
16607
16871
  } catch (_e) {
@@ -16621,7 +16885,7 @@ async function cleanupStaleProfiles(baseDir, staleMs = DEFAULT_STALE_MS) {
16621
16885
  init_log_utils();
16622
16886
  import process3 from "node:process";
16623
16887
  import cluster from "node:cluster";
16624
- import * as fs11 from "node:fs/promises";
16888
+ import * as fs12 from "node:fs/promises";
16625
16889
  var workerId = "";
16626
16890
  var BENIGN_IPC_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_IPC_CHANNEL_CLOSED", "ECONNRESET"]);
16627
16891
  function isBenignClusterIpcError(err) {
@@ -16682,7 +16946,7 @@ function logToDebugFile(level, ...args) {
16682
16946
  return String(arg);
16683
16947
  }).join(" "))
16684
16948
  };
16685
- fs11.appendFile(logFile, `${JSON.stringify(entry)}
16949
+ fs12.appendFile(logFile, `${JSON.stringify(entry)}
16686
16950
  `).catch(() => {
16687
16951
  });
16688
16952
  } catch {
@@ -16694,7 +16958,7 @@ init_service_registry();
16694
16958
  init_safe_unref();
16695
16959
  init_service_names();
16696
16960
  var __filename2 = fileURLToPath3(import.meta.url);
16697
- var __dirname = dirname9(__filename2);
16961
+ var __dirname = dirname10(__filename2);
16698
16962
  function buildWorkerPoolOptions(config) {
16699
16963
  return {
16700
16964
  workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED,
@@ -16786,8 +17050,8 @@ var WorkerPoolManager = class {
16786
17050
  void cleanupStaleProfiles(getBrowserProfileDir(config), STALE_PROFILE_MS).catch((e) => logger.debug("[WorkerPoolManager] Profile sweep (profile dir) failed:", e));
16787
17051
  void cleanupStaleProfiles(void 0, STALE_PROFILE_MS).catch((e) => logger.debug("[WorkerPoolManager] Profile sweep (legacy tmp) failed:", e));
16788
17052
  void cleanupOrphanedCamoufoxProcesses().catch((e) => logger.debug("[WorkerPoolManager] Startup orphan sweep failed:", e));
16789
- const workerPath = join13(__dirname, "./thread-worker.mjs");
16790
- if (!existsSync8(workerPath)) {
17053
+ const workerPath = join14(__dirname, "./thread-worker.mjs");
17054
+ if (!existsSync9(workerPath)) {
16791
17055
  throw new Error(
16792
17056
  `Browser worker bundle is missing at ${workerPath}. It is produced by the build step; reinstall the extension (e.g. \`pi update --extensions\`) or run \`npm install\` in the extension directory so the prepare/build script regenerates it.`
16793
17057
  );
@@ -17035,7 +17299,7 @@ var WorkerPoolManager = class {
17035
17299
  init_config();
17036
17300
  init_service_registry();
17037
17301
  init_service_names();
17038
- import * as path13 from "node:path";
17302
+ import * as path14 from "node:path";
17039
17303
  var StatePathConfiguration = class {
17040
17304
  name = ServiceNames.STATE_PATH_CONFIGURATION;
17041
17305
  lifecycle = "uninitialized" /* UNINITIALIZED */;
@@ -17047,13 +17311,13 @@ var StatePathConfiguration = class {
17047
17311
  lockFilePath;
17048
17312
  projectSettingsPath;
17049
17313
  constructor(stateDir) {
17050
- const resolvedStateDir = stateDir && path13.isAbsolute(stateDir) ? stateDir : getStateDir();
17314
+ const resolvedStateDir = stateDir && path14.isAbsolute(stateDir) ? stateDir : getStateDir();
17051
17315
  this.stateDir = resolvedStateDir;
17052
- this.stateFilePath = path13.join(resolvedStateDir, "research-state.json");
17053
- this.lockDirPath = path13.join(resolvedStateDir, ".locks");
17054
- this.backupDirPath = path13.join(resolvedStateDir, "backups");
17055
- this.lockFilePath = path13.join(this.lockDirPath, "research-state.lock");
17056
- this.projectSettingsPath = path13.join(resolvedStateDir, "project-settings.json");
17316
+ this.stateFilePath = path14.join(resolvedStateDir, "research-state.json");
17317
+ this.lockDirPath = path14.join(resolvedStateDir, ".locks");
17318
+ this.backupDirPath = path14.join(resolvedStateDir, "backups");
17319
+ this.lockFilePath = path14.join(this.lockDirPath, "research-state.lock");
17320
+ this.projectSettingsPath = path14.join(resolvedStateDir, "project-settings.json");
17057
17321
  }
17058
17322
  async initialize() {
17059
17323
  if (this._initialized) {
@@ -17172,7 +17436,7 @@ var ResearchRunSemaphore = class {
17172
17436
  if (this._initialized) return;
17173
17437
  this.lifecycle = "initializing" /* INITIALIZING */;
17174
17438
  try {
17175
- await import("node:fs/promises").then((fs18) => fs18.mkdir(this.slotDir, { recursive: true, mode: 448 }));
17439
+ await import("node:fs/promises").then((fs19) => fs19.mkdir(this.slotDir, { recursive: true, mode: 448 }));
17176
17440
  } catch (err) {
17177
17441
  logger.warn(`[ResearchRunSemaphore] Could not create slot dir ${this.slotDir}: ${err instanceof Error ? err.message : String(err)}`);
17178
17442
  }
@@ -17325,9 +17589,9 @@ init_logger();
17325
17589
  init_atomic_replace();
17326
17590
  init_service_registry();
17327
17591
  init_service_names();
17328
- import * as fs12 from "node:fs/promises";
17592
+ import * as fs13 from "node:fs/promises";
17329
17593
  import * as crypto8 from "node:crypto";
17330
- import * as path14 from "node:path";
17594
+ import * as path15 from "node:path";
17331
17595
  var QUARANTINE_KEEP = 5;
17332
17596
  var StateBackupManager = class {
17333
17597
  name = ServiceNames.STATE_BACKUP_MANAGER;
@@ -17349,7 +17613,7 @@ var StateBackupManager = class {
17349
17613
  return;
17350
17614
  }
17351
17615
  this.lifecycle = "initializing" /* INITIALIZING */;
17352
- await fs12.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17616
+ await fs13.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17353
17617
  this._initialized = true;
17354
17618
  this.lifecycle = "initialized" /* INITIALIZED */;
17355
17619
  }
@@ -17363,15 +17627,15 @@ var StateBackupManager = class {
17363
17627
  async createBackup() {
17364
17628
  try {
17365
17629
  try {
17366
- await fs12.access(this.stateFilePath);
17630
+ await fs13.access(this.stateFilePath);
17367
17631
  } catch {
17368
17632
  return;
17369
17633
  }
17370
- await fs12.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17634
+ await fs13.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17371
17635
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
17372
17636
  const backupFileName = `research-state-${timestamp}-${crypto8.randomBytes(4).toString("hex")}.json`;
17373
- const backupFilePath = path14.join(this.backupDirPath, backupFileName);
17374
- await fs12.copyFile(this.stateFilePath, backupFilePath);
17637
+ const backupFilePath = path15.join(this.backupDirPath, backupFileName);
17638
+ await fs13.copyFile(this.stateFilePath, backupFilePath);
17375
17639
  } catch (error) {
17376
17640
  throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
17377
17641
  }
@@ -17381,14 +17645,14 @@ var StateBackupManager = class {
17381
17645
  */
17382
17646
  async cleanupOldBackups() {
17383
17647
  try {
17384
- const entries = await fs12.readdir(this.backupDirPath);
17648
+ const entries = await fs13.readdir(this.backupDirPath);
17385
17649
  if (entries.length <= this.maxBackups) {
17386
17650
  return;
17387
17651
  }
17388
17652
  const backupFiles = [];
17389
17653
  for (const entry of entries) {
17390
- const filePath = path14.join(this.backupDirPath, entry);
17391
- const stats = await fs12.stat(filePath);
17654
+ const filePath = path15.join(this.backupDirPath, entry);
17655
+ const stats = await fs13.stat(filePath);
17392
17656
  if (stats.isFile() && entry.startsWith("research-state-") && entry.endsWith(".json")) {
17393
17657
  backupFiles.push({ name: entry, mtime: stats.mtime });
17394
17658
  }
@@ -17396,8 +17660,8 @@ var StateBackupManager = class {
17396
17660
  backupFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
17397
17661
  const backupsToRemove = backupFiles.slice(this.maxBackups);
17398
17662
  for (const backupFile of backupsToRemove) {
17399
- const filePath = path14.join(this.backupDirPath, backupFile.name);
17400
- await fs12.unlink(filePath);
17663
+ const filePath = path15.join(this.backupDirPath, backupFile.name);
17664
+ await fs13.unlink(filePath);
17401
17665
  }
17402
17666
  } catch (error) {
17403
17667
  logger.error(`Failed to cleanup old backups: ${error instanceof Error ? error.message : String(error)}`);
@@ -17410,10 +17674,10 @@ var StateBackupManager = class {
17410
17674
  try {
17411
17675
  let backups = [];
17412
17676
  try {
17413
- const entries = await fs12.readdir(this.backupDirPath);
17677
+ const entries = await fs13.readdir(this.backupDirPath);
17414
17678
  for (const entry of entries) {
17415
- const filePath = path14.join(this.backupDirPath, entry);
17416
- const stats = await fs12.stat(filePath);
17679
+ const filePath = path15.join(this.backupDirPath, entry);
17680
+ const stats = await fs13.stat(filePath);
17417
17681
  if (stats.isFile() && entry.startsWith("research-state-") && entry.endsWith(".json")) {
17418
17682
  backups.push({ name: entry, mtime: stats.mtime });
17419
17683
  }
@@ -17422,10 +17686,10 @@ var StateBackupManager = class {
17422
17686
  }
17423
17687
  backups.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
17424
17688
  for (const backup of backups) {
17425
- const backupPath = path14.join(this.backupDirPath, backup.name);
17689
+ const backupPath = path15.join(this.backupDirPath, backup.name);
17426
17690
  let restoreContent;
17427
17691
  try {
17428
- const content = await fs12.readFile(backupPath, "utf-8");
17692
+ const content = await fs13.readFile(backupPath, "utf-8");
17429
17693
  const validated = this.validator.migrateAndValidate(JSON.parse(content));
17430
17694
  restoreContent = JSON.stringify(validated, null, 2);
17431
17695
  } catch (err) {
@@ -17453,27 +17717,27 @@ var StateBackupManager = class {
17453
17717
  */
17454
17718
  async quarantineFutureState(fileVersion) {
17455
17719
  try {
17456
- await fs12.access(this.stateFilePath);
17720
+ await fs13.access(this.stateFilePath);
17457
17721
  } catch {
17458
17722
  return;
17459
17723
  }
17460
- await fs12.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17724
+ await fs13.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17461
17725
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
17462
17726
  const name = `research-state-future-v${fileVersion}-${timestamp}.quarantine`;
17463
- const dest = path14.join(this.backupDirPath, name);
17464
- await fs12.copyFile(this.stateFilePath, dest);
17727
+ const dest = path15.join(this.backupDirPath, name);
17728
+ await fs13.copyFile(this.stateFilePath, dest);
17465
17729
  logger.warn(`[StateManager] Quarantined newer-build state (v${fileVersion}) copy at ${dest}`);
17466
17730
  await this.pruneQuarantineFiles(QUARANTINE_KEEP);
17467
17731
  }
17468
17732
  /** Keep only the newest `keep` *.quarantine files; delete the rest. Best-effort. */
17469
17733
  async pruneQuarantineFiles(keep) {
17470
17734
  try {
17471
- const entries = await fs12.readdir(this.backupDirPath);
17735
+ const entries = await fs13.readdir(this.backupDirPath);
17472
17736
  const quarantines = [];
17473
17737
  for (const entry of entries) {
17474
17738
  if (!entry.endsWith(".quarantine")) continue;
17475
17739
  try {
17476
- const stats = await fs12.stat(path14.join(this.backupDirPath, entry));
17740
+ const stats = await fs13.stat(path15.join(this.backupDirPath, entry));
17477
17741
  if (stats.isFile()) quarantines.push({ name: entry, mtime: stats.mtimeMs });
17478
17742
  } catch {
17479
17743
  }
@@ -17482,7 +17746,7 @@ var StateBackupManager = class {
17482
17746
  quarantines.sort((a, b) => b.mtime - a.mtime);
17483
17747
  for (const stale of quarantines.slice(keep)) {
17484
17748
  try {
17485
- await fs12.unlink(path14.join(this.backupDirPath, stale.name));
17749
+ await fs13.unlink(path15.join(this.backupDirPath, stale.name));
17486
17750
  } catch {
17487
17751
  }
17488
17752
  }
@@ -17494,11 +17758,11 @@ var StateBackupManager = class {
17494
17758
  * a Windows copy+delete fallback since rename fails on NTFS when the target exists).
17495
17759
  */
17496
17760
  async atomicWriteStateFile(content) {
17497
- const stateDir = path14.dirname(this.stateFilePath);
17498
- await fs12.mkdir(stateDir, { recursive: true, mode: 448 });
17761
+ const stateDir = path15.dirname(this.stateFilePath);
17762
+ await fs13.mkdir(stateDir, { recursive: true, mode: 448 });
17499
17763
  const tempFile = `research-state-${crypto8.randomBytes(16).toString("hex")}.tmp`;
17500
- const tempPath = path14.join(stateDir, tempFile);
17501
- const fh = await fs12.open(tempPath, "w", 384);
17764
+ const tempPath = path15.join(stateDir, tempFile);
17765
+ const fh = await fs13.open(tempPath, "w", 384);
17502
17766
  try {
17503
17767
  await fh.writeFile(content, { encoding: "utf-8" });
17504
17768
  await fh.sync();
@@ -17507,11 +17771,11 @@ var StateBackupManager = class {
17507
17771
  }
17508
17772
  try {
17509
17773
  if (await replaceFile(tempPath, this.stateFilePath) === "copied") {
17510
- await fs12.unlink(tempPath).catch(() => {
17774
+ await fs13.unlink(tempPath).catch(() => {
17511
17775
  });
17512
17776
  }
17513
17777
  } catch (renameErr) {
17514
- await fs12.unlink(tempPath).catch(() => {
17778
+ await fs13.unlink(tempPath).catch(() => {
17515
17779
  });
17516
17780
  throw renameErr;
17517
17781
  }
@@ -18149,7 +18413,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
18149
18413
  if (isNativeBindingError(sampleWorkerError)) {
18150
18414
  metrics.increment("browser_search_total_failures_total", 1, { cause: "native_binding" });
18151
18415
  throw new Error(
18152
- `Search completely failed: ${reason}, all with a missing native module. This is an incomplete install, not a network problem: better-sqlite3 13 ships prebuilt bindings for every supported platform and loads them at runtime, so a missing binding means an old or broken npm install. Upgrade npm to \u226512 (npm install -g npm@12) and reinstall this package \u2014 modern npm skips the needless recompile that fails on toolchain-less machines, and the prebuilt binding then loads as-is.` + (sampleWorkerError ? ` Last worker error: ${sampleWorkerError}` : "")
18416
+ `Search completely failed: ${reason}, all with a missing native module. This is an incomplete install, not a network problem: the browser launcher's native dependency (impit) ships a prebuilt binding per platform as a separate package, and npm sometimes drops it (npm/cli#4828). Reinstall this package (npm install, or pi install again) to restore it.` + (sampleWorkerError ? ` Last worker error: ${sampleWorkerError}` : "")
18153
18417
  );
18154
18418
  }
18155
18419
  throw new Error(
@@ -18330,7 +18594,7 @@ async function checkBrowserCapability() {
18330
18594
  }
18331
18595
  return { healthy: true, diagnostic: { status: mockMode ? "mocked" : "available" } };
18332
18596
  } else {
18333
- return { healthy: false, error: 'Camoufox (browser) not found. Run "npx camoufox-js fetch" to install the browser.' };
18597
+ return { healthy: false, error: 'Camoufox (browser) not found. Run "npx camoufox fetch" to install the browser.' };
18334
18598
  }
18335
18599
  }
18336
18600
  function knowledgeStoreDisabledStatus() {
@@ -18922,9 +19186,9 @@ function formatErrorWithCause(err) {
18922
19186
  function isPrivateIp(ip) {
18923
19187
  const parts = ip.split(".").map(Number);
18924
19188
  if (parts.length !== 4 || parts.some((p) => isNaN(p))) return false;
18925
- const [a, b] = parts;
19189
+ const [a, b, c] = parts;
18926
19190
  if (a === void 0 || b === void 0) return false;
18927
- return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a >= 224;
19191
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a === 192 && b === 0 && (c === 0 || c === 2) || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113 || a >= 224;
18928
19192
  }
18929
19193
  function isPrivateIpv6(ip) {
18930
19194
  const normalized = ip.toLowerCase().replace(/^\[|\]$/g, "");
@@ -19003,10 +19267,29 @@ ${kept.join("\n")}
19003
19267
  ${rest}` : rest;
19004
19268
  }
19005
19269
  function cleanConvertedMarkdown(markdown) {
19006
- return stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown))).replace(/\n{3,}/g, "\n\n").trim();
19270
+ return collapseBlankLines(stripEmptyFragmentLinks(stripImageLinks(trimFrontMatter(markdown)))).trim();
19271
+ }
19272
+ var CODE_FENCE = /^ {0,3}(`{3,}|~{3,})/;
19273
+ function collapseBlankLines(markdown) {
19274
+ if (!markdown.includes("\n\n\n")) return markdown;
19275
+ const out = [];
19276
+ let fence = null;
19277
+ for (const line of markdown.split("\n")) {
19278
+ const m = CODE_FENCE.exec(line);
19279
+ if (fence === null) {
19280
+ if (m) fence = m[1];
19281
+ } else if (m && m[1][0] === fence[0] && m[1].length >= fence.length && line.slice(m[0].length).trim() === "") {
19282
+ fence = null;
19283
+ }
19284
+ if (fence === null && line === "" && out.length > 0 && out[out.length - 1] === "") continue;
19285
+ out.push(line);
19286
+ }
19287
+ return out.join("\n");
19007
19288
  }
19008
19289
  function stripImageLinks(markdown) {
19009
- return markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1").replace(/\n{3,}/g, "\n\n").trim();
19290
+ return collapseBlankLines(
19291
+ markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1")
19292
+ ).trim();
19010
19293
  }
19011
19294
  function createNativeMarkdownConverter(nativeModule) {
19012
19295
  return async (html) => {
@@ -19079,11 +19362,11 @@ init_service_registry();
19079
19362
  init_logger();
19080
19363
  init_metrics();
19081
19364
  init_error_tracker();
19082
- import { existsSync as existsSync11 } from "node:fs";
19083
- import { dirname as dirname12, join as join20 } from "node:path";
19365
+ import { existsSync as existsSync12 } from "node:fs";
19366
+ import { dirname as dirname13, join as join21 } from "node:path";
19084
19367
  import { fileURLToPath as fileURLToPath4 } from "node:url";
19085
19368
  import { Worker } from "node:worker_threads";
19086
- var MODULE_DIR = dirname12(fileURLToPath4(import.meta.url));
19369
+ var MODULE_DIR = dirname13(fileURLToPath4(import.meta.url));
19087
19370
  var PdfExtractError = class extends Error {
19088
19371
  kind;
19089
19372
  constructor(kind, message, options) {
@@ -19151,8 +19434,8 @@ var nextRequestId = 1;
19151
19434
  var pendingRequests = /* @__PURE__ */ new Map();
19152
19435
  var warnedWorkerUnavailable = false;
19153
19436
  function resolveWorkerPath() {
19154
- const candidate = join20(MODULE_DIR, WORKER_FILENAME);
19155
- return existsSync11(candidate) ? candidate : null;
19437
+ const candidate = join21(MODULE_DIR, WORKER_FILENAME);
19438
+ return existsSync12(candidate) ? candidate : null;
19156
19439
  }
19157
19440
  function pdfWorkerEnabled() {
19158
19441
  if (process.env["PI_RESEARCH_PDF_WORKER"] === "off") return false;
@@ -19316,7 +19599,7 @@ var FETCH_RETRY_DELAY_MS = 250;
19316
19599
  var playwrightAvailable = false;
19317
19600
  var markdownConverterPromise = null;
19318
19601
  function initScraperDependencies() {
19319
- playwrightAvailable = checkModule("playwright-core") && checkModule("camoufox-js");
19602
+ playwrightAvailable = checkModule("playwright-core") && checkModule("@camoufox/camoufox");
19320
19603
  }
19321
19604
  initScraperDependencies();
19322
19605
  async function getMarkdownConverter() {
@@ -24052,6 +24335,18 @@ function isUnretriableResearcherError(error) {
24052
24335
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
24053
24336
  return message.includes("requires more credits") || message.includes("insufficient credits") || message.includes("insufficient_quota");
24054
24337
  }
24338
+ function isInFlightBudgetError(error) {
24339
+ const lower = (error instanceof Error ? error.message : String(error)).toLowerCase();
24340
+ return lower.includes("in_flight_budget_exhausted") || lower.includes("would exceed your available credits given your current in-flight requests");
24341
+ }
24342
+ function parseRetryAfterMs(error) {
24343
+ const message = error instanceof Error ? error.message : String(error);
24344
+ const m = /["']?retry-after["']?\s*[:=]\s*["']?(\d{1,4})/i.exec(message);
24345
+ if (!m) return null;
24346
+ const seconds = Number(m[1]);
24347
+ if (!Number.isFinite(seconds) || seconds <= 0) return null;
24348
+ return Math.min(Math.max(seconds, 5), 60) * 1e3;
24349
+ }
24055
24350
  async function runResearcher(options) {
24056
24351
  const {
24057
24352
  config: researcherConfig,
@@ -24144,6 +24439,7 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
24144
24439
  const researcherExecutionStartMs = Date.now();
24145
24440
  const deliveredSteeringIds = /* @__PURE__ */ new Set();
24146
24441
  const researcherSystemPrompt = injectCurrentDate(researcherPromptTemplate, "researcher").replace("{{extra_tool_guidelines}}", "").replaceAll("{{max_scrape_urls}}", String(config.MAX_SCRAPE_URLS)).replace("{{digest_section}}", () => RESEARCHER_DIGEST_SECTION).trim();
24442
+ let inFlightBackoffMs = null;
24147
24443
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
24148
24444
  if (signal?.aborted) throw new Error("Aborted");
24149
24445
  if (stopRequested()) {
@@ -24151,7 +24447,8 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
24151
24447
  return;
24152
24448
  }
24153
24449
  if (attempt > 1) {
24154
- const delay2 = Math.min(1e3 * Math.pow(2, attempt - 2), config.RESEARCHER_MAX_RETRY_DELAY_MS);
24450
+ const delay2 = inFlightBackoffMs ?? Math.min(1e3 * Math.pow(2, attempt - 2), config.RESEARCHER_MAX_RETRY_DELAY_MS);
24451
+ inFlightBackoffMs = null;
24155
24452
  logger.warn(`[ResearcherExecutor] Researcher ${id} retry ${attempt - 1}/${config.RESEARCHER_MAX_RETRIES} after ${delay2}ms`);
24156
24453
  observer?.onResearcherProgress?.(id, "retry");
24157
24454
  await new Promise((resolve9) => {
@@ -24394,6 +24691,11 @@ ${responseText}`);
24394
24691
  metrics.increment("researcher_unretriable_total", 1, { mode: "deep", complexity: String(complexity) });
24395
24692
  break;
24396
24693
  }
24694
+ if (isInFlightBudgetError(err) && attempt < maxAttempts) {
24695
+ inFlightBackoffMs = parseRetryAfterMs(err) ?? 15e3;
24696
+ metrics.increment("researcher_inflight_budget_backoff_total", 1, { mode: "deep", complexity: String(complexity) });
24697
+ logger.warn(`[ResearcherExecutor] Researcher ${id} hit the provider's in-flight budget; backing off ${inFlightBackoffMs}ms (provider Retry-After, else 15s) before attempt ${attempt + 1}`);
24698
+ }
24397
24699
  if (attempt < maxAttempts) {
24398
24700
  logger.warn(`[ResearcherExecutor] Researcher ${id} attempt ${attempt} failed: ${errMsg}; will retry`);
24399
24701
  } else {
@@ -27074,9 +27376,9 @@ init_url_utils();
27074
27376
  // src/utils/research-export.ts
27075
27377
  init_logger();
27076
27378
  init_constants();
27077
- import { promises as fs15 } from "node:fs";
27078
- import { homedir as homedir4, tmpdir as tmpdir4, platform as platform3 } from "node:os";
27079
- import { join as join21, resolve as resolve5, normalize, sep as sep2 } from "node:path";
27379
+ import { promises as fs16 } from "node:fs";
27380
+ import { homedir as homedir6, tmpdir as tmpdir4, platform as platform4 } from "node:os";
27381
+ import { join as join22, resolve as resolve5, normalize, sep as sep2 } from "node:path";
27080
27382
  import { randomBytes as randomBytes6 } from "node:crypto";
27081
27383
  var PREFERRED_SUBDIRS = [
27082
27384
  "research",
@@ -27118,9 +27420,9 @@ var WIN_SYSTEM_PREFIXES_LC = [
27118
27420
  ];
27119
27421
  function isSafeToWriteDirectly(dir) {
27120
27422
  const normalized = normalize(resolve5(dir));
27121
- const home = normalize(resolve5(homedir4()));
27423
+ const home = normalize(resolve5(homedir6()));
27122
27424
  if (normalized === home) return false;
27123
- if (platform3() === "win32") {
27425
+ if (platform4() === "win32") {
27124
27426
  if (/^[a-zA-Z]:\\?$/.test(normalized)) return false;
27125
27427
  const lc = normalized.toLowerCase();
27126
27428
  for (const prefix of WIN_SYSTEM_PREFIXES_LC) {
@@ -27140,16 +27442,16 @@ function isSafeToWriteDirectly(dir) {
27140
27442
  async function findPreferredSubdir(cwd) {
27141
27443
  let entries;
27142
27444
  try {
27143
- entries = await fs15.readdir(cwd);
27445
+ entries = await fs16.readdir(cwd);
27144
27446
  } catch {
27145
27447
  return null;
27146
27448
  }
27147
27449
  for (const target of PREFERRED_SUBDIRS) {
27148
27450
  const match = entries.find((e) => e.toLowerCase() === target);
27149
27451
  if (match === void 0) continue;
27150
- const candidate = join21(cwd, match);
27452
+ const candidate = join22(cwd, match);
27151
27453
  try {
27152
- const stat6 = await fs15.stat(candidate);
27454
+ const stat6 = await fs16.stat(candidate);
27153
27455
  if (stat6.isDirectory()) return candidate;
27154
27456
  } catch {
27155
27457
  }
@@ -27181,7 +27483,7 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
27181
27483
  if (explicitDir && explicitDir.trim()) {
27182
27484
  targetDir = resolve5(explicitDir.trim());
27183
27485
  try {
27184
- await fs15.mkdir(targetDir, { recursive: true });
27486
+ await fs16.mkdir(targetDir, { recursive: true });
27185
27487
  } catch (error) {
27186
27488
  logger.error(`[export] Could not create export dir "${targetDir}":`, error);
27187
27489
  return null;
@@ -27192,9 +27494,9 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
27192
27494
  for (let attempt = 0; attempt < MAX_EXPORT_RETRIES; attempt++) {
27193
27495
  const hash = generateHash();
27194
27496
  const filename = `${baseFilename}-${hash}.md`;
27195
- const filepath = join21(targetDir, filename);
27497
+ const filepath = join22(targetDir, filename);
27196
27498
  try {
27197
- await fs15.writeFile(filepath, result, { flag: "wx" });
27499
+ await fs16.writeFile(filepath, result, { flag: "wx", mode: 384 });
27198
27500
  logger.log(`[export] Research report saved to: ${filepath}`);
27199
27501
  return filepath;
27200
27502
  } catch (error) {
@@ -27228,7 +27530,7 @@ import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
27228
27530
  function readPkgVersion() {
27229
27531
  try {
27230
27532
  const pkgPath = fileURLToPath6(new URL("../package.json", import.meta.url));
27231
- const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf-8"));
27533
+ const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
27232
27534
  return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
27233
27535
  } catch {
27234
27536
  return "0.0.0";
@@ -27270,7 +27572,7 @@ var EXIT = {
27270
27572
  CANCELLED: 130
27271
27573
  };
27272
27574
  function exitCodeForSignal(sig) {
27273
- const signum = os7.constants.signals[sig];
27575
+ const signum = os8.constants.signals[sig];
27274
27576
  return typeof signum === "number" ? 128 + signum : EXIT.CANCELLED;
27275
27577
  }
27276
27578
  function toStderr(s) {
@@ -27361,11 +27663,11 @@ function bridgeConfigEnv(explicitConfigPath) {
27361
27663
  for (const filePath of layers) {
27362
27664
  const isExplicit = explicitConfigPath !== void 0 && filePath === explicitConfigPath;
27363
27665
  try {
27364
- if (!fs17.existsSync(filePath)) {
27666
+ if (!fs18.existsSync(filePath)) {
27365
27667
  if (isExplicit) throw new ConfigFileError(`--config file not found: ${filePath}`);
27366
27668
  continue;
27367
27669
  }
27368
- const parsed = parseDotEnv(fs17.readFileSync(filePath, "utf-8"));
27670
+ const parsed = parseDotEnv(fs18.readFileSync(filePath, "utf-8"));
27369
27671
  Object.assign(merged, parsed);
27370
27672
  if (isExplicit) {
27371
27673
  for (const k of Object.keys(parsed)) fromExplicitBase.add(k);
@@ -27396,14 +27698,14 @@ function resolvedConfigPaths() {
27396
27698
  configDir,
27397
27699
  cliIfaceEnv: getInterfaceEnvFilePath("cli"),
27398
27700
  piIfaceEnv: getInterfaceEnvFilePath("pi"),
27399
- piAuth: path19.join(agentDir, "auth.json"),
27400
- piModels: path19.join(agentDir, "models.json"),
27701
+ piAuth: path20.join(agentDir, "auth.json"),
27702
+ piModels: path20.join(agentDir, "models.json"),
27401
27703
  piState: getStateDir()
27402
27704
  };
27403
27705
  }
27404
27706
  function fileExists(p) {
27405
27707
  try {
27406
- return fs17.existsSync(p);
27708
+ return fs18.existsSync(p);
27407
27709
  } catch {
27408
27710
  return false;
27409
27711
  }
@@ -27523,6 +27825,10 @@ function configBlock(det, extraNote) {
27523
27825
  if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
27524
27826
  return lines.join("\n");
27525
27827
  }
27828
+ function resolveCliDepth(explicit, cfg) {
27829
+ const d = explicit ?? (cfg.QUICK_RESEARCH ? cfg.DEFAULT_RESEARCH_DEPTH : Math.max(1, cfg.DEFAULT_RESEARCH_DEPTH));
27830
+ return d;
27831
+ }
27526
27832
  async function cmdResearch(args) {
27527
27833
  const det = await detectCredentials(args.model);
27528
27834
  if (det.problem) {
@@ -27545,7 +27851,7 @@ Error: ${vmsg}
27545
27851
  `);
27546
27852
  return EXIT.USAGE;
27547
27853
  }
27548
- const depth = args.depth ?? getConfig(process.cwd(), "cli").DEFAULT_RESEARCH_DEPTH;
27854
+ const depth = resolveCliDepth(args.depth, getConfig(process.cwd(), "cli"));
27549
27855
  const runModel = det.model;
27550
27856
  toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ""}\u2026
27551
27857
  `);
@@ -27695,15 +28001,12 @@ async function cmdStatus(json) {
27695
28001
  installed: browserInstalled,
27696
28002
  nativeDepsOk: nativeDeps.ok,
27697
28003
  nativeDepsError: nativeDeps.ok ? null : nativeDeps.error,
27698
- // better-sqlite3 13 ships prebuilds/ for all eight platform/arch pairs and
27699
- // loads them at runtime via node-gyp-build — no install script involved.
27700
- // On modern npm (≥11.19) the injected `node-gyp rebuild` that a
27701
- // binding.gyp-without-install-script triggers is skipped, so a missing
27702
- // binding here means an old/broken install, not an approval problem:
27703
- // point at the upgrade that fixes it.
27704
- nativeDepsFix: nativeDeps.ok ? null : "Upgrade npm and reinstall: npm install -g npm@12, then reinstall @lincoln504/pi-research. better-sqlite3 13 ships prebuilt bindings (no script needed); older npm needlessly recompiles it and fails on machines without a C++ toolchain.",
28004
+ // impit ships its binding as a per-platform optional package, no install script
28005
+ // involved: a missing binding means npm dropped that package, and a reinstall
28006
+ // restores it.
28007
+ nativeDepsFix: nativeDeps.ok ? null : "Reinstall @lincoln504/pi-research (npm install). The browser launcher's native module (impit) ships as a separate platform package that npm sometimes drops (npm/cli#4828); a fresh install restores it.",
27705
28008
  // Same remedy the health check prints, so both surfaces name one command.
27706
- fix: browserInstalled ? null : "npx camoufox-js fetch",
28009
+ fix: browserInstalled ? null : "npx camoufox fetch",
27707
28010
  note: browserInstalled ? null : "Stealth browser not installed \u2014 scraping falls back to plain fetch, which some sites block. npm 12 blocks install scripts by default, which skips this package's postinstall; the engine is fetched on first use instead."
27708
28011
  },
27709
28012
  knowledgeStoreMode: cfg.KNOWLEDGE_STORE_MODE,
@@ -27815,7 +28118,7 @@ change the machine-wide defaults: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> an
27815
28118
  }
27816
28119
  function cmdCleanup(c) {
27817
28120
  const script = fileURLToPath6(new URL("../scripts/cleanup.cjs", import.meta.url));
27818
- if (!fs17.existsSync(script)) {
28121
+ if (!fs18.existsSync(script)) {
27819
28122
  toStderr(
27820
28123
  `
27821
28124
  Error: cleanup script not found at ${script}.
@@ -28094,7 +28397,8 @@ COMMANDS
28094
28397
  state tree (<config dir>/research/state) and the cache, including
28095
28398
  downloaded embedding models. Keeps config.env and the knowledge
28096
28399
  store database. --purge-browsers also deletes the shared
28097
- stealth-browser cache (~/.cache/camoufox) other tools use.
28400
+ stealth-browser cache (~/.cache/camoufox, every installed
28401
+ browser build) other tools use.
28098
28402
 
28099
28403
  help, --help, -h Show this help.
28100
28404
  --version, -v Print the pi-research version.
@@ -28488,8 +28792,8 @@ Error: ${err.message}
28488
28792
  var _isMain = (() => {
28489
28793
  try {
28490
28794
  if (!process.argv[1]) return false;
28491
- const modulePath = fs17.realpathSync(path19.resolve(fileURLToPath6(import.meta.url)));
28492
- const invokedPath = fs17.realpathSync(path19.resolve(process.argv[1]));
28795
+ const modulePath = fs18.realpathSync(path20.resolve(fileURLToPath6(import.meta.url)));
28796
+ const invokedPath = fs18.realpathSync(path20.resolve(process.argv[1]));
28493
28797
  return modulePath === invokedPath;
28494
28798
  } catch {
28495
28799
  return false;
@@ -28518,5 +28822,6 @@ export {
28518
28822
  exitCodeForSignal,
28519
28823
  makeProgressObserver,
28520
28824
  parseArgs,
28521
- reportError
28825
+ reportError,
28826
+ resolveCliDepth
28522
28827
  };