@companion-ai/feynman 0.2.4 → 0.2.6

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,13 +6,10 @@
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",
13
- "npm:@kaiserlich-dev/pi-session-search",
14
12
  "npm:pi-schedule-prompt",
15
- "npm:@samfp/pi-memory",
16
13
  "npm:@tmustier/pi-ralph-wiggum"
17
14
  ],
18
15
  "quietStartup": true,
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/bin/feynman.js CHANGED
@@ -1,2 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import "../dist/index.js";
2
+ const v = process.versions.node.split(".").map(Number);
3
+ if (v[0] < 20) {
4
+ console.error(`feynman requires Node.js 20 or later (you have ${process.versions.node})`);
5
+ console.error("upgrade: https://nodejs.org or nvm install 20");
6
+ process.exit(1);
7
+ }
8
+ import("../dist/index.js");
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,71 @@
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:pi-schedule-prompt",
12
+ "npm:@tmustier/pi-ralph-wiggum",
13
+ ];
14
+ export const OPTIONAL_PACKAGE_PRESETS = {
15
+ "generative-ui": {
16
+ description: "Interactive Glimpse UI widgets.",
17
+ sources: ["npm:pi-generative-ui"],
18
+ },
19
+ memory: {
20
+ description: "Cross-session memory and preference recall.",
21
+ sources: ["npm:@samfp/pi-memory"],
22
+ },
23
+ "session-search": {
24
+ description: "Indexed session recall with SQLite-backed search.",
25
+ sources: ["npm:@kaiserlich-dev/pi-session-search"],
26
+ },
27
+ "all-extras": {
28
+ description: "Install all optional packages.",
29
+ sources: ["npm:pi-generative-ui", "npm:@samfp/pi-memory", "npm:@kaiserlich-dev/pi-session-search"],
30
+ },
31
+ };
32
+ const LEGACY_DEFAULT_PACKAGE_SOURCES = [
33
+ ...CORE_PACKAGE_SOURCES,
34
+ "npm:pi-generative-ui",
35
+ "npm:@kaiserlich-dev/pi-session-search",
36
+ "npm:@samfp/pi-memory",
37
+ ];
38
+ function arraysMatchAsSets(left, right) {
39
+ if (left.length !== right.length) {
40
+ return false;
41
+ }
42
+ const rightSet = new Set(right);
43
+ return left.every((entry) => rightSet.has(entry));
44
+ }
45
+ export function shouldPruneLegacyDefaultPackages(packages) {
46
+ if (!Array.isArray(packages)) {
47
+ return false;
48
+ }
49
+ if (packages.some((entry) => typeof entry !== "string")) {
50
+ return false;
51
+ }
52
+ return arraysMatchAsSets(packages, LEGACY_DEFAULT_PACKAGE_SOURCES);
53
+ }
54
+ export function getOptionalPackagePresetSources(name) {
55
+ const normalized = name.trim().toLowerCase();
56
+ if (normalized === "ui") {
57
+ return [...OPTIONAL_PACKAGE_PRESETS["generative-ui"].sources];
58
+ }
59
+ if (normalized === "search") {
60
+ return [...OPTIONAL_PACKAGE_PRESETS["session-search"].sources];
61
+ }
62
+ const preset = OPTIONAL_PACKAGE_PRESETS[normalized];
63
+ return preset ? [...preset.sources] : undefined;
64
+ }
65
+ export function listOptionalPackagePresets() {
66
+ return Object.entries(OPTIONAL_PACKAGE_PRESETS).map(([name, preset]) => ({
67
+ name: name,
68
+ description: preset.description,
69
+ sources: [...preset.sources],
70
+ }));
71
+ }
@@ -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) => ({
@@ -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,8 +1,11 @@
1
1
  {
2
2
  "name": "@companion-ai/feynman",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Research-first CLI agent built on Pi and alphaXiv",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=20.18.1"
8
+ },
6
9
  "bin": {
7
10
  "feynman": "bin/feynman.js"
8
11
  },
@@ -57,9 +60,6 @@
57
60
  "tsx": "^4.21.0",
58
61
  "typescript": "^5.9.3"
59
62
  },
60
- "engines": {
61
- "node": ">=20.18.1"
62
- },
63
63
  "repository": {
64
64
  "type": "git",
65
65
  "url": "git+https://github.com/getcompanion-ai/feynman.git"
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const here = dirname(fileURLToPath(import.meta.url));
7
7
  const appRoot = resolve(here, "..");
8
+ const isGlobalInstall = process.env.npm_config_global === "true" || process.env.npm_config_location === "global";
8
9
 
9
10
  function findNodeModules() {
10
11
  let dir = appRoot;
@@ -53,7 +54,75 @@ const settingsPath = resolve(appRoot, ".feynman", "settings.json");
53
54
  const workspaceDir = resolve(appRoot, ".feynman", "npm");
54
55
  const workspacePackageJsonPath = resolve(workspaceDir, "package.json");
55
56
 
56
- // Pi handles package installation from .feynman/settings.json at runtime — no manual install needed
57
+ function resolveExecutable(name, fallbackPaths = []) {
58
+ for (const candidate of fallbackPaths) {
59
+ if (existsSync(candidate)) return candidate;
60
+ }
61
+
62
+ const result = spawnSync("sh", ["-lc", `command -v ${name}`], {
63
+ encoding: "utf8",
64
+ stdio: ["ignore", "pipe", "ignore"],
65
+ });
66
+ if (result.status === 0) {
67
+ const resolved = result.stdout.trim();
68
+ if (resolved) return resolved;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function ensurePackageWorkspace() {
74
+ if (!existsSync(settingsPath)) return;
75
+
76
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
77
+ const packageSpecs = Array.isArray(settings.packages)
78
+ ? settings.packages
79
+ .filter((v) => typeof v === "string" && v.startsWith("npm:"))
80
+ .map((v) => v.slice(4))
81
+ : [];
82
+
83
+ if (packageSpecs.length === 0) return;
84
+ if (existsSync(resolve(workspaceRoot, packageSpecs[0]))) return;
85
+
86
+ mkdirSync(workspaceDir, { recursive: true });
87
+ writeFileSync(
88
+ workspacePackageJsonPath,
89
+ JSON.stringify({ name: "feynman-packages", private: true }, null, 2) + "\n",
90
+ "utf8",
91
+ );
92
+
93
+ console.log("[feynman] installing research packages...");
94
+ const result = spawnSync("npm", ["install", "--prefer-offline", "--no-audit", "--no-fund", "--prefix", workspaceDir, ...packageSpecs], {
95
+ stdio: "inherit",
96
+ timeout: 300000,
97
+ });
98
+
99
+ if (result.status !== 0) {
100
+ console.warn("[feynman] warning: package install failed, Pi will retry on first launch");
101
+ }
102
+ }
103
+
104
+ ensurePackageWorkspace();
105
+
106
+ function ensurePandoc() {
107
+ if (!isGlobalInstall) return;
108
+ if (process.platform !== "darwin") return;
109
+ if (process.env.FEYNMAN_SKIP_PANDOC_INSTALL === "1") return;
110
+ if (resolveExecutable("pandoc", ["/opt/homebrew/bin/pandoc", "/usr/local/bin/pandoc"])) return;
111
+
112
+ const brewPath = resolveExecutable("brew", ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]);
113
+ if (!brewPath) return;
114
+
115
+ console.log("[feynman] installing pandoc...");
116
+ const result = spawnSync(brewPath, ["install", "pandoc"], {
117
+ stdio: "inherit",
118
+ timeout: 300000,
119
+ });
120
+ if (result.status !== 0) {
121
+ console.warn("[feynman] warning: pandoc install failed, run `feynman --setup-preview` later");
122
+ }
123
+ }
124
+
125
+ ensurePandoc();
57
126
 
58
127
  if (existsSync(packageJsonPath)) {
59
128
  const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));