@archon-claw/cli 0.6.1 → 0.7.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,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "url";
3
3
  import path from "path";
4
- import { readFileSync, readdirSync, statSync } from "fs";
4
+ import { readFileSync } 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
- import { loadAgentConfig } from "./config.js";
9
8
  import { createServer } from "./server.js";
10
- import { SessionStore } from "./session.js";
11
9
  import { startDev } from "./dev.js";
10
+ import { scanAgentDirs, loadAgents, setupWatch } from "./watch.js";
12
11
  import { runToolTests, formatResults } from "./test-runner.js";
13
12
  import { runEvals } from "./eval/runner.js";
14
13
  const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../package.json"), "utf-8"));
@@ -24,33 +23,6 @@ program
24
23
  : path.resolve(process.cwd(), ".env");
25
24
  dotenv.config({ path: envPath });
26
25
  });
27
- /** Scan a directory for agent subdirectories and load them all */
28
- async function loadAgentsFromDir(agentsDir) {
29
- const absDir = path.resolve(agentsDir);
30
- const entries = readdirSync(absDir);
31
- const agents = new Map();
32
- for (const name of entries) {
33
- const fullPath = path.join(absDir, name);
34
- if (!statSync(fullPath).isDirectory())
35
- continue;
36
- // Skip hidden dirs and common non-agent dirs
37
- if (name.startsWith(".") || name === "node_modules")
38
- continue;
39
- try {
40
- const config = await loadAgentConfig(fullPath);
41
- const sessions = new SessionStore(fullPath);
42
- await sessions.init();
43
- agents.set(name, { config, sessions });
44
- }
45
- catch (err) {
46
- console.warn(`[warn] Skipping "${name}": ${err instanceof Error ? err.message : err}`);
47
- }
48
- }
49
- if (agents.size === 0) {
50
- throw new Error(`No valid agents found in ${absDir}`);
51
- }
52
- return agents;
53
- }
54
26
  program
55
27
  .command("dev")
56
28
  .description("Start dev server with hot-reload for agent development")
@@ -74,9 +46,18 @@ program
74
46
  .description("Start an agent HTTP server")
75
47
  .requiredOption("--agents-dir <path>", "Path to directory containing agent subdirectories")
76
48
  .option("-p, --port <port>", "Server port", "5100")
