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,348 @@
1
+ /**
2
+ * MetaClaw Swarm — HTTP Relay Server (Phase 1)
3
+ * Lightweight bucket relay: stores index + outcome packets.
4
+ * Every MetaClaw install knows this URL. Gets the meta swarm functional immediately.
5
+ *
6
+ * Usage:
7
+ * npx tsx swarm/relay-server.ts [--port 7891]
8
+ */
9
+
10
+ import { createServer, IncomingMessage, ServerResponse } from "node:http";
11
+ import { URL } from "node:url";
12
+ import Database from "better-sqlite3";
13
+ import path from "path";
14
+ import fs from "fs";
15
+ import crypto from "crypto";
16
+ import type { OutcomePacket, BucketMeta } from "./types.js";
17
+
18
+ function getBucketId(bucketPath: string): string {
19
+ return crypto.createHash("sha256").update(bucketPath).digest("hex").slice(0, 16);
20
+ }
21
+
22
+ async function readBody(req: IncomingMessage): Promise<any> {
23
+ return new Promise((resolve) => {
24
+ let data = "";
25
+ req.on("data", (chunk) => (data += chunk));
26
+ req.on("end", () => {
27
+ try { resolve(JSON.parse(data)); } catch { resolve({}); }
28
+ });
29
+ });
30
+ }
31
+
32
+ function respond(res: ServerResponse, status: number, body: any) {
33
+ res.writeHead(status, {
34
+ "Content-Type": "application/json",
35
+ "Access-Control-Allow-Origin": "*",
36
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
37
+ "Access-Control-Allow-Headers": "Content-Type",
38
+ });
39
+ res.end(JSON.stringify(body));
40
+ }
41
+
42
+ function match(method: string, reqMethod: string, pattern: string, pathname: string): Record<string, string> | null {
43
+ if (method !== reqMethod) return null;
44
+ const regex = new RegExp("^" + pattern.replace(/:(\w+)/g, "(?<$1>[^/]+)") + "$");
45
+ const m = pathname.match(regex);
46
+ if (!m) return null;
47
+ return m.groups || {};
48
+ }
49
+
50
+ export function startRelayServer(opts: { port?: number; dbPath?: string } = {}) {
51
+ const port = opts.port || 7891;
52
+ const dbFile = opts.dbPath || path.join(process.cwd(), "data", "swarm-relay.db");
53
+ fs.mkdirSync(path.dirname(dbFile), { recursive: true });
54
+
55
+ const db = new Database(dbFile);
56
+ db.pragma("journal_mode = WAL");
57
+ db.pragma("busy_timeout = 5000");
58
+
59
+ db.exec(`
60
+ CREATE TABLE IF NOT EXISTS buckets (
61
+ bucket_id TEXT PRIMARY KEY,
62
+ path TEXT NOT NULL UNIQUE,
63
+ domain TEXT NOT NULL,
64
+ title TEXT NOT NULL DEFAULT '',
65
+ description TEXT NOT NULL DEFAULT '',
66
+ conditions TEXT DEFAULT '[]',
67
+ exclusions TEXT DEFAULT '[]',
68
+ packet_count INTEGER DEFAULT 0,
69
+ last_updated TEXT DEFAULT (datetime('now')),
70
+ confidence_avg REAL DEFAULT 0
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS packets (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ bucket_id TEXT NOT NULL REFERENCES buckets(bucket_id),
76
+ agent_id TEXT NOT NULL,
77
+ signal TEXT NOT NULL,
78
+ confidence REAL NOT NULL,
79
+ insight TEXT NOT NULL,
80
+ context TEXT DEFAULT '{}',
81
+ metrics TEXT DEFAULT '{}',
82
+ ts INTEGER NOT NULL,
83
+ created_at TEXT DEFAULT (datetime('now')),
84
+ UNIQUE(bucket_id, agent_id)
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS taxonomy (
88
+ path TEXT PRIMARY KEY,
89
+ domain TEXT NOT NULL,
90
+ title TEXT NOT NULL,
91
+ description TEXT NOT NULL DEFAULT '',
92
+ parent TEXT,
93
+ depth INTEGER DEFAULT 0
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_packets_bucket ON packets(bucket_id);
97
+ CREATE INDEX IF NOT EXISTS idx_buckets_domain ON buckets(domain);
98
+ `);
99
+
100
+ // Seed base taxonomy if empty
101
+ const taxCount = (db.prepare("SELECT COUNT(*) as c FROM taxonomy").get() as any).c;
102
+ if (taxCount === 0) seedTaxonomy(db);
103
+
104
+ const server = createServer(async (req, res) => {
105
+ if (req.method === "OPTIONS") { respond(res, 200, {}); return; }
106
+
107
+ const url = new URL(req.url!, `http://${req.headers.host}`);
108
+ const pathname = url.pathname;
109
+ const method = req.method!;
110
+ let params: Record<string, string> | null;
111
+
112
+ try {
113
+ // --- Health ---
114
+ if ((params = match("GET", method, "/health", pathname))) {
115
+ const bucketCount = (db.prepare("SELECT COUNT(*) as c FROM buckets").get() as any).c;
116
+ const packetCount = (db.prepare("SELECT COUNT(*) as c FROM packets").get() as any).c;
117
+ respond(res, 200, { status: "ok", buckets: bucketCount, packets: packetCount });
118
+ return;
119
+ }
120
+
121
+ // --- Get taxonomy (the menu of all problem categories) ---
122
+ if ((params = match("GET", method, "/taxonomy", pathname))) {
123
+ const domain = url.searchParams.get("domain");
124
+ const rows = domain
125
+ ? db.prepare("SELECT * FROM taxonomy WHERE domain = ? ORDER BY path").all(domain)
126
+ : db.prepare("SELECT * FROM taxonomy ORDER BY path").all();
127
+ respond(res, 200, { taxonomy: rows });
128
+ return;
129
+ }
130
+
131
+ // --- Get bucket index (list all buckets with metadata) ---
132
+ if ((params = match("GET", method, "/buckets", pathname))) {
133
+ const domain = url.searchParams.get("domain");
134
+ const search = url.searchParams.get("q");
135
+ let rows;
136
+ if (search) {
137
+ rows = db.prepare("SELECT * FROM buckets WHERE path LIKE ? OR title LIKE ? OR description LIKE ? ORDER BY packet_count DESC")
138
+ .all(`%${search}%`, `%${search}%`, `%${search}%`);
139
+ } else if (domain) {
140
+ rows = db.prepare("SELECT * FROM buckets WHERE domain = ? ORDER BY packet_count DESC").all(domain);
141
+ } else {
142
+ rows = db.prepare("SELECT * FROM buckets ORDER BY packet_count DESC").all();
143
+ }
144
+ respond(res, 200, { buckets: rows.map(parseBucketRow) });
145
+ return;
146
+ }
147
+
148
+ // --- Get a specific bucket's packets ---
149
+ if ((params = match("GET", method, "/buckets/:id", pathname))) {
150
+ const bucket = db.prepare("SELECT * FROM buckets WHERE bucket_id = ?").get(params.id);
151
+ if (!bucket) { respond(res, 404, { error: "bucket not found" }); return; }
152
+ const packets = db.prepare("SELECT * FROM packets WHERE bucket_id = ? ORDER BY confidence DESC").all(params.id);
153
+ respond(res, 200, {
154
+ bucket: parseBucketRow(bucket),
155
+ packets: packets.map(parsePacketRow),
156
+ });
157
+ return;
158
+ }
159
+
160
+ // --- Lookup bucket by canonical path ---
161
+ if ((params = match("GET", method, "/lookup", pathname))) {
162
+ const bucketPath = url.searchParams.get("path");
163
+ if (!bucketPath) { respond(res, 400, { error: "path param required" }); return; }
164
+ const bucketId = getBucketId(bucketPath);
165
+ const bucket = db.prepare("SELECT * FROM buckets WHERE bucket_id = ?").get(bucketId);
166
+ if (!bucket) { respond(res, 404, { error: "no bucket for path", bucket_id: bucketId }); return; }
167
+ const packets = db.prepare("SELECT * FROM packets WHERE bucket_id = ? ORDER BY confidence DESC").all(bucketId);
168
+ respond(res, 200, {
169
+ bucket: parseBucketRow(bucket),
170
+ packets: packets.map(parsePacketRow),
171
+ });
172
+ return;
173
+ }
174
+
175
+ // --- Deposit an outcome packet ---
176
+ if ((params = match("POST", method, "/packets", pathname))) {
177
+ const body: OutcomePacket = await readBody(req);
178
+ if (!body.bucket || !body.outcome?.insight || !body.agent_id) {
179
+ respond(res, 400, { error: "bucket, agent_id, and outcome.insight required" }); return;
180
+ }
181
+
182
+ const bucketId = body.bucket_id || getBucketId(body.bucket);
183
+ const domain = body.bucket.split(".")[0];
184
+
185
+ // Ensure bucket exists
186
+ db.prepare(`
187
+ INSERT OR IGNORE INTO buckets (bucket_id, path, domain, title, description)
188
+ VALUES (?, ?, ?, ?, ?)
189
+ `).run(bucketId, body.bucket, domain, body.bucket.split(".").pop() || "", body.outcome.insight.slice(0, 100));
190
+
191
+ // Upsert packet (one per agent per bucket — latest wins)
192
+ db.prepare(`
193
+ INSERT INTO packets (bucket_id, agent_id, signal, confidence, insight, context, metrics, ts)
194
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
195
+ ON CONFLICT(bucket_id, agent_id) DO UPDATE SET
196
+ signal=excluded.signal, confidence=excluded.confidence, insight=excluded.insight,
197
+ context=excluded.context, metrics=excluded.metrics, ts=excluded.ts, created_at=datetime('now')
198
+ `).run(
199
+ bucketId, body.agent_id,
200
+ body.outcome.signal || "neutral",
201
+ body.outcome.confidence || 0.5,
202
+ body.outcome.insight,
203
+ JSON.stringify(body.outcome.context || {}),
204
+ JSON.stringify(body.outcome.metrics || {}),
205
+ body.ts || Math.floor(Date.now() / 1000)
206
+ );
207
+
208
+ // Update bucket stats
209
+ const stats = db.prepare("SELECT COUNT(*) as c, AVG(confidence) as avg FROM packets WHERE bucket_id = ?").get(bucketId) as any;
210
+ db.prepare("UPDATE buckets SET packet_count = ?, confidence_avg = ?, last_updated = datetime('now') WHERE bucket_id = ?")
211
+ .run(stats.c, stats.avg, bucketId);
212
+
213
+ respond(res, 201, { ok: true, bucket_id: bucketId, packet_count: stats.c });
214
+ return;
215
+ }
216
+
217
+ // --- Split a bucket ---
218
+ if ((params = match("POST", method, "/buckets/:id/split", pathname))) {
219
+ const body = await readBody(req);
220
+ if (!body.new_paths || !Array.isArray(body.new_paths) || body.new_paths.length < 2) {
221
+ respond(res, 400, { error: "new_paths array with 2+ paths required" }); return;
222
+ }
223
+ const oldBucket = db.prepare("SELECT * FROM buckets WHERE bucket_id = ?").get(params.id) as any;
224
+ if (!oldBucket) { respond(res, 404, { error: "bucket not found" }); return; }
225
+
226
+ // Create new sub-buckets
227
+ const newBuckets = body.new_paths.map((p: string) => ({
228
+ id: getBucketId(p),
229
+ path: p,
230
+ domain: p.split(".")[0],
231
+ }));
232
+
233
+ for (const nb of newBuckets) {
234
+ db.prepare("INSERT OR IGNORE INTO buckets (bucket_id, path, domain, title, description) VALUES (?, ?, ?, ?, ?)")
235
+ .run(nb.id, nb.path, nb.domain, nb.path.split(".").pop() || "", `Split from ${oldBucket.path}`);
236
+ }
237
+
238
+ // Mark old bucket as split (keep it as a redirect)
239
+ db.prepare("UPDATE buckets SET description = ? WHERE bucket_id = ?")
240
+ .run(`SPLIT → ${body.new_paths.join(", ")}`, params.id);
241
+
242
+ respond(res, 200, { ok: true, old_bucket: params.id, new_buckets: newBuckets });
243
+ return;
244
+ }
245
+
246
+ // --- Swarm stats (for dashboard) ---
247
+ if ((params = match("GET", method, "/stats", pathname))) {
248
+ const bucketCount = (db.prepare("SELECT COUNT(*) as c FROM buckets").get() as any).c;
249
+ const packetCount = (db.prepare("SELECT COUNT(*) as c FROM packets").get() as any).c;
250
+ const agentCount = (db.prepare("SELECT COUNT(DISTINCT agent_id) as c FROM packets").get() as any).c;
251
+ const domains = db.prepare("SELECT domain, COUNT(*) as count FROM buckets GROUP BY domain ORDER BY count DESC").all();
252
+ const topBuckets = db.prepare("SELECT path, packet_count, confidence_avg FROM buckets ORDER BY packet_count DESC LIMIT 10").all();
253
+ const recentPackets = db.prepare("SELECT p.insight, p.signal, p.confidence, b.path FROM packets p JOIN buckets b ON p.bucket_id = b.bucket_id ORDER BY p.created_at DESC LIMIT 10").all();
254
+
255
+ respond(res, 200, {
256
+ buckets: bucketCount,
257
+ packets: packetCount,
258
+ agents: agentCount,
259
+ domains,
260
+ top_buckets: topBuckets,
261
+ recent_insights: recentPackets,
262
+ });
263
+ return;
264
+ }
265
+
266
+ respond(res, 404, { error: "not found" });
267
+ } catch (err: any) {
268
+ console.error("Swarm relay error:", err);
269
+ respond(res, 500, { error: err.message || "internal error" });
270
+ }
271
+ });
272
+
273
+ server.listen(port, "0.0.0.0", () => {
274
+ console.log(`Meta Swarm relay running on http://0.0.0.0:${port}`);
275
+ console.log(`Agents: POST /packets to deposit, GET /buckets to browse, GET /lookup?path=... to query`);
276
+ });
277
+
278
+ return server;
279
+ }
280
+
281
+ function parseBucketRow(row: any) {
282
+ return { ...row, conditions: safeJson(row.conditions), exclusions: safeJson(row.exclusions) };
283
+ }
284
+
285
+ function parsePacketRow(row: any) {
286
+ return { ...row, context: safeJson(row.context), metrics: safeJson(row.metrics) };
287
+ }
288
+
289
+ function safeJson(val: any) {
290
+ if (typeof val === "string") { try { return JSON.parse(val); } catch { return val; } }
291
+ return val;
292
+ }
293
+
294
+ function seedTaxonomy(db: Database.Database) {
295
+ const categories = [
296
+ // Outreach
297
+ ["outreach", "outreach", "Outreach", "Email, cold outreach, prospecting"],
298
+ ["outreach.email", "outreach", "Email Outreach", "Email-specific strategies"],
299
+ ["outreach.email.cold", "outreach", "Cold Email", "First-contact cold emails"],
300
+ ["outreach.email.cold.subject_lines", "outreach", "Subject Lines", "What subject lines work"],
301
+ ["outreach.email.cold.response_rate", "outreach", "Response Rate", "Improving reply rates"],
302
+ ["outreach.email.cold.deliverability", "outreach", "Deliverability", "Avoiding spam, domain health"],
303
+ ["outreach.email.followup", "outreach", "Follow-ups", "Follow-up timing and content"],
304
+ ["outreach.email.personalization", "outreach", "Personalization", "Personalizing at scale"],
305
+ ["outreach.prospecting", "outreach", "Prospecting", "Finding and qualifying prospects"],
306
+ // Research
307
+ ["research", "research", "Research", "Web research, report generation"],
308
+ ["research.web", "research", "Web Research", "Web scraping and search strategies"],
309
+ ["research.synthesis", "research", "Synthesis", "Combining multiple sources"],
310
+ ["research.accuracy", "research", "Accuracy", "Verifying facts and sources"],
311
+ // Support
312
+ ["support", "support", "Support", "Customer support and ticketing"],
313
+ ["support.triage", "support", "Triage", "Ticket classification and routing"],
314
+ ["support.auto_resolve", "support", "Auto-Resolution", "Automated ticket resolution"],
315
+ ["support.escalation", "support", "Escalation", "When and how to escalate"],
316
+ // Social
317
+ ["social", "social", "Social Media", "Content creation and engagement"],
318
+ ["social.content", "social", "Content Strategy", "What content performs well"],
319
+ ["social.scheduling", "social", "Scheduling", "Optimal posting times"],
320
+ ["social.engagement", "social", "Engagement", "Driving interaction"],
321
+ // Agent Operations
322
+ ["ops", "ops", "Agent Operations", "Running autonomous agents"],
323
+ ["ops.self_update", "ops", "Self-Update", "Prompt evolution and versioning"],
324
+ ["ops.error_handling", "ops", "Error Handling", "Recovering from failures"],
325
+ ["ops.cost_optimization", "ops", "Cost Optimization", "Reducing API spend"],
326
+ ["ops.scheduling", "ops", "Scheduling", "Cron and task scheduling"],
327
+ ["ops.session_management", "ops", "Sessions", "Claude session persistence"],
328
+ // Platform
329
+ ["platform", "platform", "Platform", "MetaClaw platform issues"],
330
+ ["platform.install", "platform", "Installation", "Installer and setup issues"],
331
+ ["platform.dashboard", "platform", "Dashboard", "Dashboard generation and display"],
332
+ ["platform.comms", "platform", "Communications", "Inter-agent messaging"],
333
+ ];
334
+
335
+ const stmt = db.prepare("INSERT OR IGNORE INTO taxonomy (path, domain, title, description, parent, depth) VALUES (?, ?, ?, ?, ?, ?)");
336
+ for (const [p, domain, title, desc] of categories) {
337
+ const parts = (p as string).split(".");
338
+ const parent = parts.length > 1 ? parts.slice(0, -1).join(".") : null;
339
+ stmt.run(p, domain, title, desc, parent, parts.length - 1);
340
+ }
341
+ }
342
+
343
+ // --- CLI entry point ---
344
+ if (process.argv[1]?.replace(/\\/g, "/").includes("swarm/relay")) {
345
+ const portArg = process.argv.indexOf("--port");
346
+ const port = portArg >= 0 ? parseInt(process.argv[portArg + 1]) : 7891;
347
+ startRelayServer({ port });
348
+ }
@@ -0,0 +1,303 @@
1
+ /**
2
+ * MetaClaw Swarm Client — Publish & Subscribe to QIS Buckets
3
+ *
4
+ * Two tiers:
5
+ * 1. Personal Swarm (local) — shared directory, LAN, private
6
+ * 2. Meta Swarm (global) — HTTP relay, opt-in, no PII
7
+ *
8
+ * Usage:
9
+ * import { SwarmClient } from "./swarm/swarm-client.js";
10
+ * const swarm = new SwarmClient({ agentName: "Atlas", relayUrl: "__RELAY_URL__" });
11
+ * await swarm.deposit("ops.session.resume_reliability", { method: "...", outcome: "...", score: 9 });
12
+ * const insights = await swarm.query("ops.session.resume_reliability");
13
+ * const results = await swarm.search("session resume");
14
+ */
15
+
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import crypto from "crypto";
19
+
20
+ export interface SwarmConfig {
21
+ agentName: string;
22
+ relayUrl?: string; // Global meta swarm relay URL
23
+ localBucketsPath?: string; // Personal swarm path (default: data/buckets)
24
+ enableGlobal?: boolean; // Opt-in to meta swarm (default false)
25
+ enableLocal?: boolean; // Use local personal swarm (default true)
26
+ agentId?: string; // Anonymized ID for global deposits
27
+ }
28
+
29
+ export interface DepositData {
30
+ method: string;
31
+ outcome: string;
32
+ score: number;
33
+ template_fields?: Record<string, any>;
34
+ metadata?: Record<string, any>;
35
+ }
36
+
37
+ export interface InsightPacket {
38
+ agent: string;
39
+ timestamp: string;
40
+ method: string;
41
+ outcome: string;
42
+ score: number;
43
+ template_fields?: Record<string, any>;
44
+ metadata?: Record<string, any>;
45
+ source: "local" | "global";
46
+ }
47
+
48
+ export class SwarmClient {
49
+ private config: Required<SwarmConfig>;
50
+
51
+ constructor(opts: SwarmConfig) {
52
+ this.config = {
53
+ agentName: opts.agentName,
54
+ relayUrl: opts.relayUrl || "__RELAY_URL__",
55
+ localBucketsPath: opts.localBucketsPath || "__LOCAL_BUCKETS_PATH__",
56
+ enableGlobal: opts.enableGlobal ?? false,
57
+ enableLocal: opts.enableLocal ?? true,
58
+ agentId: opts.agentId || this.generateAnonId(opts.agentName),
59
+ };
60
+ }
61
+
62
+ // --- Deposit insight to bucket(s) ---
63
+ async deposit(bucketPath: string, data: DepositData): Promise<{ local: boolean; global: boolean }> {
64
+ const results = { local: false, global: false };
65
+
66
+ // Local personal swarm
67
+ if (this.config.enableLocal) {
68
+ try {
69
+ this.depositLocal(bucketPath, data);
70
+ results.local = true;
71
+ } catch (e) {
72
+ console.error("Local deposit failed:", (e as Error).message);
73
+ }
74
+ }
75
+
76
+ // Global meta swarm
77
+ if (this.config.enableGlobal) {
78
+ try {
79
+ await this.depositGlobal(bucketPath, data);
80
+ results.global = true;
81
+ } catch (e) {
82
+ console.error("Global deposit failed:", (e as Error).message);
83
+ }
84
+ }
85
+
86
+ return results;
87
+ }
88
+
89
+ // --- Query a specific bucket ---
90
+ async query(bucketPath: string): Promise<InsightPacket[]> {
91
+ const insights: InsightPacket[] = [];
92
+
93
+ // Read local
94
+ if (this.config.enableLocal) {
95
+ insights.push(...this.queryLocal(bucketPath));
96
+ }
97
+
98
+ // Read global
99
+ if (this.config.enableGlobal) {
100
+ try {
101
+ insights.push(...await this.queryGlobal(bucketPath));
102
+ } catch {}
103
+ }
104
+
105
+ // Sort by score descending
106
+ return insights.sort((a, b) => b.score - a.score);
107
+ }
108
+
109
+ // --- Search across all buckets by keyword ---
110
+ async search(keyword: string): Promise<Array<{ path: string; problem: string; packets: number; source: string }>> {
111
+ const results: Array<{ path: string; problem: string; packets: number; source: string }> = [];
112
+
113
+ // Search local
114
+ if (this.config.enableLocal) {
115
+ results.push(...this.searchLocal(keyword));
116
+ }
117
+
118
+ // Search global
119
+ if (this.config.enableGlobal) {
120
+ try {
121
+ results.push(...await this.searchGlobal(keyword));
122
+ } catch {}
123
+ }
124
+
125
+ return results;
126
+ }
127
+
128
+ // --- Get taxonomy (list of all available problem categories) ---
129
+ async getTaxonomy(domain?: string): Promise<any[]> {
130
+ const results: any[] = [];
131
+
132
+ if (this.config.enableLocal) {
133
+ try {
134
+ const reg = JSON.parse(fs.readFileSync(path.join(this.config.localBucketsPath, "REGISTRY.json"), "utf-8"));
135
+ const buckets = domain ? reg.buckets.filter((b: any) => b.domain === domain) : reg.buckets;
136
+ results.push(...buckets.map((b: any) => ({ ...b, source: "local" })));
137
+ } catch {}
138
+ }
139
+
140
+ if (this.config.enableGlobal) {
141
+ try {
142
+ const url = domain
143
+ ? `${this.config.relayUrl}/taxonomy?domain=${domain}`
144
+ : `${this.config.relayUrl}/taxonomy`;
145
+ const res = await fetch(url);
146
+ const data = await res.json();
147
+ results.push(...(data.taxonomy || []).map((t: any) => ({ ...t, source: "global" })));
148
+ } catch {}
149
+ }
150
+
151
+ return results;
152
+ }
153
+
154
+ // --- Stats ---
155
+ async stats(): Promise<{ local: any; global: any }> {
156
+ let local = null;
157
+ let global = null;
158
+
159
+ if (this.config.enableLocal) {
160
+ try {
161
+ const dirs = fs.readdirSync(this.config.localBucketsPath, { withFileTypes: true })
162
+ .filter(d => d.isDirectory() && d.name !== "scripts");
163
+ let totalPackets = 0;
164
+ for (const d of dirs) {
165
+ const jsons = fs.readdirSync(path.join(this.config.localBucketsPath, d.name))
166
+ .filter(f => f.endsWith(".json"));
167
+ totalPackets += jsons.length;
168
+ }
169
+ local = { buckets: dirs.length, packets: totalPackets };
170
+ } catch {}
171
+ }
172
+
173
+ if (this.config.enableGlobal) {
174
+ try {
175
+ const res = await fetch(`${this.config.relayUrl}/stats`);
176
+ global = await res.json();
177
+ } catch { global = { error: "relay unreachable" }; }
178
+ }
179
+
180
+ return { local, global };
181
+ }
182
+
183
+ // --- Local Operations ---
184
+
185
+ private depositLocal(bucketPath: string, data: DepositData): void {
186
+ const folderName = bucketPath.replace(/\./g, ".");
187
+ const bucketDir = path.join(this.config.localBucketsPath, folderName);
188
+ fs.mkdirSync(bucketDir, { recursive: true });
189
+
190
+ const packet = {
191
+ agent: this.config.agentName,
192
+ timestamp: new Date().toISOString(),
193
+ method: data.method,
194
+ outcome: data.outcome,
195
+ score: data.score,
196
+ ...(data.template_fields ? { template_fields: data.template_fields } : {}),
197
+ ...(data.metadata ? { metadata: data.metadata } : {}),
198
+ };
199
+
200
+ const filename = this.config.agentName.toLowerCase().replace(/[^a-z0-9]/g, "-") + ".json";
201
+ fs.writeFileSync(path.join(bucketDir, filename), JSON.stringify(packet, null, 2));
202
+ }
203
+
204
+ private queryLocal(bucketPath: string): InsightPacket[] {
205
+ const bucketDir = path.join(this.config.localBucketsPath, bucketPath);
206
+ if (!fs.existsSync(bucketDir)) return [];
207
+
208
+ const files = fs.readdirSync(bucketDir).filter(f => f.endsWith(".json"));
209
+ return files.map(f => {
210
+ try {
211
+ const raw = JSON.parse(fs.readFileSync(path.join(bucketDir, f), "utf-8"));
212
+ return { ...raw, source: "local" as const };
213
+ } catch { return null; }
214
+ }).filter(Boolean) as InsightPacket[];
215
+ }
216
+
217
+ private searchLocal(keyword: string): Array<{ path: string; problem: string; packets: number; source: string }> {
218
+ const kw = keyword.toLowerCase();
219
+ try {
220
+ const reg = JSON.parse(fs.readFileSync(path.join(this.config.localBucketsPath, "REGISTRY.json"), "utf-8"));
221
+ return (reg.buckets || [])
222
+ .filter((b: any) =>
223
+ b.name.toLowerCase().includes(kw) ||
224
+ (b.problem || "").toLowerCase().includes(kw))
225
+ .map((b: any) => {
226
+ const dir = path.join(this.config.localBucketsPath, b.name);
227
+ let packets = 0;
228
+ try { packets = fs.readdirSync(dir).filter((f: string) => f.endsWith(".json")).length; } catch {}
229
+ return { path: b.name, problem: b.problem || "", packets, source: "local" };
230
+ });
231
+ } catch { return []; }
232
+ }
233
+
234
+ // --- Global Operations ---
235
+
236
+ private async depositGlobal(bucketPath: string, data: DepositData): Promise<void> {
237
+ const packet = {
238
+ v: 1,
239
+ bucket: bucketPath,
240
+ bucket_id: this.hashBucket(bucketPath),
241
+ ts: Math.floor(Date.now() / 1000),
242
+ agent_id: this.config.agentId,
243
+ outcome: {
244
+ signal: data.score >= 7 ? "positive" : data.score >= 4 ? "neutral" : "negative",
245
+ confidence: data.score / 10,
246
+ insight: data.method + " → " + data.outcome,
247
+ context: data.template_fields || {},
248
+ metrics: data.metadata || {},
249
+ },
250
+ };
251
+
252
+ const res = await fetch(`${this.config.relayUrl}/packets`, {
253
+ method: "POST",
254
+ headers: { "Content-Type": "application/json" },
255
+ body: JSON.stringify(packet),
256
+ });
257
+
258
+ if (!res.ok) throw new Error(`Relay error: ${res.status}`);
259
+ }
260
+
261
+ private async queryGlobal(bucketPath: string): Promise<InsightPacket[]> {
262
+ const bucketId = this.hashBucket(bucketPath);
263
+ const res = await fetch(`${this.config.relayUrl}/buckets/${bucketId}`);
264
+ if (!res.ok) return [];
265
+ const data = await res.json();
266
+
267
+ return (data.packets || []).map((p: any) => ({
268
+ agent: p.agent_id,
269
+ timestamp: new Date(p.ts * 1000).toISOString(),
270
+ method: p.insight?.split(" → ")[0] || p.insight,
271
+ outcome: p.insight?.split(" → ")[1] || "",
272
+ score: Math.round((p.confidence || 0) * 10),
273
+ template_fields: p.context,
274
+ metadata: p.metrics,
275
+ source: "global" as const,
276
+ }));
277
+ }
278
+
279
+ private async searchGlobal(keyword: string): Promise<Array<{ path: string; problem: string; packets: number; source: string }>> {
280
+ const res = await fetch(`${this.config.relayUrl}/buckets?q=${encodeURIComponent(keyword)}`);
281
+ if (!res.ok) return [];
282
+ const data = await res.json();
283
+ return (data.buckets || []).map((b: any) => ({
284
+ path: b.path,
285
+ problem: b.description || b.title || "",
286
+ packets: b.packet_count || 0,
287
+ source: "global",
288
+ }));
289
+ }
290
+
291
+ // --- Utilities ---
292
+
293
+ private hashBucket(bucketPath: string): string {
294
+ return crypto.createHash("sha256").update(bucketPath).digest("hex").slice(0, 16);
295
+ }
296
+
297
+ private generateAnonId(agentName: string): string {
298
+ const hash = crypto.createHash("sha256")
299
+ .update(agentName + (process.env.COMPUTERNAME || "unknown"))
300
+ .digest("hex").slice(0, 4);
301
+ return `mc_anon_${hash}`;
302
+ }
303
+ }