diff --git a/broker/broker.ts b/broker/broker.ts index a2e1cc5..b57db68 100644 --- a/broker/broker.ts +++ b/broker/broker.ts @@ -1,13 +1,12 @@ import net from "net"; import { writeFileSync, unlinkSync, mkdirSync } from "fs"; import { join } from "path"; -import { homedir } from "os"; import { randomUUID } from "crypto"; import { writeMessage, createMessageReader } from "./framing.js"; -import { getBrokerSocketPath } from "./paths.js"; +import { getBrokerSocketPath, getIntercomDir } from "./paths.js"; import type { SessionInfo, Message, Attachment, BrokerMessage } from "../types.js"; -const INTERCOM_DIR = join(homedir(), ".pi/agent/intercom"); +const INTERCOM_DIR = getIntercomDir(); const SOCKET_PATH = getBrokerSocketPath(); const PID_PATH = join(INTERCOM_DIR, "broker.pid"); diff --git a/broker/paths.test.ts b/broker/paths.test.ts index 049b77c..17bf5a0 100644 --- a/broker/paths.test.ts +++ b/broker/paths.test.ts @@ -1,6 +1,18 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { getBrokerSocketPath } from "./paths.js"; +import { getAgentDir, getBrokerSocketPath, getIntercomDir } from "./paths.js"; + +function withAgentDirEnv(value: string | undefined, fn: () => T): T { + const prev = process.env.PI_CODING_AGENT_DIR; + if (value === undefined) delete process.env.PI_CODING_AGENT_DIR; + else process.env.PI_CODING_AGENT_DIR = value; + try { + return fn(); + } finally { + if (prev === undefined) delete process.env.PI_CODING_AGENT_DIR; + else process.env.PI_CODING_AGENT_DIR = prev; + } +} test("getBrokerSocketPath uses named pipe on Windows", () => { const pipePath = getBrokerSocketPath("win32", "C:/Users/rcroh"); @@ -9,7 +21,29 @@ test("getBrokerSocketPath uses named pipe on Windows", () => { }); test("getBrokerSocketPath uses broker.sock on non-Windows", () => { - const socketPath = getBrokerSocketPath("linux", "/home/rcroh"); + const socketPath = withAgentDirEnv(undefined, () => getBrokerSocketPath("linux", "/home/rcroh")); assert.match(socketPath, /broker\.sock$/); assert.match(socketPath, /rcroh/); }); + +test("getAgentDir falls back to ~/.pi/agent and expands ~", () => { + withAgentDirEnv(undefined, () => { + assert.equal(getAgentDir("/home/rcroh"), "/home/rcroh/.pi/agent"); + }); + withAgentDirEnv("~", () => { + assert.equal(getAgentDir("/home/rcroh"), "/home/rcroh"); + }); + withAgentDirEnv("~/relocated", () => { + assert.equal(getAgentDir("/home/rcroh"), "/home/rcroh/relocated"); + }); + withAgentDirEnv("/abs/agent", () => { + assert.equal(getAgentDir("/home/rcroh"), "/abs/agent"); + }); +}); + +test("getIntercomDir and broker socket honor PI_CODING_AGENT_DIR", () => { + withAgentDirEnv("/abs/agent", () => { + assert.equal(getIntercomDir("/home/rcroh"), "/abs/agent/intercom"); + assert.equal(getBrokerSocketPath("linux", "/home/rcroh"), "/abs/agent/intercom/broker.sock"); + }); +}); diff --git a/broker/paths.ts b/broker/paths.ts index 981e69f..a0707b0 100644 --- a/broker/paths.ts +++ b/broker/paths.ts @@ -8,6 +8,29 @@ function sanitizePipeSegment(value: string): string { .toLowerCase() || "default"; } +function expandAgentDir(value: string, homeDir: string): string { + if (value === "~") return homeDir; + if (value.startsWith("~/")) return join(homeDir, value.slice(2)); + return value; +} + +/** + * Resolve the Pi agent directory. Honors PI_CODING_AGENT_DIR (with `~` / `~/` + * expansion) to stay consistent with pi-subagents' getAgentDir, so a relocated + * agent dir keeps the intercom broker socket, pid, and config co-located with + * the rest of Pi's writable state. Falls back to `~/.pi/agent`. + */ +export function getAgentDir(homeDir: string = homedir()): string { + const configured = process.env.PI_CODING_AGENT_DIR; + if (configured) return expandAgentDir(configured, homeDir); + return join(homeDir, ".pi", "agent"); +} + +/** Resolve the intercom state directory (broker socket/pid/config) under the agent dir. */ +export function getIntercomDir(homeDir: string = homedir()): string { + return join(getAgentDir(homeDir), "intercom"); +} + export function getBrokerSocketPath( platform: NodeJS.Platform = process.platform, homeDir: string = homedir(), @@ -16,5 +39,5 @@ export function getBrokerSocketPath( return `\\\\.\\pipe\\pi-intercom-${sanitizePipeSegment(homeDir)}`; } - return join(homeDir, ".pi/agent/intercom/broker.sock"); + return join(getIntercomDir(homeDir), "broker.sock"); } diff --git a/broker/spawn.ts b/broker/spawn.ts index 24296be..65b63a2 100644 --- a/broker/spawn.ts +++ b/broker/spawn.ts @@ -2,11 +2,10 @@ import { spawn } from "child_process"; import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; -import { homedir } from "os"; import net from "net"; -import { getBrokerSocketPath } from "./paths.js"; +import { getBrokerSocketPath, getIntercomDir } from "./paths.js"; -const INTERCOM_DIR = join(homedir(), ".pi/agent/intercom"); +const INTERCOM_DIR = getIntercomDir(); const EXTENSION_DIR = join(dirname(fileURLToPath(import.meta.url)), ".."); const BROKER_SOCKET = getBrokerSocketPath(); const BROKER_PID = join(INTERCOM_DIR, "broker.pid"); diff --git a/config.ts b/config.ts index 32e3127..fe7f0b8 100644 --- a/config.ts +++ b/config.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from "fs"; import { join } from "path"; -import { homedir } from "os"; +import { getIntercomDir } from "./broker/paths.js"; export interface IntercomConfig { /** Broker command used to spawn the broker process (e.g. "npx" or "bun") */ @@ -22,7 +22,9 @@ export interface IntercomConfig { replyHint: boolean; } -const CONFIG_PATH = join(homedir(), ".pi/agent/intercom/config.json"); +function getConfigPath(): string { + return join(getIntercomDir(), "config.json"); +} const defaults: IntercomConfig = { brokerCommand: "npx", @@ -33,12 +35,13 @@ const defaults: IntercomConfig = { }; export function loadConfig(): IntercomConfig { - if (!existsSync(CONFIG_PATH)) { + const configPath = getConfigPath(); + if (!existsSync(configPath)) { return { ...defaults }; } - + try { - const raw = readFileSync(CONFIG_PATH, "utf-8"); + const raw = readFileSync(configPath, "utf-8"); const parsed: unknown = JSON.parse(raw); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { throw new Error("Config must be a JSON object"); @@ -102,7 +105,7 @@ export function loadConfig(): IntercomConfig { return config; } catch (error) { - console.error(`Failed to load intercom config at ${CONFIG_PATH}:`, error); + console.error(`Failed to load intercom config at ${configPath}:`, error); return { ...defaults }; } }