@companion-ai/feynman 0.5.40 → 0.5.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASES.md +11 -0
- package/dist/cli.js +13 -3
- package/dist/model/catalog.js +0 -24
- package/dist/model/commands.js +21 -13
- package/dist/pi/launch.js +2 -1
- package/dist/pi/runtime.js +2 -2
- 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 +6 -1
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,17 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.41 - 2026-10-12
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
- **`--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.
|
|
14
|
+
- **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.
|
|
15
|
+
- A provider error hint now appears once the run has stopped, not in the middle of an automatic retry.
|
|
16
|
+
- Launching Feynman no longer erases your terminal's scrollback.
|
|
17
|
+
- The llama.cpp provider is recognized as a local model.
|
|
18
|
+
- **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).
|
|
19
|
+
|
|
9
20
|
## v0.5.40 - 2026-10-12
|
|
10
21
|
|
|
11
22
|
- **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";
|
|
@@ -421,7 +421,9 @@ export async function main() {
|
|
|
421
421
|
...commandTelemetry,
|
|
422
422
|
duration_ms: durationMs,
|
|
423
423
|
exit_code: exitCode,
|
|
424
|
-
|
|
424
|
+
// A fixed reason for a deliberate non-zero exit, so it isn't read as a crash.
|
|
425
|
+
...(exitReason ? { exit_reason: exitReason } : {}),
|
|
426
|
+
...(exitCode === 0 || exitReason ? {} : { pi_stderr: getLastPiStderr() }),
|
|
425
427
|
};
|
|
426
428
|
captureTelemetryEvent(exitCode === 0 ? "feynman_command_completed" : "feynman_command_failed", completeProperties);
|
|
427
429
|
}
|
|
@@ -440,6 +442,8 @@ export async function main() {
|
|
|
440
442
|
await shutdownPostHogTelemetry();
|
|
441
443
|
}
|
|
442
444
|
}
|
|
445
|
+
// Set when Feynman exits non-zero on purpose (see the completion telemetry in main).
|
|
446
|
+
let exitReason;
|
|
443
447
|
async function runMain(input) {
|
|
444
448
|
const { appRoot, feynmanVersion } = input;
|
|
445
449
|
const bundledSettingsPath = resolve(appRoot, ".feynman", "settings.json");
|
|
@@ -641,10 +645,14 @@ async function runMain(input) {
|
|
|
641
645
|
throw new Error(`Pro-class model disabled: ${requestedExplicitModelSpec}. Choose an approved research model.`);
|
|
642
646
|
}
|
|
643
647
|
const modelRuntime = await createModelRuntime(feynmanAuthPath);
|
|
644
|
-
const canonicalModelSpec =
|
|
648
|
+
const canonicalModelSpec = resolveExplicitModelSpec(requestedExplicitModelSpec, modelRuntime);
|
|
645
649
|
if (!canonicalModelSpec) {
|
|
646
650
|
throw new Error(`Unknown model: ${requestedExplicitModelSpec}. Run \`feynman model list\` to see available models.`);
|
|
647
651
|
}
|
|
652
|
+
// Check the resolved model too: a short name can resolve to a Pro-class one.
|
|
653
|
+
if (isProClassModelSpec(canonicalModelSpec.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, ""))) {
|
|
654
|
+
throw new Error(`Pro-class model disabled: ${canonicalModelSpec}. Choose an approved research model.`);
|
|
655
|
+
}
|
|
648
656
|
explicitModelSpec = canonicalModelSpec;
|
|
649
657
|
}
|
|
650
658
|
const workflowCommandNames = new Set(readPromptSpecs(appRoot).filter((s) => s.topLevelCli).map((s) => s.name));
|
|
@@ -666,6 +674,7 @@ async function runMain(input) {
|
|
|
666
674
|
if (!getCurrentModelSpec(feynmanSettingsPath)) {
|
|
667
675
|
console.error(noModelMessage(promptGiven));
|
|
668
676
|
process.exitCode = 1;
|
|
677
|
+
exitReason = "no_model";
|
|
669
678
|
return;
|
|
670
679
|
}
|
|
671
680
|
await ensureFeynmanSettings(feynmanSettingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, feynmanAuthPath);
|
|
@@ -674,6 +683,7 @@ async function runMain(input) {
|
|
|
674
683
|
&& (await getAuthenticatedModelRecords(feynmanAuthPath)).length === 0) {
|
|
675
684
|
console.error(noModelMessage(promptGiven));
|
|
676
685
|
process.exitCode = 1;
|
|
686
|
+
exitReason = "no_model";
|
|
677
687
|
return;
|
|
678
688
|
}
|
|
679
689
|
// 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,7 +1,7 @@
|
|
|
1
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { readFileSync, writeFileSync } 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
6
|
import { readJson } from "../pi/settings.js";
|
|
7
7
|
import { promptChoice, promptSecret, promptSelect, promptText, SetupCancelledError, validateApiKeyInput } from "../setup/prompts.js";
|
|
@@ -125,7 +125,8 @@ function normalizeCustomProviderBaseUrl(api, baseUrl) {
|
|
|
125
125
|
function isLocalBaseUrl(baseUrl) {
|
|
126
126
|
return /^(https?:\/\/)?(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|\/|$)/i.test(baseUrl);
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
// "llama.cpp" is the id of Pi's bundled llama.cpp provider.
|
|
129
|
+
const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
|
|
129
130
|
export function isLocalModelProvider(authPath, providerId) {
|
|
130
131
|
if (!providerId) {
|
|
131
132
|
return false;
|
|
@@ -653,6 +654,7 @@ async function configureApiKeyProvider(authPath, providerId) {
|
|
|
653
654
|
printWarning(result.error);
|
|
654
655
|
}
|
|
655
656
|
}
|
|
657
|
+
await refreshProviderModels(modelRuntime, provider.id);
|
|
656
658
|
return true;
|
|
657
659
|
}
|
|
658
660
|
async function resolveAvailableModelSpec(authPath, input) {
|
|
@@ -672,8 +674,8 @@ async function resolveAvailableModelSpec(authPath, input) {
|
|
|
672
674
|
// When multiple providers expose the same bare model ID, prefer providers the
|
|
673
675
|
// user explicitly configured in auth storage.
|
|
674
676
|
if (exactIdMatches.length > 1) {
|
|
675
|
-
const
|
|
676
|
-
const configuredProviders = new Set(
|
|
677
|
+
const credentials = await (await createModelRuntime(authPath)).listCredentials();
|
|
678
|
+
const configuredProviders = new Set(credentials.map((credential) => credential.providerId));
|
|
677
679
|
const configuredMatches = exactIdMatches.filter((model) => configuredProviders.has(model.provider));
|
|
678
680
|
if (configuredMatches.length === 1) {
|
|
679
681
|
return `${configuredMatches[0].provider}/${configuredMatches[0].id}`;
|
|
@@ -733,13 +735,11 @@ export async function authenticateModelProvider(authPath, settingsPath) {
|
|
|
733
735
|
}
|
|
734
736
|
// Sign in with ChatGPT sends a stable installation ID. Pi's own /login keeps it
|
|
735
737
|
// 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");
|
|
738
|
+
// Pi's own device ID, the one its /login sends, kept in the agent settings.
|
|
739
|
+
export async function getOrCreateDeviceId(authPath) {
|
|
740
|
+
const settings = SettingsManager.create(process.cwd(), dirname(authPath));
|
|
741
|
+
const deviceId = settings.getOrCreateDeviceId();
|
|
742
|
+
await settings.flush();
|
|
743
743
|
return deviceId;
|
|
744
744
|
}
|
|
745
745
|
// A provider answers invalid_grant when it rejects the authorization code, and
|
|
@@ -752,6 +752,12 @@ export function describeOAuthLoginFailure(providerId, error) {
|
|
|
752
752
|
const alternative = providerId === "openai" ? " Or sign in with `feynman model login openai-codex` instead." : "";
|
|
753
753
|
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
754
|
}
|
|
755
|
+
// Pi's /login refreshes the provider's model list from the network after
|
|
756
|
+
// signing in (OpenRouter keys get the models that key may use); do the same,
|
|
757
|
+
// so the recommended default comes from that list. A failure keeps the saved list.
|
|
758
|
+
async function refreshProviderModels(modelRuntime, providerId) {
|
|
759
|
+
await modelRuntime.refresh({ providers: [providerId], allowNetwork: true, signal: AbortSignal.timeout(15_000) }).catch(() => undefined);
|
|
760
|
+
}
|
|
755
761
|
export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
756
762
|
if (providerId) {
|
|
757
763
|
const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
|
|
@@ -776,6 +782,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
776
782
|
}
|
|
777
783
|
const modelRuntime = await createModelRuntime(authPath);
|
|
778
784
|
const abortController = new AbortController();
|
|
785
|
+
const deviceId = await getOrCreateDeviceId(authPath);
|
|
779
786
|
await modelRuntime.login(provider.id, "oauth", {
|
|
780
787
|
prompt: async (prompt) => {
|
|
781
788
|
if (prompt.type === "select") {
|
|
@@ -819,10 +826,11 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
819
826
|
}
|
|
820
827
|
},
|
|
821
828
|
signal: abortController.signal,
|
|
822
|
-
}, { getDeviceId: () =>
|
|
829
|
+
}, { getDeviceId: () => deviceId }).catch((error) => {
|
|
823
830
|
throw describeOAuthLoginFailure(provider.id, error);
|
|
824
831
|
});
|
|
825
832
|
printSuccess(`Model provider login complete: ${provider.id}`);
|
|
833
|
+
await refreshProviderModels(modelRuntime, provider.id);
|
|
826
834
|
await maybeSetRecommendedDefaultModel(settingsPath, authPath);
|
|
827
835
|
return true;
|
|
828
836
|
}
|
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,7 +1,7 @@
|
|
|
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 {
|
|
4
|
+
import { MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable, } from "../system/executables.js";
|
|
5
5
|
import { getPostHogChildEnv } 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).
|
|
@@ -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]
|
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.41",
|
|
4
4
|
"description": "The open source AI research agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@clack/prompts": "^1.8.1",
|
|
81
81
|
"@companion-ai/alpha-hub": "0.1.7",
|
|
82
|
+
"@earendil-works/chord": "1.1.0",
|
|
83
|
+
"@earendil-works/pi-codemode": "1.1.0",
|
|
84
|
+
"@earendil-works/pi-mcp": "1.1.0",
|
|
85
|
+
"@earendil-works/pi-telemetry": "1.1.0",
|
|
82
86
|
"fast-xml-parser": "5.11.1",
|
|
83
87
|
"pi-btw": "0.8.0",
|
|
84
88
|
"pi-docparser": "4.0.0",
|
|
@@ -87,6 +91,7 @@
|
|
|
87
91
|
"posthog-node": "^5.53.0"
|
|
88
92
|
},
|
|
89
93
|
"peerDependencies": {
|
|
94
|
+
"@earendil-works/pi-agent-core": "1.1.0",
|
|
90
95
|
"@earendil-works/pi-ai": "1.1.0",
|
|
91
96
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
92
97
|
"@earendil-works/pi-tui": "*",
|