@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(nodeText(record.title), 240);
333
- const text = nodeText(record, new Set(["fig", "table-wrap", "ref-list"]));
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 = collectNodes(article, "fig").slice(0, MAX_CAPTIONS_RETURNED).map((node) => {
344
- const record = recordValue(node);
337
+ const figureCaptions = orderedFigures.slice(0, MAX_CAPTIONS_RETURNED).map((node) => {
338
+ const children = jatsChildren(node, "fig")[0];
345
339
  return {
346
- id: stringValue(record["@_id"]),
347
- label: truncateText(nodeText(record.label), 80),
348
- caption: truncateText(nodeText(record.caption), 600),
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 = collectNodes(article, "table-wrap").slice(0, MAX_CAPTIONS_RETURNED).map((node) => {
352
- const record = recordValue(node);
345
+ const tableCaptions = orderedTables.slice(0, MAX_CAPTIONS_RETURNED).map((node) => {
346
+ const children = jatsChildren(node, "table-wrap")[0];
353
347
  return {
354
- id: stringValue(record["@_id"]),
355
- label: truncateText(nodeText(record.label), 80),
356
- caption: truncateText(nodeText(record.caption), 600),
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 abstractCandidates = arrayValue(findFirst(article, "abstract"));
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(nodeText(findFirst(article, "article-title")), 500),
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 body = recordValue(article.body);
232
- const sections = listValue(body.sec)
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 record = recordValue(section);
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(sectionText(section), SECTION_TEXT_SNIPPET_CHARS),
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: textValue(recordValue(meta["title-group"])["article-title"]) ?? stringValue(fallback.title),
254
- abstract: truncateText(textValue(recordValue(meta.abstract).p), 2_000) ?? stringValue(fallback.abstractText),
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.68",
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.0",
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.0",
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"