@companion-ai/feynman 0.5.18 → 0.5.19

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,13 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## v0.5.19 - 2026-10-01
10
+
11
+ - **Word drafts open without LibreOffice.** `document_parse` needs LibreOffice for `.docx` files, which most machines lack, so reviewing a thesis or draft in Word format failed. When LibreOffice is missing, Feynman now reads the document's paragraph text directly from the `.docx` and says so; install LibreOffice for page layout, tables, footnotes, and images.
12
+ - `feynman_science_database_search` accepts the `"null"` some models send for an omitted `sort` or `limit`, which failed validation before the search ran.
13
+ - **Pi 0.99.2** (faster prompt submission in long sessions; saved default models with extension providers load reliably), **pi-subagents 0.74.0**, and **pi-btw 0.7.0**. pi-subagents now ships the patched undici 8.10.2, so the undici security exceptions and Feynman's own undici pin are gone. It removed the `workflowScript` parameter; `/deepresearch` and `/summarize` now write the workflow script to `outputs/.plans/<slug>-workflow.js` and run it by path. Its other new form, an inline ```` ```js workflow ```` block, failed in testing when Claude put the block in its hidden reasoning, while a script file worked with every model, and the file stays next to the plan as a record of the run. Subagent children also follow the project's trust setting, and `/reload` no longer stops running background workflows.
14
+ - Security: Pi 0.99.2 still locks brace-expansion 5.0.9; the exception for it stays until Pi ships 5.0.12.
15
+
9
16
  ## v0.5.18 - 2026-09-30
10
17
 
