aoaoe 0.68.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 +5 -0
- package/dist/config.js +35 -2
- package/dist/index.js +42 -1
- package/dist/stats.d.ts +54 -0
- package/dist/stats.js +221 -0
- package/dist/tail.d.ts +41 -0
- package/dist/tail.js +211 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -41,6 +41,11 @@ export declare function parseCliArgs(argv: string[]): {
|
|
|
41
41
|
runInit: boolean;
|
|
42
42
|
initForce: boolean;
|
|
43
43
|
runTaskCli: boolean;
|
|
44
|
+
runTail: boolean;
|
|
45
|
+
tailFollow: boolean;
|
|
46
|
+
tailCount?: number;
|
|
47
|
+
runStats: boolean;
|
|
48
|
+
statsLast?: string;
|
|
44
49
|
registerTitle?: string;
|
|
45
50
|
};
|
|
46
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 };
|
|
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 };
|
|
@@ -395,6 +395,30 @@ export function parseCliArgs(argv) {
|
|
|
395
395
|
const force = argv.includes("--force") || argv.includes("-f");
|
|
396
396
|
return { ...defaults, runInit: true, initForce: force };
|
|
397
397
|
}
|
|
398
|
+
if (argv[2] === "tail") {
|
|
399
|
+
let follow = false;
|
|
400
|
+
let count;
|
|
401
|
+
for (let i = 3; i < argv.length; i++) {
|
|
402
|
+
if (argv[i] === "-f" || argv[i] === "--follow") {
|
|
403
|
+
follow = true;
|
|
404
|
+
}
|
|
405
|
+
else if ((argv[i] === "-n" || argv[i] === "--count") && argv[i + 1]) {
|
|
406
|
+
const val = parseInt(argv[++i], 10);
|
|
407
|
+
if (!isNaN(val) && val > 0)
|
|
408
|
+
count = val;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { ...defaults, runTail: true, tailFollow: follow, tailCount: count };
|
|
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
|
+
}
|
|
398
422
|
if (argv[2] === "register") {
|
|
399
423
|
register = true;
|
|
400
424
|
// parse --title from remaining args
|
|
@@ -488,7 +512,7 @@ export function parseCliArgs(argv) {
|
|
|
488
512
|
break;
|
|
489
513
|
}
|
|
490
514
|
}
|
|
491
|
-
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 };
|
|
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 };
|
|
492
516
|
}
|
|
493
517
|
export function printHelp() {
|
|
494
518
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -518,6 +542,11 @@ commands:
|
|
|
518
542
|
export --format <json|markdown> output format (default: json)
|
|
519
543
|
export --output <file> write to file (default: stdout)
|
|
520
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)
|
|
547
|
+
tail live-stream daemon activity to a separate terminal
|
|
548
|
+
tail -f follow mode — keep watching for new entries (Ctrl+C to stop)
|
|
549
|
+
tail -n <N> number of entries to show (default: 50)
|
|
521
550
|
task manage tasks and sessions (list, start, stop, new, rm, edit)
|
|
522
551
|
tasks show task progress (from aoaoe.tasks.json)
|
|
523
552
|
history review recent actions (from ~/.aoaoe/actions.log)
|
|
@@ -550,6 +579,10 @@ logs options:
|
|
|
550
579
|
--grep, -g <pattern> filter entries by substring or regex
|
|
551
580
|
-n, --count <number> number of entries to show (default: 50)
|
|
552
581
|
|
|
582
|
+
tail options:
|
|
583
|
+
-f, --follow keep watching for new entries (Ctrl+C to stop)
|
|
584
|
+
-n, --count <number> number of entries to show (default: 50)
|
|
585
|
+
|
|
553
586
|
register options:
|
|
554
587
|
--title, -t <name> session title in AoE (default: aoaoe)
|
|
555
588
|
|
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, 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);
|
|
@@ -121,6 +126,12 @@ async function main() {
|
|
|
121
126
|
await doInit(initForce);
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
129
|
+
// `aoaoe tail` -- live-stream daemon activity to a separate terminal
|
|
130
|
+
if (isTail) {
|
|
131
|
+
const { runTail: doTail } = await import("./tail.js");
|
|
132
|
+
await doTail({ count: tailCount ?? 50, follow: tailFollow });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
124
135
|
// auto-init: if no config file exists, run init automatically
|
|
125
136
|
if (!configFileExists()) {
|
|
126
137
|
console.error("");
|
|
@@ -1449,6 +1460,36 @@ async function runTimelineExport(format, output, last) {
|
|
|
1449
1460
|
process.stdout.write(content);
|
|
1450
1461
|
}
|
|
1451
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
|
+
}
|
|
1452
1493
|
// `aoaoe test` -- dynamically import and run the integration test
|
|
1453
1494
|
async function runIntegrationTest() {
|
|
1454
1495
|
const testModule = resolve(__dirname, "integration-test.js");
|
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
|
package/dist/tail.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type HistoryEntry } from "./tui-history.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a HistoryEntry as a colorized terminal line.
|
|
4
|
+
* Matches TUI formatActivity style but for plain scrolling output.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatTailEntry(entry: HistoryEntry): string;
|
|
7
|
+
/**
|
|
8
|
+
* Format a date string for the tail header.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatTailDate(ts: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Load the last N entries from the history file.
|
|
13
|
+
* Returns entries newest-last (chronological order).
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadTailEntries(count: number, filePath?: string): HistoryEntry[];
|
|
16
|
+
/**
|
|
17
|
+
* Get the current file size (for follow mode to detect appends).
|
|
18
|
+
*/
|
|
19
|
+
export declare function getFileSize(filePath: string): number;
|
|
20
|
+
/**
|
|
21
|
+
* Read new bytes appended since a given offset, parse into entries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readNewEntries(filePath: string, fromByte: number): {
|
|
24
|
+
entries: HistoryEntry[];
|
|
25
|
+
newSize: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Print entries to stderr (colorized).
|
|
29
|
+
*/
|
|
30
|
+
export declare function printEntries(entries: HistoryEntry[]): void;
|
|
31
|
+
/**
|
|
32
|
+
* Run the tail command.
|
|
33
|
+
* Without follow: print last N entries, exit.
|
|
34
|
+
* With follow: print last N, then watch for new entries (blocks until Ctrl+C).
|
|
35
|
+
*/
|
|
36
|
+
export declare function runTail(opts: {
|
|
37
|
+
count: number;
|
|
38
|
+
follow: boolean;
|
|
39
|
+
filePath?: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
//# sourceMappingURL=tail.d.ts.map
|
package/dist/tail.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// tail.ts — live-stream daemon activity to a separate terminal.
|
|
2
|
+
// reads from tui-history.jsonl, optionally follows for new entries.
|
|
3
|
+
// pure exported functions for testability.
|
|
4
|
+
import { readFileSync, statSync, existsSync, openSync, readSync, closeSync } from "node:fs";
|
|
5
|
+
import { watch } from "node:fs";
|
|
6
|
+
import { TUI_HISTORY_FILE } from "./tui-history.js";
|
|
7
|
+
import { SLATE, RESET, DIM, BOLD, CYAN, AMBER, ROSE, LIME, SKY, } from "./colors.js";
|
|
8
|
+
/**
|
|
9
|
+
* Format a HistoryEntry as a colorized terminal line.
|
|
10
|
+
* Matches TUI formatActivity style but for plain scrolling output.
|
|
11
|
+
*/
|
|
12
|
+
export function formatTailEntry(entry) {
|
|
13
|
+
let color = SLATE;
|
|
14
|
+
let prefix = entry.tag;
|
|
15
|
+
switch (entry.tag) {
|
|
16
|
+
case "observation":
|
|
17
|
+
color = SLATE;
|
|
18
|
+
prefix = "obs";
|
|
19
|
+
break;
|
|
20
|
+
case "reasoner":
|
|
21
|
+
color = SKY;
|
|
22
|
+
break;
|
|
23
|
+
case "explain":
|
|
24
|
+
color = `${BOLD}${CYAN}`;
|
|
25
|
+
prefix = "AI";
|
|
26
|
+
break;
|
|
27
|
+
case "+ action":
|
|
28
|
+
case "action":
|
|
29
|
+
color = AMBER;
|
|
30
|
+
prefix = "→ action";
|
|
31
|
+
break;
|
|
32
|
+
case "! action":
|
|
33
|
+
case "error":
|
|
34
|
+
color = ROSE;
|
|
35
|
+
prefix = "✗ error";
|
|
36
|
+
break;
|
|
37
|
+
case "you":
|
|
38
|
+
color = LIME;
|
|
39
|
+
break;
|
|
40
|
+
case "system":
|
|
41
|
+
color = SLATE;
|
|
42
|
+
break;
|
|
43
|
+
case "status":
|
|
44
|
+
color = SLATE;
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
color = SLATE;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return `${SLATE}${entry.time}${RESET} ${color}${prefix}${RESET} ${DIM}│${RESET} ${entry.text}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format a date string for the tail header.
|
|
54
|
+
*/
|
|
55
|
+
export function formatTailDate(ts) {
|
|
56
|
+
const d = new Date(ts);
|
|
57
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load the last N entries from the history file.
|
|
61
|
+
* Returns entries newest-last (chronological order).
|
|
62
|
+
*/
|
|
63
|
+
export function loadTailEntries(count, filePath = TUI_HISTORY_FILE) {
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync(filePath))
|
|
66
|
+
return [];
|
|
67
|
+
const content = readFileSync(filePath, "utf-8");
|
|
68
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
69
|
+
const entries = [];
|
|
70
|
+
// read from the end to minimize parsing
|
|
71
|
+
const start = Math.max(0, lines.length - count);
|
|
72
|
+
for (let i = start; i < lines.length; i++) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(lines[i]);
|
|
75
|
+
if (isValidEntry(parsed))
|
|
76
|
+
entries.push(parsed);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// skip malformed
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the current file size (for follow mode to detect appends).
|
|
90
|
+
*/
|
|
91
|
+
export function getFileSize(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
return statSync(filePath).size;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read new bytes appended since a given offset, parse into entries.
|
|
101
|
+
*/
|
|
102
|
+
export function readNewEntries(filePath, fromByte) {
|
|
103
|
+
try {
|
|
104
|
+
const size = statSync(filePath).size;
|
|
105
|
+
if (size === 0)
|
|
106
|
+
return { entries: [], newSize: 0 };
|
|
107
|
+
// file was truncated/rotated — read from start
|
|
108
|
+
const start = size < fromByte ? 0 : fromByte;
|
|
109
|
+
if (start === size)
|
|
110
|
+
return { entries: [], newSize: size };
|
|
111
|
+
const buf = Buffer.alloc(size - start);
|
|
112
|
+
const fd = openSync(filePath, "r");
|
|
113
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
const text = buf.toString("utf-8");
|
|
116
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
117
|
+
const entries = [];
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(line);
|
|
121
|
+
if (isValidEntry(parsed))
|
|
122
|
+
entries.push(parsed);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// skip
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { entries, newSize: size };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return { entries: [], newSize: fromByte };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Print entries to stderr (colorized).
|
|
136
|
+
*/
|
|
137
|
+
export function printEntries(entries) {
|
|
138
|
+
for (const e of entries) {
|
|
139
|
+
process.stderr.write(formatTailEntry(e) + "\n");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run the tail command.
|
|
144
|
+
* Without follow: print last N entries, exit.
|
|
145
|
+
* With follow: print last N, then watch for new entries (blocks until Ctrl+C).
|
|
146
|
+
*/
|
|
147
|
+
export async function runTail(opts) {
|
|
148
|
+
const filePath = opts.filePath ?? TUI_HISTORY_FILE;
|
|
149
|
+
// print initial entries
|
|
150
|
+
const entries = loadTailEntries(opts.count, filePath);
|
|
151
|
+
if (entries.length === 0) {
|
|
152
|
+
process.stderr.write(`${DIM}no history entries found${RESET}\n`);
|
|
153
|
+
if (!opts.follow)
|
|
154
|
+
return;
|
|
155
|
+
process.stderr.write(`${DIM}waiting for new entries... (Ctrl+C to stop)${RESET}\n`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// date header
|
|
159
|
+
const firstDate = formatTailDate(entries[0].ts);
|
|
160
|
+
const lastDate = formatTailDate(entries[entries.length - 1].ts);
|
|
161
|
+
const dateRange = firstDate === lastDate ? firstDate : `${firstDate} → ${lastDate}`;
|
|
162
|
+
process.stderr.write(`${DIM}── ${entries.length} entries (${dateRange}) ──${RESET}\n`);
|
|
163
|
+
printEntries(entries);
|
|
164
|
+
}
|
|
165
|
+
if (!opts.follow)
|
|
166
|
+
return;
|
|
167
|
+
// follow mode: watch file for changes
|
|
168
|
+
process.stderr.write(`${DIM}── following (Ctrl+C to stop) ──${RESET}\n`);
|
|
169
|
+
let lastSize = getFileSize(filePath);
|
|
170
|
+
return new Promise((_resolve) => {
|
|
171
|
+
let watcher = null;
|
|
172
|
+
try {
|
|
173
|
+
watcher = watch(filePath, { persistent: true }, () => {
|
|
174
|
+
const { entries: newEntries, newSize } = readNewEntries(filePath, lastSize);
|
|
175
|
+
if (newEntries.length > 0) {
|
|
176
|
+
printEntries(newEntries);
|
|
177
|
+
}
|
|
178
|
+
lastSize = newSize;
|
|
179
|
+
});
|
|
180
|
+
watcher.on("error", () => {
|
|
181
|
+
// file may be rotated — try to recover
|
|
182
|
+
lastSize = 0;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
process.stderr.write(`${ROSE}failed to watch ${filePath}${RESET}\n`);
|
|
187
|
+
}
|
|
188
|
+
// Ctrl+C cleanup
|
|
189
|
+
const cleanup = () => {
|
|
190
|
+
if (watcher) {
|
|
191
|
+
try {
|
|
192
|
+
watcher.close();
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
process.exit(0);
|
|
197
|
+
};
|
|
198
|
+
process.on("SIGINT", cleanup);
|
|
199
|
+
process.on("SIGTERM", cleanup);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function isValidEntry(val) {
|
|
203
|
+
if (typeof val !== "object" || val === null)
|
|
204
|
+
return false;
|
|
205
|
+
const obj = val;
|
|
206
|
+
return (typeof obj.ts === "number" &&
|
|
207
|
+
typeof obj.time === "string" &&
|
|
208
|
+
typeof obj.tag === "string" &&
|
|
209
|
+
typeof obj.text === "string");
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=tail.js.map
|