@companion-ai/feynman 0.2.0

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.
Files changed (72) hide show
  1. package/.env.example +8 -0
  2. package/.feynman/SYSTEM.md +62 -0
  3. package/.feynman/agents/researcher.md +63 -0
  4. package/.feynman/agents/reviewer.md +84 -0
  5. package/.feynman/agents/verifier.md +38 -0
  6. package/.feynman/agents/writer.md +51 -0
  7. package/.feynman/settings.json +20 -0
  8. package/.feynman/themes/feynman.json +85 -0
  9. package/AGENTS.md +53 -0
  10. package/README.md +99 -0
  11. package/bin/feynman.js +2 -0
  12. package/dist/bootstrap/sync.js +98 -0
  13. package/dist/cli.js +297 -0
  14. package/dist/config/commands.js +71 -0
  15. package/dist/config/feynman-config.js +42 -0
  16. package/dist/config/paths.js +32 -0
  17. package/dist/feynman-prompt.js +63 -0
  18. package/dist/index.js +5 -0
  19. package/dist/model/catalog.js +238 -0
  20. package/dist/model/commands.js +165 -0
  21. package/dist/pi/launch.js +31 -0
  22. package/dist/pi/runtime.js +70 -0
  23. package/dist/pi/settings.js +101 -0
  24. package/dist/pi/web-access.js +74 -0
  25. package/dist/search/commands.js +12 -0
  26. package/dist/setup/doctor.js +126 -0
  27. package/dist/setup/preview.js +20 -0
  28. package/dist/setup/prompts.js +29 -0
  29. package/dist/setup/setup.js +119 -0
  30. package/dist/system/executables.js +38 -0
  31. package/dist/system/promise-polyfill.js +12 -0
  32. package/dist/ui/terminal.js +53 -0
  33. package/dist/web-search.js +1 -0
  34. package/extensions/research-tools/alpha.ts +212 -0
  35. package/extensions/research-tools/header.ts +379 -0
  36. package/extensions/research-tools/help.ts +93 -0
  37. package/extensions/research-tools/preview.ts +233 -0
  38. package/extensions/research-tools/project.ts +116 -0
  39. package/extensions/research-tools/session-search.ts +223 -0
  40. package/extensions/research-tools/shared.ts +46 -0
  41. package/extensions/research-tools.ts +25 -0
  42. package/metadata/commands.d.mts +46 -0
  43. package/metadata/commands.mjs +133 -0
  44. package/package.json +71 -0
  45. package/prompts/audit.md +15 -0
  46. package/prompts/autoresearch.md +63 -0
  47. package/prompts/compare.md +16 -0
  48. package/prompts/deepresearch.md +167 -0
  49. package/prompts/delegate.md +21 -0
  50. package/prompts/draft.md +16 -0
  51. package/prompts/jobs.md +16 -0
  52. package/prompts/lit.md +16 -0
  53. package/prompts/log.md +14 -0
  54. package/prompts/replicate.md +22 -0
  55. package/prompts/review.md +15 -0
  56. package/prompts/watch.md +14 -0
  57. package/scripts/patch-embedded-pi.mjs +319 -0
  58. package/skills/agentcomputer/SKILL.md +108 -0
  59. package/skills/agentcomputer/references/acp-flow.md +23 -0
  60. package/skills/agentcomputer/references/cli-cheatsheet.md +68 -0
  61. package/skills/autoresearch/SKILL.md +12 -0
  62. package/skills/deep-research/SKILL.md +12 -0
  63. package/skills/docker/SKILL.md +84 -0
  64. package/skills/jobs/SKILL.md +10 -0
  65. package/skills/literature-review/SKILL.md +12 -0
  66. package/skills/paper-code-audit/SKILL.md +12 -0
  67. package/skills/paper-writing/SKILL.md +12 -0
  68. package/skills/peer-review/SKILL.md +12 -0
  69. package/skills/replication/SKILL.md +14 -0
  70. package/skills/session-log/SKILL.md +10 -0
  71. package/skills/source-comparison/SKILL.md +12 -0
  72. package/skills/watch/SKILL.md +12 -0
