@companion-ai/feynman 0.2.59 → 0.2.60

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.
Binary file
package/RELEASES.md CHANGED
@@ -4,6 +4,20 @@ This file is the public release history for Feynman. Keep entries user-facing: w
4
4
 
5
5
  GitHub release notes are generated from the matching `## vX.Y.Z` section in this file.
6
6
 
7
+ ## v0.2.60 - 2026-06-11
8
+
9
+ ### Node Support
10
+
11
+ - Feynman now supports Node.js 25 (#177). The full test suite and live CLI flows (launch, update, alpha search, parallel web search) were validated on Node 20, 24, and 25; the supported range is now 20.19.0 through 25.x.
12
+
13
+ ### Runtime Reliability
14
+
15
+ - Fixed the cryptic `Cannot convert argument to a ByteString because the character at index N has a value of M` crash (#171). It fires when a custom provider in `models.json` has a header value or API key containing characters above U+00FF (e.g. Chinese text) — HTTP headers cannot carry them. Feynman now reports exactly which provider and header is at fault and how to fix it, instead of an unattributed undici error.
16
+
17
+ ### Validation
18
+
19
+ - Added a multi-OS end-to-end install workflow that exercises the published package on Windows, Linux, and macOS runners (Node 24 and 25): global install, version/update/package flows, launch-time patch assertions for the subagent spawn (#172) and structured search parser (#167) fixes, plus live model and subagent smokes.
20
+
7
21
  ## v0.2.59 - 2026-06-11
8
22
 
9
23
  ### Research Tools
package/bin/feynman.js CHANGED
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
5
5
  const MIN_NODE_VERSION = "20.19.0";
6
- const MAX_NODE_MAJOR = 24;
6
+ const MAX_NODE_MAJOR = 25;
7
7
  const PREFERRED_NODE_MAJOR = 24;
8
8
 
9
9
  function parseNodeVersion(version) {
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import { patchAlphaHubAuthSource } from "../../scripts/lib/alpha-hub-auth-patch.mjs";
4
4
  import { patchAlphaHubSearchResultsSource, patchAlphaHubSearchSource } from "../../scripts/lib/alpha-hub-search-patch.mjs";
5
5
  import { patchPiAgentCoreSource } from "../../scripts/lib/pi-agent-core-patch.mjs";
6
+ import { patchPiModelRegistrySource } from "../../scripts/lib/pi-model-registry-patch.mjs";
6
7
  import { patchPiPackageManagerSource } from "../../scripts/lib/pi-package-manager-patch.mjs";
7
8
  import { PI_SUBAGENTS_PATCH_TARGETS, patchPiSubagentsSource } from "../../scripts/lib/pi-subagents-patch.mjs";
8
9
  import { patchPiEditorSource, patchPiInteractiveThemeSource, patchPiTuiSource } from "../../scripts/lib/pi-tui-patch.mjs";
@@ -45,6 +46,7 @@ export function patchPiRuntimeNodeModules(appRoot) {
45
46
  changed = patchScopedPiPackageFileIfPresent(nodeModulesPath, "pi-tui", "dist/components/editor.js", patchPiEditorSource) || changed;
46
47
  changed = patchScopedPiPackageFileIfPresent(nodeModulesPath, "pi-coding-agent", "dist/modes/interactive/theme/theme.js", patchPiInteractiveThemeSource) || changed;
47
48
  changed = patchScopedPiPackageFileIfPresent(nodeModulesPath, "pi-coding-agent", "dist/core/package-manager.js", patchPiPackageManagerSource) || changed;
49
+ changed = patchScopedPiPackageFileIfPresent(nodeModulesPath, "pi-coding-agent", "dist/core/model-registry.js", patchPiModelRegistrySource) || changed;
48
50
  changed = patchFileIfPresent(resolve(nodeModulesPath, "@companion-ai", "alpha-hub", "src", "lib", "auth.js"), patchAlphaHubAuthSource) || changed;
49
51
  changed = patchFileIfPresent(resolve(nodeModulesPath, "@companion-ai", "alpha-hub", "src", "lib", "alphaxiv.js"), patchAlphaHubSearchSource) || changed;
50
52
  changed = patchFileIfPresent(resolve(nodeModulesPath, "@companion-ai", "alpha-hub", "src", "lib", "index.js"), patchAlphaHubSearchResultsSource) || changed;
@@ -1,5 +1,5 @@
1
1
  export const MIN_NODE_VERSION = "20.19.0";
2
- export const MAX_NODE_MAJOR = 24;
2
+ export const MAX_NODE_MAJOR = 25;
3
3
  export const PREFERRED_NODE_MAJOR = 24;
4
4
  function parseNodeVersion(version) {
5
5
  const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split(".");
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.2.59",
3
+ "version": "0.2.60",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=20.19.0 <25"
8
+ "node": ">=20.19.0 <26"
9
9
  },
10
10
  "bin": {
11
11
  "feynman": "bin/feynman.js"
@@ -1,5 +1,5 @@
1
1
  const MIN_NODE_VERSION = "20.19.0";
2
- const MAX_NODE_MAJOR = 24;
2
+ const MAX_NODE_MAJOR = 25;
3
3
  const PREFERRED_NODE_MAJOR = 24;
4
4
 
5
5
  function parseNodeVersion(version) {
@@ -0,0 +1 @@
1
+ export declare function patchPiModelRegistrySource(source: string): string;
@@ -0,0 +1,59 @@
1
+ // Issue #171: a models.json provider header or API key containing characters
2
+ // above U+00FF (e.g. Chinese text) makes undici's fetch throw the cryptic
3
+ // "Cannot convert argument to a ByteString because the character at index N
4
+ // has a value of M which is greater than 255" with no hint of which config
5
+ // value caused it. Validate at request-assembly time and name the exact
6
+ // provider and header instead; the surrounding try/catch in
7
+ // getApiKeyAndHeaders turns the throw into a readable model error.
8
+ const LATIN1_GUARD_HELPER = [
9
+ "function findNonLatin1CharIndex(value) {",
10
+ ' if (typeof value !== "string") return -1;',
11
+ " for (let index = 0; index < value.length; index++) {",
12
+ " if (value.charCodeAt(index) > 255) return index;",
13
+ " }",
14
+ " return -1;",
15
+ "}",
16
+ "function assertHeaderSafeRequestConfig(provider, apiKey, headers) {",
17
+ " const apiKeyIndex = findNonLatin1CharIndex(apiKey);",
18
+ " if (apiKeyIndex !== -1) {",
19
+ " throw new Error(`The API key for provider \"${provider}\" contains a non-Latin-1 character at index ${apiKeyIndex} (code point ${apiKey.codePointAt(apiKeyIndex)}). HTTP headers cannot carry characters above U+00FF - check models.json or your stored auth for stray non-ASCII characters.`);",
20
+ " }",
21
+ " for (const [headerName, headerValue] of Object.entries(headers ?? {})) {",
22
+ ' const value = typeof headerValue === "string" ? headerValue : String(headerValue);',
23
+ " const nameIndex = findNonLatin1CharIndex(headerName);",
24
+ " const valueIndex = findNonLatin1CharIndex(value);",
25
+ " if (nameIndex === -1 && valueIndex === -1) continue;",
26
+ " const offending = nameIndex !== -1 ? headerName : value;",
27
+ " const offendingIndex = nameIndex !== -1 ? nameIndex : valueIndex;",
28
+ " throw new Error(`Header \"${headerName}\" for provider \"${provider}\" contains a non-Latin-1 character at index ${offendingIndex} (code point ${offending.codePointAt(offendingIndex)}). HTTP headers cannot carry characters above U+00FF - remove or URL-encode the value in models.json.`);",
29
+ " }",
30
+ "}",
31
+ ].join("\n");
32
+
33
+ const RETURN_ORIGINAL = [
34
+ " return {",
35
+ " ok: true,",
36
+ " apiKey,",
37
+ " headers: headers && Object.keys(headers).length > 0 ? headers : undefined,",
38
+ " };",
39
+ ].join("\n");
40
+
41
+ const RETURN_PATCHED = [
42
+ " assertHeaderSafeRequestConfig(model.provider, apiKey, headers);",
43
+ RETURN_ORIGINAL,
44
+ ].join("\n");
45
+
46
+ const HELPER_ANCHOR = "function formatValidationPath(error) {";
47
+
48
+ export function patchPiModelRegistrySource(source) {
49
+ if (source.includes("function assertHeaderSafeRequestConfig(")) {
50
+ return source;
51
+ }
52
+ if (!source.includes(RETURN_ORIGINAL) || !source.includes(HELPER_ANCHOR)) {
53
+ return source;
54
+ }
55
+
56
+ let patched = source.replace(RETURN_ORIGINAL, RETURN_PATCHED);
57
+ patched = patched.replace(HELPER_ANCHOR, `${LATIN1_GUARD_HELPER}\n${HELPER_ANCHOR}`);
58
+ return patched;
59
+ }
@@ -9,6 +9,7 @@ import { patchAlphaHubAuthSource } from "./lib/alpha-hub-auth-patch.mjs";
9
9
  import { patchAlphaHubSearchResultsSource, patchAlphaHubSearchSource } from "./lib/alpha-hub-search-patch.mjs";
10
10
  import { patchPiAgentCoreSource } from "./lib/pi-agent-core-patch.mjs";
11
11
  import { patchPiExtensionLoaderSource } from "./lib/pi-extension-loader-patch.mjs";
12
+ import { patchPiModelRegistrySource } from "./lib/pi-model-registry-patch.mjs";
12
13
  import { patchPiPackageManagerSource } from "./lib/pi-package-manager-patch.mjs";
13
14
  import { patchPiEditorSource, patchPiInteractiveThemeSource, patchPiTuiSource } from "./lib/pi-tui-patch.mjs";
14
15
  import { PI_WEB_ACCESS_PATCH_TARGETS, patchPiWebAccessSource } from "./lib/pi-web-access-patch.mjs";
@@ -72,6 +73,7 @@ const interactiveModePath = piPackageRoot ? resolve(piPackageRoot, "dist", "mode
72
73
  const interactiveThemePath = piPackageRoot ? resolve(piPackageRoot, "dist", "modes", "interactive", "theme", "theme.js") : null;
73
74
  const extensionLoaderPath = piPackageRoot ? resolve(piPackageRoot, "dist", "core", "extensions", "loader.js") : null;
74
75
  const packageManagerPath = piPackageRoot ? resolve(piPackageRoot, "dist", "core", "package-manager.js") : null;
76
+ const modelRegistryPath = piPackageRoot ? resolve(piPackageRoot, "dist", "core", "model-registry.js") : null;
75
77
  const agentLoopPath = piAgentCoreRoot ? resolve(piAgentCoreRoot, "dist", "agent-loop.js") : null;
76
78
  const tuiPath = piTuiRoot ? resolve(piTuiRoot, "dist", "tui.js") : null;
77
79
  const terminalPath = piTuiRoot ? resolve(piTuiRoot, "dist", "terminal.js") : null;
@@ -725,6 +727,16 @@ for (const entryPath of [packageManagerPath, workspacePackageManagerPath].filter
725
727
  }
726
728
  }
727
729
 
730
+ const workspaceModelRegistryPath = resolveWorkspacePiFile("pi-coding-agent", "dist", "core", "model-registry.js");
731
+ for (const entryPath of [modelRegistryPath, workspaceModelRegistryPath].filter(Boolean)) {
732
+ if (!existsSync(entryPath)) continue;
733
+ const source = readFileSync(entryPath, "utf8");
734
+ const patched = patchPiModelRegistrySource(source);
735
+ if (patched !== source) {
736
+ writeFileSync(entryPath, patched, "utf8");
737
+ }
738
+ }
739
+
728
740
  for (const entryPath of [agentLoopPath, workspaceAgentLoopPath].filter(Boolean)) {
729
741
  if (!existsSync(entryPath)) {
730
742
  continue;