Add ASCII logo, spinner during install, all packages core, fix tests

- ASCII art logo on website hero and OAuth callback page
- Clean spinner during postinstall instead of npm noise
- pi-session-search and pi-memory moved to core (13 packages)
- pi-generative-ui is the only optional package
- Alpha Hub callback page branded with Feynman logo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Advait Paliwal
2026-03-23 22:33:35 -07:00
parent e73743d407
commit 7ef1ca2859
12 changed files with 97 additions and 38 deletions

View File

@@ -9,7 +9,9 @@
"npm:pi-mermaid", "npm:pi-mermaid",
"npm:@aliou/pi-processes", "npm:@aliou/pi-processes",
"npm:pi-zotero", "npm:pi-zotero",
"npm:@kaiserlich-dev/pi-session-search",
"npm:pi-schedule-prompt", "npm:pi-schedule-prompt",
"npm:@samfp/pi-memory",
"npm:@tmustier/pi-ralph-wiggum" "npm:@tmustier/pi-ralph-wiggum"
], ],
"quietStartup": true, "quietStartup": true,

View File

@@ -14,14 +14,7 @@ export const FEYNMAN_VERSION = (() => {
} }
})(); })();
export const FEYNMAN_AGENT_LOGO = [ export { FEYNMAN_ASCII_LOGO as FEYNMAN_AGENT_LOGO } from "../../logo.mjs";
"███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
"██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
"█████╗ █████╗ ╚████╔╝ ██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║",
"██╔══╝ ██╔══╝ ╚██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║",
"██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
"╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
];
export const FEYNMAN_RESEARCH_TOOLS = [ export const FEYNMAN_RESEARCH_TOOLS = [
"alpha_search", "alpha_search",

3
logo.d.mts Normal file
View File

@@ -0,0 +1,3 @@
export declare const FEYNMAN_ASCII_LOGO: string[];
export declare const FEYNMAN_ASCII_LOGO_TEXT: string;
export declare const FEYNMAN_ASCII_LOGO_HTML: string;

12
logo.mjs Normal file
View File

@@ -0,0 +1,12 @@
export const FEYNMAN_ASCII_LOGO = [
"███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
"██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
"█████╗ █████╗ ╚████╔╝ ██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║",
"██╔══╝ ██╔══╝ ╚██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║",
"██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
"╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
];
export const FEYNMAN_ASCII_LOGO_TEXT = FEYNMAN_ASCII_LOGO.join("\n");
export const FEYNMAN_ASCII_LOGO_HTML = `<pre style="font-size:8px;line-height:1.15;font-weight:700;color:#10b981;font-family:monospace;white-space:pre;margin:0">${FEYNMAN_ASCII_LOGO_TEXT}</pre>`;

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.2.6", "version": "0.2.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.2.6", "version": "0.2.7",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@companion-ai/alpha-hub": "^0.1.2", "@companion-ai/alpha-hub": "^0.1.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@companion-ai/feynman", "name": "@companion-ai/feynman",
"version": "0.2.6", "version": "0.2.7",
"description": "Research-first CLI agent built on Pi and alphaXiv", "description": "Research-first CLI agent built on Pi and alphaXiv",
"type": "module", "type": "module",
"engines": { "engines": {

View File

@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { FEYNMAN_ASCII_LOGO_HTML } from "../logo.mjs";
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
const appRoot = resolve(here, ".."); const appRoot = resolve(here, "..");
@@ -90,14 +91,27 @@ function ensurePackageWorkspace() {
"utf8", "utf8",
); );
console.log("[feynman] installing research packages..."); const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const result = spawnSync("npm", ["install", "--prefer-offline", "--no-audit", "--no-fund", "--prefix", workspaceDir, ...packageSpecs], { let frame = 0;
stdio: "inherit", const start = Date.now();
const spinner = setInterval(() => {
const elapsed = Math.round((Date.now() - start) / 1000);
process.stderr.write(`\r${frames[frame++ % frames.length]} setting up feynman... ${elapsed}s`);
}, 80);
const result = spawnSync("npm", ["install", "--prefer-offline", "--no-audit", "--no-fund", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs], {
stdio: ["ignore", "ignore", "pipe"],
timeout: 300000, timeout: 300000,
}); });
clearInterval(spinner);
const elapsed = Math.round((Date.now() - start) / 1000);
if (result.status !== 0) { if (result.status !== 0) {
console.warn("[feynman] warning: package install failed, Pi will retry on first launch"); process.stderr.write(`\r✗ setup failed (${elapsed}s)\n`);
if (result.stderr?.length) process.stderr.write(result.stderr);
} else {
process.stderr.write(`\r✓ feynman ready (${elapsed}s)\n`);
} }
} }
@@ -351,12 +365,33 @@ if (oauthPagePath && existsSync(oauthPagePath)) {
let source = readFileSync(oauthPagePath, "utf8"); let source = readFileSync(oauthPagePath, "utf8");
const piLogo = 'const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>`;'; const piLogo = 'const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg>`;';
if (source.includes(piLogo)) { if (source.includes(piLogo)) {
const feynmanLogo = 'const LOGO_SVG = `<span style="font-size:32px;font-weight:700;color:#10b981;font-family:system-ui,sans-serif;letter-spacing:-0.02em">feynman</span>`;'; const feynmanLogo = `const LOGO_SVG = \`${FEYNMAN_ASCII_LOGO_HTML}\`;`;
source = source.replace(piLogo, feynmanLogo); source = source.replace(piLogo, feynmanLogo);
writeFileSync(oauthPagePath, source, "utf8"); writeFileSync(oauthPagePath, source, "utf8");
} }
} }
const alphaHubAuthPath = findPackageRoot("@companion-ai/alpha-hub")
? resolve(findPackageRoot("@companion-ai/alpha-hub"), "src", "lib", "auth.js")
: null;
if (alphaHubAuthPath && existsSync(alphaHubAuthPath)) {
let source = readFileSync(alphaHubAuthPath, "utf8");
const callbackStyle = `style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:80vh;background:#050a08;color:#f0f5f2"`;
const logoHtml = FEYNMAN_ASCII_LOGO_HTML.replace('color:#10b981', 'color:#34d399');
const successPage = `<html><body ${callbackStyle}>${logoHtml}<h2 style="color:#34d399;margin-top:24px">Logged in</h2><p style="color:#8aaa9a">You can close this tab.</p></body></html>`;
const errorPage = `<html><body ${callbackStyle}>${logoHtml}<h2 style="color:#ef4444;margin-top:24px">Login failed</h2><p style="color:#8aaa9a">You can close this tab.</p></body></html>`;
const oldSuccess = `'<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>'`;
const oldError = `'<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>'`;
if (source.includes(oldSuccess)) {
source = source.replace(oldSuccess, `'${successPage}'`);
}
if (source.includes(oldError)) {
source = source.replace(oldError, `'${errorPage}'`);
}
writeFileSync(alphaHubAuthPath, source, "utf8");
}
if (existsSync(piMemoryPath)) { if (existsSync(piMemoryPath)) {
let source = readFileSync(piMemoryPath, "utf8"); let source = readFileSync(piMemoryPath, "utf8");
const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");'; const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");';

View File

@@ -10,7 +10,9 @@ export const CORE_PACKAGE_SOURCES = [
"npm:pi-mermaid", "npm:pi-mermaid",
"npm:@aliou/pi-processes", "npm:@aliou/pi-processes",
"npm:pi-zotero", "npm:pi-zotero",
"npm:@kaiserlich-dev/pi-session-search",
"npm:pi-schedule-prompt", "npm:pi-schedule-prompt",
"npm:@samfp/pi-memory",
"npm:@tmustier/pi-ralph-wiggum", "npm:@tmustier/pi-ralph-wiggum",
] as const; ] as const;
@@ -19,25 +21,11 @@ export const OPTIONAL_PACKAGE_PRESETS = {
description: "Interactive Glimpse UI widgets.", description: "Interactive Glimpse UI widgets.",
sources: ["npm:pi-generative-ui"], sources: ["npm:pi-generative-ui"],
}, },
memory: {
description: "Cross-session memory and preference recall.",
sources: ["npm:@samfp/pi-memory"],
},
"session-search": {
description: "Indexed session recall with SQLite-backed search.",
sources: ["npm:@kaiserlich-dev/pi-session-search"],
},
"all-extras": {
description: "Install all optional packages.",
sources: ["npm:pi-generative-ui", "npm:@samfp/pi-memory", "npm:@kaiserlich-dev/pi-session-search"],
},
} as const; } as const;
const LEGACY_DEFAULT_PACKAGE_SOURCES = [ const LEGACY_DEFAULT_PACKAGE_SOURCES = [
...CORE_PACKAGE_SOURCES, ...CORE_PACKAGE_SOURCES,
"npm:pi-generative-ui", "npm:pi-generative-ui",
"npm:@kaiserlich-dev/pi-session-search",
"npm:@samfp/pi-memory",
] as const; ] as const;
export type OptionalPackagePresetName = keyof typeof OPTIONAL_PACKAGE_PRESETS; export type OptionalPackagePresetName = keyof typeof OPTIONAL_PACKAGE_PRESETS;
@@ -66,9 +54,6 @@ export function getOptionalPackagePresetSources(name: string): string[] | undefi
if (normalized === "ui") { if (normalized === "ui") {
return [...OPTIONAL_PACKAGE_PRESETS["generative-ui"].sources]; return [...OPTIONAL_PACKAGE_PRESETS["generative-ui"].sources];
} }
if (normalized === "search") {
return [...OPTIONAL_PACKAGE_PRESETS["session-search"].sources];
}
const preset = OPTIONAL_PACKAGE_PRESETS[normalized as OptionalPackagePresetName]; const preset = OPTIONAL_PACKAGE_PRESETS[normalized as OptionalPackagePresetName];
return preset ? [...preset.sources] : undefined; return preset ? [...preset.sources] : undefined;

