/** * claude-code adapter — launches/resumes the `claude` CLI and scans its * transcript store for resumable history. All Sapiom-ness is injected via * flags (settings/mcp-config/system-prompt); nothing here mutates the user's * own `~/.claude` config. */ import type { DoctorCheck, HarnessAdapter, LaunchOpts, PastSessionRecord, SpawnSpec } from "../../shared/types.js"; /** * Minimum `claude` version the harness supports. * * Below this, the binary predates behavior {@link ClaudeCodeAdapter.launch} / * `resume` rely on for EVERY spawn — Auto permission mode at this floor, and * `--plugin-dir` on older releases. A commander-style CLI aborts on an unknown * option or value with a fast `exit 1`, BEFORE it ever runs its SessionStart * hook, so the session dies with no `agentSessionId` and only an opaque exit * code (the pty's stderr is discarded on exit). `doctor()` reports a * below-floor claude as NOT ok so the desktop host installs a current one and * the CLI surfaces an actionable upgrade remedy instead. * * Why 2.1.83: * - Claude's permission-mode documentation identifies 2.1.83 as the first * version that supports Auto mode, which interactive Harness sessions use as * their initial mode. * - The plugin system did not exist before the "Plugin System Released" entry * in `2.0.12`, so no `1.x` or `2.0.0`–`2.0.11` build can recognize * `--plugin-dir` — they reject it outright. * - The changelog itemizes `--plugin-dir` only as modifications from `2.1.74` * onward, and the harness's own `--plugin-dir` skills usage is verified * against `2.1.x` (see core/inject/skills-plugin.ts). `2.1.x` is thus the * earliest range we can GUARANTEE both recognizes the flag and loads our * skills. * This is the SINGLE source of truth — bump it whenever the adapter starts * sending a flag, or relying on behavior, a newer `claude` introduced. */ export declare const MIN_CLAUDE_CODE_VERSION = "2.1.83"; /** * Extract a leading `major.minor.patch` from a `claude --version` line such as * `"2.1.3 (Claude Code)"`. Returns null when no semver is present. */ export declare function parseClaudeVersion(versionLine: string | null | undefined): [number, number, number] | null; /** * Whether a `claude --version` line is at least {@link MIN_CLAUDE_CODE_VERSION}. * * Deliberately surgical: the ONLY case treated as unsupported is a version we * can parse AND that is below the floor. An absent or unparseable version * returns `true` (supported) so a future change to claude's `--version` format * can never mass-reject working installs — the floor exists to catch provably * ancient binaries, not to gate on our own parser's limits. */ export declare function isClaudeVersionSupported(versionLine: string | null | undefined): boolean; /** * Maps a project cwd to the directory name Claude Code uses for its transcript * store under `~/.claude/projects/`. Claude Code applies this encoding before * creating the directory — see its own source for the canonical definition. * * Exported so tests assert against the real encoder rather than a copy of it: * a private duplicate in the test file is exactly the thing that would let * encoder drift pass the tests written to catch it. * * Encodes the path as given. Callers starting from a user-supplied cwd want * {@link projectDirsFor}, which handles the symlink case this encoder can't * see on its own — see its docstring. */ export declare function encodeProjectPath(cwd: string): string; /** * Every directory under `~/.claude/projects` that could hold `cwd`'s * transcripts, realpath-resolved form first. * * Claude Code encodes the cwd's **realpath**, not the path the session was * opened through — so a session in `/tmp/foo` on macOS stores its transcripts * under the encoding of `/private/tmp/foo` (`/tmp` is a symlink), and one in * `os.tmpdir()` under `/private/var/folders/…`. Both shapes are present in a * real `~/.claude` on this machine. Encoding the registry's raw `cwd` string * therefore stats a path that does not exist, which for a resumability probe * is worse than a miss: it reports "the agent has no conversation" for a * conversation the agent has, and refuses a resume that would have worked. * * The raw form is kept as a second candidate (when it differs) so nothing that * resolved before can stop resolving, and a cwd that no longer exists on disk * — realpath fails — still falls back to it. * * Exported for the session-record reader's optional vendor enrichment * (core/session-record.ts), which reads the same transcript files. Claude's * directory layout — symlink handling included — is defined here once. */ export declare function projectDirsFor(homeDir: string, cwd: string): Promise; export interface ClaudeCodeAdapterOptions { /** Overridable for tests (e.g. spawn `bash` instead of a real, auth-gated `claude`). */ binary?: string; /** Host-supplied interpreter/entry script for a managed CLI (e.g. Electron-as-Node). */ binaryArgs?: string[]; binaryEnv?: Record; /** Overridable for tests. Defaults to the real home directory. */ homeDir?: string; /** Overridable for tests. Max transcript size (bytes) to read in full for an * exact turn count; larger files are read only at head+tail. */ fullScanMaxBytes?: number; } /** * Claude Code's known blocking startup screens, matched against stripped-ANSI * scrollback. Exported so the packaged smoke / e2e layers can pin the same * patterns this adapter gates the ready-fallback on (see detectBlockingPrompt). * Wording verified against Claude Code 2.1.x; a future rewording fails SAFE: * an unmatched dialog only means the fallback flips `ready` and the injected * prompt lands in a dialog the user is looking at anyway — the pre-fallback * behaviour was the prompt silently vanishing after ten minutes. */ export declare const CLAUDE_BLOCKING_PROMPT_PATTERNS: readonly RegExp[]; export declare class ClaudeCodeAdapter implements HarnessAdapter { readonly id: "claude-code"; readonly eventSource: "hooks"; /** `--append-system-prompt` carries `systemPromptFile`'s contents on every * launch/resume path below, so a rehydration brief composed into that file * needs no separate delivery. */ readonly systemPromptDelivery: "launch-flag"; /** * See `HarnessAdapter.readyFallback`. The SessionStart hook stays the * primary signal (isReadyEnough gives claude-code NO immediate scrollback * shortcut); this only lets SessionManager's generously-timed fallback * rescue a session whose hook chain is broken — on Windows the hook runs * `node` through Claude's hook shell, and when that resolution fails the * POST never fires, `ready` never flips, and the held first prompt was * silently dropped. */ readonly readyFallback: "hook-timeout"; /** * See `HarnessAdapter.assumesBracketedPaste`. Claude Code's Ink input layer * enables mode 2004 in every interactive session — the macOS/Linux paste * detection observing `ESC[?2004h` on real ptys is the in-repo evidence. * Declared so Windows (where ConPTY hides that announcement) still paste- * wraps multi-line prompts instead of submitting at the first newline. */ readonly assumesBracketedPaste = true; private readonly binary; private readonly binaryArgs; private readonly binaryEnv; private readonly homeDir; private readonly fullScanMaxBytes; constructor(options?: ClaudeCodeAdapterOptions); /** * See `HarnessAdapter.detectBlockingPrompt` and `readyFallback` above: this * exists ONLY to gate the hook-timeout fallback (never type a stray Enter * into an unanswered trust dialog) — it deliberately does NOT give * claude-code Codex's immediate scrollback shortcut, because the hook is * reliable wherever the hook chain itself works. */ detectBlockingPrompt(scrollback: string): boolean; doctor(): Promise; launch(opts: LaunchOpts): SpawnSpec; resume(agentSessionId: string, opts: LaunchOpts): SpawnSpec; /** * Headless one-shot run for TaskManager (see HarnessAdapter.launchTask). * Verified against a real `claude` binary: `-p` carries the exact same * --settings/--mcp-config/--append-system-prompt injection as launch() * (all six hooks fire), skips the trust dialog entirely, and exits on its * own when the turn completes. The extra flags: * - --permission-mode acceptEdits: a headless task has no human to click * through a permission prompt — without it a tool call hangs forever. * - --output-format stream-json --verbose: line-oriented JSON progress on * stdout (parsed by core/task-stream.ts) instead of a bare final answer. * - --model / --max-turns: only when the caller sets them — a bounded task * (canvas enrichment) pins a cheaper model and a hard turn cap instead of * inheriting the user's interactive defaults. */ launchTask(opts: LaunchOpts): SpawnSpec; /** * See `HarnessAdapter.canResume`. One `stat` per candidate project dir on the * transcript `claude --resume ` would read — `/.jsonl` * — sharing `projectDirsFor` with `listPastSessions` so the vendor's * directory layout, symlink handling included, stays defined in one place. * * A zero-byte file counts as absent: the transcript is created before the * first turn is written, so an empty one is precisely the never-prompted * session that `--resume` rejects with "No conversation found". * * Deliberately a weaker test than `listPastSessions`', which additionally * requires a line OUR parser understands. A non-empty transcript we can't * parse is still Claude's to interpret, and refusing the resume on the * strength of our own parser's limits would be the same fail-closed mistake * as the symlink bug above. So this can say `agent-resume` for a transcript * that never shows up as a history row of its own. */ canResume(agentSessionId: string, cwd: string): Promise; listPastSessions(cwd: string): Promise; } export declare function createClaudeCodeAdapter(options?: ClaudeCodeAdapterOptions): HarnessAdapter; //# sourceMappingURL=claude-code.d.ts.map