@companion-ai/feynman 0.5.16 → 0.5.17
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 +8 -0
- package/dist/cli.js +7 -1
- package/dist/model/commands.js +23 -4
- package/dist/pi/runtime.js +29 -2
- package/dist/setup/doctor.js +5 -1
- package/dist/setup/prompts.js +15 -2
- package/extensions/research-tools/science-databases.ts +3 -0
- package/package.json +1 -1
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,14 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.17 - 2026-09-30
|
|
10
|
+
|
|
11
|
+
- **`feynman model login openai` works again.** Since Pi 0.99, Sign in with ChatGPT needs a stable ID for the installation, which Feynman did not pass, so the login failed at once with "requires a device ID (UUID)". Feynman now shares the `deviceId` Pi's own `/login` keeps in `~/.feynman/agent/settings.json`.
|
|
12
|
+
- **Windows finds per-user Git Bash.** Pi looks for Git Bash only under Program Files and on PATH, but a per-user Git for Windows install puts only `Git\cmd` on PATH, so shell commands failed with "No bash shell found" (about one in five Windows installs that run tools). Feynman now finds that Git Bash and gives it to Pi. When there is no Bash at all, Feynman says so before the session starts, and `feynman doctor` reports the `bash` it found.
|
|
13
|
+
- Pressing Enter at the "paste the authorization code / redirect URL" login prompt no longer ends the login with "Missing authorization code"; Feynman asks again, and finishing in the browser still works.
|
|
14
|
+
- API keys are now masked as you paste them during setup, and a key with non-ASCII characters (for example typed with a non-Latin keyboard layout) is rejected with an explanation instead of failing every request with "Cannot convert argument to a ByteString".
|
|
15
|
+
- A rejected `SEMANTIC_SCHOLAR_API_KEY` now says so instead of `request failed: 403 Forbidden`.
|
|
16
|
+
|
|
9
17
|
## v0.5.16 - 2026-09-30
|
|
10
18
|
|
|
11
19
|
- **Subagent research files land in the workspace.** When a workflow gave a subagent a relative output path such as `outputs/.drafts/<slug>-research-web.md`, pi-subagents saved it under `~/.feynman/sessions/subagent-artifacts/outputs/<run>/` instead, so the lead agent could not find the research notes, citations, or verification it asked for (about one in seven installs running research workflows hit this in the last week). Relative subagent outputs now resolve against the workspace, the researcher, reviewer, verifier, and writer no longer default to generic file names such as `research.md`, and `/deepresearch` and `/lit` name every subagent file under `outputs/.drafts/`. If you set `singleRunOutputBaseDir` in `~/.feynman/agent/extensions/subagent/config.json`, Feynman keeps your value.
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynman
|
|
|
20
20
|
import { getLastPiStderr, launchPiChat, runPi } from "./pi/launch.js";
|
|
21
21
|
import { installPiPackage, removePiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
|
|
22
22
|
import { canonicalizeModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
|
|
23
|
-
import { BUNDLED_PI_PACKAGES, buildPiEnv } from "./pi/runtime.js";
|
|
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";
|
|
26
26
|
import { buildModelStatusSnapshotFromRecords, getAuthenticatedModelRecords, isProClassModelSpec, getSupportedModelRecords, } from "./model/catalog.js";
|
|
@@ -681,6 +681,12 @@ async function runMain(input) {
|
|
|
681
681
|
preLaunchNotice = buildLocalModelWorkflowNotice(effectiveSpec, command);
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
+
if (process.platform === "win32" && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
|
|
685
|
+
const shellPath = readJson(feynmanSettingsPath).shellPath;
|
|
686
|
+
if (typeof shellPath !== "string" && !findWindowsBash()) {
|
|
687
|
+
preLaunchNotice = [preLaunchNotice, WINDOWS_BASH_MISSING_NOTICE].filter(Boolean).join("\n");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
684
690
|
await launchPiChat({
|
|
685
691
|
...piOptions,
|
|
686
692
|
mode,
|
package/dist/model/commands.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { exec as execCallback } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
3
5
|
import { promisify } from "node:util";
|
|
4
6
|
import { readJson } from "../pi/settings.js";
|
|
5
|
-
import { promptChoice, promptSelect, promptText } from "../setup/prompts.js";
|
|
7
|
+
import { promptChoice, promptSecret, promptSelect, promptText, validateApiKeyInput } from "../setup/prompts.js";
|
|
6
8
|
import { openUrl } from "../system/open-url.js";
|
|
7
9
|
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
|
8
10
|
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
|
|
@@ -624,7 +626,7 @@ async function configureApiKeyProvider(authPath, providerId) {
|
|
|
624
626
|
if (provider.envVar) {
|
|
625
627
|
printInfo(`Tip: to avoid writing secrets to disk, set ${provider.envVar} in your shell or .env.`);
|
|
626
628
|
}
|
|
627
|
-
const apiKey = await
|
|
629
|
+
const apiKey = await promptSecret("Paste API key (leave empty to use env var instead)", undefined, validateApiKeyInput);
|
|
628
630
|
if (!apiKey) {
|
|
629
631
|
if (provider.envVar) {
|
|
630
632
|
printInfo(`Set ${provider.envVar} and rerun setup (or run \`feynman model list\`).`);
|
|
@@ -729,6 +731,17 @@ export async function authenticateModelProvider(authPath, settingsPath) {
|
|
|
729
731
|
printInfo("Authentication cancelled.");
|
|
730
732
|
return false;
|
|
731
733
|
}
|
|
734
|
+
// Sign in with ChatGPT sends a stable installation ID. Pi's own /login keeps it
|
|
735
|
+
// as `deviceId` in the agent settings.json; share that one.
|
|
736
|
+
export function getOrCreateDeviceId(authPath) {
|
|
737
|
+
const settingsPath = resolve(dirname(authPath), "settings.json");
|
|
738
|
+
const settings = readJson(settingsPath);
|
|
739
|
+
if (typeof settings.deviceId === "string" && settings.deviceId)
|
|
740
|
+
return settings.deviceId;
|
|
741
|
+
const deviceId = randomUUID();
|
|
742
|
+
writeFileSync(settingsPath, `${JSON.stringify({ ...settings, deviceId }, null, 2)}\n`, "utf8");
|
|
743
|
+
return deviceId;
|
|
744
|
+
}
|
|
732
745
|
export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
733
746
|
if (providerId) {
|
|
734
747
|
const resolvedProvider = await resolveModelProviderForCommand(authPath, providerId);
|
|
@@ -762,7 +775,13 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
762
775
|
hint: option.description,
|
|
763
776
|
})), undefined, prompt.signal);
|
|
764
777
|
}
|
|
765
|
-
|
|
778
|
+
if (prompt.type === "secret") {
|
|
779
|
+
return promptSecret(prompt.message, prompt.signal);
|
|
780
|
+
}
|
|
781
|
+
// An empty answer here fails the login with "Missing authorization code";
|
|
782
|
+
// keep asking, since the browser callback can still finish meanwhile.
|
|
783
|
+
const required = prompt.type === "manual_code" ? "Paste the redirect URL or code, or finish signing in in the browser." : undefined;
|
|
784
|
+
return promptText(prompt.message, "", prompt.placeholder, prompt.signal, required);
|
|
766
785
|
},
|
|
767
786
|
notify: (event) => {
|
|
768
787
|
if (event.type === "auth_url") {
|
|
@@ -790,7 +809,7 @@ export async function loginModelProvider(authPath, providerId, settingsPath) {
|
|
|
790
809
|
}
|
|
791
810
|
},
|
|
792
811
|
signal: abortController.signal,
|
|
793
|
-
});
|
|
812
|
+
}, { getDeviceId: () => getOrCreateDeviceId(authPath) });
|
|
794
813
|
printSuccess(`Model provider login complete: ${provider.id}`);
|
|
795
814
|
await maybeSetRecommendedDefaultModel(settingsPath, authPath);
|
|
796
815
|
return true;
|
package/dist/pi/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { delimiter, dirname, resolve } from "node:path";
|
|
3
|
+
import { delimiter, dirname, resolve, win32 } from "node:path";
|
|
4
4
|
import { BROWSER_FALLBACK_PATHS, MERMAID_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, 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
|
|
@@ -116,6 +116,31 @@ export function buildPiArgs(options) {
|
|
|
116
116
|
}
|
|
117
117
|
return args;
|
|
118
118
|
}
|
|
119
|
+
// Pi looks for Git Bash only under Program Files and for bash.exe on PATH. A
|
|
120
|
+
// per-user Git for Windows install (%LOCALAPPDATA%\Programs\Git) puts only
|
|
121
|
+
// Git\cmd on PATH, so Pi reports "No bash shell found" although Git Bash is
|
|
122
|
+
// installed. `addDir` is the Git\bin directory to add to Pi's PATH in that case.
|
|
123
|
+
export function findWindowsBash(env = process.env, exists = existsSync) {
|
|
124
|
+
const pathDirs = (env.PATH ?? env.Path ?? "").split(";").filter(Boolean);
|
|
125
|
+
const known = [env.ProgramFiles, env["ProgramFiles(x86)"]]
|
|
126
|
+
.filter((dir) => Boolean(dir))
|
|
127
|
+
.map((dir) => win32.join(dir, "Git", "bin", "bash.exe"));
|
|
128
|
+
const found = [...known, ...pathDirs.map((dir) => win32.join(dir, "bash.exe"))].find(exists);
|
|
129
|
+
if (found)
|
|
130
|
+
return { path: found };
|
|
131
|
+
const gitRoots = [
|
|
132
|
+
// git.exe lives in Git\cmd or Git\mingw64\bin.
|
|
133
|
+
...pathDirs.filter((dir) => exists(win32.join(dir, "git.exe"))).flatMap((dir) => [win32.dirname(dir), win32.dirname(win32.dirname(dir))]),
|
|
134
|
+
...(env.LOCALAPPDATA ? [win32.join(env.LOCALAPPDATA, "Programs", "Git")] : []),
|
|
135
|
+
];
|
|
136
|
+
for (const root of gitRoots) {
|
|
137
|
+
const bash = win32.join(root, "bin", "bash.exe");
|
|
138
|
+
if (exists(bash))
|
|
139
|
+
return { path: bash, addDir: win32.join(root, "bin") };
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
export const WINDOWS_BASH_MISSING_NOTICE = "No Bash found: Feynman runs shell commands with Bash, so they will fail. Install Git for Windows (https://git-scm.com/download/win), or set shellPath in ~/.feynman/agent/settings.json to another bash.exe.";
|
|
119
144
|
export function buildPiEnv(options, executables) {
|
|
120
145
|
const binPath = [getFeynmanCommandShimDir(options.feynmanAgentDir), resolve(options.appRoot, "node_modules", ".bin")].join(delimiter);
|
|
121
146
|
const pandocPath = process.env.PANDOC_PATH ?? executables?.pandoc ?? resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
|
@@ -123,7 +148,9 @@ export function buildPiEnv(options, executables) {
|
|
|
123
148
|
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? executables?.browser ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS);
|
|
124
149
|
return {
|
|
125
150
|
...process.env,
|
|
126
|
-
PATH:
|
|
151
|
+
PATH: [binPath, process.env.PATH ?? "", process.platform === "win32" ? findWindowsBash()?.addDir : undefined]
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join(delimiter),
|
|
127
154
|
FEYNMAN_VERSION: options.feynmanVersion,
|
|
128
155
|
FEYNMAN_NODE_EXECUTABLE: process.execPath,
|
|
129
156
|
FEYNMAN_BIN_PATH: getFeynmanCliBinPath(options.appRoot),
|
package/dist/setup/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { verifyAlphaAuthStatus } from "../alpha-auth-status.js";
|
|
|
5
5
|
import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
|
|
6
6
|
import { BROWSER_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js";
|
|
7
7
|
import { readJson } from "../pi/settings.js";
|
|
8
|
-
import { validatePiInstallation } from "../pi/runtime.js";
|
|
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";
|
|
@@ -163,6 +163,10 @@ export async function runDoctor(options) {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
console.log(`pandoc: ${pandocPath ?? "missing"}`);
|
|
166
|
+
if (process.platform === "win32") {
|
|
167
|
+
const shellPath = settings.shellPath;
|
|
168
|
+
console.log(`bash: ${typeof shellPath === "string" ? `${shellPath} (shellPath)` : findWindowsBash()?.path ?? `missing. ${WINDOWS_BASH_MISSING_NOTICE}`}`);
|
|
169
|
+
}
|
|
166
170
|
console.log(`browser preview runtime: ${browserPath ?? "missing"}`);
|
|
167
171
|
for (const line of formatPiWebAccessDoctorLines()) {
|
|
168
172
|
console.log(line);
|
package/dist/setup/prompts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { confirm as clackConfirm, intro as clackIntro, isCancel, multiselect as clackMultiselect, outro as clackOutro, select as clackSelect, text as clackText, } from "@clack/prompts";
|
|
1
|
+
import { confirm as clackConfirm, intro as clackIntro, isCancel, multiselect as clackMultiselect, outro as clackOutro, password as clackPassword, select as clackSelect, text as clackText, } from "@clack/prompts";
|
|
2
2
|
export class SetupCancelledError extends Error {
|
|
3
3
|
constructor(message = "setup cancelled") {
|
|
4
4
|
super(message);
|
|
@@ -36,17 +36,30 @@ export async function promptOutro(message) {
|
|
|
36
36
|
ensureInteractiveTerminal();
|
|
37
37
|
clackOutro(message);
|
|
38
38
|
}
|
|
39
|
-
export async function promptText(question, defaultValue = "", placeholder, signal) {
|
|
39
|
+
export async function promptText(question, defaultValue = "", placeholder, signal, requiredMessage) {
|
|
40
40
|
ensureInteractiveTerminal();
|
|
41
41
|
const value = guardCancelled(await clackText({
|
|
42
42
|
message: question,
|
|
43
43
|
initialValue: defaultValue || undefined,
|
|
44
44
|
placeholder: placeholder ?? (defaultValue || undefined),
|
|
45
45
|
signal,
|
|
46
|
+
validate: requiredMessage ? (input) => (input?.trim() ? undefined : requiredMessage) : undefined,
|
|
46
47
|
}));
|
|
47
48
|
const normalized = String(value ?? "").trim();
|
|
48
49
|
return normalized || defaultValue;
|
|
49
50
|
}
|
|
51
|
+
export async function promptSecret(question, signal, validate) {
|
|
52
|
+
ensureInteractiveTerminal();
|
|
53
|
+
const value = guardCancelled(await clackPassword({ message: question, signal, validate: validate ? (input) => validate((input ?? "").trim()) : undefined }));
|
|
54
|
+
return String(value ?? "").trim();
|
|
55
|
+
}
|
|
56
|
+
// API keys are printable ASCII. A key typed with a non-Latin keyboard layout
|
|
57
|
+
// fails every request with "Cannot convert argument to a ByteString".
|
|
58
|
+
export function validateApiKeyInput(value) {
|
|
59
|
+
return /^[\x21-\x7e]*$/.test(value)
|
|
60
|
+
? undefined
|
|
61
|
+
: "API keys contain only ASCII letters, digits, and symbols. Check your keyboard input language and paste the key again.";
|
|
62
|
+
}
|
|
50
63
|
export async function promptSelect(question, options, initialValue, signal) {
|
|
51
64
|
ensureInteractiveTerminal();
|
|
52
65
|
const selection = guardCancelled(await clackSelect({
|
|
@@ -270,6 +270,9 @@ async function fetchSemanticScholar(url: URL): Promise<unknown> {
|
|
|
270
270
|
try {
|
|
271
271
|
return await fetchJson(url, headers);
|
|
272
272
|
} catch (error) {
|
|
273
|
+
if (apiKey && error instanceof ScienceDatabaseRequestError && error.status === 403) {
|
|
274
|
+
throw new ScienceDatabaseRequestError("Semantic Scholar rejected SEMANTIC_SCHOLAR_API_KEY (HTTP 403). Check the key, or unset it to use the shared pool; search with source openalex meanwhile.", 403);
|
|
275
|
+
}
|
|
273
276
|
if (!(error instanceof ScienceDatabaseRequestError) || error.status !== 429) throw error;
|
|
274
277
|
if (attempt >= 1) {
|
|
275
278
|
throw new ScienceDatabaseRequestError(apiKey
|