@companion-ai/feynman 0.5.13 → 0.5.15

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/RELEASES.md CHANGED
@@ -6,6 +6,19 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.15 - 2026-09-29
10
+
11
+ - **Pi 0.99.1, pinned.** Feynman accepted any Pi version, so npm installs picked up Pi 0.99.1 the day it was released, untested, and Pi 0.99 warns on every launch about how Feynman declared it. Feynman now runs on Pi 0.99.1, which adds MCP server support (`/mcp`), declared the way Pi asks, and pinned so new Pi releases arrive only after they pass Feynman's checks. If npm has `legacy-peer-deps` on, it skips Pi; `feynman` now says to reinstall with `--legacy-peer-deps=false` instead of failing on a missing package.
12
+ - **npm installs use a patched undici.** pi-subagents pins undici 8.10.0, which ten advisories published on 2026-09-29 affect (plus GHSA-3wwx-pv8p-q78v), and npm hoisted that copy for every package in an npm install. Feynman now depends on a patched undici directly, so only pi-subagents keeps 8.10.0 until it releases its bump (nicobailon/pi-subagents#2548); the standalone installers were already patched. Correction to the 0.5.14 notes: pi-subagents' background runner installs its undici as the process-wide fetch and WebSocket, so the advisories do apply to background subagent runs.
13
+ - **`feynman model set` explains a model it cannot use.** "Model not available in Pi auth storage" gave no reason. It now says which applies: the provider has no credentials (with the `feynman model login` command to run), a local provider in `models.json` has no `apiKey` (Pi hides providers without one; a placeholder such as `"local"` is enough), or the ID is unknown (with close matches, for example `gpt-5.6` suggests `openai/gpt-5.6-terra`).
14
+ - **Parallel OpenAlex semantic searches no longer fail.** OpenAlex allows one semantic search per second, and parallel searches got `429 Too Many Requests`: in a live test of six at once, five failed. They now run one after another at that pace, and a 429 that asks for a short wait is retried once. Six parallel searches now all succeed.
15
+ - `feynman model set` with no model opens a picker of the available models instead of failing. `feynman model help` lists the model commands, an unknown model subcommand points to it, and `-h` works like `--help`.
16
+
17
+ ## v0.5.14 - 2026-09-28
18
+
19
+ - **The Windows installer no longer fails on slow networks (#309).** When usage telemetry could not reach PostHog quickly, the PostHog library printed a timeout to stderr, and Windows PowerShell treats stderr from the installer's `feynman --help` check as a failure. Each telemetry send now has a 1.5 s budget and ends quietly, so no command waits on telemetry (on a stalled network `feynman --help` took 11 s and printed two errors; it now takes about 2 s and prints nothing). The installers also run their checks with telemetry off and judge them by exit code.
20
+ - pi-subagents 0.73.1 (no longer discards the prompt cache when subagents turn on; caps very large workflow output) and pi-web-access 0.33.0 (resumed sessions keep their tools; no false Pi version warnings). undici is overridden to a patched version for GHSA-3wwx-pv8p-q78v in the standalone installers; npm installs get the fix when pi-subagents releases its bump (nicobailon/pi-subagents#2548); pi-subagents only uses undici's HTTP proxy agent, not the affected WebSocket client.
21
+
9
22
  ## v0.5.13 - 2026-09-28
10
23
 
11
24
  - **OpenCode no longer recommends a retired Kimi model.** OpenCode Go and OpenCode Zen retired `kimi-k2.6` ("Use kimi-k2.7-code instead"), but Feynman still picked it as the default for new OpenCode users. It now recommends `kimi-k2.7-code`. If `feynman status` shows `kimi-k2.6`, switch with `feynman model set opencode-go/kimi-k2.7-code`. Three model preferences that no longer exist in the catalog were removed.
package/bin/feynman.js CHANGED
@@ -40,4 +40,15 @@ if (process.argv.length === 3 && process.argv[2] === "--version") {
40
40
  process.exit(0);
41
41
  }
42
42
 
43
+ // Pi is a peer dependency, as Pi requires of extension packages, and npm skips
44
+ // peers when legacy-peer-deps is on.
45
+ try {
46
+ import.meta.resolve("@earendil-works/pi-coding-agent");
47
+ import.meta.resolve("@earendil-works/pi-ai");
48
+ } catch {
49
+ console.error("feynman could not find its Pi runtime (@earendil-works/pi-coding-agent), which npm skips when legacy-peer-deps is on.");
50
+ console.error("Reinstall with: npm install -g @companion-ai/feynman --legacy-peer-deps=false");
51
+ process.exit(1);
52
+ }
53
+
43
54
  await import(pathToFileURL(resolve(here, "..", "dist", "index.js")).href);
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import { installPiPackage, listOptionalPackagePresets, normalizeOptionalPackageP
22
22
  import { canonicalizeModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
23
23
  import { BUNDLED_PI_PACKAGES, buildPiEnv } from "./pi/runtime.js";
24
24
  import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js";
25
- import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, setDefaultModelSpec, } from "./model/commands.js";
25
+ import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, selectDefaultModel, setDefaultModelSpec, } from "./model/commands.js";
26
26
  import { buildModelStatusSnapshotFromRecords, getAuthenticatedModelRecords, isProClassModelSpec, getSupportedModelRecords, } from "./model/catalog.js";
27
27
  import { clearSearchConfig, printSearchStatus, setSearchProvider } from "./search/commands.js";
28
28
  import { fetchLatestFeynmanVersion, getFeynmanUpgradeLines, isNewerVersion } from "./system/self-update.js";
@@ -189,10 +189,15 @@ async function handleModelCommand(subcommand, args, feynmanSettingsPath, feynman
189
189
  }
190
190
  if (subcommand === "set") {
191
191
  const spec = args[0];
192
- if (!spec) {
192
+ if (spec) {
193
+ await setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
194
+ }
195
+ else if (process.stdin.isTTY && process.stdout.isTTY) {
196
+ await selectDefaultModel(feynmanSettingsPath, feynmanAuthPath);
197
+ }
198
+ else {
193
199
  throw new Error("Usage: feynman model set <provider/model|provider:model>");
194
200
  }
195
- await setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
196
201
  return;
197
202
  }
198
203
  if (subcommand === "tier") {
@@ -214,7 +219,14 @@ async function handleModelCommand(subcommand, args, feynmanSettingsPath, feynman
214
219
  console.log(`Service tier set to ${tier}`);
215
220
  return;
216
221
  }
217
- throw new Error(`Unknown model command: ${subcommand}`);
222
+ if (subcommand === "help") {
223
+ printSection("Model Management");
224
+ for (const command of cliCommandSections.find((section) => section.title === "Model Management")?.commands ?? []) {
225
+ printHelpLine(command.usage, command.description);
226
+ }
227
+ return;
228
+ }
229
+ throw new Error(`Unknown model command: ${subcommand}\nRun \`feynman model help\` to see model commands.`);
218
230
  }
219
231
  async function handleUpdateCommand(piOptions, feynmanVersion, source) {
220
232
  const latestFeynmanVersionPromise = fetchLatestFeynmanVersion();
@@ -436,7 +448,7 @@ async function runMain(input) {
436
448
  doctor: { type: "boolean" },
437
449
  export: { type: "string" },
438
450
  fork: { type: "string" },
439
- help: { type: "boolean" },
451
+ help: { type: "boolean", short: "h" },
440
452
  version: { type: "boolean" },
441
453
  "alpha-login": { type: "boolean" },
442
454
  "alpha-logout": { type: "boolean" },
@@ -614,7 +626,7 @@ async function runMain(input) {
614
626
  const modelRuntime = await createModelRuntime(feynmanAuthPath);
615
627
  const canonicalModelSpec = canonicalizeModelSpec(requestedExplicitModelSpec, modelRuntime);
616
628
  if (!canonicalModelSpec) {
617
- throw new Error(`Unknown model: ${requestedExplicitModelSpec}`);
629
+ throw new Error(`Unknown model: ${requestedExplicitModelSpec}. Run \`feynman model list\` to see available models.`);
618
630
  }
619
631
  explicitModelSpec = canonicalModelSpec;
620
632
  }
@@ -5,7 +5,7 @@ import { readJson } from "../pi/settings.js";
5
5
  import { promptChoice, promptSelect, promptText } from "../setup/prompts.js";
6
6
  import { openUrl } from "../system/open-url.js";
7
7
  import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
8
- import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, } from "./catalog.js";
8
+ import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
9
9
  import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
10
10
  import { createModelRegistry, createModelRuntime, getModelsJsonPath } from "./registry.js";
11
11
  import { upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
@@ -821,10 +821,32 @@ export async function logoutModelProvider(authPath, providerId) {
821
821
  await modelRuntime.logout(provider.id);
822
822
  printSuccess(`Model provider logout complete: ${provider.id}`);
823
823
  }
824
+ // Explains why `feynman model set <spec>` found no usable model.
825
+ async function describeUnavailableModelSpec(authPath, spec) {
826
+ const input = spec.trim().replace(/^([^/:]+):(.+)$/, "$1/$2").toLowerCase();
827
+ const known = (await getSupportedModelRecords(authPath)).find((model) => `${model.provider}/${model.id}`.toLowerCase() === input || model.id.toLowerCase() === input);
828
+ if (known) {
829
+ const knownSpec = `${known.provider}/${known.id}`;
830
+ if (isProClassModelSpec(knownSpec)) {
831
+ return `Pro-class model disabled: ${knownSpec}. Choose an approved research model.`;
832
+ }
833
+ if (isLocalModelProvider(authPath, known.provider)) {
834
+ return `${knownSpec} has no API key. Pi only lists a local provider when it has one: add "apiKey": "local" to the ${known.provider} provider in ${getModelsJsonPath(authPath)}.`;
835
+ }
836
+ return `${knownSpec} has no credentials. Run \`feynman model login ${known.provider}\` first.`;
837
+ }
838
+ const modelId = input.slice(input.lastIndexOf("/") + 1);
839
+ const matches = (await getAvailableModelRecords(authPath))
840
+ .map((model) => `${model.provider}/${model.id}`)
841
+ .filter((candidate) => candidate.toLowerCase().includes(modelId))
842
+ .slice(0, 5);
843
+ const suggestion = matches.length > 0 ? ` Did you mean ${matches.join(", ")}?` : "";
844
+ return `Unknown model: ${spec}.${suggestion} Run \`feynman model list\` to see available models.`;
845
+ }
824
846
  export async function setDefaultModelSpec(settingsPath, authPath, spec) {
825
847
  const resolvedSpec = await resolveAvailableModelSpec(authPath, spec);
826
848
  if (!resolvedSpec) {
827
- throw new Error(`Model not available in Pi auth storage: ${spec}. Run \`feynman model list\` first.`);
849
+ throw new Error(await describeUnavailableModelSpec(authPath, spec));
828
850
  }
829
851
  const [provider, ...rest] = resolvedSpec.split("/");
830
852
  const modelId = rest.join("/");
@@ -834,6 +856,22 @@ export async function setDefaultModelSpec(settingsPath, authPath, spec) {
834
856
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
835
857
  printSuccess(`Research default model set to ${resolvedSpec}`);
836
858
  }
859
+ export async function selectDefaultModel(settingsPath, authPath) {
860
+ const status = await collectModelStatus(settingsPath, authPath);
861
+ if (status.availableModels.length === 0) {
862
+ printWarning("No authenticated Pi models are currently available.");
863
+ for (const line of status.guidance) {
864
+ printInfo(line);
865
+ }
866
+ return;
867
+ }
868
+ const spec = await promptSelect("Choose the default research model:", status.availableModels.map((value) => ({
869
+ value,
870
+ label: value,
871
+ hint: value === status.current ? "current" : value === status.recommended ? "recommended" : undefined,
872
+ })), status.current ?? status.recommended);
873
+ await setDefaultModelSpec(settingsPath, authPath, spec);
874
+ }
837
875
  export async function runModelSetup(settingsPath, authPath) {
838
876
  let status = await collectModelStatus(settingsPath, authPath);
839
877
  while (status.availableModels.length === 0) {
@@ -21,13 +21,20 @@ let telemetryInitialized = false;
21
21
  let telemetryStartWarningPrinted = false;
22
22
  let telemetryTransportFailed = false;
23
23
  let telemetryNoticeThisProcess;
24
+ // On a slow or filtered network a pending send made every command wait for
25
+ // posthog-node's 10s deadline, and the library then printed its timeout to
26
+ // stderr, which Windows PowerShell treats as a failure (#309). Give each send
27
+ // a short budget and end it here, where it counts as a quiet transport failure.
28
+ export const TELEMETRY_REQUEST_BUDGET_MS = 1500;
24
29
  /** One silent attempt per session: after the first failure, later requests are dropped. */
25
- export function createTelemetryCircuitBreakerFetch(fetchImpl, onTransportFailure) {
30
+ export function createTelemetryCircuitBreakerFetch(fetchImpl, onTransportFailure, budgetMs = TELEMETRY_REQUEST_BUDGET_MS) {
26
31
  let open = false;
27
32
  return async (url, options) => {
28
33
  if (!open) {
29
34
  try {
30
- const response = await fetchImpl(url, options);
35
+ const budget = AbortSignal.timeout(budgetMs);
36
+ const signal = options.signal ? AbortSignal.any([options.signal, budget]) : budget;
37
+ const response = await fetchImpl(url, { ...options, signal });
31
38
  if (response.status >= 200 && response.status < 400)
32
39
  return response;
33
40
  throw new Error(`PostHog transport returned HTTP ${response.status}`);
@@ -0,0 +1,17 @@
1
+ // Runs requests one after another, each starting at least `minGapMs` after the
2
+ // previous one started. Pi runs parallel tool calls in one process, so this keeps
3
+ // them under a service's per-second limit.
4
+ export function createRequestPacer(): <T>(minGapMs: number, run: () => Promise<T>) => Promise<T> {
5
+ let queue: Promise<unknown> = Promise.resolve();
6
+ let lastStartedAt = Number.NEGATIVE_INFINITY;
7
+ return <T>(minGapMs: number, run: () => Promise<T>): Promise<T> => {
8
+ const next = queue.then(async () => {
9
+ const wait = lastStartedAt + minGapMs - Date.now();
10
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
11
+ lastStartedAt = Date.now();
12
+ return run();
13
+ });
14
+ queue = next.catch(() => {});
15
+ return next;
16
+ };
17
+ }
@@ -1,4 +1,5 @@
1
1
  import { OPENALEX_API_KEY_HINT, openAlexRequestFailure, searchExactOpenAlex } from "./science-database-openalex-exact.js";
2
+ import { createRequestPacer } from "./request-pacer.js";
2
3
 
3
4
  export type OpenAlexScienceDatabaseSource = "openalex";
4
5
 
@@ -96,9 +97,18 @@ function scrubOpenAlexText(text: string, url: URL): string {
96
97
  return scrubbed;
97
98
  }
98
99
 
99
- async function fetchJson(url: URL): Promise<{ credentialStatus: string; endpoint: string; payload: unknown; usingApiKey: boolean }> {
100
- const auth = addAuth(url);
101
- const endpoint = scrubOpenAlexEndpoint(url);
100
+ type OpenAlexResponse =
101
+ | { ok: true; payload: unknown }
102
+ | { ok: false; status: number; statusText: string; retryAfterMs?: number; body: string };
103
+
104
+ // Semantic search allows one request per second (429 with Retry-After: 1), so
105
+ // semantic requests are paced. A 429 that asks for a short wait, such as one
106
+ // caused by a subagent in another process, is retried once.
107
+ const semanticPacer = createRequestPacer();
108
+ const SEMANTIC_MIN_GAP_MS = 1100;
109
+ const MAX_RETRY_WAIT_MS = 5000;
110
+
111
+ async function requestOnce(url: URL): Promise<OpenAlexResponse> {
102
112
  const controller = new AbortController();
103
113
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
104
114
  try {
@@ -109,16 +119,39 @@ async function fetchJson(url: URL): Promise<{ credentialStatus: string; endpoint
109
119
  },
110
120
  signal: controller.signal,
111
121
  });
112
- if (!response.ok) {
113
- const snippet = scrubOpenAlexText((await response.text()).slice(0, 4096), url).slice(0, 240);
114
- throw openAlexRequestFailure(response.status, response.statusText, snippet, auth.usingApiKey);
115
- }
116
- return { ...auth, endpoint, payload: await response.json() };
122
+ if (response.ok) return { ok: true, payload: await response.json() };
123
+ const retryAfter = response.headers.get("retry-after");
124
+ const retryAfterSeconds = retryAfter === null ? Number.NaN : Number(retryAfter);
125
+ return {
126
+ ok: false,
127
+ status: response.status,
128
+ statusText: response.statusText,
129
+ retryAfterMs: Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0 ? retryAfterSeconds * 1000 : undefined,
130
+ body: (await response.text()).slice(0, 4096),
131
+ };
117
132
  } finally {
118
133
  clearTimeout(timeout);
119
134
  }
120
135
  }
121
136
 
137
+ async function fetchJson(url: URL): Promise<{ credentialStatus: string; endpoint: string; payload: unknown; usingApiKey: boolean }> {
138
+ const auth = addAuth(url);
139
+ const endpoint = scrubOpenAlexEndpoint(url);
140
+ const request = () =>
141
+ url.searchParams.has("search.semantic") ? semanticPacer(SEMANTIC_MIN_GAP_MS, () => requestOnce(url)) : requestOnce(url);
142
+ let response = await request();
143
+ if (!response.ok && response.status === 429 && response.retryAfterMs !== undefined && response.retryAfterMs <= MAX_RETRY_WAIT_MS) {
144
+ const waitMs = response.retryAfterMs + Math.random() * 500;
145
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
146
+ response = await request();
147
+ }
148
+ if (!response.ok) {
149
+ const snippet = scrubOpenAlexText(response.body, url).slice(0, 240);
150
+ throw openAlexRequestFailure(response.status, response.statusText, snippet, auth.usingApiKey);
151
+ }
152
+ return { ...auth, endpoint, payload: response.payload };
153
+ }
154
+
122
155
  function shortOpenAlexId(value: unknown): string | undefined {
123
156
  const raw = stringValue(value);
124
157
  if (!raw) return undefined;
@@ -5,6 +5,7 @@ import { searchOpenAlex } from "./science-database-openalex.js";
5
5
  import { searchPreprints } from "./science-database-preprints.js";
6
6
  import { searchPubMed } from "./science-database-pubmed.js";
7
7
  import { isEuropePmcFullTextQuery, searchEuropePmcFullText } from "./science-database-europepmc-fulltext.js";
8
+ import { createRequestPacer } from "./request-pacer.js";
8
9
 
9
10
  type ScienceDatabaseSource = "arxiv" | "biorxiv" | "crossref" | "europepmc" | "medrxiv" | "openalex" | "pubmed" | "semanticscholar";
10
11
 
@@ -141,21 +142,12 @@ function crossrefAuthors(value: unknown): string[] {
141
142
  // (x-concurrency-limit: 1); the polite pool, used when a mailto is sent, allows
142
143
  // three per second. Parallel DOI checks got 429 on most calls, so run Crossref
143
144
  // requests one after another at the pool's pace.
144
- let crossrefQueue: Promise<unknown> = Promise.resolve();
145
- let crossrefLastStartedAt = Number.NEGATIVE_INFINITY;
145
+ const crossrefPacer = createRequestPacer();
146
146
 
147
147
  export function withCrossrefPacing<T>(polite: boolean, run: () => Promise<T>): Promise<T> {
148
148
  // A little over the pool's limit: network jitter can land two requests paced
149
149
  // exactly one second apart inside the same window.
150
- const minGapMs = polite ? 400 : 1200;
151
- const next = crossrefQueue.then(async () => {
152
- const wait = crossrefLastStartedAt + minGapMs - Date.now();
153
- if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
154
- crossrefLastStartedAt = Date.now();
155
- return run();
156
- });
157
- crossrefQueue = next.catch(() => {});
158
- return next;
150
+ return crossrefPacer(polite ? 400 : 1200, run);
159
151
  }
160
152
 
161
153
  async function searchCrossref(params: ScienceDatabaseSearchParams): Promise<Record<string, unknown>> {
@@ -56,7 +56,11 @@ function createPostHogClient(config: ResearchTelemetryConfig): ResearchTelemetry
56
56
  const fetchOnce: NonNullable<PostHogOptions["fetch"]> = async (url, options) => {
57
57
  if (!transportFailed) {
58
58
  try {
59
- const response = await fetch(url, options as RequestInit);
59
+ // A short budget per send, so a slow network ends quietly instead of
60
+ // hitting posthog-node's 10s deadline, which logs to stderr.
61
+ const budget = AbortSignal.timeout(1500);
62
+ const signal = options.signal ? AbortSignal.any([options.signal as AbortSignal, budget]) : budget;
63
+ const response = await fetch(url, { ...(options as RequestInit), signal });
60
64
  if (response.status >= 200 && response.status < 400) return response;
61
65
  } catch {}
62
66
  transportFailed = true;
@@ -115,7 +115,7 @@ export const cliCommandSections = [
115
115
  { usage: "feynman model list", description: "List available models in Pi auth storage." },
116
116
  { usage: "feynman model login [id]", description: "Authenticate a model provider with OAuth or API-key setup." },
117
117
  { usage: "feynman model logout [id]", description: "Clear stored auth for a model provider." },
118
- { usage: "feynman model set <provider/model>", description: "Set the default approved research model (also accepts provider:model)." },
118
+ { usage: "feynman model set [provider/model]", description: "Set the default approved research model (also accepts provider:model); without one, pick from a list." },
119
119
  { usage: "feynman model tier [value]", description: "View or set the request service tier override." },
120
120
  ],
121
121
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -79,18 +79,17 @@
79
79
  "dependencies": {
80
80
  "@clack/prompts": "^1.8.1",
81
81
  "@companion-ai/alpha-hub": "0.1.6",
82
- "@earendil-works/pi-ai": "*",
83
- "@earendil-works/pi-coding-agent": "*",
84
82
  "fast-xml-parser": "5.11.1",
85
83
  "pi-btw": "0.6.1",
86
84
  "pi-docparser": "4.0.0",
87
- "pi-subagents": "0.71.0",
88
- "pi-web-access": "0.31.0",
89
- "posthog-node": "^5.53.0"
85
+ "pi-subagents": "0.73.1",
86
+ "pi-web-access": "0.33.0",
87
+ "posthog-node": "^5.53.0",
88
+ "undici": "^8.11.2"
90
89
  },
91
90
  "peerDependencies": {
92
- "@earendil-works/pi-ai": "*",
93
- "@earendil-works/pi-coding-agent": "*",
91
+ "@earendil-works/pi-ai": "0.99.1",
92
+ "@earendil-works/pi-coding-agent": "0.99.1",
94
93
  "@earendil-works/pi-tui": "*",
95
94
  "typebox": "*"
96
95
  },
@@ -115,5 +114,8 @@
115
114
  "author": "Advait Paliwal",
116
115
  "publishConfig": {
117
116
  "access": "public"
117
+ },
118
+ "overrides": {
119
+ "undici": "$undici"
118
120
  }
119
121
  }
@@ -325,39 +325,51 @@ Workarounds:
325
325
  throw "Downloaded archive did not contain the expected launcher: $candidate"
326
326
  }
327
327
  }
328
- $candidateVersionOutput = @(& $candidateCmd --version 2>&1)
329
- if ($LASTEXITCODE -ne 0) {
330
- throw "Downloaded launcher failed --version: $candidateCmd"
331
- }
332
- $candidateVersion = ($candidateVersionOutput | Select-Object -Last 1).ToString().Trim()
333
- if ($candidateVersion -ne $resolvedVersion) {
334
- throw "Downloaded bundle version mismatch: expected=$resolvedVersion actual=$candidateVersion"
335
- }
336
- $candidateHelp = @(& $candidateCmd --help 2>&1)
337
- if ($LASTEXITCODE -ne 0 -or $candidateHelp.Count -eq 0) {
338
- throw "Downloaded launcher failed --help: $candidateCmd"
339
- }
340
-
341
- # The public one-line installer can run under Windows PowerShell's default
342
- # Restricted policy because it executes an in-memory scriptblock. Validate
343
- # the packaged PowerShell launcher in a child host with an explicit process-
344
- # scoped bypass rather than invoking the downloaded .ps1 file directly.
345
- $powerShellExecutable = (Get-Process -Id $PID).Path
346
- $candidateVersionOutput = @(
347
- & $powerShellExecutable -NoProfile -ExecutionPolicy Bypass -File $candidatePs1 --version 2>&1
348
- )
349
- if ($LASTEXITCODE -ne 0) {
350
- throw "Downloaded launcher failed --version: $candidatePs1"
351
- }
352
- $candidateVersion = ($candidateVersionOutput | Select-Object -Last 1).ToString().Trim()
353
- if ($candidateVersion -ne $resolvedVersion) {
354
- throw "Downloaded bundle version mismatch: expected=$resolvedVersion actual=$candidateVersion"
355
- }
356
- $candidateHelp = @(
357
- & $powerShellExecutable -NoProfile -ExecutionPolicy Bypass -File $candidatePs1 --help 2>&1
358
- )
359
- if ($LASTEXITCODE -ne 0 -or $candidateHelp.Count -eq 0) {
360
- throw "Downloaded launcher failed --help: $candidatePs1"
328
+ # Verify with telemetry off: the check is not usage, and under
329
+ # $ErrorActionPreference = "Stop" any stderr line from a native command
330
+ # aborts the install (#309). Judge the launcher by its exit code instead.
331
+ $previousTelemetry = $env:FEYNMAN_TELEMETRY
332
+ $previousErrorAction = $ErrorActionPreference
333
+ try {
334
+ $env:FEYNMAN_TELEMETRY = "off"
335
+ $ErrorActionPreference = "Continue"
336
+ $candidateVersionOutput = @(& $candidateCmd --version 2>&1)
337
+ if ($LASTEXITCODE -ne 0) {
338
+ throw "Downloaded launcher failed --version: $candidateCmd"
339
+ }
340
+ $candidateVersion = ($candidateVersionOutput | Select-Object -Last 1).ToString().Trim()
341
+ if ($candidateVersion -ne $resolvedVersion) {
342
+ throw "Downloaded bundle version mismatch: expected=$resolvedVersion actual=$candidateVersion"
343
+ }
344
+ $candidateHelp = @(& $candidateCmd --help 2>&1)
345
+ if ($LASTEXITCODE -ne 0 -or $candidateHelp.Count -eq 0) {
346
+ throw "Downloaded launcher failed --help: $candidateCmd"
347
+ }
348
+
349
+ # The public one-line installer can run under Windows PowerShell's default
350
+ # Restricted policy because it executes an in-memory scriptblock. Validate
351
+ # the packaged PowerShell launcher in a child host with an explicit process-
352
+ # scoped bypass rather than invoking the downloaded .ps1 file directly.
353
+ $powerShellExecutable = (Get-Process -Id $PID).Path
354
+ $candidateVersionOutput = @(
355
+ & $powerShellExecutable -NoProfile -ExecutionPolicy Bypass -File $candidatePs1 --version 2>&1
356
+ )
357
+ if ($LASTEXITCODE -ne 0) {
358
+ throw "Downloaded launcher failed --version: $candidatePs1"
359
+ }
360
+ $candidateVersion = ($candidateVersionOutput | Select-Object -Last 1).ToString().Trim()
361
+ if ($candidateVersion -ne $resolvedVersion) {
362
+ throw "Downloaded bundle version mismatch: expected=$resolvedVersion actual=$candidateVersion"
363
+ }
364
+ $candidateHelp = @(
365
+ & $powerShellExecutable -NoProfile -ExecutionPolicy Bypass -File $candidatePs1 --help 2>&1
366
+ )
367
+ if ($LASTEXITCODE -ne 0 -or $candidateHelp.Count -eq 0) {
368
+ throw "Downloaded launcher failed --help: $candidatePs1"
369
+ }
370
+ } finally {
371
+ $ErrorActionPreference = $previousErrorAction
372
+ $env:FEYNMAN_TELEMETRY = $previousTelemetry
361
373
  }
362
374
 
363
375
  New-Item -ItemType Directory -Path $installRoot -Force | Out-Null
@@ -320,12 +320,12 @@ if [ ! -x "$candidate_bundle/feynman" ]; then
320
320
  echo "Downloaded archive did not contain the expected executable: ${bundle_name}/feynman" >&2
321
321
  exit 1
322
322
  fi
323
- candidate_version="$("$candidate_bundle/feynman" --version | tail -n 1)"
323
+ candidate_version="$(FEYNMAN_TELEMETRY=off "$candidate_bundle/feynman" --version | tail -n 1)"
324
324
  if [ "$candidate_version" != "$resolved_version" ]; then
325
325
  echo "Downloaded bundle version mismatch: expected ${resolved_version}, found ${candidate_version}." >&2
326
326
  exit 1
327
327
  fi
328
- "$candidate_bundle/feynman" --help >/dev/null
328
+ FEYNMAN_TELEMETRY=off "$candidate_bundle/feynman" --help >/dev/null
329
329
 
330
330
  mkdir -p "$INSTALL_APP_DIR"
331
331
  mkdir -p "$INSTALL_BIN_DIR"