aoaoe 0.69.0 → 0.71.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 +5 -0
- package/dist/config.js +40 -2
- package/dist/index.js +42 -1
- package/dist/replay.d.ts +43 -0
- package/dist/replay.js +174 -0
- package/dist/stats.d.ts +54 -0
- package/dist/stats.js +221 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export declare function parseCliArgs(argv: string[]): {
|
|
|
44
44
|
runTail: boolean;
|
|
45
45
|
tailFollow: boolean;
|
|
46
46
|
tailCount?: number;
|
|
47
|
+
runStats: boolean;
|
|
48
|
+
statsLast?: string;
|
|
49
|
+
runReplay: boolean;
|
|
50
|
+
replaySpeed?: number;
|
|
51
|
+
replayLast?: string;
|
|
47
52
|
registerTitle?: string;
|
|
48
53
|
};
|
|
49
54
|
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, runReplay: false, replaySpeed: undefined, replayLast: 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,33 @@ 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
|
+
}
|
|
422
|
+
if (argv[2] === "replay") {
|
|
423
|
+
let speed;
|
|
424
|
+
let last;
|
|
425
|
+
for (let i = 3; i < argv.length; i++) {
|
|
426
|
+
if ((argv[i] === "--speed" || argv[i] === "-s") && argv[i + 1]) {
|
|
427
|
+
const val = parseFloat(argv[++i]);
|
|
428
|
+
if (!isNaN(val) && val >= 0)
|
|
429
|
+
speed = val;
|
|
430
|
+
}
|
|
431
|
+
else if ((argv[i] === "--last" || argv[i] === "-l") && argv[i + 1]) {
|
|
432
|
+
last = argv[++i];
|
|
433
|
+
}
|
|
434
|
+
else if (argv[i] === "--instant") {
|
|
435
|
+
speed = 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { ...defaults, runReplay: true, replaySpeed: speed, replayLast: last };
|
|
439
|
+
}
|
|
413
440
|
if (argv[2] === "register") {
|
|
414
441
|
register = true;
|
|
415
442
|
// parse --title from remaining args
|
|
@@ -503,7 +530,7 @@ export function parseCliArgs(argv) {
|
|
|
503
530
|
break;
|
|
504
531
|
}
|
|
505
532
|
}
|
|
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 };
|
|
533
|
+
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, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
507
534
|
}
|
|
508
535
|
export function printHelp() {
|
|
509
536
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -533,6 +560,12 @@ commands:
|
|
|
533
560
|
export --format <json|markdown> output format (default: json)
|
|
534
561
|
export --output <file> write to file (default: stdout)
|
|
535
562
|
export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
|
|
563
|
+
stats show aggregate daemon statistics (actions, sessions, activity)
|
|
564
|
+
stats --last <duration> time window: 1h, 6h, 24h, 7d (default: all time)
|
|
565
|
+
replay play back tui-history.jsonl like a movie with simulated timing
|
|
566
|
+
replay --speed <N> playback speed: 1=realtime, 5=5x (default), 10=fast, 0=instant
|
|
567
|
+
replay --instant same as --speed 0 (no delays, dump all entries immediately)
|
|
568
|
+
replay --last <duration> only replay entries from the last 1h, 6h, 24h, 7d
|
|
536
569
|
tail live-stream daemon activity to a separate terminal
|
|
537
570
|
tail -f follow mode — keep watching for new entries (Ctrl+C to stop)
|
|
538
571
|
tail -n <N> number of entries to show (default: 50)
|
|
@@ -568,6 +601,11 @@ logs options:
|
|
|
568
601
|
--grep, -g <pattern> filter entries by substring or regex
|
|
569
602
|
-n, --count <number> number of entries to show (default: 50)
|
|
570
603
|
|
|
604
|
+
replay options:
|
|
605
|
+
--speed, -s <number> playback speed multiplier (default: 5)
|
|
606
|
+
--instant no delays, dump all entries immediately
|
|
607
|
+
--last, -l <duration> time window: 1h, 6h, 24h, 7d
|
|
608
|
+
|
|
571
609
|
tail options:
|
|
572
610
|
-f, --follow keep watching for new entries (Ctrl+C to stop)
|
|
573
611
|
-n, --count <number> 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, runReplay: isReplay, replaySpeed, replayLast, registerTitle } = parseCliArgs(process.argv);
|
|
36
36
|
if (help) {
|
|
37
37
|
printHelp();
|
|
38
38
|
process.exit(0);
|
|
@@ -110,6 +110,17 @@ 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
|
+
}
|
|
118
|
+
// `aoaoe replay` -- play back tui-history.jsonl like a movie
|
|
119
|
+
if (isReplay) {
|
|
120
|
+
const { runReplay: doReplay } = await import("./replay.js");
|
|
121
|
+
await doReplay({ speed: replaySpeed ?? 5, last: replayLast });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
113
124
|
// `aoaoe task` -- task management CLI
|
|
114
125
|
if (isTaskCli) {
|
|
115
126
|
await runTaskCli(process.argv);
|
|
@@ -1455,6 +1466,36 @@ async function runTimelineExport(format, output, last) {
|
|
|
1455
1466
|
process.stdout.write(content);
|
|
1456
1467
|
}
|
|
1457
1468
|
}
|
|
1469
|
+
// `aoaoe stats` -- show aggregate daemon statistics
|
|
1470
|
+
async function runStatsCommand(last) {
|
|
1471
|
+
const { parseActionStats, parseHistoryStats, combineStats, formatStats } = await import("./stats.js");
|
|
1472
|
+
const { parseDuration } = await import("./export.js");
|
|
1473
|
+
const { loadTuiHistory } = await import("./tui-history.js");
|
|
1474
|
+
const maxAgeMs = last ? parseDuration(last) : undefined;
|
|
1475
|
+
if (last && maxAgeMs === undefined) {
|
|
1476
|
+
console.error(`error: --last must be like "1h", "6h", "24h", "7d", got "${last}"`);
|
|
1477
|
+
process.exit(1);
|
|
1478
|
+
}
|
|
1479
|
+
const windowLabel = last ?? "all time";
|
|
1480
|
+
// read actions.log
|
|
1481
|
+
const actionsFile = join(homedir(), ".aoaoe", "actions.log");
|
|
1482
|
+
let actionLines = [];
|
|
1483
|
+
try {
|
|
1484
|
+
if (existsSync(actionsFile)) {
|
|
1485
|
+
actionLines = readFileSync(actionsFile, "utf-8").trim().split("\n").filter((l) => l.trim());
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
catch {
|
|
1489
|
+
// no actions — that's fine
|
|
1490
|
+
}
|
|
1491
|
+
// read tui-history
|
|
1492
|
+
const retentionMs = maxAgeMs ?? 365 * 24 * 60 * 60 * 1000; // 1 year default
|
|
1493
|
+
const historyEntries = loadTuiHistory(100_000, undefined, retentionMs);
|
|
1494
|
+
const actionStats = parseActionStats(actionLines, maxAgeMs ?? undefined);
|
|
1495
|
+
const historyStats = parseHistoryStats(historyEntries, maxAgeMs ?? undefined);
|
|
1496
|
+
const combined = combineStats(actionStats, historyStats);
|
|
1497
|
+
console.log(formatStats(combined, windowLabel));
|
|
1498
|
+
}
|
|
1458
1499
|
// `aoaoe test` -- dynamically import and run the integration test
|
|
1459
1500
|
async function runIntegrationTest() {
|
|
1460
1501
|
const testModule = resolve(__dirname, "integration-test.js");
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type HistoryEntry } from "./tui-history.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute the delay between two entries, scaled by speed multiplier.
|
|
4
|
+
* Caps individual delays to prevent long waits on idle periods.
|
|
5
|
+
*/
|
|
6
|
+
export declare function computeDelay(prevTs: number, currTs: number, speed: number, maxDelayMs?: number): number;
|
|
7
|
+
/**
|
|
8
|
+
* Format a speed multiplier for display.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatSpeed(speed: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a speed string ("2x", "10x", "0.5x", "instant") into a number.
|
|
13
|
+
* Returns 0 for instant, null for invalid.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseSpeed(input: string): number | null;
|
|
16
|
+
/**
|
|
17
|
+
* Filter entries by time window.
|
|
18
|
+
*/
|
|
19
|
+
export declare function filterByWindow(entries: HistoryEntry[], maxAgeMs?: number, now?: number): HistoryEntry[];
|
|
20
|
+
/**
|
|
21
|
+
* Build the replay header.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatReplayHeader(entries: HistoryEntry[], speed: number, windowLabel?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Build the replay footer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatReplayFooter(entries: HistoryEntry[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Load entries from the history file, optionally filtering by time window.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadReplayEntries(maxAgeMs?: number, filePath?: string): HistoryEntry[];
|
|
32
|
+
/**
|
|
33
|
+
* Run the replay.
|
|
34
|
+
* Prints entries with simulated delays between them.
|
|
35
|
+
* Speed 0 = instant (no delays).
|
|
36
|
+
* Ctrl+C stops playback.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runReplay(opts: {
|
|
39
|
+
speed: number;
|
|
40
|
+
last?: string;
|
|
41
|
+
filePath?: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=replay.d.ts.map
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// replay.ts — play back tui-history.jsonl like a movie.
|
|
2
|
+
// shows what the daemon did with simulated timing or instant output.
|
|
3
|
+
// reuses formatTailEntry from tail.ts for consistent rendering.
|
|
4
|
+
// pure exported functions for testability.
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { TUI_HISTORY_FILE } from "./tui-history.js";
|
|
7
|
+
import { formatTailEntry, formatTailDate } from "./tail.js";
|
|
8
|
+
import { parseDuration } from "./export.js";
|
|
9
|
+
import { DIM, RESET, AMBER } from "./colors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Compute the delay between two entries, scaled by speed multiplier.
|
|
12
|
+
* Caps individual delays to prevent long waits on idle periods.
|
|
13
|
+
*/
|
|
14
|
+
export function computeDelay(prevTs, currTs, speed, maxDelayMs = 3000) {
|
|
15
|
+
if (speed <= 0)
|
|
16
|
+
return 0;
|
|
17
|
+
const raw = currTs - prevTs;
|
|
18
|
+
if (raw <= 0)
|
|
19
|
+
return 0;
|
|
20
|
+
return Math.min(Math.round(raw / speed), maxDelayMs);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Format a speed multiplier for display.
|
|
24
|
+
*/
|
|
25
|
+
export function formatSpeed(speed) {
|
|
26
|
+
if (speed <= 0)
|
|
27
|
+
return "instant";
|
|
28
|
+
if (speed === 1)
|
|
29
|
+
return "1x (realtime)";
|
|
30
|
+
if (Number.isInteger(speed))
|
|
31
|
+
return `${speed}x`;
|
|
32
|
+
return `${speed.toFixed(1)}x`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a speed string ("2x", "10x", "0.5x", "instant") into a number.
|
|
36
|
+
* Returns 0 for instant, null for invalid.
|
|
37
|
+
*/
|
|
38
|
+
export function parseSpeed(input) {
|
|
39
|
+
if (input === "instant" || input === "0")
|
|
40
|
+
return 0;
|
|
41
|
+
const match = input.match(/^(\d+(?:\.\d+)?)x?$/);
|
|
42
|
+
if (!match)
|
|
43
|
+
return null;
|
|
44
|
+
const val = parseFloat(match[1]);
|
|
45
|
+
if (!isFinite(val) || val < 0)
|
|
46
|
+
return null;
|
|
47
|
+
return val;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Filter entries by time window.
|
|
51
|
+
*/
|
|
52
|
+
export function filterByWindow(entries, maxAgeMs, now) {
|
|
53
|
+
if (!maxAgeMs)
|
|
54
|
+
return entries;
|
|
55
|
+
const cutoff = (now ?? Date.now()) - maxAgeMs;
|
|
56
|
+
return entries.filter((e) => e.ts >= cutoff);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the replay header.
|
|
60
|
+
*/
|
|
61
|
+
export function formatReplayHeader(entries, speed, windowLabel) {
|
|
62
|
+
if (entries.length === 0)
|
|
63
|
+
return `${DIM}no entries to replay${RESET}`;
|
|
64
|
+
const first = entries[0];
|
|
65
|
+
const last = entries[entries.length - 1];
|
|
66
|
+
const dateRange = formatTailDate(first.ts) === formatTailDate(last.ts)
|
|
67
|
+
? formatTailDate(first.ts)
|
|
68
|
+
: `${formatTailDate(first.ts)} → ${formatTailDate(last.ts)}`;
|
|
69
|
+
const span = last.ts - first.ts;
|
|
70
|
+
const spanStr = span < 60_000 ? `${Math.floor(span / 1000)}s`
|
|
71
|
+
: span < 3_600_000 ? `${Math.floor(span / 60_000)}m`
|
|
72
|
+
: `${Math.floor(span / 3_600_000)}h ${Math.floor((span % 3_600_000) / 60_000)}m`;
|
|
73
|
+
const speedStr = formatSpeed(speed);
|
|
74
|
+
const windowStr = windowLabel ? ` (${windowLabel})` : "";
|
|
75
|
+
return `${DIM}── replay: ${entries.length} entries, ${dateRange}, span ${spanStr}, ${AMBER}${speedStr}${RESET}${DIM}${windowStr} ──${RESET}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the replay footer.
|
|
79
|
+
*/
|
|
80
|
+
export function formatReplayFooter(entries) {
|
|
81
|
+
if (entries.length === 0)
|
|
82
|
+
return "";
|
|
83
|
+
return `${DIM}── replay complete: ${entries.length} entries ──${RESET}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sleep for a given number of milliseconds.
|
|
87
|
+
*/
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Load entries from the history file, optionally filtering by time window.
|
|
93
|
+
*/
|
|
94
|
+
export function loadReplayEntries(maxAgeMs, filePath = TUI_HISTORY_FILE) {
|
|
95
|
+
try {
|
|
96
|
+
if (!existsSync(filePath))
|
|
97
|
+
return [];
|
|
98
|
+
const content = readFileSync(filePath, "utf-8");
|
|
99
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(line);
|
|
104
|
+
if (isValidEntry(parsed))
|
|
105
|
+
entries.push(parsed);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// skip malformed
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return filterByWindow(entries, maxAgeMs);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run the replay.
|
|
119
|
+
* Prints entries with simulated delays between them.
|
|
120
|
+
* Speed 0 = instant (no delays).
|
|
121
|
+
* Ctrl+C stops playback.
|
|
122
|
+
*/
|
|
123
|
+
export async function runReplay(opts) {
|
|
124
|
+
const filePath = opts.filePath ?? TUI_HISTORY_FILE;
|
|
125
|
+
const maxAgeMs = opts.last ? parseDuration(opts.last) ?? undefined : undefined;
|
|
126
|
+
if (opts.last && maxAgeMs === undefined) {
|
|
127
|
+
process.stderr.write(`error: --last must be like "1h", "6h", "24h", "7d", got "${opts.last}"\n`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const entries = loadReplayEntries(maxAgeMs, filePath);
|
|
131
|
+
const windowLabel = opts.last ?? undefined;
|
|
132
|
+
// header
|
|
133
|
+
process.stderr.write(formatReplayHeader(entries, opts.speed, windowLabel) + "\n");
|
|
134
|
+
if (entries.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
// Ctrl+C cleanup
|
|
137
|
+
let stopped = false;
|
|
138
|
+
const onSignal = () => { stopped = true; };
|
|
139
|
+
process.on("SIGINT", onSignal);
|
|
140
|
+
process.on("SIGTERM", onSignal);
|
|
141
|
+
try {
|
|
142
|
+
for (let i = 0; i < entries.length; i++) {
|
|
143
|
+
if (stopped)
|
|
144
|
+
break;
|
|
145
|
+
// delay between entries
|
|
146
|
+
if (i > 0 && opts.speed > 0) {
|
|
147
|
+
const delay = computeDelay(entries[i - 1].ts, entries[i].ts, opts.speed);
|
|
148
|
+
if (delay > 0)
|
|
149
|
+
await sleep(delay);
|
|
150
|
+
}
|
|
151
|
+
if (stopped)
|
|
152
|
+
break;
|
|
153
|
+
process.stderr.write(formatTailEntry(entries[i]) + "\n");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
process.removeListener("SIGINT", onSignal);
|
|
158
|
+
process.removeListener("SIGTERM", onSignal);
|
|
159
|
+
}
|
|
160
|
+
// footer
|
|
161
|
+
if (!stopped) {
|
|
162
|
+
process.stderr.write(formatReplayFooter(entries) + "\n");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isValidEntry(val) {
|
|
166
|
+
if (typeof val !== "object" || val === null)
|
|
167
|
+
return false;
|
|
168
|
+
const obj = val;
|
|
169
|
+
return (typeof obj.ts === "number" &&
|
|
170
|
+
typeof obj.time === "string" &&
|
|
171
|
+
typeof obj.tag === "string" &&
|
|
172
|
+
typeof obj.text === "string");
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=replay.js.map
|
package/dist/stats.d.ts
ADDED
|
@@ -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
|