@companion-ai/feynman 0.5.15 → 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.
@@ -3,7 +3,6 @@ name: researcher
3
3
  description: Gather primary evidence across papers, web sources, repos, docs, and local artifacts.
4
4
  thinking: high
5
5
  tools: read, write, edit, bash, grep, find, ls, web_search, fetch_content, get_search_content, feynman_science_database_search, hf_dataset_info, hf_repo_files, hf_repo_read_file
6
- output: research.md
7
6
  defaultProgress: true
8
7
  async: true
9
8
  ---
@@ -94,7 +93,7 @@ Numbered list matching the evidence table:
94
93
  - If you were assigned multiple questions, track them explicitly in the file and mark each as `done`, `blocked`, or `needs follow-up`. Do not silently skip questions.
95
94
 
96
95
  ## Output contract
97
- - Save to the output path specified by the parent (default: `research.md`).
96
+ - Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
98
97
  - Minimum viable output: evidence table with ≥5 numbered entries, findings with inline references, and a numbered Sources section.
99
98
  - Include a short `Coverage Status` section listing what you checked directly, what remains uncertain, and any tasks you could not complete.
100
99
  - Write to the file and pass a lightweight reference back — do not dump full content into the parent context.
@@ -2,7 +2,6 @@
2
2
  name: reviewer
3
3
  description: Run tough but constructive internal research critique of an AI research artifact.
4
4
  thinking: high
5
- output: review.md
6
5
  defaultProgress: true
7
6
  ---
8
7
 
@@ -88,5 +87,5 @@ Reference the weakness/question IDs from Part 1 so annotations link back to the
88
87
  - End with a `Sources` section containing direct URLs for anything additionally inspected during review.
89
88
 
90
89
  ## Output contract
91
- - Save the main artifact to the output path specified by the parent (default: `review.md`).
90
+ - Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
92
91
  - The review must contain both the structured review AND inline annotations.
@@ -3,7 +3,6 @@ name: verifier
3
3
  description: Post-process a draft to add inline citations and verify every source URL.
4
4
  thinking: medium
5
5
  tools: read, bash, grep, find, ls, write, edit, web_search, fetch_content, get_search_content, feynman_science_database_search
6
- output: cited.md
7
6
  defaultProgress: true
8
7
  async: true
9
8
  ---
@@ -57,6 +56,6 @@ Before saving the final document, scan for:
57
56
  For each item, verify that it maps to a source URL, research note, raw artifact path, or script path. If not, remove it or replace it with a TODO. Add a short `Removed Unsupported Claims` section only when you remove material.
58
57
 
59
58
  ## Output contract
60
- - Save to the output path specified by the parent (default: `cited.md`).
59
+ - Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
61
60
  - The output is the complete final document — same structure as the input draft, but with inline citations added throughout and a verified Sources section.
62
61
  - Do not change the intended structure of the draft, but you may delete or soften unsupported factual claims when necessary to maintain integrity.
@@ -3,7 +3,6 @@ name: writer
3
3
  description: Turn research notes into clear, structured briefs and drafts.
4
4
  thinking: medium
5
5
  tools: read, bash, grep, find, ls, write, edit
6
- output: draft.md
7
6
  defaultProgress: true
8
7
  ---
9
8
 
@@ -52,5 +51,5 @@ Unresolved issues, disagreements between sources, gaps in evidence.
52
51
  - Before finishing, do a result-provenance sweep for numeric results, figures, charts, benchmarks, tables, and images.
53
52
 
54
53
  ## Output contract
55
- - Save the main artifact to the specified output path (default: `draft.md`).
54
+ - Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
56
55
  - Focus on clarity, structure, and evidence traceability.
package/RELEASES.md CHANGED
@@ -6,6 +6,22 @@ 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
+
17
+ ## v0.5.16 - 2026-09-30
18
+
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.
20
+ - `feynman packages remove <preset>` (also `rm` and `uninstall`) removes an optional package such as `memory`.
21
+ - On Windows, Feynman no longer fails to start when another Feynman start is copying `keybindings.json` (`EBUSY: resource busy or locked`).
22
+ - pi-web-access 0.34.0: `fetch_content` falls back to your other fetch providers on Cloudflare "Just a moment..." pages, web tools start available on models such as DeepSeek without costing a prompt-cache miss, and its undici moves to 8.11.2.
23
+ - Security: three brace-expansion advisories published on 2026-09-29 affect the 5.0.9 that Pi 0.99.1's shrinkwrap locks, which Feynman cannot override. Pi uses it only for package filters and model patterns from your own settings and flags; the next Pi release that ships 5.0.12 replaces it. pi-subagents has still not released its undici fix.
24
+
9
25
  ## v0.5.15 - 2026-09-29
10
26
 
