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,322 @@
1
+ /**
2
+ * MetaClaw Boardroom — Database Layer
3
+ * SQLite WAL mode, local to coordinator machine only (never on network share)
4
+ */
5
+
6
+ import Database from "better-sqlite3";
7
+ import path from "path";
8
+ import fs from "fs";
9
+
10
+ let db: Database.Database | null = null;
11
+
12
+ export function getBoardroomDb(dbPath?: string): Database.Database {
13
+ if (db) return db;
14
+ const p = dbPath || path.join(process.cwd(), "data", "boardroom.db");
15
+ fs.mkdirSync(path.dirname(p), { recursive: true });
16
+ db = new Database(p);
17
+ db.pragma("journal_mode = WAL");
18
+ db.pragma("busy_timeout = 5000");
19
+ initSchema(db);
20
+ return db;
21
+ }
22
+
23
+ function initSchema(db: Database.Database) {
24
+ db.exec(`
25
+ CREATE TABLE IF NOT EXISTS meetings (
26
+ id TEXT PRIMARY KEY,
27
+ topic TEXT NOT NULL,
28
+ created_by TEXT NOT NULL,
29
+ status TEXT DEFAULT 'active',
30
+ max_participants INTEGER DEFAULT 8,
31
+ scheduled_for TEXT,
32
+ time_limit_minutes INTEGER DEFAULT 30,
33
+ auto_end_at TEXT,
34
+ priority TEXT DEFAULT 'normal',
35
+ created_at TEXT DEFAULT (datetime('now')),
36
+ ended_at TEXT,
37
+ summary TEXT
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS scheduled_meetings (
41
+ id TEXT PRIMARY KEY,
42
+ topic TEXT NOT NULL,
43
+ created_by TEXT NOT NULL,
44
+ scheduled_for TEXT NOT NULL,
45
+ invited_agents TEXT DEFAULT '[]',
46
+ status TEXT DEFAULT 'scheduled',
47
+ meeting_id TEXT,
48
+ priority TEXT DEFAULT 'normal',
49
+ time_limit_minutes INTEGER DEFAULT 30,
50
+ created_at TEXT DEFAULT (datetime('now'))
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS participants (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ meeting_id TEXT NOT NULL REFERENCES meetings(id),
56
+ agent_name TEXT NOT NULL,
57
+ expertise TEXT DEFAULT '[]',
58
+ status TEXT DEFAULT 'active',
59
+ joined_at TEXT DEFAULT (datetime('now')),
60
+ last_poll_at TEXT,
61
+ UNIQUE(meeting_id, agent_name)
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS messages (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ meeting_id TEXT NOT NULL REFERENCES meetings(id),
67
+ agent_name TEXT NOT NULL,
68
+ content TEXT NOT NULL,
69
+ addresses TEXT,
70
+ msg_type TEXT DEFAULT 'statement',
71
+ created_at TEXT DEFAULT (datetime('now'))
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS turn_state (
75
+ meeting_id TEXT NOT NULL REFERENCES meetings(id),
76
+ agent_name TEXT NOT NULL,
77
+ wants_turn INTEGER DEFAULT 0,
78
+ last_spoke_at TEXT,
79
+ speak_count INTEGER DEFAULT 0,
80
+ PRIMARY KEY (meeting_id, agent_name)
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS hand_raises (
84
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
85
+ meeting_id TEXT NOT NULL REFERENCES meetings(id),
86
+ agent_name TEXT NOT NULL,
87
+ raised_at TEXT DEFAULT (datetime('now')),
88
+ in_response_to INTEGER,
89
+ self_score REAL DEFAULT 0.5,
90
+ intent_hash TEXT DEFAULT '',
91
+ urgency TEXT DEFAULT 'normal',
92
+ status TEXT DEFAULT 'pending',
93
+ UNIQUE(meeting_id, agent_name, status)
94
+ );
95
+
96
+ CREATE TABLE IF NOT EXISTS meeting_artifacts (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ meeting_id TEXT NOT NULL REFERENCES meetings(id),
99
+ artifact_type TEXT NOT NULL,
100
+ content TEXT NOT NULL,
101
+ assigned_to TEXT,
102
+ created_at TEXT DEFAULT (datetime('now'))
103
+ );
104
+
105
+ CREATE INDEX IF NOT EXISTS idx_messages_meeting ON messages(meeting_id, id);
106
+ CREATE INDEX IF NOT EXISTS idx_participants_meeting ON participants(meeting_id);
107
+ CREATE INDEX IF NOT EXISTS idx_hand_raises_meeting ON hand_raises(meeting_id, status);
108
+ CREATE INDEX IF NOT EXISTS idx_scheduled_meetings ON scheduled_meetings(status, scheduled_for);
109
+ `);
110
+ }
111
+
112
+ // --- Meeting Operations ---
113
+
114
+ export function createMeeting(db: Database.Database, id: string, topic: string, createdBy: string): void {
115
+ db.prepare("INSERT INTO meetings (id, topic, created_by) VALUES (?, ?, ?)").run(id, topic, createdBy);
116
+ }
117
+
118
+ export function getMeeting(db: Database.Database, id: string): any {
119
+ return db.prepare("SELECT * FROM meetings WHERE id = ?").get(id);
120
+ }
121
+
122
+ export function listActiveMeetings(db: Database.Database): any[] {
123
+ return db.prepare("SELECT id, topic, created_by, status, created_at FROM meetings WHERE status = 'active'").all();
124
+ }
125
+
126
+ export function endMeeting(db: Database.Database, id: string, summary?: string): void {
127
+ db.prepare("UPDATE meetings SET status = 'ended', ended_at = datetime('now'), summary = ? WHERE id = ?").run(summary || null, id);
128
+ }
129
+
130
+ // --- Participant Operations ---
131
+
132
+ export function addParticipant(db: Database.Database, meetingId: string, agentName: string, expertise: string[]): void {
133
+ db.prepare(`
134
+ INSERT OR IGNORE INTO participants (meeting_id, agent_name, expertise) VALUES (?, ?, ?)
135
+ `).run(meetingId, agentName, JSON.stringify(expertise));
136
+ db.prepare(`
137
+ INSERT OR IGNORE INTO turn_state (meeting_id, agent_name) VALUES (?, ?)
138
+ `).run(meetingId, agentName);
139
+ }
140
+
141
+ export function getParticipants(db: Database.Database, meetingId: string): any[] {
142
+ return db.prepare("SELECT * FROM participants WHERE meeting_id = ? AND status = 'active'").all(meetingId);
143
+ }
144
+
145
+ export function updatePollTime(db: Database.Database, meetingId: string, agentName: string): void {
146
+ db.prepare("UPDATE participants SET last_poll_at = datetime('now') WHERE meeting_id = ? AND agent_name = ?").run(meetingId, agentName);
147
+ }
148
+
149
+ export function markIdleParticipants(db: Database.Database, meetingId: string, timeoutSeconds: number = 30): void {
150
+ db.prepare(`
151
+ UPDATE participants SET status = 'idle'
152
+ WHERE meeting_id = ? AND status = 'active'
153
+ AND last_poll_at IS NOT NULL
154
+ AND (julianday('now') - julianday(last_poll_at)) * 86400 > ?
155
+ `).run(meetingId, timeoutSeconds);
156
+ }
157
+
158
+ export function reactivateParticipant(db: Database.Database, meetingId: string, agentName: string): void {
159
+ db.prepare("UPDATE participants SET status = 'active', last_poll_at = datetime('now') WHERE meeting_id = ? AND agent_name = ?").run(meetingId, agentName);
160
+ }
161
+
162
+ // --- Message Operations ---
163
+
164
+ export function addMessage(db: Database.Database, meetingId: string, agentName: string, content: string, addresses?: string[], msgType?: string): number {
165
+ const result = db.prepare(`
166
+ INSERT INTO messages (meeting_id, agent_name, content, addresses, msg_type)
167
+ VALUES (?, ?, ?, ?, ?)
168
+ `).run(meetingId, agentName, content, addresses ? JSON.stringify(addresses) : null, msgType || "statement");
169
+
170
+ // Update turn state
171
+ db.prepare(`
172
+ UPDATE turn_state SET last_spoke_at = datetime('now'), speak_count = speak_count + 1, wants_turn = 0
173
+ WHERE meeting_id = ? AND agent_name = ?
174
+ `).run(meetingId, agentName);
175
+
176
+ return Number(result.lastInsertRowid);
177
+ }
178
+
179
+ export function getMessagesSince(db: Database.Database, meetingId: string, sinceId: number): any[] {
180
+ return db.prepare("SELECT * FROM messages WHERE meeting_id = ? AND id > ? ORDER BY id ASC").all(meetingId, sinceId);
181
+ }
182
+
183
+ export function getRecentMessages(db: Database.Database, meetingId: string, limit: number = 5): any[] {
184
+ return db.prepare("SELECT * FROM messages WHERE meeting_id = ? ORDER BY id DESC LIMIT ?").all(meetingId, limit).reverse();
185
+ }
186
+
187
+ export function getMessageCount(db: Database.Database, meetingId: string): number {
188
+ const row = db.prepare("SELECT COUNT(*) as count FROM messages WHERE meeting_id = ?").get(meetingId) as any;
189
+ return row?.count || 0;
190
+ }
191
+
192
+ // --- Turn State Operations ---
193
+
194
+ export function getTurnStates(db: Database.Database, meetingId: string): any[] {
195
+ return db.prepare("SELECT * FROM turn_state WHERE meeting_id = ?").all(meetingId);
196
+ }
197
+
198
+ export function setWantsTurn(db: Database.Database, meetingId: string, agentName: string): void {
199
+ db.prepare("UPDATE turn_state SET wants_turn = 1 WHERE meeting_id = ? AND agent_name = ?").run(meetingId, agentName);
200
+ }
201
+
202
+ // --- Running Summary ---
203
+
204
+ export function buildRunningSummary(db: Database.Database, meetingId: string): string | null {
205
+ const count = getMessageCount(db, meetingId);
206
+ if (count <= 5) return null;
207
+
208
+ // Get messages beyond the most recent 5
209
+ const older = db.prepare(`
210
+ SELECT agent_name, content, msg_type FROM messages
211
+ WHERE meeting_id = ? AND id NOT IN (
212
+ SELECT id FROM messages WHERE meeting_id = ? ORDER BY id DESC LIMIT 5
213
+ )
214
+ ORDER BY id ASC
215
+ `).all(meetingId, meetingId) as any[];
216
+
217
+ if (older.length === 0) return null;
218
+
219
+ const lines = older.map((m: any) => {
220
+ const prefix = m.msg_type === "question" ? "asked" : m.msg_type === "proposal" ? "proposed" : "said";
221
+ const snippet = m.content.length > 120 ? m.content.slice(0, 120) + "..." : m.content;
222
+ return `[${m.agent_name}] ${prefix}: ${snippet}`;
223
+ });
224
+
225
+ return lines.join("\n");
226
+ }
227
+
228
+ // --- Scheduling Operations ---
229
+
230
+ export function createScheduledMeeting(db: Database.Database, id: string, topic: string, createdBy: string, scheduledFor: string, invited: string[], priority: string = "normal", timeLimit: number = 30): void {
231
+ db.prepare(`INSERT INTO scheduled_meetings (id, topic, created_by, scheduled_for, invited_agents, priority, time_limit_minutes) VALUES (?, ?, ?, ?, ?, ?, ?)`)
232
+ .run(id, topic, createdBy, scheduledFor, JSON.stringify(invited), priority, timeLimit);
233
+ }
234
+
235
+ export function getUpcomingMeetings(db: Database.Database, agentName?: string): any[] {
236
+ const all = db.prepare("SELECT * FROM scheduled_meetings WHERE status = 'scheduled' ORDER BY scheduled_for ASC").all() as any[];
237
+ if (!agentName) return all;
238
+ return all.filter((m: any) => {
239
+ const invited = safeParseArr(m.invited_agents);
240
+ return m.created_by === agentName || invited.includes(agentName);
241
+ });
242
+ }
243
+
244
+ export function activateScheduledMeeting(db: Database.Database, scheduleId: string, meetingId: string): void {
245
+ db.prepare("UPDATE scheduled_meetings SET status = 'activated', meeting_id = ? WHERE id = ?").run(meetingId, scheduleId);
246
+ }
247
+
248
+ // --- Hand-Raise Operations ---
249
+
250
+ export function raiseHand(db: Database.Database, meetingId: string, agentName: string, selfScore: number, intentHash: string, inResponseTo: number | null, urgency: string = "normal"): void {
251
+ // Withdraw any existing pending hand-raise
252
+ db.prepare("UPDATE hand_raises SET status = 'withdrawn' WHERE meeting_id = ? AND agent_name = ? AND status = 'pending'").run(meetingId, agentName);
253
+ // Insert new
254
+ db.prepare(`INSERT INTO hand_raises (meeting_id, agent_name, self_score, intent_hash, in_response_to, urgency) VALUES (?, ?, ?, ?, ?, ?)`)
255
+ .run(meetingId, agentName, selfScore, intentHash, inResponseTo, urgency);
256
+ }
257
+
258
+ export function lowerHand(db: Database.Database, meetingId: string, agentName: string): void {
259
+ db.prepare("UPDATE hand_raises SET status = 'withdrawn' WHERE meeting_id = ? AND agent_name = ? AND status = 'pending'").run(meetingId, agentName);
260
+ }
261
+
262
+ export function reEvaluateHand(db: Database.Database, meetingId: string, agentName: string, newScore: number, reason: string): void {
263
+ if (reason === "withdraw") {
264
+ lowerHand(db, meetingId, agentName);
265
+ return;
266
+ }
267
+ db.prepare("UPDATE hand_raises SET self_score = ? WHERE meeting_id = ? AND agent_name = ? AND status = 'pending'").run(newScore, meetingId, agentName);
268
+ }
269
+
270
+ export function getPendingHandRaises(db: Database.Database, meetingId: string): any[] {
271
+ return db.prepare("SELECT * FROM hand_raises WHERE meeting_id = ? AND status = 'pending' ORDER BY raised_at ASC").all(meetingId);
272
+ }
273
+
274
+ export function clearHandRaise(db: Database.Database, meetingId: string, agentName: string): void {
275
+ db.prepare("UPDATE hand_raises SET status = 'speaking' WHERE meeting_id = ? AND agent_name = ? AND status = 'pending'").run(meetingId, agentName);
276
+ }
277
+
278
+ export function expireStaleHandRaises(db: Database.Database, meetingId: string, maxAgeSec: number = 300): void {
279
+ db.prepare(`UPDATE hand_raises SET status = 'expired' WHERE meeting_id = ? AND status = 'pending' AND (julianday('now') - julianday(raised_at)) * 86400 > ?`).run(meetingId, maxAgeSec);
280
+ }
281
+
282
+ // --- Meeting Artifacts ---
283
+
284
+ export function addArtifact(db: Database.Database, meetingId: string, type: string, content: string, assignedTo?: string): void {
285
+ db.prepare("INSERT INTO meeting_artifacts (meeting_id, artifact_type, content, assigned_to) VALUES (?, ?, ?, ?)").run(meetingId, type, content, assignedTo || null);
286
+ }
287
+
288
+ export function getArtifacts(db: Database.Database, meetingId: string): any[] {
289
+ return db.prepare("SELECT * FROM meeting_artifacts WHERE meeting_id = ? ORDER BY id").all(meetingId);
290
+ }
291
+
292
+ export function generateMeetingArtifacts(db: Database.Database, meetingId: string): any {
293
+ const meeting = getMeeting(db, meetingId);
294
+ const messages = db.prepare("SELECT * FROM messages WHERE meeting_id = ? ORDER BY id").all(meetingId) as any[];
295
+ const participants = getParticipants(db, meetingId);
296
+
297
+ const decisions = messages.filter((m: any) => m.msg_type === "decision");
298
+ const actionPattern = /(?:I will|I'll|action[: ]|TODO[: ]|will do|my task|assigned to me)/i;
299
+ const actionItems = messages.filter((m: any) => actionPattern.test(m.content));
300
+
301
+ for (const d of decisions) {
302
+ addArtifact(db, meetingId, "decision", d.content, d.agent_name);
303
+ }
304
+ for (const a of actionItems) {
305
+ addArtifact(db, meetingId, "action_item", a.content, a.agent_name);
306
+ }
307
+
308
+ return {
309
+ meeting_id: meetingId,
310
+ topic: meeting?.topic,
311
+ participant_count: participants.length,
312
+ message_count: messages.length,
313
+ decisions: decisions.length,
314
+ action_items: actionItems.length,
315
+ };
316
+ }
317
+
318
+ function safeParseArr(val: any): string[] {
319
+ if (Array.isArray(val)) return val;
320
+ if (typeof val === "string") { try { return JSON.parse(val); } catch { return []; } }
321
+ return [];
322
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * MetaClaw Boardroom — Meeting Mode Agent Script
3
+ *
4
+ * When a cron agent detects a meeting, this script runs instead of the normal cycle.
5
+ * It joins the meeting, enters a polling loop, responds when signaled,
6
+ * and exits ONLY when the meeting ends.
7
+ *
8
+ * Usage: npx tsx boardroom/meeting-agent.ts <meeting_id> <agent_name> [coordinator_url]
9
+ */
10
+
11
+ const meetingId = process.argv[2];
12
+ const agentName = process.argv[3];
13
+ const coordinator = process.argv[4] || "http://localhost:7890";
14
+
15
+ if (!meetingId || !agentName) {
16
+ console.error("Usage: npx tsx boardroom/meeting-agent.ts <meeting_id> <agent_name> [coordinator_url]");
17
+ process.exit(1);
18
+ }
19
+
20
+ const POLL_INTERVAL = 2000; // 2 seconds
21
+ let lastMessageId = 0;
22
+
23
+ async function post(path: string, body: any): Promise<any> {
24
+ const res = await fetch(`${coordinator}${path}`, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify(body),
28
+ });
29
+ return res.json();
30
+ }
31
+
32
+ async function get(path: string): Promise<any> {
33
+ const res = await fetch(`${coordinator}${path}`);
34
+ return res.json();
35
+ }
36
+
37
+ async function joinMeeting() {
38
+ console.log(`[${agentName}] Joining meeting ${meetingId}...`);
39
+ const result = await post(`/meetings/${meetingId}/join`, {
40
+ agent_name: agentName,
41
+ expertise: [], // Agent should fill this based on its config
42
+ });
43
+ console.log(`[${agentName}] Joined. Participants: ${result.context?.participants?.join(", ") || "unknown"}`);
44
+ console.log(`[${agentName}] Topic: ${result.context?.meeting_topic || "unknown"}`);
45
+ return result;
46
+ }
47
+
48
+ async function pollLoop() {
49
+ console.log(`[${agentName}] Entering meeting polling loop...`);
50
+
51
+ while (true) {
52
+ try {
53
+ const data = await get(`/meetings/${meetingId}/poll?agent=${encodeURIComponent(agentName)}&since=${lastMessageId}`);
54
+
55
+ // Meeting ended?
56
+ if (data.meeting_status === "ended") {
57
+ console.log(`[${agentName}] Meeting ended.`);
58
+ break;
59
+ }
60
+
61
+ // Process new messages
62
+ if (data.messages && data.messages.length > 0) {
63
+ for (const msg of data.messages) {
64
+ lastMessageId = Math.max(lastMessageId, msg.id);
65
+ if (msg.agent_name !== agentName) {
66
+ console.log(`[${msg.agent_name}]: ${msg.content.slice(0, 100)}${msg.content.length > 100 ? "..." : ""}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ // Coverage check — should we re-evaluate?
72
+ if (data.coverage_check && data.coverage_check.action_required === "re-evaluate") {
73
+ console.log(`[${agentName}] Coverage detected — re-evaluating relevance...`);
74
+ // In a real implementation, the Claude session would evaluate this
75
+ // For now, we lower our score slightly
76
+ await post(`/meetings/${meetingId}/re-evaluate`, {
77
+ agent_name: agentName,
78
+ new_self_score: 0.4,
79
+ reason: "covered",
80
+ });
81
+ }
82
+
83
+ // Turn signal — should we speak?
84
+ if (data.turn_signal && data.turn_signal.should_speak) {
85
+ console.log(`[${agentName}] My turn (score: ${data.turn_signal.final_score || data.turn_signal.score}, reason: ${data.turn_signal.reason})`);
86
+ // In a real Claude session, the agent generates a thoughtful response here
87
+ // For this template, we signal readiness but the actual content comes from the Claude session
88
+ console.log(`[${agentName}] Generating response...`);
89
+ // The Claude session would call speak() here
90
+ }
91
+
92
+ // Queue position info
93
+ if (data.queue_position) {
94
+ console.log(`[${agentName}] Queue position: ${data.queue_position}/${data.queue_size}`);
95
+ }
96
+
97
+ } catch (err) {
98
+ console.error(`[${agentName}] Poll error (retrying):`, (err as Error).message);
99
+ }
100
+
101
+ await new Promise(r => setTimeout(r, POLL_INTERVAL));
102
+ }
103
+ }
104
+
105
+ async function main() {
106
+ try {
107
+ await joinMeeting();
108
+ await pollLoop();
109
+ console.log(`[${agentName}] Meeting mode complete. Returning to normal cycle.`);
110
+ } catch (err) {
111
+ console.error(`[${agentName}] Meeting mode error:`, err);
112
+ }
113
+
114
+ // Clean up
115
+ try {
116
+ const fs = await import("fs");
117
+ // Remove meeting lock
118
+ fs.unlinkSync("data/.meeting-lock");
119
+ // Remove MEETING_READY sentinel
120
+ const inbox = `__INBOX_ROOT__\\${agentName.toLowerCase()}`;
121
+ const files = fs.readdirSync(inbox).filter((f: string) => f.includes(`MEETING_READY_${meetingId}`));
122
+ for (const f of files) fs.unlinkSync(`${inbox}/${f}`);
123
+ // Remove MEETING_ENDED sentinel
124
+ const endFiles = fs.readdirSync(inbox).filter((f: string) => f.includes(`MEETING_ENDED_${meetingId}`));
125
+ for (const f of endFiles) fs.unlinkSync(`${inbox}/${f}`);
126
+ } catch {}
127
+ }
128
+
129
+ main();
@@ -0,0 +1,194 @@
1
+ /**
2
+ * MetaClaw Boardroom — Meeting Scheduler
3
+ *
4
+ * Scans inbox for boardroom summons with scheduled times.
5
+ * Sets timers to auto-join at the EXACT scheduled time.
6
+ * No early joins. No late joins. Precise.
7
+ *
8
+ * Usage:
9
+ * Called at the start of every agent cycle:
10
+ * npx tsx boardroom/meeting-scheduler.ts <agent_name>
11
+ *
12
+ * Or run as a persistent watcher:
13
+ * npx tsx boardroom/meeting-scheduler.ts <agent_name> --watch
14
+ */
15
+
16
+ import fs from "fs";
17
+ import path from "path";
18
+
19
+ const agentName = process.argv[2];
20
+ const watchMode = process.argv.includes("--watch");
21
+
22
+ if (!agentName) {
23
+ console.error("Usage: npx tsx boardroom/meeting-scheduler.ts <agent_name> [--watch]");
24
+ process.exit(1);
25
+ }
26
+
27
+ const INBOX_PATH = path.join("__INBOX_ROOT__", agentName.toLowerCase());
28
+ const PENDING_FILE = path.join(process.cwd(), "data", "pending-meetings.json");
29
+
30
+ interface PendingMeeting {
31
+ meeting_id: string;
32
+ coordinator_url: string;
33
+ topic: string;
34
+ scheduled_time: string; // ISO 8601 UTC
35
+ from: string;
36
+ timer_set: boolean;
37
+ }
38
+
39
+ function loadPending(): PendingMeeting[] {
40
+ try { return JSON.parse(fs.readFileSync(PENDING_FILE, "utf-8")); } catch { return []; }
41
+ }
42
+
43
+ function savePending(meetings: PendingMeeting[]) {
44
+ fs.mkdirSync(path.dirname(PENDING_FILE), { recursive: true });
45
+ fs.writeFileSync(PENDING_FILE, JSON.stringify(meetings, null, 2));
46
+ }
47
+
48
+ /**
49
+ * Scan inbox for boardroom summons with scheduled times
50
+ */
51
+ function scanForSummons(): PendingMeeting[] {
52
+ if (!fs.existsSync(INBOX_PATH)) return [];
53
+ const files = fs.readdirSync(INBOX_PATH).filter(f => f.endsWith(".json"));
54
+ const found: PendingMeeting[] = [];
55
+
56
+ for (const file of files) {
57
+ try {
58
+ const msg = JSON.parse(fs.readFileSync(path.join(INBOX_PATH, file), "utf-8"));
59
+ if (msg.boardroom?.scheduled_time && msg.boardroom?.meeting_id) {
60
+ found.push({
61
+ meeting_id: msg.boardroom.meeting_id,
62
+ coordinator_url: msg.boardroom.coordinator_url || "http://localhost:7890",
63
+ topic: msg.boardroom.topic || msg.subject || "Meeting",
64
+ scheduled_time: msg.boardroom.scheduled_time,
65
+ from: msg.from || "unknown",
66
+ timer_set: false,
67
+ });
68
+ }
69
+ } catch {}
70
+ }
71
+ return found;
72
+ }
73
+
74
+ /**
75
+ * Join a meeting at the exact scheduled time
76
+ */
77
+ async function joinAtExactTime(meeting: PendingMeeting) {
78
+ const scheduledMs = new Date(meeting.scheduled_time).getTime();
79
+ const now = Date.now();
80
+ const waitMs = scheduledMs - now;
81
+
82
+ if (waitMs < -300000) {
83
+ // More than 5 min past — meeting probably over
84
+ console.log(`[${agentName}] Meeting "${meeting.topic}" is ${Math.round(-waitMs/60000)} min past scheduled time. Skipping.`);
85
+ return;
86
+ }
87
+
88
+ if (waitMs > 0) {
89
+ console.log(`[${agentName}] Meeting "${meeting.topic}" in ${Math.round(waitMs/60000)} min. Waiting...`);
90
+ await new Promise(r => setTimeout(r, waitMs));
91
+ }
92
+
93
+ console.log(`[${agentName}] === JOINING MEETING: ${meeting.topic} ===`);
94
+
95
+ try {
96
+ // Join
97
+ const joinRes = await fetch(`${meeting.coordinator_url}/meetings/${meeting.meeting_id}/join`, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ agent_name: agentName, expertise: [] }),
101
+ });
102
+ const joinData = await joinRes.json();
103
+ console.log(`[${agentName}] Joined. Entering meeting loop...`);
104
+
105
+ // Meeting loop — stay until meeting ends
106
+ let lastMsgId = 0;
107
+ while (true) {
108
+ try {
109
+ const pollRes = await fetch(
110
+ `${meeting.coordinator_url}/meetings/${meeting.meeting_id}/poll?agent=${encodeURIComponent(agentName)}&since=${lastMsgId}`
111
+ );
112
+ const data = await pollRes.json();
113
+
114
+ if (data.meeting_status === "ended") {
115
+ console.log(`[${agentName}] Meeting ended.`);
116
+ break;
117
+ }
118
+
119
+ // Log new messages
120
+ if (data.messages?.length > 0) {
121
+ for (const msg of data.messages) {
122
+ lastMsgId = Math.max(lastMsgId, msg.id);
123
+ if (msg.agent_name !== agentName) {
124
+ console.log(` [${msg.agent_name}]: ${msg.content.slice(0, 120)}...`);
125
+ }
126
+ }
127
+ }
128
+
129
+ // Turn signal
130
+ if (data.turn_signal?.should_speak) {
131
+ console.log(`[${agentName}] MY TURN (score: ${data.turn_signal.score || data.turn_signal.final_score}, reason: ${data.turn_signal.reason})`);
132
+ // In a real agent session, this is where the LLM generates a response
133
+ // The meeting-agent.ts or the Claude session handles the actual speaking
134
+ }
135
+ } catch (err) {
136
+ console.error(`[${agentName}] Poll error:`, (err as Error).message);
137
+ }
138
+
139
+ await new Promise(r => setTimeout(r, 2000));
140
+ }
141
+ } catch (err) {
142
+ console.error(`[${agentName}] Join failed:`, (err as Error).message);
143
+ }
144
+
145
+ // Clean up: remove from pending
146
+ const pending = loadPending().filter(m => m.meeting_id !== meeting.meeting_id);
147
+ savePending(pending);
148
+ }
149
+
150
+ /**
151
+ * Main: scan, schedule, join
152
+ */
153
+ async function main() {
154
+ // Load existing pending meetings
155
+ let pending = loadPending();
156
+
157
+ // Scan inbox for new summons
158
+ const newSummons = scanForSummons();
159
+ for (const s of newSummons) {
160
+ if (!pending.find(p => p.meeting_id === s.meeting_id)) {
161
+ pending.push(s);
162
+ console.log(`[${agentName}] New meeting scheduled: "${s.topic}" at ${s.scheduled_time} from ${s.from}`);
163
+ }
164
+ }
165
+ savePending(pending);
166
+
167
+ // Check for meetings that should be joined NOW (within next 5 min)
168
+ const now = Date.now();
169
+ const imminent = pending.filter(m => {
170
+ const scheduledMs = new Date(m.scheduled_time).getTime();
171
+ const diff = scheduledMs - now;
172
+ return diff < 300000 && diff > -300000; // Within 5 min window
173
+ });
174
+
175
+ if (imminent.length > 0) {
176
+ console.log(`[${agentName}] ${imminent.length} meeting(s) imminent. Joining...`);
177
+ // Join the first one (if multiple, join highest priority)
178
+ await joinAtExactTime(imminent[0]);
179
+ } else if (pending.length > 0) {
180
+ const next = pending.sort((a, b) => new Date(a.scheduled_time).getTime() - new Date(b.scheduled_time).getTime())[0];
181
+ const minUntil = Math.round((new Date(next.scheduled_time).getTime() - now) / 60000);
182
+ console.log(`[${agentName}] Next meeting: "${next.topic}" in ${minUntil} min.`);
183
+
184
+ if (watchMode) {
185
+ // In watch mode, wait for the meeting
186
+ console.log(`[${agentName}] Watch mode — waiting for meeting time...`);
187
+ await joinAtExactTime(next);
188
+ }
189
+ } else {
190
+ console.log(`[${agentName}] No pending meetings.`);
191
+ }
192
+ }
193
+
194
+ main().catch(console.error);