feat(cli): implement tui command wrapping existing TUI logic
This commit is contained in:
+201
-2
@@ -1,4 +1,31 @@
|
|||||||
import type { Command } from 'commander';
|
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 {
|
export function registerTuiCommand(program: Command): void {
|
||||||
program
|
program
|
||||||
@@ -6,8 +33,180 @@ export function registerTuiCommand(program: Command): void {
|
|||||||
.description('Launch the interactive TUI')
|
.description('Launch the interactive TUI')
|
||||||
.option('-f, --fullscreen', 'Start in fullscreen mode')
|
.option('-f, --fullscreen', 'Start in fullscreen mode')
|
||||||
.option('-c, --config <path>', 'Config file path')
|
.option('-c, --config <path>', 'Config file path')
|
||||||
.action(async (_opts: { fullscreen?: boolean; config?: string }) => {
|
.action(async (opts: { fullscreen?: boolean; config?: string }) => {
|
||||||
console.error('Not yet implemented');
|
const configPath = opts.config ?? getConfigPath();
|
||||||
|
const { config, error } = loadConfigSafe(configPath);
|
||||||
|
if (!config) {
|
||||||
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic imports to keep CLI startup fast
|
||||||
|
const { SessionStore, SessionManager } = await import('../session/index.js');
|
||||||
|
const { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, 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 } = 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;
|
||||||
|
|
||||||
|
// Build model router
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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 = () => {
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user