aoaoe 0.62.0 → 0.64.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
@@ -248,6 +248,10 @@ commands:
248
248
  logs --actions show action log entries (from ~/.aoaoe/actions.log)
249
249
  logs --grep <pattern> filter log entries by substring or regex
250
250
  logs -n <count> number of entries to show (default: 50)
251
+ export export session timeline as JSON or Markdown for post-mortems
252
+ export --format <json|markdown> output format (default: json)
253
+ export --output <file> write to file (default: stdout)
254
+ export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
251
255
  task manage tasks and sessions (list, start, stop, new, rm, edit)
252
256
  tasks show task progress (from aoaoe.tasks.json)
253
257
  history review recent actions (from ~/.aoaoe/actions.log)
package/dist/config.d.ts CHANGED
@@ -34,6 +34,10 @@ export declare function parseCliArgs(argv: string[]): {
34
34
  logsActions: boolean;
35
35
  logsGrep?: string;
36
36
  logsCount?: number;
37
+ runExport: boolean;
38
+ exportFormat?: string;
39
+ exportOutput?: string;
40
+ exportLast?: string;
37
41
  runInit: boolean;
38
42
  initForce: boolean;
39
43
  runTaskCli: boolean;
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, 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 };
331
331
  // check for subcommand as first non-flag arg
332
332
  if (argv[2] === "test-context") {
333
333
  return { ...defaults, testContext: true };
@@ -374,6 +374,23 @@ export function parseCliArgs(argv) {
374
374
  }
375
375
  return { ...defaults, runLogs: true, logsActions: actions, logsGrep: grep, logsCount: count };
376
376
  }
377
+ if (argv[2] === "export") {
378
+ let format;
379
+ let output;
380
+ let last;
381
+ for (let i = 3; i < argv.length; i++) {
382
+ if ((argv[i] === "--format" || argv[i] === "-f") && argv[i + 1]) {
383
+ format = argv[++i];
384
+ }
385
+ else if ((argv[i] === "--output" || argv[i] === "-o") && argv[i + 1]) {
386
+ output = argv[++i];
387
+ }
388
+ else if ((argv[i] === "--last" || argv[i] === "-l") && argv[i + 1]) {
389
+ last = argv[++i];
390
+ }
391
+ }
392
+ return { ...defaults, runExport: true, exportFormat: format, exportOutput: output, exportLast: last };
393
+ }
377
394
  if (argv[2] === "init") {
378
395
  const force = argv.includes("--force") || argv.includes("-f");
379
396
  return { ...defaults, runInit: true, initForce: force };
@@ -471,7 +488,7 @@ export function parseCliArgs(argv) {
471
488
  break;
472
489
  }
473
490
  }
474
- 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, runInit: false, initForce: false, runTaskCli: false };
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 };
475
492
  }
