aoaoe 0.53.0 → 0.55.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
@@ -240,6 +240,8 @@ commands:
240
240
  init detect tools + sessions, import history, generate config
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
+ config --validate validate config + check tool availability
244
+ config --diff show only fields that differ from defaults
243
245
  notify-test send a test notification to configured webhooks
244
246
  task manage tasks and sessions (list, start, stop, new, rm, edit)
245
247
  tasks show task progress (from aoaoe.tasks.json)
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;
@@ -20,6 +26,8 @@ export declare function parseCliArgs(argv: string[]): {
20
26
  showHistory: boolean;
21
27
  showStatus: boolean;
22
28
  showConfig: boolean;
29
+ configValidate: boolean;
30
+ configDiff: boolean;
23
31
  notifyTest: boolean;
24
32
  runInit: boolean;
25
33
  initForce: 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, 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, 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 };
@@ -295,7 +320,9 @@ export function parseCliArgs(argv) {
295
320
  return { ...defaults, showStatus: true };
296
321
  }
297
322
  if (argv[2] === "config") {
298
- return { ...defaults, showConfig: true };
323
+ const validate = argv.includes("--validate") || argv.includes("-V");
324
+ const diff = argv.includes("--diff");
325
+ return { ...defaults, showConfig: true, configValidate: validate, configDiff: diff };
299
326
  }
300
327
  if (argv[2] === "notify-test") {
301
328
  return { ...defaults, notifyTest: true };
@@ -389,7 +416,7 @@ export function parseCliArgs(argv) {
389
416
  break;
390
417
  }
391
418
  }
392
- return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, notifyTest: false, runInit: false, initForce: false, runTaskCli: false };
419
+ return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runInit: false, initForce: false, runTaskCli: false };
393
420
  }
