@companion-ai/feynman 0.5.5 → 0.5.7

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/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
  ---
@@ -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,22 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.7 - 2026-09-26
10
+
11
+ - When Feynman runs as a package inside another Pi setup, an extension error no longer appears on every turn ("Cannot set properties of undefined (setting 'current_date')") if another extension or the host leaves the system-prompt sections unset (#298, thanks @Oscar-Williams in #300).
12
+
13
+ ## v0.5.6 - 2026-09-26
14
+
15
+ ### Failures report what went wrong
16
+
17
+ - 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.
18
+ - CI runs and the test suite no longer report telemetry, so usage numbers count real installs only.
19
+
20
+ ### Fixes
21
+
22
+ - 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`.
23
+ - `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`.
24
+
9
25
  ## v0.5.5 - 2026-09-24
10
26
 
11
27
  ### Node.js 26
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,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;
@@ -16,6 +16,7 @@ export function registerCurrentDateResearchContext(
16
16
  now: () => Date = () => new Date(),
17
17
  ): void {
18
18
  pi.on("before_agent_start", (event) => {
19
+ event.systemPromptOptions.sections ??= {};
19
20
  event.systemPromptOptions.sections.current_date = buildCurrentDateResearchContext(now());
20
21
  });
21
22
  }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",