@archon-claw/cli
Advanced tools
+18
-14
@@ -64,25 +64,29 @@ #!/usr/bin/env node | ||
| } | ||
| const agents = await loadAgents(agentDirs, "warn"); | ||
| if (agents.size === 0) { | ||
| throw new Error(`No valid agents found in ${absDir}`); | ||
| } | ||
| const { agents, errors } = await loadAgents(agentDirs, "warn"); | ||
| const entry = opts.entry ?? loadProjectConfig(absDir)?.entry; | ||
| if (entry && !agents.has(entry)) { | ||
| // Only fatal if entry is specified but not found — no fallback | ||
| throw new Error(`Entry agent "${entry}" not found. Available agents: ${[...agents.keys()].join(", ")}`); | ||
| } | ||
| const port = parseInt(opts.port, 10); | ||
| console.log("Agents loaded:"); | ||
| for (const [id, entry] of agents) { | ||
| const { config } = entry; | ||
| const mcpCount = config.mcpManager ? config.mcpManager.getServerNames().length : 0; | ||
| console.log(` [${id}] Model: ${config.model.provider}/${config.model.model}` + | ||
| ` Tools: ${config.tools.length}` + | ||
| (mcpCount > 0 ? ` MCP: ${mcpCount} servers` : "") + | ||
| ` Skills: ${Object.keys(config.skills).length}`); | ||
| if (agents.size > 0) { | ||
| console.log("Agents loaded:"); | ||
| for (const [id, entry] of agents) { | ||
| const { config } = entry; | ||
| const mcpCount = config.mcpManager ? config.mcpManager.getServerNames().length : 0; | ||
| console.log(` [${id}] Model: ${config.model.provider}/${config.model.model}` + | ||
| ` Tools: ${config.tools.length}` + | ||
| (mcpCount > 0 ? ` MCP: ${mcpCount} servers` : "") + | ||
| ` Skills: ${Object.keys(config.skills).length}`); | ||
| } | ||
| } | ||
| if (errors.length > 0) { | ||
| console.warn(`\n${errors.length} agent(s) failed validation (errors exposed via GET /api/health)`); | ||
| } | ||
| // With --watch: pass getter function so server always sees latest agents | ||
| // Without --watch: pass static Map (no file watching overhead) | ||
| const serverOpts = { entry, errors }; | ||
| const server = opts.watch | ||
| ? createServer(() => agents, port, { entry }) | ||
| : createServer(agents, port, { entry }); | ||
| ? createServer(() => agents, port, serverOpts) | ||
| : createServer(agents, port, serverOpts); | ||
| let watchHandle; | ||
@@ -89,0 +93,0 @@ if (opts.watch) { |
+2
-31
@@ -149,24 +149,2 @@ import fs from "node:fs/promises"; | ||
| } | ||
| // Build tool template variables (object map by name, no array form) | ||
| const toolMap = {}; | ||
| for (const t of tools) { | ||
| toolMap[t.name] = { name: t.name, description: t.description }; | ||
| } | ||
| // Build mcp template variable: mcp.{serverName}.tools (object map) + mcp.servers | ||
| const mcpVar = { servers: [] }; | ||
| if (mcpManager) { | ||
| const byServer = mcpManager.getToolsByServer(); | ||
| const servers = []; | ||
| for (const serverName of mcpManager.getServerNames()) { | ||
| const serverTools = byServer[serverName] ?? []; | ||
| const serverToolMap = {}; | ||
| for (const t of serverTools) { | ||
| serverToolMap[t.name] = { name: t.name, description: t.description }; | ||
| } | ||
| const serverObj = { name: serverName, tools: serverToolMap }; | ||
| mcpVar[serverName] = serverObj; | ||
| servers.push(serverObj); | ||
| } | ||
| mcpVar.servers = servers; | ||
| } | ||
| // Inject built-in skill_load tool when skills/ directory has content | ||
@@ -202,16 +180,9 @@ if (Object.keys(skills).length > 0) { | ||
| } | ||
| // Rebuild tool template variables after potential skill_load injection | ||
| const toolMap2 = {}; | ||
| for (const t of tools) { | ||
| toolMap2[t.name] = { name: t.name, description: t.description }; | ||
| } | ||
| // Load and render system prompt (after tools/MCP so they're available in template) | ||
| // Load and render system prompt (datasets available as template variables) | ||
| const promptRaw = await fs.readFile(path.join(absDir, "system-prompt.md"), "utf-8"); | ||
| const engine = new Liquid(); | ||
| const templateVars = { ...datasets, tools: toolMap2, mcp: mcpVar }; | ||
| const systemPrompt = (await engine.parseAndRender(promptRaw, templateVars)).trim(); | ||
| const systemPrompt = (await engine.parseAndRender(promptRaw, datasets)).trim(); | ||
| return { | ||
| systemPrompt, model: model, tools, toolImpls, skills, mcpManager, agentDir: absDir, toolUIs, | ||
| _promptTemplate: promptRaw, _templateVars: templateVars, | ||
| }; | ||
| } |
+3
-3
@@ -12,4 +12,4 @@ import path from "node:path"; | ||
| // Initial load of all agents | ||
| const currentAgents = await loadAgents(agentDirs, "dev"); | ||
| if (currentAgents.size === 0) { | ||
| const { agents: currentAgents, errors } = await loadAgents(agentDirs, "dev"); | ||
| if (currentAgents.size === 0 && errors.length === 0) { | ||
| throw new Error(`No valid agents found in ${absDir}`); | ||
@@ -21,3 +21,3 @@ } | ||
| // Start server with getter — every request reads latest agents | ||
| const server = createServer(() => currentAgents, opts.port, { entry: opts.entry }); | ||
| const server = createServer(() => currentAgents, opts.port, { entry: opts.entry, errors }); | ||
| printBanner(currentAgents, opts.port, absDir); | ||
@@ -24,0 +24,0 @@ // Open browser (skip in SSH) |
+2
-1
| import http from "node:http"; | ||
| import type { AgentConfig } from "./types.js"; | ||
| import type { AgentConfig, AgentLoadError } from "./types.js"; | ||
| import type { SessionStore } from "./session.js"; | ||
@@ -13,4 +13,5 @@ export interface AgentEntry { | ||
| entry?: string; | ||
| errors?: AgentLoadError[]; | ||
| } | ||
| export declare function createServer(agents: AgentResolver, port: number, options?: ServerOptions): http.Server; | ||
| export {}; |
+12
-1
@@ -128,2 +128,3 @@ import http from "node:http"; | ||
| const entryAgentId = options?.entry; | ||
| const loadErrors = options?.errors ?? []; | ||
| const server = http.createServer(async (req, res) => { | ||
@@ -168,3 +169,13 @@ const agentMap = resolveAgents(); | ||
| if (method === "GET" && url.pathname === "/api/health") { | ||
| json(res, 200, { status: "ok" }); | ||
| const agentIds = [...agentMap.keys()]; | ||
| if (loadErrors.length > 0) { | ||
| json(res, agentIds.length > 0 ? 200 : 503, { | ||
| status: agentIds.length > 0 ? "degraded" : "error", | ||
| agents: agentIds, | ||
| errors: loadErrors, | ||
| }); | ||
| } | ||
| else { | ||
| json(res, 200, { status: "ok", agents: agentIds }); | ||
| } | ||
| return; | ||
@@ -171,0 +182,0 @@ } |
+0
-10
| import fs from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import { Liquid } from "liquidjs"; | ||
| import { runAgentLoop } from "./agent.js"; | ||
@@ -150,12 +149,3 @@ /** Check if an agent directory contains team.json */ | ||
| }); | ||
| // Re-render system prompt with {{ team.agents }} variable | ||
| if (config._promptTemplate && config._templateVars) { | ||
| const teamVar = { agents: buildTeamAgentList(agents, agentId) }; | ||
| const engine = new Liquid(); | ||
| config.systemPrompt = (await engine.parseAndRender(config._promptTemplate, { | ||
| ...config._templateVars, | ||
| team: teamVar, | ||
| })).trim(); | ||
| } | ||
| } | ||
| } |
+5
-4
@@ -21,6 +21,2 @@ export type { JSONSchemaProperty, JSONSchema, ToolSchema, ModelConfig, } from "./schemas.js"; | ||
| toolUIs: Set<string>; | ||
| /** @internal Raw Liquid template for re-rendering (e.g., after team injection) */ | ||
| _promptTemplate?: string; | ||
| /** @internal Template variables for system prompt re-rendering */ | ||
| _templateVars?: Record<string, unknown>; | ||
| } | ||
@@ -100,2 +96,7 @@ /** OpenAI-compatible chat message */ | ||
| }; | ||
| /** Per-agent validation error for health reporting */ | ||
| export interface AgentLoadError { | ||
| agent: string; | ||
| messages: string[]; | ||
| } | ||
| /** Chat session state */ | ||
@@ -102,0 +103,0 @@ export interface Session { |
+7
-1
| import { type FSWatcher } from "chokidar"; | ||
| import type { AgentLoadError } from "./types.js"; | ||
| import type { AgentEntry } from "./server.js"; | ||
@@ -14,4 +15,9 @@ export interface AgentDir { | ||
| export declare function scanAgentDirs(agentsDir: string): AgentDir[]; | ||
| /** Result of loading agents: successfully loaded agents + any validation errors */ | ||
| export interface LoadAgentsResult { | ||
| agents: Map<string, AgentEntry>; | ||
| errors: AgentLoadError[]; | ||
| } | ||
| /** Load all agents from scanned directories into a Map */ | ||
| export declare function loadAgents(agentDirs: AgentDir[], logPrefix: string): Promise<Map<string, AgentEntry>>; | ||
| export declare function loadAgents(agentDirs: AgentDir[], logPrefix: string): Promise<LoadAgentsResult>; | ||
| /** | ||
@@ -18,0 +24,0 @@ * Setup file watchers for agent directories. |
+9
-2
@@ -29,2 +29,3 @@ import path from "node:path"; | ||
| const agents = new Map(); | ||
| const errors = []; | ||
| for (const { id, path: agentPath } of agentDirs) { | ||
@@ -38,3 +39,9 @@ try { | ||
| catch (err) { | ||
| console.warn(`[${logPrefix}] Skipping "${id}": ${err instanceof Error ? err.message : err}`); | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| console.warn(`[${logPrefix}] Skipping "${id}": ${message}`); | ||
| // Parse multi-line error messages (e.g. "Invalid agent directory:\n - [model.json] /provider Required") | ||
| const lines = message.split("\n").map((l) => l.replace(/^\s*-\s*/, "").trim()).filter(Boolean); | ||
| // Skip the header line if it's a wrapper message | ||
| const detailLines = lines.length > 1 && lines[0].endsWith(":") ? lines.slice(1) : lines; | ||
| errors.push({ agent: id, messages: detailLines }); | ||
| } | ||
@@ -44,3 +51,3 @@ } | ||
| await injectTeamTools(agents); | ||
| return agents; | ||
| return { agents, errors }; | ||
| } | ||
@@ -47,0 +54,0 @@ /** |
+1
-1
| { | ||
| "name": "@archon-claw/cli", | ||
| "version": "0.9.0", | ||
| "version": "0.10.0", | ||
| "description": "AI Agent CLI", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
810442
-0.04%7380
-0.12%