coding-agent-adapters
CLI adapters for AI coding agents. Works with pty-manager to spawn and manage coding agents like Claude Code, Gemini CLI, OpenAI Codex, Aider, and Hermes Agent.
Each adapter provides source-derived detection patterns for the full session lifecycle: login/auth, blocking prompts, ready state, exit conditions, and auto-response rules — all based on deep analysis of each CLI's open-source codebase.
Installation
npm install coding-agent-adapters pty-manager
Quick Start
import { PTYManager, AdapterRegistry } from 'pty-manager';
import { ClaudeAdapter, GeminiAdapter, AiderAdapter, HermesAdapter } from 'coding-agent-adapters';
const registry = new AdapterRegistry();
registry.register(new ClaudeAdapter());
registry.register(new GeminiAdapter());
registry.register(new AiderAdapter());
registry.register(new HermesAdapter());
const manager = new PTYManager({ adapters: registry });
const session = await manager.spawn({
name: 'code-assistant',
type: 'claude',
workdir: '/path/to/project',
adapterConfig: {
anthropicKey: process.env.ANTHROPIC_API_KEY,
},
});
session.on('output', (data) => console.log(data));
session.send('Help me refactor this function to use async/await');
Available Adapters
ClaudeAdapter | Claude Code | claude | TUI menus | 5 rules | 500ms |
GeminiAdapter | Gemini CLI | gemini | TUI menus | 3 rules | 300ms |
CodexAdapter | OpenAI Codex | codex | TUI menus | 6 rules | 300ms |
AiderAdapter | Aider | aider | Text (Y)es/(N)o | 17 rules | 200ms |
HermesAdapter | Hermes Agent | hermes | TUI prompts | 0 rules | 400ms |
Session Lifecycle Detection
Each adapter implements detection for every stage of a CLI session.
All detection methods use stripAnsi() which strips ANSI escape sequences, cursor positioning codes, bare control characters, TUI spinner/box-drawing characters, and collapses whitespace — ensuring regex patterns match through raw terminal output. Prompt indicators (❯, ›, ◇) are preserved.
Login / Auth Detection
Adapters detect various auth states and methods:
const adapter = new GeminiAdapter();
const login = adapter.detectLogin(output);
| Claude | API key, OAuth browser | CLI runtime |
| Gemini | Google OAuth, API key entry, auth in-progress (ignores "Both keys set" success messages) | AuthDialog.tsx, ApiAuthDialog.tsx, AuthInProgress.tsx |
| Codex | Device code flow, onboarding auth menu | auth.rs, headless_chatgpt_login.rs |
| Aider | API key missing/invalid, OpenRouter OAuth | onboarding.py, models.py |
| Hermes | First-run setup gate (no API keys/providers found, Run setup now?) | hermes_cli/setup.py, hermes_cli/main.py |
Ready State Detection
Each adapter knows exactly what "ready for input" looks like:
| Claude | $ prompt | CLI runtime |
| Gemini | Prompt glyphs (>, !, *, (r:)), composer placeholder | InputPrompt.tsx, Composer.tsx |
| Codex | > glyph, placeholder suggestions | chat_composer.rs |
| Aider | ask>, code>, architect>, help>, multi>, startup banner | io.py, base_coder.py |
| Hermes | Idle ❯ prompt when not working; completion response box | cli.py, agent/display.py |
Ready Settle Delay
Each adapter sets readySettleMs to control how long pty-manager waits after detectReady() matches before emitting session_ready. This prevents the orchestrator from sending input while the TUI is still rendering (status bar, shortcuts, update notices). The base default is 300ms; adapters override based on their rendering weight.
| Claude Code | 500ms | Heaviest TUI — status bar, shortcuts, update notices, /ide suggestions |
| Gemini CLI | 300ms | Moderate Ink TUI (inherits base default) |
| Codex | 300ms | Moderate Rust TUI (inherits base default) |
| Aider | 200ms | Minimal TUI, mostly text output |
| Hermes Agent | 400ms | Prompt-toolkit UI with spinner + activity feed that settles after render |
Blocking Prompt Detection
Adapters detect prompts that block the session and require user action:
| Claude | Permission requests, update notices |
| Gemini | Folder trust, tool execution, validation dialogs, privacy consent |
| Codex | Directory trust, tool approval, update available, model migration, CWD selection |
| Aider | File operations, shell commands, git init, pip install, destructive operations |
| Hermes | Clarify prompts, sudo password prompts, dangerous command approval choices |
Loading / Active-Work Detection
Each adapter implements detectLoading(output) to detect when the CLI is actively processing — thinking spinners, file reading, model streaming. When loading is detected, pty-manager suppresses stall detection entirely, avoiding unnecessary LLM classifier calls during normal operation.
| Claude | esc to interrupt, Reading N files | claude_active_reading_files |
| Gemini | esc to cancel, Waiting for user confirmation | gemini_active_loading_line |
| Codex | esc to interrupt, Booting MCP server, Searching the web | codex_active_status_row, codex_active_booting_mcp |
| Aider | Waiting for LLM/<model>, Generating commit message with | aider_active_waiting_model, aider_active_waiting_llm_default |
| Hermes | Thinking spinner verb + elapsed time (deliberating... (2.4s)), working prompt (⚕ ❯) | HermesAdapter.detectLoading() |
const claude = new ClaudeAdapter();
claude.detectLoading('• Working (5s • esc to interrupt)');
claude.detectLoading('Reading 42 files…');
claude.detectLoading('❯ ');
const aider = new AiderAdapter();
aider.detectLoading('Waiting for claude-sonnet-4-20250514');
aider.detectLoading('code> ');
Task Completion Detection
Each adapter implements detectTaskComplete(output) to recognize when the CLI has finished a task and returned to its idle prompt. This is more specific than detectReady() — it matches high-confidence completion indicators (duration summaries, explicit done messages) that short-circuit the LLM stall classifier in pty-manager. Patterns match through raw ANSI-laden TUI output including spinner characters and cursor positioning codes.
| Claude | Turn duration (Cooked for 3m 12s, custom verb) + ❯ prompt (tolerates trailing status bar) | claude_completed_turn_duration |
| Gemini | ◇ Ready window title, Type your message composer | gemini_ready_title |
| Codex | Worked for 1m 05s separator + › prompt | codex_completed_worked_for_separator, codex_ready_prompt |
| Aider | Aider is waiting for your input, mode prompts (including plain >) with edit/cost markers | aider_completed_llm_response_ready |
| Hermes | Final response box (╭─ ⚕ Hermes ... ╰), or tool-feed line + idle prompt | HermesAdapter.detectTaskComplete() |
const claude = new ClaudeAdapter();
claude.detectTaskComplete('Cooked for 3m 12s\n❯ ');
claude.detectTaskComplete('Reading 5 files…');
const aider = new AiderAdapter();
aider.detectTaskComplete('Applied edit to main.ts\nTokens: 1234\ncode> ');
aider.detectTaskComplete('Waiting for claude-sonnet-4-20250514');
Exit Detection
Adapters detect when a CLI session has ended:
| Claude | Base exit detection |
| Gemini | Folder trust rejection, logout confirmation |
| Codex | Session end, update completion |
| Aider | Ctrl+C / KeyboardInterrupt, version update requiring restart |
| Hermes | Goodbye! ⚕ |
Auto-Response Rules
Adapters include pre-configured rules to automatically handle known prompts. Rules use two response modes depending on the CLI's input style.
These CLIs use arrow-key menus rendered with Ink/Ratatui. Rules send key sequences:
const codex = new CodexAdapter();
codex.autoResponseRules;
Text Prompt CLIs (Aider)
Aider uses plain text (Y)es/(N)o prompts via io.py. Rules send typed text:
const aider = new AiderAdapter();
aider.autoResponseRules;
Adapters declare their input style via usesTuiMenus. This affects how auto-response rules with no explicit responseType are delivered:
usesTuiMenus: true (Gemini, Codex, Claude, Hermes) — defaults to sendKeys('enter')
usesTuiMenus: false (Aider) — defaults to writeRaw(response + '\r')
Model Recommendations
Each adapter provides model recommendations based on available credentials:
const aider = new AiderAdapter();
aider.getRecommendedModels({ anthropicKey: 'sk-ant-...' });
aider.getRecommendedModels({ googleKey: 'AIza...' });
Workspace Files & Memory
Each coding agent CLI has its own convention for project-level memory files (instructions the agent reads on startup) and config files. Adapters expose this knowledge so orchestration systems can write context to the correct files before spawning an agent.
Discovering Workspace Files
import { ClaudeAdapter, AiderAdapter } from 'coding-agent-adapters';
const claude = new ClaudeAdapter();
claude.getWorkspaceFiles();
claude.memoryFilePath;
const aider = new AiderAdapter();
aider.memoryFilePath;
Per-Adapter File Mappings
| Claude | CLAUDE.md | .claude/settings.json | .claude/commands |
| Gemini | GEMINI.md | .gemini/settings.json | .gemini/styles |
| Codex | AGENTS.md | .codex/config.json | codex.md |
| Aider | .aider.conventions.md | .aider.conf.yml | .aiderignore |
| Hermes | AGENTS.md | cli-config.yaml | SOUL.md |
Writing Memory Files
Use writeMemoryFile() to write instructions into a workspace before spawning an agent. Parent directories are created automatically.
const adapter = new ClaudeAdapter();
await adapter.writeMemoryFile('/path/to/workspace', `# Project Context
This is a TypeScript monorepo using pnpm workspaces.
Always run tests before committing.
`);
await adapter.writeMemoryFile('/path/to/workspace', '\n## Additional Rules\nUse snake_case.\n', {
append: true,
});
await adapter.writeMemoryFile('/path/to/workspace', '# Task-Specific Context\n...', {
fileName: 'TASK_CONTEXT.md',
});
Approval Presets
Each coding agent CLI has its own config format for controlling tool permissions. The approval preset system provides 4 named levels that translate to the correct per-CLI config files and CLI flags.
Preset Levels
readonly | Read-only. Safe for auditing. | file_read, planning, user_interaction | — | file_write, shell, web, agent |
standard | Standard dev. Reads + web auto, writes/shell prompt. | file_read, planning, user_interaction, web | file_write, shell, agent | — |
permissive | File ops auto-approved, shell still prompts. | file_read, file_write, planning, user_interaction, web, agent | shell | — |
autonomous | Everything auto-approved. Use with sandbox. | all categories | — | — |
Generating Configs
import { generateApprovalConfig, listPresets, getPresetDefinition } from 'coding-agent-adapters';
const presets = listPresets();
const config = generateApprovalConfig('claude', 'permissive');
generateApprovalConfig('gemini', 'readonly');
generateApprovalConfig('codex', 'autonomous');
generateApprovalConfig('aider', 'permissive');
Using Presets with Adapters
When approvalPreset is set in adapterConfig, the adapter's getArgs() automatically appends the correct CLI flags:
const session = await manager.spawn({
name: 'sandboxed-agent',
type: 'claude',
workdir: '/path/to/project',
adapterConfig: {
anthropicKey: process.env.ANTHROPIC_API_KEY,
approvalPreset: 'autonomous',
},
});
You can also write the config files to a workspace manually using writeApprovalConfig():
const adapter = new ClaudeAdapter();
const writtenFiles = await adapter.writeApprovalConfig('/path/to/workspace', {
adapterConfig: { approvalPreset: 'permissive' },
});
Per-CLI Output
| Claude Code | .claude/settings.json | permissions.allow, permissions.deny, sandbox.* |
| Gemini CLI | .gemini/settings.json | general.defaultApprovalMode, tools.allowed, tools.exclude |
| Codex | .codex/config.json | approval_policy, sandbox_mode, tools.web_search |
| Aider | .aider.conf.yml | yes-always, no-auto-commits |
| Hermes Agent | none (CLI-managed) | approvals currently handled in-session by Hermes safety prompts |
Preflight Check
Before spawning agents, check if the required CLIs are installed:
import { checkAdapters, checkAllAdapters, printMissingAdapters } from 'coding-agent-adapters';
const results = await checkAdapters(['claude', 'aider']);
for (const result of results) {
if (result.installed) {
console.log(`${result.adapter} v${result.version}`);
} else {
console.log(`${result.adapter} - Install: ${result.installCommand}`);
}
}
const allResults = await checkAllAdapters();
await printMissingAdapters(['claude', 'gemini']);
Passing Credentials
You can pass API keys either via environment variables or through adapterConfig:
process.env.ANTHROPIC_API_KEY = 'sk-ant-...';
const session = await manager.spawn({
name: 'claude-agent',
type: 'claude',
workdir: '/project',
});
const session = await manager.spawn({
name: 'claude-agent',
type: 'claude',
workdir: '/project',
adapterConfig: {
anthropicKey: 'sk-ant-...',
openaiKey: 'sk-...',
googleKey: 'AIza...',
},
});
Claude Hook Telemetry (Optional)
Claude Code hooks can emit deterministic lifecycle markers so PTY orchestration does not rely only on screen-text heuristics.
ClaudeAdapter supports this as an opt-in mode:
const session = await manager.spawn({
name: 'claude-agent',
type: 'claude',
workdir: '/project',
adapterConfig: {
anthropicKey: process.env.ANTHROPIC_API_KEY,
claudeHookTelemetry: true,
},
});
When enabled, the adapter sets:
PARALLAX_CLAUDE_HOOK_TELEMETRY=1
PARALLAX_CLAUDE_HOOK_MARKER_PREFIX=PARALLAX_CLAUDE_HOOK (or your override)
Marker Protocol
Hook scripts should print single-line markers:
PARALLAX_CLAUDE_HOOK {"event":"Notification","notification_type":"permission_prompt","message":"..."}
Supported high-value events:
Notification (permission_prompt, elicitation_dialog, idle_prompt)
PreToolUse (includes tool_name)
TaskCompleted
SessionEnd
The adapter consumes these markers in:
detectBlockingPrompt()
detectLoading()
detectToolRunning()
detectTaskComplete()
detectReady()
detectExit()
Generate Hook Config Template
const claude = new ClaudeAdapter();
const protocol = claude.getHookTelemetryProtocol();
settingsHooks is ready to merge into .claude/settings.json under hooks.
Gemini Hook Telemetry (Optional)
Gemini CLI hooks can emit deterministic lifecycle markers so PTY orchestration does not rely only on screen-text heuristics.
GeminiAdapter supports this as an opt-in mode:
const session = await manager.spawn({
name: 'gemini-agent',
type: 'gemini',
workdir: '/project',
adapterConfig: {
googleKey: process.env.GEMINI_API_KEY,
geminiHookTelemetry: true,
},
});
When enabled, the adapter sets:
PARALLAX_GEMINI_HOOK_TELEMETRY=1
PARALLAX_GEMINI_HOOK_MARKER_PREFIX=PARALLAX_GEMINI_HOOK (or your override)
Marker Protocol
Gemini hook command output must be valid JSON. The helper script emits marker lines via systemMessage:
PARALLAX_GEMINI_HOOK {"event":"BeforeTool","tool_name":"run_shell_command"}
Supported high-value events:
Notification (ToolPermission)
BeforeTool (includes tool_name)
AfterAgent
SessionEnd
The adapter consumes these markers in:
detectBlockingPrompt()
detectLoading()
detectToolRunning()
detectTaskComplete()
detectReady()
detectExit()
Generate Hook Config Template
const gemini = new GeminiAdapter();
const protocol = gemini.getHookTelemetryProtocol();
settingsHooks is ready to merge into .gemini/settings.json under hooks.
Creating Custom Adapters
Extend BaseCodingAdapter to create adapters for other coding CLIs:
import { BaseCodingAdapter } from 'coding-agent-adapters';
import type { AgentFileDescriptor, InstallationInfo, ModelRecommendations } from 'coding-agent-adapters';
import type { SpawnConfig, ParsedOutput, LoginDetection, AutoResponseRule } from 'pty-manager';
export class CursorAdapter extends BaseCodingAdapter {
readonly adapterType = 'cursor';
readonly displayName = 'Cursor';
readonly installation: InstallationInfo = {
command: 'npm install -g cursor-cli',
docsUrl: 'https://cursor.sh/docs',
};
override readonly usesTuiMenus = false;
override readonly readySettleMs = 250;
readonly autoResponseRules: AutoResponseRule[] = [
{ pattern: /accept terms/i, type: 'tos', response: 'y', responseType: 'text', description: 'Accept TOS', safe: true, once: true },
];
getWorkspaceFiles(): AgentFileDescriptor[] {
return [
{ relativePath: '.cursor/rules', description: 'Project rules', autoLoaded: true, type: 'memory', format: 'markdown' },
];
}
getRecommendedModels(): ModelRecommendations {
return { powerful: 'claude-sonnet-4', fast: 'gpt-4o-mini' };
}
getCommand(): string { return 'cursor'; }
getArgs(config: SpawnConfig): string[] { return ['--cli']; }
getEnv(config: SpawnConfig): Record<string, string> { return {}; }
detectLogin(output: string): LoginDetection {
if (/login required/i.test(output)) {
return { required: true, type: 'browser' };
}
return { required: false };
}
detectReady(output: string): boolean {
return /cursor>\s*$/m.test(output);
}
detectLoading(output: string): boolean {
return /processing|thinking/i.test(output);
}
detectTaskComplete(output: string): boolean {
return /completed in \d+s/.test(output) && /cursor>\s*$/m.test(output);
}
parseOutput(output: string): ParsedOutput | null {
return { type: 'response', content: output.trim(), isComplete: true, isQuestion: output.includes('?') };
}
getPromptPattern(): RegExp { return /cursor>\s*$/; }
}
Convenience Functions
import { createAllAdapters, createAdapter, ADAPTER_TYPES } from 'coding-agent-adapters';
const allAdapters = createAllAdapters();
const claude = createAdapter('claude');
console.log(Object.keys(ADAPTER_TYPES));
License
MIT