@lincoln504/pi-research 1.0.3 → 1.0.5
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/README.md +1 -1
- package/dist/cli.mjs +256 -51
- package/dist/prompts/researcher.md +1 -1
- package/docs/CONFIGURATION.md +2 -2
- package/docs/KNOWLEDGE-STORE.md +5 -3
- package/package.json +1 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/skills/pi-research/references/configuration.md +2 -2
- package/src/cli.ts +78 -31
- package/src/core/llm/model-registry-factory.ts +17 -6
- package/src/knowledge/store.ts +17 -7
- package/src/orchestration/deep-research-orchestrator.ts +38 -12
- package/src/orchestration/headless-observer.ts +45 -0
- package/src/orchestration/quick-research-orchestrator.ts +5 -4
- package/src/orchestration/research-orchestration-service.ts +3 -2
- package/src/orchestration/research-synthesis-service.ts +11 -3
- package/src/orchestration/researcher-executor.ts +1 -1
- package/src/orchestration/session-state.ts +33 -1
- package/src/prompts/researcher.md +1 -1
- package/src/sdk.ts +20 -1
- package/src/security/cisa-kev.ts +3 -0
- package/src/security/github-advisories.ts +5 -0
- package/src/security/nvd-client.ts +3 -0
- package/src/security/osv-client.ts +3 -0
- package/src/tools/youtube-transcript.ts +6 -2
- package/src/utils/shared-links.ts +26 -0
- package/src/web-research/scraper-utils.ts +10 -3
- package/src/web-research/web-scraper.ts +95 -4
- package/src/youtube/index.ts +7 -3
- package/src/youtube/transcript-client.ts +2 -0
package/dist/cli.mjs
CHANGED
|
@@ -3252,7 +3252,7 @@ function buildModelRegistry(apiKey, provider) {
|
|
|
3252
3252
|
fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
|
|
3253
3253
|
);
|
|
3254
3254
|
}
|
|
3255
|
-
if (fs5.existsSync(authPath)) {
|
|
3255
|
+
if (fs5.existsSync(authPath) || fs5.existsSync(modelsJsonPath)) {
|
|
3256
3256
|
const authStorage = AuthStorage.create(authPath);
|
|
3257
3257
|
return ModelRegistry.create(
|
|
3258
3258
|
authStorage,
|
|
@@ -3266,7 +3266,17 @@ function constructMinimalModel(provider, modelId, _apiKey) {
|
|
|
3266
3266
|
provider,
|
|
3267
3267
|
id: modelId,
|
|
3268
3268
|
name: modelId,
|
|
3269
|
-
|
|
3269
|
+
// Map to a REAL pi-ai api id — "<provider>-conversations" is not one, and a
|
|
3270
|
+
// keyed no-registry setup would pass pre-flight then die on the first call.
|
|
3271
|
+
// Unknown providers default to the OpenAI-compatible protocol, which is what
|
|
3272
|
+
// virtually every third-party endpoint (openrouter, groq, cerebras, …) speaks.
|
|
3273
|
+
api: {
|
|
3274
|
+
openai: "openai-completions",
|
|
3275
|
+
anthropic: "anthropic-messages",
|
|
3276
|
+
google: "google-generative-ai",
|
|
3277
|
+
gemini: "google-generative-ai",
|
|
3278
|
+
mistral: "mistral-conversations"
|
|
3279
|
+
}[provider] ?? "openai-completions",
|
|
3270
3280
|
baseUrl: "",
|
|
3271
3281
|
// Provider-specific base URLs are handled by pi-ai internal registry
|
|
3272
3282
|
reasoning: false,
|
|
@@ -4702,9 +4712,6 @@ var init_store = __esm({
|
|
|
4702
4712
|
if (strategy !== "drop" && strategy !== "backup") {
|
|
4703
4713
|
logger.warn("[store] Falling back to backup strategy after migration failure");
|
|
4704
4714
|
await this.handleModelChange(storedModel || "unknown", this.options.modelName, "backup");
|
|
4705
|
-
} else if (strategy === "backup") {
|
|
4706
|
-
logger.warn("[store] Falling back to drop strategy after backup failure");
|
|
4707
|
-
await this.handleModelChange(storedModel || "unknown", this.options.modelName, "drop");
|
|
4708
4715
|
} else {
|
|
4709
4716
|
throw new Error(errorMsg, { cause: err });
|
|
4710
4717
|
}
|
|
@@ -4753,7 +4760,12 @@ var init_store = __esm({
|
|
|
4753
4760
|
if (!this.table || !this.db) {
|
|
4754
4761
|
throw new Error("Table not connected");
|
|
4755
4762
|
}
|
|
4756
|
-
|
|
4763
|
+
let count = -1;
|
|
4764
|
+
try {
|
|
4765
|
+
count = await this.table.countRows();
|
|
4766
|
+
} catch (err) {
|
|
4767
|
+
logger.warn(`[store] countRows failed before backup (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
4768
|
+
}
|
|
4757
4769
|
this.table = null;
|
|
4758
4770
|
const oldDir = path13.join(this.options.dbDir, `${this.tableName}.lance`);
|
|
4759
4771
|
const backupDir = path13.join(this.options.dbDir, `${backupTableName}.lance`);
|
|
@@ -4768,8 +4780,8 @@ var init_store = __esm({
|
|
|
4768
4780
|
}
|
|
4769
4781
|
this.table = await this.createTable();
|
|
4770
4782
|
await this.saveManifest();
|
|
4771
|
-
logger.info(`[store] Migration complete: ${count} documents backed up, fresh table created with model ${newModel}`);
|
|
4772
|
-
return { strategy: "backup", success: true, documentsProcessed: count };
|
|
4783
|
+
logger.info(`[store] Migration complete: ${count >= 0 ? count : "unknown count of"} documents backed up, fresh table created with model ${newModel}`);
|
|
4784
|
+
return { strategy: "backup", success: true, documentsProcessed: Math.max(0, count) };
|
|
4773
4785
|
}
|
|
4774
4786
|
async migrationDrop(_oldModel, newModel) {
|
|
4775
4787
|
logger.warn(`[store] Dropping table and recreating with model ${newModel} (data will be lost)`);
|
|
@@ -9358,6 +9370,7 @@ init_constants();
|
|
|
9358
9370
|
var sessionLinks = /* @__PURE__ */ new Map();
|
|
9359
9371
|
var sessionScrapedContent = /* @__PURE__ */ new Map();
|
|
9360
9372
|
var researcherScrapes = /* @__PURE__ */ new Map();
|
|
9373
|
+
var transcribedUrls = /* @__PURE__ */ new Map();
|
|
9361
9374
|
var sessionTimestamps = /* @__PURE__ */ new Map();
|
|
9362
9375
|
var SESSION_MAX_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
9363
9376
|
var MAX_CACHED_CONTENT_PER_SESSION = 500;
|
|
@@ -9373,6 +9386,7 @@ function startCleanupTimer() {
|
|
|
9373
9386
|
sessionScrapedContent.delete(researchId);
|
|
9374
9387
|
sessionTimestamps.delete(researchId);
|
|
9375
9388
|
researcherScrapes.delete(researchId);
|
|
9389
|
+
transcribedUrls.delete(researchId);
|
|
9376
9390
|
cleaned++;
|
|
9377
9391
|
}
|
|
9378
9392
|
}
|
|
@@ -9410,6 +9424,17 @@ function registerScrapedLinks(researchId, links) {
|
|
|
9410
9424
|
function getScrapedLinks(researchId) {
|
|
9411
9425
|
return Array.from(sessionLinks.get(researchId) || []);
|
|
9412
9426
|
}
|
|
9427
|
+
function registerTranscribedLinks(researchId, links) {
|
|
9428
|
+
if (!transcribedUrls.has(researchId)) {
|
|
9429
|
+
transcribedUrls.set(researchId, /* @__PURE__ */ new Set());
|
|
9430
|
+
}
|
|
9431
|
+
sessionTimestamps.set(researchId, Date.now());
|
|
9432
|
+
const pool = transcribedUrls.get(researchId);
|
|
9433
|
+
links.forEach((l) => pool.add(normalizeUrl(l)));
|
|
9434
|
+
}
|
|
9435
|
+
function isTranscribedLink(researchId, url) {
|
|
9436
|
+
return transcribedUrls.get(researchId)?.has(normalizeUrl(url)) ?? false;
|
|
9437
|
+
}
|
|
9413
9438
|
function deduplicateUrls(urls, researchId) {
|
|
9414
9439
|
const pool = sessionLinks.get(researchId) || /* @__PURE__ */ new Set();
|
|
9415
9440
|
const kept = [];
|
|
@@ -9429,6 +9454,7 @@ function cleanupSharedLinks(researchId) {
|
|
|
9429
9454
|
sessionScrapedContent.delete(researchId);
|
|
9430
9455
|
sessionTimestamps.delete(researchId);
|
|
9431
9456
|
researcherScrapes.delete(researchId);
|
|
9457
|
+
transcribedUrls.delete(researchId);
|
|
9432
9458
|
logger.debug(`[Shared Links] Cleaned up session: ${researchId}`);
|
|
9433
9459
|
}
|
|
9434
9460
|
function clearAllSharedLinks() {
|
|
@@ -9440,6 +9466,7 @@ function clearAllSharedLinks() {
|
|
|
9440
9466
|
sessionScrapedContent.clear();
|
|
9441
9467
|
sessionTimestamps.clear();
|
|
9442
9468
|
researcherScrapes.clear();
|
|
9469
|
+
transcribedUrls.clear();
|
|
9443
9470
|
logger.debug("[Shared Links] All global state cleared");
|
|
9444
9471
|
}
|
|
9445
9472
|
function registerResearcherScrapes(researchId, researcherId, urls) {
|
|
@@ -15699,6 +15726,48 @@ function decodeHtmlBody(bytes, contentTypeHeader) {
|
|
|
15699
15726
|
return new TextDecoder("utf-8").decode(bytes);
|
|
15700
15727
|
}
|
|
15701
15728
|
}
|
|
15729
|
+
var BodyTooLargeError = class extends Error {
|
|
15730
|
+
constructor(bytesRead, maxBytes) {
|
|
15731
|
+
super(`Response body exceeded ${maxBytes} byte cap`);
|
|
15732
|
+
this.bytesRead = bytesRead;
|
|
15733
|
+
this.maxBytes = maxBytes;
|
|
15734
|
+
this.name = "BodyTooLargeError";
|
|
15735
|
+
}
|
|
15736
|
+
bytesRead;
|
|
15737
|
+
maxBytes;
|
|
15738
|
+
};
|
|
15739
|
+
async function readBodyCapped(response, maxBytes) {
|
|
15740
|
+
const body = response.body;
|
|
15741
|
+
if (!body || typeof body.getReader !== "function") {
|
|
15742
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
15743
|
+
}
|
|
15744
|
+
const reader = body.getReader();
|
|
15745
|
+
const chunks = [];
|
|
15746
|
+
let total = 0;
|
|
15747
|
+
try {
|
|
15748
|
+
for (; ; ) {
|
|
15749
|
+
const { done, value } = await reader.read();
|
|
15750
|
+
if (done) break;
|
|
15751
|
+
if (value) {
|
|
15752
|
+
total += value.byteLength;
|
|
15753
|
+
if (total > maxBytes) throw new BodyTooLargeError(total, maxBytes);
|
|
15754
|
+
chunks.push(value);
|
|
15755
|
+
}
|
|
15756
|
+
}
|
|
15757
|
+
} finally {
|
|
15758
|
+
try {
|
|
15759
|
+
await reader.cancel();
|
|
15760
|
+
} catch {
|
|
15761
|
+
}
|
|
15762
|
+
}
|
|
15763
|
+
const out = new Uint8Array(total);
|
|
15764
|
+
let offset = 0;
|
|
15765
|
+
for (const c of chunks) {
|
|
15766
|
+
out.set(c, offset);
|
|
15767
|
+
offset += c.byteLength;
|
|
15768
|
+
}
|
|
15769
|
+
return out;
|
|
15770
|
+
}
|
|
15702
15771
|
async function scrapeWithFetch(url, signal) {
|
|
15703
15772
|
await validateUrlForSSRF(url);
|
|
15704
15773
|
const dispatcher = await getSsrfSafeDispatcher();
|
|
@@ -15734,6 +15803,10 @@ async function scrapeWithFetch(url, signal) {
|
|
|
15734
15803
|
if (!location) break;
|
|
15735
15804
|
const resolved = new URL(location, currentUrl).href;
|
|
15736
15805
|
await validateUrlForSSRF(resolved);
|
|
15806
|
+
try {
|
|
15807
|
+
await response.body?.cancel();
|
|
15808
|
+
} catch {
|
|
15809
|
+
}
|
|
15737
15810
|
currentUrl = resolved;
|
|
15738
15811
|
continue;
|
|
15739
15812
|
}
|
|
@@ -15741,6 +15814,10 @@ async function scrapeWithFetch(url, signal) {
|
|
|
15741
15814
|
}
|
|
15742
15815
|
if (!response.ok) {
|
|
15743
15816
|
metrics.increment("scrape_errors_total", 1, { error_type: "http_error", status_code: String(response.status) });
|
|
15817
|
+
try {
|
|
15818
|
+
await response.body?.cancel();
|
|
15819
|
+
} catch {
|
|
15820
|
+
}
|
|
15744
15821
|
throw new Error(`HTTP ${response.status}`);
|
|
15745
15822
|
}
|
|
15746
15823
|
const contentType = response.headers.get("content-type") ?? "";
|
|
@@ -15753,24 +15830,62 @@ async function scrapeWithFetch(url, signal) {
|
|
|
15753
15830
|
const sizeMB = Math.round(size / 1024 / 1024);
|
|
15754
15831
|
logger.warn(`[Scrapers] PDF too large (Content-Length: ${sizeMB}MB, max 100MB), skipping`);
|
|
15755
15832
|
metrics.increment("scrape_pdf_errors_total", 1, { error_type: "size_exceeded" });
|
|
15833
|
+
try {
|
|
15834
|
+
await response.body?.cancel();
|
|
15835
|
+
} catch {
|
|
15836
|
+
}
|
|
15756
15837
|
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
15757
15838
|
}
|
|
15758
15839
|
} else if (size > MAX_HTML_SIZE) {
|
|
15759
15840
|
const sizeMB = Math.round(size / 1024 / 1024);
|
|
15760
15841
|
logger.warn(`[Scrapers] HTML response too large (Content-Length: ${sizeMB}MB, max 25MB), skipping`);
|
|
15761
15842
|
metrics.increment("scrape_errors_total", 1, { error_type: "size_exceeded", content_type: contentType.split(";")[0] || "unknown" });
|
|
15843
|
+
try {
|
|
15844
|
+
await response.body?.cancel();
|
|
15845
|
+
} catch {
|
|
15846
|
+
}
|
|
15762
15847
|
throw new Error(`HTML response too large (${sizeMB}MB, max 25MB)`);
|
|
15763
15848
|
}
|
|
15764
15849
|
}
|
|
15765
15850
|
if (contentType.includes("application/pdf") || url.toLowerCase().endsWith(".pdf")) {
|
|
15766
|
-
|
|
15767
|
-
|
|
15851
|
+
let pdfBytes;
|
|
15852
|
+
try {
|
|
15853
|
+
pdfBytes = await readBodyCapped(response, MAX_PDF_SIZE);
|
|
15854
|
+
} catch (e) {
|
|
15855
|
+
if (e instanceof BodyTooLargeError) {
|
|
15856
|
+
const sizeMB = Math.round(e.bytesRead / 1024 / 1024);
|
|
15857
|
+
logger.warn(`[Scrapers] PDF too large (streamed ${sizeMB}MB, max 100MB), skipping`);
|
|
15858
|
+
metrics.increment("scrape_pdf_errors_total", 1, { error_type: "size_exceeded" });
|
|
15859
|
+
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`, { cause: e });
|
|
15860
|
+
}
|
|
15861
|
+
throw e;
|
|
15862
|
+
}
|
|
15863
|
+
const markdown2 = await extractPdfToMarkdown(pdfBytes);
|
|
15768
15864
|
validateContent("", markdown2, url);
|
|
15769
15865
|
metrics.increment("scrape_operations_total", 1, { layer: "fetch", content_type: "pdf", status: "success" });
|
|
15770
15866
|
metrics.observe("scrape_latency_ms", fetchDuration, { layer: "fetch", content_type: "pdf", status: "success" });
|
|
15771
15867
|
return { source: "fetch", layer: "fetch", markdown: markdown2 };
|
|
15772
15868
|
}
|
|
15773
|
-
|
|
15869
|
+
let bodyBytes;
|
|
15870
|
+
try {
|
|
15871
|
+
bodyBytes = await readBodyCapped(response, MAX_HTML_SIZE);
|
|
15872
|
+
} catch (e) {
|
|
15873
|
+
if (e instanceof BodyTooLargeError) {
|
|
15874
|
+
const sizeMB = Math.round(e.bytesRead / 1024 / 1024);
|
|
15875
|
+
logger.warn(`[Scrapers] HTML response too large (streamed ${sizeMB}MB, max 25MB), skipping`);
|
|
15876
|
+
metrics.increment("scrape_errors_total", 1, { error_type: "size_exceeded", content_type: "html" });
|
|
15877
|
+
errorTracker.trackError(new Error(`HTML response too large (${sizeMB}MB, max 25MB)`), {
|
|
15878
|
+
component: "scrapers",
|
|
15879
|
+
operation: "fetch",
|
|
15880
|
+
url,
|
|
15881
|
+
domain: extractDomain(url),
|
|
15882
|
+
layer: "fetch",
|
|
15883
|
+
contentType: "html"
|
|
15884
|
+
});
|
|
15885
|
+
throw new Error(`HTML response too large (${sizeMB}MB, max 25MB)`, { cause: e });
|
|
15886
|
+
}
|
|
15887
|
+
throw e;
|
|
15888
|
+
}
|
|
15774
15889
|
if (bodyBytes.length > MAX_HTML_SIZE) {
|
|
15775
15890
|
const sizeMB = Math.round(bodyBytes.length / 1024 / 1024);
|
|
15776
15891
|
logger.warn(`[Scrapers] HTML response too large (actual: ${sizeMB}MB, max 25MB), skipping`);
|
|
@@ -16468,6 +16583,8 @@ function handleFetchError(error) {
|
|
|
16468
16583
|
}
|
|
16469
16584
|
function handleResponseStatus(response) {
|
|
16470
16585
|
if (!response.ok) {
|
|
16586
|
+
void response.body?.cancel()?.catch(() => {
|
|
16587
|
+
});
|
|
16471
16588
|
if (response.status === 429) {
|
|
16472
16589
|
throw new Error("NVD API rate limit exceeded (HTTP 429). Retrying with backoff...");
|
|
16473
16590
|
}
|
|
@@ -16662,6 +16779,8 @@ async function fetchWithRetryImpl(url, options, signal) {
|
|
|
16662
16779
|
async () => {
|
|
16663
16780
|
const response = await fetch(url, options);
|
|
16664
16781
|
if (!response.ok) {
|
|
16782
|
+
void response.body?.cancel()?.catch(() => {
|
|
16783
|
+
});
|
|
16665
16784
|
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
16666
16785
|
error.status = response.status;
|
|
16667
16786
|
metrics.increment("cisa_kev_errors_total", 1, { endpoint, status_code: response.status.toString() });
|
|
@@ -17018,6 +17137,8 @@ async function searchGitHubAdvisories(terms, options) {
|
|
|
17018
17137
|
signal: createTimeoutSignal(1e4, options?.signal)
|
|
17019
17138
|
});
|
|
17020
17139
|
if (!resp.ok) {
|
|
17140
|
+
void resp.body?.cancel()?.catch(() => {
|
|
17141
|
+
});
|
|
17021
17142
|
if (resp.status === 404) {
|
|
17022
17143
|
throw new Error(`Repository "${owner}/${name}" not found or no access to security advisories.`);
|
|
17023
17144
|
}
|
|
@@ -17085,6 +17206,8 @@ async function searchGitHubAdvisories(terms, options) {
|
|
|
17085
17206
|
signal: createTimeoutSignal(1e4, options?.signal)
|
|
17086
17207
|
});
|
|
17087
17208
|
if (!resp.ok) {
|
|
17209
|
+
void resp.body?.cancel()?.catch(() => {
|
|
17210
|
+
});
|
|
17088
17211
|
if (resp.status === 404) {
|
|
17089
17212
|
throw new Error("Advisory not found (HTTP 404)");
|
|
17090
17213
|
}
|
|
@@ -17405,6 +17528,8 @@ async function fetchWithRetry2(url, options, signal) {
|
|
|
17405
17528
|
async () => {
|
|
17406
17529
|
const response = await fetch(url, options);
|
|
17407
17530
|
if (!response.ok) {
|
|
17531
|
+
void response.body?.cancel()?.catch(() => {
|
|
17532
|
+
});
|
|
17408
17533
|
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
17409
17534
|
error.status = response.status;
|
|
17410
17535
|
metrics.increment("osv_errors_total", 1, { endpoint, status_code: response.status.toString() });
|
|
@@ -19143,6 +19268,8 @@ async function fetchOne(session, videoId, contentToken, lang, timeoutMs, signal,
|
|
|
19143
19268
|
headers: { "User-Agent": "Mozilla/5.0", "Accept-Language": lang }
|
|
19144
19269
|
});
|
|
19145
19270
|
if (!response.ok) {
|
|
19271
|
+
void response.body?.cancel()?.catch(() => {
|
|
19272
|
+
});
|
|
19146
19273
|
throw new Error(`HTTP ${response.status} from timedtext endpoint`);
|
|
19147
19274
|
}
|
|
19148
19275
|
const body = await response.text();
|
|
@@ -19319,7 +19446,7 @@ function formatResults(results, rejected) {
|
|
|
19319
19446
|
if (meta.length) md += `**${meta.join(" \xB7 ")}**
|
|
19320
19447
|
`;
|
|
19321
19448
|
if (r.title) {
|
|
19322
|
-
md += `**Cite as:** '${r.title}'${r.author ? ` by ${r.author}` : ""}
|
|
19449
|
+
md += `**Cite as:** ${r.url} \u2014 '${r.title}'${r.author ? ` by ${r.author}` : ""}
|
|
19323
19450
|
`;
|
|
19324
19451
|
}
|
|
19325
19452
|
md += `
|
|
@@ -19378,7 +19505,7 @@ function createYoutubeTranscriptTool(options) {
|
|
|
19378
19505
|
"You may call this tool only ONCE, so choose the videos carefully before calling.",
|
|
19379
19506
|
"Pass full watch URLs, youtu.be links, or shorts/embed/live links. Non-YouTube links are ignored.",
|
|
19380
19507
|
"Videos without captions, or that are private/age-restricted, are reported as unavailable \u2014 the rest still succeed.",
|
|
19381
|
-
`When you cite a YouTube source in CITED LINKS,
|
|
19508
|
+
`When you cite a YouTube source in CITED LINKS, the entry's link MUST be the video's watch URL (shown in this tool's output) \u2014 a title-only entry loses the source from the final report. Put the video's ACTUAL title and channel in the Description \u2014 e.g. "'<exact title>' by <channel>". Do NOT paraphrase or invent a title.`
|
|
19382
19509
|
],
|
|
19383
19510
|
parameters: YoutubeParamsSchema,
|
|
19384
19511
|
executionMode: "parallel",
|
|
@@ -19429,6 +19556,7 @@ function createYoutubeTranscriptTool(options) {
|
|
|
19429
19556
|
});
|
|
19430
19557
|
if (researchId && cachedUrls.length > 0) {
|
|
19431
19558
|
registerScrapedLinks(researchId, cachedUrls);
|
|
19559
|
+
registerTranscribedLinks(researchId, cachedUrls);
|
|
19432
19560
|
options.updateGlobalLinks?.(cachedUrls);
|
|
19433
19561
|
if (options.researcherId) {
|
|
19434
19562
|
registerResearcherScrapes(researchId, options.researcherId, cachedUrls);
|
|
@@ -19812,6 +19940,7 @@ function getPiState(piSessionId) {
|
|
|
19812
19940
|
if (!state) {
|
|
19813
19941
|
state = {
|
|
19814
19942
|
failures: /* @__PURE__ */ new Map(),
|
|
19943
|
+
failureReasons: /* @__PURE__ */ new Map(),
|
|
19815
19944
|
order: [],
|
|
19816
19945
|
panels: /* @__PURE__ */ new Map(),
|
|
19817
19946
|
aborts: /* @__PURE__ */ new Map(),
|
|
@@ -19883,6 +20012,7 @@ function refreshAllSessions(piSessionId) {
|
|
|
19883
20012
|
for (const id of state.order) {
|
|
19884
20013
|
if (!validIds.includes(id)) {
|
|
19885
20014
|
state.failures.delete(id);
|
|
20015
|
+
state.failureReasons.delete(id);
|
|
19886
20016
|
state.panels.delete(id);
|
|
19887
20017
|
}
|
|
19888
20018
|
}
|
|
@@ -19914,6 +20044,7 @@ function endResearchSession(piSessionId, researchId) {
|
|
|
19914
20044
|
const state = piSessions.get(sid);
|
|
19915
20045
|
if (!state) return;
|
|
19916
20046
|
state.failures.delete(researchId);
|
|
20047
|
+
state.failureReasons.delete(researchId);
|
|
19917
20048
|
state.panels.delete(researchId);
|
|
19918
20049
|
state.aborts.delete(researchId);
|
|
19919
20050
|
const index = state.order.indexOf(researchId);
|
|
@@ -19931,6 +20062,7 @@ function endResearchSession(piSessionId, researchId) {
|
|
|
19931
20062
|
piSessions.delete(sid);
|
|
19932
20063
|
} else {
|
|
19933
20064
|
state.failures.clear();
|
|
20065
|
+
state.failureReasons.clear();
|
|
19934
20066
|
state.aborts.clear();
|
|
19935
20067
|
}
|
|
19936
20068
|
}
|
|
@@ -19952,12 +20084,23 @@ function clearAllSessionState() {
|
|
|
19952
20084
|
clearAllSharedLinks();
|
|
19953
20085
|
logger.log("[session-state] All session state cleared");
|
|
19954
20086
|
}
|
|
19955
|
-
function recordResearcherFailure(piSessionId, researchId, researcherId) {
|
|
20087
|
+
function recordResearcherFailure(piSessionId, researchId, researcherId, reason) {
|
|
19956
20088
|
const sid = normalizeSessionId(piSessionId);
|
|
19957
20089
|
const state = getPiState(sid);
|
|
19958
20090
|
const failures = state.failures.get(researchId) || [];
|
|
19959
20091
|
failures.push(researcherId);
|
|
19960
20092
|
state.failures.set(researchId, failures);
|
|
20093
|
+
if (reason) {
|
|
20094
|
+
const reasons = state.failureReasons.get(researchId) ?? /* @__PURE__ */ new Map();
|
|
20095
|
+
if (!reasons.has(researcherId)) reasons.set(researcherId, reason);
|
|
20096
|
+
state.failureReasons.set(researchId, reasons);
|
|
20097
|
+
}
|
|
20098
|
+
}
|
|
20099
|
+
function getResearcherFailureReasons(piSessionId, researchId) {
|
|
20100
|
+
const sid = normalizeSessionId(piSessionId);
|
|
20101
|
+
const state = getPiState(sid);
|
|
20102
|
+
const reasons = state.failureReasons.get(researchId);
|
|
20103
|
+
return reasons ? Object.fromEntries(reasons) : {};
|
|
19961
20104
|
}
|
|
19962
20105
|
function getFailedResearchers(piSessionId, researchId) {
|
|
19963
20106
|
const sid = normalizeSessionId(piSessionId);
|
|
@@ -19974,9 +20117,13 @@ function getResearchStopMessage(piSessionId, researchId) {
|
|
|
19974
20117
|
const sid = normalizeSessionId(piSessionId);
|
|
19975
20118
|
const failed = getFailedResearchers(sid, researchId);
|
|
19976
20119
|
const count = failed.length;
|
|
20120
|
+
const state = getPiState(sid);
|
|
20121
|
+
const reasons = state.failureReasons.get(researchId);
|
|
20122
|
+
const recorded = reasons && reasons.size > 0 ? ["Recorded errors:", ...[...reasons].map(([id, r]) => `\u2022 researcher ${id}: ${r}`), ""] : [];
|
|
19977
20123
|
return [
|
|
19978
20124
|
`Research stopped: ${count} researcher(s) did not return a usable report: ${failed.join(", ")}.`,
|
|
19979
20125
|
"",
|
|
20126
|
+
...recorded,
|
|
19980
20127
|
"Most likely cause \u2014 the research model could not produce grounded results:",
|
|
19981
20128
|
'\u2022 Try a more capable research model. Very small or "thinking-only" models often',
|
|
19982
20129
|
" emit no final text after their tool calls, which fails every researcher.",
|
|
@@ -20026,7 +20173,7 @@ ${currentPlan.allQueries.map((q) => `- ${q}`).join("\n")}
|
|
|
20026
20173
|
const researcherPromptTemplate = loadPrompt("researcher");
|
|
20027
20174
|
if (initialLinks.length === 0 && historicalUrls.length === 0) {
|
|
20028
20175
|
logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results or historical links; skipping.`);
|
|
20029
|
-
recordResearcherFailure(sessionId, researchId, id);
|
|
20176
|
+
recordResearcherFailure(sessionId, researchId, id, "no initial search results or historical links (search produced nothing usable)");
|
|
20030
20177
|
metrics.increment("researcher_skipped_total", 1, { mode: "deep", complexity: String(complexity), reason: "no_initial_links" });
|
|
20031
20178
|
observer?.onResearcherFailure?.(id, "No initial search results or historical links available");
|
|
20032
20179
|
return;
|
|
@@ -20280,6 +20427,34 @@ init_constants();
|
|
|
20280
20427
|
|
|
20281
20428
|
// src/orchestration/headless-observer.ts
|
|
20282
20429
|
init_logger();
|
|
20430
|
+
function makeSafeObserver(observer) {
|
|
20431
|
+
return new Proxy(observer, {
|
|
20432
|
+
get(target, prop) {
|
|
20433
|
+
let value;
|
|
20434
|
+
try {
|
|
20435
|
+
value = Reflect.get(target, prop, target);
|
|
20436
|
+
} catch (err) {
|
|
20437
|
+
logger.warn(`[observer] Ignored error reading observer.${String(prop)}:`, err);
|
|
20438
|
+
return void 0;
|
|
20439
|
+
}
|
|
20440
|
+
if (typeof value !== "function") return value;
|
|
20441
|
+
return (...args) => {
|
|
20442
|
+
try {
|
|
20443
|
+
const out = value.apply(target, args);
|
|
20444
|
+
if (out && typeof out.then === "function") {
|
|
20445
|
+
return out.catch((err) => {
|
|
20446
|
+
logger.warn(`[observer] Ignored rejection from observer.${String(prop)}:`, err);
|
|
20447
|
+
});
|
|
20448
|
+
}
|
|
20449
|
+
return out;
|
|
20450
|
+
} catch (err) {
|
|
20451
|
+
logger.warn(`[observer] Ignored error from observer.${String(prop)}:`, err);
|
|
20452
|
+
return void 0;
|
|
20453
|
+
}
|
|
20454
|
+
};
|
|
20455
|
+
}
|
|
20456
|
+
});
|
|
20457
|
+
}
|
|
20283
20458
|
var HeadlessObserver = class {
|
|
20284
20459
|
constructor(options = {}) {
|
|
20285
20460
|
this.options = options;
|
|
@@ -20369,9 +20544,9 @@ var QuickResearchOrchestrator = class {
|
|
|
20369
20544
|
this.options = options;
|
|
20370
20545
|
this.config = options.config || getConfig(options.ctx.cwd);
|
|
20371
20546
|
if (options.observer && typeof options.observer.onProgress === "function" && !(options.observer instanceof HeadlessObserver)) {
|
|
20372
|
-
this.observer = new HeadlessObserver(options.observer);
|
|
20547
|
+
this.observer = makeSafeObserver(new HeadlessObserver(options.observer));
|
|
20373
20548
|
} else {
|
|
20374
|
-
this.observer = options.observer;
|
|
20549
|
+
this.observer = options.observer ? makeSafeObserver(options.observer) : void 0;
|
|
20375
20550
|
}
|
|
20376
20551
|
}
|
|
20377
20552
|
options;
|
|
@@ -20685,9 +20860,9 @@ var DeepResearchOrchestrator = class {
|
|
|
20685
20860
|
this.options = options;
|
|
20686
20861
|
this.config = options.config || getConfig(options.ctx.cwd);
|
|
20687
20862
|
if (options.observer && typeof options.observer.onProgress === "function" && !(options.observer instanceof HeadlessObserver)) {
|
|
20688
|
-
this.observer = new HeadlessObserver(options.observer);
|
|
20863
|
+
this.observer = makeSafeObserver(new HeadlessObserver(options.observer));
|
|
20689
20864
|
} else {
|
|
20690
|
-
this.observer = options.observer;
|
|
20865
|
+
this.observer = options.observer ? makeSafeObserver(options.observer) : void 0;
|
|
20691
20866
|
}
|
|
20692
20867
|
if (options.orchestrationService) {
|
|
20693
20868
|
this.orchestrationService = options.orchestrationService;
|
|
@@ -20960,13 +21135,22 @@ var DeepResearchOrchestrator = class {
|
|
|
20960
21135
|
observer?.onEvaluationDecision?.("synthesize", finalReport, maxRounds);
|
|
20961
21136
|
}
|
|
20962
21137
|
let result = finalReport.content || "";
|
|
21138
|
+
const synthesisCheckService = await getService(ServiceNames.RESEARCH_SYNTHESIS_SERVICE, ctx, container);
|
|
21139
|
+
const hasReports = synthesisCheckService.hasReports(researchId);
|
|
21140
|
+
const failedIds = getFailedResearchers(this.options.sessionId, researchId);
|
|
21141
|
+
if (!hasReports && failedIds.length > 0) {
|
|
21142
|
+
const reasons = getResearcherFailureReasons(this.options.sessionId, researchId);
|
|
21143
|
+
const causes = failedIds.map((id) => `researcher ${id}: ${reasons[id] ?? "no usable report"}`).join("; ");
|
|
21144
|
+
throw new Error(
|
|
21145
|
+
`Research produced no report \u2014 no source material was collected. Causes: ${causes}`
|
|
21146
|
+
);
|
|
21147
|
+
}
|
|
20963
21148
|
if (!result.trim()) {
|
|
20964
|
-
|
|
20965
|
-
|
|
20966
|
-
|
|
20967
|
-
result = synthesisService2.buildFallbackSynthesis(researchId, this.currentRound);
|
|
21149
|
+
if (hasReports) {
|
|
21150
|
+
logger.warn(`[DeepOrchestrator] LLM returned empty synthesis for ${researchId}, building fallback from ${synthesisCheckService.getReportCount(researchId)} reports`);
|
|
21151
|
+
result = synthesisCheckService.buildFallbackSynthesis(researchId, this.currentRound);
|
|
20968
21152
|
} else {
|
|
20969
|
-
|
|
21153
|
+
throw new Error("Research produced no report \u2014 no source material was collected.");
|
|
20970
21154
|
}
|
|
20971
21155
|
}
|
|
20972
21156
|
try {
|
|
@@ -21268,7 +21452,7 @@ var ResearchOrchestrationService = class {
|
|
|
21268
21452
|
logger.debug(`[ResearchOrchestrationService] Researcher ${id} cancelled (aborted).`);
|
|
21269
21453
|
} else {
|
|
21270
21454
|
logger.error(`[ResearchOrchestrationService] Researcher ${id} failed: ${errMsg}`);
|
|
21271
|
-
recordResearcherFailure(sessionId, researchId, id);
|
|
21455
|
+
recordResearcherFailure(sessionId, researchId, id, errMsg);
|
|
21272
21456
|
observer?.onResearcherFailure?.(id, errMsg);
|
|
21273
21457
|
}
|
|
21274
21458
|
}
|
|
@@ -21697,9 +21881,14 @@ Research performed using ${modelId}`;
|
|
|
21697
21881
|
if (globalCitations.length === 0) {
|
|
21698
21882
|
const scraped = getScrapedLinks(sessionId);
|
|
21699
21883
|
if (scraped.length > 0) {
|
|
21700
|
-
globalCitations = scraped.map((url, i) => ({
|
|
21884
|
+
globalCitations = scraped.map((url, i) => ({
|
|
21885
|
+
id: i + 1,
|
|
21886
|
+
url,
|
|
21887
|
+
description: "",
|
|
21888
|
+
source: isTranscribedLink(sessionId, url) ? "YouTube Transcript" : "Scrape"
|
|
21889
|
+
}));
|
|
21701
21890
|
logger.warn(
|
|
21702
|
-
`[ResearchSynthesisService] No citations parsed from reports; rebuilt ${scraped.length} source(s) from scrape provenance.`
|
|
21891
|
+
`[ResearchSynthesisService] No citations parsed from reports; rebuilt ${scraped.length} source(s) from scrape/transcript provenance.`
|
|
21703
21892
|
);
|
|
21704
21893
|
}
|
|
21705
21894
|
}
|
|
@@ -22000,6 +22189,10 @@ async function _doInit(options = {}) {
|
|
|
22000
22189
|
}
|
|
22001
22190
|
globalApiKey = options.apiKey || process.env["PI_RESEARCH_API_KEY"];
|
|
22002
22191
|
let parsedProvider = options.provider || process.env["PI_RESEARCH_PROVIDER"];
|
|
22192
|
+
if (options.model) {
|
|
22193
|
+
const explicit = typeof options.model === "string" ? options.model : `${options.model.provider}/${options.model.id}`;
|
|
22194
|
+
globalConfig = { ...globalConfig, RESEARCH_MODEL: explicit };
|
|
22195
|
+
}
|
|
22003
22196
|
const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
|
|
22004
22197
|
if (!parsedProvider && effectiveModel) {
|
|
22005
22198
|
if (typeof effectiveModel === "string") {
|
|
@@ -22229,6 +22422,7 @@ async function _doShutdown() {
|
|
|
22229
22422
|
_lastResearchId = null;
|
|
22230
22423
|
_lastRunSummary = null;
|
|
22231
22424
|
_lastErrorReport = null;
|
|
22425
|
+
resetConfig();
|
|
22232
22426
|
_removeSignalHandlers();
|
|
22233
22427
|
if (errors.length > 0) {
|
|
22234
22428
|
throw new AggregateError(errors, `${errors.length} error(s) during SDK shutdown`);
|
|
@@ -22475,6 +22669,7 @@ Total cost: ${costStr}`;
|
|
|
22475
22669
|
|
|
22476
22670
|
// src/cli.ts
|
|
22477
22671
|
init_config();
|
|
22672
|
+
init_model_registry_factory();
|
|
22478
22673
|
import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
|
|
22479
22674
|
function readPkgVersion() {
|
|
22480
22675
|
try {
|
|
@@ -22593,7 +22788,16 @@ function fileExists(p) {
|
|
|
22593
22788
|
return false;
|
|
22594
22789
|
}
|
|
22595
22790
|
}
|
|
22596
|
-
function
|
|
22791
|
+
function piKeysAvailable(piAuthPresent, provider) {
|
|
22792
|
+
try {
|
|
22793
|
+
const available = safeGetAvailable(buildModelRegistry(void 0, void 0));
|
|
22794
|
+
if (!provider) return available.length > 0;
|
|
22795
|
+
return available.some((m) => m.provider === provider);
|
|
22796
|
+
} catch {
|
|
22797
|
+
return piAuthPresent;
|
|
22798
|
+
}
|
|
22799
|
+
}
|
|
22800
|
+
function detectCredentials(explicitModel) {
|
|
22597
22801
|
const paths = resolvedConfigPaths();
|
|
22598
22802
|
const piAuthPresent = fileExists(paths.piAuth);
|
|
22599
22803
|
const piModelsPresent = fileExists(paths.piModels);
|
|
@@ -22601,9 +22805,10 @@ function detectCredentials() {
|
|
|
22601
22805
|
const providerEnv = process.env["PI_RESEARCH_PROVIDER"];
|
|
22602
22806
|
const modelEnv = process.env["PI_RESEARCH_MODEL"];
|
|
22603
22807
|
const modelConfig = getConfig(process.cwd(), "cli").RESEARCH_MODEL;
|
|
22604
|
-
const model = modelEnv ?? modelConfig;
|
|
22605
|
-
const modelFrom = modelEnv ? "PI_RESEARCH_MODEL" : modelConfig ? "config.env" : "unset";
|
|
22808
|
+
const model = explicitModel ?? modelEnv ?? modelConfig;
|
|
22809
|
+
const modelFrom = explicitModel ? "--model" : modelEnv ? "PI_RESEARCH_MODEL" : modelConfig ? "config.env" : "unset";
|
|
22606
22810
|
const provider = providerEnv ?? (model && model.includes("/") ? model.slice(0, model.indexOf("/")) : void 0);
|
|
22811
|
+
const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
|
|
22607
22812
|
if (apiKey) {
|
|
22608
22813
|
if (!provider) {
|
|
22609
22814
|
return {
|
|
@@ -22626,7 +22831,7 @@ function detectCredentials() {
|
|
|
22626
22831
|
modelFrom,
|
|
22627
22832
|
piAuthPresent,
|
|
22628
22833
|
piModelsPresent,
|
|
22629
|
-
problem: 'PI_RESEARCH_API_KEY is set but no model is configured. Set PI_RESEARCH_MODEL to a "provider/model-id" (e.g. openai/gpt-4o).'
|
|
22834
|
+
problem: 'PI_RESEARCH_API_KEY is set but no model is configured. Set PI_RESEARCH_MODEL to a "provider/model-id" (e.g. openai/gpt-4o), or pass --model <provider/id> for a single run.'
|
|
22630
22835
|
};
|
|
22631
22836
|
}
|
|
22632
22837
|
return {
|
|
@@ -22639,7 +22844,7 @@ function detectCredentials() {
|
|
|
22639
22844
|
piModelsPresent
|
|
22640
22845
|
};
|
|
22641
22846
|
}
|
|
22642
|
-
if (
|
|
22847
|
+
if (piKeysUsable) {
|
|
22643
22848
|
if (!model) {
|
|
22644
22849
|
return {
|
|
22645
22850
|
source: "pi-config",
|
|
@@ -22649,13 +22854,13 @@ function detectCredentials() {
|
|
|
22649
22854
|
modelFrom,
|
|
22650
22855
|
piAuthPresent,
|
|
22651
22856
|
piModelsPresent,
|
|
22652
|
-
problem:
|
|
22857
|
+
problem: `pi credentials were found (in pi's configuration), but no research model is configured. The standalone CLI / agent skill run only on an explicitly configured model \u2014 they do not follow the model selected inside the pi extension. Set PI_RESEARCH_MODEL to a "provider/model-id" (in the environment, or in config.env / cli.env below), or pass --model <provider/id> for a single run.`
|
|
22653
22858
|
};
|
|
22654
22859
|
}
|
|
22655
22860
|
return {
|
|
22656
22861
|
source: "pi-config",
|
|
22657
22862
|
apiKeyConfigured: true,
|
|
22658
|
-
// key lives in
|
|
22863
|
+
// key lives in pi's configuration; the SDK reads it at call time
|
|
22659
22864
|
provider,
|
|
22660
22865
|
model,
|
|
22661
22866
|
modelFrom,
|
|
@@ -22671,7 +22876,7 @@ function detectCredentials() {
|
|
|
22671
22876
|
modelFrom,
|
|
22672
22877
|
piAuthPresent,
|
|
22673
22878
|
piModelsPresent,
|
|
22674
|
-
problem: "No model or API key is configured for pi-research. Provide credentials via environment variables (PI_RESEARCH_API_KEY + PI_RESEARCH_PROVIDER + PI_RESEARCH_MODEL) or, if you use pi, via ~/.pi/agent/auth.json."
|
|
22879
|
+
problem: "No model or API key is configured for pi-research. Provide credentials via environment variables (PI_RESEARCH_API_KEY + PI_RESEARCH_PROVIDER + PI_RESEARCH_MODEL) or, if you use pi, via pi's configuration (a provider entry in ~/.pi/agent/auth.json, or a per-provider apiKey in ~/.pi/agent/models.json)."
|
|
22675
22880
|
};
|
|
22676
22881
|
}
|
|
22677
22882
|
function configBlock(det, extraNote) {
|
|
@@ -22689,13 +22894,13 @@ function configBlock(det, extraNote) {
|
|
|
22689
22894
|
lines.push(` \u2022 api key configured: ${det.apiKeyConfigured}`);
|
|
22690
22895
|
lines.push(` \u2022 provider: ${det.provider ?? "(unset \u2014 inferred from a provider/model-id model)"}`);
|
|
22691
22896
|
lines.push(` \u2022 model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : "(not set \u2014 set PI_RESEARCH_MODEL, required)"}`);
|
|
22692
|
-
lines.push(` \u2022 pi auth.json:
|
|
22693
|
-
lines.push(` \u2022 pi models.json:
|
|
22897
|
+
lines.push(` \u2022 pi config (auth.json): ${det.piAuthPresent ? "present" : "absent"}`);
|
|
22898
|
+
lines.push(` \u2022 pi config (models.json): ${det.piModelsPresent ? "present" : "absent"}`);
|
|
22694
22899
|
if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
|
|
22695
22900
|
return lines.join("\n");
|
|
22696
22901
|
}
|
|
22697
22902
|
async function cmdResearch(args) {
|
|
22698
|
-
const det = detectCredentials();
|
|
22903
|
+
const det = detectCredentials(args.model);
|
|
22699
22904
|
if (det.problem) {
|
|
22700
22905
|
if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
22701
22906
|
else toStderr(`
|
|
@@ -22717,7 +22922,7 @@ Error: ${vmsg}
|
|
|
22717
22922
|
return EXIT.USAGE;
|
|
22718
22923
|
}
|
|
22719
22924
|
const depth = args.depth ?? getConfig(process.cwd(), "cli").DEFAULT_RESEARCH_DEPTH;
|
|
22720
|
-
const runModel =
|
|
22925
|
+
const runModel = det.model;
|
|
22721
22926
|
toStderr(`[pi-research] starting research (depth ${depth})${runModel ? ` with ${runModel}` : ""}\u2026
|
|
22722
22927
|
`);
|
|
22723
22928
|
const observer = makeProgressObserver();
|
|
@@ -23035,7 +23240,7 @@ function reportError(err, what, json) {
|
|
|
23035
23240
|
const lower = msg2.toLowerCase();
|
|
23036
23241
|
const isConfigError = lower.includes("no llm model available") || lower.includes("not found in pi") || lower.includes("no api key") || lower.includes("provider must be specified") || lower.includes("invalid model string") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("api key");
|
|
23037
23242
|
const isRateLimit = lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
|
|
23038
|
-
const exitCode = isConfigError ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23243
|
+
const exitCode = isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23039
23244
|
if (json) {
|
|
23040
23245
|
const payload = { ok: false, error: msg2, exitCode };
|
|
23041
23246
|
if (process.env["PI_RESEARCH_DEBUG"] === "true" && err instanceof Error && err.stack) {
|
|
@@ -23044,18 +23249,18 @@ function reportError(err, what, json) {
|
|
|
23044
23249
|
toStdout(pretty(payload));
|
|
23045
23250
|
return exitCode;
|
|
23046
23251
|
}
|
|
23047
|
-
if (
|
|
23252
|
+
if (isRateLimit) {
|
|
23048
23253
|
toStderr(`
|
|
23049
|
-
Error: pi-research ${what}
|
|
23050
|
-
|
|
23051
|
-
${configBlock(detectCredentials())}
|
|
23254
|
+
Error: pi-research ${what} halted: provider rate limit reached. Wait a moment and retry.
|
|
23255
|
+
${msg2}
|
|
23052
23256
|
`);
|
|
23053
23257
|
return exitCode;
|
|
23054
23258
|
}
|
|
23055
|
-
if (
|
|
23259
|
+
if (isConfigError) {
|
|
23056
23260
|
toStderr(`
|
|
23057
|
-
Error: pi-research ${what}
|
|
23058
|
-
|
|
23261
|
+
Error: pi-research ${what} failed: ${msg2}
|
|
23262
|
+
|
|
23263
|
+
${configBlock(detectCredentials())}
|
|
23059
23264
|
`);
|
|
23060
23265
|
return exitCode;
|
|
23061
23266
|
}
|
|
@@ -23092,7 +23297,7 @@ USAGE
|
|
|
23092
23297
|
COMMANDS
|
|
23093
23298
|
research "<query>" Run multi-agent web research (default depth: 1).
|
|
23094
23299
|
--depth <0-3> 0 = quick, 1 = normal (default), 2 = deep, 3 = ultra.
|
|
23095
|
-
--model <provider/id>
|
|
23300
|
+
--model <provider/id> Model for this run (outranks PI_RESEARCH_MODEL; satisfies the required-model check).
|
|
23096
23301
|
--exclude-tools <a,b> Disable internal tools (e.g. security_search,stackexchange).
|
|
23097
23302
|
--initial-links <url ...> Seed URLs to investigate first; requires a query (-- ends options).
|
|
23098
23303
|
--config <path> Use this config file instead of the base config.env.
|
|
@@ -23129,9 +23334,9 @@ CONFIGURE
|
|
|
23129
23334
|
pi config (keys): ${p.piAuth}
|
|
23130
23335
|
pi config (models): ${p.piModels}
|
|
23131
23336
|
|
|
23132
|
-
A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id"
|
|
23133
|
-
the agent skill run only on that configured
|
|
23134
|
-
selected inside the pi extension.
|
|
23337
|
+
A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id" (or pass
|
|
23338
|
+
--model per run). This CLI and the agent skill run only on that configured
|
|
23339
|
+
model \u2014 they do not follow the model selected inside the pi extension.
|
|
23135
23340
|
|
|
23136
23341
|
The pi extension has its own optional overlay file:
|
|
23137
23342
|
pi extension: ${p.piIfaceEnv}
|