Add 5 pi extensions: pi-subagents, pi-crew, rpiv-pi, pi-interactive-shell, pi-intercom

This commit is contained in:
2026-05-08 15:59:25 +10:00
parent d0d1d9b045
commit 31b4110c87
457 changed files with 85157 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { getAgentDir } from "@mariozechner/pi-coding-agent";
export type SpawnAgent = "pi" | "codex" | "claude" | "cursor";
export interface SpawnConfig {
defaultAgent: SpawnAgent;
shortcut: string;
commands: Record<SpawnAgent, string>;
defaultArgs: Record<SpawnAgent, string[]>;
worktree: boolean;
worktreeBaseDir?: string;
}
export interface InteractiveShellConfig {
exitAutoCloseDelay: number;
overlayWidthPercent: number;
overlayHeightPercent: number;
focusShortcut: string;
spawn: SpawnConfig;
scrollbackLines: number;
ansiReemit: boolean;
handoffPreviewEnabled: boolean;
handoffPreviewLines: number;
handoffPreviewMaxChars: number;
handoffSnapshotEnabled: boolean;
handoffSnapshotLines: number;
handoffSnapshotMaxChars: number;
transferLines: number;
transferMaxChars: number;
completionNotifyLines: number;
completionNotifyMaxChars: number;
handsFreeUpdateMode: "on-quiet" | "interval";
handsFreeUpdateInterval: number;
handsFreeQuietThreshold: number;
autoExitGracePeriod: number;
handsFreeUpdateMaxChars: number;
handsFreeMaxTotalChars: number;
minQueryIntervalSeconds: number;
}
const DEFAULT_SPAWN_CONFIG: SpawnConfig = {
defaultAgent: "pi",
shortcut: "alt+shift+p",
commands: {
pi: "pi",
codex: "codex",
claude: "claude",
cursor: "agent",
},
defaultArgs: {
pi: [],
codex: [],
claude: [],
cursor: ["--model", "composer-2-fast"],
},
worktree: false,
worktreeBaseDir: undefined,
};
const DEFAULT_CONFIG: InteractiveShellConfig = {
exitAutoCloseDelay: 10,
overlayWidthPercent: 95,
overlayHeightPercent: 60,
focusShortcut: "alt+shift+f",
spawn: DEFAULT_SPAWN_CONFIG,
scrollbackLines: 5000,
ansiReemit: true,
handoffPreviewEnabled: true,
handoffPreviewLines: 30,
handoffPreviewMaxChars: 2000,
handoffSnapshotEnabled: false,
handoffSnapshotLines: 200,
handoffSnapshotMaxChars: 12000,
transferLines: 200,
transferMaxChars: 20000,
completionNotifyLines: 50,
completionNotifyMaxChars: 5000,
handsFreeUpdateMode: "on-quiet",
handsFreeUpdateInterval: 60000,
handsFreeQuietThreshold: 8000,
autoExitGracePeriod: 15000,
handsFreeUpdateMaxChars: 1500,
handsFreeMaxTotalChars: 100000,
minQueryIntervalSeconds: 60,
};
export function loadConfig(cwd: string): InteractiveShellConfig {
const projectPath = join(cwd, ".pi", "interactive-shell.json");
const globalPath = join(getAgentDir(), "interactive-shell.json");
let globalConfig: Partial<InteractiveShellConfig> = {};
let projectConfig: Partial<InteractiveShellConfig> = {};
if (existsSync(globalPath)) {
try {
globalConfig = JSON.parse(readFileSync(globalPath, "utf-8"));
} catch (error) {
console.error(`Warning: Could not parse ${globalPath}:`, error);
}
}
if (existsSync(projectPath)) {
try {
projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
} catch (error) {
console.error(`Warning: Could not parse ${projectPath}:`, error);
}
}
const mergedSpawn = mergeSpawnConfig(globalConfig.spawn, projectConfig.spawn);
const merged = { ...DEFAULT_CONFIG, ...globalConfig, ...projectConfig, spawn: mergedSpawn };
return {
...merged,
exitAutoCloseDelay: clampInt(merged.exitAutoCloseDelay, DEFAULT_CONFIG.exitAutoCloseDelay, 0, 60),
overlayWidthPercent: clampPercent(merged.overlayWidthPercent, DEFAULT_CONFIG.overlayWidthPercent),
overlayHeightPercent: clampInt(merged.overlayHeightPercent, DEFAULT_CONFIG.overlayHeightPercent, 20, 90),
focusShortcut: resolveShortcut(merged.focusShortcut, DEFAULT_CONFIG.focusShortcut),
spawn: mergedSpawn,
scrollbackLines: clampInt(merged.scrollbackLines, DEFAULT_CONFIG.scrollbackLines, 200, 50000),
ansiReemit: merged.ansiReemit !== false,
handoffPreviewEnabled: merged.handoffPreviewEnabled !== false,
handoffPreviewLines: clampInt(merged.handoffPreviewLines, DEFAULT_CONFIG.handoffPreviewLines, 0, 500),
handoffPreviewMaxChars: clampInt(
merged.handoffPreviewMaxChars,
DEFAULT_CONFIG.handoffPreviewMaxChars,
0,
50000,
),
handoffSnapshotEnabled: merged.handoffSnapshotEnabled === true,
handoffSnapshotLines: clampInt(merged.handoffSnapshotLines, DEFAULT_CONFIG.handoffSnapshotLines, 0, 5000),
handoffSnapshotMaxChars: clampInt(
merged.handoffSnapshotMaxChars,
DEFAULT_CONFIG.handoffSnapshotMaxChars,
0,
200000,
),
transferLines: clampInt(merged.transferLines, DEFAULT_CONFIG.transferLines, 10, 1000),
transferMaxChars: clampInt(merged.transferMaxChars, DEFAULT_CONFIG.transferMaxChars, 1000, 100000),
completionNotifyLines: clampInt(merged.completionNotifyLines, DEFAULT_CONFIG.completionNotifyLines, 10, 500),
completionNotifyMaxChars: clampInt(merged.completionNotifyMaxChars, DEFAULT_CONFIG.completionNotifyMaxChars, 1000, 50000),
handsFreeUpdateMode: merged.handsFreeUpdateMode === "interval" ? "interval" : "on-quiet",
handsFreeUpdateInterval: clampInt(
merged.handsFreeUpdateInterval,
DEFAULT_CONFIG.handsFreeUpdateInterval,
5000,
300000,
),
handsFreeQuietThreshold: clampInt(
merged.handsFreeQuietThreshold,
DEFAULT_CONFIG.handsFreeQuietThreshold,
1000,
30000,
),
autoExitGracePeriod: clampInt(
merged.autoExitGracePeriod,
DEFAULT_CONFIG.autoExitGracePeriod,
5000,
120000,
),
handsFreeUpdateMaxChars: clampInt(
merged.handsFreeUpdateMaxChars,
DEFAULT_CONFIG.handsFreeUpdateMaxChars,
500,
50000,
),
handsFreeMaxTotalChars: clampInt(
merged.handsFreeMaxTotalChars,
DEFAULT_CONFIG.handsFreeMaxTotalChars,
10000,
1000000,
),
minQueryIntervalSeconds: clampInt(
merged.minQueryIntervalSeconds,
DEFAULT_CONFIG.minQueryIntervalSeconds,
5,
300,
),
};
}
function mergeSpawnConfig(globalValue: unknown, projectValue: unknown): SpawnConfig {
const globalSpawn = isPlainObject(globalValue) ? globalValue : undefined;
const projectSpawn = isPlainObject(projectValue) ? projectValue : undefined;
const globalCommands = isPlainObject(globalSpawn?.commands) ? globalSpawn.commands : undefined;
const projectCommands = isPlainObject(projectSpawn?.commands) ? projectSpawn.commands : undefined;
const globalArgs = isPlainObject(globalSpawn?.defaultArgs) ? globalSpawn.defaultArgs : undefined;
const projectArgs = isPlainObject(projectSpawn?.defaultArgs) ? projectSpawn.defaultArgs : undefined;
const mergedCommands = {
pi: resolveCommand(projectCommands?.pi ?? globalCommands?.pi, DEFAULT_SPAWN_CONFIG.commands.pi),
codex: resolveCommand(projectCommands?.codex ?? globalCommands?.codex, DEFAULT_SPAWN_CONFIG.commands.codex),
claude: resolveCommand(projectCommands?.claude ?? globalCommands?.claude, DEFAULT_SPAWN_CONFIG.commands.claude),
cursor: resolveCommand(projectCommands?.cursor ?? globalCommands?.cursor, DEFAULT_SPAWN_CONFIG.commands.cursor),
};
const mergedDefaultArgs = {
pi: resolveStringArray(projectArgs?.pi ?? globalArgs?.pi, DEFAULT_SPAWN_CONFIG.defaultArgs.pi),
codex: resolveStringArray(projectArgs?.codex ?? globalArgs?.codex, DEFAULT_SPAWN_CONFIG.defaultArgs.codex),
claude: resolveStringArray(projectArgs?.claude ?? globalArgs?.claude, DEFAULT_SPAWN_CONFIG.defaultArgs.claude),
cursor: resolveStringArray(projectArgs?.cursor ?? globalArgs?.cursor, DEFAULT_SPAWN_CONFIG.defaultArgs.cursor),
};
return {
defaultAgent: resolveSpawnAgent(projectSpawn?.defaultAgent ?? globalSpawn?.defaultAgent, DEFAULT_SPAWN_CONFIG.defaultAgent),
shortcut: resolveShortcut(projectSpawn?.shortcut ?? globalSpawn?.shortcut, DEFAULT_SPAWN_CONFIG.shortcut),
commands: mergedCommands,
defaultArgs: mergedDefaultArgs,
worktree: resolveBoolean(projectSpawn?.worktree ?? globalSpawn?.worktree, DEFAULT_SPAWN_CONFIG.worktree),
worktreeBaseDir: resolveOptionalString(projectSpawn?.worktreeBaseDir ?? globalSpawn?.worktreeBaseDir),
};
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function resolveSpawnAgent(value: unknown, fallback: SpawnAgent): SpawnAgent {
return value === "pi" || value === "codex" || value === "claude" || value === "cursor" ? value : fallback;
}
function resolveCommand(value: unknown, fallback: string): string {
return resolveShortcut(typeof value === "string" ? value : undefined, fallback);
}
function resolveStringArray(value: unknown, fallback: string[]): string[] {
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) return fallback;
return value;
}
function resolveBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function resolveOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function clampPercent(value: number | undefined, fallback: number): number {
if (typeof value !== "number" || Number.isNaN(value)) return fallback;
return Math.min(100, Math.max(10, value));
}
function clampInt(value: number | undefined, fallback: number, min: number, max: number): number {
if (typeof value !== "number" || Number.isNaN(value)) return fallback;
const rounded = Math.trunc(value);
return Math.min(max, Math.max(min, rounded));
}
function resolveShortcut(value: string | undefined, fallback: string): string {
if (typeof value !== "string") return fallback;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : fallback;
}