@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.
- package/.feynman/runtime-workspace.tgz +0 -0
- package/RELEASES.md +28 -0
- package/bin/feynman.js +1 -1
- package/dist/pi/package-ops.js +2 -13
- package/dist/pi/runtime-patches.js +2 -0
- package/dist/system/node-version.js +1 -1
- package/package.json +2 -2
- package/scripts/check-node-version.mjs +1 -1
- package/scripts/lib/npm-command.d.mts +10 -0
- package/scripts/lib/npm-command.mjs +21 -0
- package/scripts/lib/pi-model-registry-patch.d.mts +1 -0
- package/scripts/lib/pi-model-registry-patch.mjs +59 -0
- package/scripts/patch-embedded-pi.mjs +54 -2
|
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
package/dist/pi/package-ops.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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 <
|
|
8
|
+
"node": ">=20.19.0 <26"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"feynman": "bin/feynman.js"
|
|
@@ -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
|
|
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
|
-
|
|
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");';
|