@iqai/adk-cli
Version:
CLI tool for creating, running, and testing ADK agents
1 lines โข 72.4 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../package.json","../src/commands/new.ts","../src/commands/run.ts","../src/commands/serve.ts","../src/server/index.ts","../src/server/routes.ts","../src/server/services.ts","../src/commands/web.ts"],"sourcesContent":["import chalk from \"chalk\";\nimport { program } from \"commander\";\nimport packageJson from \"../package.json\";\nimport { createProject } from \"./commands/new.js\";\nimport { runAgent } from \"./commands/run.js\";\nimport { serveCommand } from \"./commands/serve.js\";\nimport { webCommand } from \"./commands/web.js\";\n\nprogram\n\t.name(\"adk\")\n\t.description(packageJson.description)\n\t.version(packageJson.version);\n\nprogram\n\t.command(\"new\")\n\t.description(\"Create a new ADK project\")\n\t.argument(\"[project-name]\", \"Name of the project to create\")\n\t.option(\n\t\t\"-t, --template <template>\",\n\t\t\"Template to use (simple-agent, discord-bot, telegram-bot, hono-server, mcp-starter)\",\n\t)\n\t.action(async (projectName, options) => {\n\t\ttry {\n\t\t\tawait createProject(projectName, options);\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.red(\"Error creating project:\"), error);\n\t\t\tprocess.exit(1);\n\t\t}\n\t});\n\nprogram\n\t.command(\"run\")\n\t.description(\"Start an interactive chat with an agent\")\n\t.argument(\n\t\t\"[agent-path]\",\n\t\t\"Path to specific agent (optional - will show selector if multiple agents found)\",\n\t)\n\t.option(\"-s, --server\", \"Start ADK server only (without chat interface)\")\n\t.option(\n\t\t\"-h, --host <host>\",\n\t\t\"Host for server (when using --server)\",\n\t\t\"localhost\",\n\t)\n\t.action(async (agentPath, options) => {\n\t\ttry {\n\t\t\tawait runAgent(agentPath, options);\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.red(\"Error running agent:\"), error);\n\t\t\tprocess.exit(1);\n\t\t}\n\t});\n\nprogram\n\t.command(\"web\")\n\t.description(\"Start a web interface for testing agents\")\n\t.option(\"-p, --port <port>\", \"Port for API server\", \"8042\")\n\t.option(\"--web-port <port>\", \"Port for web app (when using --local)\", \"3000\")\n\t.option(\"-h, --host <host>\", \"Host for servers\", \"localhost\")\n\t.option(\n\t\t\"-d, --dir <directory>\",\n\t\t\"Directory to scan for agents (default: current directory)\",\n\t\t\".\",\n\t)\n\t.option(\n\t\t\"--local\",\n\t\t\"Run local web app instead of opening production URL\",\n\t\tfalse,\n\t)\n\t.option(\n\t\t\"--web-url <url>\",\n\t\t\"URL of the web application (used when not --local)\",\n\t\t\"https://adk-web.iqai.com\",\n\t)\n\t.action(async (options) => {\n\t\ttry {\n\t\t\tawait webCommand(options);\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.red(\"Error starting web UI:\"), error);\n\t\t\tprocess.exit(1);\n\t\t}\n\t});\n\nprogram\n\t.command(\"serve\")\n\t.description(\"Start an API server for agent management\")\n\t.option(\"-p, --port <port>\", \"Port for the server\", \"8042\")\n\t.option(\"-h, --host <host>\", \"Host for the server\", \"localhost\")\n\t.option(\n\t\t\"-d, --dir <directory>\",\n\t\t\"Directory to scan for agents (default: current directory)\",\n\t\t\".\",\n\t)\n\t.action(async (options) => {\n\t\ttry {\n\t\t\tawait serveCommand(options);\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.red(\"Error starting server:\"), error);\n\t\t\tprocess.exit(1);\n\t\t}\n\t});\n\nprogram.parse();\n","{\n\t\"name\": \"@iqai/adk-cli\",\n\t\"version\": \"0.2.6\",\n\t\"description\": \"CLI tool for creating, running, and testing ADK agents\",\n\t\"main\": \"dist/index.js\",\n\t\"types\": \"dist/index.d.ts\",\n\t\"bin\": {\n\t\t\"adk\": \"./dist/index.mjs\"\n\t},\n\t\"scripts\": {\n\t\t\"build\": \"tsup\",\n\t\t\"dev\": \"tsup --watch\",\n\t\t\"test\": \"vitest run\",\n\t\t\"test:watch\": \"vitest\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/IQAIcom/adk-ts.git\",\n\t\t\"directory\": \"packages/adk-cli\"\n\t},\n\t\"keywords\": [\n\t\t\"ai\",\n\t\t\"llm\",\n\t\t\"agent\",\n\t\t\"cli\",\n\t\t\"adk\",\n\t\t\"typescript\"\n\t],\n\t\"author\": \"IQAI\",\n\t\"license\": \"MIT\",\n\t\"dependencies\": {\n\t\t\"@clack/prompts\": \"^0.11.0\",\n\t\t\"@hono/node-server\": \"^1.18.1\",\n\t\t\"@iqai/adk\": \"workspace:*\",\n\t\t\"chalk\": \"^5.4.1\",\n\t\t\"commander\": \"^12.1.0\",\n\t\t\"dedent\": \"^1.6.0\",\n\t\t\"giget\": \"^2.0.0\",\n\t\t\"hono\": \"^4.6.13\",\n\t\t\"marked\": \"^14.1.3\",\n\t\t\"marked-terminal\": \"^7.2.1\",\n\t\t\"esbuild\": \"^0.23.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@iqai/tsconfig\": \"workspace:*\",\n\t\t\"@types/marked-terminal\": \"^6.1.1\",\n\t\t\"@types/node\": \"^20.17.30\",\n\t\t\"tsup\": \"^8.4.0\",\n\t\t\"typescript\": \"^5.3.2\",\n\t\t\"vitest\": \"^3.1.3\"\n\t},\n\t\"packageManager\": \"pnpm@9.0.0\",\n\t\"engines\": {\n\t\t\"node\": \">=22.0\"\n\t},\n\t\"files\": [\n\t\t\"dist\",\n\t\t\"!**/*.test.*\",\n\t\t\"!**/*.json\",\n\t\t\"CHANGELOG.md\",\n\t\t\"LICENSE\",\n\t\t\"README.md\"\n\t],\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t}\n}\n","import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { confirm, intro, outro, select, spinner, text } from \"@clack/prompts\";\nimport chalk from \"chalk\";\nimport dedent from \"dedent\";\nimport { downloadTemplate } from \"giget\";\n\ninterface Template {\n\tvalue: string;\n\tlabel: string;\n\thint: string;\n\tsource: string;\n}\n\nconst templates: Template[] = [\n\t{\n\t\tvalue: \"simple-agent\",\n\t\tlabel: \"๐ค Simple Agent\",\n\t\thint: \"Basic agent with chat capabilities\",\n\t\tsource: \"github:IQAIcom/adk-ts/apps/starter-templates/simple-agent\",\n\t},\n\t{\n\t\tvalue: \"discord-bot\",\n\t\tlabel: \"๐ฎ Discord Bot\",\n\t\thint: \"Agent integrated with Discord\",\n\t\tsource: \"github:IQAIcom/adk-ts/apps/starter-templates/discord-bot\",\n\t},\n\t{\n\t\tvalue: \"telegram-bot\",\n\t\tlabel: \"๐ฑ Telegram Bot\",\n\t\thint: \"Agent integrated with Telegram\",\n\t\tsource: \"github:IQAIcom/adk-ts/apps/starter-templates/telegram-bot\",\n\t},\n\t{\n\t\tvalue: \"hono-server\",\n\t\tlabel: \"๐ Hono Server\",\n\t\thint: \"Web server with agent endpoints\",\n\t\tsource: \"github:IQAIcom/adk-ts/apps/starter-templates/hono-server\",\n\t},\n\t{\n\t\tvalue: \"mcp-starter\",\n\t\tlabel: \"๐ MCP Integration\",\n\t\thint: \"Model Context Protocol server\",\n\t\tsource: \"github:IQAIcom/adk-ts/apps/starter-templates/mcp-starter\",\n\t},\n];\n\ninterface PackageManager {\n\tname: string;\n\tcommand: string;\n\targs: string[];\n\tlabel: string;\n}\n\nconst packageManagers: PackageManager[] = [\n\t{ name: \"npm\", command: \"npm\", args: [\"install\"], label: \"๐ฆ npm\" },\n\t{ name: \"pnpm\", command: \"pnpm\", args: [\"install\"], label: \"โก pnpm\" },\n\t{ name: \"yarn\", command: \"yarn\", args: [\"install\"], label: \"๐งถ yarn\" },\n\t{ name: \"bun\", command: \"bun\", args: [\"install\"], label: \"๐ bun\" },\n];\n\nasync function detectAvailablePackageManagers(): Promise<PackageManager[]> {\n\tconst { spawn } = await import(\"node:child_process\");\n\tconst available: PackageManager[] = [];\n\n\tfor (const pm of packageManagers) {\n\t\ttry {\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tconst child = spawn(pm.command, [\"--version\"], {\n\t\t\t\t\tstdio: \"pipe\",\n\t\t\t\t});\n\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\tif (code === 0) {\n\t\t\t\t\t\tavailable.push(pm);\n\t\t\t\t\t}\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t\tchild.on(\"error\", () => resolve());\n\t\t\t});\n\t\t} catch {\n\t\t\t// Package manager not available\n\t\t}\n\t}\n\n\treturn available.length > 0 ? available : [packageManagers[0]]; // Fallback to npm\n}\n\nexport async function createProject(\n\tprojectName?: string,\n\toptions?: { template?: string },\n) {\n\tconsole.clear();\n\n\t// Minimal, elegant intro\n\tintro(chalk.magentaBright(\"๐ง Create new ADK-TS project\"));\n\n\tlet finalProjectName = projectName;\n\tif (!finalProjectName) {\n\t\tconst response = await text({\n\t\t\tmessage: \"What is your project name?\",\n\t\t\tplaceholder: \"my-adk-project\",\n\t\t\tvalidate: (value) => {\n\t\t\t\tif (!value) return \"Project name is required\";\n\t\t\t\tif (value.includes(\" \")) return \"Project name cannot contain spaces\";\n\t\t\t\tif (existsSync(value)) return `Directory \"${value}\" already exists`;\n\t\t\t\treturn undefined;\n\t\t\t},\n\t\t});\n\n\t\tif (typeof response === \"symbol\") {\n\t\t\toutro(\"Operation cancelled\");\n\t\t\tprocess.exit(0);\n\t\t}\n\t\tfinalProjectName = response;\n\t}\n\n\tlet selectedTemplate = options?.template;\n\tif (\n\t\t!selectedTemplate ||\n\t\t!templates.find((t) => t.value === selectedTemplate)\n\t) {\n\t\tconst framework = await select({\n\t\t\tmessage: \"Which template would you like to use?\",\n\t\t\toptions: templates.map((t) => ({\n\t\t\t\tvalue: t.value,\n\t\t\t\tlabel: t.label,\n\t\t\t\thint: t.hint,\n\t\t\t})),\n\t\t});\n\n\t\tif (typeof framework === \"symbol\") {\n\t\t\toutro(\"Operation cancelled\");\n\t\t\tprocess.exit(0);\n\t\t}\n\t\tselectedTemplate = framework;\n\t}\n\n\tconst template = templates.find((t) => t.value === selectedTemplate);\n\tif (!template) {\n\t\toutro(\"Invalid template selected\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if directory already exists\n\tif (existsSync(finalProjectName)) {\n\t\toutro(chalk.red(`Directory \"${finalProjectName}\" already exists`));\n\t\tprocess.exit(1);\n\t}\n\n\tconst s = spinner();\n\ts.start(\"Downloading template...\");\n\n\ttry {\n\t\tawait downloadTemplate(template.source, {\n\t\t\tdir: finalProjectName,\n\t\t\tregistry: \"gh\",\n\t\t});\n\t\ts.stop(\"Template downloaded!\");\n\t} catch (error) {\n\t\ts.stop(\"Failed to download template\");\n\t\toutro(chalk.red(`Error: ${error}`));\n\t\tprocess.exit(1);\n\t}\n\n\t// Detect available package managers\n\tconst availablePackageManagers = await detectAvailablePackageManagers();\n\n\tlet selectedPackageManager: PackageManager;\n\tif (availablePackageManagers.length === 1) {\n\t\tselectedPackageManager = availablePackageManagers[0];\n\t} else {\n\t\tconst packageManagerChoice = await select({\n\t\t\tmessage: \"Which package manager would you like to use?\",\n\t\t\toptions: availablePackageManagers.map((pm) => ({\n\t\t\t\tvalue: pm.name,\n\t\t\t\tlabel: pm.label,\n\t\t\t})),\n\t\t});\n\n\t\tif (typeof packageManagerChoice === \"symbol\") {\n\t\t\toutro(\"Operation cancelled\");\n\t\t\tprocess.exit(0);\n\t\t}\n\n\t\tselectedPackageManager = availablePackageManagers.find(\n\t\t\t(pm) => pm.name === packageManagerChoice,\n\t\t)!;\n\t}\n\n\tconst shouldInstall = await confirm({\n\t\tmessage: \"Install dependencies?\",\n\t\tinitialValue: true,\n\t});\n\n\tif (typeof shouldInstall === \"symbol\") {\n\t\toutro(\"Operation cancelled\");\n\t\tprocess.exit(0);\n\t}\n\n\tif (shouldInstall) {\n\t\tconst s = spinner();\n\t\ts.start(`Installing dependencies with ${selectedPackageManager.name}...`);\n\n\t\tconst { spawn } = await import(\"node:child_process\");\n\t\tconst projectPath = join(process.cwd(), finalProjectName);\n\n\t\ttry {\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tconst child = spawn(\n\t\t\t\t\tselectedPackageManager.command,\n\t\t\t\t\tselectedPackageManager.args,\n\t\t\t\t\t{\n\t\t\t\t\t\tcwd: projectPath,\n\t\t\t\t\t\tstdio: \"pipe\",\n\t\t\t\t\t},\n\t\t\t\t);\n\n\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\tif (code === 0) {\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(new Error(`Package installation failed with code ${code}`));\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tchild.on(\"error\", reject);\n\t\t\t});\n\n\t\t\ts.stop(\"Dependencies installed!\");\n\t\t} catch (error) {\n\t\t\ts.stop(\"Failed to install dependencies\");\n\t\t\tconsole.log(\n\t\t\t\tchalk.yellow(\"\\nYou can install dependencies manually by running:\"),\n\t\t\t);\n\t\t\tconsole.log(\n\t\t\t\tchalk.cyan(\n\t\t\t\t\t`cd ${finalProjectName} && ${selectedPackageManager.command} ${selectedPackageManager.args.join(\" \")}`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\toutro(\n\t\tchalk.green(dedent`\n ๐ Project created successfully!\n\n Next steps:\n ${chalk.cyan(`cd ${finalProjectName}`)}\n ${shouldInstall ? \"\" : chalk.cyan(`${selectedPackageManager.command} ${selectedPackageManager.args.join(\" \")}`)}\n ${chalk.cyan(\"npm run dev\")} or ${chalk.cyan(\"yarn dev\")} or ${chalk.cyan(\"pnpm dev\")}\n\n Happy coding! ๐\n `),\n\t);\n}\n","import * as p from \"@clack/prompts\";\nimport { log, spinner } from \"@clack/prompts\";\nimport chalk from \"chalk\";\nimport { marked } from \"marked\";\nimport { markedTerminal } from \"marked-terminal\";\nimport { type ServeOptions, serveCommand } from \"./serve.js\";\n\n// Configure marked for terminal output\nmarked.use(markedTerminal() as any);\n\n// Render markdown to terminal-formatted text\nasync function renderMarkdown(text: string): Promise<string> {\n\ttry {\n\t\tconst result = await marked(text);\n\t\treturn typeof result === \"string\" ? result : text;\n\t} catch (error) {\n\t\t// Fallback to plain text if markdown parsing fails\n\t\treturn text;\n\t}\n}\n\ninterface Agent {\n\trelativePath: string;\n\tname: string;\n\tabsolutePath: string;\n}\n\nclass AgentChatClient {\n\tprivate apiUrl: string;\n\tprivate selectedAgent: Agent | null = null;\n\n\tconstructor(apiUrl: string) {\n\t\tthis.apiUrl = apiUrl;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\t// Test connection\n\t\ttry {\n\t\t\tconst response = await fetch(`${this.apiUrl}/health`);\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(\"Connection failed\");\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow new Error(\"โ Connection failed\");\n\t\t}\n\t}\n\n\tasync fetchAgents(): Promise<Agent[]> {\n\t\ttry {\n\t\t\tconst response = await fetch(`${this.apiUrl}/api/agents`);\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`);\n\t\t\t}\n\t\t\tconst data = await response.json();\n\n\t\t\t// Handle different response structures\n\t\t\tif (Array.isArray(data)) {\n\t\t\t\treturn data;\n\t\t\t}\n\t\t\tif (data && Array.isArray(data.agents)) {\n\t\t\t\treturn data.agents;\n\t\t\t}\n\t\t\tthrow new Error(`Unexpected response format: ${JSON.stringify(data)}`);\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to fetch agents: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync selectAgent(): Promise<Agent> {\n\t\tconst agents = await this.fetchAgents();\n\n\t\tif (agents.length === 0) {\n\t\t\tthrow new Error(\"No agents found in the current directory\");\n\t\t}\n\n\t\tif (agents.length === 1) {\n\t\t\treturn agents[0];\n\t\t}\n\n\t\tconst selectedAgent = await p.select({\n\t\t\tmessage: \"Choose an agent to chat with:\",\n\t\t\toptions: agents.map((agent) => ({\n\t\t\t\tlabel: agent.name,\n\t\t\t\tvalue: agent,\n\t\t\t\thint: agent.relativePath,\n\t\t\t})),\n\t\t});\n\n\t\tif (p.isCancel(selectedAgent)) {\n\t\t\tp.cancel(\"Operation cancelled\");\n\t\t\tprocess.exit(0);\n\t\t}\n\n\t\treturn selectedAgent;\n\t}\n\n\t// No-op: agents are auto-loaded on message; keeping method removed\n\n\tasync sendMessage(message: string): Promise<void> {\n\t\tif (!this.selectedAgent) {\n\t\t\tthrow new Error(\"No agent selected\");\n\t\t}\n\n\t\tconst s = spinner();\n\t\ts.start(\"๐ค Thinking...\");\n\n\t\ttry {\n\t\t\tconst response = await fetch(\n\t\t\t\t`${this.apiUrl}/api/agents/${encodeURIComponent(this.selectedAgent.relativePath)}/message`,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify({ message }),\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tconst errorText = await response.text();\n\t\t\t\ts.stop(\"โ Failed to send message\");\n\t\t\t\tthrow new Error(`Failed to send message: ${errorText}`);\n\t\t\t}\n\n\t\t\tconst result = await response.json();\n\t\t\ts.stop(\"๐ค Assistant:\");\n\n\t\t\t// Display agent response using Clack's log.message\n\t\t\tif (result.response) {\n\t\t\t\tconst formattedResponse = await renderMarkdown(result.response);\n\t\t\t\tlog.message(formattedResponse.trim());\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.error(\"Failed to send message\");\n\t\t}\n\t}\n\n\tasync startChat(): Promise<void> {\n\t\tif (!this.selectedAgent) {\n\t\t\tthrow new Error(\"Agent not selected\");\n\t\t}\n\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tconst message = await p.text({\n\t\t\t\t\tmessage: \"๐ฌ Message:\",\n\t\t\t\t\tplaceholder: \"Type your message here...\",\n\t\t\t\t});\n\n\t\t\t\tif (p.isCancel(message)) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (message.trim()) {\n\t\t\t\t\tawait this.sendMessage(message.trim());\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.red(\"Error in chat:\"), error);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tdisconnect(): void {\n\t\t// No cleanup needed for HTTP-only client\n\t}\n\n\tsetSelectedAgent(agent: Agent): void {\n\t\tthis.selectedAgent = agent;\n\t}\n}\n\nexport async function runAgent(\n\tagentPath?: string,\n\toptions: {\n\t\twatch?: boolean;\n\t\tport?: string;\n\t\tserver?: boolean;\n\t\thost?: string;\n\t\t// verbose is intentionally not exposed on the command; it can be passed programmatically\n\t\t// and also inferred from the ADK_VERBOSE environment variable.\n\t\tverbose?: boolean;\n\t} = {},\n) {\n\tconst envVerbose = process.env.ADK_VERBOSE;\n\tconst isVerbose =\n\t\toptions.verbose ?? (envVerbose === \"1\" || envVerbose === \"true\");\n\t// If server option is enabled, start ADK server only\n\tif (options.server) {\n\t\tconst apiPort = 8042; // Use new default port\n\t\tconst host = options.host || \"localhost\";\n\n\t\tconsole.log(chalk.blue(\"๐ Starting ADK Server...\"));\n\n\t\tconst serveOptions: ServeOptions = {\n\t\t\tport: apiPort,\n\t\t\tdir: process.cwd(),\n\t\t\thost,\n\t\t\tquiet: !isVerbose,\n\t\t};\n\n\t\ttry {\n\t\t\tconst server = await serveCommand(serveOptions);\n\n\t\t\tconsole.log(chalk.cyan(\"Press Ctrl+C to stop the server\"));\n\n\t\t\t// Handle cleanup\n\t\t\tprocess.on(\"SIGINT\", async () => {\n\t\t\t\tconsole.log(chalk.yellow(\"\\n๐ Stopping server...\"));\n\t\t\t\tawait server.stop();\n\t\t\t\tprocess.exit(0);\n\t\t\t});\n\n\t\t\t// Keep the process running\n\t\t\treturn new Promise(() => {});\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.red(\"โ Failed to start server\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\t// Interactive chat mode (default)\n\tconst apiUrl = `http://${options.host || \"localhost\"}:8042`; // Use new default port\n\n\tp.intro(\"๐ค ADK Agent Chat\");\n\n\ttry {\n\t\t// Check if server is running\n\t\tconst healthResponse = await fetch(`${apiUrl}/health`).catch(() => null);\n\n\t\tif (!healthResponse || !healthResponse.ok) {\n\t\t\tconst serverSpinner = spinner();\n\t\t\tserverSpinner.start(\"๐ Starting server...\");\n\n\t\t\t// Start server in the background\n\t\t\tconst serveOptions: ServeOptions = {\n\t\t\t\tport: 8042, // Use new default port\n\t\t\t\tdir: process.cwd(),\n\t\t\t\thost: options.host || \"localhost\",\n\t\t\t\tquiet: !isVerbose,\n\t\t\t};\n\n\t\t\tawait serveCommand(serveOptions);\n\n\t\t\t// Wait a moment for server to be ready\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 1000));\n\t\t\tserverSpinner.stop(\"โ
Server ready\");\n\t\t}\n\n\t\tconst client = new AgentChatClient(apiUrl);\n\n\t\t// Connect to server\n\t\tawait client.connect();\n\n\t\t// Select agent with spinner\n\t\tconst agentSpinner = spinner();\n\t\tagentSpinner.start(\"๐ Scanning for agents...\");\n\n\t\tconst agents = await client.fetchAgents();\n\n\t\tlet selectedAgent: Agent;\n\t\tif (agents.length === 0) {\n\t\t\tagentSpinner.stop(\"โ No agents found\");\n\t\t\tp.cancel(\"No agents found in the current directory\");\n\t\t\tprocess.exit(1);\n\t\t} else if (agents.length === 1) {\n\t\t\tselectedAgent = agents[0];\n\t\t\tagentSpinner.stop(`๐ค Found agent: ${selectedAgent.name}`);\n\t\t} else {\n\t\t\tagentSpinner.stop(`๐ค Found ${agents.length} agents`);\n\t\t\tconst choice = await p.select({\n\t\t\t\tmessage: \"Choose an agent to chat with:\",\n\t\t\t\toptions: agents.map((agent) => ({\n\t\t\t\t\tlabel: agent.name,\n\t\t\t\t\tvalue: agent,\n\t\t\t\t\thint: agent.relativePath,\n\t\t\t\t})),\n\t\t\t});\n\n\t\t\tif (p.isCancel(choice)) {\n\t\t\t\tp.cancel(\"Operation cancelled\");\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tselectedAgent = choice;\n\t\t}\n\n\t\tclient.setSelectedAgent(selectedAgent);\n\n\t\tawait client.startChat();\n\n\t\tclient.disconnect();\n\t\tp.outro(\"Chat ended\");\n\t} catch (error) {\n\t\tp.cancel(\n\t\t\t`Error: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t\tprocess.exit(1);\n\t}\n}\n","import { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport { ADKServer } from \"../server/index.js\";\n\nexport interface ServeOptions {\n\tport?: number;\n\tdir?: string;\n\thost?: string;\n\tquiet?: boolean;\n}\n\nexport async function serveCommand(\n\toptions: ServeOptions = {},\n): Promise<ADKServer> {\n\tconst port = options.port || 8042; // Default to port 8042 to avoid common conflicts\n\tconst host = options.host || \"localhost\";\n\tconst agentsDir = resolve(options.dir || \".\");\n\n\tif (!existsSync(agentsDir)) {\n\t\tconsole.error(chalk.red(`โ Directory not found: ${agentsDir}`));\n\t\tprocess.exit(1);\n\t}\n\n\tif (!options.quiet) {\n\t\tconsole.log(chalk.blue(`๐ ADK Server starting on http://${host}:${port}`));\n\t}\n\n\tconst server = new ADKServer(agentsDir, port, host, options.quiet);\n\n\ttry {\n\t\tawait server.start();\n\n\t\tif (!options.quiet) {\n\t\t\tconsole.log(chalk.green(\"โ
Server ready\"));\n\t\t}\n\n\t\t// Handle shutdown gracefully\n\t\tconst cleanup = async () => {\n\t\t\tif (!options.quiet) {\n\t\t\t\tconsole.log(chalk.yellow(\"\\n๐ Stopping server...\"));\n\t\t\t}\n\t\t\tawait server.stop();\n\t\t\tprocess.exit(0);\n\t\t};\n\n\t\tprocess.on(\"SIGINT\", cleanup);\n\t\tprocess.on(\"SIGTERM\", cleanup);\n\n\t\treturn server;\n\t} catch (error) {\n\t\tconsole.error(chalk.red(\"โ Failed to start ADK server:\"), error);\n\t\tprocess.exit(1);\n\t}\n}\n","import { type ServerType, serve } from \"@hono/node-server\";\nimport { InMemorySessionService } from \"@iqai/adk\";\nimport { Hono } from \"hono\";\nimport { setupRoutes } from \"./routes.js\";\nimport { AgentManager, SessionManager } from \"./services.js\";\nimport type { ServerConfig } from \"./types.js\";\n\nexport class ADKServer {\n\tprivate agentManager: AgentManager;\n\tprivate sessionManager: SessionManager;\n\tprivate sessionService: InMemorySessionService;\n\tprivate app: Hono;\n\tprivate server: ServerType | undefined;\n\tprivate config: ServerConfig;\n\n\tconstructor(\n\t\tagentsDir: string,\n\t\tport = 8042,\n\t\thost = \"localhost\",\n\t\tquiet = false,\n\t) {\n\t\tthis.config = { agentsDir, port, host, quiet };\n\t\tthis.sessionService = new InMemorySessionService();\n\t\tthis.agentManager = new AgentManager(this.sessionService, quiet);\n\t\tthis.sessionManager = new SessionManager(this.sessionService);\n\t\tthis.app = new Hono();\n\n\t\t// Setup routes\n\t\tsetupRoutes(this.app, this.agentManager, this.sessionManager, agentsDir);\n\n\t\t// Initial agent scan\n\t\tthis.agentManager.scanAgents(agentsDir);\n\t}\n\n\tasync start(): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.server = serve({\n\t\t\t\tfetch: this.app.fetch,\n\t\t\t\tport: this.config.port,\n\t\t\t\thostname: this.config.host,\n\t\t\t});\n\n\t\t\t// Give the server a moment to start\n\t\t\tsetTimeout(() => {\n\t\t\t\tresolve();\n\t\t\t}, 100);\n\t\t});\n\t}\n\n\tasync stop(): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\t// Stop all running agents\n\t\t\tthis.agentManager.stopAllAgents();\n\n\t\t\tif (this.server) {\n\t\t\t\tthis.server.close();\n\t\t\t}\n\t\t\tresolve();\n\t\t});\n\t}\n\n\tgetPort(): number {\n\t\treturn this.config.port;\n\t}\n}\n","import type { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport type { AgentManager, SessionManager } from \"./services.js\";\nimport type {\n\tAgentListResponse,\n\tMessageRequest,\n\tMessageResponse,\n\tMessagesResponse,\n} from \"./types.js\";\n\nexport function setupRoutes(\n\tapp: Hono,\n\tagentManager: AgentManager,\n\tsessionManager: SessionManager,\n\tagentsDir: string,\n): void {\n\t// CORS middleware\n\tapp.use(\"/*\", cors());\n\n\t// Health check\n\tapp.get(\"/health\", (c) => c.json({ status: \"ok\" }));\n\n\t// List agents\n\tapp.get(\"/api/agents\", (c) => {\n\t\tconst agentsList: AgentListResponse[] = Array.from(\n\t\t\tagentManager.getAgents().values(),\n\t\t).map((agent) => ({\n\t\t\tpath: agent.absolutePath,\n\t\t\tname: agent.name,\n\t\t\tdirectory: agent.absolutePath,\n\t\t\trelativePath: agent.relativePath,\n\t\t}));\n\t\treturn c.json({ agents: agentsList });\n\t});\n\n\t// Refresh agents list\n\tapp.post(\"/api/agents/refresh\", (c) => {\n\t\tagentManager.scanAgents(agentsDir);\n\t\tconst agentsList: AgentListResponse[] = Array.from(\n\t\t\tagentManager.getAgents().values(),\n\t\t).map((agent) => ({\n\t\t\tpath: agent.absolutePath,\n\t\t\tname: agent.name,\n\t\t\tdirectory: agent.absolutePath,\n\t\t\trelativePath: agent.relativePath,\n\t\t}));\n\t\treturn c.json({ agents: agentsList });\n\t});\n\n\t// Get agent messages\n\tapp.get(\"/api/agents/:id/messages\", async (c) => {\n\t\tconst agentPath = decodeURIComponent(c.req.param(\"id\"));\n\t\tconst loadedAgent = agentManager.getLoadedAgents().get(agentPath);\n\t\tif (!loadedAgent) {\n\t\t\treturn c.json({ messages: [] });\n\t\t}\n\n\t\tconst messages = await sessionManager.getSessionMessages(loadedAgent);\n\t\tconst response: MessagesResponse = { messages };\n\t\treturn c.json(response);\n\t});\n\n\t// Send message to agent\n\tapp.post(\"/api/agents/:id/message\", async (c) => {\n\t\tconst agentPath = decodeURIComponent(c.req.param(\"id\"));\n\t\tconst { message, attachments }: MessageRequest = await c.req.json();\n\t\tconst response = await agentManager.sendMessageToAgent(\n\t\t\tagentPath,\n\t\t\tmessage,\n\t\t\tattachments,\n\t\t);\n\t\tconst messageResponse: MessageResponse = { response };\n\t\treturn c.json(messageResponse);\n\t});\n}\n","import {\n\texistsSync,\n\tmkdirSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\tunlinkSync,\n} from \"node:fs\";\nimport { dirname, join, relative } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { FullMessage, InMemorySessionService, LlmAgent } from \"@iqai/adk\";\nimport { AgentBuilder } from \"@iqai/adk\";\nimport type { Agent, LoadedAgent } from \"./types.js\";\n\nexport class AgentScanner {\n\tconstructor(private quiet = false) {}\n\n\tscanAgents(\n\t\tagentsDir: string,\n\t\tloadedAgents: Map<string, LoadedAgent>,\n\t): Map<string, Agent> {\n\t\tconst agents = new Map<string, Agent>();\n\n\t\t// Use current directory if agentsDir doesn't exist or is empty\n\t\tconst scanDir =\n\t\t\t!agentsDir || !existsSync(agentsDir) ? process.cwd() : agentsDir;\n\n\t\tconst shouldSkipDirectory = (dirName: string): boolean => {\n\t\t\tconst skipDirs = [\n\t\t\t\t\"node_modules\",\n\t\t\t\t\".git\",\n\t\t\t\t\".next\",\n\t\t\t\t\"dist\",\n\t\t\t\t\"build\",\n\t\t\t\t\".turbo\",\n\t\t\t\t\"coverage\",\n\t\t\t\t\".vscode\",\n\t\t\t\t\".idea\",\n\t\t\t];\n\t\t\treturn skipDirs.includes(dirName);\n\t\t};\n\n\t\tconst scanDirectory = (dir: string): void => {\n\t\t\tif (!existsSync(dir)) return;\n\n\t\t\tconst items = readdirSync(dir);\n\t\t\tfor (const item of items) {\n\t\t\t\tconst fullPath = join(dir, item);\n\t\t\t\tconst stat = statSync(fullPath);\n\n\t\t\t\tif (stat.isDirectory()) {\n\t\t\t\t\t// Skip common build/dependency directories\n\t\t\t\t\tif (!shouldSkipDirectory(item)) {\n\t\t\t\t\t\tscanDirectory(fullPath);\n\t\t\t\t\t}\n\t\t\t\t} else if (item === \"agent.ts\" || item === \"agent.js\") {\n\t\t\t\t\tconst relativePath = relative(scanDir, dir);\n\n\t\t\t\t\t// Try to get the actual agent name if it's already loaded\n\t\t\t\t\tconst loadedAgent = loadedAgents.get(relativePath);\n\t\t\t\t\tlet agentName = relativePath.split(\"/\").pop() || \"unknown\";\n\n\t\t\t\t\t// If agent is loaded, use its actual name\n\t\t\t\t\tif (loadedAgent?.agent?.name) {\n\t\t\t\t\t\tagentName = loadedAgent.agent.name;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Try to quickly extract name from agent file if not loaded\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst agentFilePath = join(dir, item);\n\t\t\t\t\t\t\tagentName =\n\t\t\t\t\t\t\t\tthis.extractAgentNameFromFile(agentFilePath) || agentName;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Fallback to directory name if extraction fails\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tagents.set(relativePath, {\n\t\t\t\t\t\trelativePath,\n\t\t\t\t\t\tname: agentName,\n\t\t\t\t\t\tabsolutePath: dir,\n\t\t\t\t\t\tinstance: loadedAgent?.agent,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tscanDirectory(scanDir);\n\t\tif (!this.quiet) {\n\t\t\tconsole.log(`โ
Agent scan complete. Found ${agents.size} agents.`);\n\t\t}\n\n\t\treturn agents;\n\t}\n\n\tprivate extractAgentNameFromFile(filePath: string): string | null {\n\t\ttry {\n\t\t\tconst content = readFileSync(filePath, \"utf-8\");\n\n\t\t\t// Look for agent name in export statements\n\t\t\t// Match patterns like: name: \"agent_name\" or name:\"agent_name\"\n\t\t\tconst nameMatch = content.match(/name\\s*:\\s*[\"']([^\"']+)[\"']/);\n\t\t\tif (nameMatch?.[1]) {\n\t\t\t\treturn nameMatch[1];\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n\nexport class AgentLoader {\n\tconstructor(private quiet = false) {}\n\n\t/**\n\t * Import a TypeScript file by compiling it on-demand\n\t */\n\tasync importTypeScriptFile(filePath: string): Promise<any> {\n\t\t// Determine project root (for tsconfig and resolving deps)\n\t\tconst startDir = dirname(filePath);\n\t\tlet projectRoot = startDir;\n\t\twhile (projectRoot !== \"/\" && projectRoot !== dirname(projectRoot)) {\n\t\t\tif (\n\t\t\t\texistsSync(join(projectRoot, \"package.json\")) ||\n\t\t\t\texistsSync(join(projectRoot, \".env\"))\n\t\t\t) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tprojectRoot = dirname(projectRoot);\n\t\t}\n\t\t// If we reached root without finding markers, use the original start directory\n\t\tif (projectRoot === \"/\") {\n\t\t\tprojectRoot = startDir;\n\t\t}\n\n\t\t// Transpile with esbuild and import (bundles local files, preserves tools)\n\t\ttry {\n\t\t\tconst { build } = await import(\"esbuild\");\n\t\t\tconst cacheDir = join(projectRoot, \".adk-cache\");\n\t\t\tif (!existsSync(cacheDir)) {\n\t\t\t\tmkdirSync(cacheDir, { recursive: true });\n\t\t\t}\n\t\t\tconst outFile = join(cacheDir, `agent-${Date.now()}.mjs`);\n\t\t\t// Externalize bare module imports (node_modules), bundle relative/local files\n\t\t\tconst plugin = {\n\t\t\t\tname: \"externalize-bare-imports\",\n\t\t\t\tsetup(build: any) {\n\t\t\t\t\tbuild.onResolve({ filter: /.*/ }, (args: any) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\targs.path.startsWith(\".\") ||\n\t\t\t\t\t\t\targs.path.startsWith(\"/\") ||\n\t\t\t\t\t\t\targs.path.startsWith(\"..\")\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treturn; // use default resolve (to get bundled)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { path: args.path, external: true };\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t};\n\n\t\t\tconst tsconfigPath = join(projectRoot, \"tsconfig.json\");\n\t\t\tawait build({\n\t\t\t\tentryPoints: [filePath],\n\t\t\t\toutfile: outFile,\n\t\t\t\tbundle: true,\n\t\t\t\tformat: \"esm\",\n\t\t\t\tplatform: \"node\",\n\t\t\t\ttarget: [\"node22\"],\n\t\t\t\tsourcemap: false,\n\t\t\t\tlogLevel: \"silent\",\n\t\t\t\tplugins: [plugin],\n\t\t\t\tabsWorkingDir: projectRoot,\n\t\t\t\t// Use tsconfig if present for path aliases\n\t\t\t\t...(existsSync(tsconfigPath) ? { tsconfig: tsconfigPath } : {}),\n\t\t\t});\n\n\t\t\tconst mod = await import(\n\t\t\t\t`${pathToFileURL(outFile).href}?t=${Date.now()}`\n\t\t\t);\n\t\t\tlet agentExport = (mod as any)?.agent;\n\t\t\tif (!agentExport && (mod as any)?.default) {\n\t\t\t\tagentExport = (mod as any).default.agent ?? (mod as any).default;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tunlinkSync(outFile);\n\t\t\t} catch {}\n\t\t\tif (agentExport) {\n\t\t\t\tconst isPrimitive = (v: any) =>\n\t\t\t\t\tv == null || [\"string\", \"number\", \"boolean\"].includes(typeof v);\n\t\t\t\tif (isPrimitive(agentExport)) {\n\t\t\t\t\t// Primitive named 'agent' export (e.g., a string) isn't a real agent; fall through to full-module scan\n\t\t\t\t\tif (!this.quiet) {\n\t\t\t\t\t\tconsole.log(\n\t\t\t\t\t\t\t`โน๏ธ Ignoring primitive 'agent' export in ${filePath}; scanning module for factory...`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (!this.quiet) {\n\t\t\t\t\t\tconsole.log(`โ
TS agent imported via esbuild: ${filePath}`);\n\t\t\t\t\t}\n\t\t\t\t\treturn { agent: agentExport };\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Fallback: return full module so downstream resolver can inspect named exports (e.g., getFooAgent)\n\t\t\treturn mod;\n\t\t} catch (e) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to import TS agent via esbuild: ${e instanceof Error ? e.message : String(e)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tloadEnvironmentVariables(agentFilePath: string): void {\n\t\t// Load environment variables from the project directory before importing\n\t\tlet projectRoot = dirname(agentFilePath);\n\t\twhile (projectRoot !== \"/\" && projectRoot !== dirname(projectRoot)) {\n\t\t\tif (\n\t\t\t\texistsSync(join(projectRoot, \"package.json\")) ||\n\t\t\t\texistsSync(join(projectRoot, \".env\"))\n\t\t\t) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tprojectRoot = dirname(projectRoot);\n\t\t}\n\n\t\t// Check for multiple env files in priority order\n\t\tconst envFiles = [\n\t\t\t\".env.local\",\n\t\t\t\".env.development.local\",\n\t\t\t\".env.production.local\",\n\t\t\t\".env.development\",\n\t\t\t\".env.production\",\n\t\t\t\".env\",\n\t\t];\n\n\t\tfor (const envFile of envFiles) {\n\t\t\tconst envPath = join(projectRoot, envFile);\n\t\t\tif (existsSync(envPath)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst envContent = readFileSync(envPath, \"utf8\");\n\t\t\t\t\tconst envLines = envContent.split(\"\\n\");\n\t\t\t\t\tfor (const line of envLines) {\n\t\t\t\t\t\tconst trimmedLine = line.trim();\n\t\t\t\t\t\tif (trimmedLine && !trimmedLine.startsWith(\"#\")) {\n\t\t\t\t\t\t\tconst [key, ...valueParts] = trimmedLine.split(\"=\");\n\t\t\t\t\t\t\tif (key && valueParts.length > 0) {\n\t\t\t\t\t\t\t\tconst value = valueParts.join(\"=\").replace(/^\"(.*)\"$/, \"$1\");\n\t\t\t\t\t\t\t\t// Set environment variables in current process (only if not already set)\n\t\t\t\t\t\t\t\tif (!process.env[key.trim()]) {\n\t\t\t\t\t\t\t\t\tprocess.env[key.trim()] = value.trim();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`โ ๏ธ Warning: Could not load ${envFile} file: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Minimal resolution logic for agent exports: supports\n\t// 1) export const agent = new LlmAgent(...)\n\t// 2) export function agent() { return new LlmAgent(...) }\n\t// 3) export async function agent() { return new LlmAgent(...) }\n\t// 4) default export (object or function) returning or containing .agent\n\tasync resolveAgentExport(mod: any): Promise<{ agent: LlmAgent }> {\n\t\tlet candidate = mod?.agent ?? mod?.default?.agent ?? mod?.default ?? mod;\n\n\t\tconst isLikelyAgentInstance = (obj: any) =>\n\t\t\tobj && typeof obj === \"object\" && typeof obj.name === \"string\";\n\t\tconst isPrimitive = (v: any) =>\n\t\t\tv == null || [\"string\", \"number\", \"boolean\"].includes(typeof v);\n\n\t\tconst invokeMaybe = async (fn: any) => {\n\t\t\tlet out = fn();\n\t\t\tif (out && typeof out === \"object\" && \"then\" in out) {\n\t\t\t\tout = await out;\n\t\t\t}\n\t\t\treturn out;\n\t\t};\n\n\t\t// If initial candidate is invalid primitive (e.g., exported const agent = \"foo\"), or\n\t\t// the entire module namespace (no direct agent), then probe named exports.\n\t\tif (\n\t\t\t(!isLikelyAgentInstance(candidate) && isPrimitive(candidate)) ||\n\t\t\t(!isLikelyAgentInstance(candidate) && candidate && candidate === mod)\n\t\t) {\n\t\t\tcandidate = mod; // ensure we iterate full namespace\n\t\t\tfor (const [key, value] of Object.entries(mod)) {\n\t\t\t\tif (key === \"default\") continue;\n\t\t\t\t// Prefer keys containing 'agent'\n\t\t\t\tconst keyLower = key.toLowerCase();\n\t\t\t\tif (isPrimitive(value)) continue; // skip obvious non-candidates\n\t\t\t\tif (isLikelyAgentInstance(value)) {\n\t\t\t\t\tcandidate = value;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\t// Handle static container object: export const container = { agent: <LlmAgent> }\n\t\t\t\tif (\n\t\t\t\t\tvalue &&\n\t\t\t\t\ttypeof value === \"object\" &&\n\t\t\t\t\t(value as any).agent &&\n\t\t\t\t\tisLikelyAgentInstance((value as any).agent)\n\t\t\t\t) {\n\t\t\t\t\tcandidate = (value as any).agent;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\ttypeof value === \"function\" &&\n\t\t\t\t\t(/(agent|build|create)/i.test(keyLower) ||\n\t\t\t\t\t\t(value.name &&\n\t\t\t\t\t\t\t/(agent|build|create)/i.test(value.name.toLowerCase())))\n\t\t\t\t) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst maybe = await invokeMaybe(value);\n\t\t\t\t\t\tif (isLikelyAgentInstance(maybe)) {\n\t\t\t\t\t\t\tcandidate = maybe;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tmaybe &&\n\t\t\t\t\t\t\ttypeof maybe === \"object\" &&\n\t\t\t\t\t\t\tmaybe.agent &&\n\t\t\t\t\t\t\tisLikelyAgentInstance(maybe.agent)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tcandidate = maybe.agent;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t// Swallow and continue trying other exports\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// If candidate is a function (sync or async), invoke it\n\t\tif (typeof candidate === \"function\") {\n\t\t\ttry {\n\t\t\t\tcandidate = await invokeMaybe(candidate);\n\t\t\t} catch (e) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Failed executing exported agent function: ${e instanceof Error ? e.message : String(e)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\t// Handle built structure { agent, runner, session }\n\t\tif (\n\t\t\tcandidate &&\n\t\t\ttypeof candidate === \"object\" &&\n\t\t\tcandidate.agent &&\n\t\t\tisLikelyAgentInstance(candidate.agent)\n\t\t) {\n\t\t\tcandidate = candidate.agent;\n\t\t}\n\t\t// Unwrap { agent: ... } pattern if present\n\t\tif (candidate?.agent && isLikelyAgentInstance(candidate.agent)) {\n\t\t\tcandidate = candidate.agent;\n\t\t}\n\t\tif (!candidate || !isLikelyAgentInstance(candidate)) {\n\t\t\tthrow new Error(\n\t\t\t\t\"No agent export resolved (expected variable, function, or function returning an agent)\",\n\t\t\t);\n\t\t}\n\t\treturn { agent: candidate as LlmAgent };\n\t}\n}\n\nexport class AgentManager {\n\tprivate agents = new Map<string, Agent>();\n\tprivate loadedAgents = new Map<string, LoadedAgent>();\n\tprivate scanner: AgentScanner;\n\tprivate loader: AgentLoader;\n\n\tconstructor(\n\t\tprivate sessionService: InMemorySessionService,\n\t\tprivate quiet = false,\n\t) {\n\t\tthis.scanner = new AgentScanner(quiet);\n\t\tthis.loader = new AgentLoader(quiet);\n\t}\n\n\tgetAgents(): Map<string, Agent> {\n\t\treturn this.agents;\n\t}\n\n\tgetLoadedAgents(): Map<string, LoadedAgent> {\n\t\treturn this.loadedAgents;\n\t}\n\n\tscanAgents(agentsDir: string): void {\n\t\tthis.agents = this.scanner.scanAgents(agentsDir, this.loadedAgents);\n\t}\n\n\tasync startAgent(agentPath: string): Promise<void> {\n\t\tconst agent = this.agents.get(agentPath);\n\t\tif (!agent) {\n\t\t\tthrow new Error(`Agent not found: ${agentPath}`);\n\t\t}\n\n\t\tif (this.loadedAgents.has(agentPath)) {\n\t\t\treturn; // Already running\n\t\t}\n\n\t\ttry {\n\t\t\t// Load the agent module dynamically\n\t\t\t// Try both .js and .ts files, prioritizing .js if it exists\n\t\t\tlet agentFilePath = join(agent.absolutePath, \"agent.js\");\n\t\t\tif (!existsSync(agentFilePath)) {\n\t\t\t\tagentFilePath = join(agent.absolutePath, \"agent.ts\");\n\t\t\t}\n\n\t\t\tif (!existsSync(agentFilePath)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No agent.js or agent.ts file found in ${agent.absolutePath}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Load environment variables from the project directory before importing\n\t\t\tthis.loader.loadEnvironmentVariables(agentFilePath);\n\n\t\t\tconst agentFileUrl = pathToFileURL(agentFilePath).href;\n\n\t\t\t// Use dynamic import to load the agent (TS path uses esbuild wrapper returning { agent })\n\t\t\tconst agentModule: any = agentFilePath.endsWith(\".ts\")\n\t\t\t\t? await this.loader.importTypeScriptFile(agentFilePath)\n\t\t\t\t: await import(agentFileUrl);\n\n\t\t\tconst resolved = await this.loader.resolveAgentExport(agentModule);\n\t\t\tconst exportedAgent = resolved.agent;\n\n\t\t\t// Validate basic shape\n\t\t\tif (!exportedAgent?.name) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Invalid agent export in ${agentFilePath}. Expected an LlmAgent instance with a name property.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Soft validation of optional fields (model/instruction not strictly required for all custom agents)\n\n\t\t\t// Build runner/session while preserving the exact exported agent (including subAgents, tools, callbacks, etc.)\n\t\t\t// We use withAgent() so we don't accidentally drop configuration like subAgents which was happening before\n\t\t\tconst agentBuilder = AgentBuilder.create(exportedAgent.name)\n\t\t\t\t.withAgent(exportedAgent as any)\n\t\t\t\t.withSessionService(this.sessionService, {\n\t\t\t\t\tuserId: `user_${agentPath}`,\n\t\t\t\t\tappName: \"adk-server\",\n\t\t\t\t});\n\n\t\t\tconst { runner, session } = await agentBuilder.build();\n\n\t\t\t// Store the loaded agent with its runner\n\t\t\tconst loadedAgent: LoadedAgent = {\n\t\t\t\tagent: exportedAgent,\n\t\t\t\trunner: runner,\n\t\t\t\tsessionId: session.id,\n\t\t\t\tuserId: `user_${agentPath}`,\n\t\t\t\tappName: \"adk-server\",\n\t\t\t};\n\n\t\t\tthis.loadedAgents.set(agentPath, loadedAgent);\n\t\t\tagent.instance = exportedAgent;\n\t\t\tagent.name = exportedAgent.name;\n\t\t} catch (error) {\n\t\t\tconsole.error(`โ Failed to load agent \"${agent.name}\":`, error);\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to load agent: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tasync stopAgent(agentPath: string): Promise<void> {\n\t\t// Deprecated: explicit stop not needed; keep method no-op for backward compatibility\n\t\tthis.loadedAgents.delete(agentPath);\n\t\tconst agent = this.agents.get(agentPath);\n\t\tif (agent) {\n\t\t\tagent.instance = undefined;\n\t\t}\n\t}\n\n\tasync sendMessageToAgent(\n\t\tagentPath: string,\n\t\tmessage: string,\n\t\tattachments?: Array<{ name: string; mimeType: string; data: string }>,\n\t): Promise<string> {\n\t\t// Auto-start the agent if it's not already running\n\t\tif (!this.loadedAgents.has(agentPath)) {\n\t\t\tawait this.startAgent(agentPath);\n\t\t}\n\n\t\tconst loadedAgent = this.loadedAgents.get(agentPath);\n\t\tif (!loadedAgent) {\n\t\t\tthrow new Error(\"Agent failed to start\");\n\t\t}\n\n\t\ttry {\n\t\t\t// If attachments are present, construct a structured request compatible with the runner\n\t\t\tif (attachments && attachments.length > 0) {\n\t\t\t\tconst request: FullMessage = {\n\t\t\t\t\tparts: [\n\t\t\t\t\t\t{ text: message },\n\t\t\t\t\t\t...attachments.map((file) => ({\n\t\t\t\t\t\t\tinlineData: {\n\t\t\t\t\t\t\t\tmimeType: file.mimeType,\n\t\t\t\t\t\t\t\tdata: file.data,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})),\n\t\t\t\t\t],\n\t\t\t\t};\n\n\t\t\t\tconst response = await loadedAgent.runner.ask(request);\n\t\t\t\treturn response as string;\n\t\t\t}\n\n\t\t\t// No attachments: simple text\n\t\t\tconst response = await loadedAgent.runner.ask(message);\n\t\t\treturn response;\n\t\t} catch (error) {\n\t\t\tconst errorMessage =\n\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\tconsole.error(\n\t\t\t\t`Error sending message to agent ${agentPath}:`,\n\t\t\t\terrorMessage,\n\t\t\t);\n\t\t\tthrow new Error(`Failed to send message to agent: ${errorMessage}`);\n\t\t}\n\t}\n\n\tstopAllAgents(): void {\n\t\tfor (const [agentPath] of this.loadedAgents.entries()) {\n\t\t\tthis.stopAgent(agentPath);\n\t\t}\n\t}\n}\n\nexport class SessionManager {\n\tconstructor(private sessionService: InMemorySessionService) {}\n\n\tasync getSessionMessages(loadedAgent: LoadedAgent) {\n\t\ttry {\n\t\t\t// Get session from session service\n\t\t\tconst session = await this.sessionService.getSession(\n\t\t\t\tloadedAgent.appName,\n\t\t\t\tloadedAgent.userId,\n\t\t\t\tloadedAgent.sessionId,\n\t\t\t);\n\n\t\t\tif (!session || !session.events) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// Convert session events to message format\n\t\t\t// TODO(adk-web/tool-calls): Enhance this endpoint to better represent tool activity.\n\t\t\t// - Option A: Do not persist or return assistant events with empty text (current web filters these client-side).\n\t\t\t// - Option B: Keep raw history but add a query flag like `includeEmpty=false` to suppress blanks for clients that want clean text-only history.\n\t\t\t// - Option C (preferred): Emit explicit tool events, e.g., { type: \"tool\", name, args, output, status, timestamps } derived from non-text parts.\n\t\t\t// This enables the web UI to render compact \"Used tool: <name>\" chips and show outputs, instead of blank assistant messages.\n\t\t\t// When implemented, maintain backward compatibility by keeping the current shape under a flag (e.g., `format=legacy`).\n\t\t\tconst messages = session.events.map((event, index) => ({\n\t\t\t\tid: index + 1,\n\t\t\t\ttype:\n\t\t\t\t\tevent.author === \"user\" ? (\"user\" as const) : (\"assistant\" as const),\n\t\t\t\tcontent:\n\t\t\t\t\tevent.content?.parts\n\t\t\t\t\t\t?.map((part) =>\n\t\t\t\t\t\t\ttypeof part === \"object\" && \"text\" in part ? part.text : \"\",\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.join(\"\") || \"\",\n\t\t\t\ttimestamp: new Date(event.timestamp || Date.now()).toISOString(),\n\t\t\t}));\n\n\t\t\treturn messages;\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error fetching messages:\", error);\n\t\t\treturn [];\n\t\t}\n\t}\n}\n","import chalk from \"chalk\";\nimport { type ServeOptions, serveCommand } from \"./serve.js\";\n\nexport interface WebCommandOptions {\n\tport?: number;\n\tdir?: string;\n\thost?: string;\n\twebPort?: number;\n\tlocal?: boolean;\n\twebUrl?: string;\n}\n\nexport async function webCommand(\n\toptions: WebCommandOptions = {},\n): Promise<void> {\n\tconst apiPort = options.port || 8042; // Default to port 8042 to match serve command\n\tconst webPort = options.webPort || 3000;\n\tconst host = options.host || \"localhost\";\n\tconst useLocal = options.local || false;\n\tconst webUrl = options.webUrl || \"https://adk-web.iqai.com\";\n\n\tconsole.log(chalk.blue(\"๐ Starting ADK Web Interface...\"));\n\n\t// Start the API server first\n\tconst serveOptions: ServeOptions = {\n\t\tport: apiPort,\n\t\tdir: options.dir,\n\t\thost,\n\t\tquiet: true,\n\t};\n\n\tawait serveCommand(serveOptions);\n\n\tlet webAppUrl: string;\n\n\tif (useLocal) {\n\t\t// Local development: assume web app is already running on webPort\n\t\t// Only add port parameter if it's not the default\n\t\tif (apiPort === 8042) {\n\t\t\twebAppUrl = `http://${host}:${webPort}`;\n\t\t} else {\n\t\t\twebAppUrl = `http://${host}:${webPort}?port=${apiPort}`;\n\t\t}\n\t} else {\n\t\t// Production: use hosted web interface\n\t\t// Only add port parameter if it's not the default\n\t\tif (apiPort === 8042) {\n\t\t\twebAppUrl = webUrl;\n\t\t} else {\n\t\t\twebAppUrl = `${webUrl}?port=${apiPort}`;\n\t\t}\n\t}\n\n\t// Show the URL and server info\n\tconsole.log(chalk.cyan(`๐ Open this URL in your browser: ${webAppUrl}`));\n\tconsole.log(chalk.gray(` API Server: http://${host}:${apiPort}`));\n\n\tconsole.log(chalk.cyan(\"Press Ctrl+C to stop the API server\"));\n}\n"],"mappings":"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