Context
RemoteClaw runs AI agent CLIs (Claude, Gemini, Codex, OpenCode) as subprocesses and parses their NDJSON streaming output into normalized AgentEvent objects. All four CLI runtimes share ~80% of their logic: spawn a subprocess, parse NDJSON lines from stdout, capture stderr, handle abort signals, manage watchdog timers.
CLIRuntimeBase is the abstract base class that encapsulates this shared subprocess machinery. Each concrete runtime (Claude, Gemini, Codex, OpenCode) extends it and only implements the CLI-specific parts: argument construction, event extraction, and environment setup.
Depends on: #3 (AgentRuntime interface — merged)
Architecture
AgentRuntime (interface, from types.ts)
^
| implements
|
CLIRuntimeBase (abstract class — this issue)
^
| extends
|
├── ClaudeCliRuntime (future issue)
├── GeminiCliRuntime (future issue)
├── CodexCliRuntime (future issue)
└── OpenCodeCliRuntime (future issue)
CLIRuntimeBase handles:
- Subprocess spawning via
child_process.spawn()
- NDJSON line parsing from stdout
- Async event queue (yields
AgentEvent items)
- Watchdog timer (configurable timeout)
- Abort signal propagation to child process
- Stderr capture for error classification
- Stdin delivery for long prompts
Subclasses handle:
buildArgs(params) — construct CLI-specific arguments
extractEvent(line) — parse provider-specific NDJSON into AgentEvent
buildEnv(params) — construct provider-specific environment variables
File Location
src/middleware/cli-runtime-base.ts (~150 lines estimated)
Implementation Details
Abstract Methods
/** Construct CLI-specific command-line arguments. */
protected abstract buildArgs(params: AgentExecuteParams): string[];
/** Parse a single NDJSON line into an AgentEvent (or null to skip). */
protected abstract extractEvent(line: string): AgentEvent | null;
/** Construct provider-specific environment variables. */
protected abstract buildEnv(params: AgentExecuteParams): Record<string, string>;
Subprocess Spawning
- Use
child_process.spawn() (not exec) for streaming output
- Set
cwd from params.workingDirectory
- Merge
buildEnv(params) with process.env
stdio: ["pipe", "pipe", "pipe"] — stdin for prompts, stdout for NDJSON, stderr for errors
Long Prompt Handling
Prompts exceeding 10K characters must be delivered via stdin (not CLI argument) to avoid OS argument length limits:
- Write prompt to
child.stdin, then close stdin
- Subclasses indicate whether the CLI accepts stdin prompts via a class property or method
NDJSON Parsing
- Read stdout line-by-line (split on
\n)
- Each line: attempt
JSON.parse(), pass to extractEvent()
- Malformed lines: log warning, skip (not fatal)
- Yield parsed
AgentEvent objects via async generator
Watchdog Timer
- Configurable timeout (default: 5 minutes, override via constructor)
- Reset on each NDJSON line received (activity = alive)
- On timeout: kill child process, yield
AgentErrorEvent with timeout info
Abort Signal Propagation
- Wire
params.abortSignal to child.kill('SIGTERM')
- On abort: yield
AgentErrorEvent with abort info, then AgentDoneEvent
Stderr Capture
- Accumulate stderr output into a buffer
- Expose to subclasses (or ErrorClassifier) after subprocess exits
- Don't treat stderr as fatal — some CLIs write progress/debug info to stderr
execute() — Async Generator
The public execute() method implements AgentRuntime.execute() as an async generator:
async *execute(params: AgentExecuteParams): AsyncIterable<AgentEvent> {
const args = this.buildArgs(params);
const env = this.buildEnv(params);
const child = spawn(this.command, args, { cwd: params.workingDirectory, env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
// ... wire abort signal, watchdog timer, stderr capture ...
// ... for each NDJSON line from stdout: extractEvent() → yield ...
// ... on subprocess exit: yield AgentDoneEvent with AgentRunResult ...
}
Constructor
constructor(
/** CLI command name (e.g., "claude", "gemini", "codex", "opencode"). */
protected readonly command: string,
/** Subprocess timeout in milliseconds (default: 5 minutes). */
protected readonly timeoutMs: number = 300_000,
)
Types Used (from src/middleware/types.ts)
AgentRuntime — interface this class implements
AgentExecuteParams — input parameter type
AgentEvent — discriminated union yielded from execute()
AgentRunResult — final summary carried in AgentDoneEvent
AgentErrorEvent — emitted on timeout/abort/error
Acceptance Criteria
Context
RemoteClaw runs AI agent CLIs (Claude, Gemini, Codex, OpenCode) as subprocesses and parses their NDJSON streaming output into normalized
AgentEventobjects. All four CLI runtimes share ~80% of their logic: spawn a subprocess, parse NDJSON lines from stdout, capture stderr, handle abort signals, manage watchdog timers.CLIRuntimeBaseis the abstract base class that encapsulates this shared subprocess machinery. Each concrete runtime (Claude, Gemini, Codex, OpenCode) extends it and only implements the CLI-specific parts: argument construction, event extraction, and environment setup.Depends on: #3 (AgentRuntime interface — merged)
Architecture
CLIRuntimeBasehandles:child_process.spawn()AgentEventitems)Subclasses handle:
buildArgs(params)— construct CLI-specific argumentsextractEvent(line)— parse provider-specific NDJSON intoAgentEventbuildEnv(params)— construct provider-specific environment variablesFile Location
src/middleware/cli-runtime-base.ts(~150 lines estimated)Implementation Details
Abstract Methods
Subprocess Spawning
child_process.spawn()(notexec) for streaming outputcwdfromparams.workingDirectorybuildEnv(params)withprocess.envstdio: ["pipe", "pipe", "pipe"]— stdin for prompts, stdout for NDJSON, stderr for errorsLong Prompt Handling
Prompts exceeding 10K characters must be delivered via stdin (not CLI argument) to avoid OS argument length limits:
child.stdin, then close stdinNDJSON Parsing
\n)JSON.parse(), pass toextractEvent()AgentEventobjects via async generatorWatchdog Timer
AgentErrorEventwith timeout infoAbort Signal Propagation
params.abortSignaltochild.kill('SIGTERM')AgentErrorEventwith abort info, thenAgentDoneEventStderr Capture
execute()— Async GeneratorThe public
execute()method implementsAgentRuntime.execute()as an async generator:Constructor
Types Used (from
src/middleware/types.ts)AgentRuntime— interface this class implementsAgentExecuteParams— input parameter typeAgentEvent— discriminated union yielded fromexecute()AgentRunResult— final summary carried inAgentDoneEventAgentErrorEvent— emitted on timeout/abort/errorAcceptance Criteria
src/middleware/cli-runtime-base.tsexports abstract classCLIRuntimeBaseAgentRuntimeinterfacebuildArgs(),extractEvent(),buildEnv()child_process.spawn()with configurablecommandexecute()is an async generator yieldingAgentEventobjectsspawn()cwdoptionparams.abortSignalkills child processpnpm buildpasses