@lincoln504/pi-research 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ pi install npm:@lincoln504/pi-research
63
63
 
64
64
  See [Agent skill](docs/AGENT-SKILL.md) or [SDK](docs/SDK.md) for standalone (no pi extension) installation.
65
65
 
66
- Then configure a model and key — set `PI_RESEARCH_MODEL` (e.g. `openai/gpt-4o`) plus `PI_RESEARCH_API_KEY` and `PI_RESEARCH_PROVIDER`, either as environment variables or in `~/.pi/research/config.env`. See [Configuration](docs/CONFIGURATION.md). (The pi extension needs no key — it uses pi's own auth.)
66
+ The pi extension works out of the box — it runs on your pi session's model and pi's own configuration. Standalone use (agent skill / CLI / SDK) requires a configured model: set `PI_RESEARCH_MODEL` (e.g. `openai/gpt-4o`) as an environment variable or in `~/.pi/research/config.env`, plus `PI_RESEARCH_API_KEY` if you don't use pi. See [Configuration](docs/CONFIGURATION.md).
67
67
 
68
68
  The first install pulls the stealth browser engine, which takes a few minutes.
69
69
 
package/dist/cli.mjs CHANGED
@@ -20166,7 +20166,8 @@ ${prompt}`);
20166
20166
  const msg2 = `Researcher ${id} (${researcherConfig.name}) timed out after ${config.RESEARCHER_TIMEOUT_MS}ms`;
20167
20167
  session.abort().catch((err) => {
20168
20168
  logger.warn("[ResearcherExecutor] Failed to abort timed-out researcher session:", err);
20169
- }).finally(() => reject(new Error(msg2)));
20169
+ });
20170
+ reject(new Error(msg2));
20170
20171
  }, config.RESEARCHER_TIMEOUT_MS);
20171
20172
  });
20172
20173
  let abortCleanup;
@@ -20248,9 +20249,18 @@ ${responseText}`);
20248
20249
  }
