@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 +23 -32
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +7 -97
- package/dist/watch.d.ts +21 -0
- package/dist/watch.js +109 -0
- package/package.json +4 -2
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
|
|
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
|
|
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
|
-
|
|
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
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 {
|
|
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 =
|
|
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
|
-
//
|
|
51
|
-
const
|
|
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
|
-
|
|
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
|
}
|
package/dist/watch.d.ts
ADDED
|
@@ -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.
|
|
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
|
}
|