@lincoln504/pi-research 1.7.8 → 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 (34) 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 +3 -0
  4. package/dist/cli.mjs +652 -380
  5. package/dist/pdf-extract-worker.mjs +1 -1
  6. package/dist/thread-worker.mjs +6 -6
  7. package/docs/AGENT-SKILL.md +3 -3
  8. package/docs/ARCHITECTURE.md +64 -50
  9. package/docs/CHANGELOG.md +21 -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 +3 -0
  18. package/src/cli.ts +12 -13
  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 +6 -6
  27. package/src/infrastructure/knowledge-store-service.ts +6 -6
  28. package/src/orchestration/researcher-executor.ts +47 -1
  29. package/src/tui/research-health.ts +1 -1
  30. package/src/utils/log-utils.ts +1 -1
  31. package/src/web-research/browser-search.ts +8 -10
  32. package/src/web-research/pdf-extract-worker.mjs +1 -1
  33. package/src/web-research/scraper-utils.ts +11 -2
  34. package/src/web-research/web-scraper.ts +1 -1
package/dist/cli.mjs CHANGED
@@ -277,7 +277,7 @@ var init_log_utils = __esm({
277
277
  "gi"
278
278
  );
279
279
  KNOWN_TOKEN_PATTERN = new RegExp(
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}|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}`,
281
281
  "g"
282
282
  );
283
283
  JWT_PATTERN = new RegExp(
@@ -4563,12 +4563,12 @@ async function isModelCached(model) {
4563
4563
  const cacheDir = env.cacheDir;
4564
4564
  if (!cacheDir) return false;
4565
4565
  const { access: access3, readdir: readdir6, readFile: readFile7, stat: stat6, rm: rm3 } = await import("node:fs/promises");
4566
- const path20 = await import("node:path");
4567
- const modelDir = path20.default.join(cacheDir, model);
4568
- const onnxDir = path20.default.join(modelDir, "onnx");
4569
- 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"));
4570
4570
  try {
4571
- 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");
4572
4572
  JSON.parse(cfgRaw);
4573
4573
  } catch {
4574
4574
  return false;
@@ -4576,7 +4576,7 @@ async function isModelCached(model) {
4576
4576
  const tokenizerFiles = ["tokenizer.json", "tokenizer.model", "vocab.txt"];
4577
4577
  let hasTokenizer = false;
4578
4578
  for (const f of tokenizerFiles) {
4579
- const st = await stat6(path20.default.join(modelDir, f)).catch(() => null);
4579
+ const st = await stat6(path21.default.join(modelDir, f)).catch(() => null);
4580
4580
  if (st && st.size > 0) {
4581
4581
  hasTokenizer = true;
4582
4582
  break;
@@ -4584,7 +4584,7 @@ async function isModelCached(model) {
4584
4584
  }
4585
4585
  if (!hasTokenizer) return false;
4586
4586
  const entries = await readdir6(onnxDir).catch(() => []);
4587
- const onnxPath = path20.default.join(onnxDir, "model.onnx");
4587
+ const onnxPath = path21.default.join(onnxDir, "model.onnx");
4588
4588
  const onnxStat = await stat6(onnxPath).catch(() => null);
4589
4589
  if (onnxStat && onnxStat.size > 0 && onnxStat.size <= 1024 * 1024) {
4590
4590
  try {
@@ -4604,7 +4604,7 @@ async function isModelCached(model) {
4604
4604
  }
4605
4605
  const hasExternalWeights = entries.includes("model.onnx_data");
4606
4606
  if (hasExternalWeights) {
4607
- 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);
4608
4608
  if (!dataStat || dataStat.size === 0) return false;
4609
4609
  }
4610
4610
  const partialArtifacts = entries.filter(
@@ -4613,7 +4613,7 @@ async function isModelCached(model) {
4613
4613
  if (partialArtifacts.length > 0) {
4614
4614
  if (hasExternalWeights) {
4615
4615
  for (const f of partialArtifacts) {
4616
- await rm3(path20.default.join(onnxDir, f), { force: true }).catch(() => {
4616
+ await rm3(path21.default.join(onnxDir, f), { force: true }).catch(() => {
4617
4617
  });
4618
4618
  }
4619
4619
  } else {
@@ -4639,10 +4639,10 @@ async function purgeModelCache(model) {
4639
4639
  const cacheDir = env.cacheDir;
4640
4640
  if (!cacheDir) return;
4641
4641
  const { rm: rm3 } = await import("node:fs/promises");
4642
- const path20 = await import("node:path");
4643
- const resolvedCache = path20.default.resolve(cacheDir);
4644
- const modelDir = path20.default.resolve(resolvedCache, model);
4645
- 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)) {
4646
4646
  logger.warn(`[embedder] Refusing to purge unsafe model-cache path for model id ${JSON.stringify(model)}`);
4647
4647
  return;
4648
4648
  }
@@ -6219,7 +6219,7 @@ var init_embedding_client = __esm({
6219
6219
  }
6220
6220
  }
6221
6221
  // ---- Internal HTTP helpers ----
6222
- getRequest(path20, timeoutMs = 12e4) {
6222
+ getRequest(path21, timeoutMs = 12e4) {
6223
6223
  return new Promise((resolve9, reject) => {
6224
6224
  let resolved = false;
6225
6225
  let req;
@@ -6227,7 +6227,7 @@ var init_embedding_client = __esm({
6227
6227
  if (!resolved) {
6228
6228
  resolved = true;
6229
6229
  req?.destroy();
6230
- reject(new Error(`[EmbeddingClient] GET ${path20} timed out after ${timeoutMs}ms`));
6230
+ reject(new Error(`[EmbeddingClient] GET ${path21} timed out after ${timeoutMs}ms`));
6231
6231
  }
6232
6232
  }, timeoutMs);
6233
6233
  if (timer.unref) timer.unref();
@@ -6235,7 +6235,7 @@ var init_embedding_client = __esm({
6235
6235
  {
6236
6236
  hostname: "127.0.0.1",
6237
6237
  port: this.port,
6238
- path: path20,
6238
+ path: path21,
6239
6239
  method: "GET",
6240
6240
  headers: this.authHeaders()
6241
6241
  },
@@ -6251,19 +6251,19 @@ var init_embedding_client = __esm({
6251
6251
  try {
6252
6252
  const parsed = JSON.parse(collected.toString());
6253
6253
  if (res.statusCode !== 200) {
6254
- reject(new Error(`[EmbeddingClient] GET ${path20} returned HTTP ${res.statusCode}`));
6254
+ reject(new Error(`[EmbeddingClient] GET ${path21} returned HTTP ${res.statusCode}`));
6255
6255
  } else {
6256
6256
  resolve9(parsed);
6257
6257
  }
6258
6258
  } catch (_e) {
6259
- 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()}`));
6260
6260
  }
6261
6261
  });
6262
6262
  res.on("error", (err) => {
6263
6263
  if (resolved) return;
6264
6264
  resolved = true;
6265
6265
  clearTimeout(timer);
6266
- reject(new Error(`[EmbeddingClient] GET ${path20} response error: ${err.message}`));
6266
+ reject(new Error(`[EmbeddingClient] GET ${path21} response error: ${err.message}`));
6267
6267
  });
6268
6268
  }
6269
6269
  );
@@ -6271,12 +6271,12 @@ var init_embedding_client = __esm({
6271
6271
  if (resolved) return;
6272
6272
  resolved = true;
6273
6273
  clearTimeout(timer);
6274
- reject(new Error(`[EmbeddingClient] GET ${path20} request error: ${err.message}`));
6274
+ reject(new Error(`[EmbeddingClient] GET ${path21} request error: ${err.message}`));
6275
6275
  });
6276
6276
  req.end();
6277
6277
  });
6278
6278
  }
