create-metaclaw 3.3.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.
Files changed (92) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +282 -0
  3. package/docs/assets/favicon.png +0 -0
  4. package/docs/assets/metaclaw-banner.svg +86 -0
  5. package/docs/assets/qis-logo.png +0 -0
  6. package/docs/assets/yz-favicon.png +0 -0
  7. package/docs/assets/yz-logo.png +0 -0
  8. package/docs/index.html +895 -0
  9. package/installer/assets/favicon.png +0 -0
  10. package/installer/auto-start.ts +330 -0
  11. package/installer/brand.ts +115 -0
  12. package/installer/core-scaffold.ts +448 -0
  13. package/installer/dashboard-generator.ts +657 -0
  14. package/installer/detect.ts +129 -0
  15. package/installer/index.ts +355 -0
  16. package/installer/module-loader.ts +412 -0
  17. package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
  18. package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
  19. package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
  20. package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
  21. package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
  22. package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
  23. package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
  24. package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
  25. package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
  26. package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
  27. package/installer/modules/boardroom/metaclaw-module.json +35 -0
  28. package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
  29. package/installer/modules/core/metaclaw-module.json +51 -0
  30. package/installer/modules/core/src/db.ts.txt +277 -0
  31. package/installer/modules/core/src/health-check.ts.txt +128 -0
  32. package/installer/modules/core/src/observability.ts.txt +20 -0
  33. package/installer/modules/core/src/safety.ts.txt +26 -0
  34. package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
  35. package/installer/modules/core/src/self-improve.ts.txt +48 -0
  36. package/installer/modules/core/src/self-update.ts.txt +345 -0
  37. package/installer/modules/core/src/sync-context.ts.txt +133 -0
  38. package/installer/modules/core/src/tasks.ts.txt +159 -0
  39. package/installer/modules/custom/metaclaw-module.json +15 -0
  40. package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
  41. package/installer/modules/dashboard/metaclaw-module.json +23 -0
  42. package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
  43. package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
  44. package/installer/modules/outreach/metaclaw-module.json +29 -0
  45. package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
  46. package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
  47. package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
  48. package/installer/modules/research/metaclaw-module.json +15 -0
  49. package/installer/modules/research/src/agent-research.ts.txt +127 -0
  50. package/installer/modules/scheduler/metaclaw-module.json +27 -0
  51. package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
  52. package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
  53. package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
  54. package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
  55. package/installer/modules/social/metaclaw-module.json +15 -0
  56. package/installer/modules/social/src/agent-social.ts.txt +110 -0
  57. package/installer/modules/support/metaclaw-module.json +15 -0
  58. package/installer/modules/support/src/agent-support.ts.txt +60 -0
  59. package/installer/modules/swarm/metaclaw-module.json +25 -0
  60. package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
  61. package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
  62. package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
  63. package/installer/modules/swarm/swarm/types.ts.txt +51 -0
  64. package/installer/modules/voice/metaclaw-module.json +16 -0
  65. package/installer/questionnaire.ts +277 -0
  66. package/installer/research.ts +258 -0
  67. package/installer/scaffold-from-config.ts +270 -0
  68. package/installer/task-generator.ts +324 -0
  69. package/installer/templates/agent-custom.ts.txt +100 -0
  70. package/installer/templates/agent-cycle.bat.txt +19 -0
  71. package/installer/templates/agent-outreach.ts.txt +193 -0
  72. package/installer/templates/agent-research.ts.txt +127 -0
  73. package/installer/templates/agent-social.ts.txt +110 -0
  74. package/installer/templates/agent-support.ts.txt +60 -0
  75. package/installer/templates/build-dashboard.cjs.txt +51 -0
  76. package/installer/templates/cron-manager.ts.txt +273 -0
  77. package/installer/templates/dashboard.html.txt +450 -0
  78. package/installer/templates/db.ts.txt +277 -0
  79. package/installer/templates/detect-session.bat.txt +41 -0
  80. package/installer/templates/health-check.ts.txt +128 -0
  81. package/installer/templates/inbox-agent.ts.txt +283 -0
  82. package/installer/templates/launch.bat.txt +120 -0
  83. package/installer/templates/morning-report.ts.txt +124 -0
  84. package/installer/templates/observability.ts.txt +20 -0
  85. package/installer/templates/safety.ts.txt +26 -0
  86. package/installer/templates/self-improve.ts.txt +48 -0
  87. package/installer/templates/self-update.ts.txt +345 -0
  88. package/installer/templates/state.json.txt +33 -0
  89. package/installer/templates/system-context.json.txt +33 -0
  90. package/installer/templates/update-dashboard.ts.txt +126 -0
  91. package/package.json +31 -0
  92. package/setup.bat +178 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Core Database — universal state management for any Claw
