@companion-ai/feynman 0.5.39 → 0.5.41

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,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.41 - 2026-10-12
10
+
11
+ - **A fresh `npm install` works again.** Pi 1.2.0, released today, made new npm installs of Feynman 0.5.40 fail on every command except `-v` (`does not provide an export named 'MCP_TYPESCRIPT_PREAMBLE'`). Pi 1.1.0 asks for its own sub-packages with open version ranges, so npm pulled Pi 1.2.0's copies. Feynman now pins those sub-packages to the Pi version it ships. The curl and PowerShell installers were not affected.
12
+ - **An expired sign-in no longer looks like no model.** Feynman treated a ChatGPT, Claude or Copilot sign-in whose short-lived access token had expired as if no model were set up. `feynman` reopened the setup menu, and since 0.5.36 `feynman -p` refused to run. Pi renews the token on the next request, so Feynman now leaves that to Pi and asks you to sign in again only when renewal really fails.
13
+ - **`--model` accepts the names Pi accepts.** Bare model IDs (`claude-opus-5-5`), short names (`sonnet`) and a thinking suffix (`anthropic/claude-opus-5-5:high`) now work, preferring a provider you are signed in to; `provider/model` and `provider:model` still work. Pro-class models stay blocked.
14
+ - **After you sign in, the model list matches your account.** `feynman model login` refreshes that provider's model list, so an OpenRouter key gets the models it can actually use before Feynman picks a default.
15
+ - A provider error hint now appears once the run has stopped, not in the middle of an automatic retry.
16
+ - Launching Feynman no longer erases your terminal's scrollback.
17
+ - The llama.cpp provider is recognized as a local model.
18
+ - **Preview and PDF export find more browsers.** Chromium, Edge and Brave on Linux, Edge under `Program Files (x86)` and Brave in your user folder on Windows, and apps in `~/Applications` on macOS. Thanks to @A-ryanVAT-S (#343).
19
+
20
+ ## v0.5.40 - 2026-10-12
21
+
22
+ - **alphaXiv sign-in messages name the right command.** When you weren't signed in, `feynman alpha search`, `get` and the paper tools said to run `alpha login`, a command that isn't installed with Feynman. They now say `feynman alpha login`. (Alpha Hub 0.1.7.)
23
+ - Security: Alpha Hub now requires `@modelcontextprotocol/sdk` 1.32.1 or later, so a fresh install can't resolve a version affected by GHSA-6qxp-vccf-f47h.
24
+
9
25
  ## v0.5.39 - 2026-10-12
10
26
 
11
27
  - **Web search and fetching are steadier (pi-web-access 0.39.0).** Search summaries now read each source's snippet, so providers that return only links still produce a summary. `auto` search moves on when a provider returns nothing, a stalled Perplexity request gives up after 30 seconds, and a DuckDuckGo bot check is reported as a block instead of an empty search. `fetch_content` answers end with the `get_search_content` call that reads the stored page, answer mode with several URLs shows every page's answer, and PDFs can go to a document parser you run yourself (`pdf.provider: "endpoint"` in `~/.feynman/agent/web-search.json`).
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import { ensureFeynmanAgentDir } from "./bootstrap/home.js";
19
19
  import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
20
20
  import { getLastPiStderr, launchPiChat, runPi } from "./pi/launch.js";
21
21
  import { installPiPackage, removePiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
22
- import { canonicalizeModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
22
+ import { resolveExplicitModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
23
23
  import { BUNDLED_PI_PACKAGES, buildPiEnv, findWindowsBash, WINDOWS_BASH_MISSING_NOTICE } from "./pi/runtime.js";
24
24
  import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js";
25
25
  import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, selectDefaultModel, setDefaultModelSpec, } from "./model/commands.js";
@@ -421,7 +421,9 @@ export async function main() {
421
421
  ...commandTelemetry,
422
422
  duration_ms: durationMs,
423
423
  exit_code: exitCode,
424
- ...(exitCode === 0 ? {} : { pi_stderr: getLastPiStderr() }),
424
+ // A fixed reason for a deliberate non-zero exit, so it isn't read as a crash.
425
+ ...(exitReason ? { exit_reason: exitReason } : {}),
426
+ ...(exitCode === 0 || exitReason ? {} : { pi_stderr: getLastPiStderr() }),
425
427
  };
426
428
  captureTelemetryEvent(exitCode === 0 ? "feynman_command_completed" : "feynman_command_failed", completeProperties);
427
429
  }
@@ -440,6 +442,8 @@ export async function main() {
440
442
  await shutdownPostHogTelemetry();
441
443
  }
442
444
  }
445
+ // Set when Feynman exits non-zero on purpose (see the completion telemetry in main).
446
+ let exitReason;
443
447
  async function runMain(input) {
444
448
  const { appRoot, feynmanVersion } = input;
445
449
  const bundledSettingsPath = resolve(appRoot, ".feynman", "settings.json");
@@ -641,10 +645,14 @@ async function runMain(input) {
641
645
  throw new Error(`Pro-class model disabled: ${requestedExplicitModelSpec}. Choose an approved research model.`);
642
646
  }
643
647
  const modelRuntime = await createModelRuntime(feynmanAuthPath);
644
- const canonicalModelSpec = canonicalizeModelSpec(requestedExplicitModelSpec, modelRuntime);
648
+ const canonicalModelSpec = resolveExplicitModelSpec(requestedExplicitModelSpec, modelRuntime);
645
649
  if (!canonicalModelSpec) {
646
650
  throw new Error(`Unknown model: ${requestedExplicitModelSpec}. Run \`feynman model list\` to see available models.`);
647
651
  }
652
+ // Check the resolved model too: a short name can resolve to a Pro-class one.
653
+ if (isProClassModelSpec(canonicalModelSpec.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, ""))) {
654
+ throw new Error(`Pro-class model disabled: ${canonicalModelSpec}. Choose an approved research model.`);
655
+ }
648
656
  explicitModelSpec = canonicalModelSpec;
649
657
  }
