@lincoln504/pi-research 1.0.0 → 1.0.2
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 +2 -17
- package/dist/cli.mjs +180 -5
- package/docs/AGENT-SKILL.md +29 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +1 -1
- package/docs/KNOWLEDGE-STORE.md +4 -6
- package/docs/SDK.md +9 -0
- package/docs/THIRD-PARTY-NOTICES.md +7 -5
- package/package.json +1 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +151 -0
- package/src/orchestration/quick-research-orchestrator.ts +5 -1
- package/src/orchestration/researcher-executor.ts +23 -4
package/README.md
CHANGED
|
@@ -52,36 +52,21 @@ Ask in natural language — the tool understands the depth needed:
|
|
|
52
52
|
- An LLM with a 100k+ context window (bring your own key)
|
|
53
53
|
- Internet access
|
|
54
54
|
- A residential IP address — search, scraping, and YouTube transcripts all rely on a residential connection. A datacenter/VPS/cloud IP gets bot-blocked by the providers these features depend on.
|
|
55
|
-
- The pi runtime the engine builds on — `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui`. The pi extension uses the host's copies; the standalone CLI / agent skill install them as dependencies
|
|
55
|
+
- The pi runtime the engine builds on — `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui`. The pi extension uses the host's copies; the standalone CLI / agent skill install them as dependencies.
|
|
56
56
|
- Cloudflare and similar anti-automation systems block scraping on some sites, so runs will identify sources they cannot reach. pi-research compensates with volume: the search tool rapidly pulls a large set of results off free DuckDuckGo, giving the model a wide selection of reachable content to cite.
|
|
57
57
|
|
|
58
58
|
### Install
|
|
59
59
|
|
|
60
|
-
Install the front-end you want.
|
|
61
|
-
|
|
62
|
-
pi extension
|
|
63
|
-
|
|
64
60
|
```bash
|
|
65
61
|
pi install npm:@lincoln504/pi-research
|
|
66
62
|
```
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm install -g @lincoln504/pi-research
|
|
72
|
-
```
|
|
64
|
+
See [Agent skill](docs/AGENT-SKILL.md) or [SDK](docs/SDK.md) for standalone (no pi extension) installation.
|
|
73
65
|
|
|
74
66
|
Then configure a model and key — set `PI_RESEARCH_MODEL` (e.g. `openai/gpt-4o`) plus `PI_RESEARCH_API_KEY` and `PI_RESEARCH_PROVIDER`, either as environment variables or in `~/.pi/research/config.env`. See [Configuration](docs/CONFIGURATION.md). (The pi extension needs no key — it uses pi's own auth.)
|
|
75
67
|
|
|
76
|
-
See [Agent skill](docs/AGENT-SKILL.md) for details.
|
|
77
|
-
|
|
78
68
|
The first install pulls the stealth browser engine, which takes a few minutes.
|
|
79
69
|
|
|
80
|
-
On Windows, run the CLI from `cmd` or use `pi-research.cmd` — stock PowerShell
|
|
81
|
-
execution policy (`Restricted`) blocks npm's `.ps1` shims with a "running
|
|
82
|
-
scripts is disabled" error. Alternatively run
|
|
83
|
-
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
|
|
84
|
-
|
|
85
70
|
### Stability (v1.x.x)
|
|
86
71
|
|
|
87
72
|
v0.1.13 (April 2026) was the last release before a rebuild against current pi APIs
|
package/dist/cli.mjs
CHANGED
|
@@ -20166,7 +20166,8 @@ ${prompt}`);
|
|
|
20166
20166
|
const msg2 = `Researcher ${id} (${researcherConfig.name}) timed out after ${config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
20167
20167
|
session.abort().catch((err) => {
|
|
20168
20168
|
logger.warn("[ResearcherExecutor] Failed to abort timed-out researcher session:", err);
|
|
20169
|
-
})
|
|
20169
|
+
});
|
|
20170
|
+
reject(new Error(msg2));
|
|
20170
20171
|
}, config.RESEARCHER_TIMEOUT_MS);
|
|
20171
20172
|
});
|
|
20172
20173
|
let abortCleanup;
|
|
@@ -20248,9 +20249,18 @@ ${responseText}`);
|
|
|
20248
20249
|
}
|
|
20249
20250
|
} finally {
|
|
20250
20251
|
subscription();
|
|
20251
|
-
await
|
|
20252
|
-
|
|
20253
|
-
|
|
20252
|
+
await Promise.race([
|
|
20253
|
+
session.abort().catch((err) => {
|
|
20254
|
+
logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
|
|
20255
|
+
}),
|
|
20256
|
+
new Promise((resolve8) => {
|
|
20257
|
+
const t = setTimeout(() => {
|
|
20258
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} session abort did not settle within 10s; continuing cleanup`);
|
|
20259
|
+
resolve8();
|
|
20260
|
+
}, 1e4);
|
|
20261
|
+
t.unref?.();
|
|
20262
|
+
})
|
|
20263
|
+
]);
|
|
20254
20264
|
sessionService?.unregisterSession(researchId, id);
|
|
20255
20265
|
if (ctx.mode !== "tui" && ctx.hasUI && typeof ctx.ui.setHiddenThinkingLabel === "function") {
|
|
20256
20266
|
ctx.ui.setHiddenThinkingLabel();
|
|
@@ -20526,7 +20536,8 @@ ${prompt}`);
|
|
|
20526
20536
|
const msg2 = `Quick research timed out after ${this.config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
20527
20537
|
session.abort().catch((err) => {
|
|
20528
20538
|
logger.warn("[QuickOrchestrator] Failed to abort timed-out session:", err);
|
|
20529
|
-
})
|
|
20539
|
+
});
|
|
20540
|
+
reject(new Error(msg2));
|
|
20530
20541
|
}, this.config.RESEARCHER_TIMEOUT_MS);
|
|
20531
20542
|
});
|
|
20532
20543
|
let abortCleanup;
|
|
@@ -22858,6 +22869,135 @@ change the machine-wide default: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> in
|
|
|
22858
22869
|
);
|
|
22859
22870
|
return EXIT.OK;
|
|
22860
22871
|
}
|
|
22872
|
+
async function cmdSkill(s) {
|
|
22873
|
+
const {
|
|
22874
|
+
installSkill: installSkill2,
|
|
22875
|
+
uninstallSkill: uninstallSkill2,
|
|
22876
|
+
skillInstallCandidates: skillInstallCandidates2,
|
|
22877
|
+
skillUninstallCandidates: skillUninstallCandidates2,
|
|
22878
|
+
detectHarnesses: detectHarnesses2,
|
|
22879
|
+
SKILL_AGENT_TARGETS: SKILL_AGENT_TARGETS2
|
|
22880
|
+
} = await Promise.resolve().then(() => (init_skill_installer(), skill_installer_exports));
|
|
22881
|
+
const isTarget2 = (id) => SKILL_AGENT_TARGETS2.includes(id);
|
|
22882
|
+
if (s.action === "status") {
|
|
22883
|
+
const agents = detectHarnesses2().filter((d) => isTarget2(d.id));
|
|
22884
|
+
if (s.json) {
|
|
22885
|
+
toStdout(pretty({
|
|
22886
|
+
command: "skill",
|
|
22887
|
+
action: "status",
|
|
22888
|
+
agents: agents.map((d) => ({ id: d.id, label: d.label, present: d.present, installed: d.installed, path: d.absSkillPath }))
|
|
22889
|
+
}));
|
|
22890
|
+
return EXIT.OK;
|
|
22891
|
+
}
|
|
22892
|
+
toStdout(`pi-research agent skill \u2014 install status
|
|
22893
|
+
|
|
22894
|
+
`);
|
|
22895
|
+
for (const d of agents) {
|
|
22896
|
+
const state = !d.present ? "agent not detected" : d.installed === "none" ? "not installed" : d.installed === "foreign" ? "a different skill occupies this slot \u2014 left untouched" : `installed (${d.installed === "owned-copy" ? "copy" : "symlink"})`;
|
|
22897
|
+
const loc = d.installed === "owned-symlink" || d.installed === "owned-copy" ? ` \u2192 ${d.absSkillPath}` : "";
|
|
22898
|
+
toStdout(` ${d.label.padEnd(18)} ${state}${loc}
|
|
22899
|
+
`);
|
|
22900
|
+
}
|
|
22901
|
+
toStdout(`
|
|
22902
|
+
install: ${BINARY_NAME} skill install
|
|
22903
|
+
uninstall: ${BINARY_NAME} skill uninstall
|
|
22904
|
+
`);
|
|
22905
|
+
return EXIT.OK;
|
|
22906
|
+
}
|
|
22907
|
+
if (s.action === "install") {
|
|
22908
|
+
const candidates2 = skillInstallCandidates2();
|
|
22909
|
+
if (candidates2.length === 0) {
|
|
22910
|
+
const note = "No supported coding agents detected under $HOME (looked for Claude, OpenAI Codex CLI, OpenClaw). Set one up first, then re-run.";
|
|
22911
|
+
if (s.json) {
|
|
22912
|
+
toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: [], note }));
|
|
22913
|
+
return EXIT.OK;
|
|
22914
|
+
}
|
|
22915
|
+
toStderr(`
|
|
22916
|
+
[pi-research] ${note}
|
|
22917
|
+
`);
|
|
22918
|
+
return EXIT.OK;
|
|
22919
|
+
}
|
|
22920
|
+
let results2;
|
|
22921
|
+
try {
|
|
22922
|
+
results2 = installSkill2(candidates2.map((d) => d.id), { dryRun: s.dryRun, copy: s.copy });
|
|
22923
|
+
} catch (err) {
|
|
22924
|
+
return reportError(err, "skill install", s.json);
|
|
22925
|
+
}
|
|
22926
|
+
if (s.json) {
|
|
22927
|
+
toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2 }));
|
|
22928
|
+
} else {
|
|
22929
|
+
toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "install (dry run)" : "install"}
|
|
22930
|
+
|
|
22931
|
+
`);
|
|
22932
|
+
for (const r of results2) toStdout(` ${r.tool.padEnd(10)} ${describeInstall(r)}
|
|
22933
|
+
`);
|
|
22934
|
+
toStdout(`
|
|
22935
|
+
The skill activates automatically \u2014 ask the agent to research something.
|
|
22936
|
+
`);
|
|
22937
|
+
}
|
|
22938
|
+
return results2.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
|
|
22939
|
+
}
|
|
22940
|
+
const candidates = skillUninstallCandidates2();
|
|
22941
|
+
if (candidates.length === 0) {
|
|
22942
|
+
const note = "No pi-research skill installs found to remove.";
|
|
22943
|
+
if (s.json) {
|
|
22944
|
+
toStdout(pretty({ command: "skill", action: "uninstall", dryRun: !!s.dryRun, results: [], note }));
|
|
22945
|
+
return EXIT.OK;
|
|
22946
|
+
}
|
|
22947
|
+
toStdout(`
|
|
22948
|
+
[pi-research] ${note}
|
|
22949
|
+
`);
|
|
22950
|
+
return EXIT.OK;
|
|
22951
|
+
}
|
|
22952
|
+
let results;
|
|
22953
|
+
try {
|
|
22954
|
+
results = uninstallSkill2(candidates.map((d) => d.id), { dryRun: s.dryRun });
|
|
22955
|
+
} catch (err) {
|
|
22956
|
+
return reportError(err, "skill uninstall", s.json);
|
|
22957
|
+
}
|
|
22958
|
+
if (s.json) {
|
|
22959
|
+
toStdout(pretty({ command: "skill", action: "uninstall", dryRun: !!s.dryRun, results }));
|
|
22960
|
+
} else {
|
|
22961
|
+
toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "uninstall (dry run)" : "uninstall"}
|
|
22962
|
+
|
|
22963
|
+
`);
|
|
22964
|
+
for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeUninstall(r)}
|
|
22965
|
+
`);
|
|
22966
|
+
}
|
|
22967
|
+
return results.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
|
|
22968
|
+
}
|
|
22969
|
+
function describeInstall(r) {
|
|
22970
|
+
switch (r.status) {
|
|
22971
|
+
case "installed":
|
|
22972
|
+
return `${r.type === "copy" ? "copied" : "symlinked"} \u2192 ${r.path}`;
|
|
22973
|
+
case "already-installed":
|
|
22974
|
+
return `already installed \u2192 ${r.path}`;
|
|
22975
|
+
case "planned":
|
|
22976
|
+
return `would ${r.type === "copy" ? "copy" : "symlink"} \u2192 ${r.path}`;
|
|
22977
|
+
case "skipped-foreign":
|
|
22978
|
+
return "a different skill occupies this slot \u2014 left untouched";
|
|
22979
|
+
case "error":
|
|
22980
|
+
return `failed: ${r.message ?? "unknown error"}`;
|
|
22981
|
+
default:
|
|
22982
|
+
return r.status;
|
|
22983
|
+
}
|
|
22984
|
+
}
|
|
22985
|
+
function describeUninstall(r) {
|
|
22986
|
+
switch (r.status) {
|
|
22987
|
+
case "removed":
|
|
22988
|
+
return `removed \u2192 ${r.path}`;
|
|
22989
|
+
case "planned":
|
|
22990
|
+
return `would remove \u2192 ${r.path}`;
|
|
22991
|
+
case "not-present":
|
|
22992
|
+
return "nothing installed";
|
|
22993
|
+
case "skipped-foreign":
|
|
22994
|
+
return "not ours \u2014 left untouched";
|
|
22995
|
+
case "error":
|
|
22996
|
+
return `failed: ${r.message ?? "unknown error"}`;
|
|
22997
|
+
default:
|
|
22998
|
+
return r.status;
|
|
22999
|
+
}
|
|
23000
|
+
}
|
|
22861
23001
|
function reportError(err, what, json) {
|
|
22862
23002
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
22863
23003
|
const lower = msg2.toLowerCase();
|
|
@@ -22940,6 +23080,12 @@ COMMANDS
|
|
|
22940
23080
|
--config <path> Use this config file instead of the base config.env.
|
|
22941
23081
|
--json Emit a JSON object.
|
|
22942
23082
|
|
|
23083
|
+
skill [status] Show where the agent skill is installed across your coding agents.
|
|
23084
|
+
skill install Install the skill into detected agents (Claude, Codex, OpenClaw).
|
|
23085
|
+
--copy | --dry-run | --json Copy instead of symlink; plan only; JSON output.
|
|
23086
|
+
skill uninstall Remove the skill from agents where pi-research installed it.
|
|
23087
|
+
--dry-run | --json Plan only; JSON output.
|
|
23088
|
+
|
|
22943
23089
|
help, --help, -h Show this help.
|
|
22944
23090
|
|
|
22945
23091
|
CONFIGURE
|
|
@@ -23055,6 +23201,33 @@ function parseArgs(argv) {
|
|
|
23055
23201
|
}
|
|
23056
23202
|
throw new UsageError(`unknown knowledge-config action "${action}". Use: show | set <none|project|global>.`);
|
|
23057
23203
|
}
|
|
23204
|
+
if (cmd === "skill") {
|
|
23205
|
+
let json = false;
|
|
23206
|
+
let dryRun = false;
|
|
23207
|
+
let copy = false;
|
|
23208
|
+
const positional = [];
|
|
23209
|
+
for (let i = 0; i < rest.length; i++) {
|
|
23210
|
+
const a = rest[i];
|
|
23211
|
+
if (a === "--json") json = true;
|
|
23212
|
+
else if (a === "--dry-run") dryRun = true;
|
|
23213
|
+
else if (a === "--copy") copy = true;
|
|
23214
|
+
else if (a?.startsWith("--")) {
|
|
23215
|
+
throw new UsageError(`unknown option for skill: ${a}`);
|
|
23216
|
+
} else if (a !== void 0) {
|
|
23217
|
+
positional.push(a);
|
|
23218
|
+
}
|
|
23219
|
+
}
|
|
23220
|
+
const action = positional[0] ?? "status";
|
|
23221
|
+
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
23222
|
+
throw new UsageError(`unknown skill action "${action}". Use: status | install | uninstall.`);
|
|
23223
|
+
}
|
|
23224
|
+
if (positional.length > 1) {
|
|
23225
|
+
throw new UsageError(`unexpected argument "${positional[1]}" after "skill ${action}".`);
|
|
23226
|
+
}
|
|
23227
|
+
out.command = "skill";
|
|
23228
|
+
out.skill = { action, json, dryRun, copy };
|
|
23229
|
+
return out;
|
|
23230
|
+
}
|
|
23058
23231
|
if (cmd === "research") {
|
|
23059
23232
|
const r = { query: "" };
|
|
23060
23233
|
const initialLinks = [];
|
|
@@ -23178,6 +23351,8 @@ ${buildHelp()}`);
|
|
|
23178
23351
|
return cmdKnowledge(parsed.knowledge.queries, parsed.knowledge.json);
|
|
23179
23352
|
case "knowledge-config":
|
|
23180
23353
|
return cmdKnowledgeConfig(parsed.knowledgeConfig);
|
|
23354
|
+
case "skill":
|
|
23355
|
+
return cmdSkill(parsed.skill);
|
|
23181
23356
|
case "research":
|
|
23182
23357
|
return cmdResearch(parsed.research);
|
|
23183
23358
|
default:
|
package/docs/AGENT-SKILL.md
CHANGED
|
@@ -5,6 +5,29 @@ so any skills-compatible coding agent (Claude, OpenAI Codex CLI, OpenClaw, and
|
|
|
5
5
|
others using the same `SKILL.md` directory model) can run web research through the
|
|
6
6
|
pi-research software.
|
|
7
7
|
|
|
8
|
+
### Install
|
|
9
|
+
|
|
10
|
+
Already running the `pi` extension (`pi install npm:@lincoln504/pi-research`)? You
|
|
11
|
+
already have the engine — install the skill into your other agents with
|
|
12
|
+
`/research-config` → Install in External Agents (see [Installation flow](#installation-flow))
|
|
13
|
+
and skip the rest.
|
|
14
|
+
|
|
15
|
+
For standalone use without pi, install the engine globally, then link the skill into
|
|
16
|
+
every coding agent detected on this machine:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @lincoln504/pi-research # the engine (puts `pi-research` on PATH)
|
|
20
|
+
pi-research skill install # link the skill into Claude, Codex, OpenClaw
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`skill install` targets only agents already set up under `$HOME`, never overwrites a
|
|
24
|
+
different skill in the slot, and records what it created so `pi-research skill uninstall`
|
|
25
|
+
removes exactly that. Run `pi-research skill status` to see where it is installed.
|
|
26
|
+
|
|
27
|
+
On Windows, run `pi-research` from `cmd` or use `pi-research.cmd`: PowerShell's default
|
|
28
|
+
execution policy (`Restricted`) blocks npm's `.ps1` shims ("running scripts is
|
|
29
|
+
disabled"); or run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
|
|
30
|
+
|
|
8
31
|

|
|
9
32
|
|
|
10
33
|
### How it works
|
|
@@ -16,7 +39,7 @@ agent
|
|
|
16
39
|
run.mjs — zero-dep launcher (skills/pi-research/scripts/)
|
|
17
40
|
│ locates the installed engine, or fails fast with guidance
|
|
18
41
|
▼
|
|
19
|
-
pi-research engine — the
|
|
42
|
+
pi-research engine — the CLI (dist/cli.mjs)
|
|
20
43
|
│ init → run → shutdown
|
|
21
44
|
▼
|
|
22
45
|
cited Markdown report → stdout → back to the agent
|
|
@@ -50,6 +73,11 @@ Install in External Agents. The installer:
|
|
|
50
73
|
removes only its own links. Stale links are also garbage-collected on startup and
|
|
51
74
|
on `npm uninstall`.
|
|
52
75
|
|
|
76
|
+
Standalone (no pi extension). `pi-research skill install` and `pi-research skill uninstall`
|
|
77
|
+
do exactly the same from the CLI — same agent detection, same manifest, same
|
|
78
|
+
never-clobber-a-foreign-skill guarantee — for people who installed the engine with
|
|
79
|
+
`npm install -g` and never open the interactive extension.
|
|
80
|
+
|
|
53
81
|
**OpenClaw without `pi`**
|
|
54
82
|
|
|
55
83
|
If you run OpenClaw but not the `pi` extension, register the skill with OpenClaw's
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -301,7 +301,7 @@ Browser & scraping
|
|
|
301
301
|
|
|
302
302
|
- [Camoufox](https://camoufox.com) — stealth Firefox (driven via [Playwright](https://playwright.dev)) for undetected search and scraping
|
|
303
303
|
- [poolifier](https://github.com/poolifier/poolifier) — the worker-process pool behind the browser workers
|
|
304
|
-
- [html-to-markdown](https://github.com/
|
|
304
|
+
- [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown) — converts scraped HTML to Markdown (node-html-markdown serves as the pure-JS fallback)
|
|
305
305
|
- `pdf-oxide-wasm` — PDF text extraction (Rust/WASM)
|
|
306
306
|
|
|
307
307
|
Knowledge store & embeddings
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -75,7 +75,7 @@ Research
|
|
|
75
75
|
| `PI_RESEARCH_RETRY_DELAY_MS` | `2000` | 100–10000 | Base delay between retries. |
|
|
76
76
|
| `PI_RESEARCH_WORKER_THREADS` | `4` | 1–10 | Browser worker processes. Higher = more throughput, more CPU/RAM. |
|
|
77
77
|
| `PI_RESEARCH_WORKER_CONCURRENCY` | `2` | 1–10 | Tasks per worker process. |
|
|
78
|
-
| `PI_RESEARCH_MODEL` | _(session model)_ | — | Model override for researcher sub-agents (deep and quick) and knowledge synthesis.
|
|
78
|
+
| `PI_RESEARCH_MODEL` | _(session model)_ | — | Model override for researcher sub-agents (deep and quick) and knowledge synthesis. In the pi extension the coordinator and evaluator keep using the session model; on the standalone CLI/SDK, when no `--model`/`model` option is given, this also selects the session model itself. Accepts `provider/id` or a bare model id. |
|
|
79
79
|
| `PI_RESEARCH_DISABLED_TOOLS` | _(none)_ | — | Comma-separated research tools to disable for a run (`search`, `scrape`, `security_search`, `stackexchange`, `youtube_transcript`, `grep`). Removed from every researcher's toolset and named in the coordinator/evaluator prompt. |
|
|
80
80
|
| `PI_RESEARCH_REPORT_EXPORT_ENABLED` (TUI) | `false` | — | Front-ends write a Markdown report to disk and surface its path. |
|
|
81
81
|
| `PI_RESEARCH_REPORT_EXPORT_DIR` | _(smart cwd)_ | — | Pin exported reports to a fixed directory, bypassing the cwd-relative resolution. Useful for the agent skill, which runs from the host agent's arbitrary directory. |
|
package/docs/KNOWLEDGE-STORE.md
CHANGED
|
@@ -41,15 +41,13 @@ a project-scoped setting you can change per directory:
|
|
|
41
41
|
|------|----------|
|
|
42
42
|
| `global` (default) | One store shared across every directory. A finding cached in one project is retrievable from any other. |
|
|
43
43
|
| `project` | Findings are scoped to the working directory they were created in; only that directory retrieves them. |
|
|
44
|
-
| `none` | The store is disabled —
|
|
44
|
+
| `none` | The store is disabled — nothing is read or written, the `research_knowledge_search` tool is not advertised to the agent, and `/knowledge-store` is unavailable. Re-enabling needs no restart (see [PI-EXTENSION.md](PI-EXTENSION.md) for the registration mechanics). |
|
|
45
45
|
|
|
46
46
|
Change the mode for the current directory with the `/research-config` TUI (Knowledge Mode)
|
|
47
47
|
in the pi extension, or with `pi-research knowledge-config set none|project|global` on the
|
|
48
|
-
standalone CLI
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
overrides a machine-wide `PI_RESEARCH_KNOWLEDGE_STORE_MODE` in `config.env`, and a real
|
|
52
|
-
environment variable overrides everything. The change applies on the next run — no restart.
|
|
48
|
+
standalone CLI. The setting persists to the per-directory project registry — see
|
|
49
|
+
[CONFIGURATION.md](CONFIGURATION.md) for the full precedence chain. The change applies on
|
|
50
|
+
the next run — no restart.
|
|
53
51
|
|
|
54
52
|
All scopes share one physical LanceDB directory; project vs. global rows are
|
|
55
53
|
distinguished by columns (a normalized workspace path and a global flag) and filtered
|
package/docs/SDK.md
CHANGED
|
@@ -4,6 +4,15 @@ The programmatic SDK for scripts, CI, and custom tooling. For configuration —
|
|
|
4
4
|
layering model, the TUI settings, and every environment variable — see
|
|
5
5
|
[CONFIGURATION.md](CONFIGURATION.md).
|
|
6
6
|
|
|
7
|
+
### Install
|
|
8
|
+
|
|
9
|
+
Install it as a dependency of your project so the imports resolve — even if you already
|
|
10
|
+
run the `pi` extension, which keeps its own private copy your scripts can't import:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @lincoln504/pi-research
|
|
14
|
+
```
|
|
15
|
+
|
|
7
16
|
`src/sdk.ts` is a library for scripts, CI, and custom tooling. It is configured
|
|
8
17
|
from code, not from a global overlay file — there is no `sdk.env`. It reads the
|
|
9
18
|
base `~/.pi/research/config.env` as a baseline, and everything is
|
|
@@ -6,8 +6,9 @@ license covers this project's own source code only.
|
|
|
6
6
|
This package depends on third-party software distributed under its own license
|
|
7
7
|
terms. Dependencies are resolved and installed by npm at install time (by
|
|
8
8
|
reference); they are **not** bundled into this package's published artifacts
|
|
9
|
-
(
|
|
10
|
-
|
|
9
|
+
(the esbuild-bundled entry points — `dist/cli.mjs`, `dist/thread-worker.mjs` — are
|
|
10
|
+
built with `--packages=external`, and the remaining `dist/` files are copied
|
|
11
|
+
first-party sources, so no third-party code is inlined).
|
|
11
12
|
|
|
12
13
|
### Notable dependency licensing
|
|
13
14
|
|
|
@@ -15,7 +16,7 @@ camoufox-js / Camoufox browser
|
|
|
15
16
|
|
|
16
17
|
The `camoufox-js` npm package itself is licensed under **MPL-2.0** (file-level
|
|
17
18
|
copyleft). It is resolved at install time and **not bundled** into this package's
|
|
18
|
-
artifacts (
|
|
19
|
+
artifacts (the bundled entry points are built `--packages=external`), so no MPL-covered source is
|
|
19
20
|
combined into or redistributed by this MIT package. It is used in-process only to
|
|
20
21
|
configure and launch the Camoufox (Firefox-fork) browser **binary as a separate
|
|
21
22
|
operating-system process**, which this package then drives over the Playwright/CDP
|
|
@@ -27,7 +28,7 @@ ua-parser-js
|
|
|
27
28
|
`ua-parser-js` 2.x is licensed under AGPL-3.0-or-later. To keep this MIT-licensed
|
|
28
29
|
package free of AGPL-licensed code in its dependency tree, `package.json`
|
|
29
30
|
`overrides` pins `ua-parser-js` to the `1.x` line, which is MIT-licensed. The
|
|
30
|
-
1.x and 2.x APIs are compatible for the only surface our
|
|
31
|
+
1.x and 2.x APIs are compatible for the only surface our direct dependency
|
|
31
32
|
(`camoufox-js`) uses, and the pin is behavior-neutral for this package's usage
|
|
32
33
|
(no custom `navigator.userAgent`/`fingerprint` is passed to Camoufox).
|
|
33
34
|
|
|
@@ -48,7 +49,8 @@ sharp / @img/sharp-libvips (via @huggingface/transformers)
|
|
|
48
49
|
in turn installs prebuilt `@img/sharp-libvips-*` native packages licensed under
|
|
49
50
|
**LGPL-3.0-or-later**. These are platform-specific shared libraries linked
|
|
50
51
|
**dynamically** at runtime and resolved at install time; no LGPL code is statically
|
|
51
|
-
linked into or bundled with this package (
|
|
52
|
+
linked into or bundled with this package (the bundled entry points are built
|
|
53
|
+
`--packages=external`).
|
|
52
54
|
Dynamic linkage of an unmodified LGPL library from an MIT-licensed program is
|
|
53
55
|
permitted by the LGPL. The image path is only exercised by the embedding/knowledge
|
|
54
56
|
stack; text-only research does not load it.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pi-research
|
|
3
3
|
description: "Multi-agent web research for any coding agent. Use for anything requiring current information from the internet — news, trends, product/service/company facts, statistics, people, places, events, APIs, library behavior, or 'what is X / how does X work' questions. Also use to search a local knowledge store of previously researched findings before doing live research. Powered by the pi-research engine (search, stealth scraping, security databases, Stack Exchange). Not for reading local files, running project commands, or analyzing this repository's own source."
|
|
4
4
|
license: MIT
|
|
5
|
-
metadata: { "author": "Lincoln Deen", "version": "1.0.
|
|
5
|
+
metadata: { "author": "Lincoln Deen", "version": "1.0.2", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
|
|
6
6
|
allowed-tools: Bash(node:*)
|
|
7
7
|
---
|
|
8
8
|
|
package/src/cli.ts
CHANGED
|
@@ -598,6 +598,120 @@ async function cmdKnowledgeConfig(kc: NonNullable<ParsedArgs['knowledgeConfig']>
|
|
|
598
598
|
return EXIT.OK;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// skill subcommand
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* skill subcommand: install / uninstall / inspect the pi-research Agent Skill in
|
|
607
|
+
* the coding agents on this machine (Claude, OpenAI Codex CLI, OpenClaw). This is
|
|
608
|
+
* the CLI equivalent of the pi extension's `/research-config → Install in External
|
|
609
|
+
* Agents`, for standalone users who installed the engine with `npm install -g` and
|
|
610
|
+
* never open the interactive pi extension. It drives the SAME skill-installer
|
|
611
|
+
* library the TUI uses, so the guarantees are identical: only agents whose config
|
|
612
|
+
* dir already exists are targeted, a foreign skill already in the slot is never
|
|
613
|
+
* clobbered, and every link/copy is manifest-tracked so uninstall removes exactly
|
|
614
|
+
* what was created.
|
|
615
|
+
*/
|
|
616
|
+
async function cmdSkill(s: NonNullable<ParsedArgs['skill']>): Promise<number> {
|
|
617
|
+
const {
|
|
618
|
+
installSkill, uninstallSkill, skillInstallCandidates, skillUninstallCandidates,
|
|
619
|
+
detectHarnesses, SKILL_AGENT_TARGETS,
|
|
620
|
+
} = await import('./skill-install/skill-installer.ts');
|
|
621
|
+
const isTarget = (id: string): boolean =>
|
|
622
|
+
(SKILL_AGENT_TARGETS as readonly string[]).includes(id);
|
|
623
|
+
|
|
624
|
+
if (s.action === 'status') {
|
|
625
|
+
const agents = detectHarnesses().filter((d) => isTarget(d.id));
|
|
626
|
+
if (s.json) {
|
|
627
|
+
toStdout(pretty({
|
|
628
|
+
command: 'skill', action: 'status',
|
|
629
|
+
agents: agents.map((d) => ({ id: d.id, label: d.label, present: d.present, installed: d.installed, path: d.absSkillPath })),
|
|
630
|
+
}));
|
|
631
|
+
return EXIT.OK;
|
|
632
|
+
}
|
|
633
|
+
toStdout(`pi-research agent skill — install status\n\n`);
|
|
634
|
+
for (const d of agents) {
|
|
635
|
+
const state = !d.present ? 'agent not detected'
|
|
636
|
+
: d.installed === 'none' ? 'not installed'
|
|
637
|
+
: d.installed === 'foreign' ? 'a different skill occupies this slot — left untouched'
|
|
638
|
+
: `installed (${d.installed === 'owned-copy' ? 'copy' : 'symlink'})`;
|
|
639
|
+
const loc = d.installed === 'owned-symlink' || d.installed === 'owned-copy' ? ` → ${d.absSkillPath}` : '';
|
|
640
|
+
toStdout(` ${d.label.padEnd(18)} ${state}${loc}\n`);
|
|
641
|
+
}
|
|
642
|
+
toStdout(`\ninstall: ${BINARY_NAME} skill install\nuninstall: ${BINARY_NAME} skill uninstall\n`);
|
|
643
|
+
return EXIT.OK;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (s.action === 'install') {
|
|
647
|
+
const candidates = skillInstallCandidates();
|
|
648
|
+
if (candidates.length === 0) {
|
|
649
|
+
const note = 'No supported coding agents detected under $HOME (looked for Claude, OpenAI Codex CLI, OpenClaw). Set one up first, then re-run.';
|
|
650
|
+
if (s.json) { toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results: [], note })); return EXIT.OK; }
|
|
651
|
+
toStderr(`\n[pi-research] ${note}\n`);
|
|
652
|
+
return EXIT.OK;
|
|
653
|
+
}
|
|
654
|
+
let results;
|
|
655
|
+
try {
|
|
656
|
+
results = installSkill(candidates.map((d) => d.id), { dryRun: s.dryRun, copy: s.copy });
|
|
657
|
+
} catch (err) {
|
|
658
|
+
return reportError(err, 'skill install', s.json);
|
|
659
|
+
}
|
|
660
|
+
if (s.json) {
|
|
661
|
+
toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results }));
|
|
662
|
+
} else {
|
|
663
|
+
toStdout(`pi-research agent skill — ${s.dryRun ? 'install (dry run)' : 'install'}\n\n`);
|
|
664
|
+
for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeInstall(r)}\n`);
|
|
665
|
+
toStdout(`\nThe skill activates automatically — ask the agent to research something.\n`);
|
|
666
|
+
}
|
|
667
|
+
return results.some((r) => r.status === 'error') ? EXIT.SOFTWARE : EXIT.OK;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// uninstall
|
|
671
|
+
const candidates = skillUninstallCandidates();
|
|
672
|
+
if (candidates.length === 0) {
|
|
673
|
+
const note = 'No pi-research skill installs found to remove.';
|
|
674
|
+
if (s.json) { toStdout(pretty({ command: 'skill', action: 'uninstall', dryRun: !!s.dryRun, results: [], note })); return EXIT.OK; }
|
|
675
|
+
toStdout(`\n[pi-research] ${note}\n`);
|
|
676
|
+
return EXIT.OK;
|
|
677
|
+
}
|
|
678
|
+
let results;
|
|
679
|
+
try {
|
|
680
|
+
results = uninstallSkill(candidates.map((d) => d.id), { dryRun: s.dryRun });
|
|
681
|
+
} catch (err) {
|
|
682
|
+
return reportError(err, 'skill uninstall', s.json);
|
|
683
|
+
}
|
|
684
|
+
if (s.json) {
|
|
685
|
+
toStdout(pretty({ command: 'skill', action: 'uninstall', dryRun: !!s.dryRun, results }));
|
|
686
|
+
} else {
|
|
687
|
+
toStdout(`pi-research agent skill — ${s.dryRun ? 'uninstall (dry run)' : 'uninstall'}\n\n`);
|
|
688
|
+
for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeUninstall(r)}\n`);
|
|
689
|
+
}
|
|
690
|
+
return results.some((r) => r.status === 'error') ? EXIT.SOFTWARE : EXIT.OK;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function describeInstall(r: { status: string; type?: string; path: string; message?: string }): string {
|
|
694
|
+
switch (r.status) {
|
|
695
|
+
case 'installed': return `${r.type === 'copy' ? 'copied' : 'symlinked'} → ${r.path}`;
|
|
696
|
+
case 'already-installed': return `already installed → ${r.path}`;
|
|
697
|
+
case 'planned': return `would ${r.type === 'copy' ? 'copy' : 'symlink'} → ${r.path}`;
|
|
698
|
+
case 'skipped-foreign': return 'a different skill occupies this slot — left untouched';
|
|
699
|
+
case 'error': return `failed: ${r.message ?? 'unknown error'}`;
|
|
700
|
+
default: return r.status;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function describeUninstall(r: { status: string; path: string; message?: string }): string {
|
|
705
|
+
switch (r.status) {
|
|
706
|
+
case 'removed': return `removed → ${r.path}`;
|
|
707
|
+
case 'planned': return `would remove → ${r.path}`;
|
|
708
|
+
case 'not-present': return 'nothing installed';
|
|
709
|
+
case 'skipped-foreign': return 'not ours — left untouched';
|
|
710
|
+
case 'error': return `failed: ${r.message ?? 'unknown error'}`;
|
|
711
|
+
default: return r.status;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
601
715
|
// ---------------------------------------------------------------------------
|
|
602
716
|
// Error reporting & shutdown
|
|
603
717
|
// ---------------------------------------------------------------------------
|
|
@@ -682,6 +796,7 @@ interface ParsedArgs {
|
|
|
682
796
|
knowledge?: { queries: string[]; json?: boolean };
|
|
683
797
|
knowledgeConfig?: { action: 'show' | 'set'; mode?: 'none' | 'project' | 'global'; json?: boolean };
|
|
684
798
|
status?: { json?: boolean };
|
|
799
|
+
skill?: { action: 'install' | 'uninstall' | 'status'; json?: boolean; dryRun?: boolean; copy?: boolean };
|
|
685
800
|
configPath?: string;
|
|
686
801
|
json?: boolean;
|
|
687
802
|
}
|
|
@@ -716,6 +831,12 @@ COMMANDS
|
|
|
716
831
|
--config <path> Use this config file instead of the base config.env.
|
|
717
832
|
--json Emit a JSON object.
|
|
718
833
|
|
|
834
|
+
skill [status] Show where the agent skill is installed across your coding agents.
|
|
835
|
+
skill install Install the skill into detected agents (Claude, Codex, OpenClaw).
|
|
836
|
+
--copy | --dry-run | --json Copy instead of symlink; plan only; JSON output.
|
|
837
|
+
skill uninstall Remove the skill from agents where pi-research installed it.
|
|
838
|
+
--dry-run | --json Plan only; JSON output.
|
|
839
|
+
|
|
719
840
|
help, --help, -h Show this help.
|
|
720
841
|
|
|
721
842
|
CONFIGURE
|
|
@@ -839,6 +960,34 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
839
960
|
throw new UsageError(`unknown knowledge-config action "${action}". Use: show | set <none|project|global>.`);
|
|
840
961
|
}
|
|
841
962
|
|
|
963
|
+
if (cmd === 'skill') {
|
|
964
|
+
let json = false;
|
|
965
|
+
let dryRun = false;
|
|
966
|
+
let copy = false;
|
|
967
|
+
const positional: string[] = [];
|
|
968
|
+
for (let i = 0; i < rest.length; i++) {
|
|
969
|
+
const a = rest[i];
|
|
970
|
+
if (a === '--json') json = true;
|
|
971
|
+
else if (a === '--dry-run') dryRun = true;
|
|
972
|
+
else if (a === '--copy') copy = true;
|
|
973
|
+
else if (a?.startsWith('--')) {
|
|
974
|
+
throw new UsageError(`unknown option for skill: ${a}`);
|
|
975
|
+
} else if (a !== undefined) {
|
|
976
|
+
positional.push(a);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const action = positional[0] ?? 'status';
|
|
980
|
+
if (action !== 'install' && action !== 'uninstall' && action !== 'status') {
|
|
981
|
+
throw new UsageError(`unknown skill action "${action}". Use: status | install | uninstall.`);
|
|
982
|
+
}
|
|
983
|
+
if (positional.length > 1) {
|
|
984
|
+
throw new UsageError(`unexpected argument "${positional[1]}" after "skill ${action}".`);
|
|
985
|
+
}
|
|
986
|
+
out.command = 'skill';
|
|
987
|
+
out.skill = { action, json, dryRun, copy };
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
|
|
842
991
|
if (cmd === 'research') {
|
|
843
992
|
const r: ResearchArgs = { query: '' };
|
|
844
993
|
const initialLinks: string[] = [];
|
|
@@ -1001,6 +1150,8 @@ async function main(argv: string[]): Promise<number> {
|
|
|
1001
1150
|
return cmdKnowledge(parsed.knowledge!.queries, parsed.knowledge!.json);
|
|
1002
1151
|
case 'knowledge-config':
|
|
1003
1152
|
return cmdKnowledgeConfig(parsed.knowledgeConfig!);
|
|
1153
|
+
case 'skill':
|
|
1154
|
+
return cmdSkill(parsed.skill!);
|
|
1004
1155
|
case 'research':
|
|
1005
1156
|
return cmdResearch(parsed.research!);
|
|
1006
1157
|
default:
|
|
@@ -269,9 +269,13 @@ export class QuickResearchOrchestrator {
|
|
|
269
269
|
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
270
270
|
timeoutId = setTimeout(() => {
|
|
271
271
|
const msg = `Quick research timed out after ${this.config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
272
|
+
// Reject FIRST, abort as fire-and-forget cleanup — see researcher-executor:
|
|
273
|
+
// abort() awaits the stuck in-flight request, so gating the rejection on it
|
|
274
|
+
// settling disables the timeout exactly when it is needed.
|
|
272
275
|
session.abort().catch((err) => {
|
|
273
276
|
logger.warn('[QuickOrchestrator] Failed to abort timed-out session:', err);
|
|
274
|
-
})
|
|
277
|
+
});
|
|
278
|
+
reject(new Error(msg));
|
|
275
279
|
}, this.config.RESEARCHER_TIMEOUT_MS);
|
|
276
280
|
});
|
|
277
281
|
|
|
@@ -264,9 +264,14 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
|
|
|
264
264
|
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
265
265
|
timeoutId = setTimeout(() => {
|
|
266
266
|
const msg = `Researcher ${id} (${researcherConfig.name}) timed out after ${config.RESEARCHER_TIMEOUT_MS}ms`;
|
|
267
|
+
// Reject FIRST, abort as fire-and-forget cleanup: session.abort() awaits the
|
|
268
|
+
// in-flight request, which is the very thing that is stuck (e.g. a provider
|
|
269
|
+
// retry loop against an exhausted quota) — gating the rejection on it settling
|
|
270
|
+
// makes the timeout ineffective exactly when it is needed.
|
|
267
271
|
session.abort().catch((err) => {
|
|
268
272
|
logger.warn('[ResearcherExecutor] Failed to abort timed-out researcher session:', err);
|
|
269
|
-
})
|
|
273
|
+
});
|
|
274
|
+
reject(new Error(msg));
|
|
270
275
|
}, config.RESEARCHER_TIMEOUT_MS);
|
|
271
276
|
});
|
|
272
277
|
|
|
@@ -375,9 +380,23 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
|
|
|
375
380
|
}
|
|
376
381
|
} finally {
|
|
377
382
|
subscription();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
// Bounded: after a timeout, session.abort() awaits the very in-flight request
|
|
384
|
+
// that is stuck (e.g. a provider retry loop against an exhausted quota) and may
|
|
385
|
+
// never settle — never let per-attempt cleanup hang the retry loop on it. The
|
|
386
|
+
// retry builds a fresh session; the abandoned abort keeps draining in the
|
|
387
|
+
// background and its session dies with the run's teardown.
|
|
388
|
+
await Promise.race([
|
|
389
|
+
session.abort().catch((err) => {
|
|
390
|
+
logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
|
|
391
|
+
}),
|
|
392
|
+
new Promise<void>((resolve) => {
|
|
393
|
+
const t = setTimeout(() => {
|
|
394
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} session abort did not settle within 10s; continuing cleanup`);
|
|
395
|
+
resolve();
|
|
396
|
+
}, 10_000);
|
|
397
|
+
t.unref?.();
|
|
398
|
+
}),
|
|
399
|
+
]);
|
|
381
400
|
sessionService?.unregisterSession(researchId, id);
|
|
382
401
|
|
|
383
402
|
// Restore the default thinking label now that the researcher is done.
|