@companion-ai/feynman 0.5.40 → 0.5.42

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,5 +2,6 @@
2
2
  "theme": "feynman",
3
3
  "quietStartup": true,
4
4
  "collapseChangelog": true,
5
- "editorPaddingX": 1
5
+ "editorPaddingX": 1,
6
+ "showHardwareCursor": true
6
7
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3
3
  "name": "feynman",
4
4
  "vars": {
5
5
  "ink": "#d3c6aa",
package/RELEASES.md CHANGED
@@ -6,6 +6,32 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.42 - 2026-10-13
10
+
11
+ - **Pi 1.2.0 and pi-subagents 0.78.0.**
12
+ - With an OpenRouter key, `/model` lists only the models that key can use, with their real context sizes and prices.
13
+ - "The upstream provider returned an error" is retried instead of ending the turn, and Anthropic requests rejected for too many strict tools are retried once.
14
+ - `feynman -p` warns and exits 1 when the answer was cut off at the model's output limit.
15
+ - On Windows, reading or writing saved logins no longer fails with `EPERM` while an old lock is cleaned up.
16
+ - Pressing `Esc` keeps a research run stopped; a subagent finishing a moment later no longer restarts it.
17
+ - Reports are no longer overwritten when a subagent run is resumed, and a timed-out subagent's partial output reaches the lead agent.
18
+ - The `workflowScript contains dynamic child launches` warning no longer prints on every parallel run.
19
+ - **`feynman model login` offers every provider Pi supports with an API key.** That is 41 providers instead of Feynman's own list of 16, adding DeepSeek, Fireworks, Together, NVIDIA, Google Vertex, Cloudflare, Qwen and others. Pi now asks for what each provider needs, so Cloudflare's extra fields and Bedrock's profile or bearer-token options work. Pressing Enter on an empty key no longer saves an empty credential.
20
+ - **Turning off Feynman's telemetry also turns off Pi's.** `FEYNMAN_TELEMETRY=off` (or `DO_NOT_TRACK=1`) now sets Pi's own `PI_TELEMETRY=0` for its install reporting.
21
+ - Feynman now honors Pi's own `sessionDir` setting and `PI_CODING_AGENT_SESSION_DIR` when you don't pass `--session-dir`.
22
+ - `feynman model set` and the default-model check use Pi's settings API, so they match what Pi launches with (including trusted project settings).
23
+
24
+ ## v0.5.41 - 2026-10-12
25
+
26
+ - **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.
27
+ - **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.
28
+ - **`--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.
29
+ - **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.
30
+ - A provider error hint now appears once the run has stopped, not in the middle of an automatic retry.
31
+ - Launching Feynman no longer erases your terminal's scrollback.
32
+ - The llama.cpp provider is recognized as a local model.
33
+ - **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).
34
+
9
35
  ## v0.5.40 - 2026-10-12
10
36
 
11
37
  - **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.)
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";
@@ -29,6 +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 { SettingsManager } from "@earendil-works/pi-coding-agent";
32
33
  import { noModelMessage } from "./setup/prompts.js";
33
34
  import { captureTelemetryEvent, captureTelemetryException, getCliTelemetryMetadata, initializePostHogTelemetry, shutdownPostHogTelemetry, telemetryErrorProperties, telemetryFirstRunNotice, } from "./telemetry/posthog.js";
34
35
  import { ASH, printAsciiHeader, printInfo, printPanel, printSection, RESET, SAGE } from "./ui/terminal.js";
@@ -421,7 +422,9 @@ export async function main() {
421
422
  ...commandTelemetry,
422
423
  duration_ms: durationMs,
423
424
  exit_code: exitCode,
424
- ...(exitCode === 0 ? {} : { pi_stderr: getLastPiStderr() }),
425
+ // A fixed reason for a deliberate non-zero exit, so it isn't read as a crash.
426
+ ...(exitReason ? { exit_reason: exitReason } : {}),
427
+ ...(exitCode === 0 || exitReason ? {} : { pi_stderr: getLastPiStderr() }),
425
428
  };
426
429
  captureTelemetryEvent(exitCode === 0 ? "feynman_command_completed" : "feynman_command_failed", completeProperties);
427
430
  }
@@ -440,6 +443,8 @@ export async function main() {
440
443
  await shutdownPostHogTelemetry();
441
444
  }
442
445
  }
446
+ // Set when Feynman exits non-zero on purpose (see the completion telemetry in main).
447
+ let exitReason;
443
448
  async function runMain(input) {
444
449
  const { appRoot, feynmanVersion } = input;
445
450
  const bundledSettingsPath = resolve(appRoot, ".feynman", "settings.json");
@@ -512,7 +517,12 @@ async function runMain(input) {
512
517
  throw new Error("Unable to determine the installed Feynman version.");
513
518
  }
514
519
  const workingDir = resolve(values.cwd ?? process.cwd());
515
- const sessionDir = resolve(values["session-dir"] ?? getDefaultSessionDir(feynmanHome));
520
+ // Pi's order: --session-dir, then PI_CODING_AGENT_SESSION_DIR, then the sessionDir
521
+ // setting; Feynman's own default comes last.
522
+ const sessionDir = resolve(workingDir, values["session-dir"] ??
523
+ process.env.PI_CODING_AGENT_SESSION_DIR ??
524
+ SettingsManager.create(workingDir, feynmanAgentDir).getSessionDir() ??
525
+ getDefaultSessionDir(feynmanHome));
516
526
  const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
517
527
  const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
518
528
  const { defaultThinkingLevel, launchThinkingLevel } = resolveThinkingConfig(values.thinking ?? process.env.FEYNMAN_THINKING);
@@ -641,10 +651,14 @@ async function runMain(input) {
641
651
  throw new Error(`Pro-class model disabled: ${requestedExplicitModelSpec}. Choose an approved research model.`);
642
652
  }
643
653
  const modelRuntime = await createModelRuntime(feynmanAuthPath);
644
- const canonicalModelSpec = canonicalizeModelSpec(requestedExplicitModelSpec, modelRuntime);
654
+ const canonicalModelSpec = resolveExplicitModelSpec(requestedExplicitModelSpec, modelRuntime);
645
655
  if (!canonicalModelSpec) {
646
656
  throw new Error(`Unknown model: ${requestedExplicitModelSpec}. Run \`feynman model list\` to see available models.`);
