@kal-elsam/kairo-runtime 0.8.0 → 0.9.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.
@@ -33,8 +33,14 @@ import {
33
33
  listRecoverySnapshots,
34
34
  reduceRecoveryAction
35
35
  } from "./cockpit-recovery.js";
36
+ import {
37
+ createSettingsActionState,
38
+ listCuratedIntegrations,
39
+ reduceSettingsAction
40
+ } from "./cockpit-settings.js";
36
41
  import { listReviewReceipts } from "../runtime/review/review-receipts.js";
37
42
  import { assertReceiptSecretFree } from "../runtime/review/review-validate.js";
43
+ import { listAlerts, resolveAlert, dismissAlert } from "../runtime/alerts/alert-store.js";
38
44
 
39
45
  export function useOrchestratorData({
40
46
  homeDir,
@@ -54,6 +60,7 @@ export function useOrchestratorData({
54
60
  const [selectedEvents, setSelectedEvents] = useState([]);
55
61
  const [reviews, setReviews] = useState([]);
56
62
  const [selectedReview, setSelectedReview] = useState(null);
63
+ const [alerts, setAlerts] = useState(null);
57
64
  const [statusMessage, setStatusMessage] = useState(null);
58
65
  const [launchAgentIndex, setLaunchAgentIndex] = useState(0);
59
66
  const [launchStep, setLaunchStep] = useState(LAUNCH_WIZARD_STEPS.AGENT);
@@ -61,6 +68,7 @@ export function useOrchestratorData({
61
68
  const [launchPermissionIndex, setLaunchPermissionIndex] = useState(0);
62
69
  const [changesAction, setChangesAction] = useState(createChangesActionState);
63
70
  const [recoveryAction, setRecoveryAction] = useState(createRecoveryActionState);
71
+ const [settingsAction, setSettingsAction] = useState(createSettingsActionState);
64
72
 
65
73
  const serializedReload = useMemo(() => createSerializedReloader(() => loadCockpitScanBundle({
66
74
  homeDir,
@@ -90,6 +98,11 @@ export function useOrchestratorData({
90
98
  setDashboard(outcome.result.dashboard);
91
99
  setDiagnostics(outcome.result.diagnostics);
92
100
  setSnapshot(outcome.result.snapshot);
101
+ try {
102
+ setAlerts(await listAlerts({ homeDir, limit: 50 }));
103
+ } catch {
104
+ setAlerts(null);
105
+ }
93
106
  setError(null);
94
107
  setLoading(false);
95
108
  setRetrying(false);
@@ -149,6 +162,22 @@ export function useOrchestratorData({
149
162
  });
150
163
  };
151
164
 
165
+ const handleAlertTransition = async (alert, action) => {
166
+ if (!alert) return;
167
+ setBusy(true);
168
+ try {
169
+ if (action === "dismiss") await dismissAlert(alert.alertId, { homeDir });
170
+ else await resolveAlert(alert.alertId, { homeDir });
171
+ setAlerts(await listAlerts({ homeDir, limit: 50 }));
172
+ setStatusMessage(action === "dismiss" ? "Alert dismissed" : "Alert resolved");
173
+ } catch (error) {
174
+ setError(error instanceof Error ? error.message : String(error));
175
+ setAlerts(null);
176
+ } finally {
177
+ setBusy(false);
178
+ }
179
+ };
180
+
152
181
  const handleLaunch = async (draft, profile, dispatch) => {
153
182
  if (!draft.agentId || !draft.task.trim()) {
154
183
  setError("Agent and task are required.");
@@ -366,6 +395,26 @@ export function useOrchestratorData({
366
395
  await reload();
367
396
  };
368
397
 
398
+ const previewSettings = (id) => {
399
+ setSettingsAction((prev) => reduceSettingsAction(prev, { type: "preview", id }));
400
+ };
401
+
402
+ const promptConfirmSettings = () => {
403
+ setSettingsAction((prev) => reduceSettingsAction(prev, { type: "confirm-prompt" }));
404
+ };
405
+
406
+ const confirmSettings = () => {
407
+ setSettingsAction((prev) => reduceSettingsAction(prev, { type: "confirm" }));
408
+ };
409
+
410
+ const cancelSettings = () => {
411
+ setSettingsAction((prev) => reduceSettingsAction(prev, { type: "cancel" }));
412
+ };
413
+
414
+ const resetSettings = () => {
415
+ setSettingsAction(() => createSettingsActionState());
416
+ };
417
+
369
418
  return {
370
419
  loading,
371
420
  busy,
@@ -381,6 +430,7 @@ export function useOrchestratorData({
381
430
  reviews,
382
431
  selectedReview,
383
432
  setSelectedReview,
433
+ alerts,
384
434
  statusMessage,
385
435
  launchAgentIndex,
386
436
  setLaunchAgentIndex,
@@ -394,11 +444,14 @@ export function useOrchestratorData({
394
444
  scanOptions: CONTROL_PLANE_AUTO_SCAN,
395
445
  changesAction,
396
446
  recoveryAction,
447
+ settingsAction,
448
+ curatedIntegrations: listCuratedIntegrations(),
397
449
  reload,
398
450
  resetLaunchWizard,
399
451
  openRunDetail,
400
452
  loadReviews,
401
453
  openReviewDetail,
454
+ handleAlertTransition,
402
455
  handleLaunch,
403
456
  handleCancelRun,
404
457
  previewChanges,
@@ -409,6 +462,11 @@ export function useOrchestratorData({
409
462
  cancelRecovery,
410
463
  confirmApplyRecovery,
411
464
  rescanRecovery,
465
+ previewSettings,
466
+ promptConfirmSettings,
467
+ confirmSettings,
468
+ cancelSettings,
469
+ resetSettings,
412
470
  recoverySnapshots: listRecoverySnapshots(snapshot)
413
471
  };
414
472
  }
@@ -19,6 +19,9 @@ export function harnessHomePaths(homeDir) {
19
19
  historyPath: join(root, "history.jsonl"),
20
20
  runsDir: join(root, "runs"),
21
21
  reviewsDir: join(root, "reviews"),
22
+ alertsDir: join(root, "alerts"),
23
+ monitorDir: join(root, "monitor"),
24
+ monitorStatePath: join(root, "monitor", "state.json"),
22
25
  coreDir: join(root, "core"),
23
26
  backupsDir: join(root, "backups")
24
27
  };
@@ -0,0 +1,216 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, readFile, unlink } from "node:fs/promises";
3
+ import { basename, join } from "node:path";
4
+ import { harnessHomePaths } from "../../paths.js";
5
+ import { writeAtomicJson } from "../write-atomic-json.js";
6
+ import {
7
+ ALERT_STATES,
8
+ assertSafeAlertId,
9
+ createAlert,
10
+ createAlertFingerprint
11
+ } from "./alert-types.js";
12
+ import { assertAlertSecretFree } from "./alert-validate.js";
13
+
14
+ const TERMINAL_ALERT_STATES = new Set([ALERT_STATES.RESOLVED, ALERT_STATES.DISMISSED]);
15
+
16
+ export class AlertStoreError extends Error {
17
+ constructor(message, { code = "alert_store_error", details = null } = {}) {
18
+ super(message);
19
+ this.name = "AlertStoreError";
20
+ this.code = code;
21
+ this.details = details;
22
+ }
23
+ }
24
+
25
+ export function alertPaths(homeDir, alertId) {
26
+ assertSafeAlertId(alertId);
27
+ const alertDir = join(harnessHomePaths(homeDir).alertsDir, alertId);
28
+ return { alertDir, alertPath: join(alertDir, "alert.json") };
29
+ }
30
+
31
+ function openIndexPath(homeDir, fingerprint) {
32
+ if (typeof fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(fingerprint)) {
33
+ throw new AlertStoreError(`Invalid alert fingerprint "${fingerprint}".`, {
34
+ code: "invalid_fingerprint"
35
+ });
36
+ }
37
+ return join(harnessHomePaths(homeDir).alertsDir, "open", fingerprint);
38
+ }
39
+
40
+ function openDirPath(homeDir) {
41
+ return join(harnessHomePaths(homeDir).alertsDir, "open");
42
+ }
43
+
44
+ async function readOpenAlert(indexPath) {
45
+ const expectedFingerprint = basename(indexPath);
46
+ const alert = assertAlertSecretFree(JSON.parse(await readFile(indexPath, "utf8")));
47
+ if (alert.fingerprint !== expectedFingerprint || alert.state !== ALERT_STATES.OPEN) {
48
+ throw new AlertStoreError(`Corrupt open alert claim "${expectedFingerprint}".`, {
49
+ code: "corrupt_alert",
50
+ details: {
51
+ fingerprint: expectedFingerprint,
52
+ payloadFingerprint: alert.fingerprint,
53
+ state: alert.state
54
+ }
55
+ });
56
+ }
57
+ return alert;
58
+ }
59
+
60
+ async function readHistoryAlert(homeDir, alertId) {
61
+ const alert = assertAlertSecretFree(
62
+ JSON.parse(await readFile(alertPaths(homeDir, alertId).alertPath, "utf8"))
63
+ );
64
+ if (alert.alertId !== alertId || !TERMINAL_ALERT_STATES.has(alert.state)) {
65
+ throw new AlertStoreError(`Corrupt history alert "${alertId}".`, {
66
+ code: "corrupt_alert",
67
+ details: {
68
+ alertId,
69
+ payloadAlertId: alert.alertId,
70
+ state: alert.state
71
+ }
72
+ });
73
+ }
74
+ return alert;
75
+ }
76
+
77
+ function wrapCorruptAlert(label, details, error) {
78
+ if (error instanceof AlertStoreError) throw error;
79
+ throw new AlertStoreError(`Corrupt or unreadable ${label}.`, {
80
+ code: "corrupt_alert",
81
+ details: {
82
+ ...details,
83
+ cause: error instanceof Error ? error.message : String(error)
84
+ }
85
+ });
86
+ }
87
+
88
+ async function listOpenAlerts(homeDir) {
89
+ const dir = openDirPath(homeDir);
90
+ if (!existsSync(dir)) return [];
91
+ const alerts = [];
92
+ for (const name of await readdir(dir)) {
93
+ if (!/^[a-f0-9]{64}$/.test(name)) continue;
94
+ try {
95
+ alerts.push(await readOpenAlert(join(dir, name)));
96
+ } catch (error) {
97
+ wrapCorruptAlert(`open alert "${name}"`, { fingerprint: name }, error);
98
+ }
99
+ }
100
+ return alerts;
101
+ }
102
+
103
+ async function findOpenAlert(homeDir, alertId) {
104
+ assertSafeAlertId(alertId);
105
+ for (const alert of await listOpenAlerts(homeDir)) {
106
+ if (alert.alertId === alertId) return alert;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ export async function loadAlert(alertId, { homeDir } = {}) {
112
+ const open = await findOpenAlert(homeDir, alertId);
113
+ if (open) return open;
114
+ const { alertPath } = alertPaths(homeDir, alertId);
115
+ if (!existsSync(alertPath)) throw new Error(`Alert not found: ${alertId}`);
116
+ return readHistoryAlert(homeDir, alertId);
117
+ }
118
+
119
+ async function writeHistoryAlert(alert, { homeDir } = {}) {
120
+ const sanitized = assertAlertSecretFree(alert);
121
+ const { alertDir, alertPath } = alertPaths(homeDir, sanitized.alertId);
122
+ await mkdir(alertDir, { recursive: true });
123
+ await writeAtomicJson(alertPath, sanitized);
124
+ return sanitized;
125
+ }
126
+
127
+ /**
128
+ * Persist an open alert. `open/<fingerprint>` is the authoritative open record
129
+ * (exclusive create); no secondary mutex.
130
+ */
131
+ export async function saveAlert(input, { homeDir } = {}) {
132
+ const draft = input?.version === 1 ? { ...input } : createAlert(input);
133
+ draft.fingerprint = createAlertFingerprint(draft);
134
+ const candidate = assertAlertSecretFree(draft);
135
+ if (candidate.state !== ALERT_STATES.OPEN) {
136
+ throw new AlertStoreError("Only open alerts can be saved to the open index.", {
137
+ code: "invalid_alert_state",
138
+ details: { state: candidate.state, fingerprint: candidate.fingerprint }
139
+ });
140
+ }
141
+ const indexPath = openIndexPath(homeDir, candidate.fingerprint);
142
+ await mkdir(openDirPath(homeDir), { recursive: true });
143
+
144
+ for (let attempt = 0; attempt < 5; attempt += 1) {
145
+ try {
146
+ await writeAtomicJson(indexPath, candidate, { createExclusive: true });
147
+ return { alert: candidate, deduped: false };
148
+ } catch (error) {
149
+ if (error?.code !== "EEXIST") throw error;
150
+ try {
151
+ return { alert: await readOpenAlert(indexPath), deduped: true };
152
+ } catch (readError) {
153
+ if (readError?.code !== "ENOENT") throw readError;
154
+ // Claim removed between EEXIST and read (resolve/dismiss race) — retry create.
155
+ }
156
+ }
157
+ }
158
+
159
+ throw new AlertStoreError("Unable to claim open alert fingerprint.", {
160
+ code: "claim_failed",
161
+ details: { fingerprint: candidate.fingerprint }
162
+ });
163
+ }
164
+
165
+ /**
166
+ * List alerts. Open records come from open/<fingerprint>;
167
+ * terminal history comes from alt-<id>/alert.json.
168
+ * Corrupt records fail closed.
169
+ */
170
+ export async function listAlerts({ homeDir, state = null, limit = null } = {}) {
171
+ const dir = harnessHomePaths(homeDir).alertsDir;
172
+ if (!existsSync(dir)) return [];
173
+
174
+ const open = await listOpenAlerts(homeDir);
175
+ const openIds = new Set(open.map((alert) => alert.alertId));
176
+ const alerts = [...open];
177
+
178
+ for (const alertId of (await readdir(dir)).filter((n) => /^alt-[a-f0-9]{16,32}$/.test(n))) {
179
+ if (openIds.has(alertId)) continue;
180
+ try {
181
+ alerts.push(await readHistoryAlert(homeDir, alertId));
182
+ } catch (error) {
183
+ wrapCorruptAlert(`alert "${alertId}"`, { alertId }, error);
184
+ }
185
+ }
186
+
187
+ alerts.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))
188
+ || String(a.alertId).localeCompare(String(b.alertId)));
189
+ const filtered = state ? alerts.filter((a) => a.state === state) : alerts;
190
+ return Number.isInteger(limit) && limit >= 0 ? filtered.slice(0, limit) : filtered;
191
+ }
192
+
193
+ async function transitionAlert(alertId, nextState, { homeDir } = {}) {
194
+ const current = await loadAlert(alertId, { homeDir });
195
+ if (current.state !== ALERT_STATES.OPEN) return current;
196
+ const now = new Date().toISOString();
197
+ const updated = await writeHistoryAlert({
198
+ ...current,
199
+ state: nextState,
200
+ updatedAt: now,
201
+ resolvedAt: now
202
+ }, { homeDir });
203
+ const indexPath = openIndexPath(homeDir, current.fingerprint);
204
+ await unlink(indexPath).catch((error) => {
205
+ if (error?.code !== "ENOENT") throw error;
206
+ });
207
+ return updated;
208
+ }
209
+
210
+ export async function resolveAlert(alertId, { homeDir } = {}) {
211
+ return transitionAlert(alertId, ALERT_STATES.RESOLVED, { homeDir });
212
+ }
213
+
214
+ export async function dismissAlert(alertId, { homeDir } = {}) {
215
+ return transitionAlert(alertId, ALERT_STATES.DISMISSED, { homeDir });
216
+ }
@@ -0,0 +1,59 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+
3
+ export const ALERT_STATES = Object.freeze({
4
+ OPEN: "open", RESOLVED: "resolved", DISMISSED: "dismissed"
5
+ });
6
+
7
+ export const ALERT_SEVERITIES = Object.freeze({
8
+ HIGH: "high", MEDIUM: "medium", LOW: "low"
9
+ });
10
+
11
+ export function createAlertId() {
12
+ return `alt-${randomBytes(12).toString("hex")}`;
13
+ }
14
+
15
+ export function assertSafeAlertId(alertId) {
16
+ if (typeof alertId !== "string" || !/^alt-[a-f0-9]{16,32}$/.test(alertId)) {
17
+ throw new Error(`Invalid alert id "${alertId}".`);
18
+ }
19
+ }
20
+
21
+ /** Stable dedupe key — kind + source + title only (never payloads). */
22
+ export function createAlertFingerprint({ kind, source = null, title }) {
23
+ return createHash("sha256")
24
+ .update([kind, source ?? "", title].map(String).join("\0"))
25
+ .digest("hex");
26
+ }
27
+
28
+ export function createAlert({
29
+ alertId = createAlertId(),
30
+ kind,
31
+ severity = ALERT_SEVERITIES.MEDIUM,
32
+ title,
33
+ summary = "",
34
+ source = null,
35
+ state = ALERT_STATES.OPEN,
36
+ createdAt = null,
37
+ updatedAt = null,
38
+ resolvedAt = null
39
+ } = {}) {
40
+ assertSafeAlertId(alertId);
41
+ const safeKind = String(kind ?? "").trim();
42
+ const safeTitle = String(title ?? "").trim();
43
+ if (!safeKind || !safeTitle) throw new Error("Alert kind and title are required.");
44
+ const now = new Date().toISOString();
45
+ return {
46
+ version: 1,
47
+ alertId,
48
+ kind: safeKind,
49
+ severity,
50
+ title: safeTitle,
51
+ summary: String(summary ?? "").trim(),
52
+ source: source == null ? null : String(source),
53
+ fingerprint: createAlertFingerprint({ kind: safeKind, source, title: safeTitle }),
54
+ state,
55
+ createdAt: createdAt ?? now,
56
+ updatedAt: updatedAt ?? now,
57
+ resolvedAt: resolvedAt ?? null
58
+ };
59
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ ALERT_SEVERITIES,
3
+ ALERT_STATES,
4
+ assertSafeAlertId,
5
+ createAlertFingerprint
6
+ } from "./alert-types.js";
7
+
8
+ export const ALERT_VALIDATION_ERROR_CODES = Object.freeze({
9
+ INVALID_ALERT: "invalid_alert",
10
+ FORBIDDEN_FIELD: "forbidden_field",
11
+ FINGERPRINT_MISMATCH: "fingerprint_mismatch"
12
+ });
13
+
14
+ const SEVERITY_SET = new Set(Object.values(ALERT_SEVERITIES));
15
+ const STATE_SET = new Set(Object.values(ALERT_STATES));
16
+ const FORBIDDEN_KEYS = new Set([
17
+ "prompt", "diff", "transcript", "raw", "rawOutput", "stdout", "stderr",
18
+ "output", "message", "messages", "content", "secret", "secrets", "token", "apiKey"
19
+ ]);
20
+ const ALLOWED = new Set([
21
+ "version", "alertId", "kind", "severity", "title", "summary",
22
+ "source", "fingerprint", "state", "createdAt", "updatedAt", "resolvedAt"
23
+ ]);
24
+
25
+ export class AlertValidationError extends Error {
26
+ constructor(message, { code, details = null } = {}) {
27
+ super(message);
28
+ this.name = "AlertValidationError";
29
+ this.code = code;
30
+ this.details = details;
31
+ }
32
+ }
33
+
34
+ function assertNoForbiddenKeys(value, label) {
35
+ if (!value || typeof value !== "object") return;
36
+ if (Array.isArray(value)) {
37
+ for (const item of value) assertNoForbiddenKeys(item, label);
38
+ return;
39
+ }
40
+ for (const [key, child] of Object.entries(value)) {
41
+ if (FORBIDDEN_KEYS.has(key)) {
42
+ throw new AlertValidationError(`Forbidden field "${key}" in ${label}.`, {
43
+ code: ALERT_VALIDATION_ERROR_CODES.FORBIDDEN_FIELD,
44
+ details: { key, label }
45
+ });
46
+ }
47
+ assertNoForbiddenKeys(child, `${label}.${key}`);
48
+ }
49
+ }
50
+
51
+ function requireString(alert, key, { allowEmpty = false } = {}) {
52
+ const value = alert[key];
53
+ if (typeof value !== "string" || (!allowEmpty && !value.trim())) {
54
+ throw new AlertValidationError(`Invalid alert.${key}: expected string.`, {
55
+ code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT,
56
+ details: { key }
57
+ });
58
+ }
59
+ return value;
60
+ }
61
+
62
+ /** Fail-closed scalar schema + recursive secret scan + derived fingerprint. */
63
+ export function assertAlertSecretFree(alert) {
64
+ if (!alert || typeof alert !== "object" || Array.isArray(alert)) {
65
+ throw new AlertValidationError("Invalid alert: expected object.", {
66
+ code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
67
+ });
68
+ }
69
+ assertNoForbiddenKeys(alert, "alert");
70
+ for (const key of Object.keys(alert)) {
71
+ if (!ALLOWED.has(key)) {
72
+ throw new AlertValidationError(`Unknown field "${key}" in alert.`, {
73
+ code: ALERT_VALIDATION_ERROR_CODES.FORBIDDEN_FIELD,
74
+ details: { key }
75
+ });
76
+ }
77
+ }
78
+ if (typeof alert.version !== "number" || !Number.isFinite(alert.version)) {
79
+ throw new AlertValidationError("Invalid alert.version.", {
80
+ code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
81
+ });
82
+ }
83
+ assertSafeAlertId(requireString(alert, "alertId"));
84
+ requireString(alert, "kind");
85
+ requireString(alert, "title");
86
+ requireString(alert, "summary", { allowEmpty: true });
87
+ requireString(alert, "fingerprint");
88
+ requireString(alert, "createdAt");
89
+ requireString(alert, "updatedAt");
90
+ if (!(alert.source === null || typeof alert.source === "string")) {
91
+ throw new AlertValidationError("Invalid alert.source.", {
92
+ code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
93
+ });
94
+ }
95
+ if (!(alert.resolvedAt === null || typeof alert.resolvedAt === "string")) {
96
+ throw new AlertValidationError("Invalid alert.resolvedAt.", {
97
+ code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
98
+ });
99
+ }
100
+ if (!SEVERITY_SET.has(alert.severity) || !STATE_SET.has(alert.state)) {
101
+ throw new AlertValidationError("Unknown alert severity or state.", {
102
+ code: ALERT_VALIDATION_ERROR_CODES.INVALID_ALERT
103
+ });
104
+ }
105
+ const expected = createAlertFingerprint({
106
+ kind: alert.kind,
107
+ source: alert.source,
108
+ title: alert.title
109
+ });
110
+ if (alert.fingerprint !== expected) {
111
+ throw new AlertValidationError("Alert fingerprint does not match kind+source+title.", {
112
+ code: ALERT_VALIDATION_ERROR_CODES.FINGERPRINT_MISMATCH,
113
+ details: { expected }
114
+ });
115
+ }
116
+ return alert;
117
+ }
@@ -0,0 +1,62 @@
1
+ import { join } from "node:path";
2
+ import { resolveHomeDir } from "../../paths.js";
3
+ import { printJson } from "../../json-output.js";
4
+ import { commandHeader } from "../../brand/index.js";
5
+ import {
6
+ disableMonitor, enableMonitor, getMonitorStatus, runMonitorTick
7
+ } from "./monitor.js";
8
+
9
+ export async function runGlobalMonitor(options, _manifest, deps = {}) {
10
+ const homeDir = deps.homeDir ?? resolveHomeDir();
11
+ const action = options.monitorAction ?? "status";
12
+ const packageRoot = deps.packageRoot;
13
+ const entry = deps.cliEntry ?? (packageRoot ? join(packageRoot, "bin", "kairo.js") : null);
14
+ try {
15
+ if (action === "enable") {
16
+ await (deps.enableMonitorImpl ?? enableMonitor)(homeDir, {
17
+ cliEntry: entry, nodePath: deps.nodePath ?? process.execPath, platform: deps.platform
18
+ });
19
+ await (deps.runMonitorTickImpl ?? runMonitorTick)(homeDir, {
20
+ packageRoot, workspaceRoot: options.cwd, notifyImpl: deps.notifyImpl
21
+ });
22
+ } else if (action === "disable") {
23
+ await (deps.disableMonitorImpl ?? disableMonitor)(homeDir, { platform: deps.platform });
24
+ } else if (action === "tick") {
25
+ const result = await (deps.runMonitorTickImpl ?? runMonitorTick)(homeDir, {
26
+ packageRoot, workspaceRoot: options.cwd, notifyImpl: deps.notifyImpl
27
+ });
28
+ return done(options, { ok: true, action, raised: result.raised.length, lastTick: result.state.lastTick });
29
+ }
30
+ const status = await (deps.getMonitorStatusImpl ?? getMonitorStatus)(homeDir, {
31
+ platform: deps.platform
32
+ });
33
+ return done(options, { ok: true, action: action === "status" ? "status" : action, ...status });
34
+ } catch (error) {
35
+ const message = String(error?.message ?? error);
36
+ if (options.json) printJson({ ok: false, error: message });
37
+ else console.error(message);
38
+ process.exitCode = 1;
39
+ return { ok: false, error: message };
40
+ }
41
+ }
42
+
43
+ function done(options, payload) {
44
+ if (options.json) { printJson(payload); return payload; }
45
+ console.log(commandHeader(`monitor ${payload.action}`));
46
+ if (payload.action === "tick") {
47
+ console.log(`Raised ${payload.raised} alert(s) this tick.`);
48
+ return payload;
49
+ }
50
+ console.log(`Enabled: ${payload.corrupt ? "unavailable" : payload.enabled ? "yes" : "no"} · ${payload.platform}`);
51
+ if (payload.corrupt) console.log("State: corrupt — run monitor disable to repair");
52
+ else {
53
+ const a = payload.autostart;
54
+ console.log(`Autostart: ${a?.loaded ? "loaded" : a?.configured ? "configured (not loaded)" : "off"}`
55
+ + `${a?.supported === false ? " (unsupported)" : ""}`);
56
+ }
57
+ console.log(`Open alerts: ${payload.openAlerts ?? "unavailable"}`);
58
+ if (payload.lastTickAt) {
59
+ console.log(`Last tick: ${payload.lastTickAt}${payload.lastTick?.complete === false ? " (incomplete)" : ""}`);
60
+ }
61
+ return payload;
62
+ }
@@ -0,0 +1,95 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { execFile as execFileCb } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFile = promisify(execFileCb);
9
+ export const MONITOR_LABEL = "local.kairo.monitor";
10
+
11
+ export function resolveMonitorPlatform(platform = process.platform) {
12
+ if (platform === "darwin") {
13
+ const agentsDir = join(homedir(), "Library", "LaunchAgents");
14
+ return {
15
+ id: "darwin", supportsAutostart: true, supportsNotify: true,
16
+ agentsDir, plistPath: join(agentsDir, `${MONITOR_LABEL}.plist`)
17
+ };
18
+ }
19
+ return { id: platform, supportsAutostart: false, supportsNotify: platform === "linux" };
20
+ }
21
+
22
+ function gui() { return `gui/${process.getuid?.() ?? 501}`; }
23
+
24
+ function plist({ nodePath, cliEntry, homeDir, intervalSec }) {
25
+ const e = (v) => String(v).replaceAll("&", "&amp;").replaceAll("<", "&lt;")
26
+ .replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
27
+ const log = join(homeDir, ".harness", "monitor");
28
+ const n = Math.max(60, Number(intervalSec) || 300);
29
+ return `<?xml version="1.0" encoding="UTF-8"?>
30
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
31
+ <plist version="1.0"><dict>
32
+ <key>Label</key><string>${MONITOR_LABEL}</string>
33
+ <key>ProgramArguments</key><array><string>${e(nodePath)}</string><string>${e(cliEntry)}</string><string>monitor</string><string>tick</string></array>
34
+ <key>StartInterval</key><integer>${n}</integer><key>RunAtLoad</key><true/>
35
+ <key>EnvironmentVariables</key><dict><key>HARNESS_HOME</key><string>${e(homeDir)}</string></dict>
36
+ <key>StandardOutPath</key><string>${e(join(log, "out.log"))}</string>
37
+ <key>StandardErrorPath</key><string>${e(join(log, "err.log"))}</string>
38
+ </dict></plist>`;
39
+ }
40
+
41
+ export async function notifyNewAlert({
42
+ title, body, platform = resolveMonitorPlatform(), execFileImpl = execFile
43
+ } = {}) {
44
+ const t = String(title ?? "Kairo").slice(0, 80);
45
+ const b = String(body ?? "").slice(0, 180).replace(/[\r\n]+/g, " ");
46
+ try {
47
+ if (platform.id === "darwin") {
48
+ await execFileImpl("osascript", [
49
+ "-e", `display notification ${JSON.stringify(b)} with title ${JSON.stringify(t)}`
50
+ ], { shell: false, timeout: 5000 });
51
+ return { sent: true };
52
+ }
53
+ if (platform.id === "linux") {
54
+ await execFileImpl("notify-send", [t, b], { shell: false, timeout: 5000 });
55
+ return { sent: true };
56
+ }
57
+ } catch { /* degrade */ }
58
+ return { sent: false };
59
+ }
60
+
61
+ export async function installAutostart({
62
+ homeDir, platform = resolveMonitorPlatform(), nodePath, cliEntry,
63
+ intervalSec = 300, execFileImpl = execFile
64
+ } = {}) {
65
+ if (!platform.supportsAutostart) {
66
+ return {
67
+ supported: false, configured: false, loaded: false, installed: false,
68
+ detail: `unsupported on ${platform.id}`
69
+ };
70
+ }
71
+ await mkdir(platform.agentsDir, { recursive: true });
72
+ await mkdir(join(homeDir, ".harness", "monitor"), { recursive: true });
73
+ await writeFile(platform.plistPath, plist({ nodePath, cliEntry, homeDir, intervalSec }));
74
+ try {
75
+ await execFileImpl("launchctl", ["bootout", `${gui()}/${MONITOR_LABEL}`], { shell: false }).catch(() => {});
76
+ await execFileImpl("launchctl", ["bootstrap", gui(), platform.plistPath], { shell: false });
77
+ return { supported: true, configured: true, loaded: true, installed: true, detail: "LaunchAgent loaded" };
78
+ } catch (error) {
79
+ return {
80
+ supported: true, configured: true, loaded: false, installed: false,
81
+ detail: `plist configured; not loaded (${error?.message ?? error})`
82
+ };
83
+ }
84
+ }
85
+
86
+ export async function removeAutostart({
87
+ platform = resolveMonitorPlatform(), execFileImpl = execFile
88
+ } = {}) {
89
+ if (!platform.supportsAutostart) {
90
+ return { supported: false, configured: false, loaded: false, installed: false };
91
+ }
92
+ await execFileImpl("launchctl", ["bootout", `${gui()}/${MONITOR_LABEL}`], { shell: false }).catch(() => {});
93
+ if (platform.plistPath && existsSync(platform.plistPath)) await unlink(platform.plistPath).catch(() => {});
94
+ return { supported: true, configured: false, loaded: false, installed: false };
95
+ }