@companion-ai/feynman 0.5.40 → 0.5.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.feynman/settings.json +2 -1
- package/.feynman/themes/feynman.json +1 -1
- package/RELEASES.md +26 -0
- package/dist/cli.js +20 -4
- package/dist/model/catalog.js +0 -24
- package/dist/model/commands.js +110 -107
- package/dist/pi/launch.js +2 -1
- package/dist/pi/runtime.js +5 -4
- package/dist/pi/settings.js +24 -0
- package/dist/setup/doctor.js +3 -3
- package/dist/system/executables.js +51 -7
- package/extensions/research-tools/model-error-hint.ts +12 -2
- package/extensions/research-tools/telemetry.ts +4 -3
- package/metadata/commands.mjs +1 -1
- package/package.json +8 -4
package/.feynman/settings.json
CHANGED
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,32 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.42 - 2026-10-13
|
|
10
|
+
|
|
11
|
+
- **Pi 1.2.0 and pi-subagents 0.78.0.**
|
|
12
|
+
- With an OpenRouter key, `/model` lists only the models that key can use, with their real context sizes and prices.
|
|
13
|
+
- "The upstream provider returned an error" is retried instead of ending the turn, and Anthropic requests rejected for too many strict tools are retried once.
|
|
14
|
+
- `feynman -p` warns and exits 1 when the answer was cut off at the model's output limit.
|
|
15
|
+
- On Windows, reading or writing saved logins no longer fails with `EPERM` while an old lock is cleaned up.
|
|
16
|
+
- Pressing `Esc` keeps a research run stopped; a subagent finishing a moment later no longer restarts it.
|
|
17
|
+
- Reports are no longer overwritten when a subagent run is resumed, and a timed-out subagent's partial output reaches the lead agent.
|
|
18
|
+
- The `workflowScript contains dynamic child launches` warning no longer prints on every parallel run.
|
|
19
|
+
- **`feynman model login` offers every provider Pi supports with an API key.** That is 41 providers instead of Feynman's own list of 16, adding DeepSeek, Fireworks, Together, NVIDIA, Google Vertex, Cloudflare, Qwen and others. Pi now asks for what each provider needs, so Cloudflare's extra fields and Bedrock's profile or bearer-token options work. Pressing Enter on an empty key no longer saves an empty credential.
|
|
20
|
+
- **Turning off Feynman's telemetry also turns off Pi's.** `FEYNMAN_TELEMETRY=off` (or `DO_NOT_TRACK=1`) now sets Pi's own `PI_TELEMETRY=0` for its install reporting.
|
|
21
|
+
- Feynman now honors Pi's own `sessionDir` setting and `PI_CODING_AGENT_SESSION_DIR` when you don't pass `--session-dir`.
|
|
22
|
+
- `feynman model set` and the default-model check use Pi's settings API, so they match what Pi launches with (including trusted project settings).
|
|
23
|
+
|
|
24
|
+
## v0.5.41 - 2026-10-12
|
|
25
|
+
|
|
26
|
+
- **A fresh `npm install` works again.** Pi 1.2.0, released today, made new npm installs of Feynman 0.5.40 fail on every command except `-v` (`does not provide an export named 'MCP_TYPESCRIPT_PREAMBLE'`). Pi 1.1.0 asks for its own sub-packages with open version ranges, so npm pulled Pi 1.2.0's copies. Feynman now pins those sub-packages to the Pi version it ships. The curl and PowerShell installers were not affected.
|
|
27
|
+
- **An expired sign-in no longer looks like no model.** Feynman treated a ChatGPT, Claude or Copilot sign-in whose short-lived access token had expired as if no model were set up. `feynman` reopened the setup menu, and since 0.5.36 `feynman -p` refused to run. Pi renews the token on the next request, so Feynman now leaves that to Pi and asks you to sign in again only when renewal really fails.
|
|
28
|
+
- **`--model` accepts the names Pi accepts.** Bare model IDs (`claude-opus-5-5`), short names (`sonnet`) and a thinking suffix (`anthropic/claude-opus-5-5:high`) now work, preferring a provider you are signed in to; `provider/model` and `provider:model` still work. Pro-class models stay blocked.
|
|
29
|
+
- **After you sign in, the model list matches your account.** `feynman model login` refreshes that provider's model list, so an OpenRouter key gets the models it can actually use before Feynman picks a default.
|
|
30
|
+
- A provider error hint now appears once the run has stopped, not in the middle of an automatic retry.
|
|
31
|
+
- Launching Feynman no longer erases your terminal's scrollback.
|
|
32
|
+
- The llama.cpp provider is recognized as a local model.
|
|
33
|
+
- **Preview and PDF export find more browsers.** Chromium, Edge and Brave on Linux, Edge under `Program Files (x86)` and Brave in your user folder on Windows, and apps in `~/Applications` on macOS. Thanks to @A-ryanVAT-S (#343).
|
|
34
|
+
|
|
9
35
|
## v0.5.40 - 2026-10-12
|
|
10
36
|
|
|
11
37
|
- **alphaXiv sign-in messages name the right command.** When you weren't signed in, `feynman alpha search`, `get` and the paper tools said to run `alpha login`, a command that isn't installed with Feynman. They now say `feynman alpha login`. (Alpha Hub 0.1.7.)
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { ensureFeynmanAgentDir } from "./bootstrap/home.js";
|
|
|
19
19
|
import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
|
|
20
20
|
import { getLastPiStderr, launchPiChat, runPi } from "./pi/launch.js";
|
|
21
21
|
import { installPiPackage, removePiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
|
|
22
|
-
import {
|
|
22
|
+
import { resolveExplicitModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
|
|
23
23
|
import { BUNDLED_PI_PACKAGES, buildPiEnv, findWindowsBash, WINDOWS_BASH_MISSING_NOTICE } from "./pi/runtime.js";
|
|
24
24
|
import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js";
|
|
25
25
|
import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, selectDefaultModel, setDefaultModelSpec, } from "./model/commands.js";
|
|
@@ -29,6 +29,7 @@ import { fetchLatestFeynmanVersion, getFeynmanUpgradeLines, isNewerVersion } fro
|
|
|
29
29
|
import { runDoctor, runStatus } from "./setup/doctor.js";
|
|
30
30
|
import { setupPreviewDependencies } from "./setup/preview.js";
|
|
31
31
|
import { runSetup } from "./setup/setup.js";
|
|
32
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
32
33
|
import { noModelMessage } from "./setup/prompts.js";
|
|
33
34
|
import { captureTelemetryEvent, captureTelemetryException, getCliTelemetryMetadata, initializePostHogTelemetry, shutdownPostHogTelemetry, telemetryErrorProperties, telemetryFirstRunNotice, } from "./telemetry/posthog.js";
|
|
34
35
|
import { ASH, printAsciiHeader, printInfo, printPanel, printSection, RESET, SAGE } from "./ui/terminal.js";
|
|
@@ -421,7 +422,9 @@ export async function main() {
|
|
|
421
422
|
...commandTelemetry,
|
|
422
423
|
duration_ms: durationMs,
|
|
423
424
|
exit_code: exitCode,
|
|
424
|
-
|
|
425
|
+
// A fixed reason for a deliberate non-zero exit, so it isn't read as a crash.
|
|
426
|
+
...(exitReason ? { exit_reason: exitReason } : {}),
|
|
427
|
+
...(exitCode === 0 || exitReason ? {} : { pi_stderr: getLastPiStderr() }),
|
|
425
428
|
};
|
|
426
429
|
captureTelemetryEvent(exitCode === 0 ? "feynman_command_completed" : "feynman_command_failed", completeProperties);
|
|
427
430
|
}
|
|
@@ -440,6 +443,8 @@ export async function main() {
|
|
|
440
443
|
await shutdownPostHogTelemetry();
|
|
441
444
|
}
|
|
442
445
|
}
|
|
446
|
+
// Set when Feynman exits non-zero on purpose (see the completion telemetry in main).
|
|
447
|
+
let exitReason;
|
|
443
448
|
async function runMain(input) {
|
|
444
449
|
const { appRoot, feynmanVersion } = input;
|
|
445
450
|
const bundledSettingsPath = resolve(appRoot, ".feynman", "settings.json");
|
|
@@ -512,7 +517,12 @@ async function runMain(input) {
|
|
|
512
517
|
throw new Error("Unable to determine the installed Feynman version.");
|
|
513
518
|
}
|
|
514
519
|
const workingDir = resolve(values.cwd ?? process.cwd());
|
|
515
|
-
|
|
520
|
+
// Pi's order: --session-dir, then PI_CODING_AGENT_SESSION_DIR, then the sessionDir
|
|
521
|
+
// setting; Feynman's own default comes last.
|
|
522
|
+
const sessionDir = resolve(workingDir, values["session-dir"] ??
|
|
523
|
+
process.env.PI_CODING_AGENT_SESSION_DIR ??
|
|
524
|
+
SettingsManager.create(workingDir, feynmanAgentDir).getSessionDir() ??
|
|
525
|
+
getDefaultSessionDir(feynmanHome));
|
|
516
526
|
const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
|
|
517
527
|
const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
|
|
518
528
|
const { defaultThinkingLevel, launchThinkingLevel } = resolveThinkingConfig(values.thinking ?? process.env.FEYNMAN_THINKING);
|
|
@@ -641,10 +651,14 @@ async function runMain(input) {
|
|
|
641
651
|
throw new Error(`Pro-class model disabled: ${requestedExplicitModelSpec}. Choose an approved research model.`);
|
|
642
652
|
}
|
|
643
653
|
const modelRuntime = await createModelRuntime(feynmanAuthPath);
|
|
644
|
-
const canonicalModelSpec =
|
|
654
|
+
const canonicalModelSpec = resolveExplicitModelSpec(requestedExplicitModelSpec, modelRuntime);
|
|
645
655
|
if (!canonicalModelSpec) {
|
|
646
656
|
throw new Error(`Unknown model: ${requestedExplicitModelSpec}. Run \`feynman model list\` to see available models.`);
|
|
647
657
|
}
|
|
658
|
+
// Check the resolved model too: a short name can resolve to a Pro-class one.
|
|
659
|
+
if (isProClassModelSpec(canonicalModelSpec.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, ""))) {
|
|
660
|
+
throw new Error(`Pro-class model disabled: ${canonicalModelSpec}. Choose an approved research model.`);
|
|
661
|
+
}
|
|
648
662
|
explicitModelSpec = canonicalModelSpec;
|
|
649
663
|
}
|
|
650
664
|
const workflowCommandNames = new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name));
|
|
@@ -666,6 +680,7 @@ async function runMain(input) {
|
|
|
666
680
|
if (!getCurrentModelSpec(feynmanSettingsPath)) {
|
|
667
681
|
console.error(noModelMessage(promptGiven));
|
|
668
682
|
process.exitCode = 1;
|
|
683
|
+
exitReason = "no_model";
|
|
669
684
|
return;
|
|
670
685
|
}
|
|
671
686
|
await ensureFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, feynmanAuthPath);
|
|
@@ -674,6 +689,7 @@ async function runMain(input) {
|
|
|
674
689
|
&& (await getAuthenticatedModelRecords(feynmanAuthPath)).length === 0) {
|
|
675
690
|
console.error(noModelMessage(promptGiven));
|
|
676
691
|
process.exitCode = 1;
|
|
692
|
+
exitReason = "no_model";
|
|
677
693
|
return;
|
|
678
694
|
}
|
|
679
695
|
// Pi's own session flags pass through; Feynman continues the latest
|
package/dist/model/catalog.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { getEnvApiKey } from "@earendil-works/pi-ai/compat";
|
|
2
|
-
import { readJsonFile } from "../config/json-file.js";
|
|
3
1
|
import { createModelRuntime } from "./registry.js";
|
|
4
2
|
const PRO_CLASS_MODEL_PATTERN = /(?:^|[-_.:/])pro(?:$|[-_.:/])/i;
|
|
5
3
|
// Pi catalogs these exact DeepSeek V4 Pro model IDs as ordinary open-weight
|
|
@@ -329,10 +327,8 @@ function sortProviders(left, right) {
|
|
|
329
327
|
return left.label.localeCompare(right.label);
|
|
330
328
|
}
|
|
331
329
|
export async function getAuthenticatedModelRecords(authPath) {
|
|
332
|
-
const expiredOAuthProviders = readExpiredOAuthProviders(authPath);
|
|
333
330
|
const modelRuntime = await createModelRuntime(authPath);
|
|
334
331
|
return (await modelRuntime.getAvailable())
|
|
335
|
-
.filter((model) => !expiredOAuthProviders.has(model.provider))
|
|
336
332
|
.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
|
|
337
333
|
}
|
|
338
334
|
export async function getAvailableModelRecords(authPath) {
|
|
@@ -344,26 +340,6 @@ export async function getSupportedModelRecords(authPath) {
|
|
|
344
340
|
.getModels()
|
|
345
341
|
.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
|
|
346
342
|
}
|
|
347
|
-
function readExpiredOAuthProviders(authPath) {
|
|
348
|
-
const expired = new Set();
|
|
349
|
-
try {
|
|
350
|
-
const parsed = readJsonFile(authPath);
|
|
351
|
-
for (const [provider, credential] of Object.entries(parsed)) {
|
|
352
|
-
if (!credential || typeof credential !== "object")
|
|
353
|
-
continue;
|
|
354
|
-
const typedCredential = credential;
|
|
355
|
-
if (typedCredential.type !== "oauth" || typeof typedCredential.expires !== "number")
|
|
356
|
-
continue;
|
|
357
|
-
if (typedCredential.expires > Date.now())
|
|
358
|
-
continue;
|
|
359
|
-
if (getEnvApiKey(provider))
|
|
360
|
-
continue;
|
|
361
|
-
expired.add(provider);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
catch { }
|
|
365
|
-
return expired;
|
|
366
|
-
}
|
|
367
343
|
export async function chooseRecommendedModel(authPath) {
|
|
368
344
|
const preferred = choosePreferredModelRecord(await getAvailableModelRecords(authPath));
|
|
369
345
|
if (!preferred) {
|
package/dist/model/commands.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { exec as execCallback } from "node:child_process";
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { readJson } from "../pi/settings.js";
|
|
7
6
|
import { promptChoice, promptSecret, promptSelect, promptText, SetupCancelledError, validateApiKeyInput } from "../setup/prompts.js";
|
|
8
7
|
import { openUrl } from "../system/open-url.js";
|
|
9
8
|
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
|
@@ -54,19 +53,32 @@ async function selectOAuthProvider(authPath, action) {
|
|
|
54
53
|
}
|
|
55
54
|
return selection;
|
|
56
55
|
}
|
|
57
|
-
|
|
56
|
+
// Endpoints Pi has no API-key sign-in for: Feynman writes their models.json entry.
|
|
57
|
+
const LOCAL_API_KEY_SETUPS = MODEL_API_KEY_PROVIDERS.filter((provider) => ["lm-studio", "litellm", "__custom__"].includes(provider.id));
|
|
58
|
+
// Every provider Pi can sign in to with an API key (its own list, labels and
|
|
59
|
+
// prompts), plus Feynman's local and custom endpoint setups.
|
|
60
|
+
export async function listApiKeyProviders(authPath) {
|
|
61
|
+
const envVars = new Map(MODEL_API_KEY_PROVIDERS.map((provider) => [provider.id, provider.envVar]));
|
|
62
|
+
const piProviders = (await createModelRuntime(authPath))
|
|
63
|
+
.getProviders()
|
|
64
|
+
.filter((provider) => provider.auth.apiKey)
|
|
65
|
+
.map((provider) => ({ id: provider.id, label: provider.name, envVar: envVars.get(provider.id) }))
|
|
66
|
+
.sort((left, right) => left.label.localeCompare(right.label));
|
|
67
|
+
return [...piProviders, ...LOCAL_API_KEY_SETUPS];
|
|
68
|
+
}
|
|
69
|
+
async function resolveApiKeyProvider(authPath, input) {
|
|
58
70
|
const normalizedInput = normalizeProviderId(input);
|
|
59
71
|
if (!normalizedInput) {
|
|
60
72
|
return undefined;
|
|
61
73
|
}
|
|
62
|
-
return
|
|
74
|
+
return (await listApiKeyProviders(authPath)).find((provider) => provider.id === normalizedInput);
|
|
63
75
|
}
|
|
64
76
|
export async function resolveModelProviderForCommand(authPath, input) {
|
|
65
77
|
const oauthProvider = await resolveOAuthProvider(authPath, input);
|
|
66
78
|
if (oauthProvider) {
|
|
67
79
|
return { kind: "oauth", id: oauthProvider.id };
|
|
68
80
|
}
|
|
69
|
-
const apiKeyProvider = resolveApiKeyProvider(input);
|
|
81
|
+
const apiKeyProvider = await resolveApiKeyProvider(authPath, input);
|
|
70
82
|
if (apiKeyProvider) {
|
|
71
83
|
return { kind: "api-key", id: apiKeyProvider.id };
|
|
72
84
|
}
|
|
@@ -84,14 +96,15 @@ function apiKeyProviderHint(provider) {
|
|
|
84
96
|
}
|
|
85
97
|
return provider.envVar ?? provider.id;
|
|
86
98
|
}
|
|
87
|
-
async function selectApiKeyProvider() {
|
|
88
|
-
const
|
|
99
|
+
async function selectApiKeyProvider(authPath) {
|
|
100
|
+
const providers = await listApiKeyProviders(authPath);
|
|
101
|
+
const options = providers.map((provider) => ({
|
|
89
102
|
value: provider,
|
|
90
103
|
label: provider.label,
|
|
91
104
|
hint: apiKeyProviderHint(provider),
|
|
92
105
|
}));
|
|
93
106
|
options.push({ value: "cancel", label: "Cancel" });
|
|
94
|
-
const defaultProvider =
|
|
107
|
+
const defaultProvider = providers.find((provider) => provider.id === "openai") ?? providers[0];
|
|
95
108
|
const selection = await promptSelect("Choose an API-key provider:", options, defaultProvider);
|
|
96
109
|
if (selection === "cancel") {
|
|
97
110
|
return undefined;
|
|
@@ -125,7 +138,8 @@ function normalizeCustomProviderBaseUrl(api, baseUrl) {
|
|
|
125
138
|
function isLocalBaseUrl(baseUrl) {
|
|
126
139
|
return /^(https?:\/\/)?(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(baseUrl);
|
|
127
140
|
}
|
|
128
|
-
|
|
141
|
+
// "llama.cpp" is the id of Pi's bundled llama.cpp provider.
|
|
142
|
+
const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
|
|
129
143
|
export function isLocalModelProvider(authPath, providerId) {
|
|
130
144
|
if (!providerId) {
|
|
131
145
|
return false;
|
|
@@ -500,36 +514,6 @@ async function verifyCustomProvider(setup, authPath) {
|
|
|
500
514
|
}
|
|
501
515
|
printInfo("Verification: skipped network probe for this API mode.");
|
|
502
516
|
}
|
|
503
|
-
async function verifyBedrockCredentialChain() {
|
|
504
|
-
const { defaultProvider } = await import("@aws-sdk/credential-provider-node");
|
|
505
|
-
const credentials = await defaultProvider({})();
|
|
506
|
-
if (!credentials?.accessKeyId || !credentials?.secretAccessKey) {
|
|
507
|
-
throw new Error("AWS credential chain resolved without usable Bedrock credentials.");
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
async function configureBedrockProvider(authPath) {
|
|
511
|
-
printSection("AWS Credentials: Amazon Bedrock");
|
|
512
|
-
printInfo("Feynman will verify the AWS SDK credential chain used by Pi's Bedrock provider.");
|
|
513
|
-
printInfo("Supported sources include AWS_PROFILE, ~/.aws credentials/config, SSO, ECS/IRSA, and EC2 instance roles.");
|
|
514
|
-
try {
|
|
515
|
-
await verifyBedrockCredentialChain();
|
|
516
|
-
const modelRuntime = await createModelRuntime(authPath);
|
|
517
|
-
if (!(await modelRuntime.checkAuth("amazon-bedrock"))) {
|
|
518
|
-
throw new Error("Pi could not resolve the verified AWS credential chain for Amazon Bedrock.");
|
|
519
|
-
}
|
|
520
|
-
printSuccess("Verified the AWS credential chain used by Amazon Bedrock.");
|
|
521
|
-
printInfo("Use `feynman model list` to see available Bedrock models.");
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
printWarning(`AWS credential verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
526
|
-
printInfo("Configure AWS credentials first, for example:");
|
|
527
|
-
printInfo(" export AWS_PROFILE=default");
|
|
528
|
-
printInfo(" # or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY");
|
|
529
|
-
printInfo(" # or use an EC2/ECS/IRSA role with valid Bedrock access");
|
|
530
|
-
return false;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
517
|
async function maybeSetRecommendedDefaultModel(settingsPath, authPath) {
|
|
534
518
|
if (!settingsPath) {
|
|
535
519
|
return;
|
|
@@ -545,7 +529,7 @@ async function maybeSetRecommendedDefaultModel(settingsPath, authPath) {
|
|
|
545
529
|
}
|
|
546
530
|
}
|
|
547
531
|
async function configureApiKeyProvider(authPath, providerId) {
|
|
548
|
-
const provider = providerId ? resolveApiKeyProvider(providerId) : await selectApiKeyProvider();
|
|
532
|
+
const provider = providerId ? await resolveApiKeyProvider(authPath, providerId) : await selectApiKeyProvider(authPath);
|
|
549
533
|
if (!provider) {
|
|
550
534
|
if (providerId) {
|
|
551
535
|
throw new Error(`Unknown API-key model provider: ${providerId}`);
|
|
@@ -553,9 +537,6 @@ async function configureApiKeyProvider(authPath, providerId) {
|
|
|
553
537
|
printInfo("API key setup cancelled.");
|
|
554
538
|
return false;
|
|
555
539
|
}
|
|
556
|
-
if (provider.id === "amazon-bedrock") {
|
|
557
|
-
return configureBedrockProvider(authPath);
|
|
558
|
-
}
|
|
559
540
|
if (provider.id === "lm-studio") {
|
|
560
541
|
const setup = await promptLmStudioProviderSetup();
|
|
561
542
|
if (!setup) {
|
|
@@ -626,22 +607,34 @@ async function configureApiKeyProvider(authPath, providerId) {
|
|
|
626
607
|
if (provider.envVar) {
|
|
627
608
|
printInfo(`Tip: to avoid writing secrets to disk, set ${provider.envVar} in your shell or .env.`);
|
|
628
609
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (provider.envVar) {
|
|
632
|
-
printInfo(`Set ${provider.envVar} and rerun setup (or run \`feynman model list\`).`);
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
printInfo("No API key provided.");
|
|
636
|
-
}
|
|
637
|
-
return false;
|
|
638
|
-
}
|
|
610
|
+
// Pi asks for whatever this provider needs (a key, or e.g. Cloudflare's account
|
|
611
|
+
// and gateway, Bedrock's profile or token). Esc cancels.
|
|
639
612
|
const modelRuntime = await createModelRuntime(authPath);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
613
|
+
const interaction = terminalAuthInteraction(provider.label, new AbortController().signal);
|
|
614
|
+
const signedIn = await modelRuntime
|
|
615
|
+
.login(provider.id, "api_key", {
|
|
616
|
+
...interaction,
|
|
617
|
+
prompt: async (prompt) => {
|
|
618
|
+
if (prompt.type !== "secret")
|
|
619
|
+
return interaction.prompt(prompt);
|
|
620
|
+
const secret = await promptSecret(prompt.message, prompt.signal, validateApiKeyInput);
|
|
621
|
+
// An empty key would be saved as a credential that fails every request.
|
|
622
|
+
if (!secret)
|
|
623
|
+
throw new SetupCancelledError("no API key entered");
|
|
624
|
+
return secret;
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
.then(() => true)
|
|
628
|
+
.catch((error) => {
|
|
629
|
+
if (!(error instanceof SetupCancelledError))
|
|
630
|
+
throw error;
|
|
631
|
+
if (provider.envVar)
|
|
632
|
+
printInfo(`Or set ${provider.envVar} and run feynman again.`);
|
|
633
|
+
return false;
|
|
643
634
|
});
|
|
644
|
-
|
|
635
|
+
if (!signedIn)
|
|
636
|
+
return false;
|
|
637
|
+
printSuccess(`Saved ${provider.label} credentials in auth storage.`);
|
|
645
638
|
const baseUrl = await promptText("Base URL override (optional, include /v1 for OpenAI-compatible endpoints)", "");
|
|
646
639
|
if (baseUrl) {
|
|
647
640
|
const modelsJsonPath = getModelsJsonPath(authPath);
|
|
@@ -653,6 +646,7 @@ async function configureApiKeyProvider(authPath, providerId) {
|
|
|
653
646
|
printWarning(result.error);
|
|
654
647
|
}
|
|
655
648
|
}
|
|
649
|
+
await refreshProviderModels(modelRuntime, provider.id);
|
|
656
650
|
return true;
|
|
657
651
|
}
|
|
658
652
|
async function resolveAvailableModelSpec(authPath, input) {
|
|
@@ -672,8 +666,8 @@ async function resolveAvailableModelSpec(authPath, input) {
|
|
|
672
666
|
// When multiple providers expose the same bare model ID, prefer providers the
|
|
673
667
|
// user explicitly configured in auth storage.
|
|
674
668
|
if (exactIdMatches.length > 1) {
|
|
675
|
-
const
|
|
676
|
-
const configuredProviders = new Set(
|
|
669
|
+
const credentials = await (await createModelRuntime(authPath)).listCredentials();
|
|
670
|
+
const configuredProviders = new Set(credentials.map((credential) => credential.providerId));
|
|
677
671
|
const configuredMatches = exactIdMatches.filter((model) => configuredProviders.has(model.provider));
|
|
678
672
|
if (configuredMatches.length === 1) {
|
|
679
673
|
return `${configuredMatches[0].provider}/${configuredMatches[0].id}`;
|
|
@@ -681,12 +675,12 @@ async function resolveAvailableModelSpec(authPath, input) {
|
|
|
681
675
|
}
|
|
682
676
|
return undefined;
|
|
683
677
|
}
|
|
678
|
+
// The default model Pi will launch with, read through Pi's SettingsManager.
|
|
684
679
|
export function getCurrentModelSpec(settingsPath) {
|
|
685
|
-
const settings =
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
return undefined;
|
|
680
|
+
const settings = SettingsManager.create(process.cwd(), dirname(settingsPath));
|
|
681
|
+
const provider = settings.getDefaultProvider();
|
|
682
|
+
const model = settings.getDefaultModel();
|
|
683
|
+
return provider && model ? `${provider}/${model}` : undefined;
|
|
690
684
|
}
|
|
691
685
|
export async function printModelList(settingsPath, authPath) {
|
|
692
686
|
const status = await collectModelStatus(settingsPath, authPath);
|
|
@@ -733,13 +727,11 @@ export async function authenticateModelProvider(authPath, settingsPath) {
|
|
|
733
727
|
}
|
|
734
728
|
// Sign in with ChatGPT sends a stable installation ID. Pi's own /login keeps it
|
|
735
729
|
// as `deviceId` in the agent settings.json; share that one.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const settings =
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const deviceId = randomUUID();
|
|
742
|
-
writeFileSync(settingsPath, `${JSON.stringify({ ...settings, deviceId }, null, 2)}\n`, "utf8");
|
|
730
|
+
// Pi's own device ID, the one its /login sends, kept in the agent settings.
|
|
731
|
+
export async function getOrCreateDeviceId(authPath) {
|
|
732
|
+
const settings = SettingsManager.create(process.cwd(), dirname(authPath));
|
|
733
|
+
const deviceId = settings.getOrCreateDeviceId();
|
|
734
|
+
await settings.flush();
|
|
743
735
|
return deviceId;
|
|
744
736
|
}
|
|
745
737
|
// A provider answers invalid_grant when it rejects the authorization code, and
|
|
@@ -752,31 +744,15 @@ export function describeOAuthLoginFailure(providerId, error) {
|
|
|
752
744
|
const alternative = providerId === "openai" ? " Or sign in with `feynman model login openai-codex` instead." : "";
|
|
753
745
|
return new Error(`${error.message}\nThe sign-in was rejected, usually because of a stale browser session. Sign out of the provider's site in your browser, or use a private window, then run \`feynman model login ${providerId}\` again.${alternative}`);
|
|
754
746
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
await maybeSetRecommendedDefaultModel(settingsPath, authPath);
|
|
765
|
-
}
|
|
766
|
-
return configured;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
const provider = providerId ? await resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
|
|
770
|
-
if (!provider) {
|
|
771
|
-
if (providerId) {
|
|
772
|
-
throw new Error(`Unknown model provider: ${providerId}`);
|
|
773
|
-
}
|
|
774
|
-
printInfo("Login cancelled.");
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
const modelRuntime = await createModelRuntime(authPath);
|
|
778
|
-
const abortController = new AbortController();
|
|
779
|
-
await modelRuntime.login(provider.id, "oauth", {
|
|
747
|
+
// Pi's /login refreshes the provider's model list from the network after
|
|
748
|
+
// signing in (OpenRouter keys get the models that key may use); do the same,
|
|
749
|
+
// so the recommended default comes from that list. A failure keeps the saved list.
|
|
750
|
+
async function refreshProviderModels(modelRuntime, providerId) {
|
|
751
|
+
await modelRuntime.refresh({ providers: [providerId], allowNetwork: true, signal: AbortSignal.timeout(15_000) }).catch(() => undefined);
|
|
752
|
+
}
|
|
753
|
+
// Runs Pi's own login prompts (selects, secrets, text, browser URLs) in the terminal.
|
|
754
|
+
function terminalAuthInteraction(providerLabel, signal) {
|
|
755
|
+
return {
|
|
780
756
|
prompt: async (prompt) => {
|
|
781
757
|
if (prompt.type === "select") {
|
|
782
758
|
return promptSelect(prompt.message, prompt.options.map((option) => ({
|
|
@@ -795,7 +771,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
795
771
|
},
|
|
796
772
|
notify: (event) => {
|
|
797
773
|
if (event.type === "auth_url") {
|
|
798
|
-
printSection(`Login: ${
|
|
774
|
+
printSection(`Login: ${providerLabel}`);
|
|
799
775
|
if (openUrl(event.url)) {
|
|
800
776
|
printInfo("Opened the login URL in your browser.");
|
|
801
777
|
}
|
|
@@ -809,7 +785,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
809
785
|
return;
|
|
810
786
|
}
|
|
811
787
|
if (event.type === "device_code") {
|
|
812
|
-
printSection(`Login: ${
|
|
788
|
+
printSection(`Login: ${providerLabel}`);
|
|
813
789
|
printInfo(`Visit ${event.verificationUri} and enter code: ${event.userCode}`);
|
|
814
790
|
openUrl(event.verificationUri);
|
|
815
791
|
return;
|
|
@@ -818,11 +794,39 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
818
794
|
printInfo(event.message);
|
|
819
795
|
}
|
|
820
796
|
},
|
|
821
|
-
signal
|
|
822
|
-
}
|
|
797
|
+
signal,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
801
|
+
if (providerId) {
|
|
802
|
+
const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
|
|
803
|
+
if (!resolvedProvider) {
|
|
804
|
+
throw new Error(`Unknown model provider: ${providerId}`);
|
|
805
|
+
}
|
|
806
|
+
if (resolvedProvider.kind === "api-key") {
|
|
807
|
+
const configured = await configureApiKeyProvider(authPath, resolvedProvider.id);
|
|
808
|
+
if (configured) {
|
|
809
|
+
await maybeSetRecommendedDefaultModel(settingsPath, authPath);
|
|
810
|
+
}
|
|
811
|
+
return configured;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const provider = providerId ? await resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
|
|
815
|
+
if (!provider) {
|
|
816
|
+
if (providerId) {
|
|
817
|
+
throw new Error(`Unknown model provider: ${providerId}`);
|
|
818
|
+
}
|
|
819
|
+
printInfo("Login cancelled.");
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
const modelRuntime = await createModelRuntime(authPath);
|
|
823
|
+
const abortController = new AbortController();
|
|
824
|
+
const deviceId = await getOrCreateDeviceId(authPath);
|
|
825
|
+
await modelRuntime.login(provider.id, "oauth", terminalAuthInteraction(provider.name ?? provider.id, abortController.signal), { getDeviceId: () => deviceId }).catch((error) => {
|
|
823
826
|
throw describeOAuthLoginFailure(provider.id, error);
|
|
824
827
|
});
|
|
825
828
|
printSuccess(`Model provider login complete: ${provider.id}`);
|
|
829
|
+
await refreshProviderModels(modelRuntime, provider.id);
|
|
826
830
|
await maybeSetRecommendedDefaultModel(settingsPath, authPath);
|
|
827
831
|
return true;
|
|
828
832
|
}
|
|
@@ -880,11 +884,10 @@ export async function setDefaultModelSpec(settingsPath, authPath, spec) {
|
|
|
880
884
|
throw new Error(await describeUnavailableModelSpec(authPath, spec));
|
|
881
885
|
}
|
|
882
886
|
const [provider, ...rest] = resolvedSpec.split("/");
|
|
883
|
-
|
|
884
|
-
const settings =
|
|
885
|
-
settings.
|
|
886
|
-
settings.
|
|
887
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
887
|
+
// Pi's SettingsManager merges only the changed keys under its lock.
|
|
888
|
+
const settings = SettingsManager.create(process.cwd(), dirname(settingsPath));
|
|
889
|
+
settings.setDefaultModelAndProvider(provider, rest.join("/"));
|
|
890
|
+
await settings.flush();
|
|
888
891
|
printSuccess(`Research default model set to ${resolvedSpec}`);
|
|
889
892
|
}
|
|
890
893
|
export async function selectDefaultModel(settingsPath, authPath) {
|
package/dist/pi/launch.js
CHANGED
|
@@ -42,8 +42,9 @@ export async function runPi(options, args, env, stdin = "inherit") {
|
|
|
42
42
|
}
|
|
43
43
|
export async function launchPiChat(options) {
|
|
44
44
|
let telemetryNotice;
|
|
45
|
+
// No screen clear: Pi's fullscreen mode uses the alternate screen, and
|
|
46
|
+
// `--tui-mode regular` keeps the terminal's scrollback.
|
|
45
47
|
if (process.stdout.isTTY && options.mode !== "rpc") {
|
|
46
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
47
48
|
telemetryNotice = telemetryFirstRunNotice();
|
|
48
49
|
}
|
|
49
50
|
const executables = await resolveAllExecutables();
|
package/dist/pi/runtime.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { delimiter, dirname, resolve, win32 } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { getPostHogChildEnv } from "../telemetry/posthog.js";
|
|
4
|
+
import { MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable, } from "../system/executables.js";
|
|
5
|
+
import { getPostHogChildEnv, isTelemetryDisabled } from "../telemetry/posthog.js";
|
|
6
6
|
// Pi packages shipped as Feynman dependencies and loaded from their install
|
|
7
7
|
// paths through settings.json `packages` (Pi's documented local-path source).
|
|
8
8
|
export const BUNDLED_PI_PACKAGES = ["pi-subagents", "pi-web-access", "pi-docparser", "pi-btw"];
|
|
@@ -145,7 +145,7 @@ export function buildPiEnv(options, executables) {
|
|
|
145
145
|
const binPath = [getFeynmanCommandShimDir(options.feynmanAgentDir), resolve(options.appRoot, "node_modules", ".bin")].join(delimiter);
|
|
146
146
|
const pandocPath = process.env.PANDOC_PATH ?? executables?.pandoc ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
|
147
147
|
const mermaidPath = process.env.MERMAID_CLI_PATH ?? executables?.mermaid ?? resolveExecutable("mmdc", MERMAID_FALLBACK_PATHS);
|
|
148
|
-
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? executables?.browser ??
|
|
148
|
+
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? executables?.browser ?? resolveBrowser();
|
|
149
149
|
return {
|
|
150
150
|
...process.env,
|
|
151
151
|
PATH: [binPath, process.env.PATH ?? "", process.platform === "win32" ? findWindowsBash()?.addDir : undefined]
|
|
@@ -156,8 +156,9 @@ export function buildPiEnv(options, executables) {
|
|
|
156
156
|
FEYNMAN_BIN_PATH: getFeynmanCliBinPath(options.appRoot),
|
|
157
157
|
PI_CODING_AGENT_DIR: options.feynmanAgentDir,
|
|
158
158
|
PANDOC_PATH: pandocPath,
|
|
159
|
-
PI_HARDWARE_CURSOR: process.env.PI_HARDWARE_CURSOR ?? "1",
|
|
160
159
|
PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK ?? "1",
|
|
160
|
+
// A Feynman telemetry opt-out also turns off Pi's install telemetry (docs/environment-variables.md).
|
|
161
|
+
...(isTelemetryDisabled() && process.env.PI_TELEMETRY === undefined ? { PI_TELEMETRY: "0" } : {}),
|
|
161
162
|
MERMAID_CLI_PATH: mermaidPath,
|
|
162
163
|
PUPPETEER_EXECUTABLE_PATH: browserPath,
|
|
163
164
|
...getPostHogChildEnv(),
|
package/dist/pi/settings.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { resolveCliModel } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { parseJsonText, readJsonFile } from "../config/json-file.js";
|
|
4
5
|
import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
|
|
5
6
|
import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
|
|
@@ -32,6 +33,29 @@ export function canonicalizeModelSpec(spec, modelLookup) {
|
|
|
32
33
|
const model = parseModelSpec(spec, modelLookup);
|
|
33
34
|
return model ? `${model.provider}/${model.id}` : undefined;
|
|
34
35
|
}
|
|
36
|
+
// `--model` input, resolved the way Pi resolves it (bare ids, fuzzy names, a
|
|
37
|
+
// `:<thinking>` suffix) after Feynman's exact provider/id and provider:id
|
|
38
|
+
// forms. Pi's fuzzy match ignores sign-ins, so the same model id on a provider
|
|
39
|
+
// you are signed in to wins.
|
|
40
|
+
export function resolveExplicitModelSpec(spec, modelRuntime) {
|
|
41
|
+
const exact = canonicalizeModelSpec(spec, modelRuntime);
|
|
42
|
+
if (exact)
|
|
43
|
+
return exact;
|
|
44
|
+
let result = resolveCliModel({ cliModel: spec.trim(), modelRuntime });
|
|
45
|
+
if (!result.model)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (!modelRuntime.hasConfiguredAuth(result.model.provider)) {
|
|
48
|
+
for (const provider of new Set(modelRuntime.getAvailableSnapshot().map((model) => model.provider))) {
|
|
49
|
+
const signedIn = resolveCliModel({ cliProvider: provider, cliModel: spec.trim(), modelRuntime });
|
|
50
|
+
if (signedIn.model && !signedIn.warning) {
|
|
51
|
+
result = signedIn;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const model = result.model;
|
|
57
|
+
return `${model.provider}/${model.id}${result.thinkingLevel ? `:${result.thinkingLevel}` : ""}`;
|
|
58
|
+
}
|
|
35
59
|
export function normalizeThinkingLevel(value) {
|
|
36
60
|
if (!value) {
|
|
37
61
|
return undefined;
|
package/dist/setup/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getValidToken as getValidAlphaToken } from "@companion-ai/alpha-hub/lib
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { verifyAlphaAuthStatus } from "../alpha-auth-status.js";
|
|
5
5
|
import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
|
|
6
|
-
import {
|
|
6
|
+
import { PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable } from "../system/executables.js";
|
|
7
7
|
import { readJson } from "../pi/settings.js";
|
|
8
8
|
import { findWindowsBash, validatePiInstallation, WINDOWS_BASH_MISSING_NOTICE } from "../pi/runtime.js";
|
|
9
9
|
import { isTelemetryDisabled } from "../telemetry/posthog.js";
|
|
@@ -41,7 +41,7 @@ function findProvidersMissingApiKey(modelsJsonPath) {
|
|
|
41
41
|
}
|
|
42
42
|
export async function collectStatusSnapshot(options) {
|
|
43
43
|
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
|
44
|
-
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ??
|
|
44
|
+
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveBrowser();
|
|
45
45
|
const missingPiBits = validatePiInstallation(options.appRoot);
|
|
46
46
|
const webStatus = getPiWebAccessStatus();
|
|
47
47
|
const modelStatus = buildModelStatusSnapshotFromRecords(await getSupportedModelRecords(options.authPath), await getAvailableModelRecords(options.authPath), getCurrentModelSpec(options.settingsPath));
|
|
@@ -105,7 +105,7 @@ export async function runDoctor(options) {
|
|
|
105
105
|
const supportedModels = await getSupportedModelRecords(options.authPath);
|
|
106
106
|
const modelStatus = await collectStatusSnapshot(options);
|
|
107
107
|
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
|
108
|
-
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ??
|
|
108
|
+
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveBrowser();
|
|
109
109
|
const missingPiBits = validatePiInstallation(options.appRoot);
|
|
110
110
|
printPanel("Feynman Doctor", [
|
|
111
111
|
"Checks config, auth, runtime wiring, and preview dependencies.",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFile, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, delimiter } from "node:path";
|
|
4
5
|
const isWindows = process.platform === "win32";
|
|
6
|
+
const isLinux = process.platform === "linux";
|
|
5
7
|
const programFiles = process.env.PROGRAMFILES ?? "C:\\Program Files";
|
|
6
8
|
const localAppData = process.env.LOCALAPPDATA ?? "";
|
|
7
9
|
export const PANDOC_FALLBACK_PATHS = isWindows
|
|
@@ -16,14 +18,35 @@ export const BROWSER_FALLBACK_PATHS = isWindows
|
|
|
16
18
|
`${programFiles} (x86)\\Google\\Chrome\\Application\\chrome.exe`,
|
|
17
19
|
`${localAppData}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
18
20
|
`${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
21
|
+
`${programFiles} (x86)\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
19
22
|
`${programFiles}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
23
|
+
`${localAppData}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
20
24
|
]
|
|
21
|
-
:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
: isLinux
|
|
26
|
+
? [
|
|
27
|
+
"/usr/bin/google-chrome",
|
|
28
|
+
"/usr/bin/google-chrome-stable",
|
|
29
|
+
"/usr/bin/chromium",
|
|
30
|
+
"/usr/bin/chromium-browser",
|
|
31
|
+
"/snap/bin/chromium",
|
|
32
|
+
"/usr/bin/microsoft-edge",
|
|
33
|
+
"/usr/bin/brave-browser",
|
|
34
|
+
"/opt/google/chrome/chrome",
|
|
35
|
+
]
|
|
36
|
+
: ["/Applications", `${homedir()}/Applications`].flatMap((dir) => [
|
|
37
|
+
`${dir}/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
38
|
+
`${dir}/Chromium.app/Contents/MacOS/Chromium`,
|
|
39
|
+
`${dir}/Brave Browser.app/Contents/MacOS/Brave Browser`,
|
|
40
|
+
`${dir}/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`,
|
|
41
|
+
]);
|
|
42
|
+
export const BROWSER_COMMANDS = [
|
|
43
|
+
"google-chrome",
|
|
44
|
+
"google-chrome-stable",
|
|
45
|
+
"chromium",
|
|
46
|
+
"chromium-browser",
|
|
47
|
+
"microsoft-edge",
|
|
48
|
+
"brave-browser",
|
|
49
|
+
];
|
|
27
50
|
export const MERMAID_FALLBACK_PATHS = isWindows
|
|
28
51
|
? []
|
|
29
52
|
: ["/opt/homebrew/bin/mmdc", "/usr/local/bin/mmdc"];
|
|
@@ -81,11 +104,32 @@ export async function resolveExecutableAsync(name, fallbackPaths = []) {
|
|
|
81
104
|
});
|
|
82
105
|
});
|
|
83
106
|
}
|
|
107
|
+
export function resolveBrowser({ fallbackPaths = BROWSER_FALLBACK_PATHS, fileExists = existsSync, resolveCommand = (name) => resolveExecutable(name), } = {}) {
|
|
108
|
+
const fallback = fallbackPaths.find((candidate) => fileExists(candidate));
|
|
109
|
+
if (fallback) {
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
112
|
+
for (const name of BROWSER_COMMANDS) {
|
|
113
|
+
const resolved = resolveCommand(name);
|
|
114
|
+
if (resolved) {
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
export async function resolveBrowserAsync() {
|
|
121
|
+
const fallback = findInFallbackPaths(BROWSER_FALLBACK_PATHS);
|
|
122
|
+
if (fallback) {
|
|
123
|
+
return fallback;
|
|
124
|
+
}
|
|
125
|
+
const resolved = await Promise.all(BROWSER_COMMANDS.map((name) => resolveExecutableAsync(name)));
|
|
126
|
+
return resolved.find(Boolean);
|
|
127
|
+
}
|
|
84
128
|
export async function resolveAllExecutables() {
|
|
85
129
|
const [pandoc, mermaid, browser] = await Promise.all([
|
|
86
130
|
resolveExecutableAsync("pandoc", PANDOC_FALLBACK_PATHS),
|
|
87
131
|
resolveExecutableAsync("mmdc", MERMAID_FALLBACK_PATHS),
|
|
88
|
-
|
|
132
|
+
resolveBrowserAsync(),
|
|
89
133
|
]);
|
|
90
134
|
return { pandoc, mermaid, browser };
|
|
91
135
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
|
|
3
4
|
// Most failed research runs end on a provider error (quota, a key, a model the
|
|
@@ -24,9 +25,18 @@ export function modelErrorHint(provider: string, error: string): string | undefi
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function registerModelErrorHint(pi: ExtensionAPI): void {
|
|
27
|
-
|
|
28
|
+
// Pi retries rate limits and outages itself; hint only about the error the
|
|
29
|
+
// run actually ended on, once it has settled.
|
|
30
|
+
let lastError: AssistantMessage | undefined;
|
|
31
|
+
pi.on("message_end", (event) => {
|
|
28
32
|
const message = event.message;
|
|
29
|
-
if (message.role !== "assistant"
|
|
33
|
+
if (message.role !== "assistant") return;
|
|
34
|
+
lastError = message.stopReason === "error" ? message : undefined;
|
|
35
|
+
});
|
|
36
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
37
|
+
const message = lastError;
|
|
38
|
+
lastError = undefined;
|
|
39
|
+
if (!message || event.aborted || !ctx.hasUI) return;
|
|
30
40
|
const hint = modelErrorHint(message.provider, message.errorMessage ?? "");
|
|
31
41
|
ctx.ui.notify(
|
|
32
42
|
`${hint ?? `${message.provider} returned an error.`} If this looks like a Feynman bug, report it at https://github.com/Companion-Inc/feynman/issues (Pi's /bug goes to Pi's developers).`,
|
|
@@ -252,7 +252,8 @@ export function registerResearchTelemetry(
|
|
|
252
252
|
$ai_cache_creation_input_tokens: message.usage.cacheWrite,
|
|
253
253
|
// Pi reports input tokens with cache reads and writes already subtracted.
|
|
254
254
|
$ai_cache_reporting_exclusive: true,
|
|
255
|
-
|
|
255
|
+
// Pi times each response itself; older messages may lack it.
|
|
256
|
+
$ai_latency: message.durationMs !== undefined ? message.durationMs / 1000 : requestStartedAt === undefined ? undefined : (now() - requestStartedAt) / 1000,
|
|
256
257
|
$ai_http_status: httpStatus,
|
|
257
258
|
$ai_is_error: message.stopReason === "error",
|
|
258
259
|
$ai_error: message.stopReason === "error" ? errorText(message.errorMessage) : undefined,
|
|
@@ -264,8 +265,8 @@ export function registerResearchTelemetry(
|
|
|
264
265
|
if (run) run.stopReason = message.stopReason;
|
|
265
266
|
});
|
|
266
267
|
|
|
267
|
-
pi.on("agent_settled", (
|
|
268
|
-
finishRun(ctx.cwd, stopStatus(run?.stopReason));
|
|
268
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
269
|
+
finishRun(ctx.cwd, event.aborted ? "aborted" : stopStatus(run?.stopReason));
|
|
269
270
|
});
|
|
270
271
|
|
|
271
272
|
pi.on("session_shutdown", async (event, ctx) => {
|
package/metadata/commands.mjs
CHANGED
|
@@ -151,7 +151,7 @@ export const legacyFlags = [
|
|
|
151
151
|
{ usage: "--alpha-login", description: "Sign in to alphaXiv and exit." },
|
|
152
152
|
{ usage: "--alpha-logout", description: "Clear alphaXiv auth and exit." },
|
|
153
153
|
{ usage: "--alpha-status", description: "Show alphaXiv auth status and exit." },
|
|
154
|
-
{ usage: "--model <
|
|
154
|
+
{ usage: "--model <model>", description: "Force an approved research model: provider/model, a bare ID, or a short name; add :high etc. for thinking." },
|
|
155
155
|
{ usage: "--service-tier <tier>", description: "Override request service tier for this run." },
|
|
156
156
|
{ usage: "--thinking <level>", description: "Set thinking level: off | minimal | low | medium | high | xhigh | max." },
|
|
157
157
|
{ usage: "--cwd <path>", description: "Set the working directory for tools." },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companion-ai/feynman",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.42",
|
|
4
4
|
"description": "The open source AI research agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -79,16 +79,20 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@clack/prompts": "^1.8.1",
|
|
81
81
|
"@companion-ai/alpha-hub": "0.1.7",
|
|
82
|
+
"@earendil-works/pi-codemode": "1.2.0",
|
|
83
|
+
"@earendil-works/pi-mcp": "1.2.0",
|
|
84
|
+
"@earendil-works/pi-telemetry": "1.2.0",
|
|
82
85
|
"fast-xml-parser": "5.11.1",
|
|
83
86
|
"pi-btw": "0.8.0",
|
|
84
87
|
"pi-docparser": "4.0.0",
|
|
85
|
-
"pi-subagents": "0.
|
|
88
|
+
"pi-subagents": "0.78.0",
|
|
86
89
|
"pi-web-access": "0.39.0",
|
|
87
90
|
"posthog-node": "^5.53.0"
|
|
88
91
|
},
|
|
89
92
|
"peerDependencies": {
|
|
90
|
-
"@earendil-works/pi-
|
|
91
|
-
"@earendil-works/pi-
|
|
93
|
+
"@earendil-works/pi-agent-core": "1.2.0",
|
|
94
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
95
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
92
96
|
"@earendil-works/pi-tui": "*",
|
|
93
97
|
"typebox": "*"
|
|
94
98
|
},
|