@companion-ai/feynman 0.2.5 → 0.2.7

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.
@@ -6,7 +6,6 @@
6
6
  "npm:pi-web-access",
7
7
  "npm:pi-markdown-preview",
8
8
  "npm:@walterra/pi-charts",
9
- "npm:pi-generative-ui",
10
9
  "npm:pi-mermaid",
11
10
  "npm:@aliou/pi-processes",
12
11
  "npm:pi-zotero",
package/README.md CHANGED
@@ -61,7 +61,7 @@ Four bundled research agents, dispatched automatically or via subagent commands.
61
61
  - **Docker** — isolated container execution for safe experiments on your machine
62
62
  - **[Agent Computer](https://agentcomputer.ai)** — secure cloud execution for long-running research and GPU workloads
63
63
  - **Web search** — Gemini or Perplexity, zero-config default via signed-in Chromium
64
- - **Session search** — indexed recall across prior research sessions
64
+ - **Session search** — optional indexed recall across prior research sessions
65
65
  - **Preview** — browser and PDF export of generated artifacts
66
66
 
67
67
  ---
@@ -76,6 +76,8 @@ feynman status # current config summary
76
76
  feynman model login [provider] # model auth
77
77
  feynman model set <provider/model> # set default model
78
78
  feynman alpha login # alphaXiv auth
79
+ feynman packages list # core vs optional packages
80
+ feynman packages install memory # opt into heavier packages on demand
79
81
  feynman search status # web search config
80
82
  ```
81
83
 
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { AuthStorage, DefaultPackageManager, ModelRegistry, SettingsManager } fr
8
8
  import { syncBundledAssets } from "./bootstrap/sync.js";
9
9
  import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
10
10
  import { launchPiChat } from "./pi/launch.js";
11
+ import { CORE_PACKAGE_SOURCES, getOptionalPackagePresetSources, listOptionalPackagePresets } from "./pi/package-presets.js";
11
12
  import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js";
12
13
  import { loginModelProvider, logoutModelProvider, printModelList, setDefaultModelSpec, } from "./model/commands.js";
13
14
  import { printSearchStatus } from "./search/commands.js";
@@ -125,6 +126,65 @@ async function handleUpdateCommand(workingDir, feynmanAgentDir, source) {
125
126
  await settingsManager.flush();
126
127
  console.log("All packages up to date.");
127
128
  }
129
+ async function handlePackagesCommand(subcommand, args, workingDir, feynmanAgentDir) {
130
+ const settingsManager = SettingsManager.create(workingDir, feynmanAgentDir);
131
+ const configuredSources = new Set(settingsManager
132
+ .getPackages()
133
+ .map((entry) => (typeof entry === "string" ? entry : entry.source))
134
+ .filter((entry) => typeof entry === "string"));
135
+ if (!subcommand || subcommand === "list") {
136
+ printPanel("Feynman Packages", [
137
+ "Core packages are installed by default to keep first-run setup fast.",
138
+ ]);
139
+ printSection("Core");
140
+ for (const source of CORE_PACKAGE_SOURCES) {
141
+ printInfo(source);
142
+ }
143
+ printSection("Optional");
144
+ for (const preset of listOptionalPackagePresets()) {
145
+ const installed = preset.sources.every((source) => configuredSources.has(source));
146
+ printInfo(`${preset.name}${installed ? " (installed)" : ""} ${preset.description}`);
147
+ }
148
+ printInfo("Install with: feynman packages install <preset>");
149
+ return;
150
+ }
151
+ if (subcommand !== "install") {
152
+ throw new Error(`Unknown packages command: ${subcommand}`);
153
+ }
154
+ const target = args[0];
155
+ if (!target) {
156
+ throw new Error("Usage: feynman packages install <generative-ui|memory|session-search|all-extras>");
157
+ }
158
+ const sources = getOptionalPackagePresetSources(target);
159
+ if (!sources) {
160
+ throw new Error(`Unknown package preset: ${target}`);
161
+ }
162
+ const packageManager = new DefaultPackageManager({
163
+ cwd: workingDir,
164
+ agentDir: feynmanAgentDir,
165
+ settingsManager,
166
+ });
167
+ packageManager.setProgressCallback((event) => {
168
+ if (event.type === "start") {
169
+ console.log(`Installing ${event.source}...`);
170
+ }
171
+ else if (event.type === "complete") {
172
+ console.log(`Installed ${event.source}`);
173
+ }
174
+ else if (event.type === "error") {
175
+ console.error(`Failed to install ${event.source}: ${event.message ?? "unknown error"}`);
176
+ }
177
+ });
178
+ for (const source of sources) {
179
+ if (configuredSources.has(source)) {
180
+ console.log(`${source} already installed`);
181
+ continue;
182
+ }
183
+ await packageManager.install(source);
184
+ }
185
+ await settingsManager.flush();
186
+ console.log("Optional packages installed.");
187
+ }
128
188
  function handleSearchCommand(subcommand) {
129
189
  if (!subcommand || subcommand === "status") {
130
190
  printSearchStatus();
@@ -267,6 +327,10 @@ export async function main() {
267
327
  handleSearchCommand(rest[0]);
268
328
  return;
269
329
  }
330
+ if (command === "packages") {
331
+ await handlePackagesCommand(rest[0], rest.slice(1), workingDir, feynmanAgentDir);
332
+ return;
333
+ }
270
334
  if (command === "update") {
271
335
  await handleUpdateCommand(workingDir, feynmanAgentDir, rest[0]);
272
336
  return;
@@ -0,0 +1,56 @@
1
+ export const CORE_PACKAGE_SOURCES = [
2
+ "npm:pi-subagents",
3
+ "npm:pi-btw",
4
+ "npm:pi-docparser",
5
+ "npm:pi-web-access",
6
+ "npm:pi-markdown-preview",
7
+ "npm:@walterra/pi-charts",
8
+ "npm:pi-mermaid",
9
+ "npm:@aliou/pi-processes",
10
+ "npm:pi-zotero",
11
+ "npm:@kaiserlich-dev/pi-session-search",
12
+ "npm:pi-schedule-prompt",
13
+ "npm:@samfp/pi-memory",
14
+ "npm:@tmustier/pi-ralph-wiggum",
15
+ ];
16
+ export const OPTIONAL_PACKAGE_PRESETS = {
17
+ "generative-ui": {
18
+ description: "Interactive Glimpse UI widgets.",
19
+ sources: ["npm:pi-generative-ui"],
20
+ },
21
+ };
22
+ const LEGACY_DEFAULT_PACKAGE_SOURCES = [
23
+ ...CORE_PACKAGE_SOURCES,
24
+ "npm:pi-generative-ui",
25
+ ];
26
+ function arraysMatchAsSets(left, right) {
27
+ if (left.length !== right.length) {
28
+ return false;
29
+ }
30
+ const rightSet = new Set(right);
31
+ return left.every((entry) => rightSet.has(entry));
32
+ }
33
+ export function shouldPruneLegacyDefaultPackages(packages) {
34
+ if (!Array.isArray(packages)) {
35
+ return false;
36
+ }
37
+ if (packages.some((entry) => typeof entry !== "string")) {
38
+ return false;
39
+ }
40
+ return arraysMatchAsSets(packages, LEGACY_DEFAULT_PACKAGE_SOURCES);
41
+ }
42
+ export function getOptionalPackagePresetSources(name) {
43
+ const normalized = name.trim().toLowerCase();
44
+ if (normalized === "ui") {
45
+ return [...OPTIONAL_PACKAGE_PRESETS["generative-ui"].sources];
46
+ }
47
+ const preset = OPTIONAL_PACKAGE_PRESETS[normalized];
48
+ return preset ? [...preset.sources] : undefined;
49
+ }
50
+ export function listOptionalPackagePresets() {
51
+ return Object.entries(OPTIONAL_PACKAGE_PRESETS).map(([name, preset]) => ({
52
+ name: name,
53
+ description: preset.description,
54
+ sources: [...preset.sources],
55
+ }));
56
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
4
+ import { CORE_PACKAGE_SOURCES, shouldPruneLegacyDefaultPackages } from "./package-presets.js";
4
5
  export function parseModelSpec(spec, modelRegistry) {
5
6
  const trimmed = spec.trim();
6
7
  const separator = trimmed.includes(":") ? ":" : trimmed.includes("/") ? "/" : null;
@@ -83,6 +84,12 @@ export function normalizeFeynmanSettings(settingsPath, bundledSettingsPath, defa
83
84
  settings.theme = "feynman";
84
85
  settings.quietStartup = true;
85
86
  settings.collapseChangelog = true;
87
+ if (!Array.isArray(settings.packages) || settings.packages.length === 0) {
88
+ settings.packages = [...CORE_PACKAGE_SOURCES];
89
+ }
90
+ else if (shouldPruneLegacyDefaultPackages(settings.packages)) {
91
+ settings.packages = [...CORE_PACKAGE_SOURCES];
92
+ }
86
93
  const authStorage = AuthStorage.create(authPath);
87
94
  const modelRegistry = new ModelRegistry(authStorage);
88
95
  const availableModels = modelRegistry.getAvailable().map((model) => ({
@@ -14,14 +14,7 @@ export const FEYNMAN_VERSION = (() => {
14
14
  }
15
15
  })();
16
16
 
17
- export const FEYNMAN_AGENT_LOGO = [
18
- "███████╗███████╗██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
19
- "██╔════╝██╔════╝╚██╗ ██╔╝████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
20
- "█████╗ █████╗ ╚████╔╝ ██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║",
21
- "██╔══╝ ██╔══╝ ╚██╔╝ ██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║",
22
- "██║ ███████╗ ██║ ██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
23
- "╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
24
- ];
17
+ export { FEYNMAN_ASCII_LOGO as FEYNMAN_AGENT_LOGO } from "../../logo.mjs";
25
18
 
26
19
  export const FEYNMAN_RESEARCH_TOOLS = [
27
20
  "alpha_search",
@@ -98,6 +98,8 @@ export const cliCommandSections = [
98
98
  {
99
99
  title: "Utilities",
100
100
  commands: [
101
+ { usage: "feynman packages list", description: "Show core and optional Pi package presets." },
102
+ { usage: "feynman packages install <preset>", description: "Install optional package presets on demand." },
101
103
  { usage: "feynman search status", description: "Show Pi web-access status and config path." },
102
104
  { usage: "feynman update [package]", description: "Update installed packages, or a specific package." },
103
105
  ],
@@ -118,7 +120,7 @@ export const legacyFlags = [
118
120
  { usage: "--setup-preview", description: "Alias for `feynman setup preview`." },
119
121
  ];
120
122
 
121
- export const topLevelCommandNames = ["alpha", "chat", "doctor", "help", "model", "search", "setup", "status", "update"];
123
+ export const topLevelCommandNames = ["alpha", "chat", "doctor", "help", "model", "packages", "search", "setup", "status", "update"];
122
124
 
123
125
  export function formatSlashUsage(command) {
124
126
  return `/${command.name}${command.args ? ` ${command.args}` : ""}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "type": "module",
6
6
  "engines": {
@@ -2,9 +2,11 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { FEYNMAN_ASCII_LOGO_HTML } from "../logo.mjs";
5
6
 
6
7
  const here = dirname(fileURLToPath(import.meta.url));
7
8
  const appRoot = resolve(here, "..");
9
+ const isGlobalInstall = process.env.npm_config_global === "true" || process.env.npm_config_location === "global";
8
10
 
9
11
  function findNodeModules() {
10
12
  let dir = appRoot;
@@ -53,7 +55,88 @@ const settingsPath = resolve(appRoot, ".feynman", "settings.json");
53
55
  const workspaceDir = resolve(appRoot, ".feynman", "npm");
54
56
  const workspacePackageJsonPath = resolve(workspaceDir, "package.json");
55
57
 
56
- // Pi handles package installation from .feynman/settings.json at runtime — no manual install needed
58
+ function resolveExecutable(name, fallbackPaths = []) {
59
+ for (const candidate of fallbackPaths) {
60
+ if (existsSync(candidate)) return candidate;
61
+ }
62
+
63
+ const result = spawnSync("sh", ["-lc", `command -v ${name}`], {
64
+ encoding: "utf8",
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ });
67
+ if (result.status === 0) {
68
+ const resolved = result.stdout.trim();
69
+ if (resolved) return resolved;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function ensurePackageWorkspace() {
75
+ if (!existsSync(settingsPath)) return;
76
+
77
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
78
+ const packageSpecs = Array.isArray(settings.packages)
79
+ ? settings.packages
80
+ .filter((v) => typeof v === "string" && v.startsWith("npm:"))
81
+ .map((v) => v.slice(4))
82
+ : [];
83
+
84
+ if (packageSpecs.length === 0) return;
85
+ if (existsSync(resolve(workspaceRoot, packageSpecs[0]))) return;
86
+
87
+ mkdirSync(workspaceDir, { recursive: true });
88
+ writeFileSync(
89
+ workspacePackageJsonPath,
90
+ JSON.stringify({ name: "feynman-packages", private: true }, null, 2) + "\n",
91
+ "utf8",
92
+ );
93
+
94
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
95
+ let frame = 0;
96
+ const start = Date.now();
97
+ const spinner = setInterval(() => {
98
+ const elapsed = Math.round((Date.now() - start) / 1000);
99
+ process.stderr.write(`\r${frames[frame++ % frames.length]} setting up feynman... ${elapsed}s`);
100
+ }, 80);
101
+
102
+ const result = spawnSync("npm", ["install", "--prefer-offline", "--no-audit", "--no-fund", "--loglevel", "error", "--prefix", workspaceDir, ...packageSpecs], {
103
+ stdio: ["ignore", "ignore", "pipe"],
104
+ timeout: 300000,
105
+ });
106
+
107
+ clearInterval(spinner);
108
+ const elapsed = Math.round((Date.now() - start) / 1000);
109
+
110
+ if (result.status !== 0) {
111
+ process.stderr.write(`\r✗ setup failed (${elapsed}s)\n`);
112
+ if (result.stderr?.length) process.stderr.write(result.stderr);
113
+ } else {
114
+ process.stderr.write(`\r✓ feynman ready (${elapsed}s)\n`);
115
+ }
116
+ }
117
+
118
+ ensurePackageWorkspace();
119
+
120
+ function ensurePandoc() {
121
+ if (!isGlobalInstall) return;
122
+ if (process.platform !== "darwin") return;
123
+ if (process.env.FEYNMAN_SKIP_PANDOC_INSTALL === "1") return;
124
+ if (resolveExecutable("pandoc", ["/opt/homebrew/bin/pandoc", "/usr/local/bin/pandoc"])) return;
125
+
126
+ const brewPath = resolveExecutable("brew", ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]);
127
+ if (!brewPath) return;
128
+
129
+ console.log("[feynman] installing pandoc...");
130
+ const result = spawnSync(brewPath, ["install", "pandoc"], {
131
+ stdio: "inherit",
132
+ timeout: 300000,
133
+ });
134
+ if (result.status !== 0) {
135
+ console.warn("[feynman] warning: pandoc install failed, run `feynman --setup-preview` later");
136
+ }
137
+ }
138
+
139
+ ensurePandoc();
57
140
 
58
141
  if (existsSync(packageJsonPath)) {
59
142
  const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
@@ -282,12 +365,33 @@ if (oauthPagePath && existsSync(oauthPagePath)) {
282
365
  let source = readFileSync(oauthPagePath, "utf8");
283
366
  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>`;';
284
367
  if (source.includes(piLogo)) {
285
- 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>`;';
368
+ const feynmanLogo = `const LOGO_SVG = \`${FEYNMAN_ASCII_LOGO_HTML}\`;`;
286
369
  source = source.replace(piLogo, feynmanLogo);
287
370
  writeFileSync(oauthPagePath, source, "utf8");
288
371
  }
289
372
  }
290
373
 
374
+ const alphaHubAuthPath = findPackageRoot("@companion-ai/alpha-hub")
375
+ ? resolve(findPackageRoot("@companion-ai/alpha-hub"), "src", "lib", "auth.js")
376
+ : null;
377
+
378
+ if (alphaHubAuthPath && existsSync(alphaHubAuthPath)) {
379
+ let source = readFileSync(alphaHubAuthPath, "utf8");
380
+ 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"`;
381
+ const logoHtml = FEYNMAN_ASCII_LOGO_HTML.replace('color:#10b981', 'color:#34d399');
382
+ 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>`;
383
+ 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>`;
384
+ const oldSuccess = `'<html><body><h2>Logged in to Alpha Hub</h2><p>You can close this tab.</p></body></html>'`;
385
+ const oldError = `'<html><body><h2>Login failed</h2><p>You can close this tab.</p></body></html>'`;
386
+ if (source.includes(oldSuccess)) {
387
+ source = source.replace(oldSuccess, `'${successPage}'`);
388
+ }
389
+ if (source.includes(oldError)) {
390
+ source = source.replace(oldError, `'${errorPage}'`);
391
+ }
392
+ writeFileSync(alphaHubAuthPath, source, "utf8");
393
+ }
394
+
291
395
  if (existsSync(piMemoryPath)) {
292
396
  let source = readFileSync(piMemoryPath, "utf8");
293
397
  const memoryOriginal = 'const MEMORY_DIR = join(homedir(), ".pi", "memory");';