11
27
  - **Pi 0.99.1, pinned.** Feynman accepted any Pi version, so npm installs picked up Pi 0.99.1 the day it was released, untested, and Pi 0.99 warns on every launch about how Feynman declared it. Feynman now runs on Pi 0.99.1, which adds MCP server support (`/mcp`), declared the way Pi asks, and pinned so new Pi releases arrive only after they pass Feynman's checks. If npm has `legacy-peer-deps` on, it skips Pi; `feynman` now says to reinstall with `--legacy-peer-deps=false` instead of failing on a missing package.
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, readFileSync, renameSync, rmSync } from "node:fs";
2
+ import { constants, copyFileSync, existsSync, readFileSync, renameSync, rmSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { getBootstrapStatePath } from "../config/paths.js";
5
5
  const LEGACY_SYNCED_SCOPES = { themes: "themes", agents: "agents", skills: "skills" };
@@ -41,6 +41,12 @@ export function ensureFeynmanAgentDir(appRoot, home, agentDir) {
41
41
  }
42
42
  const keybindingsPath = resolve(agentDir, "keybindings.json");
43
43
  if (!existsSync(keybindingsPath)) {
44
- copyFileSync(resolve(appRoot, ".feynman", "config", "keybindings.json"), keybindingsPath);
44
+ try {
45
+ copyFileSync(resolve(appRoot, ".feynman", "config", "keybindings.json"), keybindingsPath, constants.COPYFILE_EXCL);
46
+ }
47
+ catch {
48
+ // Another Feynman start may be writing it (Windows reports EBUSY). Pi
49
+ // uses its default keys without the file, and the next start copies it.
50
+ }
45
51
  }
46
52
  }
package/dist/cli.js CHANGED
@@ -18,9 +18,9 @@ import { verifyAlphaAuthStatus } from "./alpha-auth-status.js";
18
18
  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