20249
20250
  } finally {
20250
20251
  subscription();
20251
- await session.abort().catch((err) => {
20252
- logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
20253
- });
20252
+ await Promise.race([
20253
+ session.abort().catch((err) => {
20254
+ logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
20255
+ }),
20256
+ new Promise((resolve8) => {
20257
+ const t = setTimeout(() => {
20258
+ logger.warn(`[ResearcherExecutor] Researcher ${id} session abort did not settle within 10s; continuing cleanup`);
20259
+ resolve8();
20260
+ }, 1e4);
20261
+ t.unref?.();
20262
+ })
20263
+ ]);
20254
20264
  sessionService?.unregisterSession(researchId, id);
20255
20265
  if (ctx.mode !== "tui" && ctx.hasUI && typeof ctx.ui.setHiddenThinkingLabel === "function") {
20256
20266
  ctx.ui.setHiddenThinkingLabel();
@@ -20526,7 +20536,8 @@ ${prompt}`);
20526
20536
  const msg2 = `Quick research timed out after ${this.config.RESEARCHER_TIMEOUT_MS}ms`;
20527
20537
  session.abort().catch((err) => {
20528
20538
  logger.warn("[QuickOrchestrator] Failed to abort timed-out session:", err);
20529
- }).finally(() => reject(new Error(msg2)));
20539
+ });
20540
+ reject(new Error(msg2));
20530
20541
  }, this.config.RESEARCHER_TIMEOUT_MS);
20531
20542
  });
20532
20543
  let abortCleanup;
@@ -21989,14 +22000,15 @@ async function _doInit(options = {}) {
21989
22000
  }
21990
22001
  globalApiKey = options.apiKey || process.env["PI_RESEARCH_API_KEY"];
21991
22002
  let parsedProvider = options.provider || process.env["PI_RESEARCH_PROVIDER"];
21992
- if (!parsedProvider && options.model) {
21993
- if (typeof options.model === "string") {
21994
- const slashIdx = options.model.indexOf("/");
22003
+ const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
22004
+ if (!parsedProvider && effectiveModel) {
22005
+ if (typeof effectiveModel === "string") {
22006
+ const slashIdx = effectiveModel.indexOf("/");
21995
22007
  if (slashIdx > 0) {
21996
- parsedProvider = options.model.slice(0, slashIdx);
22008
+ parsedProvider = effectiveModel.slice(0, slashIdx);
21997
22009
  }
21998
- } else if (options.model.provider) {
21999
- parsedProvider = options.model.provider;
22010
+ } else if (effectiveModel.provider) {
22011
+ parsedProvider = effectiveModel.provider;
22000
22012
  }
22001
22013
  }
22002
22014
  if (globalApiKey && !parsedProvider) {
@@ -22004,7 +22016,16 @@ async function _doInit(options = {}) {
22004
22016
  }
22005
22017
  globalRegistry = buildModelRegistry(globalApiKey, parsedProvider);
22006
22018
  try {
22007
- globalModel = resolveModel(globalRegistry, typeof options.model === "string" ? options.model : void 0, parsedProvider, globalApiKey);
22019
+ if (effectiveModel && typeof effectiveModel === "object" && effectiveModel.id) {
22020
+ globalModel = effectiveModel;
22021
+ } else {
22022
+ globalModel = resolveModel(
22023
+ globalRegistry,
22024
+ typeof effectiveModel === "string" ? effectiveModel : void 0,
22025
+ parsedProvider,
22026
+ globalApiKey
22027
+ );
22028
+ }
22008
22029
  globalContainer = getServiceContainer();
22009
22030
  registerCoreServices(globalContainer);
22010
22031
  registerInfrastructureServices(globalContainer);
@@ -22619,6 +22640,18 @@ function detectCredentials() {
22619
22640
  };
22620
22641
  }
22621
22642
  if (piAuthPresent) {
22643
+ if (!model) {
22644
+ return {
22645
+ source: "pi-config",
22646
+ apiKeyConfigured: true,
22647
+ provider,
22648
+ model,
22649
+ modelFrom,
22650
+ piAuthPresent,
22651
+ piModelsPresent,
22652
+ problem: 'pi credentials were found (auth.json), but no research model is configured. The standalone CLI / agent skill run only on an explicitly configured model \u2014 they do not follow the model selected inside the pi extension. Set PI_RESEARCH_MODEL to a "provider/model-id" (in the environment, or in config.env / cli.env below).'
22653
+ };
22654
+ }
22622
22655
  return {
22623
22656
  source: "pi-config",
22624
22657
  apiKeyConfigured: true,
@@ -22648,14 +22681,14 @@ function configBlock(det, extraNote) {
22648
22681
  lines.push(` \u2022 base config file: ${paths.configEnv}`);
22649
22682
  lines.push(` \u2022 cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
22650
22683
  lines.push(` \u2022 env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
22651
- lines.push(` \u2022 pi auth storage: ${paths.piAuth}`);
22652
- lines.push(` \u2022 pi models: ${paths.piModels}`);
22684
+ lines.push(` \u2022 pi config (keys): ${paths.piAuth}`);
22685
+ lines.push(` \u2022 pi config (models): ${paths.piModels}`);
22653
22686
  lines.push("");
22654
22687
  lines.push("Detected:");
22655
22688
  lines.push(` \u2022 credential source: ${det.source}`);
22656
22689
  lines.push(` \u2022 api key configured: ${det.apiKeyConfigured}`);
22657
- lines.push(` \u2022 provider: ${det.provider ?? "(unset)"}`);
22658
- lines.push(` \u2022 model: ${det.model ?? "(unset)"} [from: ${det.modelFrom}]`);
22690
+ lines.push(` \u2022 provider: ${det.provider ?? "(unset \u2014 inferred from a provider/model-id model)"}`);
22691
+ lines.push(` \u2022 model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : "(not set \u2014 set PI_RESEARCH_MODEL, required)"}`);
22659
22692
  lines.push(` \u2022 pi auth.json: ${det.piAuthPresent ? "present" : "absent"}`);
22660
22693
  lines.push(` \u2022 pi models.json: ${det.piModelsPresent ? "present" : "absent"}`);
22661
22694
  if (extraNote) lines.push(` \u2022 note: ${extraNote}`);
@@ -22912,8 +22945,9 @@ uninstall: ${BINARY_NAME} skill uninstall
22912
22945
  } catch (err) {
22913
22946
  return reportError(err, "skill install", s.json);
22914
22947
  }
22948
+ const skillModel = process.env["PI_RESEARCH_MODEL"] ?? getConfig(process.cwd(), "cli").RESEARCH_MODEL;
22915
22949
  if (s.json) {
22916
- toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2 }));
22950
+ toStdout(pretty({ command: "skill", action: "install", dryRun: !!s.dryRun, results: results2, modelConfigured: Boolean(skillModel) }));
22917
22951
  } else {
22918
22952
  toStdout(`pi-research agent skill \u2014 ${s.dryRun ? "install (dry run)" : "install"}
22919
22953
 
@@ -22923,6 +22957,15 @@ uninstall: ${BINARY_NAME} skill uninstall
22923
22957
  toStdout(`
22924
22958
  The skill activates automatically \u2014 ask the agent to research something.
22925
22959
  `);
22960
+ if (!skillModel) {
22961
+ toStdout(
22962
+ `
22963
+ Required: set PI_RESEARCH_MODEL=provider/model-id in ${resolvedConfigPaths().configEnv}
22964
+ (or as an env var) \u2014 the skill runs only on this configured model, not your pi
22965
+ session model. Verify with: ${BINARY_NAME} status
22966
+ `
22967
+ );
22968
+ }
22926
22969
  }
22927
22970
  return results2.some((r) => r.status === "error") ? EXIT.SOFTWARE : EXIT.OK;
22928
22971
  }
@@ -23080,11 +23123,15 @@ COMMANDS
23080
23123
  CONFIGURE
23081
23124
  Credentials are resolved in this order (first match wins):
23082
23125
 
23083
- env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
23084
- cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
23085
- base config: ${p.configEnv}
23086
- pi auth: ${p.piAuth}
23087
- pi models: ${p.piModels}
23126
+ env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
23127
+ cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
23128
+ base config: ${p.configEnv}
23129
+ pi config (keys): ${p.piAuth}
23130
+ pi config (models): ${p.piModels}
23131
+
23132
+ A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id". This CLI and
23133
+ the agent skill run only on that configured model \u2014 they do not follow the model
23134
+ selected inside the pi extension.
23088
23135
 