647
657
  }
658
+ // Check the resolved model too: a short name can resolve to a Pro-class one.
659
+ if (isProClassModelSpec(canonicalModelSpec.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, ""))) {
660
+ throw new Error(`Pro-class model disabled: ${canonicalModelSpec}. Choose an approved research model.`);
661
+ }
648
662
  explicitModelSpec = canonicalModelSpec;
649
663
  }
650
664
  const workflowCommandNames = new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name));
@@ -666,6 +680,7 @@ async function runMain(input) {
666
680
  if (!getCurrentModelSpec(feynmanSettingsPath)) {
667
681
  console.error(noModelMessage(promptGiven));
668
682
  process.exitCode = 1;
683
+ exitReason = "no_model";
669
684
  return;
670
685
  }
671
686
  await ensureFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, feynmanAuthPath);
@@ -674,6 +689,7 @@ async function runMain(input) {
674
689
  && (await getAuthenticatedModelRecords(feynmanAuthPath)).length === 0) {
675
690
  console.error(noModelMessage(promptGiven));
676
691
  process.exitCode = 1;
692
+ exitReason = "no_model";
677
693
  return;
678
694
  }
679
695
  // Pi's own session flags pass through; Feynman continues the latest
@@ -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,9 +1,8 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
1
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
2
+ import { readFileSync } 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
- import { readJson } from "../pi/settings.js";
7
6
  import { promptChoice, promptSecret, promptSelect, promptText, SetupCancelledError, validateApiKeyInput } from "../setup/prompts.js";
8
7
  import { openUrl } from "../system/open-url.js";
9
8
  import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
@@ -54,19 +53,32 @@ async function selectOAuthProvider(authPath, action) {
54
53
  }
55
54
  return selection;
56
55
  }
