@lincoln504/pi-research 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.mjs +2599 -1268
  3. package/dist/thread-worker.mjs +224 -147
  4. package/docs/AGENT-SKILL.md +11 -4
  5. package/docs/ARCHITECTURE.md +20 -10
  6. package/docs/CHANGELOG.md +195 -17
  7. package/docs/CONFIGURATION.md +9 -7
  8. package/docs/KNOWLEDGE-STORE.md +4 -3
  9. package/docs/PI-EXTENSION.md +1 -1
  10. package/docs/SDK.md +55 -0
  11. package/docs/THIRD-PARTY-NOTICES.md +1 -1
  12. package/package.json +5 -4
  13. package/scripts/cleanup.cjs +114 -12
  14. package/scripts/prepare.cjs +15 -5
  15. package/scripts/setup.cjs +24 -4
  16. package/skills/pi-research/README.md +9 -2
  17. package/skills/pi-research/SKILL.md +6 -3
  18. package/skills/pi-research/references/configuration.md +6 -2
  19. package/skills/pi-research/scripts/run.mjs +24 -2
  20. package/src/cleanup/research-cleanup.ts +12 -4
  21. package/src/cli.ts +372 -86
  22. package/src/config.ts +134 -17
  23. package/src/constants.ts +31 -0
  24. package/src/core/interfaces/health-check-interfaces.ts +3 -39
  25. package/src/core/interfaces/knowledge-interfaces.ts +6 -1
  26. package/src/core/interfaces/planning-interfaces.ts +8 -0
  27. package/src/core/interfaces/service-names.ts +0 -1
  28. package/src/core/interfaces/state-manager-interfaces.ts +11 -3
  29. package/src/core/llm/agentic-repair.ts +14 -0
  30. package/src/core/pi-version.ts +136 -0
  31. package/src/core/planning-service.ts +43 -1
  32. package/src/core/service-initialization.ts +6 -14
  33. package/src/core/service-interfaces.ts +1 -1
  34. package/src/core/service-registry.ts +117 -8
  35. package/src/healthcheck/index.ts +7 -2
  36. package/src/index.ts +11 -18
  37. package/src/infrastructure/browser/browser-client.ts +80 -13
  38. package/src/infrastructure/browser/browser-error-utils.ts +6 -1
  39. package/src/infrastructure/browser/browser-server.ts +41 -12
  40. package/src/infrastructure/browser/browser-task-scheduler.ts +159 -35
  41. package/src/infrastructure/browser/cleanup-utils.ts +95 -9
  42. package/src/infrastructure/browser/client-agent.ts +6 -5
  43. package/src/infrastructure/browser/config.ts +14 -3
  44. package/src/infrastructure/browser/priority-task-queue.ts +10 -3
  45. package/src/infrastructure/browser/scheduler-factory.ts +44 -4
  46. package/src/infrastructure/browser/task-execution-service.ts +92 -28
  47. package/src/infrastructure/browser/thread-worker-lifecycle.ts +50 -13
  48. package/src/infrastructure/browser/thread-worker-messaging.ts +54 -2
  49. package/src/infrastructure/browser/thread-worker-types.ts +3 -1
  50. package/src/infrastructure/browser/thread-worker.mjs +224 -147
  51. package/src/infrastructure/browser/worker-pool-manager.ts +18 -5
  52. package/src/infrastructure/embedding/embedding-client.ts +22 -8
  53. package/src/infrastructure/embedding/embedding-factory.ts +61 -15
  54. package/src/infrastructure/embedding/embedding-server.ts +105 -11
  55. package/src/infrastructure/file-lock-service.ts +295 -28
  56. package/src/infrastructure/knowledge-store-service.ts +48 -3
  57. package/src/infrastructure/process-lifecycle-service.ts +134 -34
  58. package/src/infrastructure/research-run-semaphore.ts +37 -3
  59. package/src/infrastructure/state/state-backup-manager.ts +5 -7
  60. package/src/infrastructure/state/state-browser-api.ts +5 -2
  61. package/src/infrastructure/state/state-browser-manager.ts +15 -1
  62. package/src/infrastructure/state/state-manager-service.ts +5 -5
  63. package/src/infrastructure/state/state-manager.ts +34 -23
  64. package/src/infrastructure/types/state-types.ts +9 -0
  65. package/src/knowledge/chunker.ts +88 -1
  66. package/src/knowledge/embedder-init.ts +1 -1
  67. package/src/knowledge/embedder.ts +188 -46
  68. package/src/knowledge/index.ts +1 -1
  69. package/src/knowledge/store.ts +101 -21
  70. package/src/knowledge/webgpu-viability.ts +9 -4
  71. package/src/knowledge/writer-queue.ts +88 -46
  72. package/src/logger.ts +5 -1
  73. package/src/observers/research-observer-impl.ts +31 -0
  74. package/src/orchestration/abort-utils.ts +60 -0
  75. package/src/orchestration/deep-research-orchestrator.ts +25 -4
  76. package/src/orchestration/headless-observer.ts +8 -0
  77. package/src/orchestration/quick-research-orchestrator.ts +2 -2
  78. package/src/orchestration/research-orchestration-service.ts +65 -15
  79. package/src/orchestration/research-session-service.ts +37 -11
  80. package/src/orchestration/research-synthesis-service.ts +33 -11
  81. package/src/orchestration/researcher-executor.ts +100 -20
  82. package/src/orchestration/researcher.ts +39 -20
  83. package/src/orchestration/session-state.ts +11 -0
  84. package/src/sdk.ts +125 -19
  85. package/src/security/cisa-kev.ts +10 -3
  86. package/src/security/github-advisories.ts +18 -7
  87. package/src/security/index.ts +51 -30
  88. package/src/security/nvd-client.ts +24 -8
  89. package/src/security/osv-client.ts +14 -4
  90. package/src/skill-install/skill-installer.ts +153 -11
  91. package/src/stackexchange/index.ts +20 -5
  92. package/src/stackexchange/output/table.ts +7 -2
  93. package/src/stackexchange/rest-client.ts +41 -3
  94. package/src/tools/research-knowledge-search.ts +15 -0
  95. package/src/tools/research-tool-definition.ts +8 -2
  96. package/src/tools/scrape.ts +48 -25
  97. package/src/tools/search.ts +16 -2
  98. package/src/tools/security.ts +68 -7
  99. package/src/tui/research-health.ts +18 -2
  100. package/src/utils/atomic-replace.ts +101 -0
  101. package/src/utils/circuit-breaker.ts +9 -2
  102. package/src/utils/citation-utils.ts +3 -1
  103. package/src/utils/disk-space-checker.ts +11 -6
  104. package/src/utils/http-body.ts +176 -0
  105. package/src/utils/json-utils.ts +116 -76
  106. package/src/utils/llm-usage.ts +17 -1
  107. package/src/utils/log-rotation.ts +42 -10
  108. package/src/utils/log-utils.ts +40 -0
  109. package/src/utils/metrics-summary.ts +27 -9
  110. package/src/utils/stdio-capture.ts +63 -22
  111. package/src/utils/text-utils.ts +8 -1
  112. package/src/utils/tool-usage-tracker.ts +9 -2
  113. package/src/web-research/browser-search.ts +70 -5
  114. package/src/web-research/scraper-utils.ts +13 -0
  115. package/src/web-research/search.ts +25 -6
  116. package/src/web-research/types.ts +19 -4
  117. package/src/web-research/web-scraper.ts +72 -54
  118. package/src/youtube/transcript-client.ts +5 -1
  119. package/src/core/health-check-service.ts +0 -265
