@lincoln504/pi-research 1.0.0 → 1.0.1

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 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 (`@earendil-works/pi-coding-agent` is the dependency the launcher checks is resolvable, and it will tell you to reinstall if a partial install leaves it out).
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
- Standalone CLI / agent skill
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
@@ -22858,6 +22858,135 @@ change the machine-wide default: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> in
22858
22858
  );
22859
22859
  return EXIT.OK;
22860
22860
  }
22861
+ async function cmdSkill(s) {
22862
+ const {
22863
+ installSkill: installSkill2,
22864
+ uninstallSkill: uninstallSkill2,
22865
+ skillInstallCandidates: skillInstallCandidates2,
22866
+ skillUninstallCandidates: skillUninstallCandidates2,
22867
+ detectHarnesses: detectHarnesses2,
22868
+ SKILL_AGENT_TARGETS: SKILL_AGENT_TARGETS2
22869
+ } = await Promise.resolve().then(() => (init_skill_installer(), skill_installer_exports));
22870
+ const isTarget2 = (id) => SKILL_AGENT_TARGETS2.includes(id);
22871
+ if (s.action === "status") {
22872
+ const agents = detectHarnesses2().filter((d) => isTarget2(d.id));
22873
+ if (s.json) {
22874
+ toStdout(pretty({
22875
+ command: "skill",
22876
+ action: "status",
22877
+ agents: agents.map((d) => ({ id: d.id, label: d.label, present: d.present, installed: d.installed, path: d.absSkillPath }))
22878
+ }));
22879
+ return EXIT.OK;
22880
+ }
22881
+ toStdout(`pi-research agent skill \u2014 install status
22882
+
22883
+ `);
22884
+ for (const d of agents) {
22885
+ 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"})`;
22886
+ const loc = d.installed === "owned-symlink" || d.installed === "owned-copy" ? ` \u2192 ${d.absSkillPath}` : "";
22887
+ toStdout(` ${d.label.padEnd(18)} ${state}${loc}
22888
+ `);
22889
+ }
22890
+ toStdout(`
22891
+ install: ${BINARY_NAME} skill install
22892
+ uninstall: ${BINARY_NAME} skill uninstall
22893
+ `);
22894
+ return EXIT.OK;
22895
+ }
22896
+ if (s.action === "install") {
22897
+ const candidates2 = skillInstallCandidates2();
22898
+ if (candidates2.length === 0) {
22899
+ const note = "No supported coding agents detected under $HOME (looked for Claude, OpenAI Codex CLI, OpenClaw). Set one up first, then re-run.";
22900
+ if (s.json) {
22901
+ toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: [], note }));
22902
+ return EXIT.OK;
22903
+ }
22904
+ toStderr(`
22905
+ [pi-research] ${note}
22906
+ `);
22907
+ return EXIT.OK;
22908
+ }
22909
+ let results2;
22910
+ try {
22911
+ results2 = installSkill2(candidates2.map((d) => d.id), { dryRun: s.dryRun, copy: s.copy });
22912
+ } catch (err) {
22913
+ return reportError(err, "skill install", s.json);
22914
+ }
22915
+ if (s.json) {
22916
+ toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2 }));
22917
+ } else {
22918
+ toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "install (dry run)" : "install"}
22919
+
22920
+ `);
22921
+ for (const r of results2) toStdout(` ${r.tool.padEnd(10)} ${describeInstall(r)}
22922
+ `);
22923
+ toStdout(`
22924
+ The skill activates automatically \u2014 ask the agent to research something.
22925
+ `);
22926
+ }
22927
+ return results2.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
22928
+ }
22929
+ const candidates = skillUninstallCandidates2();
22930
+ if (candidates.length === 0) {
22931
+ const note = "No pi-research skill installs found to remove.";
22932
+ if (s.json) {
22933
+ toStdout(pretty({ command: "skill", action: "uninstall", dryRun: !!s.dryRun, results: [], note }));
22934
+ return EXIT.OK;
22935
+ }
22936
+ toStdout(`
22937
+ [pi-research] ${note}
22938
+ `);
22939
+ return EXIT.OK;
22940
+ }
22941
+ let results;
22942
+ try {
22943
+ results = uninstallSkill2(candidates.map((d) => d.id), { dryRun: s.dryRun });
22944
+ } catch (err) {
22945
+ return reportError(err, "skill uninstall", s.json);
22946
+ }
22947
+ if (s.json) {
22948
+ toStdout(pretty({ command: "skill", action: "uninstall", dryRun: !!s.dryRun, results }));
22949
+ } else {
22950
+ toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "uninstall (dry run)" : "uninstall"}
22951
+
22952
+ `);
22953
+ for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeUninstall(r)}
22954
+ `);
22955
+ }
22956
+ return results.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
22957
+ }
22958
+ function describeInstall(r) {
22959
+ switch (r.status) {
22960
+ case "installed":
22961
+ return `${r.type === "copy" ? "copied" : "symlinked"} \u2192 ${r.path}`;
22962
+ case "already-installed":
22963
+ return `already installed \u2192 ${r.path}`;
22964
+ case "planned":
22965
+ return `would ${r.type === "copy" ? "copy" : "symlink"} \u2192 ${r.path}`;
22966
+ case "skipped-foreign":
22967
+ return "a different skill occupies this slot \u2014 left untouched";
22968
+ case "error":
22969
+ return `failed: ${r.message ?? "unknown error"}`;
22970
+ default:
22971
+ return r.status;
22972
+ }
22973
+ }
22974
+ function describeUninstall(r) {
22975
+ switch (r.status) {
22976
+ case "removed":
22977
+ return `removed \u2192 ${r.path}`;
22978
+ case "planned":
22979
+ return `would remove \u2192 ${r.path}`;
22980
+ case "not-present":
22981
+ return "nothing installed";
22982
+ case "skipped-foreign":
22983
+ return "not ours \u2014 left untouched";
22984
+ case "error":
22985
+ return `failed: ${r.message ?? "unknown error"}`;
22986
+ default:
22987
+ return r.status;
22988
+ }
22989
+ }
22861
22990
  function reportError(err, what, json) {
22862
22991
  const msg2 = err instanceof Error ? err.message : String(err);
22863
22992
  const lower = msg2.toLowerCase();
@@ -22940,6 +23069,12 @@ COMMANDS
22940
23069
  --config <path> Use this config file instead of the base config.env.
22941
23070
  --json Emit a JSON object.
22942
23071
 
23072
+ skill [status] Show where the agent skill is installed across your coding agents.
23073
+ skill install Install the skill into detected agents (Claude, Codex, OpenClaw).
23074
+ --copy | --dry-run | --json Copy instead of symlink; plan only; JSON output.
23075
+ skill uninstall Remove the skill from agents where pi-research installed it.
23076
+ --dry-run | --json Plan only; JSON output.
23077
+
22943
23078
  help, --help, -h Show this help.
22944
23079
 
22945
23080
  CONFIGURE
@@ -23055,6 +23190,33 @@ function parseArgs(argv) {
23055
23190
  }
23056
23191
  throw new UsageError(`unknown knowledge-config action "${action}". Use: show | set <none|project|global>.`);
23057
23192
  }
23193
+ if (cmd === "skill") {
23194
+ let json = false;
23195
+ let dryRun = false;
23196
+ let copy = false;
23197
+ const positional = [];
23198
+ for (let i = 0; i < rest.length; i++) {
23199
+ const a = rest[i];
23200
+ if (a === "--json") json = true;
23201
+ else if (a === "--dry-run") dryRun = true;
23202
+ else if (a === "--copy") copy = true;
23203
+ else if (a?.startsWith("--")) {
23204
+ throw new UsageError(`unknown option for skill: ${a}`);
23205
+ } else if (a !== void 0) {
23206
+ positional.push(a);
23207
+ }
23208
+ }
23209
+ const action = positional[0] ?? "status";
23210
+ if (action !== "install" && action !== "uninstall" && action !== "status") {
23211
+ throw new UsageError(`unknown skill action "${action}". Use: status | install | uninstall.`);
23212
+ }
23213
+ if (positional.length > 1) {
23214
+ throw new UsageError(`unexpected argument "${positional[1]}" after "skill ${action}".`);
23215
+ }
23216
+ out.command = "skill";
23217
+ out.skill = { action, json, dryRun, copy };
23218
+ return out;
23219
+ }
23058
23220
  if (cmd === "research") {
23059
23221
  const r = { query: "" };
23060
23222
  const initialLinks = [];
@@ -23178,6 +23340,8 @@ ${buildHelp()}`);
23178
23340
  return cmdKnowledge(parsed.knowledge.queries, parsed.knowledge.json);
