aoaoe 0.146.0 → 0.149.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/index.js CHANGED
@@ -1023,6 +1023,45 @@ async function main() {
1023
1023
  tui.log("system", `quiet hours: ${specs.join(", ")} — watchdog+burn alerts suppressed`);
1024
1024
  persistPrefs();
1025
1025
  });
1026
+ // wire /budget cost alerts
1027
+ input.onBudget((target, budgetUSD) => {
1028
+ if (budgetUSD === null) {
1029
+ // clear global budget
1030
+ tui.setGlobalBudget(null);
1031
+ tui.log("system", "budget: global budget cleared");
1032
+ return;
1033
+ }
1034
+ if (target === null) {
1035
+ tui.setGlobalBudget(budgetUSD);
1036
+ tui.log("system", `budget: global budget set to $${budgetUSD.toFixed(2)}`);
1037
+ return;
1038
+ }
1039
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
1040
+ const ok = tui.setSessionBudget(num ?? target, budgetUSD);
1041
+ if (ok)
1042
+ tui.log("system", `budget: $${budgetUSD.toFixed(2)} set for ${target}`);
1043
+ else
1044
+ tui.log("system", `session not found: ${target}`);
1045
+ });
1046
+ // wire /pause-all and /resume-all
1047
+ input.onBulkControl((action) => {
1048
+ const sessions = tui.getSessions();
1049
+ if (sessions.length === 0) {
1050
+ tui.log("system", `${action}-all: no sessions`);
1051
+ return;
1052
+ }
1053
+ // ESC for pause, Enter for resume (nudge sessions out of waiting)
1054
+ const keys = action === "pause" ? "Escape" : "Enter";
1055
+ let count = 0;
1056
+ for (const s of sessions) {
1057
+ const tmuxName = computeTmuxName(s.title, s.id);
1058
+ shellExec("tmux", ["send-keys", "-t", tmuxName, "", keys])
1059
+ .then(() => { })
1060
+ .catch((_e) => { });
1061
+ count++;
1062
+ }
1063
+ tui.log("system", `${action}-all: sent to ${count} session${count !== 1 ? "s" : ""}`);
1064
+ });
1026
1065
  // wire /quiet-status
