@companion-ai/feynman 0.5.41 → 0.5.43

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,26 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.43 - 2026-10-13
10
+
11
+ - **Feynman reads and writes Pi's configuration only through Pi.** Settings now go through Pi's settings manager, which locks the file and changes only the keys Feynman sets, so a change Pi or another Feynman window makes at the same moment is no longer overwritten. Model checks, local-model detection and `feynman doctor` read your models through Pi, so comments in `models.json`, a `baseUrl` set on one model, and a key saved with `feynman model login` count the same way they do when Feynman runs.
12
+ - `feynman doctor` no longer warns about a custom provider whose key you saved with `feynman model login` rather than writing it into `models.json`.
13
+
14
+ ## v0.5.42 - 2026-10-13
15
+
16
+ - **Pi 1.2.0 and pi-subagents 0.78.0.**
17
+ - With an OpenRouter key, `/model` lists only the models that key can use, with their real context sizes and prices.
18
+ - "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.
19
+ - `feynman -p` warns and exits 1 when the answer was cut off at the model's output limit.
20
+ - On Windows, reading or writing saved logins no longer fails with `EPERM` while an old lock is cleaned up.
21
+ - Pressing `Esc` keeps a research run stopped; a subagent finishing a moment later no longer restarts it.
22
+ - Reports are no longer overwritten when a subagent run is resumed, and a timed-out subagent's partial output reaches the lead agent.
23
+ - The `workflowScript contains dynamic child launches` warning no longer prints on every parallel run.
24
+ - **`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.
25
+ - **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.
26
+ - Feynman now honors Pi's own `sessionDir` setting and `PI_CODING_AGENT_SESSION_DIR` when you don't pass `--session-dir`.
27
+ - `feynman model set` and the default-model check use Pi's settings API, so they match what Pi launches with (including trusted project settings).
28
+
9
29
  ## v0.5.41 - 2026-10-12
10
30
 
11
31
  - **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.
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 { resolveExplicitModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
22
+ import { resolveExplicitModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, } 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";
@@ -254,7 +255,8 @@ async function handleUpdateCommand(piOptions, feynmanVersion, source) {
254
255
  }
255
256
  }
256
257
  async function handlePackagesCommand(subcommand, args, piOptions) {
257
- const configuredSources = new Set((readJson(resolve(piOptions.feynmanAgentDir, "settings.json")).packages ?? [])
258
+ const configuredSources = new Set(SettingsManager.create(process.cwd(), piOptions.feynmanAgentDir, { projectTrusted: false })
259
+ .getPackages()
258
260
  .map((entry) => (typeof entry === "string" ? entry : entry.source)));
259
261
  if (!subcommand || subcommand === "list") {
260
262
  printPanel("Feynman Packages", [
@@ -516,7 +518,12 @@ async function runMain(input) {
516
518
  throw new Error("Unable to determine the installed Feynman version.");
517
519
  }
518
520
  const workingDir = resolve(values.cwd ?? process.cwd());
519
- const sessionDir = resolve(values["session-dir"] ?? getDefaultSessionDir(feynmanHome));
521
+ // Pi's order: --session-dir, then PI_CODING_AGENT_SESSION_DIR, then the sessionDir
522
+ // setting; Feynman's own default comes last.
523
+ const sessionDir = resolve(workingDir, values["session-dir"] ??
524
+ process.env.PI_CODING_AGENT_SESSION_DIR ??
525
+ SettingsManager.create(workingDir, feynmanAgentDir).getSessionDir() ??
526
+ getDefaultSessionDir(feynmanHome));
520
527
  const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
521
528
  const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
522
529
  const { defaultThinkingLevel, launchThinkingLevel } = resolveThinkingConfig(values.thinking ?? process.env.FEYNMAN_THINKING);
@@ -705,13 +712,12 @@ async function runMain(input) {
705
712
  if (command && workflowCommandNames.has(command) && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
706
713
  const effectiveSpec = explicitModelSpec ?? getCurrentModelSpec(feynmanSettingsPath);
707
714
  const providerId = effectiveSpec?.split("/")[0] ?? "";
708
- if (effectiveSpec && isLocalModelProvider(feynmanAuthPath, providerId)) {
715
+ if (effectiveSpec && await isLocalModelProvider(feynmanAuthPath, providerId)) {
709
716
  preLaunchNotice = buildLocalModelWorkflowNotice(effectiveSpec, command);
710
717
  }
711
718
  }
712
719
  if (process.platform === "win32" && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
713
- const shellPath = readJson(feynmanSettingsPath).shellPath;
714
- if (typeof shellPath !== "string" && !findWindowsBash()) {
720
+ if (!SettingsManager.create(workingDir, feynmanAgentDir, { projectTrusted: false }).getShellPath() && !findWindowsBash()) {
715
721
  preLaunchNotice = [preLaunchNotice, WINDOWS_BASH_MISSING_NOTICE].filter(Boolean).join("\n");
716
722
  }
717
723
  }
@@ -1,15 +1,13 @@
1
1
  import { SettingsManager } from "@earendil-works/pi-coding-agent";
2
- import { readFileSync, writeFileSync } from "node:fs";
3
2
  import { exec as execCallback } from "node:child_process";
4
3
  import { dirname } from "node:path";
5
4
  import { promisify } from "node:util";
6
- import { readJson } from "../pi/settings.js";
7
5
  import { promptChoice, promptSecret, promptSelect, promptText, SetupCancelledError, validateApiKeyInput } from "../setup/prompts.js";
8
6
  import { openUrl } from "../system/open-url.js";
9
7
  import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
10
8
  import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
11
9
  import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
12
- import { createModelRegistry, createModelRuntime, getModelsJsonPath } from "./registry.js";
10
+ import { createModelRuntime, getModelsJsonPath } from "./registry.js";
13
11
  import { apiKeyReference, upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
14
12
  const exec = promisify(execCallback);
15
13
  async function collectModelStatus(settingsPath, authPath) {
@@ -54,19 +52,32 @@ async function selectOAuthProvider(authPath, action) {
54
52
  }
55
53
  return selection;
56
54
  }
57
- function resolveApiKeyProvider(input) {
55
+ // Endpoints Pi has no API-key sign-in for: Feynman writes their models.json entry.
56
+ const LOCAL_API_KEY_SETUPS = MODEL_API_KEY_PROVIDERS.filter((provider) => ["lm-studio", "litellm", "__custom__"].includes(provider.id));
57
+ // Every provider Pi can sign in to with an API key (its own list, labels and
58
+ // prompts), plus Feynman's local and custom endpoint setups.
59
+ export async function listApiKeyProviders(authPath) {
60
+ const envVars = new Map(MODEL_API_KEY_PROVIDERS.map((provider) => [provider.id, provider.envVar]));
61
+ const piProviders = (await createModelRuntime(authPath))
62
+ .getProviders()
63
+ .filter((provider) => provider.auth.apiKey)
64
+ .map((provider) => ({ id: provider.id, label: provider.name, envVar: envVars.get(provider.id) }))
65
+ .sort((left, right) => left.label.localeCompare(right.label));
66
+ return [...piProviders, ...LOCAL_API_KEY_SETUPS];
67
+ }
68
+ async function resolveApiKeyProvider(authPath, input) {
58
69
  const normalizedInput = normalizeProviderId(input);
59
70
  if (!normalizedInput) {
60
71
  return undefined;
61
72
  }
62
- return MODEL_API_KEY_PROVIDERS.find((provider) => provider.id === normalizedInput);
73
+ return (await listApiKeyProviders(authPath)).find((provider) => provider.id === normalizedInput);
63
74
  }
64
75
  export async function resolveModelProviderForCommand(authPath, input) {
65
76
  const oauthProvider = await resolveOAuthProvider(authPath, input);
66
77
  if (oauthProvider) {
67
78
  return { kind: "oauth", id: oauthProvider.id };
68
79
  }
69
- const apiKeyProvider = resolveApiKeyProvider(input);
80
+ const apiKeyProvider = await resolveApiKeyProvider(authPath, input);
70
81
  if (apiKeyProvider) {
71
82
  return { kind: "api-key", id: apiKeyProvider.id };
72
83
  }
@@ -84,14 +95,15 @@ function apiKeyProviderHint(provider) {
84
95
  }
85
96
  return provider.envVar ?? provider.id;
86
97
  }
87
- async function selectApiKeyProvider() {
88
- const options = MODEL_API_KEY_PROVIDERS.map((provider) => ({
98
+ async function selectApiKeyProvider(authPath) {
99
+ const providers = await listApiKeyProviders(authPath);
100
+ const options = providers.map((provider) => ({
89
101
  value: provider,
90
102
  label: provider.label,
91
103
  hint: apiKeyProviderHint(provider),
92
104
  }));
93
105
  options.push({ value: "cancel", label: "Cancel" });
94
- const defaultProvider = MODEL_API_KEY_PROVIDERS.find((provider) => provider.id === "openai") ?? MODEL_API_KEY_PROVIDERS[0];
106
+ const defaultProvider = providers.find((provider) => provider.id === "openai") ?? providers[0];
95
107
  const selection = await promptSelect("Choose an API-key provider:", options, defaultProvider);
96
108
  if (selection === "cancel") {
97
109
  return undefined;
@@ -127,22 +139,18 @@ function isLocalBaseUrl(baseUrl) {
127
139
  }
128
140
  // "llama.cpp" is the id of Pi's bundled llama.cpp provider.
129
141
  const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
130
- export function isLocalModelProvider(authPath, providerId) {
142
+ // Reads models.json through Pi, so its comments, BOM handling, and per-model
143
+ // baseUrl overrides count the same way they do at launch.
144
+ export async function isLocalModelProvider(authPath, providerId) {
131
145
  if (!providerId) {
132
146
  return false;
133
147
  }
134
148
  if (LOCAL_PROVIDER_IDS.has(providerId)) {
135
149
  return true;
136
150
  }
137
- try {
138
- const raw = readFileSync(getModelsJsonPath(authPath), "utf8");
139
- const parsed = JSON.parse(raw);
140
- const provider = parsed.providers?.[providerId];
141
- return typeof provider?.baseUrl === "string" && isLocalBaseUrl(provider.baseUrl);
142
- }
143
- catch {
144
- return false;
145
- }
151
+ const modelRuntime = await createModelRuntime(authPath);
152
+ return [modelRuntime.getProvider(providerId)?.baseUrl, ...modelRuntime.getModels(providerId).map((model) => model.baseUrl)]
153
+ .some((baseUrl) => typeof baseUrl === "string" && isLocalBaseUrl(baseUrl));
146
154
  }
147
155
  async function resolveApiKeyConfig(apiKeyConfig) {
148
156
  const trimmed = apiKeyConfig.trim();
@@ -381,8 +389,8 @@ async function promptLiteLlmProviderSetup() {
381
389
  };
382
390
  }
383
391
  async function verifyCustomProvider(setup, authPath) {
384
- const registry = await createModelRegistry(authPath);
385
- const modelsError = registry.getError();
392
+ const modelRuntime = await createModelRuntime(authPath);
393
+ const modelsError = modelRuntime.getError();
386
394
  if (modelsError) {
387
395
  printWarning("Verification: models.json failed to load.");
388
396
  for (const line of modelsError.split("\n")) {
@@ -390,19 +398,19 @@ async function verifyCustomProvider(setup, authPath) {
390
398
  }
391
399
  return;
392
400
  }
393
- const all = registry.getAll();
394
- const hasModel = setup.modelIds.some((id) => all.some((model) => model.provider === setup.providerId && model.id === id));
401
+ const all = modelRuntime.getModels(setup.providerId);
402
+ const hasModel = setup.modelIds.some((id) => all.some((model) => model.id === id));
395
403
  if (!hasModel) {
396
404
  printWarning("Verification: model registry does not contain the configured provider/model ids.");
397
405
  return;
398
406
  }
399
- const available = registry.getAvailable();
400
- const hasAvailable = setup.modelIds.some((id) => available.some((model) => model.provider === setup.providerId && model.id === id));
407
+ const available = await modelRuntime.getAvailable(setup.providerId);
408
+ const hasAvailable = setup.modelIds.some((id) => available.some((model) => model.id === id));
401
409
  if (!hasAvailable) {
402
410
  printWarning("Verification: provider is not considered authenticated/available.");
403
411
  return;
404
412
  }
405
- const apiKey = await registry.getApiKeyForProvider(setup.providerId);
413
+ const apiKey = (await modelRuntime.getAuth(setup.providerId).catch(() => undefined))?.auth.apiKey;
406
414
  if (!apiKey) {
407
415
  printWarning("Verification: API key could not be resolved (check env var name / !command).");
408
416
  return;
@@ -501,36 +509,6 @@ async function verifyCustomProvider(setup, authPath) {
501
509
  }
502
510
  printInfo("Verification: skipped network probe for this API mode.");
503
511
  }
504
- async function verifyBedrockCredentialChain() {
505
- const { defaultProvider } = await import("@aws-sdk/credential-provider-node");
506
- const credentials = await defaultProvider({})();
507
- if (!credentials?.accessKeyId || !credentials?.secretAccessKey) {
508
- throw new Error("AWS credential chain resolved without usable Bedrock credentials.");
509
- }
510
- }
511
- async function configureBedrockProvider(authPath) {
512
- printSection("AWS Credentials: Amazon Bedrock");
513
- printInfo("Feynman will verify the AWS SDK credential chain used by Pi's Bedrock provider.");
514
- printInfo("Supported sources include AWS_PROFILE, ~/.aws credentials/config, SSO, ECS/IRSA, and EC2 instance roles.");
515
- try {
516
- await verifyBedrockCredentialChain();
517
- const modelRuntime = await createModelRuntime(authPath);
518
- if (!(await modelRuntime.checkAuth("amazon-bedrock"))) {
519
- throw new Error("Pi could not resolve the verified AWS credential chain for Amazon Bedrock.");
520
- }
521
- printSuccess("Verified the AWS credential chain used by Amazon Bedrock.");
522
- printInfo("Use `feynman model list` to see available Bedrock models.");
523
- return true;
524
- }
525
- catch (error) {
526
- printWarning(`AWS credential verification failed: ${error instanceof Error ? error.message : String(error)}`);
527
- printInfo("Configure AWS credentials first, for example:");
528
- printInfo(" export AWS_PROFILE=default");
529
- printInfo(" # or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY");
530
- printInfo(" # or use an EC2/ECS/IRSA role with valid Bedrock access");
531
- return false;
532
- }
533
- }
534
512
  async function maybeSetRecommendedDefaultModel(settingsPath, authPath) {
535
513
  if (!settingsPath) {
536
514
  return;
@@ -546,7 +524,7 @@ async function maybeSetRecommendedDefaultModel(settingsPath, authPath) {
546
524
  }
547
525
  }
548
526
  async function configureApiKeyProvider(authPath, providerId) {
549
- const provider = providerId ? resolveApiKeyProvider(providerId) : await selectApiKeyProvider();
527
+ const provider = providerId ? await resolveApiKeyProvider(authPath, providerId) : await selectApiKeyProvider(authPath);
550
528
  if (!provider) {
551
529
  if (providerId) {
552
530
  throw new Error(`Unknown API-key model provider: ${providerId}`);
@@ -554,9 +532,6 @@ async function configureApiKeyProvider(authPath, providerId) {
554
532
  printInfo("API key setup cancelled.");
555
533
  return false;
556
534
  }
557
- if (provider.id === "amazon-bedrock") {
558
- return configureBedrockProvider(authPath);
559
- }
560
535
  if (provider.id === "lm-studio") {
561
536
  const setup = await promptLmStudioProviderSetup();
562
537
  if (!setup) {
@@ -627,22 +602,34 @@ async function configureApiKeyProvider(authPath, providerId) {
627
602
  if (provider.envVar) {
628
603
  printInfo(`Tip: to avoid writing secrets to disk, set ${provider.envVar} in your shell or .env.`);
629
604
  }
630
- const apiKey = await promptSecret("Paste API key (leave empty to use env var instead)", undefined, validateApiKeyInput);
631
- if (!apiKey) {
632
- if (provider.envVar) {
633
- printInfo(`Set ${provider.envVar} and rerun setup (or run \`feynman model list\`).`);
634
- }
635
- else {
636
- printInfo("No API key provided.");
637
- }
638
- return false;
639
- }
605
+ // Pi asks for whatever this provider needs (a key, or e.g. Cloudflare's account
606
+ // and gateway, Bedrock's profile or token). Esc cancels.
640
607
  const modelRuntime = await createModelRuntime(authPath);
