@archon-claw/cli 0.0.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "url";
3
3
  import path from "path";
4
- import { readFileSync } from "fs";
4
+ import { readFileSync, readdirSync, statSync } from "fs";
5
5
  import dotenv from "dotenv";
6
6
  import { Command } from "commander";
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  import { loadAgentConfig } from "./config.js";
9
9
  import { createServer } from "./server.js";
10
- import { initSessionStore } from "./session.js";
10
+ import { SessionStore } from "./session.js";
11
+ import { startDev } from "./dev.js";
11
12
  import { runToolTests, formatResults } from "./test-runner.js";
12
13
  import { runEvals } from "./eval/runner.js";
13
- import { scaffoldAgent, scaffoldWorkspace } from "./scaffold.js";
14
+ import { scaffoldAgent, scaffoldWorkspace, updateSkills } from "./scaffold.js";
14
15
  const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../package.json"), "utf-8"));
15
16
  const program = new Command();
16
17
  program
@@ -30,31 +31,76 @@ program
30
31
  .action(() => {
31
32
  console.log("Starting agent...");
32
33
  });
34
+ /** Scan a directory for agent subdirectories and load them all */
35
+ async function loadAgentsFromDir(agentsDir) {
36
+ const absDir = path.resolve(agentsDir);
37
+ const entries = readdirSync(absDir);
38
+ const agents = new Map();
39
+ for (const name of entries) {
40
+ const fullPath = path.join(absDir, name);
41
+ if (!statSync(fullPath).isDirectory())
42
+ continue;
43
+ // Skip hidden dirs and common non-agent dirs
44
+ if (name.startsWith(".") || name === "node_modules")
45
+ continue;
46
+ try {
47
+ const config = await loadAgentConfig(fullPath);
48
+ const sessions = new SessionStore(fullPath);
49
+ await sessions.init();
50
+ agents.set(name, { config, sessions });
51
+ }
52
+ catch (err) {
53
+ console.warn(`[warn] Skipping "${name}": ${err instanceof Error ? err.message : err}`);
54
+ }
55
+ }
56
+ if (agents.size === 0) {
57
+ throw new Error(`No valid agents found in ${absDir}`);
58
+ }
59
+ return agents;
60
+ }
61
+ program
62
+ .command("dev")
63
+ .description("Start dev server with hot-reload for agent development")
64
+ .requiredOption("--agents-dir <path>", "Path to directory containing agent subdirectories")
65
+ .option("-p, --port <port>", "Server port", "5100")
66
+ .option("--no-open", "Don't auto-open browser")
67
+ .action(async (opts) => {
68
+ try {
69
+ await startDev(opts.agentsDir, {
70
+ port: parseInt(opts.port, 10),
71
+ open: opts.open,
72
+ });
73
+ }
74
+ catch (err) {
75
+ console.error(err instanceof Error ? err.message : err);
76
+ process.exit(1);
77
+ }
78
+ });
33
79
  program
34
80
  .command("start")
35
81
  .description("Start an agent HTTP server")
36
- .argument("<agent-dir>", "Path to agent directory")
82
+ .requiredOption("--agents-dir <path>", "Path to directory containing agent subdirectories")
37
83
  .option("-p, --port <port>", "Server port", "5100")
