@iqai/adk-cli
Version:
CLI tool for creating, running, and testing ADK-TS agents
281 lines • 12.3 kB
JavaScript
;
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