@lincoln504/pi-research 1.7.3 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,11 +19,11 @@
19
19
  ·
20
20
  <a href="docs/ARCHITECTURE.md">Architecture</a>
21
21
  ·
22
- <a href="docs/DOCUMENTATION-ES.md">ES</a>
22
+ <a href="https://github.com/Lincoln504/pi-research/blob/main/docs/DOCUMENTATION-ES.md">ES</a>
23
23
  ·
24
- <a href="docs/DOCUMENTATION-ZH.md">中文研究</a>
24
+ <a href="https://github.com/Lincoln504/pi-research/blob/main/docs/DOCUMENTATION-ZH.md">中文研究</a>
25
25
  ·
26
- <a href="docs/DOCUMENTATION-JA.md">日本語研究</a>
26
+ <a href="https://github.com/Lincoln504/pi-research/blob/main/docs/DOCUMENTATION-JA.md">日本語研究</a>
27
27
  </p>
28
28
 
29
29
  ---
@@ -49,7 +49,7 @@ npm install -g @lincoln504/pi-research
49
49
  pi-research skill install
50
50
  ```
51
51
 
52
- No extra setup is needed. The package ships ready-made bindings for every platform and runs no install scripts. The stealth browser (1GB+ installed) downloads on first use, so the first scrape takes a few minutes. npm ≥11.19 skips install scripts by default. Leave it that way: nothing needs approving or building. Only Windows with npm older than 11.19 can fail when the install tries to compile from source. Upgrading npm fixes it.
52
+ No extra setup is needed. The package ships ready-made bindings for every platform and declares no build step, so there is nothing to approve or compile. The stealth browser (1GB+ installed) is fetched by the `postinstall` script when npm runs install scripts, which it does by default up to npm 11.18 — that is the bundled npm on the Node 22.22.2 floor this package requires, so expect the fetch during install. Newer npm (11.19+) skips install scripts by default, and the browser is then fetched lazily on the first scrape instead. Either way the first scrape takes a few minutes until it is in place. Set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` to defer the fetch deliberately. Only Windows with npm older than 11.19 can fail when the install tries to compile from source. Upgrading npm fixes it.
53
53
 
54
54
  The `@earendil-works/pi-*` host packages are **peer dependencies**. pi supplies its own copies when it loads this as an extension, and npm ≥7 installs them automatically for a standalone (global or project) install. An install that skips peers does not get them: `npm install --legacy-peer-deps`, yarn classic, and `pi install` itself (it passes that flag on purpose) are all in that group. The standalone engine and the agent-skill launcher then stop with a message naming the missing package; add it with:
55
55
 
@@ -63,7 +63,7 @@ In pi, the extension works out of the box on the session's model and pi's config
63
63
 
64
64
  ### Uninstall
65
65
 
