@companion-ai/feynman 0.5.4 → 0.5.6

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.
@@ -2,9 +2,5 @@
2
2
  "theme": "feynman",
3
3
  "quietStartup": true,
4
4
  "collapseChangelog": true,
5
- "editorPaddingX": 1,
6
- "retry": {
7
- "maxRetries": 6,
8
- "baseDelayMs": 5000
9
- }
5
+ "editorPaddingX": 1
10
6
  }
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 24.20.0
1
+ 24.21.0
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  <p align="center">
8
8
  <a href="https://feynman.is/docs"><img alt="Docs" src="https://img.shields.io/badge/docs-feynman.is-0d9668?style=flat-square" /></a>
9
9
  <a href="https://github.com/Companion-Inc/feynman/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/Companion-Inc/feynman?style=flat-square" /></a>
10
+ <a href="https://deepwiki.com/Companion-Inc/feynman"><img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" /></a>
10
11
  </p>
11
12
 
12
13
  ---
@@ -27,7 +28,7 @@ irm https://feynman.is/install.ps1 | iex
27
28
 
28
29
  The one-line installer fetches the latest tagged release as a standalone bundle with its own Node.js runtime and verifies its SHA-256 before installing. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.5.3`. Rerun the installer to upgrade; `feynman update` only updates optional Pi packages you installed.
29
30
 
30
- **npm alternative** (uses your local Node.js `>=22.22.0 <26`):
31
+ **npm alternative** (uses your local Node.js `>=22.22.0`):
31
32
 
32
33
  ```bash
33
34
  npm install -g @companion-ai/feynman
