@companion-ai/feynman 0.5.24 → 0.5.26

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,19 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.26 - 2026-10-05
10
+
11
+ - **GitHub Copilot users get a research model by default.** Copilot names Claude models with dots (`claude-opus-5.5`), which Feynman did not recognize, so a new Copilot login fell back to alphabetical order and set `claude-haiku-4.5` as the default; Copilot then rejected it with "The requested model is not supported" for several users. Feynman now picks Copilot's newest Claude Opus, then Sonnet, then GPT. If `feynman status` shows `github-copilot/claude-haiku-4.5` and you did not choose it, run `feynman model set` to pick another.
12
+ - `alpha_ask_paper` answers with the arXiv metadata and abstract, marked as a fallback, when alphaXiv does not answer (for example after the login expires), like `alpha_search` and `alpha_get_paper` already did.
13
+ - `-p` is short for `--prompt`, as in Pi. `--setup`, `--list-models`, `--new`, `feynman setup model`, and `feynman model <provider/model>` now say which command was meant.
14
+ - pi-web-access 0.36.0: `web_search` accepts lists that a model sent as a JSON string (nicobailon/pi-web-access#491), `fetch_content` asks docs sites for markdown first, Brave prepaid keys keep working, and a `fetch_content` call whose only URL failed is reported as an error.
15
+
16
+ ## v0.5.25 - 2026-10-05
17
+
18
+ - **Pi 1.0.2 and pi-subagents 0.76.0.** Pi now uses the patched brace-expansion 5.0.12 (GHSA-q2hr-2g5m-vwhr, GHSA-qhr7-859c-m2p7, GHSA-6j4f-fj2g-mc7p), so Feynman's npm audit has no exceptions left, and an npm install pulls about 120 fewer packages. Pi also retries "model at capacity" errors instead of ending the turn, and Sign in with ChatGPT reports a busy callback port instead of "OAuth state mismatch". pi-subagents frees async slots held by abandoned workflows.
19
+ - **A timed-out paper search no longer ends a research step.** A Semantic Scholar or OpenAlex topic search that hits the 25 s timeout is answered from the other index, and other sources say which one did not respond instead of "This operation was aborted".
20
+ - **arXiv batch versions.** Looking up a bare arXiv ID alongside an older version now returns the latest version for the bare ID regardless of feed order. An unavailable explicit version is reported in `not_found` instead of resolving to a different version. Thanks to @Kunyanli230 (#322).
21
+
9
22
  ## v0.5.24 - 2026-10-03
10
23
 
11
24
  - **Pi 1.0.** Feynman now runs on Pi 1.0.0, with pi-subagents 0.75.0 (background subagents start on Pi 1.0) and pi-btw 0.7.1 (allows Pi 1.x). The terminal UI is fullscreen by default and keeps its own scrollback; run `feynman --tui-mode regular`, or set `"tuiMode": "regular"` in `~/.feynman/agent/settings.json`, to use the terminal's normal scrollback. Launch warnings show inside the fullscreen UI. Pi 1.0 also adds MCP OAuth hardening, a copy-code Anthropic login for headless machines, and lower codemode token use.
package/dist/cli.js CHANGED
@@ -34,6 +34,12 @@ import { ASH, printAsciiHeader, printInfo, printPanel, printSection, RESET, SAGE
34
34
  import { createModelRuntime } from "./model/registry.js";
35
35
  import { cliCommandSections, formatCliWorkflowUsage, legacyFlags, readPromptSpecs, topLevelCommandNames, } from "../metadata/commands.mjs";
36
36
  const TOP_LEVEL_COMMANDS = new Set(topLevelCommandNames);
37
+ // Flags users reach for that Feynman spells differently.
38
+ const UNKNOWN_OPTION_HINTS = {
39
+ "--setup": "feynman setup",
40
+ "--list-models": "feynman model list",
41
+ "--new": "feynman --new-session",
42
+ };
37
43
  // Removed commands fail instead of falling through to a chat prompt.
38
44
  const REMOVED_COMMANDS = new Set(["jobs", "paper", "rank", "serve", "watch"]);
39
45
  const ALPHA_HUB_PACKAGE_PATH = ["@companion-ai", "alpha-hub"];
@@ -226,7 +232,8 @@ async function handleModelCommand(subcommand, args, feynmanSettingsPath, feynman
226
232
  }
227
233
  return;
228
234
  }
229
- throw new Error(`Unknown model command: ${subcommand}\nRun \`feynman model help\` to see model commands.`);
235
+ const setHint = /[/:]/.test(subcommand) ? `Did you mean \`feynman model set ${subcommand}\`? ` : "";
236
+ throw new Error(`Unknown model command: ${subcommand}\n${setHint}Run \`feynman model help\` to see model commands.`);
230
237
  }
231
238
  async function handleUpdateCommand(piOptions, feynmanVersion, source) {
232
239
  const latestFeynmanVersionPromise = fetchLatestFeynmanVersion();
@@ -468,7 +475,7 @@ async function runMain(input) {
468
475
  "no-session": { type: "boolean" },
469
476
  "no-themes": { type: "boolean" },
470
477
  "tui-mode": { type: "string" },
471
- prompt: { type: "string" },
478
+ prompt: { type: "string", short: "p" },
472
479
  resume: { type: "boolean", short: "r" },
473
480
  "service-tier": { type: "string" },
474
481
  session: { type: "string" },
@@ -488,7 +495,9 @@ async function runMain(input) {
488
495
  catch (error) {
489
496
  if (error && typeof error === "object" && "code" in error && error.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
490
497
  const message = error instanceof Error ? error.message : String(error);
491
- throw new Error(`${message}\nRun \`feynman help\` to see available commands and flags.`);
498
+ const option = /Unknown option '([^']+)'/.exec(message)?.[1];
499
+ const hint = option ? UNKNOWN_OPTION_HINTS[option] : undefined;
500
+ throw new Error(`${message}\n${hint ? `Did you mean \`${hint}\`? ` : ""}Run \`feynman help\` to see available commands and flags.`);
492
501
  }
493
502
  throw error;
494
503
  }
@@ -559,7 +568,7 @@ async function runMain(input) {
559
568
  return;
560
569
  }
561
570
  if (rest[0]) {
562
- throw new Error(`Unknown setup command: ${rest[0]}`);
571
+ throw new Error(`Unknown setup command: ${rest[0]}${rest[0] === "model" ? "\nTo choose a model, run `feynman model login` or `feynman model set`." : ""}`);
563
572
  }
564
573
  await runSetup({
565
574
  settingsPath: feynmanSettingsPath,
@@ -70,6 +70,19 @@ const RESEARCH_MODEL_FAMILY_PREFERENCES = [
70
70
  matches: (model) => model.provider === "opencode" && /^gpt-\d+(?:\.\d+)*(?:-.+)?$/i.test(model.id),
71
71
  reason: "newest OpenCode Zen GPT fallback when direct OpenAI access is unavailable",
72
72
  },
73
+ // GitHub Copilot writes Claude versions with dots (claude-opus-5.5).
74
+ {
75
+ matches: (model) => model.provider === "github-copilot" && /^claude-opus-\d+(?:[.-]\d+)*$/i.test(model.id),
76
+ reason: "newest Claude Opus model available through GitHub Copilot",
77
+ },
78
+ {
79
+ matches: (model) => model.provider === "github-copilot" && /^claude-sonnet-\d+(?:[.-]\d+)*$/i.test(model.id),
80
+ reason: "newest Claude Sonnet model available through GitHub Copilot",
81
+ },
82
+ {
83
+ matches: (model) => model.provider === "github-copilot" && /^gpt-\d+(?:\.\d+)*(?:-.+)?$/i.test(model.id),
84
+ reason: "newest GPT model available through GitHub Copilot",
85
+ },
73
86
  ];
74
87
  const RESEARCH_MODEL_FALLBACK_PREFERENCES = [
75
88
  exactResearchModel("opencode/kimi-k2.7-code", "good OpenCode Zen fallback for coding and research work"),
@@ -188,7 +201,7 @@ function compareCurrentModelFamily(left, right) {
188
201
  return modelSpec(left).localeCompare(modelSpec(right));
189
202
  }
190
203
  function currentFamilyPreference(model) {
191
- const anthropic = /^claude-(opus|sonnet)-(\d+(?:-\d+)*)$/i.exec(model.id);
204
+ const anthropic = /^claude-(opus|sonnet)-(\d+(?:[.-]\d+)*)$/i.exec(model.id);
192
205
  if (anthropic) {
193
206
  const family = anthropic[1].toLowerCase();
194
207
  const parsedVersion = parseClaudeVersion(anthropic[2]);
@@ -202,7 +215,7 @@ function currentFamilyPreference(model) {
202
215
  };
203
216
  }
204
217
  const openAi = /^gpt-(\d+(?:\.\d+)*)(?:-(.+))?$/i.exec(model.id);
205
- if (openAi && (model.provider === "openai" || model.provider === "openai-codex" || model.provider === "opencode")) {
218
+ if (openAi && ["openai", "openai-codex", "opencode", "github-copilot"].includes(model.provider)) {
206
219
  const suffix = openAi[2]?.toLowerCase();
207
220
  return {
208
221
  family: `${model.provider}/gpt`,
@@ -236,7 +249,7 @@ function currentFamilyPreference(model) {
236
249
  return undefined;
237
250
  }
238
251
  function parseClaudeVersion(rawVersion) {
239
- const rawParts = rawVersion.split("-");
252
+ const rawParts = rawVersion.split(/[.-]/);
240
253
  if (rawParts.length >= 2 && /^\d{8}$/.test(rawParts[rawParts.length - 1])) {
241
254
  const baseParts = rawParts.slice(0, -1).map(Number);
242
255
  return {
@@ -134,7 +134,11 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
134
134
  question: Type.String({ description: "Question about the paper." }),
135
135
  }),
136
136
  async execute(_toolCallId, params) {
137
- const result = await withPaperFallback(() => askPaper(params.paper, params.question));
137
+ const arxivId = ARXIV_ID.exec(params.paper.trim())?.[1];
138
+ const result = await withPaperFallback(() => askPaper(params.paper, params.question), arxivId ? {
139
+ source: "arXiv metadata and abstract; the question was not answered, so answer it from the abstract or read the full text with fetch_content on the arXiv PDF",
140
+ run: () => searchArxiv({ query: arxivId }),
141
+ } : undefined);
138
142
  return { content: [{ type: "text", text: formatText(result) }], details: result };
139
143
  },
140
144
  });
@@ -167,13 +167,16 @@ async function getArxivPapers(query: string): Promise<Record<string, unknown>> {
167
167
  const feed = recordValue(xmlParser.parse(xml).feed);
168
168
  const byId = new Map<string, Record<string, unknown>>();
169
169
  for (const row of arxivFeedEntries(feed).map(parseArxivEntry).filter((item): item is Record<string, unknown> => Boolean(item))) {
170
- if (row.arxiv_id) byId.set(String(row.arxiv_id), row);
170
+ if (row.arxiv_id) {
171
+ const bare = String(row.arxiv_id);
172
+ const latest = byId.get(bare);
173
+ if (!latest || Number(row.version) >= Number(latest.version)) byId.set(bare, row);
174
+ }
171
175
  if (row.id_versioned) byId.set(String(row.id_versioned), row);
172
176
  }
173
177
  const seen = new Map<Record<string, unknown>, string>();
174
178
  for (const id of ids) {
175
- const bare = id.replace(/v\d+$/i, "");
176
- const row = byId.get(id) ?? byId.get(bare);
179
+ const row = byId.get(id);
177
180
  if (!row) notFound.push(id);
178
181
  else if (seen.has(row)) duplicates.push({ requested: id, resolved_as: seen.get(row) });
179
182
  else {
@@ -385,7 +385,14 @@ async function searchSemanticScholar(params: ScienceDatabaseSearchParams): Promi
385
385
  const OPENALEX_LOOKUP_QUERY =
386
386
  /^(?:openalex_\w+|rate-limit|authors?|author-search|sources?|venues?|citations?|references?|work|detail)(?::|\s|$)|^(?:W\d+|doi:\s*10\.|10\.\S+\/)/i;
387
387
 
388
+ // Each request aborts after REQUEST_TIMEOUT_MS, which fetch reports as a bare
389
+ // "This operation was aborted".
390
+ function isTimeout(error: unknown): boolean {
391
+ return error instanceof Error && error.name === "AbortError";
392
+ }
393
+
388
394
  function isIndexUnavailable(error: unknown): boolean {
395
+ if (isTimeout(error)) return true;
389
396
  if (error instanceof ScienceDatabaseRequestError) return error.status === 429 || error.status >= 500;
390
397
  return error instanceof Error && /^OpenAlex request failed: (?:429|5\d\d)\b/.test(error.message);
391
398
  }
@@ -469,7 +476,13 @@ export function registerScienceDatabaseTools(pi: ExtensionAPI): void {
469
476
  ], { description: "PubMed sort order. For semanticscholar, prefer the default citation-count sort; use pub_date (newest first) only when recency matters. relevance falls back to the default when the anonymous pool is rate-limited. Ignored for other sources." })),
470
477
  }),
471
478
  async execute(_toolCallId, params) {
472
- const result = await scienceDatabaseSearch(params as ScienceDatabaseSearchParams);
479
+ let result: Record<string, unknown>;
480
+ try {
481
+ result = await scienceDatabaseSearch(params as ScienceDatabaseSearchParams);
482
+ } catch (error) {
483
+ if (!isTimeout(error)) throw error;
484
+ throw new Error(`${params.source} did not respond within ${REQUEST_TIMEOUT_MS / 1000} s. Retry, or search another source.`);
485
+ }
473
486
  return {
474
487
  content: [{ type: "text", text: formatText(result) }],
475
488
  details: result,
@@ -147,7 +147,7 @@ export const cliCommandSections = [
147
147
  ];
148
148
 
149
149
  export const legacyFlags = [
150
- { usage: '--prompt "<text>"', description: "Run one prompt and exit." },
150
+ { usage: '--prompt, -p "<text>"', description: "Run one prompt and exit." },
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." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.24",
3
+ "version": "0.5.26",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -82,13 +82,13 @@
82
82
  "fast-xml-parser": "5.11.1",
83
83
  "pi-btw": "0.7.1",
84
84
  "pi-docparser": "4.0.0",
85
- "pi-subagents": "0.75.0",
86
- "pi-web-access": "0.35.0",
85
+ "pi-subagents": "0.76.0",
86
+ "pi-web-access": "0.36.0",
87
87
  "posthog-node": "^5.53.0"
88
88
  },
89
89
  "peerDependencies": {
90
- "@earendil-works/pi-ai": "1.0.0",
91
- "@earendil-works/pi-coding-agent": "1.0.0",
90
+ "@earendil-works/pi-ai": "1.0.2",
91
+ "@earendil-works/pi-coding-agent": "1.0.2",
92
92
  "@earendil-works/pi-tui": "*",
93
93
  "typebox": "*"
94
94
  },