@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.
- package/.feynman/settings.json +2 -1
- package/.feynman/themes/feynman.json +1 -1
- package/RELEASES.md +20 -0
- package/dist/cli.js +12 -6
- package/dist/model/commands.js +105 -115
- package/dist/model/registry.js +1 -4
- package/dist/model/service-tier.js +1 -0
- package/dist/pi/runtime.js +3 -2
- package/dist/pi/settings.js +54 -41
- package/dist/setup/doctor.js +25 -41
- package/extensions/research-tools/service-tier.ts +1 -0
- package/package.json +8 -9
package/.feynman/settings.json
CHANGED
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,
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/model/commands.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
385
|
-
const modelsError =
|
|
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 =
|
|
394
|
-
const hasModel = setup.modelIds.some((id) => all.some((model) => model.
|
|
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 =
|
|
400
|
-
const hasAvailable = setup.modelIds.some((id) => available.some((model) => model.
|
|
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
|
|
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
|
-
|
|
631
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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 =
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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: ${
|
|
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: ${
|
|
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
|
|
829
|
-
}
|
|
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
|
-
|
|
892
|
-
const settings =
|
|
893
|
-
settings.
|
|
894
|
-
settings.
|
|
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) {
|
package/dist/model/registry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
|
-
import {
|
|
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);
|
package/dist/pi/runtime.js
CHANGED
|
@@ -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(),
|
package/dist/pi/settings.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
}
|
package/dist/setup/doctor.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
13
|
+
import { createModelRuntime, getModelsJsonPath } from "../model/registry.js";
|
|
14
14
|
import { getConfiguredServiceTier } from "../model/service-tier.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
104
|
-
const
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 =
|
|
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(
|
|
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.
|
|
168
|
-
console.log(`bash: ${
|
|
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
|
-
|
|
175
|
-
console.log(`
|
|
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.
|
|
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/
|
|
83
|
-
"@earendil-works/pi-
|
|
84
|
-
"@earendil-works/pi-
|
|
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.
|
|
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.
|
|
95
|
-
"@earendil-works/pi-ai": "1.
|
|
96
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
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
|
},
|