Fix extension repair and add Opus 4.7 overlay
This commit is contained in:
@@ -25,7 +25,7 @@ curl -fsSL https://feynman.is/install | bash
|
||||
irm https://feynman.is/install.ps1 | iex
|
||||
```
|
||||
|
||||
The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.20`.
|
||||
The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.21`.
|
||||
|
||||
The installer downloads a standalone native bundle with its own Node.js runtime.
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@companion-ai/feynman",
|
||||
"version": "0.2.20",
|
||||
"version": "0.2.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@companion-ai/feynman",
|
||||
"version": "0.2.20",
|
||||
"version": "0.2.21",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@companion-ai/feynman",
|
||||
"version": "0.2.20",
|
||||
"version": "0.2.21",
|
||||
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
||||
Workarounds:
|
||||
- try again after the release finishes publishing
|
||||
- pass the latest published version explicitly, e.g.:
|
||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.20
|
||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
|
||||
"@
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
||||
Workarounds:
|
||||
- try again after the release finishes publishing
|
||||
- pass the latest published version explicitly, e.g.:
|
||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.20
|
||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -260,6 +260,23 @@ function ensureParentDir(path) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
}
|
||||
|
||||
function packageDependencyExists(packagePath, globalNodeModulesRoot, dependency) {
|
||||
return existsSync(resolve(packagePath, "node_modules", dependency)) ||
|
||||
existsSync(resolve(globalNodeModulesRoot, dependency));
|
||||
}
|
||||
|
||||
function installedPackageLooksUsable(packagePath, globalNodeModulesRoot) {
|
||||
if (!existsSync(resolve(packagePath, "package.json"))) return false;
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(resolve(packagePath, "package.json"), "utf8"));
|
||||
return Object.keys(pkg.dependencies ?? {}).every((dependency) =>
|
||||
packageDependencyExists(packagePath, globalNodeModulesRoot, dependency)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function linkPointsTo(linkPath, targetPath) {
|
||||
try {
|
||||
if (!lstatSync(linkPath).isSymbolicLink()) return false;
|
||||
@@ -281,6 +298,8 @@ function ensureBundledPackageLinks(packageSpecs) {
|
||||
try {
|
||||
if (lstatSync(targetPath).isSymbolicLink()) {
|
||||
rmSync(targetPath, { force: true });
|
||||
} else if (!installedPackageLooksUsable(targetPath, globalNodeModulesRoot)) {
|
||||
rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
} catch {}
|
||||
if (existsSync(targetPath)) continue;
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import { getModels } from "@mariozechner/pi-ai";
|
||||
import { anthropicOAuthProvider } from "@mariozechner/pi-ai/oauth";
|
||||
|
||||
export function getModelsJsonPath(authPath: string): string {
|
||||
return resolve(dirname(authPath), "models.json");
|
||||
}
|
||||
|
||||
export function createModelRegistry(authPath: string): ModelRegistry {
|
||||
return ModelRegistry.create(AuthStorage.create(authPath), getModelsJsonPath(authPath));
|
||||
function registerFeynmanModelOverlays(modelRegistry: ModelRegistry): void {
|
||||
const anthropicModels = getModels("anthropic");
|
||||
if (anthropicModels.some((model) => model.id === "claude-opus-4-7")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opus46 = anthropicModels.find((model) => model.id === "claude-opus-4-6");
|
||||
if (!opus46) {
|
||||
return;
|
||||
}
|
||||
|
||||
modelRegistry.registerProvider("anthropic", {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
oauth: anthropicOAuthProvider,
|
||||
models: [
|
||||
...anthropicModels,
|
||||
{
|
||||
...opus46,
|
||||
id: "claude-opus-4-7",
|
||||
name: "Claude Opus 4.7",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function createModelRegistry(authPath: string): ModelRegistry {
|
||||
const registry = ModelRegistry.create(AuthStorage.create(authPath), getModelsJsonPath(authPath));
|
||||
registerFeynmanModelOverlays(registry);
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
|
||||
@@ -423,6 +423,47 @@ function linkDirectory(linkPath: string, targetPath: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function packageNameToPath(root: string, packageName: string): string {
|
||||
return resolve(root, packageName);
|
||||
}
|
||||
|
||||
function packageDependencyExists(packagePath: string, globalNodeModulesRoot: string, dependency: string): boolean {
|
||||
return existsSync(packageNameToPath(resolve(packagePath, "node_modules"), dependency)) ||
|
||||
existsSync(packageNameToPath(globalNodeModulesRoot, dependency));
|
||||
}
|
||||
|
||||
function installedPackageLooksUsable(packagePath: string, globalNodeModulesRoot: string): boolean {
|
||||
if (!existsSync(resolve(packagePath, "package.json"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(resolve(packagePath, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
const dependencies = Object.keys(pkg.dependencies ?? {});
|
||||
return dependencies.every((dependency) => packageDependencyExists(packagePath, globalNodeModulesRoot, dependency));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBrokenPackageWithBundledCopy(targetPath: string, bundledPackagePath: string, globalNodeModulesRoot: string): boolean {
|
||||
if (!existsSync(targetPath)) {
|
||||
return false;
|
||||
}
|
||||
if (pathsMatchSymlinkTarget(targetPath, bundledPackagePath)) {
|
||||
return false;
|
||||
}
|
||||
if (installedPackageLooksUsable(targetPath, globalNodeModulesRoot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rmSync(targetPath, { recursive: true, force: true });
|
||||
linkDirectory(targetPath, bundledPackagePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function seedBundledWorkspacePackages(
|
||||
agentDir: string,
|
||||
appRoot: string,
|
||||
@@ -446,6 +487,10 @@ export function seedBundledWorkspacePackages(
|
||||
if (!existsSync(bundledPackagePath)) continue;
|
||||
|
||||
const targetPath = resolve(globalNodeModulesRoot, parsed.name);
|
||||
if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) {
|
||||
seeded.push(source);
|
||||
continue;
|
||||
}
|
||||
if (!existsSync(targetPath)) {
|
||||
linkDirectory(targetPath, bundledPackagePath);
|
||||
seeded.push(source);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
||||
import { resolveInitialPrompt, shouldRunInteractiveSetup } from "../src/cli.js";
|
||||
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel } from "../src/model/catalog.js";
|
||||
import { resolveModelProviderForCommand, setDefaultModelSpec } from "../src/model/commands.js";
|
||||
import { createModelRegistry } from "../src/model/registry.js";
|
||||
|
||||
function createAuthPath(contents: Record<string, unknown>): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-auth-"));
|
||||
@@ -26,6 +27,17 @@ test("chooseRecommendedModel prefers the strongest authenticated research model"
|
||||
assert.equal(recommendation?.spec, "anthropic/claude-opus-4-6");
|
||||
});
|
||||
|
||||
test("createModelRegistry overlays new Anthropic Opus model before upstream Pi updates", () => {
|
||||
const authPath = createAuthPath({
|
||||
anthropic: { type: "api_key", key: "anthropic-test-key" },
|
||||
});
|
||||
|
||||
const registry = createModelRegistry(authPath);
|
||||
|
||||
assert.ok(registry.find("anthropic", "claude-opus-4-7"));
|
||||
assert.equal(registry.getAvailable().some((model) => model.provider === "anthropic" && model.id === "claude-opus-4-7"), true);
|
||||
});
|
||||
|
||||
test("setDefaultModelSpec accepts a unique bare model id from authenticated models", () => {
|
||||
const authPath = createAuthPath({
|
||||
openai: { type: "api_key", key: "openai-test-key" },
|
||||
|
||||
@@ -6,13 +6,17 @@ import { join, resolve } from "node:path";
|
||||
|
||||
import { installPackageSources, seedBundledWorkspacePackages, updateConfiguredPackages } from "../src/pi/package-ops.js";
|
||||
|
||||
function createBundledWorkspace(appRoot: string, packageNames: string[]): void {
|
||||
function createBundledWorkspace(
|
||||
appRoot: string,
|
||||
packageNames: string[],
|
||||
dependenciesByPackage: Record<string, Record<string, string>> = {},
|
||||
): void {
|
||||
for (const packageName of packageNames) {
|
||||
const packageDir = resolve(appRoot, ".feynman", "npm", "node_modules", packageName);
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageDir, "package.json"),
|
||||
JSON.stringify({ name: packageName, version: "1.0.0" }, null, 2) + "\n",
|
||||
JSON.stringify({ name: packageName, version: "1.0.0", dependencies: dependenciesByPackage[packageName] }, null, 2) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
@@ -76,6 +80,33 @@ test("seedBundledWorkspacePackages preserves existing installed packages", () =>
|
||||
assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), false);
|
||||
});
|
||||
|
||||
test("seedBundledWorkspacePackages repairs broken existing bundled packages", () => {
|
||||
const appRoot = mkdtempSync(join(tmpdir(), "feynman-bundle-"));
|
||||
const homeRoot = mkdtempSync(join(tmpdir(), "feynman-home-"));
|
||||
const agentDir = resolve(homeRoot, "agent");
|
||||
const existingPackageDir = resolve(homeRoot, "npm-global", "lib", "node_modules", "pi-markdown-preview");
|
||||
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
createBundledWorkspace(appRoot, ["pi-markdown-preview", "puppeteer-core"], {
|
||||
"pi-markdown-preview": { "puppeteer-core": "^24.0.0" },
|
||||
});
|
||||
mkdirSync(existingPackageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
resolve(existingPackageDir, "package.json"),
|
||||
JSON.stringify({ name: "pi-markdown-preview", version: "broken", dependencies: { "puppeteer-core": "^24.0.0" } }) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const seeded = seedBundledWorkspacePackages(agentDir, appRoot, ["npm:pi-markdown-preview"]);
|
||||
|
||||
assert.deepEqual(seeded, ["npm:pi-markdown-preview"]);
|
||||
assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), true);
|
||||
assert.equal(
|
||||
readFileSync(resolve(existingPackageDir, "package.json"), "utf8").includes('"version": "1.0.0"'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("installPackageSources filters noisy npm chatter but preserves meaningful output", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "feynman-package-ops-"));
|
||||
const workingDir = resolve(root, "project");
|
||||
|
||||
@@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
||||
Workarounds:
|
||||
- try again after the release finishes publishing
|
||||
- pass the latest published version explicitly, e.g.:
|
||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.20
|
||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade
|
||||
Workarounds:
|
||||
- try again after the release finishes publishing
|
||||
- pass the latest published version explicitly, e.g.:
|
||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.20
|
||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
|
||||
"@
|
||||
}
|
||||
|
||||
|
||||
@@ -117,13 +117,13 @@ These installers download the bundled `skills/` and `prompts/` trees plus the re
|
||||
The one-line installer already targets the latest tagged release. To pin an exact version, pass it explicitly:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.20
|
||||
curl -fsSL https://feynman.is/install | bash -s -- 0.2.21
|
||||
```
|
||||
|
||||
On Windows:
|
||||
|
||||
```powershell
|
||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.20
|
||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.21
|
||||
```
|
||||
|
||||
## Post-install setup
|
||||
|
||||
Reference in New Issue
Block a user