Refine Feynman research workflows
This commit is contained in:
@@ -16,6 +16,8 @@ Operating rules:
|
||||
- Never answer a latest/current question from arXiv or alpha-backed paper search alone.
|
||||
- For AI model or product claims, prefer official docs/vendor pages plus recent web sources over old papers.
|
||||
- Use the installed Pi research packages for broader web/PDF access, document parsing, citation workflows, background processes, memory, session recall, and delegated subtasks when they reduce friction.
|
||||
- Feynman ships project subagents for research work. Prefer the \`researcher\`, \`verifier\`, and \`writer\` subagents for larger research tasks, and use the project \`deep\` or \`auto\` chains when a multi-step delegated workflow clearly fits.
|
||||
- Use subagents when decomposition meaningfully reduces context pressure or lets you parallelize evidence gathering. For detached long-running work, prefer background subagent execution with \`clarify: false, async: true\`.
|
||||
- Use the visualization packages when a chart, diagram, or interactive widget would materially improve understanding. Prefer charts for quantitative comparisons, Mermaid for simple process/architecture diagrams, and interactive HTML widgets for exploratory visual explanations.
|
||||
- Persistent memory is package-backed. Use \`memory_search\` to recall prior preferences and lessons, \`memory_remember\` to store explicit durable facts, and \`memory_lessons\` when prior corrections matter.
|
||||
- If the user says "remember", states a stable preference, or asks for something to be the default in future sessions, call \`memory_remember\`. Do not just say you will remember it.
|
||||
@@ -23,6 +25,7 @@ Operating rules:
|
||||
- Feynman is intended to support always-on research work. Use the scheduling package when recurring or deferred work is appropriate instead of telling the user to remember manually.
|
||||
- Use \`schedule_prompt\` for recurring scans, delayed follow-ups, reminders, and periodic research jobs.
|
||||
- If the user asks you to remind, check later, run something nightly, or keep watching something over time, call \`schedule_prompt\`. Do not just promise to do it later.
|
||||
- For long-running local work such as experiments, crawls, or log-following, use the process package instead of blocking the main thread unnecessarily. Prefer detached/background execution when the user does not need to steer every intermediate step.
|
||||
- Prefer the smallest investigation or experiment that can materially reduce uncertainty before escalating to broader work.
|
||||
- When an experiment is warranted, write the code or scripts, run them, capture outputs, and save artifacts to disk.
|
||||
- Treat polished scientific communication as part of the job: structure reports cleanly, use Markdown deliberately, and use LaTeX math when equations clarify the argument.
|
||||
|
||||
301
src/index.ts
301
src/index.ts
@@ -1,7 +1,7 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
@@ -23,24 +23,214 @@ import {
|
||||
import { buildFeynmanSystemPrompt } from "./feynman-prompt.js";
|
||||
|
||||
type ThinkingLevel = "off" | "low" | "medium" | "high";
|
||||
type Rgb = { r: number; g: number; b: number };
|
||||
type ThemeColorValue = string | number;
|
||||
type ThemeJson = {
|
||||
$schema?: string;
|
||||
name: string;
|
||||
vars?: Record<string, ThemeColorValue>;
|
||||
colors: Record<string, ThemeColorValue>;
|
||||
export?: Record<string, ThemeColorValue>;
|
||||
};
|
||||
|
||||
const OSC11_QUERY = "\u001b]11;?\u0007";
|
||||
const OSC11_RESPONSE_PATTERN =
|
||||
/\u001b]11;(?:rgb:([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})|#?([0-9a-fA-F]{6}))(?:\u0007|\u001b\\)/;
|
||||
const DEFAULT_SAGE_RGB: Rgb = { r: 0x88, g: 0xa8, b: 0x8a };
|
||||
|
||||
function parseHexComponent(component: string): number {
|
||||
const value = Number.parseInt(component, 16);
|
||||
if (Number.isNaN(value)) {
|
||||
throw new Error(`Invalid OSC 11 component: ${component}`);
|
||||
}
|
||||
if (component.length === 2) {
|
||||
return value;
|
||||
}
|
||||
return Math.round(value / ((1 << (component.length * 4)) - 1) * 255);
|
||||
}
|
||||
|
||||
function parseHexColor(color: string): Rgb | undefined {
|
||||
const match = color.trim().match(/^#?([0-9a-fA-F]{6})$/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
r: Number.parseInt(match[1].slice(0, 2), 16),
|
||||
g: Number.parseInt(match[1].slice(2, 4), 16),
|
||||
b: Number.parseInt(match[1].slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(rgb: Rgb): string {
|
||||
return `#${[rgb.r, rgb.g, rgb.b]
|
||||
.map((value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0"))
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
function blendRgb(base: Rgb, tint: Rgb, alpha: number): Rgb {
|
||||
const mix = (baseChannel: number, tintChannel: number) =>
|
||||
baseChannel + (tintChannel - baseChannel) * alpha;
|
||||
return {
|
||||
r: mix(base.r, tint.r),
|
||||
g: mix(base.g, tint.g),
|
||||
b: mix(base.b, tint.b),
|
||||
};
|
||||
}
|
||||
|
||||
function isLightRgb(rgb: Rgb): boolean {
|
||||
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
||||
return luminance >= 0.6;
|
||||
}
|
||||
|
||||
function resolveThemeColorValue(
|
||||
value: ThemeColorValue | undefined,
|
||||
vars: Record<string, ThemeColorValue> | undefined,
|
||||
visited = new Set<string>(),
|
||||
): ThemeColorValue | undefined {
|
||||
if (value === undefined || typeof value === "number" || value === "" || value.startsWith("#")) {
|
||||
return value;
|
||||
}
|
||||
if (!vars || !(value in vars) || visited.has(value)) {
|
||||
return value;
|
||||
}
|
||||
visited.add(value);
|
||||
return resolveThemeColorValue(vars[value], vars, visited);
|
||||
}
|
||||
|
||||
function resolveThemeRgb(
|
||||
value: ThemeColorValue | undefined,
|
||||
vars: Record<string, ThemeColorValue> | undefined,
|
||||
): Rgb | undefined {
|
||||
const resolved = resolveThemeColorValue(value, vars);
|
||||
return typeof resolved === "string" ? parseHexColor(resolved) : undefined;
|
||||
}
|
||||
|
||||
function deriveMessageBackgrounds(themeJson: ThemeJson, terminalBackgroundHex: string): Pick<ThemeJson["colors"], "userMessageBg" | "customMessageBg"> | undefined {
|
||||
const terminalBackground = parseHexColor(terminalBackgroundHex);
|
||||
if (!terminalBackground) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tint =
|
||||
resolveThemeRgb(themeJson.colors.accent, themeJson.vars) ??
|
||||
resolveThemeRgb(themeJson.vars?.sage, themeJson.vars) ??
|
||||
DEFAULT_SAGE_RGB;
|
||||
const lightBackground = isLightRgb(terminalBackground);
|
||||
const userAlpha = lightBackground ? 0.15 : 0.23;
|
||||
const customAlpha = lightBackground ? 0.11 : 0.17;
|
||||
|
||||
return {
|
||||
userMessageBg: rgbToHex(blendRgb(terminalBackground, tint, userAlpha)),
|
||||
customMessageBg: rgbToHex(blendRgb(terminalBackground, tint, customAlpha)),
|
||||
};
|
||||
}
|
||||
|
||||
async function probeTerminalBackgroundHex(timeoutMs = 120): Promise<string | undefined> {
|
||||
if (typeof process.env.FEYNMAN_TERMINAL_BG === "string" && process.env.FEYNMAN_TERMINAL_BG.trim()) {
|
||||
return process.env.FEYNMAN_TERMINAL_BG.trim();
|
||||
}
|
||||
if (typeof process.env.PI_TERMINAL_BG === "string" && process.env.PI_TERMINAL_BG.trim()) {
|
||||
return process.env.PI_TERMINAL_BG.trim();
|
||||
}
|
||||
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const wasRaw = "isRaw" in input ? Boolean((input as typeof input & { isRaw?: boolean }).isRaw) : false;
|
||||
|
||||
return await new Promise<string | undefined>((resolve) => {
|
||||
let settled = false;
|
||||
let buffer = "";
|
||||
|
||||
const finish = (value: string | undefined) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
input.off("data", onData);
|
||||
try {
|
||||
if (!wasRaw) {
|
||||
input.setRawMode(false);
|
||||
}
|
||||
} catch {
|
||||
// Ignore raw mode restore failures and return best-effort detection.
|
||||
}
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const onData = (chunk: string | Buffer) => {
|
||||
buffer += chunk.toString("utf8");
|
||||
const match = buffer.match(OSC11_RESPONSE_PATTERN);
|
||||
if (!match) {
|
||||
if (buffer.length > 512) {
|
||||
finish(undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[4]) {
|
||||
finish(`#${match[4].toLowerCase()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
finish(
|
||||
rgbToHex({
|
||||
r: parseHexComponent(match[1]),
|
||||
g: parseHexComponent(match[2]),
|
||||
b: parseHexComponent(match[3]),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
finish(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => finish(undefined), timeoutMs);
|
||||
input.on("data", onData);
|
||||
|
||||
try {
|
||||
if (!wasRaw) {
|
||||
input.setRawMode(true);
|
||||
}
|
||||
output.write(OSC11_QUERY);
|
||||
} catch {
|
||||
finish(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`Feynman commands:
|
||||
/help Show this help
|
||||
/init Initialize AGENTS.md and session-log folders
|
||||
/alpha-login Sign in to alphaXiv
|
||||
/alpha-logout Clear alphaXiv auth
|
||||
/alpha-status Show alphaXiv auth status
|
||||
/new Start a fresh persisted session
|
||||
/exit Quit the REPL
|
||||
/lit-review <topic> Expand the literature review prompt template
|
||||
/lit <topic> Expand the literature review prompt template
|
||||
/replicate <paper> Expand the replication prompt template
|
||||
/reading-list <topic> Expand the reading list prompt template
|
||||
/research-memo <topic> Expand the general research memo prompt template
|
||||
/reading <topic> Expand the reading list prompt template
|
||||
/memo <topic> Expand the general research memo prompt template
|
||||
/deepresearch <topic> Expand the thorough source-heavy research prompt template
|
||||
/autoresearch <idea> Expand the idea-to-paper autoresearch prompt template
|
||||
/compare-sources <topic> Expand the source comparison prompt template
|
||||
/paper-code-audit <item> Expand the paper/code audit prompt template
|
||||
/paper-draft <topic> Expand the paper-style writing prompt template
|
||||
/compare <topic> Expand the source comparison prompt template
|
||||
/audit <item> Expand the paper/code audit prompt template
|
||||
/draft <topic> Expand the paper-style writing prompt template
|
||||
/log Write a durable session log
|
||||
/watch <topic> Create a recurring or deferred research watch
|
||||
/jobs Inspect active background work
|
||||
|
||||
Package-powered workflows:
|
||||
/agents Open the subagent and chain manager
|
||||
/run /chain /parallel Delegate research work to subagents
|
||||
/ps Open the background process panel
|
||||
/schedule-prompt Manage deferred and recurring jobs
|
||||
/search Search prior indexed sessions
|
||||
/preview Preview generated markdown or code artifacts
|
||||
|
||||
CLI flags:
|
||||
--prompt "<text>" Run one prompt and exit
|
||||
@@ -116,6 +306,7 @@ function patchEmbeddedPiBranding(piPackageRoot: string): void {
|
||||
const packageJsonPath = resolve(piPackageRoot, "package.json");
|
||||
const cliPath = resolve(piPackageRoot, "dist", "cli.js");
|
||||
const interactiveModePath = resolve(piPackageRoot, "dist", "modes", "interactive", "interactive-mode.js");
|
||||
const footerPath = resolve(piPackageRoot, "dist", "modes", "interactive", "components", "footer.js");
|
||||
|
||||
if (existsSync(packageJsonPath)) {
|
||||
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||
@@ -149,6 +340,42 @@ function patchEmbeddedPiBranding(piPackageRoot: string): void {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(footerPath)) {
|
||||
const footerSource = readFileSync(footerPath, "utf8");
|
||||
const footerOriginal = [
|
||||
' // Add thinking level indicator if model supports reasoning',
|
||||
' let rightSideWithoutProvider = modelName;',
|
||||
' if (state.model?.reasoning) {',
|
||||
' const thinkingLevel = state.thinkingLevel || "off";',
|
||||
' rightSideWithoutProvider =',
|
||||
' thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;',
|
||||
' }',
|
||||
' // Prepend the provider in parentheses if there are multiple providers and there\'s enough room',
|
||||
' let rightSide = rightSideWithoutProvider;',
|
||||
' if (this.footerData.getAvailableProviderCount() > 1 && state.model) {',
|
||||
' rightSide = `(${state.model.provider}) ${rightSideWithoutProvider}`;',
|
||||
].join("\n");
|
||||
const footerReplacement = [
|
||||
' // Add thinking level indicator if model supports reasoning',
|
||||
' const modelLabel = theme.fg("accent", modelName);',
|
||||
' let rightSideWithoutProvider = modelLabel;',
|
||||
' if (state.model?.reasoning) {',
|
||||
' const thinkingLevel = state.thinkingLevel || "off";',
|
||||
' const separator = theme.fg("dim", " • ");',
|
||||
' rightSideWithoutProvider = thinkingLevel === "off"',
|
||||
' ? `${modelLabel}${separator}${theme.fg("muted", "thinking off")}`',
|
||||
' : `${modelLabel}${separator}${theme.getThinkingBorderColor(thinkingLevel)(thinkingLevel)}`;',
|
||||
' }',
|
||||
' // Prepend the provider in parentheses if there are multiple providers and there\'s enough room',
|
||||
' let rightSide = rightSideWithoutProvider;',
|
||||
' if (this.footerData.getAvailableProviderCount() > 1 && state.model) {',
|
||||
' rightSide = `${theme.fg("muted", `(${state.model.provider})`)} ${rightSideWithoutProvider}`;',
|
||||
].join("\n");
|
||||
if (footerSource.includes(footerOriginal)) {
|
||||
writeFileSync(footerPath, footerSource.replace(footerOriginal, footerReplacement), "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchPackageWorkspace(appRoot: string): void {
|
||||
@@ -491,7 +718,28 @@ function setupPreviewDependencies(): void {
|
||||
throw new Error("Automatic preview setup is only supported on macOS with Homebrew right now.");
|
||||
}
|
||||
|
||||
function syncFeynmanTheme(appRoot: string, agentDir: string): void {
|
||||
function syncDirectory(sourceDir: string, targetDir: string): void {
|
||||
if (!existsSync(sourceDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
||||
const sourcePath = resolve(sourceDir, entry.name);
|
||||
const targetPath = resolve(targetDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
syncDirectory(sourcePath, targetPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
writeFileSync(targetPath, readFileSync(sourcePath, "utf8"), "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncFeynmanTheme(appRoot: string, agentDir: string, terminalBackgroundHex?: string): void {
|
||||
const sourceThemePath = resolve(appRoot, ".pi", "themes", "feynman.json");
|
||||
const targetThemeDir = resolve(agentDir, "themes");
|
||||
const targetThemePath = resolve(targetThemeDir, "feynman.json");
|
||||
@@ -501,7 +749,36 @@ function syncFeynmanTheme(appRoot: string, agentDir: string): void {
|
||||
}
|
||||
|
||||
mkdirSync(targetThemeDir, { recursive: true });
|
||||
writeFileSync(targetThemePath, readFileSync(sourceThemePath, "utf8"), "utf8");
|
||||
|
||||
const sourceTheme = readFileSync(sourceThemePath, "utf8");
|
||||
if (!terminalBackgroundHex) {
|
||||
writeFileSync(targetThemePath, sourceTheme, "utf8");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedTheme = JSON.parse(sourceTheme) as ThemeJson;
|
||||
const derivedBackgrounds = deriveMessageBackgrounds(parsedTheme, terminalBackgroundHex);
|
||||
if (!derivedBackgrounds) {
|
||||
writeFileSync(targetThemePath, sourceTheme, "utf8");
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedTheme: ThemeJson = {
|
||||
...parsedTheme,
|
||||
colors: {
|
||||
...parsedTheme.colors,
|
||||
...derivedBackgrounds,
|
||||
},
|
||||
};
|
||||
writeFileSync(targetThemePath, JSON.stringify(generatedTheme, null, 2) + "\n", "utf8");
|
||||
} catch {
|
||||
writeFileSync(targetThemePath, sourceTheme, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
function syncFeynmanAgents(appRoot: string, agentDir: string): void {
|
||||
syncDirectory(resolve(appRoot, ".pi", "agents"), resolve(agentDir, "agents"));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -539,9 +816,11 @@ async function main(): Promise<void> {
|
||||
|
||||
const workingDir = resolve(values.cwd ?? process.cwd());
|
||||
const sessionDir = resolve(values["session-dir"] ?? resolve(homedir(), ".feynman", "sessions"));
|
||||
const terminalBackgroundHex = await probeTerminalBackgroundHex();
|
||||
mkdirSync(sessionDir, { recursive: true });
|
||||
mkdirSync(feynmanAgentDir, { recursive: true });
|
||||
syncFeynmanTheme(appRoot, feynmanAgentDir);
|
||||
syncFeynmanTheme(appRoot, feynmanAgentDir, terminalBackgroundHex);
|
||||
syncFeynmanAgents(appRoot, feynmanAgentDir);
|
||||
const feynmanSettingsPath = resolve(feynmanAgentDir, "settings.json");
|
||||
const feynmanAuthPath = resolve(feynmanAgentDir, "auth.json");
|
||||
const thinkingLevel = normalizeThinkingLevel(values.thinking ?? process.env.FEYNMAN_THINKING) ?? "medium";
|
||||
@@ -637,6 +916,8 @@ async function main(): Promise<void> {
|
||||
...process.env,
|
||||
PI_CODING_AGENT_DIR: feynmanAgentDir,
|
||||
FEYNMAN_CODING_AGENT_DIR: feynmanAgentDir,
|
||||
FEYNMAN_TERMINAL_BG: terminalBackgroundHex,
|
||||
PI_TERMINAL_BG: terminalBackgroundHex,
|
||||
FEYNMAN_PI_NPM_ROOT: resolve(appRoot, ".pi", "npm", "node_modules"),
|
||||
FEYNMAN_SESSION_DIR: sessionDir,
|
||||
PI_SESSION_DIR: sessionDir,
|
||||
|
||||
Reference in New Issue
Block a user