@companion-ai/feynman 0.5.20 → 0.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/RELEASES.md CHANGED
@@ -6,6 +6,15 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.22 - 2026-10-01
10
+
11
+ - **Launch warnings and the first-run telemetry notice show inside the session.** Feynman printed them to the terminal just before starting Pi, where Pi 1.0's fullscreen interface (coming in a later Feynman release) hides them for the whole session. They now appear in Pi's own interface: the Windows "No Bash found" warning, the small-local-model warning for research workflows, and the one-time telemetry disclosure. A one-shot `--prompt` run still prints them to stderr.
12
+ - `feynman doctor` reports Pi's new `quietStartup: "header"` setting as "header only" instead of "disabled".
13
+
14
+ ## v0.5.21 - 2026-10-01
15
+
16
+ - **LiteLLM and custom providers that read their key from an environment variable work again.** Since Pi 0.99, `models.json` resolves an environment variable only when written as `$NAME`; a bare name is sent as the key itself. Feynman's LiteLLM setup wrote `LITELLM_MASTER_KEY`, so requests went out as `Authorization: Bearer LITELLM_MASTER_KEY` and the proxy rejected them, and custom providers set up with a variable name broke the same way. Feynman now writes `$NAME`, and on start rewrites any `apiKey` in `models.json` that is a bare all-caps variable name, such as `LITELLM_MASTER_KEY`, to `$LITELLM_MASTER_KEY`. Literal keys, placeholders such as `local`, and `!command` values are left alone.
17
+
9
18
  ## v0.5.20 - 2026-10-01
10
19
 
11
20
  - **Settings saved by Windows Notepad or PowerShell no longer stop Feynman.** Both can save UTF-8 with a byte-order mark, which JSON parsing rejects, so Feynman refused to start with "Invalid Feynman settings ... expected a JSON object", and other commands read the file as empty. Feynman now ignores the mark in `settings.json`, `auth.json`, and `web-search.json`, as Pi does. A settings file with a real JSON error now names the error, for example a trailing comma, instead of the generic message.
@@ -10,7 +10,7 @@ import { printInfo, printSection, printSuccess, printWarning } from "../ui/termi
10
10
  import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
11
11
  import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
12
12
  import { createModelRegistry, createModelRuntime, getModelsJsonPath } from "./registry.js";
