Files
t3mp3st/src/cli.ts
T
2alfandJoseph Magly 8ccea7c82a feat(llm): add Hugging Face provider + selectable local Claude Code models (#127)
* feat(llm): add Hugging Face provider + selectable local Claude Code models

Adds two operator-selectable LLM backbones, wired through both the backend
and the War Room UI.

Hugging Face (OpenAI-compatible Inference Providers router):
- New `huggingface` provider reusing OpenAIAdapter (router.huggingface.co/v1)
- HF_TOKEN / HUGGINGFACE_API_KEY / HUGGINGFACE_TOKEN env resolution, config,
  setup wizard, CLI, factory, best-available, and .env.example
- Settings key card + 4 inference models in the picker + direct client routing
- Tests: src/__tests__/huggingface-provider.test.ts

Local Claude from Claude Code:
- Explicit "active backbone" pin in Settings so a connected agent (Claude Code)
  can be chosen; a pin outranks stored API keys in both the client resolver and
  mission dispatch
- Claude Code model picker (Account default / Opus / Sonnet / Haiku), threaded
  to the CLI's --model via an "agentId::model" encoding parsed by LocalAgentAdapter;
  requireLiveLocalAgent strips the suffix for connected-agent validation

* fix(llm): address PR #127 review — self-contained keyless path + centralized pin/model routing

Finding 1 — keyless mission path was not self-contained:
- Add the `local-agent` case to ConfigManager.getLLMConfig so provider:'local-agent'
  resolves (keyless, agent id — optionally "agentId::model" — in `model`) instead of
  hitting the default "Unknown provider" throw.

Finding 2 — inconsistent pin/model routing across dispatch surfaces:
- Centralize backend agent resolution in window.t3mpDispatchAgent (single source of truth).
  It honors an explicit pin OVER a stored API key and encodes the picked local model as
  "agentId::model" so it is never dropped.
- Route all three surfaces through it: mission dispatch, General (getGeneralConfig), and
  Admiral (_admBackbone). Previously General/Admiral only picked an agent when no key
  existed and sent the bare agent id, so a stored key overrode the pin and the selected
  Claude model was lost.

Tests:
- getLLMConfig('local-agent') regression coverage (self-contained keyless path)
- LocalAgentAdapter "agentId::model" passthrough reaches the CLI --model flag
- t3mpDispatchAgent behavior: pin>key precedence, model encoding, fallbacks (evaluated
  from the shipped docs/index.html)
- centralization invariant: mission/General/Admiral all route through t3mpDispatchAgent
- update the Op-Admiral keyless-routing static test to the centralized resolver

---------

Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
2026-07-27 15:47:06 -04:00

716 lines
20 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* T3MP3ST CLI
*
* Command-line interface for T3MP3ST operations.
*/
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import boxen from 'boxen';
import ora from 'ora';
import Table from 'cli-table3';
import gradient from 'gradient-string';
import figlet from 'figlet';
import {
createAutoTempest,
createTestTempest,
createStealthOperation,
createAggressiveOperation,
getBanner,
type Tempest,
type OperatorArchetype,
} from './index.js';
import {
config,
hasApiKey,
setApiKey,
getConfiguredProviders,
AVAILABLE_MODELS,
} from './config/index.js';
import { LLMBackbone } from './llm/index.js';
// =============================================================================
// DISPLAY HELPERS
// =============================================================================
function showBanner(): void {
try {
const text = figlet.textSync('T3MP3ST', {
font: 'ANSI Shadow',
horizontalLayout: 'default',
});
console.log(gradient.pastel.multiline(text));
} catch {
console.log(chalk.cyan(getBanner()));
}
console.log(chalk.gray(' Tactical Execution Multi-agent Platform for Elite Security Testing\n'));
}
function showBox(title: string, content: string, borderColor: string = 'cyan'): void {
console.log(
boxen(content, {
title,
titleAlignment: 'center',
padding: 1,
margin: { top: 0, bottom: 1, left: 1, right: 1 },
borderStyle: 'round',
borderColor: borderColor as any,
})
);
}
function showSuccess(message: string): void {
console.log(chalk.green('✓ ') + message);
}
function showError(message: string): void {
console.log(chalk.red('✗ ') + message);
}
function showInfo(message: string): void {
console.log(chalk.blue('ℹ ') + message);
}
function showWarning(message: string): void {
console.log(chalk.yellow('⚠ ') + message);
}
// =============================================================================
// STATUS DISPLAY
// =============================================================================
function displayStatus(tempest: Tempest): void {
const status = tempest.command.getStatus();
console.log('');
console.log(chalk.bold.cyan('═══ OPERATION STATUS ═══'));
console.log('');
// Operation Info
console.log(chalk.bold('Operation:'), status.name);
console.log(chalk.bold('Status:'), status.running
? (status.paused ? chalk.yellow('PAUSED') : chalk.green('RUNNING'))
: chalk.gray('STOPPED'));
console.log(chalk.bold('Tick Count:'), status.tickCount);
console.log('');
// Operators Table
const opTable = new Table({
head: [
chalk.cyan('Status'),
chalk.cyan('Count'),
],
style: { head: [], border: [] },
});
opTable.push(
['Available', chalk.green(status.operators.available.toString())],
['Busy', chalk.yellow(status.operators.busy.toString())],
['Cooldown', chalk.blue(status.operators.cooldown.toString())],
['Burned', chalk.red(status.operators.burned.toString())],
[chalk.bold('Total'), chalk.bold(status.operators.total.toString())],
);
console.log(chalk.bold('Operators:'));
console.log(opTable.toString());
console.log('');
// Findings Summary
const findingsTable = new Table({
head: [
chalk.cyan('Severity'),
chalk.cyan('Count'),
],
style: { head: [], border: [] },
});
findingsTable.push(
[chalk.red('Critical'), status.vault.bySeverity.critical.toString()],
[chalk.magenta('High'), status.vault.bySeverity.high.toString()],
[chalk.yellow('Medium'), status.vault.bySeverity.medium.toString()],
[chalk.blue('Low'), status.vault.bySeverity.low.toString()],
[chalk.gray('Info'), status.vault.bySeverity.info.toString()],
);
console.log(chalk.bold('Findings:'));
console.log(findingsTable.toString());
console.log('');
// OPSEC Status
const opsecColor = status.opsec.riskLevel === 'critical' ? chalk.red :
status.opsec.riskLevel === 'high' ? chalk.yellow :
status.opsec.riskLevel === 'medium' ? chalk.blue : chalk.green;
console.log(chalk.bold('OPSEC:'), opsecColor(status.opsec.riskLevel.toUpperCase()));
console.log(chalk.bold('Detections:'), `${status.opsec.activeDetections}/${status.opsec.totalDetections}`);
if (status.opsec.abortRecommended) {
console.log(chalk.red.bold('⚠ ABORT RECOMMENDED'));
}
console.log('');
}
// =============================================================================
// INTERACTIVE MODE
// =============================================================================
async function interactiveMode(): Promise<void> {
showBanner();
// Check for API keys
const providers = getConfiguredProviders().filter(p => p !== 'mock');
if (providers.length === 0) {
showBox(
'Setup Required',
`No API keys configured. Please run setup first:
${chalk.cyan('npx t3mp3st setup')}
Or set environment variables:
${chalk.gray('OPENROUTER_API_KEY=your-key-here')}`,
'yellow'
);
const { runSetup } = await inquirer.prompt([
{
type: 'confirm',
name: 'runSetup',
message: 'Would you like to run setup now?',
default: true,
},
]);
if (runSetup) {
// Run setup
const { execSync } = await import('child_process');
execSync('npx tsx src/setup.ts', { stdio: 'inherit' });
}
return;
}
// Welcome message
showBox(
'Welcome to T3MP3ST',
`Configured providers: ${providers.map(p => chalk.cyan(p)).join(', ')}
Default: ${chalk.cyan(config.get('defaultProvider'))} / ${chalk.cyan(config.get('defaultModel'))}`,
'cyan'
);
// Main menu loop
let running = true;
let tempest: Tempest | null = null;
while (running) {
const choices = [
{ name: '🚀 Start New Operation', value: 'new' },
];
if (tempest) {
choices.push(
{ name: '📊 View Status', value: 'status' },
{ name: '👤 Spawn Operator', value: 'spawn' },
{ name: '🎯 Add Target', value: 'target' },
{ name: '📋 Create Mission', value: 'mission' },
{ name: '💬 Chat with AI', value: 'chat' },
{ name: '📝 Generate Report', value: 'report' },
{ name: '⏹️ Stop Operation', value: 'stop' },
);
}
choices.push(
{ name: '⚙️ Settings', value: 'settings' },
{ name: '🚪 Exit', value: 'exit' },
);
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: tempest ? `[${tempest.command.name}] What would you like to do?` : 'What would you like to do?',
choices,
},
]);
try {
switch (action) {
case 'new':
tempest = await startNewOperation();
break;
case 'status':
if (tempest) displayStatus(tempest);
break;
case 'spawn':
if (tempest) await spawnOperator(tempest);
break;
case 'target':
if (tempest) await addTarget(tempest);
break;
case 'mission':
if (tempest) await createMission(tempest);
break;
case 'chat':
if (tempest) await chatWithAI(tempest);
break;
case 'report':
if (tempest) await generateReport(tempest);
break;
case 'stop':
if (tempest) {
tempest.command.stop();
showSuccess('Operation stopped');
tempest = null;
}
break;
case 'settings':
await openSettings();
break;
case 'exit':
if (tempest) {
const { confirmExit } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmExit',
message: 'Operation is running. Are you sure you want to exit?',
default: false,
},
]);
if (!confirmExit) continue;
tempest.command.stop();
}
running = false;
break;
}
} catch (error) {
showError(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
}
console.log(chalk.gray('\nGoodbye!\n'));
}
async function startNewOperation(): Promise<Tempest> {
const { name, type } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Operation name:',
default: `Operation ${Date.now().toString(36).toUpperCase()}`,
},
{
type: 'list',
name: 'type',
message: 'Operation type:',
choices: [
{ name: 'Auto (use configured defaults)', value: 'auto' },
{ name: 'Stealth (minimal detection)', value: 'stealth' },
{ name: 'Aggressive (maximum speed)', value: 'aggressive' },
{ name: 'Test (mock LLM)', value: 'test' },
],
},
]);
const spinner = ora('Initializing operation...').start();
let tempest: Tempest;
switch (type) {
case 'stealth':
tempest = createStealthOperation(name);
break;
case 'aggressive':
tempest = createAggressiveOperation(name);
break;
case 'test':
tempest = createTestTempest(name);
break;
default:
tempest = createAutoTempest(name);
}
tempest.command.start();
spinner.succeed(`Operation "${name}" started!`);
displayStatus(tempest);
return tempest;
}
async function spawnOperator(tempest: Tempest): Promise<void> {
const archetypes: OperatorArchetype[] = [
'recon', 'scanner', 'exploiter', 'infiltrator',
'exfiltrator', 'ghost', 'coordinator', 'analyst',
];
const { archetype, callsign } = await inquirer.prompt([
{
type: 'list',
name: 'archetype',
message: 'Select operator archetype:',
choices: archetypes.map(a => ({
name: `${a.charAt(0).toUpperCase() + a.slice(1)}`,
value: a,
})),
},
{
type: 'input',
name: 'callsign',
message: 'Operator callsign:',
default: (answers: { archetype: string }) =>
`${answers.archetype.charAt(0).toUpperCase() + answers.archetype.slice(1)}-${tempest.cell.getAllOperators().length + 1}`,
},
]);
const operator = tempest.command.spawnOperator(callsign, archetype);
showSuccess(`Operator ${callsign} (${archetype}) spawned!`);
showInfo(`ID: ${operator.id}`);
}
async function addTarget(tempest: Tempest): Promise<void> {
const { address, type, zone } = await inquirer.prompt([
{
type: 'input',
name: 'address',
message: 'Target address (URL or IP):',
validate: (input: string) => input.length > 0 || 'Address is required',
},
{
type: 'list',
name: 'type',
message: 'Target type:',
choices: [
'web_application',
'api',
'network',
'host',
'database',
'cloud',
],
},
{
type: 'list',
name: 'zone',
message: 'Target zone:',
choices: ['external', 'dmz', 'internal', 'restricted'],
},
]);
const target = tempest.targetEnv.addTarget({
name: address,
type,
zone,
address,
});
showSuccess(`Target "${address}" added!`);
showInfo(`ID: ${target.id}`);
}
async function createMission(tempest: Tempest): Promise<void> {
const { name, objectives } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Mission name:',
default: 'Primary Mission',
},
{
type: 'input',
name: 'objectives',
message: 'Objectives (comma-separated):',
default: 'Enumerate attack surface, Identify vulnerabilities, Document findings',
},
]);
const mission = tempest.mission.createMission({
name,
objectives: objectives.split(',').map((o: string) => o.trim()),
});
tempest.mission.startMission(mission.id);
showSuccess(`Mission "${name}" created and started!`);
showInfo(`ID: ${mission.id}`);
}
async function chatWithAI(tempest: Tempest): Promise<void> {
showInfo('Chat mode - type "exit" to return to menu\n');
let chatting = true;
while (chatting) {
const { message } = await inquirer.prompt([
{
type: 'input',
name: 'message',
message: chalk.cyan('You:'),
validate: (input: string) => input.length > 0 || 'Enter a message',
},
]);
if (message.toLowerCase() === 'exit') {
chatting = false;
continue;
}
const spinner = ora('Thinking...').start();
try {
const response = await tempest.llm.prompt(message);
spinner.stop();
console.log(chalk.green('AI:'), response);
console.log('');
} catch (error) {
spinner.fail('Error');
showError(`${error instanceof Error ? error.message : String(error)}`);
}
}
}
async function generateReport(tempest: Tempest): Promise<void> {
const mission = tempest.mission.getActiveMission();
if (!mission) {
// Create a temporary mission for report
const tempMission = tempest.mission.createMission({
name: 'Ad-hoc Report',
objectives: ['Generate findings report'],
});
tempest.mission.startMission(tempMission.id);
}
try {
const report = tempest.command.generateReport();
console.log('');
console.log(chalk.bold.cyan('═══ ENGAGEMENT REPORT ═══'));
console.log('');
console.log(report);
} catch (error) {
showWarning('No findings to report yet.');
}
}
async function openSettings(): Promise<void> {
const { setting } = await inquirer.prompt([
{
type: 'list',
name: 'setting',
message: 'Settings:',
choices: [
{ name: 'View current configuration', value: 'view' },
{ name: 'Change default provider', value: 'provider' },
{ name: 'Change default model', value: 'model' },
{ name: 'Add/update API key', value: 'apikey' },
{ name: 'Back', value: 'back' },
],
},
]);
switch (setting) {
case 'view':
const settings = config.getAll();
console.log('');
console.log(chalk.bold('Current Configuration:'));
console.log(chalk.cyan(' Provider:'), settings.defaultProvider);
console.log(chalk.cyan(' Model:'), settings.defaultModel);
console.log(chalk.cyan(' OpenRouter:'), hasApiKey('openrouter') ? chalk.green('configured') : chalk.red('not set'));
console.log(chalk.cyan(' Venice:'), hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set'));
console.log(chalk.cyan(' Anthropic:'), hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set'));
console.log(chalk.cyan(' OpenAI:'), hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set'));
console.log(chalk.cyan(' DeepSeek:'), hasApiKey('deepseek') ? chalk.green('configured') : chalk.red('not set'));
console.log(chalk.cyan(' HuggingFace:'), hasApiKey('huggingface') ? chalk.green('configured') : chalk.red('not set'));
console.log('');
break;
case 'provider':
const providers = getConfiguredProviders().filter(p => p !== 'mock');
if (providers.length === 0) {
showWarning('No API keys configured');
break;
}
const { provider } = await inquirer.prompt([
{
type: 'list',
name: 'provider',
message: 'Select default provider:',
choices: providers,
default: config.get('defaultProvider'),
},
]);
config.setDefaultProvider(provider);
showSuccess(`Default provider set to: ${provider}`);
break;
case 'model': {
const currentProvider = config.get('defaultProvider');
const availableModels = AVAILABLE_MODELS[currentProvider];
if (!availableModels || availableModels.length === 0) {
showWarning(`No models available for ${currentProvider}`);
break;
}
const { selectedModel } = await inquirer.prompt([
{
type: 'list',
name: 'selectedModel',
message: 'Select default model:',
choices: availableModels.map(m => ({
name: `${m.name} (${m.id}) - ${m.contextWindow.toLocaleString()} ctx`,
value: m.id,
})),
default: config.get('defaultModel'),
},
]);
config.setDefaultModel(currentProvider, selectedModel);
showSuccess(`Default model set to: ${selectedModel}`);
break;
}
case 'apikey':
const { keyProvider } = await inquirer.prompt([
{
type: 'list',
name: 'keyProvider',
message: 'Which provider?',
choices: ['openrouter', 'venice', 'anthropic', 'openai', 'deepseek', 'huggingface'],
},
]);
const { apiKey } = await inquirer.prompt([
{
type: 'password',
name: 'apiKey',
message: `Enter ${keyProvider} API key:`,
mask: '*',
},
]);
if (apiKey) {
setApiKey(keyProvider, apiKey);
showSuccess(`${keyProvider} API key saved!`);
}
break;
}
}
// =============================================================================
// CLI COMMANDS
// =============================================================================
const program = new Command();
program
.name('t3mp3st')
.description('T3MP3ST - Tactical Execution Multi-agent Platform for Elite Security Testing')
.version('1.0.0');
program
.command('interactive', { isDefault: true })
.description('Start interactive mode')
.action(interactiveMode);
program
.command('setup')
.description('Run the setup wizard')
.action(async () => {
const { execSync } = await import('child_process');
try {
execSync('npx tsx src/setup.ts', { stdio: 'inherit', cwd: process.cwd() });
} catch {
// Setup script handles its own errors
}
});
program
.command('status')
.description('Show current configuration status')
.action(() => {
showBanner();
const settings = config.getAll();
console.log(chalk.bold('Configuration Status:\n'));
const table = new Table({
style: { head: [], border: [] },
});
table.push(
[chalk.cyan('Default Provider'), settings.defaultProvider],
[chalk.cyan('Default Model'), settings.defaultModel],
[chalk.cyan('OpenRouter API Key'), hasApiKey('openrouter') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
[chalk.cyan('Venice API Key'), hasApiKey('venice') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
[chalk.cyan('Anthropic API Key'), hasApiKey('anthropic') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
[chalk.cyan('OpenAI API Key'), hasApiKey('openai') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
[chalk.cyan('DeepSeek API Key'), hasApiKey('deepseek') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
[chalk.cyan('HuggingFace API Key'), hasApiKey('huggingface') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
[chalk.cyan('Config Path'), config.getConfigPath()],
);
console.log(table.toString());
console.log('');
});
program
.command('test')
.description('Test the LLM connection')
.action(async () => {
showBanner();
const provider = config.get('defaultProvider');
if (!hasApiKey(provider as any)) {
showError(`No API key configured for ${provider}`);
showInfo('Run "npx t3mp3st setup" to configure API keys');
return;
}
const spinner = ora(`Testing connection to ${provider}...`).start();
try {
const llm = new LLMBackbone(config.getLLMConfig());
const response = await llm.prompt('Say "Connection successful!" and nothing else.');
spinner.succeed('Connection successful!');
console.log('');
console.log(chalk.cyan('Response:'), response);
console.log('');
} catch (error) {
spinner.fail('Connection failed');
showError(`${error instanceof Error ? error.message : String(error)}`);
}
});
program
.command('models')
.description('List available models')
.action(() => {
showBanner();
const provider = config.get('defaultProvider');
const models = AVAILABLE_MODELS[provider];
console.log(chalk.bold(`Available models for ${provider}:\n`));
const table = new Table({
head: [chalk.cyan('Model ID'), chalk.cyan('Name'), chalk.cyan('Context')],
style: { head: [], border: [] },
});
for (const model of models) {
table.push([
model.id,
model.name,
`${model.contextWindow.toLocaleString()} tokens`,
]);
}
console.log(table.toString());
console.log('');
});
// Run CLI
program.parse();