feat: add TUI entry point with minimal readline mode

This commit is contained in:
William Valentin
2026-02-05 00:37:40 -08:00
parent f792f8407a
commit c0deeb5cf0
2 changed files with 123 additions and 0 deletions
+2
View File
@@ -8,6 +8,8 @@
"build": "tsc", "build": "tsc",
"dev": "tsx watch src/index.ts", "dev": "tsx watch src/index.ts",
"start": "node dist/index.js", "start": "node dist/index.js",
"tui": "tsx src/tui.ts",
"tui:dev": "tsx watch src/tui.ts",
"test": "vitest", "test": "vitest",
"test:run": "vitest run", "test:run": "vitest run",
"lint": "eslint src/", "lint": "eslint src/",
+121
View File
@@ -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);
});