394
421
  export function printHelp() {
395
422
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -407,6 +434,8 @@ commands:
407
434
  (none) start the supervisor daemon (interactive TUI)
408
435
  status quick daemon health check (is it running? what's it doing?)
409
436
  config show the effective resolved config (defaults + file)
437
+ config --validate validate config + check tool availability
438
+ config --diff show only fields that differ from defaults
410
439
  notify-test send a test notification to configured webhooks
411
440
  task manage tasks and sessions (list, start, stop, new, rm, edit)
412
441
  tasks show task progress (from aoaoe.tasks.json)
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";
@@ -18,7 +18,7 @@ import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
18
18
  import { TUI } from "./tui.js";
19
19
  import { isDaemonRunningFromState } from "./chat.js";
20
20
  import { sendNotification, sendTestNotification } from "./notify.js";
21
- import { actionSession, actionDetail } from "./types.js";
21
+ import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
22
22
  import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
23
23
  import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
24
24
  import { resolve, dirname, join } from "node:path";
@@ -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, 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, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
32
32
  if (help) {
33
33
  printHelp();
34
34
  process.exit(0);
@@ -73,9 +73,17 @@ async function main() {
73
73
  showDaemonStatus();
74
74
  return;
75
75
  }
76
- // `aoaoe config` -- show effective resolved config
76
+ // `aoaoe config` -- show effective resolved config (with optional --validate or --diff)
77
77
  if (showConfig) {
78
- showEffectiveConfig();
78
+ if (configValidate) {
79
+ await runConfigValidation();
80
+ }
81
+ else if (configDiff) {
82
+ showConfigDiff();
83
+ }
84
+ else {
85
+ showEffectiveConfig();
86
+ }
79
87
  return;
80
88
  }
81
89
  // `aoaoe notify-test` -- send a test notification to configured webhooks
@@ -1136,7 +1144,9 @@ async function showActionHistory() {
1136
1144
  console.log(` ${"─".repeat(70)}`);
1137
1145
  for (const line of recent) {
1138
1146
  try {
1139
- const entry = JSON.parse(line);
1147
+ const entry = toActionLogEntry(JSON.parse(line));
1148
+ if (!entry)
1149
+ continue; // skip malformed lines
1140
1150
  const time = new Date(entry.timestamp).toLocaleTimeString();
1141
1151
  const date = new Date(entry.timestamp).toLocaleDateString();
1142
1152
  const icon = entry.success ? `${GREEN}+${RESET}` : `${RED}!${RESET}`;
@@ -1146,7 +1156,7 @@ async function showActionHistory() {
1146
1156
  console.log(` ${icon} ${DIM}${date} ${time}${RESET} ${YELLOW}${actionName.padEnd(16)}${RESET} ${session.padEnd(10)} ${detail}`);
1147
1157
  }
1148
1158
  catch {
1149
- // skip malformed lines
1159
+ // skip unparseable JSON lines
1150
1160
  }
1151
1161
  }
1152
1162
  console.log(` ${"─".repeat(70)}`);
@@ -1155,14 +1165,18 @@ async function showActionHistory() {
1155
1165
  const actionCounts = new Map();
1156
1166
  for (const line of lines) {
1157
1167
  try {
1158
- const e = JSON.parse(line);
1168
+ const e = toActionLogEntry(JSON.parse(line));
1169
+ if (!e)
1170
+ continue;
1159
1171
  if (e.success)
1160
1172
  successes++;
1161
1173
  else
1162
1174
  failures++;
1163
1175
  actionCounts.set(e.action.action, (actionCounts.get(e.action.action) ?? 0) + 1);
1164
1176
  }
1165
- catch { }
1177
+ catch {
1178
+ // skip unparseable JSON lines
1179
+ }
1166
1180
  }
1167
1181
  const breakdown = [...actionCounts.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}: ${v}`).join(", ");
1168
1182
  console.log(` total: ${lines.length} actions (${GREEN}${successes} ok${RESET}, ${RED}${failures} failed${RESET})`);
@@ -1232,6 +1246,187 @@ function showDaemonStatus() {
1232
1246
  console.log(` ${statusIcon} ${BOLD}${s.title}${RESET} (${s.tool}) ${s.status}${userTag}${taskTag}`);
1233
1247
  }
1234
1248
  }
1249
+ // last action from actions.log
1250
+ try {
1251
+ const actionsLogPath = join(homedir(), ".aoaoe", "actions.log");
1252
+ if (existsSync(actionsLogPath)) {
1253
+ const content = readFileSync(actionsLogPath, "utf-8").trim();
1254
+ if (content) {
1255
+ const logLines = content.split("\n").filter((l) => l.trim());
1256
+ // find last non-wait action
1257
+ for (let i = logLines.length - 1; i >= 0; i--) {
1258
+ try {
1259
+ const entry = toActionLogEntry(JSON.parse(logLines[i]));
1260
+ if (!entry || entry.action.action === "wait")
1261
+ continue;
1262
+ const ago = Date.now() - entry.timestamp;
1263
+ const agoStr = ago < 60_000 ? `${Math.floor(ago / 1000)}s ago` :
1264
+ ago < 3_600_000 ? `${Math.floor(ago / 60_000)}m ago` :
1265
+ `${Math.floor(ago / 3_600_000)}h ago`;
1266
+ const icon = entry.success ? `${GREEN}+${RESET}` : `${RED}!${RESET}`;
1267
+ const session = entry.action.session?.slice(0, 8) ?? entry.action.title ?? "";
1268
+ const detail = entry.detail.length > 40 ? entry.detail.slice(0, 37) + "..." : entry.detail;
1269
+ console.log("");
1270
+ console.log(` last action: ${icon} ${entry.action.action} ${session} ${DIM}(${agoStr})${RESET}`);
1271
+ if (detail)
1272
+ console.log(` ${DIM}${detail}${RESET}`);
1273
+ break;
1274
+ }
1275
+ catch {
1276
+ // skip malformed lines
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+ catch {
1283
+ // best-effort — actions.log might not exist
1284
+ }
1285
+ console.log("");
1286
+ }
1287
+ // `aoaoe config --validate` -- validate config file, field values, and tool availability
1288
+ async function runConfigValidation() {
1289
+ const configPath = findConfigFile();
1290
+ let checks = 0;
1291
+ let passed = 0;
1292
+ let warnings = 0;
1293
+ console.log("");
1294
+ console.log(" aoaoe — config validation");
1295
+ console.log(` ${"─".repeat(50)}`);
1296
+ // 1. config file exists
1297
+ checks++;
1298
+ if (configPath) {
1299
+ console.log(` ${GREEN}✓${RESET} config file found: ${configPath}`);
1300
+ passed++;
1301
+ }
1302
+ else {
1303
+ console.log(` ${YELLOW}!${RESET} no config file found (using defaults)`);
1304
+ console.log(` ${DIM}run 'aoaoe init' to create one${RESET}`);
1305
+ warnings++;
1306
+ }
1307
+ // 2. config parses + validates
1308
+ checks++;
1309
+ let config;
1310
+ try {
1311
+ const configResult = loadConfig();
1312
+ config = configResult;
1313
+ console.log(` ${GREEN}✓${RESET} config valid (all field values OK)`);
1314
+ passed++;
1315
+ }
1316
+ catch (err) {
1317
+ const msg = err instanceof Error ? err.message : String(err);
1318
+ console.log(` ${RED}✗${RESET} config validation failed:`);
1319
+ for (const line of msg.split("\n")) {
1320
+ console.log(` ${line}`);
1321
+ }
1322
+ console.log("");
1323
+ console.log(` ${passed}/${checks} checks passed, fix config errors and retry`);
1324
+ console.log("");
1325
+ process.exit(1);
1326
+ return; // unreachable, but satisfies TypeScript
1327
+ }
1328
+ // 3. required tools on PATH
1329
+ const tools = [
1330
+ { name: "aoe", label: "agent-of-empires CLI" },
1331
+ { name: "tmux", label: "terminal multiplexer" },
1332
+ ];
1333
+ if (config.reasoner === "opencode") {
1334
+ tools.push({ name: "opencode", label: "OpenCode CLI" });
1335
+ }
1336
+ else if (config.reasoner === "claude-code") {
1337
+ tools.push({ name: "claude", label: "Claude Code CLI" });
1338
+ }
1339
+ for (const tool of tools) {
1340
+ checks++;
1341
+ try {
1342
+ const { execFile: execFileCb } = await import("node:child_process");
1343
+ const { promisify } = await import("node:util");
1344
+ const execFileAsync = promisify(execFileCb);
1345
+ await execFileAsync("which", [tool.name]);
1346
+ console.log(` ${GREEN}✓${RESET} ${tool.name} found on PATH (${tool.label})`);
1347
+ passed++;
1348
+ }
1349
+ catch {
1350
+ console.log(` ${RED}✗${RESET} ${tool.name} not found on PATH (${tool.label})`);
1351
+ }
1352
+ }
1353
+ // 4. notifications config check
1354
+ checks++;
1355
+ if (config.notifications) {
1356
+ const hasWebhook = !!config.notifications.webhookUrl;
1357
+ const hasSlack = !!config.notifications.slackWebhookUrl;
1358
+ if (hasWebhook || hasSlack) {
1359
+ const targets = [hasWebhook && "webhook", hasSlack && "Slack"].filter(Boolean).join(" + ");
1360
+ console.log(` ${GREEN}✓${RESET} notifications configured (${targets})`);
1361
+ console.log(` ${DIM}run 'aoaoe notify-test' to verify delivery${RESET}`);
1362
+ passed++;
1363
+ }
1364
+ else {
1365
+ console.log(` ${YELLOW}!${RESET} notifications block exists but no webhook URLs configured`);
1366
+ warnings++;
1367
+ }
1368
+ }
1369
+ else {
1370
+ console.log(` ${DIM}○${RESET} notifications not configured (optional)`);
1371
+ passed++; // not configured is fine — it's optional
1372
+ }
1373
+ // 5. sessionDirs validation — check that mapped dirs exist
1374
+ if (config.sessionDirs && Object.keys(config.sessionDirs).length > 0) {
1375
+ const basePath = process.cwd();
1376
+ for (const [title, dir] of Object.entries(config.sessionDirs)) {
1377
+ checks++;
1378
+ const resolved = dir.startsWith("/") ? dir : resolve(basePath, dir);
1379
+ if (existsSync(resolved)) {
1380
+ console.log(` ${GREEN}✓${RESET} sessionDirs.${title} → ${resolved}`);
1381
+ passed++;
1382
+ }
1383
+ else {
1384
+ console.log(` ${YELLOW}!${RESET} sessionDirs.${title} → ${resolved} (not found)`);
1385
+ warnings++;
1386
+ }
1387
+ }
1388
+ }
1389
+ // summary
1390
+ const failed = checks - passed - warnings;
1391
+ console.log("");
1392
+ if (failed === 0 && warnings === 0) {
1393
+ console.log(` ${GREEN}${BOLD}all ${checks} checks passed${RESET}`);
1394
+ }
1395
+ else if (failed === 0) {
1396
+ console.log(` ${passed}/${checks} passed, ${YELLOW}${warnings} warning(s)${RESET}`);
1397
+ }
1398
+ else {
1399
+ console.log(` ${passed}/${checks} passed, ${RED}${failed} failed${RESET}${warnings > 0 ? `, ${YELLOW}${warnings} warning(s)${RESET}` : ""}`);
1400
+ }
1401
+ console.log("");
1402
+ if (failed > 0)
1403
+ process.exit(1);
1404
+ }
1405
+ // `aoaoe config --diff` -- show only fields that differ from defaults
1406
+ function showConfigDiff() {
1407
+ const configPath = findConfigFile();
1408
+ const configResult = loadConfig();
1409
+ const { _configPath, ...config } = configResult;
1410
+ const diffs = computeConfigDiff(config, DEFAULTS);
1411
+ console.log("");
1412
+ console.log(" aoaoe — config diff (vs. defaults)");
1413
+ console.log(` ${"─".repeat(50)}`);
1414
+ console.log(` source: ${configPath ?? "defaults (no config file found)"}`);
1415
+ console.log("");
1416
+ if (diffs.length === 0) {
1417
+ console.log(" (no differences — config matches defaults)");
1418
+ }
1419
+ else {
1420
+ for (const d of diffs) {
1421
+ const curStr = d.current === undefined ? `${DIM}(not set)${RESET}` : `${GREEN}${JSON.stringify(d.current)}${RESET}`;
1422
+ const defStr = d.default === undefined ? `${DIM}(not set)${RESET}` : `${DIM}${JSON.stringify(d.default)}${RESET}`;
1423
+ console.log(` ${YELLOW}${d.path}${RESET}`);
1424
+ console.log(` current: ${curStr}`);
1425
+ console.log(` default: ${defStr}`);
1426
+ }
1427
+ console.log("");
1428
+ console.log(` ${diffs.length} field(s) differ from defaults`);
1429
+ }
1235
1430
  console.log("");
1236
1431
  }
1237
1432
  // `aoaoe config` -- show the effective resolved config (defaults + file + any notes)
package/dist/types.d.ts CHANGED
@@ -175,4 +175,16 @@ export declare function toAoeSessionList(raw: unknown): Array<{
175
175
  title: string;
176
176
  }>;
177
177
  export declare function toReasonerBackend(raw: string): ReasonerBackend;
178
+ export interface ActionLogEntry {
179
+ timestamp: number;
180
+ action: {
181
+ action: string;
182
+ session?: string;
183
+ text?: string;
184
+ title?: string;
185
+ };
186
+ success: boolean;
187
+ detail: string;
188
+ }
189
+ export declare function toActionLogEntry(raw: unknown): ActionLogEntry | null;
178
190
  //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -112,4 +112,31 @@ export function toReasonerBackend(raw) {
112
112
  return raw;
113
113
  throw new Error(`--reasoner must be "opencode" or "claude-code", got "${raw}"`);
114
114
  }
115
+ export function toActionLogEntry(raw) {
116
+ if (!raw || typeof raw !== "object")
117
+ return null;
118
+ const obj = raw;
119
+ if (typeof obj.timestamp !== "number")
120
+ return null;
121
+ if (typeof obj.success !== "boolean")
122
+ return null;
123
+ if (typeof obj.detail !== "string")
124
+ obj.detail = "";
125
+ if (!obj.action || typeof obj.action !== "object")
126
+ return null;
127
+ const action = obj.action;
128
+ if (typeof action.action !== "string")
129
+ return null;
130
+ return {
131
+ timestamp: obj.timestamp,
132
+ action: {
133
+ action: action.action,
134
+ session: typeof action.session === "string" ? action.session : undefined,
135
+ text: typeof action.text === "string" ? action.text : undefined,
136
+ title: typeof action.title === "string" ? action.title : undefined,
137
+ },
138
+ success: obj.success,
139
+ detail: typeof obj.detail === "string" ? obj.detail : "",
140
+ };
141
+ }
115
142
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.53.0",
3
+ "version": "0.55.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",