项目文件夹

文件
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

164 行
6.9 KiB
JavaScript
可执行文件

#!/usr/bin/env node
/**
* mock-agent.mjs — pretends to be one of OD's supported agent CLIs
* (claude / opencode / codex / deepseek / qwen / grok) by streaming a
* pre-recorded session in that CLI's native stdout protocol. Zero LLM
* tokens.
*
* Usage (driven by the wrappers in bin/, not directly):
* ./mock-agent.mjs --as opencode [--no-delay] [--report-file <path>]
*
* Recording selection — see lib/recording-picker.mjs. The wrappers
* announce the picked trace id on stderr.
*
* Trace data: ./recordings/<trace-id>.jsonl (anonymized exports from
* Langfuse). Index: ./recordings/index.json.
*/
import { pickRecording, readRecording } from './lib/recording-picker.mjs';
import { renderAsOpencode } from './lib/format-opencode.mjs';
import { renderAsCodex } from './lib/format-codex.mjs';
import { renderAsClaude } from './lib/format-claude.mjs';
import { renderAsGemini } from './lib/format-gemini.mjs';
import { renderAsCursorAgent } from './lib/format-cursor-agent.mjs';
import { renderAsKimi } from './lib/format-kimi.mjs';
import { renderAsPlain } from './lib/format-plain.mjs';
import { runAcpServer } from './lib/format-acp.mjs';
import { runVelaAcpServer } from './lib/format-vela.mjs';
import { runVelaLogin, runVelaModels } from './lib/vela-subcommands.mjs';
function parseArgs(argv) {
const opts = { as: null, noDelay: false, reportFile: null, positionals: [] };
const flagsWithValue = new Set(['--as', '--agent', '--report-file', '-p', '--output-format', '--model', '-r']);
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--as' || a === '--agent') { opts.as = argv[++i]; continue; }
if (a === '--no-delay') { opts.noDelay = true; continue; }
if (a === '--report-file') { opts.reportFile = argv[++i]; continue; }
if (flagsWithValue.has(a)) { i++; continue; }
if (a.startsWith('-')) continue; // Unknown flag — silently skip (model/permission flags etc.)
// Anything left is a positional — used by vela subcommand dispatch.
opts.positionals.push(a);
}
if (process.env.OD_MOCKS_NO_DELAY === '1') opts.noDelay = true;
// Fall through to REPORT_FILE env when --report-file wasn't supplied.
// Some harnesses (e.g. the agent-pr-explore orchestrator) set
// REPORT_FILE as env but expect the agent to write there
// autonomously — real opencode/claude do via their Write tool, but
// the mock needs to project the recording's final assistant text to
// that path so the harness sees a report.
if (!opts.reportFile && process.env.REPORT_FILE) {
opts.reportFile = process.env.REPORT_FILE;
}
return opts;
}
function failUsage(message) {
process.stderr.write(`mock-agent: ${message}\n`);
process.exit(2);
}
async function readStdinIfPiped() {
if (process.stdin.isTTY) return '';
return new Promise(resolve => {
let acc = '';
process.stdin.setEncoding('utf-8');
process.stdin.on('data', c => { acc += c; });
process.stdin.on('end', () => resolve(acc));
process.stdin.on('error', () => resolve(acc));
// Safety timeout in case the parent never closes stdin (PTY).
setTimeout(() => resolve(acc), 1500);
});
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
if (!opts.as) {
failUsage(
'--as <agent> required\n' +
' supported: opencode | claude | amp | codex | gemini | cursor-agent |\n' +
' deepseek | qwen | grok | plain |\n' +
' kimi (stream-json)\n' +
' devin | hermes | kilo | kiro | vibe (ACP)\n' +
' vela (AMR — vela CLI)\n',
);
}
// `vela` dispatches by the first positional arg passed by OD (login /
// models / agent). Subcommands run BEFORE recording selection because
// they don't use trace data at all.
if (opts.as === 'vela') {
const cmd = (opts.positionals[0] || '').trim();
if (cmd === 'login') return runVelaLogin();
if (cmd === 'models') return runVelaModels();
// Default: `agent run --runtime opencode` — fall through to the ACP
// server below with the vela-flavored protocol.
}
// Modern Kimi rejects the old `kimi acp ...` launch shape; keep the
// shared mock aligned so adapter regressions fail fast in tests.
if (opts.as === 'kimi' && opts.positionals.length > 0) {
failUsage(`too many arguments: ${opts.positionals.join(' ')}`);
}
// ACP agents read JSON-RPC messages off stdin one line at a time, so the
// bulk-prompt buffering logic below doesn't apply — pickRecording sees no
// prompt for hash-mode (use OD_MOCKS_TRACE or _POOL instead).
const ACP_AGENTS = new Set(['devin', 'hermes', 'kilo', 'kiro', 'vibe', 'vela']);
const isAcp = ACP_AGENTS.has(opts.as);
const prompt = isAcp ? '' : await readStdinIfPiped();
const picked = await pickRecording({ prompt });
if (!picked) {
process.stderr.write(
'mock-agent: no recordings on disk yet.\n' +
'The recording corpus is hosted on Cloudflare R2 (see mocks/manifest.json)\n' +
'and is fetched on demand. Run:\n' +
'\n' +
' bash mocks/scripts/fetch-recordings.sh # all 179 (~30s, 4.5MB)\n' +
' bash mocks/scripts/fetch-recordings.sh --agent claude # subset\n' +
'\n' +
'Or set OD_MOCKS_RECORDINGS_DIR if you stashed them elsewhere.\n',
);
process.exit(3);
}
process.stderr.write(
`[mock-${opts.as}] picked ${picked.traceId.slice(0, 8)}… via ${picked.method}` +
(picked.pool ? ` (pool="${picked.pool}")` : '') +
'\n',
);
const events = await readRecording(picked.path);
const renderOpts = { noDelay: opts.noDelay, reportFile: opts.reportFile };
switch (opts.as) {
case 'opencode': await renderAsOpencode(events, renderOpts); break;
case 'codex': await renderAsCodex(events, renderOpts); break;
case 'claude':
case 'amp': await renderAsClaude(events, renderOpts); break;
case 'gemini': await renderAsGemini(events, renderOpts); break;
case 'cursor-agent': await renderAsCursorAgent(events, renderOpts); break;
case 'deepseek':
case 'qwen':
case 'grok':
case 'plain': await renderAsPlain(events, renderOpts); break;
case 'kimi': await renderAsKimi(events, renderOpts); break;
// ACP family — JSON-RPC server over stdio.
case 'devin':
case 'hermes':
case 'kilo':
case 'kiro':
case 'vibe': await runAcpServer(events, renderOpts); break;
// AMR (vela CLI) — ACP with vela-specific protocol extensions
// (agentCapabilities + models block + strict set_model gate).
case 'vela': await runVelaAcpServer(events, renderOpts); break;
default:
failUsage(`unknown agent "${opts.as}"`);
}
}
main().catch(err => {
process.stderr.write(`mock-agent: ${err.message}\n`);
process.exit(1);
});