1027
1066
  input.onQuietStatus(() => {
1028
1067
  const { active, message } = formatQuietStatus(tui.getQuietHours());
package/dist/input.d.ts CHANGED
@@ -56,6 +56,8 @@ export type CostSummaryHandler = () => void;
56
56
  export type SessionReportHandler = (target: string) => void;
57
57
  export type QuietStatusHandler = () => void;
58
58
  export type AlertLogHandler = (count: number) => void;
59
+ export type BudgetHandler = (target: string | null, budgetUSD: number | null) => void;
60
+ export type BulkControlHandler = (action: "pause" | "resume") => void;
59
61
  export interface MouseEvent {
60
62
  button: number;
61
63
  col: number;
@@ -134,6 +136,8 @@ export declare class InputReader {
134
136
  private sessionReportHandler;
135
137
  private quietStatusHandler;
136
138
  private alertLogHandler;
139
+ private budgetHandler;
140
+ private bulkControlHandler;
137
141
  private aliases;
138
142
  private mouseDataListener;
139
143
  onScroll(handler: (dir: ScrollDirection) => void): void;
@@ -195,6 +199,8 @@ export declare class InputReader {
195
199
  onSessionReport(handler: SessionReportHandler): void;
196
200
  onQuietStatus(handler: QuietStatusHandler): void;
197
201
  onAlertLog(handler: AlertLogHandler): void;
202
+ onBudget(handler: BudgetHandler): void;
203
+ onBulkControl(handler: BulkControlHandler): void;
198
204
  /** Set aliases from persisted prefs. */
199
205
  setAliases(aliases: Record<string, string>): void;
200
206
  /** Get current aliases as a plain object. */
package/dist/input.js CHANGED
@@ -89,6 +89,8 @@ export class InputReader {
89
89
  sessionReportHandler = null;
90
90
  quietStatusHandler = null;
91
91
  alertLogHandler = null;
92
+ budgetHandler = null;
93
+ bulkControlHandler = null;
92
94
  aliases = new Map(); // /shortcut → /full command
93
95
  mouseDataListener = null;
94
96
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
@@ -303,6 +305,8 @@ export class InputReader {
303
305
  onSessionReport(handler) { this.sessionReportHandler = handler; }
304
306
  onQuietStatus(handler) { this.quietStatusHandler = handler; }
305
307
  onAlertLog(handler) { this.alertLogHandler = handler; }
308
+ onBudget(handler) { this.budgetHandler = handler; }
309
+ onBulkControl(handler) { this.bulkControlHandler = handler; }
306
310
  /** Set aliases from persisted prefs. */
307
311
  setAliases(aliases) {
308
312
  this.aliases.clear();
@@ -565,6 +569,9 @@ ${BOLD}navigation:${RESET}
565
569
  /color-all [c] set accent color for all sessions (no color = clear all)
566
570
  /quiet-hours [H-H] suppress watchdog+burn alerts during hours (e.g. 22-06; no arg = clear)
567
571
  /quiet-status show whether quiet hours are currently active
572
+ /budget [N] [$] set cost budget: /budget 1 2.50 (session), /budget 2.50 (global), /budget clear
573
+ /pause-all send interrupt to all sessions
574
+ /resume-all send resume to all sessions
568
575
  /alert-log [N] show last N auto-generated alerts (burn-rate/watchdog/ceiling; default 20)
569
576
  /history-stats show aggregate statistics from persisted activity history
570
577
  /cost-summary show total estimated spend across all sessions
@@ -1121,6 +1128,46 @@ ${BOLD}other:${RESET}
1121
1128
  console.error(`${DIM}history-stats not available (no TUI)${RESET}`);
1122
1129
  }
1123
1130
  break;
1131
+ case "/budget": {
1132
+ const bArgs = line.slice("/budget".length).trim().split(/\s+/).filter(Boolean);
1133
+ if (bArgs.length === 0 || bArgs[0] === "clear") {
1134
+ // clear global budget
1135
+ if (this.budgetHandler)
1136
+ this.budgetHandler(null, null);
1137
+ else
1138
+ console.error(`${DIM}budget not available${RESET}`);
1139
+ break;
1140
+ }
1141
+ if (this.budgetHandler) {
1142
+ // /budget <$N> → global, /budget <N|name> <$N> → per-session
1143
+ const maybeUSD = parseFloat(bArgs[bArgs.length - 1].replace("$", ""));
1144
+ if (!isNaN(maybeUSD) && bArgs.length === 1) {
1145
+ this.budgetHandler(null, maybeUSD); // global
1146
+ }
1147
+ else if (!isNaN(maybeUSD) && bArgs.length >= 2) {
1148
+ this.budgetHandler(bArgs[0], maybeUSD); // per-session
1149
+ }
1150
+ else {
1151
+ console.error(`${DIM}usage: /budget [$N.NN] — global, /budget <N|name> $N.NN — per-session, /budget clear — remove${RESET}`);
1152
+ }
1153
+ }
1154
+ else {
1155
+ console.error(`${DIM}budget not available${RESET}`);
1156
+ }
1157
+ break;
1158
+ }
1159
+ case "/pause-all":
1160
+ if (this.bulkControlHandler)
1161
+ this.bulkControlHandler("pause");
1162
+ else
1163
+ console.error(`${DIM}pause-all not available${RESET}`);
1164
+ break;
1165
+ case "/resume-all":
1166
+ if (this.bulkControlHandler)
1167
+ this.bulkControlHandler("resume");
1168
+ else
1169
+ console.error(`${DIM}resume-all not available${RESET}`);
1170
+ break;
1124
1171
  case "/quiet-status":
1125
1172
  if (this.quietStatusHandler)
1126
1173
  this.quietStatusHandler();
package/dist/tui.d.ts CHANGED
@@ -180,6 +180,10 @@ export interface SessionReportData {
180
180
  }
181
181
  /** Format a session report as a Markdown document. */
182
182
  export declare function formatSessionReport(data: SessionReportData): string;
183
+ /** Check if a cost string exceeds a budget value. Returns true when over budget. */
184
+ export declare function isOverBudget(costStr: string | undefined, budgetUSD: number): boolean;
185
+ /** Format a budget-exceeded alert message. */
186
+ export declare function formatBudgetAlert(title: string, costStr: string, budgetUSD: number): string;
183
187
  /**
184
188
  * Build args for duplicating a session: given a source session, return
185
189
  * { path, tool, title } for use in a create_agent action.
@@ -408,6 +412,9 @@ export declare class TUI {
408
412
  private quietHoursRanges;
409
413
  private sessionHealthHistory;
410
414
  private alertLog;
415
+ private sessionBudgets;
416
+ private globalBudget;
417
+ private budgetAlerted;
411
418
  private viewMode;
412
419
  private drilldownSessionId;
413
420
  private sessionOutputs;
@@ -539,6 +546,16 @@ export declare class TUI {
539
546
  tool: string;
540
547
  title: string;
541
548
  } | null;
549
+ /** Set a per-session budget in USD. Pass null to clear. */
550
+ setSessionBudget(sessionIdOrIndex: string | number, budgetUSD: number | null): boolean;
551
+ /** Set global fallback budget (applies to all sessions without per-session budget). */
552
+ setGlobalBudget(budgetUSD: number | null): void;
553
+ /** Get the per-session budget (or null). */
554
+ getSessionBudget(id: string): number | null;
555
+ /** Get the global budget (or null if not set). */
556
+ getGlobalBudget(): number | null;
557
+ /** Return all per-session budgets. */
558
+ getAllSessionBudgets(): ReadonlyMap<string, number>;
542
559
  /** Return health history for a session (for sparkline). */
543
560
  getSessionHealthHistory(id: string): readonly HealthSnapshot[];
544
561
  /** Return all alert log entries (last 100 "status" tag entries). */
@@ -691,7 +708,7 @@ export declare class TUI {
691
708
  private repaintDrilldownContent;
692
709
  private paintInputLine;
693
710
  }
694
- declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string): string;
711
+ declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string): string;
695
712
  declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
696
713
  declare function padBoxLine(line: string, totalWidth: number): string;
697
714
  declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
package/dist/tui.js CHANGED
@@ -518,6 +518,18 @@ export function formatSessionReport(data) {
518
518
  }
519
519
  return lines.join("\n");
520
520
  }
521
+ // ── Session cost budget (pure, exported for testing) ─────────────────────────
522
+ /** Check if a cost string exceeds a budget value. Returns true when over budget. */
523
+ export function isOverBudget(costStr, budgetUSD) {
524
+ if (!costStr)
525
+ return false;
526
+ const val = parseCostValue(costStr);
527
+ return val !== null && val > budgetUSD;
528
+ }
529
+ /** Format a budget-exceeded alert message. */
530
+ export function formatBudgetAlert(title, costStr, budgetUSD) {
531
+ return `${title}: cost ${costStr} exceeded budget $${budgetUSD.toFixed(2)}`;
532
+ }
521
533
  // ── Duplicate session helpers (pure, exported for testing) ───────────────────
522
534
  /**
523
535
  * Build args for duplicating a session: given a source session, return
@@ -726,6 +738,7 @@ export const BUILTIN_COMMANDS = new Set([
726
738
  "/group", "/groups", "/group-filter", "/burn-rate", "/snapshot", "/broadcast", "/watchdog", "/top", "/ceiling", "/rename", "/copy", "/stats", "/recall", "/pin-all-errors", "/export-stats",
727
739
  "/mute-errors", "/prev-goal", "/tag", "/tags", "/tag-filter", "/find", "/reset-health", "/timeline", "/color", "/clear-history",
728
740
  "/duplicate", "/color-all", "/quiet-hours", "/quiet-status", "/history-stats", "/cost-summary", "/session-report", "/alert-log",
741
+ "/budget", "/pause-all", "/resume-all",
729
742
  ]);
730
743
  /** Resolve a slash command through the alias map. Returns the expanded command or the original. */
731
744
  export function resolveAlias(line, aliases) {
@@ -999,6 +1012,9 @@ export class TUI {
999
1012
  quietHoursRanges = []; // quiet-hour start/end pairs
1000
1013
  sessionHealthHistory = new Map(); // session ID → health snapshots
1001
1014
  alertLog = []; // recent auto-generated status alerts (ring buffer, max 100)
1015
+ sessionBudgets = new Map(); // session ID → USD budget
1016
+ globalBudget = null; // global fallback budget in USD
1017
+ budgetAlerted = new Map(); // session ID → epoch ms of last budget alert
1002
1018
  // drill-down mode: show a single session's full output
1003
1019
  viewMode = "overview";
1004
1020
  drilldownSessionId = null;
@@ -1471,6 +1487,42 @@ export class TUI {
1471
1487
  return buildDuplicateArgs(this.sessions, sessionIdOrIndex, newTitle);
1472
1488
  }
1473
1489
  // ── Session timeline ─────────────────────────────────────────────────────
1490
+ // ── Session cost budget ──────────────────────────────────────────────────
1491
+ /** Set a per-session budget in USD. Pass null to clear. */
1492
+ setSessionBudget(sessionIdOrIndex, budgetUSD) {
1493
+ let sessionId;
1494
+ if (typeof sessionIdOrIndex === "number") {
1495
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
1496
+ }
1497
+ else {
1498
+ const needle = sessionIdOrIndex.toLowerCase();
1499
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
1500
+ sessionId = match?.id;
1501
+ }
1502
+ if (!sessionId)
1503
+ return false;
1504
+ if (budgetUSD === null)
1505
+ this.sessionBudgets.delete(sessionId);
1506
+ else
1507
+ this.sessionBudgets.set(sessionId, budgetUSD);
1508
+ return true;
1509
+ }
1510
+ /** Set global fallback budget (applies to all sessions without per-session budget). */
1511
+ setGlobalBudget(budgetUSD) {
1512
+ this.globalBudget = budgetUSD;
1513
+ }
1514
+ /** Get the per-session budget (or null). */
1515
+ getSessionBudget(id) {
1516
+ return this.sessionBudgets.get(id) ?? null;
1517
+ }
1518
+ /** Get the global budget (or null if not set). */
1519
+ getGlobalBudget() {
1520
+ return this.globalBudget;
1521
+ }
1522
+ /** Return all per-session budgets. */
1523
+ getAllSessionBudgets() {
1524
+ return this.sessionBudgets;
1525
+ }
1474
1526
  /** Return health history for a session (for sparkline). */
1475
1527
  getSessionHealthHistory(id) {
1476
1528
  return this.sessionHealthHistory.get(id) ?? [];
@@ -1803,9 +1855,18 @@ export class TUI {
1803
1855
  }
1804
1856
  if (s.lastActivity !== undefined)
1805
1857
  this.prevLastActivity.set(s.id, s.lastActivity);
1806
- // track cost string
1807
- if (s.costStr)
1858
+ // track cost string + check budget
1859
+ if (s.costStr) {
1808
1860
  this.sessionCosts.set(s.id, s.costStr);
1861
+ const budget = this.sessionBudgets.get(s.id) ?? this.globalBudget;
1862
+ if (budget !== null && isOverBudget(s.costStr, budget) && !quietNow) {
1863
+ const lastAlert = this.budgetAlerted.get(s.id) ?? 0;
1864
+ if (now - lastAlert >= 5 * 60_000) {
1865
+ this.budgetAlerted.set(s.id, now);
1866
+ this.log("status", formatBudgetAlert(s.title, s.costStr, budget), s.id);
1867
+ }
1868
+ }
1869
+ }
1809
1870
  // track context token history for burn-rate alerts
1810
1871
  const tokens = parseContextTokenNumber(s.contextTokens);
1811
1872
  if (tokens !== null) {
@@ -2394,7 +2455,8 @@ export class TUI {
2394
2455
  const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
2395
2456
  const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth;
2396
2457
  const cardWidth = innerWidth - 1 - iconsWidth;
2397
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge}${colorDot}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName)}`;
2458
+ const cardAge = s.createdAt ? formatSessionAge(s.createdAt, nowMs) : undefined;
2459
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge}${colorDot}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName, cardAge || undefined)}`;
2398
2460
  const padded = padBoxLineHover(line, this.cols, isHovered);
2399
2461
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
2400
2462
  }
@@ -2464,7 +2526,8 @@ export class TUI {
2464
2526
  const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
2465
2527
  const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth2 + colorDotWidth2;
2466
2528
  const cardWidth = innerWidth - 1 - iconsWidth;
2467
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge2}${colorDot2}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2)}`;
2529
+ const cardAge2 = s.createdAt ? formatSessionAge(s.createdAt, nowMs2) : undefined;
2530
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge2}${colorDot2}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2, cardAge2 || undefined)}`;
2468
2531
  const padded = padBoxLineHover(line, this.cols, isHovered);
2469
2532
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
2470
2533
  }
@@ -2582,7 +2645,8 @@ export class TUI {
2582
2645
  // idleSinceMs: optional ms since last activity change (shown when idle/stopped)
2583
2646
  // healthBadge: optional pre-formatted health score badge ("⬡83" colored)
2584
2647
  // displayName: optional custom name override (from /rename)
2585
- function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName) {
2648
+ // ageStr: optional session age string (from createdAt)
2649
+ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr) {
2586
2650
  const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
2587
2651
  const title = displayName ?? s.title;
2588
2652
  const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
@@ -2622,7 +2686,8 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge
2622
2686
  else {
2623
2687
  desc = `${SLATE}${s.status}${RESET}`;
2624
2688
  }
2625
- return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${sparkSuffix}`, maxWidth);
2689
+ const ageSuffix = ageStr ? ` ${DIM}age:${ageStr}${RESET}` : "";
2690
+ return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${sparkSuffix}`, maxWidth);
2626
2691
  }
2627
2692
  // colorize an activity entry based on its tag
2628
2693
  function formatActivity(entry, maxCols) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.146.0",
3
+ "version": "0.149.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",