aoaoe 0.52.0 → 0.54.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 +33 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +18 -3
- package/dist/index.js +180 -9
- package/dist/init.js +1 -0
- package/dist/notify.d.ts +8 -0
- package/dist/notify.js +87 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -238,6 +238,10 @@ aoaoe [command] [options]
|
|
|
238
238
|
commands:
|
|
239
239
|
(none) start the supervisor daemon (interactive TUI)
|
|
240
240
|
init detect tools + sessions, import history, generate config
|
|
241
|
+
status quick daemon health check (is it running? what's it doing?)
|
|
242
|
+
config show the effective resolved config (defaults + file)
|
|
243
|
+
config --validate validate config + check tool availability
|
|
244
|
+
notify-test send a test notification to configured webhooks
|
|
241
245
|
task manage tasks and sessions (list, start, stop, new, rm, edit)
|
|
242
246
|
tasks show task progress (from aoaoe.tasks.json)
|
|
243
247
|
history review recent actions (from ~/.aoaoe/actions.log)
|
|
@@ -297,7 +301,12 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
|
|
|
297
301
|
"adventure": "github/adventure",
|
|
298
302
|
"cloudchamber": "cc/cloudchamber"
|
|
299
303
|
},
|
|
300
|
-
"contextFiles": []
|
|
304
|
+
"contextFiles": [],
|
|
305
|
+
"notifications": {
|
|
306
|
+
"webhookUrl": "https://example.com/webhook",
|
|
307
|
+
"slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
308
|
+
"events": ["session_error", "session_done", "daemon_started", "daemon_stopped"]
|
|
309
|
+
}
|
|
301
310
|
}
|
|
302
311
|
```
|
|
303
312
|
|
|
@@ -321,6 +330,9 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
|
|
|
321
330
|
| `sessionDirs` | Map session titles to project directories (relative to cwd or absolute). Bypasses heuristic directory search. | `{}` |
|
|
322
331
|
| `contextFiles` | Extra AI instruction file paths to load from each project root | `[]` |
|
|
323
332
|
| `captureLinesCount` | Number of tmux lines to capture per session (`-S` flag) | `100` |
|
|
333
|
+
| `notifications.webhookUrl` | Generic webhook URL (POST JSON) | (none) |
|
|
334
|
+
| `notifications.slackWebhookUrl` | Slack incoming webhook URL (block kit format) | (none) |
|
|
335
|
+
| `notifications.events` | Filter which events fire (omit to send all). Valid: `session_error`, `session_done`, `action_executed`, `action_failed`, `daemon_started`, `daemon_stopped` | (all) |
|
|
324
336
|
|
|
325
337
|
Also reads `.aoaoe.json` as an alternative config filename.
|
|
326
338
|
|
|
@@ -342,7 +354,25 @@ For non-standard layouts or when the session title doesn't match the directory n
|
|
|
342
354
|
|
|
343
355
|
Paths can be relative (resolved from the directory where you run `aoaoe`) or absolute. Case-insensitive matching is used for session title lookup. If a mapped path doesn't exist on disk, aoaoe falls back to heuristic search.
|
|
344
356
|
|
|
345
|
-
Use `aoaoe test-context` to verify resolution
|
|
357
|
+
Use `aoaoe test-context` to verify resolution.
|
|
358
|
+
|
|
359
|
+
### `notifications` — webhook alerts for daemon events
|
|
360
|
+
|
|
361
|
+
aoaoe can send webhook notifications when significant events occur (session errors, task completions, daemon start/stop). Supports generic JSON webhooks and Slack incoming webhooks with block kit formatting.
|
|
362
|
+
|
|
363
|
+
```json
|
|
364
|
+
{
|
|
365
|
+
"notifications": {
|
|
366
|
+
"webhookUrl": "https://example.com/webhook",
|
|
367
|
+
"slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
368
|
+
"events": ["session_error", "session_done", "daemon_started", "daemon_stopped"]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Both webhook URLs are optional — configure one or both. The `events` array filters which event types fire (omit it to receive all events). Notifications are fire-and-forget with a 5s timeout and 60s rate limiting per event+session combo to prevent spam.
|
|
374
|
+
|
|
375
|
+
Run `aoaoe notify-test` to verify your webhook configuration.
|
|
346
376
|
|
|
347
377
|
## How It Works
|
|
348
378
|
|
|
@@ -460,6 +490,7 @@ src/
|
|
|
460
490
|
tui.ts # in-place terminal UI (alternate screen, scroll regions)
|
|
461
491
|
input.ts # stdin readline listener with inject() for post-interrupt
|
|
462
492
|
init.ts # `aoaoe init`: auto-discover tools, sessions, generate config
|
|
493
|
+
notify.ts # webhook + Slack notification dispatcher for daemon events
|
|
463
494
|
colors.ts # shared ANSI color/style constants
|
|
464
495
|
context.ts # discoverContextFiles, resolveProjectDir, loadSessionContext
|
|
465
496
|
activity.ts # detect human keystrokes in tmux sessions
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -274,7 +274,7 @@ export function parseCliArgs(argv) {
|
|
|
274
274
|
let initForce = false;
|
|
275
275
|
let runTaskCli = false;
|
|
276
276
|
let registerTitle;
|
|
277
|
-
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, runInit: false, initForce: false, runTaskCli: false };
|
|
277
|
+
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, notifyTest: false, runInit: false, initForce: false, runTaskCli: false };
|
|
278
278
|
// check for subcommand as first non-flag arg
|
|
279
279
|
if (argv[2] === "test-context") {
|
|
280
280
|
return { ...defaults, testContext: true };
|
|
@@ -295,7 +295,11 @@ export function parseCliArgs(argv) {
|
|
|
295
295
|
return { ...defaults, showStatus: true };
|
|
296
296
|
}
|
|
297
297
|
if (argv[2] === "config") {
|
|
298
|
-
|
|
298
|
+
const validate = argv.includes("--validate") || argv.includes("-V");
|
|
299
|
+
return { ...defaults, showConfig: true, configValidate: validate };
|
|
300
|
+
}
|
|
301
|
+
if (argv[2] === "notify-test") {
|
|
302
|
+
return { ...defaults, notifyTest: true };
|
|
299
303
|
}
|
|
300
304
|
if (argv[2] === "init") {
|
|
301
305
|
const force = argv.includes("--force") || argv.includes("-f");
|
|
@@ -386,7 +390,7 @@ export function parseCliArgs(argv) {
|
|
|
386
390
|
break;
|
|
387
391
|
}
|
|
388
392
|
}
|
|
389
|
-
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, runInit: false, initForce: false, runTaskCli: false };
|
|
393
|
+
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, notifyTest: false, runInit: false, initForce: false, runTaskCli: false };
|
|
390
394
|
}
|
|
391
395
|
export function printHelp() {
|
|
392
396
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -404,6 +408,8 @@ commands:
|
|
|
404
408
|
(none) start the supervisor daemon (interactive TUI)
|
|
405
409
|
status quick daemon health check (is it running? what's it doing?)
|
|
406
410
|
config show the effective resolved config (defaults + file)
|
|
411
|
+
config --validate validate config file + check tool availability
|
|
412
|
+
notify-test send a test notification to configured webhooks
|
|
407
413
|
task manage tasks and sessions (list, start, stop, new, rm, edit)
|
|
408
414
|
tasks show task progress (from aoaoe.tasks.json)
|
|
409
415
|
history review recent actions (from ~/.aoaoe/actions.log)
|
|
@@ -446,6 +452,11 @@ example config:
|
|
|
446
452
|
"sessionDirs": {
|
|
447
453
|
"my-project": "/path/to/my-project",
|
|
448
454
|
"other-repo": "/path/to/other-repo"
|
|
455
|
+
},
|
|
456
|
+
"notifications": {
|
|
457
|
+
"webhookUrl": "https://example.com/webhook",
|
|
458
|
+
"slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
459
|
+
"events": ["session_error", "session_done", "daemon_started", "daemon_stopped"]
|
|
449
460
|
}
|
|
450
461
|
}
|
|
451
462
|
|
|
@@ -453,6 +464,10 @@ example config:
|
|
|
453
464
|
aoaoe loads AGENTS.md, claude.md, and other AI instruction files
|
|
454
465
|
from each project directory to give the reasoner per-session context.
|
|
455
466
|
|
|
467
|
+
notifications sends webhook alerts for daemon events. Both webhookUrl
|
|
468
|
+
and slackWebhookUrl are optional. events filters which events fire
|
|
469
|
+
(omit to send all). Run 'aoaoe notify-test' to verify delivery.
|
|
470
|
+
|
|
456
471
|
interactive commands (while daemon is running):
|
|
457
472
|
/help show available commands
|
|
458
473
|
/explain ask the AI to explain what's happening in plain English
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,8 @@ 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";
|
|
21
|
-
import { actionSession, actionDetail } from "./types.js";
|
|
20
|
+
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
21
|
+
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
22
22
|
import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
|
|
23
23
|
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
24
24
|
import { resolve, dirname, join } from "node:path";
|
|
@@ -28,7 +28,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
28
28
|
const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
|
|
29
29
|
const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
|
|
30
30
|
async function main() {
|
|
31
|
-
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
|
|
31
|
+
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, notifyTest, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
|
|
32
32
|
if (help) {
|
|
33
33
|
printHelp();
|
|
34
34
|
process.exit(0);
|
|
@@ -73,9 +73,19 @@ async function main() {
|
|
|
73
73
|
showDaemonStatus();
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
// `aoaoe config` -- show effective resolved config
|
|
76
|
+
// `aoaoe config` -- show effective resolved config (with optional --validate)
|
|
77
77
|
if (showConfig) {
|
|
78
|
-
|
|
78
|
+
if (configValidate) {
|
|
79
|
+
await runConfigValidation();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
showEffectiveConfig();
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// `aoaoe notify-test` -- send a test notification to configured webhooks
|
|
87
|
+
if (notifyTest) {
|
|
88
|
+
await runNotifyTest();
|
|
79
89
|
return;
|
|
80
90
|
}
|
|
81
91
|
// `aoaoe task` -- task management CLI
|
|
@@ -1041,6 +1051,43 @@ async function registerAsAoeSession(title) {
|
|
|
1041
1051
|
console.log();
|
|
1042
1052
|
console.log(`or start + enter immediately: aoe session start ${sessionTitle} && aoe`);
|
|
1043
1053
|
}
|
|
1054
|
+
// `aoaoe notify-test` -- send a test notification to all configured webhooks and report results
|
|
1055
|
+
async function runNotifyTest() {
|
|
1056
|
+
const config = loadConfig();
|
|
1057
|
+
if (!config.notifications) {
|
|
1058
|
+
console.log("");
|
|
1059
|
+
console.log(" no notifications configured.");
|
|
1060
|
+
console.log("");
|
|
1061
|
+
console.log(" add to your config (~/.aoaoe/aoaoe.config.json):");
|
|
1062
|
+
console.log(' "notifications": {');
|
|
1063
|
+
console.log(' "webhookUrl": "https://example.com/webhook",');
|
|
1064
|
+
console.log(' "slackWebhookUrl": "https://hooks.slack.com/services/...",');
|
|
1065
|
+
console.log(' "events": ["session_error", "session_done", "daemon_started", "daemon_stopped"]');
|
|
1066
|
+
console.log(" }");
|
|
1067
|
+
console.log("");
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (!config.notifications.webhookUrl && !config.notifications.slackWebhookUrl) {
|
|
1071
|
+
console.log("");
|
|
1072
|
+
console.log(" notifications block exists but no webhook URLs configured.");
|
|
1073
|
+
console.log(" add webhookUrl and/or slackWebhookUrl to your notifications config.");
|
|
1074
|
+
console.log("");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
console.log("");
|
|
1078
|
+
console.log(" sending test notification...");
|
|
1079
|
+
const result = await sendTestNotification(config);
|
|
1080
|
+
console.log("");
|
|
1081
|
+
if (result.webhookOk !== undefined) {
|
|
1082
|
+
const icon = result.webhookOk ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
1083
|
+
console.log(` ${icon} generic webhook: ${result.webhookOk ? "ok" : result.webhookError ?? "failed"}`);
|
|
1084
|
+
}
|
|
1085
|
+
if (result.slackOk !== undefined) {
|
|
1086
|
+
const icon = result.slackOk ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
1087
|
+
console.log(` ${icon} slack webhook: ${result.slackOk ? "ok" : result.slackError ?? "failed"}`);
|
|
1088
|
+
}
|
|
1089
|
+
console.log("");
|
|
1090
|
+
}
|
|
1044
1091
|
function readPkgVersion() {
|
|
1045
1092
|
try {
|
|
1046
1093
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -1094,7 +1141,9 @@ async function showActionHistory() {
|
|
|
1094
1141
|
console.log(` ${"─".repeat(70)}`);
|
|
1095
1142
|
for (const line of recent) {
|
|
1096
1143
|
try {
|
|
1097
|
-
const entry = JSON.parse(line);
|
|
1144
|
+
const entry = toActionLogEntry(JSON.parse(line));
|
|
1145
|
+
if (!entry)
|
|
1146
|
+
continue; // skip malformed lines
|
|
1098
1147
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
1099
1148
|
const date = new Date(entry.timestamp).toLocaleDateString();
|
|
1100
1149
|
const icon = entry.success ? `${GREEN}+${RESET}` : `${RED}!${RESET}`;
|
|
@@ -1104,7 +1153,7 @@ async function showActionHistory() {
|
|
|
1104
1153
|
console.log(` ${icon} ${DIM}${date} ${time}${RESET} ${YELLOW}${actionName.padEnd(16)}${RESET} ${session.padEnd(10)} ${detail}`);
|
|
1105
1154
|
}
|
|
1106
1155
|
catch {
|
|
1107
|
-
// skip
|
|
1156
|
+
// skip unparseable JSON lines
|
|
1108
1157
|
}
|
|
1109
1158
|
}
|
|
1110
1159
|
console.log(` ${"─".repeat(70)}`);
|
|
@@ -1113,14 +1162,18 @@ async function showActionHistory() {
|
|
|
1113
1162
|
const actionCounts = new Map();
|
|
1114
1163
|
for (const line of lines) {
|
|
1115
1164
|
try {
|
|
1116
|
-
const e = JSON.parse(line);
|
|
1165
|
+
const e = toActionLogEntry(JSON.parse(line));
|
|
1166
|
+
if (!e)
|
|
1167
|
+
continue;
|
|
1117
1168
|
if (e.success)
|
|
1118
1169
|
successes++;
|
|
1119
1170
|
else
|
|
1120
1171
|
failures++;
|
|
1121
1172
|
actionCounts.set(e.action.action, (actionCounts.get(e.action.action) ?? 0) + 1);
|
|
1122
1173
|
}
|
|
1123
|
-
catch {
|
|
1174
|
+
catch {
|
|
1175
|
+
// skip unparseable JSON lines
|
|
1176
|
+
}
|
|
1124
1177
|
}
|
|
1125
1178
|
const breakdown = [...actionCounts.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
1126
1179
|
console.log(` total: ${lines.length} actions (${GREEN}${successes} ok${RESET}, ${RED}${failures} failed${RESET})`);
|
|
@@ -1192,6 +1245,124 @@ function showDaemonStatus() {
|
|
|
1192
1245
|
}
|
|
1193
1246
|
console.log("");
|
|
1194
1247
|
}
|
|
1248
|
+
// `aoaoe config --validate` -- validate config file, field values, and tool availability
|
|
1249
|
+
async function runConfigValidation() {
|
|
1250
|
+
const configPath = findConfigFile();
|
|
1251
|
+
let checks = 0;
|
|
1252
|
+
let passed = 0;
|
|
1253
|
+
let warnings = 0;
|
|
1254
|
+
console.log("");
|
|
1255
|
+
console.log(" aoaoe — config validation");
|
|
1256
|
+
console.log(` ${"─".repeat(50)}`);
|
|
1257
|
+
// 1. config file exists
|
|
1258
|
+
checks++;
|
|
1259
|
+
if (configPath) {
|
|
1260
|
+
console.log(` ${GREEN}✓${RESET} config file found: ${configPath}`);
|
|
1261
|
+
passed++;
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
console.log(` ${YELLOW}!${RESET} no config file found (using defaults)`);
|
|
1265
|
+
console.log(` ${DIM}run 'aoaoe init' to create one${RESET}`);
|
|
1266
|
+
warnings++;
|
|
1267
|
+
}
|
|
1268
|
+
// 2. config parses + validates
|
|
1269
|
+
checks++;
|
|
1270
|
+
let config;
|
|
1271
|
+
try {
|
|
1272
|
+
const configResult = loadConfig();
|
|
1273
|
+
config = configResult;
|
|
1274
|
+
console.log(` ${GREEN}✓${RESET} config valid (all field values OK)`);
|
|
1275
|
+
passed++;
|
|
1276
|
+
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1279
|
+
console.log(` ${RED}✗${RESET} config validation failed:`);
|
|
1280
|
+
for (const line of msg.split("\n")) {
|
|
1281
|
+
console.log(` ${line}`);
|
|
1282
|
+
}
|
|
1283
|
+
console.log("");
|
|
1284
|
+
console.log(` ${passed}/${checks} checks passed, fix config errors and retry`);
|
|
1285
|
+
console.log("");
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
return; // unreachable, but satisfies TypeScript
|
|
1288
|
+
}
|
|
1289
|
+
// 3. required tools on PATH
|
|
1290
|
+
const tools = [
|
|
1291
|
+
{ name: "aoe", label: "agent-of-empires CLI" },
|
|
1292
|
+
{ name: "tmux", label: "terminal multiplexer" },
|
|
1293
|
+
];
|
|
1294
|
+
if (config.reasoner === "opencode") {
|
|
1295
|
+
tools.push({ name: "opencode", label: "OpenCode CLI" });
|
|
1296
|
+
}
|
|
1297
|
+
else if (config.reasoner === "claude-code") {
|
|
1298
|
+
tools.push({ name: "claude", label: "Claude Code CLI" });
|
|
1299
|
+
}
|
|
1300
|
+
for (const tool of tools) {
|
|
1301
|
+
checks++;
|
|
1302
|
+
try {
|
|
1303
|
+
const { execFile: execFileCb } = await import("node:child_process");
|
|
1304
|
+
const { promisify } = await import("node:util");
|
|
1305
|
+
const execFileAsync = promisify(execFileCb);
|
|
1306
|
+
await execFileAsync("which", [tool.name]);
|
|
1307
|
+
console.log(` ${GREEN}✓${RESET} ${tool.name} found on PATH (${tool.label})`);
|
|
1308
|
+
passed++;
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
console.log(` ${RED}✗${RESET} ${tool.name} not found on PATH (${tool.label})`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// 4. notifications config check
|
|
1315
|
+
checks++;
|
|
1316
|
+
if (config.notifications) {
|
|
1317
|
+
const hasWebhook = !!config.notifications.webhookUrl;
|
|
1318
|
+
const hasSlack = !!config.notifications.slackWebhookUrl;
|
|
1319
|
+
if (hasWebhook || hasSlack) {
|
|
1320
|
+
const targets = [hasWebhook && "webhook", hasSlack && "Slack"].filter(Boolean).join(" + ");
|
|
1321
|
+
console.log(` ${GREEN}✓${RESET} notifications configured (${targets})`);
|
|
1322
|
+
console.log(` ${DIM}run 'aoaoe notify-test' to verify delivery${RESET}`);
|
|
1323
|
+
passed++;
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
console.log(` ${YELLOW}!${RESET} notifications block exists but no webhook URLs configured`);
|
|
1327
|
+
warnings++;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
console.log(` ${DIM}○${RESET} notifications not configured (optional)`);
|
|
1332
|
+
passed++; // not configured is fine — it's optional
|
|
1333
|
+
}
|
|
1334
|
+
// 5. sessionDirs validation — check that mapped dirs exist
|
|
1335
|
+
if (config.sessionDirs && Object.keys(config.sessionDirs).length > 0) {
|
|
1336
|
+
const basePath = process.cwd();
|
|
1337
|
+
for (const [title, dir] of Object.entries(config.sessionDirs)) {
|
|
1338
|
+
checks++;
|
|
1339
|
+
const resolved = dir.startsWith("/") ? dir : resolve(basePath, dir);
|
|
1340
|
+
if (existsSync(resolved)) {
|
|
1341
|
+
console.log(` ${GREEN}✓${RESET} sessionDirs.${title} → ${resolved}`);
|
|
1342
|
+
passed++;
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
console.log(` ${YELLOW}!${RESET} sessionDirs.${title} → ${resolved} (not found)`);
|
|
1346
|
+
warnings++;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// summary
|
|
1351
|
+
const failed = checks - passed - warnings;
|
|
1352
|
+
console.log("");
|
|
1353
|
+
if (failed === 0 && warnings === 0) {
|
|
1354
|
+
console.log(` ${GREEN}${BOLD}all ${checks} checks passed${RESET}`);
|
|
1355
|
+
}
|
|
1356
|
+
else if (failed === 0) {
|
|
1357
|
+
console.log(` ${passed}/${checks} passed, ${YELLOW}${warnings} warning(s)${RESET}`);
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
console.log(` ${passed}/${checks} passed, ${RED}${failed} failed${RESET}${warnings > 0 ? `, ${YELLOW}${warnings} warning(s)${RESET}` : ""}`);
|
|
1361
|
+
}
|
|
1362
|
+
console.log("");
|
|
1363
|
+
if (failed > 0)
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1195
1366
|
// `aoaoe config` -- show the effective resolved config (defaults + file + any notes)
|
|
1196
1367
|
function showEffectiveConfig() {
|
|
1197
1368
|
const configPath = findConfigFile();
|
package/dist/init.js
CHANGED
|
@@ -291,6 +291,7 @@ export async function runInit(forceOverwrite = false) {
|
|
|
291
291
|
console.log(` ${CYAN}aoaoe${RESET}`);
|
|
292
292
|
}
|
|
293
293
|
console.log(`\n ${DIM}tip: run ${BOLD}aoaoe test-context${RESET}${DIM} to verify session discovery without starting the daemon${RESET}`);
|
|
294
|
+
console.log(` ${DIM}tip: add a "notifications" block to your config for webhook alerts (see ${BOLD}aoaoe --help${RESET}${DIM})${RESET}`);
|
|
294
295
|
console.log();
|
|
295
296
|
return { tools, sessions, reasoner, opencodePort, opencodeRunning, configPath, wrote: true };
|
|
296
297
|
}
|
package/dist/notify.d.ts
CHANGED
|
@@ -5,7 +5,15 @@ export interface NotificationPayload {
|
|
|
5
5
|
session?: string;
|
|
6
6
|
detail?: string;
|
|
7
7
|
}
|
|
8
|
+
export declare function isRateLimited(payload: NotificationPayload, now?: number): boolean;
|
|
9
|
+
export declare function resetRateLimiter(): void;
|
|
8
10
|
export declare function sendNotification(config: AoaoeConfig, payload: NotificationPayload): Promise<void>;
|
|
11
|
+
export declare function sendTestNotification(config: AoaoeConfig): Promise<{
|
|
12
|
+
webhookOk?: boolean;
|
|
13
|
+
slackOk?: boolean;
|
|
14
|
+
webhookError?: string;
|
|
15
|
+
slackError?: string;
|
|
16
|
+
}>;
|
|
9
17
|
export declare function formatSlackPayload(payload: NotificationPayload): {
|
|
10
18
|
text: string;
|
|
11
19
|
blocks: object[];
|
package/dist/notify.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
|
+
// ── rate limiting ───────────────────────────────────────────────────────────
|
|
2
|
+
// dedup key: "event:session" — prevents spam when sessions rapidly error/recover.
|
|
3
|
+
// default window: 60s per unique event+session combo.
|
|
4
|
+
const RATE_LIMIT_MS = 60_000;
|
|
5
|
+
const recentNotifications = new Map(); // key → last-sent timestamp
|
|
6
|
+
function rateLimitKey(payload) {
|
|
7
|
+
return `${payload.event}:${payload.session ?? ""}`;
|
|
8
|
+
}
|
|
9
|
+
// exported for testing
|
|
10
|
+
export function isRateLimited(payload, now) {
|
|
11
|
+
const key = rateLimitKey(payload);
|
|
12
|
+
const lastSent = recentNotifications.get(key);
|
|
13
|
+
const ts = now ?? Date.now();
|
|
14
|
+
if (lastSent !== undefined && ts - lastSent < RATE_LIMIT_MS)
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function recordSent(payload, now) {
|
|
19
|
+
const key = rateLimitKey(payload);
|
|
20
|
+
recentNotifications.set(key, now ?? Date.now());
|
|
21
|
+
// prune old entries to prevent unbounded growth (keep last 200)
|
|
22
|
+
if (recentNotifications.size > 200) {
|
|
23
|
+
const cutoff = (now ?? Date.now()) - RATE_LIMIT_MS;
|
|
24
|
+
for (const [k, v] of recentNotifications) {
|
|
25
|
+
if (v < cutoff)
|
|
26
|
+
recentNotifications.delete(k);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// exported for testing — reset rate limiter state between tests
|
|
31
|
+
export function resetRateLimiter() {
|
|
32
|
+
recentNotifications.clear();
|
|
33
|
+
}
|
|
1
34
|
// send a notification to all configured webhooks.
|
|
2
35
|
// fire-and-forget: never throws, never blocks the daemon.
|
|
36
|
+
// rate-limited: suppresses duplicate event+session combos within 60s.
|
|
3
37
|
export async function sendNotification(config, payload) {
|
|
4
38
|
const n = config.notifications;
|
|
5
39
|
if (!n)
|
|
@@ -7,6 +41,10 @@ export async function sendNotification(config, payload) {
|
|
|
7
41
|
// filter: only send if event is in the configured list (or no filter = send all)
|
|
8
42
|
if (n.events && n.events.length > 0 && !n.events.includes(payload.event))
|
|
9
43
|
return;
|
|
44
|
+
// rate limit: skip if we sent the same event+session recently
|
|
45
|
+
if (isRateLimited(payload))
|
|
46
|
+
return;
|
|
47
|
+
recordSent(payload);
|
|
10
48
|
const promises = [];
|
|
11
49
|
if (n.webhookUrl) {
|
|
12
50
|
promises.push(sendGenericWebhook(n.webhookUrl, payload));
|
|
@@ -17,6 +55,55 @@ export async function sendNotification(config, payload) {
|
|
|
17
55
|
// fire-and-forget — swallow all errors so the daemon never crashes on notification failure
|
|
18
56
|
await Promise.allSettled(promises);
|
|
19
57
|
}
|
|
58
|
+
// send a test notification and return whether delivery succeeded.
|
|
59
|
+
// unlike sendNotification, this is NOT fire-and-forget — it reports errors.
|
|
60
|
+
export async function sendTestNotification(config) {
|
|
61
|
+
const n = config.notifications;
|
|
62
|
+
if (!n)
|
|
63
|
+
return {};
|
|
64
|
+
const payload = {
|
|
65
|
+
event: "daemon_started",
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
detail: "test notification from aoaoe notify-test",
|
|
68
|
+
};
|
|
69
|
+
const result = {};
|
|
70
|
+
if (n.webhookUrl) {
|
|
71
|
+
try {
|
|
72
|
+
const resp = await fetch(n.webhookUrl, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ event: payload.event, timestamp: payload.timestamp, detail: payload.detail }),
|
|
76
|
+
signal: AbortSignal.timeout(10_000),
|
|
77
|
+
});
|
|
78
|
+
result.webhookOk = resp.ok;
|
|
79
|
+
if (!resp.ok)
|
|
80
|
+
result.webhookError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
result.webhookOk = false;
|
|
84
|
+
result.webhookError = String(err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (n.slackWebhookUrl) {
|
|
88
|
+
try {
|
|
89
|
+
const body = formatSlackPayload(payload);
|
|
90
|
+
const resp = await fetch(n.slackWebhookUrl, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
signal: AbortSignal.timeout(10_000),
|
|
95
|
+
});
|
|
96
|
+
result.slackOk = resp.ok;
|
|
97
|
+
if (!resp.ok)
|
|
98
|
+
result.slackError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
result.slackOk = false;
|
|
102
|
+
result.slackError = String(err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
20
107
|
// POST JSON payload to a generic webhook URL
|
|
21
108
|
async function sendGenericWebhook(url, payload) {
|
|
22
109
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -175,4 +175,16 @@ export declare function toAoeSessionList(raw: unknown): Array<{
|
|
|
175
175
|
title: string;
|
|
176
176
|
}>;
|
|
177
177
|
export declare function toReasonerBackend(raw: string): ReasonerBackend;
|
|
178
|
+
export interface ActionLogEntry {
|
|
179
|
+
timestamp: number;
|
|
180
|
+
action: {
|
|
181
|
+
action: string;
|
|
182
|
+
session?: string;
|
|
183
|
+
text?: string;
|
|
184
|
+
title?: string;
|
|
185
|
+
};
|
|
186
|
+
success: boolean;
|
|
187
|
+
detail: string;
|
|
188
|
+
}
|
|
189
|
+
export declare function toActionLogEntry(raw: unknown): ActionLogEntry | null;
|
|
178
190
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
CHANGED
|
@@ -112,4 +112,31 @@ export function toReasonerBackend(raw) {
|
|
|
112
112
|
return raw;
|
|
113
113
|
throw new Error(`--reasoner must be "opencode" or "claude-code", got "${raw}"`);
|
|
114
114
|
}
|
|
115
|
+
export function toActionLogEntry(raw) {
|
|
116
|
+
if (!raw || typeof raw !== "object")
|
|
117
|
+
return null;
|
|
118
|
+
const obj = raw;
|
|
119
|
+
if (typeof obj.timestamp !== "number")
|
|
120
|
+
return null;
|
|
121
|
+
if (typeof obj.success !== "boolean")
|
|
122
|
+
return null;
|
|
123
|
+
if (typeof obj.detail !== "string")
|
|
124
|
+
obj.detail = "";
|
|
125
|
+
if (!obj.action || typeof obj.action !== "object")
|
|
126
|
+
return null;
|
|
127
|
+
const action = obj.action;
|
|
128
|
+
if (typeof action.action !== "string")
|
|
129
|
+
return null;
|
|
130
|
+
return {
|
|
131
|
+
timestamp: obj.timestamp,
|
|
132
|
+
action: {
|
|
133
|
+
action: action.action,
|
|
134
|
+
session: typeof action.session === "string" ? action.session : undefined,
|
|
135
|
+
text: typeof action.text === "string" ? action.text : undefined,
|
|
136
|
+
title: typeof action.title === "string" ? action.title : undefined,
|
|
137
|
+
},
|
|
138
|
+
success: obj.success,
|
|
139
|
+
detail: typeof obj.detail === "string" ? obj.detail : "",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
115
142
|
//# sourceMappingURL=types.js.map
|