Download src/api/acp-service/acp-service.api.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 4.56 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/api/acp-service/acp-service.api.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/api/acp-service/acp-service.api.ts
-
curl -L -o acp-service.api.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/api/acp-service/acp-service.api.ts
4.56 kB
| import { BashClient } from "@openhands/typescript-client/clients"; | |
| import type { BashOutput } from "@openhands/typescript-client"; | |
| import { getAgentServerClientOptions } from "../agent-server-client-options"; | |
| export type AcpAuthStatus = "authenticated" | "unauthenticated" | "unknown"; | |
| // Hard cap (seconds) for a single detection command. These are fast local | |
| // status checks (no npx download), so 10s is ample for a healthy CLI while | |
| // keeping the onboarding spinner short before falling back to "unknown". | |
| const PROBE_TIMEOUT_SECONDS = 10; | |
| interface AcpAuthProbe { | |
| /** Shell command run on the (local) agent-server host to detect login. */ | |
| command: string; | |
| /** Classify the command's output into an auth status. */ | |
| classify: (out: BashOutput) => AcpAuthStatus; | |
| } | |
| /** Combined stdout+stderr — CLIs are inconsistent about which stream they use | |
| * (e.g. ``codex login status`` writes to stderr). */ | |
| function streams(out: BashOutput): string { | |
| return `${out.stdout ?? ""}\n${out.stderr ?? ""}`; | |
| } | |
| // Claude Code: ``claude auth status --json`` prints {"loggedIn": bool, …}. The | |
| // CLI exits non-zero when logged out, so we read the JSON, not the exit code. | |
| // No parseable ``loggedIn`` (e.g. the CLI isn't installed → "command not | |
| // found", empty stdout) ⇒ unknown, so onboarding shows the API-key fields | |
| // rather than guessing. | |
| function classifyClaude(out: BashOutput): AcpAuthStatus { | |
| let parsed: unknown; | |
| try { | |
| parsed = JSON.parse((out.stdout ?? "").trim()); | |
| } catch { | |
| return "unknown"; | |
| } | |
| const loggedIn = (parsed as { loggedIn?: unknown } | null)?.loggedIn; | |
| if (typeof loggedIn === "boolean") { | |
| return loggedIn ? "authenticated" : "unauthenticated"; | |
| } | |
| return "unknown"; | |
| } | |
| // Codex: ``codex login status`` prints "Logged in using …" / "Not logged in" | |
| // (to stderr), so check both streams. Match "not logged in" first since it | |
| // contains the "logged in" substring. Neither phrase (e.g. CLI missing) ⇒ | |
| // unknown. | |
| function classifyCodex(out: BashOutput): AcpAuthStatus { | |
| const text = streams(out).toLowerCase(); | |
| if (text.includes("not logged in")) return "unauthenticated"; | |
| if (text.includes("logged in")) return "authenticated"; | |
| return "unknown"; | |
| } | |
| // Gemini CLI signs in via Google OAuth and has no status command, so we check | |
| // its credentials file. The command echoes present/absent and exits 0 either | |
| // way; anything else (a shell failure) ⇒ unknown. | |
| function classifyGemini(out: BashOutput): AcpAuthStatus { | |
| // The command echoes exactly `present` / `absent` to stdout, so match trimmed | |
| // stdout exactly. Reading only stdout (not stderr) means a stray shell | |
| // warning can't turn a real result into `unknown`. | |
| const text = (out.stdout ?? "").trim(); | |
| if (text === "present") return "authenticated"; | |
| if (text === "absent") return "unauthenticated"; | |
| return "unknown"; | |
| } | |
| // Per-provider login detection, keyed by ``acp_server`` / OnboardingAgentId. | |
| // Providers absent here (OpenHands, custom, unknown) report ``unknown``. | |
| const ACP_AUTH_PROBES: Record<string, AcpAuthProbe> = { | |
| "claude-code": { | |
| command: "claude auth status --json", | |
| classify: classifyClaude, | |
| }, | |
| codex: { | |
| command: "codex login status", | |
| classify: classifyCodex, | |
| }, | |
| "gemini-cli": { | |
| command: | |
| 'test -f "$HOME/.gemini/oauth_creds.json" && echo present || echo absent', | |
| classify: classifyGemini, | |
| }, | |
| }; | |
| /** | |
| * Detects whether the selected ACP provider is already logged in — entirely | |
| * client-side, with **no dedicated agent-server endpoint**. It runs the | |
| * provider's own status command (or, for Gemini, a credentials-file check) | |
| * through the existing agent-server bash endpoint and classifies the output. | |
| * | |
| * Gated by the caller to **local backends**: the command runs wherever the | |
| * agent-server runs, and on a user's own machine the provider CLIs and | |
| * credential files live at their standard paths. No prompt is sent, so no model | |
| * tokens are spent. A provider that can't be classified — CLI not installed, | |
| * unexpected output, or the bash call failing — comes back as ``unknown`` so | |
| * onboarding falls back to the API-key fields rather than a misleading banner. | |
| */ | |
| class AcpService { | |
| static async getAuthStatus(server: string): Promise<AcpAuthStatus> { | |
| const probe = ACP_AUTH_PROBES[server]; | |
| if (!probe) return "unknown"; | |
| const out = await new BashClient( | |
| getAgentServerClientOptions(), | |
| ).executeCommand(probe.command, undefined, PROBE_TIMEOUT_SECONDS); | |
| return probe.classify(out); | |
| } | |
| } | |
| export default AcpService; | |