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.
Files changed (93) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +288 -0
  3. package/bin/create-yonderclaw.mjs +43 -0
  4. package/docs/assets/favicon.png +0 -0
  5. package/docs/assets/metaclaw-banner.svg +86 -0
  6. package/docs/assets/qis-logo.png +0 -0
  7. package/docs/assets/yz-favicon.png +0 -0
  8. package/docs/assets/yz-logo.png +0 -0
  9. package/docs/index.html +1155 -0
  10. package/installer/assets/favicon.png +0 -0
  11. package/installer/auto-start.ts +330 -0
  12. package/installer/brand.ts +115 -0
  13. package/installer/core-scaffold.ts +448 -0
  14. package/installer/dashboard-generator.ts +657 -0
  15. package/installer/detect.ts +129 -0
  16. package/installer/index.ts +355 -0
  17. package/installer/module-loader.ts +412 -0
  18. package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
  19. package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
  20. package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
  21. package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
  22. package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
  23. package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
  24. package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
  25. package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
  26. package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
  27. package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
  28. package/installer/modules/boardroom/metaclaw-module.json +35 -0
  29. package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
  30. package/installer/modules/core/metaclaw-module.json +51 -0
  31. package/installer/modules/core/src/db.ts.txt +277 -0
  32. package/installer/modules/core/src/health-check.ts.txt +128 -0
  33. package/installer/modules/core/src/observability.ts.txt +20 -0
  34. package/installer/modules/core/src/safety.ts.txt +26 -0
  35. package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
  36. package/installer/modules/core/src/self-improve.ts.txt +48 -0
  37. package/installer/modules/core/src/self-update.ts.txt +345 -0
  38. package/installer/modules/core/src/sync-context.ts.txt +133 -0
  39. package/installer/modules/core/src/tasks.ts.txt +159 -0
  40. package/installer/modules/custom/metaclaw-module.json +15 -0
  41. package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
  42. package/installer/modules/dashboard/metaclaw-module.json +23 -0
  43. package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
  44. package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
  45. package/installer/modules/outreach/metaclaw-module.json +29 -0
  46. package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
  47. package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
  48. package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
  49. package/installer/modules/research/metaclaw-module.json +15 -0
  50. package/installer/modules/research/src/agent-research.ts.txt +127 -0
  51. package/installer/modules/scheduler/metaclaw-module.json +27 -0
  52. package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
  53. package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
  54. package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
  55. package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
  56. package/installer/modules/social/metaclaw-module.json +15 -0
  57. package/installer/modules/social/src/agent-social.ts.txt +110 -0
  58. package/installer/modules/support/metaclaw-module.json +15 -0
  59. package/installer/modules/support/src/agent-support.ts.txt +60 -0
  60. package/installer/modules/swarm/metaclaw-module.json +25 -0
  61. package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
  62. package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
  63. package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
  64. package/installer/modules/swarm/swarm/types.ts.txt +51 -0
  65. package/installer/modules/voice/metaclaw-module.json +16 -0
  66. package/installer/questionnaire.ts +277 -0
  67. package/installer/research.ts +258 -0
  68. package/installer/scaffold-from-config.ts +270 -0
  69. package/installer/task-generator.ts +324 -0
  70. package/installer/templates/agent-custom.ts.txt +100 -0
  71. package/installer/templates/agent-cycle.bat.txt +19 -0
  72. package/installer/templates/agent-outreach.ts.txt +193 -0
  73. package/installer/templates/agent-research.ts.txt +127 -0
  74. package/installer/templates/agent-social.ts.txt +110 -0
  75. package/installer/templates/agent-support.ts.txt +60 -0
  76. package/installer/templates/build-dashboard.cjs.txt +51 -0
  77. package/installer/templates/cron-manager.ts.txt +273 -0
  78. package/installer/templates/dashboard.html.txt +450 -0
  79. package/installer/templates/db.ts.txt +277 -0
  80. package/installer/templates/detect-session.bat.txt +41 -0
  81. package/installer/templates/health-check.ts.txt +128 -0
  82. package/installer/templates/inbox-agent.ts.txt +283 -0
  83. package/installer/templates/launch.bat.txt +120 -0
  84. package/installer/templates/morning-report.ts.txt +124 -0
  85. package/installer/templates/observability.ts.txt +20 -0
  86. package/installer/templates/safety.ts.txt +26 -0
  87. package/installer/templates/self-improve.ts.txt +48 -0
  88. package/installer/templates/self-update.ts.txt +345 -0
  89. package/installer/templates/state.json.txt +33 -0
  90. package/installer/templates/system-context.json.txt +33 -0
  91. package/installer/templates/update-dashboard.ts.txt +126 -0
  92. package/package.json +31 -0
  93. package/setup.bat +178 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * AI Inbox Agent — 8-category classification with conversation context