23089
23136
  The pi extension has its own optional overlay file:
23090
23137
  pi extension: ${p.piIfaceEnv}
@@ -10,7 +10,8 @@ pi-research software.
10
10
  Already running the `pi` extension (`pi install npm:@lincoln504/pi-research`)? You
11
11
  already have the engine — install the skill into your other agents with
12
12
  `/research-config` → Install in External Agents (see [Installation flow](#installation-flow))
13
- and skip the rest.
13
+ and skip the install commands below. The model step still applies to you: the
14
+ skill runs on its own configured `PI_RESEARCH_MODEL`, not your pi session's model.
14
15
 
15
16
  For standalone use without pi, install the engine globally, then link the skill into
16
17
  every coding agent detected on this machine:
@@ -24,6 +25,18 @@ pi-research skill install # link the skill into Claude, Codex, Op
24
25
  different skill in the slot, and records what it created so `pi-research skill uninstall`
25
26
  removes exactly that. Run `pi-research skill status` to see where it is installed.
26
27
 
28
+ Then configure the model research runs on — the skill and standalone CLI use only
29
+ this explicitly configured model (they never follow the model selected inside the
30
+ pi extension) and refuse to start without one:
31
+
32
+ ```sh
33
+ # ~/.pi/research/config.env (or export as an env var)
34
+ PI_RESEARCH_MODEL=provider/model-id
35
+ ```
36
+
37
+ If you use `pi`, the API key comes from your pi configuration (`~/.pi/agent/auth.json`) automatically; otherwise
38
+ also set `PI_RESEARCH_API_KEY` (same file or env var). See [Configuration](CONFIGURATION.md).
39
+
27
40
  On Windows, run `pi-research` from `cmd` or use `pi-research.cmd`: PowerShell's default
28
41
  execution policy (`Restricted`) blocks npm's `.ps1` shims ("running scripts is
29
42
  disabled"); or run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
@@ -114,8 +127,8 @@ Manual. Symlink the directory into any agent's skills folder yourself:
114
127
  ### Prerequisites
115
128
 
116
129
  - Node.js >= 22.19.0
117
- - `pi-research` installed where the launcher can find it, plus a model + API key.
118
- See [Configuration](CONFIGURATION.md).
130
+ - `pi-research` installed where the launcher can find it, plus a configured model
131
+ (`PI_RESEARCH_MODEL`) + API key. See [Configuration](CONFIGURATION.md).
119
132
 
120
133
  ```bash
121
134
  npm install -g @lincoln504/pi-research
@@ -301,7 +301,7 @@ Browser & scraping
301
301
 
302
302
  - [Camoufox](https://camoufox.com) — stealth Firefox (driven via [Playwright](https://playwright.dev)) for undetected search and scraping
303
303
  - [poolifier](https://github.com/poolifier/poolifier) — the worker-process pool behind the browser workers
304
- - [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown) & [node-html-markdown](https://github.com/crosstype/node-html-markdown) — convert scraped HTML to Markdown
304
+ - [html-to-markdown](https://github.com/kreuzberg-dev/html-to-markdown) — converts scraped HTML to Markdown (node-html-markdown serves as the pure-JS fallback)
305
305
  - `pdf-oxide-wasm` — PDF text extraction (Rust/WASM)
306
306
 
307
307
  Knowledge store & embeddings
@@ -75,7 +75,7 @@ Research
75
75
  | `PI_RESEARCH_RETRY_DELAY_MS` | `2000` | 100–10000 | Base delay between retries. |
76
76
  | `PI_RESEARCH_WORKER_THREADS` | `4` | 1–10 | Browser worker processes. Higher = more throughput, more CPU/RAM. |
77
77
  | `PI_RESEARCH_WORKER_CONCURRENCY` | `2` | 1–10 | Tasks per worker process. |
78
- | `PI_RESEARCH_MODEL` | _(session model)_ | — | Model override for researcher sub-agents (deep and quick) and knowledge synthesis. In the pi extension the coordinator and evaluator keep using the session model; on the standalone CLI/SDK, when no `--model`/`model` option is given, this also selects the session model itself. Accepts `provider/id` or a bare model id. |
78
+ | `PI_RESEARCH_MODEL` | _(pi: session model; CLI/skill: required)_ | — | The model research runs on. **Required for the standalone CLI / agent skill** — they use only this configured model (never the model selected inside the pi extension) and refuse to start without one. On the SDK it selects the session model when no `model` option is given. In the pi extension it overrides researcher sub-agents and knowledge synthesis, while the coordinator and evaluator keep using the session model. Accepts `provider/id` or a bare model id. |
79
79
  | `PI_RESEARCH_DISABLED_TOOLS` | _(none)_ | — | Comma-separated research tools to disable for a run (`search`, `scrape`, `security_search`, `stackexchange`, `youtube_transcript`, `grep`). Removed from every researcher's toolset and named in the coordinator/evaluator prompt. |
80
80
  | `PI_RESEARCH_REPORT_EXPORT_ENABLED` (TUI) | `false` | — | Front-ends write a Markdown report to disk and surface its path. |
81
81
  | `PI_RESEARCH_REPORT_EXPORT_DIR` | _(smart cwd)_ | — | Pin exported reports to a fixed directory, bypassing the cwd-relative resolution. Useful for the agent skill, which runs from the host agent's arbitrary directory. |
@@ -128,11 +128,11 @@ See the [knowledge store doc](KNOWLEDGE-STORE.md) for what each value does.
128
128
  | `PI_RESEARCH_EMBEDDING_MODEL_INIT_TIMEOUT_MS` | `300000` | 10000–600000 | Embedding-model initialization timeout (first-time download can be slow). |
129
129
  | `PI_RESEARCH_WEBGPU_REPROBE` | _(unset)_ | — | Set `1` to discard the cached WebGPU-viability verdict and probe again on next use. |
130
130
 
131
- API keys (all optional)
131
+ API keys
132
132
 
133
133
  | Variable | Description |
134
134
  |----------|-------------|
135
- | `PI_RESEARCH_API_KEY` / `PI_RESEARCH_PROVIDER` | Explicit LLM credentials for SDK / CLI mode (not needed when using pi's own auth). Provider is required alongside the key. |
135
+ | `PI_RESEARCH_API_KEY` / `PI_RESEARCH_PROVIDER` | Explicit LLM credentials for SDK / CLI mode (not needed when pi's configuration — `~/.pi/agent/auth.json` — supplies the key). On the CLI / agent skill both can also live in `config.env` / `cli.env`. The provider is required alongside the key, or inferred from a `provider/model-id` `PI_RESEARCH_MODEL`. |
136
136
  | `STACKEXCHANGE_API_KEY` | Raises the Stack Exchange tool's limit from 300/day to 10,000/day. Obtain at <https://stackapps.com/apps/oauth>. |
137
137
  | `GITHUB_TOKEN` | Raises the security tool's GitHub Advisory limit from 60/hr to 5000/hr (any default-scope token). |
138
138
  | `NVD_API_KEY` | Raises the security tool's NVD limit ~10× and tightens request spacing. Request at <https://nvd.nist.gov/developers/request-an-api-key>. Recommended when using severity-filtered security searches: those issue a second (CVSS v2) NVD query to catch v2-only CVEs, which roughly doubles request time against the 6 s/request unauthenticated throttle. |
package/docs/SDK.md CHANGED
@@ -13,6 +13,13 @@ run the `pi` extension, which keeps its own private copy your scripts can't impo
13
13
  npm install @lincoln504/pi-research
14
14
  ```
15
15
 
16
+ Then pick the model: pass `model` to `initResearchSDK`, or set `PI_RESEARCH_MODEL`
17
+ (env or `~/.pi/research/config.env`). The SDK never follows the model selected
18
+ inside the pi extension; only when neither is set does it fall back to the first
19
+ available model in your pi registry. The API key comes from your pi
20
+ configuration (`~/.pi/agent/auth.json`) automatically, or from the `apiKey`
21
+ option / `PI_RESEARCH_API_KEY` env var.
22
+
16
23
  `src/sdk.ts` is a library for scripts, CI, and custom tooling. It is configured
17
24
  from code, not from a global overlay file — there is no `sdk.env`. It reads the
18
25
  base `~/.pi/research/config.env` as a baseline, and everything is
@@ -83,7 +90,7 @@ extension and the CLI / agent skill do it when `PI_RESEARCH_REPORT_EXPORT_ENABLE
83
90
 
84
91
  | Option | Description |
85
92
  |--------|-------------|
86
- | `model` | `"provider/id"` string or a `Model` object. Omit to use the first available pi model. |
93
+ | `model` | `"provider/id"` string or a `Model` object. Omit to use the configured `PI_RESEARCH_MODEL`; only when neither is set does the SDK fall back to the first available pi model. |
87
94
  | `apiKey` / `provider` | Explicit credentials (provider required with apiKey). |
88
95
  | `config` | `Partial<Config>` overrides, applied over the base/defaults. |
89
96
  | `ignoreGlobalConfig` | Skip `config.env` entirely — defaults + `process.env` + `config` only. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lincoln504/pi-research",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A set of web research tools (search, scrape, and others) and research agent system that uses them safely and efficiently.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -39,8 +39,8 @@ model/key is missing.
39
39
  ### Prerequisites
40
40
 
41
41
  - Node.js >= 22.19.0
42
- - `pi-research` installed somewhere the launcher can find it, and a model +
43
- API key configured. See [`references/configuration.md`](references/configuration.md).
42
+ - `pi-research` installed somewhere the launcher can find it, and a configured
43
+ model (`PI_RESEARCH_MODEL`) + API key. See [`references/configuration.md`](references/configuration.md).
44
44
 
45
45
  ```bash
46
46
  npm install -g @lincoln504/pi-research
@@ -2,7 +2,7 @@
2
2
  name: pi-research
3
3
  description: "Multi-agent web research for any coding agent. Use for anything requiring current information from the internet — news, trends, product/service/company facts, statistics, people, places, events, APIs, library behavior, or 'what is X / how does X work' questions. Also use to search a local knowledge store of previously researched findings before doing live research. Powered by the pi-research engine (search, stealth scraping, security databases, Stack Exchange). Not for reading local files, running project commands, or analyzing this repository's own source."
4
4
  license: MIT
5
- metadata: { "author": "Lincoln Deen", "version": "1.0.1", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
5
+ metadata: { "author": "Lincoln Deen", "version": "1.0.3", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
6
6
  allowed-tools: Bash(node:*)
7
7
  ---
8
8
 
@@ -91,10 +91,13 @@ On exit 78 the engine prints exact configure locations to stderr — relay verba
91
91
  ```
92
92
  config file: ~/.pi/research/config.env
93
93
  env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
94
- pi auth: ~/.pi/agent/auth.json
95
- pi models: ~/.pi/agent/models.json
94
+ pi config (keys): ~/.pi/agent/auth.json (supplies the API key only)
95
+ pi config (models): ~/.pi/agent/models.json
96
96
  ```
97
97
 
98
+ `PI_RESEARCH_MODEL` (a `provider/model-id`) is always required — pi's configuration
99
+ supplies only the key, and the skill never uses the model selected inside the pi extension.
100
+
98
101
  Run `status` for the resolved paths on this machine. Full reference:
99
102
  [`references/configuration.md`](references/configuration.md).
100
103
 
@@ -1,6 +1,8 @@
1
1
  ## Configuration
2
2
 
3
- The skill drives the **pi-research** engine, which needs a model + API key.
3
+ The skill drives the **pi-research** engine, which needs an explicitly configured
4
+ model (`PI_RESEARCH_MODEL`) + API key — it runs only on that configured model and
5
+ never follows the model selected inside the pi extension.
4
6
  Run `node "<SKILL_DIR>/scripts/run.mjs" status` to print what's detected and where
5
7
  to configure (no research runs).
6
8
 
@@ -13,7 +15,8 @@ to configure (no research runs).
13
15
  export PI_RESEARCH_MODEL=openai/gpt-4o # provider/model-id
14
16
  ```
15
17
  2. **Global config** `~/.pi/research/config.env` (same KEY=VALUE keys; read at startup).
16
- 3. **pi auth** (if you use `pi`): `~/.pi/agent/auth.json` + `~/.pi/agent/models.json`.
18
+ 3. **pi configuration** (if you use `pi`): the API key comes from `~/.pi/agent/auth.json`
19
+ automatically — `PI_RESEARCH_MODEL` must still be set (env or `config.env`).
17
20
 
18
21
  Real env vars always win. Optional CLI-only overlay `~/.pi/research/cli.env` layers
19
22
  over `config.env` for the CLI only. Precedence:
@@ -23,7 +26,7 @@ over `config.env` for the CLI only. Precedence:
23
26
 
24
27
  | Variable | Default | Description |
25
28
  |----------|---------|-------------|
26
- | `PI_RESEARCH_MODEL` | session model | Model override for all research sub-tasks. |
29
+ | `PI_RESEARCH_MODEL` | _(required)_ | The `provider/model-id` all research runs on. |
27
30
  | `PI_RESEARCH_DEFAULT_RESEARCH_DEPTH` | `1` | Default depth (1–3) when `--depth` omitted. |
28
31
  | `PI_RESEARCH_MAX_RESEARCHERS` | `3` | Parallel researchers (1–5). |
29
32
  | `PI_RESEARCH_TIMEOUT_MS` | `300000` | Per-researcher timeout (180000–1800000). |
@@ -70,7 +73,7 @@ model/key; message lists the fix) · `70` runtime error (network/provider/intern
70
73
  `export PI_RESEARCH_BIN=/path/to/pi-research/dist/cli.mjs` (exact CLI entry;
71
74
  checked first — also the escape hatch for unusual Windows shim layouts).
72
75
  - **"No model or API key" (78 from engine):** set the three env vars (or config /
73
- pi auth), then re-run `status`.
76
+ pi's configuration), then re-run `status`.
74
77
  - **Rate limit (70):** provider returned 429 — wait and retry.
75
78
  - **GPU/embedding errors:** set `PI_RESEARCH_EMBEDDING_DEVICE=cpu` (headless/no-GPU).
76
79
 
@@ -139,10 +139,11 @@ function notInstalled() {
139
139
  " # or, with pi: pi install npm:@lincoln504/pi-research",
140
140
  " # or point at a copy: export PI_RESEARCH_PATH=/path/to/pi-research",
141
141
  "",
142
- "After installing, configure a model + API key. Locations:",
142
+ "After installing, configure the model (PI_RESEARCH_MODEL is required) + API key.",
143
+ "Locations:",
143
144
  ` \u2022 env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`,
144
145
  ` \u2022 global config file: ${join(home, ".pi", "research", "config.env")}`,
145
- ` \u2022 pi auth storage: ${join(home, ".pi", "agent", "auth.json")}`,
146
+ ` \u2022 pi config (keys): ${join(home, ".pi", "agent", "auth.json")} (supplies the API key only)`,
146
147
  "",
147
148
  "Run `status` once installed to verify detection.",
148
149
  ""
package/src/cli.ts CHANGED
@@ -196,7 +196,7 @@ export interface ResolvedConfigPaths {
196
196
  cliIfaceEnv: string;
197
197
  /** Per-interface overlay for the pi extension (~/.pi/research/pi.env). */
198
198
  piIfaceEnv: string;
199
- /** pi auth storage (~/.pi/agent/auth.json). */
199
+ /** pi's key file (~/.pi/agent/auth.json), part of pi's configuration. */
200
200
  piAuth: string;
201
201
  /** pi model definitions (~/.pi/agent/models.json). */
202
202
  piModels: string;
@@ -300,9 +300,29 @@ function detectCredentials(): CredentialDetection {
300
300
  };
301
301
  }
302
302
 
303
- // --- pi auth storage path --------------------------------------------------
303
+ // --- pi configuration path (key from auth.json) -----------------------------
304
304
  if (piAuthPresent) {
305
- // The SDK will resolve a model from the registry (models.json + PI_RESEARCH_MODEL).
305
+ // Keys come from auth.json, but the MODEL must be configured explicitly:
306
+ // the standalone CLI / agent skill run only on the configured model — they
307
+ // never follow the model selected inside the pi extension, and silently
308
+ // falling back to the registry's first authed entry picks a model nobody
309
+ // chose. Fail fast here (pre-flight, before any SDK init).
310
+ if (!model) {
311
+ return {
312
+ source: 'pi-config',
313
+ apiKeyConfigured: true,
314
+ provider,
315
+ model,
316
+ modelFrom,
317
+ piAuthPresent,
318
+ piModelsPresent,
319
+ problem:
320
+ 'pi credentials were found (auth.json), but no research model is configured. ' +
321
+ 'The standalone CLI / agent skill run only on an explicitly configured model — they do not ' +
322
+ 'follow the model selected inside the pi extension. Set PI_RESEARCH_MODEL to a ' +
323
+ '"provider/model-id" (in the environment, or in config.env / cli.env below).',
324
+ };
325
+ }
306
326
  return {
307
327
  source: 'pi-config',
308
328
  apiKeyConfigured: true, // key lives in auth.json; SDK reads it
@@ -342,14 +362,14 @@ function configBlock(det: CredentialDetection, extraNote?: string): string {
342
362
  lines.push(` • base config file: ${paths.configEnv}`);
343
363
  lines.push(` • cli overlay: ${paths.cliIfaceEnv} (optional; overrides base for this CLI / agent skill)`);
344
364
  lines.push(` • env vars: PI_RESEARCH_API_KEY / PI_RESEARCH_PROVIDER / PI_RESEARCH_MODEL`);
345
- lines.push(` • pi auth storage: ${paths.piAuth}`);
346
- lines.push(` • pi models: ${paths.piModels}`);
365
+ lines.push(` • pi config (keys): ${paths.piAuth}`);
366
+ lines.push(` • pi config (models): ${paths.piModels}`);
347
367
  lines.push('');
348
368
  lines.push('Detected:');
349
369
  lines.push(` • credential source: ${det.source}`);
350
370
  lines.push(` • api key configured: ${det.apiKeyConfigured}`);
351
- lines.push(` • provider: ${det.provider ?? '(unset)'}`);
352
- lines.push(` • model: ${det.model ?? '(unset)'} [from: ${det.modelFrom}]`);
371
+ lines.push(` • provider: ${det.provider ?? '(unset — inferred from a provider/model-id model)'}`);
372
+ lines.push(` • model: ${det.model ? `${det.model} [from: ${det.modelFrom}]` : '(not set — set PI_RESEARCH_MODEL, required)'}`);
353
373
  lines.push(` • pi auth.json: ${det.piAuthPresent ? 'present' : 'absent'}`);
354
374
  lines.push(` • pi models.json: ${det.piModelsPresent ? 'present' : 'absent'}`);
355
375
  if (extraNote) lines.push(` • note: ${extraNote}`);
@@ -657,12 +677,23 @@ async function cmdSkill(s: NonNullable<ParsedArgs['skill']>): Promise<number> {
657
677
  } catch (err) {
658
678
  return reportError(err, 'skill install', s.json);
659
679
  }
680
+ // The skill runs only on an explicitly configured model — if none is set yet,
681
+ // say so right here, at the moment of install (mirrors the TUI install action).
682
+ const skillModel =
683
+ process.env['PI_RESEARCH_MODEL'] ?? getConfig(process.cwd(), 'cli').RESEARCH_MODEL;
660
684
  if (s.json) {
661
- toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results }));
685
+ toStdout(pretty({ command: 'skill', action: 'install', dryRun: !!s.dryRun, results, modelConfigured: Boolean(skillModel) }));
662
686
  } else {
663
687
  toStdout(`pi-research agent skill — ${s.dryRun ? 'install (dry run)' : 'install'}\n\n`);
664
688
  for (const r of results) toStdout(` ${r.tool.padEnd(10)} ${describeInstall(r)}\n`);
665
689
  toStdout(`\nThe skill activates automatically — ask the agent to research something.\n`);
690
+ if (!skillModel) {
691
+ toStdout(
692
+ `\nRequired: set PI_RESEARCH_MODEL=provider/model-id in ${resolvedConfigPaths().configEnv}\n` +
693
+ `(or as an env var) — the skill runs only on this configured model, not your pi\n` +
694
+ `session model. Verify with: ${BINARY_NAME} status\n`
695
+ );
696
+ }
666
697
  }
667
698
  return results.some((r) => r.status === 'error') ? EXIT.SOFTWARE : EXIT.OK;
668
699
  }
@@ -842,11 +873,15 @@ COMMANDS
842
873
  CONFIGURE
843
874
  Credentials are resolved in this order (first match wins):
844
875
 
845
- env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
846
- cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
847
- base config: ${p.configEnv}
848
- pi auth: ${p.piAuth}
849
- pi models: ${p.piModels}
876
+ env vars: PI_RESEARCH_API_KEY PI_RESEARCH_PROVIDER PI_RESEARCH_MODEL
877
+ cli overlay: ${p.cliIfaceEnv} (optional; this CLI / agent skill only)
878
+ base config: ${p.configEnv}
879
+ pi config (keys): ${p.piAuth}
880
+ pi config (models): ${p.piModels}
881
+
882
+ A model is REQUIRED: set PI_RESEARCH_MODEL to a "provider/model-id". This CLI and
883
+ the agent skill run only on that configured model — they do not follow the model
884
+ selected inside the pi extension.
850
885
 
851
886
  The pi extension has its own optional overlay file:
852
887
  pi extension: ${p.piIfaceEnv}
@@ -269,9 +269,13 @@ export class QuickResearchOrchestrator {
269
269
  const timeoutPromise = new Promise<void>((_, reject) => {
270
270
  timeoutId = setTimeout(() => {
271
271
  const msg = `Quick research timed out after ${this.config.RESEARCHER_TIMEOUT_MS}ms`;
272
+ // Reject FIRST, abort as fire-and-forget cleanup — see researcher-executor:
273
+ // abort() awaits the stuck in-flight request, so gating the rejection on it
274
+ // settling disables the timeout exactly when it is needed.
272
275
  session.abort().catch((err) => {
273
276
  logger.warn('[QuickOrchestrator] Failed to abort timed-out session:', err);
274
- }).finally(() => reject(new Error(msg)));
277
+ });
278
+ reject(new Error(msg));
275
279
  }, this.config.RESEARCHER_TIMEOUT_MS);
276
280
  });
277
281
 
@@ -264,9 +264,14 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
264
264
  const timeoutPromise = new Promise<void>((_, reject) => {
265
265
  timeoutId = setTimeout(() => {
266
266
  const msg = `Researcher ${id} (${researcherConfig.name}) timed out after ${config.RESEARCHER_TIMEOUT_MS}ms`;
267
+ // Reject FIRST, abort as fire-and-forget cleanup: session.abort() awaits the
268
+ // in-flight request, which is the very thing that is stuck (e.g. a provider
269
+ // retry loop against an exhausted quota) — gating the rejection on it settling
270
+ // makes the timeout ineffective exactly when it is needed.
267
271
  session.abort().catch((err) => {
268
272
  logger.warn('[ResearcherExecutor] Failed to abort timed-out researcher session:', err);
269
- }).finally(() => reject(new Error(msg)));
273
+ });
274
+ reject(new Error(msg));
270
275
  }, config.RESEARCHER_TIMEOUT_MS);
271
276
  });
272
277
 
@@ -375,9 +380,23 @@ export async function runResearcher(options: RunResearcherOptions): Promise<void
375
380
  }
376
381
  } finally {
377
382
  subscription();
378
- await session.abort().catch((err) => {
379
- logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
380
- });
383
+ // Bounded: after a timeout, session.abort() awaits the very in-flight request
384
+ // that is stuck (e.g. a provider retry loop against an exhausted quota) and may
385
+ // never settle — never let per-attempt cleanup hang the retry loop on it. The
386
+ // retry builds a fresh session; the abandoned abort keeps draining in the
387
+ // background and its session dies with the run's teardown.
388
+ await Promise.race([
389
+ session.abort().catch((err) => {
390
+ logger.warn(`[ResearcherExecutor] Failed to abort researcher session ${id}:`, err);
391
+ }),
392
+ new Promise<void>((resolve) => {
393
+ const t = setTimeout(() => {
394
+ logger.warn(`[ResearcherExecutor] Researcher ${id} session abort did not settle within 10s; continuing cleanup`);
395
+ resolve();
396
+ }, 10_000);
397
+ t.unref?.();
398
+ }),
399
+ ]);
381
400
  sessionService?.unregisterSession(researchId, id);
382
401
 
383
402
  // Restore the default thinking label now that the researcher is done.
@@ -23,7 +23,7 @@ import {
23
23
  } from '@earendil-works/pi-tui';
24
24
  import { setInteractiveTuiActive, initGlobalTuiController } from './tui/tui-controller.ts';
25
25
  import { normalizeSessionId } from './orchestration/session-state.ts';
26
- import { getConfig, saveConfig, resetConfig, getDbDir } from './config.ts';
26
+ import { getConfig, saveConfig, resetConfig, getDbDir, getGlobalEnvFilePath } from './config.ts';
27
27
  import { healthRegistry } from './healthcheck/index.ts';
28
28
  import { getService, clearService, tryGetServiceContainerFromCtx } from './core/service-registry.ts';
29
29
  import { ServiceNames, IKnowledgeStoreService } from './core/service-interfaces.ts';
@@ -648,6 +648,19 @@ async function installSkillAction(ctx: ExtensionContext, pi: ExtensionAPI): Prom
648
648
  }
649
649
  lines.push('', 'The skill activates automatically — just ask the agent to pi-research something.');
650
650
 
651
+ // The skill runs only on an explicitly configured model (it never follows the
652
+ // pi session model) — if none is configured yet, say so right here, at the
653
+ // moment of install, mirroring the CLI's `skill install` output.
654
+ const skillModel = process.env['PI_RESEARCH_MODEL'] ?? getConfig(ctx.cwd, 'cli').RESEARCH_MODEL;
655
+ if (!skillModel) {
656
+ lines.push(
657
+ '',
658
+ '**Required:** set `PI_RESEARCH_MODEL=provider/model-id` in ' +
659
+ `\`${getGlobalEnvFilePath()}\` (or as an env var) — the skill runs only on ` +
660
+ 'this configured model, not your pi session model.'
661
+ );
662
+ }
663
+
651
664
  pi.sendMessage({ customType: 'skill-install-result', content: lines.join('\n'), display: true });
652
665
  if (ctx.hasUI) {
653
666
  if (ok > 0) ctx.ui.notify(`Skill installed for ${ok} coding agent${ok === 1 ? '' : 's'}.`, 'info');
package/src/sdk.ts CHANGED
@@ -144,17 +144,21 @@ function _removeSignalHandlers(): void {
144
144
  * Options for initializing the Research SDK
145
145
  */
146
146
  export interface ResearchSDKOptions {
147
- /**
147
+ /**
148
148
  * The LLM model to use for coordination, planning, and evaluation.
149
- * If a string, it must match "provider/id" (e.g. "openai/gpt-4o").
150
- * If omitted, the first available model in pi's configuration will be used.
149
+ * If a string, it must match "provider/id" (e.g. "openai/gpt-4o"); a Model
150
+ * object is used as-is. If omitted, the configured RESEARCH_MODEL
151
+ * (PI_RESEARCH_MODEL / config.env, or the `config` option) is used; only
152
+ * when neither is set does the SDK fall back to the first available model
153
+ * in pi's configuration. The SDK never follows the model selected
154
+ * interactively inside the pi extension.
151
155
  */
152
156
  model?: string | Model<any>;
153
157
 
154
158
  /**
155
159
  * Optional explicit API key for the model.
156
160
  * If provided, `provider` must also be set.
157
- * If omitted, pi's global auth storage (~/.pi/agent/auth.json) is used.
161
+ * If omitted, the key from pi's configuration (~/.pi/agent/auth.json) is used.
158
162
  */
159
163
  apiKey?: string;
160
164
 
@@ -232,15 +236,18 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
232
236
  globalApiKey = options.apiKey || process.env['PI_RESEARCH_API_KEY'];
233
237
  let parsedProvider = options.provider || process.env['PI_RESEARCH_PROVIDER'];
234
238
 
235
- // Infer provider from model if not explicitly provided
236
- if (!parsedProvider && options.model) {
237
- if (typeof options.model === 'string') {
238
- const slashIdx = options.model.indexOf('/');
239
+ // Infer provider from the effective model if not explicitly provided. The
240
+ // effective model is the explicit option first, else the configured
241
+ // RESEARCH_MODEL — the same precedence the resolve below uses.
242
+ const effectiveModel = options.model ?? globalConfig.RESEARCH_MODEL;
243
+ if (!parsedProvider && effectiveModel) {
244
+ if (typeof effectiveModel === 'string') {
245
+ const slashIdx = effectiveModel.indexOf('/');
239
246
  if (slashIdx > 0) {
240
- parsedProvider = options.model.slice(0, slashIdx);
247
+ parsedProvider = effectiveModel.slice(0, slashIdx);
241
248
  }
242
- } else if ((options.model as any).provider) {
243
- parsedProvider = (options.model as any).provider;
249
+ } else if ((effectiveModel as any).provider) {
250
+ parsedProvider = (effectiveModel as any).provider;
244
251
  }
245
252
  }
246
253
 
@@ -252,8 +259,23 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
252
259
  globalRegistry = sharedBuildModelRegistry(globalApiKey, parsedProvider);
253
260
 
254
261
  try {
255
- // Resolve model using unified logic.
256
- globalModel = resolveModel(globalRegistry, typeof options.model === 'string' ? options.model : undefined, parsedProvider, globalApiKey);
262
+ // Resolve the model. Precedence: explicit option (a Model object is used
263
+ // as-is; it need not exist in the registry) → configured RESEARCH_MODEL →
264
+ // provider default → registry fallback. Seeding the resolver with the
265
+ // configured model is what keeps the coordinator (which runs on ctx.model)
266
+ // on the SAME model as the researchers/synthesis (which read
267
+ // RESEARCH_MODEL themselves) — without it a configured model would govern
268
+ // only the sub-agents and planning would silently run on the fallback.
269
+ if (effectiveModel && typeof effectiveModel === 'object' && (effectiveModel as any).id) {
270
+ globalModel = effectiveModel as Model<any>;
271
+ } else {
272
+ globalModel = resolveModel(
273
+ globalRegistry,
274
+ typeof effectiveModel === 'string' ? effectiveModel : undefined,
275
+ parsedProvider,
276
+ globalApiKey
277
+ );
278
+ }
257
279
 
258
280
  // Use the global container to ensure internal services can resolve dependencies
259
281
  globalContainer = getServiceContainer();