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.
Files changed (92) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +282 -0
  3. package/docs/assets/favicon.png +0 -0
  4. package/docs/assets/metaclaw-banner.svg +86 -0
  5. package/docs/assets/qis-logo.png +0 -0
  6. package/docs/assets/yz-favicon.png +0 -0
  7. package/docs/assets/yz-logo.png +0 -0
  8. package/docs/index.html +895 -0
  9. package/installer/assets/favicon.png +0 -0
  10. package/installer/auto-start.ts +330 -0
  11. package/installer/brand.ts +115 -0
  12. package/installer/core-scaffold.ts +448 -0
  13. package/installer/dashboard-generator.ts +657 -0
  14. package/installer/detect.ts +129 -0
  15. package/installer/index.ts +355 -0
  16. package/installer/module-loader.ts +412 -0
  17. package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
  18. package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
  19. package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
  20. package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
  21. package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
  22. package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
  23. package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
  24. package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
  25. package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
  26. package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
  27. package/installer/modules/boardroom/metaclaw-module.json +35 -0
  28. package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
  29. package/installer/modules/core/metaclaw-module.json +51 -0
  30. package/installer/modules/core/src/db.ts.txt +277 -0
  31. package/installer/modules/core/src/health-check.ts.txt +128 -0
  32. package/installer/modules/core/src/observability.ts.txt +20 -0
  33. package/installer/modules/core/src/safety.ts.txt +26 -0
  34. package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
  35. package/installer/modules/core/src/self-improve.ts.txt +48 -0
  36. package/installer/modules/core/src/self-update.ts.txt +345 -0
  37. package/installer/modules/core/src/sync-context.ts.txt +133 -0
  38. package/installer/modules/core/src/tasks.ts.txt +159 -0
  39. package/installer/modules/custom/metaclaw-module.json +15 -0
  40. package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
  41. package/installer/modules/dashboard/metaclaw-module.json +23 -0
  42. package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
  43. package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
  44. package/installer/modules/outreach/metaclaw-module.json +29 -0
  45. package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
  46. package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
  47. package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
  48. package/installer/modules/research/metaclaw-module.json +15 -0
  49. package/installer/modules/research/src/agent-research.ts.txt +127 -0
  50. package/installer/modules/scheduler/metaclaw-module.json +27 -0
  51. package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
  52. package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
  53. package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
  54. package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
  55. package/installer/modules/social/metaclaw-module.json +15 -0
  56. package/installer/modules/social/src/agent-social.ts.txt +110 -0
  57. package/installer/modules/support/metaclaw-module.json +15 -0
  58. package/installer/modules/support/src/agent-support.ts.txt +60 -0
  59. package/installer/modules/swarm/metaclaw-module.json +25 -0
  60. package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
  61. package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
  62. package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
  63. package/installer/modules/swarm/swarm/types.ts.txt +51 -0
  64. package/installer/modules/voice/metaclaw-module.json +16 -0
  65. package/installer/questionnaire.ts +277 -0
  66. package/installer/research.ts +258 -0
  67. package/installer/scaffold-from-config.ts +270 -0
  68. package/installer/task-generator.ts +324 -0
  69. package/installer/templates/agent-custom.ts.txt +100 -0
  70. package/installer/templates/agent-cycle.bat.txt +19 -0
  71. package/installer/templates/agent-outreach.ts.txt +193 -0
  72. package/installer/templates/agent-research.ts.txt +127 -0
  73. package/installer/templates/agent-social.ts.txt +110 -0
  74. package/installer/templates/agent-support.ts.txt +60 -0
  75. package/installer/templates/build-dashboard.cjs.txt +51 -0
  76. package/installer/templates/cron-manager.ts.txt +273 -0
  77. package/installer/templates/dashboard.html.txt +450 -0
  78. package/installer/templates/db.ts.txt +277 -0
  79. package/installer/templates/detect-session.bat.txt +41 -0
  80. package/installer/templates/health-check.ts.txt +128 -0
  81. package/installer/templates/inbox-agent.ts.txt +283 -0
  82. package/installer/templates/launch.bat.txt +120 -0
  83. package/installer/templates/morning-report.ts.txt +124 -0
  84. package/installer/templates/observability.ts.txt +20 -0
  85. package/installer/templates/safety.ts.txt +26 -0
  86. package/installer/templates/self-improve.ts.txt +48 -0
  87. package/installer/templates/self-update.ts.txt +345 -0
  88. package/installer/templates/state.json.txt +33 -0
  89. package/installer/templates/system-context.json.txt +33 -0
  90. package/installer/templates/update-dashboard.ts.txt +126 -0
  91. package/package.json +31 -0
  92. package/setup.bat +178 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * MetaClaw Boardroom — Coordinator Server
