aoaoe 0.69.0 → 0.70.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/config.d.ts CHANGED
@@ -44,6 +44,8 @@ export declare function parseCliArgs(argv: string[]): {
44
44
  runTail: boolean;
45
45
  tailFollow: boolean;
46
46
  tailCount?: number;
47
+ runStats: boolean;
48
+ statsLast?: string;
47
49
  registerTitle?: string;
48
50
  };
49
51
  export declare function printHelp(): void;
package/dist/config.js CHANGED
@@ -327,7 +327,7 @@ export function parseCliArgs(argv) {
327
327
  let initForce = false;
328
328
  let runTaskCli = false;
329
329
  let registerTitle;
330
- 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, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined };
330
+ 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, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined };
331
331
  // check for subcommand as first non-flag arg
332
332
  if (argv[2] === "test-context") {
333
333
  return { ...defaults, testContext: true };
@@ -410,6 +410,15 @@ export function parseCliArgs(argv) {
410
410
  }
411
411
  return { ...defaults, runTail: true, tailFollow: follow, tailCount: count };
412
412
  }
413
+ if (argv[2] === "stats") {
414
+ let last;
415
+ for (let i = 3; i < argv.length; i++) {
416
+ if ((argv[i] === "--last" || argv[i] === "-l") && argv[i + 1]) {
417
+ last = argv[++i];
418
+ }
419
+ }
420
+ return { ...defaults, runStats: true, statsLast: last };
421
+ }
413
422
  if (argv[2] === "register") {
414
423
  register = true;
415
424
  // parse --title from remaining args
@@ -503,7 +512,7 @@ export function parseCliArgs(argv) {
503
512
  break;
504
513
  }
505
514
  }
506
- 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, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined };
515
+ 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, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined };
507
516
  }
