aoaoe 0.58.0 → 0.59.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 +3 -0
- package/dist/config.js +39 -15
- package/dist/health.d.ts +26 -0
- package/dist/health.js +79 -0
- package/dist/index.js +14 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ options:
|
|
|
259
259
|
--reasoner <opencode|claude-code> reasoning backend (default: opencode)
|
|
260
260
|
--poll-interval <ms> poll interval in ms (default: 10000)
|
|
261
261
|
--port <number> opencode server port (default: 4097)
|
|
262
|
+
--health-port <number> start HTTP health check server on this port
|
|
262
263
|
--model <model> model to use
|
|
263
264
|
--profile <name> aoe profile (default: default)
|
|
264
265
|
--dry-run run full loop but only log actions (costs
|
|
@@ -336,6 +337,7 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
|
|
|
336
337
|
| `sessionDirs` | Map session titles to project directories (relative to cwd or absolute). Bypasses heuristic directory search. | `{}` |
|
|
337
338
|
| `contextFiles` | Extra AI instruction file paths to load from each project root | `[]` |
|
|
338
339
|
| `captureLinesCount` | Number of tmux lines to capture per session (`-S` flag) | `100` |
|
|
340
|
+
| `healthPort` | Start HTTP health check server on this port (e.g. `4098`). GET `/health` returns JSON status. | (none) |
|
|
339
341
|
| `notifications.webhookUrl` | Generic webhook URL (POST JSON) | (none) |
|
|
340
342
|
| `notifications.slackWebhookUrl` | Slack incoming webhook URL (block kit format) | (none) |
|
|
341
343
|
| `notifications.events` | Filter which events fire (omit to send all). Valid: `session_error`, `session_done`, `action_executed`, `action_failed`, `daemon_started`, `daemon_stopped` | (all) |
|
|
@@ -497,6 +499,7 @@ src/
|
|
|
497
499
|
input.ts # stdin readline listener with inject() for post-interrupt
|
|
498
500
|
init.ts # `aoaoe init`: auto-discover tools, sessions, generate config
|
|
499
501
|
notify.ts # webhook + Slack notification dispatcher for daemon events
|
|
502
|
+
health.ts # HTTP health check endpoint (GET /health JSON status)
|
|
500
503
|
colors.ts # shared ANSI color/style constants
|
|
501
504
|
context.ts # discoverContextFiles, resolveProjectDir, loadSessionContext
|
|
502
505
|
activity.ts # detect human keystrokes in tmux sessions
|
package/dist/config.js
CHANGED
|
@@ -83,7 +83,7 @@ export function loadConfig(overrides) {
|
|
|
83
83
|
const KNOWN_KEYS = {
|
|
84
84
|
reasoner: true, pollIntervalMs: true, captureLinesCount: true,
|
|
85
85
|
verbose: true, dryRun: true, observe: true, confirm: true,
|
|
86
|
-
contextFiles: true, sessionDirs: true, protectedSessions: true,
|
|
86
|
+
contextFiles: true, sessionDirs: true, protectedSessions: true, healthPort: true,
|
|
87
87
|
opencode: new Set(["port", "model"]),
|
|
88
88
|
claudeCode: new Set(["model", "yolo", "resume"]),
|
|
89
89
|
aoe: new Set(["profile"]),
|
|
@@ -128,6 +128,11 @@ export function validateConfig(config) {
|
|
|
128
128
|
if (typeof config.opencode?.port !== "number" || !isFinite(config.opencode.port) || config.opencode.port < 1 || config.opencode.port > 65535) {
|
|
129
129
|
errors.push(`opencode.port must be 1-65535, got ${config.opencode?.port}`);
|
|
130
130
|
}
|
|
131
|
+
if (config.healthPort !== undefined) {
|
|
132
|
+
if (typeof config.healthPort !== "number" || !isFinite(config.healthPort) || config.healthPort < 1 || config.healthPort > 65535) {
|
|
133
|
+
errors.push(`healthPort must be 1-65535, got ${config.healthPort}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
131
136
|
if (typeof config.policies?.maxErrorsBeforeRestart !== "number" || config.policies.maxErrorsBeforeRestart < 1) {
|
|
132
137
|
errors.push(`policies.maxErrorsBeforeRestart must be >= 1, got ${config.policies?.maxErrorsBeforeRestart}`);
|
|
133
138
|
}
|
|
@@ -241,22 +246,31 @@ async function which(cmd) {
|
|
|
241
246
|
return false;
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
// internal recursive merge on plain Record objects (no type assertions needed)
|
|
250
|
+
function mergeRecords(target, source) {
|
|
251
|
+
for (const [key, val] of Object.entries(source)) {
|
|
252
|
+
if (val !== undefined && val !== null) {
|
|
253
|
+
const existing = target[key];
|
|
254
|
+
// empty objects ({}) replace rather than merge — allows clearing sessionDirs etc.
|
|
255
|
+
if (typeof val === "object" && !Array.isArray(val) &&
|
|
256
|
+
typeof existing === "object" && existing !== null && !Array.isArray(existing) &&
|
|
257
|
+
Object.keys(val).length > 0) {
|
|
258
|
+
target[key] = mergeRecords({ ...existing }, val);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
target[key] = val;
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
}
|
|
265
|
+
return target;
|
|
266
|
+
}
|
|
267
|
+
// exported for testing — merges config objects with nested object support
|
|
268
|
+
export function deepMerge(...objects) {
|
|
269
|
+
let result = {};
|
|
270
|
+
for (const obj of objects) {
|
|
271
|
+
result = mergeRecords(result, obj);
|
|
272
|
+
}
|
|
273
|
+
// validated by caller (validateConfig) before use — safe cast
|
|
260
274
|
return result;
|
|
261
275
|
}
|
|
262
276
|
// compute fields that differ between two config objects (flat dot-notation paths)
|
|
@@ -368,7 +382,7 @@ export function parseCliArgs(argv) {
|
|
|
368
382
|
return argv[i + 1];
|
|
369
383
|
};
|
|
370
384
|
const knownFlags = new Set([
|
|
371
|
-
"--reasoner", "--poll-interval", "--port", "--model", "--profile",
|
|
385
|
+
"--reasoner", "--poll-interval", "--port", "--model", "--profile", "--health-port",
|
|
372
386
|
"--verbose", "-v", "--dry-run", "--observe", "--confirm", "--help", "-h", "--version",
|
|
373
387
|
]);
|
|
374
388
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -394,6 +408,14 @@ export function parseCliArgs(argv) {
|
|
|
394
408
|
i++;
|
|
395
409
|
break;
|
|
396
410
|
}
|
|
411
|
+
case "--health-port": {
|
|
412
|
+
const val = parseInt(nextArg(i, arg), 10);
|
|
413
|
+
if (isNaN(val))
|
|
414
|
+
throw new Error(`--health-port value '${argv[i + 1]}' is not a valid number`);
|
|
415
|
+
overrides.healthPort = val;
|
|
416
|
+
i++;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
397
419
|
case "--model": {
|
|
398
420
|
// applies to whichever backend is selected
|
|
399
421
|
const model = nextArg(i, arg);
|
|
@@ -472,6 +494,7 @@ options:
|
|
|
472
494
|
--reasoner <opencode|claude-code> reasoning backend (default: opencode)
|
|
473
495
|
--poll-interval <ms> poll interval in ms (default: 10000)
|
|
474
496
|
--port <number> opencode server port (default: 4097)
|
|
497
|
+
--health-port <number> start HTTP health check server on this port
|
|
475
498
|
--model <model> model to use
|
|
476
499
|
--profile <name> aoe profile (default: default)
|
|
477
500
|
--dry-run run full loop but only log actions (costs
|
|
@@ -509,6 +532,7 @@ example config:
|
|
|
509
532
|
"my-project": "/path/to/my-project",
|
|
510
533
|
"other-repo": "/path/to/other-repo"
|
|
511
534
|
},
|
|
535
|
+
"healthPort": 4098,
|
|
512
536
|
"notifications": {
|
|
513
537
|
"webhookUrl": "https://example.com/webhook",
|
|
514
538
|
"slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { DaemonState } from "./types.js";
|
|
3
|
+
export declare function buildHealthResponse(state: DaemonState | null, startedAt: number, now?: number): HealthResponse;
|
|
4
|
+
export interface HealthResponse {
|
|
5
|
+
status: "ok" | "error";
|
|
6
|
+
version: string;
|
|
7
|
+
uptimeMs: number;
|
|
8
|
+
daemon: {
|
|
9
|
+
phase: string;
|
|
10
|
+
phaseStartedAt: number;
|
|
11
|
+
pollCount: number;
|
|
12
|
+
pollIntervalMs: number;
|
|
13
|
+
sessionCount: number;
|
|
14
|
+
changeCount: number;
|
|
15
|
+
paused: boolean;
|
|
16
|
+
sessions: Array<{
|
|
17
|
+
title: string;
|
|
18
|
+
tool: string;
|
|
19
|
+
status: string;
|
|
20
|
+
currentTask?: string;
|
|
21
|
+
userActive: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function startHealthServer(port: number, startedAt: number): Server;
|
|
26
|
+
//# sourceMappingURL=health.d.ts.map
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// health.ts — HTTP health check endpoint for daemon monitoring
|
|
2
|
+
// starts a lightweight HTTP server that responds to GET /health with JSON status.
|
|
3
|
+
// enabled when config.healthPort is set (opt-in). the server reads daemon state
|
|
4
|
+
// from the IPC state file and returns uptime, phase, session info, and version.
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { readState } from "./daemon-state.js";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
// read version from package.json at startup (cached)
|
|
11
|
+
let cachedVersion;
|
|
12
|
+
function getVersion() {
|
|
13
|
+
if (cachedVersion)
|
|
14
|
+
return cachedVersion;
|
|
15
|
+
try {
|
|
16
|
+
// resolve from compiled dist/ to project root package.json
|
|
17
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(thisDir, "..", "package.json"), "utf-8"));
|
|
19
|
+
cachedVersion = pkg.version ?? "unknown";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
cachedVersion = "unknown";
|
|
23
|
+
}
|
|
24
|
+
return cachedVersion;
|
|
25
|
+
}
|
|
26
|
+
// pure function: build health response JSON from daemon state (exported for testing)
|
|
27
|
+
export function buildHealthResponse(state, startedAt, now = Date.now()) {
|
|
28
|
+
const uptimeMs = now - startedAt;
|
|
29
|
+
const version = getVersion();
|
|
30
|
+
if (!state) {
|
|
31
|
+
return {
|
|
32
|
+
status: "error",
|
|
33
|
+
version,
|
|
34
|
+
uptimeMs,
|
|
35
|
+
daemon: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: "ok",
|
|
40
|
+
version,
|
|
41
|
+
uptimeMs,
|
|
42
|
+
daemon: {
|
|
43
|
+
phase: state.phase,
|
|
44
|
+
phaseStartedAt: state.phaseStartedAt,
|
|
45
|
+
pollCount: state.pollCount,
|
|
46
|
+
pollIntervalMs: state.pollIntervalMs,
|
|
47
|
+
sessionCount: state.sessionCount,
|
|
48
|
+
changeCount: state.changeCount,
|
|
49
|
+
paused: state.paused,
|
|
50
|
+
sessions: state.sessions.map((s) => ({
|
|
51
|
+
title: s.title,
|
|
52
|
+
tool: s.tool,
|
|
53
|
+
status: s.status,
|
|
54
|
+
currentTask: s.currentTask,
|
|
55
|
+
userActive: s.userActive ?? false,
|
|
56
|
+
})),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// start the health HTTP server on the given port. returns the server for shutdown.
|
|
61
|
+
export function startHealthServer(port, startedAt) {
|
|
62
|
+
const server = createServer((req, res) => {
|
|
63
|
+
// only respond to GET /health (and GET / as convenience alias)
|
|
64
|
+
if (req.method === "GET" && (req.url === "/health" || req.url === "/")) {
|
|
65
|
+
const state = readState();
|
|
66
|
+
const body = JSON.stringify(buildHealthResponse(state, startedAt), null, 2);
|
|
67
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
|
|
68
|
+
res.end(body + "\n");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
72
|
+
res.end(JSON.stringify({ error: "not found", hint: "try GET /health" }) + "\n");
|
|
73
|
+
});
|
|
74
|
+
server.listen(port, "127.0.0.1", () => {
|
|
75
|
+
// listening — logged by caller
|
|
76
|
+
});
|
|
77
|
+
return server;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=health.js.map
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
|
|
|
18
18
|
import { TUI } from "./tui.js";
|
|
19
19
|
import { isDaemonRunningFromState } from "./chat.js";
|
|
20
20
|
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
21
|
+
import { startHealthServer } from "./health.js";
|
|
21
22
|
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
22
23
|
import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
|
|
23
24
|
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
@@ -266,8 +267,18 @@ async function main() {
|
|
|
266
267
|
}
|
|
267
268
|
catch { }
|
|
268
269
|
}
|
|
269
|
-
// ──
|
|
270
|
+
// ── health check HTTP server (opt-in via config.healthPort) ────────────────
|
|
270
271
|
const daemonStartedAt = Date.now();
|
|
272
|
+
let healthServer = null;
|
|
273
|
+
if (config.healthPort) {
|
|
274
|
+
healthServer = startHealthServer(config.healthPort, daemonStartedAt);
|
|
275
|
+
const msg = `health server listening on http://127.0.0.1:${config.healthPort}/health`;
|
|
276
|
+
if (tui)
|
|
277
|
+
tui.log("system", msg);
|
|
278
|
+
else
|
|
279
|
+
log(msg);
|
|
280
|
+
}
|
|
281
|
+
// ── session stats (for shutdown summary) ──────────────────────────────────
|
|
271
282
|
let totalDecisions = 0;
|
|
272
283
|
let totalActionsExecuted = 0;
|
|
273
284
|
let totalActionsFailed = 0;
|
|
@@ -308,6 +319,8 @@ async function main() {
|
|
|
308
319
|
console.error(` mode: dry-run (no execution)`);
|
|
309
320
|
console.error("");
|
|
310
321
|
log("shutting down...");
|
|
322
|
+
if (healthServer)
|
|
323
|
+
healthServer.close();
|
|
311
324
|
// notify: daemon stopped (fire-and-forget, don't block shutdown)
|
|
312
325
|
sendNotification(config, { event: "daemon_stopped", timestamp: Date.now(), detail: `polls: ${totalPolls}, actions: ${totalActionsExecuted}` });
|
|
313
326
|
input.stop();
|
package/dist/types.d.ts
CHANGED
|
@@ -120,6 +120,7 @@ export interface AoaoeConfig {
|
|
|
120
120
|
slackWebhookUrl?: string;
|
|
121
121
|
events?: NotificationEvent[];
|
|
122
122
|
};
|
|
123
|
+
healthPort?: number;
|
|
123
124
|
}
|
|
124
125
|
export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped";
|
|
125
126
|
export type DaemonPhase = "sleeping" | "polling" | "reasoning" | "executing" | "interrupted";
|