@companion-ai/feynman 0.5.42 → 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.
package/RELEASES.md CHANGED
@@ -6,6 +6,11 @@ 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
+
9
14
  ## v0.5.42 - 2026-10-13
10
15
 
11
16
  - **Pi 1.2.0 and pi-subagents 0.78.0.**
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";
@@ -255,7 +255,8 @@ async function handleUpdateCommand(piOptions, feynmanVersion, source) {
255
255
  }
256
256
  }
257
257
  async function handlePackagesCommand(subcommand, args, piOptions) {
258
- 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()
259
260
  .map((entry) => (typeof entry === "string" ? entry : entry.source)));
260
261
  if (!subcommand || subcommand === "list") {
261
262
  printPanel("Feynman Packages", [
@@ -711,13 +712,12 @@ async function runMain(input) {
711
712
  if (command && workflowCommandNames.has(command) && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
712
713
  const effectiveSpec = explicitModelSpec ?? getCurrentModelSpec(feynmanSettingsPath);
713
714
  const providerId = effectiveSpec?.split("/")[0] ?? "";
714
- if (effectiveSpec && isLocalModelProvider(feynmanAuthPath, providerId)) {
715
+ if (effectiveSpec && await isLocalModelProvider(feynmanAuthPath, providerId)) {
715
716
  preLaunchNotice = buildLocalModelWorkflowNotice(effectiveSpec, command);
716
717
  }
717
718
  }
718
719
  if (process.platform === "win32" && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
719
- const shellPath = readJson(feynmanSettingsPath).shellPath;
720
- if (typeof shellPath !== "string" && !findWindowsBash()) {
720
+ if (!SettingsManager.create(workingDir, feynmanAgentDir, { projectTrusted: false }).getShellPath() && !findWindowsBash()) {
721
721
  preLaunchNotice = [preLaunchNotice, WINDOWS_BASH_MISSING_NOTICE].filter(Boolean).join("\n");
722
722
  }
723
723
  }
@@ -1,5 +1,4 @@
1
1
  import { SettingsManager } from "@earendil-works/pi-coding-agent";
2
- import { readFileSync } 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";
@@ -8,7 +7,7 @@ import { openUrl } from "../system/open-url.js";
8
7
  import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
9
8
  import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
10
9
  import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
11
- import { createModelRegistry, createModelRuntime, getModelsJsonPath } from "./registry.js";
10
+ import { createModelRuntime, getModelsJsonPath } from "./registry.js";
12
11
  import { apiKeyReference, upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
13
12
  const exec = promisify(execCallback);
14
13
  async function collectModelStatus(settingsPath, authPath) {
@@ -140,22 +139,18 @@ function isLocalBaseUrl(baseUrl) {
140
139
  }
141
140
  // "llama.cpp" is the id of Pi's bundled llama.cpp provider.
142
141
  const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
143
- 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) {
144
145
  if (!providerId) {
145
146
  return false;
146
147
  }
147
148
  if (LOCAL_PROVIDER_IDS.has(providerId)) {
148
149
  return true;
149
150
  }
150
- try {
151
- const raw = readFileSync(getModelsJsonPath(authPath), "utf8");
152
- const parsed = JSON.parse(raw);
153
- const provider = parsed.providers?.[providerId];
154
- return typeof provider?.baseUrl === "string" && isLocalBaseUrl(provider.baseUrl);
155
- }
156
- catch {
157
- return false;
158
- }
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));
159
154
  }
160
155
  async function resolveApiKeyConfig(apiKeyConfig) {
161
156
  const trimmed = apiKeyConfig.trim();
@@ -394,8 +389,8 @@ async function promptLiteLlmProviderSetup() {
394
389
  };
395
390
  }
396
391
  async function verifyCustomProvider(setup, authPath) {
397
- const registry = await createModelRegistry(authPath);
398
- const modelsError = registry.getError();
392
+ const modelRuntime = await createModelRuntime(authPath);
393
+ const modelsError = modelRuntime.getError();
399
394
  if (modelsError) {
400
395
  printWarning("Verification: models.json failed to load.");
401
396
  for (const line of modelsError.split("\n")) {
@@ -403,19 +398,19 @@ async function verifyCustomProvider(setup, authPath) {
403
398
  }
404
399
  return;
405
400
  }
406
- const all = registry.getAll();
407
- 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));
408
403
  if (!hasModel) {
409
404
  printWarning("Verification: model registry does not contain the configured provider/model ids.");
410
405
  return;
411
406
  }
412
- const available = registry.getAvailable();
413
- 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));
414
409
  if (!hasAvailable) {
415
410
  printWarning("Verification: provider is not considered authenticated/available.");
416
411
  return;
417
412
  }
418
- const apiKey = await registry.getApiKeyForProvider(setup.providerId);
413
+ const apiKey = (await modelRuntime.getAuth(setup.providerId).catch(() => undefined))?.auth.apiKey;
419
414
  if (!apiKey) {
420
415
  printWarning("Verification: API key could not be resolved (check env var name / !command).");
421
416
  return;
@@ -865,7 +860,7 @@ async function describeUnavailableModelSpec(authPath, spec) {
865
860
  if (isProClassModelSpec(knownSpec)) {
866
861
  return `Pro-class model disabled: ${knownSpec}. Choose an approved research model.`;
867
862
  }
868
- if (isLocalModelProvider(authPath, known.provider)) {
863
+ if (await isLocalModelProvider(authPath, known.provider)) {
869
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)}.`;
870
865
  }
871
866
  return `${knownSpec} has no credentials. Run \`feynman model login ${known.provider}\` first.`;
@@ -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);
@@ -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.42",
3
+ "version": "0.5.43",
4
4
  "description": "The open source AI research agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",