23179
23341
  case "knowledge-config":
23180
23342
  return cmdKnowledgeConfig(parsed.knowledgeConfig);
23343
+ case "skill":
23344
+ return cmdSkill(parsed.skill);
23181
23345
  case "research":
23182
23346
  return cmdResearch(parsed.research);
23183
23347
  default:
@@ -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
  ![Installing the research skill into external agents](media/05-agent-skill.gif)
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 SDK (dist/cli.mjs)
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
@@ -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/Goldziher/html-to-markdown) & [node-html-markdown](https://github.com/crosstype/node-html-markdown) — convert scraped HTML to Markdown
304
+ - [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown) & [node-html-markdown](https://github.com/crosstype/node-html-markdown) — convert scraped HTML to Markdown
305
305
  - `pdf-oxide-wasm` — PDF text extraction (Rust/WASM)
306
306
 
307
307
  Knowledge store & embeddings
@@ -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. The coordinator and evaluator keep using the session model. Accepts `provider/id` or a bare model id. |
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. |
@@ -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 — no data is read or written. The `research_knowledge_search` tool stays registered (so re-enabling the mode later needs no pi restart) but is not advertised to the agent: its prompt guidance is stripped, and any direct call returns a "knowledge store is disabled" result. The `/knowledge-store` command is unavailable. |
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 (`pi-research knowledge-config` shows the current mode and its source). Under
49
- the agent skill you don't have to run anything — you ask the agent to change it and it runs that
50
- command for you. All persist to the per-directory project registry; a per-directory value
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
- (`dist/*` is built with esbuild `--packages=external`, so no third-party code is
10
- inlined).
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 (`dist/*` is built `--packages=external`), so no MPL-covered source is
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 transitive dependency
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 (`dist/*` is built `--packages=external`).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A set of web research tools (search, scrape, and others) and research agent system that uses them safely and efficiently.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -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.0", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.0.1", "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: