@lincoln504/pi-research 1.6.6 → 1.6.8
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 +10 -10
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +29 -4
- package/docs/CHANGELOG.md +25 -0
- package/package.json +2 -2
- package/src/index.ts +8 -19
- package/src/knowledge/embedder-utils.ts +7 -1
- package/src/knowledge/transformers-loader.ts +58 -1
- package/src/core/pi-ai-skew.ts +0 -351
package/README.md
CHANGED
|
@@ -44,21 +44,20 @@ npm install -g @lincoln504/pi-research
|
|
|
44
44
|
pi-research skill install
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
needlessly recompiles better-sqlite3; upgrading npm fixes that too.)
|
|
47
|
+
No extra setup is needed. The package uses no install scripts: it ships
|
|
48
|
+
ready-made bindings for every platform, and the stealth browser (~500MB)
|
|
49
|
+
downloads on first use, so the first scrape takes a few minutes. npm ≥11.19
|
|
50
|
+
skips install scripts by default; leave it that way, nothing needs approving
|
|
51
|
+
or building. Only Windows with npm older than 11.19 can fail when the install
|
|
52
|
+
tries to compile from source; upgrading npm fixes it.
|
|
54
53
|
|
|
55
54
|
In pi it works out of the box on the session's model and pi's configuration. Standalone use ([agent skill](docs/AGENT-SKILL.md) or [SDK](docs/SDK.md)) needs a model configured. See [Configuration](docs/CONFIGURATION.md).
|
|
56
55
|
|
|
57
56
|
### How it works
|
|
58
57
|
|
|
59
|
-
A research run loops through agent teams: a coordinator plans and starts with a search, researcher agents scrape and read in parallel, and a research lead decides whether to go another round
|
|
58
|
+
A research run loops through agent teams: a coordinator plans and starts with a search, researcher agents scrape and read in parallel, and a research lead decides whether to go another round, then writes the report from every report collected. The result is one cited Markdown report with findings optionally saved to the knowledge store.
|
|
60
59
|
|
|
61
|
-
Three depth levels (normal, deep, ultra) set the team size and number of rounds. A natural-language request is enough
|
|
60
|
+
Three depth levels (normal, deep, ultra) set the team size and number of rounds. A natural-language request is enough; the tool picks the right one:
|
|
62
61
|
|
|
63
62
|