@@ -149,7 +150,7 @@ Feynman runs on stock [Pi](https://github.com/earendil-works/pi) (`@earendil-wor
149
150
 
150
151
  ### Telemetry
151
152
 
152
- Feynman sends anonymous usage telemetry to PostHog by default and prints a one-time notice on first run. It sends commands, workflow names and outcomes, tool names, model and provider names, token counts, latency, and error flags under a random install ID. It never sends prompts, model output, paper content, file paths, or tool arguments. Set `FEYNMAN_TELEMETRY=off` (or `DO_NOT_TRACK=1`) to disable it; `feynman status` shows the current setting. The full event list is in the [configuration docs](https://feynman.is/docs/getting-started/configuration#telemetry).
153
+ Feynman sends anonymous usage telemetry to PostHog by default and prints a one-time notice on first run. It sends commands, workflow names and outcomes, tool names, model and provider names, token counts, latency, and error messages with stack traces (home folder shown as `~`) under a random install ID. It never sends prompts, model output, paper content, or tool arguments. Set `FEYNMAN_TELEMETRY=off` (or `DO_NOT_TRACK=1`) to disable it; `feynman status` shows the current setting. The full event list is in the [configuration docs](https://feynman.is/docs/getting-started/configuration#telemetry).
153
154
 
154
155
  ---
155
156
 
package/RELEASES.md CHANGED
@@ -6,6 +6,36 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.6 - 2026-09-26
10
+
11
+ ### Failures report what went wrong
12
+
13
+ - Usage telemetry now carries readable error text instead of a hash: the error message and stack trace (also sent to PostHog error tracking), the last lines of Pi's error output when a run exits with an error, a failed tool's error text, and the provider's error message on a failed model call. Your home folder is shown as `~`. Paths inside your projects can appear in this text. Prompts, model output, paper content, and tool arguments are still never sent. Opt out with `FEYNMAN_TELEMETRY=off` as before.
14
+ - CI runs and the test suite no longer report telemetry, so usage numbers count real installs only.
15
+
16
+ ### Fixes
17
+
18
+ - A dead network connection now fails in about 15 seconds with `Connection error.` instead of retrying silently for over five minutes. Feynman had set a 6-retry, 5-second backoff that also applied to connection errors; it now uses Pi's default, and the old value is removed from existing settings. Rate limits are still retried with the provider's `Retry-After`.
19
+ - `feynman setup` and `feynman model login` in a window Node cannot prompt in (Git Bash's default window, a script, or an AI coding agent) now explain how to proceed: `winpty`, PowerShell, or setting the provider's API key variable and running `feynman model set`.
20
+
21
+ ## v0.5.5 - 2026-09-24
22
+
23
+ ### Node.js 26
24
+
25
+ - Feynman now runs on Node.js 26, the current release. The upper version limit is gone: Feynman needs Node.js 22.22.0 or newer, like Pi itself. Tests run on Node 22, 24, and 26.
26
+ - The standalone installers bundle Node.js 24.21.0, the latest LTS release.
27
+
28
+ ### alphaXiv
29
+
30
+ - When an alphaXiv call fails, the tool error now points the agent to `feynman_science_database_search` (arXiv or Semantic Scholar) or `fetch_content`, so a research run continues on another source instead of retrying a broken tool. A "not logged in" error names `feynman alpha login`.
31
+ - The alphaXiv docs explain signing in from a machine without a browser (SSH, Docker, WSL): open the printed URL anywhere and paste the final `127.0.0.1:9876/callback` URL back into the terminal.
32
+
33
+ ### Other
34
+
35
+ - The setup docs explain the most common reason local models write no files: a small context window. They show how to raise Ollama's context and set `contextWindow` and `maxTokens` in `models.json`.
36
+ - feynman.is/privacy and feynman.is/telemetry go to the telemetry section, which lists what is collected and how to opt out.
37
+ - Updated dependencies: pi-btw 0.6.1, posthog-node 5.53, @clack/prompts 1.8.1, and the website toolchain (Astro 7.3, @astrojs/react 7). CI uses actions/checkout and actions/setup-node v7 and wrangler 4.139.
38
+
9
39
  ## v0.5.4 - 2026-09-24
10
40
 
11
41
  ### Use Feynman from ACP editors
package/bin/feynman.js CHANGED
@@ -3,7 +3,6 @@ import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
5
5
  const MIN_NODE_VERSION = "22.22.0";
6
- const MAX_NODE_MAJOR = 25;
7
6
  const PREFERRED_NODE_MAJOR = 24;
8
7
 
9
8
  function parseNodeVersion(version) {
@@ -21,15 +20,12 @@ function compareNodeVersions(left, right) {
21
20
  return left.patch - right.patch;
22
21
  }
23
22
 
24
- const parsedNodeVersion = parseNodeVersion(process.versions.node);
25
- if (compareNodeVersions(parsedNodeVersion, parseNodeVersion(MIN_NODE_VERSION)) < 0 || parsedNodeVersion.major > MAX_NODE_MAJOR) {
23
+ if (compareNodeVersions(parseNodeVersion(process.versions.node), parseNodeVersion(MIN_NODE_VERSION)) < 0) {
26
24
  const isWindows = process.platform === "win32";
27
- console.error(`feynman supports Node.js ${MIN_NODE_VERSION} through ${MAX_NODE_MAJOR}.x (detected ${process.versions.node}).`);
28
- console.error(parsedNodeVersion.major > MAX_NODE_MAJOR
29
- ? "This newer Node release is not supported yet."
30
- : isWindows
31
- ? "Install a supported Node.js release from https://nodejs.org, or use the standalone installer:"
32
- : `Switch to a supported Node release with \`nvm install ${PREFERRED_NODE_MAJOR} && nvm use ${PREFERRED_NODE_MAJOR}\`, or use the standalone installer:`);
25
+ console.error(`feynman requires Node.js ${MIN_NODE_VERSION} or newer (detected ${process.versions.node}).`);
26
+ console.error(isWindows
27
+ ? "Install a supported Node.js release from https://nodejs.org, or use the standalone installer:"
28
+ : `Switch to a supported Node release with \`nvm install ${PREFERRED_NODE_MAJOR} && nvm use ${PREFERRED_NODE_MAJOR}\`, or use the standalone installer:`);
33
29
  console.error(isWindows
34
30
  ? "irm https://feynman.is/install.ps1 | iex"
35
31
  : "curl -fsSL https://feynman.is/install | bash");
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { getValidToken as getValidAlphaToken } from "@companion-ai/alpha-hub/lib
17
17
  import { verifyAlphaAuthStatus } from "./alpha-auth-status.js";
18
18
  import { ensureFeynmanAgentDir } from "./bootstrap/home.js";
19
19
  import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
20
- import { launchPiChat, runPi } from "./pi/launch.js";
20
+ import { getLastPiStderr, launchPiChat, runPi } from "./pi/launch.js";
21
21
  import { installPiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
22
22
  import { canonicalizeModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
23
23
  import { BUNDLED_PI_PACKAGES, buildPiEnv } from "./pi/runtime.js";
@@ -29,7 +29,7 @@ import { fetchLatestFeynmanVersion, getFeynmanUpgradeLines, isNewerVersion } fro
29
29
  import { runDoctor, runStatus } from "./setup/doctor.js";
30
30
  import { setupPreviewDependencies } from "./setup/preview.js";
31
31
  import { runSetup } from "./setup/setup.js";
32
- import { captureTelemetryEvent, getCliTelemetryMetadata, initializePostHogTelemetry, shutdownPostHogTelemetry, telemetryErrorProperties, telemetryFirstRunNotice, } from "./telemetry/posthog.js";
32
+ import { captureTelemetryEvent, captureTelemetryException, getCliTelemetryMetadata, initializePostHogTelemetry, shutdownPostHogTelemetry, telemetryErrorProperties, telemetryFirstRunNotice, } from "./telemetry/posthog.js";
33
33
  import { ASH, printAsciiHeader, printInfo, printPanel, printSection, RESET, SAGE } from "./ui/terminal.js";
34
34
  import { createModelRuntime } from "./model/registry.js";
35
35
  import { cliCommandSections, formatCliWorkflowUsage, legacyFlags, readPromptSpecs, topLevelCommandNames, } from "../metadata/commands.mjs";
@@ -395,6 +395,7 @@ export async function main() {
395
395
  ...commandTelemetry,
396
396
  duration_ms: durationMs,
397
397
  exit_code: exitCode,
398
+ ...(exitCode === 0 ? {} : { pi_stderr: getLastPiStderr() }),
398
399
  };
399
400
  captureTelemetryEvent(exitCode === 0 ? "feynman_command_completed" : "feynman_command_failed", completeProperties);
400
401
  }
@@ -406,6 +407,7 @@ export async function main() {
406
407
  ...telemetryErrorProperties(error),
407
408
  };
408
409
  captureTelemetryEvent("feynman_command_failed", failureProperties);
410
+ await captureTelemetryException(error, commandTelemetry).catch(() => { });
409
411
  throw error;
410
412
  }
411
413
  finally {
package/dist/pi/launch.js CHANGED
@@ -7,6 +7,12 @@ export function exitCodeFromSignal(signal) {
7
7
  const signalNumber = constants.signals[signal];
8
8
  return typeof signalNumber === "number" ? 128 + signalNumber : 1;
9
9
  }
10
+ // The end of Pi's stderr from the last run, reported with a failed command so
11
+ // crashes inside Pi are debuggable.
12
+ let lastPiStderr = "";
13
+ export function getLastPiStderr() {
14
+ return lastPiStderr;
15
+ }
10
16
  export async function runPi(options, args, env, stdin = "inherit") {
11
17
  const piCliPath = resolvePiCliPath(options.appRoot);
12
18
  if (!piCliPath) {
@@ -14,9 +20,14 @@ export async function runPi(options, args, env, stdin = "inherit") {
14
20
  }
15
21
  const child = spawn(process.execPath, [piCliPath, ...args], {
16
22
  cwd: options.workingDir,
17
- stdio: [stdin, "inherit", "inherit"],
23
+ stdio: [stdin, "inherit", "pipe"],
18
24
  env,
19
25
  });
26
+ lastPiStderr = "";
27
+ child.stderr?.on("data", (chunk) => {
28
+ process.stderr.write(chunk);
29
+ lastPiStderr = (lastPiStderr + chunk.toString("utf8")).slice(-8000);
30
+ });
20
31
  return await new Promise((resolvePromise, reject) => {
21
32
  child.on("error", reject);
22
33
  child.on("exit", (code, signal) => {
@@ -163,6 +163,10 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
163
163
  }
164
164
  settings.packages = reconcileFeynmanPackages(settings.packages, appRoot);
165
165
  removeLegacyResearcherExtension(settings);
166
+ // 0.5.0-0.5.5 seeded a 6-retry, 5s backoff that left a dead connection
167
+ // retrying silently for over five minutes; drop that exact value for Pi's default.
168
+ if (isRecord(settings.retry) && JSON.stringify(settings.retry) === '{"maxRetries":6,"baseDelayMs":5000}')
169
+ delete settings.retry;
166
170
  // A ~/.agents/<name>.md would otherwise silently replace Feynman's agents.
167
171
  if (settings.subagents === undefined)
168
172
  settings.subagents = {};
@@ -5,9 +5,18 @@ export class SetupCancelledError extends Error {
5
5
  this.name = "SetupCancelledError";
6
6
  }
7
7
  }
8
+ export function nonInteractiveTerminalMessage(env = process.env, platform = process.platform) {
9
+ return [
10
+ "feynman setup requires an interactive terminal.",
11
+ platform === "win32" && env.MSYSTEM
12
+ ? "Git Bash's default window is not one Node can prompt in: run `winpty feynman setup`, or use PowerShell or Windows Terminal."
13
+ : "Run it yourself in a terminal window, not through a script or an AI coding agent.",
14
+ "To skip the prompts, set your provider's API key variable (for example OPENAI_API_KEY or ANTHROPIC_API_KEY) and run `feynman model set <provider/model>`.",
15
+ ].join("\n");
16
+ }
8
17
  function ensureInteractiveTerminal() {
9
18
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
10
- throw new Error("feynman setup requires an interactive terminal.");
19
+ throw new Error(nonInteractiveTerminalMessage());
11
20
  }
12
21
  }
13
22
  function guardCancelled(value) {
@@ -1,5 +1,4 @@
1
1
  export const MIN_NODE_VERSION = "22.22.0";
2
- export const MAX_NODE_MAJOR = 25;
3
2
  export const PREFERRED_NODE_MAJOR = 24;
4
3
  function parseNodeVersion(version) {
5
4
  const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split(".");
@@ -17,20 +16,15 @@ function compareNodeVersions(left, right) {
17
16
  return left.patch - right.patch;
18
17
  }
19
18
  export function isSupportedNodeVersion(version = process.versions.node) {
20
- const parsed = parseNodeVersion(version);
21
- return compareNodeVersions(parsed, parseNodeVersion(MIN_NODE_VERSION)) >= 0 && parsed.major <= MAX_NODE_MAJOR;
19
+ return compareNodeVersions(parseNodeVersion(version), parseNodeVersion(MIN_NODE_VERSION)) >= 0;
22
20
  }
23
21
  export function getUnsupportedNodeVersionLines(version = process.versions.node) {
24
22
  const isWindows = process.platform === "win32";
25
- const parsed = parseNodeVersion(version);
26
- const rangeText = `Node.js ${MIN_NODE_VERSION} through ${MAX_NODE_MAJOR}.x`;
27
23
  return [
28
- `feynman supports ${rangeText} (detected ${version}).`,
29
- parsed.major > MAX_NODE_MAJOR
30
- ? "This newer Node release is not supported yet."
31
- : isWindows
32
- ? "Install a supported Node.js release from https://nodejs.org, or use the standalone installer:"
33
- : `Switch to a supported Node release with \`nvm install ${PREFERRED_NODE_MAJOR} && nvm use ${PREFERRED_NODE_MAJOR}\`, or use the standalone installer:`,
24
+ `feynman requires Node.js ${MIN_NODE_VERSION} or newer (detected ${version}).`,
25
+ isWindows
26
+ ? "Install a supported Node.js release from https://nodejs.org, or use the standalone installer:"
27
+ : `Switch to a supported Node release with \`nvm install ${PREFERRED_NODE_MAJOR} && nvm use ${PREFERRED_NODE_MAJOR}\`, or use the standalone installer:`,
34
28
  isWindows
35
29
  ? "irm https://feynman.is/install.ps1 | iex"
36
30
  : "curl -fsSL https://feynman.is/install | bash",
@@ -1,5 +1,6 @@
1
- import { randomUUID, createHash } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { dirname, resolve } from "node:path";
4
5
  import { PostHog } from "posthog-node";
5
6
  import { getFeynmanHome, getFeynmanStateDir } from "../config/paths.js";
@@ -11,7 +12,7 @@ const TELEMETRY_DISABLED_VALUES = new Set(["0", "false", "no", "off", "disabled"
11
12
  const TELEMETRY_KEY_PATTERN = /^[A-Za-z0-9_$./-]+$/;
12
13
  export const TELEMETRY_NOTICE = [
13
14
  "Attention: Feynman collects anonymous usage telemetry: commands, workflows, tool names, models, token counts, and errors.",
14
- "It never sends prompts, paper content, file paths, or tool arguments.",
15
+ "Errors include their message and stack trace, with your home folder shown as ~. It never sends prompts, model output, paper content, or tool arguments.",
15
16
  "To opt out, set FEYNMAN_TELEMETRY=off. Learn more: https://www.feynman.is/docs/getting-started/configuration#telemetry",
16
17
  ].join("\n");
17
18
  let posthogClient;
@@ -145,13 +146,26 @@ function normalizeTelemetryKey(key) {
145
146
  return undefined;
146
147
  return normalized.slice(0, 80);
147
148
  }
148
- function normalizeTelemetryValue(value) {
149
+ // Error text is sent as-is (home folder shown as ~) so failures can be debugged.
150
+ const RAW_TEXT_KEYS = new Set(["error_message", "pi_stderr"]);
151
+ const RAW_TEXT_LIMIT = 4000;
152
+ export function redactTelemetryText(text, home = homedir()) {
153
+ let redacted = text;
154
+ if (home) {
155
+ for (const variant of new Set([home, home.replace(/\\/g, "/")]))
156
+ redacted = redacted.split(variant).join("~");
157
+ }
158
+ return redacted.length > RAW_TEXT_LIMIT ? `...${redacted.slice(-RAW_TEXT_LIMIT)}` : redacted;
159
+ }
160
+ function normalizeTelemetryValue(value, raw = false) {
149
161
  if (value === undefined)
150
162
  return undefined;
151
163
  if (value === null || typeof value === "boolean")
152
164
  return value;
153
165
  if (typeof value === "number")
154
166
  return Number.isFinite(value) ? value : undefined;
167
+ if (raw)
168
+ return value.trim() ? redactTelemetryText(value.trim()) : undefined;
155
169
  const trimmed = value.replace(/\s+/g, " ").trim();
156
170
  if (!trimmed)
157
171
  return undefined;
@@ -161,7 +175,7 @@ export function normalizeTelemetryProperties(properties = {}) {
161
175
  const normalized = {};
162
176
  for (const [key, value] of Object.entries(properties)) {
163
177
  const normalizedKey = normalizeTelemetryKey(key);
164
- const normalizedValue = normalizeTelemetryValue(value);
178
+ const normalizedValue = normalizeTelemetryValue(value, RAW_TEXT_KEYS.has(normalizedKey ?? ""));
165
179
  if (!normalizedKey || normalizedValue === undefined)
166
180
  continue;
167
181
  normalized[normalizedKey] = normalizedValue;
@@ -179,11 +193,6 @@ function baseTelemetryProperties(config) {
179
193
  $process_person_profile: false,
180
194
  });
181
195
  }
182
- export function stableTelemetryHash(value) {
183
- if (!value)
184
- return undefined;
185
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
186
- }
187
196
  function telemetryErrorName(error) {
188
197
  if (!(error instanceof Error))
189
198
  return typeof error;
@@ -193,7 +202,7 @@ export function telemetryErrorProperties(error) {
193
202
  const message = error instanceof Error ? error.message : String(error);
194
203
  return {
195
204
  error_name: telemetryErrorName(error),
196
- error_message_hash: stableTelemetryHash(message),
205
+ error_message: message,
197
206
  };
198
207
  }
199
208
  export function initializePostHogTelemetry(options = {}) {
@@ -234,6 +243,20 @@ export function captureTelemetryEvent(event, properties = {}) {
234
243
  },
235
244
  });
236
245
  }
246
+ /** Send an error with its stack trace to PostHog error tracking. */
247
+ export async function captureTelemetryException(error, properties = {}) {
248
+ if (!activeConfig || !posthogClient)
249
+ return;
250
+ const source = error instanceof Error ? error : new Error(String(error));
251
+ const redacted = new Error(redactTelemetryText(source.message));
252
+ redacted.name = source.name;
253
+ redacted.stack = source.stack ? redactTelemetryText(source.stack) : undefined;
254
+ // The immediate variant is awaited, so the event is sent before the CLI exits.
255
+ await posthogClient.captureExceptionImmediate(redacted, activeConfig.distinctId, {
256
+ ...baseTelemetryProperties(activeConfig),
257
+ ...normalizeTelemetryProperties(properties),
258
+ });
259
+ }
237
260
  export async function captureTelemetryEventImmediate(event, properties = {}) {
238
261
  if (!activeConfig || !posthogClient)
239
262
  return;
@@ -18,6 +18,19 @@ function formatText(value: unknown): string {
18
18
  return JSON.stringify(value, null, 2);
19
19
  }
20
20
 
21
+ // alphaXiv's API has changed without notice before; when a call fails, point
22
+ // the model at the other paper sources instead of leaving it on a broken tool.
23
+ async function withPaperFallback<T>(run: () => Promise<T>): Promise<T> {
24
+ try {
25
+ return await run();
26
+ } catch (error) {
27
+ const message = (error instanceof Error ? error.message : String(error)).replace("`alpha login`", "`feynman alpha login`");
28
+ throw new Error(
29
+ `${message}\nalphaXiv did not answer this call. Use feynman_science_database_search (source "arxiv" or "semanticscholar") or fetch_content on https://arxiv.org/abs/<id> instead.`,
30
+ );
31
+ }
32
+ }
33
+
21
34
  // Pi converts Type.Array inputs before validating them, which would turn null into ["null"].
22
35
  // Preserve the JSON Schema array contract without the Type.Array conversion marker.
23
36
  const paperSectionsSchema = Type.Unsafe<string[]>({
@@ -47,7 +60,7 @@ export function registerAlphaTools(pi: ExtensionAPI): void {
47
60
  ),
48
61
  }),
49
62
  async execute(_toolCallId, params) {
50
- const result = await searchPapers(params.query, params.mode?.trim() || "semantic");
63
+ const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic"));
51
64
  return { content: [{ type: "text", text: formatText(result) }], details: result };
52
65
  },
53
66
  });
@@ -69,7 +82,7 @@ export function registerAlphaTools(pi: ExtensionAPI): void {
69
82
  sections: Type.Optional(paperSectionsSchema),
70
83
  }),
71
84
  async execute(_toolCallId, params) {
72
- const result = await getPaper(params.paper, { fullText: params.fullText });
85
+ const result = await withPaperFallback(() => getPaper(params.paper, { fullText: params.fullText }));
73
86
  const extracted = extractPaperSections(result.content, params.section, params.sections);
74
87
  const filteredResult = extracted.requested.length
75
88
  ? {
@@ -92,7 +105,7 @@ export function registerAlphaTools(pi: ExtensionAPI): void {
92
105
  question: Type.String({ description: "Question about the paper." }),
93
106
  }),
94
107
  async execute(_toolCallId, params) {
95
- const result = await askPaper(params.paper, params.question);
108
+ const result = await withPaperFallback(() => askPaper(params.paper, params.question));
96
109
  return { content: [{ type: "text", text: formatText(result) }], details: result };
97
110
  },
98
111
  });
@@ -1,4 +1,5 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
3
4
 
4
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -75,6 +76,20 @@ function createPostHogClient(config: ResearchTelemetryConfig): ResearchTelemetry
75
76
  return client;
76
77
  }
77
78
 
79
+ // Error text is sent as-is so failures can be debugged; the home folder is shown as ~.
80
+ export function errorText(value: unknown, home = homedir()): string | undefined {
81
+ const text = typeof value === "string" ? value : undefined;
82
+ if (!text?.trim()) return undefined;
83
+ let redacted = text.trim();
84
+ if (home) for (const variant of new Set([home, home.replace(/\\/g, "/")])) redacted = redacted.split(variant).join("~");
85
+ return redacted.length > 4000 ? `...${redacted.slice(-4000)}` : redacted;
86
+ }
87
+
88
+ function toolErrorText(result: unknown): string | undefined {
89
+ const content = (result as { content?: Array<{ type?: string; text?: string }> } | undefined)?.content;
90
+ return errorText(Array.isArray(content) ? content.filter((part) => part?.type === "text").map((part) => part.text).join("\n") : undefined);
91
+ }
92
+
78
93
  export function workflowName(text: string, workflows: ReadonlySet<string>): string {
79
94
  const name = /^\/([A-Za-z0-9_-]+)(?:\s|$)/.exec(text.trimStart())?.[1];
80
95
  return name && workflows.has(name) ? name : "chat";
@@ -195,7 +210,12 @@ export function registerResearchTelemetry(
195
210
  });
196
211
 
197
212
  pi.on("tool_execution_end", (event) => {
198
- capture("feynman_tool_used", { tool: event.toolName, is_error: event.isError, subagent: !primary });
213
+ capture("feynman_tool_used", {
214
+ tool: event.toolName,
215
+ is_error: event.isError,
216
+ subagent: !primary,
217
+ ...(event.isError ? { error_message: toolErrorText(event.result) } : {}),
218
+ });
199
219
  if (!run) return;
200
220
  run.toolCalls += 1;
201
221
  if (event.toolName === "subagent") run.subagentCalls += 1;
@@ -226,6 +246,7 @@ export function registerResearchTelemetry(
226
246
  $ai_latency: requestStartedAt === undefined ? undefined : (now() - requestStartedAt) / 1000,
227
247
  $ai_http_status: httpStatus,
228
248
  $ai_is_error: message.stopReason === "error",
249
+ $ai_error: message.stopReason === "error" ? errorText(message.errorMessage) : undefined,
229
250
  $ai_stop_reason: message.stopReason,
230
251
  subagent: !primary,
231
252
  });
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=22.22.0 <26"
8
+ "node": ">=22.22.0"
9
9
  },
10
10
  "bin": {
11
11
  "feynman": "bin/feynman.js"
@@ -77,16 +77,16 @@
77
77
  }
78
78
  },
79
79
  "dependencies": {
80
- "@clack/prompts": "^1.7.0",
80
+ "@clack/prompts": "^1.8.1",
81
81
  "@companion-ai/alpha-hub": "0.1.6",
82
82
  "@earendil-works/pi-ai": "*",
83
83
  "@earendil-works/pi-coding-agent": "*",
84
84
  "fast-xml-parser": "5.11.1",
85
- "pi-btw": "0.6.0",
85
+ "pi-btw": "0.6.1",
86
86
  "pi-docparser": "4.0.0",
87
87
  "pi-subagents": "0.71.0",
88
88
  "pi-web-access": "0.31.0",
89
- "posthog-node": "^5.51.6"
89
+ "posthog-node": "^5.53.0"
90
90
  },
91
91
  "peerDependencies": {
92
92
  "@earendil-works/pi-ai": "*",
@@ -95,10 +95,10 @@
95
95
  "typebox": "*"
96
96
  },
97
97
  "devDependencies": {
98
- "@types/node": "^26.4.1",
99
- "tsx": "^4.23.13",
98
+ "@types/node": "^26.6.2",
99
+ "tsx": "^4.23.15",
100
100
  "typescript": "^7.0.2",
101
- "yaml": "2.9.0"
101
+ "yaml": "2.9.1"
102
102
  },
103
103
  "repository": {
104
104
  "type": "git",
@@ -1,5 +1,4 @@
1
1
  const MIN_NODE_VERSION = "22.22.0";
2
- const MAX_NODE_MAJOR = 25;
3
2
  const PREFERRED_NODE_MAJOR = 24;
4
3
 
5
4
  function parseNodeVersion(version) {
@@ -18,20 +17,16 @@ function compareNodeVersions(left, right) {
18
17
  }
19
18
 
20
19
  function isSupportedNodeVersion(version = process.versions.node) {
21
- const parsed = parseNodeVersion(version);
22
- return compareNodeVersions(parsed, parseNodeVersion(MIN_NODE_VERSION)) >= 0 && parsed.major <= MAX_NODE_MAJOR;
20
+ return compareNodeVersions(parseNodeVersion(version), parseNodeVersion(MIN_NODE_VERSION)) >= 0;
23
21
  }
24
22
 
25
23
  function getUnsupportedNodeVersionLines(version = process.versions.node) {
26
24
  const isWindows = process.platform === "win32";
27
- const parsed = parseNodeVersion(version);
28
25
  return [
29
- `feynman supports Node.js ${MIN_NODE_VERSION} through ${MAX_NODE_MAJOR}.x (detected ${version}).`,
30
- parsed.major > MAX_NODE_MAJOR
31
- ? "This newer Node release is not supported yet."
32
- : isWindows
33
- ? "Install a supported Node.js release from https://nodejs.org, or use the standalone installer:"
34
- : `Switch to a supported Node release with \`nvm install ${PREFERRED_NODE_MAJOR} && nvm use ${PREFERRED_NODE_MAJOR}\`, or use the standalone installer:`,
26
+ `feynman requires Node.js ${MIN_NODE_VERSION} or newer (detected ${version}).`,
27
+ isWindows
28
+ ? "Install a supported Node.js release from https://nodejs.org, or use the standalone installer:"
29
+ : `Switch to a supported Node release with \`nvm install ${PREFERRED_NODE_MAJOR} && nvm use ${PREFERRED_NODE_MAJOR}\`, or use the standalone installer:`,
35
30
  isWindows
36
31
  ? "irm https://feynman.is/install.ps1 | iex"
37
32
  : "curl -fsSL https://feynman.is/install | bash",
@@ -42,11 +37,5 @@ if (!isSupportedNodeVersion()) {
42
37
  for (const line of getUnsupportedNodeVersionLines()) {
43
38
  console.error(line);
44
39
  }
45
- // Too-new Node must not abort the install: failing preinstall makes npm roll
46
- // back to the previously installed version, which pins users to a release that
47
- // predates the fix they are trying to get (issue #177). The launcher in
48
- // bin/feynman.js enforces the supported range at runtime with the same message.
49
- if (parseNodeVersion(process.versions.node).major <= MAX_NODE_MAJOR) {
50
- process.exit(1);
51
- }
40
+ process.exit(1);
52
41
  }