@companion-ai/feynman 0.5.28 → 0.5.30

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,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.30 - 2026-10-07
10
+
11
+ - **Pi 1.1.0.** Claude browser login no longer fails with "localhost refused to connect" when port 53692 is reserved or in use (common with Hyper-V/WSL on Windows); it falls back to a free port. "Servers are busy" provider errors and Mistral stream errors are retried instead of ending the turn, fewer requests fail on context limits, and session costs count long prompts correctly. Adds Claude Haiku 5.5; Feynman's default for Anthropic stays Claude Opus.
12
+
13
+ ## v0.5.29 - 2026-10-07
14
+
15
+ - **PubMed titles and abstracts keep their italic terms.** Text inside inline tags such as `<i>BRCA1</i>` was dropped, so a title read "patient with germline mutation" instead of "germline BRCA1 mutation", and gene names went missing from abstracts. Thanks to @KennyMcSimpson (#328).
16
+ - **PubMed authors keep all their affiliations.** An author listed with two or more affiliations came back with none. Thanks to @KennyMcSimpson (#327).
17
+ - Security: GHSA-6qxp-vccf-f47h (MCP SDK OAuth client). Feynman's own copies of `@modelcontextprotocol/sdk` move to 1.32.1. pi-web-access still pins 1.27.1 for its separate `npx pi-web-access` MCP server, which Feynman does not run and which does not use the affected OAuth client; it is updated in nicobailon/pi-web-access#522.
18
+
9
19
  ## v0.5.28 - 2026-10-07
10
20
 
11
21
  - **Pi 1.0.4, pi-subagents 0.76.1, pi-web-access 0.37.0.**
@@ -8,6 +8,14 @@ const xmlParser = new XMLParser({
8
8
  removeNSPrefix: true,
9
9
  });
10
10
 
11
+ // Object-mode parsing groups tags and loses the order of mixed inline text.
12
+ const orderedTextParser = new XMLParser({
13
+ preserveOrder: true,
14
+ removeNSPrefix: true,
15
+ trimValues: false,
16
+ parseTagValue: false,
17
+ });
18
+
11
19
  function recordValue(value: unknown): Record<string, unknown> {
12
20
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
13
21
  }
@@ -49,6 +57,31 @@ function textValue(value: unknown): string | undefined {
49
57
  return parts.length ? parts.join(" ").replace(/\s+/g, " ").trim() : undefined;
50
58
  }
51
59
 
60
+ function orderedChildren(value: unknown, tag: string): unknown[] {
61
+ return listValue(value).flatMap((node) => {
62
+ const children = recordValue(node)[tag];
63
+ return children === undefined ? [] : [children];
64
+ });
65
+ }
66
+
67
+ function orderedText(value: unknown): string {
68
+ if (typeof value === "string" || typeof value === "number") return String(value);
69
+ if (Array.isArray(value)) return value.map((node) => orderedText(node)).join("");
70
+ return Object.entries(recordValue(value))
71
+ .filter(([key]) => key !== ":@" && !key.startsWith("?"))
72
+ .map(([, children]) => orderedText(children))
73
+ .join("");
74
+ }
75
+
76
+ function orderedTextValue(value: unknown): string | undefined {
77
+ return orderedText(value).replace(/\s+/g, " ").trim() || undefined;
78
+ }
79
+
80
+ function orderedPubmedArticles(xml: string): unknown[] {
81
+ const set = orderedChildren(orderedTextParser.parse(xml), "PubmedArticleSet")[0];
82
+ return orderedChildren(set, "PubmedArticle");
83
+ }
84
+
52
85
  function dateObject(value: unknown): Record<string, string> | undefined {
53
86
  const record = recordValue(value);
54
87
  const monthMap: Record<string, string> = {
@@ -101,7 +134,8 @@ function parseAuthors(value: unknown): Array<Record<string, unknown>> {
101
134
  const author = recordValue(item);
102
135
  const collective = textValue(author.CollectiveName);
103
136
  if (collective) return { collectiveName: collective, affiliations: [] };
104
- const affiliations = listValue(recordValue(author.AffiliationInfo).Affiliation)
137
+ const affiliations = listValue(author.AffiliationInfo)
138
+ .flatMap((info) => listValue(recordValue(info).Affiliation))
105
139
  .map((affiliation) => textValue(affiliation))
106
140
  .filter((affiliation): affiliation is string => Boolean(affiliation));
107
141
  return prune({
@@ -113,15 +147,17 @@ function parseAuthors(value: unknown): Array<Record<string, unknown>> {
113
147
  });
114
148
  }
115
149
 
116
- function parsePubmedArticle(value: unknown): Record<string, unknown> {
150
+ function parsePubmedArticle(value: unknown, orderedArticle: unknown): Record<string, unknown> {
117
151
  const articleRoot = recordValue(value);
118
152
  const citation = recordValue(articleRoot.MedlineCitation);
119
153
  const article = recordValue(citation.Article);
120
154
  const journal = recordValue(article.Journal);
121
155
  const issue = recordValue(journal.JournalIssue);
122
- const abstract = recordValue(article.Abstract);
123
- const abstractParts = listValue(abstract.AbstractText)
124
- .map((part) => textValue(part))
156
+ const orderedCitation = orderedChildren(orderedArticle, "MedlineCitation")[0];
157
+ const orderedContent = orderedChildren(orderedCitation, "Article")[0];
158
+ const orderedAbstract = orderedChildren(orderedContent, "Abstract")[0];
159
+ const abstractParts = orderedChildren(orderedAbstract, "AbstractText")
160
+ .map((part) => orderedTextValue(part))
125
161
  .filter((part): part is string => Boolean(part));
126
162
  const publicationTypes = listValue(recordValue(article.PublicationTypeList).PublicationType)
127
163
  .map((item) => textValue(item))
@@ -140,7 +176,7 @@ function parsePubmedArticle(value: unknown): Record<string, unknown> {
140
176
  pmid: identifiers.pmid,
141
177
  pmcid: identifiers.pmc,
142
178
  doi: identifiers.doi,
143
- title: textValue(article.ArticleTitle),
179
+ title: orderedTextValue(orderedChildren(orderedContent, "ArticleTitle")[0]),
144
180
  abstract: abstractParts.length ? abstractParts.join("\n") : undefined,
145
181
  journal: prune({
146
182
  title: textValue(journal.Title),
@@ -160,7 +196,8 @@ function parsePubmedArticle(value: unknown): Record<string, unknown> {
160
196
  export function parsePubmedArticles(xml: string): Record<string, unknown>[] {
161
197
  const parsed = recordValue(xmlParser.parse(xml));
162
198
  const set = recordValue(parsed.PubmedArticleSet);
163
- return listValue(set.PubmedArticle).map((article) => parsePubmedArticle(article));
199
+ const orderedArticles = orderedPubmedArticles(xml);
200
+ return listValue(set.PubmedArticle).map((article, index) => parsePubmedArticle(article, orderedArticles[index]));
164
201
  }
165
202
 
166
203
  function jatsArticleId(meta: Record<string, unknown>, type: string): string | undefined {
@@ -238,8 +275,9 @@ export function copyrightFromPubmedXml(xml: string): Map<string, Record<string,
238
275
  const map = new Map<string, Record<string, unknown>>();
239
276
  const parsed = recordValue(xmlParser.parse(xml));
240
277
  const set = recordValue(parsed.PubmedArticleSet);
241
- for (const rawArticle of listValue(set.PubmedArticle)) {
242
- const article = parsePubmedArticle(rawArticle);
278
+ const orderedArticles = orderedPubmedArticles(xml);
279
+ for (const [index, rawArticle] of listValue(set.PubmedArticle).entries()) {
280
+ const article = parsePubmedArticle(rawArticle, orderedArticles[index]);
243
281
  const pmid = stringValue(article.pmid);
244
282
  if (!pmid) continue;
245
283
  const pubmedArticle = recordValue(rawArticle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -87,8 +87,8 @@
87
87
  "posthog-node": "^5.53.0"
88
88
  },
89
89
  "peerDependencies": {
90
- "@earendil-works/pi-ai": "1.0.4",
91
- "@earendil-works/pi-coding-agent": "1.0.4",
90
+ "@earendil-works/pi-ai": "1.1.0",
91
+ "@earendil-works/pi-coding-agent": "1.1.0",
92
92
  "@earendil-works/pi-tui": "*",
93
93
  "typebox": "*"
94
94
  },