@companion-ai/feynman 0.5.68 → 0.5.70
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/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Results are saved to `outputs/` (drafts to `papers/`). `/lit`, `/deepresearch`,
|
|
|
59
59
|
|
|
60
60
|
## Sources
|
|
61
61
|
|
|
62
|
-
Feynman searches alphaXiv, Semantic Scholar, OpenAlex, arXiv, PubMed, Europe PMC, bioRxiv, medRxiv, Crossref, the web, and Hugging Face, and reads local PDFs and documents. Free `OPENALEX_API_KEY` and `SEMANTIC_SCHOLAR_API_KEY` keys avoid the shared rate limits. It runs on stock [Pi](https://github.com/earendil-works/pi) with four research agents: researcher, verifier, reviewer, and writer. Add your own tools, such as a Zotero library, with `feynman mcp add`.
|
|
62
|
+
Feynman searches alphaXiv, Semantic Scholar, OpenAlex, arXiv, PubMed, Europe PMC, bioRxiv, medRxiv, Crossref, the web, and Hugging Face, and reads local PDFs and documents. Free `OPENALEX_API_KEY` and `SEMANTIC_SCHOLAR_API_KEY` keys avoid the shared rate limits. PubMed and Europe PMC full-text snippets preserve inline scientific terms and paragraph boundaries. It runs on stock [Pi](https://github.com/earendil-works/pi) with four research agents: researcher, verifier, reviewer, and writer. Add your own tools, such as a Zotero library, with `feynman mcp add`.
|
|
63
63
|
|
|
64
64
|
## Telemetry
|
|
65
65
|
|
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,16 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.70 - 2026-11-14
|
|
10
|
+
|
|
11
|
+
- **Security patch: fast-xml-parser 5.12.1 and fast-xml-builder 1.3.2.** Fixes a quadratic-time attribute scan in the XML validator ([GHSA-7cfw-w2rf-f3jc](https://github.com/advisories/GHSA-7cfw-w2rf-f3jc)) and an uncontrolled recursion crash in the XML builder ([GHSA-g62r-xqpr-7fwx](https://github.com/advisories/GHSA-g62r-xqpr-7fwx)). arXiv, PubMed, and Europe PMC results parse the same as before.
|
|
12
|
+
|
|
13
|
+
## v0.5.69 - 2026-11-13
|
|
14
|
+
|
|
15
|
+
- **pi-subagents 0.88.1.** A permission prompt or other dialog that opens while the fleet inspector is up is no longer hidden behind it. `status` and `stop` no longer report a cleanly finishing background subagent as having ended without proof, and a manual `/compact` no longer wakes an idle parent while background subagents run.
|
|
16
|
+
- **Preserve JATS inline text.** PubMed and Europe PMC full-text results keep words inside inline markup in their original order, including subscripts and superscripts, without joining adjacent paragraphs. Section snippet limits and metadata are unchanged.
|
|
17
|
+
- **Patched hono.** Alpha Hub's MCP SDK now resolves hono 4.13.18 and @hono/node-server 2.1.8, which fix advisories published on November 12 (GHSA-4m76-xfj5-7975, GHSA-46wx-v44x-9hh2, GHSA-4h7h-rv7c-7m2v, GHSA-qv5m-9pm6-hqp3, GHSA-jpvv-r467-5p4w).
|
|
18
|
+
|
|
9
19
|
## v0.5.68 - 2026-11-11
|
|
10
20
|
|
|
11
21
|
- **pi-subagents 0.88.0.** A child waiting for your answer no longer runs out its `timeoutMs` while it waits, and a reply cut off at the model's output limit now fails instead of passing as a success. The subagent widget and `status` show the latest progress a running child reported. Copies of the parent session written by forked subagents are now deleted after `artifactConfig.cleanupDays`, so they no longer fill the disk. Also fixes Windows machines that block PowerShell and gateways that require a prompt cache key.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
|
|
3
|
+
// Keep mixed content in document order; object-mode XML groups repeated tags
|
|
4
|
+
// and moves their surrounding text into a separate #text property.
|
|
5
|
+
const parser = new XMLParser({
|
|
6
|
+
preserveOrder: true,
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
removeNSPrefix: true,
|
|
9
|
+
trimValues: false,
|
|
10
|
+
parseTagValue: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const blockTags = new Set([
|
|
14
|
+
"abstract", "sec", "title", "p", "caption", "label", "list", "list-item",
|
|
15
|
+
"def-list", "def-item", "term", "def", "disp-quote", "verse-line", "break",
|
|
16
|
+
"tr", "td", "th",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function entries(value: unknown): Array<[string, unknown]> {
|
|
20
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
21
|
+
? Object.entries(value).filter(([key]) => key !== ":@" && !key.startsWith("?"))
|
|
22
|
+
: [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseOrderedJats(xml: string): unknown {
|
|
26
|
+
return parser.parse(xml);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function jatsChildren(value: unknown, tag: string): unknown[] {
|
|
30
|
+
return (Array.isArray(value) ? value : [value]).flatMap((node) =>
|
|
31
|
+
entries(node).filter(([key]) => key === tag).map(([, children]) => children));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function jatsNodes(value: unknown, tag: string): unknown[] {
|
|
35
|
+
if (Array.isArray(value)) return value.flatMap((node) => jatsNodes(node, tag));
|
|
36
|
+
return entries(value).flatMap(([key, children]) => [
|
|
37
|
+
...(key === tag ? [value] : []),
|
|
38
|
+
...jatsNodes(children, tag),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function jatsDescendants(value: unknown, tag: string): unknown[] {
|
|
43
|
+
return jatsNodes(value, tag).flatMap((node) => jatsChildren(node, tag));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function jatsAttribute(node: unknown, name: string): string | undefined {
|
|
47
|
+
if (!node || typeof node !== "object") return undefined;
|
|
48
|
+
const attributes = (node as Record<string, unknown>)[":@"];
|
|
49
|
+
if (!attributes || typeof attributes !== "object") return undefined;
|
|
50
|
+
const value = (attributes as Record<string, unknown>)[`@_${name}`];
|
|
51
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function jatsText(value: unknown, excludedTags = new Set<string>()): string {
|
|
55
|
+
function text(node: unknown): string {
|
|
56
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
57
|
+
if (Array.isArray(node)) return node.map(text).join("");
|
|
58
|
+
return entries(node).map(([tag, children]) => {
|
|
59
|
+
if (excludedTags.has(tag)) return " ";
|
|
60
|
+
const content = text(children);
|
|
61
|
+
return blockTags.has(tag) ? ` ${content} ` : content;
|
|
62
|
+
}).join("");
|
|
63
|
+
}
|
|
64
|
+
return text(value).replace(/\s+/g, " ").trim();
|
|
65
|
+
}
|
|
@@ -2,6 +2,8 @@ export type EuropePmcFullTextSearchParams = { limit?: number; query: string };
|
|
|
2
2
|
|
|
3
3
|
import { XMLParser } from "fast-xml-parser";
|
|
4
4
|
|
|
5
|
+
import { jatsAttribute, jatsChildren, jatsDescendants, jatsNodes, jatsText, parseOrderedJats } from "./jats-text.js";
|
|
6
|
+
|
|
5
7
|
const DEFAULT_LIMIT = 2;
|
|
6
8
|
const MAX_LIMIT = 5;
|
|
7
9
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -246,19 +248,6 @@ async function availabilityFor(input: InputId): Promise<{ endpoint: string; reco
|
|
|
246
248
|
};
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
function nodeText(value: unknown, excludedKeys = new Set<string>()): string {
|
|
250
|
-
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
251
|
-
if (Array.isArray(value)) return value.map((item) => nodeText(item, excludedKeys)).join(" ");
|
|
252
|
-
const record = recordValue(value);
|
|
253
|
-
return Object.entries(record)
|
|
254
|
-
.filter(([key]) => key === "#text" || (!key.startsWith("@_") && key !== "#text"))
|
|
255
|
-
.filter(([key]) => !excludedKeys.has(key))
|
|
256
|
-
.map(([, item]) => nodeText(item, excludedKeys))
|
|
257
|
-
.join(" ")
|
|
258
|
-
.replace(/\s+/g, " ")
|
|
259
|
-
.trim();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
251
|
function findFirst(value: unknown, key: string): unknown {
|
|
263
252
|
const record = recordValue(value);
|
|
264
253
|
if (Object.prototype.hasOwnProperty.call(record, key)) return record[key];
|
|
@@ -327,10 +316,15 @@ function parseJats(xml: string): Record<string, unknown> {
|
|
|
327
316
|
const article = recordValue(parsed.article);
|
|
328
317
|
const body = recordValue(findFirst(article, "body"));
|
|
329
318
|
const sectionNodes = arrayValue(body.sec);
|
|
319
|
+
const orderedArticle = jatsChildren(parseOrderedJats(xml), "article")[0];
|
|
320
|
+
const orderedBody = jatsDescendants(orderedArticle, "body")[0];
|
|
321
|
+
const orderedSections = jatsChildren(orderedBody, "sec");
|
|
322
|
+
const orderedFigures = jatsNodes(orderedArticle, "fig");
|
|
323
|
+
const orderedTables = jatsNodes(orderedArticle, "table-wrap");
|
|
330
324
|
const sections = sectionNodes.map((section, index) => {
|
|
331
325
|
const record = recordValue(section);
|
|
332
|
-
const title = truncateText(
|
|
333
|
-
const text =
|
|
326
|
+
const title = truncateText(jatsText(jatsChildren(orderedSections[index], "title")[0]), 240);
|
|
327
|
+
const text = jatsText(orderedSections[index], new Set(["fig", "table-wrap", "ref-list"]));
|
|
334
328
|
return {
|
|
335
329
|
index,
|
|
336
330
|
title,
|
|
@@ -340,26 +334,25 @@ function parseJats(xml: string): Record<string, unknown> {
|
|
|
340
334
|
textSnippet: truncateText(text, SECTION_TEXT_SNIPPET_CHARS),
|
|
341
335
|
};
|
|
342
336
|
});
|
|
343
|
-
const figureCaptions =
|
|
344
|
-
const
|
|
337
|
+
const figureCaptions = orderedFigures.slice(0, MAX_CAPTIONS_RETURNED).map((node) => {
|
|
338
|
+
const children = jatsChildren(node, "fig")[0];
|
|
345
339
|
return {
|
|
346
|
-
id:
|
|
347
|
-
label: truncateText(
|
|
348
|
-
caption: truncateText(
|
|
340
|
+
id: jatsAttribute(node, "id"),
|
|
341
|
+
label: truncateText(jatsText(jatsChildren(children, "label")[0]), 80),
|
|
342
|
+
caption: truncateText(jatsText(jatsChildren(children, "caption")[0]), 600),
|
|
349
343
|
};
|
|
350
344
|
});
|
|
351
|
-
const tableCaptions =
|
|
352
|
-
const
|
|
345
|
+
const tableCaptions = orderedTables.slice(0, MAX_CAPTIONS_RETURNED).map((node) => {
|
|
346
|
+
const children = jatsChildren(node, "table-wrap")[0];
|
|
353
347
|
return {
|
|
354
|
-
id:
|
|
355
|
-
label: truncateText(
|
|
356
|
-
caption: truncateText(
|
|
348
|
+
id: jatsAttribute(node, "id"),
|
|
349
|
+
label: truncateText(jatsText(jatsChildren(children, "label")[0]), 80),
|
|
350
|
+
caption: truncateText(jatsText(jatsChildren(children, "caption")[0]), 600),
|
|
357
351
|
};
|
|
358
352
|
});
|
|
359
|
-
const
|
|
360
|
-
const abstract = truncateText(nodeText(abstractCandidates[0]), ABSTRACT_SNIPPET_CHARS);
|
|
353
|
+
const abstract = truncateText(jatsText(jatsDescendants(orderedArticle, "abstract")[0]), ABSTRACT_SNIPPET_CHARS);
|
|
361
354
|
return {
|
|
362
|
-
title: truncateText(
|
|
355
|
+
title: truncateText(jatsText(jatsDescendants(orderedArticle, "article-title")[0]), 500),
|
|
363
356
|
abstract,
|
|
364
357
|
sectionCount: sections.length,
|
|
365
358
|
sectionInventory: sections.map((section) => ({
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { XMLParser } from "fast-xml-parser";
|
|
2
2
|
|
|
3
|
+
import { jatsChildren, jatsText, parseOrderedJats } from "./jats-text.js";
|
|
4
|
+
|
|
3
5
|
const SECTION_TEXT_SNIPPET_CHARS = 1_200;
|
|
4
6
|
const MAX_SECTIONS_RETURNED = 10;
|
|
5
7
|
|
|
@@ -214,29 +216,23 @@ function truncateText(value: string | undefined, limit: number): string | undefi
|
|
|
214
216
|
return clean.length > limit ? `${clean.slice(0, limit).trimEnd()}...` : clean;
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
function sectionText(section: unknown): string {
|
|
218
|
-
const record = recordValue(section);
|
|
219
|
-
const title = textValue(record.title);
|
|
220
|
-
const bodyParts = Object.entries(record)
|
|
221
|
-
.filter(([key]) => !key.startsWith("@_") && !["title", "fig", "table-wrap", "sec"].includes(key))
|
|
222
|
-
.map(([, value]) => textValue(value))
|
|
223
|
-
.filter((value): value is string => Boolean(value));
|
|
224
|
-
return [title, ...bodyParts].filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
219
|
export function parseFullTextXml(xml: string, pmcid: string, fallback: Record<string, unknown> = {}): Record<string, unknown> {
|
|
228
220
|
const parsed = recordValue(xmlParser.parse(xml));
|
|
229
221
|
const article = recordValue(parsed.article);
|
|
230
222
|
const meta = recordValue(recordValue(article.front)["article-meta"]);
|
|
231
|
-
const
|
|
232
|
-
const
|
|
223
|
+
const orderedArticle = jatsChildren(parseOrderedJats(xml), "article")[0];
|
|
224
|
+
const orderedFront = jatsChildren(orderedArticle, "front")[0];
|
|
225
|
+
const orderedMeta = jatsChildren(orderedFront, "article-meta")[0];
|
|
226
|
+
const orderedBody = jatsChildren(orderedArticle, "body")[0];
|
|
227
|
+
const orderedTitleGroup = jatsChildren(orderedMeta, "title-group")[0];
|
|
228
|
+
const orderedAbstract = jatsChildren(orderedMeta, "abstract")[0];
|
|
229
|
+
const sections = jatsChildren(orderedBody, "sec")
|
|
233
230
|
.map((section, index) => {
|
|
234
|
-
const
|
|
235
|
-
const title = textValue(record.title) ?? `Section ${index + 1}`;
|
|
231
|
+
const title = jatsText(jatsChildren(section, "title")[0]) || `Section ${index + 1}`;
|
|
236
232
|
return {
|
|
237
233
|
index: index + 1,
|
|
238
234
|
title,
|
|
239
|
-
textSnippet: truncateText(
|
|
235
|
+
textSnippet: truncateText(jatsText(section, new Set(["fig", "table-wrap", "sec"])), SECTION_TEXT_SNIPPET_CHARS),
|
|
240
236
|
};
|
|
241
237
|
})
|
|
242
238
|
.filter((section) => section.textSnippet)
|
|
@@ -250,8 +246,8 @@ export function parseFullTextXml(xml: string, pmcid: string, fallback: Record<st
|
|
|
250
246
|
pmcid,
|
|
251
247
|
pmid: jatsArticleId(meta, "pmid") ?? stringValue(fallback.pmid),
|
|
252
248
|
doi: jatsArticleId(meta, "doi") ?? stringValue(fallback.doi),
|
|
253
|
-
title:
|
|
254
|
-
abstract: truncateText(
|
|
249
|
+
title: jatsText(jatsChildren(orderedTitleGroup, "article-title")[0]) || stringValue(fallback.title),
|
|
250
|
+
abstract: truncateText(jatsChildren(orderedAbstract, "p").map((p) => jatsText(p)).join(" "), 2_000) ?? stringValue(fallback.abstractText),
|
|
255
251
|
fullTextStatus: "retrieved",
|
|
256
252
|
fullTextAvailable: true,
|
|
257
253
|
sectionCount: sections.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companion-ai/feynman",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.70",
|
|
4
4
|
"description": "The open source AI research agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
"@earendil-works/pi-codemode": "1.5.2",
|
|
83
83
|
"@earendil-works/pi-mcp": "1.5.2",
|
|
84
84
|
"@earendil-works/pi-telemetry": "1.5.2",
|
|
85
|
-
"fast-xml-parser": "5.12.
|
|
85
|
+
"fast-xml-parser": "5.12.1",
|
|
86
86
|
"pi-btw": "0.8.1",
|
|
87
87
|
"pi-docparser": "4.0.0",
|
|
88
|
-
"pi-subagents": "0.88.
|
|
88
|
+
"pi-subagents": "0.88.1",
|
|
89
89
|
"pi-web-access": "0.47.0",
|
|
90
90
|
"posthog-node": "^5.53.0",
|
|
91
91
|
"undici": "^8.12.3"
|