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 +29 -0
- package/dist/index.js +25 -0
- package/dist/notify.d.ts +13 -0
- package/dist/notify.js +104 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
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;
|
package/dist/notify.d.ts
ADDED
|
@@ -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;
|