create-yonderclaw 1.0.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/LICENSE +44 -0
- package/README.md +288 -0
- package/bin/create-yonderclaw.mjs +43 -0
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/metaclaw-banner.svg +86 -0
- package/docs/assets/qis-logo.png +0 -0
- package/docs/assets/yz-favicon.png +0 -0
- package/docs/assets/yz-logo.png +0 -0
- package/docs/index.html +1155 -0
- package/installer/assets/favicon.png +0 -0
- package/installer/auto-start.ts +330 -0
- package/installer/brand.ts +115 -0
- package/installer/core-scaffold.ts +448 -0
- package/installer/dashboard-generator.ts +657 -0
- package/installer/detect.ts +129 -0
- package/installer/index.ts +355 -0
- package/installer/module-loader.ts +412 -0
- package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
- package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
- package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
- package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
- package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
- package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
- package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
- package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
- package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
- package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
- package/installer/modules/boardroom/metaclaw-module.json +35 -0
- package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
- package/installer/modules/core/metaclaw-module.json +51 -0
- package/installer/modules/core/src/db.ts.txt +277 -0
- package/installer/modules/core/src/health-check.ts.txt +128 -0
- package/installer/modules/core/src/observability.ts.txt +20 -0
- package/installer/modules/core/src/safety.ts.txt +26 -0
- package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
- package/installer/modules/core/src/self-improve.ts.txt +48 -0
- package/installer/modules/core/src/self-update.ts.txt +345 -0
- package/installer/modules/core/src/sync-context.ts.txt +133 -0
- package/installer/modules/core/src/tasks.ts.txt +159 -0
- package/installer/modules/custom/metaclaw-module.json +15 -0
- package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
- package/installer/modules/dashboard/metaclaw-module.json +23 -0
- package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
- package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
- package/installer/modules/outreach/metaclaw-module.json +29 -0
- package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
- package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
- package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
- package/installer/modules/research/metaclaw-module.json +15 -0
- package/installer/modules/research/src/agent-research.ts.txt +127 -0
- package/installer/modules/scheduler/metaclaw-module.json +27 -0
- package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
- package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
- package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
- package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
- package/installer/modules/social/metaclaw-module.json +15 -0
- package/installer/modules/social/src/agent-social.ts.txt +110 -0
- package/installer/modules/support/metaclaw-module.json +15 -0
- package/installer/modules/support/src/agent-support.ts.txt +60 -0
- package/installer/modules/swarm/metaclaw-module.json +25 -0
- package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
- package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
- package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
- package/installer/modules/swarm/swarm/types.ts.txt +51 -0
- package/installer/modules/voice/metaclaw-module.json +16 -0
- package/installer/questionnaire.ts +277 -0
- package/installer/research.ts +258 -0
- package/installer/scaffold-from-config.ts +270 -0
- package/installer/task-generator.ts +324 -0
- package/installer/templates/agent-custom.ts.txt +100 -0
- package/installer/templates/agent-cycle.bat.txt +19 -0
- package/installer/templates/agent-outreach.ts.txt +193 -0
- package/installer/templates/agent-research.ts.txt +127 -0
- package/installer/templates/agent-social.ts.txt +110 -0
- package/installer/templates/agent-support.ts.txt +60 -0
- package/installer/templates/build-dashboard.cjs.txt +51 -0
- package/installer/templates/cron-manager.ts.txt +273 -0
- package/installer/templates/dashboard.html.txt +450 -0
- package/installer/templates/db.ts.txt +277 -0
- package/installer/templates/detect-session.bat.txt +41 -0
- package/installer/templates/health-check.ts.txt +128 -0
- package/installer/templates/inbox-agent.ts.txt +283 -0
- package/installer/templates/launch.bat.txt +120 -0
- package/installer/templates/morning-report.ts.txt +124 -0
- package/installer/templates/observability.ts.txt +20 -0
- package/installer/templates/safety.ts.txt +26 -0
- package/installer/templates/self-improve.ts.txt +48 -0
- package/installer/templates/self-update.ts.txt +345 -0
- package/installer/templates/state.json.txt +33 -0
- package/installer/templates/system-context.json.txt +33 -0
- package/installer/templates/update-dashboard.ts.txt +126 -0
- package/package.json +31 -0
- 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,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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Observability — structured JSONL logging for all Claws
|
|
3
|
+
* Auto-generated by MetaClaw Installer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const LOG_DIR = path.join(__dirname, "..", "data", "logs");
|
|
12
|
+
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const LOG_FILE = path.join(LOG_DIR, "agent-" + new Date().toISOString().slice(0, 10) + ".jsonl");
|
|
15
|
+
|
|
16
|
+
export function log(level: string, event: string, data?: Record<string, unknown>) {
|
|
17
|
+
const entry = { timestamp: new Date().toISOString(), level, event, ...data };
|
|
18
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
|
|
19
|
+
if (level === "error" || level === "warn") console.error("[" + level.toUpperCase() + "] " + event, data || "");
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Safety Layer — universal for all Claws
|
|
3
|
+
* Auto-generated by MetaClaw Installer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getCircuitBreaker, tripCircuitBreaker, isSuppressed, getTodayMetrics } from "./db.js";
|
|
7
|
+
|
|
8
|
+
export const SAFETY_CONFIG = __SAFETY_CONFIG__;
|
|
9
|
+
|
|
10
|
+
export function checkCanAct(target?: string, isConversation: boolean = false): { allowed: boolean; reason?: string } {
|
|
11
|
+
const breaker = getCircuitBreaker("main");
|
|
12
|
+
if (breaker?.state === "open") {
|
|
13
|
+
return { allowed: false, reason: "Circuit breaker OPEN: " + breaker.reason };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (target && isSuppressed(target)) {
|
|
17
|
+
return { allowed: false, reason: target + " is suppressed" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const metrics = getTodayMetrics();
|
|
21
|
+
if (metrics && metrics.actions_taken >= SAFETY_CONFIG.maxActionsPerDay) {
|
|
22
|
+
return { allowed: false, reason: "Daily limit reached (" + metrics.actions_taken + "/" + SAFETY_CONFIG.maxActionsPerDay + ")" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { allowed: true };
|
|
26
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetaClaw — Capability Scanner
|
|
3
|
+
*
|
|
4
|
+
* Deterministic, NON-LLM scan of what this agent can actually do.
|
|
5
|
+
* Runs in cron pre-flight before the agent starts thinking.
|
|
6
|
+
* Writes raw inventory to memory/capabilities/_auto.md.
|
|
7
|
+
*
|
|
8
|
+
* The agent diffs this against memory/capabilities/tools.md to detect drift.
|
|
9
|
+
* Guarantee: the agent cannot forget capabilities that exist on disk.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
|
|
15
|
+
const PROJECT_ROOT = process.cwd();
|
|
16
|
+
const OUTPUT = path.join(PROJECT_ROOT, "memory", "capabilities", "_auto.md");
|
|
17
|
+
|
|
18
|
+
interface Inventory {
|
|
19
|
+
scripts: string[];
|
|
20
|
+
srcFiles: string[];
|
|
21
|
+
npmScripts: string[];
|
|
22
|
+
dependencies: string[];
|
|
23
|
+
allowedTools: string[];
|
|
24
|
+
mcpServers: string[];
|
|
25
|
+
envVars: string[];
|
|
26
|
+
dataFiles: string[];
|
|
27
|
+
modules: any[];
|
|
28
|
+
boardroom: boolean;
|
|
29
|
+
swarm: boolean;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scan(): Inventory {
|
|
34
|
+
const inv: Inventory = {
|
|
35
|
+
scripts: [],
|
|
36
|
+
srcFiles: [],
|
|
37
|
+
npmScripts: [],
|
|
38
|
+
dependencies: [],
|
|
39
|
+
allowedTools: [],
|
|
40
|
+
mcpServers: [],
|
|
41
|
+
envVars: [],
|
|
42
|
+
dataFiles: [],
|
|
43
|
+
modules: [],
|
|
44
|
+
boardroom: false,
|
|
45
|
+
swarm: false,
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// scripts/ directory
|
|
50
|
+
try {
|
|
51
|
+
const scriptsDir = path.join(PROJECT_ROOT, "scripts");
|
|
52
|
+
if (fs.existsSync(scriptsDir)) {
|
|
53
|
+
inv.scripts = fs.readdirSync(scriptsDir).filter(f => !f.startsWith("."));
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
57
|
+
// src/ directory
|
|
58
|
+
try {
|
|
59
|
+
const srcDir = path.join(PROJECT_ROOT, "src");
|
|
60
|
+
if (fs.existsSync(srcDir)) {
|
|
61
|
+
inv.srcFiles = fs.readdirSync(srcDir).filter(f => f.endsWith(".ts"));
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
|
|
65
|
+
// package.json scripts + deps
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8"));
|
|
68
|
+
inv.npmScripts = Object.keys(pkg.scripts || {});
|
|
69
|
+
inv.dependencies = Object.keys(pkg.dependencies || {});
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
72
|
+
// .claude/settings.json allowed tools
|
|
73
|
+
try {
|
|
74
|
+
const settings = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, ".claude", "settings.json"), "utf-8"));
|
|
75
|
+
inv.allowedTools = settings.permissions?.allow || [];
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
// .mcp.json MCP servers
|
|
79
|
+
try {
|
|
80
|
+
const mcp = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, ".mcp.json"), "utf-8"));
|
|
81
|
+
inv.mcpServers = Object.keys(mcp.mcpServers || {});
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
84
|
+
// Env vars (names only, no values) matching API_KEY/TOKEN/SECRET
|
|
85
|
+
try {
|
|
86
|
+
const envPath = path.join(PROJECT_ROOT, ".env");
|
|
87
|
+
if (fs.existsSync(envPath)) {
|
|
88
|
+
const envContent = fs.readFileSync(envPath, "utf-8");
|
|
89
|
+
const matches = envContent.match(/^([A-Z_]+(?:API_KEY|TOKEN|SECRET|PASS|USER|HOST|URL))=/gm) || [];
|
|
90
|
+
inv.envVars = matches.map(m => m.replace(/=$/, ""));
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
|
|
94
|
+
// data/ files
|
|
95
|
+
try {
|
|
96
|
+
const dataDir = path.join(PROJECT_ROOT, "data");
|
|
97
|
+
if (fs.existsSync(dataDir)) {
|
|
98
|
+
inv.dataFiles = fs.readdirSync(dataDir).filter(f => !f.startsWith(".") && !f.includes("-wal") && !f.includes("-shm"));
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
|
|
102
|
+
// Modules installed
|
|
103
|
+
try {
|
|
104
|
+
const modulesJson = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "data", "modules.json"), "utf-8"));
|
|
105
|
+
inv.modules = modulesJson.modules || [];
|
|
106
|
+
} catch {}
|
|
107
|
+
|
|
108
|
+
// Feature detection
|
|
109
|
+
inv.boardroom = fs.existsSync(path.join(PROJECT_ROOT, "boardroom", "client.ts"));
|
|
110
|
+
inv.swarm = fs.existsSync(path.join(PROJECT_ROOT, "swarm", "swarm-client.ts"));
|
|
111
|
+
|
|
112
|
+
return inv;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function format(inv: Inventory): string {
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
lines.push("# Auto-Generated Capability Inventory");
|
|
118
|
+
lines.push(`**Generated:** ${inv.timestamp}`);
|
|
119
|
+
lines.push("**Source:** Deterministic scan (not LLM — cannot hallucinate)");
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push("If something in CAPABILITIES.md disagrees with this file, THIS FILE IS RIGHT.");
|
|
122
|
+
lines.push("This is a forensic inventory of what actually exists on disk.");
|
|
123
|
+
lines.push("");
|
|
124
|
+
|
|
125
|
+
lines.push("## Installed Modules");
|
|
126
|
+
if (inv.modules.length > 0) {
|
|
127
|
+
for (const m of inv.modules) lines.push(`- ${m.name} (${m.category}) v${m.version}`);
|
|
128
|
+
} else {
|
|
129
|
+
lines.push("- (none detected — data/modules.json missing)");
|
|
130
|
+
}
|
|
131
|
+
lines.push("");
|
|
132
|
+
|
|
133
|
+
lines.push("## NPM Scripts (runnable commands)");
|
|
134
|
+
if (inv.npmScripts.length > 0) {
|
|
135
|
+
for (const s of inv.npmScripts) lines.push(`- \`npm run ${s}\``);
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
|
|
139
|
+
lines.push("## Source Files (src/)");
|
|
140
|
+
for (const f of inv.srcFiles) lines.push(`- src/${f}`);
|
|
141
|
+
lines.push("");
|
|
142
|
+
|
|
143
|
+
lines.push("## Scripts (scripts/)");
|
|
144
|
+
for (const s of inv.scripts) lines.push(`- scripts/${s}`);
|
|
145
|
+
lines.push("");
|
|
146
|
+
|
|
147
|
+
lines.push("## Dependencies (npm)");
|
|
148
|
+
for (const d of inv.dependencies) lines.push(`- ${d}`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
|
|
151
|
+
lines.push("## Data Files (data/)");
|
|
152
|
+
for (const d of inv.dataFiles) lines.push(`- data/${d}`);
|
|
153
|
+
lines.push("");
|
|
154
|
+
|
|
155
|
+
lines.push("## Allowed Tools (.claude/settings.json)");
|
|
156
|
+
if (inv.allowedTools.length > 0) {
|
|
157
|
+
for (const t of inv.allowedTools) lines.push(`- ${t}`);
|
|
158
|
+
} else {
|
|
159
|
+
lines.push("- (none detected — permissions may not be configured)");
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
|
|
163
|
+
lines.push("## MCP Servers");
|
|
164
|
+
if (inv.mcpServers.length > 0) {
|
|
165
|
+
for (const s of inv.mcpServers) lines.push(`- ${s}`);
|
|
166
|
+
} else {
|
|
167
|
+
lines.push("- (none — no .mcp.json)");
|
|
168
|
+
}
|
|
169
|
+
lines.push("");
|
|
170
|
+
|
|
171
|
+
lines.push("## Environment Variables (names only — values never logged)");
|
|
172
|
+
if (inv.envVars.length > 0) {
|
|
173
|
+
for (const v of inv.envVars) lines.push(`- ${v}`);
|
|
174
|
+
} else {
|
|
175
|
+
lines.push("- (none detected)");
|
|
176
|
+
}
|
|
177
|
+
lines.push("");
|
|
178
|
+
|
|
179
|
+
lines.push("## Features Available");
|
|
180
|
+
lines.push(`- Boardroom: ${inv.boardroom ? "YES" : "no"}`);
|
|
181
|
+
lines.push(`- Swarm/QIS: ${inv.swarm ? "YES" : "no"}`);
|
|
182
|
+
lines.push("");
|
|
183
|
+
|
|
184
|
+
lines.push("---");
|
|
185
|
+
lines.push("**Meta-rule:** If you need a capability, check this file first. If it's here, use it. If it's not here but you need it, see memory/CAPABILITIES.md for the self-extension checklist.");
|
|
186
|
+
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Run
|
|
191
|
+
const inv = scan();
|
|
192
|
+
const output = format(inv);
|
|
193
|
+
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
194
|
+
fs.writeFileSync(OUTPUT, output);
|
|
195
|
+
|
|
196
|
+
console.log(`Capability scan complete: ${inv.scripts.length + inv.srcFiles.length} files, ${inv.npmScripts.length} commands, ${inv.allowedTools.length} tools allowed. Written to ${OUTPUT}`);
|