3
+ * Auto-generated by MetaClaw Installer
4
+ */
5
+
6
+ import Database from "better-sqlite3";
7
+ import path from "path";
8
+ import fs from "fs";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const DB_PATH = path.join(__dirname, "..", "data", "claw.db");
13
+
14
+ let _db: Database.Database | null = null;
15
+
16
+ export function getDb(): Database.Database {
17
+ if (_db) return _db;
18
+ const dir = path.dirname(DB_PATH);
19
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
20
+ _db = new Database(DB_PATH);
21
+ _db.pragma("journal_mode = WAL");
22
+ _db.pragma("busy_timeout = 5000");
23
+ initSchema(_db);
24
+ return _db;
25
+ }
26
+
27
+ function initSchema(db: Database.Database) {
28
+ db.exec(`
29
+ CREATE TABLE IF NOT EXISTS action_log (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
31
+ action_type TEXT NOT NULL,
32
+ target TEXT,
33
+ status TEXT DEFAULT 'pending',
34
+ details TEXT,
35
+ cost_usd REAL DEFAULT 0,
36
+ tokens_used INTEGER DEFAULT 0,
37
+ duration_ms INTEGER DEFAULT 0,
38
+ created_at TEXT DEFAULT (datetime('now'))
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS daily_metrics (
42
+ date TEXT PRIMARY KEY,
43
+ actions_taken INTEGER DEFAULT 0,
44
+ actions_succeeded INTEGER DEFAULT 0,
45
+ actions_failed INTEGER DEFAULT 0,
46
+ total_cost_usd REAL DEFAULT 0,
47
+ total_tokens INTEGER DEFAULT 0
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS circuit_breaker (
51
+ name TEXT PRIMARY KEY,
52
+ state TEXT DEFAULT 'closed',
53
+ failure_count INTEGER DEFAULT 0,
54
+ last_failure_at TEXT,
55
+ opens_at TEXT,
56
+ reason TEXT
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS prompt_versions (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ prompt_type TEXT NOT NULL,
62
+ version INTEGER NOT NULL,
63
+ content TEXT NOT NULL,
64
+ is_active INTEGER DEFAULT 1,
65
+ traffic_pct REAL DEFAULT 1.0,
66
+ total_runs INTEGER DEFAULT 0,
67
+ avg_score REAL DEFAULT 0,
68
+ created_at TEXT DEFAULT (datetime('now')),
69
+ created_by TEXT DEFAULT 'installer'
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS suppression_list (
73
+ target TEXT PRIMARY KEY,
74
+ reason TEXT,
75
+ added_at TEXT DEFAULT (datetime('now'))
76
+ );
77
+
78
+ CREATE TABLE IF NOT EXISTS config (
79
+ key TEXT PRIMARY KEY,
80
+ value TEXT,
81
+ updated_at TEXT DEFAULT (datetime('now'))
82
+ );
83
+
84
+ -- Contacts (for outreach/support claws, universal schema)
85
+ CREATE TABLE IF NOT EXISTS contacts (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ email TEXT UNIQUE,
88
+ name TEXT NOT NULL,
89
+ company TEXT,
90
+ role TEXT,
91
+ status TEXT DEFAULT 'new',
92
+ research_notes TEXT,
93
+ score INTEGER DEFAULT 0,
94
+ created_at TEXT DEFAULT (datetime('now')),
95
+ updated_at TEXT DEFAULT (datetime('now'))
96
+ );
97
+
98
+ -- Conversation history (tracks all inbound/outbound per contact)
99
+ CREATE TABLE IF NOT EXISTS conversation_history (
100
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
101
+ contact_email TEXT NOT NULL,
102
+ direction TEXT NOT NULL,
103
+ subject TEXT,
104
+ body TEXT,
105
+ timestamp TEXT DEFAULT (datetime('now'))
106
+ );
107
+
108
+ -- Processed emails dedup (Gmail markAsRead unreliable)
109
+ CREATE TABLE IF NOT EXISTS processed_emails (
110
+ uid TEXT PRIMARY KEY,
111
+ from_email TEXT,
112
+ subject TEXT,
113
+ action_taken TEXT,
114
+ processed_at TEXT DEFAULT (datetime('now'))
115
+ );
116
+
117
+ -- Person-based dedup (same person at different email addresses)
118
+ CREATE TABLE IF NOT EXISTS person_emails (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ person_name TEXT NOT NULL,
121
+ email TEXT UNIQUE NOT NULL,
122
+ added_at TEXT DEFAULT (datetime('now'))
123
+ );
124
+
125
+ -- Send history (every outbound email)
126
+ CREATE TABLE IF NOT EXISTS send_history (
127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ contact_id INTEGER REFERENCES contacts(id),
129
+ email_to TEXT NOT NULL,
130
+ subject TEXT NOT NULL,
131
+ body TEXT NOT NULL,
132
+ mailbox_from TEXT,
133
+ status TEXT DEFAULT 'drafted',
134
+ sent_at TEXT,
135
+ created_at TEXT DEFAULT (datetime('now'))
136
+ );
137
+ `);
138
+
139
+ db.prepare("INSERT OR IGNORE INTO circuit_breaker (name) VALUES ('main')").run();
140
+ db.prepare("INSERT OR IGNORE INTO circuit_breaker (name) VALUES ('external_api')").run();
141
+ db.prepare("INSERT OR IGNORE INTO circuit_breaker (name) VALUES ('email_send')").run();
142
+ }
143
+
144
+ export function recordAction(type: string, target: string, status: string, details?: string, cost?: number, tokens?: number, duration?: number) {
145
+ const db = getDb();
146
+ db.prepare(
147
+ "INSERT INTO action_log (action_type, target, status, details, cost_usd, tokens_used, duration_ms) VALUES (?, ?, ?, ?, ?, ?, ?)"
148
+ ).run(type, target, status, details || null, cost || 0, tokens || 0, duration || 0);
149
+
150
+ const today = new Date().toISOString().slice(0, 10);
151
+ const succeeded = status === "success" ? 1 : 0;
152
+ const failed = status === "error" ? 1 : 0;
153
+ db.prepare(
154
+ `INSERT INTO daily_metrics (date, actions_taken, actions_succeeded, actions_failed, total_cost_usd, total_tokens)
155
+ VALUES (?, 1, ?, ?, ?, ?)
156
+ ON CONFLICT(date) DO UPDATE SET
157
+ actions_taken = actions_taken + 1,
158
+ actions_succeeded = actions_succeeded + ?,
159
+ actions_failed = actions_failed + ?,
160
+ total_cost_usd = total_cost_usd + ?,
161
+ total_tokens = total_tokens + ?`
162
+ ).run(today, succeeded, failed, cost || 0, tokens || 0, succeeded, failed, cost || 0, tokens || 0);
163
+ }
164
+
165
+ export function getCircuitBreaker(name: string) {
166
+ return getDb().prepare("SELECT * FROM circuit_breaker WHERE name = ?").get(name) as any;
167
+ }
168
+
169
+ export function tripCircuitBreaker(name: string, reason: string) {
170
+ getDb().prepare(
171
+ "UPDATE circuit_breaker SET state = 'open', failure_count = failure_count + 1, last_failure_at = datetime('now'), opens_at = datetime('now', '+15 minutes'), reason = ? WHERE name = ?"
172
+ ).run(reason, name);
173
+ }
174
+
175
+ export function resetCircuitBreaker(name: string) {
176
+ getDb().prepare("UPDATE circuit_breaker SET state = 'closed', failure_count = 0, reason = NULL WHERE name = ?").run(name);
177
+ }
178
+
179
+ export function isSuppressed(target: string): boolean {
180
+ return !!getDb().prepare("SELECT 1 FROM suppression_list WHERE target = ?").get(target);
181
+ }
182
+
183
+ export function getConfig(key: string): string | null {
184
+ const row = getDb().prepare("SELECT value FROM config WHERE key = ?").get(key) as any;
185
+ return row?.value || null;
186
+ }
187
+
188
+ export function setConfig(key: string, value: string) {
189
+ getDb().prepare("INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, datetime('now'))").run(key, value);
190
+ }
191
+
192
+ export function getTodayMetrics() {
193
+ const today = new Date().toISOString().slice(0, 10);
194
+ return getDb().prepare("SELECT * FROM daily_metrics WHERE date = ?").get(today) as any;
195
+ }
196
+
197
+ // --- Contact helpers ---
198
+
199
+ export function upsertContact(email: string, name: string, company?: string, role?: string) {
200
+ getDb().prepare(
201
+ `INSERT INTO contacts (email, name, company, role, updated_at)
202
+ VALUES (?, ?, ?, ?, datetime('now'))
203
+ ON CONFLICT(email) DO UPDATE SET
204
+ name = excluded.name, company = excluded.company,
205
+ role = excluded.role, updated_at = datetime('now')`
206
+ ).run(email, name, company || null, role || null);
207
+ }
208
+
209
+ export function getContact(email: string) {
210
+ return getDb().prepare("SELECT * FROM contacts WHERE email = ?").get(email) as any;
211
+ }
212
+
213
+ export function updateContactStatus(email: string, status: string) {
214
+ getDb().prepare("UPDATE contacts SET status = ?, updated_at = datetime('now') WHERE email = ?").run(status, email);
215
+ }
216
+
217
+ // --- Person-based dedup (prevents emailing same person at different addresses) ---
218
+
219
+ export function isPersonSuppressed(name: string): boolean {
220
+ const normalized = name.replace(/^(Dr\.?|Prof\.?|Mr\.?|Mrs\.?|Ms\.?)\s+/i, "").trim().toLowerCase();
221
+ const row = getDb().prepare(
222
+ "SELECT 1 FROM person_emails pe JOIN suppression_list sl ON pe.email = sl.target WHERE LOWER(pe.person_name) = ?"
223
+ ).get(normalized);
224
+ return !!row;
225
+ }
226
+
227
+ export function registerPersonEmail(name: string, email: string) {
228
+ const normalized = name.replace(/^(Dr\.?|Prof\.?|Mr\.?|Mrs\.?|Ms\.?)\s+/i, "").trim();
229
+ getDb().prepare("INSERT OR IGNORE INTO person_emails (person_name, email) VALUES (?, ?)").run(normalized, email);
230
+ }
231
+
232
+ // --- Conversation history ---
233
+
234
+ export function addConversation(email: string, direction: "inbound" | "outbound", subject: string, body: string) {
235
+ getDb().prepare(
236
+ "INSERT INTO conversation_history (contact_email, direction, subject, body) VALUES (?, ?, ?, ?)"
237
+ ).run(email.toLowerCase(), direction, subject, body?.substring(0, 2000) || "");
238
+ }
239
+
240
+ export function getConversationHistory(email: string): any[] {
241
+ return getDb().prepare(
242
+ "SELECT direction, subject, body, timestamp FROM conversation_history WHERE LOWER(contact_email) = ? ORDER BY timestamp ASC"
243
+ ).all(email.toLowerCase()) as any[];
244
+ }
245
+
246
+ // --- Processed email dedup (inbox) ---
247
+
248
+ export function isEmailProcessed(uid: string): boolean {
249
+ return !!getDb().prepare("SELECT 1 FROM processed_emails WHERE uid = ?").get(uid);
250
+ }
251
+
252
+ export function markEmailProcessed(uid: string, fromEmail: string, subject: string, action: string) {
253
+ getDb().prepare(
254
+ "INSERT OR IGNORE INTO processed_emails (uid, from_email, subject, action_taken) VALUES (?, ?, ?, ?)"
255
+ ).run(uid, fromEmail, subject, action);
256
+ }
257
+
258
+ // --- Send history ---
259
+
260
+ export function recordSend(emailTo: string, subject: string, body: string, mailboxFrom?: string, contactId?: number) {
261
+ return getDb().prepare(
262
+ "INSERT INTO send_history (contact_id, email_to, subject, body, mailbox_from, status, sent_at) VALUES (?, ?, ?, ?, ?, 'sent', datetime('now'))"
263
+ ).run(contactId || null, emailTo, subject, body, mailboxFrom || null);
264
+ }
265
+
266
+ export function isDuplicate(email: string, windowHours: number = 72): boolean {
267
+ const row = getDb().prepare(
268
+ "SELECT COUNT(*) as cnt FROM send_history WHERE email_to = ? AND status = 'sent' AND sent_at > datetime('now', ?)"
269
+ ).get(email, `-${windowHours} hours`) as any;
270
+ return row.cnt > 0;
271
+ }
272
+
273
+ // --- Add to suppression list ---
274
+
275
+ export function addToSuppressionList(target: string, reason: string) {
276
+ getDb().prepare("INSERT OR IGNORE INTO suppression_list (target, reason) VALUES (?, ?)").run(target, reason);
277
+ }
@@ -0,0 +1,41 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+ REM Detect the Claude Code session ID for this agent's project directory
4
+ REM Writes session ID to data\session-id.txt for reliable --resume launches
5
+
6
+ set "PROJECT_DIR=__PROJECT_DIR__"
7
+ set "SESSION_FILE=%PROJECT_DIR%\data\session-id.txt"
8
+
9
+ REM Encode the project path the way Claude does (replace \ and : with -)
10
+ set "ENCODED=%PROJECT_DIR:\=-%"
11
+ set "ENCODED=!ENCODED::=-!"
12
+ set "ENCODED=!ENCODED:~1!"
13
+
14
+ set "SESSION_DIR=%USERPROFILE%\.claude\projects\!ENCODED!"
15
+
16
+ REM Check if session directory exists
17
+ if not exist "!SESSION_DIR!" (
18
+ echo No Claude session found for this project yet.
19
+ echo Run Claude Code in this directory first, then run this script.
20
+ exit /b 1
21
+ )
22
+
23
+ REM Find the most recent .jsonl file (that's the session)
24
+ set "LATEST_SESSION="
25
+ set "LATEST_TIME=0"
26
+ for %%F in ("!SESSION_DIR!\*.jsonl") do (
27
+ set "FNAME=%%~nF"
28
+ set "LATEST_SESSION=!FNAME!"
29
+ )
30
+
31
+ if "!LATEST_SESSION!"=="" (
32
+ echo No session files found.
33
+ exit /b 1
34
+ )
35
+
36
+ REM Write session ID to file
37
+ echo !LATEST_SESSION!> "!SESSION_FILE!"
38
+ echo Session ID detected: !LATEST_SESSION!
39
+ echo Saved to: !SESSION_FILE!
40
+ echo.
41
+ echo Future launches will use: claude --resume !LATEST_SESSION!
@@ -0,0 +1,128 @@
1
+ /**
2
+ * MetaClaw Health Check — system health monitoring
3
+ * Auto-generated by MetaClaw Installer
4
+ *
5
+ * Checks:
6
+ * - Circuit breaker status
7
+ * - Error rate (last 24h)
8
+ * - Disk space for logs/data
9
+ * - Database integrity
10
+ * - Cron task status
11
+ * - Token/cost budget
12
+ */
13
+
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { getDb, getCircuitBreaker, resetCircuitBreaker, getTodayMetrics, getConfig, setConfig } from "./db.js";
18
+ import { log } from "./observability.js";
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const PROJECT_DIR = path.join(__dirname, "..");
22
+
23
+ export type HealthStatus = {
24
+ overall: "healthy" | "warning" | "critical";
25
+ checks: Array<{ name: string; status: "ok" | "warn" | "fail"; detail: string }>;
26
+ timestamp: string;
27
+ };
28
+
29
+ export function runHealthCheck(): HealthStatus {
30
+ const checks: HealthStatus["checks"] = [];
31
+
32
+ // 1. Circuit breaker
33
+ const cb = getCircuitBreaker("main");
34
+ if (cb?.state === "open") {
35
+ // Check if enough time has passed to try half-open
36
+ if (cb.opens_at && new Date(cb.opens_at) < new Date()) {
37
+ resetCircuitBreaker("main");
38
+ checks.push({ name: "Circuit Breaker", status: "warn", detail: "Was OPEN, auto-reset to CLOSED (cooldown expired)" });
39
+ } else {
40
+ checks.push({ name: "Circuit Breaker", status: "fail", detail: "OPEN: " + (cb.reason || "unknown") });
41
+ }
42
+ } else {
43
+ checks.push({ name: "Circuit Breaker", status: "ok", detail: "CLOSED" });
44
+ }
45
+
46
+ // 2. Error rate
47
+ const metrics = getTodayMetrics();
48
+ if (metrics && metrics.actions_taken > 0) {
49
+ const errorRate = metrics.actions_failed / metrics.actions_taken;
50
+ if (errorRate > 0.2) {
51
+ checks.push({ name: "Error Rate", status: "fail", detail: `${(errorRate * 100).toFixed(0)}% failures today (${metrics.actions_failed}/${metrics.actions_taken})` });
52
+ } else if (errorRate > 0.05) {
53
+ checks.push({ name: "Error Rate", status: "warn", detail: `${(errorRate * 100).toFixed(0)}% failures today` });
54
+ } else {
55
+ checks.push({ name: "Error Rate", status: "ok", detail: `${(errorRate * 100).toFixed(1)}% failure rate` });
56
+ }
57
+ } else {
58
+ checks.push({ name: "Error Rate", status: "ok", detail: "No actions today" });
59
+ }
60
+
61
+ // 3. Data directory size
62
+ const dataDir = path.join(PROJECT_DIR, "data");
63
+ if (fs.existsSync(dataDir)) {
64
+ let totalSize = 0;
65
+ const countFiles = (dir: string) => {
66
+ for (const f of fs.readdirSync(dir)) {
67
+ const fp = path.join(dir, f);
68
+ const stat = fs.statSync(fp);
69
+ if (stat.isDirectory()) countFiles(fp);
70
+ else totalSize += stat.size;
71
+ }
72
+ };
73
+ countFiles(dataDir);
74
+ const sizeMB = totalSize / 1024 / 1024;
75
+ if (sizeMB > 500) {
76
+ checks.push({ name: "Disk Usage", status: "warn", detail: `${sizeMB.toFixed(0)}MB in data/ — consider cleanup` });
77
+ } else {
78
+ checks.push({ name: "Disk Usage", status: "ok", detail: `${sizeMB.toFixed(1)}MB` });
79
+ }
80
+ }
81
+
82
+ // 4. Database integrity
83
+ try {
84
+ const db = getDb();
85
+ const result = db.pragma("integrity_check") as any[];
86
+ if (result[0]?.integrity_check === "ok") {
87
+ checks.push({ name: "Database", status: "ok", detail: "Integrity OK" });
88
+ } else {
89
+ checks.push({ name: "Database", status: "fail", detail: "Integrity check failed" });
90
+ }
91
+ } catch (err) {
92
+ checks.push({ name: "Database", status: "fail", detail: "Cannot open database" });
93
+ }
94
+
95
+ // 5. Cost budget
96
+ if (metrics) {
97
+ const dailyCost = metrics.total_cost_usd || 0;
98
+ if (dailyCost > 10) {
99
+ checks.push({ name: "Cost", status: "warn", detail: `$${dailyCost.toFixed(2)} today — high spend` });
100
+ } else {
101
+ checks.push({ name: "Cost", status: "ok", detail: `$${dailyCost.toFixed(2)} today` });
102
+ }
103
+ }
104
+
105
+ // Overall
106
+ const hasFail = checks.some(c => c.status === "fail");
107
+ const hasWarn = checks.some(c => c.status === "warn");
108
+ const overall = hasFail ? "critical" : hasWarn ? "warning" : "healthy";
109
+
110
+ const result: HealthStatus = { overall, checks, timestamp: new Date().toISOString() };
111
+
112
+ // Save to config for dashboard
113
+ setConfig("last_health_check", JSON.stringify(result));
114
+ log("info", "health_check.complete", { overall, checks: checks.length });
115
+
116
+ return result;
117
+ }
118
+
119
+ // CLI
120
+ if (process.argv[1]?.includes("health-check")) {
121
+ getDb();
122
+ const result = runHealthCheck();
123
+ console.log(`Health: ${result.overall.toUpperCase()}\n`);
124
+ for (const c of result.checks) {
125
+ const icon = c.status === "ok" ? "+" : c.status === "warn" ? "!" : "X";
126
+ console.log(` [${icon}] ${c.name}: ${c.detail}`);
127
+ }
128
+ }