38
- .action(async (agentDir, opts) => {
84
+ .action(async (opts) => {
39
85
  try {
40
- const config = await loadAgentConfig(agentDir);
41
- await initSessionStore(agentDir);
86
+ const agents = await loadAgentsFromDir(opts.agentsDir);
42
87
  const port = parseInt(opts.port, 10);
43
- console.log(`Agent loaded:`);
44
- console.log(` Model: ${config.model.provider}/${config.model.model}`);
45
- console.log(` Tools: ${config.tools.map((t) => t.name).join(", ")}`);
46
- if (config.mcpManager) {
47
- const servers = config.mcpManager.getServerNames();
48
- console.log(` MCP servers: ${servers.join(", ")}`);
88
+ console.log("Agents loaded:");
89
+ for (const [id, entry] of agents) {
90
+ const { config } = entry;
91
+ const mcpCount = config.mcpManager ? config.mcpManager.getServerNames().length : 0;
92
+ console.log(` [${id}] Model: ${config.model.provider}/${config.model.model}` +
93
+ ` Tools: ${config.tools.length}` +
94
+ (mcpCount > 0 ? ` MCP: ${mcpCount} servers` : "") +
95
+ ` Skills: ${Object.keys(config.skills).length}`);
49
96
  }
50
- console.log(` Skills: ${Object.keys(config.skills).join(", ") || "(none)"}`);
51
- console.log(` System prompt: ${config.systemPrompt.length} chars`);
52
- const server = createServer(config, port);
97
+ const server = createServer(agents, port);
53
98
  const shutdown = async () => {
54
99
  console.log("\nShutting down...");
55
- await config.mcpManager?.shutdown();
100
+ for (const [, entry] of agents) {
101
+ await entry.config.mcpManager?.shutdown();
102
+ }
56
103
  server.close(() => process.exit(0));
57
- // Force exit if server hasn't closed within 3s
58
104
  setTimeout(() => process.exit(1), 3000).unref();
59
105
  };
60
106
  process.on("SIGINT", shutdown);
@@ -113,11 +159,40 @@ program
113
159
  });
114
160
  program
115
161
  .command("init")
116
- .description("Initialise an agent workspace with shared .claude/ skills")
117
- .argument("[dir]", "Directory to initialise", "./agents")
118
- .action(async (dir) => {
162
+ .description("Initialise an agent workspace project")
163
+ .argument("[dir]", "Directory to initialise")
164
+ .option("--install", "Automatically install npm dependencies")
165
+ .action(async (dir, opts) => {
119
166
  try {
120
- await scaffoldWorkspace(dir);
167
+ const { intro, text, confirm, isCancel, outro } = await import("@clack/prompts");
168
+ intro("archon-claw init");
169
+ if (!dir) {
170
+ const name = await text({
171
+ message: "Project name",
172
+ placeholder: "my-ai-agent",
173
+ validate: (v) => {
174
+ if (!v?.trim())
175
+ return "Project name is required";
176
+ if (!/^[a-z0-9._-]+$/i.test(v))
177
+ return "Invalid directory name";
178
+ },
179
+ });
180
+ if (isCancel(name)) {
181
+ outro("Cancelled");
182
+ return;
183
+ }
184
+ dir = name;
185
+ }
186
+ if (opts.install === undefined) {
187
+ const install = await confirm({ message: "Install dependencies?" });
188
+ if (isCancel(install)) {
189
+ outro("Cancelled");
190
+ return;
191
+ }
192
+ opts.install = install;
193
+ }
194
+ await scaffoldWorkspace(dir, { install: opts.install });
195
+ outro("Done!");
121
196
  }
122
197
  catch (err) {
123
198
  console.error(err instanceof Error ? err.message : err);
@@ -125,13 +200,49 @@ program
125
200
  }
126
201
  });
127
202
  program
128
- .command("create")
203
+ .command("create-agent")
129
204
  .description("Create a new agent project")
130
- .argument("<agent-name>", "Name of the agent (used as directory name)")
205
+ .argument("[agent-name]", "Name of the agent (used as directory name)")
131
206
  .option("-d, --dir <path>", "Parent directory for the agent", "./agents")
132
207
  .action(async (agentName, opts) => {
133
208
  try {
134
- await scaffoldAgent(agentName, opts.dir);
209
+ if (!agentName) {
210
+ const { intro, text, isCancel, outro } = await import("@clack/prompts");
211
+ intro("archon-claw create");
212
+ const name = await text({
213
+ message: "Agent name",
214
+ placeholder: "my-agent",
215
+ validate: (v) => {
216
+ if (!v?.trim())
217
+ return "Agent name is required";
218
+ if (!/^[a-z0-9._-]+$/i.test(v))
219
+ return "Invalid directory name";
220
+ },
221
+ });
222
+ if (isCancel(name)) {
223
+ outro("Cancelled");
224
+ return;
225
+ }
226
+ agentName = name;
227
+ await scaffoldAgent(agentName, opts.dir);
228
+ outro("Done!");
229
+ }
230
+ else {
231
+ await scaffoldAgent(agentName, opts.dir);
232
+ }
233
+ }
234
+ catch (err) {
235
+ console.error(err instanceof Error ? err.message : err);
236
+ process.exit(1);
237
+ }
238
+ });
239
+ program
240
+ .command("update-skills")
241
+ .description("Update skills to the latest version bundled with @archon-claw/cli")
242
+ .argument("[dir]", "Workspace directory", ".")
243
+ .action(async (dir) => {
244
+ try {
245
+ await updateSkills(dir);
135
246
  }
136
247
  catch (err) {
137
248
  console.error(err instanceof Error ? err.message : err);
package/dist/config.js CHANGED
@@ -11,7 +11,7 @@ export async function loadAgentConfig(agentDir) {
11
11
  // Validate directory structure
12
12
  const validation = await validateDir("agent-dir", absDir);
13
13
  if (!validation.valid) {
14
- throw new Error(`Invalid agent directory:\n${validation.errors.map((e) => ` - ${e}`).join("\n")}`);
14
+ throw new Error(`Invalid agent directory:\n${validation.errors.map((e) => ` - ${e.file ? `[${e.file}] ` : ""}${e.message}`).join("\n")}`);
15
15
  }
16
16
  // Load model.json
17
17
  const modelRaw = await fs.readFile(path.join(absDir, "model.json"), "utf-8");
@@ -74,7 +74,8 @@ export async function loadAgentConfig(agentDir) {
74
74
  const implEntries = await Promise.all(implFiles.map(async (file) => {
75
75
  const name = file.replace(/\.impl\.js$/, "");
76
76
  const implPath = pathToFileURL(path.join(implsDir, file)).href;
77
- const mod = await import(implPath);
77
+ // Cache-bust ESM module cache so dev reload picks up changes
78
+ const mod = await import(`${implPath}?t=${Date.now()}`);
78
79
  return [name, mod.default];
79
80
  }));
80
81
  for (const [name, impl] of implEntries) {
package/dist/dev.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import http from "node:http";
2
+ import { type FSWatcher } from "chokidar";
3
+ import { type AgentEntry } from "./server.js";
4
+ export interface DevOptions {
5
+ port: number;
6
+ open: boolean;
7
+ }
8
+ /** Returned by startDev for cleanup and testing */
9
+ export interface DevHandle {
10
+ server: http.Server;
11
+ watchers: FSWatcher[];
12
+ /** Current agents map (mutable via reload) */
13
+ getAgents(): Map<string, AgentEntry>;
14
+ /** Manually trigger config reload for a specific agent */
15
+ reloadAgent(agentId: string, changedFiles?: string[]): Promise<void>;
16
+ /** Graceful shutdown */
17
+ close(): Promise<void>;
18
+ }
19
+ export declare function startDev(agentsDir: string, opts: DevOptions): Promise<DevHandle>;
20
+ export declare function printBanner(agents: Map<string, AgentEntry>, port: number, absDir: string): void;
package/dist/dev.js ADDED
@@ -0,0 +1,170 @@
1
+ import path from "node:path";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { watch } from "chokidar";
4
+ import open from "open";
5
+ import { loadAgentConfig } from "./config.js";
6
+ import { createServer } from "./server.js";
7
+ import { SessionStore } from "./session.js";
8
+ /** Scan agents-dir for valid agent subdirectories */
9
+ function scanAgentDirs(agentsDir) {
10
+ const results = [];
11
+ for (const name of readdirSync(agentsDir)) {
12
+ const fullPath = path.join(agentsDir, name);
13
+ if (!statSync(fullPath).isDirectory())
14
+ continue;
15
+ if (name.startsWith(".") || name === "node_modules")
16
+ continue;
17
+ results.push({ id: name, path: fullPath });
18
+ }
19
+ return results;
20
+ }
21
+ export async function startDev(agentsDir, opts) {
22
+ const absDir = path.resolve(agentsDir);
23
+ const agentDirs = scanAgentDirs(absDir);
24
+ if (agentDirs.length === 0) {
25
+ throw new Error(`No agent directories found in ${absDir}`);
26
+ }
27
+ // Initial load of all agents
28
+ const currentAgents = new Map();
29
+ for (const { id, path: agentPath } of agentDirs) {
30
+ try {
31
+ const config = await loadAgentConfig(agentPath);
32
+ const sessions = new SessionStore(agentPath);
33
+ await sessions.init();
34
+ currentAgents.set(id, { config, sessions });
35
+ }
36
+ catch (err) {
37
+ console.warn(`[dev] Skipping "${id}": ${err instanceof Error ? err.message : err}`);
38
+ }
39
+ }
40
+ if (currentAgents.size === 0) {
41
+ throw new Error(`No valid agents found in ${absDir}`);
42
+ }
43
+ // Start server with getter — every request reads latest agents
44
+ const server = createServer(() => currentAgents, opts.port);
45
+ printBanner(currentAgents, opts.port, absDir);
46
+ // Open browser (skip in SSH)
47
+ if (opts.open && !process.env.SSH_CLIENT && !process.env.SSH_TTY) {
48
+ open(`http://localhost:${opts.port}`).catch(() => { });
49
+ }
50
+ // Per-agent reload logic
51
+ const reloadInFlight = new Set();
52
+ async function reloadAgent(agentId, changedFiles) {
53
+ if (reloadInFlight.has(agentId))
54
+ return;
55
+ reloadInFlight.add(agentId);
56
+ const agentEntry = agentDirs.find((d) => d.id === agentId);
57
+ if (!agentEntry) {
58
+ reloadInFlight.delete(agentId);
59
+ return;
60
+ }
61
+ const ts = new Date().toLocaleTimeString();
62
+ if (changedFiles?.length) {
63
+ const relFiles = changedFiles.map((f) => path.relative(agentEntry.path, f));
64
+ console.log(`\n[dev] ${ts} [${agentId}] Changed: ${relFiles.join(", ")}`);
65
+ }
66
+ try {
67
+ const existing = currentAgents.get(agentId);
68
+ await existing?.config.mcpManager?.shutdown();
69
+ const newConfig = await loadAgentConfig(agentEntry.path);
70
+ const sessions = existing?.sessions ?? new SessionStore(agentEntry.path);
71
+ if (!existing)
72
+ await sessions.init();
73
+ currentAgents.set(agentId, { config: newConfig, sessions });
74
+ console.log(`[dev] ${ts} [${agentId}] Reloaded \u2713` +
75
+ ` (tools: ${newConfig.tools.length}, skills: ${Object.keys(newConfig.skills).length})`);
76
+ }
77
+ catch (err) {
78
+ console.error(`[dev] ${ts} [${agentId}] Reload failed: ${err instanceof Error ? err.message : err}`);
79
+ console.error(`[dev] Continuing with previous valid configuration`);
80
+ }
81
+ finally {
82
+ reloadInFlight.delete(agentId);
83
+ }
84
+ }
85
+ // Watch each agent directory
86
+ const watchers = [];
87
+ for (const { id, path: agentPath } of agentDirs) {
88
+ if (!currentAgents.has(id))
89
+ continue; // Skip agents that failed to load
90
+ const watcher = watch(agentPath, {
91
+ ignored: [
92
+ "**/sessions/**",
93
+ "**/node_modules/**",
94
+ "**/eval-results/**",
95
+ "**/.DS_Store",
96
+ ],
97
+ ignoreInitial: true,
98
+ });
99
+ let debounceTimer = null;
100
+ let pendingFiles = [];
101
+ watcher.on("all", (_event, filePath) => {
102
+ pendingFiles.push(filePath);
103
+ if (debounceTimer)
104
+ clearTimeout(debounceTimer);
105
+ debounceTimer = setTimeout(async () => {
106
+ const files = [...pendingFiles];
107
+ pendingFiles = [];
108
+ await reloadAgent(id, files);
109
+ }, 100);
110
+ });
111
+ watchers.push(watcher);
112
+ }
113
+ // Graceful shutdown
114
+ const shutdown = async () => {
115
+ console.log("\nShutting down...");
116
+ await close();
117
+ process.exit(0);
118
+ };
119
+ process.on("SIGINT", shutdown);
120
+ process.on("SIGTERM", shutdown);
121
+ async function close() {
122
+ process.removeListener("SIGINT", shutdown);
123
+ process.removeListener("SIGTERM", shutdown);
124
+ for (const watcher of watchers) {
125
+ await watcher.close();
126
+ }
127
+ for (const [, entry] of currentAgents) {
128
+ await entry.config.mcpManager?.shutdown();
129
+ }
130
+ await new Promise((resolve) => server.close(() => resolve()));
131
+ }
132
+ return {
133
+ server,
134
+ watchers,
135
+ getAgents: () => currentAgents,
136
+ reloadAgent,
137
+ close,
138
+ };
139
+ }
140
+ export function printBanner(agents, port, absDir) {
141
+ const lines = [
142
+ "",
143
+ " archon-claw dev",
144
+ "",
145
+ ` Agents dir: ${path.relative(process.cwd(), absDir)}`,
146
+ "",
147
+ ];
148
+ for (const [id, entry] of agents) {
149
+ const { config } = entry;
150
+ const mcpCount = config.mcpManager
151
+ ? config.mcpManager.getServerNames().length
152
+ : 0;
153
+ lines.push(` [${id}] ${config.model.provider}/${config.model.model}` +
154
+ ` tools: ${config.tools.length}${mcpCount > 0 ? ` (+${mcpCount} mcp)` : ""}` +
155
+ ` skills: ${Object.keys(config.skills).length}`);
156
+ }
157
+ lines.push("");
158
+ lines.push(` URL: http://localhost:${port}`);
159
+ lines.push("");
160
+ lines.push(" Watching for changes...");
161
+ lines.push("");
162
+ const maxLen = Math.max(...lines.map((l) => l.length));
163
+ const top = "\u250C" + "\u2500".repeat(maxLen + 1) + "\u2510";
164
+ const bot = "\u2514" + "\u2500".repeat(maxLen + 1) + "\u2518";
165
+ console.log(top);
166
+ for (const line of lines) {
167
+ console.log("\u2502" + line.padEnd(maxLen + 1) + "\u2502");
168
+ }
169
+ console.log(bot);
170
+ }
@@ -0,0 +1,5 @@
1
+ import type http from "node:http";
2
+ import type { AgentEntry } from "./server.js";
3
+ type AgentMap = Map<string, AgentEntry>;
4
+ export declare function handleMcpRequest(agents: AgentMap, req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
5
+ export {};
@@ -0,0 +1,106 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { z } from "zod";
4
+ import { runAgentLoop } from "./agent.js";
5
+ function setupTools(server, agents) {
6
+ server.registerTool("list_agents", {
7
+ description: "List available agents",
8
+ }, async () => {
9
+ const list = [...agents.entries()].map(([id, entry]) => ({
10
+ id,
11
+ toolCount: entry.config.tools.length,
12
+ skillCount: Object.keys(entry.config.skills).length,
13
+ }));
14
+ return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
15
+ });
16
+ server.registerTool("chat", {
17
+ description: "Send a message to an agent and get a response. Only server-side tools are available; client/host tools are excluded.",
18
+ inputSchema: z.object({
19
+ agent: z.string().describe("Agent ID"),
20
+ message: z.string().describe("User message"),
21
+ sessionId: z.string().optional().describe("Session ID for conversation continuity"),
22
+ context: z.record(z.unknown()).optional().describe("Additional context passed to the agent system prompt"),
23
+ }),
24
+ }, async ({ agent, message, sessionId, context }) => {
25
+ const entry = agents.get(agent);
26
+ if (!entry) {
27
+ return { content: [{ type: "text", text: `Agent not found: ${agent}` }], isError: true };
28
+ }
29
+ const { config, sessions } = entry;
30
+ // Filter to server-side tools only (client/host tools can't execute without a browser)
31
+ const serverTools = config.tools.filter(t => (t.execution_target ?? "server") === "server");
32
+ const filteredConfig = { ...config, tools: serverTools };
33
+ const session = await sessions.getOrCreate(sessionId);
34
+ const texts = [];
35
+ try {
36
+ await runAgentLoop(filteredConfig, session, message, (event) => {
37
+ if (event.type === "text")
38
+ texts.push(event.content);
39
+ }, context);
40
+ }
41
+ catch (err) {
42
+ await sessions.save(session);
43
+ return {
44
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
45
+ isError: true,
46
+ };
47
+ }
48
+ await sessions.save(session);
49
+ return {
50
+ content: [{ type: "text", text: texts.join("") }],
51
+ structuredContent: { response: texts.join(""), sessionId: session.id },
52
+ };
53
+ });
54
+ server.registerTool("list_sessions", {
55
+ description: "List chat sessions for an agent",
56
+ inputSchema: z.object({
57
+ agent: z.string().describe("Agent ID"),
58
+ }),
59
+ }, async ({ agent }) => {
60
+ const entry = agents.get(agent);
61
+ if (!entry) {
62
+ return { content: [{ type: "text", text: `Agent not found: ${agent}` }], isError: true };
63
+ }
64
+ const all = await entry.sessions.list();
65
+ const list = all.map((s) => {
66
+ const firstUser = s.messages.find((m) => m.role === "user");
67
+ const title = firstUser
68
+ ? firstUser.content.slice(0, 50) + (firstUser.content.length > 50 ? "..." : "")
69
+ : undefined;
70
+ return { id: s.id, title, messageCount: s.messages.length, createdAt: s.createdAt, updatedAt: s.updatedAt };
71
+ }).sort((a, b) => b.updatedAt - a.updatedAt);
72
+ return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
73
+ });
74
+ server.registerTool("get_session", {
75
+ description: "Get session details and message history",
76
+ inputSchema: z.object({
77
+ agent: z.string().describe("Agent ID"),
78
+ sessionId: z.string().describe("Session ID"),
79
+ }),
80
+ }, async ({ agent, sessionId }) => {
81
+ const entry = agents.get(agent);
82
+ if (!entry) {
83
+ return { content: [{ type: "text", text: `Agent not found: ${agent}` }], isError: true };
84
+ }
85
+ const session = await entry.sessions.get(sessionId);
86
+ if (!session) {
87
+ return { content: [{ type: "text", text: "Session not found" }], isError: true };
88
+ }
89
+ return { content: [{ type: "text", text: JSON.stringify(session, null, 2) }] };
90
+ });
91
+ }
92
+ export async function handleMcpRequest(agents, req, res) {
93
+ const server = new McpServer({ name: "archon-claw", version: "1.0.0" });
94
+ setupTools(server, agents);
95
+ const transport = new StreamableHTTPServerTransport({
96
+ sessionIdGenerator: undefined,
97
+ enableJsonResponse: true,
98
+ });
99
+ try {
100
+ await server.connect(transport);
101
+ await transport.handleRequest(req, res);
102
+ }
103
+ finally {
104
+ await server.close();
105
+ }
106
+ }