|
|
64
63
|
|
|
@@ -73,7 +72,7 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
|
|
|
73
72
|
|
|
74
73
|
- **No quota and no monthly fee.** Most AI search providers cap free searches and then charge, with results served from their index on their servers. pi-research searches and scrapes locally through DuckDuckGo in a stealth browser.
|
|
75
74
|
- **The index is local.** Every finding can be saved to a local [LanceDB](https://lancedb.com) knowledge store that seeds future runs, so repeat questions get faster and cheaper answers.
|
|
76
|
-
- **Read-only by design.** The research agent cannot run shell commands or write, edit, or delete anything
|
|
75
|
+
- **Read-only by design.** The research agent cannot run shell commands or write, edit, or delete anything, which is the right shape for an agent whose whole job is reading untrusted web pages. Prompt injection picked up mid-run has nothing to act on.
|
|
77
76
|
- **Search a little or a lot.** Depth levels range from a quick pass to a large-scale investigation.
|
|
78
77
|
|
|
79
78
|
### Requirements / limitations
|
|
@@ -82,6 +81,7 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
|
|
|
82
81
|
- An LLM with a 100k+ context window (an API key or a local model)
|
|
83
82
|
- Internet access on a residential IP, since search, scraping, and YouTube transcripts get bot-blocked from datacenter/VPS/cloud IPs
|
|
84
83
|
- pi. The pi extension uses the host's copy while the standalone CLI and agent skill install it as a dependency.
|
|
84
|
+
- Local knowledge-store embeddings need `@huggingface/transformers`, which is an **optional dependency**: if its native image chain (sharp) cannot install on your machine, npm skips it with a warning, the install still succeeds, and everything except local embeddings works. The store disables embeddings gracefully with a message telling you how to restore them.
|
|
85
85
|
- Cloudflare Turnstile and similar systems block scraping on some sites. A run compensates with a wide pool of search results to scrape.
|
|
86
86
|
|
|
87
87
|
### Channels
|
|
@@ -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.6.
|
|
5
|
+
metadata: { "author": "Lincoln Deen", "version": "1.6.8", "package": "@lincoln504/pi-research", "engine": "pi-research-sdk", "openclaw": { "requires": { "bins": ["node"] } } }
|
|
6
6
|
allowed-tools: Bash(node:*)
|
|
7
7
|
---
|
|
8
8
|
|
package/dist/cli.mjs
CHANGED
|
@@ -4196,10 +4196,11 @@ function isNativeStackUnavailableError(err) {
|
|
|
4196
4196
|
const msg2 = (err instanceof Error ? `${err.message} ${err.stack ?? ""}` : String(err)).toLowerCase();
|
|
4197
4197
|
const code = typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
4198
4198
|
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
4199
|
-
if (
|
|
4199
|
+
if (code === "TRANSFORMERS_UNAVAILABLE") return true;
|
|
4200
|
+
if ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && (msg2.includes("onnxruntime") || msg2.includes("lancedb") || msg2.includes("@huggingface/transformers"))) {
|
|
4200
4201
|
return true;
|
|
4201
4202
|
}
|
|
4202
|
-
return msg2.includes("cannot find native binding") || msg2.includes("onnxruntime_binding.node") || /cannot find module '[^']*onnxruntime/.test(msg2) || /cannot find module '[^']*lancedb/.test(msg2) || msg2.includes("knowledgestoreservice is not a constructor");
|
|
4203
|
+
return msg2.includes("cannot find native binding") || msg2.includes("onnxruntime_binding.node") || /cannot find module '[^']*onnxruntime/.test(msg2) || /cannot find module '[^']*lancedb/.test(msg2) || /cannot find (?:package|module) ['"]@huggingface[/\\]transformers['"]/i.test(msg2) || msg2.includes("knowledgestoreservice is not a constructor");
|
|
4203
4204
|
}
|
|
4204
4205
|
function getModelCacheDir() {
|
|
4205
4206
|
const xdgCache = process.env["XDG_CACHE_HOME"];
|
|
@@ -4292,13 +4293,37 @@ var init_embedder_utils = __esm({
|
|
|
4292
4293
|
});
|
|
4293
4294
|
|
|
4294
4295
|
// src/knowledge/transformers-loader.ts
|
|
4296
|
+
function isTransformersMissingError(err) {
|
|
4297
|
+
if (!err || typeof err !== "object") return false;
|
|
4298
|
+
const code = "code" in err ? String(err.code) : "";
|
|
4299
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
4300
|
+
return (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && /@huggingface[/\\]transformers/.test(msg2);
|
|
4301
|
+
}
|
|
4302
|
+
function transformersUnavailableError(cause) {
|
|
4303
|
+
const err = new Error(
|
|
4304
|
+
"Optional dependency '@huggingface/transformers' is not installed, so local embeddings are unavailable. npm skips this package when its native image chain cannot install (most often a failing sharp platform binary). The knowledge store runs without embeddings until it is present \u2014 reinstall this package to re-resolve its optional dependencies (pi install npm:@lincoln504/pi-research), or install it directly (npm install @huggingface/transformers).",
|
|
4305
|
+
{ cause }
|
|
4306
|
+
);
|
|
4307
|
+
err.code = TRANSFORMERS_UNAVAILABLE;
|
|
4308
|
+
return err;
|
|
4309
|
+
}
|
|
4295
4310
|
async function getTransformers() {
|
|
4296
|
-
|
|
4311
|
+
if (_transformers) return _transformers;
|
|
4312
|
+
try {
|
|
4313
|
+
_transformers = await import("@huggingface/transformers");
|
|
4314
|
+
} catch (err) {
|
|
4315
|
+
if (isTransformersMissingError(err)) {
|
|
4316
|
+
throw transformersUnavailableError(err);
|
|
4317
|
+
}
|
|
4318
|
+
throw err;
|
|
4319
|
+
}
|
|
4320
|
+
return _transformers;
|
|
4297
4321
|
}
|
|
4298
|
-
var _transformers;
|
|
4322
|
+
var TRANSFORMERS_UNAVAILABLE, _transformers;
|
|
4299
4323
|
var init_transformers_loader = __esm({
|
|
4300
4324
|
"src/knowledge/transformers-loader.ts"() {
|
|
4301
4325
|
"use strict";
|
|
4326
|
+
TRANSFORMERS_UNAVAILABLE = "TRANSFORMERS_UNAVAILABLE";
|
|
4302
4327
|
}
|
|
4303
4328
|
});
|
|
4304
4329
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.8] - 2026-08-31
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- README npm install note rewritten for clarity (no behavior or claim changes): the three tangled topics (script skipping on npm ≥11.19, better-sqlite3's bundled prebuilds, the browser's first-use download) are now three plain sentences with an explicit do and don't, and the em dashes are gone. This is the README the package site renders, so the entry exists to timestamp that refresh.
|
|
13
|
+
|
|
14
|
+
## [1.6.7] - 2026-08-31
|
|
15
|
+
|
|
16
|
+
Two threads from the issue tracker collide here: #10 ("Installation via NPM fails")
|
|
17
|
+
turned out to be an install-time sharp build failure we could — and now did — design
|
|
18
|
+
away, while the skew guard shipped across 1.5.x–1.6.6 rested on a premise about pi's
|
|
19
|
+
module loading that is not how pi works.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`pi install` no longer fails outright on machines where sharp's native image chain cannot install (#10).** The failure chain, reproduced and pinned: `@huggingface/transformers` (the local-embeddings backend) declared sharp `^0.34.5` as a regular dependency; when npm skipped sharp's platform binary (`@img/sharp-*` optionals — the npm/cli#4828 pruning family on existing trees for npm 10.3.0–11.2.x, or any interrupted install, which the re-resolve then will NOT heal because skipped optionals stay inert), sharp's install script fell back to a source build that itself is broken (node-addon-api is only a devDependency), and the whole install died with `npm error code 1` — the extension never arrived. `@huggingface/transformers` is now an **optional dependency** (the same pattern `@lancedb/lancedb` already uses for it): npm drops a failing optional subtree with a warning and the install succeeds; research, scraping, and the CLI all work without embeddings. The absence is a first-class state at runtime too: `getTransformers()` reclassifies the missing-module rejection as a typed `TRANSFORMERS_UNAVAILABLE` error carrying remediation (`src/knowledge/transformers-loader.ts`), `isNativeStackUnavailableError()` recognizes it (`src/knowledge/embedder-utils.ts`), and the knowledge store therefore memoizes DISABLED on first touch instead of re-running the full init + 5-attempt backoff storm (~15s) on every store use. A transformers-INTERNAL failure (its own onnxruntime import, a corrupt install) still routes through the pre-existing onnxruntime/lancedb arms — a broken install of a PRESENT package is not "the optional was skipped". Retry storms, probe behavior, and startup were verified: the WebGPU probe subprocess already treats a missing module as "no WebGPU" and is unreachable before the env init anyway.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- **The pi-ai/pi-coding-agent "skew guard" is gone** (`src/core/pi-ai-skew.ts`; shipped in 1.6.5 as fail-fast, reworked in 1.6.6 to walk node_modules ancestors). The premise does not survive contact with pi's loader: pi resolves EVERY `@earendil-works/*` and `typebox` specifier in extension code to the HOST's own copies via jiti `virtualModules`/`alias` (verified in the 0.84.2 and 0.84.4 loader source — coverage includes the exact four specifiers this package imports), so the on-disk copies the guard compared are never loaded in-host. Under the host the guard measured dependencies nobody executes; its FATAL 'stale' refusal would have blocked a healthy extension after every host upgrade the moment the managed tree's inert copies lagged — and the 1.6.6 ancestor-walk made that state reachable on the primary `pi install npm:` layout (pi's own install code uses `--legacy-peer-deps` precisely because it expects extensions to declare these packages as peers, not dependencies). Host-version compatibility stays where it belongs: `checkPiCompatibility()` against the running host's VERSION — refuse below the 0.84.0 floor, warn once above the tested line — which is now the only startup check. The standalone CLI/SDK never ran the guard (it lived solely in the extension entry), so removal changes nothing there. What actually caused the original 2026-08-30 named-export crash remains unproven — in-host specifier mixing should have been impossible under 0.84.2's alias table — and is recorded here honestly rather than preserved as a false safety net.
|
|
28
|
+
|
|
29
|
+
### Verified
|
|
30
|
+
|
|
31
|
+
- 2993 unit tests over 236 files (11 new: the transformers-absent typed error and its classification — ESM and CJS not-found forms, transformers-internal failures passing through untouched, unrelated-package precision, and the DISABLED-memoization handoff to `isNativeStackUnavailableError`), plus lint, all four type-checks (TS6 and TS7 native, src and tests), dependency-cruiser, the audit gate, the full integration suite (57 passed / 8 skipped parallel, 103 serial), and the tarball manifest gate (235 files — one fewer with the deleted guard module). CLI boot verified end-to-end with the entire transformers/sharp/@img subtree physically absent from the install tree.
|
|
32
|
+
|
|
8
33
|
## [1.6.6] - 2026-08-31
|
|
9
34
|
|
|
10
35
|
Issue #9 reported deep-research runs stalling for minutes inside planning salvage
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincoln504/pi-research",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.8",
|
|
4
4
|
"description": "Web research for your agents with smart and safe tooling + knowledge store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -117,7 +117,6 @@
|
|
|
117
117
|
"@earendil-works/pi-ai": ">=0.84.0 <1",
|
|
118
118
|
"@earendil-works/pi-coding-agent": ">=0.84.0 <1",
|
|
119
119
|
"@earendil-works/pi-tui": ">=0.84.0 <1",
|
|
120
|
-
"@huggingface/transformers": "4.2.0",
|
|
121
120
|
"@kreuzberg/html-to-markdown-node": "^3.7.2",
|
|
122
121
|
"@lancedb/lancedb": "0.37.1",
|
|
123
122
|
"apache-arrow": "21.1.0",
|
|
@@ -135,6 +134,7 @@
|
|
|
135
134
|
"youtubei.js": "^18.0.0"
|
|
136
135
|
},
|
|
137
136
|
"optionalDependencies": {
|
|
137
|
+
"@huggingface/transformers": "4.2.0",
|
|
138
138
|
"webgpu": "^0.4.0"
|
|
139
139
|
},
|
|
140
140
|
"overrides": {
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { createResearchTool, createHealthTool } from './tool.ts';
|
|
|
12
12
|
import { createResearchKnowledgeSearchTool } from './tools/research-knowledge-search.ts';
|
|
13
13
|
import { logger } from './logger.ts';
|
|
14
14
|
import { checkPiCompatibility } from './core/pi-version.ts';
|
|
15
|
-
import { checkPiAiSkew } from './core/pi-ai-skew.ts';
|
|
16
15
|
import { randomUUID } from 'node:crypto';
|
|
17
16
|
import { shutdownManager } from './utils/shutdown-manager.ts';
|
|
18
17
|
import { healthRegistry } from './healthcheck/index.ts';
|
|
@@ -102,11 +101,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
102
101
|
// host auto-building its ModelRuntime. This in-host check is the real
|
|
103
102
|
// enforcement point: when running as an extension the host's version is NOT
|
|
104
103
|
// constrained by npm's resolution of our own package.json range.
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
104
|
+
//
|
|
105
|
+
// This is the ONLY version check at startup, and it needs to be: pi's extension
|
|
106
|
+
// loader aliases every @earendil-works/* and typebox specifier to the HOST's own
|
|
107
|
+
// copies (virtualModules/alias in the loader), so the copies in our install tree
|
|
108
|
+
// are never loaded in-host and comparing them against the host version — as the
|
|
109
|
+
// 1.5.x–1.6.6 skew guard did — measures dependencies nobody executes. The 1.6.6
|
|
110
|
+
// form of that guard false-fataled healthy installs after every host upgrade and
|
|
111
|
+
// was removed; see docs/CHANGELOG.md [1.6.7].
|
|
110
112
|
const compat = checkPiCompatibility(PI_VERSION);
|
|
111
113
|
if (compat.fatal) {
|
|
112
114
|
throw new Error(compat.message ?? `[pi-research] Unsupported pi-coding-agent version "${PI_VERSION}".`);
|
|
@@ -115,19 +117,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
115
117
|
logger.warn(compat.message);
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
// The gate above classifies the HOST version but cannot see the copies THIS
|
|
119
|
-
// extension resolves from its own node_modules — the 2026-08-30 incident
|
|
120
|
-
// (host 0.84.4 vs extension-resolved pi-ai 0.84.2) passed it as 'ok' while
|
|
121
|
-
// every researcher died at provider load. Detect that skew here and refuse
|
|
122
|
-
// to start with the actual remediation instead.
|
|
123
|
-
const skew = checkPiAiSkew();
|
|
124
|
-
if (skew.fatal) {
|
|
125
|
-
throw new Error(skew.message ?? '[pi-research] pi-ai version skew.');
|
|
126
|
-
}
|
|
127
|
-
if (skew.message) {
|
|
128
|
-
logger.warn(skew.message);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
120
|
// Re-register the beforeExit safety net (deactivate() strips event listeners during reload).
|
|
132
121
|
// This ensures the ONNX pipeline is disposed even after extension reloads.
|
|
133
122
|
registerBeforeExitSafetyNet();
|
|
@@ -73,6 +73,9 @@ export function isEmbedderUnreachable(err: unknown): boolean {
|
|
|
73
73
|
* Matched signatures (observed in the wild):
|
|
74
74
|
* - onnxruntime-node: "Cannot find module '.../bin/napi-v6/darwin/x64/onnxruntime_binding.node'"
|
|
75
75
|
* - @lancedb/lancedb: "Cannot find native binding. npm has a bug related to optional dependencies"
|
|
76
|
+
* - @huggingface/transformers (optional dep skipped at install time, #10): the typed
|
|
77
|
+
* TRANSFORMERS_UNAVAILABLE error from transformers-loader.ts, or its
|
|
78
|
+
* raw form "Cannot find package '@huggingface/transformers'"
|
|
76
79
|
* - jiti-masked module-eval failure of the above surfaces as:
|
|
77
80
|
* "KnowledgeStoreService is not a constructor"
|
|
78
81
|
*/
|
|
@@ -89,8 +92,10 @@ export function isNativeStackUnavailableError(err: unknown): boolean {
|
|
|
89
92
|
// `err` is already known truthy (guarded above), so no `err &&` here.
|
|
90
93
|
const code = (typeof err === 'object' && 'code' in err) ? String((err as { code?: unknown }).code) : '';
|
|
91
94
|
if (code === 'ERR_DLOPEN_FAILED') return true;
|
|
95
|
+
// The typed error from transformers-loader.ts — unambiguous by construction.
|
|
96
|
+
if (code === 'TRANSFORMERS_UNAVAILABLE') return true;
|
|
92
97
|
if ((code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') &&
|
|
93
|
-
(msg.includes('onnxruntime') || msg.includes('lancedb'))) {
|
|
98
|
+
(msg.includes('onnxruntime') || msg.includes('lancedb') || msg.includes('@huggingface/transformers'))) {
|
|
94
99
|
return true;
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -99,6 +104,7 @@ export function isNativeStackUnavailableError(err: unknown): boolean {
|
|
|
99
104
|
msg.includes('onnxruntime_binding.node') ||
|
|
100
105
|
/cannot find module '[^']*onnxruntime/.test(msg) ||
|
|
101
106
|
/cannot find module '[^']*lancedb/.test(msg) ||
|
|
107
|
+
/cannot find (?:package|module) ['"]@huggingface[/\\]transformers['"]/i.test(msg) ||
|
|
102
108
|
msg.includes('knowledgestoreservice is not a constructor')
|
|
103
109
|
);
|
|
104
110
|
}
|
|
@@ -18,14 +18,71 @@
|
|
|
18
18
|
* (isNativeStackUnavailableError) and degrades the store to DISABLED. Help, version,
|
|
19
19
|
* status, and browser-only research then work with no binding present.
|
|
20
20
|
*
|
|
21
|
+
* The package itself is an OPTIONAL dependency (it can be skipped entirely at
|
|
22
|
+
* install time when its native chain cannot build — sharp's platform binary is
|
|
23
|
+
* the historic offender, #10). A genuinely absent module is therefore a SUPPORTED
|
|
24
|
+
* state, not a crash: getTransformers() reclassifies it as a typed
|
|
25
|
+
* TRANSFORMERS_UNAVAILABLE error carrying remediation, which the knowledge store's
|
|
26
|
+
* native-unavailable classifier memoizes as DISABLED instead of re-running the
|
|
27
|
+
* init + backoff storm on every store touch. A transformers-INTERNAL load failure
|
|
28
|
+
* (its own onnxruntime import, a corrupt install) is re-thrown untouched — that is
|
|
29
|
+
* the pre-existing native-stack classification's job.
|
|
30
|
+
*
|
|
21
31
|
* Modules that need transformers *types* should keep `import type { … } from
|
|
22
32
|
* '@huggingface/transformers'` (type imports are erased and never hoisted); only
|
|
23
33
|
* runtime value access (pipeline, env, …) must go through getTransformers().
|
|
24
34
|
*/
|
|
25
35
|
|
|
36
|
+
/** Error.code on the typed error thrown when the optional module is not installed. */
|
|
37
|
+
export const TRANSFORMERS_UNAVAILABLE = 'TRANSFORMERS_UNAVAILABLE';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True when `err` is Node's "the module itself is not installed" rejection for
|
|
41
|
+
* OUR specifier — i.e. the optional dependency was skipped at install time.
|
|
42
|
+
* Deliberately narrow: a transformers-INTERNAL missing module (onnxruntime
|
|
43
|
+
* binding, corrupt install) resolves to a DIFFERENT file path and must stay
|
|
44
|
+
* unclassified here so the native-stack classifier handles it instead.
|
|
45
|
+
*/
|
|
46
|
+
export function isTransformersMissingError(err: unknown): boolean {
|
|
47
|
+
if (!err || typeof err !== 'object') return false;
|
|
48
|
+
const code = 'code' in err ? String((err as { code?: unknown }).code) : '';
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
// Node's ESM form is "Cannot find package '@huggingface/transformers' imported
|
|
51
|
+
// from …"; the CJS/child-process form is "Cannot find module '…'".
|
|
52
|
+
return (
|
|
53
|
+
code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND'
|
|
54
|
+
) && /@huggingface[/\\]transformers/.test(msg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the typed, remediation-carrying error for an absent optional package.
|
|
59
|
+
* Exported for tests; the code is what isNativeStackUnavailableError() keys on.
|
|
60
|
+
*/
|
|
61
|
+
export function transformersUnavailableError(cause: unknown): Error {
|
|
62
|
+
const err = new Error(
|
|
63
|
+
"Optional dependency '@huggingface/transformers' is not installed, so local embeddings are unavailable. "
|
|
64
|
+
+ 'npm skips this package when its native image chain cannot install (most often a failing sharp platform binary). '
|
|
65
|
+
+ 'The knowledge store runs without embeddings until it is present — reinstall this package to re-resolve its '
|
|
66
|
+
+ 'optional dependencies (pi install npm:@lincoln504/pi-research), or install it directly '
|
|
67
|
+
+ '(npm install @huggingface/transformers).',
|
|
68
|
+
{ cause },
|
|
69
|
+
);
|
|
70
|
+
(err as { code?: string }).code = TRANSFORMERS_UNAVAILABLE;
|
|
71
|
+
return err;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
let _transformers: typeof import('@huggingface/transformers') | undefined;
|
|
27
75
|
|
|
28
76
|
/** Resolve the transformers module, memoizing after the first load. */
|
|
29
77
|
export async function getTransformers(): Promise<typeof import('@huggingface/transformers')> {
|
|
30
|
-
|
|
78
|
+
if (_transformers) return _transformers;
|
|
79
|
+
try {
|
|
80
|
+
_transformers = await import('@huggingface/transformers');
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (isTransformersMissingError(err)) {
|
|
83
|
+
throw transformersUnavailableError(err);
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
return _transformers;
|
|
31
88
|
}
|
package/src/core/pi-ai-skew.ts
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ai module-graph skew detection.
|
|
3
|
-
*
|
|
4
|
-
* The version gate in pi-version.ts classifies the HOST pi-coding-agent
|
|
5
|
-
* version against our supported window — but it cannot see the failure that
|
|
6
|
-
* actually took pi-research down on 2026-08-30: as an extension we resolve
|
|
7
|
-
* `@earendil-works/*` from our OWN node_modules, and when those copies lag the
|
|
8
|
-
* host (host 0.84.4, extension-resolved pi-ai 0.84.2), newer host-era modules
|
|
9
|
-
* and older extension-era modules meet in one process graph. The visible
|
|
10
|
-
* symptom was researchers dying at provider load with
|
|
11
|
-
* "The requested module './simple-options.js' does not provide an export named
|
|
12
|
-
* 'clampThinkingBudgetToAnswerRoom'" (first exported in pi-ai 0.84.3), with
|
|
13
|
-
* the knowledge store's triage/extraction LLM calls failing the same way —
|
|
14
|
-
* every LLM path, wrapped as unrelated-looking "Provider error" strings.
|
|
15
|
-
*
|
|
16
|
-
* Neither npm nor pi enforces agreement here: our declared range
|
|
17
|
-
* (">=0.84.0 <1") cannot constrain the host, and a dev-symlinked or
|
|
18
|
-
* prefix-installed extension keeps whatever node_modules it was installed
|
|
19
|
-
* with while the host updates independently. The only enforcement point is
|
|
20
|
-
* in-process, at load — this module.
|
|
21
|
-
*
|
|
22
|
-
* Detection is deliberately filesystem-based rather than module-resolution-
|
|
23
|
-
* based: the @earendil-works packages' exports maps are import-only, so
|
|
24
|
-
* `createRequire(...).resolve()` fails with ERR_PACKAGE_PATH_NOT_EXPORTED,
|
|
25
|
-
* and resolution would find the same (possibly stale) copy either way. Instead
|
|
26
|
-
* we walk directories to package.json files directly, reproducing the node_modules
|
|
27
|
-
* walk Node's bare-specifier resolution performs:
|
|
28
|
-
*
|
|
29
|
-
* - EXTENSION side: from this module's own directory, walk ancestors and at
|
|
30
|
-
* each level read node_modules/@earendil-works/{pi-ai,pi-coding-agent}/package.json;
|
|
31
|
-
* the FIRST hit is the copy our bare imports actually bind. The package's own
|
|
32
|
-
* node_modules wins when present (git installs clone + `npm install` inside the
|
|
33
|
-
* checkout; the dev repo has its own), but on the PRIMARY layout — `pi install
|
|
34
|
-
* npm:` — pi runs one flat managed npm tree at ~/.pi/agent/npm/ where the
|
|
35
|
-
* extension sits at node_modules/@lincoln504/pi-research with NO nested copies
|
|
36
|
-
* (measured) and its @earendil-works deps hoist to the managed root. A
|
|
37
|
-
* nested-only lookup misses those entirely and would false-warn "missing" on
|
|
38
|
-
* every healthy startup — so the walk must continue past the package root,
|
|
39
|
-
* exactly like Node's does. The package root (nearest package.json) is still
|
|
40
|
-
* recorded as the remedy target for genuinely-broken installs.
|
|
41
|
-
* - HOST side: from the process entry script (process.argv[1], realpath'd —
|
|
42
|
-
* Node resolves the main entry by default, so an nvm bin symlink anchors
|
|
43
|
-
* inside the host package) up to the package.json whose name IS
|
|
44
|
-
* '@earendil-works/pi-coding-agent', then its nested pi-ai. In a pi run
|
|
45
|
-
* that is the host's own tree; standalone (our CLI/SDK, tests) the walk
|
|
46
|
-
* finds no such package and the check no-ops rather than guessing.
|
|
47
|
-
*
|
|
48
|
-
* The comparison relies on the pi monorepo's lockstep versioning — every
|
|
49
|
-
* observed release ships pi-coding-agent and pi-ai (and the other
|
|
50
|
-
* @earendil-works/* packages) at the same version — so a mismatch against
|
|
51
|
-
* the host's pi-coding-agent version is a reliable skew signal for pi-ai too.
|
|
52
|
-
* A STALE extension copy (older than the host) is FATAL: that is the proven
|
|
53
|
-
* crash direction. A NEWER extension copy only warns: untested, but no known
|
|
54
|
-
* failure mode. Internal disagreement between the extension's own pi-ai and
|
|
55
|
-
* pi-coding-agent warns as well — a half-updated node_modules.
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
import { readFileSync, realpathSync } from 'node:fs';
|
|
59
|
-
import * as path from 'node:path';
|
|
60
|
-
import { fileURLToPath } from 'node:url';
|
|
61
|
-
import { compareVersions, parsePiVersion } from './pi-version.ts';
|
|
62
|
-
|
|
63
|
-
/** Minimal package.json shape this module reads. */
|
|
64
|
-
interface PkgJson {
|
|
65
|
-
name?: string;
|
|
66
|
-
version?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function readPkgJson(file: string): PkgJson | null {
|
|
70
|
-
try {
|
|
71
|
-
return JSON.parse(readFileSync(file, 'utf8')) as PkgJson;
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Walk up from `startFile` to the nearest package.json.
|
|
79
|
-
* When `name` is given, keep walking past non-matching package.json files
|
|
80
|
-
* (scoped packages nest: pi-coding-agent/dist/bundle/cli.js must climb three
|
|
81
|
-
* levels) and stop at the first match; returns null when never found.
|
|
82
|
-
*/
|
|
83
|
-
function findPackageRoot(startFile: string, name?: string): { dir: string; pkg: PkgJson } | null {
|
|
84
|
-
let dir = path.dirname(path.resolve(startFile));
|
|
85
|
-
for (let i = 0; i < 16; i++) {
|
|
86
|
-
const pkg = readPkgJson(path.join(dir, 'package.json'));
|
|
87
|
-
if (pkg && (name === undefined || pkg.name === name)) {
|
|
88
|
-
return { dir, pkg };
|
|
89
|
-
}
|
|
90
|
-
const up = path.dirname(dir);
|
|
91
|
-
if (up === dir) return null;
|
|
92
|
-
dir = up;
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** A resolved @earendil-works copy: its version and the install root that provided it. */
|
|
98
|
-
export interface ResolvedSdkCopy {
|
|
99
|
-
version: string;
|
|
100
|
-
/** The directory whose node_modules produced this copy — the `cd` target for the install remedy. */
|
|
101
|
-
installRoot: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Resolve `@earendil-works/<pkg>` from `startDir` exactly as Node's bare-specifier
|
|
106
|
-
* resolution binds it: walk ancestors, and at each level take the FIRST
|
|
107
|
-
* `<dir>/node_modules/@earendil-works/<pkg>/package.json` found. Covers the nested
|
|
108
|
-
* copies of a git checkout, the dev repo's own node_modules, and — critically — the
|
|
109
|
-
* flat managed tree `pi install npm:` produces, where the copies hoist to the
|
|
110
|
-
* managed root ABOVE the package and a nested-only lookup would miss them.
|
|
111
|
-
*/
|
|
112
|
-
function resolveSdkCopy(startDir: string, pkg: string): ResolvedSdkCopy | null {
|
|
113
|
-
let dir = path.resolve(startDir);
|
|
114
|
-
for (let i = 0; i < 32; i++) {
|
|
115
|
-
const pkgDir = path.join(dir, 'node_modules', '@earendil-works', pkg);
|
|
116
|
-
const p = readPkgJson(path.join(pkgDir, 'package.json'));
|
|
117
|
-
if (p?.version) return { version: p.version, installRoot: dir };
|
|
118
|
-
const up = path.dirname(dir);
|
|
119
|
-
if (up === dir) return null;
|
|
120
|
-
dir = up;
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export interface PiAiSkewInput {
|
|
126
|
-
/** Host pi-coding-agent version (its package.json), or null when not running under pi. */
|
|
127
|
-
hostVersion: string | null;
|
|
128
|
-
/** Extension-resolved pi-ai version, or null when our node_modules lacks it. */
|
|
129
|
-
extPiAi: string | null;
|
|
130
|
-
/** Extension-resolved pi-coding-agent version, or null. */
|
|
131
|
-
extPiCodingAgent: string | null;
|
|
132
|
-
/** Where the pi-ai copy resolved FROM (the install root whose node_modules has it).
|
|
133
|
-
* Lets the remedy `cd` into the tree that actually owns a stale copy — the flat
|
|
134
|
-
* managed tree of `pi install npm:`, not the package dir. Falls back to extRoot. */
|
|
135
|
-
piAiRoot?: string;
|
|
136
|
-
/** Same, for the pi-coding-agent copy. */
|
|
137
|
-
piCodingAgentRoot?: string;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export type PiAiSkewLevel = 'ok' | 'standalone' | 'incomplete' | 'stale' | 'newer' | 'internal';
|
|
141
|
-
|
|
142
|
-
export interface PiAiSkewResult {
|
|
143
|
-
level: PiAiSkewLevel;
|
|
144
|
-
/** True when the extension must refuse to start (proven crash direction). */
|
|
145
|
-
fatal: boolean;
|
|
146
|
-
message: string | null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Classify a skew state. Pure — no filesystem access — so the policy is
|
|
151
|
-
* testable without fixtures, mirroring checkPiCompatibility's design.
|
|
152
|
-
*/
|
|
153
|
-
export function classifyPiAiSkew(input: PiAiSkewInput, extRoot?: string): PiAiSkewResult {
|
|
154
|
-
const { hostVersion, extPiAi, extPiCodingAgent } = input;
|
|
155
|
-
|
|
156
|
-
// Standalone CLI/SDK/tests: no host pi package above us — nothing to skew
|
|
157
|
-
// against; our own imports are the only copies in play.
|
|
158
|
-
if (!hostVersion) {
|
|
159
|
-
return { level: 'standalone', fatal: false, message: null };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// The remedy must `cd` into the tree that OWNS the copy being replaced: the
|
|
163
|
-
// package's own checkout when the copy resolved nested there, or — on the flat
|
|
164
|
-
// `pi install npm:` managed tree — the managed root its deps hoisted to.
|
|
165
|
-
const fixRootOf = (copyRoot: string | undefined) => copyRoot ?? extRoot;
|
|
166
|
-
const remedyFor = (copyRoot: string | undefined) => {
|
|
167
|
-
const root = fixRootOf(copyRoot);
|
|
168
|
-
return root
|
|
169
|
-
? `Fix: cd ${root} && npm install (align this install's @earendil-works copies with host pi ${hostVersion}), then reload pi.`
|
|
170
|
-
: `Fix: reinstall this extension so its node_modules matches host pi ${hostVersion}, then reload pi.`;
|
|
171
|
-
};
|
|
172
|
-
// Default remedy when no specific copy is implicated (absent-copy warns).
|
|
173
|
-
const remedy = remedyFor(undefined);
|
|
174
|
-
|
|
175
|
-
if (!extPiAi || !extPiCodingAgent) {
|
|
176
|
-
// A PRESENT copy that already lags the host is the proven crash direction
|
|
177
|
-
// even in a half-hoisted layout (the 2026-08-30 failure had exactly this
|
|
178
|
-
// shape: stale nested pi-ai beside a hoisted pi-coding-agent) — classify it
|
|
179
|
-
// stale/fatal, not the softer incomplete warn. Only a genuinely-absent
|
|
180
|
-
// copy (nothing to compare) stays warn.
|
|
181
|
-
const presentVersion = extPiAi ?? extPiCodingAgent;
|
|
182
|
-
const presentPkg = extPiAi ? 'pi-ai' : 'pi-coding-agent';
|
|
183
|
-
const present = presentVersion ? parsePiVersion(presentVersion) : null;
|
|
184
|
-
const host = parsePiVersion(hostVersion);
|
|
185
|
-
if (present && host && compareVersions(present, host) < 0) {
|
|
186
|
-
const presentRemedy = remedyFor(extPiAi ? input.piAiRoot : input.piCodingAgentRoot);
|
|
187
|
-
return {
|
|
188
|
-
level: 'stale',
|
|
189
|
-
fatal: true,
|
|
190
|
-
message:
|
|
191
|
-
`[pi-research] Extension node_modules has @earendil-works/${presentPkg} ${presentVersion}, which LAGS host pi ${hostVersion} ` +
|
|
192
|
-
`(${extPiAi ? 'pi-coding-agent' : 'pi-ai'} is not resolvable here to compare against) — a copy lagging the host crashes every LLM call at provider load ` +
|
|
193
|
-
`(ESM export errors like the missing 'clampThinkingBudgetToAnswerRoom' of pi-ai 0.84.2-vs-0.84.3+). ${presentRemedy}`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
level: 'incomplete',
|
|
198
|
-
fatal: false,
|
|
199
|
-
message:
|
|
200
|
-
`[pi-research] This extension cannot resolve @earendil-works/` +
|
|
201
|
-
`${!extPiAi ? 'pi-ai' : 'pi-coding-agent'} from its install location (walked node_modules up the tree) — LLM calls may fail at module load. ${remedy}`,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const host = parsePiVersion(hostVersion);
|
|
206
|
-
const ai = parsePiVersion(extPiAi);
|
|
207
|
-
const ca = parsePiVersion(extPiCodingAgent);
|
|
208
|
-
if (!host || !ai || !ca) {
|
|
209
|
-
// Unparseable versions: warn, never brick — this guard exists to make
|
|
210
|
-
// failures LOUD, not to invent new startup failures.
|
|
211
|
-
return {
|
|
212
|
-
level: 'incomplete',
|
|
213
|
-
fatal: false,
|
|
214
|
-
message: `[pi-research] Could not parse pi-ai skew versions (host=${hostVersion}, pi-ai=${extPiAi}, pi-coding-agent=${extPiCodingAgent}); skipping skew check.`,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const aiOlder = compareVersions(ai, host) < 0;
|
|
219
|
-
const caOlder = compareVersions(ca, host) < 0;
|
|
220
|
-
|
|
221
|
-
// Internal disagreement (a half-updated node_modules) is the root state and
|
|
222
|
-
// the more precise diagnosis, so it is classified BEFORE the direction
|
|
223
|
-
// checks — otherwise it is unreachable (ai !== ca implies one of them
|
|
224
|
-
// differs from the host). Severity still follows the proven crash
|
|
225
|
-
// direction: fatal when either copy lags the host.
|
|
226
|
-
if (compareVersions(ai, ca) !== 0) {
|
|
227
|
-
const anyOlder = aiOlder || caOlder;
|
|
228
|
-
const base =
|
|
229
|
-
`[pi-research] Extension node_modules is internally inconsistent (pi-ai ${extPiAi} vs ` +
|
|
230
|
-
`pi-coding-agent ${extPiCodingAgent}) — a half-updated install against host pi ${hostVersion}. `;
|
|
231
|
-
// Aim the remedy at the tree owning the LAGGING copy — that is the one to update.
|
|
232
|
-
const laggingRemedy = remedyFor(aiOlder ? input.piAiRoot : input.piCodingAgentRoot);
|
|
233
|
-
if (anyOlder) {
|
|
234
|
-
return {
|
|
235
|
-
level: 'internal',
|
|
236
|
-
fatal: true,
|
|
237
|
-
message:
|
|
238
|
-
base +
|
|
239
|
-
`A copy lagging the host crashes every LLM call at provider load ` +
|
|
240
|
-
`(ESM export errors like the missing 'clampThinkingBudgetToAnswerRoom' ` +
|
|
241
|
-
`of pi-ai 0.84.2-vs-0.84.3+). ${laggingRemedy}`,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
level: 'internal',
|
|
246
|
-
fatal: false,
|
|
247
|
-
message: base + `Untested direction — if LLM calls fail at module load, this is the first suspect. ${remedy}`,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (aiOlder || caOlder) {
|
|
252
|
-
// The 2026-08-30 incident: host 0.84.4 + extension pi-ai 0.84.2. Newer
|
|
253
|
-
// host-era modules import symbols the stale copies never exported, and
|
|
254
|
-
// every researcher/knowledge LLM call dies at provider load with a
|
|
255
|
-
// confusing ESM named-export error. Refusing to start with THIS message
|
|
256
|
-
// is strictly better than five silent researcher deaths.
|
|
257
|
-
const staleRemedy = remedyFor(aiOlder ? input.piAiRoot : input.piCodingAgentRoot);
|
|
258
|
-
return {
|
|
259
|
-
level: 'stale',
|
|
260
|
-
fatal: true,
|
|
261
|
-
message:
|
|
262
|
-
`[pi-research] Fatal version skew: host pi ${hostVersion} but this extension resolves ` +
|
|
263
|
-
`pi-ai ${extPiAi} / pi-coding-agent ${extPiCodingAgent}. Mixed-version module graphs crash ` +
|
|
264
|
-
`every LLM call at provider load (ESM export errors like the missing ` +
|
|
265
|
-
`'clampThinkingBudgetToAnswerRoom' of pi-ai 0.84.2-vs-0.84.3+). ${staleRemedy}`,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const aiNewer = compareVersions(ai, host) > 0;
|
|
270
|
-
const caNewer = compareVersions(ca, host) > 0;
|
|
271
|
-
if (aiNewer || caNewer) {
|
|
272
|
-
return {
|
|
273
|
-
level: 'newer',
|
|
274
|
-
fatal: false,
|
|
275
|
-
message:
|
|
276
|
-
`[pi-research] Extension resolves pi-ai ${extPiAi} / pi-coding-agent ${extPiCodingAgent}, ` +
|
|
277
|
-
`newer than host pi ${hostVersion}. Untested direction — if LLM calls fail at module load, ` +
|
|
278
|
-
`this mismatch is the first suspect. ${remedy}`,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return { level: 'ok', fatal: false, message: null };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Extension-side resolution, exported for fixture tests: resolves both SDK copies
|
|
287
|
-
* from `fromFile`'s location the way Node would bind them, plus the package root
|
|
288
|
-
* (the remedy target of last resort for genuinely-broken installs).
|
|
289
|
-
*/
|
|
290
|
-
export interface ExtensionCopies {
|
|
291
|
-
extRoot: string | null;
|
|
292
|
-
piAi: string | null;
|
|
293
|
-
piCodingAgent: string | null;
|
|
294
|
-
piAiRoot: string | null;
|
|
295
|
-
piCodingAgentRoot: string | null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function resolveExtensionCopies(fromFile: string): ExtensionCopies {
|
|
299
|
-
// jiti hands us a file:// URL; real-path strings also accepted.
|
|
300
|
-
const anchor = fromFile.startsWith('file:') ? fileURLToPath(fromFile) : fromFile;
|
|
301
|
-
const ext = findPackageRoot(anchor);
|
|
302
|
-
// Walk from the MODULE's directory (not the package root): Node checks
|
|
303
|
-
// <dir>/node_modules at every ancestor of the importing file, so the package's
|
|
304
|
-
// own node_modules is covered AND the walk continues past it into whatever
|
|
305
|
-
// install root the package lives in — the flat managed tree `pi install npm:`
|
|
306
|
-
// produces, a project's hoisted tree, or the monorepo root.
|
|
307
|
-
const startDir = path.dirname(path.resolve(anchor));
|
|
308
|
-
const ai = resolveSdkCopy(startDir, 'pi-ai');
|
|
309
|
-
const ca = resolveSdkCopy(startDir, 'pi-coding-agent');
|
|
310
|
-
return {
|
|
311
|
-
extRoot: ext?.dir ?? null,
|
|
312
|
-
piAi: ai?.version ?? null,
|
|
313
|
-
piCodingAgent: ca?.version ?? null,
|
|
314
|
-
piAiRoot: ai?.installRoot ?? null,
|
|
315
|
-
piCodingAgentRoot: ca?.installRoot ?? null,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Collect the real skew state from this process and classify it.
|
|
321
|
-
*
|
|
322
|
-
* Never throws: any read/walk failure degrades to null inputs (classified as
|
|
323
|
-
* 'standalone'/'incomplete'), because a broken guard must not take the
|
|
324
|
-
* extension down with it.
|
|
325
|
-
*/
|
|
326
|
-
export function checkPiAiSkew(fromFile: string = import.meta.url): PiAiSkewResult {
|
|
327
|
-
const copies = resolveExtensionCopies(fromFile);
|
|
328
|
-
|
|
329
|
-
// Host side: the process entry script, realpath'd. In a pi run this anchors
|
|
330
|
-
// inside the host's own package; anywhere else the named walk finds nothing.
|
|
331
|
-
let hostVersion: string | null = null;
|
|
332
|
-
try {
|
|
333
|
-
const entry = realpathSync(process.argv[1] ?? '');
|
|
334
|
-
const host = findPackageRoot(entry, '@earendil-works/pi-coding-agent');
|
|
335
|
-
hostVersion = host?.pkg.version ?? null;
|
|
336
|
-
} catch {
|
|
337
|
-
// Entry missing/unreadable (standalone context) — hostVersion stays null
|
|
338
|
-
// and classifyPiAiSkew treats the host side as undetectable.
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return classifyPiAiSkew(
|
|
342
|
-
{
|
|
343
|
-
hostVersion,
|
|
344
|
-
extPiAi: copies.piAi,
|
|
345
|
-
extPiCodingAgent: copies.piCodingAgent,
|
|
346
|
-
piAiRoot: copies.piAiRoot ?? undefined,
|
|
347
|
-
piCodingAgentRoot: copies.piCodingAgentRoot ?? undefined,
|
|
348
|
-
},
|
|
349
|
-
copies.extRoot ?? undefined,
|
|
350
|
-
);
|
|
351
|
-
}
|