476
493
  export function printHelp() {
477
494
  console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
@@ -497,6 +514,10 @@ commands:
497
514
  logs --actions show action log entries (from ~/.aoaoe/actions.log)
498
515
  logs --grep <pattern> filter log entries by substring or regex
499
516
  logs -n <count> number of entries to show (default: 50)
517
+ export export session timeline as JSON or Markdown for post-mortems
518
+ export --format <json|markdown> output format (default: json)
519
+ export --output <file> write to file (default: stdout)
520
+ export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
500
521
  task manage tasks and sessions (list, start, stop, new, rm, edit)
501
522
  tasks show task progress (from aoaoe.tasks.json)
502
523
  history review recent actions (from ~/.aoaoe/actions.log)
@@ -1,4 +1,5 @@
1
1
  import type { DaemonState, DaemonPhase, DaemonSessionState, Observation } from "./types.js";
2
+ export declare function setStateDir(dir: string): void;
2
3
  export declare function resetInternalState(): void;
3
4
  export declare function setSessionTask(sessionId: string, task: string): void;
4
5
  export declare function writeState(phase: DaemonPhase, updates?: Partial<Omit<DaemonState, "phase" | "phaseStartedAt">>): void;
@@ -6,10 +6,19 @@ import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { toDaemonState } from "./types.js";
8
8
  import { parseTasks, formatTaskList } from "./task-parser.js";
9
- const AOAOE_DIR = join(homedir(), ".aoaoe");
10
- const STATE_FILE = join(AOAOE_DIR, "daemon-state.json");
11
- const INTERRUPT_FILE = join(AOAOE_DIR, "interrupt");
12
- const LOCK_FILE = join(AOAOE_DIR, "daemon.lock");
9
+ // default state directory — overridable via setStateDir() for test isolation
10
+ let AOAOE_DIR = join(homedir(), ".aoaoe");
11
+ let STATE_FILE = join(AOAOE_DIR, "daemon-state.json");
12
+ let INTERRUPT_FILE = join(AOAOE_DIR, "interrupt");
13
+ let LOCK_FILE = join(AOAOE_DIR, "daemon.lock");
14
+ // redirect all state file paths to a custom directory (test isolation)
15
+ export function setStateDir(dir) {
16
+ AOAOE_DIR = dir;
17
+ STATE_FILE = join(dir, "daemon-state.json");
18
+ INTERRUPT_FILE = join(dir, "interrupt");
19
+ LOCK_FILE = join(dir, "daemon.lock");
20
+ dirEnsured = false; // force re-create on next write
21
+ }
13
22
  // cache: only mkdirSync once per process (no need to stat the dir on every phase change)
14
23
  let dirEnsured = false;
15
24
  function ensureDir() {
@@ -52,14 +61,15 @@ export function setSessionTask(sessionId, task) {
52
61
  const DEBOUNCE_MS = 500;
53
62
  let lastFlushedPhase = null;
54
63
  let lastFlushTime = 0;
55
- const STATE_TMP = STATE_FILE + ".tmp";
64
+ // note: STATE_TMP is computed dynamically inside flushState() since STATE_FILE is mutable
56
65
  function flushState() {
57
66
  try {
58
67
  ensureDir();
59
68
  // atomic write: write to temp file, then rename into place.
60
69
  // prevents chat.ts from reading a partially-written JSON file.
61
- writeFileSync(STATE_TMP, JSON.stringify(currentState) + "\n");
62
- renameSync(STATE_TMP, STATE_FILE);
70
+ const tmp = STATE_FILE + ".tmp";
71
+ writeFileSync(tmp, JSON.stringify(currentState) + "\n");
72
+ renameSync(tmp, STATE_FILE);
63
73
  lastFlushedPhase = currentState.phase;
64
74
  lastFlushTime = Date.now();
65
75
  }
@@ -0,0 +1,17 @@
1
+ import type { HistoryEntry } from "./tui-history.js";
2
+ export interface TimelineEntry {
3
+ ts: number;
4
+ source: "action" | "activity";
5
+ tag: string;
6
+ text: string;
7
+ success?: boolean;
8
+ session?: string;
9
+ }
10
+ export declare function parseActionLogEntries(lines: string[]): TimelineEntry[];
11
+ export declare function parseActivityEntries(entries: HistoryEntry[]): TimelineEntry[];
12
+ export declare function mergeTimeline(...sources: TimelineEntry[][]): TimelineEntry[];
13
+ export declare function filterByAge(entries: TimelineEntry[], maxAgeMs: number, now?: number): TimelineEntry[];
14
+ export declare function parseDuration(input: string): number | null;
15
+ export declare function formatTimelineJson(entries: TimelineEntry[]): string;
16
+ export declare function formatTimelineMarkdown(entries: TimelineEntry[]): string;
17
+ //# sourceMappingURL=export.d.ts.map
package/dist/export.js ADDED
@@ -0,0 +1,132 @@
1
+ // export.ts — pure functions for exporting session timelines as JSON or Markdown
2
+ // reads actions.log (JSONL) and tui-history.jsonl, merges into a unified timeline
3
+ import { toActionLogEntry } from "./types.js";
4
+ // ── parsers ─────────────────────────────────────────────────────────────────
5
+ // parse actions.log JSONL lines into timeline entries
6
+ export function parseActionLogEntries(lines) {
7
+ const entries = [];
8
+ for (const line of lines) {
9
+ if (!line.trim())
10
+ continue;
11
+ try {
12
+ const entry = toActionLogEntry(JSON.parse(line));
13
+ if (!entry)
14
+ continue;
15
+ // skip wait actions (noise in post-mortems)
16
+ if (entry.action.action === "wait")
17
+ continue;
18
+ entries.push({
19
+ ts: entry.timestamp,
20
+ source: "action",
21
+ tag: entry.action.action,
22
+ text: entry.detail || `${entry.action.action} on ${entry.action.session ?? "unknown"}`,
23
+ success: entry.success,
24
+ session: entry.action.session ?? entry.action.title,
25
+ });
26
+ }
27
+ catch {
28
+ // skip malformed lines
29
+ }
30
+ }
31
+ return entries;
32
+ }
33
+ // convert tui-history entries into timeline entries
34
+ export function parseActivityEntries(entries) {
35
+ return entries.map((e) => ({
36
+ ts: e.ts,
37
+ source: "activity",
38
+ tag: e.tag,
39
+ text: e.text,
40
+ }));
41
+ }
42
+ // ── merge + filter ──────────────────────────────────────────────────────────
43
+ // merge multiple sources into a single chronological timeline
44
+ export function mergeTimeline(...sources) {
45
+ const all = sources.flat();
46
+ all.sort((a, b) => a.ts - b.ts);
47
+ return all;
48
+ }
49
+ // filter entries by time window (keep entries newer than cutoff)
50
+ export function filterByAge(entries, maxAgeMs, now = Date.now()) {
51
+ const cutoff = now - maxAgeMs;
52
+ return entries.filter((e) => e.ts >= cutoff);
53
+ }
54
+ // ── duration parser ─────────────────────────────────────────────────────────
55
+ // parse human-friendly duration strings: "1h", "6h", "24h", "2d", "7d", "30d"
56
+ export function parseDuration(input) {
57
+ const match = input.match(/^(\d+)(h|d)$/);
58
+ if (!match)
59
+ return null;
60
+ const val = parseInt(match[1], 10);
61
+ if (val <= 0)
62
+ return null;
63
+ const unit = match[2];
64
+ if (unit === "h")
65
+ return val * 60 * 60 * 1000;
66
+ if (unit === "d")
67
+ return val * 24 * 60 * 60 * 1000;
68
+ return null;
69
+ }
70
+ // ── formatters ──────────────────────────────────────────────────────────────
71
+ // format timeline as pretty-printed JSON
72
+ export function formatTimelineJson(entries) {
73
+ const output = entries.map((e) => {
74
+ const obj = {
75
+ time: new Date(e.ts).toISOString(),
76
+ source: e.source,
77
+ tag: e.tag,
78
+ text: e.text,
79
+ };
80
+ if (e.success !== undefined)
81
+ obj.success = e.success;
82
+ if (e.session)
83
+ obj.session = e.session;
84
+ return obj;
85
+ });
86
+ return JSON.stringify(output, null, 2) + "\n";
87
+ }
88
+ // format timeline as a readable Markdown post-mortem document
89
+ export function formatTimelineMarkdown(entries) {
90
+ if (entries.length === 0) {
91
+ return "# aoaoe Session Export\n\n_No entries in the selected time range._\n";
92
+ }
93
+ const lines = [];
94
+ const now = new Date();
95
+ const first = new Date(entries[0].ts);
96
+ const last = new Date(entries[entries.length - 1].ts);
97
+ lines.push("# aoaoe Session Export");
98
+ lines.push("");
99
+ lines.push(`**Generated**: ${now.toISOString()}`);
100
+ lines.push(`**Range**: ${first.toISOString()} — ${last.toISOString()}`);
101
+ lines.push(`**Entries**: ${entries.length}`);
102
+ lines.push("");
103
+ lines.push("## Timeline");
104
+ lines.push("");
105
+ // group entries by hour
106
+ let currentHour = "";
107
+ for (const entry of entries) {
108
+ const d = new Date(entry.ts);
109
+ const hour = d.toLocaleString("en-US", {
110
+ month: "short", day: "numeric", hour: "numeric", minute: undefined, hour12: true,
111
+ }).replace(/:00/, "");
112
+ // simpler: just use HH:00 block headers
113
+ const hourKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
114
+ if (hourKey !== currentHour) {
115
+ if (currentHour)
116
+ lines.push("");
117
+ lines.push(`### ${hourKey}`);
118
+ lines.push("");
119
+ currentHour = hourKey;
120
+ }
121
+ const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
122
+ const icon = entry.source === "action"
123
+ ? (entry.success ? "+" : "!")
124
+ : "·";
125
+ const sessionPart = entry.session ? ` → ${entry.session}` : "";
126
+ const text = entry.text.length > 120 ? entry.text.slice(0, 117) + "..." : entry.text;
127
+ lines.push(`- \`${time}\` ${icon} **${entry.tag}**${sessionPart}: ${text}`);
128
+ }
129
+ lines.push("");
130
+ return lines.join("\n");
131
+ }
132
+ //# sourceMappingURL=export.js.map
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { isDaemonRunningFromState } from "./chat.js";
20
20
  import { sendNotification, sendTestNotification } from "./notify.js";
21
21
  import { startHealthServer } from "./health.js";
22
22
  import { loadTuiHistory } from "./tui-history.js";
23
+ import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
23
24
  import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
24
25
  import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
25
26
  import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
@@ -30,7 +31,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
30
31
  const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
31
32
  const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
32
33
  async function main() {
33
- const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
34
+ 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);
34
35
  if (help) {
35
36
  printHelp();
36
37
  process.exit(0);
@@ -103,6 +104,11 @@ async function main() {
103
104
  await showLogs(logsActions, logsGrep, logsCount);
104
105
  return;
105
106
  }
107
+ // `aoaoe export` -- export session timeline as JSON or Markdown
108
+ if (runExport) {
109
+ await runTimelineExport(exportFormat, exportOutput, exportLast);
110
+ return;
111
+ }
106
112
  // `aoaoe task` -- task management CLI
107
113
  if (isTaskCli) {
108
114
  await runTaskCli(process.argv);
@@ -1311,6 +1317,47 @@ async function showLogs(actions, grep, count) {
1311
1317
  console.log("");
1312
1318
  }
1313
1319
  }
1320
+ // `aoaoe export` -- export session timeline as JSON or Markdown for post-mortems
1321
+ async function runTimelineExport(format, output, last) {
1322
+ const fmt = format ?? "json";
1323
+ if (fmt !== "json" && fmt !== "markdown" && fmt !== "md") {
1324
+ console.error(`error: --format must be "json" or "markdown", got "${fmt}"`);
1325
+ process.exit(1);
1326
+ }
1327
+ // parse time window (default 24h)
1328
+ const durationMs = last ? parseDuration(last) : 24 * 60 * 60 * 1000;
1329
+ if (durationMs === null) {
1330
+ console.error(`error: --last must be like "1h", "6h", "24h", "7d", got "${last}"`);
1331
+ process.exit(1);
1332
+ }
1333
+ // read actions.log
1334
+ const actionsFile = join(homedir(), ".aoaoe", "actions.log");
1335
+ let actionEntries = [];
1336
+ try {
1337
+ const lines = readFileSync(actionsFile, "utf-8").trim().split("\n").filter((l) => l.trim());
1338
+ actionEntries = parseActionLogEntries(lines);
1339
+ }
1340
+ catch {
1341
+ // no actions.log — that's fine
1342
+ }
1343
+ // read tui-history.jsonl
1344
+ const historyEntries = loadTuiHistory(10_000, undefined, durationMs);
1345
+ const activityEntries = parseActivityEntries(historyEntries);
1346
+ // merge and filter
1347
+ let timeline = mergeTimeline(actionEntries, activityEntries);
1348
+ timeline = filterByAge(timeline, durationMs);
1349
+ // format
1350
+ const isMarkdown = fmt === "markdown" || fmt === "md";
1351
+ const content = isMarkdown ? formatTimelineMarkdown(timeline) : formatTimelineJson(timeline);
1352
+ // output
1353
+ if (output) {
1354
+ writeFileSync(output, content);
1355
+ console.log(`exported ${timeline.length} entries to ${output}`);
1356
+ }
1357
+ else {
1358
+ process.stdout.write(content);
1359
+ }
1360
+ }
1314
1361
  // `aoaoe test` -- dynamically import and run the integration test
1315
1362
  async function runIntegrationTest() {
1316
1363
  const testModule = resolve(__dirname, "integration-test.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.62.0",
3
+ "version": "0.64.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",