logancyang_obsidian-copilot/src/agentMode/backends/claude-code/ClaudeCodeBackend.ts
Zero Liu fe25ed9e16 feat(agent-mode): add Claude Code backend and multi-backend session manager
Introduces a second ACP backend (claude-agent-acp) alongside opencode. To
support more than one backend, AgentSessionManager now resolves backends
lazily per session via a registry, and a new AgentModelPreloader probes
each ready backend at startup so the picker can show models for inactive
backends.
2026-04-26 01:47:03 -07:00

68 lines
2.5 KiB
TypeScript

import { getSettings } from "@/settings/model";
import { AcpBackend, AcpSpawnDescriptor } from "@/agentMode/acp/types";
import * as path from "node:path";
/**
* Spawns the user-provided `claude-agent-acp` binary
* (`@agentclientprotocol/claude-agent-acp`). The package wraps the local
* `claude` CLI and exposes it as an ACP server over stdio. Authentication is
* inherited from `~/.claude/` — the user logs in with `claude auth login`
* outside the plugin and we just spawn the adapter; we deliberately do not
* pass `ANTHROPIC_API_KEY` so subscription accounts work transparently.
*
* Unlike OpenCode, Claude Code does not consume Copilot's `activeModels` or
* BYOK keys; its model list comes entirely from the agent's own
* `availableModels` stream (live or preloader-cached).
*/
export class ClaudeCodeBackend implements AcpBackend {
readonly id = "claude-code" as const;
readonly displayName = "Claude Code";
async buildSpawnDescriptor(_ctx: { vaultBasePath: string }): Promise<AcpSpawnDescriptor> {
const binaryPath = getSettings().agentMode?.backends?.["claude-code"]?.binaryPath;
if (!binaryPath) {
throw new Error(
"Claude Code binary path not configured. Open Agent Mode settings and set the path to claude-agent-acp."
);
}
return {
command: binaryPath,
args: [],
env: {
...process.env,
PATH: augmentPathForNodeShebang(binaryPath, process.env.PATH),
},
};
}
}
/**
* macOS GUI apps (Obsidian) inherit a minimal PATH that omits Homebrew and
* common Node installer locations. `claude-agent-acp` is a `#!/usr/bin/env
* node` script, so the spawn fails with `env: node: No such file or
* directory` unless we put `node` on PATH ourselves.
*
* We prepend the directory containing the binary (npm globals install the
* launcher script next to `node`) plus the well-known Homebrew / system
* prefixes, then keep the inherited PATH for everything else.
*/
function augmentPathForNodeShebang(binaryPath: string, inherited: string | undefined): string {
const sep = process.platform === "win32" ? ";" : ":";
const candidates = [
path.dirname(binaryPath),
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
];
const inheritedParts = (inherited ?? "").split(sep).filter(Boolean);
const seen = new Set<string>();
const merged: string[] = [];
for (const p of [...candidates, ...inheritedParts]) {
if (!seen.has(p)) {
seen.add(p);
merged.push(p);
}
}
return merged.join(sep);
}