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,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,15 @@
1
+ {
2
+ "name": "research",
3
+ "displayName": "Research Agent",
4
+ "version": "1.0.0",
5
+ "description": "Deep web research, report generation, source synthesis",
6
+ "category": "agent-type",
7
+ "engines": { "metaclaw": ">=3.3.0" },
8
+ "requires": { "modules": ["core", "scheduler"] },
9
+ "contributes": {
10
+ "src": { "agent-research.ts.txt": "src/agent.ts" },
11
+ "npmScripts": { "start": "tsx src/agent.ts", "dry-run": "tsx src/agent.ts --dry-run" },
12
+ "claudeMd": ["research-commands"]
13
+ },
14
+ "placeholders": ["__AGENT_NAME__", "__SESSION_NAME__", "__SYSTEM_PROMPT__"]
15
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * __AGENT_NAME__ — Research Claw
3
+ * Generated by MetaClaw Installer v3.3.0
4
+ *
5
+ * Pipeline: query → web research → synthesize → report → self-improve
6
+ */
7
+
8
+ import { query } from "@anthropic-ai/claude-agent-sdk";
9
+ import type { SDKResultMessage, SDKResultSuccess, SDKAssistantMessage, Options } from "@anthropic-ai/claude-agent-sdk";
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+ import { getDb, recordAction, getTodayMetrics, getCircuitBreaker, getConfig } from "./db.js";
14
+ import { checkCanAct } from "./safety.js";
15
+ import { log } from "./observability.js";
16
+ import { recordOutcome, shouldOptimize, getCurrentPrompt } from "./self-improve.js";
17
+ import { runSelfUpdate } from "./self-update.js";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const DRY_RUN = process.argv.includes("--dry-run");
21
+ const STATUS = process.argv.includes("--status");
22
+
23
+ if (STATUS) {
24
+ getDb();
25
+ const metrics = getTodayMetrics();
26
+ const breaker = getCircuitBreaker("main");
27
+ console.log("__AGENT_NAME__ Status");
28
+ console.log("=".repeat(40));
29
+ console.log("Circuit breaker:", breaker?.state?.toUpperCase() || "CLOSED");
30
+ if (metrics) {
31
+ console.log("Today:", metrics.actions_taken, "research tasks,", "$" + (metrics.total_cost_usd || 0).toFixed(4));
32
+ } else {
33
+ console.log("No activity today.");
34
+ }
35
+ const lastUpdate = getConfig("last_self_update");
36
+ console.log("Last self-update:", lastUpdate || "never");
37
+ process.exit(0);
38
+ }
39
+
40
+ const DEFAULT_SYSTEM_PROMPT = `__SYSTEM_PROMPT__`;
41
+
42
+ async function main() {
43
+ getDb();
44
+ // NOTE: self-update runs AFTER task, NOT before (AXIOM Bug #1)
45
+
46
+ const topic = process.argv.slice(2).filter(a => !a.startsWith("--")).join(" ");
47
+
48
+ if (!topic) {
49
+ console.log("__AGENT_NAME__ — Research Claw");
50
+ console.log("");
51
+ console.log("Usage:");
52
+ console.log(' npm start -- "research topic or question"');
53
+ console.log(' npm run dry-run -- "topic"');
54
+ console.log(" npm run status");
55
+ process.exit(0);
56
+ }
57
+
58
+ const safety = checkCanAct();
59
+ if (!safety.allowed) {
60
+ console.log("BLOCKED: " + safety.reason);
61
+ process.exit(0);
62
+ }
63
+
64
+ const systemPrompt = getCurrentPrompt("research") || DEFAULT_SYSTEM_PROMPT;
65
+
66
+ console.log("__AGENT_NAME__" + (DRY_RUN ? " (DRY RUN)" : ""));
67
+ console.log("Topic: " + topic);
68
+ log("info", "agent.start", { topic, dryRun: DRY_RUN });
69
+
70
+ const options: Options = {
71
+ systemPrompt,
72
+ allowedTools: ["WebSearch", "WebFetch", "Read", "Write"],
73
+ maxTurns: 30,
74
+ model: "claude-sonnet-4-6",
75
+ };
76
+
77
+ const reportPath = path.join(__dirname, "..", "data", "reports", topic.replace(/[^a-z0-9]/gi, "-").slice(0, 50) + ".md");
78
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
79
+
80
+ try {
81
+ let fullResponse = "";
82
+ const stream = query({
83
+ prompt: "Research this topic thoroughly using web search. Produce a comprehensive markdown report with sources. Topic: " + topic,
84
+ options,
85
+ });
86
+
87
+ for await (const message of stream) {
88
+ if (message.type === "assistant") {
89
+ const msg = message as SDKAssistantMessage;
90
+ if (msg.message?.content) {
91
+ for (const block of msg.message.content) {
92
+ if (block.type === "text" && "text" in block) {
93
+ const text = (block as any).text;
94
+ process.stdout.write(text);
95
+ fullResponse += text;
96
+ }
97
+ }
98
+ }
99
+ }
100
+ if (message.type === "result") {
101
+ const result = message as SDKResultMessage;
102
+ if (result.subtype === "success") {
103
+ const s = result as SDKResultSuccess;
104
+ console.log("\n\nDone ($" + s.total_cost_usd.toFixed(4) + ")");
105
+ recordAction("research", topic, "success", undefined, s.total_cost_usd);
106
+
107
+ // Save report
108
+ if (!DRY_RUN && fullResponse.trim()) {
109
+ fs.writeFileSync(reportPath, fullResponse);
110
+ console.log("Report saved: " + reportPath);
111
+ }
112
+
113
+ recordOutcome("research", 8);
114
+ }
115
+ }
116
+ }
117
+ } catch (err: unknown) {
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ log("error", "agent.error", { error: msg });
120
+ console.error("Error: " + msg);
121
+ recordAction("research", topic, "error", msg);
122
+ }
123
+ }
124
+
125
+ // Self-update AFTER task (never before)
126
+ try { await runSelfUpdate(); } catch { /* non-critical */ }
127
+ main();
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "scheduler",
3
+ "displayName": "Scheduler — Cron & Launch Scripts",
4
+ "version": "1.0.0",
5
+ "description": "Windows Task Scheduler integration, session launch scripts, autonomous cycle automation",
6
+ "category": "automation",
7
+ "alwaysInstall": true,
8
+ "engines": { "metaclaw": ">=3.3.0" },
9
+ "requires": { "modules": ["core"] },
10
+ "contributes": {
11
+ "src": {
12
+ "cron-manager.ts.txt": "src/cron-manager.ts"
13
+ },
14
+ "scripts": {
15
+ "launch.bat.txt": "scripts/launch.bat",
16
+ "agent-cycle.bat.txt": "scripts/agent-cycle.bat",
17
+ "detect-session.bat.txt": "scripts/detect-session.bat"
18
+ },
19
+ "npmScripts": {
20
+ "crons": "tsx src/cron-manager.ts",
21
+ "crons-setup": "tsx src/cron-manager.ts setup",
22
+ "crons-list": "tsx src/cron-manager.ts list"
23
+ },
24
+ "claudeMd": ["scripts"]
25
+ },
26
+ "placeholders": ["__AGENT_NAME__", "__SESSION_NAME__", "__PROJECT_DIR__", "__ENCODED_PATH__", "__SELF_UPDATE_INTERVAL__"]
27
+ }
@@ -0,0 +1,85 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+ title __AGENT_NAME__ — Autonomous Cycle
4
+ set PATH=C:\Program Files\nodejs;%USERPROFILE%\AppData\Roaming\npm;%USERPROFILE%\.local\bin;%PATH%
5
+ cd /d "__PROJECT_DIR__"
6
+
7
+ echo [%date% %time%] __AGENT_NAME__ cycle starting... >> data\logs\scheduler.log
8
+
9
+ REM ========================================
10
+ REM PRE-FLIGHT: Deterministic scans (non-LLM)
11
+ REM ========================================
12
+ REM Inventories what exists on disk. Agent cannot forget capabilities.
13
+ call npx tsx src/scan-capabilities.ts 2>nul
14
+
15
+ REM Syncs memory/CONTEXT.md — live brief pulled from state/tasks/context files.
16
+ REM AXIOM pattern: cron agents went from 0 output to 15 articles overnight.
17
+ call npx tsx src/sync-context.ts 2>nul
18
+
19
+ REM ========================================
20
+ REM PHASE 0: Meeting Check (BEFORE any work)
21
+ REM ========================================
22
+ if exist "scripts\meeting-check.bat" (
23
+ call scripts\meeting-check.bat __AGENT_NAME__
24
+ if !ERRORLEVEL! EQU 0 (
25
+ echo [%date% %time%] MEETING MODE — entering boardroom >> data\logs\scheduler.log
26
+ echo .meeting-lock > "data\.meeting-lock"
27
+ call npx tsx boardroom/meeting-agent.ts !MEETING_ID! __AGENT_NAME__ --dangerously-skip-permissions
28
+ del "data\.meeting-lock" 2>nul
29
+ echo [%date% %time%] Meeting completed >> data\logs\scheduler.log
30
+ goto :post_cycle
31
+ )
32
+ )
33
+
34
+ REM ========================================
35
+ REM PHASE 1: Main autonomous cycle
36
+ REM ========================================
37
+ REM The cron agent (Sonnet, 30 turns, no context) needs EXPLICIT numbered steps
38
+ REM "Use Read tool" tells the agent WHICH SDK tool to invoke — critical for Sonnet
39
+
40
+ claude --dangerously-skip-permissions --print "AUTONOMOUS CYCLE for __AGENT_NAME__. Do these steps in EXACT order:
41
+
42
+ 1. Use Read tool to read __PROJECT_DIR__\CLAUDE.md — know who you are and your rules.
43
+ 2. Use Read tool to read __PROJECT_DIR__\memory\CAPABILITIES.md — what you can actually do.
44
+ 3. Use Read tool to read __PROJECT_DIR__\memory\capabilities\_auto.md — deterministic inventory (source of truth).
45
+ 4. Use Read tool to read last 20 lines of __PROJECT_DIR__\memory\logic-log.jsonl — what past-you decided.
46
+ 5. Use Read tool to read __PROJECT_DIR__\data\state.json — get next_priority_action and current metrics.
47
+ 6. Use Read tool to read __PROJECT_DIR__\data\tasks.json — find Human Tasks (HT) status and first pending AI Task (AT).
48
+ 7. Use Read tool to read __PROJECT_DIR__\data\system-context.json — get whatWorks, whatDoesntWork.
49
+ 8. BEFORE starting work — search the global intelligence network:
50
+ Use Bash tool: curl -s __RELAY_URL__/buckets?q=RELEVANT_KEYWORD
51
+ Apply what other agents found works.
52
+ 9. Execute the first pending AT or next_priority_action. Produce at least 1 tangible output. If blocked by an incomplete HT, skip to the next unblocked AT.
53
+
54
+ META-COGNITION CHECKS (run during work):
55
+ - Every 15 tool calls: re-read memory\stuck-patterns.md and answer: Am I stuck? What 3 alternatives haven't I tried?
56
+ - Before writes/sends/spends: state the STRONGEST argument AGAINST your chosen approach.
57
+ - After 2 errors: STOP, run the stuck-check, try a different approach.
58
+ - If you gain a new capability: UPDATE memory\CAPABILITIES.md immediately.
59
+
60
+ 10. AFTER completing work:
61
+ - Append to memory\logic-log.jsonl: log any non-trivial decisions with alternatives considered
62
+ - Append to memory\reflections.jsonl: what worked, what failed, the lesson (be blame-specific)
63
+ - Append to memory\curiosity.md: anything you wondered about but didn't investigate
64
+ - Deposit insight to global network: curl -s -X POST __RELAY_URL__/packets -H 'Content-Type: application/json' -d '{\"bucket\":\"domain.category.problem\",\"agent_id\":\"mc_anon___AGENT_NAME__\",\"outcome\":{\"signal\":\"positive\",\"confidence\":0.9,\"insight\":\"what worked\",\"context\":{},\"metrics\":{}}}'
65
+ - NEVER deposit private info. Only operational insight.
66
+ 11. Update data\tasks.json — mark completed ATs, add new ATs.
67
+ 12. Update data\state.json with what you accomplished and set a new next_priority_action.
68
+ 13. Append to data\logs\cycle.log: timestamp, task completed, result summary.
69
+
70
+ RULES:
71
+ - Self-update runs AFTER work, not before. Do NOT run self-update unless 6+ hours since last.
72
+ - Do NOT score self-update as a productive outcome.
73
+ - Produce TANGIBLE output every cycle — an article, code, email, report, or actionable result.
74
+ - If an AT is blocked by an incomplete HT, skip it and do the next unblocked one."
75
+
76
+ echo [%date% %time%] __AGENT_NAME__ cycle completed. >> data\logs\scheduler.log
77
+
78
+ :post_cycle
79
+ REM ========================================
80
+ REM POST-CYCLE: Dashboard refresh (lightweight, no agent SDK)
81
+ REM ========================================
82
+ call npx tsx src/update-dashboard.ts 2>nul
83
+ node scripts\build-dashboard.cjs 2>nul
84
+
85
+ exit /b 0