@lincoln504/pi-research 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/{skills → agent-skill}/pi-research/README.md +5 -9
- package/{skills → agent-skill}/pi-research/SKILL.md +5 -4
- package/{skills → agent-skill}/pi-research/scripts/run.mjs +1 -1
- package/dist/cli.mjs +158 -129
- package/dist/thread-worker.mjs +105 -17
- package/docs/AGENT-SKILL.md +26 -40
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHANGELOG.md +52 -0
- package/docs/CONFIGURATION.md +4 -4
- package/docs/PI-EXTENSION.md +1 -1
- package/docs/SDK.md +73 -10
- package/package.json +9 -21
- package/scripts/build.cjs +4 -4
- package/scripts/cleanup.cjs +8 -3
- package/scripts/prepare.cjs +1 -1
- package/src/cli.ts +1 -1
- package/src/infrastructure/browser/browser-client.ts +24 -6
- package/src/infrastructure/browser/browser-error-utils.ts +53 -0
- package/src/infrastructure/browser/browser-server.ts +9 -1
- package/src/infrastructure/browser/thread-worker-browser.ts +4 -1
- package/src/infrastructure/browser/thread-worker-messaging.ts +186 -3
- package/src/infrastructure/browser/thread-worker.mjs +105 -17
- package/src/knowledge/store-operations.ts +18 -7
- package/src/knowledge/store.ts +21 -3
- package/src/skill-install/skill-installer.ts +59 -21
- package/src/web-research/scraper-utils.ts +4 -36
- package/src/youtube/potoken.ts +47 -34
- /package/{skills → agent-skill}/pi-research/references/configuration.md +0 -0
package/dist/thread-worker.mjs
CHANGED
|
@@ -1516,11 +1516,25 @@ function isPoolShutdownError(error) {
|
|
|
1516
1516
|
// same transient swap the "being reset" string covers, not a hard fault.
|
|
1517
1517
|
err.message.includes("Cannot execute a task on not started pool"));
|
|
1518
1518
|
}
|
|
1519
|
-
|
|
1519
|
+
function isBenignScrapeFailure(error) {
|
|
1520
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1521
|
+
return msg.startsWith("Fetch blocked:") || // bot-protection interstitial (Cloudflare, DDoS-Guard, …)
|
|
1522
|
+
msg.startsWith("Fetch returned stub:") || // nav-only / near-empty page
|
|
1523
|
+
msg.startsWith("Could not extract content from PDF") || // malformed/encrypted/scanned-only PDF
|
|
1524
|
+
msg.startsWith("PDF too large") || // exceeds the deliberate 100MB policy cap
|
|
1525
|
+
msg.startsWith("Unsupported download:") || // attachment-served non-PDF (zip/xlsx/…) — nothing to extract
|
|
1526
|
+
BENIGN_NAVIGATION_FAILURE.test(msg) || // remote TLS/DNS/timeout — the URL could not be loaded
|
|
1527
|
+
msg.startsWith("HTML response too large") || // fetch-layer 25MB policy cap — same class as the PDF cap
|
|
1528
|
+
msg.startsWith("Browser HTML too large") || // browser-layer 25MB policy cap
|
|
1529
|
+
/^HTTP \d{3}\b/.test(msg) || // remote 4xx/5xx status
|
|
1530
|
+
isCloudflareBlockError(error) || isTaskTimeoutError(error) || isPoolShutdownError(error);
|
|
1531
|
+
}
|
|
1532
|
+
var BENIGN_NAVIGATION_FAILURE, DEFAULT_BREAKER_CONFIG, browserCircuitBreaker;
|
|
1520
1533
|
var init_browser_error_utils = __esm({
|
|
1521
1534
|
"src/infrastructure/browser/browser-error-utils.ts"() {
|
|
1522
1535
|
"use strict";
|
|
1523
1536
|
init_circuit_breaker();
|
|
1537
|
+
BENIGN_NAVIGATION_FAILURE = /page\.goto: (?:Timeout \d+ *ms exceeded|net::|NS_ERROR_|SEC_ERROR_|MOZILLA_PKIX_ERROR_)/;
|
|
1524
1538
|
DEFAULT_BREAKER_CONFIG = {
|
|
1525
1539
|
// Raised from 3→8: parallel search bursts of 20 queries frequently produce
|
|
1526
1540
|
// simultaneous timeouts on slow/CF-protected sites. A threshold of 3 caused
|
|
@@ -1565,17 +1579,6 @@ import * as dnsCb from "node:dns";
|
|
|
1565
1579
|
import * as net from "node:net";
|
|
1566
1580
|
import crypto from "node:crypto";
|
|
1567
1581
|
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
1568
|
-
function isBenignScrapeFailure(error) {
|
|
1569
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1570
|
-
return msg.startsWith("Fetch blocked:") || // bot-protection interstitial (Cloudflare, DDoS-Guard, …)
|
|
1571
|
-
msg.startsWith("Fetch returned stub:") || // nav-only / near-empty page
|
|
1572
|
-
msg.startsWith("Could not extract content from PDF") || // malformed/encrypted/scanned-only PDF
|
|
1573
|
-
msg.startsWith("PDF too large") || // exceeds the deliberate 100MB policy cap
|
|
1574
|
-
msg.startsWith("HTML response too large") || // fetch-layer 25MB policy cap — same class as the PDF cap
|
|
1575
|
-
msg.startsWith("Browser HTML too large") || // browser-layer 25MB policy cap
|
|
1576
|
-
/^HTTP \d{3}\b/.test(msg) || // remote 4xx/5xx status
|
|
1577
|
-
isCloudflareBlockError(error) || isTaskTimeoutError(error) || isPoolShutdownError(error);
|
|
1578
|
-
}
|
|
1579
1582
|
function getRandomUserAgent() {
|
|
1580
1583
|
try {
|
|
1581
1584
|
const index = crypto.randomInt(0, USER_AGENTS.length);
|
|
@@ -2014,7 +2017,7 @@ import { platform as platform2 } from "node:os";
|
|
|
2014
2017
|
|
|
2015
2018
|
// src/infrastructure/browser/thread-worker-messaging.ts
|
|
2016
2019
|
init_log_utils();
|
|
2017
|
-
import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
2020
|
+
import { appendFileSync as appendFileSync2, promises as fsp } from "node:fs";
|
|
2018
2021
|
|
|
2019
2022
|
// src/utils/safe-unref.ts
|
|
2020
2023
|
function safeUnref(timer) {
|
|
@@ -2286,6 +2289,66 @@ function seedSubresourceVerdict(validatedUrl) {
|
|
|
2286
2289
|
} catch {
|
|
2287
2290
|
}
|
|
2288
2291
|
}
|
|
2292
|
+
function isEvictedBodyError(err) {
|
|
2293
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2294
|
+
return /was evicted|Response body is unavailable/i.test(msg);
|
|
2295
|
+
}
|
|
2296
|
+
var PDF_REFETCH_TIMEOUT_MS = 9e4;
|
|
2297
|
+
async function refetchPdfBody(page, url, validateForWorker, signal) {
|
|
2298
|
+
await validateForWorker(url);
|
|
2299
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
2300
|
+
const requestPromise = page.context().request.get(url, {
|
|
2301
|
+
maxRedirects: 0,
|
|
2302
|
+
timeout: PDF_REFETCH_TIMEOUT_MS
|
|
2303
|
+
});
|
|
2304
|
+
requestPromise.catch(() => {
|
|
2305
|
+
});
|
|
2306
|
+
const response = await (signal ? Promise.race([
|
|
2307
|
+
requestPromise,
|
|
2308
|
+
new Promise((_, reject) => {
|
|
2309
|
+
if (signal.aborted) return reject(new Error("Aborted"));
|
|
2310
|
+
signal.addEventListener("abort", () => reject(new Error("Aborted")), { once: true });
|
|
2311
|
+
})
|
|
2312
|
+
]) : requestPromise);
|
|
2313
|
+
const status = Number(response.status?.() ?? 0);
|
|
2314
|
+
if (!response.ok?.()) throw new Error(`HTTP ${status}`);
|
|
2315
|
+
const declared = parseInt(String((await response.headersArray?.() ?? []).find?.((h) => String(h.name).toLowerCase() === "content-length")?.value ?? ""), 10);
|
|
2316
|
+
if (Number.isFinite(declared) && declared > MAX_PDF_SIZE) {
|
|
2317
|
+
throw new Error(`PDF too large (${Math.round(declared / 1024 / 1024)}MB, max 100MB)`);
|
|
2318
|
+
}
|
|
2319
|
+
const buffer = await response.body();
|
|
2320
|
+
if (buffer.byteLength > MAX_PDF_SIZE) {
|
|
2321
|
+
throw new Error(`PDF too large (${Math.round(buffer.byteLength / 1024 / 1024)}MB, max 100MB)`);
|
|
2322
|
+
}
|
|
2323
|
+
logToDebugFile2("DEBUG", `[Worker-${workerId2}] Re-fetched evicted PDF body (${buffer.byteLength} bytes) for: ${url}`);
|
|
2324
|
+
return buffer;
|
|
2325
|
+
}
|
|
2326
|
+
async function readPdfDownload(download, url, page, pendingAddrChecks, getPoisonedError) {
|
|
2327
|
+
const failure = await download.failure?.().catch(() => null);
|
|
2328
|
+
if (failure) throw new Error(`Fetch blocked: download failed (${failure})`);
|
|
2329
|
+
const suggested = String(download.suggestedFilename?.() ?? "");
|
|
2330
|
+
const downloadUrl = String(download.url?.() ?? url);
|
|
2331
|
+
const looksLikePdf = suggested.toLowerCase().endsWith(".pdf") || downloadUrl.toLowerCase().split("?")[0].endsWith(".pdf");
|
|
2332
|
+
if (!looksLikePdf) {
|
|
2333
|
+
throw new Error(`Unsupported download: ${suggested || downloadUrl} is not a PDF`);
|
|
2334
|
+
}
|
|
2335
|
+
const filePath = await download.path();
|
|
2336
|
+
if (!filePath) throw new Error("Fetch blocked: download produced no readable file");
|
|
2337
|
+
const { size } = await fsp.stat(filePath);
|
|
2338
|
+
if (size > MAX_PDF_SIZE) {
|
|
2339
|
+
const sizeMB = Math.round(size / 1024 / 1024);
|
|
2340
|
+
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
2341
|
+
}
|
|
2342
|
+
const buffer = await fsp.readFile(filePath);
|
|
2343
|
+
await Promise.allSettled(pendingAddrChecks);
|
|
2344
|
+
const poisoned = getPoisonedError();
|
|
2345
|
+
if (poisoned) throw poisoned;
|
|
2346
|
+
await download.delete?.().catch(() => {
|
|
2347
|
+
});
|
|
2348
|
+
await page.close();
|
|
2349
|
+
logToDebugFile2("DEBUG", `[Worker-${workerId2}] Recovered attachment-served PDF (${buffer.byteLength} bytes) for: ${url}`);
|
|
2350
|
+
return { contentType: "application/pdf", bufferB64: buffer.toString("base64"), jitter: 0 };
|
|
2351
|
+
}
|
|
2289
2352
|
async function executeScrapeTask(_context, url, signal) {
|
|
2290
2353
|
const page = await createPageSafe(_context);
|
|
2291
2354
|
const SCRAPE_TIMEOUT = parseTimeoutMs(process.env["PI_RESEARCH_SCRAPE_TIMEOUT_MS"], 15e3);
|
|
@@ -2302,6 +2365,7 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2302
2365
|
let poisonedError = null;
|
|
2303
2366
|
const pendingAddrChecks = [];
|
|
2304
2367
|
let oversizedPdfError = null;
|
|
2368
|
+
let pendingDownload = null;
|
|
2305
2369
|
try {
|
|
2306
2370
|
logToDebugFile2("DEBUG", `[Worker-${workerId2}] Starting scrape for: ${url}`);
|
|
2307
2371
|
const { validateUrlForSSRF: validateForWorker, validateUrlForSSRFSync: validateForWorkerSync } = await Promise.resolve().then(() => (init_scraper_utils(), scraper_utils_exports));
|
|
@@ -2368,7 +2432,25 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2368
2432
|
} catch {
|
|
2369
2433
|
}
|
|
2370
2434
|
});
|
|
2371
|
-
|
|
2435
|
+
page.on("download", (d) => {
|
|
2436
|
+
pendingDownload ??= d;
|
|
2437
|
+
});
|
|
2438
|
+
let response;
|
|
2439
|
+
try {
|
|
2440
|
+
response = await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
2441
|
+
} catch (navErr) {
|
|
2442
|
+
const navMsg = navErr instanceof Error ? navErr.message : String(navErr);
|
|
2443
|
+
const isDownloadNav = /download is starting/i.test(navMsg);
|
|
2444
|
+
if (!isDownloadNav) throw navErr;
|
|
2445
|
+
if (!pendingDownload) {
|
|
2446
|
+
await Promise.race([
|
|
2447
|
+
page.waitForEvent("download", { timeout: 5e3 }).catch(() => null),
|
|
2448
|
+
new Promise((r) => setTimeout(r, 5e3))
|
|
2449
|
+
]);
|
|
2450
|
+
}
|
|
2451
|
+
if (!pendingDownload) throw navErr;
|
|
2452
|
+
return await readPdfDownload(pendingDownload, url, page, pendingAddrChecks, () => poisonedError);
|
|
2453
|
+
}
|
|
2372
2454
|
const serverAddr = await response?.serverAddr().catch(() => null);
|
|
2373
2455
|
if (serverAddr?.ipAddress) {
|
|
2374
2456
|
const ip = serverAddr.ipAddress;
|
|
@@ -2386,7 +2468,13 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2386
2468
|
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
2387
2469
|
}
|
|
2388
2470
|
if (oversizedPdfError) throw oversizedPdfError;
|
|
2389
|
-
|
|
2471
|
+
let buffer;
|
|
2472
|
+
try {
|
|
2473
|
+
buffer = await response.body();
|
|
2474
|
+
} catch (bodyErr) {
|
|
2475
|
+
if (!isEvictedBodyError(bodyErr)) throw bodyErr;
|
|
2476
|
+
buffer = await refetchPdfBody(page, finalUrl, validateForWorker, signal);
|
|
2477
|
+
}
|
|
2390
2478
|
if (buffer.byteLength > MAX_PDF_SIZE) {
|
|
2391
2479
|
const sizeMB = Math.round(buffer.byteLength / 1024 / 1024);
|
|
2392
2480
|
throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
|
|
@@ -2749,7 +2837,7 @@ function logToDebugFile3(level, ...args) {
|
|
|
2749
2837
|
return String(arg);
|
|
2750
2838
|
}).join(" "))
|
|
2751
2839
|
};
|
|
2752
|
-
import("node:fs/promises").then((
|
|
2840
|
+
import("node:fs/promises").then((fsp2) => fsp2.appendFile(logFile, `${JSON.stringify(entry)}
|
|
2753
2841
|
`)).catch(() => {
|
|
2754
2842
|
});
|
|
2755
2843
|
} catch {
|
|
@@ -2888,7 +2976,7 @@ async function initBrowser() {
|
|
|
2888
2976
|
return initPromise;
|
|
2889
2977
|
}
|
|
2890
2978
|
async function acquireTaskContext() {
|
|
2891
|
-
const contextPromise = browser.newContext();
|
|
2979
|
+
const contextPromise = browser.newContext({ acceptDownloads: true });
|
|
2892
2980
|
contextPromise.catch((err) => logToDebugFile3("DEBUG", `[ThreadWorker] Background browser context rejection: ${err.message}`));
|
|
2893
2981
|
let contextTimeoutId;
|
|
2894
2982
|
let contextRaceLost = false;
|
package/docs/AGENT-SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
## Agent Skill
|
|
2
2
|
|
|
3
3
|
pi-research ships as a portable [Agent Skill](https://agentskills.io/specification)
|
|
4
|
-
so any skills-compatible coding agent
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
so any skills-compatible coding agent using the same `SKILL.md` directory model —
|
|
5
|
+
Claude, OpenAI Codex CLI and others — can run web research through the pi-research
|
|
6
|
+
software.
|
|
7
7
|
|
|
8
8
|
### Install
|
|
9
9
|
|
|
@@ -18,7 +18,7 @@ every coding agent detected on this machine:
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npm install -g @lincoln504/pi-research # the engine (puts `pi-research` on PATH)
|
|
21
|
-
pi-research skill install # link the skill into
|
|
21
|
+
pi-research skill install # link the skill into every detected agent
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
`skill install` targets only agents already set up under `$HOME`, never overwrites a
|
|
@@ -49,7 +49,7 @@ disabled"); or run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
|
|
|
49
49
|
agent
|
|
50
50
|
│ shells out (Bash / exec)
|
|
51
51
|
▼
|
|
52
|
-
run.mjs — zero-dep launcher (
|
|
52
|
+
run.mjs — zero-dep launcher (agent-skill/pi-research/scripts/)
|
|
53
53
|
│ locates the installed engine, or fails fast with guidance
|
|
54
54
|
▼
|
|
55
55
|
pi-research engine — the CLI (dist/cli.mjs)
|
|
@@ -70,19 +70,23 @@ is missing. It exposes four subcommands: `research "<query>"` (live research),
|
|
|
70
70
|
|
|
71
71
|
### Installation flow
|
|
72
72
|
|
|
73
|
-
The skill source lives at `
|
|
74
|
-
linking that directory into each agent's skills folder.
|
|
73
|
+
The skill source lives at `agent-skill/pi-research/` inside the package. Installing
|
|
74
|
+
means linking that directory into each agent's skills folder.
|
|
75
|
+
|
|
76
|
+
> The directory is deliberately **not** named `skills/`: `pi` treats a package-root
|
|
77
|
+
> `skills/` directory as one of its own resource roots and will load what it finds
|
|
78
|
+
> there, which would shadow the extension's native research tool with a slower
|
|
79
|
+
> subprocess copy of itself.
|
|
75
80
|
|
|
76
81
|
One-click (recommended). From the `pi` extension, run `/research-config` →
|
|
77
82
|
Install in External Agents. The installer:
|
|
78
83
|
|
|
79
84
|
1. Detects which target agents are present under `$HOME` — currently Claude
|
|
80
|
-
(`~/.claude/skills`),
|
|
81
|
-
|
|
82
|
-
(`~/.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
unrelated skill already in that slot.
|
|
85
|
+
(`~/.claude/skills`), OpenAI Codex CLI (`~/.codex/skills` — this path is not
|
|
86
|
+
confirmed by Codex's official docs; Codex skills support is still emerging)
|
|
87
|
+
and OpenClaw (`~/.openclaw/skills`).
|
|
88
|
+
2. Symlinks `agent-skill/pi-research/` into each present agent, never overwriting
|
|
89
|
+
an unrelated skill already in that slot.
|
|
86
90
|
3. Records what it created in a manifest, so Remove from External Agents
|
|
87
91
|
removes only its own links. Stale links are also garbage-collected on startup.
|
|
88
92
|
|
|
@@ -99,29 +103,12 @@ do exactly the same from the CLI — same agent detection, same manifest, same
|
|
|
99
103
|
never-clobber-a-foreign-skill guarantee — for people who installed the engine with
|
|
100
104
|
`npm install -g` and never open the interactive extension.
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
npm install -g @lincoln504/pi-research
|
|
110
|
-
openclaw skills install "$(npm root -g)/@lincoln504/pi-research/skills/pi-research" --global
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
OpenClaw drives the skill through its `exec` tool, gated on `node` being on `PATH`
|
|
114
|
-
(`metadata.openclaw.requires.bins`). A bare `openclaw skills install git:…` is *not*
|
|
115
|
-
used here: OpenClaw expects `SKILL.md` at a repo root, whereas this package nests it
|
|
116
|
-
under `skills/pi-research/` — install the engine from npm and point OpenClaw at that
|
|
117
|
-
folder, as above.
|
|
118
|
-
|
|
119
|
-
Cursor is intentionally excluded from the one-click flow — it has no global skills
|
|
120
|
-
directory and reads only project-level `.cursor/skills/`. Link it per project:
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
ln -s "$(npm root -g)/@lincoln504/pi-research/skills/pi-research" .cursor/skills/pi-research
|
|
124
|
-
```
|
|
106
|
+
An agent with its own skill-registration CLI can be pointed at the shipped folder
|
|
107
|
+
instead of symlinking it. Install the engine, then register
|
|
108
|
+
`$(npm root -g)/@lincoln504/pi-research/agent-skill/pi-research` with that agent —
|
|
109
|
+
it holds `SKILL.md` at its root, which is the layout such tools expect. Agents that
|
|
110
|
+
copy rather than link pick up engine upgrades on the next `skill install`, not
|
|
111
|
+
automatically.
|
|
125
112
|
|
|
126
113
|
Manual. Symlink the directory into any agent's skills folder yourself:
|
|
127
114
|
|
|
@@ -130,7 +117,6 @@ Manual. Symlink the directory into any agent's skills folder yourself:
|
|
|
130
117
|
| Claude | `~/.claude/skills/pi-research/` | `<project>/.claude/skills/pi-research/` |
|
|
131
118
|
| OpenAI Codex CLI | `~/.codex/skills/pi-research/` | `<project>/.codex/skills/pi-research/` |
|
|
132
119
|
| OpenClaw | `~/.openclaw/skills/pi-research/` | `<workspace>/skills/pi-research/` |
|
|
133
|
-
| Cursor | — (no global dir) | `<project>/.cursor/skills/pi-research/` |
|
|
134
120
|
|
|
135
121
|
### Prerequisites
|
|
136
122
|
|
|
@@ -146,6 +132,6 @@ node "<skill_dir>/scripts/run.mjs" status # verify the engine is detected
|
|
|
146
132
|

|
|
147
133
|
|
|
148
134
|
Once installed, ask the agent to research something — its skill system activates
|
|
149
|
-
pi-research automatically. The in-package readme (`
|
|
150
|
-
`
|
|
151
|
-
browsing the skill directly.
|
|
135
|
+
pi-research automatically. The in-package readme (`agent-skill/pi-research/README.md`)
|
|
136
|
+
and `agent-skill/pi-research/references/configuration.md` carry the same detail for
|
|
137
|
+
anyone browsing the skill directly.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -4,7 +4,7 @@ pi-research is a pi TUI extension for multi-agent web research. It runs inside t
|
|
|
4
4
|
process, registers its tools and commands, and manages its own browser worker pool,
|
|
5
5
|
service registry, and local knowledge store. One engine backs every front-end: besides
|
|
6
6
|
the pi extension it is exposed as a standalone CLI, a portable agent skill (the same
|
|
7
|
-
skill any skills-aware host runs
|
|
7
|
+
skill any skills-aware host runs) and a programmatic SDK
|
|
8
8
|
(`src/sdk.ts`).
|
|
9
9
|
|
|
10
10
|
```
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
Driven by log forensics rather than by the test suite: every entry below was found
|
|
11
|
+
by reading what the engine actually reported in production, and each fix was
|
|
12
|
+
re-measured against the exact URL, error line or counter that exposed it.
|
|
13
|
+
|
|
14
|
+
The starting observation was that the ERROR log had stopped meaning anything. Across
|
|
15
|
+
the retained run logs, **all 26 ERROR lines were either a condition the engine had
|
|
16
|
+
already recovered from or a routine per-URL outcome** — a blocked page, a slow site,
|
|
17
|
+
an untrusted certificate. Not one described an unrecovered engine fault. Two classes
|
|
18
|
+
account for all of them: content the scraper had already fetched and then dropped,
|
|
19
|
+
and recovered conditions reported as terminal failures.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **PDF sources the browser had already fetched were being discarded two different ways, both now recovered.** A server sending the PDF with `Content-Disposition: attachment` makes Firefox route the response to the download manager, so no document is produced and `page.goto()` rejects with "Download is starting" — the PDF branch was never reached even though the bytes had arrived. Separately, Firefox discards large navigation response bodies from its juggler-side cache, after which `response.body()` fails with "Response body … was evicted!". Both recur across the run logs, and both hit exactly the sources research most wants — vendor datasheets, standards-body documents, journal and university PDFs. The download is now read back from disk, and an evicted body is re-requested through the browser context's own request API — which carries the same cookies, headers, proxy and TLS profile as the navigation, the reason the URL reached the stealth-browser fallback at all. Verified live against the two URLs that failed: a Frontiers article PDF now yields 106k characters of text and a 13.9MB TU Delft lecture PDF 27k, both previously total failures. The re-fetch was measured before it was written: asking for the body earlier does not help, because the eviction happens when the browser drops the bytes, not when we ask for them — a first attempt built on that assumption was discarded after a direct camoufox probe disproved it (`src/infrastructure/browser/thread-worker-messaging.ts`, `src/infrastructure/browser/thread-worker-browser.ts`).
|
|
24
|
+
- **A knowledge-store write that was retried and succeeded no longer reports as a failed write.** `addDocumentsToStore` logged ERROR and incremented `knowledge_store_add_documents_total{status=error}` before rethrowing — but its caller catches exactly that throw, reconnects to a fresh embedder and retries, so the write usually completed. All 8 occurrences across the retained logs are immediately followed by the reconnect WARN — 8 of 8 recovered, none lost — and every one of them counted the same batch twice (once error, once success). The Lance commit-conflict retry fed the same path, so cross-process write contention did it too. Attempt-level accounting now stays in the attempt and outcome accounting moved to `KnowledgeStore.addDocuments`, which is the only layer that knows whether a write was actually abandoned — exactly one counter increment per call, and an ERROR only when the batch is genuinely lost (`src/knowledge/store-operations.ts`, `src/knowledge/store.ts`).
|
|
25
|
+
- **The browser leader logged every routine per-URL scrape outcome as a server fault.** `BrowserServer`'s handler classified Cloudflare challenges and pool-drain races but sent everything else to ERROR, including the HTTP 4xx/5xx, navigation timeouts and over-cap PDFs that the client side already classifies as expected. The leader saw each failure first, so the client's careful classification never prevented the ERROR — one false fault per affected URL. Both layers now share the same predicate (`src/infrastructure/browser/browser-server.ts`).
|
|
26
|
+
- **A leader handover was reported as a terminal error by the layer that was about to retry it.** `BrowserClient` logged ERROR and recorded a tracked error for any socket failure, including the ECONNREFUSED a follower gets against a leader that has already exited. `runBrowserTask` retries that within a second: 5 of 5 such lines in the log were followed immediately by the recovery WARN, and each was counted twice in the diagnostic error report — once here without context, once by the caller with it. A failure the caller will not retry still reports at ERROR, and a retryable one that exhausts its budget still surfaces upstream with URL context (`src/infrastructure/browser/browser-client.ts`).
|
|
27
|
+
- **Remote TLS, DNS and navigation-timeout failures are now classified as the per-URL outcomes they are.** `page.goto: SEC_ERROR_UNKNOWN_ISSUER` (a live university PDF host with an untrusted certificate) and `page.goto: Timeout 15000ms exceeded` reached the ERROR log and the diagnostic error count from both the leader and the scraper, though neither is actionable and the URL is already tallied as not scraped. `page.goto: Target closed` — how a crashed or force-closed browser surfaces — is deliberately excluded and still reports as a fault (`src/web-research/scraper-utils.ts`).
|
|
28
|
+
|
|
29
|
+
- **The unit suite was writing its deliberate failure fixtures into the user’s real shared log.** `unit-env.ts` redirects `PI_RESEARCH_LOG_PATH` before `src/logger.ts` is evaluated, but the global logger is built lazily and resolves its path at CONSTRUCTION — so a suite whose `beforeEach` deletes every `PI_RESEARCH_*` variable (config.test.ts did) left the first logger of the run binding to the production default instead. Measured at ~7KB per run appended to `/tmp/pi-research.log`, including a `{corrupt json` registry fixture whose ERROR line reads exactly like a live failure and cost real time during this release’s own forensic pass. The setup now constructs the logger while the redirect is still set, config.test.ts keeps the redirect out of its scrub, and a test pins the guarantee; whole-suite pollution measured at 0 bytes after the fix (`test/setup/unit.ts`, `test/unit/config.test.ts`, `test/unit/logger-test-isolation.test.ts`).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`bgutils-js` upgraded 3.2.0 → 4.0.3**, the YouTube PoToken stack's most upstream-brittle dependency and the one pin with no written rationale. v4 is a hard break: the barrel entry is gone (the package now exposes only `/botguard`, `/webpo` and `/utils` subpaths, so the previous bare import resolves to nothing), `BG.Challenge.create` became a free `getChallenge()` whose config drops `globalObj`/`identifier` and renames `fetch` to `fetchFunction`, and `BotGuardClient`'s `globalObj` became `globalObject`. All three renames fail silently rather than loudly — a wrong `globalObject` leaves the VM with no global to attach to and the minter factory returns a non-function — so each is now pinned by a test that asserts the field names v4 actually reads, and the whole flow was re-validated live: a real PoToken minted against Google's attestation endpoint and a real transcript fetched. Staying on v3 meant sitting on an unmaintained line for the one subsystem that breaks whenever YouTube changes BotGuard, with the failure mode being a silent empty transcript body (`src/youtube/potoken.ts`).
|
|
34
|
+
- **`jsdom` deliberately stays at 29.1.1.** jsdom 30 narrows its supported Node range to `^22.22.2 || ^24.15.0 || >=26.0.0`, which excludes both part of this package's own `>=22.19.0` floor and Node 25 entirely; 29's `^20.19.0 || ^22.13.0 || >=24.0.0` covers the whole supported surface. Attempted, measured against the engine ranges, reverted — the pin now has the same recorded rationale as the others (`package.json`).
|
|
35
|
+
- **Tags now produce GitHub Releases.** 44 tags existed and zero Releases, so each version's changelog entry was reachable only by reading `docs/CHANGELOG.md` in the tree. A new job extracts that version's section from the tag's own copy of the changelog and publishes it. It runs after `publish` and needs it, so a failed npm publish cannot announce a version nobody can install; it holds `contents: write` and nothing else — in particular no `id-token` — and installs nothing, so no dependency lifecycle script runs while that token is live. A version that reached npm with no changelog section fails the job rather than publishing an empty Release (`.github/workflows/release.yml`).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **A full SDK API reference.** `src/sdk.ts` exports 24 symbols; the documentation named 7. `runResearchDetailed`, `scrapeUrl`, `verifyUrl`, `exportKnowledge`, `getSessionMetrics`, `logRunErrorSummary` and the four `getLast*` telemetry accessors were all public and all undiscoverable short of reading the source. Every export now has a signature, a description and — where it differs — a note that it is reachable only through the `/sdk` subpath. A test keeps the two in sync (`docs/SDK.md`, `test/unit/sdk/sdk-docs-surface.test.ts`).
|
|
40
|
+
- **A contract test for the pi-host `ResourceLoader` adapter**, which had 0% coverage and was mocked out in the one test that imported it. This is the seam whose drift under pi 0.81.0 aborted *every* researcher session with "Research produced no report"; the defense against a repeat was a comment. The test runs the real adapter against the installed host: the registration lists the host iterates must be iterable, each resource getter must return the shape the host destructures, and the adapter must implement every public accessor the host's own loader exposes — so a future pi that extends the contract fails here instead of at runtime with a green suite (`test/unit/utils/make-resource-loader.test.ts`).
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- README no longer frames read-only operation as a trust caveat. Being unable to run commands or write anything is the correct shape for an agent whose whole job is reading untrusted web pages: prompt injection picked up mid-run has nothing to act on. The cost line also states plainly that LLM tokens are the only cost, and the page reads less in the second person throughout.
|
|
45
|
+
|
|
46
|
+
## [1.5.3] - 2026-08-22
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **The shipped agent skill moved from `skills/` to `agent-skill/` inside the package, so `pi` can no longer load it as one of its own agent skills.** `pi` reserves four package-root resource directory names (`extensions`, `skills`, `prompts`, `themes`) and convention-scans any of them a package's `pi` manifest does not explicitly declare. This package declares `extensions` only, so its root-level `skills/` was scanned and `SKILL.md` registered as a pi agent skill — a slower subprocess duplicate of the extension's own native `research` tool, plus a redundant `/skill:pi-research` command. Nothing fired on a plain install, a plain update, or `npm install`ing the package into a project (pi never scans project `node_modules`, and a settings entry written as a plain string takes the manifest-only path). The trap was `pi config`: toggling **any** pi-research resource rewrites the settings entry from a string into an object, after which pi resolves each resource type separately and falls back to the convention scan for the undeclared `skills` — permanently, since the TUI's revert-to-string branch is unreachable, and irrespective of whether the toggle *enabled* or *disabled* the extension. Renaming the directory removes the scanned path entirely, which needs no `pi.skills` manifest key and so cannot mislabel the package as a skill provider in the pi.dev gallery (`agent-skill/`, `package.json`).
|
|
51
|
+
- **Ownership detection now recognises both the current and the pre-1.5.3 skill source directories, so existing installs survive the move.** Installed links are symlinks into the package, and three of the four ownership call sites pass no expected source — they match the link destination's shape instead. Accepting only the new name would have stranded every link created before this release (reconcile refusing to re-point it, uninstall refusing to remove it, `skill status` reporting a foreign skill); accepting only the legacy name would have made each link unmanageable the moment it was successfully re-pointed, a failure that looks like a clean migration until the *next* upgrade. `reconcileSkillInstalls` runs on every CLI invocation and every extension activation, so a pre-1.5.3 link is re-pointed automatically with no user action. Both failure directions are pinned by a new regression suite, and the false-positive guard for lookalike paths (`…/pi-researcher/…`) is retained (`src/skill-install/skill-installer.ts`, `scripts/cleanup.cjs`, `test/unit/skill-install/legacy-source-dir-migration.test.ts`).
|
|
52
|
+
- The installer's `pi` harness entry pointed at `~/.pi/skills`, but pi's skill root is `~/.pi/agent/skills` — the entry had never matched anything. Corrected to the real path. It remains detection-only and is deliberately not an install target: pi already exposes research natively, so a skill there would only add a slower duplicate (`src/skill-install/skill-installer.ts`).
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **Cursor is no longer documented or defined as a harness.** It was never an install target — it exposes no personal/global skills directory, so the one-click flow could not serve it — and its only presence was manual per-project symlink instructions plus an unused entry in the harness table. Nothing installed to it, so there is no behavioural change (`src/skill-install/skill-installer.ts`, `docs/AGENT-SKILL.md`, `agent-skill/pi-research/README.md`).
|
|
57
|
+
- **OpenClaw is de-emphasised throughout the documentation**, becoming one supported agent among others rather than a separately-branded integration: its dedicated install section, per-harness prose and README callouts are gone, and it now appears only where the supported-agent set is enumerated factually. Support is unchanged — it remains a first-class install target with its `SKILL.md` preflight metadata intact, and existing installs are untouched. (Removing it from the target list was considered and rejected: uninstall and status are both target-filtered, so a formerly-supported harness would have become permanently unremovable through any user-facing command while reconcile kept maintaining the orphan.) The generic copy-install route it used is still documented, without naming a specific agent (`README.md`, `docs/`, `agent-skill/pi-research/`).
|
|
58
|
+
- **npm `keywords` trimmed from 25 to 13**, dropping the entries that blurred what pi actually loads from this package (`skills`, `agent-skills`, plus generic SEO filler: `ai-research`, `research`, `scraping`, `browser-automation`, `stealth-browser`, `camoufox`, `duckduckgo`, `llm-tools`, `web-access`, `agent`). Retained: the pi-ecosystem flags (`pi`, `pi-package` — load-bearing for pi.dev gallery inclusion — `pi-extension`, `pi-coding-agent`), the harnesses the standalone skill genuinely targets (`agent-skill`, `claude-code`, `codex`, `openclaw`), and the core function words (`web-research`, `deep-research`, `web-search`, `web-scraping`, `knowledge-store`). Cosmetic only: pi never reads npm keywords — resource loading is driven entirely by the `pi` manifest keys, and the pi.dev type chips derive from those same keys (`package.json`).
|
|
59
|
+
|
|
8
60
|
## [1.5.2] - 2026-08-22
|
|
9
61
|
|
|
10
62
|
### Fixed
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Configuration
|
|
2
2
|
|
|
3
|
-
Every front-end (the pi extension, the standalone CLI / agent skill that
|
|
3
|
+
Every front-end (the pi extension, the standalone CLI / agent skill that Claude Code
|
|
4
4
|
and other skills-aware hosts run, and the SDK) shares one configuration
|
|
5
5
|
model. This document covers the
|
|
6
6
|
settings exposed in the `/research-config` TUI first, then the complete
|
|
@@ -159,7 +159,7 @@ Diagnostics & platform
|
|
|
159
159
|
| `CAMOUFOX_INSTALL_DIR` | _(user cache)_ | camoufox-js's own variable, and the only one that can relocate the browser. Sets where pi-research looks the binary up, and is exported to the postinstall fetch and to browser workers. **Not effective on the currently pinned camoufox-js (0.10.x), which hardcodes its cache directory and honours no variable** — so relocating the browser is unsupported until that pin can advance (see ARCHITECTURE.md). |
|
|
160
160
|
| `PLAYWRIGHT_BROWSERS_PATH` | _(user cache)_ | Alias for the above, accepted for compatibility and exported onward as `CAMOUFOX_INSTALL_DIR`. Same caveat: on the pinned camoufox-js it moves only where pi-research looks, not where the download lands, so setting it reports the browser missing from the custom path after a successful install. |
|
|
161
161
|
| `XDG_CACHE_HOME` | `~/.cache` | Standard XDG variable. When set, every `~/.cache/pi-research/...` path below is rooted at `$XDG_CACHE_HOME/pi-research/...` instead. |
|
|
162
|
-
| `PI_RESEARCH_BIN` (alias `PI_RESEARCH_PATH`) | _(auto)_ | Agent-skill launcher only: explicit path to the pi-research engine binary when auto-resolution (PATH → local install → npx) should be bypassed. See [
|
|
162
|
+
| `PI_RESEARCH_BIN` (alias `PI_RESEARCH_PATH`) | _(auto)_ | Agent-skill launcher only: explicit path to the pi-research engine binary when auto-resolution (PATH → local install → npx) should be bypassed. See [agent-skill/pi-research/references/configuration.md](../agent-skill/pi-research/references/configuration.md). |
|
|
163
163
|
| `PLAYWRIGHT_INSTALL_DEPS` | _(unset)_ | Linux only. Set `true` during `npm install` to also install system libraries via `npx playwright install-deps` (same as `npm run install:system-deps`). |
|
|
164
164
|
| `PI_RESEARCH_STRICT_SETUP` | _(unset)_ | Read by the bundled `scripts/setup.cjs` during `npm install`. Set `1`/`true` to make a failed browser download fail the install instead of deferring it to first use. |
|
|
165
165
|
| `PI_RESEARCH_CONFIG_DIR_NAME` | `.pi` | Override the host config-directory name under your home dir (advanced; e.g. set to share another harness's config root). |
|
|
@@ -196,8 +196,8 @@ layered over the shared base, so they can be configured independently. Exactly t
|
|
|
196
196
|
exist:
|
|
197
197
|
|
|
198
198
|
- `~/.pi/research/pi.env` — the pi extension
|
|
199
|
-
- `~/.pi/research/cli.env` — the standalone CLI / agent skill (the surface
|
|
200
|
-
|
|
199
|
+
- `~/.pi/research/cli.env` — the standalone CLI / agent skill (the surface
|
|
200
|
+
skills-aware hosts run)
|
|
201
201
|
|
|
202
202
|
The overlay files do not exist by default; create the one you need by hand. There is
|
|
203
203
|
intentionally no `sdk.env`: the SDK is a library configured from code (see
|
package/docs/PI-EXTENSION.md
CHANGED
|
@@ -78,7 +78,7 @@ environment-variable list live in [CONFIGURATION.md](CONFIGURATION.md).
|
|
|
78
78
|
### Coding-agent skill installer
|
|
79
79
|
|
|
80
80
|
The `/research-config` menu can install the `pi-research` skill into your other
|
|
81
|
-
coding agents
|
|
81
|
+
coding agents detected on this machine so they can run web research through the
|
|
82
82
|
CLI, and remove it again — with exact, manifest-tracked cleanup. See
|
|
83
83
|
[AGENT-SKILL.md](AGENT-SKILL.md) for the full installation flow.
|
|
84
84
|
|
package/docs/SDK.md
CHANGED
|
@@ -75,15 +75,8 @@ await shutdownResearchSDK();
|
|
|
75
75
|
|
|
76
76
|
`initResearchSDK` must run before any research call. Auth resolves from
|
|
77
77
|
`options.apiKey` + `options.provider`, else `process.env.PI_RESEARCH_API_KEY` /
|
|
78
|
-
`PI_RESEARCH_PROVIDER`, else pi's `~/.pi/agent/auth.json`.
|
|
79
|
-
|
|
80
|
-
`getLastRunStats`, and `getSessionMetrics`, plus `exportKnowledge` (write the
|
|
81
|
-
knowledge store to a web-consumable JSON file) and the post-run telemetry accessors
|
|
82
|
-
`getLastRunMetrics`, `getLastRunSummary`, `getLastErrorReport`, and
|
|
83
|
-
`getLastResearcherOutcome` (planned/launched/succeeded/failed researcher counts plus
|
|
84
|
-
per-researcher failure reasons for the most recent run — lets a caller tell a thin
|
|
85
|
-
report caused by a sparse topic apart from one where most researchers failed). Both
|
|
86
|
-
`@lincoln504/pi-research` and `@lincoln504/pi-research/sdk` export these symbols.
|
|
78
|
+
`PI_RESEARCH_PROVIDER`, else pi's `~/.pi/agent/auth.json`. The five calls above are the
|
|
79
|
+
common path; [API reference](#api-reference) below lists every export with its signature.
|
|
87
80
|
|
|
88
81
|
> Concurrency: a single initialized SDK instance runs one research call at a time.
|
|
89
82
|
> Overlapping `runDeepResearch`/`runQuickResearch` calls on the same instance throw
|
|
@@ -141,7 +134,7 @@ exit code is the part you can rely on.
|
|
|
141
134
|
|
|
142
135
|
Treat any code ≥ 128 as a cancellation; `pi-research --help` lists the full set, and
|
|
143
136
|
the agent-facing contract is the exit-code table in
|
|
144
|
-
[`SKILL.md`](../
|
|
137
|
+
[`SKILL.md`](../agent-skill/pi-research/SKILL.md).
|
|
145
138
|
|
|
146
139
|
These are deliberately not the `70` runtime-error code and never carry
|
|
147
140
|
`retryable: true` — a cancel is a completed intention, not a fault to retry. The
|
|
@@ -156,6 +149,76 @@ that run, not the browser pool, LanceDB handles or worker processes.
|
|
|
156
149
|
The SDK does not write report files. Report export is a front-end concern — the pi
|
|
157
150
|
extension and the CLI / agent skill do it when `PI_RESEARCH_REPORT_EXPORT_ENABLED=true`.
|
|
158
151
|
|
|
152
|
+
### API reference
|
|
153
|
+
|
|
154
|
+
Everything below is exported from both `@lincoln504/pi-research` and
|
|
155
|
+
`@lincoln504/pi-research/sdk`, except the two marked *sdk subpath only* — the package
|
|
156
|
+
entry point deliberately does not re-publish them. Every call except `repairJson` and
|
|
157
|
+
`getSDKContainer` requires `initResearchSDK()` first and throws `SDK not initialized`
|
|
158
|
+
otherwise.
|
|
159
|
+
|
|
160
|
+
**Lifecycle**
|
|
161
|
+
|
|
162
|
+
| Export | Signature | Notes |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `initResearchSDK` | `(options?: ResearchSDKOptions) => Promise<void>` | Registers services. No-ops if already initialized; waits out an in-flight shutdown. |
|
|
165
|
+
| `shutdownResearchSDK` | `() => Promise<void>` | Required. Drains the writer queue, closes LanceDB, kills worker processes. Clears every `getLast*` accessor. |
|
|
166
|
+
| `getSDKContainer` | `() => ServiceContainer \| null` | *sdk subpath only.* Internal/test surface — the live service container, or `null` before init. Not covered by semver; the package entry point does not export it. |
|
|
167
|
+
|
|
168
|
+
**Research**
|
|
169
|
+
|
|
170
|
+
| Export | Signature | Notes |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `runDeepResearch` | `(query, options?, signal?) => Promise<string>` | Depth 1–3. Returns the Markdown report. `options` is `ResearchOptions` minus the fields the SDK owns (`ctx`, `query`, `model`, `sessionId`, `researchId`). |
|
|
173
|
+
| `runQuickResearch` | `(query, options?, signal?) => Promise<string>` | Depth 0. Same as above with `depth` fixed and therefore not accepted. |
|
|
174
|
+
| `runResearchDetailed` | `(query, options?, signal?) => Promise<ResearchRunResult>` | Same run as `runDeepResearch`, returning `{ report, sessionId, runId, metrics, stats, reports }` instead of the bare string. |
|
|
175
|
+
| `getResearchReports` | `(researchId?) => Promise<Map<string, string>>` | Per-researcher reports keyed by researcher id. Defaults to the most recent run; empty map if there has not been one. |
|
|
176
|
+
|
|
177
|
+
**Web access**
|
|
178
|
+
|
|
179
|
+
| Export | Signature | Notes |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `scrapeUrl` | `(url, signal?) => Promise<ScrapeResult>` | One URL through the full pipeline (SSRF gate → fetch or stealth browser → PDF extraction → Markdown). |
|
|
182
|
+
| `verifyUrl` | `(url, signal?) => Promise<boolean>` | Reachability only, no content returned. Resolves `false` rather than throwing on a blocked or dead URL. |
|
|
183
|
+
|
|
184
|
+
**Knowledge store**
|
|
185
|
+
|
|
186
|
+
| Export | Signature | Notes |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `searchKnowledge` | `(queries: string[], signal?) => Promise<KnowledgeSearchResult>` | `{ text, found: 'yes' \| 'maybe' \| 'no', documentsSearched, citations }`. Resolves `found: 'no'` when the store is disabled, empty, or unavailable — it does not throw. |
|
|
189
|
+
| `exportKnowledge` | `(outputPath: string) => Promise<void>` | Writes the store to a web-consumable JSON file. |
|
|
190
|
+
|
|
191
|
+
**Post-run telemetry** — all reflect the most recent completed run, are `null` until one completes, and are cleared by `shutdownResearchSDK()`.
|
|
192
|
+
|
|
193
|
+
| Export | Signature | Notes |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `getLastRunStats` | `() => ResearchStats \| null` | Headline counts (searches, scrapes, tokens, cost) derived from the run snapshot. |
|
|
196
|
+
| `getLastRunMetrics` | `() => IMetricsSnapshot \| null` | The raw counters/gauges/histograms for that run. |
|
|
197
|
+
| `getLastRunSummary` | `() => RunSummary \| null` | `{ runId, startedAt, completedAt, durationMs, status, snapshot }`. |
|
|
198
|
+
| `getLastErrorReport` | `() => ErrorReport \| null` | Aggregated errors — totals, patterns, by-domain, by-type. Lets an unattended caller see what failed without parsing logs. |
|
|
199
|
+
| `getLastResearcherOutcome` | `() => ResearcherOutcome \| null` | `{ planned, launched, succeeded, failed, failureReasons }`. Distinguishes a thin report caused by a sparse topic from one where most researchers failed. |
|
|
200
|
+
| `getSessionMetrics` | `() => IMetricsSnapshot` | Cumulative across every run since init, rather than per-run. |
|
|
201
|
+
| `logRunErrorSummary` | `(report, depthLabel, status) => void` | *sdk subpath only.* Emits one compact, secret-redacted line for a run's tracked errors. No-op when the report is null or empty. |
|
|
202
|
+
|
|
203
|
+
**Health and utilities**
|
|
204
|
+
|
|
205
|
+
| Export | Signature | Notes |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `getResearchHealth` | `(opts?: { force?: boolean }) => Promise<HealthReport>` | Runs every registered check. `force` bypasses the cached result. |
|
|
208
|
+
| `repairJson` | `(json: string) => string` | Repairs truncated/malformed model JSON. Pure; usable before init. |
|
|
209
|
+
|
|
210
|
+
**Types** — `ResearchSDKOptions`, `ResearchRunResult`, `KnowledgeSearchResult`, `ResearcherOutcome`,
|
|
211
|
+
`ResearchOptions`, `ResearchObserver`, `IMetricsSnapshot`, `IMetricHistogram`, `RunSummary`, and
|
|
212
|
+
`ResearchStats`.
|
|
213
|
+
|
|
214
|
+
**Also on the package entry point**, for callers that want the orchestrators directly rather
|
|
215
|
+
than through the SDK wrapper: `DeepResearchOrchestrator`, `QuickResearchOrchestrator`,
|
|
216
|
+
`HeadlessObserver` (an observer that prints progress to stdout), `ServiceNames`,
|
|
217
|
+
`shutdownManager`, `extractRunStats`, `normalizeUrl`, the config accessors
|
|
218
|
+
`getConfig` / `setConfig` / `resetConfig` / `validateConfig`, and the team-size constants.
|
|
219
|
+
These are lower-level than the SDK functions above and assume you manage the service
|
|
220
|
+
container yourself.
|
|
221
|
+
|
|
159
222
|
### Init options
|
|
160
223
|
|
|
161
224
|
| Option | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Web research for your agents with smart and safe tooling + knowledge store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"dist/thread-worker.mjs",
|
|
21
21
|
"dist/webgpu-probe.mjs",
|
|
22
22
|
"dist/prompts/",
|
|
23
|
-
"
|
|
24
|
-
"!
|
|
23
|
+
"agent-skill/",
|
|
24
|
+
"!agent-skill/pi-research/scripts/run.ts",
|
|
25
25
|
"!src/.env",
|
|
26
26
|
"!src/.env.local",
|
|
27
27
|
"!src/.env.*.local",
|
|
@@ -53,27 +53,15 @@
|
|
|
53
53
|
"pi-package",
|
|
54
54
|
"pi-extension",
|
|
55
55
|
"pi-coding-agent",
|
|
56
|
+
"agent-skill",
|
|
56
57
|
"claude-code",
|
|
57
|
-
"openclaw",
|
|
58
58
|
"codex",
|
|
59
|
-
"
|
|
60
|
-
"agent-skill",
|
|
61
|
-
"skills",
|
|
62
|
-
"web-search",
|
|
59
|
+
"openclaw",
|
|
63
60
|
"web-research",
|
|
64
61
|
"deep-research",
|
|
65
|
-
"
|
|
66
|
-
"research",
|
|
62
|
+
"web-search",
|
|
67
63
|
"web-scraping",
|
|
68
|
-
"
|
|
69
|
-
"browser-automation",
|
|
70
|
-
"stealth-browser",
|
|
71
|
-
"camoufox",
|
|
72
|
-
"duckduckgo",
|
|
73
|
-
"knowledge-store",
|
|
74
|
-
"llm-tools",
|
|
75
|
-
"web-access",
|
|
76
|
-
"agent"
|
|
64
|
+
"knowledge-store"
|
|
77
65
|
],
|
|
78
66
|
"author": "Lincoln Deen",
|
|
79
67
|
"contributors": [
|
|
@@ -105,7 +93,7 @@
|
|
|
105
93
|
"build": "node scripts/build.cjs all",
|
|
106
94
|
"prepublishOnly": "node scripts/build.cjs all && node scripts/verify-package.cjs manifest",
|
|
107
95
|
"prepare": "node scripts/prepare.cjs",
|
|
108
|
-
"version": "node scripts/sync-skill-version.cjs && git add
|
|
96
|
+
"version": "node scripts/sync-skill-version.cjs && git add agent-skill/pi-research/SKILL.md",
|
|
109
97
|
"postinstall": "node scripts/setup.cjs",
|
|
110
98
|
"preuninstall": "node scripts/cleanup.cjs",
|
|
111
99
|
"cleanup": "node scripts/cleanup.cjs",
|
|
@@ -130,7 +118,7 @@
|
|
|
130
118
|
"@kreuzberg/html-to-markdown-node": "^3.5.5",
|
|
131
119
|
"@lancedb/lancedb": "0.37.1",
|
|
132
120
|
"apache-arrow": "21.1.0",
|
|
133
|
-
"bgutils-js": "^
|
|
121
|
+
"bgutils-js": "^4.0.3",
|
|
134
122
|
"camoufox-js": "^0.10.2",
|
|
135
123
|
"esbuild": "^0.28.1",
|
|
136
124
|
"impit": "0.13.0",
|
package/scripts/build.cjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Targets (dispatch by argv, or build all when no/`all` arg):
|
|
13
13
|
* worker -> src/infrastructure/browser/thread-worker.mjs (browser worker)
|
|
14
14
|
* cli -> dist/cli.mjs (+ dist/prompts, dist/thread-worker.mjs, dist/webgpu-probe.mjs)
|
|
15
|
-
* skill ->
|
|
15
|
+
* skill -> agent-skill/pi-research/scripts/run.mjs (skill launcher, shebang)
|
|
16
16
|
*
|
|
17
17
|
* Note: `packages: 'external'` already externalizes every bare/`node:` import,
|
|
18
18
|
* so the old `--external:node:*` flag was redundant — output is identical.
|
|
@@ -114,10 +114,10 @@ const TARGETS = {
|
|
|
114
114
|
copyPrompts();
|
|
115
115
|
},
|
|
116
116
|
skill: () => {
|
|
117
|
-
const entry = p('
|
|
118
|
-
const outfile = p('
|
|
117
|
+
const entry = p('agent-skill', 'pi-research', 'scripts', 'run.ts');
|
|
118
|
+
const outfile = p('agent-skill', 'pi-research', 'scripts', 'run.mjs');
|
|
119
119
|
// The published package ships the BUILT run.mjs and deliberately excludes its
|
|
120
|
-
// run.ts source (package.json "files": "!
|
|
120
|
+
// run.ts source (package.json "files": "!agent-skill/pi-research/scripts/run.ts"),
|
|
121
121
|
// yet it also ships prepare.cjs + build.cjs — and `prepare` runs `build.cjs all`.
|
|
122
122
|
// On any install that executes prepare against the shipped tree (npm install
|
|
123
123
|
// <dir>, file:, npm link, pi install <dir>) this target had no entry point and
|