@lincoln504/pi-research 1.1.2 → 1.2.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 +1287 -540
- package/dist/thread-worker.mjs +30 -11
- package/docs/AGENT-SKILL.md +11 -4
- package/docs/ARCHITECTURE.md +19 -9
- package/docs/CHANGELOG.md +81 -1
- 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 +50 -10
- 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/cli.ts +149 -21
- package/src/config.ts +56 -15
- package/src/constants.ts +31 -0
- package/src/core/interfaces/knowledge-interfaces.ts +6 -1
- package/src/core/interfaces/state-manager-interfaces.ts +1 -1
- package/src/core/pi-version.ts +136 -0
- package/src/core/service-registry.ts +117 -8
- package/src/index.ts +11 -18
- package/src/infrastructure/browser/browser-client.ts +16 -4
- package/src/infrastructure/browser/browser-error-utils.ts +6 -1
- package/src/infrastructure/browser/browser-server.ts +17 -5
- package/src/infrastructure/browser/browser-task-scheduler.ts +101 -23
- package/src/infrastructure/browser/cleanup-utils.ts +45 -7
- package/src/infrastructure/browser/client-agent.ts +4 -3
- package/src/infrastructure/browser/priority-task-queue.ts +10 -3
- package/src/infrastructure/browser/scheduler-factory.ts +26 -1
- 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.mjs +30 -11
- package/src/infrastructure/browser/worker-pool-manager.ts +11 -3
- package/src/infrastructure/embedding/embedding-client.ts +22 -8
- package/src/infrastructure/embedding/embedding-factory.ts +28 -11
- package/src/infrastructure/embedding/embedding-server.ts +65 -4
- package/src/infrastructure/file-lock-service.ts +223 -26
- package/src/infrastructure/process-lifecycle-service.ts +108 -31
- package/src/infrastructure/state/state-backup-manager.ts +5 -7
- package/src/infrastructure/state/state-manager-service.ts +1 -1
- package/src/infrastructure/state/state-manager.ts +19 -20
- package/src/infrastructure/types/state-types.ts +9 -0
- package/src/knowledge/embedder-init.ts +1 -1
- package/src/knowledge/embedder.ts +52 -3
- package/src/knowledge/index.ts +1 -1
- package/src/knowledge/store.ts +35 -9
- package/src/knowledge/webgpu-viability.ts +9 -4
- package/src/knowledge/writer-queue.ts +88 -46
- package/src/orchestration/deep-research-orchestrator.ts +13 -2
- package/src/orchestration/quick-research-orchestrator.ts +2 -2
- package/src/orchestration/research-orchestration-service.ts +2 -4
- package/src/orchestration/research-synthesis-service.ts +33 -11
- package/src/security/cisa-kev.ts +10 -3
- package/src/security/github-advisories.ts +18 -7
- package/src/security/nvd-client.ts +17 -6
- package/src/security/osv-client.ts +14 -4
- package/src/skill-install/skill-installer.ts +135 -9
- package/src/stackexchange/rest-client.ts +6 -2
- package/src/tools/research-knowledge-search.ts +15 -0
- package/src/tools/search.ts +6 -1
- package/src/utils/atomic-replace.ts +101 -0
- package/src/utils/citation-utils.ts +3 -1
- package/src/utils/http-body.ts +176 -0
- package/src/web-research/browser-search.ts +50 -4
- package/src/web-research/search.ts +20 -5
- package/src/web-research/types.ts +17 -4
- package/src/web-research/web-scraper.ts +7 -50
- package/src/youtube/transcript-client.ts +5 -1
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.2.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
|
`);
|
package/src/cli.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import * as fs from 'node:fs';
|
|
25
|
+
import * as os from 'node:os';
|
|
25
26
|
import * as path from 'node:path';
|
|
26
27
|
import { fileURLToPath } from 'node:url';
|
|
27
28
|
|
|
@@ -84,8 +85,47 @@ export const EXIT = {
|
|
|
84
85
|
* re-running a broken command.
|
|
85
86
|
*/
|
|
86
87
|
TEMPFAIL: 75,
|
|
88
|
+
/**
|
|
89
|
+
* The run was CANCELLED via a programmatic abort (the caller's AbortSignal),
|
|
90
|
+
* with no OS signal involved. 130 — 128+SIGINT — is the conventional
|
|
91
|
+
* "interrupted" code and the natural default when there is no signal number to
|
|
92
|
+
* derive one from.
|
|
93
|
+
*
|
|
94
|
+
* A cancellation caused by an actual SIGNAL does NOT use this constant: see
|
|
95
|
+
* exitCodeForSignal below, which reports the true 128+N.
|
|
96
|
+
*
|
|
97
|
+
* Distinct from SOFTWARE for the same reason TEMPFAIL is: a deliberate stop is
|
|
98
|
+
* not a fault. Reported as SOFTWARE (70), a cancel matched the skill contract's
|
|
99
|
+
* "runtime error — suggest one retry for transient failures", so an agent
|
|
100
|
+
* relaying the result would offer to re-run the very research the user had just
|
|
101
|
+
* interrupted, and a --json consumer could not tell a cancel from a crash.
|
|
102
|
+
*/
|
|
103
|
+
CANCELLED: 130,
|
|
87
104
|
} as const;
|
|
88
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Exit code for a run stopped by `sig`, following the POSIX 128+N convention that
|
|
108
|
+
* a shell uses when reporting a signal-terminated child (SIGINT → 130,
|
|
109
|
+
* SIGTERM → 143, SIGHUP → 129, SIGQUIT → 131).
|
|
110
|
+
*
|
|
111
|
+
* Why derive it per-signal instead of returning a single "cancelled" code: this
|
|
112
|
+
* process INSTALLS handlers for those signals, so it is never actually killed by
|
|
113
|
+
* them — it catches them and exits deliberately. If it exited a flat 130, the code
|
|
114
|
+
* a wrapper observes would depend on whether our handler won the race against a
|
|
115
|
+
* force-kill: handler wins → 130, handler loses → the shell's 143. Deriving 128+N
|
|
116
|
+
* makes the handler transparent, so the same user action reports the same number
|
|
117
|
+
* either way.
|
|
118
|
+
*
|
|
119
|
+
* Every value lands in 129..143, safely inside the 0-255 range a POSIX wait status
|
|
120
|
+
* preserves — nothing here can be truncated to a false 0. Windows does not
|
|
121
|
+
* implement the 128+N convention at all (its exit codes are 32-bit and carry no
|
|
122
|
+
* signal semantics), so there these are simply our documented contract values.
|
|
123
|
+
*/
|
|
124
|
+
export function exitCodeForSignal(sig: NodeJS.Signals): number {
|
|
125
|
+
const signum = (os.constants.signals as Record<string, number | undefined>)[sig];
|
|
126
|
+
return typeof signum === 'number' ? 128 + signum : EXIT.CANCELLED;
|
|
127
|
+
}
|
|
128
|
+
|
|
89
129
|
// ---------------------------------------------------------------------------
|
|
90
130
|
// Output helpers
|
|
91
131
|
// ---------------------------------------------------------------------------
|
|
@@ -190,20 +230,33 @@ export function bridgeConfigEnv(explicitConfigPath?: string): { path: string; lo
|
|
|
190
230
|
// iteration and the "don't clobber the real environment" guard would then
|
|
191
231
|
// block the overlay's value — cli.env could never override config.env.
|
|
192
232
|
const merged: Record<string, string> = {};
|
|
233
|
+
// Keys whose winning value came from an EXPLICIT --config file (not the
|
|
234
|
+
// default base, not the cli.env overlay). Project-scoped keys are only
|
|
235
|
+
// bridged for these — see below.
|
|
236
|
+
const fromExplicitBase = new Set<string>();
|
|
193
237
|
for (const filePath of [basePath, cliOverlayPath]) {
|
|
194
238
|
try {
|
|
195
239
|
if (!fs.existsSync(filePath)) continue;
|
|
196
|
-
|
|
240
|
+
const parsed = parseDotEnv(fs.readFileSync(filePath, 'utf-8'));
|
|
241
|
+
Object.assign(merged, parsed);
|
|
242
|
+
for (const k of Object.keys(parsed)) {
|
|
243
|
+
if (explicitConfigPath !== undefined && filePath === basePath) fromExplicitBase.add(k);
|
|
244
|
+
else fromExplicitBase.delete(k);
|
|
245
|
+
}
|
|
197
246
|
loaded = true;
|
|
198
247
|
} catch {
|
|
199
248
|
// ignore unreadable file; fall through to the next
|
|
200
249
|
}
|
|
201
250
|
}
|
|
202
251
|
for (const [k, v] of Object.entries(merged)) {
|
|
203
|
-
// Skip per-directory (project-scoped) keys: promoting them into
|
|
204
|
-
// config.env out-rank the per-directory registry, defeating a
|
|
205
|
-
// still applied as a lower-precedence file layer by
|
|
206
|
-
|
|
252
|
+
// Skip per-directory (project-scoped) keys from the AMBIENT files: promoting them into
|
|
253
|
+
// process.env would make config.env out-rank the per-directory registry, defeating a
|
|
254
|
+
// per-cwd override. They are still applied as a lower-precedence file layer by
|
|
255
|
+
// getConfig()/loadEnvFiles(). An EXPLICIT --config file is different: getConfig never
|
|
256
|
+
// reads that path, so skipping its keys silently dropped them (a depth or
|
|
257
|
+
// knowledge-mode set in `--config ci.env` simply did not apply) — and a file the user
|
|
258
|
+
// named on this exact invocation outranking the per-directory registry is the intent.
|
|
259
|
+
if (isProjectScopedKey(k) && !fromExplicitBase.has(k)) continue;
|
|
207
260
|
if (process.env[k] === undefined && (k.startsWith('PI_RESEARCH_') || k === 'STACKEXCHANGE_API_KEY' || k === 'GITHUB_TOKEN' || k === 'NVD_API_KEY')) {
|
|
208
261
|
process.env[k] = v;
|
|
209
262
|
}
|
|
@@ -863,9 +916,25 @@ export async function reportError(err: unknown, what: string, json?: boolean): P
|
|
|
863
916
|
// machine as a setup problem would send the user to reconfigure a working install.
|
|
864
917
|
const isCapacity = err instanceof Error && err.name === 'ResearchRunCapacityError';
|
|
865
918
|
|
|
919
|
+
// Cancellation. Primary signal is the latch set by the SIGINT/SIGTERM handler —
|
|
920
|
+
// an exact fact, unlike the message, which varies by whichever subsystem noticed
|
|
921
|
+
// the abort first ('Aborted' from the search layer, 'Research aborted' /
|
|
922
|
+
// 'Research cancelled' from the orchestrator). The anchored message signatures
|
|
923
|
+
// only cover a programmatic abort where no signal arrived. Deliberately NOT
|
|
924
|
+
// matched: a bare `err.name === 'AbortError'` — internal timeout aborts
|
|
925
|
+
// (AbortSignal-driven retry/scrape deadlines) throw that name too, and an
|
|
926
|
+
// escaped one would report "cancelled — do not re-run" for a run nobody
|
|
927
|
+
// stopped. In the CLI every real cancellation sets the latch or carries these
|
|
928
|
+
// exact signatures. Checked before everything below: an abort can unwind
|
|
929
|
+
// through a provider call and surface wording that the config/rate-limit
|
|
930
|
+
// heuristics would otherwise claim.
|
|
931
|
+
const isCancellation =
|
|
932
|
+
cancellationRequested ||
|
|
933
|
+
(err instanceof Error && /^(aborted|research (aborted|cancelled))$/i.test(msg));
|
|
934
|
+
|
|
866
935
|
// Auth / model / config-shaped errors → CONFIG exit + config block.
|
|
867
936
|
const isConfigError =
|
|
868
|
-
!isTaggedResearchStop && !isCapacity && (
|
|
937
|
+
!isTaggedResearchStop && !isCapacity && !isCancellation && (
|
|
869
938
|
lower.includes('no llm model available') ||
|
|
870
939
|
lower.includes('not found in pi') ||
|
|
871
940
|
lower.includes('no api key') ||
|
|
@@ -879,20 +948,27 @@ export async function reportError(err: unknown, what: string, json?: boolean): P
|
|
|
879
948
|
// "api key" in its body (e.g. "rate limit exceeded for this api key"), and
|
|
880
949
|
// classifying that as a setup problem sends the user to reconfigure a
|
|
881
950
|
// perfectly working key instead of waiting out the limit.
|
|
882
|
-
const isRateLimit = lower.includes('429') || lower.includes('rate limit') || lower.includes('too many requests');
|
|
883
|
-
const exitCode =
|
|
884
|
-
? EXIT.
|
|
885
|
-
:
|
|
886
|
-
? EXIT.
|
|
887
|
-
:
|
|
951
|
+
const isRateLimit = !isCancellation && (lower.includes('429') || lower.includes('rate limit') || lower.includes('too many requests'));
|
|
952
|
+
const exitCode = isCancellation
|
|
953
|
+
? (cancellationSignal ? exitCodeForSignal(cancellationSignal) : EXIT.CANCELLED)
|
|
954
|
+
: isCapacity
|
|
955
|
+
? EXIT.TEMPFAIL
|
|
956
|
+
: isConfigError && !isRateLimit
|
|
957
|
+
? EXIT.CONFIG
|
|
958
|
+
: EXIT.SOFTWARE;
|
|
888
959
|
|
|
889
960
|
// --json: emit a structured error on stdout so a machine consumer never has to
|
|
890
961
|
// special-case plain-text stderr. Mirrors the { ok: true, ... } success shape.
|
|
891
962
|
if (json) {
|
|
892
963
|
const payload: Record<string, unknown> = { ok: false, error: msg, exitCode };
|
|
893
964
|
// Machine-readable "this will succeed later" flag, so a consumer can back off
|
|
894
|
-
// and retry without string-matching the human message.
|
|
895
|
-
|
|
965
|
+
// and retry without string-matching the human message. Gated on cancellation:
|
|
966
|
+
// a Ctrl-C landing in the same window a queue-wait expires must never emit
|
|
967
|
+
// `cancelled: true` alongside `retryable: true` — a consumer honouring the
|
|
968
|
+
// latter would re-run the research the user just stopped.
|
|
969
|
+
if ((isCapacity || isRateLimit) && !isCancellation) payload['retryable'] = true;
|
|
970
|
+
// A cancel is emphatically NOT retryable-by-default: the user asked to stop.
|
|
971
|
+
if (isCancellation) payload['cancelled'] = true;
|
|
896
972
|
if (process.env['PI_RESEARCH_DEBUG'] === 'true' && err instanceof Error && err.stack) {
|
|
897
973
|
payload['stack'] = err.stack;
|
|
898
974
|
}
|
|
@@ -900,6 +976,13 @@ export async function reportError(err: unknown, what: string, json?: boolean): P
|
|
|
900
976
|
return exitCode;
|
|
901
977
|
}
|
|
902
978
|
|
|
979
|
+
// Cancellation is a completed intention, not a failure — never a stack trace,
|
|
980
|
+
// and never the word "failed".
|
|
981
|
+
if (isCancellation) {
|
|
982
|
+
toStderr(`\npi-research ${what} cancelled.\n`);
|
|
983
|
+
return exitCode;
|
|
984
|
+
}
|
|
985
|
+
|
|
903
986
|
// Capacity exhaustion is a healthy-machine "busy" state — report it as such and
|
|
904
987
|
// never with a stack trace, which would read as a crash for a routine condition.
|
|
905
988
|
if (isCapacity) {
|
|
@@ -1024,12 +1107,14 @@ CONFIGURE
|
|
|
1024
1107
|
Run \`${BINARY_NAME} status\` to see exactly what is detected.
|
|
1025
1108
|
|
|
1026
1109
|
EXIT CODES
|
|
1027
|
-
0
|
|
1028
|
-
64
|
|
1029
|
-
75
|
|
1030
|
-
78
|
|
1031
|
-
|
|
1032
|
-
|
|
1110
|
+
0 success
|
|
1111
|
+
64 bad arguments
|
|
1112
|
+
75 at capacity — every concurrent run slot is busy; nothing is broken, retry later
|
|
1113
|
+
78 not configured — message prints the exact locations to fix
|
|
1114
|
+
(on \`knowledge\`, also returned when the store is deliberately disabled)
|
|
1115
|
+
130 cancelled — Ctrl-C; nothing failed, do not auto-retry
|
|
1116
|
+
(other stop signals report POSIX 128+N: 143 SIGTERM, 129 SIGHUP, 131 SIGQUIT)
|
|
1117
|
+
70 runtime error (network, provider, internal)
|
|
1033
1118
|
`;
|
|
1034
1119
|
}
|
|
1035
1120
|
|
|
@@ -1269,6 +1354,32 @@ const flushAndExit = (code: number): void => {
|
|
|
1269
1354
|
// outlives that one run.
|
|
1270
1355
|
let activeResearchAbortController: AbortController | null = null;
|
|
1271
1356
|
|
|
1357
|
+
/**
|
|
1358
|
+
* Set once SIGINT/SIGTERM has been handled. Read by reportError so a cancellation
|
|
1359
|
+
* is classified by the fact that we were signalled, not by pattern-matching the
|
|
1360
|
+
* message an aborted subsystem happened to throw.
|
|
1361
|
+
*/
|
|
1362
|
+
let cancellationRequested = false;
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* WHICH signal fired — so a cancellation that unwinds through reportError
|
|
1366
|
+
* reports the same 128+N the signal path itself would (SIGTERM → 143, not a
|
|
1367
|
+
* flat 130). reportError races onSignal's own flushAndExit; latching the name
|
|
1368
|
+
* makes both racers agree on the number, and keeps the `exitCode` field in
|
|
1369
|
+
* `--json` from contradicting the actual exit status.
|
|
1370
|
+
*/
|
|
1371
|
+
let cancellationSignal: NodeJS.Signals | null = null;
|
|
1372
|
+
|
|
1373
|
+
/** Test seams for the module-level cancellation latch (set only by onSignal). */
|
|
1374
|
+
export function _resetCancellationLatchForTests(): void {
|
|
1375
|
+
cancellationRequested = false;
|
|
1376
|
+
cancellationSignal = null;
|
|
1377
|
+
}
|
|
1378
|
+
export function _markCancellationForTests(sig?: NodeJS.Signals): void {
|
|
1379
|
+
cancellationRequested = true;
|
|
1380
|
+
cancellationSignal = sig ?? null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1272
1383
|
async function main(argv: string[]): Promise<number> {
|
|
1273
1384
|
// Register signal handlers so SIGINT/SIGTERM always trigger SDK shutdown
|
|
1274
1385
|
// before the process exits. safeShutdown() is idempotent — safe to call
|
|
@@ -1277,6 +1388,8 @@ async function main(argv: string[]): Promise<number> {
|
|
|
1277
1388
|
const onSignal = (sig: string) => {
|
|
1278
1389
|
if (_signalCleanupDone) return;
|
|
1279
1390
|
_signalCleanupDone = true;
|
|
1391
|
+
cancellationRequested = true;
|
|
1392
|
+
cancellationSignal = sig as NodeJS.Signals;
|
|
1280
1393
|
toStderr(`\n[pi-research] ${sig} — cleaning up…\n`);
|
|
1281
1394
|
// Abort the in-flight research run FIRST (synchronously ahead of the shutdown
|
|
1282
1395
|
// teardown below) so the orchestrator's existing signal-aware cancellation path
|
|
@@ -1286,10 +1399,25 @@ async function main(argv: string[]): Promise<number> {
|
|
|
1286
1399
|
// Fire-and-forget: we can't await here, but safeShutdown swallows errors
|
|
1287
1400
|
// and the finally blocks in each command handler also call it. Drain via
|
|
1288
1401
|
// flushAndExit (not a bare process.exit) so buffered stdout isn't truncated.
|
|
1289
|
-
|
|
1402
|
+
// 128+N, not a flat CANCELLED: see exitCodeForSignal. Because we handle these
|
|
1403
|
+
// signals rather than dying from them, a fixed code would make the observed
|
|
1404
|
+
// exit status depend on whether this handler beat a force-kill.
|
|
1405
|
+
void safeShutdown().finally(() => flushAndExit(exitCodeForSignal(sig as NodeJS.Signals)));
|
|
1290
1406
|
};
|
|
1291
1407
|
process.once('SIGINT', () => onSignal('SIGINT'));
|
|
1292
1408
|
process.once('SIGTERM', () => onSignal('SIGTERM'));
|
|
1409
|
+
// SIGHUP (terminal closed) and, on Windows, SIGBREAK (Ctrl+Break). Without these
|
|
1410
|
+
// the run dies without cancelling: no abort of the in-flight research, no browser
|
|
1411
|
+
// or embedding-server teardown, and no 128+N exit code. On Windows it matters
|
|
1412
|
+
// most — SIGTERM is never delivered there (process.kill maps to TerminateProcess),
|
|
1413
|
+
// so Ctrl-C and Ctrl+Break are the only signals that reach this handler at all.
|
|
1414
|
+
// The SDK already registers both (see _addSignalHandlers); the CLI did not, which
|
|
1415
|
+
// left the SDK's handler to tear the container down underneath a live orchestrator
|
|
1416
|
+
// — exactly the race the CLI installs its own handlers to avoid.
|
|
1417
|
+
process.once('SIGHUP', () => onSignal('SIGHUP'));
|
|
1418
|
+
if (process.platform === 'win32') {
|
|
1419
|
+
process.once('SIGBREAK', () => onSignal('SIGBREAK'));
|
|
1420
|
+
}
|
|
1293
1421
|
|
|
1294
1422
|
let parsed: ParsedArgs;
|
|
1295
1423
|
try {
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getConfigDirName } from './utils/host-config.ts';
|
|
|
14
14
|
import { Type, type Static } from 'typebox';
|
|
15
15
|
import { Value } from 'typebox/value';
|
|
16
16
|
import { normalizeWorkspacePath } from './utils/text-utils.ts';
|
|
17
|
+
import { replaceFileSync } from './utils/atomic-replace.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Validates configuration schema using TypeBox.
|
|
@@ -501,7 +502,7 @@ export function describeKnowledgeStoreMode(
|
|
|
501
502
|
let registryHasKey = false;
|
|
502
503
|
try {
|
|
503
504
|
const registry = loadProjectSettingsRegistry();
|
|
504
|
-
registryHasKey = registry
|
|
505
|
+
registryHasKey = findRegistryEntry(registry, normalizeWorkspacePath(cwd))?.[KEY] !== undefined;
|
|
505
506
|
} catch { /* unreadable registry → not the origin */ }
|
|
506
507
|
if (registryHasKey) origin = 'this directory (project settings)';
|
|
507
508
|
else if (iface && fileHasKey(getInterfaceEnvFilePath(iface))) origin = `${iface}.env overlay`;
|
|
@@ -546,12 +547,48 @@ export function parseDotEnv(content: string): Record<string, string> {
|
|
|
546
547
|
* Load environment variables from global and local files.
|
|
547
548
|
* Order: Global File < Interface overlay < Legacy .env < Centralized Registry (WINS)
|
|
548
549
|
*/
|
|
550
|
+
/**
|
|
551
|
+
* Look up a directory's entry in the project-settings registry, tolerating case.
|
|
552
|
+
*
|
|
553
|
+
* On Windows and on macOS's default (case-insensitive) filesystem, `C:\\Users\\Bob\\Proj`
|
|
554
|
+
* and `c:\\users\\bob\\proj` are the SAME directory but different object keys, so a shell
|
|
555
|
+
* that reports a different casing than the one the settings were saved under made those
|
|
556
|
+
* settings silently vanish. normalizeWorkspacePath only resolves and strips a trailing
|
|
557
|
+
* separator; it does not fold case, and it must not start doing so — the key it produces
|
|
558
|
+
* is persisted, so folding it would orphan every entry already on disk.
|
|
559
|
+
*
|
|
560
|
+
* Resolving at READ time instead fixes the lookup without touching stored data: exact
|
|
561
|
+
* match first (the only path taken on Linux, and the fast path everywhere), then a
|
|
562
|
+
* case-insensitive scan as a fallback.
|
|
563
|
+
*/
|
|
564
|
+
function findRegistryKey(
|
|
565
|
+
registry: Record<string, Record<string, string>>,
|
|
566
|
+
normalizedCwd: string,
|
|
567
|
+
): string | undefined {
|
|
568
|
+
if (registry[normalizedCwd] !== undefined) return normalizedCwd;
|
|
569
|
+
// Only case-insensitive filesystems can produce this mismatch; skip the scan elsewhere.
|
|
570
|
+
if (process.platform !== 'win32' && process.platform !== 'darwin') return undefined;
|
|
571
|
+
const lowered = normalizedCwd.toLowerCase();
|
|
572
|
+
for (const key of Object.keys(registry)) {
|
|
573
|
+
if (key.toLowerCase() === lowered) return key;
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function findRegistryEntry(
|
|
579
|
+
registry: Record<string, Record<string, string>>,
|
|
580
|
+
normalizedCwd: string,
|
|
581
|
+
): Record<string, string> | undefined {
|
|
582
|
+
const key = findRegistryKey(registry, normalizedCwd);
|
|
583
|
+
return key === undefined ? undefined : registry[key];
|
|
584
|
+
}
|
|
585
|
+
|
|
549
586
|
function loadEnvFiles(cwd: string, iface?: ConfigInterface): Record<string, string> {
|
|
550
587
|
const merged: Record<string, string> = {};
|
|
551
588
|
const globalPath = getGlobalEnvFilePath();
|
|
552
589
|
const registry = loadProjectSettingsRegistry();
|
|
553
590
|
const normalizedCwd = normalizeWorkspacePath(cwd);
|
|
554
|
-
const homeRegistry = registry
|
|
591
|
+
const homeRegistry = findRegistryEntry(registry, normalizeWorkspacePath(os.homedir()));
|
|
555
592
|
|
|
556
593
|
// 1. ONE-TIME MIGRATION: Populate config.env from HOME registry if missing
|
|
557
594
|
try {
|
|
@@ -613,15 +650,17 @@ function loadEnvFiles(cwd: string, iface?: ConfigInterface): Record<string, stri
|
|
|
613
650
|
Object.entries(legacyEnv).filter(([k]) => LOCAL_SCOPE_KEYS.has(k))
|
|
614
651
|
);
|
|
615
652
|
const existingLocal = Object.fromEntries(
|
|
616
|
-
Object.entries(registry
|
|
653
|
+
Object.entries(findRegistryEntry(registry, normalizedCwd) ?? {}).filter(([k]) => LOCAL_SCOPE_KEYS.has(k))
|
|
617
654
|
);
|
|
618
655
|
if (Object.keys(legacyLocal).length > 0 && JSON.stringify(existingLocal) !== JSON.stringify(legacyLocal)) {
|
|
619
656
|
logger.info(`[config] Migrating legacy .pi-research.env settings from ${cwd} to central registry...`);
|
|
620
657
|
// Persist under the registry lock, reading the latest on-disk state so a concurrent
|
|
621
658
|
// write isn't lost; also reflect the merge in the in-memory `registry` used just below.
|
|
622
|
-
|
|
659
|
+
const mergeKey = findRegistryKey(registry, normalizedCwd) ?? normalizedCwd;
|
|
660
|
+
registry[mergeKey] = { ...registry[mergeKey], ...legacyLocal };
|
|
623
661
|
updateProjectSettingsRegistry((disk) => {
|
|
624
|
-
|
|
662
|
+
const diskKey = findRegistryKey(disk, normalizedCwd) ?? normalizedCwd;
|
|
663
|
+
disk[diskKey] = { ...disk[diskKey], ...legacyLocal };
|
|
625
664
|
});
|
|
626
665
|
}
|
|
627
666
|
} catch (err) {
|
|
@@ -633,8 +672,9 @@ function loadEnvFiles(cwd: string, iface?: ConfigInterface): Record<string, stri
|
|
|
633
672
|
// LOCAL_SCOPE_KEYS are applied (and may override), never user-scoped keys — those belong to
|
|
634
673
|
// config.env. Any non-local key present here is stale pollution (e.g. a pre-fix legacy
|
|
635
674
|
// migration) and is ignored rather than allowed to override the user's global settings.
|
|
636
|
-
|
|
637
|
-
|
|
675
|
+
const cwdEntry = findRegistryEntry(registry, normalizedCwd);
|
|
676
|
+
if (cwdEntry) {
|
|
677
|
+
for (const [key, val] of Object.entries(cwdEntry)) {
|
|
638
678
|
if (LOCAL_SCOPE_KEYS.has(key)) {
|
|
639
679
|
// Don't interpolate values in warnings — a config key could carry a secret and
|
|
640
680
|
// redactSecrets won't catch short/non-standard tokens. Key + winner is enough.
|
|
@@ -763,7 +803,11 @@ export function saveConfig(config: Config, scope: 'local' | 'user' = 'local', cw
|
|
|
763
803
|
// Read-merge-write under one lock so a concurrent local write to another directory
|
|
764
804
|
// isn't lost (the merge onto the existing entry happens against the latest on-disk state).
|
|
765
805
|
updateProjectSettingsRegistry((registry) => {
|
|
766
|
-
|
|
806
|
+
// Merge into the entry that already represents THIS directory, even if it was
|
|
807
|
+
// written with different casing on a case-insensitive filesystem. Writing a
|
|
808
|
+
// second key would leave the earlier settings stranded and unreachable.
|
|
809
|
+
const key = findRegistryKey(registry, normalizedCwd) ?? normalizedCwd;
|
|
810
|
+
registry[key] = { ...(registry[key] ?? {}), ...newValues };
|
|
767
811
|
});
|
|
768
812
|
|
|
769
813
|
logger.debug(`[config] Saved project settings for ${normalizedCwd} to central registry.`);
|
|
@@ -870,15 +914,12 @@ export function saveConfig(config: Config, scope: 'local' | 'user' = 'local', cw
|
|
|
870
914
|
const tmpPath = `${p}.tmp.${process.pid}.${Date.now()}`;
|
|
871
915
|
fs.writeFileSync(tmpPath, outLines.join('\n'), { encoding: 'utf-8', mode: 0o600 });
|
|
872
916
|
try {
|
|
873
|
-
|
|
874
|
-
} catch (renameErr) {
|
|
875
|
-
if (process.platform === 'win32') {
|
|
876
|
-
fs.copyFileSync(tmpPath, p);
|
|
917
|
+
if (replaceFileSync(tmpPath, p) === 'copied') {
|
|
877
918
|
try { fs.unlinkSync(tmpPath); } catch { /* best effort */ }
|
|
878
|
-
} else {
|
|
879
|
-
try { fs.unlinkSync(tmpPath); } catch { /* best effort */ }
|
|
880
|
-
throw renameErr;
|
|
881
919
|
}
|
|
920
|
+
} catch (renameErr) {
|
|
921
|
+
try { fs.unlinkSync(tmpPath); } catch { /* best effort */ }
|
|
922
|
+
throw renameErr;
|
|
882
923
|
}
|
|
883
924
|
// Tighten regardless of how the file came to exist (fresh rename, Windows copy over
|
|
884
925
|
// an existing file, or a pre-hardening 0644 file that the rename replaced-in-place).
|
package/src/constants.ts
CHANGED
|
@@ -177,3 +177,34 @@ export const RESEARCHER_LAUNCH_DELAY_MS = 1500;
|
|
|
177
177
|
export const MAX_QUERIES_PER_RESEARCHER_LEVEL_1 = 10;
|
|
178
178
|
export const MAX_QUERIES_PER_RESEARCHER_LEVEL_2 = 15;
|
|
179
179
|
export const MAX_QUERIES_PER_RESEARCHER_LEVEL_3 = 20;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Tools excluded from every researcher unless the caller says otherwise.
|
|
183
|
+
*
|
|
184
|
+
* `grep` is developer-only: during WEB research a high-capability model otherwise
|
|
185
|
+
* spends turns searching the local filesystem instead of the internet.
|
|
186
|
+
*/
|
|
187
|
+
export const DEFAULT_EXCLUDED_TOOLS: readonly string[] = ['grep'];
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the effective researcher exclusion list.
|
|
191
|
+
*
|
|
192
|
+
* An EMPTY array is "no preference", not "exclude nothing". The two front-ends
|
|
193
|
+
* express no-preference differently — the pi extension always builds an array
|
|
194
|
+
* (`[...new Set([...])]`, often empty) while the CLI omits the key entirely — so a
|
|
195
|
+
* plain truthiness test made the same user input mean opposite things: grep was
|
|
196
|
+
* available in the extension and excluded on the CLI, regardless of depth.
|
|
197
|
+
*
|
|
198
|
+
* `configDisabled` (PI_RESEARCH_DISABLED_TOOLS) is strictly ADDITIVE. Folding it in
|
|
199
|
+
* as a replacement meant disabling any unrelated tool silently ENABLED grep, so a
|
|
200
|
+
* subtract-only setting could grant a capability.
|
|
201
|
+
*/
|
|
202
|
+
export function resolveExcludedTools(
|
|
203
|
+
callerExclusions: readonly string[] | undefined,
|
|
204
|
+
configDisabled: readonly string[] = [],
|
|
205
|
+
): string[] {
|
|
206
|
+
const base = callerExclusions && callerExclusions.length > 0
|
|
207
|
+
? callerExclusions
|
|
208
|
+
: DEFAULT_EXCLUDED_TOOLS;
|
|
209
|
+
return [...new Set([...base, ...configDisabled])];
|
|
210
|
+
}
|
|
@@ -120,7 +120,12 @@ export interface IKnowledgeStore extends IService {
|
|
|
120
120
|
/** Extended operations */
|
|
121
121
|
addDocuments(docs: StoreDocument[]): Promise<void>;
|
|
122
122
|
deleteByUrl(url: string): Promise<void>;
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* @param olderThan When set, only rows with `timestamp < olderThan` are removed —
|
|
125
|
+
* lets a refresh insert its replacement first and prune the superseded rows
|
|
126
|
+
* afterwards, so a failed insert can never leave the URL with nothing.
|
|
127
|
+
*/
|
|
128
|
+
deleteByUrlAndType(url: string, ingestionType: string, olderThan?: number): Promise<void>;
|
|
124
129
|
isStoreClosed?(): boolean;
|
|
125
130
|
|
|
126
131
|
/**
|