View File

@@ -49,8 +49,6 @@ test("normalizeFeynmanSettings prunes the legacy slow default package set", () =
packages: [ packages: [
...CORE_PACKAGE_SOURCES, ...CORE_PACKAGE_SOURCES,
"npm:pi-generative-ui", "npm:pi-generative-ui",
"npm:@kaiserlich-dev/pi-session-search",
"npm:@samfp/pi-memory",
], ],
}, },
null, null,
@@ -68,8 +66,8 @@ test("normalizeFeynmanSettings prunes the legacy slow default package set", () =
}); });
test("optional package presets map friendly aliases", () => { test("optional package presets map friendly aliases", () => {
assert.deepEqual(getOptionalPackagePresetSources("memory"), ["npm:@samfp/pi-memory"]); assert.deepEqual(getOptionalPackagePresetSources("memory"), undefined);
assert.deepEqual(getOptionalPackagePresetSources("ui"), ["npm:pi-generative-ui"]); assert.deepEqual(getOptionalPackagePresetSources("ui"), ["npm:pi-generative-ui"]);
assert.deepEqual(getOptionalPackagePresetSources("search"), ["npm:@kaiserlich-dev/pi-session-search"]); assert.deepEqual(getOptionalPackagePresetSources("search"), undefined);
assert.equal(shouldPruneLegacyDefaultPackages(["npm:custom"]), false); assert.equal(shouldPruneLegacyDefaultPackages(["npm:custom"]), false);
}); });