508
517
  export function printHelp() {
509
518
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -533,6 +542,8 @@ commands:
533
542
  export --format <json|markdown> output format (default: json)
534
543
  export --output <file> write to file (default: stdout)
535
544
  export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
545
+ stats show aggregate daemon statistics (actions, sessions, activity)
546
+ stats --last <duration> time window: 1h, 6h, 24h, 7d (default: all time)
536
547
  tail live-stream daemon activity to a separate terminal
537
548
  tail -f follow mode — keep watching for new entries (Ctrl+C to stop)
538
549
  tail -n <N> number of entries to show (default: 50)
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
32
32
  const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
33
33
  const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
34
34
  async function main() {
35
- const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, runTail: isTail, tailFollow, tailCount, registerTitle } = parseCliArgs(process.argv);
35
+ const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, registerTitle } = parseCliArgs(process.argv);
36
36
  if (help) {
37
37
  printHelp();
38
38
  process.exit(0);
@@ -110,6 +110,11 @@ async function main() {
110
110
  await runTimelineExport(exportFormat, exportOutput, exportLast);
111
111
  return;
112
112
  }
113
+ // `aoaoe stats` -- show aggregate daemon statistics
114
+ if (isStats) {
115
+ await runStatsCommand(statsLast);
116
+ return;
117
+ }
113
118
  // `aoaoe task` -- task management CLI
114
119
  if (isTaskCli) {
115
120
  await runTaskCli(process.argv);
@@ -1455,6 +1460,36 @@ async function runTimelineExport(format, output, last) {
1455
1460
  process.stdout.write(content);
1456
1461
  }
1457
1462
  }
1463
+ // `aoaoe stats` -- show aggregate daemon statistics
1464
+ async function runStatsCommand(last) {
1465
+ const { parseActionStats, parseHistoryStats, combineStats, formatStats } = await import("./stats.js");
1466
+ const { parseDuration } = await import("./export.js");
1467
+ const { loadTuiHistory } = await import("./tui-history.js");
1468
+ const maxAgeMs = last ? parseDuration(last) : undefined;
1469
+ if (last && maxAgeMs === undefined) {
1470
+ console.error(`error: --last must be like "1h", "6h", "24h", "7d", got "${last}"`);
1471
+ process.exit(1);
1472
+ }
1473
+ const windowLabel = last ?? "all time";
1474
+ // read actions.log
1475
+ const actionsFile = join(homedir(), ".aoaoe", "actions.log");
1476
+ let actionLines = [];
1477
+ try {
1478
+ if (existsSync(actionsFile)) {
1479
+ actionLines = readFileSync(actionsFile, "utf-8").trim().split("\n").filter((l) => l.trim());
1480
+ }
1481
+ }
1482
+ catch {
1483
+ // no actions — that's fine
1484
+ }
1485
+ // read tui-history
1486
+ const retentionMs = maxAgeMs ?? 365 * 24 * 60 * 60 * 1000; // 1 year default
1487
+ const historyEntries = loadTuiHistory(100_000, undefined, retentionMs);
1488
+ const actionStats = parseActionStats(actionLines, maxAgeMs ?? undefined);
1489
+ const historyStats = parseHistoryStats(historyEntries, maxAgeMs ?? undefined);
1490
+ const combined = combineStats(actionStats, historyStats);
1491
+ console.log(formatStats(combined, windowLabel));
1492
+ }
1458
1493
  // `aoaoe test` -- dynamically import and run the integration test
1459
1494
  async function runIntegrationTest() {
1460
1495
  const testModule = resolve(__dirname, "integration-test.js");
@@ -0,0 +1,54 @@
1
+ import type { HistoryEntry } from "./tui-history.js";
2
+ export interface ActionStats {
3
+ total: number;
4
+ succeeded: number;
5
+ failed: number;
6
+ byType: Map<string, number>;
7
+ bySession: Map<string, {
8
+ total: number;
9
+ ok: number;
10
+ fail: number;
11
+ }>;
12
+ firstTs: number;
13
+ lastTs: number;
14
+ }
15
+ export interface HistoryStats {
16
+ total: number;
17
+ byTag: Map<string, number>;
18
+ firstTs: number;
19
+ lastTs: number;
20
+ }
21
+ export interface CombinedStats {
22
+ actions: ActionStats | null;
23
+ history: HistoryStats | null;
24
+ timeRange: {
25
+ start: number;
26
+ end: number;
27
+ } | null;
28
+ }
29
+ /**
30
+ * Parse action log lines into aggregate stats.
31
+ * Skips malformed lines and wait actions.
32
+ */
33
+ export declare function parseActionStats(lines: string[], maxAgeMs?: number, now?: number): ActionStats | null;
34
+ /**
35
+ * Parse tui-history entries into aggregate stats.
36
+ */
37
+ export declare function parseHistoryStats(entries: HistoryEntry[], maxAgeMs?: number, now?: number): HistoryStats | null;
38
+ /**
39
+ * Combine action and history stats into a unified stats object.
40
+ */
41
+ export declare function combineStats(actions: ActionStats | null, history: HistoryStats | null): CombinedStats;
42
+ /**
43
+ * Format a duration in ms as a human-friendly string.
44
+ */
45
+ export declare function formatDuration(ms: number): string;
46
+ /**
47
+ * Format a rate as "X per hour" or "X per day".
48
+ */
49
+ export declare function formatRate(count: number, spanMs: number): string;
50
+ /**
51
+ * Format the full stats display for terminal output.
52
+ */
53
+ export declare function formatStats(stats: CombinedStats, windowLabel?: string): string;
54
+ //# sourceMappingURL=stats.d.ts.map
package/dist/stats.js ADDED
@@ -0,0 +1,221 @@
1
+ // stats.ts — aggregate daemon statistics from actions.log and tui-history.jsonl.
2
+ // pure exported functions for testability.
3
+ import { toActionLogEntry } from "./types.js";
4
+ import { BOLD, RESET, DIM, GREEN, RED, YELLOW, SLATE, AMBER, LIME, SKY, CYAN, } from "./colors.js";
5
+ // ── Parsing ───────────────────────────────────────────────────────────────────
6
+ /**
7
+ * Parse action log lines into aggregate stats.
8
+ * Skips malformed lines and wait actions.
9
+ */
10
+ export function parseActionStats(lines, maxAgeMs, now) {
11
+ const cutoff = maxAgeMs ? (now ?? Date.now()) - maxAgeMs : 0;
12
+ const byType = new Map();
13
+ const bySession = new Map();
14
+ let total = 0;
15
+ let succeeded = 0;
16
+ let failed = 0;
17
+ let firstTs = Infinity;
18
+ let lastTs = 0;
19
+ for (const line of lines) {
20
+ try {
21
+ const entry = toActionLogEntry(JSON.parse(line));
22
+ if (!entry)
23
+ continue;
24
+ if (entry.action.action === "wait")
25
+ continue;
26
+ if (entry.timestamp < cutoff)
27
+ continue;
28
+ total++;
29
+ if (entry.success)
30
+ succeeded++;
31
+ else
32
+ failed++;
33
+ if (entry.timestamp < firstTs)
34
+ firstTs = entry.timestamp;
35
+ if (entry.timestamp > lastTs)
36
+ lastTs = entry.timestamp;
37
+ const type = entry.action.action;
38
+ byType.set(type, (byType.get(type) ?? 0) + 1);
39
+ const session = entry.action.title ?? entry.action.session?.slice(0, 8) ?? "unknown";
40
+ const existing = bySession.get(session) ?? { total: 0, ok: 0, fail: 0 };
41
+ existing.total++;
42
+ if (entry.success)
43
+ existing.ok++;
44
+ else
45
+ existing.fail++;
46
+ bySession.set(session, existing);
47
+ }
48
+ catch {
49
+ // skip malformed
50
+ }
51
+ }
52
+ if (total === 0)
53
+ return null;
54
+ return { total, succeeded, failed, byType, bySession, firstTs, lastTs };
55
+ }
56
+ /**
57
+ * Parse tui-history entries into aggregate stats.
58
+ */
59
+ export function parseHistoryStats(entries, maxAgeMs, now) {
60
+ const cutoff = maxAgeMs ? (now ?? Date.now()) - maxAgeMs : 0;
61
+ const byTag = new Map();
62
+ let total = 0;
63
+ let firstTs = Infinity;
64
+ let lastTs = 0;
65
+ for (const entry of entries) {
66
+ if (entry.ts < cutoff)
67
+ continue;
68
+ total++;
69
+ if (entry.ts < firstTs)
70
+ firstTs = entry.ts;
71
+ if (entry.ts > lastTs)
72
+ lastTs = entry.ts;
73
+ byTag.set(entry.tag, (byTag.get(entry.tag) ?? 0) + 1);
74
+ }
75
+ if (total === 0)
76
+ return null;
77
+ return { total, byTag, firstTs, lastTs };
78
+ }
79
+ /**
80
+ * Combine action and history stats into a unified stats object.
81
+ */
82
+ export function combineStats(actions, history) {
83
+ let start = Infinity;
84
+ let end = 0;
85
+ if (actions) {
86
+ if (actions.firstTs < start)
87
+ start = actions.firstTs;
88
+ if (actions.lastTs > end)
89
+ end = actions.lastTs;
90
+ }
91
+ if (history) {
92
+ if (history.firstTs < start)
93
+ start = history.firstTs;
94
+ if (history.lastTs > end)
95
+ end = history.lastTs;
96
+ }
97
+ const timeRange = start < Infinity && end > 0 ? { start, end } : null;
98
+ return { actions, history, timeRange };
99
+ }
100
+ // ── Formatting ────────────────────────────────────────────────────────────────
101
+ /**
102
+ * Format a duration in ms as a human-friendly string.
103
+ */
104
+ export function formatDuration(ms) {
105
+ if (ms < 60_000)
106
+ return `${Math.floor(ms / 1000)}s`;
107
+ if (ms < 3_600_000) {
108
+ const m = Math.floor(ms / 60_000);
109
+ const s = Math.floor((ms % 60_000) / 1000);
110
+ return s > 0 ? `${m}m ${s}s` : `${m}m`;
111
+ }
112
+ if (ms < 86_400_000) {
113
+ const h = Math.floor(ms / 3_600_000);
114
+ const m = Math.floor((ms % 3_600_000) / 60_000);
115
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
116
+ }
117
+ const d = Math.floor(ms / 86_400_000);
118
+ const h = Math.floor((ms % 86_400_000) / 3_600_000);
119
+ return h > 0 ? `${d}d ${h}h` : `${d}d`;
120
+ }
121
+ /**
122
+ * Format a rate as "X per hour" or "X per day".
123
+ */
124
+ export function formatRate(count, spanMs) {
125
+ if (spanMs <= 0)
126
+ return `${count} total`;
127
+ const hours = spanMs / 3_600_000;
128
+ if (hours < 1)
129
+ return `${count} total`;
130
+ const perHour = count / hours;
131
+ if (perHour >= 1)
132
+ return `${perHour.toFixed(1)}/hr`;
133
+ const perDay = perHour * 24;
134
+ return `${perDay.toFixed(1)}/day`;
135
+ }
136
+ /**
137
+ * Format the full stats display for terminal output.
138
+ */
139
+ export function formatStats(stats, windowLabel) {
140
+ const lines = [];
141
+ const hr = "─".repeat(54);
142
+ lines.push("");
143
+ lines.push(` ${BOLD}aoaoe — stats${RESET}${windowLabel ? ` ${DIM}(${windowLabel})${RESET}` : ""}`);
144
+ lines.push(` ${hr}`);
145
+ // time range
146
+ if (stats.timeRange) {
147
+ const { start, end } = stats.timeRange;
148
+ const span = end - start;
149
+ const startStr = new Date(start).toLocaleString();
150
+ const endStr = new Date(end).toLocaleString();
151
+ lines.push(` ${DIM}from:${RESET} ${startStr}`);
152
+ lines.push(` ${DIM}to:${RESET} ${endStr}`);
153
+ lines.push(` ${DIM}span:${RESET} ${formatDuration(span)}`);
154
+ }
155
+ else {
156
+ lines.push(` ${DIM}no data found${RESET}`);
157
+ lines.push("");
158
+ return lines.join("\n");
159
+ }
160
+ // actions
161
+ if (stats.actions) {
162
+ const a = stats.actions;
163
+ const span = a.lastTs - a.firstTs;
164
+ const rate = formatRate(a.total, span);
165
+ lines.push("");
166
+ lines.push(` ${BOLD}actions${RESET} ${DIM}(${rate})${RESET}`);
167
+ const successRate = a.total > 0 ? Math.round((a.succeeded / a.total) * 100) : 0;
168
+ const successColor = successRate >= 90 ? GREEN : successRate >= 70 ? YELLOW : RED;
169
+ lines.push(` total: ${BOLD}${a.total}${RESET} ${GREEN}${a.succeeded} ok${RESET} ${a.failed > 0 ? `${RED}${a.failed} failed${RESET}` : `${DIM}0 failed${RESET}`} ${successColor}(${successRate}%)${RESET}`);
170
+ // by type — sorted by count descending
171
+ const sorted = [...a.byType.entries()].sort((x, y) => y[1] - x[1]);
172
+ for (const [type, count] of sorted) {
173
+ const bar = "█".repeat(Math.min(20, Math.round((count / a.total) * 20)));
174
+ const pct = Math.round((count / a.total) * 100);
175
+ lines.push(` ${AMBER}${type.padEnd(18)}${RESET} ${String(count).padStart(4)} ${SLATE}${bar}${RESET} ${DIM}${pct}%${RESET}`);
176
+ }
177
+ // top sessions — sorted by total actions
178
+ if (a.bySession.size > 0) {
179
+ lines.push("");
180
+ lines.push(` ${BOLD}top sessions${RESET}`);
181
+ const topSessions = [...a.bySession.entries()]
182
+ .sort((x, y) => y[1].total - x[1].total)
183
+ .slice(0, 8);
184
+ for (const [name, counts] of topSessions) {
185
+ const failTag = counts.fail > 0 ? ` ${RED}${counts.fail} fail${RESET}` : "";
186
+ lines.push(` ${CYAN}${name.padEnd(20)}${RESET} ${String(counts.total).padStart(4)} actions ${GREEN}${counts.ok} ok${RESET}${failTag}`);
187
+ }
188
+ }
189
+ }
190
+ else {
191
+ lines.push("");
192
+ lines.push(` ${DIM}no actions recorded${RESET}`);
193
+ }
194
+ // history activity breakdown
195
+ if (stats.history) {
196
+ const h = stats.history;
197
+ lines.push("");
198
+ lines.push(` ${BOLD}activity${RESET} ${DIM}(${h.total} events)${RESET}`);
199
+ const tagOrder = ["observation", "reasoner", "explain", "+ action", "! action", "you", "system", "status"];
200
+ const tagColors = {
201
+ "observation": SLATE, "reasoner": SKY, "explain": CYAN,
202
+ "+ action": AMBER, "! action": RED, "action": AMBER, "error": RED,
203
+ "you": LIME, "system": SLATE, "status": SLATE,
204
+ };
205
+ // sort: known tags first in order, then unknown by count
206
+ const knownTags = tagOrder.filter((t) => h.byTag.has(t));
207
+ const unknownTags = [...h.byTag.keys()].filter((t) => !tagOrder.includes(t)).sort((a, b) => (h.byTag.get(b) ?? 0) - (h.byTag.get(a) ?? 0));
208
+ const allTags = [...knownTags, ...unknownTags];
209
+ for (const tag of allTags) {
210
+ const count = h.byTag.get(tag) ?? 0;
211
+ const color = tagColors[tag] ?? SLATE;
212
+ const bar = "█".repeat(Math.min(20, Math.round((count / h.total) * 20)));
213
+ const pct = Math.round((count / h.total) * 100);
214
+ lines.push(` ${color}${tag.padEnd(18)}${RESET} ${String(count).padStart(4)} ${SLATE}${bar}${RESET} ${DIM}${pct}%${RESET}`);
215
+ }
216
+ }
217
+ lines.push(` ${hr}`);
218
+ lines.push("");
219
+ return lines.join("\n");
220
+ }
221
+ //# sourceMappingURL=stats.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.69.0",
3
+ "version": "0.70.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",