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