57
- function resolveApiKeyProvider(input) {
56
+ // Endpoints Pi has no API-key sign-in for: Feynman writes their models.json entry.
57
+ const LOCAL_API_KEY_SETUPS = MODEL_API_KEY_PROVIDERS.filter((provider) => ["lm-studio", "litellm", "__custom__"].includes(provider.id));
58
+ // Every provider Pi can sign in to with an API key (its own list, labels and
59
+ // prompts), plus Feynman's local and custom endpoint setups.
60
+ export async function listApiKeyProviders(authPath) {
61
+ const envVars = new Map(MODEL_API_KEY_PROVIDERS.map((provider) => [provider.id, provider.envVar]));
62
+ const piProviders = (await createModelRuntime(authPath))
63
+ .getProviders()
64
+ .filter((provider) => provider.auth.apiKey)
65
+ .map((provider) => ({ id: provider.id, label: provider.name, envVar: envVars.get(provider.id) }))
66
+ .sort((left, right) => left.label.localeCompare(right.label));
67
+ return [...piProviders, ...LOCAL_API_KEY_SETUPS];
68
+ }
69
+ async function resolveApiKeyProvider(authPath, input) {
58
70
  const normalizedInput = normalizeProviderId(input);
59
71
  if (!normalizedInput) {
60
72
  return undefined;
61
73
  }
62
- return MODEL_API_KEY_PROVIDERS.find((provider) => provider.id === normalizedInput);
74
+ return (await listApiKeyProviders(authPath)).find((provider) => provider.id === normalizedInput);
63
75
  }
64
76
  export async function resolveModelProviderForCommand(authPath, input) {
65
77
  const oauthProvider = await resolveOAuthProvider(authPath, input);
66
78
  if (oauthProvider) {
67
79
  return { kind: "oauth", id: oauthProvider.id };
68
80
  }
69
- const apiKeyProvider = resolveApiKeyProvider(input);
81
+ const apiKeyProvider = await resolveApiKeyProvider(authPath, input);
70
82
  if (apiKeyProvider) {
71
83
  return { kind: "api-key", id: apiKeyProvider.id };
72
84
  }
@@ -84,14 +96,15 @@ function apiKeyProviderHint(provider) {
84
96
  }
85
97
  return provider.envVar ?? provider.id;
86
98
  }
87
- async function selectApiKeyProvider() {
88
- const options = MODEL_API_KEY_PROVIDERS.map((provider) => ({
99
+ async function selectApiKeyProvider(authPath) {
100
+ const providers = await listApiKeyProviders(authPath);
101
+ const options = providers.map((provider) => ({
89
102
  value: provider,
90
103
  label: provider.label,
91
104
  hint: apiKeyProviderHint(provider),
92
105
  }));
93
106
  options.push({ value: "cancel", label: "Cancel" });
94
- const defaultProvider = MODEL_API_KEY_PROVIDERS.find((provider) => provider.id === "openai") ?? MODEL_API_KEY_PROVIDERS[0];
107
+ const defaultProvider = providers.find((provider) => provider.id === "openai") ?? providers[0];
95
108
  const selection = await promptSelect("Choose an API-key provider:", options, defaultProvider);
96
109
  if (selection === "cancel") {
97
110
  return undefined;
@@ -125,7 +138,8 @@ function normalizeCustomProviderBaseUrl(api, baseUrl) {
125
138
  function isLocalBaseUrl(baseUrl) {
126
139
  return /^(https?:\/\/)?(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(baseUrl);
127
140
  }
128
- const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama-cpp", "llamacpp"]);
141
+ // "llama.cpp" is the id of Pi's bundled llama.cpp provider.
142
+ const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
129
143
  export function isLocalModelProvider(authPath, providerId) {
130
144
  if (!providerId) {
131
145
  return false;
@@ -500,36 +514,6 @@ async function verifyCustomProvider(setup, authPath) {
500
514
  }
501
515
  printInfo("Verification: skipped network probe for this API mode.");
502
516
  }
503
- async function verifyBedrockCredentialChain() {
504
- const { defaultProvider } = await import("@aws-sdk/credential-provider-node");
505
- const credentials = await defaultProvider({})();
506
- if (!credentials?.accessKeyId || !credentials?.secretAccessKey) {
507
- throw new Error("AWS credential chain resolved without usable Bedrock credentials.");
508
- }
509
- }
510
- async function configureBedrockProvider(authPath) {
511
- printSection("AWS Credentials: Amazon Bedrock");
512
- printInfo("Feynman will verify the AWS SDK credential chain used by Pi's Bedrock provider.");
513
- printInfo("Supported sources include AWS_PROFILE, ~/.aws credentials/config, SSO, ECS/IRSA, and EC2 instance roles.");
514
- try {
515
- await verifyBedrockCredentialChain();
516
- const modelRuntime = await createModelRuntime(authPath);
517
- if (!(await modelRuntime.checkAuth("amazon-bedrock"))) {
518
- throw new Error("Pi could not resolve the verified AWS credential chain for Amazon Bedrock.");
519
- }
520
- printSuccess("Verified the AWS credential chain used by Amazon Bedrock.");
521
- printInfo("Use `feynman model list` to see available Bedrock models.");
522
- return true;
523
- }
524
- catch (error) {
525
- printWarning(`AWS credential verification failed: ${error instanceof Error ? error.message : String(error)}`);
526
- printInfo("Configure AWS credentials first, for example:");
527
- printInfo(" export AWS_PROFILE=default");
528
- printInfo(" # or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY");
529
- printInfo(" # or use an EC2/ECS/IRSA role with valid Bedrock access");
530
- return false;
531
- }
532
- }
533
517
  async function maybeSetRecommendedDefaultModel(settingsPath, authPath) {
534
518
  if (!settingsPath) {
535
519
  return;
@@ -545,7 +529,7 @@ async function maybeSetRecommendedDefaultModel(settingsPath, authPath) {
545
529
  }
546
530
  }
547
531
  async function configureApiKeyProvider(authPath, providerId) {
548
- const provider = providerId ? resolveApiKeyProvider(providerId) : await selectApiKeyProvider();
532
+ const provider = providerId ? await resolveApiKeyProvider(authPath, providerId) : await selectApiKeyProvider(authPath);
549
533
  if (!provider) {
550
534
  if (providerId) {
551
535
  throw new Error(`Unknown API-key model provider: ${providerId}`);
@@ -553,9 +537,6 @@ async function configureApiKeyProvider(authPath, providerId) {
553
537
  printInfo("API key setup cancelled.");
554
538
  return false;
555
539
  }
556
- if (provider.id === "amazon-bedrock") {
557
- return configureBedrockProvider(authPath);
558
- }
559
540
  if (provider.id === "lm-studio") {
560
541
  const setup = await promptLmStudioProviderSetup();
561
542
  if (!setup) {
@@ -626,22 +607,34 @@ async function configureApiKeyProvider(authPath, providerId) {
626
607
  if (provider.envVar) {
627
608
  printInfo(`Tip: to avoid writing secrets to disk, set ${provider.envVar} in your shell or .env.`);
628
609
  }
629
- const apiKey = await promptSecret("Paste API key (leave empty to use env var instead)", undefined, validateApiKeyInput);
630
- if (!apiKey) {
631
- if (provider.envVar) {
632
- printInfo(`Set ${provider.envVar} and rerun setup (or run \`feynman model list\`).`);
633
- }
634
- else {
635
- printInfo("No API key provided.");
636
- }
637
- return false;
638
- }
610
+ // Pi asks for whatever this provider needs (a key, or e.g. Cloudflare's account
611
+ // and gateway, Bedrock's profile or token). Esc cancels.
639
612
  const modelRuntime = await createModelRuntime(authPath);
640
- await modelRuntime.login(provider.id, "api_key", {
641
- prompt: async () => apiKey,
642
- notify: () => undefined,
613
+ const interaction = terminalAuthInteraction(provider.label, new AbortController().signal);
614
+ const signedIn = await modelRuntime
615
+ .login(provider.id, "api_key", {
616
+ ...interaction,
617
+ prompt: async (prompt) => {
618
+ if (prompt.type !== "secret")
619
+ return interaction.prompt(prompt);
620
+ const secret = await promptSecret(prompt.message, prompt.signal, validateApiKeyInput);
621
+ // An empty key would be saved as a credential that fails every request.
622
+ if (!secret)
623
+ throw new SetupCancelledError("no API key entered");
624
+ return secret;
625
+ },
626
+ })
627
+ .then(() => true)
628
+ .catch((error) => {
629
+ if (!(error instanceof SetupCancelledError))
630
+ throw error;
631
+ if (provider.envVar)
632
+ printInfo(`Or set ${provider.envVar} and run feynman again.`);
633
+ return false;
643
634
  });
644
- printSuccess(`Saved API key for ${provider.id} in auth storage.`);
635
+ if (!signedIn)
636
+ return false;
637
+ printSuccess(`Saved ${provider.label} credentials in auth storage.`);
645
638
  const baseUrl = await promptText("Base URL override (optional, include /v1 for OpenAI-compatible endpoints)", "");
646
639
  if (baseUrl) {
647
640
  const modelsJsonPath = getModelsJsonPath(authPath);
@@ -653,6 +646,7 @@ async function configureApiKeyProvider(authPath, providerId) {
653
646
  printWarning(result.error);
654
647
  }
655
648
  }
649
+ await refreshProviderModels(modelRuntime, provider.id);
656
650
  return true;
657
651
  }
658
652
  async function resolveAvailableModelSpec(authPath, input) {
@@ -672,8 +666,8 @@ async function resolveAvailableModelSpec(authPath, input) {
672
666
  // When multiple providers expose the same bare model ID, prefer providers the
673
667
  // user explicitly configured in auth storage.
674
668
  if (exactIdMatches.length > 1) {
675
- const authData = readJson(authPath);
676
- const configuredProviders = new Set(Object.keys(authData));
669
+ const credentials = await (await createModelRuntime(authPath)).listCredentials();
670
+ const configuredProviders = new Set(credentials.map((credential) => credential.providerId));
677
671
  const configuredMatches = exactIdMatches.filter((model) => configuredProviders.has(model.provider));
678
672
  if (configuredMatches.length === 1) {
679
673
  return `${configuredMatches[0].provider}/${configuredMatches[0].id}`;
@@ -681,12 +675,12 @@ async function resolveAvailableModelSpec(authPath, input) {
681
675
  }
682
676
  return undefined;
683
677
  }
678
+ // The default model Pi will launch with, read through Pi's SettingsManager.
684
679
  export function getCurrentModelSpec(settingsPath) {
685
- const settings = readJson(settingsPath);
686
- if (typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string") {
687
- return `${settings.defaultProvider}/${settings.defaultModel}`;
688
- }
689
- return undefined;
680
+ const settings = SettingsManager.create(process.cwd(), dirname(settingsPath));
681
+ const provider = settings.getDefaultProvider();
682
+ const model = settings.getDefaultModel();
683
+ return provider && model ? `${provider}/${model}` : undefined;
690
684
  }
691
685
  export async function printModelList(settingsPath, authPath) {
692
686
  const status = await collectModelStatus(settingsPath, authPath);
@@ -733,13 +727,11 @@ export async function authenticateModelProvider(authPath, settingsPath) {
733
727
  }
734
728
  // Sign in with ChatGPT sends a stable installation ID. Pi's own /login keeps it
735
729
  // 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");
730
+ // Pi's own device ID, the one its /login sends, kept in the agent settings.
731
+ export async function getOrCreateDeviceId(authPath) {
732
+ const settings = SettingsManager.create(process.cwd(), dirname(authPath));
733
+ const deviceId = settings.getOrCreateDeviceId();
734
+ await settings.flush();
743
735
  return deviceId;
744
736
  }
745
737
  // A provider answers invalid_grant when it rejects the authorization code, and
@@ -752,31 +744,15 @@ export function describeOAuthLoginFailure(providerId, error) {
752
744
  const alternative = providerId === "openai" ? " Or sign in with `feynman model login openai-codex` instead." : "";
753
745
  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
746
  }
755
- export async function loginModelProvider(authPath, providerId, settingsPath) {
756
- if (providerId) {
757
- const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
758
- if (!resolvedProvider) {
759
- throw new Error(`Unknown model provider: ${providerId}`);
760
- }
761
- if (resolvedProvider.kind === "api-key") {
762
- const configured = await configureApiKeyProvider(authPath, resolvedProvider.id);
763
- if (configured) {
764
- await maybeSetRecommendedDefaultModel(settingsPath, authPath);
765
- }
766
- return configured;
767
- }
768
- }
769
- const provider = providerId ? await resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
770
- if (!provider) {
771
- if (providerId) {
772
- throw new Error(`Unknown model provider: ${providerId}`);
773
- }
774
- printInfo("Login cancelled.");
775
- return false;
776
- }
777
- const modelRuntime = await createModelRuntime(authPath);
778
- const abortController = new AbortController();
779
- await modelRuntime.login(provider.id, "oauth", {
747
+ // Pi's /login refreshes the provider's model list from the network after
748
+ // signing in (OpenRouter keys get the models that key may use); do the same,
749
+ // so the recommended default comes from that list. A failure keeps the saved list.
750
+ async function refreshProviderModels(modelRuntime, providerId) {
751
+ await modelRuntime.refresh({ providers: [providerId], allowNetwork: true, signal: AbortSignal.timeout(15_000) }).catch(() => undefined);
752
+ }
753
+ // Runs Pi's own login prompts (selects, secrets, text, browser URLs) in the terminal.
754
+ function terminalAuthInteraction(providerLabel, signal) {
755
+ return {
780
756
  prompt: async (prompt) => {
781
757
  if (prompt.type === "select") {
782
758
  return promptSelect(prompt.message, prompt.options.map((option) => ({
@@ -795,7 +771,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
795
771
  },
796
772
  notify: (event) => {
797
773
  if (event.type === "auth_url") {
798
- printSection(`Login: ${provider.name ?? provider.id}`);
774
+ printSection(`Login: ${providerLabel}`);
799
775
  if (openUrl(event.url)) {
800
776
  printInfo("Opened the login URL in your browser.");
801
777
  }
@@ -809,7 +785,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
809
785
  return;
810
786
  }
811
787
  if (event.type === "device_code") {
812
- printSection(`Login: ${provider.name ?? provider.id}`);
788
+ printSection(`Login: ${providerLabel}`);
813
789
  printInfo(`Visit ${event.verificationUri} and enter code: ${event.userCode}`);
814
790
  openUrl(event.verificationUri);
815
791
  return;
@@ -818,11 +794,39 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
818
794
  printInfo(event.message);
819
795
  }
820
796
  },
821
- signal: abortController.signal,
822
- }, { getDeviceId: () => getOrCreateDeviceId(authPath) }).catch((error) => {
797
+ signal,
798
+ };
799
+ }
800
+ export async function loginModelProvider(authPath, providerId, settingsPath) {
801
+ if (providerId) {
802
+ const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
803
+ if (!resolvedProvider) {
804
+ throw new Error(`Unknown model provider: ${providerId}`);
805
+ }
806
+ if (resolvedProvider.kind === "api-key") {
807
+ const configured = await configureApiKeyProvider(authPath, resolvedProvider.id);
808
+ if (configured) {
809
+ await maybeSetRecommendedDefaultModel(settingsPath, authPath);
810
+ }
811
+ return configured;
812
+ }
813
+ }
814
+ const provider = providerId ? await resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
815
+ if (!provider) {
816
+ if (providerId) {
817
+ throw new Error(`Unknown model provider: ${providerId}`);
818
+ }
819
+ printInfo("Login cancelled.");
820
+ return false;
821
+ }
822
+ const modelRuntime = await createModelRuntime(authPath);
823
+ const abortController = new AbortController();
824
+ const deviceId = await getOrCreateDeviceId(authPath);
825
+ await modelRuntime.login(provider.id, "oauth", terminalAuthInteraction(provider.name ?? provider.id, abortController.signal), { getDeviceId: () => deviceId }).catch((error) => {
823
826
  throw describeOAuthLoginFailure(provider.id, error);
824
827
  });
825
828
  printSuccess(`Model provider login complete: ${provider.id}`);
829
+ await refreshProviderModels(modelRuntime, provider.id);
826
830
  await maybeSetRecommendedDefaultModel(settingsPath, authPath);
827
831
  return true;
828
832
  }
@@ -880,11 +884,10 @@ export async function setDefaultModelSpec(settingsPath, authPath, spec) {
880
884
  throw new Error(await describeUnavailableModelSpec(authPath, spec));
881
885
  }
882
886
  const [provider, ...rest] = resolvedSpec.split("/");
883
- const modelId = rest.join("/");
884
- const settings = readJson(settingsPath);
885
- settings.defaultProvider = provider;
886
- settings.defaultModel = modelId;
887
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
887
+ // Pi's SettingsManager merges only the changed keys under its lock.
888
+ const settings = SettingsManager.create(process.cwd(), dirname(settingsPath));
889
+ settings.setDefaultModelAndProvider(provider, rest.join("/"));
890
+ await settings.flush();
888
891
  printSuccess(`Research default model set to ${resolvedSpec}`);
889
892
  }
890
893
  export async function selectDefaultModel(settingsPath, authPath) {
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,8 +1,8 @@
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";
5
- import { getPostHogChildEnv } from "../telemetry/posthog.js";
4
+ import { MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable, } from "../system/executables.js";
5
+ import { getPostHogChildEnv, isTelemetryDisabled } 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).
8
8
  export const BUNDLED_PI_PACKAGES = ["pi-subagents", "pi-web-access", "pi-docparser", "pi-btw"];
@@ -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]
@@ -156,8 +156,9 @@ export function buildPiEnv(options, executables) {
156
156
  FEYNMAN_BIN_PATH: getFeynmanCliBinPath(options.appRoot),
157
157
  PI_CODING_AGENT_DIR: options.feynmanAgentDir,
158
158
  PANDOC_PATH: pandocPath,
159
- PI_HARDWARE_CURSOR: process.env.PI_HARDWARE_CURSOR ?? "1",
160
159
  PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1",
160
+ // A Feynman telemetry opt-out also turns off Pi's install telemetry (docs/environment-variables.md).
161
+ ...(isTelemetryDisabled() && process.env.PI_TELEMETRY === undefined ? { PI_TELEMETRY: "0" } : {}),
161
162
  MERMAID_CLI_PATH: mermaidPath,
162
163
  PUPPETEER_EXECUTABLE_PATH: browserPath,
163
164
  ...getPostHogChildEnv(),
@@ -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
  }
@@ -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.40",
3
+ "version": "0.5.42",
4
4
  "description": "The open source AI research agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -79,16 +79,20 @@
79
79
  "dependencies": {
80
80
  "@clack/prompts": "^1.8.1",
81
81
  "@companion-ai/alpha-hub": "0.1.7",
82
+ "@earendil-works/pi-codemode": "1.2.0",
83
+ "@earendil-works/pi-mcp": "1.2.0",
84
+ "@earendil-works/pi-telemetry": "1.2.0",
82
85
  "fast-xml-parser": "5.11.1",
83
86
  "pi-btw": "0.8.0",
84
87
  "pi-docparser": "4.0.0",
85
- "pi-subagents": "0.77.0",
88
+ "pi-subagents": "0.78.0",
86
89
  "pi-web-access": "0.39.0",
87
90
  "posthog-node": "^5.53.0"
88
91
  },
89
92
  "peerDependencies": {
90
- "@earendil-works/pi-ai": "1.1.0",
91
- "@earendil-works/pi-coding-agent": "1.1.0",
93
+ "@earendil-works/pi-agent-core": "1.2.0",
94
+ "@earendil-works/pi-ai": "1.2.0",
95
+ "@earendil-works/pi-coding-agent": "1.2.0",
92
96
  "@earendil-works/pi-tui": "*",
93
97
  "typebox": "*"
94
98
  },