create-metaclaw 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +44 -0
- package/README.md +282 -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 +895 -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,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(/ /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
|