@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.
- package/.feynman/settings.json +1 -5
- package/.nvmrc +1 -1
- package/README.md +3 -2
- package/RELEASES.md +30 -0
- package/bin/feynman.js +5 -9
- package/dist/cli.js +4 -2
- package/dist/pi/launch.js +12 -1
- package/dist/pi/settings.js +4 -0
- package/dist/setup/prompts.js +10 -1
- package/dist/system/node-version.js +5 -11
- package/dist/telemetry/posthog.js +33 -10
- package/extensions/research-tools/alpha.ts +16 -3
- package/extensions/research-tools/telemetry.ts +22 -1
- package/package.json +8 -8
- package/scripts/check-node-version.mjs +6 -17
package/.feynman/settings.json
CHANGED
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
24.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
28
|
-
console.error(
|
|
29
|
-
? "
|
|
30
|
-
:
|
|
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", "
|
|
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) => {
|
package/dist/pi/settings.js
CHANGED
|
@@ -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 = {};
|
package/dist/setup/prompts.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
? "
|
|
31
|
-
:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
99
|
-
"tsx": "^4.23.
|
|
98
|
+
"@types/node": "^26.6.2",
|
|
99
|
+
"tsx": "^4.23.15",
|
|
100
100
|
"typescript": "^7.0.2",
|
|
101
|
-
"yaml": "2.9.
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
? "
|
|
32
|
-
:
|
|
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
|
-
|
|
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
|
}
|