@@ -0,0 +1,379 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { cpus, freemem, homedir, totalmem } from "node:os";
3
+ import { execSync } from "node:child_process";
4
+ import { resolve as resolvePath } from "node:path";
5
+
6
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+
8
+ import {
9
+ APP_ROOT,
10
+ FEYNMAN_AGENT_LOGO,
11
+ FEYNMAN_VERSION,
12
+ } from "./shared.js";
13
+
14
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
15
+
16
+ function visibleLength(text: string): number {
17
+ return text.replace(ANSI_RE, "").length;
18
+ }
19
+
20
+ function formatHeaderPath(path: string): string {
21
+ const home = homedir();
22
+ return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
23
+ }
24
+
25
+ function truncateVisible(text: string, maxVisible: number): string {
26
+ const raw = text.replace(ANSI_RE, "");
27
+ if (raw.length <= maxVisible) return text;
28
+ if (maxVisible <= 3) return ".".repeat(maxVisible);
29
+ return `${raw.slice(0, maxVisible - 3)}...`;
30
+ }
31
+
32
+ function wrapWords(text: string, maxW: number): string[] {
33
+ const words = text.split(" ");
34
+ const lines: string[] = [];
35
+ let cur = "";
36
+ for (let word of words) {
37
+ if (word.length > maxW) {
38
+ if (cur) { lines.push(cur); cur = ""; }
39
+ word = maxW > 3 ? `${word.slice(0, maxW - 1)}…` : word.slice(0, maxW);
40
+ }
41
+ const test = cur ? `${cur} ${word}` : word;
42
+ if (cur && test.length > maxW) {
43
+ lines.push(cur);
44
+ cur = word;
45
+ } else {
46
+ cur = test;
47
+ }
48
+ }
49
+ if (cur) lines.push(cur);
50
+ return lines.length ? lines : [""];
51
+ }
52
+
53
+ function padRight(text: string, width: number): string {
54
+ const gap = Math.max(0, width - visibleLength(text));
55
+ return `${text}${" ".repeat(gap)}`;
56
+ }
57
+
58
+ function centerText(text: string, width: number): string {
59
+ if (text.length >= width) return text.slice(0, width);
60
+ const left = Math.floor((width - text.length) / 2);
61
+ const right = width - text.length - left;
62
+ return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
63
+ }
64
+
65
+ function getCurrentModelLabel(ctx: ExtensionContext): string {
66
+ if (ctx.model) return `${ctx.model.provider}/${ctx.model.id}`;
67
+ const branch = ctx.sessionManager.getBranch();
68
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
69
+ const entry = branch[index]!;
70
+ if (entry.type === "model_change") return `${(entry as any).provider}/${(entry as any).modelId}`;
71
+ }
72
+ return "not set";
73
+ }
74
+
75
+ function extractMessageText(message: unknown): string {
76
+ if (!message || typeof message !== "object") return "";
77
+ const content = (message as { content?: unknown }).content;
78
+ if (typeof content === "string") return content;
79
+ if (!Array.isArray(content)) return "";
80
+ return content
81
+ .map((item) => {
82
+ if (!item || typeof item !== "object") return "";
83
+ const record = item as { type?: string; text?: unknown; name?: unknown };
84
+ if (record.type === "text" && typeof record.text === "string") return record.text;
85
+ if (record.type === "toolCall") return `[${typeof record.name === "string" ? record.name : "tool"}]`;
86
+ return "";
87
+ })
88
+ .filter(Boolean)
89
+ .join(" ");
90
+ }
91
+
92
+ function getRecentActivitySummary(ctx: ExtensionContext): string {
93
+ const branch = ctx.sessionManager.getBranch();
94
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
95
+ const entry = branch[index]!;
96
+ if (entry.type !== "message") continue;
97
+ const msg = entry as any;
98
+ const text = extractMessageText(msg.message).replace(/\s+/g, " ").trim();
99
+ if (!text) continue;
100
+ const role = msg.message.role === "assistant" ? "agent" : msg.message.role === "user" ? "you" : msg.message.role;
101
+ return `${role}: ${text}`;
102
+ }
103
+ return "";
104
+ }
105
+
106
+ async function buildAgentCatalogSummary(): Promise<{ agents: string[]; chains: string[] }> {
107
+ const agents: string[] = [];
108
+ const chains: string[] = [];
109
+ try {
110
+ const entries = await readdir(resolvePath(APP_ROOT, ".feynman", "agents"), { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
113
+ if (entry.name.endsWith(".chain.md")) {
114
+ chains.push(entry.name.replace(/\.chain\.md$/i, ""));
115
+ } else {
116
+ agents.push(entry.name.replace(/\.md$/i, ""));
117
+ }
118
+ }
119
+ } catch {
120
+ return { agents: [], chains: [] };
121
+ }
122
+ agents.sort();
123
+ chains.sort();
124
+ return { agents, chains };
125
+ }
126
+
127
+ type SystemResources = {
128
+ cpu: string;
129
+ cores: number;
130
+ ramTotal: string;
131
+ ramFree: string;
132
+ gpu: string | null;
133
+ docker: boolean;
134
+ };
135
+
136
+ function detectSystemResources(): SystemResources {
137
+ const cores = cpus().length;
138
+ const cpu = cpus()[0]?.model?.trim() ?? "unknown";
139
+ const totalBytes = totalmem();
140
+ const freeBytes = freemem();
141
+ const ramTotal = `${Math.round(totalBytes / (1024 ** 3))}GB`;
142
+ const ramFree = `${Math.round(freeBytes / (1024 ** 3))}GB`;
143
+
144
+ let gpu: string | null = null;
145
+ try {
146
+ if (process.platform === "darwin") {
147
+ const out = execSync("system_profiler SPDisplaysDataType 2>/dev/null | grep 'Chipset Model\\|Chip Model'", { encoding: "utf8", timeout: 3000 }).trim();
148
+ const match = out.match(/:\s*(.+)/);
149
+ if (match) gpu = match[1]!.trim();
150
+ } else {
151
+ const out = execSync("nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null", { encoding: "utf8", timeout: 3000 }).trim();
152
+ if (out) gpu = out.split("\n")[0]!.trim();
153
+ }
154
+ } catch {}
155
+
156
+ let docker = false;
157
+ try {
158
+ execSync("docker info 2>/dev/null", { timeout: 3000 });
159
+ docker = true;
160
+ } catch {}
161
+
162
+ return { cpu, cores, ramTotal, ramFree, gpu, docker };
163
+ }
164
+
165
+ type WorkflowInfo = { name: string; description: string };
166
+
167
+ function getResearchWorkflows(pi: ExtensionAPI): WorkflowInfo[] {
168
+ return pi.getCommands()
169
+ .filter((cmd) => cmd.source === "prompt")
170
+ .map((cmd) => ({ name: `/${cmd.name}`, description: cmd.description ?? "" }))
171
+ .sort((a, b) => a.name.localeCompare(b.name));
172
+ }
173
+
174
+ function shortDescription(desc: string): string {
175
+ const lower = desc.toLowerCase();
176
+ for (const prefix of ["run a ", "run an ", "set up a ", "build a ", "build the ", "turn ", "design the ", "produce a ", "compare ", "simulate ", "inspect ", "write a ", "plan or execute a ", "prepare a "]) {
177
+ if (lower.startsWith(prefix)) return desc.slice(prefix.length);
178
+ }
179
+ return desc;
180
+ }
181
+
182
+ export function installFeynmanHeader(
183
+ pi: ExtensionAPI,
184
+ ctx: ExtensionContext,
185
+ cache: { agentSummaryPromise?: Promise<{ agents: string[]; chains: string[] }> },
186
+ ): void | Promise<void> {
187
+ if (!ctx.hasUI) return;
188
+
189
+ cache.agentSummaryPromise ??= buildAgentCatalogSummary();
190
+
191
+ return cache.agentSummaryPromise.then((agentData) => {
192
+ const resources = detectSystemResources();
193
+ const workflows = getResearchWorkflows(pi);
194
+ const toolCount = pi.getAllTools().length;
195
+ const commandCount = pi.getCommands().length;
196
+ const agentCount = agentData.agents.length + agentData.chains.length;
197
+
198
+ ctx.ui.setHeader((_tui, theme) => ({
199
+ render(width: number): string[] {
200
+ const maxW = Math.max(width - 2, 1);
201
+ const cardW = Math.min(maxW, 120);
202
+ const innerW = cardW - 2;
203
+ const contentW = innerW - 2;
204
+ const outerPad = " ".repeat(Math.max(0, Math.floor((width - cardW) / 2)));
205
+ const lines: string[] = [];
206
+
207
+ const push = (line: string) => { lines.push(`${outerPad}${line}`); };
208
+ const border = (ch: string) => theme.fg("borderMuted", ch);
209
+
210
+ const row = (content: string): string =>
211
+ `${border("│")} ${padRight(content, contentW)} ${border("│")}`;
212
+ const emptyRow = (): string =>
213
+ `${border("│")}${" ".repeat(innerW)}${border("│")}`;
214
+ const sep = (): string =>
215
+ `${border("├")}${border("─".repeat(innerW))}${border("┤")}`;
216
+
217
+ const useWideLayout = contentW >= 70;
218
+ const leftW = useWideLayout ? Math.min(38, Math.floor(contentW * 0.35)) : 0;
219
+ const divColW = useWideLayout ? 3 : 0;
220
+ const rightW = useWideLayout ? contentW - leftW - divColW : contentW;
221
+
222
+ const twoCol = (left: string, right: string): string => {
223
+ if (!useWideLayout) return row(left || right);
224
+ return row(
225
+ `${padRight(left, leftW)}${border(" │ ")}${padRight(right, rightW)}`,
226
+ );
227
+ };
228
+
229
+ const modelLabel = getCurrentModelLabel(ctx);
230
+ const sessionId = ctx.sessionManager.getSessionName()?.trim() || ctx.sessionManager.getSessionId();
231
+ const dirLabel = formatHeaderPath(ctx.cwd);
232
+ const activity = getRecentActivitySummary(ctx);
233
+
234
+ push("");
235
+ if (cardW >= 70) {
236
+ for (const logoLine of FEYNMAN_AGENT_LOGO) {
237
+ push(theme.fg("accent", theme.bold(centerText(truncateVisible(logoLine, cardW), cardW))));
238
+ }
239
+ push("");
240
+ }
241
+
242
+ const versionTag = ` v${FEYNMAN_VERSION} `;
243
+ const gap = Math.max(0, innerW - versionTag.length);
244
+ const gapL = Math.floor(gap / 2);
245
+ push(
246
+ border(`╭${"─".repeat(gapL)}`) +
247
+ theme.fg("dim", versionTag) +
248
+ border(`${"─".repeat(gap - gapL)}╮`),
249
+ );
250
+
251
+ if (useWideLayout) {
252
+ const cmdNameW = 16;
253
+ const descW = Math.max(10, rightW - cmdNameW - 2);
254
+
255
+ const leftValueW = Math.max(1, leftW - 11);
256
+ const indent = " ".repeat(11);
257
+ const leftLines: string[] = [""];
258
+
259
+ const pushLabeled = (label: string, value: string, color: "text" | "dim") => {
260
+ const wrapped = wrapWords(value, leftValueW);
261
+ leftLines.push(`${theme.fg("dim", label.padEnd(10))} ${theme.fg(color, wrapped[0]!)}`);
262
+ for (let i = 1; i < wrapped.length; i++) {
263
+ leftLines.push(`${indent}${theme.fg(color, wrapped[i]!)}`);
264
+ }
265
+ };
266
+
267
+ pushLabeled("model", modelLabel, "text");
268
+ pushLabeled("directory", dirLabel, "text");
269
+ pushLabeled("session", sessionId, "dim");
270
+ leftLines.push("");
271
+ pushLabeled("cpu", `${resources.cores} cores`, "dim");
272
+ pushLabeled("ram", `${resources.ramFree} free / ${resources.ramTotal}`, "dim");
273
+ if (resources.gpu) pushLabeled("gpu", resources.gpu, "dim");
274
+ pushLabeled("docker", resources.docker ? "available" : "not found", "dim");
275
+ leftLines.push("");
276
+ leftLines.push(theme.fg("dim", `${toolCount} tools · ${agentCount} agents`));
277
+
278
+ const pushList = (heading: string, items: string[]) => {
279
+ if (items.length === 0) return;
280
+ leftLines.push("");
281
+ leftLines.push(theme.fg("accent", theme.bold(heading)));
282
+ for (const line of wrapWords(items.join(", "), leftW)) {
283
+ leftLines.push(theme.fg("dim", line));
284
+ }
285
+ };
286
+
287
+ pushList("Agents", agentData.agents);
288
+ pushList("Chains", agentData.chains);
289
+
290
+ if (activity) {
291
+ const maxActivityLen = leftW * 2;
292
+ const trimmed = activity.length > maxActivityLen
293
+ ? `${activity.slice(0, maxActivityLen - 1)}…`
294
+ : activity;
295
+ leftLines.push("");
296
+ leftLines.push(theme.fg("accent", theme.bold("Last Activity")));
297
+ for (const line of wrapWords(trimmed, leftW)) {
298
+ leftLines.push(theme.fg("dim", line));
299
+ }
300
+ }
301
+
302
+ const rightLines: string[] = [
303
+ "",
304
+ theme.fg("accent", theme.bold("Research Workflows")),
305
+ ];
306
+
307
+ for (const wf of workflows) {
308
+ if (wf.name === "/jobs" || wf.name === "/log") continue;
309
+ const desc = shortDescription(wf.description);
310
+ const descWords = desc.split(" ");
311
+ let line = "";
312
+ let first = true;
313
+ for (const word of descWords) {
314
+ const test = line ? `${line} ${word}` : word;
315
+ if (line && test.length > descW) {
316
+ rightLines.push(
317
+ first
318
+ ? `${theme.fg("accent", wf.name.padEnd(cmdNameW))}${theme.fg("dim", line)}`
319
+ : `${" ".repeat(cmdNameW)}${theme.fg("dim", line)}`,
320
+ );
321
+ first = false;
322
+ line = word;
323
+ } else {
324
+ line = test;
325
+ }
326
+ }
327
+ if (line || first) {
328
+ rightLines.push(
329
+ first
330
+ ? `${theme.fg("accent", wf.name.padEnd(cmdNameW))}${theme.fg("dim", line)}`
331
+ : `${" ".repeat(cmdNameW)}${theme.fg("dim", line)}`,
332
+ );
333
+ }
334
+ }
335
+
336
+ const maxRows = Math.max(leftLines.length, rightLines.length);
337
+ for (let i = 0; i < maxRows; i++) {
338
+ push(twoCol(leftLines[i] ?? "", rightLines[i] ?? ""));
339
+ }
340
+ } else {
341
+ const narrowValW = Math.max(1, contentW - 11);
342
+ push(emptyRow());
343
+ push(row(`${theme.fg("dim", "model".padEnd(10))} ${theme.fg("text", truncateVisible(modelLabel, narrowValW))}`));
344
+ push(row(`${theme.fg("dim", "directory".padEnd(10))} ${theme.fg("text", truncateVisible(dirLabel, narrowValW))}`));
345
+ push(row(`${theme.fg("dim", "session".padEnd(10))} ${theme.fg("dim", truncateVisible(sessionId, narrowValW))}`));
346
+ const resourceLine = `${resources.cores} cores · ${resources.ramTotal} ram${resources.gpu ? ` · ${resources.gpu}` : ""}${resources.docker ? " · docker" : ""}`;
347
+ push(row(theme.fg("dim", truncateVisible(resourceLine, contentW))));
348
+ push(row(theme.fg("dim", truncateVisible(`${toolCount} tools · ${agentCount} agents · ${commandCount} commands`, contentW))));
349
+ push(emptyRow());
350
+
351
+ push(sep());
352
+ push(row(theme.fg("accent", theme.bold("Research Workflows"))));
353
+ const narrowDescW = Math.max(1, contentW - 17);
354
+ for (const wf of workflows) {
355
+ if (wf.name === "/jobs" || wf.name === "/log") continue;
356
+ const desc = shortDescription(wf.description);
357
+ push(row(`${theme.fg("accent", wf.name.padEnd(16))} ${theme.fg("dim", truncateVisible(desc, narrowDescW))}`));
358
+ }
359
+
360
+ if (agentData.agents.length > 0 || agentData.chains.length > 0) {
361
+ push(sep());
362
+ push(row(theme.fg("accent", theme.bold("Agents & Chains"))));
363
+ if (agentData.agents.length > 0) {
364
+ push(row(theme.fg("dim", truncateVisible(`agents ${agentData.agents.join(", ")}`, contentW))));
365
+ }
366
+ if (agentData.chains.length > 0) {
367
+ push(row(theme.fg("dim", truncateVisible(`chains ${agentData.chains.join(", ")}`, contentW))));
368
+ }
369
+ }
370
+ }
371
+
372
+ push(border(`╰${"─".repeat(innerW)}╯`));
373
+ push("");
374
+ return lines;
375
+ },
376
+ invalidate() {},
377
+ }));
378
+ });
379
+ }
@@ -0,0 +1,93 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ extensionCommandSpecs,
4
+ formatSlashUsage,
5
+ getExtensionCommandSpec,
6
+ livePackageCommandGroups,
7
+ readPromptSpecs,
8
+ } from "../../metadata/commands.mjs";
9
+ import { APP_ROOT } from "./shared.js";
10
+
11
+ type HelpCommand = { usage: string; description: string };
12
+ type HelpSection = { title: string; commands: HelpCommand[] };
13
+
14
+ function buildHelpSections(pi: ExtensionAPI): HelpSection[] {
15
+ const liveCommands = new Map(pi.getCommands().map((command) => [command.name, command]));
16
+ const promptSpecs = readPromptSpecs(APP_ROOT);
17
+ const sections = new Map<string, HelpCommand[]>();
18
+
19
+ for (const command of promptSpecs.filter((entry) => entry.section !== "Internal")) {
20
+ const live = liveCommands.get(command.name);
21
+ if (!live) continue;
22
+ const items = sections.get(command.section) ?? [];
23
+ items.push({
24
+ usage: formatSlashUsage(command),
25
+ description: live.description ?? command.description,
26
+ });
27
+ sections.set(command.section, items);
28
+ }
29
+
30
+ for (const command of extensionCommandSpecs.filter((entry) => entry.publicDocs)) {
31
+ const live = liveCommands.get(command.name);
32
+ if (!live) continue;
33
+ const items = sections.get(command.section) ?? [];
34
+ items.push({
35
+ usage: formatSlashUsage(command),
36
+ description: live.description ?? command.description,
37
+ });
38
+ sections.set(command.section, items);
39
+ }
40
+
41
+ const ownedNames = new Set([
42
+ ...promptSpecs.filter((entry) => entry.section !== "Internal").map((entry) => entry.name),
43
+ ...extensionCommandSpecs.filter((entry) => entry.publicDocs).map((entry) => entry.name),
44
+ ]);
45
+
46
+ for (const group of livePackageCommandGroups) {
47
+ const commands: HelpCommand[] = [];
48
+ for (const spec of group.commands) {
49
+ const command = liveCommands.get(spec.name);
50
+ if (!command || ownedNames.has(command.name)) continue;
51
+ commands.push({
52
+ usage: spec.usage,
53
+ description: command.description ?? "",
54
+ });
55
+ }
56
+
57
+ if (commands.length > 0) {
58
+ sections.set(group.title, commands);
59
+ }
60
+ }
61
+
62
+ return [
63
+ "Research Workflows",
64
+ "Project & Session",
65
+ "Setup",
66
+ "Agents & Delegation",
67
+ "Bundled Package Commands",
68
+ ]
69
+ .map((title) => ({ title, commands: sections.get(title) ?? [] }))
70
+ .filter((section) => section.commands.length > 0);
71
+ }
72
+
73
+ export function registerHelpCommand(pi: ExtensionAPI): void {
74
+ pi.registerCommand("help", {
75
+ description:
76
+ getExtensionCommandSpec("help")?.description ??
77
+ "Show grouped Feynman commands and prefill the editor with a selected command.",
78
+ handler: async (_args, ctx) => {
79
+ const sections = buildHelpSections(pi);
80
+ const items = sections.flatMap((section) => [
81
+ `--- ${section.title} ---`,
82
+ ...section.commands.map((cmd) => `${cmd.usage} — ${cmd.description}`),
83
+ ]);
84
+
85
+ const selected = await ctx.ui.select("Feynman Help", items);
86
+ if (!selected || selected.startsWith("---")) return;
87
+
88
+ const usage = selected.split(" — ")[0];
89
+ ctx.ui.setEditorText(usage);
90
+ ctx.ui.notify(`Prefilled ${usage}`, "info");
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,233 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, extname, join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ function isMarkdownPath(path: string): boolean {
11
+ return [".md", ".markdown", ".txt"].includes(extname(path).toLowerCase());
12
+ }
13
+
14
+ function isLatexPath(path: string): boolean {
15
+ return extname(path).toLowerCase() === ".tex";
16
+ }
17
+
18
+ function wrapCodeAsMarkdown(source: string, filePath: string): string {
19
+ const language = extname(filePath).replace(/^\./, "") || "text";
20
+ return `# ${basename(filePath)}\n\n\`\`\`${language}\n${source}\n\`\`\`\n`;
21
+ }
22
+
23
+ export async function openWithDefaultApp(targetPath: string): Promise<void> {
24
+ const target = pathToFileURL(targetPath).href;
25
+ if (process.platform === "darwin") {
26
+ await execFileAsync("open", [target]);
27
+ return;
28
+ }
29
+ if (process.platform === "win32") {
30
+ await execFileAsync("cmd", ["/c", "start", "", target]);
31
+ return;
32
+ }
33
+ await execFileAsync("xdg-open", [target]);
34
+ }
35
+
36
+ async function runCommandWithInput(
37
+ command: string,
38
+ args: string[],
39
+ input: string,
40
+ ): Promise<{ stdout: string; stderr: string }> {
41
+ return await new Promise((resolve, reject) => {
42
+ const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
43
+ const stdoutChunks: Buffer[] = [];
44
+ const stderrChunks: Buffer[] = [];
45
+
46
+ child.stdout.on("data", (chunk: Buffer | string) => {
47
+ stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
48
+ });
49
+ child.stderr.on("data", (chunk: Buffer | string) => {
50
+ stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
51
+ });
52
+
53
+ child.once("error", reject);
54
+ child.once("close", (code) => {
55
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
56
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
57
+ if (code === 0) {
58
+ resolve({ stdout, stderr });
59
+ return;
60
+ }
61
+ reject(new Error(`${command} failed with exit code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
62
+ });
63
+
64
+ child.stdin.end(input);
65
+ });
66
+ }
67
+
68
+ export async function renderHtmlPreview(filePath: string): Promise<string> {
69
+ const source = await readFile(filePath, "utf8");
70
+ const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
71
+ const inputFormat = isLatexPath(filePath)
72
+ ? "latex"
73
+ : "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris-raw_html";
74
+ const markdown = isLatexPath(filePath) || isMarkdownPath(filePath) ? source : wrapCodeAsMarkdown(source, filePath);
75
+ const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", `--resource-path=${dirname(filePath)}`];
76
+ const { stdout } = await runCommandWithInput(pandocCommand, args, markdown);
77
+ const html = `<!doctype html><html><head><meta charset="utf-8" /><base href="${pathToFileURL(dirname(filePath) + "/").href}" /><title>${basename(filePath)}</title><style>
78
+ :root{
79
+ --bg:#faf7f2;
80
+ --paper:#fffdf9;
81
+ --border:#d7cec1;
82
+ --text:#1f1c18;
83
+ --muted:#6c645a;
84
+ --code:#f3eee6;
85
+ --link:#0f6d8c;
86
+ --quote:#8b7f70;
87
+ }
88
+ @media (prefers-color-scheme: dark){
89
+ :root{
90
+ --bg:#161311;
91
+ --paper:#1d1916;
92
+ --border:#3b342d;
93
+ --text:#ebe3d6;
94
+ --muted:#b4ab9f;
95
+ --code:#221d19;
96
+ --link:#8ac6d6;
97
+ --quote:#a89d8f;
98
+ }
99
+ }
100
+ body{
101
+ font-family:Charter,"Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,Georgia,serif;
102
+ margin:0;
103
+ background:var(--bg);
104
+ color:var(--text);
105
+ line-height:1.7;
106
+ }
107
+ main{
108
+ max-width:900px;
109
+ margin:2rem auto 4rem;
110
+ padding:2.5rem 3rem;
111
+ background:var(--paper);
112
+ border:1px solid var(--border);
113
+ border-radius:18px;
114
+ box-shadow:0 12px 40px rgba(0,0,0,.06);
115
+ }
116
+ h1,h2,h3,h4,h5,h6{
117
+ font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
118
+ line-height:1.2;
119
+ margin-top:1.5em;
120
+ }
121
+ h1{font-size:2.2rem;border-bottom:1px solid var(--border);padding-bottom:.35rem;}
122
+ h2{font-size:1.6rem;border-bottom:1px solid var(--border);padding-bottom:.25rem;}
123
+ p,ul,ol,blockquote,table{margin:1rem 0;}
124
+ pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
125
+ pre{
126
+ background:var(--code);
127
+ border:1px solid var(--border);
128
+ border-radius:12px;
129
+ padding:1rem 1.1rem;
130
+ overflow:auto;
131
+ }
132
+ code{
133
+ background:var(--code);
134
+ padding:.12rem .28rem;
135
+ border-radius:6px;
136
+ }
137
+ a{color:var(--link);text-decoration:none}
138
+ a:hover{text-decoration:underline}
139
+ img{max-width:100%}
140
+ blockquote{
141
+ border-left:4px solid var(--border);
142
+ padding-left:1rem;
143
+ color:var(--quote);
144
+ }
145
+ table{border-collapse:collapse;width:100%}
146
+ th,td{border:1px solid var(--border);padding:.55rem .7rem;text-align:left}
147
+ </style></head><body><main>${stdout}</main></body></html>`;
148
+ const tempDir = await mkdtemp(join(tmpdir(), "feynman-preview-"));
149
+ const htmlPath = join(tempDir, `${basename(filePath)}.html`);
150
+ await writeFile(htmlPath, html, "utf8");
151
+ return htmlPath;
152
+ }
153
+
154
+ export async function renderPdfPreview(filePath: string): Promise<string> {
155
+ const source = await readFile(filePath, "utf8");
156
+ const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
157
+ const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
158
+ const inputFormat = isLatexPath(filePath)
159
+ ? "latex"
160
+ : "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris-raw_html";
161
+ const markdown = isLatexPath(filePath) || isMarkdownPath(filePath) ? source : wrapCodeAsMarkdown(source, filePath);
162
+ const tempDir = await mkdtemp(join(tmpdir(), "feynman-preview-"));
163
+ const pdfPath = join(tempDir, `${basename(filePath)}.pdf`);
164
+ const args = [
165
+ "-f",
166
+ inputFormat,
167
+ "-o",
168
+ pdfPath,
169
+ `--pdf-engine=${pdfEngine}`,
170
+ `--resource-path=${dirname(filePath)}`,
171
+ ];
172
+ await runCommandWithInput(pandocCommand, args, markdown);
173
+ return pdfPath;
174
+ }
175
+
176
+ export async function pathExists(path: string): Promise<boolean> {
177
+ try {
178
+ await stat(path);
179
+ return true;
180
+ } catch {
181
+ return false;
182
+ }
183
+ }
184
+
185
+ export function buildProjectAgentsTemplate(): string {
186
+ return `# Feynman Project Guide
187
+
188
+ This file is read automatically at startup. It is the durable project memory for Feynman.
189
+
190
+ ## Project Overview
191
+ - State the research question, target artifact, target venue, and key datasets or benchmarks here.
192
+
193
+ ## AI Research Context
194
+ - Problem statement:
195
+ - Core hypothesis:
196
+ - Closest prior work:
197
+ - Required baselines:
198
+ - Required ablations:
199
+ - Primary metrics:
200
+ - Datasets / benchmarks:
201
+
202
+ ## Ground Rules
203
+ - Do not modify raw data in \`Data/Raw/\` or equivalent raw-data folders.
204
+ - Read first, act second: inspect project structure and existing notes before making changes.
205
+ - Prefer durable artifacts in \`notes/\`, \`outputs/\`, \`experiments/\`, and \`papers/\`.
206
+ - Keep strong claims source-grounded. Include direct URLs in final writeups.
207
+
208
+ ## Current Status
209
+ - Replace this section with the latest project status, known issues, and next steps.
210
+
211
+ ## Session Logging
212
+ - Use \`/log\` at the end of meaningful sessions to write a durable session note into \`notes/session-logs/\`.
213
+
214
+ ## Review Readiness
215
+ - Known reviewer concerns:
216
+ - Missing experiments:
217
+ - Missing writing or framing work:
218
+ `;
219
+ }
220
+
221
+ export function buildSessionLogsReadme(): string {
222
+ return `# Session Logs
223
+
224
+ Use \`/log\` to write one durable note per meaningful Feynman session.
225
+
226
+ Recommended contents:
227
+ - what was done
228
+ - strongest findings
229
+ - artifacts written
230
+ - unresolved questions
231
+ - next steps
232
+ `;
233
+ }