UNPKG

@iqai/adk-cli

Version:

CLI tool for creating, running, and testing ADK-TS agents

281 lines 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.startHttpServer = startHttpServer; require("reflect-metadata"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const core_1 = require("@nestjs/core"); const swagger_1 = require("@nestjs/swagger"); const schema_1 = require("../common/schema"); const pretty_error_filter_1 = require("./filters/pretty-error.filter"); const http_module_1 = require("./http.module"); const agent_manager_service_1 = require("./providers/agent-manager.service"); const agent_scanner_service_1 = require("./providers/agent-scanner.service"); const hot_reload_service_1 = require("./reload/hot-reload.service"); /** * Resolves the path to the bundled web assets directory. * The web/ directory is at the package root, relative to dist/http/bootstrap.js */ function getWebAssetsDir() { // In CommonJS build, __dirname is available // Path: dist/http/bootstrap.js -> ../../web const currentDir = __dirname; return (0, node_path_1.join)(currentDir, "..", "..", "web"); } function pathHasSkippedDir(p) { const parts = p.split(node_path_1.sep).filter(Boolean); return parts.some((part) => agent_scanner_service_1.DIRECTORIES_TO_SKIP.includes(part)); } function loadGitignorePrefixes(rootDir) { try { const igPath = (0, node_path_1.resolve)(rootDir, ".gitignore"); if (!(0, node_fs_1.existsSync)(igPath)) return []; const lines = (0, node_fs_1.readFileSync)(igPath, "utf8").split("\n"); const prefixes = []; for (const raw of lines) { const line = raw.trim(); if (!line || line.startsWith("#")) continue; if (/[?*[\]]/.test(line)) continue; const normalized = line.replace(/\/+$/, ""); const abs = (0, node_path_1.resolve)(rootDir, normalized); prefixes.push(abs + node_path_1.sep); } return prefixes; } catch { return []; } } function shouldIgnorePath(fullPath, prefixes) { if (pathHasSkippedDir(fullPath)) return true; for (const pref of prefixes) { if (fullPath.startsWith(pref)) return true; } return false; } /** * Setup hot reload file watching with .gitignore filtering and well-known directory skips. * Returns watcher/timeout references and a teardown function to close resources. */ function setupHotReload(agentManager, hotReload, config, env) { const watchers = []; const debouncers = []; const shouldWatch = config.hotReload ?? env.NODE_ENV !== schema_1.environmentEnum.enum.production; const debug = env.ADK_DEBUG; if (!shouldWatch) { return { watchers, debouncers, teardownHotReload: () => { } }; } const rootDir = process.cwd(); const gitignorePrefixes = loadGitignorePrefixes(rootDir); const rawPaths = Array.isArray(config.watchPaths) && config.watchPaths.length > 0 ? config.watchPaths : [rootDir]; const paths = rawPaths.filter(Boolean).map((p) => (0, node_path_1.resolve)(p)); for (const p of paths) { try { const watcher = (0, node_fs_1.watch)(p, { recursive: true }, (_event, filename) => { const fullPath = typeof filename === "string" ? (0, node_path_1.resolve)(p, filename) : p; if (shouldIgnorePath(fullPath, gitignorePrefixes)) { if (!config.quiet && debug) console.log(`[hot-reload] Ignored change in ${fullPath}`); return; } while (debouncers.length) { const t = debouncers.pop(); if (t) clearTimeout(t); } const t = setTimeout(async () => { try { const stateChanged = await agentManager.hasInitialStateChanged(); if (stateChanged) { if (!config.quiet && debug) console.log("[hot-reload] Initial state changed - performing full reload (sessions will be cleared)"); agentManager.stopAllAgents(); agentManager.scanAgents(config.agentsDir); for (const agentPath of agentManager.getAgents().keys()) { try { await agentManager.startAgent(agentPath, undefined, true); if (!config.quiet && debug) console.log(`[hot-reload] Full reload completed for ${agentPath}`); } catch (e) { if (!config.quiet) console.error(`[hot-reload] Failed to full reload agent ${agentPath}:`, e); } } } else { const preservedSessions = agentManager.getLoadedAgentSessions(); if (!config.quiet && debug) console.log(`[hot-reload] Code changed - preserving ${preservedSessions.size} session(s)`); agentManager.stopAllAgents(); agentManager.scanAgents(config.agentsDir); for (const [agentPath, sessionId,] of preservedSessions.entries()) { try { await agentManager.startAgent(agentPath, sessionId); if (!config.quiet && debug) console.log(`[hot-reload] Restored session ${sessionId} for ${agentPath}`); } catch (e) { if (!config.quiet) console.error(`[hot-reload] Failed to restore agent ${agentPath}:`, e); } } } if (!config.quiet && debug) console.log(`[hot-reload] Reloaded agents after change in ${filename ?? p}`); try { hotReload?.broadcastReload(typeof filename === "string" ? filename : null); } catch (e) { if (debug) console.warn("[hot-reload] Failed to broadcast reload message", e); } } catch (e) { console.error("[hot-reload] Error during reload:", e); } }, 300); debouncers.push(t); }); watchers.push(watcher); if (!config.quiet && debug) console.log(`[hot-reload] Watching ${p}`); } catch (e) { console.warn(`[hot-reload] Failed to watch ${p}: ${e instanceof Error ? e.message : String(e)}`); } } const teardownHotReload = () => { for (const t of debouncers) clearTimeout(t); for (const w of watchers) { try { w.close(); } catch { } } try { hotReload?.closeAll(); } catch { } }; return { watchers, debouncers, teardownHotReload }; } /** * Start a Nest Express HTTP server with the ADK-TS controllers and providers. * Mirrors previous Hono server endpoints: * - GET /health * - /api/agents ... * - /api/agents/:id/sessions ... */ async function startHttpServer(config) { const env = schema_1.envSchema.parse(process.env); const debug = env.ADK_DEBUG; const app = await core_1.NestFactory.create(http_module_1.HttpModule.register(config), { logger: debug ? ["log", "error", "warn", "debug", "verbose"] : ["error", "warn"], }); // Apply global exception filter for pretty error formatting const showStackTraces = process.env.ADK_DEBUG_NEST === "1" || process.env.NODE_ENV !== "production"; app.useGlobalFilters(new pretty_error_filter_1.PrettyErrorFilter(showStackTraces)); // CORS parity with previous Hono app.use("/*", cors()) app.enableCors({ origin: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"], }); const bodyLimit = env.ADK_HTTP_BODY_LIMIT; app.useBodyParser("json", { limit: bodyLimit }); app.useBodyParser("urlencoded", { limit: bodyLimit, extended: true }); const agentManager = app.get(agent_manager_service_1.AgentManager, { strict: false }); const hotReload = app.get(hot_reload_service_1.HotReloadService, { strict: false }); agentManager.scanAgents(config.agentsDir); const enableSwagger = config.swagger ?? env.NODE_ENV !== schema_1.environmentEnum.enum.production; if (enableSwagger) { const builder = new swagger_1.DocumentBuilder() .setTitle("ADK-TS HTTP API") .setDescription("REST endpoints for managing and interacting with ADK-TS agents") .setVersion("1.0.0") .addTag("agents") .addTag("sessions") .addTag("events") .addTag("state") .addTag("messaging") .addTag("health") .addTag("debug") .build(); const document = swagger_1.SwaggerModule.createDocument(app, builder, { deepScanRoutes: true, }); swagger_1.SwaggerModule.setup("docs", app, document, { customSiteTitle: "ADK-TS API Docs", jsonDocumentUrl: "/openapi.json", }); if (!config.quiet && debug) console.log("[openapi] Docs available at /docs (json: /openapi.json)"); } // Serve bundled web UI when enabled (used by `adk web` command) let webAssetsAvailable = false; if (config.serveWeb) { const webDir = getWebAssetsDir(); if ((0, node_fs_1.existsSync)(webDir)) { webAssetsAvailable = true; // Serve static assets from the web/ directory app.useStaticAssets(webDir); // SPA fallback: serve index.html for non-API routes // This must be added after all other routes are registered const indexPath = (0, node_path_1.join)(webDir, "index.html"); // Use type assertion since we know NestExpressApplication uses Express app.use((req, res, next) => { // Skip API routes, health checks, docs, and reload endpoints const API_ROUTE_PREFIXES = [ "/api", "/health", "/docs", "/openapi", "/reload", ]; if (API_ROUTE_PREFIXES.some((prefix) => req.path.startsWith(prefix))) { return next(); } // Skip requests for static files (have extensions) if (req.path.includes(".")) { return next(); } // Serve index.html for SPA client-side routing return res.sendFile(indexPath); }); if (!config.quiet) console.log(`[web] Serving bundled UI from ${webDir}`); } else { if (!config.quiet) console.warn("[web] Web assets not found. Run 'pnpm build' to generate them."); } } const { teardownHotReload } = setupHotReload(agentManager, hotReload, config, env); await app.listen(config.port, config.host); const url = `http://${config.host}:${config.port}`; const stop = async () => { try { agentManager.stopAllAgents(); } finally { try { teardownHotReload(); } catch { } await app.close(); } }; return { app, url, stop, webAssetsAvailable }; } //# sourceMappingURL=bootstrap.js.map