aoaoe 0.54.0 → 0.56.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/README.md CHANGED
@@ -241,7 +241,9 @@ commands:
241
241
  status quick daemon health check (is it running? what's it doing?)
242
242
  config show the effective resolved config (defaults + file)
243
243
  config --validate validate config + check tool availability
244
+ config --diff show only fields that differ from defaults
244
245
  notify-test send a test notification to configured webhooks
246
+ doctor comprehensive health check (config, tools, daemon, disk)
245
247
  task manage tasks and sessions (list, start, stop, new, rm, edit)
246
248
  tasks show task progress (from aoaoe.tasks.json)
247
249
  history review recent actions (from ~/.aoaoe/actions.log)
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AoaoeConfig } from "./types.js";
2
+ export declare const DEFAULTS: AoaoeConfig;
2
3
  export declare function findConfigFile(): string | null;
3
4
  export declare function configFileExists(): boolean;
4
5
  export declare function defaultConfigPath(): string;
@@ -9,6 +10,11 @@ export declare function warnUnknownKeys(raw: unknown, source: string): void;
9
10
  export declare function validateConfig(config: AoaoeConfig): void;
10
11
  export declare function validateEnvironment(config: AoaoeConfig): Promise<void>;
11
12
  export declare function deepMerge(...objects: Record<string, unknown>[]): AoaoeConfig;
