@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 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,
@@ -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 promptText("Paste API key (leave empty to use env var instead)", "");
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
- return promptText(prompt.message, "", prompt.placeholder, prompt.signal);
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;
@@ -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: `${binPath}${delimiter}${process.env.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),
@@ -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);
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",