create-yonderclaw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +44 -0
- package/README.md +288 -0
- package/bin/create-yonderclaw.mjs +43 -0
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/metaclaw-banner.svg +86 -0
- package/docs/assets/qis-logo.png +0 -0
- package/docs/assets/yz-favicon.png +0 -0
- package/docs/assets/yz-logo.png +0 -0
- package/docs/index.html +1155 -0
- package/installer/assets/favicon.png +0 -0
- package/installer/auto-start.ts +330 -0
- package/installer/brand.ts +115 -0
- package/installer/core-scaffold.ts +448 -0
- package/installer/dashboard-generator.ts +657 -0
- package/installer/detect.ts +129 -0
- package/installer/index.ts +355 -0
- package/installer/module-loader.ts +412 -0
- package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
- package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
- package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
- package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
- package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
- package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
- package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
- package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
- package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
- package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
- package/installer/modules/boardroom/metaclaw-module.json +35 -0
- package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
- package/installer/modules/core/metaclaw-module.json +51 -0
- package/installer/modules/core/src/db.ts.txt +277 -0
- package/installer/modules/core/src/health-check.ts.txt +128 -0
- package/installer/modules/core/src/observability.ts.txt +20 -0
- package/installer/modules/core/src/safety.ts.txt +26 -0
- package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
- package/installer/modules/core/src/self-improve.ts.txt +48 -0
- package/installer/modules/core/src/self-update.ts.txt +345 -0
- package/installer/modules/core/src/sync-context.ts.txt +133 -0
- package/installer/modules/core/src/tasks.ts.txt +159 -0
- package/installer/modules/custom/metaclaw-module.json +15 -0
- package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
- package/installer/modules/dashboard/metaclaw-module.json +23 -0
- package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
- package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
- package/installer/modules/outreach/metaclaw-module.json +29 -0
- package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
- package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
- package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
- package/installer/modules/research/metaclaw-module.json +15 -0
- package/installer/modules/research/src/agent-research.ts.txt +127 -0
- package/installer/modules/scheduler/metaclaw-module.json +27 -0
- package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
- package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
- package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
- package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
- package/installer/modules/social/metaclaw-module.json +15 -0
- package/installer/modules/social/src/agent-social.ts.txt +110 -0
- package/installer/modules/support/metaclaw-module.json +15 -0
- package/installer/modules/support/src/agent-support.ts.txt +60 -0
- package/installer/modules/swarm/metaclaw-module.json +25 -0
- package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
- package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
- package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
- package/installer/modules/swarm/swarm/types.ts.txt +51 -0
- package/installer/modules/voice/metaclaw-module.json +16 -0
- package/installer/questionnaire.ts +277 -0
- package/installer/research.ts +258 -0
- package/installer/scaffold-from-config.ts +270 -0
- package/installer/task-generator.ts +324 -0
- package/installer/templates/agent-custom.ts.txt +100 -0
- package/installer/templates/agent-cycle.bat.txt +19 -0
- package/installer/templates/agent-outreach.ts.txt +193 -0
- package/installer/templates/agent-research.ts.txt +127 -0
- package/installer/templates/agent-social.ts.txt +110 -0
- package/installer/templates/agent-support.ts.txt +60 -0
- package/installer/templates/build-dashboard.cjs.txt +51 -0
- package/installer/templates/cron-manager.ts.txt +273 -0
- package/installer/templates/dashboard.html.txt +450 -0
- package/installer/templates/db.ts.txt +277 -0
- package/installer/templates/detect-session.bat.txt +41 -0
- package/installer/templates/health-check.ts.txt +128 -0
- package/installer/templates/inbox-agent.ts.txt +283 -0
- package/installer/templates/launch.bat.txt +120 -0
- package/installer/templates/morning-report.ts.txt +124 -0
- package/installer/templates/observability.ts.txt +20 -0
- package/installer/templates/safety.ts.txt +26 -0
- package/installer/templates/self-improve.ts.txt +48 -0
- package/installer/templates/self-update.ts.txt +345 -0
- package/installer/templates/state.json.txt +33 -0
- package/installer/templates/system-context.json.txt +33 -0
- package/installer/templates/update-dashboard.ts.txt +126 -0
- package/package.json +31 -0
- package/setup.bat +178 -0
|
@@ -0,0 +1,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,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
|
+
}
|