@@ -1,7 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require('child_process');
4
- const { rmSync, existsSync, lstatSync, readlinkSync, readFileSync, writeFileSync, unlinkSync } = require('fs');
3
+ /**
4
+ * Uninstall-time cleanup: skill links, cache tree, state tree.
5
+ *
6
+ * IMPORTANT — this does NOT run on `npm uninstall`. npm removed the
7
+ * `preuninstall`/`postuninstall` lifecycle for the package being removed in
8
+ * npm 7; verified against npm 11, where `postinstall` fires on install but
9
+ * `preuninstall` never fires on uninstall. The `preuninstall` entry in
10
+ * package.json is retained for package managers that still honour it, but it
11
+ * must not be treated as the cleanup path.
12
+ *
13
+ * The supported paths are `pi-research skill uninstall` (removes the skill
14
+ * links via the same owned-only manifest logic) and running this script
15
+ * directly: node <package-root>/scripts/cleanup.cjs
16
+ */
17
+
18
+ const { spawnSync } = require('child_process');
19
+ const { rmSync, existsSync, lstatSync, readdirSync, readlinkSync, readFileSync, writeFileSync, unlinkSync } = require('fs');
5
20
  const os = require('os');
6
21
  const path = require('path');
7
22
 
@@ -52,7 +67,7 @@ function removeInstalledSkills() {
52
67
  if (!owned) { kept.push(e); continue; }
53
68
  try {
54
69
  if (lstatSync(p).isSymbolicLink()) unlinkSync(p);
55
- else rmSync(p, { recursive: true, force: true });
70
+ else rmSync(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
56
71
  console.log(`pi-research: removed skill install at ${p}`);
57
72
  } catch (err) {
58
73
  kept.push(e);
@@ -93,8 +108,17 @@ if (process.env.PI_RESEARCH_PURGE_BROWSERS === '1') {
93
108
  if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD !== '1') {
94
109
  try {
95
110
  const bin = resolveCamoufoxBin();
96
- const cmd = bin ? `"${bin}" remove` : 'npx camoufox-js remove';
97
- execSync(cmd, { stdio: 'inherit' });
111
+ // Argv array, not a shell string — see the matching note in setup.cjs: `bin`
112
+ // is derived from the install directory, and a shell string would let a
113
+ // directory name containing `$(…)` execute at uninstall time.
114
+ const isWin = process.platform === 'win32';
115
+ const res = bin
116
+ ? (isWin
117
+ ? spawnSync(`"${bin}"`, ['remove'], { stdio: 'inherit', shell: true })
118
+ : spawnSync(bin, ['remove'], { stdio: 'inherit' }))
119
+ : spawnSync('npx', ['camoufox-js', 'remove'], { stdio: 'inherit', shell: isWin });
120
+ if (res.error) throw res.error;
121
+ if (res.status !== 0) throw new Error(`camoufox remove exited with code ${res.status}`);
98
122
  console.log('pi-research: camoufox browser binaries removed.');
99
123
  } catch (error) {
100
124
  console.warn(`pi-research: could not remove camoufox binaries: ${error instanceof Error ? error.message : String(error)}`);
@@ -109,7 +133,7 @@ if (process.env.PI_RESEARCH_PURGE_BROWSERS === '1') {
109
133
  const legacyCacheDir = path.join(projectRoot, '.browser');
110
134
  if (existsSync(legacyCacheDir)) {
111
135
  try {
112
- rmSync(legacyCacheDir, { recursive: true, force: true });
136
+ rmSync(legacyCacheDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
113
137
  } catch (error) {
114
138
  console.warn(`pi-research: could not remove ${legacyCacheDir}: ${error instanceof Error ? error.message : String(error)}`);
115
139
  }
@@ -126,7 +150,7 @@ if (existsSync(legacyCacheDir)) {
126
150
  function removeOwnedScratchDir(label, dir) {
127
151
  if (!dir || !existsSync(dir)) return;
128
152
  try {
129
- rmSync(dir, { recursive: true, force: true });
153
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
130
154
  console.log(`pi-research: removed ${label} ${dir}`);
131
155
  } catch (error) {
132
156
  console.warn(`pi-research: could not remove ${label} ${dir}: ${error instanceof Error ? error.message : String(error)}`);
@@ -136,14 +160,92 @@ function removeOwnedScratchDir(label, dir) {
136
160
  // Cache tree: browser profiles, webgpu-viability probe, downloaded embedding models.
137
161
  // Same resolution as config.ts (XDG_CACHE_HOME || ~/.cache, then pi-research) on
138
162
  // every platform. Shared camoufox binaries live in ~/.cache/camoufox (handled above).
139
- const cacheHome = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
163
+ // A RELATIVE XDG_CACHE_HOME is ignored, exactly as embedder-utils.ts does at runtime.
164
+ // Honouring one here would rmSync a cwd-relative `pi-research` tree — deleting whatever
165
+ // happens to sit there — while leaving the real cache untouched. This script's whole job
166
+ // is deletion, so it must not resolve paths more loosely than the code that created them.
167
+ const xdgCache = process.env.XDG_CACHE_HOME;
168
+ const cacheHome = xdgCache && path.isAbsolute(xdgCache) ? xdgCache : path.join(os.homedir(), '.cache');
140
169
  removeOwnedScratchDir('cache dir', path.join(cacheHome, 'pi-research'));
141
170
 
142
171
  // State tree: transient run state, locks, backups, project-settings. Now lives in
143
172
  // pi-research's own namespace (~/.pi/research/state), honoring the same overrides
144
- // as getGlobalConfigDir()/PI_RESEARCH_STATE_DIR.
145
- const stateDir = process.env.PI_RESEARCH_STATE_DIR
146
- || path.join(os.homedir(), process.env.PI_RESEARCH_CONFIG_DIR_NAME || '.pi', 'research', 'state');
147
- removeOwnedScratchDir('state dir', stateDir);
173
+ // as getGlobalConfigDir()/PI_RESEARCH_STATE_DIR — with the same strictness the
174
+ // XDG guard above applies. A RELATIVE state dir would rmSync a tree relative to
175
+ // wherever the user happens to invoke this script (generally NOT where the
176
+ // runtime created state), and a config-dir name carrying a path separator could
177
+ // traverse out of HOME entirely. Deletion must never resolve paths more loosely
178
+ // than the code that created them.
179
+ const stateOverride = process.env.PI_RESEARCH_STATE_DIR;
180
+ const rawDirName = process.env.PI_RESEARCH_CONFIG_DIR_NAME;
181
+ const dirName = rawDirName && !rawDirName.includes('/') && !rawDirName.includes('\\') && rawDirName !== '.' && rawDirName !== '..'
182
+ ? rawDirName
183
+ : '.pi';
184
+
185
+ /**
186
+ * Is this path PROVABLY equivalent to the layout the runtime itself would have
187
+ * constructed by default (`<dirName>/research`)? Wholesale rm of a state-dir
188
+ * override is only safe when the answer is yes: PI_RESEARCH_STATE_DIR may point
189
+ * at a SHARED directory, and rm'ing the override itself (unlike the cache sweep,
190
+ * which is namespaced via join(cacheHome, 'pi-research')) deleted everything the
191
+ * user kept beside our state.
192
+ *
193
+ * Deliberately NOT a bare "does any segment literally say pi-research" check —
194
+ * that matches naming convention, not ownership. A user is entirely free to
195
+ * point the override at, say, `~/agents-state/pi-research/` while keeping other
196
+ * tools' notes alongside it in the same folder (a natural way to organize
197
+ * several tools' state by name); a substring match would treat that shared
198
+ * folder as exclusively ours and wholesale-delete it. Only the exact default
199
+ * layout is something WE constructed and can therefore prove is ours.
200
+ */
201
+ function isPiResearchNamespaced(p) {
202
+ const segs = p.split(/[\\/]+/).filter(Boolean);
203
+ for (let i = 0; i < segs.length - 1; i++) {
204
+ if (segs[i] === dirName && segs[i + 1] === 'research') return true;
205
+ }
206
+ return false;
207
+ }
208
+
209
+ /**
210
+ * Targeted state sweep for a non-namespaced override dir: delete ONLY the
211
+ * entries the runtime actually creates there and leave everything else — and
212
+ * the directory itself — untouched. The entry list mirrors the writers:
213
+ * research-state.json + research-state-<hex>.tmp (state-manager)
214
+ * .locks/, backups/ (state-path-configuration)
215
+ * project-settings.json + its .lock / .tmp. / .lock.stale- companions (config.ts)
216
+ * research-run-slot-<i>.lock (research-run-semaphore)
217
+ * Best-effort; never throws.
218
+ */
219
+ function removeOwnedStateEntries(dir) {
220
+ if (!dir || !existsSync(dir)) return;
221
+ let entries;
222
+ try { entries = readdirSync(dir); } catch { return; }
223
+ const owned = (name) =>
224
+ name === 'research-state.json' ||
225
+ (name.startsWith('research-state-') && name.endsWith('.tmp')) ||
226
+ name === '.locks' ||
227
+ name === 'backups' ||
228
+ name === 'project-settings.json' ||
229
+ name.startsWith('project-settings.json.') ||
230
+ /^research-run-slot-\d+\.lock$/.test(name);
231
+ for (const name of entries) {
232
+ if (!owned(name)) continue;
233
+ try {
234
+ rmSync(path.join(dir, name), { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
235
+ } catch (error) {
236
+ console.warn(`pi-research: could not remove state entry ${path.join(dir, name)}: ${error instanceof Error ? error.message : String(error)}`);
237
+ }
238
+ }
239
+ console.log(`pi-research: removed pi-research state entries from ${dir} (directory preserved — not a pi-research-namespaced path).`);
240
+ }
241
+
242
+ const stateDir = stateOverride && path.isAbsolute(stateOverride)
243
+ ? stateOverride
244
+ : path.join(os.homedir(), dirName, 'research', 'state');
245
+ if (stateOverride && path.isAbsolute(stateOverride) && !isPiResearchNamespaced(stateDir)) {
246
+ removeOwnedStateEntries(stateDir);
247
+ } else {
248
+ removeOwnedScratchDir('state dir', stateDir);
249
+ }
148
250
 
149
251
  process.exit(0);
@@ -15,14 +15,24 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { execSync } = require('child_process');
18
+ const { spawnSync } = require('child_process');
19
19
  const path = require('path');
20
20
 
21
21
  const ROOT = path.join(__dirname, '..');
22
22
 
23
- try {
24
- execSync('node scripts/build.cjs all', { stdio: 'inherit', cwd: ROOT });
25
- } catch (err) {
26
- console.error('[prepare] Build failed:', err.message);
23
+ // spawnSync with an argv array, not execSync with a quoted string: execSync goes
24
+ // through /bin/sh -c, where $(…), backticks and quotes stay live INSIDE double
25
+ // quotes — so a Node binary path under a directory named with shell metacharacters
26
+ // would execute them during npm ci / a git install. Same fix setup.cjs and
27
+ // cleanup.cjs already carry. process.execPath (not a bare 'node') for the same
28
+ // reason as ensure-native-deps.cjs: a PATH lookup can resolve a different
29
+ // interpreter (nvm-windows, Volta, a portable Node) than the one running npm.
30
+ const r = spawnSync(process.execPath, ['scripts/build.cjs', 'all'], { stdio: 'inherit', cwd: ROOT });
31
+ if (r.error) {
32
+ console.error('[prepare] Build failed:', r.error.message);
27
33
  process.exit(1);
28
34
  }
35
+ if (r.status !== 0) {
36
+ console.error(`[prepare] Build failed with exit code ${r.status}`);
37
+ process.exit(r.status ?? 1);
38
+ }
package/scripts/setup.cjs CHANGED
@@ -11,7 +11,7 @@
11
11
  * PLAYWRIGHT_INSTALL_DEPS=true - also install Linux system deps (or pass --system-deps)
12
12
  */
13
13
 
14
- const { execSync } = require('child_process');
14
+ const { execSync, spawnSync } = require('child_process');
15
15
  const { existsSync, readdirSync, statSync } = require('fs');
16
16
  const { homedir } = require('os');
17
17
  const path = require('path');
@@ -95,18 +95,38 @@ if (process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === '1') {
95
95
  if (!alreadyInstalled) {
96
96
  try {
97
97
  const bin = resolveCamoufoxBin();
98
- const cmd = bin ? `"${bin}" fetch` : 'npx camoufox-js fetch';
98
+ // spawnSync with an ARGV ARRAY, never a shell string. `bin` is a filesystem
99
+ // path derived from the install directory, and execSync runs through
100
+ // `/bin/sh -c` where `$(…)`, backticks and quotes stay live INSIDE double
101
+ // quotes — so a package installed under a directory whose name contains
102
+ // shell metacharacters would execute them at install time. Windows still
103
+ // needs a shell to run the `.cmd` shim, and there the path is quoted; the
104
+ // runtime twin in ensure-browser.ts uses the same shape.
99
105
  // Bound the ~100MB download so a stalled/interrupted network fails fast
100
106
  // instead of hanging `npm install` forever (the catch below exits 0, and the
101
107
  // browser is re-fetchable manually). 15 min is ample even on slow links.
102
- execSync(cmd, { stdio: 'inherit', env, timeout: 15 * 60 * 1000 });
108
+ const spawnOpts = { stdio: 'inherit', env, timeout: 15 * 60 * 1000 };
109
+ const res = bin
110
+ ? (isWindows
111
+ ? spawnSync(`"${bin}"`, ['fetch'], { ...spawnOpts, shell: true })
112
+ : spawnSync(bin, ['fetch'], spawnOpts))
113
+ : spawnSync('npx', ['camoufox-js', 'fetch'], { ...spawnOpts, shell: isWindows });
114
+ if (res.error) throw res.error;
115
+ if (res.status !== 0) throw new Error(`camoufox fetch exited with code ${res.status}`);
103
116
  browsersInstalled = true;
104
117
  } catch (error) {
105
118
  const msg = error instanceof Error ? error.message : String(error);
106
119
  console.error('ERROR: Camoufox browser install failed — pi-research will not work.');
107
120
  console.error('Run manually to fix: npx camoufox-js fetch');
108
121
  console.error(`Reason: ${msg}`);
109
- process.exit(0);
122
+ // Exit 0 by default: the browser is fetched lazily on first use, so a restrictive
123
+ // network must not fail a user's install. But that also meant nothing here could
124
+ // ever turn CI red — a broken Windows launcher yielded a green `npm ci`. CI sets
125
+ // PI_RESEARCH_STRICT_SETUP=1 so the platform-specific spawn paths above are
126
+ // actually enforced somewhere.
127
+ const strict = process.env.PI_RESEARCH_STRICT_SETUP === '1' ||
128
+ process.env.PI_RESEARCH_STRICT_SETUP === 'true';
129
+ process.exit(strict ? 1 : 0);
110
130
  }
111
131
  } else {
112
132
  browsersInstalled = true;
@@ -52,7 +52,13 @@ node "<skill_dir>/scripts/run.mjs" status # verify detection
52
52
  Recommended. From the pi extension, run `/research-config` and choose
53
53
  Install in External Agents. It detects Claude, Codex, and OpenClaw and
54
54
  symlinks this skill into each one that is installed. Remove from External Agents
55
- removes those symlinks (cleanup also runs automatically on `npm uninstall`).
55
+ removes those symlinks.
56
+
57
+ Uninstalling the npm package does **not** remove them: npm 7 and newer do not run a
58
+ package's own `preuninstall` script (verified against npm 11). Use Remove from
59
+ External Agents first, or run `node scripts/cleanup.cjs` from the package directory,
60
+ otherwise the symlinks, `~/.pi/research/state` and the `~/.cache/pi-research` model
61
+ cache are all left behind.
56
62
 
57
63
  Cursor is not auto-installed — it has no global skills directory and only reads
58
64
  project-level `.cursor/skills/`. Symlink it per-project instead (see the table).
@@ -82,7 +88,8 @@ node "<skill_dir>/scripts/run.mjs" status
82
88
  ### Files
83
89
 
84
90
  - `SKILL.md` — the skill definition + system prompt (what agents read).
85
- - `scripts/run.mjs` — the launcher (compiled from `run.ts`).
91
+ - `scripts/run.mjs` — the launcher (built from `run.ts`, which is a repo-only source
92
+ file and is not shipped in the npm package).
86
93
  - `references/configuration.md` — model/key setup and the full variable reference.
87
94
 
88
95
  ### License
@@ -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.1.2", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.3.0", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
6
6
  allowed-tools: Bash(node:*)
7
7
  ---
8
8
 
@@ -55,8 +55,10 @@ node "<SKILL_DIR>/scripts/run.mjs" status [--json]
55
55
  - **Cite the report's sources.** Carry its URLs/`[n]` references into your answer;
56
56
  attribute each non-trivial claim. Never present researched facts as unsourced;
57
57
  never invent or substitute sources.
58
- - **Surface the saved file.** If stderr/stdout ends with `Research report saved to: <path>`,
59
- give the user that exact path. If absent, no file was written — say nothing about one.
58
+ - **Surface the saved file.** Look for a saved-report line: stdout's report footer ends
59
+ with `Research report saved to: <path>`, and stderr carries `[pi-research] report saved
60
+ to: <path>`. Give the user that exact path. If neither appears, no file was written —
61
+ say nothing about one.
60
62
 
61
63
  ## Exit codes
62
64
 
@@ -66,6 +68,7 @@ node "<SKILL_DIR>/scripts/run.mjs" status [--json]
66
68
  | `78` on `knowledge` | Two cases — **read stderr to tell them apart.** | Says the store is **disabled**: silent; proceed to `research`. Mentions **credentials/model/API key**: this is the same hard config failure as `78` on `research` — **stop and relay full stderr**; don't silently continue, `research` will fail the same way. |
67
69
  | `78` on `research` | Engine missing or no model/key | **Stop. Relay full stderr** (it shows where to configure). Don't retry until fixed. |
68
70
  | `75` | Machine at capacity — other research runs are already using every slot | **Nothing is broken.** Wait for an in-flight run to finish, then retry this query once. Don't reconfigure anything and don't retry in a tight loop. |
71
+ | `130` (and `129`/`131`/`143`) | Cancelled — the run was interrupted. These are the POSIX `128 + signal` codes: `130` Ctrl-C/SIGINT, `143` SIGTERM, `129` SIGHUP, `131` SIGQUIT. Treat **any exit ≥ 128 as a cancellation.** | **Nothing is broken and nothing failed.** Say the research was cancelled. **Do NOT re-run it** unless the user asks; they stopped it deliberately. |
69
72
  | `70` | Runtime error | Relay stderr; suggest one retry for transient failures (network, rate limit). |
70
73
  | `64` | Bad arguments | Fix args; don't surface unless the query itself is malformed. |
71
74
 
@@ -62,8 +62,12 @@ outranks both (the command says so when that happens).
62
62
 
63
63
  ### Exit codes
64
64
 
65
- `0` success · `64` bad arguments · `78` not configured (engine missing or no
66
- model/key; message lists the fix) · `70` runtime error (network/provider/internal).
65
+ `0` success · `64` bad arguments · `70` runtime error (network/provider/internal) ·
66
+ `75` machine at capacity — every concurrent-run slot is taken; wait and retry once ·
67
+ `78` not configured (engine missing or no model/key; message lists the fix).
68
+
69
+ A run stopped by a signal exits `128 + N`: `130` Ctrl-C (SIGINT), `143` SIGTERM,
70
+ `129` SIGHUP, `131` SIGQUIT. See SKILL.md for the full table.
67
71
 
68
72
  ### Troubleshooting
69
73
 
@@ -3,13 +3,18 @@
3
3
  // skills/pi-research/scripts/run.ts
4
4
  import { spawn } from "node:child_process";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- import { homedir } from "node:os";
6
+ import { homedir, constants as osConstants } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { createRequire } from "node:module";
9
9
  import { fileURLToPath } from "node:url";
10
10
  var PKG = "@lincoln504/pi-research";
11
11
  var REQUIRED_DEP = "@earendil-works/pi-coding-agent";
12
- var EXIT = { OK: 0, USAGE: 64, CONFIG: 78, SOFTWARE: 70 };
12
+ var EXIT = { OK: 0, USAGE: 64, CONFIG: 78, SOFTWARE: 70, CANCELLED: 130 };
13
+ var CANCELLING_SIGNALS = /* @__PURE__ */ new Set(["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"]);
14
+ function exitCodeForSignal(sig) {
15
+ const signum = osConstants.signals[sig];
16
+ return typeof signum === "number" ? 128 + signum : EXIT.CANCELLED;
17
+ }
13
18
  var argv = process.argv.slice(2);
14
19
  var subcommand = argv[0];
15
20
  if (!subcommand || subcommand === "-h" || subcommand === "--help" || subcommand === "help") {
@@ -199,6 +204,17 @@ Set PI_RESEARCH_BIN to the engine's node CLI (\u2026\\dist\\cli.mjs) or PI_RESEA
199
204
  ...useShell ? { shell: true } : {}
200
205
  }
201
206
  );
207
+ for (const sig of CANCELLING_SIGNALS) {
208
+ try {
209
+ process.on(sig, () => {
210
+ try {
211
+ child.kill(sig);
212
+ } catch {
213
+ }
214
+ });
215
+ } catch {
216
+ }
217
+ }
202
218
  child.on("error", (err) => {
203
219
  process.stderr.write(`
204
220
  Error: failed to launch pi-research (${engine2.label}): ${err.message}
@@ -207,6 +223,12 @@ Error: failed to launch pi-research (${engine2.label}): ${err.message}
207
223
  });
208
224
  child.on("exit", (code, signal) => {
209
225
  if (signal) {
226
+ if (CANCELLING_SIGNALS.has(signal)) {
227
+ process.stderr.write(`
228
+ pi-research cancelled (${signal}).
229
+ `);
230
+ process.exit(exitCodeForSignal(signal));
231
+ }
210
232
  process.stderr.write(`
211
233
  Error: pi-research killed by ${signal}
212
234
  `);
@@ -82,11 +82,19 @@ export function createCleanupFunction(
82
82
  const { resetLogger } = await import('../logger.ts');
83
83
  resetLogger(researchId);
84
84
 
85
- // FIX (#10): Clear session circuit breaker to prevent unbounded map growth.
85
+ // FIX (#10): Clear the session's browser circuit breaker to prevent unbounded
86
+ // map growth. Breakers are keyed by the plain researchId — createResearchRunId()
87
+ // generates it (`run-<8 hex>`) independently of piSessionId in
88
+ // research-tool-definition.ts, and tools/scrape.ts / tools/search.ts thread that
89
+ // same bare researchId into getBrowserCircuitBreaker() as BrowserTask.sessionId.
90
+ // (The `${piSessionId}-<8 hex>` shape this comment used to assume is produced
91
+ // only by generateSessionId(), which has no production caller.) This duplicates
92
+ // the clearSessionCircuitBreaker(targetId) call research-orchestration-service.ts
93
+ // already makes in its own finally block on the normal run-completion path; it
94
+ // matters here on paths where this cleanup runs alone, e.g. a throw before
95
+ // orchestrator.run() is ever reached.
86
96
  const { clearSessionCircuitBreaker } = await import('../infrastructure/browser/browser-error-utils.ts');
87
- if (piSessionId) {
88
- clearSessionCircuitBreaker(piSessionId);
89
- }
97
+ clearSessionCircuitBreaker(researchId);
90
98
 
91
99
  const activePanels = getPiActivePanels(piSessionId);
92
100
  if (activePanels.length === 0) {