aoaoe 0.50.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/chat.js +10 -4
- package/dist/config.d.ts +2 -0
- package/dist/config.js +39 -2
- package/dist/console.js +5 -2
- package/dist/context.js +4 -2
- package/dist/executor.js +5 -2
- package/dist/index.js +113 -3
- package/dist/init.js +4 -2
- package/dist/notify.d.ts +13 -0
- package/dist/notify.js +104 -0
- package/dist/poller.js +7 -3
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/chat.js
CHANGED
|
@@ -53,8 +53,9 @@ function main() {
|
|
|
53
53
|
lastSize = currSize;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
catch {
|
|
56
|
+
catch (e) {
|
|
57
57
|
// file may be truncated or removed — reset so we pick up from start of new file
|
|
58
|
+
console.error(`[chat] conversation log read failed: ${e}`);
|
|
58
59
|
lastSize = 0;
|
|
59
60
|
}
|
|
60
61
|
};
|
|
@@ -341,7 +342,8 @@ async function captureTmuxPane(tmuxName) {
|
|
|
341
342
|
const result = await exec("tmux", ["capture-pane", "-t", tmuxName, "-p", "-S", "-100"], 5_000);
|
|
342
343
|
return result.exitCode === 0 ? result.stdout : null;
|
|
343
344
|
}
|
|
344
|
-
catch {
|
|
345
|
+
catch (e) {
|
|
346
|
+
console.error(`[chat] tmux capture-pane failed for ${tmuxName}: ${e}`);
|
|
345
347
|
return null;
|
|
346
348
|
}
|
|
347
349
|
}
|
|
@@ -437,7 +439,9 @@ function appendToInput(msg) {
|
|
|
437
439
|
try {
|
|
438
440
|
appendFileSync(INPUT_FILE, msg + "\n");
|
|
439
441
|
}
|
|
440
|
-
catch {
|
|
442
|
+
catch (e) {
|
|
443
|
+
console.error(`[chat] pending-input write failed: ${e}`);
|
|
444
|
+
}
|
|
441
445
|
}
|
|
442
446
|
function replayLog() {
|
|
443
447
|
if (!existsSync(CONVO_LOG))
|
|
@@ -449,7 +453,9 @@ function replayLog() {
|
|
|
449
453
|
console.log(`${DIM}--- end of history ---${RESET}\n`);
|
|
450
454
|
}
|
|
451
455
|
}
|
|
452
|
-
catch {
|
|
456
|
+
catch (e) {
|
|
457
|
+
console.error(`[chat] conversation log replay failed: ${e}`);
|
|
458
|
+
}
|
|
453
459
|
}
|
|
454
460
|
export function colorize(text) {
|
|
455
461
|
// first pass: colorize tick separator lines (──── tick #N ────)
|
package/dist/config.d.ts
CHANGED
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
|
}
|
|
@@ -245,7 +274,7 @@ export function parseCliArgs(argv) {
|
|
|
245
274
|
let initForce = false;
|
|
246
275
|
let runTaskCli = false;
|
|
247
276
|
let registerTitle;
|
|
248
|
-
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: 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, runInit: false, initForce: false, runTaskCli: false };
|
|
249
278
|
// check for subcommand as first non-flag arg
|
|
250
279
|
if (argv[2] === "test-context") {
|
|
251
280
|
return { ...defaults, testContext: true };
|
|
@@ -262,6 +291,12 @@ export function parseCliArgs(argv) {
|
|
|
262
291
|
if (argv[2] === "history") {
|
|
263
292
|
return { ...defaults, showHistory: true };
|
|
264
293
|
}
|
|
294
|
+
if (argv[2] === "status") {
|
|
295
|
+
return { ...defaults, showStatus: true };
|
|
296
|
+
}
|
|
297
|
+
if (argv[2] === "config") {
|
|
298
|
+
return { ...defaults, showConfig: true };
|
|
299
|
+
}
|
|
265
300
|
if (argv[2] === "init") {
|
|
266
301
|
const force = argv.includes("--force") || argv.includes("-f");
|
|
267
302
|
return { ...defaults, runInit: true, initForce: force };
|
|
@@ -351,7 +386,7 @@ export function parseCliArgs(argv) {
|
|
|
351
386
|
break;
|
|
352
387
|
}
|
|
353
388
|
}
|
|
354
|
-
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, runInit: false, initForce: false, runTaskCli: false };
|
|
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 };
|
|
355
390
|
}
|
|
356
391
|
export function printHelp() {
|
|
357
392
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -367,6 +402,8 @@ getting started:
|
|
|
367
402
|
commands:
|
|
368
403
|
init detect tools + sessions, import history, generate config
|
|
369
404
|
(none) start the supervisor daemon (interactive TUI)
|
|
405
|
+
status quick daemon health check (is it running? what's it doing?)
|
|
406
|
+
config show the effective resolved config (defaults + file)
|
|
370
407
|
task manage tasks and sessions (list, start, stop, new, rm, edit)
|
|
371
408
|
tasks show task progress (from aoaoe.tasks.json)
|
|
372
409
|
history review recent actions (from ~/.aoaoe/actions.log)
|
package/dist/console.js
CHANGED
|
@@ -91,7 +91,8 @@ export class ReasonerConsole {
|
|
|
91
91
|
const st = statSync(INPUT_FILE);
|
|
92
92
|
return st.size > 0;
|
|
93
93
|
}
|
|
94
|
-
catch {
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.error(`[console] pending-input size check failed: ${e}`);
|
|
95
96
|
return false;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -133,7 +134,9 @@ export class ReasonerConsole {
|
|
|
133
134
|
try {
|
|
134
135
|
appendFileSync(CONVO_LOG, line + "\n");
|
|
135
136
|
}
|
|
136
|
-
catch {
|
|
137
|
+
catch (e) {
|
|
138
|
+
console.error(`[console] conversation log write failed: ${e}`);
|
|
139
|
+
}
|
|
137
140
|
// in inline mode, also print colorized output to stderr
|
|
138
141
|
if (this.inlineMode) {
|
|
139
142
|
process.stderr.write(colorizeConsoleLine(line) + "\n");
|
package/dist/context.js
CHANGED
|
@@ -78,7 +78,8 @@ export function readContextFile(filePath) {
|
|
|
78
78
|
evictCache();
|
|
79
79
|
return content;
|
|
80
80
|
}
|
|
81
|
-
catch {
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(`[context] context file read failed for ${filePath}: ${e}`);
|
|
82
83
|
return "";
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -109,8 +110,9 @@ export function discoverContextFiles(dir) {
|
|
|
109
110
|
seenInodes.add(inodeKey);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
catch {
|
|
113
|
+
catch (e) {
|
|
113
114
|
// stat failed — still add by path to avoid silently dropping
|
|
115
|
+
console.error(`[context] inode de-dup stat failed for ${resolved}: ${e}`);
|
|
114
116
|
}
|
|
115
117
|
seenPaths.add(resolved);
|
|
116
118
|
found.push(filePath);
|
package/dist/executor.js
CHANGED
|
@@ -164,7 +164,8 @@ export class Executor {
|
|
|
164
164
|
return this.logAction({ action: "create_agent", path, title, tool }, false, `path is not a directory: ${path}`);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
catch {
|
|
167
|
+
catch (e) {
|
|
168
|
+
console.error(`[executor] statSync failed for create_agent path ${path}: ${e}`);
|
|
168
169
|
return this.logAction({ action: "create_agent", path, title, tool }, false, `cannot stat path: ${path}`);
|
|
169
170
|
}
|
|
170
171
|
// validate tool name
|
|
@@ -281,7 +282,9 @@ export class Executor {
|
|
|
281
282
|
this.rotateLogIfNeeded();
|
|
282
283
|
appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
|
|
283
284
|
}
|
|
284
|
-
catch {
|
|
285
|
+
catch (e) {
|
|
286
|
+
console.error(`[executor] action log write failed: ${e}`);
|
|
287
|
+
} // best-effort, don't crash the daemon
|
|
285
288
|
}
|
|
286
289
|
return entry;
|
|
287
290
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { loadConfig, validateEnvironment, parseCliArgs, printHelp, configFileExists } from "./config.js";
|
|
2
|
+
import { loadConfig, validateEnvironment, parseCliArgs, printHelp, configFileExists, findConfigFile } from "./config.js";
|
|
3
3
|
import { Poller, computeTmuxName } from "./poller.js";
|
|
4
4
|
import { createReasoner } from "./reasoner/index.js";
|
|
5
5
|
import { Executor } from "./executor.js";
|
|
6
6
|
import { printDashboard } from "./dashboard.js";
|
|
7
7
|
import { InputReader } from "./input.js";
|
|
8
8
|
import { ReasonerConsole } from "./console.js";
|
|
9
|
-
import { writeState, buildSessionStates, checkInterrupt, clearInterrupt, cleanupState, acquireLock } from "./daemon-state.js";
|
|
9
|
+
import { writeState, buildSessionStates, checkInterrupt, clearInterrupt, cleanupState, acquireLock, readState } from "./daemon-state.js";
|
|
10
10
|
import { formatSessionSummaries, formatActionDetail, formatPlainEnglishAction, narrateObservation, summarizeRecentActions, friendlyError } from "./console.js";
|
|
11
11
|
import { loadGlobalContext, resolveProjectDirWithSource, discoverContextFiles, loadSessionContext } from "./context.js";
|
|
12
12
|
import { tick as loopTick } from "./loop.js";
|
|
@@ -16,6 +16,8 @@ import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, h
|
|
|
16
16
|
import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from "./task-manager.js";
|
|
17
17
|
import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
|
|
18
18
|
import { TUI } from "./tui.js";
|
|
19
|
+
import { isDaemonRunningFromState } from "./chat.js";
|
|
20
|
+
import { sendNotification } from "./notify.js";
|
|
19
21
|
import { actionSession, actionDetail } from "./types.js";
|
|
20
22
|
import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
|
|
21
23
|
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
@@ -26,7 +28,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
26
28
|
const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
|
|
27
29
|
const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
|
|
28
30
|
async function main() {
|
|
29
|
-
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
|
|
31
|
+
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
|
|
30
32
|
if (help) {
|
|
31
33
|
printHelp();
|
|
32
34
|
process.exit(0);
|
|
@@ -66,6 +68,16 @@ async function main() {
|
|
|
66
68
|
await showActionHistory();
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
71
|
+
// `aoaoe status` -- quick one-shot daemon health check
|
|
72
|
+
if (showStatus) {
|
|
73
|
+
showDaemonStatus();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// `aoaoe config` -- show effective resolved config
|
|
77
|
+
if (showConfig) {
|
|
78
|
+
showEffectiveConfig();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
69
81
|
// `aoaoe task` -- task management CLI
|
|
70
82
|
if (isTaskCli) {
|
|
71
83
|
await runTaskCli(process.argv);
|
|
@@ -273,6 +285,8 @@ async function main() {
|
|
|
273
285
|
console.error(` mode: dry-run (no execution)`);
|
|
274
286
|
console.error("");
|
|
275
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}` });
|
|
276
290
|
input.stop();
|
|
277
291
|
Promise.resolve()
|
|
278
292
|
.then(() => reasonerConsole.stop())
|
|
@@ -296,6 +310,8 @@ async function main() {
|
|
|
296
310
|
else {
|
|
297
311
|
log("entering main loop (Ctrl+C to stop)\n");
|
|
298
312
|
}
|
|
313
|
+
// notify: daemon started
|
|
314
|
+
sendNotification(config, { event: "daemon_started", timestamp: Date.now(), detail: `reasoner: ${config.reasoner}` });
|
|
299
315
|
// clear any stale interrupt from a previous run
|
|
300
316
|
clearInterrupt();
|
|
301
317
|
// auto-explain: on the very first tick with sessions, inject an explain prompt
|
|
@@ -669,6 +685,19 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
669
685
|
}
|
|
670
686
|
}
|
|
671
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
|
+
}
|
|
672
701
|
if (skippedReason === "no changes") {
|
|
673
702
|
if (config.verbose) {
|
|
674
703
|
if (tui)
|
|
@@ -739,6 +768,13 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
739
768
|
log(`[${icon}] ${displayText}`);
|
|
740
769
|
}
|
|
741
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
|
+
});
|
|
742
778
|
}
|
|
743
779
|
const actionsOk = executed.filter((e) => e.success && e.action.action !== "wait").length;
|
|
744
780
|
const actionsFail = executed.filter((e) => !e.success && e.action.action !== "wait").length;
|
|
@@ -1101,6 +1137,80 @@ async function runIntegrationTest() {
|
|
|
1101
1137
|
// the integration test is a self-contained script that runs main() on import
|
|
1102
1138
|
await import(testModule);
|
|
1103
1139
|
}
|
|
1140
|
+
// `aoaoe status` -- quick one-shot health check: is the daemon running? what's it doing?
|
|
1141
|
+
function showDaemonStatus() {
|
|
1142
|
+
const state = readState();
|
|
1143
|
+
const running = isDaemonRunningFromState(state);
|
|
1144
|
+
const pkg = readPkgVersion();
|
|
1145
|
+
console.log("");
|
|
1146
|
+
console.log(` aoaoe${pkg ? ` v${pkg}` : ""} — daemon status`);
|
|
1147
|
+
console.log(` ${"─".repeat(50)}`);
|
|
1148
|
+
if (!running || !state) {
|
|
1149
|
+
console.log(` ${RED}●${RESET} daemon is ${BOLD}not running${RESET}`);
|
|
1150
|
+
const configPath = findConfigFile();
|
|
1151
|
+
console.log(` config: ${configPath ?? "none found (run 'aoaoe init')"}`);
|
|
1152
|
+
console.log("");
|
|
1153
|
+
console.log(" start with: aoaoe");
|
|
1154
|
+
console.log(" or observe: aoaoe --observe");
|
|
1155
|
+
console.log("");
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
// daemon is running — show details
|
|
1159
|
+
const elapsed = Date.now() - state.phaseStartedAt;
|
|
1160
|
+
const elapsedStr = elapsed < 60_000 ? `${Math.floor(elapsed / 1000)}s` : `${Math.floor(elapsed / 60_000)}m`;
|
|
1161
|
+
const phaseIcon = state.phase === "sleeping" ? `${DIM}○${RESET}` :
|
|
1162
|
+
state.phase === "reasoning" ? `${YELLOW}●${RESET}` :
|
|
1163
|
+
state.phase === "executing" ? `${GREEN}●${RESET}` :
|
|
1164
|
+
state.phase === "polling" ? `${YELLOW}○${RESET}` :
|
|
1165
|
+
`${RED}●${RESET}`;
|
|
1166
|
+
console.log(` ${GREEN}●${RESET} daemon is ${BOLD}running${RESET} (poll #${state.pollCount})`);
|
|
1167
|
+
console.log(` ${phaseIcon} phase: ${state.phase} (${elapsedStr})`);
|
|
1168
|
+
if (state.paused)
|
|
1169
|
+
console.log(` ${YELLOW}${BOLD} PAUSED${RESET}`);
|
|
1170
|
+
console.log(` poll interval: ${state.pollIntervalMs / 1000}s`);
|
|
1171
|
+
if (state.nextTickAt > Date.now()) {
|
|
1172
|
+
const countdown = Math.ceil((state.nextTickAt - Date.now()) / 1000);
|
|
1173
|
+
console.log(` next tick: ${countdown}s`);
|
|
1174
|
+
}
|
|
1175
|
+
console.log("");
|
|
1176
|
+
// sessions
|
|
1177
|
+
if (state.sessions.length === 0) {
|
|
1178
|
+
console.log(" no active sessions");
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
console.log(` ${state.sessions.length} session(s):`);
|
|
1182
|
+
for (const s of state.sessions) {
|
|
1183
|
+
const statusIcon = s.status === "working" || s.status === "running" ? `${GREEN}●${RESET}` :
|
|
1184
|
+
s.status === "idle" ? `${DIM}○${RESET}` :
|
|
1185
|
+
s.status === "error" ? `${RED}●${RESET}` :
|
|
1186
|
+
s.status === "done" ? `${GREEN}✓${RESET}` :
|
|
1187
|
+
`${DIM}?${RESET}`;
|
|
1188
|
+
const userTag = s.userActive ? ` ${DIM}(user active)${RESET}` : "";
|
|
1189
|
+
const taskTag = s.currentTask ? ` — ${DIM}${s.currentTask.slice(0, 50)}${RESET}` : "";
|
|
1190
|
+
console.log(` ${statusIcon} ${BOLD}${s.title}${RESET} (${s.tool}) ${s.status}${userTag}${taskTag}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
console.log("");
|
|
1194
|
+
}
|
|
1195
|
+
// `aoaoe config` -- show the effective resolved config (defaults + file + any notes)
|
|
1196
|
+
function showEffectiveConfig() {
|
|
1197
|
+
const configPath = findConfigFile();
|
|
1198
|
+
const configResult = loadConfig();
|
|
1199
|
+
// strip _configPath from output
|
|
1200
|
+
const { _configPath, ...config } = configResult;
|
|
1201
|
+
console.log("");
|
|
1202
|
+
console.log(" aoaoe — effective config");
|
|
1203
|
+
console.log(` ${"─".repeat(50)}`);
|
|
1204
|
+
console.log(` source: ${configPath ?? "defaults (no config file found)"}`);
|
|
1205
|
+
console.log("");
|
|
1206
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1207
|
+
console.log("");
|
|
1208
|
+
// helpful notes
|
|
1209
|
+
if (!configPath) {
|
|
1210
|
+
console.log(` ${DIM}create a config: aoaoe init${RESET}`);
|
|
1211
|
+
console.log("");
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1104
1214
|
main().catch((err) => {
|
|
1105
1215
|
console.error(`fatal: ${err}`);
|
|
1106
1216
|
process.exit(1);
|
package/dist/init.js
CHANGED
|
@@ -60,7 +60,8 @@ async function discoverSessions() {
|
|
|
60
60
|
.filter((r) => r.status === "fulfilled")
|
|
61
61
|
.map((r) => r.value);
|
|
62
62
|
}
|
|
63
|
-
catch {
|
|
63
|
+
catch (e) {
|
|
64
|
+
console.error(`[init] failed to parse session list: ${e}`);
|
|
64
65
|
return [];
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -72,7 +73,8 @@ async function getSessionStatus(id) {
|
|
|
72
73
|
const data = JSON.parse(result.stdout);
|
|
73
74
|
return toSessionStatus(data.status);
|
|
74
75
|
}
|
|
75
|
-
catch {
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.error(`[init] failed to parse session status for ${id}: ${e}`);
|
|
76
78
|
return "unknown";
|
|
77
79
|
}
|
|
78
80
|
}
|
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/poller.js
CHANGED
|
@@ -115,7 +115,8 @@ export class Poller {
|
|
|
115
115
|
const data = JSON.parse(result.stdout);
|
|
116
116
|
return toSessionStatus(data.status);
|
|
117
117
|
}
|
|
118
|
-
catch {
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error(`[poller] failed to parse session status for ${id}: ${e}`);
|
|
119
120
|
return "unknown";
|
|
120
121
|
}
|
|
121
122
|
}
|
|
@@ -232,7 +233,8 @@ export async function listAoeSessionsShared(timeoutMs = 10_000) {
|
|
|
232
233
|
const parsed = JSON.parse(result.stdout);
|
|
233
234
|
raw = Array.isArray(parsed) ? parsed : [];
|
|
234
235
|
}
|
|
235
|
-
catch {
|
|
236
|
+
catch (e) {
|
|
237
|
+
console.error(`[poller] failed to parse session list: ${e}`);
|
|
236
238
|
return [];
|
|
237
239
|
}
|
|
238
240
|
const results = await Promise.allSettled(raw.map(async (s) => {
|
|
@@ -245,7 +247,9 @@ export async function listAoeSessionsShared(timeoutMs = 10_000) {
|
|
|
245
247
|
status = JSON.parse(showResult.stdout).status ?? "unknown";
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
|
-
catch {
|
|
250
|
+
catch (e) {
|
|
251
|
+
console.error(`[poller] failed to parse session show for ${id}: ${e}`);
|
|
252
|
+
}
|
|
249
253
|
return { id, title, tool: s.tool ?? "", status, tmuxName: computeTmuxName(id, title) };
|
|
250
254
|
}));
|
|
251
255
|
return results
|
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;
|