6279
- request(path20, data) {
6279
+ request(path21, data) {
6280
6280
  return new Promise((resolve9, reject) => {
6281
6281
  const timeoutMs = 12e4;
6282
6282
  let resolved = false;
@@ -6285,7 +6285,7 @@ var init_embedding_client = __esm({
6285
6285
  if (!resolved) {
6286
6286
  resolved = true;
6287
6287
  req?.destroy();
6288
- reject(new Error(`[EmbeddingClient] POST ${path20} timed out after ${timeoutMs}ms`));
6288
+ reject(new Error(`[EmbeddingClient] POST ${path21} timed out after ${timeoutMs}ms`));
6289
6289
  }
6290
6290
  }, timeoutMs);
6291
6291
  if (timer.unref) timer.unref();
@@ -6294,7 +6294,7 @@ var init_embedding_client = __esm({
6294
6294
  {
6295
6295
  hostname: "127.0.0.1",
6296
6296
  port: this.port,
6297
- path: path20,
6297
+ path: path21,
6298
6298
  method: "POST",
6299
6299
  headers: {
6300
6300
  "Content-Type": "application/json",
@@ -6320,14 +6320,14 @@ var init_embedding_client = __esm({
6320
6320
  resolve9(parsed);
6321
6321
  }
6322
6322
  } catch (_e) {
6323
- 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()}`));
6324
6324
  }
6325
6325
  });
6326
6326
  res.on("error", (err) => {
6327
6327
  if (resolved) return;
6328
6328
  resolved = true;
6329
6329
  clearTimeout(timer);
6330
- reject(new Error(`[EmbeddingClient] POST ${path20} response error: ${err.message}`));
6330
+ reject(new Error(`[EmbeddingClient] POST ${path21} response error: ${err.message}`));
6331
6331
  });
6332
6332
  }
6333
6333
  );
@@ -6335,7 +6335,7 @@ var init_embedding_client = __esm({
6335
6335
  if (resolved) return;
6336
6336
  resolved = true;
6337
6337
  clearTimeout(timer);
6338
- reject(new Error(`[EmbeddingClient] POST ${path20} request error: ${err.message}`));
6338
+ reject(new Error(`[EmbeddingClient] POST ${path21} request error: ${err.message}`));
6339
6339
  });
6340
6340
  req.write(body);
6341
6341
  req.end();
@@ -7788,9 +7788,9 @@ var init_store_operations = __esm({
7788
7788
  });
7789
7789
 
7790
7790
  // src/knowledge/store.ts
7791
- import * as fs13 from "node:fs";
7791
+ import * as fs14 from "node:fs";
7792
7792
  import * as fsPromises from "node:fs/promises";
7793
- import * as path15 from "node:path";
7793
+ import * as path16 from "node:path";
7794
7794
  function isLanceCommitConflict(err) {
7795
7795
  const msg2 = err instanceof Error ? err.message : String(err);
7796
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");
@@ -7852,7 +7852,7 @@ var init_store = __esm({
7852
7852
  constructor(options) {
7853
7853
  this.options = options;
7854
7854
  this.tableName = this.retrieval === "bm25" ? BM25_TABLE_NAME : "knowledge";
7855
- this.manifestPath = path15.join(this.options.dbDir, "store-manifest.json");
7855
+ this.manifestPath = path16.join(this.options.dbDir, "store-manifest.json");
7856
7856
  }
7857
7857
  /** Normalized retrieval strategy: an unset option means the historical
7858
7858
  * 'vector' (hybrid) behavior, not bm25. Every mode check goes through this. */
@@ -7890,7 +7890,7 @@ var init_store = __esm({
7890
7890
  }
7891
7891
  async loadManifest() {
7892
7892
  try {
7893
- if (fs13.existsSync(this.manifestPath)) {
7893
+ if (fs14.existsSync(this.manifestPath)) {
7894
7894
  const content = await fsPromises.readFile(this.manifestPath, "utf-8");
7895
7895
  const manifest = JSON.parse(content);
7896
7896
  const savedVectorTable = typeof manifest.activeTableName === "string" ? manifest.activeTableName : null;
@@ -7935,7 +7935,7 @@ var init_store = __esm({
7935
7935
  return true;
7936
7936
  } catch (err) {
7937
7937
  logger.warn("[store] Failed to save manifest:", err);
7938
- if (fs13.existsSync(tempPath)) {
7938
+ if (fs14.existsSync(tempPath)) {
7939
7939
  try {
7940
7940
  await fsPromises.unlink(tempPath);
7941
7941
  } catch {
@@ -7967,8 +7967,8 @@ var init_store = __esm({
7967
7967
  async openInternal() {
7968
7968
  if (this.db) return;
7969
7969
  try {
7970
- if (!fs13.existsSync(this.options.dbDir)) {
7971
- fs13.mkdirSync(this.options.dbDir, { recursive: true, mode: 448 });
7970
+ if (!fs14.existsSync(this.options.dbDir)) {
7971
+ fs14.mkdirSync(this.options.dbDir, { recursive: true, mode: 448 });
7972
7972
  }
7973
7973
  this.db = await (await getLancedb()).connect(this.options.dbDir);
7974
7974
  const tableNames = await this.db.tableNames();
@@ -8091,10 +8091,10 @@ var init_store = __esm({
8091
8091
  logger.warn(`[store] countRows failed before backup (continuing): ${err instanceof Error ? err.message : String(err)}`);
8092
8092
  }
8093
8093
  this.table = null;
8094
- const oldDir = path15.join(this.options.dbDir, `${this.tableName}.lance`);
8095
- 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`);
8096
8096
  try {
8097
- if (fs13.existsSync(oldDir)) {
8097
+ if (fs14.existsSync(oldDir)) {
8098
8098
  await fsPromises.rename(oldDir, backupDir);
8099
8099
  logger.info(`[store] Successfully backed up database directory to ${backupDir}`);
8100
8100
  }
@@ -8184,8 +8184,8 @@ var init_store = __esm({
8184
8184
  await this.db.dropTable(oldTableName);
8185
8185
  logger.info(`[store] Renaming ${tempTableName} to ${canonicalName}...`);
8186
8186
  try {
8187
- const tempDir = path15.join(this.options.dbDir, `${tempTableName}.lance`);
8188
- 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`);
8189
8189
  await fsPromises.rename(tempDir, canonicalDir);
8190
8190
  this.tableName = canonicalName;
8191
8191
  if (await this.saveManifest()) {
@@ -8559,9 +8559,9 @@ var init_store = __esm({
8559
8559
  };
8560
8560
  });
8561
8561
  try {
8562
- const dir = path15.dirname(outputPath);
8563
- await fs13.promises.mkdir(dir, { recursive: true });
8564
- 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");
8565
8565
  logger.info(`[store] Successfully exported ${exportData.length} entries to ${outputPath}`);
8566
8566
  } catch (err) {
8567
8567
  logger.error(`[store] Failed to export knowledge store for web:`, err);
@@ -8760,7 +8760,7 @@ var init_store = __esm({
8760
8760
  else if (name.includes("_migration_")) migrationDirs.push(name);
8761
8761
  }
8762
8762
  for (const name of migrationDirs) {
8763
- const fullPath = path15.join(this.options.dbDir, name);
8763
+ const fullPath = path16.join(this.options.dbDir, name);
8764
8764
  logger.info(`[store] Removing orphaned migration temp directory: ${name}`);
8765
8765
  await fsPromises.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8766
8766
  }
@@ -8773,7 +8773,7 @@ var init_store = __esm({
8773
8773
  const backupCutoff = Math.trunc(Date.now() - ttlDays * 24 * 60 * 60 * 1e3);
8774
8774
  for (const { name, ts } of ranked.slice(0, -1)) {
8775
8775
  if (ts >= backupCutoff) continue;
8776
- const fullPath = path15.join(this.options.dbDir, name);
8776
+ const fullPath = path16.join(this.options.dbDir, name);
8777
8777
  logger.info(`[store] Removing superseded backup directory: ${name}`);
8778
8778
  await fsPromises.rm(fullPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8779
8779
  }
@@ -8796,7 +8796,7 @@ var init_store = __esm({
8796
8796
  if (!Number.isNaN(parsed)) return parsed;
8797
8797
  }
8798
8798
  try {
8799
- const stat6 = await fsPromises.stat(path15.join(this.options.dbDir, name));
8799
+ const stat6 = await fsPromises.stat(path16.join(this.options.dbDir, name));
8800
8800
  return stat6.mtimeMs;
8801
8801
  } catch {
8802
8802
  return 0;
@@ -9310,8 +9310,8 @@ var init_chunker = __esm({
9310
9310
  });
9311
9311
 
9312
9312
  // src/knowledge/index.ts
9313
- import * as fs14 from "node:fs";
9314
- import * as path16 from "node:path";
9313
+ import * as fs15 from "node:fs";
9314
+ import * as path17 from "node:path";
9315
9315
  function getMigrationStrategy(config) {
9316
9316
  const strategy = config.MIGRATION_STRATEGY;
9317
9317
  if (!strategy) return void 0;
@@ -9378,12 +9378,12 @@ async function createKnowledgeStoreComponents(embedderFactory, reconnectFactory,
9378
9378
  }
9379
9379
  async function forceDeleteKnowledgeStore(config, workspace) {
9380
9380
  const dbDir = getDbDir(config, workspace);
9381
- if (!fs14.existsSync(dbDir)) return;
9381
+ if (!fs15.existsSync(dbDir)) return;
9382
9382
  let tableName = "knowledge";
9383
- const manifestPath = path16.join(dbDir, "store-manifest.json");
9383
+ const manifestPath = path17.join(dbDir, "store-manifest.json");
9384
9384
  try {
9385
- if (fs14.existsSync(manifestPath)) {
9386
- const manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf-8"));
9385
+ if (fs15.existsSync(manifestPath)) {
9386
+ const manifest = JSON.parse(fs15.readFileSync(manifestPath, "utf-8"));
9387
9387
  const key = config?.KNOWLEDGE_STORE_RETRIEVAL === "bm25" ? "activeBm25TableName" : "activeTableName";
9388
9388
  const saved = key === "activeBm25TableName" ? manifest.activeBm25TableName : manifest.activeTableName;
9389
9389
  if (saved) tableName = saved;
@@ -9391,10 +9391,10 @@ async function forceDeleteKnowledgeStore(config, workspace) {
9391
9391
  } catch (err) {
9392
9392
  logger.warn(`[knowledge] Failed to read manifest before forced delete, defaulting to table '${tableName}':`, err);
9393
9393
  }
9394
- const tableDir = path16.join(dbDir, `${tableName}.lance`);
9395
- if (!fs14.existsSync(tableDir)) return;
9394
+ const tableDir = path17.join(dbDir, `${tableName}.lance`);
9395
+ if (!fs15.existsSync(tableDir)) return;
9396
9396
  try {
9397
- fs14.rmSync(tableDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
9397
+ fs15.rmSync(tableDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
9398
9398
  logger.info(`[knowledge] Corrupted table directory ${tableDir} forcefully deleted; will be recreated on next init.`);
9399
9399
  } catch (err) {
9400
9400
  logger.error(`[knowledge] Failed to forcefully delete table directory ${tableDir}:`, err);
@@ -9424,7 +9424,7 @@ var knowledge_store_service_exports = {};
9424
9424
  __export(knowledge_store_service_exports, {
9425
9425
  KnowledgeStoreService: () => KnowledgeStoreService
9426
9426
  });
9427
- import * as path17 from "node:path";
9427
+ import * as path18 from "node:path";
9428
9428
  var KnowledgeStoreService;
9429
9429
  var init_knowledge_store_service = __esm({
9430
9430
  "src/infrastructure/knowledge-store-service.ts"() {
@@ -9527,7 +9527,7 @@ var init_knowledge_store_service = __esm({
9527
9527
  };
9528
9528
  const container = tryGetServiceContainerFromCtx(ctx);
9529
9529
  const pathConfig = await getService(ServiceNames.STATE_PATH_CONFIGURATION, void 0, container);
9530
- const lockPath = path17.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
9530
+ const lockPath = path18.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
9531
9531
  if (!this._initLock) {
9532
9532
  const processLifecycle = await getService(ServiceNames.PROCESS_LIFECYCLE, void 0, container);
9533
9533
  this._initLock = new FileLockService({
@@ -9538,12 +9538,12 @@ var init_knowledge_store_service = __esm({
9538
9538
  // liveOwnerStaleThreshold (120s default) that actually governs when a
9539
9539
  // provably-alive holder's lock may be physically reclaimed. This lock
9540
9540
  // protects a post-run FTS rebuild plus optimize with no fixed duration; if
9541
- // that holder has a stretch of non-yielding synchronous work (a single
9542
- // large better-sqlite3 call blocks the event loop for its full duration,
9543
- // during which the heartbeat literally cannot fire) longer than 20s, a
9544
- // waiting caller would abandon and throw "no sign of progress from its
9545
- // holder" against a holder that was never actually wedged — up to 100s
9546
- // 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
9547
9547
  // liveOwnerStaleThreshold's own default so a live holder is given the same
9548
9548
  // grace period here that the reclaim logic itself already grants it.
9549
9549
  lockTimeout: 12e4,
@@ -10681,20 +10681,20 @@ __export(skill_installer_exports, {
10681
10681
  uninstallSkill: () => uninstallSkill
10682
10682
  });
10683
10683
  import * as crypto10 from "node:crypto";
10684
- import * as fs16 from "node:fs";
10685
- import * as os6 from "node:os";
10686
- 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";
10687
10687
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10688
10688
  function readSkillVersion(sourceDir) {
10689
10689
  try {
10690
- const md = fs16.readFileSync(path18.join(sourceDir, "SKILL.md"), "utf-8");
10690
+ const md = fs17.readFileSync(path19.join(sourceDir, "SKILL.md"), "utf-8");
10691
10691
  return /"version"\s*:\s*"([^"]+)"/.exec(md)?.[1] ?? null;
10692
10692
  } catch {
10693
10693
  return null;
10694
10694
  }
10695
10695
  }
10696
10696
  function homeDir(opts) {
10697
- return opts?.home ?? os6.homedir();
10697
+ return opts?.home ?? os7.homedir();
10698
10698
  }
10699
10699
  function resolveSkillSourceDir(explicit) {
10700
10700
  const candidates = [];
@@ -10702,7 +10702,7 @@ function resolveSkillSourceDir(explicit) {
10702
10702
  if (process.env["PI_RESEARCH_SKILL_DIR"]) candidates.push(process.env["PI_RESEARCH_SKILL_DIR"]);
10703
10703
  const fromHere = (() => {
10704
10704
  try {
10705
- return path18.dirname(fileURLToPath5(import.meta.url));
10705
+ return path19.dirname(fileURLToPath5(import.meta.url));
10706
10706
  } catch {
10707
10707
  return void 0;
10708
10708
  }
@@ -10711,29 +10711,29 @@ function resolveSkillSourceDir(explicit) {
10711
10711
  if (!start) continue;
10712
10712
  let dir = start;
10713
10713
  for (let i = 0; i < 8; i++) {
10714
- candidates.push(path18.join(dir, SKILL_SOURCE_DIR, SKILL_NAME));
10715
- candidates.push(path18.join(dir, LEGACY_SKILL_SOURCE_DIR, SKILL_NAME));
10716
- 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);
10717
10717
  if (parent === dir) break;
10718
10718
  dir = parent;
10719
10719
  }
10720
10720
  }
10721
10721
  for (const c of candidates) {
10722
10722
  try {
10723
- 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);
10724
10724
  } catch {
10725
10725
  }
10726
10726
  }
10727
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.");
10728
10728
  }
10729
10729
  function getManifestPath(opts) {
10730
- return path18.join(homeDir(opts), ".pi", "research", "installed-skills.json");
10730
+ return path19.join(homeDir(opts), ".pi", "research", "installed-skills.json");
10731
10731
  }
10732
10732
  function readManifest(opts) {
10733
10733
  const p = getManifestPath(opts);
10734
10734
  try {
10735
- if (fs16.existsSync(p)) {
10736
- const parsed = JSON.parse(fs16.readFileSync(p, "utf-8"));
10735
+ if (fs17.existsSync(p)) {
10736
+ const parsed = JSON.parse(fs17.readFileSync(p, "utf-8"));
10737
10737
  if (parsed && Array.isArray(parsed.entries)) {
10738
10738
  return { version: 1, package: PACKAGE_NAME, entries: parsed.entries };
10739
10739
  }
@@ -10744,14 +10744,14 @@ function readManifest(opts) {
10744
10744
  }
10745
10745
  function writeManifest(manifest, opts) {
10746
10746
  const p = getManifestPath(opts);
10747
- fs16.mkdirSync(path18.dirname(p), { recursive: true });
10747
+ fs17.mkdirSync(path19.dirname(p), { recursive: true });
10748
10748
  const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${manifestTmpSeq++}`;
10749
- fs16.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
10749
+ fs17.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
10750
10750
  try {
10751
- fs16.renameSync(tmp, p);
10751
+ fs17.renameSync(tmp, p);
10752
10752
  } catch (err) {
10753
10753
  try {
10754
- fs16.unlinkSync(tmp);
10754
+ fs17.unlinkSync(tmp);
10755
10755
  } catch {
10756
10756
  }
10757
10757
  throw err;
@@ -10764,10 +10764,10 @@ function upsertEntry(manifest, entry) {
10764
10764
  }
10765
10765
  function isOwnedSymlink(targetPath, expectedSource) {
10766
10766
  try {
10767
- const st = fs16.lstatSync(targetPath);
10767
+ const st = fs17.lstatSync(targetPath);
10768
10768
  if (!st.isSymbolicLink()) return false;
10769
- const dest = path18.resolve(path18.dirname(targetPath), fs16.readlinkSync(targetPath));
10770
- 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;
10771
10771
  return OWNED_SYMLINK_DEST.test(dest);
10772
10772
  } catch {
10773
10773
  return false;
@@ -10775,7 +10775,7 @@ function isOwnedSymlink(targetPath, expectedSource) {
10775
10775
  }
10776
10776
  function isOwnedCopy(targetPath) {
10777
10777
  try {
10778
- const md = fs16.readFileSync(path18.join(targetPath, "SKILL.md"), "utf-8");
10778
+ const md = fs17.readFileSync(path19.join(targetPath, "SKILL.md"), "utf-8");
10779
10779
  return md.includes(PACKAGE_NAME);
10780
10780
  } catch {
10781
10781
  return false;
@@ -10784,16 +10784,16 @@ function isOwnedCopy(targetPath) {
10784
10784
  function detectHarnesses(opts) {
10785
10785
  const home = homeDir(opts);
10786
10786
  return HARNESSES.map((h) => {
10787
- const absSkillsDir = path18.join(home, h.skillsDir);
10788
- const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
10787
+ const absSkillsDir = path19.join(home, h.skillsDir);
10788
+ const absSkillPath = path19.join(absSkillsDir, SKILL_NAME);
10789
10789
  let present = false;
10790
10790
  try {
10791
- present = fs16.existsSync(path18.join(home, h.baseDir));
10791
+ present = fs17.existsSync(path19.join(home, h.baseDir));
10792
10792
  } catch {
10793
10793
  }
10794
10794
  let installed = "none";
10795
10795
  try {
10796
- if (fs16.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10796
+ if (fs17.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10797
10797
  if (isOwnedSymlink(absSkillPath)) installed = "owned-symlink";
10798
10798
  else if (isOwnedCopy(absSkillPath)) installed = "owned-copy";
10799
10799
  else installed = "foreign";
@@ -10806,14 +10806,14 @@ function detectHarnesses(opts) {
10806
10806
  }
10807
10807
  function isUsableSkillDir(p) {
10808
10808
  try {
10809
- 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"));
10810
10810
  } catch {
10811
10811
  return false;
10812
10812
  }
10813
10813
  }
10814
10814
  function isSymlinkPresent(p) {
10815
10815
  try {
10816
- return fs16.lstatSync(p).isSymbolicLink();
10816
+ return fs17.lstatSync(p).isSymbolicLink();
10817
10817
  } catch {
10818
10818
  return false;
10819
10819
  }
@@ -10837,22 +10837,22 @@ function installSkill(toolIds, opts = {}) {
10837
10837
  results.push({ tool: id, path: "", type: "symlink", status: "error", message: `unknown target "${id}"` });
10838
10838
  continue;
10839
10839
  }
10840
- const absSkillsDir = path18.join(home, def.skillsDir);
10841
- const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
10840
+ const absSkillsDir = path19.join(home, def.skillsDir);
10841
+ const absSkillPath = path19.join(absSkillsDir, SKILL_NAME);
10842
10842
  const type = opts.copy ? "copy" : "symlink";
10843
10843
  if (isOwnedSymlink(absSkillPath, source) || isOwnedCopy(absSkillPath)) {
10844
10844
  try {
10845
- const lst = fs16.lstatSync(absSkillPath);
10845
+ const lst = fs17.lstatSync(absSkillPath);
10846
10846
  if (lst.isSymbolicLink()) {
10847
- const dest = path18.resolve(path18.dirname(absSkillPath), fs16.readlinkSync(absSkillPath));
10848
- if (dest !== path18.resolve(source)) {
10847
+ const dest = path19.resolve(path19.dirname(absSkillPath), fs17.readlinkSync(absSkillPath));
10848
+ if (dest !== path19.resolve(source)) {
10849
10849
  if (opts.dryRun) {
10850
10850
  results.push({ tool: id, path: absSkillPath, type: "symlink", status: "planned", message: "would re-point stale symlink to current source" });
10851
10851
  continue;
10852
10852
  }
10853
- fs16.unlinkSync(absSkillPath);
10853
+ fs17.unlinkSync(absSkillPath);
10854
10854
  try {
10855
- fs16.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10855
+ fs17.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10856
10856
  upsertEntry(manifest, { tool: id, path: absSkillPath, type: "symlink", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
10857
10857
  results.push({ tool: id, path: absSkillPath, type: "symlink", status: "installed", message: "re-pointed stale symlink to current source" });
10858
10858
  } catch (relinkErr) {
@@ -10878,7 +10878,7 @@ function installSkill(toolIds, opts = {}) {
10878
10878
  }
10879
10879
  continue;
10880
10880
  }
10881
- if (fs16.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10881
+ if (fs17.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
10882
10882
  results.push({ tool: id, path: absSkillPath, type, status: "skipped-foreign", message: 'a non-pi-research "research" skill already exists here; left untouched' });
10883
10883
  continue;
10884
10884
  }
@@ -10887,10 +10887,10 @@ function installSkill(toolIds, opts = {}) {
10887
10887
  continue;
10888
10888
  }
10889
10889
  try {
10890
- fs16.mkdirSync(absSkillsDir, { recursive: true });
10890
+ fs17.mkdirSync(absSkillsDir, { recursive: true });
10891
10891
  if (type === "symlink") {
10892
10892
  try {
10893
- fs16.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10893
+ fs17.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
10894
10894
  } catch (err) {
10895
10895
  copyDir(source, absSkillPath);
10896
10896
  upsertEntry(manifest, { tool: id, path: absSkillPath, type: "copy", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
@@ -10927,7 +10927,7 @@ function uninstallSkill(toolIds, opts = {}) {
10927
10927
  continue;
10928
10928
  }
10929
10929
  const owned = isOwnedSymlink(skillPath) || isOwnedCopy(skillPath);
10930
- const present = fs16.existsSync(skillPath) || isSymlinkPresent(skillPath);
10930
+ const present = fs17.existsSync(skillPath) || isSymlinkPresent(skillPath);
10931
10931
  if (!present) {
10932
10932
  results.push({ tool, path: skillPath, status: "not-present" });
10933
10933
  continue;
@@ -10945,8 +10945,8 @@ function uninstallSkill(toolIds, opts = {}) {
10945
10945
  continue;
10946
10946
  }
10947
10947
  try {
10948
- if (isSymlinkPresent(skillPath)) fs16.unlinkSync(skillPath);
10949
- 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 });
10950
10950
  results.push({ tool, path: skillPath, status: "removed" });
10951
10951
  } catch (err) {
10952
10952
  results.push({ tool, path: skillPath, status: "error", message: err?.message ?? String(err) });
@@ -10959,7 +10959,7 @@ function uninstallSkill(toolIds, opts = {}) {
10959
10959
  writeManifest({ version: 1, package: PACKAGE_NAME, entries: keep }, opts);
10960
10960
  } else {
10961
10961
  try {
10962
- fs16.rmSync(getManifestPath(opts));
10962
+ fs17.rmSync(getManifestPath(opts));
10963
10963
  } catch {
10964
10964
  }
10965
10965
  }
@@ -10981,56 +10981,56 @@ function reconcileSkillInstalls(opts = {}) {
10981
10981
  for (const e of manifest.entries) {
10982
10982
  if (e.type !== "symlink") {
10983
10983
  const stale = currentVersion !== null && e.version !== currentVersion;
10984
- if (stale && source !== null && fs16.existsSync(e.path) && isOwnedCopy(e.path)) {
10985
- const dir2 = path18.dirname(e.path);
10986
- 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);
10987
10987
  try {
10988
- for (const name of fs16.readdirSync(dir2)) {
10988
+ for (const name of fs17.readdirSync(dir2)) {
10989
10989
  if (name.startsWith(`.${base2}.new-`) || name.startsWith(`.${base2}.old-`)) {
10990
- fs16.rmSync(path18.join(dir2, name), { recursive: true, force: true });
10990
+ fs17.rmSync(path19.join(dir2, name), { recursive: true, force: true });
10991
10991
  }
10992
10992
  }
10993
10993
  } catch {
10994
10994
  }
10995
10995
  const suffix = crypto10.randomBytes(4).toString("hex");
10996
- const fresh = path18.join(dir2, `.${base2}.new-${suffix}`);
10997
- 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}`);
10998
10998
  try {
10999
10999
  copyDir(source, fresh);
11000
11000
  } catch {
11001
11001
  try {
11002
- fs16.rmSync(fresh, { recursive: true, force: true });
11002
+ fs17.rmSync(fresh, { recursive: true, force: true });
11003
11003
  } catch {
11004
11004
  }
11005
11005
  keep.push(e);
11006
11006
  continue;
11007
11007
  }
11008
11008
  try {
11009
- fs16.renameSync(e.path, retired);
11009
+ fs17.renameSync(e.path, retired);
11010
11010
  } catch {
11011
11011
  try {
11012
- fs16.rmSync(fresh, { recursive: true, force: true });
11012
+ fs17.rmSync(fresh, { recursive: true, force: true });
11013
11013
  } catch {
11014
11014
  }
11015
11015
  keep.push(e);
11016
11016
  continue;
11017
11017
  }
11018
11018
  try {
11019
- fs16.renameSync(fresh, e.path);
11019
+ fs17.renameSync(fresh, e.path);
11020
11020
  } catch {
11021
11021
  try {
11022
- fs16.renameSync(retired, e.path);
11022
+ fs17.renameSync(retired, e.path);
11023
11023
  } catch {
11024
11024
  }
11025
11025
  try {
11026
- fs16.rmSync(fresh, { recursive: true, force: true });
11026
+ fs17.rmSync(fresh, { recursive: true, force: true });
11027
11027
  } catch {
11028
11028
  }
11029
11029
  keep.push(e);
11030
11030
  continue;
11031
11031
  }
11032
11032
  try {
11033
- fs16.rmSync(retired, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
11033
+ fs17.rmSync(retired, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
11034
11034
  } catch {
11035
11035
  }
11036
11036
  keep.push({ ...e, source, version: currentVersion });
@@ -11040,12 +11040,12 @@ function reconcileSkillInstalls(opts = {}) {
11040
11040
  }
11041
11041
  continue;
11042
11042
  }
11043
- const dir = path18.dirname(e.path);
11044
- const base = path18.basename(e.path);
11043
+ const dir = path19.dirname(e.path);
11044
+ const base = path19.basename(e.path);
11045
11045
  try {
11046
- for (const name of fs16.readdirSync(dir)) {
11046
+ for (const name of fs17.readdirSync(dir)) {
11047
11047
  if (name.startsWith(`.${base}.new-`)) {
11048
- fs16.rmSync(path18.join(dir, name), { recursive: true, force: true });
11048
+ fs17.rmSync(path19.join(dir, name), { recursive: true, force: true });
11049
11049
  }
11050
11050
  }
11051
11051
  } catch {
@@ -11058,30 +11058,30 @@ function reconcileSkillInstalls(opts = {}) {
11058
11058
  keep.push(e);
11059
11059
  continue;
11060
11060
  }
11061
- const targetLive = fs16.existsSync(e.path) && isUsableSkillDir(e.path);
11061
+ const targetLive = fs17.existsSync(e.path) && isUsableSkillDir(e.path);
11062
11062
  if (!targetLive) {
11063
11063
  if (source !== null) {
11064
11064
  try {
11065
- fs16.unlinkSync(e.path);
11065
+ fs17.unlinkSync(e.path);
11066
11066
  } catch {
11067
11067
  keep.push(e);
11068
11068
  continue;
11069
11069
  }
11070
11070
  try {
11071
- fs16.symlinkSync(source, e.path, process.platform === "win32" ? "junction" : "dir");
11071
+ fs17.symlinkSync(source, e.path, process.platform === "win32" ? "junction" : "dir");
11072
11072
  keep.push({ ...e, type: "symlink", source });
11073
11073
  result.repointed.push(e.path);
11074
11074
  } catch {
11075
11075
  const suffix = crypto10.randomBytes(4).toString("hex");
11076
- const fresh = path18.join(dir, `.${base}.new-${suffix}`);
11076
+ const fresh = path19.join(dir, `.${base}.new-${suffix}`);
11077
11077
  try {
11078
11078
  copyDir(source, fresh);
11079
- fs16.renameSync(fresh, e.path);
11079
+ fs17.renameSync(fresh, e.path);
11080
11080
  keep.push({ ...e, type: "copy", source });
11081
11081
  result.repointed.push(e.path);
11082
11082
  } catch {
11083
11083
  try {
11084
- fs16.rmSync(fresh, { recursive: true, force: true });
11084
+ fs17.rmSync(fresh, { recursive: true, force: true });
11085
11085
  } catch {
11086
11086
  }
11087
11087
  result.pruned.push(e.path);
@@ -11089,7 +11089,7 @@ function reconcileSkillInstalls(opts = {}) {
11089
11089
  }
11090
11090
  } else if (!targetLive) {
11091
11091
  try {
11092
- fs16.unlinkSync(e.path);
11092
+ fs17.unlinkSync(e.path);
11093
11093
  result.pruned.push(e.path);
11094
11094
  } catch {
11095
11095
  keep.push(e);
@@ -11104,21 +11104,21 @@ function reconcileSkillInstalls(opts = {}) {
11104
11104
  if (keep.length > 0) writeManifest({ version: 1, package: PACKAGE_NAME, entries: keep }, opts);
11105
11105
  else {
11106
11106
  try {
11107
- fs16.rmSync(getManifestPath(opts));
11107
+ fs17.rmSync(getManifestPath(opts));
11108
11108
  } catch {
11109
11109
  }
11110
11110
  }
11111
11111
  return result;
11112
11112
  }
11113
11113
  function copyDir(src, dest) {
11114
- const st = fs16.statSync(src);
11114
+ const st = fs17.statSync(src);
11115
11115
  if (st.isDirectory()) {
11116
- fs16.mkdirSync(dest, { recursive: true });
11117
- for (const name of fs16.readdirSync(src)) {
11118
- 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));
11119
11119
  }
11120
11120
  } else {
11121
- fs16.copyFileSync(src, dest);
11121
+ fs17.copyFileSync(src, dest);
11122
11122
  }
11123
11123
  }
11124
11124
  var HARNESSES, SKILL_NAME, PACKAGE_NAME, SKILL_SOURCE_DIR, LEGACY_SKILL_SOURCE_DIR, OWNED_SYMLINK_DEST, SKILL_AGENT_TARGETS, manifestTmpSeq, isTarget;
@@ -11126,15 +11126,15 @@ var init_skill_installer = __esm({
11126
11126
  "src/skill-install/skill-installer.ts"() {
11127
11127
  "use strict";
11128
11128
  HARNESSES = [
11129
- { id: "claude", label: "Claude", baseDir: ".claude", skillsDir: path18.join(".claude", "skills"), confidence: "confirmed" },
11130
- { 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)." },
11131
11131
  // pi's own skill root is under its agent dir (~/.pi/agent/skills), NOT ~/.pi/skills.
11132
11132
  // Listed for DETECTION only — pi is never an install target (see SKILL_AGENT_TARGETS):
11133
11133
  // the host already exposes research as a native tool, so a skill there would just
11134
11134
  // duplicate it through a slower subprocess.
11135
- { id: "pi", label: "pi", baseDir: ".pi", skillsDir: path18.join(".pi", "agent", "skills"), confidence: "confirmed" },
11136
- { 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." },
11137
- { 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." }
11138
11138
  ];
11139
11139
  SKILL_NAME = "pi-research";
11140
11140
  PACKAGE_NAME = "@lincoln504/pi-research";
@@ -11150,9 +11150,9 @@ var init_skill_installer = __esm({
11150
11150
  });
11151
11151
 
11152
11152
  // src/cli.ts
11153
- import * as fs17 from "node:fs";
11154
- import * as os7 from "node:os";
11155
- 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";
11156
11156
  import { spawnSync } from "node:child_process";
11157
11157
  import { fileURLToPath as fileURLToPath6 } from "node:url";
11158
11158
 
@@ -12911,47 +12911,315 @@ init_service_interfaces();
12911
12911
  init_file_lock_service();
12912
12912
  import * as crypto6 from "node:crypto";
12913
12913
  import * as net2 from "node:net";
12914
- import * as path10 from "node:path";
12914
+ import * as path11 from "node:path";
12915
12915
 
12916
12916
  // src/infrastructure/browser/config.ts
12917
- init_config();
12918
- init_logger();
12919
12917
  import * as crypto4 from "node:crypto";
12920
- import { join as join8 } from "node:path";
12921
- import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
12922
- 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";
12923
12926
  import { createRequire as createRequire4 } from "node:module";
12924
- function getWindowsCamoufoxDir() {
12925
- 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
+ }
12935
+ }
12936
+ function isFile(p) {
12937
+ try {
12938
+ return fs8.statSync(p).isFile();
12939
+ } catch {
12940
+ return false;
12941
+ }
12926
12942
  }
12927
- function getCustomCamoufoxDir() {
12928
- return process.env["CAMOUFOX_INSTALL_DIR"] || process.env["PLAYWRIGHT_BROWSERS_PATH"] || void 0;
12943
+ function customDir(env = process.env) {
12944
+ return env["CAMOUFOX_INSTALL_DIR"] || env["PLAYWRIGHT_BROWSERS_PATH"] || null;
12929
12945
  }
12930
- function getBrowserCacheDir() {
12931
- const custom = getCustomCamoufoxDir();
12932
- if (custom) {
12933
- return custom;
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");
12934
12950
  }
12935
- const osPlatform = platform();
12936
- if (osPlatform === "win32") {
12937
- return getWindowsCamoufoxDir();
12938
- } else if (osPlatform === "darwin") {
12939
- 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;
12940
13060
  } else {
12941
- const cacheHome = process.env["XDG_CACHE_HOME"] || join8(homedir3(), ".cache");
12942
- 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` };
12943
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) };
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;
12944
13212
  }
12945
13213
  function getBrowserProfileDir(config) {
12946
13214
  const configured = (config || getConfig()).TMP_DIR;
12947
13215
  let dir;
12948
13216
  if (configured && configured.trim().length > 0) {
12949
- dir = join8(configured, "pi-research", "profiles");
13217
+ dir = join9(configured, "pi-research", "profiles");
12950
13218
  } else {
12951
- const cacheHome = process.env["XDG_CACHE_HOME"] || join8(homedir3(), ".cache");
12952
- dir = join8(cacheHome, "pi-research", "profiles");
13219
+ const cacheHome = process.env["XDG_CACHE_HOME"] || join9(homedir4(), ".cache");
13220
+ dir = join9(cacheHome, "pi-research", "profiles");
12953
13221
  }
12954
- if (!existsSync6(dir)) {
13222
+ if (!existsSync7(dir)) {
12955
13223
  try {
12956
13224
  mkdirSync3(dir, { recursive: true });
12957
13225
  } catch (_e) {
@@ -12961,14 +13229,10 @@ function getBrowserProfileDir(config) {
12961
13229
  }
12962
13230
  function getBrowserEnv(config) {
12963
13231
  const env = { ...process.env };
12964
- const customPath = getCustomCamoufoxDir();
12965
- if (customPath) {
12966
- env["PLAYWRIGHT_BROWSERS_PATH"] = customPath;
12967
- env["CAMOUFOX_INSTALL_DIR"] = customPath;
12968
- } else {
12969
- delete env["PLAYWRIGHT_BROWSERS_PATH"];
12970
- delete env["CAMOUFOX_INSTALL_DIR"];
12971
- }
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"];
12972
13236
  if (!env["PI_RESEARCH_LOG_FILE"]) {
12973
13237
  const logFilePath = getLogger().getLogFilePath();
12974
13238
  if (logFilePath) {
@@ -12988,7 +13252,7 @@ function getBrowserEnv(config) {
12988
13252
  }
12989
13253
  function ensureBrowserCacheDir() {
12990
13254
  const cacheDir = getBrowserCacheDir();
12991
- if (!existsSync6(cacheDir)) {
13255
+ if (!existsSync7(cacheDir)) {
12992
13256
  try {
12993
13257
  mkdirSync3(cacheDir, { recursive: true });
12994
13258
  } catch (_e) {
@@ -12997,19 +13261,7 @@ function ensureBrowserCacheDir() {
12997
13261
  return cacheDir;
12998
13262
  }
12999
13263
  function getCamoufoxBinaryPath() {
13000
- const customPath = getCustomCamoufoxDir();
13001
- if (customPath) {
13002
- return customPath;
13003
- }
13004
- const osPlatform = platform();
13005
- if (osPlatform === "win32") {
13006
- return getWindowsCamoufoxDir();
13007
- } else if (osPlatform === "darwin") {
13008
- return join8(homedir3(), "Library", "Caches", "camoufox");
13009
- } else {
13010
- const cacheHome = process.env["XDG_CACHE_HOME"] || join8(homedir3(), ".cache");
13011
- return join8(cacheHome, "camoufox");
13012
- }
13264
+ return getBrowserCacheDir();
13013
13265
  }
13014
13266
  function generateSchedulerVersion(config) {
13015
13267
  const c = config || getConfig();
@@ -13039,16 +13291,15 @@ function probeBrowserNativeDeps(load = defaultNativeDepLoad) {
13039
13291
  }
13040
13292
  }
13041
13293
  function defaultNativeDepLoad() {
13042
- const require2 = createRequire4(import.meta.url);
13043
- const Database = require2("better-sqlite3");
13044
- const db = new Database(":memory:");
13045
- db.close();
13294
+ const require2 = createRequire5(import.meta.url);
13295
+ require2("impit");
13046
13296
  }
13047
13297
  function isBrowserAvailable() {
13048
13298
  if (isFullMockMode()) return false;
13049
13299
  try {
13050
- import.meta.resolve("camoufox-js");
13051
- return existsSync6(getCamoufoxBinaryPath());
13300
+ import.meta.resolve("@camoufox/camoufox");
13301
+ const root = findPackageRoot(import.meta.url);
13302
+ return isUsable(getCamoufoxBinaryPath(), root ? loadPin(root) : null);
13052
13303
  } catch {
13053
13304
  return false;
13054
13305
  }
@@ -13220,9 +13471,9 @@ var BrowserClient = class {
13220
13471
  get targetPort() {
13221
13472
  return this.port;
13222
13473
  }
13223
- async request(path20, data, signal, config) {
13474
+ async request(path21, data, signal, config) {
13224
13475
  const start = Date.now();
13225
- 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";
13226
13477
  return new Promise((resolve9, reject) => {
13227
13478
  const agent = getClientAgent();
13228
13479
  const timeoutMs = resolveClientRequestTimeoutMs(operation, config);
@@ -13247,7 +13498,7 @@ var BrowserClient = class {
13247
13498
  resolved = true;
13248
13499
  abortCleanup?.();
13249
13500
  controller.abort();
13250
- 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)`);
13251
13502
  errorTracker.trackError(error, {
13252
13503
  component: "browser-manager",
13253
13504
  operation,
@@ -13260,7 +13511,7 @@ var BrowserClient = class {
13260
13511
  const req = http4.request({
13261
13512
  hostname: "127.0.0.1",
13262
13513
  port: this.port,
13263
- path: path20,
13514
+ path: path21,
13264
13515
  method: "POST",
13265
13516
  agent,
13266
13517
  signal: controller.signal,
@@ -13284,7 +13535,7 @@ var BrowserClient = class {
13284
13535
  const error = new Error(
13285
13536
  `Browser pool leader is draining (${parsed.reason || "leadership-lost"}) \u2014 re-elect and retry`
13286
13537
  );
13287
- logger.debug(`[BrowserClient] Leader draining on ${path20}; re-election required.`);
13538
+ logger.debug(`[BrowserClient] Leader draining on ${path21}; re-election required.`);
13288
13539
  errorTracker.trackError(error, {
13289
13540
  component: "browser-manager",
13290
13541
  operation,
@@ -13300,7 +13551,7 @@ var BrowserClient = class {
13300
13551
  });
13301
13552
  reject(error);
13302
13553
  } else {
13303
- logger.debug(`[BrowserClient] Request ${path20} completed in ${duration}ms`);
13554
+ logger.debug(`[BrowserClient] Request ${path21} completed in ${duration}ms`);
13304
13555
  resolve9(parsed);
13305
13556
  }
13306
13557
  } catch (_e) {
@@ -13320,7 +13571,7 @@ var BrowserClient = class {
13320
13571
  resolved = true;
13321
13572
  clearTimeout(timer);
13322
13573
  abortCleanup?.();
13323
- 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}`);
13324
13575
  errorTracker.trackError(error, {
13325
13576
  component: "browser-manager",
13326
13577
  operation,
@@ -13339,23 +13590,23 @@ var BrowserClient = class {
13339
13590
  let errorMsg;
13340
13591
  let errorType;
13341
13592
  if (nodeErr.code === "ECONNRESET" || nodeErr.code === "EPIPE") {
13342
- 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}`;
13343
13594
  errorType = "connection_reset";
13344
13595
  } else if (nodeErr.code === "ECONNREFUSED") {
13345
- 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}`;
13346
13597
  errorType = "connection_refused";
13347
13598
  } else if (nodeErr.code === "ETIMEDOUT") {
13348
- errorMsg = `Browser pool ${path20} timed out (slow browser response) - ${err.message}`;
13599
+ errorMsg = `Browser pool ${path21} timed out (slow browser response) - ${err.message}`;
13349
13600
  errorType = "timeout";
13350
13601
  } else {
13351
- errorMsg = `Browser pool ${path20} error: ${err.message}`;
13602
+ errorMsg = `Browser pool ${path21} error: ${err.message}`;
13352
13603
  errorType = "unknown";
13353
13604
  }
13354
13605
  const error = new Error(errorMsg);
13355
13606
  if (isTransientSocketError(error)) {
13356
- 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}`);
13357
13608
  } else {
13358
- 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);
13359
13610
  errorTracker.trackError(error, {
13360
13611
  component: "browser-manager",
13361
13612
  operation,
@@ -13607,17 +13858,17 @@ var BrowserServer = class {
13607
13858
  init_logger();
13608
13859
  import { exec } from "node:child_process";
13609
13860
  import { promisify } from "node:util";
13610
- import * as os4 from "node:os";
13861
+ import * as os5 from "node:os";
13611
13862
  var execAsync = promisify(exec);
13612
13863
  async function cleanupOrphanedCamoufoxProcesses() {
13613
- const platform4 = os4.platform();
13864
+ const platform5 = os5.platform();
13614
13865
  try {
13615
- if (platform4 === "darwin" || platform4 === "linux") {
13866
+ if (platform5 === "darwin" || platform5 === "linux") {
13616
13867
  await cleanupOrphanedProcessesUnix();
13617
- } else if (platform4 === "win32") {
13868
+ } else if (platform5 === "win32") {
13618
13869
  await cleanupOrphanedProcessesWindows();
13619
13870
  } else {
13620
- logger.warn(`[BrowserCleanup] Platform ${platform4} not supported for orphan cleanup`);
13871
+ logger.warn(`[BrowserCleanup] Platform ${platform5} not supported for orphan cleanup`);
13621
13872
  }
13622
13873
  } catch (error) {
13623
13874
  const msg2 = error instanceof Error ? error.message : String(error);
@@ -13744,11 +13995,11 @@ async function cleanupOrphanedProcessesWindows() {
13744
13995
  }
13745
13996
  }
13746
13997
  async function getBrowserPidsForWorkers(workerPids) {
13747
- const platform4 = os4.platform();
13998
+ const platform5 = os5.platform();
13748
13999
  const pids = [];
13749
14000
  if (!workerPids || workerPids.length === 0) return pids;
13750
14001
  try {
13751
- if (platform4 === "darwin" || platform4 === "linux") {
14002
+ if (platform5 === "darwin" || platform5 === "linux") {
13752
14003
  const { stdout } = await execAsync('ps -eo pid,ppid,comm | grep -E "(firefox|camoufox)" | grep -v grep').catch(() => ({ stdout: "" }));
13753
14004
  const lines = stdout.trim().split("\n");
13754
14005
  for (const line of lines) {
@@ -13759,7 +14010,7 @@ async function getBrowserPidsForWorkers(workerPids) {
13759
14010
  if (workerPids.includes(ppid)) pids.push(pid);
13760
14011
  }
13761
14012
  }
13762
- } else if (platform4 === "win32") {
14013
+ } else if (platform5 === "win32") {
13763
14014
  const procs = await queryWindowsProcesses();
13764
14015
  for (const proc of procs) {
13765
14016
  if (!PI_BROWSER_MARKER.test(proc.commandLine)) continue;
@@ -13774,10 +14025,10 @@ async function getBrowserPidsForWorkers(workerPids) {
13774
14025
  }
13775
14026
  async function killBrowserProcesses(pids) {
13776
14027
  if (!pids || pids.length === 0) return;
13777
- const platform4 = os4.platform();
14028
+ const platform5 = os5.platform();
13778
14029
  await Promise.all(pids.map(async (pid) => {
13779
14030
  try {
13780
- if (platform4 === "win32") {
14031
+ if (platform5 === "win32") {
13781
14032
  await execAsync(`taskkill /PID ${pid} /F /T`);
13782
14033
  logger.debug(`[BrowserCleanup] Terminated worker browser process: PID ${pid}`);
13783
14034
  } else {
@@ -14508,7 +14759,7 @@ async function getBrowserInitLock(container) {
14508
14759
  if (lock) return lock;
14509
14760
  const pathConfig = await getService(ServiceNames.STATE_PATH_CONFIGURATION, void 0, container);
14510
14761
  const processLifecycle = await getService(ServiceNames.PROCESS_LIFECYCLE, void 0, container);
14511
- const lockFilePath = path10.join(pathConfig.getLockDirPath(), "browser-init.lock");
14762
+ const lockFilePath = path11.join(pathConfig.getLockDirPath(), "browser-init.lock");
14512
14763
  lock = new FileLockService({
14513
14764
  lockFilePath,
14514
14765
  lockTimeout: 6e4,
@@ -14809,8 +15060,8 @@ init_config();
14809
15060
  init_logger();
14810
15061
  init_atomic_replace();
14811
15062
  init_metrics();
14812
- import * as fs8 from "node:fs/promises";
14813
- import * as path11 from "node:path";
15063
+ import * as fs9 from "node:fs/promises";
15064
+ import * as path12 from "node:path";
14814
15065
  import * as crypto7 from "node:crypto";
14815
15066
  import process2 from "node:process";
14816
15067
 
@@ -15048,12 +15299,12 @@ var StateManager = class {
15048
15299
  } = options;
15049
15300
  let stateDir = providedStateDir;
15050
15301
  if (!stateDir) {
15051
- stateDir = path11.join(getGlobalConfigDir(), "state");
15302
+ stateDir = path12.join(getGlobalConfigDir(), "state");
15052
15303
  }
15053
- this.stateFilePath = path11.join(stateDir, "research-state.json");
15054
- this.lockDirPath = path11.join(stateDir, ".locks");
15055
- this.backupDirPath = path11.join(stateDir, "backups");
15056
- 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");
15057
15308
  this.fileLockService = fileLockService;
15058
15309
  this.processLifecycle = processLifecycle;
15059
15310
  this.gpuResourceService = gpuResourceService;
@@ -15068,13 +15319,13 @@ var StateManager = class {
15068
15319
  */
15069
15320
  async ensureDirectories() {
15070
15321
  const dirs = [
15071
- path11.dirname(this.stateFilePath),
15322
+ path12.dirname(this.stateFilePath),
15072
15323
  this.lockDirPath,
15073
15324
  this.backupDirPath
15074
15325
  ];
15075
15326
  for (const dir of dirs) {
15076
15327
  try {
15077
- await fs8.mkdir(dir, { recursive: true, mode: 448 });
15328
+ await fs9.mkdir(dir, { recursive: true, mode: 448 });
15078
15329
  } catch (err) {
15079
15330
  logger.error(`[StateManager] Failed to create directory ${dir}:`, err);
15080
15331
  throw err;
@@ -15101,17 +15352,17 @@ var StateManager = class {
15101
15352
  if (this.orphanTmpSwept) return;
15102
15353
  this.orphanTmpSwept = true;
15103
15354
  const STALE_TMP_MS = 5 * 60 * 1e3;
15104
- const stateDir = path11.dirname(this.stateFilePath);
15355
+ const stateDir = path12.dirname(this.stateFilePath);
15105
15356
  const cutoff = Date.now() - STALE_TMP_MS;
15106
15357
  try {
15107
- const entries = await fs8.readdir(stateDir);
15358
+ const entries = await fs9.readdir(stateDir);
15108
15359
  for (const entry of entries) {
15109
15360
  if (!entry.startsWith("research-state-") || !entry.endsWith(".tmp")) continue;
15110
- const filePath = path11.join(stateDir, entry);
15361
+ const filePath = path12.join(stateDir, entry);
15111
15362
  try {
15112
- const stats = await fs8.stat(filePath);
15363
+ const stats = await fs9.stat(filePath);
15113
15364
  if (!stats.isFile() || stats.mtimeMs >= cutoff) continue;
15114
- await fs8.unlink(filePath);
15365
+ await fs9.unlink(filePath);
15115
15366
  logger.debug(`[StateManager] Removed orphaned state temp file: ${entry}`);
15116
15367
  } catch {
15117
15368
  }
@@ -15156,7 +15407,7 @@ var StateManager = class {
15156
15407
  return this.getDefaultState();
15157
15408
  }
15158
15409
  try {
15159
- const content = await fs8.readFile(this.stateFilePath, "utf-8");
15410
+ const content = await fs9.readFile(this.stateFilePath, "utf-8");
15160
15411
  const state = JSON.parse(content);
15161
15412
  return this.validator.migrateAndValidate(state);
15162
15413
  } catch (error) {
@@ -15185,7 +15436,7 @@ var StateManager = class {
15185
15436
  );
15186
15437
  try {
15187
15438
  await this.recoverFromCorruptionDirect();
15188
- const recovered = await fs8.readFile(this.stateFilePath, "utf-8");
15439
+ const recovered = await fs9.readFile(this.stateFilePath, "utf-8");
15189
15440
  return this.validator.migrateAndValidate(JSON.parse(recovered));
15190
15441
  } catch (recoveryError) {
15191
15442
  logger.error(
@@ -15244,9 +15495,9 @@ var StateManager = class {
15244
15495
  logger.warn(`[StateManager] Backup before write failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
15245
15496
  });
15246
15497
  const tempFileName = `research-state-${crypto7.randomBytes(16).toString("hex")}.tmp`;
15247
- tempFilePath = path11.join(path11.dirname(this.stateFilePath), tempFileName);
15498
+ tempFilePath = path12.join(path12.dirname(this.stateFilePath), tempFileName);
15248
15499
  const content = JSON.stringify(state, null, 2);
15249
- const fh = await fs8.open(tempFilePath, "w", 384);
15500
+ const fh = await fs9.open(tempFilePath, "w", 384);
15250
15501
  try {
15251
15502
  await fh.writeFile(content, { encoding: "utf-8" });
15252
15503
  await fh.sync();
@@ -15261,7 +15512,7 @@ var StateManager = class {
15261
15512
  metrics.increment("state_write_non_atomic_total", 1);
15262
15513
  const tmp = tempFilePath;
15263
15514
  tempFilePath = null;
15264
- await fs8.unlink(tmp).catch((err) => {
15515
+ await fs9.unlink(tmp).catch((err) => {
15265
15516
  logger.warn("[StateManager] Failed to remove temp file after copy fallback (write succeeded):", err);
15266
15517
  });
15267
15518
  }
@@ -15271,7 +15522,7 @@ var StateManager = class {
15271
15522
  throw new Error(`Failed to write state: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
15272
15523
  } finally {
15273
15524
  if (tempFilePath) {
15274
- await fs8.unlink(tempFilePath).catch((err) => {
15525
+ await fs9.unlink(tempFilePath).catch((err) => {
15275
15526
  logger.warn("[StateManager] Failed to clean up temp file:", err);
15276
15527
  });
15277
15528
  }
@@ -15690,7 +15941,7 @@ var MetricsService = class {
15690
15941
  // src/infrastructure/process-lifecycle-service.ts
15691
15942
  init_service_registry();
15692
15943
  init_service_names();
15693
- import * as fs9 from "node:fs/promises";
15944
+ import * as fs10 from "node:fs/promises";
15694
15945
  import { execFile } from "node:child_process";
15695
15946
  function parseEtime(value) {
15696
15947
  const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(value.trim());
@@ -15737,7 +15988,7 @@ var ProcessLifecycleService = class _ProcessLifecycleService {
15737
15988
  async getProcessStartTime(pid) {
15738
15989
  if (process.platform === "linux") {
15739
15990
  try {
15740
- const stat6 = await fs9.readFile(`/proc/${pid}/stat`, "utf8");
15991
+ const stat6 = await fs10.readFile(`/proc/${pid}/stat`, "utf8");
15741
15992
  const lastParenIndex = stat6.lastIndexOf(")");
15742
15993
  if (lastParenIndex === -1) return null;
15743
15994
  const partsAfterName = stat6.substring(lastParenIndex + 2).trim().split(/\s+/);
@@ -16316,8 +16567,8 @@ var StateValidator = class {
16316
16567
  if (!Value4.Check(SingletonStateSchema, coerced)) {
16317
16568
  const errors = [...Value4.Errors(SingletonStateSchema, coerced)];
16318
16569
  const errorMsg = errors.map((e) => {
16319
- const path20 = String(e.path || "root");
16320
- return `${path20}: ${e.message} (${JSON.stringify(e.value)})`;
16570
+ const path21 = String(e.path || "root");
16571
+ return `${path21}: ${e.message} (${JSON.stringify(e.value)})`;
16321
16572
  }).join(", ");
16322
16573
  throw new Error(`Invalid state: ${errorMsg}`);
16323
16574
  }
@@ -16364,45 +16615,45 @@ var StateValidator = class {
16364
16615
  init_logger();
16365
16616
  init_metrics();
16366
16617
  init_config();
16367
- import { dirname as dirname9, join as join13 } from "node:path";
16368
- 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";
16369
16620
  import { fileURLToPath as fileURLToPath3 } from "node:url";
16370
16621
  import { FixedClusterPool, WorkerChoiceStrategies } from "poolifier";
16371
16622
 
16372
16623
  // src/infrastructure/browser/ensure-browser.ts
16373
16624
  init_logger();
16374
16625
  import { spawn as spawn2 } from "node:child_process";
16375
- import { createRequire as createRequire5 } from "node:module";
16626
+ import { createRequire as createRequire6 } from "node:module";
16376
16627
  import { createHash as createHash2 } from "node:crypto";
16377
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3, fstatSync, openSync as openSync4, closeSync as closeSync4, rmSync } from "node:fs";
16378
- import { join as join11, dirname as dirname8 } from "node:path";
16379
- 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";
16380
16631
  var FETCH_TIMEOUT_MS = 45 * 60 * 1e3;
16381
16632
  var STALE_LOCK_MS = 55 * 60 * 1e3;
16382
16633
  var WAIT_FOR_PEER_MS = 50 * 60 * 1e3;
16383
16634
  var POLL_INTERVAL_MS = 2e3;
16384
16635
  var inFlight = null;
16636
+ function currentPin() {
16637
+ const root = findPackageRoot(import.meta.url);
16638
+ return root ? loadPin(root) : null;
16639
+ }
16385
16640
  function isBrowserBinaryPresent() {
16386
16641
  try {
16387
- const cacheDir = getCamoufoxBinaryPath();
16388
- if (!existsSync7(cacheDir)) return false;
16389
- return readdirSync3(cacheDir).some((entry) => {
16390
- try {
16391
- return statSync3(join11(cacheDir, entry)).isDirectory();
16392
- } catch {
16393
- return false;
16394
- }
16395
- });
16642
+ return isUsable(getCamoufoxBinaryPath(), currentPin());
16396
16643
  } catch {
16397
16644
  return false;
16398
16645
  }
16399
16646
  }
16400
- function resolveCamoufoxBin() {
16647
+ function resolveCamoufoxCli() {
16401
16648
  try {
16402
- const require2 = createRequire5(import.meta.url);
16403
- const pkgJson = require2.resolve("camoufox-js/package.json");
16404
- const bin = join11(dirname8(pkgJson), "..", ".bin", process.platform === "win32" ? "camoufox-js.cmd" : "camoufox-js");
16405
- 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
+ }
16406
16657
  } catch {
16407
16658
  }
16408
16659
  return null;
@@ -16410,11 +16661,17 @@ function resolveCamoufoxBin() {
16410
16661
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
16411
16662
  function runFetch() {
16412
16663
  return new Promise((resolve9, reject) => {
16413
- const bin = resolveCamoufoxBin();
16414
- const [cmd, args] = bin ? [bin, ["fetch"]] : ["npx", ["camoufox-js", "fetch"]];
16415
- logger.info(`[ensure-browser] Camoufox not found; fetching the browser (this runs once, ~500MB): ${cmd} ${args.join(" ")}`);
16416
- const useShell = process.platform === "win32";
16417
- 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 });
16418
16675
  const timer = setTimeout(() => {
16419
16676
  child.kill("SIGKILL");
16420
16677
  reject(new Error(`camoufox fetch timed out after ${FETCH_TIMEOUT_MS}ms`));
@@ -16425,8 +16682,7 @@ function runFetch() {
16425
16682
  });
16426
16683
  child.on("exit", (code) => {
16427
16684
  clearTimeout(timer);
16428
- if (code === 0) resolve9();
16429
- else reject(new Error(`camoufox fetch exited with code ${code}`));
16685
+ resolve9(code === 0);
16430
16686
  });
16431
16687
  });
16432
16688
  }
@@ -16445,7 +16701,7 @@ async function provision() {
16445
16701
  return;
16446
16702
  }
16447
16703
  const cacheKey = createHash2("sha256").update(getCamoufoxBinaryPath()).digest("hex").slice(0, 16);
16448
- const lockPath = join11(tmpdir2(), `pi-research-camoufox-fetch-${cacheKey}.lock`);
16704
+ const lockPath = join12(tmpdir2(), `pi-research-camoufox-fetch-${cacheKey}.lock`);
16449
16705
  let haveLock = false;
16450
16706
  try {
16451
16707
  closeSync4(openSync4(lockPath, "wx", 384));
@@ -16460,7 +16716,7 @@ async function provision() {
16460
16716
  closeSync4(fd);
16461
16717
  }
16462
16718
  if (Date.now() - mtimeMs > STALE_LOCK_MS) {
16463
- rmSync(lockPath, { force: true });
16719
+ rmSync2(lockPath, { force: true });
16464
16720
  closeSync4(openSync4(lockPath, "wx", 384));
16465
16721
  haveLock = true;
16466
16722
  }
@@ -16473,16 +16729,15 @@ async function provision() {
16473
16729
  }
16474
16730
  try {
16475
16731
  if (isBrowserBinaryPresent()) return;
16476
- await runFetch();
16477
- if (isBrowserBinaryPresent()) {
16478
- logger.info("[ensure-browser] Camoufox browser is ready.");
16479
- } else {
16480
- 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");
16481
16735
  }
16736
+ logger.info("[ensure-browser] Camoufox browser is ready.");
16482
16737
  } finally {
16483
16738
  if (haveLock) {
16484
16739
  try {
16485
- rmSync(lockPath, { force: true });
16740
+ rmSync2(lockPath, { force: true });
16486
16741
  } catch {
16487
16742
  }
16488
16743
  }
@@ -16500,16 +16755,16 @@ function ensureBrowserInstalled() {
16500
16755
 
16501
16756
  // src/infrastructure/browser/cleanup-utils.ts
16502
16757
  init_logger();
16503
- import * as fs10 from "node:fs/promises";
16504
- import * as path12 from "node:path";
16505
- 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";
16506
16761
  var DEFAULT_STALE_MS = 30 * 24 * 60 * 60 * 1e3;
16507
16762
  var LOCK_FILE_NAMES = ["lock", ".parentlock", "parent.lock"];
16508
16763
  var LOCK_FRESHNESS_MS = 2 * 60 * 1e3;
16509
16764
  async function isWindowsParentLockHeld(lockPath) {
16510
16765
  let handle;
16511
16766
  try {
16512
- handle = await fs10.open(lockPath, "r");
16767
+ handle = await fs11.open(lockPath, "r");
16513
16768
  } catch (e) {
16514
16769
  const code = e?.code;
16515
16770
  if (code === "ENOENT") return false;
@@ -16536,16 +16791,16 @@ function isPidAlive(pid) {
16536
16791
  }
16537
16792
  async function isProfileLive(profileDir) {
16538
16793
  for (const name of LOCK_FILE_NAMES) {
16539
- const lockPath = path12.join(profileDir, name);
16794
+ const lockPath = path13.join(profileDir, name);
16540
16795
  let lst;
16541
16796
  try {
16542
- lst = await fs10.lstat(lockPath);
16797
+ lst = await fs11.lstat(lockPath);
16543
16798
  } catch {
16544
16799
  continue;
16545
16800
  }
16546
16801
  if (lst.isSymbolicLink()) {
16547
16802
  try {
16548
- const pid = parsePidFromLockTarget(await fs10.readlink(lockPath));
16803
+ const pid = parsePidFromLockTarget(await fs11.readlink(lockPath));
16549
16804
  if (pid !== null) return isPidAlive(pid);
16550
16805
  } catch {
16551
16806
  }
@@ -16562,42 +16817,42 @@ async function isProfileLive(profileDir) {
16562
16817
  async function canonicalizeForCompare(p) {
16563
16818
  let resolved;
16564
16819
  try {
16565
- resolved = await fs10.realpath(p);
16820
+ resolved = await fs11.realpath(p);
16566
16821
  } catch {
16567
- resolved = path12.resolve(p);
16822
+ resolved = path13.resolve(p);
16568
16823
  }
16569
16824
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
16570
16825
  }
16571
16826
  async function isPiOwnedSweepDir(dir) {
16572
16827
  const [realDir, realTmp] = await Promise.all([
16573
16828
  canonicalizeForCompare(dir),
16574
- canonicalizeForCompare(os5.tmpdir())
16829
+ canonicalizeForCompare(os6.tmpdir())
16575
16830
  ]);
16576
16831
  if (realDir === realTmp) return false;
16577
16832
  const hasPiSegment = (p) => p.split(/[\\/]+/).some((segment) => segment.toLowerCase().startsWith("pi-research"));
16578
16833
  return hasPiSegment(realDir);
16579
16834
  }
16580
16835
  async function cleanupStaleProfiles(baseDir, staleMs = DEFAULT_STALE_MS) {
16581
- const scanDir = baseDir ?? os5.tmpdir();
16836
+ const scanDir = baseDir ?? os6.tmpdir();
16582
16837
  const usePrefix = baseDir === void 0;
16583
16838
  const prefix = "playwright_firefoxdev_profile-";
16584
16839
  let removed = 0;
16585
16840
  let errors = 0;
16586
16841
  const sweepUnrecognized = usePrefix ? false : await isPiOwnedSweepDir(scanDir);
16587
16842
  try {
16588
- const entries = await fs10.readdir(scanDir);
16843
+ const entries = await fs11.readdir(scanDir);
16589
16844
  const now = Date.now();
16590
16845
  for (const entry of entries) {
16591
16846
  if (usePrefix && !entry.startsWith(prefix)) continue;
16592
- const fullPath = path12.join(scanDir, entry);
16847
+ const fullPath = path13.join(scanDir, entry);
16593
16848
  try {
16594
- const stats = await fs10.stat(fullPath);
16849
+ const stats = await fs11.stat(fullPath);
16595
16850
  if (!stats.isDirectory()) continue;
16596
16851
  const isProfileLike = entry.startsWith(prefix) || entry.startsWith("playwright-artifacts-");
16597
16852
  if (isProfileLike) {
16598
- const inner = await fs10.readdir(fullPath);
16853
+ const inner = await fs11.readdir(fullPath);
16599
16854
  if (inner.length === 0) {
16600
- await fs10.rmdir(fullPath);
16855
+ await fs11.rmdir(fullPath);
16601
16856
  removed++;
16602
16857
  continue;
16603
16858
  }
@@ -16605,12 +16860,12 @@ async function cleanupStaleProfiles(baseDir, staleMs = DEFAULT_STALE_MS) {
16605
16860
  logger.debug(`[Cleanup] Skipping profile ${entry} \u2014 owned by a live browser`);
16606
16861
  continue;
16607
16862
  }
16608
- 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 });
16609
16864
  removed++;
16610
16865
  continue;
16611
16866
  }
16612
16867
  if (sweepUnrecognized && now - stats.mtimeMs > staleMs && !await isProfileLive(fullPath)) {
16613
- 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 });
16614
16869
  removed++;
16615
16870
  }
16616
16871
  } catch (_e) {
@@ -16630,7 +16885,7 @@ async function cleanupStaleProfiles(baseDir, staleMs = DEFAULT_STALE_MS) {
16630
16885
  init_log_utils();
16631
16886
  import process3 from "node:process";
16632
16887
  import cluster from "node:cluster";
16633
- import * as fs11 from "node:fs/promises";
16888
+ import * as fs12 from "node:fs/promises";
16634
16889
  var workerId = "";
16635
16890
  var BENIGN_IPC_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_IPC_CHANNEL_CLOSED", "ECONNRESET"]);
16636
16891
  function isBenignClusterIpcError(err) {
@@ -16691,7 +16946,7 @@ function logToDebugFile(level, ...args) {
16691
16946
  return String(arg);
16692
16947
  }).join(" "))
16693
16948
  };
16694
- fs11.appendFile(logFile, `${JSON.stringify(entry)}
16949
+ fs12.appendFile(logFile, `${JSON.stringify(entry)}
16695
16950
  `).catch(() => {
16696
16951
  });
16697
16952
  } catch {
@@ -16703,7 +16958,7 @@ init_service_registry();
16703
16958
  init_safe_unref();
16704
16959
  init_service_names();
16705
16960
  var __filename2 = fileURLToPath3(import.meta.url);
16706
- var __dirname = dirname9(__filename2);
16961
+ var __dirname = dirname10(__filename2);
16707
16962
  function buildWorkerPoolOptions(config) {
16708
16963
  return {
16709
16964
  workerChoiceStrategy: WorkerChoiceStrategies.LEAST_USED,
@@ -16795,8 +17050,8 @@ var WorkerPoolManager = class {
16795
17050
  void cleanupStaleProfiles(getBrowserProfileDir(config), STALE_PROFILE_MS).catch((e) => logger.debug("[WorkerPoolManager] Profile sweep (profile dir) failed:", e));
16796
17051
  void cleanupStaleProfiles(void 0, STALE_PROFILE_MS).catch((e) => logger.debug("[WorkerPoolManager] Profile sweep (legacy tmp) failed:", e));
16797
17052
  void cleanupOrphanedCamoufoxProcesses().catch((e) => logger.debug("[WorkerPoolManager] Startup orphan sweep failed:", e));
16798
- const workerPath = join13(__dirname, "./thread-worker.mjs");
16799
- if (!existsSync8(workerPath)) {
17053
+ const workerPath = join14(__dirname, "./thread-worker.mjs");
17054
+ if (!existsSync9(workerPath)) {
16800
17055
  throw new Error(
16801
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.`
16802
17057
  );
@@ -17044,7 +17299,7 @@ var WorkerPoolManager = class {
17044
17299
  init_config();
17045
17300
  init_service_registry();
17046
17301
  init_service_names();
17047
- import * as path13 from "node:path";
17302
+ import * as path14 from "node:path";
17048
17303
  var StatePathConfiguration = class {
17049
17304
  name = ServiceNames.STATE_PATH_CONFIGURATION;
17050
17305
  lifecycle = "uninitialized" /* UNINITIALIZED */;
@@ -17056,13 +17311,13 @@ var StatePathConfiguration = class {
17056
17311
  lockFilePath;
17057
17312
  projectSettingsPath;
17058
17313
  constructor(stateDir) {
17059
- const resolvedStateDir = stateDir && path13.isAbsolute(stateDir) ? stateDir : getStateDir();
17314
+ const resolvedStateDir = stateDir && path14.isAbsolute(stateDir) ? stateDir : getStateDir();
17060
17315
  this.stateDir = resolvedStateDir;
17061
- this.stateFilePath = path13.join(resolvedStateDir, "research-state.json");
17062
- this.lockDirPath = path13.join(resolvedStateDir, ".locks");
17063
- this.backupDirPath = path13.join(resolvedStateDir, "backups");
17064
- this.lockFilePath = path13.join(this.lockDirPath, "research-state.lock");
17065
- 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");
17066
17321
  }
17067
17322
  async initialize() {
17068
17323
  if (this._initialized) {
@@ -17181,7 +17436,7 @@ var ResearchRunSemaphore = class {
17181
17436
  if (this._initialized) return;
17182
17437
  this.lifecycle = "initializing" /* INITIALIZING */;
17183
17438
  try {
17184
- 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 }));
17185
17440
  } catch (err) {
17186
17441
  logger.warn(`[ResearchRunSemaphore] Could not create slot dir ${this.slotDir}: ${err instanceof Error ? err.message : String(err)}`);
17187
17442
  }
@@ -17334,9 +17589,9 @@ init_logger();
17334
17589
  init_atomic_replace();
17335
17590
  init_service_registry();
17336
17591
  init_service_names();
17337
- import * as fs12 from "node:fs/promises";
17592
+ import * as fs13 from "node:fs/promises";
17338
17593
  import * as crypto8 from "node:crypto";
17339
- import * as path14 from "node:path";
17594
+ import * as path15 from "node:path";
17340
17595
  var QUARANTINE_KEEP = 5;
17341
17596
  var StateBackupManager = class {
17342
17597
  name = ServiceNames.STATE_BACKUP_MANAGER;
@@ -17358,7 +17613,7 @@ var StateBackupManager = class {
17358
17613
  return;
17359
17614
  }
17360
17615
  this.lifecycle = "initializing" /* INITIALIZING */;
17361
- await fs12.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17616
+ await fs13.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17362
17617
  this._initialized = true;
17363
17618
  this.lifecycle = "initialized" /* INITIALIZED */;
17364
17619
  }
@@ -17372,15 +17627,15 @@ var StateBackupManager = class {
17372
17627
  async createBackup() {
17373
17628
  try {
17374
17629
  try {
17375
- await fs12.access(this.stateFilePath);
17630
+ await fs13.access(this.stateFilePath);
17376
17631
  } catch {
17377
17632
  return;
17378
17633
  }
17379
- await fs12.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17634
+ await fs13.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17380
17635
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
17381
17636
  const backupFileName = `research-state-${timestamp}-${crypto8.randomBytes(4).toString("hex")}.json`;
17382
- const backupFilePath = path14.join(this.backupDirPath, backupFileName);
17383
- await fs12.copyFile(this.stateFilePath, backupFilePath);
17637
+ const backupFilePath = path15.join(this.backupDirPath, backupFileName);
17638
+ await fs13.copyFile(this.stateFilePath, backupFilePath);
17384
17639
  } catch (error) {
17385
17640
  throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
17386
17641
  }
@@ -17390,14 +17645,14 @@ var StateBackupManager = class {
17390
17645
  */
17391
17646
  async cleanupOldBackups() {
17392
17647
  try {
17393
- const entries = await fs12.readdir(this.backupDirPath);
17648
+ const entries = await fs13.readdir(this.backupDirPath);
17394
17649
  if (entries.length <= this.maxBackups) {
17395
17650
  return;
17396
17651
  }
17397
17652
  const backupFiles = [];
17398
17653
  for (const entry of entries) {
17399
- const filePath = path14.join(this.backupDirPath, entry);
17400
- const stats = await fs12.stat(filePath);
17654
+ const filePath = path15.join(this.backupDirPath, entry);
17655
+ const stats = await fs13.stat(filePath);
17401
17656
  if (stats.isFile() && entry.startsWith("research-state-") && entry.endsWith(".json")) {
17402
17657
  backupFiles.push({ name: entry, mtime: stats.mtime });
17403
17658
  }
@@ -17405,8 +17660,8 @@ var StateBackupManager = class {
17405
17660
  backupFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
17406
17661
  const backupsToRemove = backupFiles.slice(this.maxBackups);
17407
17662
  for (const backupFile of backupsToRemove) {
17408
- const filePath = path14.join(this.backupDirPath, backupFile.name);
17409
- await fs12.unlink(filePath);
17663
+ const filePath = path15.join(this.backupDirPath, backupFile.name);
17664
+ await fs13.unlink(filePath);
17410
17665
  }
17411
17666
  } catch (error) {
17412
17667
  logger.error(`Failed to cleanup old backups: ${error instanceof Error ? error.message : String(error)}`);
@@ -17419,10 +17674,10 @@ var StateBackupManager = class {
17419
17674
  try {
17420
17675
  let backups = [];
17421
17676
  try {
17422
- const entries = await fs12.readdir(this.backupDirPath);
17677
+ const entries = await fs13.readdir(this.backupDirPath);
17423
17678
  for (const entry of entries) {
17424
- const filePath = path14.join(this.backupDirPath, entry);
17425
- const stats = await fs12.stat(filePath);
17679
+ const filePath = path15.join(this.backupDirPath, entry);
17680
+ const stats = await fs13.stat(filePath);
17426
17681
  if (stats.isFile() && entry.startsWith("research-state-") && entry.endsWith(".json")) {
17427
17682
  backups.push({ name: entry, mtime: stats.mtime });
17428
17683
  }
@@ -17431,10 +17686,10 @@ var StateBackupManager = class {
17431
17686
  }
17432
17687
  backups.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
17433
17688
  for (const backup of backups) {
17434
- const backupPath = path14.join(this.backupDirPath, backup.name);
17689
+ const backupPath = path15.join(this.backupDirPath, backup.name);
17435
17690
  let restoreContent;
17436
17691
  try {
17437
- const content = await fs12.readFile(backupPath, "utf-8");
17692
+ const content = await fs13.readFile(backupPath, "utf-8");
17438
17693
  const validated = this.validator.migrateAndValidate(JSON.parse(content));
17439
17694
  restoreContent = JSON.stringify(validated, null, 2);
17440
17695
  } catch (err) {
@@ -17462,27 +17717,27 @@ var StateBackupManager = class {
17462
17717
  */
17463
17718
  async quarantineFutureState(fileVersion) {
17464
17719
  try {
17465
- await fs12.access(this.stateFilePath);
17720
+ await fs13.access(this.stateFilePath);
17466
17721
  } catch {
17467
17722
  return;
17468
17723
  }
17469
- await fs12.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17724
+ await fs13.mkdir(this.backupDirPath, { recursive: true, mode: 448 });
17470
17725
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
17471
17726
  const name = `research-state-future-v${fileVersion}-${timestamp}.quarantine`;
17472
- const dest = path14.join(this.backupDirPath, name);
17473
- await fs12.copyFile(this.stateFilePath, dest);
17727
+ const dest = path15.join(this.backupDirPath, name);
17728
+ await fs13.copyFile(this.stateFilePath, dest);
17474
17729
  logger.warn(`[StateManager] Quarantined newer-build state (v${fileVersion}) copy at ${dest}`);
17475
17730
  await this.pruneQuarantineFiles(QUARANTINE_KEEP);
17476
17731
  }
17477
17732
  /** Keep only the newest `keep` *.quarantine files; delete the rest. Best-effort. */
17478
17733
  async pruneQuarantineFiles(keep) {
17479
17734
  try {
17480
- const entries = await fs12.readdir(this.backupDirPath);
17735
+ const entries = await fs13.readdir(this.backupDirPath);
17481
17736
  const quarantines = [];
17482
17737
  for (const entry of entries) {
17483
17738
  if (!entry.endsWith(".quarantine")) continue;
17484
17739
  try {
17485
- const stats = await fs12.stat(path14.join(this.backupDirPath, entry));
17740
+ const stats = await fs13.stat(path15.join(this.backupDirPath, entry));
17486
17741
  if (stats.isFile()) quarantines.push({ name: entry, mtime: stats.mtimeMs });
17487
17742
  } catch {
17488
17743
  }
@@ -17491,7 +17746,7 @@ var StateBackupManager = class {
17491
17746
  quarantines.sort((a, b) => b.mtime - a.mtime);
17492
17747
  for (const stale of quarantines.slice(keep)) {
17493
17748
  try {
17494
- await fs12.unlink(path14.join(this.backupDirPath, stale.name));
17749
+ await fs13.unlink(path15.join(this.backupDirPath, stale.name));
17495
17750
  } catch {
17496
17751
  }
17497
17752
  }
@@ -17503,11 +17758,11 @@ var StateBackupManager = class {
17503
17758
  * a Windows copy+delete fallback since rename fails on NTFS when the target exists).
17504
17759
  */
17505
17760
  async atomicWriteStateFile(content) {
17506
- const stateDir = path14.dirname(this.stateFilePath);
17507
- await fs12.mkdir(stateDir, { recursive: true, mode: 448 });
17761
+ const stateDir = path15.dirname(this.stateFilePath);
17762
+ await fs13.mkdir(stateDir, { recursive: true, mode: 448 });
17508
17763
  const tempFile = `research-state-${crypto8.randomBytes(16).toString("hex")}.tmp`;
17509
- const tempPath = path14.join(stateDir, tempFile);
17510
- const fh = await fs12.open(tempPath, "w", 384);
17764
+ const tempPath = path15.join(stateDir, tempFile);
17765
+ const fh = await fs13.open(tempPath, "w", 384);
17511
17766
  try {
17512
17767
  await fh.writeFile(content, { encoding: "utf-8" });
17513
17768
  await fh.sync();
@@ -17516,11 +17771,11 @@ var StateBackupManager = class {
17516
17771
  }
17517
17772
  try {
17518
17773
  if (await replaceFile(tempPath, this.stateFilePath) === "copied") {
17519
- await fs12.unlink(tempPath).catch(() => {
17774
+ await fs13.unlink(tempPath).catch(() => {
17520
17775
  });
17521
17776
  }
17522
17777
  } catch (renameErr) {
17523
- await fs12.unlink(tempPath).catch(() => {
17778
+ await fs13.unlink(tempPath).catch(() => {
17524
17779
  });
17525
17780
  throw renameErr;
17526
17781
  }
@@ -18158,7 +18413,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
18158
18413
  if (isNativeBindingError(sampleWorkerError)) {
18159
18414
  metrics.increment("browser_search_total_failures_total", 1, { cause: "native_binding" });
18160
18415
  throw new Error(
18161
- `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}` : "")
18162
18417
  );
18163
18418
  }
18164
18419
  throw new Error(
@@ -18339,7 +18594,7 @@ async function checkBrowserCapability() {
18339
18594
  }
18340
18595
  return { healthy: true, diagnostic: { status: mockMode ? "mocked" : "available" } };
18341
18596
  } else {
18342
- 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.' };
18343
18598
  }
18344
18599
  }
18345
18600
  function knowledgeStoreDisabledStatus() {
@@ -18931,9 +19186,9 @@ function formatErrorWithCause(err) {
18931
19186
  function isPrivateIp(ip) {
18932
19187
  const parts = ip.split(".").map(Number);
18933
19188
  if (parts.length !== 4 || parts.some((p) => isNaN(p))) return false;
18934
- const [a, b] = parts;
19189
+ const [a, b, c] = parts;
18935
19190
  if (a === void 0 || b === void 0) return false;
18936
- 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;
18937
19192
  }
18938
19193
  function isPrivateIpv6(ip) {
18939
19194
  const normalized = ip.toLowerCase().replace(/^\[|\]$/g, "");
@@ -19107,11 +19362,11 @@ init_service_registry();
19107
19362
  init_logger();
19108
19363
  init_metrics();
19109
19364
  init_error_tracker();
19110
- import { existsSync as existsSync11 } from "node:fs";
19111
- 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";
19112
19367
  import { fileURLToPath as fileURLToPath4 } from "node:url";
19113
19368
  import { Worker } from "node:worker_threads";
19114
- var MODULE_DIR = dirname12(fileURLToPath4(import.meta.url));
19369
+ var MODULE_DIR = dirname13(fileURLToPath4(import.meta.url));
19115
19370
  var PdfExtractError = class extends Error {
19116
19371
  kind;
19117
19372
  constructor(kind, message, options) {
@@ -19179,8 +19434,8 @@ var nextRequestId = 1;
19179
19434
  var pendingRequests = /* @__PURE__ */ new Map();
19180
19435
  var warnedWorkerUnavailable = false;
19181
19436
  function resolveWorkerPath() {
19182
- const candidate = join20(MODULE_DIR, WORKER_FILENAME);
19183
- return existsSync11(candidate) ? candidate : null;
19437
+ const candidate = join21(MODULE_DIR, WORKER_FILENAME);
19438
+ return existsSync12(candidate) ? candidate : null;
19184
19439
  }
19185
19440
  function pdfWorkerEnabled() {
19186
19441
  if (process.env["PI_RESEARCH_PDF_WORKER"] === "off") return false;
@@ -19344,7 +19599,7 @@ var FETCH_RETRY_DELAY_MS = 250;
19344
19599
  var playwrightAvailable = false;
19345
19600
  var markdownConverterPromise = null;
19346
19601
  function initScraperDependencies() {
19347
- playwrightAvailable = checkModule("playwright-core") && checkModule("camoufox-js");
19602
+ playwrightAvailable = checkModule("playwright-core") && checkModule("@camoufox/camoufox");
19348
19603
  }
19349
19604
  initScraperDependencies();
19350
19605
  async function getMarkdownConverter() {
@@ -24080,6 +24335,18 @@ function isUnretriableResearcherError(error) {
24080
24335
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
24081
24336
  return message.includes("requires more credits") || message.includes("insufficient credits") || message.includes("insufficient_quota");
24082
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
+ }
24083
24350
  async function runResearcher(options) {
24084
24351
  const {
24085
24352
  config: researcherConfig,
@@ -24172,6 +24439,7 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
24172
24439
  const researcherExecutionStartMs = Date.now();
24173
24440
  const deliveredSteeringIds = /* @__PURE__ */ new Set();
24174
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;
24175
24443
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
24176
24444
  if (signal?.aborted) throw new Error("Aborted");
24177
24445
  if (stopRequested()) {
@@ -24179,7 +24447,8 @@ ${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
24179
24447
  return;
24180
24448
  }
24181
24449
  if (attempt > 1) {
24182
- 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;
24183
24452
  logger.warn(`[ResearcherExecutor] Researcher ${id} retry ${attempt - 1}/${config.RESEARCHER_MAX_RETRIES} after ${delay2}ms`);
24184
24453
  observer?.onResearcherProgress?.(id, "retry");
24185
24454
  await new Promise((resolve9) => {
@@ -24422,6 +24691,11 @@ ${responseText}`);
24422
24691
  metrics.increment("researcher_unretriable_total", 1, { mode: "deep", complexity: String(complexity) });
24423
24692
  break;
24424
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
+ }
24425
24699
  if (attempt < maxAttempts) {
24426
24700
  logger.warn(`[ResearcherExecutor] Researcher ${id} attempt ${attempt} failed: ${errMsg}; will retry`);
24427
24701
  } else {
@@ -27102,9 +27376,9 @@ init_url_utils();
27102
27376
  // src/utils/research-export.ts
27103
27377
  init_logger();
27104
27378
  init_constants();
27105
- import { promises as fs15 } from "node:fs";
27106
- import { homedir as homedir4, tmpdir as tmpdir4, platform as platform3 } from "node:os";
27107
- 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";
27108
27382
  import { randomBytes as randomBytes6 } from "node:crypto";
27109
27383
  var PREFERRED_SUBDIRS = [
27110
27384
  "research",
@@ -27146,9 +27420,9 @@ var WIN_SYSTEM_PREFIXES_LC = [
27146
27420
  ];
27147
27421
  function isSafeToWriteDirectly(dir) {
27148
27422
  const normalized = normalize(resolve5(dir));
27149
- const home = normalize(resolve5(homedir4()));
27423
+ const home = normalize(resolve5(homedir6()));
27150
27424
  if (normalized === home) return false;
27151
- if (platform3() === "win32") {
27425
+ if (platform4() === "win32") {
27152
27426
  if (/^[a-zA-Z]:\\?$/.test(normalized)) return false;
27153
27427
  const lc = normalized.toLowerCase();
27154
27428
  for (const prefix of WIN_SYSTEM_PREFIXES_LC) {
@@ -27168,16 +27442,16 @@ function isSafeToWriteDirectly(dir) {
27168
27442
  async function findPreferredSubdir(cwd) {
27169
27443
  let entries;
27170
27444
  try {
27171
- entries = await fs15.readdir(cwd);
27445
+ entries = await fs16.readdir(cwd);
27172
27446
  } catch {
27173
27447
  return null;
27174
27448
  }
27175
27449
  for (const target of PREFERRED_SUBDIRS) {
27176
27450
  const match = entries.find((e) => e.toLowerCase() === target);
27177
27451
  if (match === void 0) continue;
27178
- const candidate = join21(cwd, match);
27452
+ const candidate = join22(cwd, match);
27179
27453
  try {
27180
- const stat6 = await fs15.stat(candidate);
27454
+ const stat6 = await fs16.stat(candidate);
27181
27455
  if (stat6.isDirectory()) return candidate;
27182
27456
  } catch {
27183
27457
  }
@@ -27209,7 +27483,7 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
27209
27483
  if (explicitDir && explicitDir.trim()) {
27210
27484
  targetDir = resolve5(explicitDir.trim());
27211
27485
  try {
27212
- await fs15.mkdir(targetDir, { recursive: true });
27486
+ await fs16.mkdir(targetDir, { recursive: true });
27213
27487
  } catch (error) {
27214
27488
  logger.error(`[export] Could not create export dir "${targetDir}":`, error);
27215
27489
  return null;
@@ -27220,9 +27494,9 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
27220
27494
  for (let attempt = 0; attempt < MAX_EXPORT_RETRIES; attempt++) {
27221
27495
  const hash = generateHash();
27222
27496
  const filename = `${baseFilename}-${hash}.md`;
27223
- const filepath = join21(targetDir, filename);
27497
+ const filepath = join22(targetDir, filename);
27224
27498
  try {
27225
- await fs15.writeFile(filepath, result, { flag: "wx", mode: 384 });
27499
+ await fs16.writeFile(filepath, result, { flag: "wx", mode: 384 });
27226
27500
  logger.log(`[export] Research report saved to: ${filepath}`);
27227
27501
  return filepath;
27228
27502
  } catch (error) {
@@ -27256,7 +27530,7 @@ import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
27256
27530
  function readPkgVersion() {
27257
27531
  try {
27258
27532
  const pkgPath = fileURLToPath6(new URL("../package.json", import.meta.url));
27259
- const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf-8"));
27533
+ const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf-8"));
27260
27534
  return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
27261
27535
  } catch {
27262
27536
  return "0.0.0";
@@ -27298,7 +27572,7 @@ var EXIT = {
27298
27572
  CANCELLED: 130
27299
27573
  };
27300
27574
  function exitCodeForSignal(sig) {
27301
- const signum = os7.constants.signals[sig];
27575
+ const signum = os8.constants.signals[sig];
27302
27576
  return typeof signum === "number" ? 128 + signum : EXIT.CANCELLED;
27303
27577
  }
27304
27578
  function toStderr(s) {
@@ -27389,11 +27663,11 @@ function bridgeConfigEnv(explicitConfigPath) {
27389
27663
  for (const filePath of layers) {
27390
27664
  const isExplicit = explicitConfigPath !== void 0 && filePath === explicitConfigPath;
27391
27665
  try {
27392
- if (!fs17.existsSync(filePath)) {
27666
+ if (!fs18.existsSync(filePath)) {
27393
27667
  if (isExplicit) throw new ConfigFileError(`--config file not found: ${filePath}`);
27394
27668
  continue;
27395
27669
  }
27396
- const parsed = parseDotEnv(fs17.readFileSync(filePath, "utf-8"));
27670
+ const parsed = parseDotEnv(fs18.readFileSync(filePath, "utf-8"));
27397
27671
  Object.assign(merged, parsed);
27398
27672
  if (isExplicit) {
27399
27673
  for (const k of Object.keys(parsed)) fromExplicitBase.add(k);
@@ -27424,14 +27698,14 @@ function resolvedConfigPaths() {
27424
27698
  configDir,
27425
27699
  cliIfaceEnv: getInterfaceEnvFilePath("cli"),
27426
27700
  piIfaceEnv: getInterfaceEnvFilePath("pi"),
27427
- piAuth: path19.join(agentDir, "auth.json"),
27428
- piModels: path19.join(agentDir, "models.json"),
27701
+ piAuth: path20.join(agentDir, "auth.json"),
27702
+ piModels: path20.join(agentDir, "models.json"),
27429
27703
  piState: getStateDir()
27430
27704
  };
27431
27705
  }
27432
27706
  function fileExists(p) {
27433
27707
  try {
27434
- return fs17.existsSync(p);
27708
+ return fs18.existsSync(p);
27435
27709
  } catch {
27436
27710
  return false;
27437
27711
  }
@@ -27727,15 +28001,12 @@ async function cmdStatus(json) {
27727
28001
  installed: browserInstalled,
27728
28002
  nativeDepsOk: nativeDeps.ok,
27729
28003
  nativeDepsError: nativeDeps.ok ? null : nativeDeps.error,
27730
- // better-sqlite3 13 ships prebuilds/ for all eight platform/arch pairs and
27731
- // loads them at runtime via node-gyp-build — no install script involved.
27732
- // On modern npm (≥11.19) the injected `node-gyp rebuild` that a
27733
- // binding.gyp-without-install-script triggers is skipped, so a missing
27734
- // binding here means an old/broken install, not an approval problem:
27735
- // point at the upgrade that fixes it.
27736
- 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.",
27737
28008
  // Same remedy the health check prints, so both surfaces name one command.
27738
- fix: browserInstalled ? null : "npx camoufox-js fetch",
28009
+ fix: browserInstalled ? null : "npx camoufox fetch",
27739
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."
27740
28011
  },
27741
28012
  knowledgeStoreMode: cfg.KNOWLEDGE_STORE_MODE,
@@ -27847,7 +28118,7 @@ change the machine-wide defaults: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> an
27847
28118
  }
27848
28119
  function cmdCleanup(c) {
27849
28120
  const script = fileURLToPath6(new URL("../scripts/cleanup.cjs", import.meta.url));
27850
- if (!fs17.existsSync(script)) {
28121
+ if (!fs18.existsSync(script)) {
27851
28122
  toStderr(
27852
28123
  `
27853
28124
  Error: cleanup script not found at ${script}.
@@ -28126,7 +28397,8 @@ COMMANDS
28126
28397
  state tree (<config dir>/research/state) and the cache, including
28127
28398
  downloaded embedding models. Keeps config.env and the knowledge
28128
28399
  store database. --purge-browsers also deletes the shared
28129
- stealth-browser cache (~/.cache/camoufox) other tools use.
28400
+ stealth-browser cache (~/.cache/camoufox, every installed
28401
+ browser build) other tools use.
28130
28402
 
28131
28403
  help, --help, -h Show this help.
28132
28404
  --version, -v Print the pi-research version.
@@ -28520,8 +28792,8 @@ Error: ${err.message}
28520
28792
  var _isMain = (() => {
28521
28793
  try {
28522
28794
  if (!process.argv[1]) return false;
28523
- const modulePath = fs17.realpathSync(path19.resolve(fileURLToPath6(import.meta.url)));
28524
- 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]));
28525
28797
  return modulePath === invokedPath;
28526
28798
  } catch {
28527
28799
  return false;