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