import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs"; import { homedir } from "node:os"; import { dirname, isAbsolute, join, resolve, sep } from "node:path"; import { createAssistantMessageEventStream, Type, } from "@earendil-works/pi-ai"; import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage, } from "@earendil-works/pi-ai"; import { getAgentDir as getPiAgentDir, main as runPiMain, } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI, ExtensionFactory, } from "@earendil-works/pi-coding-agent"; import { loadEnvFile, loadProviderProfiles, resolveProviderProfile } from "./model/env.ts"; import { ModelRuntime } from "./model/model-runtime.ts"; import { createGeneratedTools } from "./harness/builtin-tools.ts"; import { createRsiEditor, createRsiHeader, } from "./tui/rsih-tui.ts"; import { renderHarnessSystemPrompt, } from "./harness/genome.ts"; import { resolveHarnessGenome, genomeDisplayName, } from "./harness/genome-loader.ts"; import { validateJsonSchema } from "./core/schema.ts"; import { createMcpTools } from "./harness/mcp.ts"; import { applyModelOptionsToPayload, genomeResourceIsolationArgs, projectGenomeResources, projectGenomeSettings, } from "./harness/pi-projection.ts"; import { applyManagedConfiguration } from "./harness/settings-layer.ts"; import { runGenomeCommand } from "./cli/genome-command.ts"; /** Kept in sync with package.json; shown in the header and settings stamp. */ export const RSIH_VERSION = "0.1.0"; const EMPTY_USAGE: Usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, }, }; const CUSTOM_OPTIONS = new Set([ "--genome", "--profile", "--config", "--env", "--cwd", "--max-turns", "--run-id", ]); const RSIH_FLAG_DESCRIPTIONS = Object.freeze([ ["profile", "Provider profile from --config (also sets Pi's --provider)."], ["config", "Provider profile JSON with custom model endpoints."], ["env", "Env file to load before starting (default: ./.env)."], ["cwd", "Working directory to run in."], ["max-turns", "Abort the agent after this many turns."], ["run-id", "Session id to create or reuse (Pi's --session-id)."], ]); /** * Settings RSIH itself needs regardless of Genome. RSIH draws its own header, * so Pi's startup resource listing would be printed twice; `quietStartup` * suppresses the listing without disabling any resource. */ const RSIH_BASE_SETTINGS = Object.freeze({ quietStartup: true }); function requireValue(argv, index, option) { const value = argv[index + 1]; if (value === undefined || value.startsWith("-")) { throw new Error(`${option} requires a value.`); } return value; } export function parseCliArgs(argv) { const result = { cwd: ".", maxTurns: undefined, json: false, newSession: false, piArgs: [], }; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; // Pi accepts both `--option value` and `--option=value`. RSIH consumes its // own options before Pi parses argv, so it has to understand both // spellings — otherwise `--genome=paperlab` falls through to Pi untouched and // silently starts the default Genome. const equals = argument.indexOf("="); const option = equals === -1 ? argument : argument.slice(0, equals); if (CUSTOM_OPTIONS.has(option)) { let value; if (equals === -1) { value = requireValue(argv, index, option); index += 1; } else { value = argument.slice(equals + 1); if (value === "") throw new Error(`${option} requires a value.`); } switch (option) { case "--genome": result.genome = value; break; case "--profile": result.profile = value; result.piArgs.push("--provider", value); break; case "--config": result.config = value; break; case "--env": result.env = value; break; case "--cwd": result.cwd = value; break; case "--max-turns": { const maxTurns = Number(value); if (!Number.isInteger(maxTurns) || maxTurns < 1) { throw new Error("--max-turns must be a positive integer."); } result.maxTurns = maxTurns; break; } case "--run-id": result.piArgs.push("--session-id", value); break; } continue; } if (argument === "--json") { result.json = true; result.piArgs.push("--mode", "json"); continue; } if (argument === "--new") { result.newSession = true; continue; } result.piArgs.push(argument); } return result; } function optionValue(args, name) { const equalsPrefix = `${name}=`; for (let index = args.length - 1; index >= 0; index -= 1) { if (args[index].startsWith(equalsPrefix)) { return args[index].slice(equalsPrefix.length); } if (args[index] === name) return args[index + 1]; } return undefined; } function hasOption(args, ...names) { return args.some((argument) => names.some((name) => argument === name || argument.startsWith(`${name}=`)), ); } /** * Theme selection and the theme-discovery switch are CLI-level; the theme * *paths* a Genome ships are supplied through `resources_discover` instead. */ export function genomeAppearanceArgs(genome, piArgs = []) { const args = []; if ( genome.appearance?.theme && !hasOption(piArgs, "--use-theme") ) { args.push("--use-theme", genome.appearance.theme); } if (genome.appearance?.no_themes && !hasOption(piArgs, "--no-themes")) { args.push("--no-themes"); } return args; } function sourceEntry(entry) { if (typeof entry === "string") return { source: entry, enabled: true }; return { ...entry, enabled: entry.enabled !== false }; } function resolveGenomeSources(entries, baseDirectory) { return (entries ?? []) .map(sourceEntry) .filter( (entry) => entry.enabled && typeof entry.source === "string" && entry.source.trim() !== "", ) .map((entry) => resolve(baseDirectory, entry.source)); } function sessionFiles(sessionDirectory) { try { return readdirSync(sessionDirectory) .filter((name) => name.endsWith(".jsonl")) .map((name) => join(sessionDirectory, name)); } catch { return []; } } function readSessionEntries(path) { try { return readFileSync(path, "utf8") .split(/\r?\n/) .filter(Boolean) .map((line) => JSON.parse(line)); } catch { return []; } } function sessionId(path) { return readSessionEntries(path).find((entry) => entry.type === "session")?.id; } function findSessionFile(args, sessionDirectory, cwd) { if (hasOption(args, "--no-session", "--resume", "-r")) return undefined; const requested = optionValue(args, "--session"); if (requested) { if ( isAbsolute(requested) || requested.includes("/") || requested.includes("\\") || requested.endsWith(".jsonl") ) { return resolve(cwd, requested); } return sessionFiles(sessionDirectory).find((path) => { const id = sessionId(path); return id === requested || id?.startsWith(requested); }); } const exactId = optionValue(args, "--session-id"); if (exactId) { return sessionFiles(sessionDirectory).find( (path) => sessionId(path) === exactId, ); } if (hasOption(args, "--continue", "-c")) { return sessionFiles(sessionDirectory) .map((path) => ({ path, modified: statSync(path).mtimeMs })) .sort((left, right) => right.modified - left.modified)[0]?.path; } return undefined; } function storedGenomeReference(sessionFile) { if (!sessionFile) return undefined; const entry = readSessionEntries(sessionFile) .filter( (candidate) => candidate.type === "custom" && candidate.customType === "rsih.genome", ) .at(-1); if (!entry?.data?.genome) return undefined; return { genome: entry.data.genome, reference: entry.data.reference ?? entry.data.genome.genome_id, path: undefined, baseDirectory: entry.data.baseDirectory, }; } function mockResponsesFromProfiles(profiles) { return Object.fromEntries( Object.entries(profiles) .filter(([, profile]) => profile.kind === "mock") .map(([id, profile]) => [ id, profile.mock_responses ?? profile.responses ?? [], ]), ); } function builtinProviderProfiles() { return { gpt: resolveProviderProfile("gpt", { kind: "http", api: "openai-responses", provider: "openai", model_env: "GPT_MODEL", base_url_env: "GPT_API_BASE_URL", api_key_env: "GPT_API_KEY", max_tokens: 16384, capabilities: { tools: true, structured_output: true, streaming: false, reasoning: true, images: false, usage: true, }, }), local: resolveProviderProfile("local", { kind: "http", api: "openai-completions", provider: "local", model_env: "LOCAL_MODEL_ID", base_url_env: "LOCAL_MODEL_BASE_URL", api_key_env: "LOCAL_MODEL_API_KEY", allow_unauthenticated: true, reasoning: true, max_tokens: 24576, timeout_ms: 3600000, chat_template_kwargs: { enable_thinking: true }, compat: { supportsStrictMode: false }, capabilities: { tools: true, structured_output: false, streaming: false, reasoning: true, images: false, usage: true, }, }), }; } function messageText(content) { if (typeof content === "string") return content; return (content ?? []) .filter((item) => item.type === "text") .map((item) => item.text) .join(""); } function fromPiContext(context: Context) { return context.messages.map((message) => { if (message.role === "user") { return { role: "user", content: messageText(message.content) }; } if (message.role === "toolResult") { return { role: "tool", tool_call_id: message.toolCallId, name: message.toolName, content: messageText(message.content), is_error: message.isError, }; } return { role: "assistant", content: message.content .filter((item) => item.type === "text") .map((item) => item.text) .join(""), tool_calls: message.content .filter((item) => item.type === "toolCall") .map((item) => ({ id: item.id, name: item.name, arguments: item.arguments, })), stop_reason: message.stopReason === "toolUse" ? "tool_use" : message.stopReason, }; }); } function toUsage(usage = {}): Usage { return { ...EMPTY_USAGE, input: usage.input ?? 0, output: usage.output ?? 0, cacheRead: usage.cacheRead ?? usage.cache_read ?? 0, cacheWrite: usage.cacheWrite ?? usage.cache_write ?? 0, totalTokens: usage.totalTokens ?? usage.total_tokens ?? 0, cost: { ...EMPTY_USAGE.cost, ...(usage.cost ?? {}) }, }; } function streamProfile( runtime, profileId, genome, model: Model, context: Context, options?: SimpleStreamOptions, ) { const stream = createAssistantMessageEventStream(); void runtime .generate(profileId, { system: context.systemPrompt, messages: fromPiContext(context), tools: context.tools ?? [], maxTokens: options?.maxTokens ?? genome.model_options?.max_tokens, temperature: options?.temperature ?? genome.model_options?.temperature, chatTemplateKwargs: genome.model_options?.chat_template_kwargs, timeoutMs: options?.timeoutMs, maxRetries: options?.maxRetries, signal: options?.signal, }) .then((response) => { const message: AssistantMessage = { role: "assistant", content: [ ...(response.reasoning ? [{ type: "thinking", thinking: response.reasoning }] : []), ...(response.text ? [{ type: "text", text: response.text }] : []), ...(response.tool_calls ?? []).map((call) => ({ type: "toolCall", id: call.id, name: call.name, arguments: call.arguments ?? {}, })), ], api: model.api, provider: profileId, model: response.model ?? model.id, responseId: response.response_id ?? undefined, usage: toUsage(response.usage), stopReason: response.stop_reason === "tool_use" ? "toolUse" : response.stop_reason === "length" ? "length" : "stop", timestamp: Date.now(), }; stream.push({ type: "start", partial: { ...message, content: [] } }); stream.push({ type: "done", reason: message.stopReason, message }); }) .catch((error) => { const message: AssistantMessage = { role: "assistant", content: [], api: model.api, provider: profileId, model: model.id, usage: EMPTY_USAGE, stopReason: options?.signal?.aborted ? "aborted" : "error", errorMessage: error instanceof Error ? error.message : String(error), timestamp: Date.now(), }; stream.push({ type: "error", reason: message.stopReason, error: message, }); }); return stream; } function registerProfiles(pi: ExtensionAPI, profiles, genome) { const runtime = new ModelRuntime({ profiles, mockResponses: mockResponsesFromProfiles(profiles), }); for (const [profileId, profile] of Object.entries(profiles)) { if (!profile.model) continue; const api = `rsih-${profileId}`; pi.registerProvider(profileId, { name: profileId, baseUrl: profile.base_url ?? "http://127.0.0.1", apiKey: profile.api_key ?? "rsih-runtime", api, authHeader: false, models: [ { id: profile.model, name: profile.model, api, reasoning: profile.reasoning === true || profile.capabilities?.reasoning === true, input: profile.capabilities?.images ? ["text", "image"] : ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: profile.context_window ?? 128000, maxTokens: profile.max_tokens ?? 16384, }, ], streamSimple: (model, context, options) => streamProfile(runtime, profileId, genome, model, context, options), }); } } function supplementalSystemPrompt(genome) { return renderHarnessSystemPrompt({ ...genome, system_prompt: "", append_system_prompt: "", }); } function registerInlineResources(pi: ExtensionAPI, genome) { const inlineSkills = (genome.skills ?? []).filter( (skill) => typeof skill === "object" && skill.source === undefined, ); if (inlineSkills.length > 0) { const byName = new Map(inlineSkills.map((skill) => [skill.name, skill])); pi.registerTool({ name: "load_skill", label: "Load skill", description: "Load the complete instructions for an inline Genome skill.", parameters: Type.Object({ name: Type.String() }), async execute(_toolCallId, { name }) { const skill = byName.get(name); if (!skill) { throw new Error(`Unknown Genome skill "${name}".`); } return { content: [ { type: "text", text: [ `Skill: ${skill.name}`, `Description: ${skill.description}`, "Instructions:", skill.content, ].join("\n"), }, ], details: {}, }; }, }); for (const skill of inlineSkills) { pi.registerCommand(`skill:${skill.name}`, { description: skill.description, async handler(args, ctx) { pi.sendUserMessage( [`Use the "${skill.name}" skill.`, skill.content, args] .filter(Boolean) .join("\n\n"), ); }, }); } } // File-backed templates are discovered by Pi through `resources_discover`; // only inline ones become commands here. const inlineTemplates = (genome.prompt_templates ?? []).filter( (template) => template && typeof template === "object" && template.source === undefined, ); for (const template of inlineTemplates) { pi.registerCommand(template.name, { description: template.description ?? "", async handler(args) { pi.sendUserMessage( [template.content, args].filter(Boolean).join("\n\n"), ); }, }); } } function registerScratchpad(pi: ExtensionAPI, genome) { if (!genome.policies?.scratchpad?.enabled) return; const values = new Map(); const policy = genome.policies.scratchpad; pi.registerTool({ name: "scratchpad", label: "Scratchpad", description: "Read or update session-only Genome state.", parameters: Type.Object({ action: Type.Union([ Type.Literal("list"), Type.Literal("get"), Type.Literal("set"), Type.Literal("append"), Type.Literal("delete"), ]), key: Type.Optional(Type.String()), value: Type.Optional(Type.String()), }), async execute(_toolCallId, { action, key, value }) { if (action === "list") { return { content: [{ type: "text", text: JSON.stringify(Object.fromEntries(values)) }], details: {}, }; } if (!key) throw new Error(`Scratchpad action "${action}" requires key.`); if (action === "get") { return { content: [{ type: "text", text: values.get(key) ?? "" }], details: {}, }; } if (action === "delete") { values.delete(key); return { content: [{ type: "text", text: `Deleted ${key}` }], details: {}, }; } if (typeof value !== "string") { throw new Error(`Scratchpad action "${action}" requires value.`); } if (!values.has(key) && values.size >= Number(policy.max_entries ?? 32)) { throw new Error(`Scratchpad is limited to ${policy.max_entries ?? 32} entries.`); } const updated = action === "append" ? `${values.get(key) ?? ""}${value}` : value; if (updated.length > Number(policy.max_value_chars ?? 4000)) { throw new Error( `Scratchpad values are limited to ${policy.max_value_chars ?? 4000} characters.`, ); } values.set(key, updated); return { content: [{ type: "text", text: `Stored ${key}` }], details: {}, }; }, }); } /** * Tools whose implementation belongs to Pi. A Genome can enable, disable, and * narrow their parameters, but it cannot restate their description: doing so * would mean re-registering — and therefore reimplementing — Pi's tool. */ function genomeNarrowedSchemas(genome) { const narrowed = new Map(); for (const tool of genome.tools ?? []) { if (tool.parameters) narrowed.set(toolName(tool.name), tool.parameters); } return narrowed; } /** Pi calls its directory-listing tool `ls`; older Genomes wrote `list`. */ function toolName(name) { return name === "list" ? "ls" : name; } /** * Genome `tools` entries are a patch on whatever Pi already activated, not a * whitelist. A Genome that never mentions tools keeps Pi's full tool set, and a * Genome that disables one tool keeps the rest. */ export function genomeActiveToolNames(activeToolNames, allToolNames, genome) { const entries = genome.tools ?? []; if (entries.length === 0) return undefined; const known = new Set(allToolNames); const active = new Set(activeToolNames); for (const tool of entries) { const name = toolName(tool.name); if (tool.enabled === false) { active.delete(name); } else if (known.has(name)) { active.add(name); } } return allToolNames.filter((name) => active.has(name)); } /** * What to tell the user about the relationship between their installed Genome * and the one this RSIH ships. A stale unmodified copy is refreshed silently by * the loader, but the user still needs to know their Genome changed underneath * them; a modified copy is never touched, so the only honest move is to say a * newer version exists and leave the decision to them. */ export function genomeSeedNotice(resolvedGenome, label) { if (resolvedGenome.seeded) { return { message: `Installed Genome "${label}" to ${dirname(resolvedGenome.path)}`, level: "info", }; } if (resolvedGenome.refreshed) { return { message: `Updated Genome "${label}" to the version shipped with this RSIH`, level: "info", }; } if (resolvedGenome.outdated) { return { message: `Genome "${label}" differs from the version shipped with this RSIH. Run \`rsih genome install ${label}\` to replace your copy.`, level: "warning", }; } return undefined; } export function genomeFooterStatus(label) { return `\u001b[35mGenome: ${label}\u001b[39m`; } function genomeResourceLabels(entries): string[] { return (entries ?? []) .map((entry) => { if (typeof entry === "string") { return entry.split(/[\\/]/).filter(Boolean).at(-1) ?? entry; } if (!entry || typeof entry !== "object") return ""; return ( entry.name ?? entry.source?.split(/[\\/]/).filter(Boolean).at(-1) ?? "" ); }) .filter( (label): label is string => typeof label === "string" && label.length > 0, ); } function resourcePathLabel(path: string): string { if (path.startsWith("<") && path.endsWith(">")) return path; return path.split(/[\\/]/).filter(Boolean).at(-1) ?? path; } function runtimeResourceLabels(pi, genome) { const skills = new Set(genomeResourceLabels(genome.skills)); const extensions = new Set(genomeResourceLabels(genome.extensions)); for (const command of pi.getCommands()) { if (command.source === "skill") { skills.add(command.name.replace(/^skill:/, "")); } else if (command.source === "extension") { extensions.add(resourcePathLabel(command.sourceInfo.path)); } } for (const tool of pi.getAllTools()) { const sourcePath = tool.sourceInfo?.path; if (sourcePath && !sourcePath.startsWith(" { return async (pi) => { const activeResources = { skills: [...(resources?.skills ?? [])], extensions: [...(resources?.extensions ?? [])], }; const narrowedSchemas = genomeNarrowedSchemas(genome); // RSIH consumes these before Pi parses argv; registering them is what puts // them in `--help` alongside Pi's own options. pi.registerFlag("genome", { type: "string", description: "Genome name or JSON path (./.rsih/genomes, then ~/.rsih/genomes). Shorthand: rsih +name, :name or ::name.", default: reference, }); for (const [name, description] of RSIH_FLAG_DESCRIPTIONS) { pi.registerFlag(name, { type: "string", description }); } pi.registerFlag("json", { type: "boolean", description: "Shorthand for --mode json.", }); pi.registerFlag("new", { type: "boolean", description: "Start a fresh session instead of resuming.", }); registerProfiles(pi, profiles, genome); registerInlineResources(pi, genome); registerScratchpad(pi, genome); for (const tool of createGeneratedTools({ cwd, genome })) { pi.registerTool({ name: tool.name, label: tool.name, description: tool.description, parameters: tool.parameters, executionMode: genome.runtime?.tool_execution, async execute(_toolCallId, params, signal) { const text = await tool.execute(params, { signal }); return { content: [{ type: "text", text: String(text ?? "") }], details: {}, }; }, }); } const mcp = await createMcpTools({ cwd, genome }); for (const tool of mcp.tools) { pi.registerTool({ name: tool.name, label: tool.name, description: tool.description, parameters: tool.parameters, executionMode: genome.runtime?.tool_execution, async execute(_toolCallId, params, signal) { const text = await tool.execute(params, { signal }); return { content: [{ type: "text", text: String(text ?? "") }], details: {}, }; }, }); } pi.on("resources_discover", () => projectGenomeResources(genome, (path) => resolve(baseDirectory, path)), ); pi.on("before_provider_request", (event) => applyModelOptionsToPayload(event.payload, genome.model_options), ); pi.on("session_start", (_event, ctx) => { if (ctx.mode === "tui") { const loadedResources = runtimeResourceLabels(pi, genome); activeResources.skills = loadedResources.skills; activeResources.extensions = loadedResources.extensions; ctx.ui.setHeader((tui, theme) => createRsiHeader(theme, { cwd, model: ctx.model?.id ?? modelLabel, genome: genomeLabel, version: RSIH_VERSION, resources: activeResources, }), ); ctx.ui.setEditorComponent( (tui, theme, keybindings) => createRsiEditor(tui, theme, keybindings), ); } if (showGenomeStatus && ctx.mode === "tui") { ctx.ui.setStatus("rsih-genome", genomeFooterStatus(genomeLabel)); } if (seedNotice && ctx.mode === "tui") { ctx.ui.notify(seedNotice.message, seedNotice.level); } const prior = ctx.sessionManager .getEntries() .filter( (entry) => entry.type === "custom" && entry.customType === "rsih.genome", ) .at(-1); if ( prior?.data?.genome_id !== genome.genome_id || prior?.data?.reference !== reference ) { pi.appendEntry("rsih.genome", { reference, baseDirectory, genome_id: genome.genome_id, genome, }); } if (!cliOverridesTools) { const activeTools = genomeActiveToolNames( pi.getActiveTools(), pi.getAllTools().map((tool) => tool.name), genome, ); if (activeTools) pi.setActiveTools(activeTools); } if ( !cliOverridesThinking && !preserveSessionDefaults && genome.runtime?.thinking_level ) { pi.setThinkingLevel(genome.runtime.thinking_level); } }); pi.on("turn_start", (event, ctx) => { // Pi has no turn cap of its own, so only enforce one that was asked for. if (maxTurns !== undefined && event.turnIndex >= maxTurns) ctx.abort(); }); pi.on("tool_call", (event) => { if (genome.policies?.tool?.blocked_tools?.includes(event.toolName)) { return { block: true, reason: `Tool "${event.toolName}" is blocked by Genome policy.`, }; } // Pi owns its tool implementations, so a Genome narrows a tool by // rejecting out-of-contract calls rather than re-registering the tool. const schema = narrowedSchemas.get(event.toolName); if (schema) { const errors = validateJsonSchema(event.input, schema); if (errors.length > 0) { return { block: true, reason: `Tool "${event.toolName}" was called outside its Genome parameter contract:\n- ${errors.join("\n- ")}`, }; } } return undefined; }); pi.on("tool_result", (event) => { const maxChars = genome.policies?.tool?.max_result_chars; if (maxChars === undefined) return undefined; let remaining = Number(maxChars); let truncated = false; const content = event.content.map((item) => { if (item.type !== "text") return item; if (item.text.length <= remaining) { remaining -= item.text.length; return item; } truncated = true; const text = `${item.text.slice(0, Math.max(0, remaining))}\n[truncated]`; remaining = 0; return { ...item, text }; }); return truncated ? { content } : undefined; }); pi.on("session_shutdown", async () => { await mcp.close(); }); }; } /** * Mirror of Pi's default session directory (`/sessions/----`). * RSIH only needs it to find the Genome snapshot stored in a resumed session; * Pi still owns session creation and layout. * * This runs before the Genome's settings are compiled, so it reads the settings * file as it stands — which is where the run being resumed put its sessions. */ function piSessionDirectory(cwd, piArgs) { const agentDirectory = getPiAgentDir(); const settingsPath = join(agentDirectory, "settings.json"); let settingsSessionDir; if (existsSync(settingsPath)) { try { settingsSessionDir = JSON.parse(readFileSync(settingsPath, "utf8")) ?.sessionDir; } catch { // Pi reports malformed settings; fall back to its default layout. } } const explicit = optionValue(piArgs, "--session-dir") ?? process.env.RSIH_CODING_AGENT_SESSION_DIR ?? settingsSessionDir; if (explicit) return resolve(cwd, explicit); const encoded = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; return join(agentDirectory, "sessions", encoded); } export async function runPiCli(argv) { // `rsih genome ...` is RSIH's own subcommand; everything else, including Pi's // install/update/list/config/auth subcommands, falls through to Pi. if (runGenomeCommand(argv)) return; const parsed = parseCliArgs(argv); if ( parsed.newSession && hasOption(parsed.piArgs, "--continue", "-c", "--resume", "-r", "--session") ) { throw new Error("--new cannot be combined with session resume options."); } process.chdir(resolve(parsed.cwd)); // Read the directory back so RSIH and Pi agree on the canonical cwd; on // macOS `/tmp` and `/var` resolve through symlinks and the session directory // Pi derives would not match. const cwd = process.cwd(); if (parsed.env) { loadEnvFile(resolve(cwd, parsed.env)); } else { try { loadEnvFile(join(cwd, ".env")); } catch (error) { if (error?.code !== "ENOENT") throw error; } } // Session storage stays wherever Pi puts it, so `rsih` and `pi` resume the // same way. A Genome that wants project-local runs sets its own session dir. const sessionDirectory = piSessionDirectory(cwd, parsed.piArgs); const existingSession = findSessionFile(parsed.piArgs, sessionDirectory, cwd); const storedGenome = parsed.genome === undefined ? storedGenomeReference(existingSession) : undefined; const resolvedGenome = storedGenome ?? resolveHarnessGenome(parsed.genome, { cwd, homeDirectory: homedir(), }); const { genome } = resolvedGenome; const genomeLabel = genomeDisplayName(resolvedGenome); const baseDirectory = resolve(resolvedGenome.baseDirectory ?? cwd); // Pi exposes no API for settings or keybindings, so the Genome's projection // is compiled into the files Pi reads. Only Genome-declared keys are touched. const projection = projectGenomeSettings(genome); applyManagedConfiguration({ agentDirectory: getPiAgentDir(), settings: { ...RSIH_BASE_SETTINGS, ...projection.settings }, keybindings: projection.keybindings, stamp: { genome: resolvedGenome.reference, genomeId: genome.genome_id, rsih: RSIH_VERSION, }, }); const profiles = parsed.config ? loadProviderProfiles(resolve(cwd, parsed.config)) : builtinProviderProfiles(); const selectedProfile = parsed.profile ?? genome.model?.profile; const selectedModel = optionValue(parsed.piArgs, "--model") ?? genome.model?.id ?? (selectedProfile ? profiles[selectedProfile]?.model : undefined); const preserveSessionDefaults = existingSession !== undefined && parsed.genome === undefined; const genomeArgs = []; if ( !preserveSessionDefaults && !hasOption(parsed.piArgs, "--provider") && selectedProfile ) { genomeArgs.push("--provider", selectedProfile); } if ( !preserveSessionDefaults && !hasOption(parsed.piArgs, "--model") && selectedModel ) { genomeArgs.push("--model", selectedModel); } // Only replace Pi's own system prompt when the Genome actually declares one. if ( genome.system_prompt && !hasOption(parsed.piArgs, "--system-prompt") ) { genomeArgs.push("--system-prompt", genome.system_prompt); } const appendedPrompt = [ genome.append_system_prompt, supplementalSystemPrompt(genome), ] .filter(Boolean) .join("\n\n"); if (appendedPrompt && !hasOption(parsed.piArgs, "--append-system-prompt")) { genomeArgs.push("--append-system-prompt", appendedPrompt); } // Skills, prompt templates, and themes reach Pi through the // `resources_discover` hook. Extensions have no such hook, so they stay on // argv; explicit `-e` paths keep working even under resource isolation. for (const path of resolveGenomeSources(genome.extensions, baseDirectory)) { genomeArgs.push("--extension", path); } genomeArgs.push(...genomeAppearanceArgs(genome, parsed.piArgs)); genomeArgs.push(...genomeResourceIsolationArgs(genome, parsed.piArgs)); if (parsed.genome !== undefined) { genomeArgs.push("--genome", parsed.genome); } const customProfiles = parsed.config || (selectedProfile && ["gpt", "local"].includes(selectedProfile)) ? profiles : {}; if (selectedProfile && parsed.config && !profiles[selectedProfile]) { throw new Error( `Provider profile "${selectedProfile}" was not found in ${resolve(cwd, parsed.config)}.`, ); } if ( selectedProfile && Object.hasOwn(customProfiles, selectedProfile) && !customProfiles[selectedProfile].model ) { throw new Error( `Provider profile "${selectedProfile}" has no resolved model.`, ); } const extensionFactory = await createGenomeExtension({ genome, genomeLabel, modelLabel: selectedModel ?? "", resources: { skills: genomeResourceLabels(genome.skills), extensions: genomeResourceLabels(genome.extensions), }, showGenomeStatus: resolvedGenome.reference !== "default", reference: resolvedGenome.reference, // A built-in Genome is copied into ~/.rsih/genomes on first use, so that the // Genome the agent runs -- and reads its skills from -- is always the one in // the user's own directory. Every write, and every refusal to write, is // announced rather than done silently. seedNotice: genomeSeedNotice(resolvedGenome, genomeLabel), baseDirectory, profiles: customProfiles, maxTurns: parsed.maxTurns ?? genome.runtime?.max_turns, cliOverridesTools: hasOption( parsed.piArgs, "--tools", "-t", "--exclude-tools", "-xt", "--no-tools", "-nt", "--no-builtin-tools", "-nbt", ), cliOverridesThinking: hasOption(parsed.piArgs, "--thinking"), preserveSessionDefaults, cwd, }); await runPiMain([...genomeArgs, ...parsed.piArgs], { extensionFactories: [extensionFactory], }); }