@companion-ai/feynman 0.2.59 → 0.2.61

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,34 @@ 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.61 - 2026-06-11
8
+
9
+ ### Windows
10
+
11
+ - Fixed bundled-package setup failing on every launch (#177, #170). Two root causes found by running the published package on real Windows runners: GNU tar (Git for Windows) treats the workspace archive's absolute `C:\...` path as a remote host spec ("Cannot connect ... resolve failed"), and the npm fallback spawned bare `npm` without a shell, which Windows rejects with EINVAL. The archive now extracts with relative paths, and npm is invoked through `npm-cli.js` with the running Node executable.
12
+
13
+ ### Runtime Reliability
14
+
15
+ - The bundled workspace's alpha-hub copy now receives the same launch-time patches as the package-local copy, so the #167 search fix applies regardless of which copy resolves.
16
+
17
+ ### Validation
18
+
19
+ - The multi-OS end-to-end workflow now verifies install, update, patch application, and live model + subagent smokes on Windows, Linux, and macOS at Node 24 and 25.
20
+
21
+ ## v0.2.60 - 2026-06-11
22
+
23
+ ### Node Support
24
+
25
+ - 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.
26
+
27
+ ### Runtime Reliability
28
+
29
+ - 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.
30
+
31
+ ### Validation
32
+
33
+ - 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.
34
+
7
35
  ## v0.2.59 - 2026-06-11
8
36
 
9
37
  ### 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,7 +3,9 @@ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, re
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
6
+ import { resolveAdjacentNpmCommand } from "../../scripts/lib/npm-command.mjs";
6
7
  import { NATIVE_PACKAGE_SOURCES, supportsNativePackageSources } from "./package-presets.js";
8
+ export { resolveAdjacentNpmCommand };
7
9
  import { applyFeynmanPackageManagerEnv, getFeynmanNpmPrefixPath } from "./runtime.js";
8
10
  import { getPathWithCurrentNode, resolveExecutable } from "../system/executables.js";
9
11
  const FILTERED_INSTALL_OUTPUT_PATTERNS = [
@@ -160,19 +162,6 @@ function ensureProjectInstallRoot(workingDir) {
160
162
  }
161
163
  return installRoot;
162
164
  }
163
- export function resolveAdjacentNpmCommand(nodeExecutablePath = process.execPath, platform = process.platform) {
164
- const executableDir = dirname(nodeExecutablePath);
165
- if (platform === "win32") {
166
- const npmCliPath = resolve(executableDir, "node_modules", "npm", "bin", "npm-cli.js");
167
- if (existsSync(npmCliPath)) {
168
- return { command: nodeExecutablePath, args: [npmCliPath] };
169
- }
170
- const npmCmdPath = resolve(executableDir, "npm.cmd");
171
- return existsSync(npmCmdPath) ? { command: npmCmdPath, args: [], shell: true } : undefined;
172
- }
173
- const candidate = resolve(executableDir, "npm");
174
- return existsSync(candidate) ? { command: candidate, args: [] } : undefined;
175
- }
176
165
  function resolvePackageManagerCommand(settingsManager) {
177
166
  const configured = settingsManager.getNpmCommand();
178
167
  if (!configured || configured.length === 0) {
@@ -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.61",
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,10 @@
1
+ export type PackageManagerCommand = {
2
+ command: string;
3
+ args: string[];
4
+ shell?: boolean;
5
+ };
6
+
7
+ export declare function resolveAdjacentNpmCommand(
8
+ nodeExecutablePath?: string,
9
+ platform?: NodeJS.Platform,
10
+ ): PackageManagerCommand | undefined;
@@ -0,0 +1,21 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ // Windows cannot spawn `npm`/`npm.cmd` without a shell (EINVAL since the
5
+ // CVE-2024-27980 hardening), and shell spawns with an args array trip
6
+ // DEP0190. Preferring the npm-cli.js entry point next to the running Node
7
+ // executable avoids both: it is a plain Node script spawn on every platform.
8
+ export function resolveAdjacentNpmCommand(nodeExecutablePath = process.execPath, platform = process.platform) {
9
+ const executableDir = dirname(nodeExecutablePath);
10
+ if (platform === "win32") {
11
+ const npmCliPath = resolve(executableDir, "node_modules", "npm", "bin", "npm-cli.js");
12
+ if (existsSync(npmCliPath)) {
13
+ return { command: nodeExecutablePath, args: [npmCliPath] };
14
+ }
15
+ const npmCmdPath = resolve(executableDir, "npm.cmd");
16
+ return existsSync(npmCmdPath) ? { command: npmCmdPath, args: [], shell: true } : undefined;
17
+ }
18
+
19
+ const candidate = resolve(executableDir, "npm");
20
+ return existsSync(candidate) ? { command: candidate, args: [] } : undefined;
21
+ }
@@ -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,8 @@ 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 { resolveAdjacentNpmCommand } from "./lib/npm-command.mjs";
13
+ import { patchPiModelRegistrySource } from "./lib/pi-model-registry-patch.mjs";
12
14
  import { patchPiPackageManagerSource } from "./lib/pi-package-manager-patch.mjs";
13
15
  import { patchPiEditorSource, patchPiInteractiveThemeSource, patchPiTuiSource } from "./lib/pi-tui-patch.mjs";
14
16
  import { PI_WEB_ACCESS_PATCH_TARGETS, patchPiWebAccessSource } from "./lib/pi-web-access-patch.mjs";
@@ -72,6 +74,7 @@ const interactiveModePath = piPackageRoot ? resolve(piPackageRoot, "dist", "mode
72
74
  const interactiveThemePath = piPackageRoot ? resolve(piPackageRoot, "dist", "modes", "interactive", "theme", "theme.js") : null;
73
75
  const extensionLoaderPath = piPackageRoot ? resolve(piPackageRoot, "dist", "core", "extensions", "loader.js") : null;
74
76
  const packageManagerPath = piPackageRoot ? resolve(piPackageRoot, "dist", "core", "package-manager.js") : null;
77
+ const modelRegistryPath = piPackageRoot ? resolve(piPackageRoot, "dist", "core", "model-registry.js") : null;
75
78
  const agentLoopPath = piAgentCoreRoot ? resolve(piAgentCoreRoot, "dist", "agent-loop.js") : null;
76
79
  const tuiPath = piTuiRoot ? resolve(piTuiRoot, "dist", "tui.js") : null;
77
80
  const terminalPath = piTuiRoot ? resolve(piTuiRoot, "dist", "terminal.js") : null;
@@ -190,6 +193,18 @@ function resolvePackageManager() {
190
193
  return null;
191
194
  }
192
195
 
196
+ function resolvePackageManagerInvocation(packageManager) {
197
+ if (packageManager === "npm") {
198
+ const adjacent = resolveAdjacentNpmCommand();
199
+ if (adjacent) return adjacent;
200
+ }
201
+ const resolved = resolveExecutable(packageManager);
202
+ if (!resolved) return null;
203
+ // Windows cannot spawn .cmd/.bat shims without a shell (EINVAL).
204
+ const needsShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(resolved);
205
+ return { command: resolved, args: [], shell: needsShell };
206
+ }
207
+
193
208
  function installWorkspacePackages(packageSpecs) {
194
209
  const packageManager = resolvePackageManager();
195
210
  if (!packageManager) {
@@ -199,8 +214,14 @@ function installWorkspacePackages(packageSpecs) {
199
214
  return false;
200
215
  }
201
216
 
202
- const result = spawnSync(packageManager, createInstallCommand(packageManager, packageSpecs), {
217
+ const invocation = resolvePackageManagerInvocation(packageManager);
218
+ if (!invocation) {
219
+ process.stderr.write(`[feynman] could not resolve ${packageManager} executable for bundled package setup.\n`);
220
+ return false;
221
+ }
222
+ const result = spawnSync(invocation.command, [...invocation.args, ...createInstallCommand(packageManager, packageSpecs)], {
203
223
  cwd: workspaceDir,
224
+ shell: invocation.shell,
204
225
  stdio: ["ignore", "pipe", "pipe"],
205
226
  timeout: 300000,
206
227
  env: {
@@ -417,7 +438,11 @@ function restorePackagedWorkspace(packageSpecs) {
417
438
  rmSync(workspaceDir, { recursive: true, force: true });
418
439
  mkdirSync(resolve(appRoot, ".feynman"), { recursive: true });
419
440
 
420
- const result = spawnSync("tar", ["-xzf", workspaceArchivePath, "-C", resolve(appRoot, ".feynman")], {
441
+ // Run tar from .feynman with relative paths: GNU tar (Git for Windows)
442
+ // treats absolute paths with a drive-letter colon ("C:\...") as remote
443
+ // host specs and fails with "Cannot connect ... resolve failed".
444
+ const result = spawnSync("tar", ["-xzf", "runtime-workspace.tgz", "-C", "."], {
445
+ cwd: resolve(appRoot, ".feynman"),
421
446
  stdio: ["ignore", "ignore", "pipe"],
422
447
  timeout: 300000,
423
448
  });
@@ -725,6 +750,16 @@ for (const entryPath of [packageManagerPath, workspacePackageManagerPath].filter
725
750
  }
726
751
  }
727
752
 
753
+ const workspaceModelRegistryPath = resolveWorkspacePiFile("pi-coding-agent", "dist", "core", "model-registry.js");
754
+ for (const entryPath of [modelRegistryPath, workspaceModelRegistryPath].filter(Boolean)) {
755
+ if (!existsSync(entryPath)) continue;
756
+ const source = readFileSync(entryPath, "utf8");
757
+ const patched = patchPiModelRegistrySource(source);
758
+ if (patched !== source) {
759
+ writeFileSync(entryPath, patched, "utf8");
760
+ }
761
+ }
762
+
728
763
  for (const entryPath of [agentLoopPath, workspaceAgentLoopPath].filter(Boolean)) {
729
764
  if (!existsSync(entryPath)) {
730
765
  continue;
@@ -843,6 +878,23 @@ if (alphaHubIndexPath && existsSync(alphaHubIndexPath)) {
843
878
  }
844
879
  }
845
880
 
881
+ // The bundled workspace carries its own alpha-hub copy; patch it the same way
882
+ // so search fixes apply regardless of which copy resolves at runtime.
883
+ const workspaceAlphaHubLib = resolve(workspaceRoot, "@companion-ai", "alpha-hub", "src", "lib");
884
+ for (const [fileName, patchFn] of [
885
+ ["auth.js", patchAlphaHubAuthSource],
886
+ ["alphaxiv.js", patchAlphaHubSearchSource],
887
+ ["index.js", patchAlphaHubSearchResultsSource],
888
+ ]) {
889
+ const filePath = resolve(workspaceAlphaHubLib, fileName);
890
+ if (!existsSync(filePath)) continue;
891
+ const source = readFileSync(filePath, "utf8");
892
+ const patched = patchFn(source);
893
+ if (patched !== source) {
894
+ writeFileSync(filePath, patched, "utf8");
895
+ }
896
+ }
897
+
846
898
  if (existsSync(piMemoryPath)) {
847
899
  let source = readFileSync(piMemoryPath, "utf8");
848
900
  const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");';