Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
584d065902 | ||
|
|
151956ea24 | ||
|
|
75b0467761 |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -112,6 +112,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- shell: bash
|
- shell: bash
|
||||||
env:
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
VERSION: ${{ needs.version-check.outputs.version }}
|
VERSION: ${{ needs.version-check.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
@@ -153,6 +154,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- shell: bash
|
- shell: bash
|
||||||
env:
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
VERSION: ${{ needs.version-check.outputs.version }}
|
VERSION: ${{ needs.version-check.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@companion-ai/feynman",
|
"name": "@companion-ai/feynman",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@companion-ai/feynman",
|
"name": "@companion-ai/feynman",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@companion-ai/alpha-hub": "^0.1.2",
|
"@companion-ai/alpha-hub": "^0.1.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@companion-ai/feynman",
|
"name": "@companion-ai/feynman",
|
||||||
"version": "0.2.13",
|
"version": "0.2.14",
|
||||||
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
25
skills/eli5/SKILL.md
Normal file
25
skills/eli5/SKILL.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: eli5
|
||||||
|
description: Explain research, papers, or technical ideas in plain English with minimal jargon, concrete analogies, and clear takeaways. Use when the user says "ELI5 this", asks for a simple explanation of a paper or research result, wants jargon removed, or asks what something technically dense actually means.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ELI5
|
||||||
|
|
||||||
|
Use `alpha` first when the user names a specific paper, arXiv id, DOI, or paper URL.
|
||||||
|
|
||||||
|
If the user gives only a topic, identify 1-3 representative papers and anchor the explanation around the clearest or most important one.
|
||||||
|
|
||||||
|
Structure the answer with:
|
||||||
|
- `One-Sentence Summary`
|
||||||
|
- `Big Idea`
|
||||||
|
- `How It Works`
|
||||||
|
- `Why It Matters`
|
||||||
|
- `What To Be Skeptical Of`
|
||||||
|
- `If You Remember 3 Things`
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- Use short sentences and concrete words.
|
||||||
|
- Define jargon immediately or remove it.
|
||||||
|
- Prefer one good analogy over several weak ones.
|
||||||
|
- Separate what the paper actually shows from speculation or interpretation.
|
||||||
|
- Keep the explanation inline unless the user explicitly asks to save it as an artifact.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, relative, resolve } from "node:path";
|
import { dirname, relative, resolve } from "node:path";
|
||||||
|
|
||||||
import { getBootstrapStatePath } from "../config/paths.js";
|
import { getBootstrapStatePath } from "../config/paths.js";
|
||||||
@@ -64,27 +64,76 @@ function listFiles(root: string): string[] {
|
|||||||
return files.sort();
|
return files.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeEmptyParentDirectories(path: string, stopAt: string): void {
|
||||||
|
let current = dirname(path);
|
||||||
|
while (current.startsWith(stopAt) && current !== stopAt) {
|
||||||
|
if (!existsSync(current)) {
|
||||||
|
current = dirname(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (readdirSync(current).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rmSync(current, { recursive: true, force: true });
|
||||||
|
current = dirname(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function syncManagedFiles(
|
function syncManagedFiles(
|
||||||
sourceRoot: string,
|
sourceRoot: string,
|
||||||
targetRoot: string,
|
targetRoot: string,
|
||||||
|
scope: string,
|
||||||
state: BootstrapState,
|
state: BootstrapState,
|
||||||
result: BootstrapSyncResult,
|
result: BootstrapSyncResult,
|
||||||
): void {
|
): void {
|
||||||
|
const sourcePaths = new Set(listFiles(sourceRoot).map((sourcePath) => relative(sourceRoot, sourcePath)));
|
||||||
|
|
||||||
|
for (const targetPath of listFiles(targetRoot)) {
|
||||||
|
const key = relative(targetRoot, targetPath);
|
||||||
|
if (sourcePaths.has(key)) continue;
|
||||||
|
|
||||||
|
const scopedKey = `${scope}:${key}`;
|
||||||
|
const previous = state.files[scopedKey] ?? state.files[key];
|
||||||
|
if (!previous) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
delete state.files[scopedKey];
|
||||||
|
delete state.files[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTargetText = readFileSync(targetPath, "utf8");
|
||||||
|
const currentTargetHash = sha256(currentTargetText);
|
||||||
|
if (currentTargetHash !== previous.lastAppliedTargetHash) {
|
||||||
|
result.skipped.push(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(targetPath, { force: true });
|
||||||
|
removeEmptyParentDirectories(targetPath, targetRoot);
|
||||||
|
delete state.files[scopedKey];
|
||||||
|
delete state.files[key];
|
||||||
|
}
|
||||||
|
|
||||||
for (const sourcePath of listFiles(sourceRoot)) {
|
for (const sourcePath of listFiles(sourceRoot)) {
|
||||||
const key = relative(sourceRoot, sourcePath);
|
const key = relative(sourceRoot, sourcePath);
|
||||||
const targetPath = resolve(targetRoot, key);
|
const targetPath = resolve(targetRoot, key);
|
||||||
const sourceText = readFileSync(sourcePath, "utf8");
|
const sourceText = readFileSync(sourcePath, "utf8");
|
||||||
const sourceHash = sha256(sourceText);
|
const sourceHash = sha256(sourceText);
|
||||||
const previous = state.files[key];
|
const scopedKey = `${scope}:${key}`;
|
||||||
|
const previous = state.files[scopedKey] ?? state.files[key];
|
||||||
|
|
||||||
mkdirSync(dirname(targetPath), { recursive: true });
|
mkdirSync(dirname(targetPath), { recursive: true });
|
||||||
|
|
||||||
if (!existsSync(targetPath)) {
|
if (!existsSync(targetPath)) {
|
||||||
writeFileSync(targetPath, sourceText, "utf8");
|
writeFileSync(targetPath, sourceText, "utf8");
|
||||||
state.files[key] = {
|
state.files[scopedKey] = {
|
||||||
lastAppliedSourceHash: sourceHash,
|
lastAppliedSourceHash: sourceHash,
|
||||||
lastAppliedTargetHash: sourceHash,
|
lastAppliedTargetHash: sourceHash,
|
||||||
};
|
};
|
||||||
|
delete state.files[key];
|
||||||
result.copied.push(key);
|
result.copied.push(key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -93,10 +142,11 @@ function syncManagedFiles(
|
|||||||
const currentTargetHash = sha256(currentTargetText);
|
const currentTargetHash = sha256(currentTargetText);
|
||||||
|
|
||||||
if (currentTargetHash === sourceHash) {
|
if (currentTargetHash === sourceHash) {
|
||||||
state.files[key] = {
|
state.files[scopedKey] = {
|
||||||
lastAppliedSourceHash: sourceHash,
|
lastAppliedSourceHash: sourceHash,
|
||||||
lastAppliedTargetHash: currentTargetHash,
|
lastAppliedTargetHash: currentTargetHash,
|
||||||
};
|
};
|
||||||
|
delete state.files[key];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,10 +161,11 @@ function syncManagedFiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(targetPath, sourceText, "utf8");
|
writeFileSync(targetPath, sourceText, "utf8");
|
||||||
state.files[key] = {
|
state.files[scopedKey] = {
|
||||||
lastAppliedSourceHash: sourceHash,
|
lastAppliedSourceHash: sourceHash,
|
||||||
lastAppliedTargetHash: sourceHash,
|
lastAppliedTargetHash: sourceHash,
|
||||||
};
|
};
|
||||||
|
delete state.files[key];
|
||||||
result.updated.push(key);
|
result.updated.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,9 +179,9 @@ export function syncBundledAssets(appRoot: string, agentDir: string): BootstrapS
|
|||||||
skipped: [],
|
skipped: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
syncManagedFiles(resolve(appRoot, ".feynman", "themes"), resolve(agentDir, "themes"), state, result);
|
syncManagedFiles(resolve(appRoot, ".feynman", "themes"), resolve(agentDir, "themes"), "themes", state, result);
|
||||||
syncManagedFiles(resolve(appRoot, ".feynman", "agents"), resolve(agentDir, "agents"), state, result);
|
syncManagedFiles(resolve(appRoot, ".feynman", "agents"), resolve(agentDir, "agents"), "agents", state, result);
|
||||||
syncManagedFiles(resolve(appRoot, "skills"), resolve(agentDir, "skills"), state, result);
|
syncManagedFiles(resolve(appRoot, "skills"), resolve(agentDir, "skills"), "skills", state, result);
|
||||||
|
|
||||||
writeBootstrapState(statePath, state);
|
writeBootstrapState(statePath, state);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { writeFileSync } from "node:fs";
|
|||||||
|
|
||||||
import { readJson } from "../pi/settings.js";
|
import { readJson } from "../pi/settings.js";
|
||||||
import { promptChoice, promptText } from "../setup/prompts.js";
|
import { promptChoice, promptText } from "../setup/prompts.js";
|
||||||
|
import { openUrl } from "../system/open-url.js";
|
||||||
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
||||||
import {
|
import {
|
||||||
buildModelStatusSnapshotFromRecords,
|
buildModelStatusSnapshotFromRecords,
|
||||||
@@ -126,7 +127,13 @@ export async function loginModelProvider(authPath: string, providerId?: string,
|
|||||||
await authStorage.login(provider.id, {
|
await authStorage.login(provider.id, {
|
||||||
onAuth: (info: { url: string; instructions?: string }) => {
|
onAuth: (info: { url: string; instructions?: string }) => {
|
||||||
printSection(`Login: ${provider.name ?? provider.id}`);
|
printSection(`Login: ${provider.name ?? provider.id}`);
|
||||||
printInfo(`Open this URL: ${info.url}`);
|
const opened = openUrl(info.url);
|
||||||
|
if (opened) {
|
||||||
|
printInfo("Opened the login URL in your browser.");
|
||||||
|
} else {
|
||||||
|
printWarning("Couldn't open your browser automatically.");
|
||||||
|
}
|
||||||
|
printInfo(`Auth URL: ${info.url}`);
|
||||||
if (info.instructions) {
|
if (info.instructions) {
|
||||||
printInfo(info.instructions);
|
printInfo(info.instructions);
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/system/open-url.ts
Normal file
51
src/system/open-url.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
import { resolveExecutable } from "./executables.js";
|
||||||
|
|
||||||
|
type ResolveExecutableFn = (name: string, fallbackPaths?: string[]) => string | undefined;
|
||||||
|
|
||||||
|
type OpenUrlCommand = {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getOpenUrlCommand(
|
||||||
|
url: string,
|
||||||
|
platform = process.platform,
|
||||||
|
resolveCommand: ResolveExecutableFn = resolveExecutable,
|
||||||
|
): OpenUrlCommand | undefined {
|
||||||
|
if (platform === "win32") {
|
||||||
|
return {
|
||||||
|
command: "cmd",
|
||||||
|
args: ["/c", "start", "", url],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === "darwin") {
|
||||||
|
const command = resolveCommand("open");
|
||||||
|
return command ? { command, args: [url] } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = resolveCommand("xdg-open");
|
||||||
|
return command ? { command, args: [url] } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openUrl(url: string): boolean {
|
||||||
|
const command = getOpenUrlCommand(url);
|
||||||
|
if (!command) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const child = spawn(command.command, command.args, {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
child.on("error", () => {});
|
||||||
|
child.unref();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
@@ -49,3 +49,34 @@ test("syncBundledAssets preserves user-modified files and updates managed files"
|
|||||||
assert.equal(readFileSync(join(agentDir, "themes", "feynman.json"), "utf8"), '{"theme":"v2"}\n');
|
assert.equal(readFileSync(join(agentDir, "themes", "feynman.json"), "utf8"), '{"theme":"v2"}\n');
|
||||||
assert.equal(readFileSync(join(agentDir, "agents", "researcher.md"), "utf8"), "# user-custom\n");
|
assert.equal(readFileSync(join(agentDir, "agents", "researcher.md"), "utf8"), "# user-custom\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("syncBundledAssets removes deleted managed files but preserves user-modified stale files", () => {
|
||||||
|
const appRoot = createAppRoot();
|
||||||
|
const home = mkdtempSync(join(tmpdir(), "feynman-home-"));
|
||||||
|
process.env.FEYNMAN_HOME = home;
|
||||||
|
const agentDir = join(home, "agent");
|
||||||
|
mkdirSync(agentDir, { recursive: true });
|
||||||
|
|
||||||
|
mkdirSync(join(appRoot, "skills", "paper-eli5"), { recursive: true });
|
||||||
|
writeFileSync(join(appRoot, "skills", "paper-eli5", "SKILL.md"), "# old skill\n", "utf8");
|
||||||
|
syncBundledAssets(appRoot, agentDir);
|
||||||
|
|
||||||
|
rmSync(join(appRoot, "skills", "paper-eli5"), { recursive: true, force: true });
|
||||||
|
mkdirSync(join(appRoot, "skills", "eli5"), { recursive: true });
|
||||||
|
writeFileSync(join(appRoot, "skills", "eli5", "SKILL.md"), "# new skill\n", "utf8");
|
||||||
|
|
||||||
|
const firstResult = syncBundledAssets(appRoot, agentDir);
|
||||||
|
assert.deepEqual(firstResult.copied, ["eli5/SKILL.md"]);
|
||||||
|
assert.equal(existsSync(join(agentDir, "skills", "paper-eli5", "SKILL.md")), false);
|
||||||
|
assert.equal(readFileSync(join(agentDir, "skills", "eli5", "SKILL.md"), "utf8"), "# new skill\n");
|
||||||
|
|
||||||
|
mkdirSync(join(appRoot, "skills", "legacy"), { recursive: true });
|
||||||
|
writeFileSync(join(appRoot, "skills", "legacy", "SKILL.md"), "# managed legacy\n", "utf8");
|
||||||
|
syncBundledAssets(appRoot, agentDir);
|
||||||
|
writeFileSync(join(agentDir, "skills", "legacy", "SKILL.md"), "# user legacy override\n", "utf8");
|
||||||
|
rmSync(join(appRoot, "skills", "legacy"), { recursive: true, force: true });
|
||||||
|
|
||||||
|
const secondResult = syncBundledAssets(appRoot, agentDir);
|
||||||
|
assert.deepEqual(secondResult.skipped, ["legacy/SKILL.md"]);
|
||||||
|
assert.equal(readFileSync(join(agentDir, "skills", "legacy", "SKILL.md"), "utf8"), "# user legacy override\n");
|
||||||
|
});
|
||||||
|
|||||||
45
tests/open-url.test.ts
Normal file
45
tests/open-url.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { getOpenUrlCommand } from "../src/system/open-url.js";
|
||||||
|
|
||||||
|
test("getOpenUrlCommand uses open on macOS when available", () => {
|
||||||
|
const command = getOpenUrlCommand(
|
||||||
|
"https://example.com",
|
||||||
|
"darwin",
|
||||||
|
(name) => (name === "open" ? "/usr/bin/open" : undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(command, {
|
||||||
|
command: "/usr/bin/open",
|
||||||
|
args: ["https://example.com"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getOpenUrlCommand uses xdg-open on Linux when available", () => {
|
||||||
|
const command = getOpenUrlCommand(
|
||||||
|
"https://example.com",
|
||||||
|
"linux",
|
||||||
|
(name) => (name === "xdg-open" ? "/usr/bin/xdg-open" : undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(command, {
|
||||||
|
command: "/usr/bin/xdg-open",
|
||||||
|
args: ["https://example.com"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getOpenUrlCommand uses cmd start on Windows", () => {
|
||||||
|
const command = getOpenUrlCommand("https://example.com", "win32");
|
||||||
|
|
||||||
|
assert.deepEqual(command, {
|
||||||
|
command: "cmd",
|
||||||
|
args: ["/c", "start", "", "https://example.com"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getOpenUrlCommand returns undefined when no opener is available", () => {
|
||||||
|
const command = getOpenUrlCommand("https://example.com", "linux", () => undefined);
|
||||||
|
|
||||||
|
assert.equal(command, undefined);
|
||||||
|
});
|
||||||
@@ -41,7 +41,7 @@ On Windows:
|
|||||||
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version stable
|
& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version stable
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also pin an exact version by replacing `stable` with a version such as `0.2.13`.
|
You can also pin an exact version by replacing `stable` with a version such as `0.2.14`.
|
||||||
|
|
||||||
## pnpm
|
## pnpm
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const workflows = [
|
|||||||
{ command: "/draft", description: "Polished paper-style draft with inline citations from findings" },
|
{ command: "/draft", description: "Polished paper-style draft with inline citations from findings" },
|
||||||
{ command: "/autoresearch", description: "Autonomous loop: hypothesize, experiment, measure, repeat" },
|
{ command: "/autoresearch", description: "Autonomous loop: hypothesize, experiment, measure, repeat" },
|
||||||
{ command: "/watch", description: "Recurring monitor for new papers, code, or product updates" },
|
{ command: "/watch", description: "Recurring monitor for new papers, code, or product updates" },
|
||||||
{ command: "/outputs", description: "Browse all research artifacts, papers, notes, and experiments" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const agents = [
|
const agents = [
|
||||||
|
|||||||
Reference in New Issue
Block a user