@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.
- package/README.md +1 -0
- package/README_zh.md +143 -0
- package/build/index.js +1 -1
- package/build/resources/index.js +29 -1
- package/build/storage/index.js +1 -35
- package/build/storage/meetings.js +242 -0
- package/build/storage/sqlite-meeting.js +249 -0
- package/build/storage/sqlite.js +120 -0
- package/build/storage/store.js +139 -0
- package/build/tools/definitions.js +69 -9
- package/build/tools/handlers.js +153 -6
- package/build/utils/async-mutex.js +36 -0
- package/docs/ASSISTANT_GUIDE.md +66 -0
- package/docs/CHANGELOG.md +141 -0
- package/docs/CHANGELOG_zh.md +141 -0
- package/docs/MEETING_MINUTES_2025-12-29.md +160 -0
- package/docs/discussion_2025-12-29_en.json +330 -0
- package/docs/discussion_2025-12-29_en.md +717 -0
- package/package.json +6 -2
|
@@ -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.
|
|
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.
|
|
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: "
|
|
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.
|
|
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: "
|
|
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]
|
|
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: "
|
|
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
|
];
|