11
18
  - **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.
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { inflateRawSync } from "node:zlib";
4
+
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+
7
+ // Reads one stored or deflated entry through the zip central directory.
8
+ function readZipEntry(zip: Buffer, name: string): Buffer | undefined {
9
+ const end = zip.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
10
+ if (end < 0) return undefined;
11
+ let offset = zip.readUInt32LE(end + 16);
12
+ for (let index = zip.readUInt16LE(end + 10); index > 0; index -= 1) {
13
+ if (zip.readUInt32LE(offset) !== 0x02014b50) return undefined;
14
+ const method = zip.readUInt16LE(offset + 10);
15
+ const size = zip.readUInt32LE(offset + 20);
16
+ const nameLength = zip.readUInt16LE(offset + 28);
17
+ const local = zip.readUInt32LE(offset + 42);
18
+ if (zip.toString("utf8", offset + 46, offset + 46 + nameLength) === name) {
19
+ const start = local + 30 + zip.readUInt16LE(local + 26) + zip.readUInt16LE(local + 28);
20
+ const data = zip.subarray(start, start + size);
21
+ return method === 0 ? data : method === 8 ? inflateRawSync(data) : undefined;
22
+ }
23
+ offset += 46 + nameLength + zip.readUInt16LE(offset + 30) + zip.readUInt16LE(offset + 32);
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ function decodeXml(text: string): string {
29
+ return text.replace(/&(?:#x([0-9a-f]+)|#(\d+)|(amp|lt|gt|quot|apos));/gi, (_match, hex: string, dec: string, named: string) => {
30
+ if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
31
+ if (dec) return String.fromCodePoint(Number(dec));
32
+ return { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[named.toLowerCase()] ?? "";
33
+ });
34
+ }
35
+
36
+ // Paragraph text of a .docx body: runs, tabs, and line breaks; no tables'
37
+ // layout, headers, footnotes, or images.
38
+ export function docxText(file: Buffer): string | undefined {
39
+ const xml = readZipEntry(file, "word/document.xml")?.toString("utf8");
40
+ if (!xml) return undefined;
41
+ return xml
42
+ .split("</w:p>")
43
+ .map((paragraph) =>
44
+ [...paragraph.matchAll(/<w:t(?:\s[^>]*)?>([^<]*)<\/w:t>|<w:tab\/>|<w:br\/>/g)]
45
+ .map((match) => (match[0] === "<w:tab/>" ? "\t" : match[0] === "<w:br/>" ? "\n" : decodeXml(match[1] ?? "")))
46
+ .join(""),
47
+ )
48
+ .join("\n")
49
+ .replace(/\n{3,}/g, "\n\n")
50
+ .trim();
51
+ }
52
+
53
+ // document_parse needs LibreOffice for Word files, which most machines lack.
54
+ // Answer a .docx from its own XML instead of failing the read.
55
+ export function registerDocxFallback(pi: ExtensionAPI): void {
56
+ pi.on("tool_result", (event, ctx) => {
57
+ if (event.toolName !== "document_parse" || !event.isError) return undefined;
58
+ const errorText = event.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
59
+ const path = typeof event.input.path === "string" ? event.input.path.replace(/^@/, "") : "";
60
+ if (!errorText.includes("LibreOffice is not installed") || !/\.doc[xm]$/i.test(path)) return undefined;
61
+ let text: string | undefined;
62
+ try {
63
+ text = docxText(readFileSync(resolve(ctx.cwd, path)));
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ if (!text) return undefined;
68
+ return {
69
+ isError: false,
70
+ content: [{
71
+ type: "text",
72
+ text: `LibreOffice is not installed, so this is the document's paragraph text read directly from the .docx (no page layout, table structure, headers, footnotes, or images). Install LibreOffice for a full parse.\n\n${text}`,
73
+ }],
74
+ };
75
+ });
76
+ }
@@ -445,6 +445,17 @@ export function registerScienceDatabaseTools(pi: ExtensionAPI): void {
445
445
  "Preserve returned PMIDs, PMCIDs, DOIs, arXiv IDs, Semantic Scholar paper IDs, preprint DOIs, OpenAlex W/A/S IDs, author ORCIDs, citation/reference counts, OA status, Europe PMC full-text statuses and section inventories, source URLs, and endpoint provenance in research artifacts and answers.",
446
446
  "Treat database summaries as retrieval evidence, then verify decisive claims against the full paper when needed.",
447
447
  ],
448
+ // Some models fill omitted optional fields with "null", which fails validation.
449
+ prepareArguments: (args) => {
450
+ if (!args || typeof args !== "object" || Array.isArray(args)) return args as never;
451
+ const input = { ...(args as Record<string, unknown>) };
452
+ for (const key of ["sort", "limit"]) {
453
+ if (input[key] === null || input[key] === "" || input[key] === "null") delete input[key];
454
+ }
455
+ if (typeof input.sort === "string") input.sort = input.sort.trim().toLowerCase();
456
+ if (typeof input.limit === "string" && /^\d+$/.test(input.limit.trim())) input.limit = Number(input.limit);
457
+ return input as never;
458
+ },
448
459
  parameters: Type.Object({
449
460
  source: SCIENCE_DATABASE_SOURCE_SCHEMA,
450
461
  query: Type.String({
@@ -3,6 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { registerAlphaTools } from "./research-tools/alpha.js";
4
4
  import { registerCurrentDateResearchContext } from "./research-tools/current-date.js";
5
5
  import { registerDiscoveryCommands } from "./research-tools/discovery.js";
6
+ import { registerDocxFallback } from "./research-tools/docx-fallback.js";
6
7
  import { installFeynmanHeader } from "./research-tools/header.js";
7
8
  import { registerHelpCommand } from "./research-tools/help.js";
8
9
  import { registerHuggingFaceTools } from "./research-tools/huggingface.js";
@@ -23,6 +24,7 @@ export default function researchTools(pi: ExtensionAPI): void {
23
24
  registerCurrentDateResearchContext(pi);
24
25
  registerHuggingFaceTools(pi);
25
26
  registerDiscoveryCommands(pi);
27
+ registerDocxFallback(pi);
26
28
  registerHelpCommand(pi);
27
29
  registerInitCommand(pi);
28
30
  registerOutputsCommand(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.18",
3
+ "version": "0.5.19",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -80,16 +80,15 @@
80
80
  "@clack/prompts": "^1.8.1",
81
81
  "@companion-ai/alpha-hub": "0.1.6",
82
82
  "fast-xml-parser": "5.11.1",
83
- "pi-btw": "0.6.1",
83
+ "pi-btw": "0.7.0",
84
84
  "pi-docparser": "4.0.0",
85
- "pi-subagents": "0.73.1",
85
+ "pi-subagents": "0.74.0",
86
86
  "pi-web-access": "0.35.0",
87
- "posthog-node": "^5.53.0",
88
- "undici": "^8.11.2"
87
+ "posthog-node": "^5.53.0"
89
88
  },
90
89
  "peerDependencies": {
91
- "@earendil-works/pi-ai": "0.99.1",
92
- "@earendil-works/pi-coding-agent": "0.99.1",
90
+ "@earendil-works/pi-ai": "0.99.2",
91
+ "@earendil-works/pi-coding-agent": "0.99.2",
93
92
  "@earendil-works/pi-tui": "*",
94
93
  "typebox": "*"
95
94
  },
@@ -114,8 +113,5 @@
114
113
  "author": "Advait Paliwal",
115
114
  "publishConfig": {
116
115
  "access": "public"
117
- },
118
- "overrides": {
119
- "undici": "$undici"
120
116
  }
121
117
  }
@@ -68,18 +68,23 @@ If subagents were chosen:
68
68
  - Keep `subagent` tool-call JSON small and valid.
69
69
  - Do not place multi-paragraph instructions inside the `subagent` JSON.
70
70
  - Use only supported `subagent` keys. Do not add extra keys such as `artifacts` unless the tool schema explicitly exposes them.
71
- - Use one async `workflowScript` with `await runs.all(...)` for parallel evidence gathering. Each item needs a unique stable `key`, plus its agent, short task, and output path. Set `globalConcurrencyLimit: 4` on the outer call.
71
+ - For parallel evidence gathering, write a workflow script that returns `await runs.all(...)` to `outputs/.plans/<slug>-workflow.js` with the write tool. After the write succeeds, call `subagent` with `workflow: "./outputs/.plans/<slug>-workflow.js"`, `async: true`, and `globalConcurrencyLimit: 4`. Each item needs a unique stable `key`, plus its agent, short task, and output path. Do not pass the script text inside the tool call.
72
72
  - Read the ordered result array and record each child's `ok`, error, and returned output/artifact paths. Ordinary child failures are collected by `runs.all`; validation or infrastructure failure can still fail the workflow. Do not assume every output exists.
73
73
  - Prefer broad guidance such as "use paper search and web search"; if a PDF parser or paper fetch fails, the researcher must continue from metadata, abstracts, and web sources and mark PDF parsing as blocked.
74
74
 
75
- Example shape:
75
+ Example shape: `outputs/.plans/<slug>-workflow.js` contains
76
+
77
+ ```js
78
+ return await runs.all([
79
+ { 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" },
80
+ { 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" },
81
+ ]);
82
+ ```
83
+
84
+ and the `subagent` call is:
76
85
 
77
86
  ```json
78
- {
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
- "async": true,
81
- "globalConcurrencyLimit": 4
82
- }
87
+ { "workflow": "./outputs/.plans/<slug>-workflow.js", "async": true, "globalConcurrencyLimit": 4 }
83
88
  ```
84
89
 
85
90
  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.
@@ -116,14 +116,12 @@ Briefly summarize: "Source is ~<chars> chars -> <N> chunks -> <N> researcher sub
116
116
 
117
117
  ### 3c. Dispatch researcher subagents
118
118
 
119
- Use one async workflow with a stable key for each chunk and a concurrency limit of four. Expand the example item below for the actual chunk inventory; retain the single-source restriction and explicit output declaration for every child.
120
-
121
- ```json
122
- {
123
- "workflowScript": "return await runs.all([{key:'chunk-NNN',agent:'researcher',task:'Read ONLY outputs/.notes/<slug>-chunk-NNN.txt. Extract: (1) key claims, (2) methodology or technical approach, (3) cited evidence. Do NOT use web_search or fetch external URLs — this is single-source summarization. If a claim appears to start or end mid-sentence at the file boundary, mark it BOUNDARY PARTIAL. Write to outputs/.notes/<slug>-summary-chunk-NNN.md.',output:'outputs/.notes/<slug>-summary-chunk-NNN.md'}]);",
124
- "async": true,
125
- "globalConcurrencyLimit": 4
126
- }
119
+ Use one async workflow with a stable key for each chunk and a concurrency limit of four: write the script to `outputs/.plans/<slug>-workflow.js` with the write tool, then, after the write succeeds, call `subagent` with `{ "workflow": "./outputs/.plans/<slug>-workflow.js", "async": true, "globalConcurrencyLimit": 4 }`. Expand the example item below for the actual chunk inventory; retain the single-source restriction and explicit output declaration for every child.
120
+
121
+ ```js
122
+ return await runs.all([
123
+ { key: "chunk-NNN", agent: "researcher", task: "Read ONLY outputs/.notes/<slug>-chunk-NNN.txt. Extract: (1) key claims, (2) methodology or technical approach, (3) cited evidence. Do NOT use web_search or fetch external URLs — this is single-source summarization. If a claim appears to start or end mid-sentence at the file boundary, mark it BOUNDARY PARTIAL. Write to outputs/.notes/<slug>-summary-chunk-NNN.md.", output: "outputs/.notes/<slug>-summary-chunk-NNN.md" },
124
+ ]);
127
125
  ```
128
126
 
129
127
  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.