Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 9 additions & 5 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ function resolveExecutableOnPath(command: string, env: NodeJS.ProcessEnv = proce
if (!trimmed) return null;
const lookup = process.platform === "win32"
? { command: "where.exe", args: [trimmed] }
: { command: env.SHELL?.trim() || "/bin/sh", args: ["-lc", `command -v ${shellEscapeArg(trimmed)}`] };
// `-l` lets a user's shell profile replace PATH (notably on macOS, where
// zsh can restore Homebrew's PATH ahead of a lane-provided executable).
// Resolution must honor the environment we pass to the PTY, so use a
// non-login shell and let the caller supply the already-resolved PATH.
: { command: env.SHELL?.trim() || "/bin/sh", args: ["-c", `command -v ${shellEscapeArg(trimmed)}`] };
const result = spawnSync(lookup.command, lookup.args, {
encoding: "utf8",
env,
Expand Down Expand Up @@ -327,7 +331,7 @@ const TOOL_SPECS: ToolSpec[] = [
additionalProperties: false,
properties: {
laneId: { type: "string", minLength: 1 },
provider: { type: "string", enum: ["claude", "codex", "cursor", "droid", "opencode", "shell"] },
provider: { type: "string", enum: ["claude", "codex", "cursor", "droid", "opencode", "pi", "shell"] },
permissionMode: { type: "string", enum: ["default", "auto", "plan", "edit", "full-auto", "config-toml"], default: "default" },
title: { type: "string" },
initialInput: { type: "string" },
Expand Down Expand Up @@ -1693,7 +1697,7 @@ function parseCliSessionProvider(value: unknown): LaunchProfile {
if (!isLaunchProfile(provider)) {
throw new JsonRpcError(
JsonRpcErrorCode.invalidParams,
"provider must be one of claude, codex, cursor, droid, opencode, or shell",
"provider must be one of claude, codex, cursor, droid, opencode, pi, or shell",
);
}
return provider;
Expand Down Expand Up @@ -2773,7 +2777,7 @@ function scopeBuiltInBrowserAdeActionArgs(
}

const EXTERNAL_SESSION_AUTH_FIND_LIMIT = 500;
const EXTERNAL_SESSION_PROVIDER_NAMES = new Set<string>(["claude", "codex", "cursor", "droid", "opencode"]);
const EXTERNAL_SESSION_PROVIDER_NAMES = new Set<string>(["claude", "codex", "cursor", "droid", "opencode", "pi"]);

function isExternalSessionProviderName(value: string | null): value is ExternalSessionProvider {
return Boolean(value && EXTERNAL_SESSION_PROVIDER_NAMES.has(value));
Expand Down Expand Up @@ -2870,7 +2874,7 @@ function scopeExternalSessionsListArgs(

function externalSessionImportUsesSourceRunCwd(provider: ExternalSessionProvider, mode: string): boolean {
if (mode === "resume") return provider !== "codex";
if (mode === "fork") return provider === "opencode";
if (mode === "fork") return provider === "opencode" || provider === "pi";
return false;
}

Expand Down
9 changes: 9 additions & 0 deletions apps/ade-cli/src/services/agentRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ describe("classifyAgentCliError", () => {
});
});

it("recognizes Pi provider credential failures and keeps its native login command", () => {
expect(classifyAgentCliError("No API key found for openai", "pi")).toMatchObject({
agent: "pi",
displayName: "Pi",
category: "unauthenticated",
authCommand: "pi",
});
});

it("provides Factory Droid install and interactive authentication recovery", () => {
expect(classifyAgentCliError("spawn droid ENOENT")).toMatchObject({
agent: "droid",
Expand Down
16 changes: 16 additions & 0 deletions apps/ade-cli/src/services/agentRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ export const AGENT_CLI_REGISTRY: AgentCliDescriptor[] = [
/\bcursor(?:-agent)?\b.*\b(not logged in|not authenticated|unauthorized|authentication failed|login required)\b/i,
],
},
{
agent: "pi",
displayName: "Pi",
binaryNames: ["pi"],
installCommand: npmGlobalInstallCommand("@earendil-works/pi-coding-agent"),
authCommand: "pi",
missingErrorPatterns: [
/\bpi\b.*\b(command not found|not recognized|not found|enoent)\b/i,
/\bspawn\s+pi\s+enoent\b/i,
],
notAuthErrorPatterns: [
/\bpi\b.*\b(not logged in|not authenticated|unauthorized|authentication failed|login required|authentication required|no api key|api key required|no credentials|provider not configured)\b/i,
/\b(?:no api key|api key required|no credentials|provider not configured)\b.*\b(?:for|pi|provider)\b/i,
/\brun\s+[`'"]?pi\s+\/login[`'"]?/i,
],
},
{
agent: "droid",
displayName: "Factory Droid",
Expand Down
14 changes: 13 additions & 1 deletion apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ const EXTERNAL_SESSION_PROVIDERS = new Set<ExternalSessionProvider>([
"cursor",
"droid",
"opencode",
"pi",
]);

type SyncRemoteCommandServiceArgs = {
Expand Down Expand Up @@ -1259,6 +1260,11 @@ async function summarizeChatSessionForRemote(
...(session.codexSandbox ? { codexSandbox: session.codexSandbox } : {}),
...(session.codexConfigSource ? { codexConfigSource: session.codexConfigSource } : {}),
...(session.opencodePermissionMode ? { opencodePermissionMode: session.opencodePermissionMode } : {}),
...(session.piProfileId ? { piProfileId: session.piProfileId } : {}),
...(session.piProviderId ? { piProviderId: session.piProviderId } : {}),
...(session.piModelId ? { piModelId: session.piModelId } : {}),
...(session.piSessionId ? { piSessionId: session.piSessionId } : {}),
...(session.piSessionFile ? { piSessionFile: session.piSessionFile } : {}),
...(session.droidPermissionMode ? { droidPermissionMode: session.droidPermissionMode } : {}),
...(session.cursorModeSnapshot ? { cursorModeSnapshot: session.cursorModeSnapshot } : {}),
...(session.cursorModeId !== undefined ? { cursorModeId: session.cursorModeId } : {}),
Expand Down Expand Up @@ -2375,6 +2381,11 @@ function parseAgentChatCreateArgs(value: Record<string, unknown>): AgentChatCrea
parsed.fastMode = asOptionalBoolean(value.fastMode) ?? asOptionalBoolean(value.codexFastMode);
}
if ("opencodePermissionMode" in value) parsed.opencodePermissionMode = value.opencodePermissionMode == null ? undefined : asTrimmedString(value.opencodePermissionMode) as AgentChatCreateArgs["opencodePermissionMode"];
if ("piProfileId" in value) parsed.piProfileId = value.piProfileId == null ? null : asTrimmedString(value.piProfileId) ?? null;
if ("piProviderId" in value) parsed.piProviderId = value.piProviderId == null ? null : asTrimmedString(value.piProviderId) ?? null;
if ("piModelId" in value) parsed.piModelId = value.piModelId == null ? null : asTrimmedString(value.piModelId) ?? null;
if ("piSessionId" in value) parsed.piSessionId = value.piSessionId == null ? null : asTrimmedString(value.piSessionId) ?? null;
if ("piSessionFile" in value) parsed.piSessionFile = value.piSessionFile == null ? null : asTrimmedString(value.piSessionFile) ?? null;
if ("droidPermissionMode" in value) parsed.droidPermissionMode = value.droidPermissionMode == null ? undefined : (asTrimmedString(value.droidPermissionMode) ?? undefined) as AgentChatCreateArgs["droidPermissionMode"];
if ("cursorModeId" in value) parsed.cursorModeId = value.cursorModeId == null ? null : asTrimmedString(value.cursorModeId) ?? null;
if ("cursorConfigValues" in value) parsed.cursorConfigValues = parseCursorConfigValues(value.cursorConfigValues);
Expand Down Expand Up @@ -2971,6 +2982,7 @@ function parseChatModelCatalogArgs(value: Record<string, unknown>): AgentChatMod
...(mode === "cached" || mode === "refresh-stale" || mode === "force" ? { mode } : {}),
...(
refreshProvider === "opencode"
|| refreshProvider === "pi"
|| refreshProvider === "cursor"
|| refreshProvider === "droid"
|| refreshProvider === "lmstudio"
Expand Down Expand Up @@ -3554,7 +3566,7 @@ async function resolveChatCreateArgs<T extends AgentChatCreateArgs>(
if (payload.model.trim().length > 0) return payload;
const available = await service.getAvailableModels({
provider: payload.provider,
...(payload.provider === "opencode" ? { activateRuntime: true } : {}),
...(payload.provider === "opencode" || payload.provider === "pi" ? { activateRuntime: true } : {}),
});
const chosen = available[0];
if (!chosen) {
Expand Down
9 changes: 6 additions & 3 deletions apps/ade-cli/src/tuiClient/adeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ const CHAT_BACKED_TERMINAL_TOOL_TYPES = new Set([
"opencode-chat",
"cursor",
"droid-chat",
"pi-chat",
]);

const TRACKED_CLI_PROVIDERS = new Set<AdeCodeProvider>([
Expand All @@ -380,6 +381,7 @@ const TRACKED_CLI_PROVIDERS = new Set<AdeCodeProvider>([
"cursor",
"droid",
"opencode",
"pi",
]);

/**
Expand All @@ -400,6 +402,7 @@ export function trackedCliTerminalProvider(session: ChatTerminalSession): AdeCod
if (toolType.startsWith("cursor")) return "cursor";
if (toolType.startsWith("droid")) return "droid";
if (toolType.startsWith("opencode")) return "opencode";
if (toolType.startsWith("pi")) return "pi";
if (toolType.startsWith("claude")) return "claude";
const resumeCommand = typeof session.resumeCommand === "string" ? session.resumeCommand.trim().toLowerCase() : "";
return resumeCommand && /\bclaude\b/.test(resumeCommand) ? "claude" : null;
Expand Down Expand Up @@ -454,8 +457,8 @@ export async function signalTerminal(
await connection.action("terminal", "signal", { terminalId, signal });
}

/** The five provider CLIs the TUI can launch as a tracked terminal session. */
export type CliTerminalProvider = Extract<AdeCodeProvider, "claude" | "codex" | "cursor" | "droid" | "opencode">;
/** Provider CLIs the TUI can launch as tracked terminal sessions. */
export type CliTerminalProvider = Extract<AdeCodeProvider, "claude" | "codex" | "cursor" | "droid" | "opencode" | "pi">;

export type StartCliTerminalSessionResult = {
provider: string;
Expand Down Expand Up @@ -686,7 +689,7 @@ export async function getAvailableModels(
// IDs such as `claude-opus-4-6-fast`, not a separate service-tier toggle.
// Codex is intentionally NOT here: its tiers come from the app-server, which
// loadAvailableModels always queries regardless of activateRuntime.
activateRuntime: provider === "cursor" || provider === "droid",
activateRuntime: provider === "cursor" || provider === "droid" || provider === "pi",
...(provider === "cursor" ? { cursorSource } : {}),
});
}
Expand Down
Loading
Loading