Skip to content
This repository was archived by the owner on Jun 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 47 additions & 26 deletions .opencode/PAI/Tools/pai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
/**
* pai - Personal AI CLI Tool
*
* Comprehensive CLI for managing Claude Code with dynamic MCP loading,
* Comprehensive CLI for managing OpenCode with dynamic MCP loading,
* updates, version checking, and profile management.
*
* Usage:
* pai Launch Claude (default profile)
* pai Launch OpenCode (default profile)
* pai -m bd Launch with Bright Data MCP
* pai -m bd,ap Launch with multiple MCPs
* pai -r / --resume Resume last session
* pai --local Stay in current directory (don't cd to ~/.opencode)
* pai update Update Claude Code
* pai update Update OpenCode
* pai version Show version info
* pai profiles List available profiles
* pai mcp list List available MCPs
Expand All @@ -29,12 +29,14 @@ import { join, basename } from "path";
// ============================================================================

const OPENCODE_DIR = process.env.OPENCODE_DIR || join(homedir(), ".opencode");
const LOCAL_OPENCODE_BIN = join(homedir(), ".local", "bin", "opencode");
const COMPAT_OPENCODE_BIN = join(OPENCODE_DIR, "tools", "opencode");
const MCP_DIR = join(OPENCODE_DIR, "MCPs");
const ACTIVE_MCP = join(OPENCODE_DIR, ".mcp.json");
const BANNER_SCRIPT = join(OPENCODE_DIR, "PAI", "Tools", "Banner.ts");
const VOICE_SERVER = "http://localhost:8888/notify/personality";
const WALLPAPER_DIR = join(homedir(), "Projects", "Wallpaper");
// Note: RAW archiving removed - Claude Code handles its own cleanup (30-day retention in projects/)
// Note: RAW archiving removed - OpenCode handles its own cleanup (30-day retention in projects/)

// MCP shorthand mappings
const MCP_SHORTCUTS: Record<string, string> = {
Expand Down Expand Up @@ -127,8 +129,25 @@ function displayBanner() {
}
}

function resolveOpenCodeExecutable(): string {
if (existsSync(LOCAL_OPENCODE_BIN)) {
return LOCAL_OPENCODE_BIN;
}

if (existsSync(COMPAT_OPENCODE_BIN)) {
return COMPAT_OPENCODE_BIN;
}

const opencodeOnPath = spawnSync(["opencode", "--version"]);
if (opencodeOnPath.exitCode === 0) {
return "opencode";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

error("OpenCode executable not found. Re-run installer or ensure ~/.local/bin is in PATH.");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getCurrentVersion(): string | null {
const result = spawnSync(["claude", "--version"]);
const result = spawnSync([resolveOpenCodeExecutable(), "--version"]);
const output = result.stdout.toString();
const match = output.match(/([0-9]+\.[0-9]+\.[0-9]+)/);
return match ? match[1] : null;
Expand All @@ -146,12 +165,10 @@ function compareVersions(a: string, b: string): number {

async function getLatestVersion(): Promise<string | null> {
try {
const response = await fetch(
"https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest"
);
const version = (await response.text()).trim();
if (/^[0-9]+\.[0-9]+\.[0-9]+/.test(version)) {
return version;
const response = await fetch("https://registry.npmjs.org/opencode-ai/latest");
const data = (await response.json()) as { version?: string };
if (data?.version && /^[0-9]+\.[0-9]+\.[0-9]+/.test(data.version)) {
return data.version;
}
} catch {
return null;
Expand Down Expand Up @@ -229,7 +246,7 @@ function setMcpProfile(profile: string) {
// Create symlink
symlinkSync(profileFile, ACTIVE_MCP);
log(`Switched to '${profile}' profile`, "✅");
log("Restart Claude Code to apply", "⚠️");
log("Restart OpenCode to apply", "⚠️");
}

function setMcpCustom(mcpNames: string[]) {
Expand Down Expand Up @@ -399,7 +416,7 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; skipPerms?:
// (InstantiatePAI.ts is retired — kept for reference only)

displayBanner();
const args = ["claude"];
const args = [resolveOpenCodeExecutable()];

// Handle MCP configuration
if (options.mcp) {
Expand All @@ -423,13 +440,13 @@ async function cmdLaunch(options: { mcp?: string; resume?: boolean; skipPerms?:
// Voice notification (using focused marker for calmer tone)
notifyVoice(`[🎯 focused] ${getDAName()} here, ready to go.`);

// Launch Claude
// Launch OpenCode
const proc = spawn(args, {
stdio: ["inherit", "inherit", "inherit"],
env: { ...process.env },
});

// Wait for Claude to exit
// Wait for OpenCode to exit
await proc.exited;
}

Expand All @@ -454,7 +471,7 @@ async function cmdUpdate() {
return;
}

log("Updating Claude Code...", "🔄");
log("Updating OpenCode...", "🔄");

// Step 1: Update Bun
log("Step 1/2: Updating Bun...", "📦");
Expand All @@ -465,13 +482,17 @@ async function cmdUpdate() {
log("Bun updated", "✅");
}

// Step 2: Update Claude Code
log("Step 2/2: Installing latest Claude Code...", "🤖");
const claudeResult = spawnSync(["bash", "-c", "curl -fsSL https://claude.ai/install.sh | bash"]);
if (claudeResult.exitCode !== 0) {
error("Claude Code installation failed");
// Step 2: Update OpenCode
log("Step 2/2: Upgrading OpenCode...", "🤖");
const opencodeResult = spawnSync([resolveOpenCodeExecutable(), "upgrade"], {
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
if (opencodeResult.exitCode !== 0) {
error("OpenCode upgrade failed");
}
log("Claude Code updated", "✅");
log("OpenCode updated", "✅");

// Show final version
const newVersion = getCurrentVersion();
Expand Down Expand Up @@ -557,7 +578,7 @@ function cmdMcpList() {
async function cmdPrompt(prompt: string) {
// One-shot prompt execution
// NOTE: No --dangerously-skip-permissions - rely on settings.json permissions
const args = ["claude", "-p", prompt];
const args = [resolveOpenCodeExecutable(), "-p", prompt];

process.chdir(OPENCODE_DIR);

Expand All @@ -575,14 +596,14 @@ function cmdHelp() {
pai - Personal AI CLI Tool (v2.0.0)

USAGE:
k Launch Claude (no MCPs, max performance)
k Launch OpenCode (no MCPs, max performance)
k -m <mcp> Launch with specific MCP(s)
k -m bd,ap Launch with multiple MCPs
k -r, --resume Resume last session
k -l, --local Stay in current directory (don't cd to ~/.opencode)

COMMANDS:
k update Update Claude Code to latest version
k update Update runtime dependencies
k version, -v Show version information
k profiles List available MCP profiles
k mcp list List all available MCPs
Expand All @@ -609,7 +630,7 @@ EXAMPLES:
k -m bd,ap,chrome Start with multiple MCPs
k -r Resume last session
k mcp set research Switch to research profile
k update Update Claude Code
k update Update OpenCode
k prompt "What time is it?" One-shot prompt
k -w List available wallpapers
k -w circuit-board Switch wallpaper (Kitty + macOS)
Expand Down
16 changes: 9 additions & 7 deletions PAI-Install/engine/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,11 +790,11 @@ export async function runConfiguration(
const isFish = userShell.includes("fish");
const rcFile = userShell.includes("bash") ? ".bashrc" : isFish ? ".config/fish/config.fish" : ".zshrc";
const rcPath = join(homedir(), rcFile);
// Fish uses function/end; bash/zsh use function wrapper to ensure cwd safety
// Fish uses function/end; bash/zsh use function wrappers.
const aliasLine = isFish
? `function pai\n\tset -l __pai_oldpwd (pwd)\n\tcd "${paiDir}"\n\tand bun run .opencode/PAI/Tools/pai.ts $argv\n\tcd $__pai_oldpwd\nend`
: `pai() { (cd "${paiDir}" && bun run .opencode/PAI/Tools/pai.ts "$@"); }`;
const marker = "# PAI alias";
? `set -gx PATH $HOME/.bun/bin $HOME/.local/bin $PATH\n\nfunction opencode\n\tif test -x \"$HOME/.local/bin/opencode\"\n\t\t$HOME/.local/bin/opencode $argv\n\telse if test -x \"$HOME/.opencode/tools/opencode\"\n\t\t$HOME/.opencode/tools/opencode $argv\n\telse\n\t\tcommand opencode $argv\n\tend\nend\n\nfunction pai\n\tset -l __pai_oldpwd (pwd)\n\tset -l __pai_bun \"$HOME/.bun/bin/bun\"\n\tcd "${paiDir}"\n\tif test -x $__pai_bun\n\t\t$__pai_bun run .opencode/PAI/Tools/pai.ts $argv\n\telse\n\t\tbun run .opencode/PAI/Tools/pai.ts $argv\n\tend\n\tcd $__pai_oldpwd\nend`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
: `export PATH=\"$HOME/.bun/bin:$HOME/.local/bin:$PATH\"\n\nopencode() {\n if [ -x \"$HOME/.local/bin/opencode\" ]; then\n \"$HOME/.local/bin/opencode\" \"$@\"\n elif [ -x \"$HOME/.opencode/tools/opencode\" ]; then\n \"$HOME/.opencode/tools/opencode\" \"$@\"\n else\n command opencode \"$@\"\n fi\n}\n\npai() {\n (cd "${paiDir}" &&\n if [ -x \"$HOME/.bun/bin/bun\" ]; then\n \"$HOME/.bun/bin/bun\" run .opencode/PAI/Tools/pai.ts \"$@\"\n else\n bun run .opencode/PAI/Tools/pai.ts \"$@\"\n fi\n )\n}`;
const marker = "# PAI shell setup";

if (isFish) {
// Ensure fish config directory exists
Expand All @@ -804,17 +804,19 @@ export async function runConfiguration(
if (existsSync(rcPath)) {
let content = readFileSync(rcPath, "utf-8");
// Remove any existing pai alias/function (old CORE/PAI paths, legacy markers)
content = content.replace(/\n?# PAI alias — added by PAI installer[\s\S]*?(?=\n#|\n$|$)/g, "");
content = content.replace(/\n?# PAI (?:alias|shell setup)(?: — added by PAI installer)?[\s\S]*?(?=\n#|\n$|$)/g, "");
content = content.replace(/^#\s*(?:PAI|CORE)\s*alias.*\n.*alias pai=.*\n?/gm, "");
content = content.replace(/^alias pai=.*\n?/gm, "");
content = content.replace(/^alias pai\s+.*\n?/gm, ""); // Fish syntax
content = content.replace(/^pai\(\)\s*\{[\s\S]*?^\}\s*\n?/gm, "");
content = content.replace(/^opencode\(\)\s*\{[\s\S]*?^\}\s*\n?/gm, "");
content = content.replace(/^function pai\s*$[\s\S]*?^end\s*\n?/gm, "");
content = content.replace(/^function opencode\s*$[\s\S]*?^end\s*\n?/gm, "");
// Add fresh alias
content = content.trimEnd() + `\n\n${marker}\n${aliasLine}\n`;
content = content.trimEnd() + `\n\n${marker} — added by PAI installer\n${aliasLine}\n`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
writeFileSync(rcPath, content);
} else {
writeFileSync(rcPath, `${marker}\n${aliasLine}\n`);
writeFileSync(rcPath, `${marker} — added by PAI installer\n${aliasLine}\n`);
}

// Fix permissions - only make scripts executable, not everything
Expand Down
8 changes: 5 additions & 3 deletions PAI-Install/engine/steps-fresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,8 @@ ${providerEnvVar}=${state.collected.apiKey || ""}
// POSIX shells use "$@"; fish uses $argv and function/end syntax.
const escapedInstallDir = installDir.replaceAll('"', '\\"');
const aliasBlock = shellName === "fish"
? `\n# PAI alias — added by PAI installer\nfunction pai\n\tset -l __pai_oldpwd (pwd)\n\tcd "${escapedInstallDir}"\n\tand bun run .opencode/PAI/Tools/pai.ts $argv\n\tcd $__pai_oldpwd\nend\n`
: `\n# PAI alias — added by PAI installer\npai() { (cd "${escapedInstallDir}" && bun run .opencode/PAI/Tools/pai.ts "$@"); }\n`;
? `\n# PAI shell setup — added by PAI installer\nset -gx PATH $HOME/.bun/bin $HOME/.local/bin $PATH\n\nfunction opencode\n\tif test -x "$HOME/.local/bin/opencode"\n\t\t$HOME/.local/bin/opencode $argv\n\telse if test -x "$HOME/.opencode/tools/opencode"\n\t\t$HOME/.opencode/tools/opencode $argv\n\telse\n\t\tcommand opencode $argv\n\tend\nend\n\nfunction pai\n\tset -l __pai_oldpwd (pwd)\n\tset -l __pai_bun "$HOME/.bun/bin/bun"\n\tcd "${escapedInstallDir}"\n\tif test -x $__pai_bun\n\t\t$__pai_bun run .opencode/PAI/Tools/pai.ts $argv\n\telse\n\t\tbun run .opencode/PAI/Tools/pai.ts $argv\n\tend\n\tcd $__pai_oldpwd\nend\n`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
: `\n# PAI shell setup — added by PAI installer\nexport PATH="$HOME/.bun/bin:$HOME/.local/bin:$PATH"\n\nopencode() {\n if [ -x "$HOME/.local/bin/opencode" ]; then\n "$HOME/.local/bin/opencode" "$@"\n elif [ -x "$HOME/.opencode/tools/opencode" ]; then\n "$HOME/.opencode/tools/opencode" "$@"\n else\n command opencode "$@"\n fi\n}\n\npai() {\n (cd "${escapedInstallDir}" &&\n if [ -x "$HOME/.bun/bin/bun" ]; then\n "$HOME/.bun/bin/bun" run .opencode/PAI/Tools/pai.ts "$@"\n else\n bun run .opencode/PAI/Tools/pai.ts "$@"\n fi\n )\n}\n`;

try {
// Ensure parent directory exists (matters for fish: ~/.config/fish/ may be absent)
Expand All @@ -440,9 +440,11 @@ ${providerEnvVar}=${state.collected.apiKey || ""}
: "";

let content = existing;
content = content.replace(/\n?# PAI alias — added by PAI installer[\s\S]*?(?=\n#|\n$|$)/g, "");
content = content.replace(/\n?# PAI (?:alias|shell setup) — added by PAI installer[\s\S]*?(?=\n#|\n$|$)/g, "");
content = content.replace(/^pai\(\)\s*\{[\s\S]*?^\}\s*\n?/gm, "");
content = content.replace(/^opencode\(\)\s*\{[\s\S]*?^\}\s*\n?/gm, "");
content = content.replace(/^function pai\s*$[\s\S]*?^end\s*\n?/gm, "");
content = content.replace(/^function opencode\s*$[\s\S]*?^end\s*\n?/gm, "");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
content = content.replace(/^alias pai=.*\n?/gm, "");
content = content.replace(/^alias pai\s+.*\n?/gm, "");

Expand Down
33 changes: 31 additions & 2 deletions PAI-Install/engine/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,15 @@ export async function runValidation(state: InstallState): Promise<ValidationChec

let aliasConfigured = false;
let aliasSource = "";
let shellPathSetup = false;
let shellPathSource = "";

for (const shell of shellConfigs) {
if (existsSync(shell.path)) {
try {
const content = readFileSync(shell.path, "utf-8");
// Check for PAI alias marker
if (!content.includes("# PAI alias")) continue;
if (!content.includes("# PAI shell setup")) continue;

// POSIX syntax: alias pai=... or pai() { ... }
const hasPosixAlias = content.includes("alias pai=");
Expand All @@ -235,10 +237,16 @@ export async function runValidation(state: InstallState): Promise<ValidationChec
const hasLegacyBrokenPath = content.includes(".opencode/tools/pai.ts");
const hasValidPaiPath = content.includes(".opencode/PAI/Tools/pai.ts") || content.includes("PAI/Tools/pai.ts");

const hasPathSetup = content.includes("$HOME/.bun/bin") && content.includes("$HOME/.local/bin");
if (hasPathSetup && !shellPathSetup) {
shellPathSetup = true;
shellPathSource = shell.name;
}

if (!hasLegacyBrokenPath && hasValidPaiPath && (hasPosixAlias || hasPosixFunction || hasFishAlias || hasFishFunction)) {
aliasConfigured = true;
aliasSource = shell.name;
break;
if (shellPathSetup) break;
}
} catch {
// Continue to next shell
Expand All @@ -255,6 +263,27 @@ export async function runValidation(state: InstallState): Promise<ValidationChec
critical: true,
});

// 9. Shell PATH wiring for bun and local opencode binary
checks.push({
name: "Shell PATH wiring",
passed: shellPathSetup,
detail: shellPathSetup
? `Configured in ${shellPathSource}`
: "Missing ~/.bun/bin and ~/.local/bin path wiring in shell config",
critical: true,
});

// 10. Bun runtime binary present
const bunBinaryPath = join(homedir(), ".bun", "bin", "bun");
checks.push({
name: "Bun runtime",
passed: existsSync(bunBinaryPath),
detail: existsSync(bunBinaryPath)
? `Found at ${bunBinaryPath}`
: "Missing ~/.bun/bin/bun",
critical: true,
});

return checks;
}

Expand Down
Loading