refactor(01-03): extract services/skills/gateway/mcp into services.ts, reduce index.ts to 140 lines
- Extract initSkills(), initMcp(), loadSystemPrompt(), initPairingManager(), createGateway(), startServices() into services.ts - daemon/index.ts reduced from 386 to 140 lines (64% reduction, 87% from 1087 baseline) - Organize imports with section comments (External, Config, Daemon Modules, Infrastructure) - Add section dividers in startDaemon() (Data & Sessions, Core Services, Model & Prompt, Gateway & Channels, Tier 1 Tools, Lifecycle) - Convert unused value imports to type-only imports - DaemonContext interface and re-exports unchanged
This commit is contained in:
+57
-302
@@ -1,32 +1,35 @@
|
|||||||
import { Lifecycle } from './lifecycle.js';
|
// ── External ──
|
||||||
import { createModelRouter } from './models.js';
|
|
||||||
import { initMemory } from './memory.js';
|
|
||||||
import { createMessageRouter } from './routing.js';
|
|
||||||
import { initAgents } from './agents.js';
|
|
||||||
import { initTools } from './tools.js';
|
|
||||||
import { registerChannels } from './channels.js';
|
|
||||||
import type { Config } from '../config/index.js';
|
|
||||||
import type { AudioTranscriptionConfig } from '../models/media.js';
|
|
||||||
import { ModelRouter } from '../models/index.js';
|
|
||||||
import { AgentOrchestrator } from '../backends/index.js';
|
|
||||||
import { OutboundAttachmentCollector } from '../backends/native/attachments.js';
|
|
||||||
import { SessionStore, SessionManager, parseDuration } from '../session/index.js';
|
|
||||||
import { HookEngine } from '../hooks/index.js';
|
|
||||||
import type { ToolRegistry, ToolExecutor, BrowserManager } from '../tools/index.js';
|
|
||||||
import { createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools } from '../tools/index.js';
|
|
||||||
import { GatewayServer } from '../gateway/index.js';
|
|
||||||
import { ChannelRegistry, PairingManager } from '../channels/index.js';
|
|
||||||
import type { CronScheduler } from '../automation/index.js';
|
|
||||||
import { HeartbeatMonitor } from '../automation/index.js';
|
|
||||||
import { McpManager } from '../mcp/index.js';
|
|
||||||
import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js';
|
|
||||||
import { assembleSystemPrompt } from '../prompt/index.js';
|
|
||||||
import type { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
|
|
||||||
import type { SandboxManager } from '../sandbox/index.js';
|
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { mkdirSync } from 'fs';
|
import { mkdirSync } from 'fs';
|
||||||
|
|
||||||
|
// ── Config & Types ──
|
||||||
|
import type { Config } from '../config/index.js';
|
||||||
|
import type { AudioTranscriptionConfig } from '../models/media.js';
|
||||||
|
import type { ToolRegistry, ToolExecutor, BrowserManager } from '../tools/index.js';
|
||||||
|
import type { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
|
||||||
|
import type { SandboxManager } from '../sandbox/index.js';
|
||||||
|
|
||||||
|
// ── Daemon Modules ──
|
||||||
|
import { Lifecycle } from './lifecycle.js';
|
||||||
|
import { createModelRouter } from './models.js';
|
||||||
|
import { initMemory } from './memory.js';
|
||||||
|
import { initTools } from './tools.js';
|
||||||
|
import { initAgents } from './agents.js';
|
||||||
|
import { createMessageRouter } from './routing.js';
|
||||||
|
import { registerChannels } from './channels.js';
|
||||||
|
import { initSkills, initMcp, loadSystemPrompt, initPairingManager, createGateway, startServices } from './services.js';
|
||||||
|
|
||||||
|
// ── Infrastructure ──
|
||||||
|
import type { ModelRouter } from '../models/index.js';
|
||||||
|
import { SessionStore, SessionManager, parseDuration } from '../session/index.js';
|
||||||
|
import { HookEngine } from '../hooks/index.js';
|
||||||
|
import { createSessionTools, createAgentsListTool, createMessageSendTool, createCronTools } from '../tools/index.js';
|
||||||
|
import { ChannelRegistry } from '../channels/index.js';
|
||||||
|
import type { McpManager } from '../mcp/index.js';
|
||||||
|
import type { SkillRegistry, SkillInstaller } from '../skills/index.js';
|
||||||
|
import type { GatewayServer } from '../gateway/index.js';
|
||||||
|
|
||||||
export interface DaemonContext {
|
export interface DaemonContext {
|
||||||
config: Config;
|
config: Config;
|
||||||
lifecycle: Lifecycle;
|
lifecycle: Lifecycle;
|
||||||
@@ -47,33 +50,13 @@ export interface DaemonContext {
|
|||||||
browserManager?: BrowserManager;
|
browserManager?: BrowserManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSystemPrompt(config: Config): string {
|
|
||||||
const searchDirs = [
|
|
||||||
process.cwd(),
|
|
||||||
resolve(import.meta.dirname, '../..'),
|
|
||||||
...(config.prompt.search_dirs ?? []),
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = assembleSystemPrompt({
|
|
||||||
searchDirs,
|
|
||||||
extraSections: config.prompt.extra_sections,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.loadedFiles.length > 0) {
|
|
||||||
console.log(`Loaded prompt templates: ${result.loadedFiles.map(f => f.split('/').pop()).join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.prompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startDaemon(config: Config): Promise<DaemonContext> {
|
export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||||
const lifecycle = new Lifecycle();
|
const lifecycle = new Lifecycle();
|
||||||
|
|
||||||
// Ensure data directory exists (FLYNN_DATA_DIR overrides default for Docker/custom deployments)
|
// ── Data & Sessions ──
|
||||||
const dataDir = process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn');
|
const dataDir = process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn');
|
||||||
mkdirSync(dataDir, { recursive: true });
|
mkdirSync(dataDir, { recursive: true });
|
||||||
|
|
||||||
// Initialize session store and manager
|
|
||||||
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
|
const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db'));
|
||||||
const sessionManager = new SessionManager(sessionStore);
|
const sessionManager = new SessionManager(sessionStore);
|
||||||
|
|
||||||
@@ -82,304 +65,76 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
|
|||||||
console.log('Session store closed');
|
console.log('Session store closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Session pruning timer (TTL-based cleanup)
|
|
||||||
const ttlMs = parseDuration(config.sessions?.ttl ?? '30d');
|
const ttlMs = parseDuration(config.sessions?.ttl ?? '30d');
|
||||||
if (ttlMs) {
|
if (ttlMs) {
|
||||||
const pruneInterval = setInterval(() => {
|
const pruneInterval = setInterval(() => {
|
||||||
const cutoff = Math.floor((Date.now() - ttlMs) / 1000); // created_at is unix seconds
|
const cutoff = Math.floor((Date.now() - ttlMs) / 1000);
|
||||||
const pruned = sessionStore.pruneStale(cutoff);
|
const pruned = sessionStore.pruneStale(cutoff);
|
||||||
if (pruned.length > 0) {
|
if (pruned.length > 0) {
|
||||||
sessionManager.evictSessions(pruned);
|
sessionManager.evictSessions(pruned);
|
||||||
console.log(`Pruned ${pruned.length} stale session(s) (TTL: ${config.sessions?.ttl ?? '30d'})`);
|
console.log(`Pruned ${pruned.length} stale session(s) (TTL: ${config.sessions?.ttl ?? '30d'})`);
|
||||||
}
|
}
|
||||||
}, 3_600_000); // every hour
|
}, 3_600_000);
|
||||||
|
lifecycle.onShutdown(async () => { clearInterval(pruneInterval); });
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
clearInterval(pruneInterval);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize hook engine
|
// ── Core Services ──
|
||||||
const hookEngine = new HookEngine(config.hooks);
|
const hookEngine = new HookEngine(config.hooks);
|
||||||
|
|
||||||
// Initialize tool registry, executor, web search, process tools, browser tools, and tool policy
|
|
||||||
const { toolRegistry, toolExecutor, browserManager } = initTools({ config, lifecycle, hookEngine });
|
const { toolRegistry, toolExecutor, browserManager } = initTools({ config, lifecycle, hookEngine });
|
||||||
|
const { memoryStore, memoryDir } = await initMemory({ config, dataDir, lifecycle, toolRegistry });
|
||||||
// Initialize memory store, vector search, and memory tools
|
const mcpManager = await initMcp(config, lifecycle, toolRegistry);
|
||||||
const { memoryStore, hybridSearch, memoryDir } = await initMemory({ config, dataDir, lifecycle, toolRegistry });
|
const { skillRegistry, skillInstaller } = initSkills(config);
|
||||||
|
|
||||||
// Initialize MCP manager and start configured servers
|
|
||||||
const mcpManager = new McpManager(toolRegistry);
|
|
||||||
|
|
||||||
if (config.mcp.servers.length > 0) {
|
|
||||||
console.log(`Starting ${config.mcp.servers.length} MCP server(s)...`);
|
|
||||||
await mcpManager.startAll(config.mcp.servers);
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
await mcpManager.stopAll();
|
|
||||||
console.log('MCP servers stopped');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize skills system
|
|
||||||
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
|
|
||||||
const skillRegistry = new SkillRegistry();
|
|
||||||
const skillInstaller = new SkillInstaller(config.skills.managed_dir ?? defaultManagedDir);
|
|
||||||
|
|
||||||
const skills = loadAllSkills({
|
|
||||||
bundledDir: config.skills.bundled_dir,
|
|
||||||
managedDir: config.skills.managed_dir ?? defaultManagedDir,
|
|
||||||
workspaceDir: config.skills.workspace_dir,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const skill of skills) {
|
|
||||||
skillRegistry.register(skill);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skills.length > 0) {
|
|
||||||
const available = skillRegistry.listAvailable().length;
|
|
||||||
console.log(`Loaded ${skills.length} skill(s) (${available} available)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize agent config registry, router, and sandbox manager
|
|
||||||
const { agentConfigRegistry, agentRouter, sandboxManager } = await initAgents({ config, lifecycle });
|
const { agentConfigRegistry, agentRouter, sandboxManager } = await initAgents({ config, lifecycle });
|
||||||
|
|
||||||
// Initialize audio transcription config
|
// ── Model & Prompt ──
|
||||||
const audioConfig: AudioTranscriptionConfig = {
|
const audioConfig: AudioTranscriptionConfig = {
|
||||||
endpoint: config.audio.transcription_endpoint,
|
endpoint: config.audio.transcription_endpoint,
|
||||||
apiKey: config.audio.transcription_api_key,
|
apiKey: config.audio.transcription_api_key,
|
||||||
model: config.audio.transcription_model,
|
model: config.audio.transcription_model,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize model router
|
|
||||||
const modelRouter = createModelRouter(config);
|
const modelRouter = createModelRouter(config);
|
||||||
|
const systemPrompt = loadSystemPrompt(config, skillRegistry);
|
||||||
|
|
||||||
// Load system prompt and append skill instructions
|
// ── Gateway & Channels ──
|
||||||
let systemPrompt = loadSystemPrompt(config);
|
|
||||||
const skillAdditions = skillRegistry.getSystemPromptAdditions();
|
|
||||||
if (skillAdditions) {
|
|
||||||
systemPrompt = `${systemPrompt}\n\n# Available Skills\n\n${skillAdditions}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize channel registry (created early so the gateway can reference it)
|
|
||||||
const channelRegistry = new ChannelRegistry();
|
const channelRegistry = new ChannelRegistry();
|
||||||
|
const pairingManager = initPairingManager(config);
|
||||||
|
|
||||||
// Create PairingManager if pairing is enabled
|
let channelAgents: ReturnType<typeof createMessageRouter>['agents'] | null = null;
|
||||||
let pairingManager: PairingManager | undefined;
|
|
||||||
if (config.pairing.enabled) {
|
|
||||||
// Parse code_ttl: supports '5m', '1h', '30s' → milliseconds
|
|
||||||
const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/);
|
|
||||||
const codeTtlMs = ttlMatch
|
|
||||||
? Number(ttlMatch[1]) * ({ s: 1000, m: 60_000, h: 3_600_000 }[ttlMatch[2] as 's' | 'm' | 'h'])
|
|
||||||
: 5 * 60_000; // default 5 minutes
|
|
||||||
|
|
||||||
pairingManager = new PairingManager({
|
const gateway = createGateway({
|
||||||
enabled: true,
|
config, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
|
||||||
codeTtl: codeTtlMs,
|
channelRegistry, pairingManager, lifecycle,
|
||||||
codeLength: config.pairing.code_length,
|
getChannelAgents: () => channelAgents,
|
||||||
});
|
|
||||||
console.log(`Pairing codes enabled (TTL: ${config.pairing.code_ttl}, length: ${config.pairing.code_length})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable reference to channel agents map — set after createMessageRouter() below.
|
|
||||||
// This allows the gateway's getTokenUsage callback to access channel agent usage data.
|
|
||||||
let channelAgents: Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }> | null = null;
|
|
||||||
|
|
||||||
// Initialize gateway WebSocket server
|
|
||||||
const gateway = new GatewayServer({
|
|
||||||
port: config.server.port,
|
|
||||||
host: config.server.localhost ? '127.0.0.1' : '0.0.0.0',
|
|
||||||
sessionManager,
|
|
||||||
modelClient: modelRouter,
|
|
||||||
systemPrompt,
|
|
||||||
toolRegistry,
|
|
||||||
toolExecutor,
|
|
||||||
auth: {
|
|
||||||
token: config.server.token,
|
|
||||||
tailscaleIdentity: config.server.tailscale_identity,
|
|
||||||
},
|
|
||||||
authHttp: config.server.auth_http,
|
|
||||||
lock: config.server.lock,
|
|
||||||
uiDir: resolve(import.meta.dirname, '../gateway/ui'),
|
|
||||||
config,
|
|
||||||
channelRegistry,
|
|
||||||
pairingManager,
|
|
||||||
restart: async () => {
|
|
||||||
console.log('Restart requested via gateway');
|
|
||||||
await lifecycle.shutdown();
|
|
||||||
// Exit with code 75 (EX_TEMPFAIL) — process supervisor should restart
|
|
||||||
process.exit(75);
|
|
||||||
},
|
|
||||||
getTokenUsage: () => {
|
|
||||||
const results: Array<{
|
|
||||||
sessionId: string;
|
|
||||||
primary: { inputTokens: number; outputTokens: number; calls: number };
|
|
||||||
delegation: Record<string, { inputTokens: number; outputTokens: number; calls: number }>;
|
|
||||||
total: { inputTokens: number; outputTokens: number; calls: number; estimatedCost: number };
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
// Collect usage from gateway WebSocket sessions (NativeAgent-based)
|
|
||||||
const sessionBridge = gateway.getSessionBridge();
|
|
||||||
for (const entry of sessionBridge.getAllUsage()) {
|
|
||||||
results.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect usage from channel agents (AgentOrchestrator-based, has full delegation data)
|
|
||||||
if (channelAgents) {
|
|
||||||
for (const [sessionId, { orchestrator }] of channelAgents) {
|
|
||||||
const usage = orchestrator.getUsage();
|
|
||||||
results.push({
|
|
||||||
sessionId,
|
|
||||||
primary: usage.primary,
|
|
||||||
delegation: usage.delegation,
|
|
||||||
total: usage.total,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.server.token) {
|
|
||||||
console.log(`Gateway auth: token required${config.server.tailscale_identity ? ' + Tailscale identity' : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Channel Registry ──────────────────────────────────────────
|
|
||||||
|
|
||||||
// Set up the unified message handler
|
|
||||||
const messageRouter = createMessageRouter({
|
const messageRouter = createMessageRouter({
|
||||||
sessionManager,
|
sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
|
||||||
modelRouter,
|
config, memoryStore, agentConfigRegistry, agentRouter, sandboxManager, audioConfig,
|
||||||
systemPrompt,
|
|
||||||
toolRegistry,
|
|
||||||
toolExecutor,
|
|
||||||
config,
|
|
||||||
memoryStore,
|
|
||||||
agentConfigRegistry,
|
|
||||||
agentRouter,
|
|
||||||
sandboxManager,
|
|
||||||
audioConfig,
|
|
||||||
});
|
});
|
||||||
channelRegistry.setMessageHandler(messageRouter.handler);
|
channelRegistry.setMessageHandler(messageRouter.handler);
|
||||||
|
|
||||||
// Wire channel agents into the getTokenUsage callback (late binding)
|
|
||||||
channelAgents = messageRouter.agents;
|
channelAgents = messageRouter.agents;
|
||||||
|
|
||||||
// Register all channel adapters (Telegram, Discord, Slack, WhatsApp, WebChat, cron, webhooks, Gmail)
|
|
||||||
const { cronScheduler } = registerChannels({ config, channelRegistry, hookEngine, pairingManager, gateway });
|
const { cronScheduler } = registerChannels({ config, channelRegistry, hookEngine, pairingManager, gateway });
|
||||||
|
|
||||||
// ── Register Tier 1 agent tools ─────────────────────────────
|
// ── Tier 1 Tools ──
|
||||||
|
for (const tool of createSessionTools(sessionManager)) { toolRegistry.register(tool); }
|
||||||
// Session management tools (list, history, create, delete)
|
|
||||||
for (const tool of createSessionTools(sessionManager)) {
|
|
||||||
toolRegistry.register(tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agent discovery tool
|
|
||||||
toolRegistry.register(createAgentsListTool(agentConfigRegistry));
|
toolRegistry.register(createAgentsListTool(agentConfigRegistry));
|
||||||
|
|
||||||
// Cross-channel messaging tool
|
|
||||||
toolRegistry.register(createMessageSendTool(channelRegistry));
|
toolRegistry.register(createMessageSendTool(channelRegistry));
|
||||||
|
|
||||||
// Cron management tools (if scheduler is active)
|
|
||||||
if (cronScheduler) {
|
if (cronScheduler) {
|
||||||
for (const tool of createCronTools(cronScheduler)) {
|
for (const tool of createCronTools(cronScheduler)) { toolRegistry.register(tool); }
|
||||||
toolRegistry.register(tool);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Signal Handlers ───────────────────────────────────────────
|
// ── Lifecycle ──
|
||||||
|
await startServices({ config, lifecycle, channelRegistry, gateway, modelRouter, memoryDir, dataDir });
|
||||||
const signalHandler = () => {
|
|
||||||
lifecycle.shutdown().then(() => process.exit(0));
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('SIGINT', signalHandler);
|
|
||||||
process.on('SIGTERM', signalHandler);
|
|
||||||
|
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
process.off('SIGINT', signalHandler);
|
|
||||||
process.off('SIGTERM', signalHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Start Services ────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Register shutdown handler for channels (stops Telegram bot etc.)
|
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
await channelRegistry.stopAll();
|
|
||||||
console.log('Channel adapters stopped');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start all channel adapters (Telegram long polling, WebChat status)
|
|
||||||
await channelRegistry.startAll();
|
|
||||||
|
|
||||||
// Start gateway (HTTP + WS server)
|
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
await gateway.stop();
|
|
||||||
console.log('Gateway server stopped');
|
|
||||||
});
|
|
||||||
|
|
||||||
await gateway.start();
|
|
||||||
|
|
||||||
// ── Tailscale Serve ────────────────────────────────────────────
|
|
||||||
if (config.server.tailscale?.serve) {
|
|
||||||
const { startTailscaleServe, stopTailscaleServe } = await import('../gateway/tailscale.js');
|
|
||||||
const tsConfig = {
|
|
||||||
localPort: config.server.port,
|
|
||||||
servePort: config.server.tailscale.port,
|
|
||||||
hostname: config.server.tailscale.hostname,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await startTailscaleServe(tsConfig);
|
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
await stopTailscaleServe(tsConfig);
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
console.warn('Tailscale Serve failed to start — gateway still accessible on local port');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Heartbeat Monitor ──────────────────────────────────────────
|
|
||||||
const heartbeatMonitor = new HeartbeatMonitor({
|
|
||||||
config: config.automation.heartbeat,
|
|
||||||
getGatewayPort: () => config.server.port,
|
|
||||||
modelRouter,
|
|
||||||
channelLister: channelRegistry,
|
|
||||||
memoryDir: config.memory.enabled ? memoryDir : undefined,
|
|
||||||
dataDir,
|
|
||||||
channelLookup: channelRegistry,
|
|
||||||
});
|
|
||||||
|
|
||||||
heartbeatMonitor.start();
|
|
||||||
|
|
||||||
lifecycle.onShutdown(async () => {
|
|
||||||
heartbeatMonitor.stop();
|
|
||||||
console.log('Heartbeat monitor stopped');
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Flynn daemon started');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config, lifecycle, sessionStore, sessionManager, hookEngine, modelRouter,
|
||||||
lifecycle,
|
toolRegistry, toolExecutor, gateway, channelRegistry, mcpManager,
|
||||||
sessionStore,
|
skillRegistry, skillInstaller, agentConfigRegistry, agentRouter,
|
||||||
sessionManager,
|
sandboxManager, browserManager,
|
||||||
hookEngine,
|
|
||||||
modelRouter,
|
|
||||||
toolRegistry,
|
|
||||||
toolExecutor,
|
|
||||||
gateway,
|
|
||||||
channelRegistry,
|
|
||||||
mcpManager,
|
|
||||||
skillRegistry,
|
|
||||||
skillInstaller,
|
|
||||||
agentConfigRegistry,
|
|
||||||
agentRouter,
|
|
||||||
sandboxManager,
|
|
||||||
browserManager,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Re-exports for backward compatibility ──
|
||||||
export { Lifecycle } from './lifecycle.js';
|
export { Lifecycle } from './lifecycle.js';
|
||||||
export { createClientFromConfig, anthropicToGitHubModel, createAutoFallbackClient, createModelRouter } from './models.js';
|
export { createClientFromConfig, anthropicToGitHubModel, createAutoFallbackClient, createModelRouter } from './models.js';
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import type { Config } from '../config/index.js';
|
||||||
|
import type { Lifecycle } from './lifecycle.js';
|
||||||
|
import type { ToolRegistry, ToolExecutor } from '../tools/index.js';
|
||||||
|
import type { AgentOrchestrator } from '../backends/index.js';
|
||||||
|
import type { OutboundAttachmentCollector } from '../backends/native/attachments.js';
|
||||||
|
import { ModelRouter } from '../models/index.js';
|
||||||
|
import { SessionManager } from '../session/index.js';
|
||||||
|
import { GatewayServer } from '../gateway/index.js';
|
||||||
|
import { ChannelRegistry, PairingManager } from '../channels/index.js';
|
||||||
|
import { HeartbeatMonitor } from '../automation/index.js';
|
||||||
|
import { McpManager } from '../mcp/index.js';
|
||||||
|
import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js';
|
||||||
|
import { assembleSystemPrompt } from '../prompt/index.js';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
|
||||||
|
// ── Skills ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface SkillsResult {
|
||||||
|
skillRegistry: SkillRegistry;
|
||||||
|
skillInstaller: SkillInstaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSkills(config: Config): SkillsResult {
|
||||||
|
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
|
||||||
|
const skillRegistry = new SkillRegistry();
|
||||||
|
const skillInstaller = new SkillInstaller(config.skills.managed_dir ?? defaultManagedDir);
|
||||||
|
|
||||||
|
const skills = loadAllSkills({
|
||||||
|
bundledDir: config.skills.bundled_dir,
|
||||||
|
managedDir: config.skills.managed_dir ?? defaultManagedDir,
|
||||||
|
workspaceDir: config.skills.workspace_dir,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const skill of skills) {
|
||||||
|
skillRegistry.register(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skills.length > 0) {
|
||||||
|
const available = skillRegistry.listAvailable().length;
|
||||||
|
console.log(`Loaded ${skills.length} skill(s) (${available} available)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { skillRegistry, skillInstaller };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MCP ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function initMcp(config: Config, lifecycle: Lifecycle, toolRegistry: ToolRegistry): Promise<McpManager> {
|
||||||
|
const mcpManager = new McpManager(toolRegistry);
|
||||||
|
|
||||||
|
if (config.mcp.servers.length > 0) {
|
||||||
|
console.log(`Starting ${config.mcp.servers.length} MCP server(s)...`);
|
||||||
|
await mcpManager.startAll(config.mcp.servers);
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
await mcpManager.stopAll();
|
||||||
|
console.log('MCP servers stopped');
|
||||||
|
});
|
||||||
|
|
||||||
|
return mcpManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── System Prompt ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function loadSystemPrompt(config: Config, skillRegistry: SkillRegistry): string {
|
||||||
|
const searchDirs = [
|
||||||
|
process.cwd(),
|
||||||
|
resolve(import.meta.dirname, '../..'),
|
||||||
|
...(config.prompt.search_dirs ?? []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = assembleSystemPrompt({
|
||||||
|
searchDirs,
|
||||||
|
extraSections: config.prompt.extra_sections,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.loadedFiles.length > 0) {
|
||||||
|
console.log(`Loaded prompt templates: ${result.loadedFiles.map(f => f.split('/').pop()).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = result.prompt;
|
||||||
|
const skillAdditions = skillRegistry.getSystemPromptAdditions();
|
||||||
|
if (skillAdditions) {
|
||||||
|
prompt = `${prompt}\n\n# Available Skills\n\n${skillAdditions}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pairing Manager ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function initPairingManager(config: Config): PairingManager | undefined {
|
||||||
|
if (!config.pairing.enabled) return undefined;
|
||||||
|
|
||||||
|
const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/);
|
||||||
|
const codeTtlMs = ttlMatch
|
||||||
|
? Number(ttlMatch[1]) * ({ s: 1000, m: 60_000, h: 3_600_000 }[ttlMatch[2] as 's' | 'm' | 'h'])
|
||||||
|
: 5 * 60_000;
|
||||||
|
|
||||||
|
const manager = new PairingManager({
|
||||||
|
enabled: true,
|
||||||
|
codeTtl: codeTtlMs,
|
||||||
|
codeLength: config.pairing.code_length,
|
||||||
|
});
|
||||||
|
console.log(`Pairing codes enabled (TTL: ${config.pairing.code_ttl}, length: ${config.pairing.code_length})`);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gateway ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface GatewayDeps {
|
||||||
|
config: Config;
|
||||||
|
sessionManager: SessionManager;
|
||||||
|
modelRouter: ModelRouter;
|
||||||
|
systemPrompt: string;
|
||||||
|
toolRegistry: ToolRegistry;
|
||||||
|
toolExecutor: ToolExecutor;
|
||||||
|
channelRegistry: ChannelRegistry;
|
||||||
|
pairingManager?: PairingManager;
|
||||||
|
lifecycle: Lifecycle;
|
||||||
|
getChannelAgents: () => Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGateway(deps: GatewayDeps): GatewayServer {
|
||||||
|
const { config, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor, channelRegistry, pairingManager, lifecycle, getChannelAgents } = deps;
|
||||||
|
|
||||||
|
const gateway = new GatewayServer({
|
||||||
|
port: config.server.port,
|
||||||
|
host: config.server.localhost ? '127.0.0.1' : '0.0.0.0',
|
||||||
|
sessionManager,
|
||||||
|
modelClient: modelRouter,
|
||||||
|
systemPrompt,
|
||||||
|
toolRegistry,
|
||||||
|
toolExecutor,
|
||||||
|
auth: {
|
||||||
|
token: config.server.token,
|
||||||
|
tailscaleIdentity: config.server.tailscale_identity,
|
||||||
|
},
|
||||||
|
authHttp: config.server.auth_http,
|
||||||
|
lock: config.server.lock,
|
||||||
|
uiDir: resolve(import.meta.dirname, '../gateway/ui'),
|
||||||
|
config,
|
||||||
|
channelRegistry,
|
||||||
|
pairingManager,
|
||||||
|
restart: async () => {
|
||||||
|
console.log('Restart requested via gateway');
|
||||||
|
await lifecycle.shutdown();
|
||||||
|
process.exit(75);
|
||||||
|
},
|
||||||
|
getTokenUsage: () => {
|
||||||
|
const results: Array<{
|
||||||
|
sessionId: string;
|
||||||
|
primary: { inputTokens: number; outputTokens: number; calls: number };
|
||||||
|
delegation: Record<string, { inputTokens: number; outputTokens: number; calls: number }>;
|
||||||
|
total: { inputTokens: number; outputTokens: number; calls: number; estimatedCost: number };
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
const sessionBridge = gateway.getSessionBridge();
|
||||||
|
for (const entry of sessionBridge.getAllUsage()) {
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelAgents = getChannelAgents();
|
||||||
|
if (channelAgents) {
|
||||||
|
for (const [sessionId, { orchestrator }] of channelAgents) {
|
||||||
|
const usage = orchestrator.getUsage();
|
||||||
|
results.push({
|
||||||
|
sessionId,
|
||||||
|
primary: usage.primary,
|
||||||
|
delegation: usage.delegation,
|
||||||
|
total: usage.total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.server.token) {
|
||||||
|
console.log(`Gateway auth: token required${config.server.tailscale_identity ? ' + Tailscale identity' : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Service Startup ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function startServices(deps: {
|
||||||
|
config: Config;
|
||||||
|
lifecycle: Lifecycle;
|
||||||
|
channelRegistry: ChannelRegistry;
|
||||||
|
gateway: GatewayServer;
|
||||||
|
modelRouter: ModelRouter;
|
||||||
|
memoryDir: string;
|
||||||
|
dataDir: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { config, lifecycle, channelRegistry, gateway, modelRouter, memoryDir, dataDir } = deps;
|
||||||
|
|
||||||
|
// Register shutdown handler for channels
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
await channelRegistry.stopAll();
|
||||||
|
console.log('Channel adapters stopped');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start all channel adapters
|
||||||
|
await channelRegistry.startAll();
|
||||||
|
|
||||||
|
// Start gateway (HTTP + WS server)
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
await gateway.stop();
|
||||||
|
console.log('Gateway server stopped');
|
||||||
|
});
|
||||||
|
|
||||||
|
await gateway.start();
|
||||||
|
|
||||||
|
// Tailscale Serve
|
||||||
|
if (config.server.tailscale?.serve) {
|
||||||
|
const { startTailscaleServe, stopTailscaleServe } = await import('../gateway/tailscale.js');
|
||||||
|
const tsConfig = {
|
||||||
|
localPort: config.server.port,
|
||||||
|
servePort: config.server.tailscale.port,
|
||||||
|
hostname: config.server.tailscale.hostname,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await startTailscaleServe(tsConfig);
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
await stopTailscaleServe(tsConfig);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
console.warn('Tailscale Serve failed to start — gateway still accessible on local port');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat Monitor
|
||||||
|
const heartbeatMonitor = new HeartbeatMonitor({
|
||||||
|
config: config.automation.heartbeat,
|
||||||
|
getGatewayPort: () => config.server.port,
|
||||||
|
modelRouter,
|
||||||
|
channelLister: channelRegistry,
|
||||||
|
memoryDir: config.memory.enabled ? memoryDir : undefined,
|
||||||
|
dataDir,
|
||||||
|
channelLookup: channelRegistry,
|
||||||
|
});
|
||||||
|
|
||||||
|
heartbeatMonitor.start();
|
||||||
|
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
heartbeatMonitor.stop();
|
||||||
|
console.log('Heartbeat monitor stopped');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal handlers
|
||||||
|
const signalHandler = () => {
|
||||||
|
lifecycle.shutdown().then(() => process.exit(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', signalHandler);
|
||||||
|
process.on('SIGTERM', signalHandler);
|
||||||
|
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
process.off('SIGINT', signalHandler);
|
||||||
|
process.off('SIGTERM', signalHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Flynn daemon started');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user