aoaoe 0.51.0 → 0.52.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.js CHANGED
@@ -91,6 +91,7 @@ const KNOWN_KEYS = {
91
91
  "maxIdleBeforeNudgeMs", "maxErrorsBeforeRestart", "autoAnswerPermissions",
92
92
  "actionCooldownMs", "userActivityThresholdMs", "allowDestructive",
93
93
  ]),
94
+ notifications: new Set(["webhookUrl", "slackWebhookUrl", "events"]),
94
95
  };
95
96
  export function warnUnknownKeys(raw, source) {
96
97
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -182,6 +183,34 @@ export function validateConfig(config) {
182
183
  if (config.policies?.allowDestructive !== undefined && typeof config.policies.allowDestructive !== "boolean") {
183
184
  errors.push(`policies.allowDestructive must be a boolean, got ${typeof config.policies.allowDestructive}`);
184
185
  }
186
+ // notifications.webhookUrl must be a string starting with http:// or https://
187
+ if (config.notifications?.webhookUrl !== undefined) {
188
+ const u = config.notifications.webhookUrl;
189
+ if (typeof u !== "string" || (!u.startsWith("http://") && !u.startsWith("https://"))) {
190
+ errors.push(`notifications.webhookUrl must be a URL starting with http:// or https://, got ${JSON.stringify(u)}`);
191
+ }
192
+ }
193
+ // notifications.slackWebhookUrl must be a string starting with http:// or https://
194
+ if (config.notifications?.slackWebhookUrl !== undefined) {
195
+ const u = config.notifications.slackWebhookUrl;
196
+ if (typeof u !== "string" || (!u.startsWith("http://") && !u.startsWith("https://"))) {
197
+ errors.push(`notifications.slackWebhookUrl must be a URL starting with http:// or https://, got ${JSON.stringify(u)}`);
198
+ }
199
+ }
200
+ // notifications.events must be an array of valid NotificationEvent values
201
+ if (config.notifications?.events !== undefined) {
202
+ const VALID_EVENTS = new Set(["session_error", "session_done", "action_executed", "action_failed", "daemon_started", "daemon_stopped"]);
203
+ if (!Array.isArray(config.notifications.events)) {
204
+ errors.push(`notifications.events must be an array, got ${typeof config.notifications.events}`);
205
+ }
206
+ else {
207
+ for (const e of config.notifications.events) {
208
+ if (!VALID_EVENTS.has(e)) {
209
+ errors.push(`notifications.events contains invalid event "${e}"`);
210
+ }
211
+ }
212
+ }
213
+ }
185
214
  if (errors.length > 0) {
186
215
  throw new Error(`invalid config:\n ${errors.join("\n ")}`);
187
216
  }
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from
17
17
  import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
18
18
  import { TUI } from "./tui.js";
19
19
  import { isDaemonRunningFromState } from "./chat.js";
20
+ import { sendNotification } from "./notify.js";
20
21
  import { actionSession, actionDetail } from "./types.js";
21
22
  import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
22
23
  import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
@@ -284,6 +285,8 @@ async function main() {
284
285
  console.error(` mode: dry-run (no execution)`);
285
286
  console.error("");
286
287
  log("shutting down...");
288
+ // notify: daemon stopped (fire-and-forget, don't block shutdown)
289
+ sendNotification(config, { event: "daemon_stopped", timestamp: Date.now(), detail: `polls: ${totalPolls}, actions: ${totalActionsExecuted}` });
287
290
  input.stop();
288
291
  Promise.resolve()
289
292
  .then(() => reasonerConsole.stop())
@@ -307,6 +310,8 @@ async function main() {
307
310
  else {
308
311
  log("entering main loop (Ctrl+C to stop)\n");
309
312
  }
313
+ // notify: daemon started
314
+ sendNotification(config, { event: "daemon_started", timestamp: Date.now(), detail: `reasoner: ${config.reasoner}` });
310
315
  // clear any stale interrupt from a previous run
311
316
  clearInterrupt();
312
317
  // auto-explain: on the very first tick with sessions, inject an explain prompt
@@ -680,6 +685,19 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
680
685
  }
681
686
  }
682
687
  }
688
+ // notify: session error/done events (fires for both TUI and non-TUI modes)
689
+ {
690
+ const changedSet = new Set(observation.changes.map((c) => c.title));
691
+ for (const snap of observation.sessions) {
692
+ const s = snap.session;
693
+ if (s.status === "error" && changedSet.has(s.title)) {
694
+ sendNotification(config, { event: "session_error", timestamp: Date.now(), session: s.title, detail: `status: ${s.status}` });
695
+ }
696
+ if (s.status === "done" && changedSet.has(s.title)) {
697
+ sendNotification(config, { event: "session_done", timestamp: Date.now(), session: s.title });
698
+ }
699
+ }
700
+ }
683
701
  if (skippedReason === "no changes") {
684
702
  if (config.verbose) {
685
703
  if (tui)
@@ -750,6 +768,13 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
750
768
  log(`[${icon}] ${displayText}`);
751
769
  }
752
770
  reasonerConsole.writeAction(entry.action.action, richDetail, entry.success);
771
+ // notify: action executed or failed
772
+ sendNotification(config, {
773
+ event: entry.success ? "action_executed" : "action_failed",
774
+ timestamp: Date.now(),
775
+ session: sessionTitle,
776
+ detail: `${entry.action.action}${actionText ? `: ${actionText.slice(0, 200)}` : ""}`,
777
+ });
753
778
  }
754
779
  const actionsOk = executed.filter((e) => e.success && e.action.action !== "wait").length;
755
780
  const actionsFail = executed.filter((e) => !e.success && e.action.action !== "wait").length;
@@ -0,0 +1,13 @@
1
+ import type { AoaoeConfig, NotificationEvent } from "./types.js";
2
+ export interface NotificationPayload {
3
+ event: NotificationEvent;
4
+ timestamp: number;
5
+ session?: string;
6
+ detail?: string;
7
+ }
8
+ export declare function sendNotification(config: AoaoeConfig, payload: NotificationPayload): Promise<void>;
9
+ export declare function formatSlackPayload(payload: NotificationPayload): {
10
+ text: string;
11
+ blocks: object[];
12
+ };
13
+ //# sourceMappingURL=notify.d.ts.map
package/dist/notify.js ADDED
@@ -0,0 +1,104 @@
1
+ // send a notification to all configured webhooks.
2
+ // fire-and-forget: never throws, never blocks the daemon.
3
+ export async function sendNotification(config, payload) {
4
+ const n = config.notifications;
5
+ if (!n)
6
+ return;
7
+ // filter: only send if event is in the configured list (or no filter = send all)
8
+ if (n.events && n.events.length > 0 && !n.events.includes(payload.event))
9
+ return;
10
+ const promises = [];
11
+ if (n.webhookUrl) {
12
+ promises.push(sendGenericWebhook(n.webhookUrl, payload));
13
+ }
14
+ if (n.slackWebhookUrl) {
15
+ promises.push(sendSlackWebhook(n.slackWebhookUrl, payload));
16
+ }
17
+ // fire-and-forget — swallow all errors so the daemon never crashes on notification failure
18
+ await Promise.allSettled(promises);
19
+ }
20
+ // POST JSON payload to a generic webhook URL
21
+ async function sendGenericWebhook(url, payload) {
22
+ try {
23
+ await fetch(url, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({
27
+ event: payload.event,
28
+ timestamp: payload.timestamp,
29
+ session: payload.session,
30
+ detail: payload.detail,
31
+ }),
32
+ signal: AbortSignal.timeout(5000),
33
+ });
34
+ }
35
+ catch (err) {
36
+ console.error(`[notify] generic webhook failed: ${err}`);
37
+ }
38
+ }
39
+ // POST Slack block format to a Slack incoming webhook URL
40
+ async function sendSlackWebhook(url, payload) {
41
+ try {
42
+ const body = formatSlackPayload(payload);
43
+ await fetch(url, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify(body),
47
+ signal: AbortSignal.timeout(5000),
48
+ });
49
+ }
50
+ catch (err) {
51
+ console.error(`[notify] slack webhook failed: ${err}`);
52
+ }
53
+ }
54
+ // format a notification payload into Slack block kit format.
55
+ // exported for testing.
56
+ export function formatSlackPayload(payload) {
57
+ const icon = eventIcon(payload.event);
58
+ const title = eventTitle(payload.event);
59
+ const fallbackText = payload.session
60
+ ? `${icon} ${title}: ${payload.session}${payload.detail ? ` — ${payload.detail}` : ""}`
61
+ : `${icon} ${title}${payload.detail ? ` — ${payload.detail}` : ""}`;
62
+ const blocks = [
63
+ {
64
+ type: "section",
65
+ text: {
66
+ type: "mrkdwn",
67
+ text: `*${icon} ${title}*${payload.session ? `\n*Session:* ${payload.session}` : ""}${payload.detail ? `\n${payload.detail}` : ""}`,
68
+ },
69
+ },
70
+ {
71
+ type: "context",
72
+ elements: [
73
+ {
74
+ type: "mrkdwn",
75
+ text: `aoaoe | ${new Date(payload.timestamp).toISOString()}`,
76
+ },
77
+ ],
78
+ },
79
+ ];
80
+ return { text: fallbackText, blocks };
81
+ }
82
+ // human-readable title for each event type
83
+ function eventTitle(event) {
84
+ switch (event) {
85
+ case "session_error": return "Session Error";
86
+ case "session_done": return "Session Done";
87
+ case "action_executed": return "Action Executed";
88
+ case "action_failed": return "Action Failed";
89
+ case "daemon_started": return "Daemon Started";
90
+ case "daemon_stopped": return "Daemon Stopped";
91
+ }
92
+ }
93
+ // emoji icon for each event type (used in Slack messages)
94
+ function eventIcon(event) {
95
+ switch (event) {
96
+ case "session_error": return "\u{1F6A8}"; // 🚨
97
+ case "session_done": return "\u2705"; // ✅
98
+ case "action_executed": return "\u2699\uFE0F"; // ⚙️
99
+ case "action_failed": return "\u274C"; // ❌
100
+ case "daemon_started": return "\u{1F680}"; // 🚀
101
+ case "daemon_stopped": return "\u{1F6D1}"; // 🛑
102
+ }
103
+ }
104
+ //# sourceMappingURL=notify.js.map
package/dist/types.d.ts CHANGED
@@ -115,7 +115,13 @@ export interface AoaoeConfig {
115
115
  dryRun: boolean;
116
116
  observe: boolean;
117
117
  confirm: boolean;
118
+ notifications?: {
119
+ webhookUrl?: string;
120
+ slackWebhookUrl?: string;
121
+ events?: NotificationEvent[];
122
+ };
118
123
  }
124
+ export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped";
119
125
  export type DaemonPhase = "sleeping" | "polling" | "reasoning" | "executing" | "interrupted";
120
126
  export interface DaemonSessionState {
121
127
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.51.0",
3
+ "version": "0.52.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",