4876bad9ab
Previously these tools were only available in daemon mode. Now TUI mode also registers web search tools (when credentials are configured) and process management tools with proper cleanup on exit.
237 lines
8.9 KiB
TypeScript
237 lines
8.9 KiB
TypeScript
import type { Command } from 'commander';
|
|
import type { Config } from '../config/index.js';
|
|
import { loadConfigSafe, getConfigPath } from './shared.js';
|
|
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { homedir } from 'os';
|
|
|
|
// ANSI color codes for tool status display
|
|
const toolColors = {
|
|
reset: '\x1b[0m',
|
|
dim: '\x1b[2m',
|
|
cyan: '\x1b[36m',
|
|
green: '\x1b[32m',
|
|
red: '\x1b[31m',
|
|
};
|
|
|
|
function loadSystemPrompt(): string {
|
|
const paths = [
|
|
resolve(process.cwd(), 'SOUL.md'),
|
|
resolve(import.meta.dirname, '../../SOUL.md'),
|
|
];
|
|
for (const soulPath of paths) {
|
|
if (existsSync(soulPath)) {
|
|
return readFileSync(soulPath, 'utf-8');
|
|
}
|
|
}
|
|
return 'You are Flynn, a helpful personal AI assistant. Be direct, concise, and helpful. Use markdown when it improves readability.';
|
|
}
|
|
|
|
export function registerTuiCommand(program: Command): void {
|
|
program
|
|
.command('tui')
|
|
.description('Launch the interactive TUI')
|
|
.option('-f, --fullscreen', 'Start in fullscreen mode')
|
|
.option('-c, --config <path>', 'Config file path')
|
|
.action(async (opts: { fullscreen?: boolean; config?: string }) => {
|
|
const configPath = opts.config ?? getConfigPath();
|
|
const { config, error } = loadConfigSafe(configPath);
|
|
if (!config) {
|
|
console.error(error);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Dynamic imports to keep CLI startup fast
|
|
const { SessionStore, SessionManager } = await import('../session/index.js');
|
|
const { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GitHubModelsClient, GeminiClient, BedrockClient, ModelRouter } = await import('../models/index.js');
|
|
const { MinimalTui, startFullscreenTui } = await import('../frontends/tui/index.js');
|
|
const { NativeAgent } = await import('../backends/index.js');
|
|
const { ToolRegistry, ToolExecutor, allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager } = await import('../tools/index.js');
|
|
const { HookEngine } = await import('../hooks/index.js');
|
|
|
|
const dataDir = resolve(homedir(), '.local/share/flynn');
|
|
mkdirSync(dataDir, { recursive: true });
|
|
|
|
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
|
|
const sessionManager = new SessionManager(sessionStore);
|
|
const models = config.models;
|
|
|
|
// Provider-agnostic client factory for TUI
|
|
function createClient(cfg: typeof models.default) {
|
|
switch (cfg.provider) {
|
|
case 'anthropic':
|
|
return new AnthropicClient({ model: cfg.model, apiKey: cfg.api_key, authToken: cfg.auth_token });
|
|
case 'openai':
|
|
return new OpenAIClient({ model: cfg.model, apiKey: cfg.api_key });
|
|
case 'gemini':
|
|
return new GeminiClient({ model: cfg.model, apiKey: cfg.api_key });
|
|
case 'ollama':
|
|
return new OllamaClient({ model: cfg.model, host: cfg.endpoint, numGpu: cfg.num_gpu });
|
|
case 'llamacpp':
|
|
return new LlamaCppClient({ endpoint: cfg.endpoint ?? 'http://localhost:8080', model: cfg.model, authToken: cfg.auth_token });
|
|
case 'openrouter':
|
|
return new OpenAIClient({ model: cfg.model, apiKey: cfg.api_key ?? process.env.OPENROUTER_API_KEY, baseURL: cfg.endpoint ?? 'https://openrouter.ai/api/v1' });
|
|
case 'bedrock':
|
|
return new BedrockClient({ model: cfg.model, region: cfg.endpoint, accessKeyId: cfg.api_key, secretAccessKey: cfg.auth_token });
|
|
case 'github':
|
|
return new GitHubModelsClient({
|
|
model: cfg.model,
|
|
apiKey: cfg.api_key,
|
|
endpoint: cfg.endpoint,
|
|
onLoginRequired: async () => {
|
|
const { loginGitHub } = await import('../auth/index.js');
|
|
console.log('\nGitHub authentication required. Starting login flow...');
|
|
return loginGitHub((userCode, verificationUri) => {
|
|
console.log(`\nVisit: ${verificationUri}`);
|
|
console.log(`Enter code: ${userCode}\n`);
|
|
console.log('Waiting for authorization...');
|
|
});
|
|
},
|
|
});
|
|
default:
|
|
throw new Error(`Unknown provider: ${cfg.provider}`);
|
|
}
|
|
}
|
|
|
|
const defaultClient = createClient(models.default);
|
|
const fastClient = models.fast ? createClient(models.fast) : undefined;
|
|
const complexClient = models.complex ? createClient(models.complex) : undefined;
|
|
const localClient = models.local ? createClient(models.local) : undefined;
|
|
|
|
const fallbackChain = [];
|
|
for (const providerName of models.fallback_chain) {
|
|
if (providerName === 'openai') {
|
|
fallbackChain.push(new OpenAIClient({ model: 'gpt-4o' }));
|
|
} else if (providerName === 'local' && localClient) {
|
|
fallbackChain.push(localClient);
|
|
}
|
|
}
|
|
|
|
const modelRouter = new ModelRouter({
|
|
default: defaultClient,
|
|
fast: fastClient,
|
|
complex: complexClient,
|
|
local: localClient,
|
|
fallbackChain,
|
|
});
|
|
|
|
const systemPrompt = loadSystemPrompt();
|
|
|
|
const hookEngine = new HookEngine(config.hooks);
|
|
const toolRegistry = new ToolRegistry();
|
|
for (const tool of allBuiltinTools) {
|
|
toolRegistry.register(tool);
|
|
}
|
|
|
|
// Register web search tools if configured with credentials
|
|
if (config.web_search.api_key || config.web_search.endpoint) {
|
|
for (const tool of createWebSearchTools({
|
|
provider: config.web_search.provider,
|
|
apiKey: config.web_search.api_key,
|
|
endpoint: config.web_search.endpoint,
|
|
maxResults: config.web_search.max_results,
|
|
})) {
|
|
toolRegistry.register(tool);
|
|
}
|
|
}
|
|
|
|
// Initialize process manager and register process tools
|
|
const processManager = new ProcessManager({
|
|
maxConcurrent: config.process.max_concurrent,
|
|
maxRuntimeMinutes: config.process.max_runtime_minutes,
|
|
bufferSize: config.process.buffer_size,
|
|
});
|
|
|
|
for (const tool of createProcessTools(processManager)) {
|
|
toolRegistry.register(tool);
|
|
}
|
|
|
|
const toolExecutor = new ToolExecutor(toolRegistry, hookEngine);
|
|
|
|
const session = sessionManager.getSession('tui', 'local');
|
|
|
|
const agent = new NativeAgent({
|
|
modelClient: modelRouter,
|
|
systemPrompt,
|
|
session,
|
|
toolRegistry,
|
|
toolExecutor,
|
|
onToolUse: (event) => {
|
|
if (event.type === 'start') {
|
|
const argsStr = event.args ? ` ${toolColors.dim}${JSON.stringify(event.args)}${toolColors.reset}` : '';
|
|
process.stdout.write(`${toolColors.cyan}> ${event.tool}${toolColors.reset}${argsStr}\n`);
|
|
} else if (event.type === 'end' && event.result) {
|
|
const icon = event.result.success ? `${toolColors.green}done` : `${toolColors.red}error`;
|
|
const detail = event.result.success
|
|
? `${toolColors.dim}(${event.result.output.split('\n').length} lines)${toolColors.reset}`
|
|
: `${toolColors.dim}${event.result.error ?? 'unknown error'}${toolColors.reset}`;
|
|
process.stdout.write(` ${icon}${toolColors.reset} ${detail}\n`);
|
|
}
|
|
},
|
|
});
|
|
|
|
const cleanup = () => {
|
|
processManager.shutdown();
|
|
sessionStore.close();
|
|
};
|
|
|
|
process.on('SIGINT', () => {
|
|
cleanup();
|
|
process.exit(0);
|
|
});
|
|
|
|
if (opts.fullscreen) {
|
|
await startFullscreenTui({
|
|
session,
|
|
modelClient: modelRouter,
|
|
modelRouter,
|
|
systemPrompt,
|
|
model: config.models.default.model,
|
|
onExit: cleanup,
|
|
});
|
|
} else {
|
|
let switchingToFullscreen = false;
|
|
|
|
const tui = new MinimalTui({
|
|
session,
|
|
modelClient: modelRouter,
|
|
modelRouter,
|
|
systemPrompt,
|
|
agent,
|
|
localProviders: config.models.local_providers,
|
|
currentLocalProvider: config.models.local?.provider,
|
|
onTransfer: (target) => {
|
|
if (target === 'telegram') {
|
|
const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
|
|
sessionManager.transferSession('tui', 'local', 'telegram', telegramUserId);
|
|
console.log(`Session transferred to Telegram (${telegramUserId})\n`);
|
|
} else {
|
|
console.log(`Unknown transfer target: ${target}\n`);
|
|
}
|
|
},
|
|
onFullscreen: () => {
|
|
switchingToFullscreen = true;
|
|
tui.stop(true);
|
|
},
|
|
});
|
|
|
|
await tui.start();
|
|
|
|
if (switchingToFullscreen) {
|
|
console.clear();
|
|
await startFullscreenTui({
|
|
session,
|
|
modelClient: modelRouter,
|
|
modelRouter,
|
|
systemPrompt,
|
|
model: config.models.default.model,
|
|
onExit: cleanup,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
cleanup();
|
|
});
|
|
}
|