mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
feat: Add ACP logout support (#816)
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
ListSessionsResponse,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
LogoutRequest,
|
||||
methods,
|
||||
ndJsonStream,
|
||||
NewSessionRequest,
|
||||
@@ -74,11 +75,13 @@ import {
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
||||
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { Stats } from "node:fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import {
|
||||
applyAskElicitationResponse,
|
||||
@@ -109,6 +112,8 @@ import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./u
|
||||
export const CLAUDE_CONFIG_DIR =
|
||||
process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const MAX_TITLE_LENGTH = 256;
|
||||
|
||||
function sanitizeTitle(text: string): string {
|
||||
@@ -855,6 +860,9 @@ export class ClaudeAcpAgent {
|
||||
http: true,
|
||||
sse: true,
|
||||
},
|
||||
auth: {
|
||||
logout: {},
|
||||
},
|
||||
loadSession: true,
|
||||
sessionCapabilities: {
|
||||
additionalDirectories: {},
|
||||
@@ -992,6 +1000,30 @@ export class ClaudeAcpAgent {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async logout(_params: LogoutRequest): Promise<void> {
|
||||
// Clear in-memory gateway credentials supplied via `authenticate`. The
|
||||
// gateway method never touches the on-disk credential store, so dropping
|
||||
// this reference is the whole logout for that path.
|
||||
this.gatewayAuthRequest = undefined;
|
||||
|
||||
// For the Claude/Console login methods the credentials live in the native
|
||||
// CLI's store (keychain or config dir), which only the binary can clear.
|
||||
// `claude auth logout` is non-interactive and idempotent.
|
||||
const cliPath = await claudeCliPath();
|
||||
try {
|
||||
await execFileAsync(cliPath, ["auth", "logout"]);
|
||||
} catch (error) {
|
||||
const stderr =
|
||||
typeof error === "object" && error && "stderr" in error
|
||||
? String((error as { stderr: unknown }).stderr).trim()
|
||||
: undefined;
|
||||
throw RequestError.internalError(
|
||||
{ stderr: stderr || undefined },
|
||||
`claude auth logout failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
||||
const session = this.sessions[params.sessionId];
|
||||
if (!session) {
|
||||
@@ -4572,6 +4604,7 @@ export function runAcp() {
|
||||
agent.setSessionConfigOption(ctx.params),
|
||||
)
|
||||
.onRequest(methods.agent.authenticate, (ctx) => agent.authenticate(ctx.params))
|
||||
.onRequest(methods.agent.logout, (ctx) => agent.logout(ctx.params))
|
||||
.onRequest(methods.agent.session.prompt, (ctx) =>
|
||||
runPromptWithCancellation(agent, ctx.params, ctx.signal),
|
||||
)
|
||||
|
||||
11
src/index.ts
11
src/index.ts
@@ -4,10 +4,10 @@ import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { claudeCliPath, runAcp } from "./acp-agent.js";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
|
||||
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
||||
console.log(packageJson.version);
|
||||
process.exit(0);
|
||||
} else if (process.argv.includes("--cli")) {
|
||||
// `--cli` is checked first so that `--version`/`-v` (and any other flags) are
|
||||
// forwarded to the wrapped native CLI rather than swallowed by our own version
|
||||
// handler below. Our version flag only applies when not delegating.
|
||||
if (process.argv.includes("--cli")) {
|
||||
const { spawn } = await import("node:child_process");
|
||||
const args = process.argv.slice(2).filter((arg) => arg !== "--cli");
|
||||
const child = spawn(await claudeCliPath(), args, { stdio: "inherit" });
|
||||
@@ -37,6 +37,9 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
||||
console.log(packageJson.version);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Apply env vars from the managed-policy tier before any SDK call so the
|
||||
// SDK subprocess inherits them. Going through resolveSettings (vs. a raw
|
||||
|
||||
@@ -2523,6 +2523,24 @@ describe("stop reason propagation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("logout", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
} as unknown as AcpClient;
|
||||
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
}
|
||||
|
||||
it("advertises the logout capability during initialize", async () => {
|
||||
const agent = createMockAgent();
|
||||
const response = await agent.initialize({
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {},
|
||||
});
|
||||
expect(response.agentCapabilities?.auth?.logout).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("session/close", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
|
||||
Reference in New Issue
Block a user