/** * `agentguard advisor`: local LLM-driven policy setup. */ import * as readline from 'readline'; import { AGENTGUARD_SPEND_VERSION } from '../index'; import { AdvisorConversation, formatCents } from '../advisor/conversation'; import { normalizePosture, type GovernancePosture } from '../advisor/posture'; import { forecastMonthEnd } from '../advisor/forecast'; import { createAdvisorClient, resolveAdvisorApiKey, type AdvisorChatMessage, type AdvisorProvider } from '../advisor/llm-client'; import { readDecisionSpend, reviewAnomalies } from '../advisor/anomaly'; import { ADVISOR_SYSTEM_PROMPT } from '../advisor/system-prompt'; import { createAdvisorSessionLogger, writeAdvisorOutputs } from '../advisor/output'; import { banner, cyanBold, dim, green, yellow } from './colors'; import { confirmPostureSoftCap } from './soft-cap'; interface AdvisorCliOptions { provider: AdvisorProvider; baseUrl?: string; model?: string; language?: 'ts' | 'py'; posture?: GovernancePosture | 'custom'; defaults: boolean; yes: boolean; } const HELP = `agentguard advisor: local LLM-driven policy setup usage: agentguard advisor [--provider openrouter|openai|anthropic|compatible] [--base-url ] [--model ] [--posture velocity|standard|compliance|custom] agentguard advisor review [--scope ] agentguard advisor forecast [--scope ] [--cap-cents ] examples: agentguard auth openrouter agentguard advisor agentguard advisor --posture velocity --provider mock --defaults agentguard advisor --base-url https://api.deepseek.com/v1 --model deepseek-chat agentguard advisor review `; export async function runAdvisor(argv: string[]): Promise { if (argv.includes('--help') || argv.includes('-h')) { console.log(HELP); return 0; } if (argv[0] === 'review') return runAdvisorReview(argv.slice(1)); if (argv[0] === 'forecast') return runAdvisorForecast(argv.slice(1)); const options = parseOptions(argv); if (options.posture === 'custom') { const ok = await confirmPostureSoftCap('custom', { yes: options.yes || options.defaults }); if (!ok) return 0; options.posture = 'standard'; } if (options.posture && options.posture !== 'standard' && !process.env.AGENTGUARD_LICENSE_KEY) { const ok = await confirmPostureSoftCap(options.posture, { yes: options.yes || options.defaults }); if (!ok) return 0; options.posture = 'standard'; } const apiKey = resolveAdvisorApiKey(options.provider); if (options.provider !== 'mock' && !apiKey) { console.log(''); console.log(' ' + banner(AGENTGUARD_SPEND_VERSION)); console.log(''); console.log(` ${yellow('agentguard advisor needs a local provider key')}`); console.log(' Run agentguard auth openrouter, set OPENROUTER_API_KEY, or pass --base-url with AGENTGUARD_ADVISOR_API_KEY.'); console.log(' Prompts and policy details stay in your terminal and go only to your chosen provider.'); console.log(''); return 0; } let cancelled = false; const logger = createAdvisorSessionLogger(); const onSigint = () => { cancelled = true; logger.append('cancelled', { reason: 'sigint' }); process.stdout.write('\nadvisor cancelled. No policy files were written.\n'); }; process.once('SIGINT', onSigint); try { const conversation = new AdvisorConversation({ ...(options.language ? { language: options.language } : {}), ...(options.posture ? { posture: options.posture } : {}) }); const history: AdvisorChatMessage[] = [{ role: 'system', content: ADVISOR_SYSTEM_PROMPT }]; const client = createAdvisorClient({ provider: options.provider, baseUrl: options.baseUrl, model: options.model, apiKey: apiKey ?? undefined }); console.log(''); console.log(' ' + banner(AGENTGUARD_SPEND_VERSION)); console.log(''); console.log(cyanBold('AgentGuard Advisor')); console.log(dim('Local LLM setup. No AgentGuard service receives prompts, completions, keys, or policies.')); console.log(''); if (options.defaults || !process.stdin.isTTY) { fillDefaults(conversation); logger.append('answers_defaulted', { answers: conversation.snapshot() }); } else { await runInteractiveQuestions(conversation, logger, history, client, () => cancelled); } if (cancelled) return 130; if (!conversation.isComplete()) { console.log(yellow('advisor did not collect enough answers to write policy files.')); return 0; } const profile = conversation.profile(process.cwd()); if (options.language) profile.language = options.language; if (options.posture) profile.posture = options.posture; logger.append('profile_built', { profile }); console.log(''); await streamAdvisorSummary(client, history, profileSummaryPrompt(profile), logger, () => cancelled); if (cancelled) return 130; const outputs = writeAdvisorOutputs(profile, { language: profile.language, overwrite: options.yes || options.defaults }); logger.append('files_written', { policyPath: outputs.policyPath, quickstartPath: outputs.quickstartPath, sessionLogPath: logger.path }); console.log(''); console.log(outputs.savingsTable); console.log(''); console.log(`${green('created')} ${outputs.policyPath}`); console.log(`${green('created')} ${outputs.quickstartPath}`); console.log(`${green('session')} ${logger.path}`); console.log(''); console.log('AgentGuard now proves what your AI agent attempted, who authorized it,'); console.log('what it cost, and whether it succeeded.'); console.log(''); console.log('Next: agentguard demo --policy ~/.agentguard/policy.yaml'); console.log('Verify receipts: https://agentguard.run/verify'); console.log(''); return 0; } finally { process.removeListener('SIGINT', onSigint); } } async function runAdvisorReview(argv: string[]): Promise { const scope = valueAfter(argv, '--scope') ?? 'default'; const anomalies = reviewAnomalies(readDecisionSpend(scope)); console.log(''); console.log(' ' + banner(AGENTGUARD_SPEND_VERSION)); console.log(''); if (anomalies.length === 0) { console.log('No local spend anomalies found for the last 24 hours.'); return 0; } for (const item of anomalies) { console.log(`${item.scope}/${item.agentId}: ${formatCents(item.last24hCents)} in last 24h. Baseline ${formatCents(item.baselineCents)}, sigma ${formatCents(item.sigmaCents)}.`); console.log(`Suggestion: ${item.suggestion}`); } return 0; } async function runAdvisorForecast(argv: string[]): Promise { const scope = valueAfter(argv, '--scope') ?? 'default'; const cap = valueAfter(argv, '--cap-cents'); const forecast = forecastMonthEnd(readDecisionSpend(scope), cap ? Number(cap) : null); console.log(''); console.log(' ' + banner(AGENTGUARD_SPEND_VERSION)); console.log(''); console.log(`Days observed: ${forecast.daysObserved}`); console.log(`Projected month-end: ${formatCents(forecast.monthEndCents)}`); if (forecast.capCents !== null) console.log(`Cap: ${formatCents(forecast.capCents)}`); console.log(forecast.message); return 0; } async function runInteractiveQuestions( conversation: AdvisorConversation, logger: ReturnType, history: AdvisorChatMessage[], client: ReturnType, isCancelled: () => boolean, ): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (prompt: string) => new Promise((resolve) => rl.question(`${prompt}\n> `, resolve)); try { while (!conversation.isComplete() && !isCancelled()) { const question = conversation.currentQuestion(); if (!question) break; const answer = (await ask(question.prompt)).trim(); if (answer.toLowerCase() === 'back') { conversation.back(); logger.append('back', { to: conversation.currentQuestion()?.id }); continue; } if (!answer) continue; conversation.answer(answer); logger.append('answer', { question: question.id, answer }); history.push({ role: 'user', content: `${question.prompt}\n${answer}` }); await streamAdvisorSummary(client, history, advisorQuestionPrompt(question.id), logger, isCancelled); conversation.next(); } } finally { rl.close(); } } async function streamAdvisorSummary( client: ReturnType, history: AdvisorChatMessage[], prompt: string, logger: ReturnType, isCancelled: () => boolean, ): Promise { history.push({ role: 'user', content: prompt }); let content = ''; process.stdout.write('\n'); try { for await (const token of client.streamChat(history)) { if (isCancelled()) break; content += token; process.stdout.write(token); } process.stdout.write('\n'); } catch (err) { const message = err instanceof Error ? err.message : String(err); content = `Advisor provider unavailable: ${message}`; console.log(content); } history.push({ role: 'assistant', content }); logger.append('assistant', { content }); } function fillDefaults(conversation: AdvisorConversation): void { const answers: Record = { building: 'A customer support agent for an ecommerce business', posture: '', scale: 'Team of 5, around 2000 support tickets and orders per month', tasks: 'triage refunds, draft support replies, assemble chargeback evidence', budget: '$199 monthly budget with low per-call costs', confirm: 'Confirmed', }; while (conversation.currentQuestion()) { const question = conversation.currentQuestion(); if (!question) break; conversation.answer(answers[question.id] ?? 'Confirmed'); conversation.next(); } } function advisorQuestionPrompt(id: string): string { return `Acknowledge the ${id} answer in 2 short sentences. Ask only the next unanswered setup question. Do not write files yet.`; } function profileSummaryPrompt(profile: ReturnType): string { return `Prepare a concise final setup summary for this profile before files are written: ${JSON.stringify(profile)}. Include projected savings math in words. Do not invent model names or pricing.`; } function parseOptions(argv: string[]): AdvisorCliOptions { const baseUrl = valueAfter(argv, '--base-url'); const providerFlag = valueAfter(argv, '--provider') as AdvisorProvider | undefined; const provider = providerFlag ?? (baseUrl ? 'compatible' : 'openrouter'); const language = valueAfter(argv, '--language') as 'ts' | 'py' | undefined; const postureValue = valueAfter(argv, '--posture'); const posture = postureValue === 'custom' ? 'custom' : normalizePosture(postureValue); return { provider, baseUrl, model: valueAfter(argv, '--model'), language: language === 'py' ? 'py' : language === 'ts' ? 'ts' : undefined, posture: posture ?? undefined, defaults: argv.includes('--defaults'), yes: argv.includes('--yes') || argv.includes('-y'), }; } function valueAfter(argv: string[], flag: string): string | undefined { const index = argv.indexOf(flag); return index >= 0 ? argv[index + 1] : undefined; }