@datafrog-io/n2n-nexus 0.1.7 → 0.1.8

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.
@@ -0,0 +1,249 @@
1
+ import { getDatabase, initDatabase } from "./sqlite.js";
2
+ /**
3
+ * SQLite-backed Meeting Store
4
+ * Provides ACID-compliant concurrent access to meeting data
5
+ */
6
+ export class SqliteMeetingStore {
7
+ /**
8
+ * Initialize the database
9
+ */
10
+ static init() {
11
+ initDatabase();
12
+ }
13
+ /**
14
+ * Generate a unique meeting ID
15
+ */
16
+ static generateMeetingId(topic) {
17
+ const now = new Date();
18
+ const timestamp = now.toISOString().replace(/[-:T]/g, "").substring(0, 14);
19
+ // Create slug from topic, fallback to base64 hash for non-ASCII
20
+ let slug = topic
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, "-")
23
+ .replace(/^-|-$/g, "")
24
+ .substring(0, 30);
25
+ // If slug is empty (e.g., Chinese topic), use base64 of topic
26
+ if (!slug) {
27
+ slug = Buffer.from(topic).toString("base64").replace(/[^a-zA-Z0-9]/g, "").substring(0, 8).toLowerCase();
28
+ }
29
+ // Add random suffix for uniqueness (prevents collision in same second)
30
+ const suffix = Math.random().toString(36).substring(2, 6);
31
+ return `${timestamp}-${slug || "meeting"}-${suffix}`;
32
+ }
33
+ /**
34
+ * Start a new meeting
35
+ */
36
+ static startMeeting(topic, initiator) {
37
+ const db = getDatabase();
38
+ const id = this.generateMeetingId(topic);
39
+ const now = new Date().toISOString();
40
+ const participants = JSON.stringify([initiator]);
41
+ const stmt = db.prepare(`
42
+ INSERT INTO meetings (id, topic, status, participants, created_at)
43
+ VALUES (?, ?, 'active', ?, ?)
44
+ `);
45
+ stmt.run(id, topic, participants, now);
46
+ // Update state
47
+ this.updateState("default_meeting", id);
48
+ const activeMeetings = this.getActiveMeetingIds();
49
+ activeMeetings.push(id);
50
+ this.updateState("active_meetings", JSON.stringify(activeMeetings));
51
+ return {
52
+ id,
53
+ topic,
54
+ status: "active",
55
+ startTime: now,
56
+ participants: [initiator],
57
+ messages: [],
58
+ decisions: []
59
+ };
60
+ }
61
+ /**
62
+ * Get a meeting by ID
63
+ */
64
+ static getMeeting(id) {
65
+ const db = getDatabase();
66
+ const meetingStmt = db.prepare("SELECT * FROM meetings WHERE id = ?");
67
+ const meeting = meetingStmt.get(id);
68
+ if (!meeting)
69
+ return null;
70
+ // Get messages
71
+ const messagesStmt = db.prepare(`
72
+ SELECT sender as "from", text, category, timestamp
73
+ FROM messages WHERE meeting_id = ?
74
+ ORDER BY timestamp ASC
75
+ `);
76
+ const messages = messagesStmt.all(id);
77
+ // Get decisions
78
+ const decisionsStmt = db.prepare(`
79
+ SELECT content FROM decisions WHERE meeting_id = ?
80
+ ORDER BY timestamp ASC
81
+ `);
82
+ const decisions = decisionsStmt.all(id).map(d => d.content);
83
+ return {
84
+ id: meeting.id,
85
+ topic: meeting.topic,
86
+ status: meeting.status,
87
+ startTime: meeting.created_at,
88
+ endTime: meeting.closed_at || undefined,
89
+ participants: JSON.parse(meeting.participants),
90
+ messages,
91
+ decisions,
92
+ summary: meeting.summary || undefined
93
+ };
94
+ }
95
+ /**
96
+ * Add a message to a meeting
97
+ */
98
+ static addMessage(meetingId, message) {
99
+ const db = getDatabase();
100
+ // Check meeting status
101
+ const checkStmt = db.prepare("SELECT status, participants FROM meetings WHERE id = ?");
102
+ const meeting = checkStmt.get(meetingId);
103
+ if (!meeting)
104
+ throw new Error(`Meeting '${meetingId}' not found.`);
105
+ if (meeting.status !== "active")
106
+ throw new Error(`Meeting '${meetingId}' is ${meeting.status}, cannot add messages.`);
107
+ // Insert message
108
+ const insertStmt = db.prepare(`
109
+ INSERT INTO messages (meeting_id, sender, text, category, timestamp)
110
+ VALUES (?, ?, ?, ?, ?)
111
+ `);
112
+ insertStmt.run(meetingId, message.from, message.text, message.category || null, message.timestamp);
113
+ // Track participant
114
+ const participants = JSON.parse(meeting.participants);
115
+ if (!participants.includes(message.from)) {
116
+ participants.push(message.from);
117
+ const updateStmt = db.prepare("UPDATE meetings SET participants = ? WHERE id = ?");
118
+ updateStmt.run(JSON.stringify(participants), meetingId);
119
+ }
120
+ // Extract decision
121
+ if (message.category === "DECISION") {
122
+ const decisionStmt = db.prepare(`
123
+ INSERT INTO decisions (meeting_id, content, timestamp)
124
+ VALUES (?, ?, ?)
125
+ `);
126
+ decisionStmt.run(meetingId, message.text, message.timestamp);
127
+ }
128
+ }
129
+ /**
130
+ * End a meeting
131
+ */
132
+ static endMeeting(meetingId, summary) {
133
+ const db = getDatabase();
134
+ const now = new Date().toISOString();
135
+ // Check meeting exists and is active
136
+ const meeting = this.getMeeting(meetingId);
137
+ if (!meeting)
138
+ throw new Error(`Meeting '${meetingId}' not found.`);
139
+ if (meeting.status !== "active")
140
+ throw new Error(`Meeting '${meetingId}' is already ${meeting.status}.`);
141
+ // Update meeting
142
+ const stmt = db.prepare(`
143
+ UPDATE meetings SET status = 'closed', closed_at = ?, summary = ?
144
+ WHERE id = ?
145
+ `);
146
+ stmt.run(now, summary || null, meetingId);
147
+ // Update state
148
+ const activeMeetings = this.getActiveMeetingIds().filter(id => id !== meetingId);
149
+ this.updateState("active_meetings", JSON.stringify(activeMeetings));
150
+ this.updateState("default_meeting", activeMeetings.length > 0 ? activeMeetings[activeMeetings.length - 1] : "");
151
+ // Refresh meeting data
152
+ const updatedMeeting = this.getMeeting(meetingId);
153
+ // Suggest sync targets based on participants
154
+ const suggestedSyncTargets = updatedMeeting.participants
155
+ .map(p => p.split("@")[1])
156
+ .filter((v, i, a) => v && v !== "Global" && a.indexOf(v) === i);
157
+ return { meeting: updatedMeeting, suggestedSyncTargets };
158
+ }
159
+ /**
160
+ * Archive a meeting
161
+ */
162
+ static archiveMeeting(meetingId) {
163
+ const db = getDatabase();
164
+ const meeting = this.getMeeting(meetingId);
165
+ if (!meeting)
166
+ throw new Error(`Meeting '${meetingId}' not found.`);
167
+ if (meeting.status === "active")
168
+ throw new Error(`Meeting '${meetingId}' is still active. End it first.`);
169
+ const stmt = db.prepare("UPDATE meetings SET status = 'archived' WHERE id = ?");
170
+ stmt.run(meetingId);
171
+ }
172
+ /**
173
+ * List meetings with optional status filter
174
+ */
175
+ static listMeetings(status) {
176
+ const db = getDatabase();
177
+ let query = "SELECT id, topic, status, participants, created_at FROM meetings";
178
+ const params = [];
179
+ if (status) {
180
+ query += " WHERE status = ?";
181
+ params.push(status);
182
+ }
183
+ query += " ORDER BY created_at DESC";
184
+ const stmt = db.prepare(query);
185
+ const meetings = (params.length > 0 ? stmt.all(...params) : stmt.all());
186
+ return meetings.map(m => ({
187
+ id: m.id,
188
+ topic: m.topic,
189
+ status: m.status,
190
+ startTime: m.created_at,
191
+ participantCount: JSON.parse(m.participants).length
192
+ }));
193
+ }
194
+ /**
195
+ * Get the current active meeting
196
+ */
197
+ static getActiveMeeting() {
198
+ const defaultId = this.getState("default_meeting");
199
+ if (!defaultId)
200
+ return null;
201
+ return this.getMeeting(defaultId);
202
+ }
203
+ /**
204
+ * Get recent messages from a meeting
205
+ */
206
+ static getRecentMessages(count = 10, meetingId) {
207
+ const db = getDatabase();
208
+ const targetId = meetingId || this.getState("default_meeting");
209
+ if (!targetId)
210
+ return [];
211
+ const stmt = db.prepare(`
212
+ SELECT sender as "from", text, category, timestamp
213
+ FROM messages WHERE meeting_id = ?
214
+ ORDER BY timestamp DESC LIMIT ?
215
+ `);
216
+ const messages = stmt.all(targetId, count);
217
+ // Reverse to get chronological order
218
+ return messages.reverse();
219
+ }
220
+ /**
221
+ * Get meeting state value
222
+ */
223
+ static getState(key) {
224
+ const db = getDatabase();
225
+ const stmt = db.prepare("SELECT value FROM meeting_state WHERE key = ?");
226
+ const row = stmt.get(key);
227
+ return row?.value || "";
228
+ }
229
+ /**
230
+ * Update meeting state value
231
+ */
232
+ static updateState(key, value) {
233
+ const db = getDatabase();
234
+ const stmt = db.prepare("INSERT OR REPLACE INTO meeting_state (key, value) VALUES (?, ?)");
235
+ stmt.run(key, value);
236
+ }
237
+ /**
238
+ * Get list of active meeting IDs
239
+ */
240
+ static getActiveMeetingIds() {
241
+ const value = this.getState("active_meetings");
242
+ try {
243
+ return JSON.parse(value) || [];
244
+ }
245
+ catch {
246
+ return [];
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,120 @@
1
+ import Database from "better-sqlite3";
2
+ import path from "path";
3
+ import { CONFIG } from "../config.js";
4
+ let db = null;
5
+ /**
6
+ * SQLite Database Schema for Nexus Meetings
7
+ */
8
+ const SCHEMA = `
9
+ -- Meetings table
10
+ CREATE TABLE IF NOT EXISTS meetings (
11
+ id TEXT PRIMARY KEY,
12
+ topic TEXT NOT NULL,
13
+ status TEXT CHECK(status IN ('active', 'closed', 'archived')) DEFAULT 'active',
14
+ participants TEXT DEFAULT '[]',
15
+ created_at TEXT NOT NULL,
16
+ closed_at TEXT,
17
+ summary TEXT
18
+ );
19
+
20
+ -- Messages table
21
+ CREATE TABLE IF NOT EXISTS messages (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ meeting_id TEXT NOT NULL,
24
+ sender TEXT NOT NULL,
25
+ text TEXT NOT NULL,
26
+ category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
27
+ timestamp TEXT NOT NULL,
28
+ FOREIGN KEY (meeting_id) REFERENCES meetings(id)
29
+ );
30
+
31
+ -- Decisions table (extracted from DECISION messages)
32
+ CREATE TABLE IF NOT EXISTS decisions (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ meeting_id TEXT NOT NULL,
35
+ content TEXT NOT NULL,
36
+ timestamp TEXT NOT NULL,
37
+ FOREIGN KEY (meeting_id) REFERENCES meetings(id)
38
+ );
39
+
40
+ -- Meeting state (Key-Value store)
41
+ CREATE TABLE IF NOT EXISTS meeting_state (
42
+ key TEXT PRIMARY KEY,
43
+ value TEXT NOT NULL
44
+ );
45
+
46
+ -- Indexes for performance
47
+ CREATE INDEX IF NOT EXISTS idx_messages_meeting ON messages(meeting_id);
48
+ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
49
+ CREATE INDEX IF NOT EXISTS idx_decisions_meeting ON decisions(meeting_id);
50
+ CREATE INDEX IF NOT EXISTS idx_meetings_status ON meetings(status);
51
+ `;
52
+ /**
53
+ * Get the database file path
54
+ */
55
+ export function getDbPath() {
56
+ return path.join(CONFIG.rootStorage, "nexus.db");
57
+ }
58
+ /**
59
+ * Initialize the SQLite database with WAL mode
60
+ */
61
+ export function initDatabase() {
62
+ if (db)
63
+ return db;
64
+ const dbPath = getDbPath();
65
+ db = new Database(dbPath);
66
+ // Enable WAL mode for better concurrent access
67
+ db.pragma("journal_mode = WAL");
68
+ // Initialize schema
69
+ db.exec(SCHEMA);
70
+ // Initialize default state if not exists
71
+ const stmt = db.prepare("INSERT OR IGNORE INTO meeting_state (key, value) VALUES (?, ?)");
72
+ stmt.run("active_meetings", "[]");
73
+ stmt.run("default_meeting", "");
74
+ console.error("[Nexus] SQLite database initialized at:", dbPath);
75
+ return db;
76
+ }
77
+ /**
78
+ * Get the database instance
79
+ */
80
+ export function getDatabase() {
81
+ if (!db) {
82
+ return initDatabase();
83
+ }
84
+ return db;
85
+ }
86
+ /**
87
+ * Close the database connection
88
+ */
89
+ export function closeDatabase() {
90
+ if (db) {
91
+ db.close();
92
+ db = null;
93
+ console.error("[Nexus] SQLite database closed");
94
+ }
95
+ }
96
+ /**
97
+ * Check if SQLite is available
98
+ */
99
+ export function isSqliteAvailable() {
100
+ try {
101
+ // Try to load better-sqlite3
102
+ require("better-sqlite3");
103
+ return true;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ // Cleanup on process exit
110
+ process.on("exit", () => {
111
+ closeDatabase();
112
+ });
113
+ process.on("SIGINT", () => {
114
+ closeDatabase();
115
+ process.exit(0);
116
+ });
117
+ process.on("SIGTERM", () => {
118
+ closeDatabase();
119
+ process.exit(0);
120
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Meeting Store Entry Point
3
+ *
4
+ * Provides a unified interface that automatically selects between:
5
+ * - SQLite backend (preferred, for concurrent access safety)
6
+ * - JSON backend (fallback, for environments without native module support)
7
+ */
8
+ // Lazy-loaded store implementation
9
+ let storeType = null;
10
+ let SqliteStore = null;
11
+ let JsonStore = null;
12
+ /**
13
+ * Detect and initialize the appropriate store
14
+ */
15
+ async function getStore() {
16
+ if (storeType === "sqlite" && SqliteStore) {
17
+ return { type: "sqlite", store: SqliteStore };
18
+ }
19
+ if (storeType === "json" && JsonStore) {
20
+ return { type: "json", store: JsonStore };
21
+ }
22
+ // Try SQLite first
23
+ try {
24
+ // Dynamic import to avoid bundling issues
25
+ const sqliteModule = await import("./sqlite-meeting.js");
26
+ SqliteStore = sqliteModule.SqliteMeetingStore;
27
+ SqliteStore.init();
28
+ storeType = "sqlite";
29
+ console.error("[Nexus MeetingStore] Using SQLite backend");
30
+ return { type: "sqlite", store: SqliteStore };
31
+ }
32
+ catch (e) {
33
+ console.error("[Nexus MeetingStore] SQLite unavailable:", e.message);
34
+ console.error("[Nexus MeetingStore] Falling back to JSON backend");
35
+ console.warn("[Nexus MeetingStore] ⚠️ JSON mode is single-process only. For multi-IDE environments, install better-sqlite3.");
36
+ // Fall back to JSON
37
+ const jsonModule = await import("./meetings.js");
38
+ JsonStore = jsonModule.MeetingStore;
39
+ storeType = "json";
40
+ return { type: "json", store: JsonStore };
41
+ }
42
+ }
43
+ /**
44
+ * Unified Meeting Store Interface
45
+ */
46
+ export const UnifiedMeetingStore = {
47
+ /**
48
+ * Start a new meeting
49
+ */
50
+ async startMeeting(topic, initiator) {
51
+ const { store } = await getStore();
52
+ return store.startMeeting(topic, initiator);
53
+ },
54
+ /**
55
+ * Get a meeting by ID
56
+ */
57
+ async getMeeting(id) {
58
+ const { store } = await getStore();
59
+ return store.getMeeting(id);
60
+ },
61
+ /**
62
+ * Add a message to a meeting
63
+ */
64
+ async addMessage(meetingId, message) {
65
+ const { store } = await getStore();
66
+ return store.addMessage(meetingId, message);
67
+ },
68
+ /**
69
+ * End a meeting
70
+ */
71
+ async endMeeting(meetingId, summary) {
72
+ const { store } = await getStore();
73
+ return store.endMeeting(meetingId, summary);
74
+ },
75
+ /**
76
+ * Archive a meeting
77
+ */
78
+ async archiveMeeting(meetingId) {
79
+ const { store } = await getStore();
80
+ return store.archiveMeeting(meetingId);
81
+ },
82
+ /**
83
+ * List meetings
84
+ */
85
+ async listMeetings(status) {
86
+ const { store } = await getStore();
87
+ return store.listMeetings(status);
88
+ },
89
+ /**
90
+ * Get the current active meeting
91
+ */
92
+ async getActiveMeeting() {
93
+ const { store } = await getStore();
94
+ return store.getActiveMeeting();
95
+ },
96
+ /**
97
+ * Get recent messages
98
+ */
99
+ async getRecentMessages(count, meetingId) {
100
+ const { store } = await getStore();
101
+ return store.getRecentMessages(count || 10, meetingId);
102
+ },
103
+ /**
104
+ * Get the current backend type
105
+ */
106
+ async getBackendType() {
107
+ const { type } = await getStore();
108
+ return type;
109
+ },
110
+ /**
111
+ * Get meeting state (SQLite only, returns empty for JSON)
112
+ */
113
+ async getState() {
114
+ const { type, store } = await getStore();
115
+ if (type === "sqlite") {
116
+ const sqliteStore = store;
117
+ const activeMeetings = JSON.parse(sqliteStore.getState("active_meetings") || "[]");
118
+ const defaultMeetingId = sqliteStore.getState("default_meeting") || null;
119
+ return { activeMeetings, defaultMeetingId };
120
+ }
121
+ else {
122
+ // JSON backend
123
+ const jsonStore = store;
124
+ const state = await jsonStore.getState();
125
+ return state;
126
+ }
127
+ },
128
+ /**
129
+ * Get storage info for status display
130
+ * @returns storage_mode and is_degraded flag
131
+ */
132
+ async getStorageInfo() {
133
+ const { type } = await getStore();
134
+ return {
135
+ storage_mode: type,
136
+ is_degraded: type === "json" // JSON mode is considered degraded
137
+ };
138
+ }
139
+ };
@@ -78,7 +78,7 @@ export const TOOL_DEFINITIONS = [
78
78
  },
79
79
  {
80
80
  name: "upload_project_asset",
81
- description: "Upload a file. Both fileName and base64Content are MANDATORY.",
81
+ description: "Upload a binary file (images, PDFs, etc.) to the current project's asset folder. Requires active session (call register_session_context first). Returns the relative path of the saved file.",
82
82
  inputSchema: {
83
83
  type: "object",
84
84
  properties: {
@@ -90,7 +90,7 @@ export const TOOL_DEFINITIONS = [
90
90
  },
91
91
  {
92
92
  name: "get_global_topology",
93
- description: "Retrieve complete project relationship graph. Use this to understand current IDs and their connections.",
93
+ description: "Retrieve complete project relationship graph. Returns { nodes: [{ id, name }], edges: [{ from, to, type }] }. Use this to visualize dependencies.",
94
94
  inputSchema: { type: "object", properties: {} }
95
95
  },
96
96
  {
@@ -116,7 +116,7 @@ export const TOOL_DEFINITIONS = [
116
116
  },
117
117
  {
118
118
  name: "post_global_discussion",
119
- description: "Join the 'Nexus Meeting Room' to collaborate with other AI agents. Use this for initiating meetings, making cross-project proposals, or announcing key decisions. Every message is shared across all assistants in real-time.",
119
+ description: "Post a message to the Nexus collaboration space. If an active meeting exists, the message is automatically routed to that meeting. Otherwise, it goes to the global discussion log. Use this for proposals, decisions, or general coordination.",
120
120
  inputSchema: {
121
121
  type: "object",
122
122
  properties: {
@@ -124,7 +124,7 @@ export const TOOL_DEFINITIONS = [
124
124
  category: {
125
125
  type: "string",
126
126
  enum: ["MEETING_START", "PROPOSAL", "DECISION", "UPDATE", "CHAT"],
127
- description: "The nature of this message. Use MEETING_START to call for a synchronous discussion."
127
+ description: "The nature of this message. DECISION messages are auto-extracted into meeting decisions[]."
128
128
  }
129
129
  },
130
130
  required: ["message"]
@@ -132,11 +132,12 @@ export const TOOL_DEFINITIONS = [
132
132
  },
133
133
  {
134
134
  name: "read_recent_discussion",
135
- description: "Quickly 'listen' to the last few messages in the Nexus Room to catch up on the context of the current meeting or collaboration.",
135
+ description: "Read recent messages. Automatically reads from the active meeting if one exists, otherwise reads from global discussion log.",
136
136
  inputSchema: {
137
137
  type: "object",
138
138
  properties: {
139
- count: { type: "number", description: "Number of recent messages to retrieve (defaults to 10).", default: 10 }
139
+ count: { type: "number", description: "Number of recent messages to retrieve (defaults to 10).", default: 10 },
140
+ meetingId: { type: "string", description: "Optional: Read from a specific meeting instead of the active one." }
140
141
  }
141
142
  }
142
143
  },
@@ -204,7 +205,7 @@ export const TOOL_DEFINITIONS = [
204
205
  },
205
206
  {
206
207
  name: "moderator_maintenance",
207
- description: "[ADMIN ONLY] Prune or clear logs. Both action and count are MANDATORY.",
208
+ description: "[ADMIN ONLY] Manage global discussion logs. 'prune' removes the oldest N entries (keeps newest). 'clear' wipes all logs (use count=0). Returns summary of removed entries. Irreversible.",
208
209
  inputSchema: {
209
210
  type: "object",
210
211
  properties: {
@@ -215,8 +216,8 @@ export const TOOL_DEFINITIONS = [
215
216
  }
216
217
  },
217
218
  {
218
- name: "delete_project",
219
- description: "[ADMIN ONLY] Completely remove a project, its manifest, and all its assets from Nexus.",
219
+ name: "moderator_delete_project",
220
+ description: "[ADMIN ONLY] Completely remove a project, its manifest, and all its assets from Nexus Hub. Irreversible.",
220
221
  inputSchema: {
221
222
  type: "object",
222
223
  properties: {
@@ -224,5 +225,64 @@ export const TOOL_DEFINITIONS = [
224
225
  },
225
226
  required: ["projectId"]
226
227
  }
228
+ },
229
+ // --- Meeting Management Tools (Phase 1) ---
230
+ {
231
+ name: "start_meeting",
232
+ description: "Start a new meeting session. Creates a dedicated file for the meeting. Returns the meeting ID and details.",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ topic: { type: "string", description: "The topic/agenda for this meeting (e.g., 'Architecture Review', 'Sprint Planning')." }
237
+ },
238
+ required: ["topic"]
239
+ }
240
+ },
241
+ {
242
+ name: "end_meeting",
243
+ description: "End an active meeting. Locks the session for further messages. Returns suggested sync targets based on participants.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ meetingId: { type: "string", description: "The ID of the meeting to end. If omitted, ends the current active meeting." },
248
+ summary: { type: "string", description: "Optional summary of the meeting conclusions." }
249
+ }
250
+ }
251
+ },
252
+ {
253
+ name: "list_meetings",
254
+ description: "List all meetings with optional status filter. Returns meeting metadata without full message history.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ status: {
259
+ type: "string",
260
+ enum: ["active", "closed", "archived"],
261
+ description: "Filter by meeting status. Omit to list all."
262
+ }
263
+ }
264
+ }
265
+ },
266
+ {
267
+ name: "read_meeting",
268
+ description: "Read the full content of a specific meeting including all messages and decisions.",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ meetingId: { type: "string", description: "The ID of the meeting to read." }
273
+ },
274
+ required: ["meetingId"]
275
+ }
276
+ },
277
+ {
278
+ name: "archive_meeting",
279
+ description: "Archive a closed meeting. Archived meetings are read-only and excluded from active queries.",
280
+ inputSchema: {
281
+ type: "object",
282
+ properties: {
283
+ meetingId: { type: "string", description: "The ID of the closed meeting to archive." }
284
+ },
285
+ required: ["meetingId"]
286
+ }
227
287
  }
228
288
  ];