@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.
- package/.feynman/settings.json +1 -5
- package/README.md +2 -1
- package/RELEASES.md +16 -0
- 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/telemetry/posthog.js +33 -10
- package/extensions/research-tools/current-date.ts +1 -0
- package/extensions/research-tools/telemetry.ts +22 -1
- package/package.json +1 -1
package/.feynman/settings.json
CHANGED
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
|
|
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", "
|
|
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,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;
|
|
@@ -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", {
|
|
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
|
});
|