@companion-ai/feynman 0.5.16 → 0.5.18
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/.feynman/agents/researcher.md +1 -1
- package/RELEASES.md +14 -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/alpha.ts +27 -3
- package/extensions/research-tools/science-databases.ts +44 -6
- package/package.json +2 -2
|
@@ -31,7 +31,7 @@ You are Feynman's evidence-gathering subagent.
|
|
|
31
31
|
| Biomedical papers | `source: "pubmed"`, then `source: "europepmc"` for open-access full-text sections | `semanticscholar` for citation counts |
|
|
32
32
|
| Citation graph | `source: "openalex"` with `openalex_citations:` / `openalex_references:` | `semanticscholar` citation counts to spot seminal work |
|
|
33
33
|
| Conceptual or recent work that keyword search misses | `source: "openalex"` with a `semantic:` query prefix | `semanticscholar` with `sort: "relevance"` |
|
|
34
|
-
| Web, docs, repos, grey literature | `web_search` (if the default provider fails or is rate-limited, retry with `provider
|
|
34
|
+
| Web, docs, repos, grey literature | `web_search` (if the default provider fails or is rate-limited, retry with `provider` set to the plain string `parallel-mcp`, not a JSON-encoded list; it needs no key) | `fetch_content` on the best results |
|
|
35
35
|
| Known paper ID | `source: "arxiv"` for arXiv IDs, `source: "crossref"` for DOIs | `fetch_content` on `arxiv.org/html/<id>` for full text |
|
|
36
36
|
|
|
37
37
|
Run 2–4 reworded queries for each question (synonyms, the method's name, the problem's name) and merge the results. Do not trust one query's ranking; seminal papers often appear only under one phrasing or one sort order.
|
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,20 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.18 - 2026-09-30
|
|
10
|
+
|
|
11
|
+
- **A rate-limited paper index no longer ends a research step.** When Semantic Scholar is rate-limited or down, a search is answered from OpenAlex, and an OpenAlex topic search that hits OpenAlex's anonymous rate limit or an outage is answered from Semantic Scholar. The result says first which index answered and why. Lookups by OpenAlex ID, DOI, author, venue, or citation still report the error, since the other index has no equivalent.
|
|
12
|
+
- **alphaXiv tools answer when alphaXiv cannot.** If your alphaXiv login expired or alphaXiv does not respond, `alpha_search` returns OpenAlex semantic search results and `alpha_get_paper` returns the arXiv metadata and abstract for an arXiv ID, each marked as a fallback. Run `feynman alpha login` to get alphaXiv's reports back.
|
|
13
|
+
- pi-web-access 0.35.0: OpenAI web search works when you signed in with ChatGPT (`feynman model login openai`), and Brave search keeps to its rate limits. The researcher is told to pass a search provider as a plain string, since some models sent `provider: "[\"parallel-mcp\"]"`, which `web_search` rejects; nicobailon/pi-web-access#491 makes `web_search` accept that form too.
|
|
14
|
+
|
|
15
|
+
## v0.5.17 - 2026-09-30
|
|
16
|
+
|
|
17
|
+
- **`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`.
|
|
18
|
+
- **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.
|
|
19
|
+
- 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.
|
|
20
|
+
- 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".
|
|
21
|
+
- A rejected `SEMANTIC_SCHOLAR_API_KEY` now says so instead of `request failed: 403 Forbidden`.
|
|
22
|
+
|
|
9
23
|
## v0.5.16 - 2026-09-30
|
|
10
24
|
|
|
11
25
|
- **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({
|
|
@@ -13,6 +13,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import { Type } from "typebox";
|
|
14
14
|
|
|
15
15
|
import { extractPaperSections } from "./alpha-sections.js";
|
|
16
|
+
import { searchArxiv } from "./science-database-arxiv.js";
|
|
17
|
+
import { searchOpenAlex } from "./science-database-openalex.js";
|
|
16
18
|
|
|
17
19
|
function formatText(value: unknown): string {
|
|
18
20
|
if (typeof value === "string") return value;
|
|
@@ -21,11 +23,23 @@ function formatText(value: unknown): string {
|
|
|
21
23
|
|
|
22
24
|
// alphaXiv's API has changed without notice before; when a call fails, point
|
|
23
25
|
// the model at the other paper sources instead of leaving it on a broken tool.
|
|
24
|
-
|
|
26
|
+
const ARXIV_ID = /(?:^|arxiv\.org\/(?:abs|pdf)\/|alphaxiv\.org\/(?:abs|overview)\/)(\d{4}\.\d{4,5}(?:v\d+)?|[a-z-]+(?:\.[A-Z]{2})?\/\d{7}(?:v\d+)?)/i;
|
|
27
|
+
|
|
28
|
+
// When alphaXiv does not answer (expired login, outage), answer from another
|
|
29
|
+
// paper source and say so first, instead of failing the research step.
|
|
30
|
+
async function withPaperFallback<T>(
|
|
31
|
+
run: () => Promise<T>,
|
|
32
|
+
fallback?: { source: string; run: () => Promise<Record<string, unknown>> },
|
|
33
|
+
): Promise<T | Record<string, unknown>> {
|
|
25
34
|
try {
|
|
26
35
|
return await run();
|
|
27
36
|
} catch (error) {
|
|
28
37
|
const message = (error instanceof Error ? error.message : String(error)).replace("`alpha login`", "`feynman alpha login`");
|
|
38
|
+
if (fallback) {
|
|
39
|
+
try {
|
|
40
|
+
return { fallbackNote: `alphaXiv did not answer (${message}); these results are from ${fallback.source}.`, ...(await fallback.run()) };
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
29
43
|
throw new Error(
|
|
30
44
|
`${message}\nalphaXiv did not answer this call. Use feynman_science_database_search (source "arxiv" or "semanticscholar") or fetch_content on https://arxiv.org/abs/<id> instead.`,
|
|
31
45
|
);
|
|
@@ -65,7 +79,10 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
|
|
|
65
79
|
),
|
|
66
80
|
}),
|
|
67
81
|
async execute(_toolCallId, params) {
|
|
68
|
-
const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic")
|
|
82
|
+
const result = await withPaperFallback(() => searchPapers(params.query, params.mode?.trim() || "semantic"), {
|
|
83
|
+
source: "OpenAlex semantic search",
|
|
84
|
+
run: () => searchOpenAlex({ source: "openalex", query: `semantic: ${params.query}`, limit: 10 }),
|
|
85
|
+
});
|
|
69
86
|
return { content: [{ type: "text", text: formatText(result) }], details: result };
|
|
70
87
|
},
|
|
71
88
|
});
|
|
@@ -87,7 +104,14 @@ export function registerAlphaTools(pi: ExtensionAPI, signedIn = isLoggedIn()): v
|
|
|
87
104
|
sections: Type.Optional(paperSectionsSchema),
|
|
88
105
|
}),
|
|
89
106
|
async execute(_toolCallId, params) {
|
|
90
|
-
const
|
|
107
|
+
const arxivId = ARXIV_ID.exec(params.paper.trim())?.[1];
|
|
108
|
+
const result = await withPaperFallback(() => getPaper(params.paper, { fullText: params.fullText }), arxivId ? {
|
|
109
|
+
source: "arXiv metadata and abstract (no full text or sections)",
|
|
110
|
+
run: () => searchArxiv({ query: arxivId }),
|
|
111
|
+
} : undefined);
|
|
112
|
+
if ("fallbackNote" in result) {
|
|
113
|
+
return { content: [{ type: "text", text: formatText(result) }], details: result };
|
|
114
|
+
}
|
|
91
115
|
const extracted = extractPaperSections(result.content, params.section, params.sections);
|
|
92
116
|
const filteredResult = extracted.requested.length
|
|
93
117
|
? {
|
|
@@ -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
|
|
@@ -377,19 +380,54 @@ async function searchSemanticScholar(params: ScienceDatabaseSearchParams): Promi
|
|
|
377
380
|
};
|
|
378
381
|
}
|
|
379
382
|
|
|
383
|
+
// OpenAlex lookups by ID, DOI, author, venue, or citation have no Semantic
|
|
384
|
+
// Scholar equivalent; only plain topic searches fall back.
|
|
385
|
+
const OPENALEX_LOOKUP_QUERY =
|
|
386
|
+
/^(?:openalex_\w+|rate-limit|authors?|author-search|sources?|venues?|citations?|references?|work|detail)(?::|\s|$)|^(?:W\d+|doi:\s*10\.|10\.\S+\/)/i;
|
|
387
|
+
|
|
388
|
+
function isIndexUnavailable(error: unknown): boolean {
|
|
389
|
+
if (error instanceof ScienceDatabaseRequestError) return error.status === 429 || error.status >= 500;
|
|
390
|
+
return error instanceof Error && /^OpenAlex request failed: (?:429|5\d\d)\b/.test(error.message);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// A rate-limited or overloaded index should not end a research step: answer a
|
|
394
|
+
// topic search from the other general index, and say so first in the result.
|
|
395
|
+
async function withIndexFallback(
|
|
396
|
+
run: () => Promise<Record<string, unknown>>,
|
|
397
|
+
fallback: (() => Promise<Record<string, unknown>>) | undefined,
|
|
398
|
+
describe: (reason: string) => string,
|
|
399
|
+
): Promise<Record<string, unknown>> {
|
|
400
|
+
try {
|
|
401
|
+
return await run();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (!fallback || !isIndexUnavailable(error)) throw error;
|
|
404
|
+
let result: Record<string, unknown>;
|
|
405
|
+
try {
|
|
406
|
+
result = await fallback();
|
|
407
|
+
} catch {
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
return { fallbackNote: describe(error instanceof Error ? error.message : String(error)), ...result };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
380
414
|
async function scienceDatabaseSearch(params: ScienceDatabaseSearchParams): Promise<Record<string, unknown>> {
|
|
381
415
|
if (params.source === "arxiv") return searchArxiv(params);
|
|
382
416
|
if (params.source === "biorxiv") return searchPreprints(params, "biorxiv");
|
|
383
417
|
if (params.source === "medrxiv") return searchPreprints(params, "medrxiv");
|
|
384
418
|
if (params.source === "pubmed") return searchPubMed(params);
|
|
385
419
|
if (params.source === "crossref") return searchCrossref(params);
|
|
386
|
-
|
|
420
|
+
const openAlex = () => searchOpenAlex({ limit: params.limit, query: params.query, source: "openalex" });
|
|
421
|
+
const semanticScholar = () => searchSemanticScholar({ ...params, source: "semanticscholar" });
|
|
422
|
+
if (params.source === "semanticscholar") {
|
|
423
|
+
return withIndexFallback(semanticScholar, openAlex, (reason) => `Semantic Scholar was unavailable (${reason}); these results are from OpenAlex.`);
|
|
424
|
+
}
|
|
387
425
|
if (params.source === "openalex") {
|
|
388
|
-
return
|
|
389
|
-
|
|
390
|
-
query:
|
|
391
|
-
|
|
392
|
-
|
|
426
|
+
return withIndexFallback(
|
|
427
|
+
openAlex,
|
|
428
|
+
OPENALEX_LOOKUP_QUERY.test(params.query.trim()) ? undefined : semanticScholar,
|
|
429
|
+
(reason) => `OpenAlex was unavailable (${reason}); these results are from Semantic Scholar.`,
|
|
430
|
+
);
|
|
393
431
|
}
|
|
394
432
|
return searchEuropePmc(params);
|
|
395
433
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companion-ai/feynman",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
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.
|
|
86
|
+
"pi-web-access": "0.35.0",
|
|
87
87
|
"posthog-node": "^5.53.0",
|
|
88
88
|
"undici": "^8.11.2"
|
|
89
89
|
},
|