641
- await modelRuntime.login(provider.id, "api_key", {
642
- prompt: async () => apiKey,
643
- notify: () => undefined,
608
+ const interaction = terminalAuthInteraction(provider.label, new AbortController().signal);
609
+ const signedIn = await modelRuntime
610
+ .login(provider.id, "api_key", {
611
+ ...interaction,
612
+ prompt: async (prompt) => {
613
+ if (prompt.type !== "secret")
614
+ return interaction.prompt(prompt);
615
+ const secret = await promptSecret(prompt.message, prompt.signal, validateApiKeyInput);
616
+ // An empty key would be saved as a credential that fails every request.
617
+ if (!secret)
618
+ throw new SetupCancelledError("no API key entered");
619
+ return secret;
620
+ },
621
+ })
622
+ .then(() => true)
623
+ .catch((error) => {
624
+ if (!(error instanceof SetupCancelledError))
625
+ throw error;
626
+ if (provider.envVar)
627
+ printInfo(`Or set ${provider.envVar} and run feynman again.`);
628
+ return false;
644
629
  });
645
- printSuccess(`Saved API key for ${provider.id} in auth storage.`);
630
+ if (!signedIn)
631
+ return false;
632
+ printSuccess(`Saved ${provider.label} credentials in auth storage.`);
646
633
  const baseUrl = await promptText("Base URL override (optional, include /v1 for OpenAI-compatible endpoints)", "");
647
634
  if (baseUrl) {
648
635
  const modelsJsonPath = getModelsJsonPath(authPath);
@@ -683,12 +670,12 @@ async function resolveAvailableModelSpec(authPath, input) {
683
670
  }
684
671
  return undefined;
685
672
  }
673
+ // The default model Pi will launch with, read through Pi's SettingsManager.
686
674
  export function getCurrentModelSpec(settingsPath) {
687
- const settings = readJson(settingsPath);
688
- if (typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string") {
689
- return `${settings.defaultProvider}/${settings.defaultModel}`;
690
- }
691
- return undefined;
675
+ const settings = SettingsManager.create(process.cwd(), dirname(settingsPath));
676
+ const provider = settings.getDefaultProvider();
677
+ const model = settings.getDefaultModel();
678
+ return provider && model ? `${provider}/${model}` : undefined;
692
679
  }
693
680
  export async function printModelList(settingsPath, authPath) {
694
681
  const status = await collectModelStatus(settingsPath, authPath);
@@ -758,32 +745,9 @@ export function describeOAuthLoginFailure(providerId, error) {
758
745
  async function refreshProviderModels(modelRuntime, providerId) {
759
746
  await modelRuntime.refresh({ providers: [providerId], allowNetwork: true, signal: AbortSignal.timeout(15_000) }).catch(() => undefined);
760
747
  }
761
- export async function loginModelProvider(authPath, providerId, settingsPath) {
762
- if (providerId) {
763
- const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
764
- if (!resolvedProvider) {
765
- throw new Error(`Unknown model provider: ${providerId}`);
766
- }
767
- if (resolvedProvider.kind === "api-key") {
768
- const configured = await configureApiKeyProvider(authPath, resolvedProvider.id);
769
- if (configured) {
770
- await maybeSetRecommendedDefaultModel(settingsPath, authPath);
771
- }
772
- return configured;
773
- }
774
- }
775
- const provider = providerId ? await resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
776
- if (!provider) {
777
- if (providerId) {
778
- throw new Error(`Unknown model provider: ${providerId}`);
779
- }
780
- printInfo("Login cancelled.");
781
- return false;
782
- }
783
- const modelRuntime = await createModelRuntime(authPath);
784
- const abortController = new AbortController();
785
- const deviceId = await getOrCreateDeviceId(authPath);
786
- await modelRuntime.login(provider.id, "oauth", {
748
+ // Runs Pi's own login prompts (selects, secrets, text, browser URLs) in the terminal.
749
+ function terminalAuthInteraction(providerLabel, signal) {
750
+ return {
787
751
  prompt: async (prompt) => {
788
752
  if (prompt.type === "select") {
789
753
  return promptSelect(prompt.message, prompt.options.map((option) => ({
@@ -802,7 +766,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
802
766
  },
803
767
  notify: (event) => {
804
768
  if (event.type === "auth_url") {
805
- printSection(`Login: ${provider.name ?? provider.id}`);
769
+ printSection(`Login: ${providerLabel}`);
806
770
  if (openUrl(event.url)) {
807
771
  printInfo("Opened the login URL in your browser.");
808
772
  }
@@ -816,7 +780,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
816
780
  return;
817
781
  }
818
782
  if (event.type === "device_code") {
819
- printSection(`Login: ${provider.name ?? provider.id}`);
783
+ printSection(`Login: ${providerLabel}`);
820
784
  printInfo(`Visit ${event.verificationUri} and enter code: ${event.userCode}`);
821
785
  openUrl(event.verificationUri);
822
786
  return;
@@ -825,8 +789,35 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
825
789
  printInfo(event.message);
826
790
  }
827
791
  },
828
- signal: abortController.signal,
829
- }, { getDeviceId: () => deviceId }).catch((error) => {
792
+ signal,
793
+ };
794
+ }
795
+ export async function loginModelProvider(authPath, providerId, settingsPath) {
796
+ if (providerId) {
797
+ const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
798
+ if (!resolvedProvider) {
799
+ throw new Error(`Unknown model provider: ${providerId}`);
800
+ }
801
+ if (resolvedProvider.kind === "api-key") {
802
+ const configured = await configureApiKeyProvider(authPath, resolvedProvider.id);
803
+ if (configured) {
804
+ await maybeSetRecommendedDefaultModel(settingsPath, authPath);
805
+ }
806
+ return configured;
807
+ }
808
+ }
809
+ const provider = providerId ? await resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
810
+ if (!provider) {
811
+ if (providerId) {
812
+ throw new Error(`Unknown model provider: ${providerId}`);
813
+ }
814
+ printInfo("Login cancelled.");
815
+ return false;
816
+ }
817
+ const modelRuntime = await createModelRuntime(authPath);
818
+ const abortController = new AbortController();
819
+ const deviceId = await getOrCreateDeviceId(authPath);
820
+ await modelRuntime.login(provider.id, "oauth", terminalAuthInteraction(provider.name ?? provider.id, abortController.signal), { getDeviceId: () => deviceId }).catch((error) => {
830
821
  throw describeOAuthLoginFailure(provider.id, error);
831
822
  });
832
823
  printSuccess(`Model provider login complete: ${provider.id}`);
@@ -869,7 +860,7 @@ async function describeUnavailableModelSpec(authPath, spec) {
869
860
  if (isProClassModelSpec(knownSpec)) {
870
861
  return `Pro-class model disabled: ${knownSpec}. Choose an approved research model.`;
871
862
  }
872
- if (isLocalModelProvider(authPath, known.provider)) {
863
+ if (await isLocalModelProvider(authPath, known.provider)) {
873
864
  return `${knownSpec} has no API key. Pi only lists a local provider when it has one: add "apiKey": "local" to the ${known.provider} provider in ${getModelsJsonPath(authPath)}.`;
874
865
  }
875
866
  return `${knownSpec} has no credentials. Run \`feynman model login ${known.provider}\` first.`;
@@ -888,11 +879,10 @@ export async function setDefaultModelSpec(settingsPath, authPath, spec) {
888
879
  throw new Error(await describeUnavailableModelSpec(authPath, spec));
889
880
  }
890
881
  const [provider, ...rest] = resolvedSpec.split("/");
891
- const modelId = rest.join("/");
892
- const settings = readJson(settingsPath);
893
- settings.defaultProvider = provider;
894
- settings.defaultModel = modelId;
895
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
882
+ // Pi's SettingsManager merges only the changed keys under its lock.
883
+ const settings = SettingsManager.create(process.cwd(), dirname(settingsPath));
884
+ settings.setDefaultModelAndProvider(provider, rest.join("/"));
885
+ await settings.flush();
896
886
  printSuccess(`Research default model set to ${resolvedSpec}`);
897
887
  }
898
888
  export async function selectDefaultModel(settingsPath, authPath) {
@@ -1,5 +1,5 @@
1
1
  import { dirname, resolve } from "node:path";
2
- import { ModelRegistry, ModelRuntime } from "@earendil-works/pi-coding-agent";
2
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
3
3
  export function getModelsJsonPath(authPath) {
4
4
  return resolve(dirname(authPath), "models.json");
5
5
  }
@@ -10,6 +10,3 @@ export async function createModelRuntime(authPath) {
10
10
  allowModelNetwork: false,
11
11
  });
12
12
  }
13
- export async function createModelRegistry(authPath) {
14
- return new ModelRegistry(await createModelRuntime(authPath));
15
- }
@@ -11,6 +11,7 @@ export const FEYNMAN_SERVICE_TIERS = [
11
11
  const SERVICE_TIER_SET = new Set(FEYNMAN_SERVICE_TIERS);
12
12
  const OPENAI_SERVICE_TIERS = new Set(["auto", "default", "flex", "priority"]);
13
13
  const ANTHROPIC_SERVICE_TIERS = new Set(["auto", "standard_only"]);
14
+ // serviceTier is Feynman's key; Pi's SettingsManager has no getter or setter for it, so it is read and written directly.
14
15
  function readSettings(settingsPath) {
15
16
  try {
16
17
  return readJsonFile(settingsPath);
@@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
2
2
  import { createRequire } from "node:module";
3
3
  import { delimiter, dirname, resolve, win32 } from "node:path";
4
4
  import { MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable, } from "../system/executables.js";
5
- import { getPostHogChildEnv } from "../telemetry/posthog.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"];
@@ -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,18 +1,13 @@
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";
4
- import { parseJsonText, readJsonFile } from "../config/json-file.js";
3
+ import { resolveCliModel, SettingsManager } from "@earendil-works/pi-coding-agent";
4
+ import { parseJsonText } from "../config/json-file.js";
5
5
  import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
6
6
  import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
7
7
  import { migrateBareEnvApiKeys } from "../model/models-json.js";
8
8
  import { getModelsJsonPath } from "../model/registry.js";
9
9
  // Written by Feynman <= 0.4.0 next to a subagent extension path it managed.
10
10
  const LEGACY_RESEARCHER_EXTENSION_MARKER = "_feynmanResearchToolsExtension";
11
- function findModel(modelLookup, provider, id) {
12
- return "find" in modelLookup
13
- ? modelLookup.find(provider, id)
14
- : modelLookup.getModel(provider, id);
15
- }
16
11
  export function parseModelSpec(spec, modelLookup) {
17
12
  const trimmed = spec.trim();
18
13
  for (const separator of ["/", ":"]) {
@@ -22,7 +17,7 @@ export function parseModelSpec(spec, modelLookup) {
22
17
  }
23
18
  const provider = trimmed.slice(0, separatorIndex);
24
19
  const id = trimmed.slice(separatorIndex + 1);
25
- const model = findModel(modelLookup, provider, id);
20
+ const model = modelLookup.getModel(provider, id);
26
21
  if (model) {
27
22
  return model;
28
23
  }
@@ -72,20 +67,6 @@ export function normalizeThinkingLevel(value) {
72
67
  }
73
68
  return undefined;
74
69
  }
75
- export function readJson(path) {
76
- if (!existsSync(path)) {
77
- return {};
78
- }
79
- try {
80
- return readJsonFile(path);
81
- }
82
- catch (error) {
83
- if (process.env.FEYNMAN_DEBUG === "1") {
84
- process.stderr.write(`[feynman] warning: failed to parse ${path}, treating as empty (${error instanceof Error ? error.message : "unknown error"})\n`);
85
- }
86
- return {};
87
- }
88
- }
89
70
  function isRecord(value) {
90
71
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
91
72
  }
@@ -208,19 +189,25 @@ export function feynmanSubagentExtensions(appRoot) {
208
189
  function isFeynmanSubagentExtension(path) {
209
190
  return typeof path === "string" && (/[\\/]extensions[\\/]research-tools\.ts$/.test(path) || /[\\/]pi-web-access$/.test(path));
210
191
  }
211
- export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, authPath) {
212
- // Validate before model discovery or either settings write. Invalid custom
213
- // configuration must not silently turn into an empty/default configuration.
214
- const subagentDefaults = prepareSubagentDefaults(settingsPath);
215
- const existing = existsSync(settingsPath) ? readConfigObject(settingsPath, "Feynman settings") : undefined;
216
- const settings = existing ? { ...existing.value } : {};
217
- const defaults = readConfigObject(bundledSettingsPath, "bundled Feynman settings").value;
218
- migrateBareEnvApiKeys(getModelsJsonPath(authPath));
219
- for (const [key, value] of Object.entries({ ...defaults, defaultThinkingLevel })) {
220
- if (settings[key] === undefined)
192
+ // Bundled defaults (.feynman/settings.json) and the thinking level, seeded
193
+ // through Pi's SettingsManager setters.
194
+ const PI_SETTING_SETTERS = {
195
+ theme: (settings, value) => settings.setTheme(value),
196
+ quietStartup: (settings, value) => settings.setQuietStartup(value),
197
+ collapseChangelog: (settings, value) => settings.setCollapseChangelog(value),
198
+ editorPaddingX: (settings, value) => settings.setEditorPaddingX(value),
199
+ showHardwareCursor: (settings, value) => settings.setShowHardwareCursor(value),
200
+ defaultThinkingLevel: (settings, value) => settings.setDefaultThinkingLevel(value),
201
+ };
202
+ // SettingsManager has no setter for `subagents` (pi-subagents' key), for deleting `retry`, or for a
203
+ // bundled default outside PI_SETTING_SETTERS, so those are edited in the file directly.
204
+ function updateSettingsWithoutPiSetter(settingsPath, appRoot, defaults) {
205
+ const existing = existsSync(settingsPath) ? readConfigObject(settingsPath, "Feynman settings").value : {};
206
+ const settings = structuredClone(existing);
207
+ for (const [key, value] of Object.entries(defaults)) {
208
+ if (!(key in PI_SETTING_SETTERS) && settings[key] === undefined)
221
209
  settings[key] = value;
222
210
  }
223
- settings.packages = reconcileFeynmanPackages(settings.packages, appRoot);
224
211
  removeLegacyResearcherExtension(settings);
225
212
  // 0.5.0-0.5.5 seeded a 6-retry, 5s backoff that left a dead connection
226
213
  // retrying silently for over five minutes; drop that exact value for Pi's default.
@@ -238,11 +225,35 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
238
225
  settings.subagents.defaultSubagentOnlyExtensions = feynmanSubagentExtensions(appRoot);
239
226
  }
240
227
  }
241
- if (!settings.defaultProvider || !settings.defaultModel) {
228
+ if (JSON.stringify(settings) === JSON.stringify(existing))
229
+ return;
230
+ mkdirSync(dirname(settingsPath), { recursive: true });
231
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
232
+ }
233
+ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, authPath) {
234
+ // Validate before model discovery or either settings write. Invalid custom
235
+ // configuration must not silently turn into an empty/default configuration.
236
+ const subagentDefaults = prepareSubagentDefaults(settingsPath);
237
+ // Feynman's agent settings are global; an untrusted project's .pi/settings.json stays out.
238
+ const settings = SettingsManager.create(process.cwd(), dirname(settingsPath), { projectTrusted: false });
239
+ const loadError = settings.drainErrors().find((error) => error.scope === "global")?.error;
240
+ const current = settings.getGlobalSettings();
241
+ if (loadError || !isRecord(current)) {
242
+ throw new Error(`Invalid Feynman settings at ${settingsPath}: ${loadError?.message ?? "expected a JSON object"}. Fix the JSON or delete the file. The file was not changed.`);
243
+ }
244
+ const defaults = readConfigObject(bundledSettingsPath, "bundled Feynman settings").value;
245
+ migrateBareEnvApiKeys(getModelsJsonPath(authPath));
246
+ for (const [key, value] of Object.entries({ ...defaults, defaultThinkingLevel })) {
247
+ if (current[key] === undefined)
248
+ PI_SETTING_SETTERS[key]?.(settings, value);
249
+ }
250
+ const packages = reconcileFeynmanPackages(current.packages, appRoot);
251
+ if (JSON.stringify(packages) !== JSON.stringify(current.packages))
252
+ settings.setPackages(packages);
253
+ if (!settings.getDefaultProvider() || !settings.getDefaultModel()) {
242
254
  const preferredModel = choosePreferredModelRecord(await getAvailableModelRecords(authPath));
243
255
  if (preferredModel) {
244
- settings.defaultProvider = preferredModel.provider;
245
- settings.defaultModel = preferredModel.id;
256
+ settings.setDefaultModelAndProvider(preferredModel.provider, preferredModel.id);
246
257
  }
247
258
  }
248
259
  // If another process wrote the subagent config while models were being
@@ -260,9 +271,11 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
260
271
  throw error;
261
272
  }
262
273
  }
263
- const content = JSON.stringify(settings, null, 2) + "\n";
264
- if (content === existing?.source)
265
- return;
266
- mkdirSync(dirname(settingsPath), { recursive: true });
267
- writeFileSync(settingsPath, content, "utf8");
274
+ // Read the file again after model discovery so edits made meanwhile survive;
275
+ // SettingsManager likewise merges only the keys it changed, under Pi's lock.
276
+ updateSettingsWithoutPiSetter(settingsPath, appRoot, defaults);
277
+ await settings.flush();
278
+ const [writeError] = settings.drainErrors();
279
+ if (writeError)
280
+ throw writeError.error;
268
281
  }
@@ -1,43 +1,26 @@
1
1
  import { getUserName as getAlphaUserName, isLoggedIn as isAlphaLoggedIn } from "@companion-ai/alpha-hub/lib";
2
2
  import { getValidToken as getValidAlphaToken } from "@companion-ai/alpha-hub/lib/auth";
3
- import { readFileSync } from "node:fs";
3
+ import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
4
+ import { dirname } from "node:path";
4
5
  import { verifyAlphaAuthStatus } from "../alpha-auth-status.js";
5
6
  import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
6
7
  import { PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable } from "../system/executables.js";
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";
10
10
  import { printInfo, printPanel, printSection } from "../ui/terminal.js";
11
11
  import { getCurrentModelSpec } from "../model/commands.js";
12
12
  import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js";
13
- import { createModelRegistry, getModelsJsonPath } from "../model/registry.js";
13
+ import { createModelRuntime, getModelsJsonPath } from "../model/registry.js";
14
14
  import { getConfiguredServiceTier } from "../model/service-tier.js";
15
- function findProvidersMissingApiKey(modelsJsonPath) {
16
- try {
17
- const raw = readFileSync(modelsJsonPath, "utf8").trim();
18
- if (!raw)
19
- return [];
20
- const parsed = JSON.parse(raw);
21
- const providers = parsed?.providers;
22
- if (!providers || typeof providers !== "object")
23
- return [];
24
- const missing = [];
25
- for (const [providerId, config] of Object.entries(providers)) {
26
- if (!config || typeof config !== "object")
27
- continue;
28
- const models = config.models;
29
- if (!Array.isArray(models) || models.length === 0)
30
- continue;
31
- const apiKey = config.apiKey;
32
- if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
33
- missing.push(providerId);
34
- }
35
- }
36
- return missing;
37
- }
38
- catch {
39
- return [];
40
- }
15
+ // Custom providers (in models.json, not Pi's built-in catalog) that list models
16
+ // but have no credential Pi can resolve, so none of their models are available.
17
+ async function findProvidersMissingApiKey(modelRuntime, authPath) {
18
+ const builtIn = await ModelRuntime.create({ authPath, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
19
+ return modelRuntime
20
+ .getProviders()
21
+ .filter((provider) => !builtIn.getProvider(provider.id) && modelRuntime.getModels(provider.id).length > 0)
22
+ .filter((provider) => !modelRuntime.getProviderAuthStatus(provider.id).configured)
23
+ .map((provider) => provider.id);
41
24
  }
42
25
  export async function collectStatusSnapshot(options) {
43
26
  const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
@@ -100,8 +83,8 @@ export async function runStatus(options) {
100
83
  }
101
84
  }
102
85
  export async function runDoctor(options) {
103
- const settings = readJson(options.settingsPath);
104
- const modelRegistry = await createModelRegistry(options.authPath);
86
+ const settings = SettingsManager.create(options.workingDir, dirname(options.settingsPath), { projectTrusted: false });
87
+ const modelRuntime = await createModelRuntime(options.authPath);
105
88
  const supportedModels = await getSupportedModelRecords(options.authPath);
106
89
  const modelStatus = await collectStatusSnapshot(options);
107
90
  const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
@@ -135,9 +118,9 @@ export async function runDoctor(options) {
135
118
  .join(", ");
136
119
  console.log(` authenticated sample: ${sample}`);
137
120
  }
138
- console.log(`default model: ${typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string"
139
- ? `${settings.defaultProvider}/${settings.defaultModel}`
140
- : "not set"}`);
121
+ const defaultProvider = settings.getDefaultProvider();
122
+ const defaultModel = settings.getDefaultModel();
123
+ console.log(`default model: ${defaultProvider && defaultModel ? `${defaultProvider}/${defaultModel}` : "not set"}`);
141
124
  console.log(`default model valid: ${modelStatus.modelValid ? "yes" : "no"}`);
142
125
  console.log(`authenticated providers: ${modelStatus.authenticatedProviderCount}`);
143
126
  console.log(`authenticated models: ${modelStatus.authenticatedModelCount}`);
@@ -146,7 +129,7 @@ export async function runDoctor(options) {
146
129
  if (modelStatus.recommendedModelReason) {
147
130
  console.log(` why: ${modelStatus.recommendedModelReason}`);
148
131
  }
149
- const modelsError = modelRegistry.getError();
132
+ const modelsError = modelRuntime.getError();
150
133
  if (modelsError) {
151
134
  console.log("models.json: error");
152
135
  for (const line of modelsError.split("\n")) {
@@ -156,23 +139,24 @@ export async function runDoctor(options) {
156
139
  else {
157
140
  const modelsJsonPath = getModelsJsonPath(options.authPath);
158
141
  console.log(`models.json: ${modelsJsonPath}`);
159
- const missingApiKeyProviders = findProvidersMissingApiKey(modelsJsonPath);
142
+ const missingApiKeyProviders = await findProvidersMissingApiKey(modelRuntime, options.authPath);
160
143
  if (missingApiKeyProviders.length > 0) {
161
144
  console.log(` warning: provider(s) missing apiKey: ${missingApiKeyProviders.join(", ")}`);
162
- console.log(" note: custom providers with a models[] list need apiKey in models.json to be available.");
145
+ console.log(" note: custom providers with a models[] list need an apiKey in models.json or a stored key to be available.");
163
146
  }
164
147
  }
165
148
  console.log(`pandoc: ${pandocPath ?? "missing"}`);
166
149
  if (process.platform === "win32") {
167
- const shellPath = settings.shellPath;
168
- console.log(`bash: ${typeof shellPath === "string" ? `${shellPath} (shellPath)` : findWindowsBash()?.path ?? `missing. ${WINDOWS_BASH_MISSING_NOTICE}`}`);
150
+ const shellPath = settings.getShellPath();
151
+ console.log(`bash: ${shellPath ? `${shellPath} (shellPath)` : findWindowsBash()?.path ?? `missing. ${WINDOWS_BASH_MISSING_NOTICE}`}`);
169
152
  }
170
153
  console.log(`browser preview runtime: ${browserPath ?? "missing"}`);
171
154
  for (const line of formatPiWebAccessDoctorLines()) {
172
155
  console.log(line);
173
156
  }
174
- console.log(`quiet startup: ${settings.quietStartup === "header" ? "header only" : settings.quietStartup === true ? "enabled" : "disabled"}`);
175
- console.log(`theme: ${typeof settings.theme === "string" ? settings.theme : "not set"}`);
157
+ const quietStartup = settings.getQuietStartup();
158
+ console.log(`quiet startup: ${quietStartup === "header" ? "header only" : quietStartup === true ? "enabled" : "disabled"}`);
159
+ console.log(`theme: ${settings.getThemeSetting() ?? "not set"}`);
176
160
  if (missingPiBits.length > 0) {
177
161
  console.log("pi runtime: missing files");
178
162
  for (const entry of missingPiBits) {
@@ -34,6 +34,7 @@ function normalizeServiceTier(value: string | undefined): FeynmanServiceTier | u
34
34
  return SERVICE_TIER_SET.has(normalized) ? (normalized as FeynmanServiceTier) : undefined;
35
35
  }
36
36
 
37
+ // serviceTier is Feynman's key; Pi's SettingsManager has no getter or setter for it, so it is read and written directly.
37
38
  function getConfiguredServiceTier(settingsPath: string): FeynmanServiceTier | undefined {
38
39
  try {
39
40
  const parsed = JSON.parse(readFileSync(settingsPath, "utf8")) as { serviceTier?: string };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.41",
3
+ "version": "0.5.43",
4
4
  "description": "The open source AI research agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -79,21 +79,20 @@
79
79
  "dependencies": {
80
80
  "@clack/prompts": "^1.8.1",
81
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
+ "@earendil-works/pi-codemode": "1.2.0",
83
+ "@earendil-works/pi-mcp": "1.2.0",
84
+ "@earendil-works/pi-telemetry": "1.2.0",
86
85
  "fast-xml-parser": "5.11.1",
87
86
  "pi-btw": "0.8.0",
88
87
  "pi-docparser": "4.0.0",
89
- "pi-subagents": "0.77.0",
88
+ "pi-subagents": "0.78.0",
90
89
  "pi-web-access": "0.39.0",
91
90
  "posthog-node": "^5.53.0"
92
91
  },
93
92
  "peerDependencies": {
94
- "@earendil-works/pi-agent-core": "1.1.0",
95
- "@earendil-works/pi-ai": "1.1.0",
96
- "@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",
97
96
  "@earendil-works/pi-tui": "*",
98
97
  "typebox": "*"
99
98
  },