- import { installPiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
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";
@@ -267,18 +267,27 @@ async function handlePackagesCommand(subcommand, args, piOptions) {
267
267
  printInfo(`Install with: feynman packages install <${optionalPresets.map((preset) => preset.name).join("|")}>`);
268
268
  return;
269
269
  }
270
- if (subcommand !== "install") {
271
- throw new Error(`Unknown packages command: ${subcommand}`);
270
+ const remove = subcommand === "remove" || subcommand === "rm" || subcommand === "uninstall";
271
+ if (subcommand !== "install" && !remove) {
272
+ throw new Error(`Unknown packages command: ${subcommand}\nUse: feynman packages list, install <preset>, or remove <preset>.`);
272
273
  }
273
274
  const target = args[0];
274
275
  if (!target) {
275
- throw new Error(`Usage: feynman packages install <${listOptionalPackagePresets().map((preset) => preset.name).join("|")}>`);
276
+ throw new Error(`Usage: feynman packages ${remove ? "remove" : "install"} <${listOptionalPackagePresets().map((preset) => preset.name).join("|")}>`);
276
277
  }
277
278
  const presetName = normalizeOptionalPackagePresetName(target);
278
279
  if (!presetName) {
279
280
  throw new Error(`Unknown package preset: ${target}`);
280
281
  }
281
282
  const source = resolvePackageSource(presetName);
283
+ if (remove) {
284
+ if (!configuredSources.has(source)) {
285
+ console.log(`${source} is not installed`);
286
+ return;
287
+ }
288
+ process.exitCode = await removePiPackage(piOptions, source);
289
+ return;
290
+ }
282
291
  if (configuredSources.has(source)) {
283
292
  console.log(`${source} already installed`);
284
293
  return;
@@ -672,6 +681,12 @@ async function runMain(input) {
672
681
  preLaunchNotice = buildLocalModelWorkflowNotice(effectiveSpec, command);
673
682
  }
674
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
+ }
675
690
  await launchPiChat({
676
691
  ...piOptions,
677
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;
@@ -27,6 +27,9 @@ export function resolvePackageSource(name) {
27
27
  export async function installPiPackage(options, source) {
28
28
  return runPi(options, ["install", source], buildPiEnv(options));
29
29
  }
30
+ export async function removePiPackage(options, source) {
31
+ return runPi(options, ["remove", source], buildPiEnv(options));
32
+ }
30
33
  export async function updatePiPackages(options, source) {
31
34
  return runPi(options, source ? ["update", source] : ["update", "--extensions"], buildPiEnv(options));
32
35
  }
@@ -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),
@@ -95,7 +95,13 @@ function prepareSubagentDefaults(settingsPath) {
95
95
  throw new Error(`Invalid subagent config at ${path}: ${key} must be a boolean. The file was not changed.`);
96
96
  }
97
97
  }
98
- if (missions.enabled !== undefined && config.fleetView !== undefined && config.asyncByDefault !== undefined) {
98
+ if (config.singleRunOutputBaseDir !== undefined && typeof config.singleRunOutputBaseDir !== "string") {
99
+ throw new Error(`Invalid subagent config at ${path}: singleRunOutputBaseDir must be a string. The file was not changed.`);
100
+ }
101
+ if (missions.enabled !== undefined &&
102
+ config.fleetView !== undefined &&
103
+ config.asyncByDefault !== undefined &&
104
+ config.singleRunOutputBaseDir !== undefined) {
99
105
  return undefined;
100
106
  }
101
107
  const next = {
@@ -103,6 +109,10 @@ function prepareSubagentDefaults(settingsPath) {
103
109
  missions: { ...missions, enabled: missions.enabled ?? false },
104
110
  fleetView: config.fleetView ?? false,
105
111
  asyncByDefault: config.asyncByDefault ?? true,
112
+ // pi-subagents otherwise saves a relative `output` such as
113
+ // outputs/.drafts/x.md under the session's subagent-artifacts folder, where
114
+ // the lead agent never looks. "." resolves against Pi's cwd, the workspace.
115
+ singleRunOutputBaseDir: config.singleRunOutputBaseDir ?? ".",
106
116
  };
107
117
  return { path, original: existing?.source, content: `${JSON.stringify(next, null, 2)}\n` };
108
118
  }
@@ -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
@@ -137,6 +137,7 @@ export const cliCommandSections = [
137
137
  commands: [
138
138
  { usage: "feynman packages list", description: "Show core and optional Pi package presets." },
139
139
  { usage: "feynman packages install <preset>", description: "Install optional package presets on demand." },
140
+ { usage: "feynman packages remove <preset>", description: "Remove an installed optional package preset." },
140
141
  { usage: "feynman search status", description: "Show Pi web-access status and config path." },
141
142
  { usage: "feynman search set <provider> [api-key]", description: "Set the web search provider and optionally save its API key." },
142
143
  { usage: "feynman search clear", description: "Reset web search provider to auto while preserving API keys." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.15",
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",
@@ -83,7 +83,7 @@
83
83
  "pi-btw": "0.6.1",
84
84
  "pi-docparser": "4.0.0",
85
85
  "pi-subagents": "0.73.1",
86
- "pi-web-access": "0.33.0",
86
+ "pi-web-access": "0.34.0",
87
87
  "posthog-node": "^5.53.0",
88
88
  "undici": "^8.11.2"
89
89
  },
@@ -76,13 +76,13 @@ Example shape:
76
76
 
77
77
  ```json
78
78
  {
79
- "workflowScript": "return await runs.all([{key:'web',agent:'researcher',task:'Read outputs/.plans/<slug>-T1.md and write <slug>-research-web.md.',output:'<slug>-research-web.md'},{key:'papers',agent:'researcher',task:'Read outputs/.plans/<slug>-T2.md and write <slug>-research-papers.md.',output:'<slug>-research-papers.md'}]);",
79
+ "workflowScript": "return await runs.all([{key:'web',agent:'researcher',task:'Read outputs/.plans/<slug>-T1.md and write outputs/.drafts/<slug>-research-web.md.',output:'outputs/.drafts/<slug>-research-web.md'},{key:'papers',agent:'researcher',task:'Read outputs/.plans/<slug>-T2.md and write outputs/.drafts/<slug>-research-papers.md.',output:'outputs/.drafts/<slug>-research-papers.md'}]);",
80
80
  "async": true,
81
81
  "globalConcurrencyLimit": 4
82
82
  }
83
83
  ```
84
84
 
85
- Continue independent work after launch, then consume completion results before synthesis. Use the returned output references to locate managed child files; verify them on disk and copy them to the planned research paths when necessary. After evidence gathering, update the plan ledger and verification log. If research failed, record exactly what failed and proceed with a blocked or partial draft.
85
+ Continue independent work after launch, then consume completion results before synthesis. Relative output paths resolve against the workspace, so each child writes its declared path; verify each file on disk, and use the returned output reference for any that is missing. After evidence gathering, update the plan ledger and verification log. If research failed, record exactly what failed and proceed with a blocked or partial draft.
86
86
 
87
87
  ## Step 4: Draft
88
88
 
@@ -123,7 +123,7 @@ Use this shape:
123
123
  }
124
124
  ```
125
125
 
126
- Wait for the verifier's completion result before review, not merely the async launch receipt. Verify on disk that `outputs/.drafts/<slug>-cited.md` exists. If managed output routing wrote elsewhere, use the returned output reference to find the cited file and move or copy it to `outputs/.drafts/<slug>-cited.md`.
126
+ Wait for the verifier's completion result before review, not merely the async launch receipt. Verify on disk that `outputs/.drafts/<slug>-cited.md` exists. If it is missing, use the returned output reference to find the cited file and copy it there.
127
127
 
128
128
  ## Step 6: Review
129
129
 
@@ -142,7 +142,7 @@ Use this shape:
142
142
  "agent": "reviewer",
143
143
  "async": true,
144
144
  "task": "Verify outputs/.drafts/<slug>-cited.md. Flag unsupported claims, logical gaps, single-source critical claims, and overstated confidence. This is a verification pass, not a peer review.",
145
- "output": "<slug>-verification.md"
145
+ "output": "outputs/.drafts/<slug>-verification.md"
146
146
  }
147
147
  ```
148
148
 
package/prompts/lit.md CHANGED
@@ -12,7 +12,7 @@ Derive a short slug from the topic (lowercase, hyphens, no filler words, ≤5 wo
12
12
 
13
13
  1. **Plan** — Outline the scope: key questions, source types to search (papers, web, repos), time period, expected sections, and a small task ledger plus verification log. When the input appears to name a lab, PI, author, institution lab page, or author profile, run the review as a publication-corpus review: find the lab/author identity first, collect the reachable publication list, then map the research trajectory across that corpus. Write the plan to `outputs/.plans/<slug>.md`. Briefly summarize the plan to the user and continue immediately in the same turn with the next tool call; never end a turn with a message that only announces what you will do next. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
14
14
  - When updating the plan ledger later, keep edits small and valid. If an `edit` tool call fails with a JSON parse error or the replacement would require embedding a large markdown block, rewrite the full corrected plan file with the file-writing tool instead, then continue to final artifact/provenance verification.
15
- 2. **Gather** — Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis. For narrow topics, search directly. Researcher outputs go to `<slug>-research-*.md`. For publication-corpus reviews, the lead agent owns identity resolution and writes `notes/<slug>-publications.md` with reachable titles, years, venues, URLs/DOIs, and gaps before delegating trajectory synthesis. Prefer lab publication pages, author profiles, arXiv/OpenReview/Semantic Scholar pages, and paper search results that expose stable source URLs. Do not silently skip assigned questions; mark them `done`, `blocked`, or `superseded`.
15
+ 2. **Gather** — Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis. For narrow topics, search directly. Researcher outputs go to `outputs/.drafts/<slug>-research-*.md`; pass that path as each researcher's `output`. For publication-corpus reviews, the lead agent owns identity resolution and writes `notes/<slug>-publications.md` with reachable titles, years, venues, URLs/DOIs, and gaps before delegating trajectory synthesis. Prefer lab publication pages, author profiles, arXiv/OpenReview/Semantic Scholar pages, and paper search results that expose stable source URLs. Do not silently skip assigned questions; mark them `done`, `blocked`, or `superseded`.
16
16
  3. **Synthesize** — Separate consensus, disagreements, and open questions. For publication-corpus reviews, also identify 3-5 research trajectories and the 3-5 papers that most changed the corpus direction; rank them by contrastive originality, methodology strength, and relationship to prior art rather than by author prestige alone. When useful, propose concrete next experiments or follow-up reading. Use Mermaid diagrams for taxonomies, method pipelines, or lab trajectory maps when the structure is source-supported and changes the reader's research decision. Keep the output to research evidence, source coverage, and next research decisions; do not create non-research operational artifacts from a literature review run.
17
17
  4. **Cite** — Spawn the `verifier` agent to add inline citations and verify every source URL in the draft.
18
18
  5. **Verify** — Spawn the `reviewer` agent to check the cited draft for unsupported claims, logical gaps, zombie sections, and single-source critical findings. Fix FATAL issues before delivering. Note MAJOR issues in Open Questions. If FATAL issues were found, run one more verification pass after the fixes.
@@ -126,7 +126,7 @@ Use one async workflow with a stable key for each chunk and a concurrency limit
126
126
  }
127
127
  ```
128
128
 
129
- Consume completion results before aggregation. `runs.all` returns an ordered array including ordinary child failures; inspect each `ok` and error. A validation or infrastructure failure can still fail the workflow. Use actual returned output/artifact references to locate child files, verify them on disk, and copy them to the expected chunk-summary paths when necessary. Record missing or failed chunks rather than treating an async launch receipt as a finished summary.
129
+ Consume completion results before aggregation. `runs.all` returns an ordered array including ordinary child failures; inspect each `ok` and error. A validation or infrastructure failure can still fail the workflow. Relative output paths resolve against the workspace; verify each chunk summary on disk, and use the returned output reference for any that is missing. Record missing or failed chunks rather than treating an async launch receipt as a finished summary.
130
130
 
131
131
  ### 3d. Aggregate
132
132