View File

@@ -0,0 +1,26 @@
---
interface Props {
class?: string;
size?: 'nav' | 'hero';
}
const { class: className = '', size = 'hero' } = Astro.props;
const sizeClasses = size === 'nav'
? 'text-[4px] sm:text-[5px]'
: 'text-[6px] sm:text-[8px] md:text-[10px]';
---
<pre
class:list={[
'font-mono text-accent font-bold leading-[1.15] m-0 p-0 inline-block',
sizeClasses,
className,
]}
aria-label="Feynman"
>███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗
██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║
█████╗ █████╗ ╚████╔╝ ██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║
██╔══╝ ██╔══╝ ╚██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║
██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝</pre>

View File

@@ -1,5 +1,6 @@
--- ---
import ThemeToggle from './ThemeToggle.astro'; import ThemeToggle from './ThemeToggle.astro';
import AsciiLogo from './AsciiLogo.astro';
interface Props { interface Props {
active?: 'home' | 'docs'; active?: 'home' | 'docs';
@@ -10,7 +11,9 @@ const { active = 'home' } = Astro.props;
<nav class="sticky top-0 z-50 bg-bg"> <nav class="sticky top-0 z-50 bg-bg">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between"> <div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<a href="/" class="text-xl font-bold text-accent tracking-tight">Feynman</a> <a href="/" class="hover:opacity-80 transition-opacity" aria-label="Feynman">
<AsciiLogo size="nav" />
</a>
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<a href="/docs/getting-started/installation" <a href="/docs/getting-started/installation"
class:list={["text-sm transition-colors", active === 'docs' ? 'text-text-primary' : 'text-text-muted hover:text-text-primary']}> class:list={["text-sm transition-colors", active === 'docs' ? 'text-text-primary' : 'text-text-muted hover:text-text-primary']}>

View File

@@ -1,10 +1,12 @@
--- ---
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
import AsciiLogo from '../components/AsciiLogo.astro';
--- ---
<Base title="Feynman — The open source AI research agent" active="home"> <Base title="Feynman — The open source AI research agent" active="home">
<section class="text-center pt-24 pb-20 px-6"> <section class="text-center pt-24 pb-20 px-6">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<AsciiLogo size="hero" class="mb-8" />
<h1 class="text-5xl sm:text-6xl font-bold tracking-tight mb-6" style="text-wrap: balance">The open source AI research agent</h1> <h1 class="text-5xl sm:text-6xl font-bold tracking-tight mb-6" style="text-wrap: balance">The open source AI research agent</h1>
<p class="text-lg text-text-muted mb-10 leading-relaxed" style="text-wrap: pretty">Investigate topics, write papers, run experiments, review research, audit codebases &mdash; every output cited and source-grounded</p> <p class="text-lg text-text-muted mb-10 leading-relaxed" style="text-wrap: pretty">Investigate topics, write papers, run experiments, review research, audit codebases &mdash; every output cited and source-grounded</p>
<div class="inline-flex items-center gap-3 bg-surface rounded-lg px-5 py-3 mb-8 font-mono text-sm"> <div class="inline-flex items-center gap-3 bg-surface rounded-lg px-5 py-3 mb-8 font-mono text-sm">