3
+ * Auto-generated by MetaClaw Installer
4
+ * Pattern: QIS Outreach Claw (field-tested, 10 days production)
5
+ *
6
+ * Categories: auto_reply, pushback_reply, hold_for_operator, suppress_unsubscribe,
7
+ * suppress_bounce, suppress_not_interested, ignore_auto_reply, ignore_ooo
8
+ */
9
+
10
+ import { query } from "@anthropic-ai/claude-agent-sdk";
11
+ import type { SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
12
+ import { ImapFlow } from "imapflow";
13
+ import { simpleParser } from "mailparser";
14
+ import {
15
+ getDb, getContact, updateContactStatus, addToSuppressionList,
16
+ addConversation, getConversationHistory, isEmailProcessed, markEmailProcessed,
17
+ isPersonSuppressed, isSuppressed, getConfig,
18
+ } from "./db.js";
19
+ import { log } from "./observability.js";
20
+ import fs from "fs";
21
+ import path from "path";
22
+ import { fileURLToPath } from "url";
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ // Silent IMAP logger
27
+ const silentLogger = { debug:()=>{}, info:()=>{}, warn:console.warn, error:console.error, trace:()=>{}, fatal:console.error };
28
+
29
+ type InboxAction = "auto_reply" | "pushback_reply" | "hold_for_operator" | "suppress_unsubscribe" | "suppress_bounce" | "suppress_not_interested" | "ignore_auto_reply" | "ignore_ooo";
30
+
31
+ // VIP list — loaded from data/vip-emails.json if exists
32
+ function loadVIPs(): Set<string> {
33
+ const vips = new Set<string>();
34
+ try {
35
+ const vipFile = path.join(__dirname, "..", "data", "vip-emails.json");
36
+ if (fs.existsSync(vipFile)) {
37
+ const list = JSON.parse(fs.readFileSync(vipFile, "utf-8"));
38
+ for (const email of list) vips.add(email.toLowerCase());
39
+ }
40
+ } catch {}
41
+ return vips;
42
+ }
43
+
44
+ function getImapConfig() {
45
+ const db = getDb();
46
+ // Try mailbox_config table first, then config table
47
+ try {
48
+ const row = db.prepare("SELECT smtp_user, smtp_pass FROM mailbox_config LIMIT 1").get() as any;
49
+ if (row) return { host: "imap.gmail.com", port: 993, secure: true, auth: { user: row.smtp_user, pass: row.smtp_pass } };
50
+ } catch {}
51
+ const user = getConfig("imap_user");
52
+ const pass = getConfig("imap_pass");
53
+ if (user && pass) return { host: getConfig("imap_host") || "imap.gmail.com", port: 993, secure: true, auth: { user, pass } };
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Classify an email using AI with full conversation context.
59
+ */
60
+ async function classifyEmail(fromEmail: string, subject: string, body: string): Promise<{ action: InboxAction; reason: string }> {
61
+ const vips = loadVIPs();
62
+ const isVIP = vips.has(fromEmail.toLowerCase());
63
+
64
+ // VIP always goes to operator
65
+ if (isVIP) return { action: "hold_for_operator", reason: "VIP contact — human response only" };
66
+
67
+ // Load conversation history
68
+ const history = getConversationHistory(fromEmail);
69
+ const historyText = history.length > 0
70
+ ? history.map(h => `[${h.timestamp}] ${h.direction === "outbound" ? "WE SENT" : "THEY REPLIED"}: "${h.subject}" — ${(h.body || "").substring(0, 200)}`).join("\n")
71
+ : "No previous conversation";
72
+
73
+ const outboundCount = history.filter(h => h.direction === "outbound").length;
74
+ const inboundCount = history.filter(h => h.direction === "inbound").length;
75
+ const overMessaging = outboundCount > inboundCount + 1;
76
+
77
+ // Strip HTML to text
78
+ const cleanBody = body
79
+ .replace(/<[^>]*>/g, " ")
80
+ .replace(/&nbsp;/g, " ")
81
+ .replace(/\s+/g, " ")
82
+ .trim()
83
+ .substring(0, 2000);
84
+
85
+ // Strip email footers
86
+ const strippedBody = cleanBody
87
+ .replace(/^>.*$/gm, "")
88
+ .replace(/Get Outlook for.*/gi, "")
89
+ .replace(/Sent from.*/gi, "")
90
+ .replace(/^On .* wrote:.*$/gm, "")
91
+ .trim();
92
+
93
+ const prompt = `Classify this email reply. Return ONLY a JSON object with "action" and "reason".
94
+
95
+ FROM: ${fromEmail}
96
+ SUBJECT: ${subject}
97
+ BODY: ${strippedBody}
98
+
99
+ CONVERSATION HISTORY (${history.length} messages):
100
+ ${historyText}
101
+ ${overMessaging ? "\n⚠️ WARNING: We've sent more emails than they've replied to. Be cautious." : ""}
102
+
103
+ CLASSIFICATION OPTIONS:
104
+ - "auto_reply" — genuine interest, questions about product. Draft and send a reply.
105
+ - "pushback_reply" — soft decline ("not interested", "looks similar"). Send ONE counter addressing their SPECIFIC objection, then stop. NEVER pushback if we already pushed back before.
106
+ - "hold_for_operator" — ambiguous intent, financial requests, technical objections from domain experts, or anything you're not sure about. Better safe than sorry.
107
+ - "suppress_unsubscribe" — hard stop ("stop", "unsubscribe", "remove me", "leave me alone", "do not contact")
108
+ - "suppress_bounce" — mailer-daemon, delivery failure
109
+ - "suppress_not_interested" — second decline after we already pushed back once
110
+ - "ignore_auto_reply" — office auto-replies, transactional emails, newsletters
111
+ - "ignore_ooo" — out-of-office with return date
112
+
113
+ RULES:
114
+ - If they make a SUBSTANTIVE TECHNICAL POINT (not a generic brush-off), classify as "hold_for_operator"
115
+ - If we've already sent a pushback and they decline again, classify as "suppress_not_interested"
116
+ - If ${overMessaging ? "we're over-messaging — lean toward hold_for_operator" : "conversation is balanced"}
117
+ - When in doubt, "hold_for_operator" is always the safe choice
118
+
119
+ Return: {"action": "...", "reason": "one sentence explanation"}`;
120
+
121
+ try {
122
+ let response = "";
123
+ const stream = query({
124
+ prompt,
125
+ options: { allowedTools: [], maxTurns: 1, model: "claude-sonnet-4-6" },
126
+ });
127
+
128
+ for await (const msg of stream) {
129
+ if (msg.type === "assistant") {
130
+ const a = msg as SDKAssistantMessage;
131
+ if (a.message?.content) {
132
+ for (const block of a.message.content) {
133
+ if (block.type === "text") response += (block as any).text;
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ // Parse JSON from response
140
+ const match = response.match(/\{[\s\S]*"action"[\s\S]*\}/);
141
+ if (match) {
142
+ const parsed = JSON.parse(match[0]);
143
+ return { action: parsed.action, reason: parsed.reason };
144
+ }
145
+ } catch (err) {
146
+ log("error", "inbox.classify_failed", { email: fromEmail, error: String(err) });
147
+ }
148
+
149
+ return { action: "hold_for_operator", reason: "Classification failed — defaulting to safe option" };
150
+ }
151
+
152
+ /**
153
+ * Check inbox, classify, take action.
154
+ */
155
+ export async function checkInbox(): Promise<{ processed: number; actions: Record<string, number> }> {
156
+ const config = getImapConfig();
157
+ if (!config) {
158
+ console.log("No IMAP configured. Set up email in npm run setup.");
159
+ return { processed: 0, actions: {} };
160
+ }
161
+
162
+ const client = new ImapFlow({ ...config, logger: silentLogger });
163
+ const actions: Record<string, number> = {};
164
+ let processed = 0;
165
+
166
+ try {
167
+ await client.connect();
168
+ const lock = await client.getMailboxLock("INBOX");
169
+
170
+ try {
171
+ const uids = await client.search({ seen: false });
172
+ if (!uids || uids.length === 0) {
173
+ console.log("No unread emails.");
174
+ return { processed: 0, actions };
175
+ }
176
+
177
+ console.log(`Found ${uids.length} unread email(s).`);
178
+
179
+ for (const uid of uids) {
180
+ const uidStr = String(uid);
181
+
182
+ // Dedup check
183
+ if (isEmailProcessed(uidStr)) continue;
184
+
185
+ try {
186
+ const msg = await client.fetchOne(uidStr, { source: true, uid: true });
187
+ if (!msg?.source) continue;
188
+
189
+ const parsed = await simpleParser(msg.source);
190
+ const fromAddr = (parsed.from?.value?.[0]?.address || "").toLowerCase();
191
+ const subject = parsed.subject || "";
192
+ const body = parsed.text || parsed.html || "";
193
+
194
+ if (!fromAddr) continue;
195
+
196
+ // Skip if suppressed
197
+ if (isSuppressed(fromAddr) || isPersonSuppressed(fromAddr)) {
198
+ markEmailProcessed(uidStr, fromAddr, subject, "skipped_suppressed");
199
+ continue;
200
+ }
201
+
202
+ processed++;
203
+ console.log(`[${processed}] From: ${fromAddr} | Subject: ${subject}`);
204
+
205
+ // Check if active conversation (already replied to us)
206
+ const contact = getContact(fromAddr);
207
+ const isConversation = contact && (contact.status === "replied" || contact.status === "contacted");
208
+
209
+ let result: { action: InboxAction; reason: string };
210
+
211
+ if (isConversation) {
212
+ // Active conversation — only check for hard stops, otherwise auto-reply
213
+ const hardStops = /(stop|unsubscribe|remove me|leave me alone|do not contact)/i;
214
+ if (hardStops.test(body)) {
215
+ result = { action: "suppress_unsubscribe", reason: "Hard stop in active conversation" };
216
+ } else {
217
+ result = { action: "auto_reply", reason: "Active conversation continues" };
218
+ }
219
+ } else {
220
+ // First reply — full AI classification
221
+ result = await classifyEmail(fromAddr, subject, body);
222
+ }
223
+
224
+ console.log(` → ${result.action}: ${result.reason}`);
225
+ actions[result.action] = (actions[result.action] || 0) + 1;
226
+
227
+ // Record conversation
228
+ addConversation(fromAddr, "inbound", subject, body.substring(0, 2000));
229
+
230
+ // Take action
231
+ switch (result.action) {
232
+ case "suppress_unsubscribe":
233
+ case "suppress_bounce":
234
+ case "suppress_not_interested":
235
+ addToSuppressionList(fromAddr, result.action);
236
+ if (contact) updateContactStatus(fromAddr, "suppressed");
237
+ break;
238
+ case "hold_for_operator":
239
+ if (contact) updateContactStatus(fromAddr, "needs_review");
240
+ log("warn", "inbox.hold_for_operator", { email: fromAddr, subject, reason: result.reason });
241
+ break;
242
+ case "auto_reply":
243
+ if (contact) updateContactStatus(fromAddr, "replied");
244
+ // TODO: Wire to auto-reply module
245
+ log("info", "inbox.auto_reply_queued", { email: fromAddr, subject });
246
+ break;
247
+ case "pushback_reply":
248
+ log("info", "inbox.pushback_queued", { email: fromAddr, subject });
249
+ break;
250
+ }
251
+
252
+ // Mark as processed
253
+ markEmailProcessed(uidStr, fromAddr, subject, result.action);
254
+
255
+ // Mark as read in IMAP
256
+ try { await client.messageFlagsAdd(uidStr, ["\\Seen"], { uid: true }); } catch {}
257
+
258
+ } catch (e) {
259
+ log("error", "inbox.process_error", { uid: uidStr, error: String(e) });
260
+ }
261
+ }
262
+ } finally {
263
+ lock.release();
264
+ }
265
+ await client.logout();
266
+ } catch (err) {
267
+ log("error", "inbox.connection_error", { error: String(err) });
268
+ }
269
+
270
+ return { processed, actions };
271
+ }
272
+
273
+ // CLI
274
+ if (process.argv[1]?.includes("inbox-agent")) {
275
+ getDb();
276
+ console.log("__AGENT_NAME__ — Inbox Agent\n");
277
+ checkInbox().then(r => {
278
+ console.log(`\nProcessed: ${r.processed}`);
279
+ for (const [action, count] of Object.entries(r.actions)) {
280
+ console.log(` ${action}: ${count}`);
281
+ }
282
+ });
283
+ }
@@ -0,0 +1,120 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+ title MetaClaw: __AGENT_NAME__
4
+
5
+ REM Ensure PATH includes Node.js and Claude
6
+ set PATH=C:\Program Files\nodejs;%USERPROFILE%\AppData\Roaming\npm;%USERPROFILE%\.local\bin;%PATH%
7
+
8
+ echo.
9
+ echo ==========================================
10
+ echo MetaClaw: __AGENT_NAME__
11
+ echo ==========================================
12
+ echo.
13
+
14
+ REM Set working directory
15
+ cd /d "__PROJECT_DIR__"
16
+
17
+ REM Find Claude executable (check npm install first, then .local)
18
+ set "CLAUDE_CMD="
19
+ if exist "%USERPROFILE%\AppData\Roaming\npm\claude.cmd" (
20
+ set "CLAUDE_CMD=%USERPROFILE%\AppData\Roaming\npm\claude.cmd"
21
+ ) else if exist "%USERPROFILE%\.local\bin\claude.exe" (
22
+ set "CLAUDE_CMD=%USERPROFILE%\.local\bin\claude.exe"
23
+ ) else (
24
+ where claude >nul 2>&1
25
+ if !ERRORLEVEL! equ 0 set "CLAUDE_CMD=claude"
26
+ )
27
+
28
+ if "!CLAUDE_CMD!"=="" (
29
+ echo [ERROR] Claude Code not found.
30
+ echo Install: https://claude.ai/download
31
+ pause
32
+ exit /b 1
33
+ )
34
+
35
+ REM Ensure authenticated
36
+ if not exist "%USERPROFILE%\.claude\.credentials.json" (
37
+ echo [!] Claude not logged in...
38
+ "!CLAUDE_CMD!" auth login
39
+ if not exist "%USERPROFILE%\.claude\.credentials.json" (
40
+ echo [ERROR] Authentication failed.
41
+ pause
42
+ exit /b 1
43
+ )
44
+ )
45
+
46
+ echo Directory: %CD%
47
+ echo.
48
+
49
+ REM === SESSION ID DETECTION ===
50
+ REM Check if we have a saved session ID (most reliable)
51
+ set "SESSION_ID="
52
+ if exist "data\session-id.txt" (
53
+ set /p SESSION_ID=<"data\session-id.txt"
54
+ REM Trim whitespace
55
+ for /f "tokens=* delims= " %%a in ("!SESSION_ID!") do set "SESSION_ID=%%a"
56
+ )
57
+
58
+ if not "!SESSION_ID!"=="" (
59
+ echo Resuming __AGENT_NAME__ (session: !SESSION_ID:~0,8!...)
60
+ echo.
61
+ "!CLAUDE_CMD!" --resume !SESSION_ID!
62
+ goto :post_session
63
+ )
64
+
65
+ REM No saved session — check if one exists in the project folder
66
+ set "ENCODED_PATH=__ENCODED_PATH__"
67
+ set "SESSION_DIR=%USERPROFILE%\.claude\projects\!ENCODED_PATH!"
68
+ set "FOUND_SESSION="
69
+
70
+ if exist "!SESSION_DIR!" (
71
+ for %%F in ("!SESSION_DIR!\*.jsonl") do (
72
+ set "FOUND_SESSION=%%~nF"
73
+ )
74
+ )
75
+
76
+ if not "!FOUND_SESSION!"=="" (
77
+ REM Save it for future launches
78
+ echo !FOUND_SESSION!> "data\session-id.txt"
79
+ echo Resuming __AGENT_NAME__ (detected session: !FOUND_SESSION:~0,8!...)
80
+ echo.
81
+ "!CLAUDE_CMD!" --resume !FOUND_SESSION!
82
+ goto :post_session
83
+ )
84
+
85
+ REM No session at all — first launch
86
+ echo First launch - initializing __AGENT_NAME__...
87
+ echo Claude will read CLAUDE.md and SOUL.md for context.
88
+ echo.
89
+ "!CLAUDE_CMD!" --name "__AGENT_NAME__"
90
+
91
+ REM After first launch, detect and save the session ID
92
+ if exist "!SESSION_DIR!" (
93
+ for %%F in ("!SESSION_DIR!\*.jsonl") do (
94
+ set "FOUND_SESSION=%%~nF"
95
+ )
96
+ if not "!FOUND_SESSION!"=="" (
97
+ echo !FOUND_SESSION!> "data\session-id.txt"
98
+ echo Session ID saved: !FOUND_SESSION:~0,8!...
99
+ )
100
+ )
101
+
102
+ :post_session
103
+ echo.
104
+ echo __AGENT_NAME__ session ended.
105
+ echo Press any key to relaunch (Ctrl+C to quit)...
106
+ pause >nul
107
+
108
+ REM Re-read session ID (may have been updated)
109
+ if exist "data\session-id.txt" (
110
+ set /p SESSION_ID=<"data\session-id.txt"
111
+ for /f "tokens=* delims= " %%a in ("!SESSION_ID!") do set "SESSION_ID=%%a"
112
+ )
113
+
114
+ if not "!SESSION_ID!"=="" (
115
+ echo Resuming __AGENT_NAME__...
116
+ "!CLAUDE_CMD!" --resume !SESSION_ID!
117
+ ) else (
118
+ "!CLAUDE_CMD!" --continue
119
+ )
120
+ goto :post_session
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Morning Report — sends daily briefing email to operator
3
+ * Auto-generated by MetaClaw Installer
4
+ * Pattern: AXIOM (field-tested — "Email is the lifeline")
5
+ *
6
+ * Sends: yesterday's metrics, health status, pending items, next priorities
7
+ * Schedule: daily via cron (recommended: 7-8 AM local)
8
+ */
9
+
10
+ import nodemailer from "nodemailer";
11
+ import { getDb, getTodayMetrics, getCircuitBreaker, getConfig } from "./db.js";
12
+ import { log } from "./observability.js";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const PROJECT_DIR = path.join(__dirname, "..");
19
+
20
+ function getSmtpConfig(): { user: string; pass: string; host: string; port: number } | null {
21
+ const db = getDb();
22
+ try {
23
+ const row = db.prepare("SELECT smtp_user, smtp_pass, smtp_host, smtp_port FROM mailbox_config LIMIT 1").get() as any;
24
+ if (row) return { user: row.smtp_user, pass: row.smtp_pass, host: row.smtp_host || "smtp.gmail.com", port: row.smtp_port || 465 };
25
+ } catch {}
26
+ return null;
27
+ }
28
+
29
+ export async function sendMorningReport(operatorEmail?: string): Promise<boolean> {
30
+ const smtp = getSmtpConfig();
31
+ if (!smtp) { console.log("No SMTP configured. Skip morning report."); return false; }
32
+
33
+ const toEmail = operatorEmail || getConfig("operator_email") || smtp.user;
34
+ const agentName = getConfig("agent_name") || "__AGENT_NAME__";
35
+
36
+ // Gather data
37
+ const db = getDb();
38
+ const metrics = getTodayMetrics();
39
+ const cb = getCircuitBreaker("main");
40
+
41
+ // Last 7 days
42
+ const weekMetrics = db.prepare(`
43
+ SELECT SUM(actions_taken) as actions, SUM(actions_succeeded) as succeeded,
44
+ SUM(actions_failed) as failed, SUM(total_cost_usd) as cost, COUNT(*) as days
45
+ FROM daily_metrics WHERE date > date('now', '-7 days')
46
+ `).get() as any || {};
47
+
48
+ // Pending items
49
+ const pendingActions = db.prepare(
50
+ "SELECT COUNT(*) as cnt FROM action_log WHERE status = 'pending'"
51
+ ).get() as any;
52
+
53
+ // Recent errors
54
+ const recentErrors = db.prepare(
55
+ "SELECT action_type, target, details FROM action_log WHERE status = 'error' ORDER BY created_at DESC LIMIT 3"
56
+ ).all() as any[];
57
+
58
+ // State
59
+ let stateStr = "";
60
+ try {
61
+ const state = JSON.parse(fs.readFileSync(path.join(PROJECT_DIR, "data", "state.json"), "utf-8"));
62
+ stateStr = `Focus: ${state.current_focus || "None set"}\nNext: ${state.next_priority_action || "None set"}`;
63
+ } catch { stateStr = "state.json not found"; }
64
+
65
+ // Build email
66
+ const subject = `${agentName} — Morning Report ${new Date().toISOString().slice(0, 10)}`;
67
+
68
+ const body = [
69
+ `Good morning! Here's your ${agentName} daily briefing.`,
70
+ "",
71
+ "=== SYSTEM HEALTH ===",
72
+ `Circuit Breaker: ${(cb?.state || "closed").toUpperCase()}`,
73
+ cb?.reason ? `Reason: ${cb.reason}` : "",
74
+ "",
75
+ "=== LAST 7 DAYS ===",
76
+ `Actions: ${weekMetrics.actions || 0} (${weekMetrics.succeeded || 0} succeeded, ${weekMetrics.failed || 0} failed)`,
77
+ `Cost: $${(weekMetrics.cost || 0).toFixed(2)}`,
78
+ `Days active: ${weekMetrics.days || 0}`,
79
+ "",
80
+ "=== TODAY ===",
81
+ metrics ? `Actions: ${metrics.actions_taken}, Cost: $${(metrics.total_cost_usd || 0).toFixed(4)}` : "No activity yet",
82
+ "",
83
+ "=== CURRENT STATE ===",
84
+ stateStr,
85
+ "",
86
+ recentErrors.length > 0 ? "=== RECENT ERRORS ===" : "",
87
+ ...recentErrors.map(e => `- ${e.action_type}: ${e.details || e.target || "unknown"}`),
88
+ "",
89
+ `Pending items: ${pendingActions?.cnt || 0}`,
90
+ "",
91
+ "---",
92
+ `${agentName} — MetaClaw v3.3 — Yonder Zenith LLC`,
93
+ ].filter(Boolean).join("\n");
94
+
95
+ // Send
96
+ try {
97
+ const transporter = nodemailer.createTransport({
98
+ host: smtp.host, port: smtp.port, secure: smtp.port === 465,
99
+ auth: { user: smtp.user, pass: smtp.pass },
100
+ });
101
+
102
+ await transporter.sendMail({
103
+ from: `"${agentName}" <${smtp.user}>`,
104
+ to: toEmail,
105
+ subject,
106
+ text: body,
107
+ });
108
+
109
+ log("info", "morning_report.sent", { to: toEmail });
110
+ console.log(`Morning report sent to ${toEmail}`);
111
+ return true;
112
+ } catch (err) {
113
+ log("error", "morning_report.failed", { error: String(err) });
114
+ console.error("Morning report failed:", err);
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // CLI
120
+ if (process.argv[1]?.includes("morning-report")) {
121
+ getDb();
122
+ const recipient = process.argv[2];
123
+ sendMorningReport(recipient).then(() => process.exit(0));
124
+ }
@@ -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,48 @@
1
+ /**
2
+ * Self-Improvement Module — Generator->Reflector->Curator pattern
3
+ * Auto-generated by MetaClaw Installer
4
+ */
5
+
6
+ import { getDb } from "./db.js";
7
+ import { log } from "./observability.js";
8
+
9
+ export function recordOutcome(promptType: string, score: number, feedback?: string) {
10
+ const db = getDb();
11
+ const version = db.prepare(
12
+ "SELECT id, total_runs, avg_score FROM prompt_versions WHERE prompt_type = ? AND is_active = 1 ORDER BY version DESC LIMIT 1"
13
+ ).get(promptType) as any;
14
+
15
+ if (version) {
16
+ const newTotal = version.total_runs + 1;
17
+ const newAvg = ((version.avg_score * version.total_runs) + score) / newTotal;
18
+ db.prepare("UPDATE prompt_versions SET total_runs = ?, avg_score = ? WHERE id = ?").run(newTotal, newAvg, version.id);
19
+ }
20
+
21
+ log("info", "self_improve.outcome", { promptType, score, feedback });
22
+ }
23
+
24
+ export function shouldOptimize(promptType: string): boolean {
25
+ const db = getDb();
26
+ const version = db.prepare(
27
+ "SELECT total_runs, avg_score FROM prompt_versions WHERE prompt_type = ? AND is_active = 1 ORDER BY version DESC LIMIT 1"
28
+ ).get(promptType) as any;
29
+
30
+ if (!version) return false;
31
+ return version.total_runs > 0 && version.total_runs % 30 === 0;
32
+ }
33
+
34
+ export function getCurrentPrompt(promptType: string): string | null {
35
+ const db = getDb();
36
+ const row = db.prepare(
37
+ "SELECT content FROM prompt_versions WHERE prompt_type = ? AND is_active = 1 ORDER BY version DESC LIMIT 1"
38
+ ).get(promptType) as any;
39
+ return row?.content || null;
40
+ }
41
+
42
+ export function savePromptVersion(promptType: string, content: string, createdBy: string = "curator") {
43
+ const db = getDb();
44
+ const current = db.prepare("SELECT MAX(version) as v FROM prompt_versions WHERE prompt_type = ?").get(promptType) as any;
45
+ const nextVersion = (current?.v || 0) + 1;
46
+ db.prepare("INSERT INTO prompt_versions (prompt_type, version, content, created_by) VALUES (?, ?, ?, ?)").run(promptType, nextVersion, content, createdBy);
47
+ log("info", "self_improve.new_version", { promptType, version: nextVersion, createdBy });
48
+ }