650
658
  const workflowCommandNames = new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name));
@@ -666,6 +674,7 @@ async function runMain(input) {
666
674
  if (!getCurrentModelSpec(feynmanSettingsPath)) {
667
675
  console.error(noModelMessage(promptGiven));
668
676
  process.exitCode = 1;
677
+ exitReason = "no_model";
669
678
  return;
670
679
  }
671
680
  await ensureFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, feynmanAuthPath);
@@ -674,6 +683,7 @@ async function runMain(input) {
674
683
  && (await getAuthenticatedModelRecords(feynmanAuthPath)).length === 0) {
675
684
  console.error(noModelMessage(promptGiven));
676
685
  process.exitCode = 1;
686
+ exitReason = "no_model";
677
687
  return;
678
688
  }
679
689
  // Pi's own session flags pass through; Feynman continues the latest
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { ensureSupportedNodeVersion } from "./system/node-version.js";
2
2
  async function run() {
3
3
  ensureSupportedNodeVersion();
4
+ // alphaXiv login hints from the bundled alpha-hub then name `feynman alpha login`.
5
+ process.env.ALPHA_HUB_CLI ??= "feynman alpha";
4
6
  const { main } = await import("./cli.js");
5
7
  await main();
6
8
  }
@@ -1,5 +1,3 @@
1
- import { getEnvApiKey } from "@earendil-works/pi-ai/compat";
2
- import { readJsonFile } from "../config/json-file.js";
3
1
  import { createModelRuntime } from "./registry.js";
4
2
  const PRO_CLASS_MODEL_PATTERN = /(?:^|[-_.:/])pro(?:$|[-_.:/])/i;
5
3
  // Pi catalogs these exact DeepSeek V4 Pro model IDs as ordinary open-weight
@@ -329,10 +327,8 @@ function sortProviders(left, right) {
329
327
  return left.label.localeCompare(right.label);
330
328
  }
331
329
  export async function getAuthenticatedModelRecords(authPath) {
332
- const expiredOAuthProviders = readExpiredOAuthProviders(authPath);
333
330
  const modelRuntime = await createModelRuntime(authPath);
334
331
  return (await modelRuntime.getAvailable())
335
- .filter((model) => !expiredOAuthProviders.has(model.provider))
336
332
  .map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
337
333
  }
