2026-02-22 19:57:40 -08:00
|
|
|
/**
|
|
|
|
|
* invoke-llm.ts
|
|
|
|
|
* Unified LLM invocation for plan2code-metrics.
|
|
|
|
|
* Supports Claude Code (temp file → stdin) and Copilot CLI (stdin string).
|
|
|
|
|
* Mirrors the agent pattern from plan2code-loop.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { execa } from 'execa';
|
|
|
|
|
import { writeFileSync, unlinkSync } from 'fs';
|
|
|
|
|
import { join } from 'path';
|
|
|
|
|
import { tmpdir } from 'os';
|
|
|
|
|
|
|
|
|
|
// ── Agent definitions ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export type AgentType = 'claude-code' | 'copilot-cli';
|
|
|
|
|
|
|
|
|
|
export interface AgentDef {
|
|
|
|
|
name: AgentType;
|
|
|
|
|
displayName: string;
|
|
|
|
|
command: string;
|
2026-03-01 12:24:11 -08:00
|
|
|
defaultModel: string;
|
2026-02-22 19:57:40 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const AGENTS: Record<AgentType, AgentDef> = {
|
|
|
|
|
'claude-code': {
|
|
|
|
|
name: 'claude-code',
|
|
|
|
|
displayName: 'Claude Code',
|
|
|
|
|
command: 'claude',
|
2026-03-01 12:24:11 -08:00
|
|
|
defaultModel: 'default',
|
2026-02-22 19:57:40 -08:00
|
|
|
},
|
|
|
|
|
'copilot-cli': {
|
|
|
|
|
name: 'copilot-cli',
|
|
|
|
|
displayName: 'GitHub Copilot CLI',
|
|
|
|
|
command: 'copilot',
|
2026-03-01 12:24:11 -08:00
|
|
|
defaultModel: 'claude-sonnet-4',
|
2026-02-22 19:57:40 -08:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Invocation ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export interface InvokeLLMOptions {
|
|
|
|
|
prompt: string;
|
|
|
|
|
model: string;
|
|
|
|
|
agent: AgentType;
|
|
|
|
|
timeout?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function invokeLLM(opts: InvokeLLMOptions): Promise<string> {
|
|
|
|
|
const { prompt, model, agent, timeout = 300_000 } = opts;
|
|
|
|
|
const def = AGENTS[agent];
|
|
|
|
|
|
|
|
|
|
if (agent === 'claude-code') {
|
|
|
|
|
// Write prompt to temp file — more reliable than stdin on Windows
|
|
|
|
|
const tempFile = join(tmpdir(), `plan2code-metrics-prompt-${Date.now()}.txt`);
|
|
|
|
|
writeFileSync(tempFile, prompt, 'utf-8');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const args: string[] = [
|
|
|
|
|
'--print',
|
|
|
|
|
'--dangerously-skip-permissions',
|
|
|
|
|
];
|
2026-03-01 12:24:11 -08:00
|
|
|
// Only add --model if not using default
|
|
|
|
|
if (model && model !== 'default') {
|
2026-02-22 19:57:40 -08:00
|
|
|
args.push('--model', model);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await execa(def.command, args, {
|
|
|
|
|
inputFile: tempFile,
|
|
|
|
|
timeout,
|
|
|
|
|
});
|
|
|
|
|
return result.stdout;
|
|
|
|
|
} finally {
|
|
|
|
|
try { unlinkSync(tempFile); } catch { /* ignore cleanup errors */ }
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Copilot CLI: pipe prompt via stdin string
|
|
|
|
|
const args: string[] = [];
|
2026-03-01 12:24:11 -08:00
|
|
|
// Only add --model if not using default
|
|
|
|
|
if (model && model !== 'default') {
|
2026-02-22 19:57:40 -08:00
|
|
|
args.push('--model', model);
|
|
|
|
|
}
|
|
|
|
|
args.push('--allow-all-tools', '-s');
|
|
|
|
|
|
|
|
|
|
const result = await execa(def.command, args, {
|
|
|
|
|
input: prompt,
|
|
|
|
|
timeout,
|
|
|
|
|
});
|
|
|
|
|
return result.stdout;
|
|
|
|
|
}
|
|
|
|
|
}
|