UNPKG

@iqai/adk-cli

Version:

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

668 lines 27.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RunCommand = void 0; const p = __importStar(require("@clack/prompts")); const chalk_1 = __importDefault(require("chalk")); const marked_1 = require("marked"); const markedTerminal = __importStar(require("marked-terminal")); const nest_commander_1 = require("nest-commander"); const constants_1 = require("../common/constants"); const schema_1 = require("../common/schema"); const bootstrap_1 = require("../http/bootstrap"); // Setup markdown terminal renderer const mt = markedTerminal.markedTerminal ?? markedTerminal; marked_1.marked.use(mt()); // Console management for quiet mode class ConsoleManager { originals = null; originalStdoutWrite = null; originalStderrWrite = null; originalSpawn = null; verbose; outputAllowed = false; isDestroyed = false; constructor(verbose) { this.verbose = verbose; // Ensure cleanup on process exit process.on("exit", () => this.restore()); process.on("SIGINT", () => this.restore()); process.on("SIGTERM", () => this.restore()); } hookConsole() { if (this.verbose || this.originals || this.isDestroyed) return; try { this.originals = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug, }; this.originalStdoutWrite = process.stdout.write.bind(process.stdout); this.originalStderrWrite = process.stderr.write.bind(process.stderr); // Smart console method replacement - allow errors and warnings in verbose mode const shouldSilenceConsole = (level) => { if (this.outputAllowed) return false; // Always allow error and warn messages to prevent diagnostic issues return !["error", "warn"].includes(level); }; const createConsoleFn = (level, original) => { return ((...args) => { if (!shouldSilenceConsole(level)) { original.apply(console, args); } }); }; console.log = createConsoleFn("log", this.originals.log); console.info = createConsoleFn("info", this.originals.info); console.warn = createConsoleFn("warn", this.originals.warn); console.error = createConsoleFn("error", this.originals.error); console.debug = createConsoleFn("debug", this.originals.debug); // Smart stdout silencing - allow certain output patterns const shouldSilenceStdout = (chunk) => { if (this.outputAllowed) return false; return !this.isImportantOutput(chunk); }; process.stdout.write = ((chunk, encoding, callback) => { if (shouldSilenceStdout(chunk)) return true; return this.originalStdoutWrite(chunk, encoding, callback); }); // Allow stderr for error messages but filter out non-critical output process.stderr.write = ((chunk, encoding, callback) => { if (this.outputAllowed) { return this.originalStderrWrite(chunk, encoding, callback); } // Allow error-like content through stderr const str = String(chunk).toLowerCase(); if (str.includes("error") || str.includes("warning") || str.includes("failed")) { return this.originalStderrWrite(chunk, encoding, callback); } return true; }); } catch (error) { // If console hooking fails, continue without it to avoid breaking the app if (this.verbose) { console.error("Failed to hook console:", error); } } } isImportantOutput(chunk) { const str = String(chunk); // Allow spinner characters and UI elements const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; // Allow interactive prompts, errors, and control sequences return (spinnerChars.some((char) => str.includes(char)) || str.includes("🤖") || str.includes("Thinking") || str.includes("\r") || str.includes("\x1b") || str.toLowerCase().includes("error") || str.toLowerCase().includes("warning") || str.includes("?") // Interactive prompts ); } hookChildProcessSilence() { if (this.verbose || this.originalSpawn || this.isDestroyed) return; try { const cp = require("node:child_process"); this.originalSpawn = cp.spawn; const shouldSilenceProcess = (command, args) => { if (!command) return false; const cmd = command.toLowerCase(); const allArgs = (args || []).map((a) => String(a).toLowerCase()); const fullCommand = [cmd, ...allArgs].join(" "); // Only silence specific known noisy processes const silencePatterns = [ "mcp-remote", "@iqai/mcp", "modelcontextprotocol", "@modelcontextprotocol", ]; return silencePatterns.some((pattern) => fullCommand.includes(pattern)); }; cp.spawn = ((command, args, options) => { try { const shouldSilence = shouldSilenceProcess(command, Array.isArray(args) ? args : options?.args); if (shouldSilence) { // Determine the correct options object const opts = Array.isArray(args) ? options || {} : args || {}; // Create safer stdio configuration const currentStdio = opts.stdio; let newStdio; if (Array.isArray(currentStdio)) { newStdio = [ currentStdio[0] || "pipe", "pipe", // stdout to pipe (can be handled) "pipe", // stderr to pipe (allow error monitoring) ]; } else if (typeof currentStdio === "string") { newStdio = ["pipe", "pipe", "pipe"]; } else { newStdio = ["pipe", "pipe", "pipe"]; } const patchedOpts = { ...opts, stdio: newStdio }; if (Array.isArray(args)) { return this.originalSpawn(command, args, patchedOpts); } return this.originalSpawn(command, patchedOpts); } } catch (error) { // Log error and continue with original spawn to avoid breaking functionality if (this.verbose) { console.error("Error in child process hook:", error); } } return this.originalSpawn(command, args, options); }); } catch (error) { if (this.verbose) { console.error("Failed to hook child process:", error); } } } restore() { if (this.isDestroyed) return; this.isDestroyed = true; try { if (this.originals) { console.log = this.originals.log; console.info = this.originals.info; console.warn = this.originals.warn; console.error = this.originals.error; console.debug = this.originals.debug; this.originals = null; } if (this.originalStdoutWrite) { process.stdout.write = this.originalStdoutWrite; this.originalStdoutWrite = null; } if (this.originalStderrWrite) { process.stderr.write = this.originalStderrWrite; this.originalStderrWrite = null; } if (this.originalSpawn) { const cp = require("node:child_process"); cp.spawn = this.originalSpawn; this.originalSpawn = null; } } catch (error) { // Use original console.error if available, fallback to process.stderr if (this.originals?.error) { this.originals.error("Error during ConsoleManager restore:", error); } else { process.stderr.write(`Error during ConsoleManager restore: ${error}\n`); } } } writeOut(text) { if (this.originalStdoutWrite) { this.originalStdoutWrite(text); } else { process.stdout.write(text); } } writeErr(text) { if (this.originalStderrWrite) { this.originalStderrWrite(text); } else { process.stderr.write(text); } } async withAllowedOutput(fn) { if (this.verbose || this.isDestroyed) { return await fn(); } const wasOutputAllowed = this.outputAllowed; this.outputAllowed = true; // Allow output during this function try { return await fn(); } finally { this.outputAllowed = wasOutputAllowed; // Restore previous state } } error(text) { this.writeErr(`${chalk_1.default.red(text)}\n`); } renderMarkdown(text) { const input = text ?? ""; const out = marked_1.marked.parse(input); return typeof out === "string" ? out : String(out ?? ""); } printAnswer(markdown) { const rendered = this.renderMarkdown(markdown); this.writeOut(`${(rendered || "").trim()}\n`); } } class AgentChatClient { apiUrl; selectedAgent = null; consoleManager; constructor(apiUrl, consoleManager) { this.apiUrl = apiUrl; this.consoleManager = consoleManager; } async connect() { try { const response = await fetch(`${this.apiUrl}/health`).catch(() => null); if (!response || !response.ok) { throw new Error("Connection failed"); } } catch { throw new Error("❌ Connection failed"); } } async fetchAgents() { try { const response = await fetch(`${this.apiUrl}/api/agents`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (Array.isArray(data)) return data; if (data && Array.isArray(data.agents)) return data.agents; throw new Error(`Unexpected response format: ${JSON.stringify(data)}`); } catch (error) { throw new Error(`Failed to fetch agents: ${error instanceof Error ? error.message : String(error)}`); } } async selectAgent() { const agents = await this.fetchAgents(); if (agents.length === 0) { throw new Error("No agents found in the current directory"); } if (agents.length === 1) { return agents[0]; } return await this.consoleManager.withAllowedOutput(async () => { const choice = await p.select({ message: "Choose an agent to chat with:", options: agents.map((agent) => ({ label: agent.name, value: agent, hint: agent.relativePath, })), }); if (p.isCancel(choice)) { process.exit(0); } return choice; }); } async sendMessage(message) { if (!this.selectedAgent) { throw new Error("No agent selected"); } await this.consoleManager.withAllowedOutput(async () => { const spinner = p.spinner(); spinner.start("🤖 Thinking..."); // Save original methods for targeted silencing during the request const savedStdout = process.stdout.write; const savedStderr = process.stderr.write; const savedConsoleLog = console.log; const savedConsoleInfo = console.info; const savedConsoleWarn = console.warn; const savedConsoleError = console.error; // Intelligent stdout filtering - allow spinner chars but block log messages const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const isSpinnerOutput = (chunk) => { const str = String(chunk); return (spinnerChars.some((char) => str.includes(char)) || str.includes("🤖 Thinking") || str.includes("\r") || // carriage returns for spinner updates str.includes("\x1b") // ANSI escape codes ); }; // Temporarily override output during the fetch process.stdout.write = (chunk, encoding, callback) => { if (isSpinnerOutput(chunk)) { return savedStdout.call(process.stdout, chunk, encoding, callback); } return true; // Block everything else }; process.stderr.write = (() => true); // Block all stderr console.log = (() => { }); console.info = (() => { }); console.warn = (() => { }); console.error = (() => { }); try { if (!this.selectedAgent?.relativePath) { throw new Error("No agent selected or agent path not available"); } const response = await fetch(`${this.apiUrl}/api/agents/${encodeURIComponent(this.selectedAgent.relativePath)}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }), }); if (!response.ok) { spinner.stop("❌ Failed to send message"); const errorText = await response.text(); // Try to parse JSON error for nicer formatting try { const errorJson = JSON.parse(errorText); if (errorJson.error && errorJson.message) { let message = `\n❌ ${errorJson.error}\n${"━".repeat(40)}\n${errorJson.message}`; if (errorJson.details && Array.isArray(errorJson.details)) { const details = errorJson.details .map((detail) => ` ${detail}`) .join("\n"); message += `\n${details}`; } throw new Error(message); } } catch { // Not JSON → fall through } throw new Error(errorText); } const result = (await response.json()); spinner.stop(`🤖 ${result.agentName ?? "Assistant"}:`); if (result.response) { this.consoleManager.printAnswer(result.response); } } catch (error) { spinner.stop("❌ Error"); const errorMessage = error instanceof Error ? error.message : String(error); this.consoleManager.error(`Failed to send message: ${errorMessage}`); throw error; } finally { // Restore all methods process.stdout.write = savedStdout; process.stderr.write = savedStderr; console.log = savedConsoleLog; console.info = savedConsoleInfo; console.warn = savedConsoleWarn; console.error = savedConsoleError; } }); } async startChat() { if (!this.selectedAgent) { throw new Error("Agent not selected"); } const sigintHandler = () => { this.consoleManager.withAllowedOutput(async () => { p.outro("Chat ended"); }); process.exit(0); }; process.on("SIGINT", sigintHandler); try { while (true) { try { const input = await this.consoleManager.withAllowedOutput(async () => { const res = await p.text({ message: "💬 Message:", placeholder: "Type your message here... (type 'exit' or 'quit' to end)", }); if (p.isCancel(res)) return "exit"; return typeof res === "symbol" ? String(res) : (res ?? ""); }); const trimmed = (input || "").trim(); if (["exit", "quit"].includes(trimmed.toLowerCase())) { process.exit(0); } if (trimmed) { await this.sendMessage(trimmed); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.consoleManager.error(`Error in chat: ${errorMessage}`); process.exit(1); } } } finally { process.removeListener("SIGINT", sigintHandler); } } setSelectedAgent(agent) { this.selectedAgent = agent; } } let RunCommand = class RunCommand extends nest_commander_1.CommandRunner { async run(passed, options) { const agentPathArg = passed?.[0]; const env = schema_1.envSchema.parse(process.env); const isVerbose = options?.verbose ?? env.ADK_VERBOSE; const consoleManager = new ConsoleManager(isVerbose); // Hook console and child process only in non-verbose mode if (!isVerbose) { consoleManager.hookConsole(); consoleManager.hookChildProcessSilence(); } if (options?.server) { const apiPort = constants_1.DEFAULT_API_PORT; const host = options.host || "localhost"; if (isVerbose) { console.log(chalk_1.default.blue("🚀 Starting ADK-TS Server...")); } const server = await (0, bootstrap_1.startHttpServer)({ port: apiPort, host, agentsDir: process.cwd(), quiet: !isVerbose, hotReload: options?.hot, watchPaths: options?.watch, }); if (isVerbose) { console.log(chalk_1.default.cyan("Press Ctrl+C to stop the server")); } process.on("SIGINT", async () => { console.log(chalk_1.default.yellow("\n🛑 Stopping server...")); await server.stop(); process.exit(0); }); await new Promise(() => { }); return; } // Interactive chat mode const apiUrl = `http://${options?.host || "localhost"}:${constants_1.DEFAULT_API_PORT}`; await consoleManager.withAllowedOutput(async () => { p.intro("🤖 ADK-TS Agent Chat"); }); // Start server if not running const healthResponse = await fetch(`${apiUrl}/health`).catch(() => null); if (!healthResponse || !healthResponse.ok) { await (0, bootstrap_1.startHttpServer)({ port: constants_1.DEFAULT_API_PORT, host: options?.host || "localhost", agentsDir: process.cwd(), quiet: !isVerbose, hotReload: options?.hot, watchPaths: options?.watch, }); await new Promise((resolve) => setTimeout(resolve, 1000)); } const client = new AgentChatClient(apiUrl, consoleManager); try { await client.connect(); const agents = await client.fetchAgents(); let selectedAgent; if (agents.length === 0) { consoleManager.error("No agents found in the current directory"); process.exit(1); } else if (agents.length === 1 || agentPathArg) { selectedAgent = (agentPathArg && agents.find((a) => a.relativePath === agentPathArg)) || agents[0]; } else { selectedAgent = await consoleManager.withAllowedOutput(async () => { const choice = await p.select({ message: "Choose an agent to chat with:", options: agents.map((agent) => ({ label: agent.name, value: agent, hint: agent.relativePath, })), }); if (p.isCancel(choice)) { process.exit(0); } return choice; }); } client.setSelectedAgent(selectedAgent); await client.startChat(); await consoleManager.withAllowedOutput(async () => { p.outro("Chat ended"); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); consoleManager.error(`Error: ${errorMessage}`); process.exit(1); } finally { // Ensure cleanup happens consoleManager.restore(); } } parseServer() { return true; } parseHost(val) { return val; } parseVerbose() { return true; } parseHot() { return true; } parseWatch(val) { return (val || "") .split(",") .map((s) => s.trim()) .filter(Boolean); } }; exports.RunCommand = RunCommand; __decorate([ (0, nest_commander_1.Option)({ flags: "-s, --server", description: "Start ADK-TS server only (without chat interface)", }), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Boolean) ], RunCommand.prototype, "parseServer", null); __decorate([ (0, nest_commander_1.Option)({ flags: "-h, --host <host>", description: "Host for server (when using --server) or API URL target", }), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", String) ], RunCommand.prototype, "parseHost", null); __decorate([ (0, nest_commander_1.Option)({ flags: "--verbose", description: "Enable verbose logs", }), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Boolean) ], RunCommand.prototype, "parseVerbose", null); __decorate([ (0, nest_commander_1.Option)({ flags: "--hot", description: "Enable hot reloading (watches agents and optional paths)", }), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Boolean) ], RunCommand.prototype, "parseHot", null); __decorate([ (0, nest_commander_1.Option)({ flags: "--watch <paths>", description: "Comma-separated list of additional paths to watch for reloads", }), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Array) ], RunCommand.prototype, "parseWatch", null); exports.RunCommand = RunCommand = __decorate([ (0, nest_commander_1.Command)({ name: "run", description: "Start an interactive chat with an agent", arguments: "[agent-path]", }) ], RunCommand); //# sourceMappingURL=run.command.js.map