aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
220 lines (194 loc) • 6.02 kB
JavaScript
/**
* OpenCode CLI Provider Adapter for External Ralph Loop
*
* Provides support for OpenCode as a provider for
* autonomous task execution in Ralph loops.
*
* OpenCode CLI differences from Claude:
* - Binary: `opencode` instead of `claude`
* - Headless mode: `opencode run` subcommand
* - Format flag: `--format json` instead of `--output-format stream-json`
* - No --dangerously-skip-permissions (no permission bypass needed)
* - No --print flag (run subcommand is already non-interactive)
* - No --append-system-prompt (inject into main prompt)
* - No --max-budget-usd flag
* - No --max-turns flag
* - No --mcp-config flag
* - Session resume via -s/--session instead of --session-id
* - Model format: provider/model (e.g., opencode/big-pickle, anthropic/claude-sonnet-4-6)
* - Agent flag: --agent
* - JSON output: newline-delimited events, text in type="text" events
*
* Model tiers:
* - Free tier (default): opencode/* models (opencode/big-pickle, etc.)
* - Anthropic account: anthropic/claude-* models available after `opencode auth add`
*
* @implements Plan: Multi-Provider Support for External Ralph Loop
*/
import { ProviderAdapter, registerProvider } from './provider-adapter.mjs';
/**
* Model mapping from generic names to OpenCode provider/model format.
*
* Free tier uses opencode/* models. Users who have connected an Anthropic
* account via `opencode auth add` can pass anthropic/* IDs directly
* (e.g., --model anthropic/claude-sonnet-4-6) and they pass through unchanged
* because mapModel() only substitutes known generic aliases.
*/
const MODEL_MAP = {
'opus': 'opencode/big-pickle',
'sonnet': 'opencode/big-pickle',
'haiku': 'opencode/big-pickle',
};
export class OpenCodeAdapter extends ProviderAdapter {
/** @returns {string} */
getBinary() {
return 'opencode';
}
/** @returns {string} */
getName() {
return 'opencode';
}
/**
* OpenCode has limited capabilities compared to Claude.
* Supports session resume, model selection, agent mode, and JSON output.
* @returns {import('./provider-adapter.mjs').ProviderCapabilities}
*/
getCapabilities() {
return {
streamJson: false,
sessionResume: true,
budgetControl: false,
systemPrompt: false,
agentMode: true,
mcpConfig: false,
maxTurns: false,
};
}
/**
* Build args for the main headless session.
*
* OpenCode uses `opencode run --format json` for headless operation.
*
* @param {import('./provider-adapter.mjs').SessionArgs} options
* @returns {string[]}
*/
buildSessionArgs(options) {
const args = [
'run',
'--format', 'json',
];
// Model selection (map generic to OpenCode provider/model format)
if (options.model) {
args.push('-m', this.mapModel(options.model));
}
// Session resume via -s flag
if (options.sessionId) {
args.push('-s', options.sessionId);
}
// Budget control not supported
if (options.budget) {
this.warnUnsupported('budgetControl', 'Budget control (--max-budget-usd)');
}
// Max turns not supported
if (options.maxTurns) {
this.warnUnsupported('maxTurns', 'Max turns (--max-turns)');
}
// MCP configuration not supported
if (options.mcpConfig) {
this.warnUnsupported('mcpConfig', 'MCP configuration (--mcp-config)');
}
// System prompt: OpenCode doesn't support --append-system-prompt,
// so we prepend it to the main prompt
let prompt = options.prompt;
if (options.systemPrompt) {
prompt = `[System Context]\n${options.systemPrompt}\n\n[Task]\n${prompt}`;
}
// The prompt itself (must be last)
args.push(prompt);
return args;
}
/**
* Build args for short analysis calls (spawnSync).
*
* @param {import('./provider-adapter.mjs').AnalysisArgs} options
* @returns {string[]}
*/
buildAnalysisArgs(options) {
const args = [
'run',
'--format', 'json',
];
// Model selection
if (options.model) {
args.push('-m', this.mapModel(options.model));
}
// Agent flag supported by OpenCode
if (options.agent) {
args.push('--agent', options.agent);
}
// The analysis prompt (must be last)
args.push(options.prompt);
return args;
}
/**
* Map generic model names to OpenCode provider/model format.
*
* @param {string} genericModel
* @returns {string}
*/
mapModel(genericModel) {
const mapped = MODEL_MAP[genericModel.toLowerCase()];
if (mapped) return mapped;
// Pass through if already an OpenCode model name or unknown
return genericModel;
}
/**
* Environment overrides for headless OpenCode sessions.
* @returns {Object<string, string>}
*/
getEnvOverrides() {
return {
CI: 'true',
};
}
/**
* OpenCode does not store session transcripts in a known location.
* @returns {null}
*/
getTranscriptPath() {
return null;
}
/**
* Parse OpenCode output — extracts text from newline-delimited JSON events.
*
* OpenCode --format json produces one JSON object per line. Text content
* lives in events with type="text" at part.text. This method collects all
* text parts and returns the concatenated result as a synthetic object so
* callers get a consistent { text } shape.
*
* @param {string} stdout
* @returns {{ text: string }|null}
*/
parseOutput(stdout) {
try {
const lines = stdout.split('\n').filter(l => l.trim());
let text = '';
for (const line of lines) {
try {
const event = JSON.parse(line);
if (event.type === 'text' && event.part?.text) {
text += event.part.text;
}
} catch {
// skip malformed lines
}
}
return text ? { text } : null;
} catch {
return null;
}
}
}
// Self-register on import
registerProvider('opencode', () => new OpenCodeAdapter());
export default OpenCodeAdapter;