3
+ * Thin HTTP server with SQLite backend. Manages meetings, turn-taking, context.
4
+ *
5
+ * Usage:
6
+ * Standalone: npx tsx boardroom/server.ts [--port 7890]
7
+ * Embedded: import { startBoardroomServer } from "./boardroom/server.js"
8
+ */
9
+
10
+ import { createServer, IncomingMessage, ServerResponse } from "node:http";
11
+ import { URL } from "node:url";
12
+ import { getBoardroomDb, createMeeting, getMeeting, listActiveMeetings, endMeeting as dbEndMeeting,
13
+ addParticipant, getParticipants, updatePollTime, markIdleParticipants, reactivateParticipant,
14
+ addMessage, getMessagesSince, getRecentMessages, getMessageCount, setWantsTurn,
15
+ buildRunningSummary, createScheduledMeeting, getUpcomingMeetings, activateScheduledMeeting,
16
+ raiseHand, lowerHand, reEvaluateHand, getPendingHandRaises, clearHandRaise,
17
+ getArtifacts, generateMeetingArtifacts } from "./db.js";
18
+ import { scoreForAgent, getNextSpeaker } from "./turn.js";
19
+ import { computePriorityQueue, getNextSpeakerV2, scoreForAgentV2, checkCoverage } from "./turn-v2.js";
20
+ import type { ContextPackage } from "./types.js";
21
+ import { summonMultiple } from "./summons.js";
22
+ import fs from "fs";
23
+
24
+ function genId(): string {
25
+ return `mtg_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`;
26
+ }
27
+
28
+ async function readBody(req: IncomingMessage): Promise<any> {
29
+ return new Promise((resolve) => {
30
+ let data = "";
31
+ req.on("data", (chunk) => (data += chunk));
32
+ req.on("end", () => {
33
+ try { resolve(JSON.parse(data)); } catch { resolve({}); }
34
+ });
35
+ });
36
+ }
37
+
38
+ function respond(res: ServerResponse, status: number, body: any) {
39
+ res.writeHead(status, {
40
+ "Content-Type": "application/json",
41
+ "Access-Control-Allow-Origin": "*",
42
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
43
+ "Access-Control-Allow-Headers": "Content-Type",
44
+ });
45
+ res.end(JSON.stringify(body));
46
+ }
47
+
48
+ function match(method: string, reqMethod: string, pattern: string, pathname: string): Record<string, string> | null {
49
+ if (method !== reqMethod) return null;
50
+ const regex = new RegExp("^" + pattern.replace(/:(\w+)/g, "(?<$1>[^/]+)") + "$");
51
+ const m = pathname.match(regex);
52
+ if (!m) return null;
53
+ return m.groups || {};
54
+ }
55
+
56
+ export function startBoardroomServer(opts: { port?: number; dbPath?: string } = {}) {
57
+ const port = opts.port || 7890;
58
+ const db = getBoardroomDb(opts.dbPath);
59
+
60
+ const server = createServer(async (req, res) => {
61
+ // Handle CORS preflight
62
+ if (req.method === "OPTIONS") { respond(res, 200, {}); return; }
63
+
64
+ const url = new URL(req.url!, `http://${req.headers.host}`);
65
+ const path = url.pathname;
66
+ const method = req.method!;
67
+ let params: Record<string, string> | null;
68
+
69
+ try {
70
+ // --- Health ---
71
+ if ((params = match("GET", method, "/health", path))) {
72
+ const meetings = listActiveMeetings(db);
73
+ respond(res, 200, { status: "ok", meetings_active: meetings.length });
74
+ return;
75
+ }
76
+
77
+ // --- Create Meeting ---
78
+ if ((params = match("POST", method, "/meetings", path))) {
79
+ const body = await readBody(req);
80
+ if (!body.topic || !body.created_by) {
81
+ respond(res, 400, { error: "topic and created_by required" }); return;
82
+ }
83
+ const id = genId();
84
+ createMeeting(db, id, body.topic, body.created_by);
85
+ addParticipant(db, id, body.created_by, body.expertise || []);
86
+
87
+ // Send summons to invited agents
88
+ if (body.invite && Array.isArray(body.invite)) {
89
+ const coordUrl = `http://${req.headers.host}`;
90
+ summonMultiple(body.invite, id, body.topic, coordUrl, body.created_by);
91
+ }
92
+
93
+ respond(res, 201, { meeting_id: id, status: "created" });
94
+ return;
95
+ }
96
+
97
+ // --- List Active Meetings ---
98
+ if ((params = match("GET", method, "/meetings", path))) {
99
+ const meetings = listActiveMeetings(db);
100
+ respond(res, 200, { meetings });
101
+ return;
102
+ }
103
+
104
+ // --- Get Meeting Details ---
105
+ if ((params = match("GET", method, "/meetings/:id", path))) {
106
+ const meeting = getMeeting(db, params.id);
107
+ if (!meeting) { respond(res, 404, { error: "meeting not found" }); return; }
108
+ const participants = getParticipants(db, params.id);
109
+ const recent = getRecentMessages(db, params.id, 5);
110
+ respond(res, 200, {
111
+ meeting,
112
+ participants: participants.map((p: any) => ({
113
+ name: p.agent_name, expertise: safeJson(p.expertise), status: p.status,
114
+ })),
115
+ recent_messages: recent.map(parseMsg),
116
+ total_messages: getMessageCount(db, params.id),
117
+ });
118
+ return;
119
+ }
120
+
121
+ // --- Join Meeting ---
122
+ if ((params = match("POST", method, "/meetings/:id/join", path))) {
123
+ const body = await readBody(req);
124
+ if (!body.agent_name) { respond(res, 400, { error: "agent_name required" }); return; }
125
+ const meeting = getMeeting(db, params.id);
126
+ if (!meeting) { respond(res, 404, { error: "meeting not found" }); return; }
127
+ if (meeting.status !== "active") { respond(res, 400, { error: "meeting not active" }); return; }
128
+
129
+ addParticipant(db, params.id, body.agent_name, body.expertise || []);
130
+ reactivateParticipant(db, params.id, body.agent_name);
131
+
132
+ const ctx = buildContext(db, params.id, body.agent_name);
133
+ respond(res, 200, { ok: true, context: ctx });
134
+ return;
135
+ }
136
+
137
+ // --- Schedule Future Meeting ---
138
+ if ((params = match("POST", method, "/meetings/schedule", path))) {
139
+ const body = await readBody(req);
140
+ if (!body.topic || !body.created_by || !body.scheduled_for) {
141
+ respond(res, 400, { error: "topic, created_by, scheduled_for required" }); return;
142
+ }
143
+ const schedId = `sched_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`;
144
+ createScheduledMeeting(db, schedId, body.topic, body.created_by, body.scheduled_for,
145
+ body.invited || [], body.priority || "normal", body.time_limit_minutes || 30);
146
+
147
+ // Send summons with scheduled time
148
+ if (body.invited && Array.isArray(body.invited)) {
149
+ const coordUrl = `http://${req.headers.host}`;
150
+ summonMultiple(body.invited, schedId, body.topic, coordUrl, body.created_by);
151
+ }
152
+
153
+ respond(res, 201, { schedule_id: schedId, scheduled_for: body.scheduled_for, status: "scheduled" });
154
+ return;
155
+ }
156
+
157
+ // --- Upcoming Scheduled Meetings ---
158
+ if ((params = match("GET", method, "/meetings/upcoming", path))) {
159
+ const agentName = url.searchParams.get("agent");
160
+ const upcoming = getUpcomingMeetings(db, agentName || undefined);
161
+ const now = Date.now();
162
+ respond(res, 200, {
163
+ upcoming: upcoming.map((m: any) => ({
164
+ ...m,
165
+ invited_agents: safeJson(m.invited_agents),
166
+ minutes_until: Math.max(0, Math.round((new Date(m.scheduled_for).getTime() - now) / 60000)),
167
+ })),
168
+ });
169
+ return;
170
+ }
171
+
172
+ // --- Speak ---
173
+ if ((params = match("POST", method, "/meetings/:id/speak", path))) {
174
+ const body = await readBody(req);
175
+ if (!body.agent_name || !body.content) {
176
+ respond(res, 400, { error: "agent_name and content required" }); return;
177
+ }
178
+ const meeting = getMeeting(db, params.id);
179
+ if (!meeting || meeting.status !== "active") {
180
+ respond(res, 400, { error: "meeting not active" }); return;
181
+ }
182
+
183
+ // Clear this agent's hand-raise since they're now speaking
184
+ clearHandRaise(db, params.id, body.agent_name);
185
+
186
+ const msgId = addMessage(db, params.id, body.agent_name, body.content, body.addresses, body.msg_type);
187
+
188
+ // v2: compute priority queue for next speaker from hand-raises
189
+ const nextV2 = getNextSpeakerV2(params.id, db);
190
+ // Fallback to v1 if no hand-raises
191
+ const next = nextV2 || getNextSpeaker(params.id, db, body.agent_name);
192
+
193
+ // Check coverage: see if any pending hand-raise's intent overlaps with this message
194
+ const pendingRaises = getPendingHandRaises(db, params.id);
195
+ const coverageAlerts: any[] = [];
196
+ for (const hr of pendingRaises as any[]) {
197
+ if (hr.intent_hash) {
198
+ const overlap = checkCoverage(hr.intent_hash, body.content);
199
+ if (overlap > 0.5) {
200
+ coverageAlerts.push({
201
+ agent: hr.agent_name,
202
+ overlap_score: Math.round(overlap * 100),
203
+ message: "Your intended response may overlap with what was just said. Consider re-evaluating.",
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ respond(res, 200, {
210
+ ok: true,
211
+ message_id: msgId,
212
+ next_speaker: next ? { agent: next.agent_name, reason: next.reason, score: next.final_score || (next as any).score } : null,
213
+ coverage_alerts: coverageAlerts,
214
+ queue: computePriorityQueue(params.id, db).slice(0, 5),
215
+ });
216
+ return;
217
+ }
218
+
219
+ // --- Hand Raise (v2 turn-taking) ---
220
+ if ((params = match("POST", method, "/meetings/:id/hand-raise", path))) {
221
+ const body = await readBody(req);
222
+ if (!body.agent_name) { respond(res, 400, { error: "agent_name required" }); return; }
223
+ raiseHand(db, params.id, body.agent_name,
224
+ body.self_score ?? 0.5, body.intent_hash || "", body.in_response_to || null, body.urgency || "normal");
225
+ const queue = computePriorityQueue(params.id, db);
226
+ const myPos = queue.findIndex(q => q.agent_name === body.agent_name);
227
+ respond(res, 200, { ok: true, queue_position: myPos + 1, queue_size: queue.length });
228
+ return;
229
+ }
230
+
231
+ // --- Hand Lower ---
232
+ if ((params = match("POST", method, "/meetings/:id/hand-lower", path))) {
233
+ const body = await readBody(req);
234
+ if (!body.agent_name) { respond(res, 400, { error: "agent_name required" }); return; }
235
+ lowerHand(db, params.id, body.agent_name);
236
+ respond(res, 200, { ok: true });
237
+ return;
238
+ }
239
+
240
+ // --- Re-evaluate hand raise ---
241
+ if ((params = match("POST", method, "/meetings/:id/re-evaluate", path))) {
242
+ const body = await readBody(req);
243
+ if (!body.agent_name) { respond(res, 400, { error: "agent_name required" }); return; }
244
+ reEvaluateHand(db, params.id, body.agent_name, body.new_self_score ?? 0.5, body.reason || "no_change");
245
+ const queue = computePriorityQueue(params.id, db);
246
+ const myPos = queue.findIndex(q => q.agent_name === body.agent_name);
247
+ respond(res, 200, { ok: true, queue_position: myPos >= 0 ? myPos + 1 : null });
248
+ return;
249
+ }
250
+
251
+ // --- Request Turn (v1 compat — maps to hand-raise) ---
252
+ if ((params = match("POST", method, "/meetings/:id/request-turn", path))) {
253
+ const body = await readBody(req);
254
+ if (!body.agent_name) { respond(res, 400, { error: "agent_name required" }); return; }
255
+ raiseHand(db, params.id, body.agent_name, 0.7, "", null, "normal");
256
+ respond(res, 200, { ok: true });
257
+ return;
258
+ }
259
+
260
+ // --- View Priority Queue ---
261
+ if ((params = match("GET", method, "/meetings/:id/queue", path))) {
262
+ const queue = computePriorityQueue(params.id, db);
263
+ respond(res, 200, { queue });
264
+ return;
265
+ }
266
+
267
+ // --- Poll ---
268
+ if ((params = match("GET", method, "/meetings/:id/poll", path))) {
269
+ const agentName = url.searchParams.get("agent");
270
+ const sinceStr = url.searchParams.get("since") || "0";
271
+ const since = parseInt(sinceStr) || 0;
272
+
273
+ if (!agentName) { respond(res, 400, { error: "agent param required" }); return; }
274
+
275
+ const meeting = getMeeting(db, params.id);
276
+ if (!meeting) { respond(res, 404, { error: "meeting not found" }); return; }
277
+
278
+ // Check time limit
279
+ if (meeting.auto_end_at && new Date(meeting.auto_end_at) <= new Date()) {
280
+ dbEndMeeting(db, params.id, "Meeting time limit reached.");
281
+ addMessage(db, params.id, "SYSTEM", "Meeting time limit reached. Meeting ended automatically.", undefined, "summary");
282
+ }
283
+
284
+ // Update heartbeat
285
+ updatePollTime(db, params.id, agentName);
286
+ reactivateParticipant(db, params.id, agentName);
287
+ markIdleParticipants(db, params.id);
288
+
289
+ // Get new messages
290
+ const messages = getMessagesSince(db, params.id, since).map(parseMsg);
291
+
292
+ // v2: check if this agent has a pending hand-raise and compute their position
293
+ const queue = computePriorityQueue(params.id, db);
294
+ const myQueueEntry = queue.find(q => q.agent_name === agentName);
295
+ const isNextSpeaker = queue.length > 0 && queue[0].agent_name === agentName;
296
+
297
+ // v1 fallback turn signal
298
+ const turnSignalV1 = scoreForAgent(params.id, agentName, db);
299
+
300
+ // Combined turn signal
301
+ const turnSignal = myQueueEntry
302
+ ? { ...myQueueEntry, should_speak: isNextSpeaker && myQueueEntry.should_speak }
303
+ : { ...turnSignalV1, should_speak: turnSignalV1.should_speak && queue.length === 0 };
304
+
305
+ // Coverage check for agents with pending hand-raises
306
+ let coverageCheck = null;
307
+ if (myQueueEntry && messages.length > 0) {
308
+ const lastMsg = messages[messages.length - 1];
309
+ if (lastMsg.agent_name !== agentName && myQueueEntry.intent_hash) {
310
+ const overlap = checkCoverage(myQueueEntry.intent_hash, lastMsg.content);
311
+ if (overlap > 0.3) {
312
+ coverageCheck = {
313
+ new_message_by: lastMsg.agent_name,
314
+ overlap_score: Math.round(overlap * 100),
315
+ your_intent: myQueueEntry.intent_hash,
316
+ action_required: overlap > 0.6 ? "re-evaluate" : "consider",
317
+ };
318
+ }
319
+ }
320
+ }
321
+
322
+ // Build context if needed
323
+ let context: ContextPackage | null = null;
324
+ if (since === 0 || messages.length > 10) {
325
+ context = buildContext(db, params.id, agentName);
326
+ }
327
+
328
+ const refreshedMeeting = getMeeting(db, params.id);
329
+
330
+ respond(res, 200, {
331
+ messages,
332
+ turn_signal: turnSignal,
333
+ context,
334
+ meeting_status: refreshedMeeting?.status || meeting.status,
335
+ queue_position: myQueueEntry ? queue.indexOf(myQueueEntry) + 1 : null,
336
+ queue_size: queue.length,
337
+ coverage_check: coverageCheck,
338
+ });
339
+ return;
340
+ }
341
+
342
+ // --- End Meeting ---
343
+ if ((params = match("POST", method, "/meetings/:id/end", path))) {
344
+ const body = await readBody(req);
345
+ const meeting = getMeeting(db, params.id);
346
+ if (!meeting) { respond(res, 404, { error: "meeting not found" }); return; }
347
+
348
+ // Generate artifacts before ending
349
+ const artifacts = generateMeetingArtifacts(db, params.id);
350
+ dbEndMeeting(db, params.id, body.summary);
351
+
352
+ // Write MEETING_ENDED sentinel for agents in meeting-lock mode
353
+ const participants = getParticipants(db, params.id);
354
+ for (const p of participants as any[]) {
355
+ try {
356
+ const endFile = `__INBOX_ROOT__\\${p.agent_name.toLowerCase()}\\MEETING_ENDED_${params.id}.json`;
357
+ fs.writeFileSync(endFile, JSON.stringify({
358
+ meeting_id: params.id, status: "ended", summary: body.summary, artifacts
359
+ }));
360
+ } catch {}
361
+ }
362
+
363
+ respond(res, 200, { ok: true, status: "ended", artifacts });
364
+ return;
365
+ }
366
+
367
+ // --- Meeting Summary + Artifacts ---
368
+ if ((params = match("GET", method, "/meetings/:id/summary", path))) {
369
+ const meeting = getMeeting(db, params.id);
370
+ if (!meeting) { respond(res, 404, { error: "meeting not found" }); return; }
371
+ const artifacts = getArtifacts(db, params.id);
372
+ const messages = getRecentMessages(db, params.id, 100);
373
+ const summary = buildRunningSummary(db, params.id);
374
+ respond(res, 200, { meeting, artifacts, summary, transcript: messages.map(parseMsg) });
375
+ return;
376
+ }
377
+
378
+ // --- Meeting Transcript ---
379
+ if ((params = match("GET", method, "/meetings/:id/transcript", path))) {
380
+ const messages = db.prepare("SELECT * FROM messages WHERE meeting_id = ? ORDER BY id").all(params.id);
381
+ respond(res, 200, { transcript: messages.map(parseMsg) });
382
+ return;
383
+ }
384
+
385
+ respond(res, 404, { error: "not found" });
386
+ } catch (err: any) {
387
+ console.error("Boardroom error:", err);
388
+ respond(res, 500, { error: err.message || "internal error" });
389
+ }
390
+ });
391
+
392
+ // Meeting activation timer — checks every 30s for scheduled meetings to activate
393
+ setInterval(() => {
394
+ try {
395
+ const upcoming = db.prepare(
396
+ "SELECT * FROM scheduled_meetings WHERE status = 'scheduled' AND datetime(scheduled_for) <= datetime('now', '+2 minutes')"
397
+ ).all() as any[];
398
+
399
+ for (const sched of upcoming) {
400
+ const mtgId = genId();
401
+ createMeeting(db, mtgId, sched.topic, sched.created_by);
402
+
403
+ // Set time limit
404
+ if (sched.time_limit_minutes) {
405
+ db.prepare("UPDATE meetings SET scheduled_for = ?, time_limit_minutes = ?, auto_end_at = datetime(?, '+' || ? || ' minutes'), priority = ? WHERE id = ?")
406
+ .run(sched.scheduled_for, sched.time_limit_minutes, sched.scheduled_for, sched.time_limit_minutes, sched.priority || "normal", mtgId);
407
+ }
408
+
409
+ activateScheduledMeeting(db, sched.id, mtgId);
410
+
411
+ // Write MEETING_READY sentinel files
412
+ const invited = safeJson(sched.invited_agents) || [];
413
+ for (const agent of [sched.created_by, ...invited]) {
414
+ try {
415
+ const readyFile = `__INBOX_ROOT__\\${agent.toLowerCase()}\\MEETING_READY_${mtgId}.json`;
416
+ fs.writeFileSync(readyFile, JSON.stringify({
417
+ meeting_id: mtgId, schedule_id: sched.id, topic: sched.topic,
418
+ coordinator_url: `http://0.0.0.0:${port}`, priority: sched.priority || "normal",
419
+ }));
420
+ } catch {}
421
+ }
422
+
423
+ console.log(`Activated scheduled meeting: ${sched.topic} → ${mtgId}`);
424
+ }
425
+ } catch (e) { console.error("Meeting activation check error:", e); }
426
+ }, 30000);
427
+
428
+ server.listen(port, "0.0.0.0", () => {
429
+ console.log(`Boardroom coordinator v2 running on http://0.0.0.0:${port}`);
430
+ console.log(`Features: scheduling, hand-raise turn-taking, meeting lock, auto-activation`);
431
+ console.log(`Agents can connect from any machine on the network.`);
432
+ });
433
+
434
+ return server;
435
+ }
436
+
437
+ function buildContext(db: ReturnType<typeof getBoardroomDb>, meetingId: string, agentName: string): ContextPackage {
438
+ const meeting = getMeeting(db, meetingId);
439
+ const participants = getParticipants(db, meetingId);
440
+ const recent = getRecentMessages(db, meetingId, 5);
441
+ const summary = buildRunningSummary(db, meetingId);
442
+ const turnSignal = scoreForAgent(meetingId, agentName, db);
443
+ const total = getMessageCount(db, meetingId);
444
+
445
+ return {
446
+ meeting_topic: meeting.topic,
447
+ meeting_id: meetingId,
448
+ participants: participants.map((p: any) => p.agent_name),
449
+ recent_messages: recent.map(parseMsg),
450
+ running_summary: summary,
451
+ your_turn: turnSignal,
452
+ total_messages: total,
453
+ };
454
+ }
455
+
456
+ function parseMsg(m: any) {
457
+ return {
458
+ ...m,
459
+ addresses: safeJson(m.addresses),
460
+ };
461
+ }
462
+
463
+ function safeJson(val: any): any {
464
+ if (typeof val === "string") { try { return JSON.parse(val); } catch { return val; } }
465
+ return val;
466
+ }
467
+
468
+ // --- CLI entry point ---
469
+ if (process.argv[1]?.replace(/\\/g, "/").includes("boardroom/server")) {
470
+ const portArg = process.argv.indexOf("--port");
471
+ const port = portArg >= 0 ? parseInt(process.argv[portArg + 1]) : 7890;
472
+ startBoardroomServer({ port });
473
+ }
@@ -0,0 +1,26 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+ title MetaClaw Boardroom Coordinator
4
+
5
+ set PATH=C:\Program Files\nodejs;%USERPROFILE%\AppData\Roaming\npm;%USERPROFILE%\.local\bin;%PATH%
6
+
7
+ echo.
8
+ echo ==========================================
9
+ echo MetaClaw Boardroom Coordinator
10
+ echo ==========================================
11
+ echo.
12
+
13
+ cd /d "__PROJECT_DIR__"
14
+
15
+ set "PORT=7890"
16
+ if not "%1"=="" set "PORT=%1"
17
+
18
+ echo Starting on port %PORT%...
19
+ echo Agents can connect from any machine on the network.
20
+ echo.
21
+
22
+ npx tsx boardroom/server.ts --port %PORT%
23
+
24
+ echo.
25
+ echo Boardroom coordinator stopped.
26
+ pause
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MetaClaw Boardroom — Meeting Summons
3
+ * Drops invitation JSON into agent's shared inbox
4
+ */
5
+
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import type { MeetingSummons } from "./types.js";
9
+
10
+ const INBOX_ROOT = "__INBOX_ROOT__";
11
+
12
+ export function summonAgent(
13
+ targetAgent: string,
14
+ meetingId: string,
15
+ topic: string,
16
+ coordinatorUrl: string,
17
+ fromAgent: string
18
+ ): string {
19
+ const inboxPath = path.join(INBOX_ROOT, targetAgent.toLowerCase());
20
+ fs.mkdirSync(inboxPath, { recursive: true });
21
+
22
+ const ts = new Date().toISOString();
23
+ const slug = ts.replace(/[:.]/g, "-");
24
+ const filename = `${slug}_boardroom-summons.json`;
25
+ const filepath = path.join(inboxPath, filename);
26
+
27
+ const summons: MeetingSummons = {
28
+ id: `mtg_summons_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`,
29
+ from: fromAgent,
30
+ to: targetAgent,
31
+ timestamp: ts,
32
+ subject: `Boardroom Meeting: ${topic}`,
33
+ body: [
34
+ `You are invited to a boardroom meeting.`,
35
+ ``,
36
+ `Topic: ${topic}`,
37
+ `Meeting ID: ${meetingId}`,
38
+ `Coordinator: ${coordinatorUrl}`,
39
+ ``,
40
+ `To join, run: npx tsx boardroom/join.ts ${meetingId}`,
41
+ `Or import BoardroomClient and call joinMeeting("${meetingId}")`,
42
+ ].join("\n"),
43
+ type: "request",
44
+ priority: "high",
45
+ boardroom: {
46
+ meeting_id: meetingId,
47
+ coordinator_url: coordinatorUrl,
48
+ topic: topic,
49
+ },
50
+ };
51
+
52
+ fs.writeFileSync(filepath, JSON.stringify(summons, null, 2));
53
+ return filepath;
54
+ }
55
+
56
+ export function summonMultiple(
57
+ agents: string[],
58
+ meetingId: string,
59
+ topic: string,
60
+ coordinatorUrl: string,
61
+ fromAgent: string
62
+ ): string[] {
63
+ return agents
64
+ .filter(a => a.toLowerCase() !== fromAgent.toLowerCase())
65
+ .map(a => summonAgent(a, meetingId, topic, coordinatorUrl, fromAgent));
66
+ }
67
+
68
+ export function detectSummons(inboxPath: string): MeetingSummons[] {
69
+ if (!fs.existsSync(inboxPath)) return [];
70
+ const files = fs.readdirSync(inboxPath).filter(f => f.includes("boardroom-summons"));
71
+ return files.map(f => {
72
+ try {
73
+ return JSON.parse(fs.readFileSync(path.join(inboxPath, f), "utf-8"));
74
+ } catch { return null; }
75
+ }).filter(Boolean) as MeetingSummons[];
76
+ }