49
+ .option("-w, --watch", "Watch agent files for changes and hot-reload")
77
50
  .action(async (opts) => {
78
51
  try {
79
- const agents = await loadAgentsFromDir(opts.agentsDir);
52
+ const absDir = path.resolve(opts.agentsDir);
53
+ const agentDirs = scanAgentDirs(absDir);
54
+ if (agentDirs.length === 0) {
55
+ throw new Error(`No agent directories found in ${absDir}`);
56
+ }
57
+ const agents = await loadAgents(agentDirs, "warn");
58
+ if (agents.size === 0) {
59
+ throw new Error(`No valid agents found in ${absDir}`);
60
+ }
80
61
  const port = parseInt(opts.port, 10);
81
62
  console.log("Agents loaded:");
82
63
  for (const [id, entry] of agents) {
@@ -87,9 +68,19 @@ program
87
68
  (mcpCount > 0 ? ` MCP: ${mcpCount} servers` : "") +
88
69
  ` Skills: ${Object.keys(config.skills).length}`);
89
70
  }
90
- const server = createServer(agents, port);
71
+ // With --watch: pass getter function so server always sees latest agents
72
+ // Without --watch: pass static Map (no file watching overhead)
73
+ const server = opts.watch
74
+ ? createServer(() => agents, port)
75
+ : createServer(agents, port);
76
+ let watchHandle;
77
+ if (opts.watch) {
78
+ watchHandle = setupWatch(agentDirs, agents, "watch");
79
+ console.log("Watching for file changes...");
80
+ }
91
81
  const shutdown = async () => {
92
82
  console.log("\nShutting down...");
83
+ await watchHandle?.close();
93
84
  for (const [, entry] of agents) {
94
85
  await entry.config.mcpManager?.shutdown();
95
86
  }
package/dist/dev.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from "node:http";
2
- import { type FSWatcher } from "chokidar";
2
+ import type { FSWatcher } from "chokidar";
3
3
  import { type AgentEntry } from "./server.js";
4
4
  export interface DevOptions {
5
5
  port: number;
package/dist/dev.js CHANGED
@@ -1,23 +1,7 @@
1
1
  import path from "node:path";
2
- import { readdirSync, statSync } from "node:fs";
3
- import { watch } from "chokidar";
4
2
  import open from "open";
5
- import { loadAgentConfig } from "./config.js";
6
3
  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
- }
4
+ import { scanAgentDirs, loadAgents, setupWatch } from "./watch.js";
21
5
  export async function startDev(agentsDir, opts) {
22
6
  const absDir = path.resolve(agentsDir);
23
7
  const agentDirs = scanAgentDirs(absDir);
@@ -25,18 +9,7 @@ export async function startDev(agentsDir, opts) {
25
9
  throw new Error(`No agent directories found in ${absDir}`);
26
10
  }
27
11
  // 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
- }
12
+ const currentAgents = await loadAgents(agentDirs, "dev");
40
13
  if (currentAgents.size === 0) {
41
14
  throw new Error(`No valid agents found in ${absDir}`);
42
15
  }
@@ -47,69 +20,8 @@ export async function startDev(agentsDir, opts) {
47
20
  if (opts.open && !process.env.SSH_CLIENT && !process.env.SSH_TTY) {
48
21
  open(`http://localhost:${opts.port}`).catch(() => { });
49
22
  }
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
- }
23
+ // Setup file watchers
24
+ const watchHandle = setupWatch(agentDirs, currentAgents, "dev");
113
25
  // Graceful shutdown
114
26
  const shutdown = async () => {
115
27
  console.log("\nShutting down...");
@@ -121,9 +33,7 @@ export async function startDev(agentsDir, opts) {
121
33
  async function close() {
122
34
  process.removeListener("SIGINT", shutdown);
123
35
  process.removeListener("SIGTERM", shutdown);
124
- for (const watcher of watchers) {
125
- await watcher.close();
126
- }
36
+ await watchHandle.close();
127
37
  for (const [, entry] of currentAgents) {
128
38
  await entry.config.mcpManager?.shutdown();
129
39
  }
@@ -131,9 +41,9 @@ export async function startDev(agentsDir, opts) {
131
41
  }
132
42
  return {
133
43
  server,
134
- watchers,
44
+ watchers: watchHandle.watchers,
135
45
  getAgents: () => currentAgents,
136
- reloadAgent,
46
+ reloadAgent: watchHandle.reloadAgent,
137
47
  close,
138
48
  };
139
49
  }
@@ -0,0 +1,21 @@
1
+ import { type FSWatcher } from "chokidar";
2
+ import type { AgentEntry } from "./server.js";
3
+ export interface AgentDir {
4
+ id: string;
5
+ path: string;
6
+ }
7
+ export interface WatchHandle {
8
+ watchers: FSWatcher[];
9
+ reloadAgent(agentId: string, changedFiles?: string[]): Promise<void>;
10
+ close(): Promise<void>;
11
+ }
12
+ /** Scan agents-dir for valid agent subdirectories */
13
+ export declare function scanAgentDirs(agentsDir: string): AgentDir[];
14
+ /** Load all agents from scanned directories into a Map */
15
+ export declare function loadAgents(agentDirs: AgentDir[], logPrefix: string): Promise<Map<string, AgentEntry>>;
16
+ /**
17
+ * Setup file watchers for agent directories.
18
+ * On file changes, automatically reloads the affected agent config while
19
+ * preserving existing sessions and gracefully handling errors.
20
+ */
21
+ export declare function setupWatch(agentDirs: AgentDir[], currentAgents: Map<string, AgentEntry>, logPrefix?: string): WatchHandle;
package/dist/watch.js ADDED
@@ -0,0 +1,109 @@
1
+ import path from "node:path";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { watch } from "chokidar";
4
+ import { loadAgentConfig } from "./config.js";
5
+ import { SessionStore } from "./session.js";
6
+ const IGNORED = [
7
+ "**/sessions/**",
8
+ "**/node_modules/**",
9
+ "**/eval-results/**",
10
+ "**/.DS_Store",
11
+ ];
12
+ /** Scan agents-dir for valid agent subdirectories */
13
+ export function scanAgentDirs(agentsDir) {
14
+ const results = [];
15
+ for (const name of readdirSync(agentsDir)) {
16
+ const fullPath = path.join(agentsDir, name);
17
+ if (!statSync(fullPath).isDirectory())
18
+ continue;
19
+ if (name.startsWith(".") || name === "node_modules")
20
+ continue;
21
+ results.push({ id: name, path: fullPath });
22
+ }
23
+ return results;
24
+ }
25
+ /** Load all agents from scanned directories into a Map */
26
+ export async function loadAgents(agentDirs, logPrefix) {
27
+ const agents = new Map();
28
+ for (const { id, path: agentPath } of agentDirs) {
29
+ try {
30
+ const config = await loadAgentConfig(agentPath);
31
+ const sessions = new SessionStore(agentPath);
32
+ await sessions.init();
33
+ agents.set(id, { config, sessions });
34
+ }
35
+ catch (err) {
36
+ console.warn(`[${logPrefix}] Skipping "${id}": ${err instanceof Error ? err.message : err}`);
37
+ }
38
+ }
39
+ return agents;
40
+ }
41
+ /**
42
+ * Setup file watchers for agent directories.
43
+ * On file changes, automatically reloads the affected agent config while
44
+ * preserving existing sessions and gracefully handling errors.
45
+ */
46
+ export function setupWatch(agentDirs, currentAgents, logPrefix = "watch") {
47
+ const reloadInFlight = new Set();
48
+ async function reloadAgent(agentId, changedFiles) {
49
+ if (reloadInFlight.has(agentId))
50
+ return;
51
+ reloadInFlight.add(agentId);
52
+ const agentEntry = agentDirs.find((d) => d.id === agentId);
53
+ if (!agentEntry) {
54
+ reloadInFlight.delete(agentId);
55
+ return;
56
+ }
57
+ const ts = new Date().toLocaleTimeString();
58
+ if (changedFiles?.length) {
59
+ const relFiles = changedFiles.map((f) => path.relative(agentEntry.path, f));
60
+ console.log(`\n[${logPrefix}] ${ts} [${agentId}] Changed: ${relFiles.join(", ")}`);
61
+ }
62
+ try {
63
+ const existing = currentAgents.get(agentId);
64
+ await existing?.config.mcpManager?.shutdown();
65
+ const newConfig = await loadAgentConfig(agentEntry.path);
66
+ const sessions = existing?.sessions ?? new SessionStore(agentEntry.path);
67
+ if (!existing)
68
+ await sessions.init();
69
+ currentAgents.set(agentId, { config: newConfig, sessions });
70
+ console.log(`[${logPrefix}] ${ts} [${agentId}] Reloaded \u2713` +
71
+ ` (tools: ${newConfig.tools.length}, skills: ${Object.keys(newConfig.skills).length})`);
72
+ }
73
+ catch (err) {
74
+ console.error(`[${logPrefix}] ${ts} [${agentId}] Reload failed: ${err instanceof Error ? err.message : err}`);
75
+ console.error(`[${logPrefix}] Continuing with previous valid configuration`);
76
+ }
77
+ finally {
78
+ reloadInFlight.delete(agentId);
79
+ }
80
+ }
81
+ const watchers = [];
82
+ for (const { id, path: agentPath } of agentDirs) {
83
+ if (!currentAgents.has(id))
84
+ continue;
85
+ const watcher = watch(agentPath, {
86
+ ignored: IGNORED,
87
+ ignoreInitial: true,
88
+ });
89
+ let debounceTimer = null;
90
+ let pendingFiles = [];
91
+ watcher.on("all", (_event, filePath) => {
92
+ pendingFiles.push(filePath);
93
+ if (debounceTimer)
94
+ clearTimeout(debounceTimer);
95
+ debounceTimer = setTimeout(async () => {
96
+ const files = [...pendingFiles];
97
+ pendingFiles = [];
98
+ await reloadAgent(id, files);
99
+ }, 100);
100
+ });
101
+ watchers.push(watcher);
102
+ }
103
+ async function close() {
104
+ for (const watcher of watchers) {
105
+ await watcher.close();
106
+ }
107
+ }
108
+ return { watchers, reloadAgent, close };
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archon-claw/cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "AI Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,8 @@
44
44
  "test": "vitest run --exclude dist --exclude examples",
45
45
  "test:watch": "vitest --exclude dist --exclude examples",
46
46
  "e2e": "playwright test",
47
- "e2e:ui": "playwright test --ui"
47
+ "e2e:ui": "playwright test --ui",
48
+ "e2e:start-watch": "playwright test --config playwright.start-watch.config.ts",
49
+ "e2e:start-watch:ui": "playwright test --config playwright.start-watch.config.ts --ui"
48
50
  }
49
51
  }