import type { ProviderAdapter, RunOpts, RunResult, AvailabilityCheck } from './types'; import { estimateCostUsd } from '../pricing'; import { execFileSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; /** * Grok adapter — wraps the `grok` CLI via -p / --single. * * Auth readiness is boolean only: CLI present + (~/.grok/auth.json OR * XAI_API_KEY / GROK_API_KEY env names present). Never log token values. */ export class GrokAdapter implements ProviderAdapter { readonly name = 'grok'; readonly family = 'grok' as const; async available(): Promise { // Boolean PATH presence only — never log secrets let hasBinary = false; try { execFileSync('which', ['grok'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }); hasBinary = true; } catch { try { execFileSync('grok', ['--version'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, }); hasBinary = true; } catch { hasBinary = false; } } if (!hasBinary) { return { ok: false, reason: 'grok CLI not found on PATH. Install Grok Build from xAI, or ensure `grok` is on PATH.', }; } const authPath = path.join(os.homedir(), '.grok', 'auth.json'); const hasAuthFile = fs.existsSync(authPath); // Presence of env *names* only — never read/log values const hasKey = !!(process.env.XAI_API_KEY || process.env.GROK_API_KEY); if (!hasAuthFile && !hasKey) { return { ok: false, reason: 'No Grok auth found. Log in via `grok` interactive session, or export XAI_API_KEY / GROK_API_KEY.', }; } return { ok: true }; } async run(opts: RunOpts): Promise { const start = Date.now(); // Short single-turn: -p / --single. Prefer file-free short prompts for benchmark. const args = ['--single', opts.prompt]; if (opts.model) args.push('--model', opts.model); if (opts.extraArgs) args.push(...opts.extraArgs); try { const out = execFileSync('grok', args, { cwd: opts.workdir, timeout: opts.timeoutMs, encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024, env: { ...process.env, GSTACK_HEADLESS: '1' }, }); return { output: typeof out === 'string' ? out : String(out), tokens: { input: 0, output: 0 }, durationMs: Date.now() - start, toolCalls: 0, modelUsed: opts.model || 'grok', }; } catch (err: unknown) { const durationMs = Date.now() - start; const e = err as { code?: string; stderr?: Buffer; signal?: string; message?: string }; const stderr = e.stderr?.toString() ?? ''; if (e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT') { return this.emptyResult(durationMs, { code: 'timeout', reason: `exceeded ${opts.timeoutMs}ms` }, opts.model); } if (/unauthorized|auth|login|api.?key/i.test(stderr)) { return this.emptyResult(durationMs, { code: 'auth', reason: stderr.slice(0, 400) }, opts.model); } if (/rate[- ]?limit|429/i.test(stderr)) { return this.emptyResult(durationMs, { code: 'rate_limit', reason: stderr.slice(0, 400) }, opts.model); } if (/ENOENT|not found/i.test(e.message ?? '') || e.code === 'ENOENT') { return this.emptyResult(durationMs, { code: 'binary_missing', reason: 'grok CLI not found' }, opts.model); } return this.emptyResult(durationMs, { code: 'unknown', reason: (e.message ?? stderr ?? 'unknown').slice(0, 400) }, opts.model); } } estimateCost(tokens: { input: number; output: number; cached?: number }, model?: string): number { return estimateCostUsd(tokens, model ?? 'grok'); } private emptyResult(durationMs: number, error: RunResult['error'], model?: string): RunResult { return { output: '', tokens: { input: 0, output: 0 }, durationMs, toolCalls: 0, modelUsed: model || 'grok', error, }; } }