diff --git a/src/index.ts b/src/index.ts index a551cc8..194f369 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,2 @@ -import { loadConfig } from './config/index.js'; -import { startDaemon } from './daemon/index.js'; -import { resolve } from 'path'; -import { homedir } from 'os'; -import { existsSync } from 'fs'; - -const CONFIG_PATH = process.env.FLYNN_CONFIG - ?? resolve(homedir(), '.config/flynn/config.yaml'); - -async function main() { - console.log('Flynn starting...'); - console.log(`Loading config from: ${CONFIG_PATH}`); - - if (!existsSync(CONFIG_PATH)) { - console.error(`Config file not found: ${CONFIG_PATH}`); - console.error('Copy config/default.yaml to ~/.config/flynn/config.yaml and configure it.'); - process.exit(1); - } - - try { - const config = loadConfig(CONFIG_PATH); - const daemon = await startDaemon(config); - - console.log(`Allowed Telegram chat IDs: ${config.telegram.allowed_chat_ids.join(', ')}`); - - // Keep process alive - await new Promise((resolve) => { - daemon.lifecycle.onShutdown(async () => resolve()); - }); - } catch (error) { - console.error('Failed to start Flynn:', error); - process.exit(1); - } -} - -main(); +// Legacy entry point — delegates to CLI +import './cli/index.js'; diff --git a/src/tui.ts b/src/tui.ts index b7ea742..e5fa8e9 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,227 +1,7 @@ -import { loadConfig } from './config/index.js'; -import { SessionStore, SessionManager } from './session/index.js'; -import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, ModelRouter } from './models/index.js'; -import { MinimalTui, startFullscreenTui } from './frontends/tui/index.js'; -import { NativeAgent } from './backends/index.js'; -import { ToolRegistry, ToolExecutor, allBuiltinTools } from './tools/index.js'; -import { HookEngine } from './hooks/index.js'; -import type { Config } from './config/index.js'; -import { resolve } from 'path'; -import { homedir } from 'os'; -import { existsSync, mkdirSync, readFileSync } from 'fs'; +// Legacy entry point — delegates to CLI +// When invoked directly, behaves like 'flynn tui' +import { createProgram } from './cli/index.js'; -const CONFIG_PATH = process.env.FLYNN_CONFIG - ?? resolve(homedir(), '.config/flynn/config.yaml'); - -// 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.'; -} - -function createModelRouter(config: Config): ModelRouter { - const models = config.models; - - const defaultClient = new AnthropicClient({ - model: models.default.model, - apiKey: models.default.api_key, - authToken: models.default.auth_token, - }); - - let fastClient; - let complexClient; - let localClient; - - if (models.fast) { - fastClient = new AnthropicClient({ - model: models.fast.model, - apiKey: models.fast.api_key, - authToken: models.fast.auth_token, - }); - } - - if (models.complex) { - complexClient = new AnthropicClient({ - model: models.complex.model, - apiKey: models.complex.api_key, - authToken: models.complex.auth_token, - }); - } - - if (models.local) { - if (models.local.provider === 'ollama') { - localClient = new OllamaClient({ - model: models.local.model, - host: models.local.endpoint, - numGpu: models.local.num_gpu, - }); - } else if (models.local.provider === 'llamacpp') { - localClient = new LlamaCppClient({ - endpoint: models.local.endpoint ?? 'http://localhost:8080', - model: models.local.model, - authToken: models.local.auth_token, - }); - } - } - - 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); - } - } - - return new ModelRouter({ - default: defaultClient, - fast: fastClient, - complex: complexClient, - local: localClient, - fallbackChain, - }); -} - -async function main() { - const args = process.argv.slice(2); - const fullscreenMode = args.includes('--fullscreen') || args.includes('-f'); - - console.log('Flynn TUI starting...'); - - if (!existsSync(CONFIG_PATH)) { - console.error(`Config file not found: ${CONFIG_PATH}`); - console.error('Copy config/default.yaml to ~/.config/flynn/config.yaml and configure it.'); - process.exit(1); - } - - const config = loadConfig(CONFIG_PATH); - - // Ensure data directory exists - const dataDir = resolve(homedir(), '.local/share/flynn'); - mkdirSync(dataDir, { recursive: true }); - - // Initialize components - const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db')); - const sessionManager = new SessionManager(sessionStore); - const modelRouter = createModelRouter(config); - const systemPrompt = loadSystemPrompt(); - - // Initialize tool registry and executor - const hookEngine = new HookEngine(config.hooks); - const toolRegistry = new ToolRegistry(); - for (const tool of allBuiltinTools) { - toolRegistry.register(tool); - } - const toolExecutor = new ToolExecutor(toolRegistry, hookEngine); - - // Get TUI session - const session = sessionManager.getSession('tui', 'local'); - - // Create agent with tools and tool status display - 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 = () => { - sessionStore.close(); - }; - - process.on('SIGINT', () => { - cleanup(); - process.exit(0); - }); - - if (fullscreenMode) { - // Start fullscreen Ink UI - await startFullscreenTui({ - session, - modelClient: modelRouter, - modelRouter, - systemPrompt: systemPrompt, - model: config.models.default.model, - onExit: cleanup, - }); - } else { - // Start minimal readline UI - 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); // Preserve stdin for fullscreen mode - }, - }); - - await tui.start(); - - if (switchingToFullscreen) { - console.clear(); - await startFullscreenTui({ - session, - modelClient: modelRouter, - modelRouter, - systemPrompt, - model: config.models.default.model, - onExit: cleanup, - }); - return; - } - } - - cleanup(); -} - -main().catch((error) => { - console.error('Failed to start TUI:', error); - process.exit(1); -}); +const program = createProgram(); +const args = ['node', 'flynn', 'tui', ...process.argv.slice(2)]; +program.parse(args);