338
334
  export async function getAvailableModelRecords(authPath) {
@@ -344,26 +340,6 @@ export async function getSupportedModelRecords(authPath) {
344
340
  .getModels()
345
341
  .map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
346
342
  }
347
- function readExpiredOAuthProviders(authPath) {
348
- const expired = new Set();
349
- try {
350
- const parsed = readJsonFile(authPath);
351
- for (const [provider, credential] of Object.entries(parsed)) {
352
- if (!credential || typeof credential !== "object")
353
- continue;
354
- const typedCredential = credential;
355
- if (typedCredential.type !== "oauth" || typeof typedCredential.expires !== "number")
356
- continue;
357
- if (typedCredential.expires > Date.now())
358
- continue;
359
- if (getEnvApiKey(provider))
360
- continue;
361
- expired.add(provider);
362
- }
363
- }
364
- catch { }
365
- return expired;
366
- }
367
343
  export async function chooseRecommendedModel(authPath) {
368
344
  const preferred = choosePreferredModelRecord(await getAvailableModelRecords(authPath));
369
345
  if (!preferred) {
@@ -1,7 +1,7 @@
1
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
1
2
  import { readFileSync, writeFileSync } from "node:fs";
2
3
  import { exec as execCallback } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
- import { dirname, resolve } from "node:path";
4
+ import { dirname } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { readJson } from "../pi/settings.js";
7
7
  import { promptChoice, promptSecret, promptSelect, promptText, SetupCancelledError, validateApiKeyInput } from "../setup/prompts.js";
@@ -125,7 +125,8 @@ function normalizeCustomProviderBaseUrl(api, baseUrl) {
125
125
  function isLocalBaseUrl(baseUrl) {
126
126
  return /^(https?:\/\/)?(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(baseUrl);
127
127
  }
128
- const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama-cpp", "llamacpp"]);
128
+ // "llama.cpp" is the id of Pi's bundled llama.cpp provider.
129
+ const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
129
130
  export function isLocalModelProvider(authPath, providerId) {
130
131
  if (!providerId) {
131
132
  return false;
@@ -653,6 +654,7 @@ async function configureApiKeyProvider(authPath, providerId) {
653
654
  printWarning(result.error);
654
655
  }
655
656
  }
657
+ await refreshProviderModels(modelRuntime, provider.id);
656
658
  return true;
657
659
  }
658
660
  async function resolveAvailableModelSpec(authPath, input) {
@@ -672,8 +674,8 @@ async function resolveAvailableModelSpec(authPath, input) {
672
674
  // When multiple providers expose the same bare model ID, prefer providers the
673
675
  // user explicitly configured in auth storage.
674
676
  if (exactIdMatches.length > 1) {
675
- const authData = readJson(authPath);
676
- const configuredProviders = new Set(Object.keys(authData));
677
+ const credentials = await (await createModelRuntime(authPath)).listCredentials();
678
+ const configuredProviders = new Set(credentials.map((credential) => credential.providerId));
677
679
  const configuredMatches = exactIdMatches.filter((model) => configuredProviders.has(model.provider));
678
680
  if (configuredMatches.length === 1) {
679
681
  return `${configuredMatches[0].provider}/${configuredMatches[0].id}`;
@@ -733,13 +735,11 @@ export async function authenticateModelProvider(authPath, settingsPath) {
733
735
  }
734
736
  // Sign in with ChatGPT sends a stable installation ID. Pi's own /login keeps it
735
737
  // as `deviceId` in the agent settings.json; share that one.
736
- export function getOrCreateDeviceId(authPath) {
737
- const settingsPath = resolve(dirname(authPath), "settings.json");
738
- const settings = readJson(settingsPath);
739
- if (typeof settings.deviceId === "string" && settings.deviceId)
740
- return settings.deviceId;
741
- const deviceId = randomUUID();
742
- writeFileSync(settingsPath, `${JSON.stringify({ ...settings, deviceId }, null, 2)}\n`, "utf8");
738
+ // Pi's own device ID, the one its /login sends, kept in the agent settings.
739
+ export async function getOrCreateDeviceId(authPath) {
740
+ const settings = SettingsManager.create(process.cwd(), dirname(authPath));
741
+ const deviceId = settings.getOrCreateDeviceId();
742
+ await settings.flush();
743
743
  return deviceId;
744
744
  }
745
745
  // A provider answers invalid_grant when it rejects the authorization code, and
@@ -752,6 +752,12 @@ export function describeOAuthLoginFailure(providerId, error) {
752
752
  const alternative = providerId === "openai" ? " Or sign in with `feynman model login openai-codex` instead." : "";
753
753
  return new Error(`${error.message}\nThe sign-in was rejected, usually because of a stale browser session. Sign out of the provider's site in your browser, or use a private window, then run \`feynman model login ${providerId}\` again.${alternative}`);
754
754
  }
755
+ // Pi's /login refreshes the provider's model list from the network after
756
+ // signing in (OpenRouter keys get the models that key may use); do the same,
757
+ // so the recommended default comes from that list. A failure keeps the saved list.
758
+ async function refreshProviderModels(modelRuntime, providerId) {
759
+ await modelRuntime.refresh({ providers: [providerId], allowNetwork: true, signal: AbortSignal.timeout(15_000) }).catch(() => undefined);
760
+ }
755
761
  export async function loginModelProvider(authPath, providerId, settingsPath) {
756
762
  if (providerId) {
757
763
  const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
@@ -776,6 +782,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
776
782
  }
777
783
  const modelRuntime = await createModelRuntime(authPath);
778
784
  const abortController = new AbortController();
785
+ const deviceId = await getOrCreateDeviceId(authPath);
779
786
  await modelRuntime.login(provider.id, "oauth", {
780
787
  prompt: async (prompt) => {
781
788
  if (prompt.type === "select") {
@@ -819,10 +826,11 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
819
826
  }
820
827
  },
821
828
  signal: abortController.signal,
822
- }, { getDeviceId: () => getOrCreateDeviceId(authPath) }).catch((error) => {
829
+ }, { getDeviceId: () => deviceId }).catch((error) => {
823
830
  throw describeOAuthLoginFailure(provider.id, error);
824
831
  });
825
832
  printSuccess(`Model provider login complete: ${provider.id}`);
833
+ await refreshProviderModels(modelRuntime, provider.id);
826
834
  await maybeSetRecommendedDefaultModel(settingsPath, authPath);
827
835
  return true;
828
836
  }
package/dist/pi/launch.js CHANGED
@@ -42,8 +42,9 @@ export async function runPi(options, args, env, stdin = "inherit") {
42
42
  }
43
43
  export async function launchPiChat(options) {
44
44
  let telemetryNotice;
45
+ // No screen clear: Pi's fullscreen mode uses the alternate screen, and
46
+ // `--tui-mode regular` keeps the terminal's scrollback.
45
47
  if (process.stdout.isTTY && options.mode !== "rpc") {
46
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
47
48
  telemetryNotice = telemetryFirstRunNotice();
48
49
  }
49
50
  const executables = await resolveAllExecutables();
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { delimiter, dirname, resolve, win32 } from "node:path";
4
- import { BROWSER_FALLBACK_PATHS, MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable, } from "../system/executables.js";
4
+ import { MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable, } from "../system/executables.js";
5
5
  import { getPostHogChildEnv } from "../telemetry/posthog.js";
6
6
  // Pi packages shipped as Feynman dependencies and loaded from their install
7
7
  // paths through settings.json `packages` (Pi's documented local-path source).
@@ -145,7 +145,7 @@ export function buildPiEnv(options, executables) {
145
145
  const binPath = [getFeynmanCommandShimDir(options.feynmanAgentDir), resolve(options.appRoot, "node_modules", ".bin")].join(delimiter);
146
146
  const pandocPath = process.env.PANDOC_PATH ?? executables?.pandoc ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
147
147
  const mermaidPath = process.env.MERMAID_CLI_PATH ?? executables?.mermaid ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS);
148
- const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? executables?.browser ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS);
148
+ const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? executables?.browser ?? resolveBrowser();
149
149
  return {
150
150
  ...process.env,
151
151
  PATH: [binPath, process.env.PATH ?? "", process.platform === "win32" ? findWindowsBash()?.addDir : undefined]
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { resolveCliModel } from "@earendil-works/pi-coding-agent";
3
4
  import { parseJsonText, readJsonFile } from "../config/json-file.js";
4
5
  import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
5
6
  import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
@@ -32,6 +33,29 @@ export function canonicalizeModelSpec(spec, modelLookup) {
32
33
  const model = parseModelSpec(spec, modelLookup);
33
34
  return model ? `${model.provider}/${model.id}` : undefined;
34
35
  }
36
+ // `--model` input, resolved the way Pi resolves it (bare ids, fuzzy names, a
37
+ // `:<thinking>` suffix) after Feynman's exact provider/id and provider:id
38
+ // forms. Pi's fuzzy match ignores sign-ins, so the same model id on a provider
39
+ // you are signed in to wins.
40
+ export function resolveExplicitModelSpec(spec, modelRuntime) {
41
+ const exact = canonicalizeModelSpec(spec, modelRuntime);
42
+ if (exact)
43
+ return exact;
44
+ let result = resolveCliModel({ cliModel: spec.trim(), modelRuntime });
45
+ if (!result.model)
46
+ return undefined;
47
+ if (!modelRuntime.hasConfiguredAuth(result.model.provider)) {
48
+ for (const provider of new Set(modelRuntime.getAvailableSnapshot().map((model) => model.provider))) {
49
+ const signedIn = resolveCliModel({ cliProvider: provider, cliModel: spec.trim(), modelRuntime });
50
+ if (signedIn.model && !signedIn.warning) {
51
+ result = signedIn;
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ const model = result.model;
57
+ return `${model.provider}/${model.id}${result.thinkingLevel ? `:${result.thinkingLevel}` : ""}`;
58
+ }
35
59
  export function normalizeThinkingLevel(value) {
36
60
  if (!value) {
37
61
  return undefined;
@@ -3,7 +3,7 @@ import { getValidToken as getValidAlphaToken } from "@companion-ai/alpha-hub/lib
3
3
  import { readFileSync } from "node:fs";
4
4
  import { verifyAlphaAuthStatus } from "../alpha-auth-status.js";
5
5
  import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
6
- import { BROWSER_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js";
6
+ import { PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable } from "../system/executables.js";
7
7
  import { readJson } from "../pi/settings.js";
8
8
  import { findWindowsBash, validatePiInstallation, WINDOWS_BASH_MISSING_NOTICE } from "../pi/runtime.js";
9
9
  import { isTelemetryDisabled } from "../telemetry/posthog.js";
@@ -41,7 +41,7 @@ function findProvidersMissingApiKey(modelsJsonPath) {
41
41
  }
42
42
  export async function collectStatusSnapshot(options) {
43
43
  const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
44
- const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS);
44
+ const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveBrowser();
45
45
  const missingPiBits = validatePiInstallation(options.appRoot);
46
46
  const webStatus = getPiWebAccessStatus();
47
47
  const modelStatus = buildModelStatusSnapshotFromRecords(await getSupportedModelRecords(options.authPath), await getAvailableModelRecords(options.authPath), getCurrentModelSpec(options.settingsPath));
@@ -105,7 +105,7 @@ export async function runDoctor(options) {
105
105
  const supportedModels = await getSupportedModelRecords(options.authPath);
106
106
  const modelStatus = await collectStatusSnapshot(options);
107
107
  const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
108
- const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS);
108
+ const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveBrowser();
109
109
  const missingPiBits = validatePiInstallation(options.appRoot);
110
110
  printPanel("Feynman Doctor", [
111
111
  "Checks config, auth, runtime wiring, and preview dependencies.",
@@ -1,7 +1,9 @@
1
1
  import { execFile, spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { dirname, delimiter } from "node:path";
4
5
  const isWindows = process.platform === "win32";
6
+ const isLinux = process.platform === "linux";
5
7
  const programFiles = process.env.PROGRAMFILES ?? "C:\\Program Files";
6
8
  const localAppData = process.env.LOCALAPPDATA ?? "";
7
9
  export const PANDOC_FALLBACK_PATHS = isWindows
@@ -16,14 +18,35 @@ export const BROWSER_FALLBACK_PATHS = isWindows
16
18
  `${programFiles} (x86)\\Google\\Chrome\\Application\\chrome.exe`,
17
19
  `${localAppData}\\Google\\Chrome\\Application\\chrome.exe`,
18
20
  `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
21
+ `${programFiles} (x86)\\Microsoft\\Edge\\Application\\msedge.exe`,
19
22
  `${programFiles}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
23
+ `${localAppData}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
20
24
  ]
21
- : [
22
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
23
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
24
- "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
25
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
26
- ];
25
+ : isLinux
26
+ ? [
27
+ "/usr/bin/google-chrome",
28
+ "/usr/bin/google-chrome-stable",
29
+ "/usr/bin/chromium",
30
+ "/usr/bin/chromium-browser",
31
+ "/snap/bin/chromium",
32
+ "/usr/bin/microsoft-edge",
33
+ "/usr/bin/brave-browser",
34
+ "/opt/google/chrome/chrome",
35
+ ]
36
+ : ["/Applications", `${homedir()}/Applications`].flatMap((dir) => [
37
+ `${dir}/Google Chrome.app/Contents/MacOS/Google Chrome`,
38
+ `${dir}/Chromium.app/Contents/MacOS/Chromium`,
39
+ `${dir}/Brave Browser.app/Contents/MacOS/Brave Browser`,
40
+ `${dir}/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`,
41
+ ]);
42
+ export const BROWSER_COMMANDS = [
43
+ "google-chrome",
44
+ "google-chrome-stable",
45
+ "chromium",
46
+ "chromium-browser",
47
+ "microsoft-edge",
48
+ "brave-browser",
49
+ ];
27
50
  export const MERMAID_FALLBACK_PATHS = isWindows
28
51
  ? []
29
52
  : ["/opt/homebrew/bin/mmdc", "/usr/local/bin/mmdc"];
@@ -81,11 +104,32 @@ export async function resolveExecutableAsync(name, fallbackPaths = []) {
81
104
  });
82
105
  });
83
106
  }
107
+ export function resolveBrowser({ fallbackPaths = BROWSER_FALLBACK_PATHS, fileExists = existsSync, resolveCommand = (name) => resolveExecutable(name), } = {}) {
108
+ const fallback = fallbackPaths.find((candidate) => fileExists(candidate));
109
+ if (fallback) {
110
+ return fallback;
111
+ }
112
+ for (const name of BROWSER_COMMANDS) {
113
+ const resolved = resolveCommand(name);
114
+ if (resolved) {
115
+ return resolved;
116
+ }
117
+ }
118
+ return undefined;
119
+ }
120
+ export async function resolveBrowserAsync() {
121
+ const fallback = findInFallbackPaths(BROWSER_FALLBACK_PATHS);
122
+ if (fallback) {
123
+ return fallback;
124
+ }
125
+ const resolved = await Promise.all(BROWSER_COMMANDS.map((name) => resolveExecutableAsync(name)));
126
+ return resolved.find(Boolean);
127
+ }
84
128
  export async function resolveAllExecutables() {
85
129
  const [pandoc, mermaid, browser] = await Promise.all([
86
130
  resolveExecutableAsync("pandoc", PANDOC_FALLBACK_PATHS),
87
131
  resolveExecutableAsync("mmdc", MERMAID_FALLBACK_PATHS),
88
- resolveExecutableAsync("google-chrome", BROWSER_FALLBACK_PATHS),
132
+ resolveBrowserAsync(),
89
133
  ]);
90
134
  return { pandoc, mermaid, browser };
91
135
  }
@@ -34,7 +34,7 @@ async function withPaperFallback<T>(
34
34
  try {
35
35
  return await run();
36
36
  } catch (error) {
37
- const message = (error instanceof Error ? error.message : String(error)).replace("`alpha login`", "`feynman alpha login`");
37
+ const message = error instanceof Error ? error.message : String(error);
38
38
  if (fallback) {
39
39
  try {
40
40
  return { fallbackNote: `alphaXiv did not answer (${message}); these results are from ${fallback.source}.`, ...(await fallback.run()) };
@@ -1,3 +1,4 @@
1
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
1
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
 
3
4
  // Most failed research runs end on a provider error (quota, a key, a model the
@@ -24,9 +25,18 @@ export function modelErrorHint(provider: string, error: string): string | undefi
24
25
  }
25
26
 
26
27
  export function registerModelErrorHint(pi: ExtensionAPI): void {
27
- pi.on("message_end", (event, ctx) => {
28
+ // Pi retries rate limits and outages itself; hint only about the error the
29
+ // run actually ended on, once it has settled.
30
+ let lastError: AssistantMessage | undefined;
31
+ pi.on("message_end", (event) => {
28
32
  const message = event.message;
29
- if (message.role !== "assistant" || message.stopReason !== "error" || !ctx.hasUI) return;
33
+ if (message.role !== "assistant") return;
34
+ lastError = message.stopReason === "error" ? message : undefined;
35
+ });
36
+ pi.on("agent_settled", (event, ctx) => {
37
+ const message = lastError;
38
+ lastError = undefined;
39
+ if (!message || event.aborted || !ctx.hasUI) return;
30
40
  const hint = modelErrorHint(message.provider, message.errorMessage ?? "");
31
41
  ctx.ui.notify(
32
42
  `${hint ?? `${message.provider} returned an error.`} If this looks like a Feynman bug, report it at https://github.com/Companion-Inc/feynman/issues (Pi's /bug goes to Pi's developers).`,
@@ -252,7 +252,8 @@ export function registerResearchTelemetry(
252
252
  $ai_cache_creation_input_tokens: message.usage.cacheWrite,
253
253
  // Pi reports input tokens with cache reads and writes already subtracted.
254
254
  $ai_cache_reporting_exclusive: true,
255
- $ai_latency: requestStartedAt === undefined ? undefined : (now() - requestStartedAt) / 1000,
255
+ // Pi times each response itself; older messages may lack it.
256
+ $ai_latency: message.durationMs !== undefined ? message.durationMs / 1000 : requestStartedAt === undefined ? undefined : (now() - requestStartedAt) / 1000,
256
257
  $ai_http_status: httpStatus,
257
258
  $ai_is_error: message.stopReason === "error",
258
259
  $ai_error: message.stopReason === "error" ? errorText(message.errorMessage) : undefined,
@@ -264,8 +265,8 @@ export function registerResearchTelemetry(
264
265
  if (run) run.stopReason = message.stopReason;
265
266
  });
266
267
 
267
- pi.on("agent_settled", (_event, ctx) => {
268
- finishRun(ctx.cwd, stopStatus(run?.stopReason));
268
+ pi.on("agent_settled", (event, ctx) => {
269
+ finishRun(ctx.cwd, event.aborted ? "aborted" : stopStatus(run?.stopReason));
269
270
  });
270
271
 
271
272
  pi.on("session_shutdown", async (event, ctx) => {
@@ -151,7 +151,7 @@ export const legacyFlags = [
151
151
  { usage: "--alpha-login", description: "Sign in to alphaXiv and exit." },
152
152
  { usage: "--alpha-logout", description: "Clear alphaXiv auth and exit." },
153
153
  { usage: "--alpha-status", description: "Show alphaXiv auth status and exit." },
154
- { usage: "--model <provider/model|provider:model>", description: "Force a specific approved research model." },
154
+ { usage: "--model <model>", description: "Force an approved research model: provider/model, a bare ID, or a short name; add :high etc. for thinking." },
155
155
  { usage: "--service-tier <tier>", description: "Override request service tier for this run." },
156
156
  { usage: "--thinking <level>", description: "Set thinking level: off | minimal | low | medium | high | xhigh | max." },
157
157
  { usage: "--cwd <path>", description: "Set the working directory for tools." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.39",
3
+ "version": "0.5.41",
4
4
  "description": "The open source AI research agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -78,7 +78,11 @@
78
78
  },
79
79
  "dependencies": {
80
80
  "@clack/prompts": "^1.8.1",
81
- "@companion-ai/alpha-hub": "0.1.6",
81
+ "@companion-ai/alpha-hub": "0.1.7",
82
+ "@earendil-works/chord": "1.1.0",
83
+ "@earendil-works/pi-codemode": "1.1.0",
84
+ "@earendil-works/pi-mcp": "1.1.0",
85
+ "@earendil-works/pi-telemetry": "1.1.0",
82
86
  "fast-xml-parser": "5.11.1",
83
87
  "pi-btw": "0.8.0",
84
88
  "pi-docparser": "4.0.0",
@@ -87,6 +91,7 @@
87
91
  "posthog-node": "^5.53.0"
88
92
  },
89
93
  "peerDependencies": {
94
+ "@earendil-works/pi-agent-core": "1.1.0",
90
95
  "@earendil-works/pi-ai": "1.1.0",
91
96
  "@earendil-works/pi-coding-agent": "1.1.0",
92
97
  "@earendil-works/pi-tui": "*",