13
+ export declare function computeConfigDiff(current: Record<string, unknown>, defaults: Record<string, unknown>, prefix?: string): Array<{
14
+ path: string;
15
+ current: unknown;
16
+ default: unknown;
17
+ }>;
12
18
  export declare function parseCliArgs(argv: string[]): {
13
19
  overrides: Partial<AoaoeConfig>;
14
20
  help: boolean;
@@ -21,7 +27,9 @@ export declare function parseCliArgs(argv: string[]): {
21
27
  showStatus: boolean;
22
28
  showConfig: boolean;
23
29
  configValidate: boolean;
30
+ configDiff: boolean;
24
31
  notifyTest: boolean;
32
+ runDoctor: boolean;
25
33
  runInit: boolean;
26
34
  initForce: boolean;
27
35
  runTaskCli: boolean;
package/dist/config.js CHANGED
@@ -9,7 +9,7 @@ const AOAOE_DIR = join(homedir(), ".aoaoe");
9
9
  const CONFIG_NAMES = ["aoaoe.config.json", ".aoaoe.json"];
10
10
  // search order: ~/.aoaoe/ first (canonical), then cwd (local override for dev)
11
11
  const CONFIG_SEARCH_DIRS = [AOAOE_DIR, process.cwd()];
12
- const DEFAULTS = {
12
+ export const DEFAULTS = {
13
13
  reasoner: "opencode",
14
14
  pollIntervalMs: 10_000,
15
15
  opencode: {
@@ -259,6 +259,31 @@ export function deepMerge(...objects) {
259
259
  }
260
260
  return result;
261
261
  }
262
+ // compute fields that differ between two config objects (flat dot-notation paths)
263
+ // exported for testing
264
+ export function computeConfigDiff(current, defaults, prefix = "") {
265
+ const diffs = [];
266
+ const allKeys = new Set([...Object.keys(current), ...Object.keys(defaults)]);
267
+ for (const key of allKeys) {
268
+ const fullPath = prefix ? `${prefix}.${key}` : key;
269
+ const curVal = current[key];
270
+ const defVal = defaults[key];
271
+ // both are plain objects — recurse
272
+ if (curVal && defVal &&
273
+ typeof curVal === "object" && !Array.isArray(curVal) &&
274
+ typeof defVal === "object" && !Array.isArray(defVal)) {
275
+ diffs.push(...computeConfigDiff(curVal, defVal, fullPath));
276
+ continue;
277
+ }
278
+ // compare with JSON.stringify for arrays/objects, === for primitives
279
+ const curStr = JSON.stringify(curVal);
280
+ const defStr = JSON.stringify(defVal);
281
+ if (curStr !== defStr) {
282
+ diffs.push({ path: fullPath, current: curVal, default: defVal });
283
+ }
284
+ }
285
+ return diffs;
286
+ }
262
287
  function log(msg) {
263
288
  console.error(`[config] ${msg}`);
264
289
  }
@@ -274,7 +299,7 @@ export function parseCliArgs(argv) {
274
299
  let initForce = false;
275
300
  let runTaskCli = false;
276
301
  let registerTitle;
277
- const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, notifyTest: false, runInit: false, initForce: false, runTaskCli: false };
302
+ const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runInit: false, initForce: false, runTaskCli: false };
278
303
  // check for subcommand as first non-flag arg
279
304
  if (argv[2] === "test-context") {
280
305
  return { ...defaults, testContext: true };
@@ -296,11 +321,15 @@ export function parseCliArgs(argv) {
296
321
  }
297
322
  if (argv[2] === "config") {
298
323
  const validate = argv.includes("--validate") || argv.includes("-V");
299
- return { ...defaults, showConfig: true, configValidate: validate };
324
+ const diff = argv.includes("--diff");
325
+ return { ...defaults, showConfig: true, configValidate: validate, configDiff: diff };
300
326
  }
301
327
  if (argv[2] === "notify-test") {
302
328
  return { ...defaults, notifyTest: true };
303
329
  }
330
+ if (argv[2] === "doctor") {
331
+ return { ...defaults, runDoctor: true };
332
+ }
304
333
  if (argv[2] === "init") {
305
334
  const force = argv.includes("--force") || argv.includes("-f");
306
335
  return { ...defaults, runInit: true, initForce: force };
@@ -390,7 +419,7 @@ export function parseCliArgs(argv) {
390
419
  break;
391
420
  }
392
421
  }
393
- return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, notifyTest: false, runInit: false, initForce: false, runTaskCli: false };
422
+ return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runInit: false, initForce: false, runTaskCli: false };
394
423
  }
395
424
  export function printHelp() {
396
425
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -408,8 +437,10 @@ commands:
408
437
  (none) start the supervisor daemon (interactive TUI)
409
438
  status quick daemon health check (is it running? what's it doing?)
410
439
  config show the effective resolved config (defaults + file)
411
- config --validate validate config file + check tool availability
440
+ config --validate validate config + check tool availability
441
+ config --diff show only fields that differ from defaults
412
442
  notify-test send a test notification to configured webhooks
443
+ doctor comprehensive health check (config, tools, daemon, disk)
413
444
  task manage tasks and sessions (list, start, stop, new, rm, edit)
414
445
  tasks show task progress (from aoaoe.tasks.json)
415
446
  history review recent actions (from ~/.aoaoe/actions.log)
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { loadConfig, validateEnvironment, parseCliArgs, printHelp, configFileExists, findConfigFile } from "./config.js";
2
+ import { loadConfig, validateEnvironment, parseCliArgs, printHelp, configFileExists, findConfigFile, DEFAULTS, computeConfigDiff } from "./config.js";
3
3
  import { Poller, computeTmuxName } from "./poller.js";
4
4
  import { createReasoner } from "./reasoner/index.js";
5
5
  import { Executor } from "./executor.js";
@@ -28,7 +28,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
28
28
  const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
29
29
  const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
30
30
  async function main() {
31
- const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, notifyTest, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
31
+ const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, configDiff, notifyTest, runDoctor, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
32
32
  if (help) {
33
33
  printHelp();
34
34
  process.exit(0);
@@ -73,11 +73,14 @@ async function main() {
73
73
  showDaemonStatus();
74
74
  return;
75
75
  }
76
- // `aoaoe config` -- show effective resolved config (with optional --validate)
76
+ // `aoaoe config` -- show effective resolved config (with optional --validate or --diff)
77
77
  if (showConfig) {
78
78
  if (configValidate) {
79
79
  await runConfigValidation();
80
80
  }
81
+ else if (configDiff) {
82
+ showConfigDiff();
83
+ }
81
84
  else {
82
85
  showEffectiveConfig();
83
86
  }
@@ -88,6 +91,11 @@ async function main() {
88
91
  await runNotifyTest();
89
92
  return;
90
93
  }
94
+ // `aoaoe doctor` -- comprehensive health check
95
+ if (runDoctor) {
96
+ await runDoctorCheck();
97
+ return;
98
+ }
91
99
  // `aoaoe task` -- task management CLI
92
100
  if (isTaskCli) {
93
101
  await runTaskCli(process.argv);
@@ -1243,6 +1251,42 @@ function showDaemonStatus() {
1243
1251
  console.log(` ${statusIcon} ${BOLD}${s.title}${RESET} (${s.tool}) ${s.status}${userTag}${taskTag}`);
1244
1252
  }
1245
1253
  }
1254
+ // last action from actions.log
1255
+ try {
1256
+ const actionsLogPath = join(homedir(), ".aoaoe", "actions.log");
1257
+ if (existsSync(actionsLogPath)) {
1258
+ const content = readFileSync(actionsLogPath, "utf-8").trim();
1259
+ if (content) {
1260
+ const logLines = content.split("\n").filter((l) => l.trim());
1261
+ // find last non-wait action
1262
+ for (let i = logLines.length - 1; i >= 0; i--) {
1263
+ try {
1264
+ const entry = toActionLogEntry(JSON.parse(logLines[i]));
1265
+ if (!entry || entry.action.action === "wait")
1266
+ continue;
1267
+ const ago = Date.now() - entry.timestamp;
1268
+ const agoStr = ago < 60_000 ? `${Math.floor(ago / 1000)}s ago` :
1269
+ ago < 3_600_000 ? `${Math.floor(ago / 60_000)}m ago` :
1270
+ `${Math.floor(ago / 3_600_000)}h ago`;
1271
+ const icon = entry.success ? `${GREEN}+${RESET}` : `${RED}!${RESET}`;
1272
+ const session = entry.action.session?.slice(0, 8) ?? entry.action.title ?? "";
1273
+ const detail = entry.detail.length > 40 ? entry.detail.slice(0, 37) + "..." : entry.detail;
1274
+ console.log("");
1275
+ console.log(` last action: ${icon} ${entry.action.action} ${session} ${DIM}(${agoStr})${RESET}`);
1276
+ if (detail)
1277
+ console.log(` ${DIM}${detail}${RESET}`);
1278
+ break;
1279
+ }
1280
+ catch {
1281
+ // skip malformed lines
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ catch {
1288
+ // best-effort — actions.log might not exist
1289
+ }
1246
1290
  console.log("");
1247
1291
  }
1248
1292
  // `aoaoe config --validate` -- validate config file, field values, and tool availability
@@ -1363,6 +1407,231 @@ async function runConfigValidation() {
1363
1407
  if (failed > 0)
1364
1408
  process.exit(1);
1365
1409
  }
1410
+ // `aoaoe doctor` -- comprehensive health check: config, tools, daemon, disk, sessions
1411
+ async function runDoctorCheck() {
1412
+ const pkg = readPkgVersion();
1413
+ let checks = 0;
1414
+ let passed = 0;
1415
+ let warnings = 0;
1416
+ console.log("");
1417
+ console.log(` aoaoe${pkg ? ` v${pkg}` : ""} — doctor`);
1418
+ console.log(` ${"─".repeat(50)}`);
1419
+ // ── 1. config ──────────────────────────────────────────────────────────
1420
+ console.log(`\n ${BOLD}config${RESET}`);
1421
+ const configPath = findConfigFile();
1422
+ checks++;
1423
+ if (configPath) {
1424
+ console.log(` ${GREEN}✓${RESET} config file: ${configPath}`);
1425
+ passed++;
1426
+ }
1427
+ else {
1428
+ console.log(` ${YELLOW}!${RESET} no config file (using defaults)`);
1429
+ warnings++;
1430
+ }
1431
+ let config;
1432
+ checks++;
1433
+ try {
1434
+ config = loadConfig();
1435
+ console.log(` ${GREEN}✓${RESET} config validates OK`);
1436
+ passed++;
1437
+ }
1438
+ catch (err) {
1439
+ const msg = err instanceof Error ? err.message : String(err);
1440
+ console.log(` ${RED}✗${RESET} config invalid: ${msg.split("\n")[0]}`);
1441
+ // use defaults to continue checking other things
1442
+ config = loadConfig({});
1443
+ }
1444
+ // ── 2. tools ───────────────────────────────────────────────────────────
1445
+ console.log(`\n ${BOLD}tools${RESET}`);
1446
+ const toolChecks = [
1447
+ { cmd: "node", label: "Node.js", versionArg: ["--version"], required: true },
1448
+ { cmd: "aoe", label: "agent-of-empires", versionArg: ["--version"], required: true },
1449
+ { cmd: "tmux", label: "terminal multiplexer", versionArg: ["-V"], required: true },
1450
+ ];
1451
+ if (config.reasoner === "opencode") {
1452
+ toolChecks.push({ cmd: "opencode", label: "OpenCode CLI", versionArg: ["version"], required: true });
1453
+ }
1454
+ else {
1455
+ toolChecks.push({ cmd: "claude", label: "Claude Code CLI", versionArg: ["--version"], required: true });
1456
+ }
1457
+ for (const tool of toolChecks) {
1458
+ checks++;
1459
+ try {
1460
+ const result = await shellExec(tool.cmd, tool.versionArg);
1461
+ const ver = result.stdout.trim().split("\n")[0].slice(0, 60) || result.stderr.trim().split("\n")[0].slice(0, 60);
1462
+ console.log(` ${GREEN}✓${RESET} ${tool.cmd} — ${ver}`);
1463
+ passed++;
1464
+ }
1465
+ catch {
1466
+ if (tool.required) {
1467
+ console.log(` ${RED}✗${RESET} ${tool.cmd} not found (${tool.label})`);
1468
+ }
1469
+ else {
1470
+ console.log(` ${YELLOW}!${RESET} ${tool.cmd} not found (${tool.label}, optional)`);
1471
+ warnings++;
1472
+ }
1473
+ }
1474
+ }
1475
+ // ── 3. reasoner server ─────────────────────────────────────────────────
1476
+ if (config.reasoner === "opencode") {
1477
+ console.log(`\n ${BOLD}reasoner${RESET}`);
1478
+ checks++;
1479
+ try {
1480
+ const resp = await fetch(`http://127.0.0.1:${config.opencode.port}/health`, {
1481
+ signal: AbortSignal.timeout(3000),
1482
+ });
1483
+ if (resp.ok) {
1484
+ console.log(` ${GREEN}✓${RESET} opencode serve responding on port ${config.opencode.port}`);
1485
+ passed++;
1486
+ }
1487
+ else {
1488
+ console.log(` ${YELLOW}!${RESET} opencode serve on port ${config.opencode.port} returned ${resp.status}`);
1489
+ warnings++;
1490
+ }
1491
+ }
1492
+ catch {
1493
+ console.log(` ${RED}✗${RESET} opencode serve not responding on port ${config.opencode.port}`);
1494
+ console.log(` ${DIM}start with: opencode serve --port ${config.opencode.port}${RESET}`);
1495
+ }
1496
+ }
1497
+ // ── 4. daemon ──────────────────────────────────────────────────────────
1498
+ console.log(`\n ${BOLD}daemon${RESET}`);
1499
+ const state = readState();
1500
+ const daemonRunning = isDaemonRunningFromState(state);
1501
+ checks++;
1502
+ if (daemonRunning && state) {
1503
+ console.log(` ${GREEN}✓${RESET} daemon running (poll #${state.pollCount}, phase: ${state.phase})`);
1504
+ console.log(` ${state.sessions.length} session(s) monitored`);
1505
+ passed++;
1506
+ }
1507
+ else {
1508
+ console.log(` ${DIM}○${RESET} daemon not running`);
1509
+ passed++; // not running is fine for doctor — just informational
1510
+ }
1511
+ // lock file check
1512
+ const lockPath = join(homedir(), ".aoaoe", "daemon.lock");
1513
+ if (existsSync(lockPath) && !daemonRunning) {
1514
+ checks++;
1515
+ console.log(` ${YELLOW}!${RESET} stale lock file found: ${lockPath}`);
1516
+ console.log(` ${DIM}remove with: rm ${lockPath}${RESET}`);
1517
+ warnings++;
1518
+ }
1519
+ // ── 5. disk / data ─────────────────────────────────────────────────────
1520
+ console.log(`\n ${BOLD}data${RESET}`);
1521
+ const aoaoeDir = join(homedir(), ".aoaoe");
1522
+ if (existsSync(aoaoeDir)) {
1523
+ checks++;
1524
+ try {
1525
+ const files = await import("node:fs").then(fs => fs.readdirSync(aoaoeDir));
1526
+ let totalSize = 0;
1527
+ for (const f of files) {
1528
+ try {
1529
+ totalSize += statSync(join(aoaoeDir, f)).size;
1530
+ }
1531
+ catch { /* skip unreadable */ }
1532
+ }
1533
+ const sizeStr = totalSize < 1024 ? `${totalSize}B` :
1534
+ totalSize < 1_048_576 ? `${(totalSize / 1024).toFixed(1)}KB` :
1535
+ `${(totalSize / 1_048_576).toFixed(1)}MB`;
1536
+ console.log(` ${GREEN}✓${RESET} ~/.aoaoe/ — ${files.length} files, ${sizeStr}`);
1537
+ passed++;
1538
+ }
1539
+ catch {
1540
+ console.log(` ${YELLOW}!${RESET} could not read ~/.aoaoe/`);
1541
+ warnings++;
1542
+ }
1543
+ // actions log stats
1544
+ const actionsPath = join(aoaoeDir, "actions.log");
1545
+ if (existsSync(actionsPath)) {
1546
+ checks++;
1547
+ try {
1548
+ const content = readFileSync(actionsPath, "utf-8").trim();
1549
+ const lineCount = content ? content.split("\n").length : 0;
1550
+ const size = statSync(actionsPath).size;
1551
+ const sizeStr = size < 1024 ? `${size}B` : `${(size / 1024).toFixed(1)}KB`;
1552
+ console.log(` ${GREEN}✓${RESET} actions.log — ${lineCount} entries, ${sizeStr}`);
1553
+ passed++;
1554
+ }
1555
+ catch {
1556
+ console.log(` ${YELLOW}!${RESET} actions.log unreadable`);
1557
+ warnings++;
1558
+ }
1559
+ }
1560
+ }
1561
+ else {
1562
+ console.log(` ${DIM}○${RESET} ~/.aoaoe/ does not exist yet (run 'aoaoe init')`);
1563
+ }
1564
+ // ── 6. aoe sessions ───────────────────────────────────────────────────
1565
+ console.log(`\n ${BOLD}sessions${RESET}`);
1566
+ checks++;
1567
+ try {
1568
+ const listResult = await shellExec("aoe", ["list", "--json"]);
1569
+ if (listResult.exitCode === 0 && listResult.stdout.trim()) {
1570
+ const sessions = JSON.parse(listResult.stdout);
1571
+ if (Array.isArray(sessions) && sessions.length > 0) {
1572
+ console.log(` ${GREEN}✓${RESET} ${sessions.length} aoe session(s) found`);
1573
+ for (const s of sessions.slice(0, 5)) {
1574
+ console.log(` ${DIM}${s.title ?? s.id} (${s.tool ?? "?"})${RESET}`);
1575
+ }
1576
+ if (sessions.length > 5)
1577
+ console.log(` ${DIM}...and ${sessions.length - 5} more${RESET}`);
1578
+ passed++;
1579
+ }
1580
+ else {
1581
+ console.log(` ${DIM}○${RESET} no aoe sessions (start some with 'aoe add')`);
1582
+ passed++;
1583
+ }
1584
+ }
1585
+ else {
1586
+ console.log(` ${YELLOW}!${RESET} aoe list returned non-zero (is aoe running?)`);
1587
+ warnings++;
1588
+ }
1589
+ }
1590
+ catch {
1591
+ console.log(` ${RED}✗${RESET} could not run 'aoe list --json'`);
1592
+ }
1593
+ // ── summary ────────────────────────────────────────────────────────────
1594
+ const failed = checks - passed - warnings;
1595
+ console.log("");
1596
+ console.log(` ${"─".repeat(50)}`);
1597
+ if (failed === 0 && warnings === 0) {
1598
+ console.log(` ${GREEN}${BOLD}all ${checks} checks passed${RESET} — looking healthy`);
1599
+ }
1600
+ else if (failed === 0) {
1601
+ console.log(` ${passed}/${checks} passed, ${YELLOW}${warnings} warning(s)${RESET}`);
1602
+ }
1603
+ else {
1604
+ console.log(` ${passed}/${checks} passed, ${RED}${failed} failed${RESET}${warnings > 0 ? `, ${YELLOW}${warnings} warning(s)${RESET}` : ""}`);
1605
+ }
1606
+ console.log("");
1607
+ }
1608
+ // `aoaoe config --diff` -- show only fields that differ from defaults
1609
+ function showConfigDiff() {
1610
+ const configPath = findConfigFile();
1611
+ const configResult = loadConfig();
1612
+ const { _configPath, ...config } = configResult;
1613
+ const diffs = computeConfigDiff(config, DEFAULTS);
1614
+ console.log("");
1615
+ console.log(" aoaoe — config diff (vs. defaults)");
1616
+ console.log(` ${"─".repeat(50)}`);
1617
+ console.log(` source: ${configPath ?? "defaults (no config file found)"}`);
1618
+ console.log("");
1619
+ if (diffs.length === 0) {
1620
+ console.log(" (no differences — config matches defaults)");
1621
+ }
1622
+ else {
1623
+ for (const d of diffs) {
1624
+ const curStr = d.current === undefined ? `${DIM}(not set)${RESET}` : `${GREEN}${JSON.stringify(d.current)}${RESET}`;
1625
+ const defStr = d.default === undefined ? `${DIM}(not set)${RESET}` : `${DIM}${JSON.stringify(d.default)}${RESET}`;
1626
+ console.log(` ${YELLOW}${d.path}${RESET}`);
1627
+ console.log(` current: ${curStr}`);
1628
+ console.log(` default: ${defStr}`);
1629
+ }
1630
+ console.log("");
1631
+ console.log(` ${diffs.length} field(s) differ from defaults`);
1632
+ }
1633
+ console.log("");
1634
+ }
1366
1635
  // `aoaoe config` -- show the effective resolved config (defaults + file + any notes)
1367
1636
  function showEffectiveConfig() {
1368
1637
  const configPath = findConfigFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",