66
- `pi remove npm:@lincoln504/pi-research` removes the extension. `npm uninstall -g @lincoln504/pi-research` removes the standalone engine. npm 7+ no longer runs `preuninstall`, so nothing else is removed on its own: skill links into other agents (Claude Code, Codex, …), the state directory (`~/.pi/research/state`), and the cache (`~/.cache/pi-research`, including any downloaded embedding models) stay in place. Remove the skill links first with `pi-research skill uninstall` (or `/research-config` → Remove from External Agents). The shared stealth-browser cache (`~/.cache/camoufox`) is preserved unless `PI_RESEARCH_PURGE_BROWSERS=1`. See [AGENT-SKILL.md](docs/AGENT-SKILL.md#installation-flow) for the full picture.
66
+ `pi remove npm:@lincoln504/pi-research` removes the extension. `npm uninstall -g @lincoln504/pi-research` removes the standalone engine. npm 7+ no longer runs `preuninstall`, so nothing else is removed on its own: skill links into other agents (Claude Code, Codex, …), the state directory (`~/.pi/research/state`), and the cache (`~/.cache/pi-research`, including any downloaded embedding models) stay in place. Remove the skill links first with `pi-research skill uninstall` (or `/research-config` → Remove from External Agents), then remove the rest with `pi-research cleanup`: The shared stealth-browser cache (`~/.cache/camoufox`) is preserved unless `PI_RESEARCH_PURGE_BROWSERS=1`. See [AGENT-SKILL.md](docs/AGENT-SKILL.md#installation-flow) for the full picture.
67
67
 
68
68
  ### How it works
69
69
 
@@ -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.7.3", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.7.5", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
6
6
  allowed-tools: Bash(node:*)
7
7
  ---
8
8
 
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The installed `pi-research` bin.
4
+ *
5
+ * WHY THIS SHIM EXISTS INSTEAD OF POINTING `bin` STRAIGHT AT dist/cli.mjs
6
+ *
7
+ * dist/cli.mjs statically imports the `@earendil-works/pi-*` host packages, which
8
+ * are declared as PEER dependencies: pi supplies its own copies when it loads
9
+ * this package as an extension, and npm >= 7 resolves them automatically for a
10
+ * standalone (global or project) install. An install that skips peers leaves
11
+ * them absent — `npm install --legacy-peer-deps`, yarn classic, and a pnpm
12
+ * config with `auto-install-peers=false` all do that — and Node then dies at
13
+ * module load with a raw `ERR_MODULE_NOT_FOUND` stack before any of our code
14
+ * runs. The user sees an internal Node error instead of the one-line fix.
15
+ *
16
+ * The preflight cannot live inside the bundle: esbuild hoists the bundle's
17
+ * external `import` declarations to the very top of the output file, ahead of
18
+ * every statement in the program, so anything the bundle body does runs AFTER
19
+ * the failing import has already thrown. (Verified: a wrapper entry point whose
20
+ * first statement is the preflight still crashes, because the emitted file
21
+ * begins with `import "…/pi-ai";`.) It has to be a separate module, which is
22
+ * why this file is shipped unbundled and why `bin` points at it.
23
+ *
24
+ * The check uses `import.meta.resolve()`, NOT `createRequire().resolve()`: both
25
+ * host packages are ESM-only (`exports` maps with no `require` condition), so
26
+ * the CJS resolver fails on them with `ERR_PACKAGE_PATH_NOT_EXPORTED` even when
27
+ * they are correctly installed — a false negative that would break every
28
+ * healthy install. `import.meta.resolve()` uses the same ESM resolution the
29
+ * bundle itself uses, returns when the package is present, and throws
30
+ * `ERR_MODULE_NOT_FOUND` when it is not.
31
+ *
32
+ * Exit code 78 (EXIT.CONFIG) and the remedy text match the agent-skill
33
+ * launcher's `depMissing()` — one failure, one message, whichever entry point
34
+ * the user reached for.
35
+ */
36
+
37
+ import { fileURLToPath } from 'node:url';
38
+
39
+ const REQUIRED_PEERS = ['@earendil-works/pi-ai', '@earendil-works/pi-coding-agent'];
40
+
41
+ const missing = REQUIRED_PEERS.filter((name) => {
42
+ try {
43
+ import.meta.resolve(name);
44
+ return false;
45
+ } catch {
46
+ return true;
47
+ }
48
+ });
49
+
50
+ if (missing.length > 0) {
51
+ const lines = [
52
+ '',
53
+ 'Error: pi-research cannot start — it is missing required runtime dependencies:',
54
+ ...missing.map((name) => ` ${name}`),
55
+ '',
56
+ 'These are PEER dependencies. pi provides its own copies when it loads this',
57
+ 'package as an extension, and npm >= 7 installs them automatically for a',
58
+ 'standalone (global or project) install. This install skipped them, which is',
59
+ 'what --legacy-peer-deps, yarn classic, and a pnpm config with',
60
+ 'auto-install-peers=false do.',
61
+ '',
62
+ 'Either of these fixes it:',
63
+ '',
64
+ ' npm install -g @earendil-works/pi-coding-agent # add just the missing host package',
65
+ ' npm install -g @lincoln504/pi-research # or reinstall, resolving peers',
66
+ '',
67
+ ];
68
+ process.stderr.write(lines.join('\n'));
69
+ process.exit(78);
70
+ }
71
+
72
+ // dist/cli.mjs decides whether to run by comparing its own module URL against
73
+ // process.argv[1]. Because this shim is what the OS (or npm's Windows .cmd shim)
74
+ // actually invokes, argv[1] is THIS file; without the rewrite below that check
75
+ // would be false and the CLI would exit 0 having done nothing at all. Point it
76
+ // at the real entry point before importing it, so both sides resolve to the
77
+ // same realpath.
78
+ //
79
+ // fileURLToPath, not URL.pathname: on Windows the latter yields
80
+ // `/C:/…/cli.mjs`, which is not a path any filesystem call accepts.
81
+ process.argv[1] = fileURLToPath(new URL('./cli.mjs', import.meta.url));
82
+
83
+ await import('./cli.mjs');
package/dist/cli.mjs CHANGED
@@ -11122,6 +11122,7 @@ var init_skill_installer = __esm({
11122
11122
  import * as fs17 from "node:fs";
11123
11123
  import * as os7 from "node:os";
11124
11124
  import * as path19 from "node:path";
11125
+ import { spawnSync } from "node:child_process";
11125
11126
  import { fileURLToPath as fileURLToPath6 } from "node:url";
11126
11127
 
11127
11128
  // src/core/service-initialization.ts
@@ -27790,6 +27791,30 @@ change the machine-wide defaults: set PI_RESEARCH_KNOWLEDGE_STORE_MODE=<mode> an
27790
27791
  return reportError(err, "knowledge-config", kc.json);
27791
27792
  }
27792
27793
  }
27794
+ function cmdCleanup(c) {
27795
+ const script = fileURLToPath6(new URL("../scripts/cleanup.cjs", import.meta.url));
27796
+ if (!fs17.existsSync(script)) {
27797
+ toStderr(
27798
+ `
27799
+ Error: cleanup script not found at ${script}.
27800
+ Run it from the installed package instead: node <package-dir>/scripts/cleanup.cjs
27801
+ `
27802
+ );
27803
+ return EXIT.CONFIG;
27804
+ }
27805
+ const env = { ...process.env };
27806
+ if (c.purgeBrowsers) {
27807
+ env["PI_RESEARCH_PURGE_BROWSERS"] = "1";
27808
+ }
27809
+ const res = spawnSync(process.execPath, [script], { stdio: "inherit", env });
27810
+ if (res.error) {
27811
+ toStderr(`
27812
+ Error: could not run ${script}: ${res.error.message}
27813
+ `);
27814
+ return EXIT.SOFTWARE;
27815
+ }
27816
+ return res.status ?? EXIT.SOFTWARE;
27817
+ }
27793
27818
  async function cmdSkill(s) {
27794
27819
  const {
27795
27820
  installSkill: installSkill2,
@@ -28043,6 +28068,12 @@ COMMANDS
28043
28068
  skill uninstall Remove the skill from agents where pi-research installed it.
28044
28069
  --dry-run | --json Plan only; JSON output.
28045
28070
 
28071
+ cleanup Remove what npm uninstall leaves behind: the skill links, the
28072
+ state tree (<config dir>/research/state) and the cache, including
28073
+ downloaded embedding models. Keeps config.env and the knowledge
28074
+ store database. --purge-browsers also deletes the shared
28075
+ stealth-browser cache (~/.cache/camoufox) other tools use.
28076
+
28046
28077
  help, --help, -h Show this help.
28047
28078
  --version, -v Print the pi-research version.
28048
28079
 
@@ -28199,6 +28230,17 @@ function parseArgs(argv) {
28199
28230
  }
28200
28231
  throw new UsageError(`unknown knowledge-config action "${action}". Use: show | set <none|project|global> | set retrieval <vector|bm25>.`);
28201
28232
  }
28233
+ if (cmd === "cleanup") {
28234
+ let purgeBrowsers = false;
28235
+ for (const a of rest) {
28236
+ if (a === "--purge-browsers") purgeBrowsers = true;
28237
+ else if (a?.startsWith("--")) throw new UsageError(`unknown option for cleanup: ${a}`);
28238
+ else throw new UsageError(`unexpected argument "${a}" after "cleanup".`);
28239
+ }
28240
+ out.command = "cleanup";
28241
+ out.cleanup = { purgeBrowsers };
28242
+ return out;
28243
+ }
28202
28244
  if (cmd === "skill") {
28203
28245
  let json = false;
28204
28246
  let dryRun = false;
@@ -28412,6 +28454,8 @@ Error: ${err.message}
28412
28454
  return cmdKnowledgeConfig(parsed.knowledgeConfig);
28413
28455
  case "skill":
28414
28456
  return cmdSkill(parsed.skill);
28457
+ case "cleanup":
28458
+ return cmdCleanup(parsed.cleanup);
28415
28459
  case "research":
28416
28460
  return cmdResearch(parsed.research);
28417
28461
  default:
@@ -110,7 +110,10 @@ Install in External Agents. The installer:
110
110
  > @lincoln504/pi-research` leaves the skill links, the state directory
111
111
  > (`~/.pi/research/state`) and the cache directory (`~/.cache/pi-research`,
112
112
  > including downloaded embedding models) in place. Run `pi-research skill uninstall`
113
- > **before** removing the package to take the links with you.
113
+ > **before** removing the package to take the links with you. Then run
114
+ > `pi-research cleanup` to remove the state tree and the cache (it keeps `config.env`
115
+ > and the knowledge store database); add `--purge-browsers` to delete the shared
116
+ > `~/.cache/camoufox` cache as well.
114
117
 
115
118
  Standalone (no pi extension). `pi-research skill install` and `pi-research skill uninstall`
116
119
  do exactly the same from the CLI — same agent detection, same manifest, same
@@ -384,16 +384,35 @@ validates with. Bump it in lockstep with the pi host, not independently. (`undic
384
384
  contrast, tracks the host's major — the host is on undici 8, and pi-research only uses the
385
385
  stable `Agent` connector API, so it follows `^8`.)
386
386
 
387
- Pinned embedding runtime — `@huggingface/transformers` is exact at `4.2.0`. Tried `4.3.0`
388
- on 2026-10-01 and did not take it: it moves the native stack with it (onnxruntime-node
389
- `1.24.3` → `1.30.0`, onnxruntime-web `1.26.0-dev.20260416` → `1.31.0-dev.20260914`) and
390
- reshapes the tree (`roarr`, `boolean` and `detect-node` leave `global-agent`'s subtree). The
391
- knowledge-store unit suite (26 files, 322 tests) and the integration suites
392
- (`knowledge-stack`, `knowledge-models`, `research-knowledge-search`; 33 tests, 25 run, 8
393
- skipped) pass on both versions, so those suites do not decide it — they mock the backend or
394
- run against an already-cached model, and neither exercises a real model download and backend
395
- selection. Bump it only with a live embedding check (model load, CPU and WebGPU backends) on
396
- the new ONNX Runtime.
387
+ Pinned embedding runtime — `@huggingface/transformers` is exact at `4.3.0`. The hold at `4.2.0`
388
+ was lifted on 2026-10-01 for a security reason, not a feature one: `4.2.0` declares
389
+ `sharp ^0.34.5` and pins `onnxruntime-node 1.24.3`, whose `adm-zip ^0.5.16` resolves to the
390
+ advisory-ridden `0.5.18`, and neither range can be lifted downstream (npm ignores a published
391
+ dependency's `overrides`, so the repository's own `sharp`/`adm-zip` overrides reach only the
392
+ tree rooted here). `4.3.0` ships `sharp ^0.35.4` and `onnxruntime-node 1.30.0`
393
+ (`adm-zip ^0.6.0`), which clears both advisory groups for consumers as well. It also moves the
394
+ native stack with it (onnxruntime-web `1.26.0-dev.20260416` → `1.31.0-dev.20260914`, left
395
+ overridden at the stable `1.24.3` as before) and reshapes the tree (`global-agent` 3.0.0 →
396
+ 4.1.3, dropping seven packages from its old subtree: `roarr`, `boolean`, `detect-node`, `es6-error`,
397
+ `json-stringify-safe`, `semver-compare`, `sprintf-js`).
398
+
399
+ The unit suite (26 knowledge files, 322 tests) and the integration suites (`knowledge-stack`,
400
+ `knowledge-models`, `knowledge-migrations`, `research-knowledge-search`; 44 tests, 36 run, 8
401
+ skipped) pass on both versions, so those suites do NOT decide a bump — they mock the backend or
402
+ run against an already-cached model, and neither exercises real model load and backend
403
+ selection. What decided this one was a live embedding check against the real backend, run on
404
+ both release candidates with `onnx-community/granite-embedding-small-english-r2-ONNX`:
405
+
406
+ dimension 384 on both backends; CPU `initialize` 536 ms (4.2.0) vs 524 ms (4.3.0) and
407
+ `embedMany` of 3 texts 48 ms vs 39 ms; WebGPU (Dawn/Vulkan) `initialize` 1003 ms vs 1409 ms and
408
+ `embedMany` 110 ms vs 71 ms; cosine between the two related sentences 0.90431857 (4.2.0 CPU) vs
409
+ 0.90431858 (4.3.0 CPU) against 0.70265303 vs 0.70265297 for the unrelated one; embedding the
410
+ same text twice returns cosine 1.0; and a full store round-trip (two documents written through
411
+ the real embedder, then a hybrid vector + FTS + RRF `search` on both devices) returns the
412
+ expected top hit. The shipped `dist/webgpu-probe.mjs` answers `PROBE_OK 384` on the new runtime.
413
+ The remaining hold condition is unchanged: bump this package only with a live embedding check
414
+ (model load, CPU and WebGPU backends, and a store round-trip) on the new ONNX Runtime, because
415
+ nothing in the test suite fails when the backend is broken.
397
416
 
398
417
  Transient-failure resilience — every LLM call is a potential single point of failure on a
399
418
  streaming endpoint that can drop mid-response (undici surfaces this as `terminated`). The
@@ -409,7 +428,7 @@ to support testing (mock replacement) and enforce init → use → dispose lifec
409
428
  Pure ESM — the codebase is ES Modules (`"type": "module"`). Worker bundles are built with
410
429
  esbuild (`npm run build:worker`) before integration tests or publishing.
411
430
 
412
- Enforced boundaries — `docs/deps.svg` is regenerated on every push (madge), and
431
+ Enforced boundaries — [`docs/deps.svg`](https://github.com/Lincoln504/pi-research/blob/main/docs/deps.svg) is regenerated on every push (madge), and
413
432
  architectural rules are enforced by dependency-cruiser
414
433
  (`config/tooling/dependency-cruiser.cjs`).
415
434
 
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.7.5] - 2026-10-01
9
+
10
+ ### Added
11
+
12
+ - **`pi-research cleanup` removes what `npm uninstall` leaves behind.** npm 7 dropped `preuninstall`, so uninstalling removes the package directory and nothing else: skill links in other agents, the state tree and the cache (downloaded embedding models included) all stay on disk. The deletion logic already existed in `scripts/cleanup.cjs` (shipped in the tarball, wired to the `preuninstall` and `cleanup` npm scripts), but reaching it meant knowing where your package manager put the package. The new subcommand delegates to that script rather than reimplementing it, so there is one deletion implementation and one set of ownership guards, prints `--purge-browsers` to also delete the shared `~/.cache/camoufox` cache, and exits with the script's status. Verified end to end against a redirected HOME: state and cache removed, `config.env` and `knowledge_db/` preserved, second run idempotent. Alongside it the uninstall story is now documented as commands rather than only as a list of what stays: README, `docs/AGENT-SKILL.md`, `docs/CONFIGURATION.md` (which also gained the three leftover rows it was missing: the embedding model cache, the camoufox cache, the skills manifest, and the debug log that nothing removes) and the ES/JA/ZH docs.
13
+
14
+ ### Fixed
15
+
16
+ - **A silently dropped optional `@huggingface/transformers` subtree no longer fails the type-check gate, and both the release and CI type-check jobs now repair it.** `npm ci` (npm/cli#4828, the same bug the lancedb binding self-heal exists for) can drop the ENTIRE optional embedding tree without printing any `npm warn optional` line: the v1.7.4 release run installed 592 packages from the same lockfile that had installed 606 minutes earlier in CI, and its `npm run type-check` then failed with six `TS2307: Cannot find module '@huggingface/transformers'` errors, because `src/` imports that package's declarations unconditionally. Re-running the identical job installed 606 and passed, then published. The runtime tolerates the absence by design (the knowledge store disables cleanly, which is why the package is optional), but the type-check gate does not, so the failure was a delivery flake rather than a code defect. `scripts/ensure-native-deps.cjs` now reads the pin from `optionalDependencies` (one source of truth), verifies it resolves, and when it does not installs that exact version by name, which is not subject to the optional-dependency resolution bug. The repair uses `--no-save --no-package-lock` so package.json and the tracked lockfile stay untouched, emits a GitHub warning annotation so it is visible in the run summary, and exits non-zero if the repair does not take. Verified against a scratch tree with the subtree deleted: the package is restored at the pinned version, both manifest hashes are unchanged, and `npm run type-check` then passes. The CI type-check job now runs the self-heal too (it was exposed to the same flake and had no repair step), the lancedb install in the same script gained `--no-package-lock` for the same reason, and `test/unit/scripts/ensure-native-deps.test.ts` covers the pin lookup, the resolve check and the platform-package selection.
17
+ - **The `pi-research` bin reports a missing peer dependency instead of dying with a raw module error.** The CLI statically imports the `@earendil-works/pi-*` host packages, which are peer dependencies, so an install that skips peers (`npm install --legacy-peer-deps`, yarn classic, pnpm with `auto-install-peers=false`) left `pi-research` printing an internal `ERR_MODULE_NOT_FOUND` stack trace from inside Node. `bin` now points at a small unbundled shim (`src/cli-bin.mjs` → `dist/cli-bin.mjs`) that preflights both packages and exits 78 with the same remedy text and code the agent-skill launcher already prints, then loads the bundle. It cannot live inside the bundle: esbuild hoists the bundle's external imports to the top of the emitted file, ahead of every statement, so a wrapper entry point still crashed (verified). Two traps are pinned by tests: the check must use `import.meta.resolve()`, because `createRequire().resolve()` reports `ERR_PACKAGE_PATH_NOT_EXPORTED` for these ESM-only packages even when they are installed correctly, and the shim must rewrite `process.argv[1]` to the bundle path, because `cli.ts` runs `main()` only when argv[1] resolves to its own module file (otherwise the CLI exits 0 having done nothing). `scripts/verify-package.cjs` now also asserts that every `bin` target is shipped, so a shim dropped from `files[]` fails the publish gate instead of installing a command that does not exist.
18
+ - **The postinstall no longer starts a browser fetch it cannot finish, and `--system-deps` no longer installs an unpinned package.** `scripts/setup.cjs` used `npx playwright install-deps`, but `playwright` is not a dependency of this package (`playwright-core` is), so that resolved a different, unpinned package from the registry at install time; it now invokes the installed `playwright-core` CLI, which ships the same subcommand. When `CAMOUFOX_INSTALL_DIR` / `PLAYWRIGHT_BROWSERS_PATH` points at a path that exists but is not a usable directory (a plain file, no permission, a broken mount), the script used to log the error and then download the full 663MB browser anyway before failing; it now reports and skips the fetch, and the summary no longer claims the browser is "ready" when the path could not be read. Both paths still exit 0, because a failed install is worse than a browser fetched later.
19
+ - **Install claims and links that did not match reality are corrected.** The README said the package "runs no install scripts" and that the browser "downloads on first use". It declares three lifecycle scripts, and on npm up to 11.18 (the bundled npm on the Node 22.22.2 floor this package requires) the `postinstall` fetches that 663MB browser during install; only npm 11.19+ skips install scripts and defers the fetch to first use. The README, `docs/AGENT-SKILL.md` and the ES/JA/ZH docs now say which happens when, and how to defer it deliberately. The README's three translation links and `docs/ARCHITECTURE.md`'s `deps.svg` reference pointed at files that are not in the published tarball (the translations and the 660KB diagram are deliberately unshipped), so they are absolute repository URLs now and resolve from an installed package as well as from the npm page. `scripts/setup.cjs`'s header no longer claims it can never exit 1, which stopped being true when `PI_RESEARCH_STRICT_SETUP` (used by CI) was added.
20
+
21
+ ## [1.7.4] - 2026-09-30
22
+
23
+ ### Changed
24
+
25
+ - **`@huggingface/transformers` moves 4.2.0 → 4.3.0, which closes the `adm-zip` advisory group for consumers.** The hold was lifted for a security reason, not a feature one. `4.2.0` declares `sharp ^0.34.5` and pins `onnxruntime-node` 1.24.3, whose `adm-zip ^0.5.16` resolves to the advisory-ridden `0.5.18`, and no downstream lever could lift either range: npm ignores a published dependency's `overrides`, so this repository's own `sharp` and `adm-zip` overrides reached only the tree rooted here, and the audit gate (which audits that tree) stayed green while consumers resolved the vulnerable versions. `4.3.0` ships `sharp ^0.35.4` and `onnxruntime-node` 1.30.0 (`adm-zip ^0.6.0`), so a consumer install of the published tarball drops from 7 HIGH advisories to 5, or 4 on the peer-skipping shape `pi install` uses. What remains is the `sharp 0.33.5` copy that `@lancedb/lancedb` 0.39.0's hard-pinned `@huggingface/transformers` 3.0.2 brings, clearable only by a consumer-side `overrides: {"sharp": "^0.35.4"}` (verified: 0 on the `pi install` shape), plus the host shrinkwrap's `brace-expansion`. The bump also moves the native stack with it (onnxruntime-web `1.26.0-dev.20260416` → `1.31.0-dev.20260914`, still overridden to the stable `1.24.3`) and reshapes the tree (`global-agent` 3.0.0 → 4.1.3, which drops seven packages from its old subtree: `roarr`, `boolean`, `detect-node`, `es6-error`, `json-stringify-safe`, `semver-compare`, `sprintf-js`).
26
+ - **Incidental in-range dev refresh from the same install:** `@types/node` 26.4.1 → 26.6.3. `npm outdated` afterwards reports only the four documented holds: `apache-arrow` 21.1.0 (21.2.0 breaks LanceDB 0.39.0 IPC reads), `playwright-core` 1.60.0 (camoufox-js 0.12.0 peer cap `<1.61.0`), `typebox` 1.3.27 (lockstep with pi 0.99.2's own pin) and `typescript` 6.0.3.
27
+ - Decided by a live embedding check, which is the only thing that can decide it: the unit suite and the four knowledge integration suites (44 tests, 36 run, 8 skipped) pass on both versions because they mock the backend or run against an already-cached model. Against `onnx-community/granite-embedding-small-english-r2-ONNX` the new stack gives dimension 384 on both backends, CPU `initialize` 536 ms → 524 ms and `embedMany` of three texts 48 ms → 39 ms, WebGPU (Dawn/Vulkan) `initialize` 1003 ms → 1409 ms and `embedMany` 110 ms → 71 ms, related-sentence cosine 0.90431857 → 0.90431858 against 0.70265303 → 0.70265297 for the unrelated one, same-input cosine 1.0, a full store round-trip (two documents written through the real embedder, then a hybrid vector + FTS + RRF search) returning the expected top hit on both devices, and the shipped `dist/webgpu-probe.mjs` answering `PROBE_OK 384`.
28
+
29
+ ### Fixed
30
+
31
+ - **The audit-gate runbook's claim that consumers see zero vulnerabilities was false and is withdrawn.** `docs/AUDIT-GATE.md` stated that a consumer's `npm audit --omit=dev` reported zero vulnerabilities since 2026-09-16, on the strength of an empirical run. Measured again on 2026-10-01 against the published tarball, a consumer saw 7 HIGH (adm-zip 0.5.18, sharp 0.34.5 and 0.33.5, brace-expansion 5.0.9), because the gate audits the tree rooted in this repository, where `package.json`'s `overrides` apply, and npm honors those only at the root of an install. The section now carries the measured table for all four install shapes, the consumer-side override recipe that reaches zero, the two groups that remain unfixable downstream (`sharp` via LanceDB's hard-pinned `transformers` 3.0.2, and `brace-expansion` 5.0.9 inside the pi host's `npm-shrinkwrap.json`, which survives even an explicit consumer override), and an explicit statement that a green gate says nothing about a consumer's tree. The same section's claim that both adm-zip pinning parents require `^0.6.0` is corrected: `onnxruntime-node` 1.24.3, the copy `transformers` 4.2.0 pinned, required `adm-zip ^0.5.16`.
32
+ - **`docs/ARCHITECTURE.md`'s embedding-runtime rationale was rewritten around the new pin.** It recorded 4.3.0 as tried and rejected, and told a future maintainer that the knowledge-store suites decide a bump. They do not, since they mock the backend or use a cached model. It now records the security reason the hold was lifted, the measured before-and-after numbers for both backends, and the standing rule that a bump needs a live check (model load, CPU and WebGPU backends, store round-trip) rather than the suite.
33
+
8
34
  ## [1.7.3] - 2026-09-30
9
35
 
10
36
  ### Changed
@@ -363,7 +363,20 @@ All pi-research state lives under its own namespace, `~/.pi/research/`:
363
363
  | `~/.pi/research/state/` | Active sessions, browser status, locks. |
364
364
  | `~/.pi/research/knowledge_db/` | The knowledge store (LanceDB), unless `PI_RESEARCH_KNOWLEDGE_DIR` is set. |
365
365
  | `~/.cache/pi-research/profiles/` | Transient browser profiles, unless `PI_RESEARCH_TMP_DIR` is set. Rooted at `$XDG_CACHE_HOME` when that is set. |
366
+ | `~/.cache/pi-research/models/` | Downloaded embedding models (the `@huggingface/transformers` backend). Re-downloaded on demand if removed. Rooted at `$XDG_CACHE_HOME` when that is set. |
366
367
  | `~/.cache/pi-research/webgpu-viability.json` | Cached WebGPU-viability verdict (see the knowledge store doc). Rooted at `$XDG_CACHE_HOME` when that is set. |
368
+ | `~/.cache/camoufox/` | The stealth browser itself (1GB+). Shared with any other camoufox user on the machine, so it is never removed by default. |
369
+ | `~/.pi/research/installed-skills.json` | Manifest of the skill links/copies `pi-research skill install` created, so `skill uninstall` removes exactly those. |
370
+ | `$TMPDIR/pi-research.log` | Debug log, kept while `PI_RESEARCH_DEBUG=true`. Rotates at 10MB and keeps 10 archives. `PI_RESEARCH_LOG_PATH` relocates it. |
367
371
 
368
372
  Paths can be relocated with `PI_RESEARCH_STATE_DIR`, `PI_RESEARCH_KNOWLEDGE_DIR`,
369
373
  and `PI_RESEARCH_TMP_DIR`.
374
+
375
+ ### What uninstalling leaves behind
376
+
377
+ `npm uninstall` removes the package directory and nothing else: npm 7 removed the
378
+ `preuninstall` lifecycle, so the script declared in `package.json` never runs.
379
+ `pi-research skill uninstall` removes the skill links, and `pi-research cleanup`
380
+ removes the state tree and the cache (embedding models included) while keeping
381
+ `config.env` and `knowledge_db/`. Add `--purge-browsers` to also delete the shared
382
+ `~/.cache/camoufox` cache. The debug log is not removed by either.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "Web research for your agents with smart and safe tooling + knowledge store",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "main": "./src/index.ts",
8
8
  "types": "./src/index.ts",
9
9
  "bin": {
10
- "pi-research": "./dist/cli.mjs"
10
+ "pi-research": "./dist/cli-bin.mjs"
11
11
  },
12
12
  "exports": {
13
13
  ".": "./src/index.ts",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "src/",
19
+ "dist/cli-bin.mjs",
19
20
  "dist/cli.mjs",
20
21
  "dist/thread-worker.mjs",
21
22
  "dist/pdf-extract-worker.mjs",
@@ -137,7 +138,7 @@
137
138
  "@earendil-works/pi-tui": ">=0.99.0 <1"
138
139
  },
139
140
  "optionalDependencies": {
140
- "@huggingface/transformers": "4.2.0",
141
+ "@huggingface/transformers": "4.3.0",
141
142
  "webgpu": "^0.6.0"
142
143
  },
143
144
  "overrides": {
package/scripts/build.cjs CHANGED
@@ -49,6 +49,16 @@ function copyWebGpuProbe() {
49
49
  );
50
50
  }
51
51
 
52
+ // The `pi-research` bin shim (src/cli-bin.mjs), copied unbundled next to the
53
+ // bundle it loads. It MUST NOT be bundled into dist/cli.mjs: its whole job is to
54
+ // run BEFORE that bundle's external peer-dependency imports are evaluated, and
55
+ // esbuild hoists those to the top of the emitted file. See the shim's own header
56
+ // for the full story and for why `bin` points here instead of at dist/cli.mjs.
57
+ function copyCliBin() {
58
+ fs.mkdirSync(p('dist'), { recursive: true });
59
+ fs.copyFileSync(p('src', 'cli-bin.mjs'), p('dist', 'cli-bin.mjs'));
60
+ }
61
+
52
62
  // Build the browser worker bundle. Factored out so the cli target can guarantee
53
63
  // it exists when built in isolation (`build:cli`), not only via `build all`.
54
64
  function buildWorker() {
@@ -146,6 +156,7 @@ const TARGETS = {
146
156
  copyThreadWorker();
147
157
  copyPdfWorker();
148
158
  copyWebGpuProbe();
159
+ copyCliBin();
149
160
  copyPrompts();
150
161
  },
151
162
  skill: () => {
package/scripts/setup.cjs CHANGED
@@ -3,7 +3,13 @@
3
3
  /**
4
4
  * pi-research postinstall setup.
5
5
  * Installs Camoufox browser binaries.
6
- * Never exits with code 1 — that would break npm install.
6
+ *
7
+ * Exits 0 on EVERY failure path, because a non-zero exit from postinstall makes
8
+ * `npm install` fail and leaves the user without the package at all — the
9
+ * browser is re-fetchable on first use, a failed install is not. The single
10
+ * exception is PI_RESEARCH_STRICT_SETUP (set by this repo's CI), which restates
11
+ * the failure as exit 1 so an install regression fails the build loudly instead
12
+ * of hiding behind the graceful path.
7
13
  *
8
14
  * Environment:
9
15
  * PLAYWRIGHT_BROWSERS_PATH - override browser install location
@@ -82,6 +88,10 @@ function camoufoxCachePath() {
82
88
  }
83
89
 
84
90
  let browsersInstalled = false;
91
+ // Set when a fetch cannot possibly succeed (an unusable configured install dir),
92
+ // so the download is skipped rather than run to completion and then fail. Kept
93
+ // separate from browsersInstalled so the summary below still tells the truth.
94
+ let fetchSkipped = false;
85
95
 
86
96
  if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
87
97
  console.log('pi-research: skipping browser download (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1)');
@@ -98,7 +108,15 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
98
108
  const installDeps = process.argv.includes('--system-deps') || process.env.PLAYWRIGHT_INSTALL_DEPS === 'true';
99
109
  if (installDeps && isLinux) {
100
110
  try {
101
- execSync('npx playwright install-deps', { stdio: 'inherit', env: { ...env, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '0' } });
111
+ // Use the installed playwright-core CLI, NOT `npx playwright`: `playwright`
112
+ // is not a dependency of this package (only `playwright-core` is), so
113
+ // `npx playwright` resolved a DIFFERENT, unpinned package from the registry
114
+ // at install time — an extra network fetch, an extra supply-chain surface,
115
+ // and a version that can disagree with the browser we drive. playwright-core
116
+ // ships the same `install-deps` subcommand (verified: its CLI lists it).
117
+ const playwrightCoreBin = path.join(projectRoot, 'node_modules', '.bin', isWindows ? 'playwright-core.cmd' : 'playwright-core');
118
+ const cli = existsSync(playwrightCoreBin) ? playwrightCoreBin : 'playwright-core';
119
+ execSync(`"${cli}" install-deps`, { stdio: 'inherit', env: { ...env, PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '0' } });
102
120
  } catch (e) {
103
121
  console.warn(`WARNING: could not install system dependencies. Run: sudo apt-get install -y libgbm1 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libxcomposite1\nReason: ${e instanceof Error ? e.message : String(e)}\n(These are the libraries headless camoufox needs. Xvfb is NOT required for the default headless mode — only add it if you opt into virtual-display mode with PI_RESEARCH_USE_XVFB=true.)`);
104
122
  }
@@ -114,11 +132,23 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
114
132
  console.log(`pi-research: Camoufox already installed at ${cachePath}. Skipping fetch.`);
115
133
  }
116
134
  } catch (e) {
117
- console.warn(`pi-research: error checking camoufox path ${cachePath}: ${e instanceof Error ? e.message : String(e)}`);
135
+ // A configured install dir that exists but cannot be READ as a directory
136
+ // (a plain file, a broken symlink mount, no permission) makes the fetch
137
+ // below impossible: camoufox downloads the whole ~663MB archive and then
138
+ // fails to place it. Report and skip instead, which is what the user's
139
+ // install needs either way (the catch further down exits 0, and the
140
+ // browser is re-fetchable once the path is usable).
141
+ console.warn(`pi-research: cannot use the browser install directory ${cachePath}: ${e instanceof Error ? e.message : String(e)}`);
142
+ if (customCamoufoxDir()) {
143
+ console.warn('pi-research: skipping the browser fetch because CAMOUFOX_INSTALL_DIR / PLAYWRIGHT_BROWSERS_PATH points at a path that is not a usable directory. Fix or unset it, then run: npx camoufox-js fetch');
144
+ fetchSkipped = true;
145
+ }
118
146
  }
119
147
  }
120
148
 
121
- if (!alreadyInstalled) {
149
+ if (fetchSkipped) {
150
+ // Reported above; the fetch cannot work until the configured path is fixed.
151
+ } else if (!alreadyInstalled) {
122
152
  try {
123
153
  const bin = resolveCamoufoxBin();
124
154
  // spawnSync with an ARGV ARRAY, never a shell string. `bin` is a filesystem
@@ -169,7 +199,11 @@ if (existsSync(cachePath)) {
169
199
  const versions = readdirSync(cachePath).filter(f => statSync(path.join(cachePath, f)).isDirectory());
170
200
  console.log(`pi-research: camoufox ready (${versions.join(', ') || 'installed'})`);
171
201
  } catch (e) {
172
- console.log(`pi-research: camoufox ready (path check error: ${e instanceof Error ? e.message : String(e)})`);
202
+ // Not "ready": the path exists but cannot be read as an install dir (a plain
203
+ // file, no permission, a broken mount). Claiming readiness here is how a
204
+ // broken CAMOUFOX_INSTALL_DIR turned into a mysterious "browser missing at
205
+ // runtime" much later. The fetch path above already printed the remedy.
206
+ console.warn(`pi-research: camoufox install path ${cachePath} could not be verified: ${e instanceof Error ? e.message : String(e)}`);
173
207
  }
174
208
  } else if (browsersInstalled) {
175
209
  console.warn(`pi-research: camoufox binary not found at expected path ${cachePath}`);
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The installed `pi-research` bin.
4
+ *
5
+ * WHY THIS SHIM EXISTS INSTEAD OF POINTING `bin` STRAIGHT AT dist/cli.mjs
6
+ *
7
+ * dist/cli.mjs statically imports the `@earendil-works/pi-*` host packages, which
8
+ * are declared as PEER dependencies: pi supplies its own copies when it loads
9
+ * this package as an extension, and npm >= 7 resolves them automatically for a
10
+ * standalone (global or project) install. An install that skips peers leaves
11
+ * them absent — `npm install --legacy-peer-deps`, yarn classic, and a pnpm
12
+ * config with `auto-install-peers=false` all do that — and Node then dies at
13
+ * module load with a raw `ERR_MODULE_NOT_FOUND` stack before any of our code
14
+ * runs. The user sees an internal Node error instead of the one-line fix.
15
+ *
16
+ * The preflight cannot live inside the bundle: esbuild hoists the bundle's
17
+ * external `import` declarations to the very top of the output file, ahead of
18
+ * every statement in the program, so anything the bundle body does runs AFTER
19
+ * the failing import has already thrown. (Verified: a wrapper entry point whose
20
+ * first statement is the preflight still crashes, because the emitted file
21
+ * begins with `import "…/pi-ai";`.) It has to be a separate module, which is
22
+ * why this file is shipped unbundled and why `bin` points at it.
23
+ *
24
+ * The check uses `import.meta.resolve()`, NOT `createRequire().resolve()`: both
25
+ * host packages are ESM-only (`exports` maps with no `require` condition), so
26
+ * the CJS resolver fails on them with `ERR_PACKAGE_PATH_NOT_EXPORTED` even when
27
+ * they are correctly installed — a false negative that would break every
28
+ * healthy install. `import.meta.resolve()` uses the same ESM resolution the
29
+ * bundle itself uses, returns when the package is present, and throws
30
+ * `ERR_MODULE_NOT_FOUND` when it is not.
31
+ *
32
+ * Exit code 78 (EXIT.CONFIG) and the remedy text match the agent-skill
33
+ * launcher's `depMissing()` — one failure, one message, whichever entry point
34
+ * the user reached for.
35
+ */
36
+
37
+ import { fileURLToPath } from 'node:url';
38
+
39
+ const REQUIRED_PEERS = ['@earendil-works/pi-ai', '@earendil-works/pi-coding-agent'];
40
+
41
+ const missing = REQUIRED_PEERS.filter((name) => {
42
+ try {
43
+ import.meta.resolve(name);
44
+ return false;
45
+ } catch {
46
+ return true;
47
+ }
48
+ });
49
+
50
+ if (missing.length > 0) {
51
+ const lines = [
52
+ '',
53
+ 'Error: pi-research cannot start — it is missing required runtime dependencies:',
54
+ ...missing.map((name) => ` ${name}`),
55
+ '',
56
+ 'These are PEER dependencies. pi provides its own copies when it loads this',
57
+ 'package as an extension, and npm >= 7 installs them automatically for a',
58
+ 'standalone (global or project) install. This install skipped them, which is',
59
+ 'what --legacy-peer-deps, yarn classic, and a pnpm config with',
60
+ 'auto-install-peers=false do.',
61
+ '',
62
+ 'Either of these fixes it:',
63
+ '',
64
+ ' npm install -g @earendil-works/pi-coding-agent # add just the missing host package',
65
+ ' npm install -g @lincoln504/pi-research # or reinstall, resolving peers',
66
+ '',
67
+ ];
68
+ process.stderr.write(lines.join('\n'));
69
+ process.exit(78);
70
+ }
71
+
72
+ // dist/cli.mjs decides whether to run by comparing its own module URL against
73
+ // process.argv[1]. Because this shim is what the OS (or npm's Windows .cmd shim)
74
+ // actually invokes, argv[1] is THIS file; without the rewrite below that check
75
+ // would be false and the CLI would exit 0 having done nothing at all. Point it
76
+ // at the real entry point before importing it, so both sides resolve to the
77
+ // same realpath.
78
+ //
79
+ // fileURLToPath, not URL.pathname: on Windows the latter yields
80
+ // `/C:/…/cli.mjs`, which is not a path any filesystem call accepts.
81
+ process.argv[1] = fileURLToPath(new URL('./cli.mjs', import.meta.url));
82
+
83
+ await import('./cli.mjs');
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  import * as fs from 'node:fs';
26
26
  import * as os from 'node:os';
27
27
  import * as path from 'node:path';
28
+ import { spawnSync } from 'node:child_process';
28
29
  import { fileURLToPath } from 'node:url';
29
30
 
30
31
  // pi-research internals (bundled; native/npm deps stay external).
@@ -989,6 +990,55 @@ export async function cmdKnowledgeConfig(kc: NonNullable<ParsedArgs['knowledgeCo
989
990
  * clobbered, and every link/copy is manifest-tracked so uninstall removes exactly
990
991
  * what was created.
991
992
  */
993
+ /**
994
+ * `pi-research cleanup` — the supported way to remove what `npm uninstall`
995
+ * leaves behind.
996
+ *
997
+ * npm removed `preuninstall` in npm 7, so an uninstall removes the package
998
+ * directory and nothing else: skill links in other agents, the state tree and
999
+ * the cache (downloaded embedding models included) all stay on disk. The
1000
+ * deletion logic already exists in `scripts/cleanup.cjs`, which ships in the
1001
+ * tarball and is wired to the `preuninstall` and `cleanup` npm scripts — but
1002
+ * reaching it means knowing where your package manager put the package.
1003
+ *
1004
+ * Delegates to that script rather than reimplementing it, deliberately: one
1005
+ * deletion implementation, one set of ownership guards (it removes only what it
1006
+ * can prove it created, and refuses to touch the shared camoufox cache unless
1007
+ * asked). spawnSync with an argv array and process.execPath, never a shell
1008
+ * string, so a package installed under a path containing shell metacharacters
1009
+ * cannot execute anything.
1010
+ *
1011
+ * Resolves the script relative to THIS module: `src/cli.ts` and the bundled
1012
+ * `dist/cli.mjs` are each one level below the package root, so `../scripts/`
1013
+ * is correct for a source checkout, a local install and a global install alike.
1014
+ */
1015
+ function cmdCleanup(c: NonNullable<ParsedArgs['cleanup']>): number {
1016
+ const script = fileURLToPath(new URL('../scripts/cleanup.cjs', import.meta.url));
1017
+ if (!fs.existsSync(script)) {
1018
+ toStderr(
1019
+ `\nError: cleanup script not found at ${script}.\n` +
1020
+ 'Run it from the installed package instead: node <package-dir>/scripts/cleanup.cjs\n',
1021
+ );
1022
+ return EXIT.CONFIG;
1023
+ }
1024
+
1025
+ const env: NodeJS.ProcessEnv = { ...process.env };
1026
+ if (c.purgeBrowsers) {
1027
+ // Shared with every other camoufox user on the box, which is why it is not
1028
+ // the default. The script prints what it is about to delete.
1029
+ env['PI_RESEARCH_PURGE_BROWSERS'] = '1';
1030
+ }
1031
+
1032
+ // stdio: 'inherit' — the script narrates each removal, and that narration is
1033
+ // the user's only confirmation of what was deleted.
1034
+ const res = spawnSync(process.execPath, [script], { stdio: 'inherit', env });
1035
+ if (res.error) {
1036
+ toStderr(`\nError: could not run ${script}: ${res.error.message}\n`);
1037
+ return EXIT.SOFTWARE;
1038
+ }
1039
+ return res.status ?? EXIT.SOFTWARE;
1040
+ }
1041
+
992
1042
  async function cmdSkill(s: NonNullable<ParsedArgs['skill']>): Promise<number> {
993
1043
  const {
994
1044
  installSkill, uninstallSkill, skillInstallCandidates, skillUninstallCandidates,
@@ -1268,6 +1318,7 @@ interface ParsedArgs {
1268
1318
  knowledgeConfig?: { action: 'show' | 'set' | 'set-retrieval'; mode?: 'none' | 'project' | 'global'; retrieval?: 'vector' | 'bm25'; json?: boolean };
1269
1319
  status?: { json?: boolean };
1270
1320
  skill?: { action: 'install' | 'uninstall' | 'status'; json?: boolean; dryRun?: boolean; copy?: boolean };
1321
+ cleanup?: { purgeBrowsers?: boolean };
1271
1322
  configPath?: string;
1272
1323
  json?: boolean;
1273
1324
  }
@@ -1316,6 +1367,12 @@ COMMANDS
1316
1367
  skill uninstall Remove the skill from agents where pi-research installed it.
1317
1368
  --dry-run | --json Plan only; JSON output.
1318
1369
 
1370
+ cleanup Remove what npm uninstall leaves behind: the skill links, the
1371
+ state tree (<config dir>/research/state) and the cache, including
1372
+ downloaded embedding models. Keeps config.env and the knowledge
1373
+ store database. --purge-browsers also deletes the shared
1374
+ stealth-browser cache (~/.cache/camoufox) other tools use.
1375
+
1319
1376
  help, --help, -h Show this help.
1320
1377
  --version, -v Print the pi-research version.
1321
1378
 
@@ -1492,6 +1549,22 @@ export function parseArgs(argv: string[]): ParsedArgs {
1492
1549
  throw new UsageError(`unknown knowledge-config action "${action}". Use: show | set <none|project|global> | set retrieval <vector|bm25>.`);
1493
1550
  }
1494
1551
 
1552
+ if (cmd === 'cleanup') {
1553
+ // Wrapper for the shipped scripts/cleanup.cjs. npm 7+ never runs
1554
+ // `preuninstall`, so an uninstall leaves the state tree, the cache and any
1555
+ // skill links in other agents behind; this is the documented way to remove
1556
+ // them without locating the script inside node_modules or a global prefix.
1557
+ let purgeBrowsers = false;
1558
+ for (const a of rest) {
1559
+ if (a === '--purge-browsers') purgeBrowsers = true;
1560
+ else if (a?.startsWith('--')) throw new UsageError(`unknown option for cleanup: ${a}`);
1561
+ else throw new UsageError(`unexpected argument "${a}" after "cleanup".`);
1562
+ }
1563
+ out.command = 'cleanup';
1564
+ out.cleanup = { purgeBrowsers };
1565
+ return out;
1566
+ }
1567
+
1495
1568
  if (cmd === 'skill') {
1496
1569
  let json = false;
1497
1570
  let dryRun = false;
@@ -1847,6 +1920,8 @@ async function main(argv: string[]): Promise<number> {
1847
1920
  return cmdKnowledgeConfig(parsed.knowledgeConfig!);
1848
1921
  case 'skill':
1849
1922
  return cmdSkill(parsed.skill!);
1923
+ case 'cleanup':
1924
+ return cmdCleanup(parsed.cleanup!);
1850
1925
  case 'research':
1851
1926
  return cmdResearch(parsed.research!);
1852
1927
  default: