@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.
- package/README.md +3 -3
- package/dist/cli.mjs +2599 -1268
- package/dist/thread-worker.mjs +224 -147
- package/docs/AGENT-SKILL.md +11 -4
- package/docs/ARCHITECTURE.md +20 -10
- package/docs/CHANGELOG.md +195 -17
- package/docs/CONFIGURATION.md +9 -7
- package/docs/KNOWLEDGE-STORE.md +4 -3
- package/docs/PI-EXTENSION.md +1 -1
- package/docs/SDK.md +55 -0
- package/docs/THIRD-PARTY-NOTICES.md +1 -1
- package/package.json +5 -4
- package/scripts/cleanup.cjs +114 -12
- package/scripts/prepare.cjs +15 -5
- package/scripts/setup.cjs +24 -4
- package/skills/pi-research/README.md +9 -2
- package/skills/pi-research/SKILL.md +6 -3
- package/skills/pi-research/references/configuration.md +6 -2
- package/skills/pi-research/scripts/run.mjs +24 -2
- package/src/cleanup/research-cleanup.ts +12 -4
- package/src/cli.ts +372 -86
- package/src/config.ts +134 -17
- package/src/constants.ts +31 -0
- package/src/core/interfaces/health-check-interfaces.ts +3 -39
- package/src/core/interfaces/knowledge-interfaces.ts +6 -1
- package/src/core/interfaces/planning-interfaces.ts +8 -0
- package/src/core/interfaces/service-names.ts +0 -1
- package/src/core/interfaces/state-manager-interfaces.ts +11 -3
- package/src/core/llm/agentic-repair.ts +14 -0
- package/src/core/pi-version.ts +136 -0
- package/src/core/planning-service.ts +43 -1
- package/src/core/service-initialization.ts +6 -14
- package/src/core/service-interfaces.ts +1 -1
- package/src/core/service-registry.ts +117 -8
- package/src/healthcheck/index.ts +7 -2
- package/src/index.ts +11 -18
- package/src/infrastructure/browser/browser-client.ts +80 -13
- package/src/infrastructure/browser/browser-error-utils.ts +6 -1
- package/src/infrastructure/browser/browser-server.ts +41 -12
- package/src/infrastructure/browser/browser-task-scheduler.ts +159 -35
- package/src/infrastructure/browser/cleanup-utils.ts +95 -9
- package/src/infrastructure/browser/client-agent.ts +6 -5
- package/src/infrastructure/browser/config.ts +14 -3
- package/src/infrastructure/browser/priority-task-queue.ts +10 -3
- package/src/infrastructure/browser/scheduler-factory.ts +44 -4
- package/src/infrastructure/browser/task-execution-service.ts +92 -28
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +50 -13
- package/src/infrastructure/browser/thread-worker-messaging.ts +54 -2
- package/src/infrastructure/browser/thread-worker-types.ts +3 -1
- package/src/infrastructure/browser/thread-worker.mjs +224 -147
- package/src/infrastructure/browser/worker-pool-manager.ts +18 -5
- package/src/infrastructure/embedding/embedding-client.ts +22 -8
- package/src/infrastructure/embedding/embedding-factory.ts +61 -15
- package/src/infrastructure/embedding/embedding-server.ts +105 -11
- package/src/infrastructure/file-lock-service.ts +295 -28
- package/src/infrastructure/knowledge-store-service.ts +48 -3
- package/src/infrastructure/process-lifecycle-service.ts +134 -34
- package/src/infrastructure/research-run-semaphore.ts +37 -3
- package/src/infrastructure/state/state-backup-manager.ts +5 -7
- package/src/infrastructure/state/state-browser-api.ts +5 -2
- package/src/infrastructure/state/state-browser-manager.ts +15 -1
- package/src/infrastructure/state/state-manager-service.ts +5 -5
- package/src/infrastructure/state/state-manager.ts +34 -23
- package/src/infrastructure/types/state-types.ts +9 -0
- package/src/knowledge/chunker.ts +88 -1
- package/src/knowledge/embedder-init.ts +1 -1
- package/src/knowledge/embedder.ts +188 -46
- package/src/knowledge/index.ts +1 -1
- package/src/knowledge/store.ts +101 -21
- package/src/knowledge/webgpu-viability.ts +9 -4
- package/src/knowledge/writer-queue.ts +88 -46
- package/src/logger.ts +5 -1
- package/src/observers/research-observer-impl.ts +31 -0
- package/src/orchestration/abort-utils.ts +60 -0
- package/src/orchestration/deep-research-orchestrator.ts +25 -4
- package/src/orchestration/headless-observer.ts +8 -0
- package/src/orchestration/quick-research-orchestrator.ts +2 -2
- package/src/orchestration/research-orchestration-service.ts +65 -15
- package/src/orchestration/research-session-service.ts +37 -11
- package/src/orchestration/research-synthesis-service.ts +33 -11
- package/src/orchestration/researcher-executor.ts +100 -20
- package/src/orchestration/researcher.ts +39 -20
- package/src/orchestration/session-state.ts +11 -0
- package/src/sdk.ts +125 -19
- package/src/security/cisa-kev.ts +10 -3
- package/src/security/github-advisories.ts +18 -7
- package/src/security/index.ts +51 -30
- package/src/security/nvd-client.ts +24 -8
- package/src/security/osv-client.ts +14 -4
- package/src/skill-install/skill-installer.ts +153 -11
- package/src/stackexchange/index.ts +20 -5
- package/src/stackexchange/output/table.ts +7 -2
- package/src/stackexchange/rest-client.ts +41 -3
- package/src/tools/research-knowledge-search.ts +15 -0
- package/src/tools/research-tool-definition.ts +8 -2
- package/src/tools/scrape.ts +48 -25
- package/src/tools/search.ts +16 -2
- package/src/tools/security.ts +68 -7
- package/src/tui/research-health.ts +18 -2
- package/src/utils/atomic-replace.ts +101 -0
- package/src/utils/circuit-breaker.ts +9 -2
- package/src/utils/citation-utils.ts +3 -1
- package/src/utils/disk-space-checker.ts +11 -6
- package/src/utils/http-body.ts +176 -0
- package/src/utils/json-utils.ts +116 -76
- package/src/utils/llm-usage.ts +17 -1
- package/src/utils/log-rotation.ts +42 -10
- package/src/utils/log-utils.ts +40 -0
- package/src/utils/metrics-summary.ts +27 -9
- package/src/utils/stdio-capture.ts +63 -22
- package/src/utils/text-utils.ts +8 -1
- package/src/utils/tool-usage-tracker.ts +9 -2
- package/src/web-research/browser-search.ts +70 -5
- package/src/web-research/scraper-utils.ts +13 -0
- package/src/web-research/search.ts +25 -6
- package/src/web-research/types.ts +19 -4
- package/src/web-research/web-scraper.ts +72 -54
- package/src/youtube/transcript-client.ts +5 -1
- package/src/core/health-check-service.ts +0 -265
package/scripts/cleanup.cjs
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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);
|
package/scripts/prepare.cjs
CHANGED
|
@@ -15,14 +15,24 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
|
|
21
21
|
const ROOT = path.join(__dirname, '..');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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.**
|
|
59
|
-
|
|
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 · `
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
clearSessionCircuitBreaker(piSessionId);
|
|
89
|
-
}
|
|
97
|
+
clearSessionCircuitBreaker(researchId);
|
|
90
98
|
|
|
91
99
|
const activePanels = getPiActivePanels(piSessionId);
|
|
92
100
|
if (activePanels.length === 0) {
|