13
- import { upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
13
+ import { apiKeyReference, upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
14
14
  const exec = promisify(execCallback);
15
15
  async function collectModelStatus(settingsPath, authPath) {
16
16
  return buildModelStatusSnapshotFromRecords(await getSupportedModelRecords(authPath), await getAuthenticatedModelRecords(authPath), getCurrentModelSpec(settingsPath));
@@ -161,7 +161,7 @@ async function resolveApiKeyConfig(apiKeyConfig) {
161
161
  return undefined;
162
162
  }
163
163
  }
164
- const envValue = process.env[trimmed];
164
+ const envValue = process.env[trimmed.replace(/^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/, "$1")];
165
165
  if (typeof envValue === "string" && envValue.trim()) {
166
166
  return envValue.trim();
167
167
  }
@@ -267,10 +267,10 @@ async function promptCustomProviderSetup() {
267
267
  }
268
268
  printInfo("API key value supports:");
269
269
  printInfo(" - literal secret (stored in models.json)");
270
- printInfo(" - env var name (resolved at runtime)");
270
+ printInfo(" - env var name such as MY_API_KEY or $MY_API_KEY (resolved at runtime)");
271
271
  printInfo(" - !command (executes and uses stdout)");
272
272
  const apiKeyConfigRaw = (await promptText("API key / resolver", "")).trim();
273
- const apiKeyConfig = apiKeyConfigRaw || "local";
273
+ const apiKeyConfig = apiKeyReference(apiKeyConfigRaw) || "local";
274
274
  if (!apiKeyConfigRaw) {
275
275
  printInfo("Using placeholder apiKey value (required by Pi for custom providers).");
276
276
  }
@@ -346,7 +346,7 @@ async function promptLiteLlmProviderSetup() {
346
346
  return undefined;
347
347
  }
348
348
  const hasKey = keySelection === 0;
349
- const apiKeyConfig = hasKey ? "LITELLM_MASTER_KEY" : "local";
349
+ const apiKeyConfig = hasKey ? "$LITELLM_MASTER_KEY" : "local";
350
350
  const authHeader = hasKey;
351
351
  if (hasKey) {
352
352
  printInfo("Set LITELLM_MASTER_KEY in your shell or .env before using Feynman.");
@@ -63,7 +63,9 @@ export function upsertProviderConfig(modelsJsonPath, providerId, patch) {
63
63
  if (patch.models !== undefined)
64
64
  nextProvider.models = patch.models;
65
65
  providers[providerId] = nextProvider;
66
- const next = { ...value, providers };
66
+ return writeModelsJson(modelsJsonPath, { ...value, providers });
67
+ }
68
+ function writeModelsJson(modelsJsonPath, next) {
67
69
  try {
68
70
  mkdirSync(dirname(modelsJsonPath), { recursive: true });
69
71
  writeFileSync(modelsJsonPath, JSON.stringify(next, null, 2) + "\n", "utf8");
@@ -80,3 +82,31 @@ export function upsertProviderConfig(modelsJsonPath, providerId, patch) {
80
82
  return { ok: false, error: `Failed to write models.json: ${error instanceof Error ? error.message : String(error)}` };
81
83
  }
82
84
  }
85
+ // Pi resolves an environment variable in `apiKey` only as $NAME or ${NAME}; a
86
+ // bare name such as LITELLM_MASTER_KEY is sent as the key itself.
87
+ const BARE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
88
+ export function apiKeyReference(value) {
89
+ return BARE_ENV_NAME.test(value) ? `$${value}` : value;
90
+ }
91
+ // Feynman's setup wrote bare names before Pi 0.99 stopped resolving them, so a
92
+ // LiteLLM or custom provider sent "LITELLM_MASTER_KEY" as its key. Returns the
93
+ // providers it rewrote to $NAME.
94
+ export function migrateBareEnvApiKeys(modelsJsonPath) {
95
+ if (!existsSync(modelsJsonPath))
96
+ return [];
97
+ const read = readModelsJson(modelsJsonPath);
98
+ if (!read.ok)
99
+ return [];
100
+ const providers = { ...(read.value.providers ?? {}) };
101
+ const migrated = Object.keys(providers).filter((id) => {
102
+ const apiKey = providers[id]?.apiKey;
103
+ return typeof apiKey === "string" && BARE_ENV_NAME.test(apiKey);
104
+ });
105
+ if (migrated.length === 0)
106
+ return [];
107
+ for (const id of migrated) {
108
+ const provider = providers[id];
109
+ providers[id] = { ...provider, apiKey: apiKeyReference(provider.apiKey) };
110
+ }
111
+ return writeModelsJson(modelsJsonPath, { ...read.value, providers }).ok ? migrated : [];
112
+ }
package/dist/pi/launch.js CHANGED
@@ -41,15 +41,10 @@ export async function runPi(options, args, env, stdin = "inherit") {
41
41
  });
42
42
  }
43
43
  export async function launchPiChat(options) {
44
+ let telemetryNotice;
44
45
  if (process.stdout.isTTY && options.mode !== "rpc") {
45
46
  process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
46
- // Clearing the screen erased the first-run notice printed at startup.
47
- const telemetryNotice = telemetryFirstRunNotice();
48
- if (telemetryNotice)
49
- process.stderr.write(`${telemetryNotice}\n`);
50
- }
51
- if (options.preLaunchNotice) {
52
- process.stdout.write(`${options.preLaunchNotice}\n`);
47
+ telemetryNotice = telemetryFirstRunNotice();
53
48
  }
54
49
  const executables = await resolveAllExecutables();
55
50
  ensureFeynmanCommandShim(options.appRoot, options.feynmanAgentDir);
@@ -59,5 +54,13 @@ export async function launchPiChat(options) {
59
54
  // explicit prompt forever.
60
55
  const explicitPrompt = Boolean(options.oneShotPrompt || options.initialPrompt);
61
56
  const stdin = explicitPrompt && options.mode !== "rpc" && !process.stdin.isTTY ? "ignore" : "inherit";
62
- process.exitCode = await runPi(options, buildPiArgs(options), buildPiEnv(options, executables), stdin);
57
+ // Pi's fullscreen UI hides anything printed before it starts, so the TUI
58
+ // session shows these itself; one-shot and JSON runs print them as before.
59
+ const notice = [telemetryNotice, options.preLaunchNotice].filter(Boolean).join("\n");
60
+ const tui = options.mode !== "rpc" && options.mode !== "json" && !options.oneShotPrompt;
61
+ if (notice && !tui)
62
+ process.stderr.write(`${notice}\n`);
63
+ const env = buildPiEnv(options, executables);
64
+ env.FEYNMAN_LAUNCH_NOTICE = notice && tui ? notice : undefined;
65
+ process.exitCode = await runPi(options, buildPiArgs(options), env, stdin);
63
66
  }
@@ -3,6 +3,8 @@ import { dirname, join } from "node:path";
3
3
  import { parseJsonText, readJsonFile } from "../config/json-file.js";
4
4
  import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
5
5
  import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
6
+ import { migrateBareEnvApiKeys } from "../model/models-json.js";
7
+ import { getModelsJsonPath } from "../model/registry.js";
6
8
  // Written by Feynman <= 0.4.0 next to a subagent extension path it managed.
7
9
  const LEGACY_RESEARCHER_EXTENSION_MARKER = "_feynmanResearchToolsExtension";
8
10
  function findModel(modelLookup, provider, id) {
@@ -180,6 +182,7 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
180
182
  const existing = existsSync(settingsPath) ? readConfigObject(settingsPath, "Feynman settings") : undefined;
181
183
  const settings = existing ? { ...existing.value } : {};
182
184
  const defaults = readConfigObject(bundledSettingsPath, "bundled Feynman settings").value;
185
+ migrateBareEnvApiKeys(getModelsJsonPath(authPath));
183
186
  for (const [key, value] of Object.entries({ ...defaults, defaultThinkingLevel })) {
184
187
  if (settings[key] === undefined)
185
188
  settings[key] = value;
@@ -171,7 +171,7 @@ export async function runDoctor(options) {
171
171
  for (const line of formatPiWebAccessDoctorLines()) {
172
172
  console.log(line);
173
173
  }
174
- console.log(`quiet startup: ${settings.quietStartup === true ? "enabled" : "disabled"}`);
174
+ console.log(`quiet startup: ${settings.quietStartup === "header" ? "header only" : settings.quietStartup === true ? "enabled" : "disabled"}`);
175
175
  console.log(`theme: ${typeof settings.theme === "string" ? settings.theme : "not set"}`);
176
176
  if (missingPiBits.length > 0) {
177
177
  console.log("pi runtime: missing files");
@@ -0,0 +1,12 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ export function registerLaunchNotice(pi: ExtensionAPI): void {
4
+ pi.on("session_start", (_event, ctx) => {
5
+ const notice = process.env.FEYNMAN_LAUNCH_NOTICE;
6
+ delete process.env.FEYNMAN_LAUNCH_NOTICE;
7
+ if (notice && ctx.mode === "tui" && ctx.hasUI) {
8
+ // Pi labels warnings itself.
9
+ ctx.ui.notify(notice.replace(/^Warning: /gm, ""), "warning");
10
+ }
11
+ });
12
+ }
@@ -7,6 +7,7 @@ import { registerDocxFallback } from "./research-tools/docx-fallback.js";
7
7
  import { installFeynmanHeader } from "./research-tools/header.js";
8
8
  import { registerHelpCommand } from "./research-tools/help.js";
9
9
  import { registerHuggingFaceTools } from "./research-tools/huggingface.js";
10
+ import { registerLaunchNotice } from "./research-tools/launch-notice.js";
10
11
  import { registerInitCommand, registerOutputsCommand } from "./research-tools/project.js";
11
12
  import { registerServiceTierControls } from "./research-tools/service-tier.js";
12
13
  import { registerScienceDatabaseTools } from "./research-tools/science-databases.js";
@@ -27,6 +28,7 @@ export default function researchTools(pi: ExtensionAPI): void {
27
28
  registerDocxFallback(pi);
28
29
  registerHelpCommand(pi);
29
30
  registerInitCommand(pi);
31
+ registerLaunchNotice(pi);
30
32
  registerOutputsCommand(pi);
31
33
  registerServiceTierControls(pi);
32
34
  registerScienceDatabaseTools(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",