This commit is contained in:
2026-02-23 22:52:27 +00:00
parent 8fb96c7cf8
commit 2126f3912e
35 changed files with 1126 additions and 137 deletions
+30 -2
View File
@@ -1,13 +1,33 @@
import { join, relative } from "path";
import { readdirSync, existsSync, mkdirSync } from "node:fs";
import type { Subprocess } from "bun";
import type { PiEvent, MessageCost } from "./types";
import { readApiKeys } from "../server-settings/pi-mono";
import { PI_CONFIG_DIR } from "../../data-path";
import { PI_CONFIG_DIR, getGlobalSkillsDir, getUserSkillsDir } from "../../data-path";
import { ensureDockerContainer } from "../terminal/websocket";
import { logger } from "./logger";
export type PiEventHandler = (event: PiEvent) => void;
function collectSkillFlags(email: string): string[] {
const flags: string[] = [];
const dirs = [getGlobalSkillsDir(), getUserSkillsDir(email)];
for (const dir of dirs) {
if (!existsSync(dir)) continue;
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillFile = join(dir, entry.name, 'SKILL.md');
if (existsSync(skillFile)) {
flags.push('--skill', join(dir, entry.name));
}
}
}
return flags;
}
type SandboxOptions = {
userId: number;
username: string;
@@ -18,6 +38,7 @@ type SandboxOptions = {
export async function spawnPi(
cwd: string,
model: string,
email: string,
onEvent: PiEventHandler,
sandbox?: SandboxOptions,
): Promise<Subprocess> {
@@ -59,9 +80,14 @@ export async function spawnPi(
logger.info('Spawned Pi in container', { containerId, model });
} else {
const storedKeys = await readApiKeys();
const args = ['pi', '--mode', 'rpc', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes'];
const skillFlags = collectSkillFlags(email);
const args = ['pi', '--mode', 'rpc', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes', ...skillFlags];
if (model) args.push('--model', model);
if (!existsSync(cwd)) {
mkdirSync(cwd, { recursive: true });
}
proc = Bun.spawn(args, {
cwd,
stdin: 'pipe',
@@ -69,6 +95,8 @@ export async function spawnPi(
stderr: 'pipe',
env: { ...process.env, ...storedKeys, PI_CODING_AGENT_DIR: PI_CONFIG_DIR },
});
logger.info('Spawned Pi locally', { model, skills: skillFlags.filter((f) => f !== '--skill').length });
}
// Read stdout JSON event stream (runs in background)
+20 -18
View File
@@ -36,23 +36,25 @@ type WSData = {
const IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
const resolveRoot = (email: string, root?: string) => {
if (!root || root === 'home') return getHomeDir(email);
if (root === '~') return homedir();
if (root === 'officer.dev') return resolve(process.cwd(), '..');
return getHomeDir(email);
const resolveSandboxedCwd = (email: string, cwdRoot?: string, cwd?: string) => {
const root = !cwdRoot || cwdRoot === 'home' ? getHomeDir(email) : getHomeDir(email);
if (!cwd || cwd === '~') return root;
if (cwd.startsWith('~/')) return join(root, cwd.slice(2));
if (cwd.startsWith('/')) return join(root, cwd.slice(1));
return root;
};
const resolveCwd = (home: string, cwd?: string) => {
if (!cwd || cwd === '~') return home;
if (cwd.startsWith('~/')) return join(home, cwd.slice(2));
if (cwd.startsWith('/')) return join(home, cwd.slice(1));
return home;
const resolveHostCwd = (cwdRoot?: string, cwd?: string) => {
if (cwdRoot === 'officer.dev') return resolve(process.cwd(), '..');
const root = homedir();
if (!cwd || cwd === '~') return root;
if (cwd.startsWith('/')) return cwd;
if (cwd.startsWith('~/')) return join(root, cwd.slice(2));
return join(root, cwd);
};
export const resolveBaseCwd = (email: string, cwdRoot?: string, cwd?: string) => {
const root = resolveRoot(email, cwdRoot);
return resolveCwd(root, cwd);
return resolveHostCwd(cwdRoot, cwd);
};
const wsToSessionMap = new WeakMap<any, string>();
@@ -271,11 +273,11 @@ async function handleChat(
});
const homeDir = getHomeDir(email);
const rootDir = resolveRoot(email, msg.cwdRoot);
const cwd = resolveCwd(rootDir, msg.cwd);
const groupSlug = msg.groupSlug || null;
const sandboxed = msg.sandboxed ?? false;
const cwd = sandboxed
? resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd)
: resolveHostCwd(msg.cwdRoot, msg.cwd);
const groupSlug = msg.groupSlug || null;
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug);
session.sandboxed = sandboxed;
session.userId = userId;
@@ -286,7 +288,7 @@ async function handleChat(
if (!session.piProcess) {
try {
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
session.piProcess = await piBridge.spawnPi(cwd, model, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
session.piProcess = await piBridge.spawnPi(cwd, model, email, onEvent, sandboxed ? { userId, username, email, homeDir } : undefined);
logger.info('Spawned Pi process for session', { sessionId, model, cwd, sandboxed });
} catch (err) {
logger.error('Failed to spawn Pi process', { sessionId, model, error: String(err) });
@@ -355,7 +357,7 @@ async function handleResume(
const homeDir = getHomeDir(email);
const sandbox = session.sandboxed && session.userId ? { userId: session.userId, username: ws.data.username, email, homeDir } : undefined;
const onEvent = createEventHandler(sessionId, session.model, session.cwd, homeDir);
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, onEvent, sandbox);
session.piProcess = await piBridge.spawnPi(session.cwd, session.model, email, onEvent, sandbox);
logger.info('Spawned fresh Pi process for resumed session', { sessionId, model: session.model, sandboxed: session.sandboxed });
} catch (err) {
logger.error('Failed to spawn Pi process for resume', { sessionId, error: String(err) });