From c0deeb5cf080b54727962adac52e8445ee6d3311 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 00:37:40 -0800 Subject: [PATCH] feat: add TUI entry point with minimal readline mode --- package.json | 2 + src/tui.ts | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/tui.ts diff --git a/package.json b/package.json index 5735e2a..31ac74b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "build": "tsc", "dev": "tsx watch src/index.ts", "start": "node dist/index.js", + "tui": "tsx src/tui.ts", + "tui:dev": "tsx watch src/tui.ts", "test": "vitest", "test:run": "vitest run", "lint": "eslint src/", diff --git a/src/tui.ts b/src/tui.ts new file mode 100644 index 0000000..38109e0 --- /dev/null +++ b/src/tui.ts @@ -0,0 +1,121 @@ +import { loadConfig } from './config/index.js'; +import { SessionStore, SessionManager } from './session/index.js'; +import { AnthropicClient, OpenAIClient, OllamaClient, ModelRouter } from './models/index.js'; +import { MinimalTui } from './frontends/tui/index.js'; +import type { Config } from './config/index.js'; +import { resolve } from 'path'; +import { homedir } from 'os'; +import { existsSync, mkdirSync } from 'fs'; + +const CONFIG_PATH = process.env.FLYNN_CONFIG + ?? resolve(homedir(), '.config/flynn/config.yaml'); + +const SYSTEM_PROMPT = `You are Flynn, a helpful personal AI assistant. You are direct, concise, and helpful. You can help with a variety of tasks including answering questions, providing information, and having conversations. + +Keep responses focused and avoid unnecessary verbosity. Use markdown formatting when it improves readability.`; + +function createModelRouter(config: Config): ModelRouter { + const models = config.models; + + const defaultClient = new AnthropicClient({ + model: models.default.model, + }); + + let fastClient; + let complexClient; + let localClient; + + if (models.fast) { + fastClient = new AnthropicClient({ model: models.fast.model }); + } + + if (models.complex) { + complexClient = new AnthropicClient({ model: models.complex.model }); + } + + if (models.local) { + if (models.local.provider === 'ollama') { + localClient = new OllamaClient({ + model: models.local.model, + host: models.local.endpoint, + }); + } + } + + 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() { + 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); + + // Get TUI session + const session = sessionManager.getSession('tui', 'local'); + + // Create and start minimal TUI + const tui = new MinimalTui({ + session, + modelClient: modelRouter, + systemPrompt: SYSTEM_PROMPT, + 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: () => { + console.log('Fullscreen mode not yet implemented.\n'); + }, + }); + + // Handle shutdown + process.on('SIGINT', () => { + tui.stop(); + sessionStore.close(); + process.exit(0); + }); + + await tui.start(); + + // Cleanup + sessionStore.close(); +} + +main().catch((error) => { + console.error('Failed to start TUI:', error); + process.exit(1); +});