@datafrog-io/n2n-nexus 0.1.7 → 0.2.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.
@@ -0,0 +1,146 @@
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, callerId) {
72
+ const { store } = await getStore();
73
+ return store.endMeeting(meetingId, summary, callerId);
74
+ },
75
+ /**
76
+ * Archive a meeting
77
+ */
78
+ async archiveMeeting(meetingId, callerId) {
79
+ const { store } = await getStore();
80
+ return store.archiveMeeting(meetingId, callerId);
81
+ },
82
+ /**
83
+ * Reopen a meeting
84
+ */
85
+ async reopenMeeting(meetingId, callerId) {
86
+ const { store } = await getStore();
87
+ return store.reopenMeeting(meetingId, callerId);
88
+ },
89
+ /**
90
+ * List meetings
91
+ */
92
+ async listMeetings(status) {
93
+ const { store } = await getStore();
94
+ return store.listMeetings(status);
95
+ },
96
+ /**
97
+ * Get the current active meeting
98
+ */
99
+ async getActiveMeeting() {
100
+ const { store } = await getStore();
101
+ return store.getActiveMeeting();
102
+ },
103
+ /**
104
+ * Get recent messages
105
+ */
106
+ async getRecentMessages(count, meetingId) {
107
+ const { store } = await getStore();
108
+ return store.getRecentMessages(count || 10, meetingId);
109
+ },
110
+ /**
111
+ * Get the current backend type
112
+ */
113
+ async getBackendType() {
114
+ const { type } = await getStore();
115
+ return type;
116
+ },
117
+ /**
118
+ * Get meeting state (SQLite only, returns empty for JSON)
119
+ */
120
+ async getState() {
121
+ const { type, store } = await getStore();
122
+ if (type === "sqlite") {
123
+ const sqliteStore = store;
124
+ const activeMeetings = JSON.parse(sqliteStore.getState("active_meetings") || "[]");
125
+ const defaultMeetingId = sqliteStore.getState("default_meeting") || null;
126
+ return { activeMeetings, defaultMeetingId };
127
+ }
128
+ else {
129
+ // JSON backend
130
+ const jsonStore = store;
131
+ const state = await jsonStore.getState();
132
+ return state;
133
+ }
134
+ },
135
+ /**
136
+ * Get storage info for status display
137
+ * @returns storage_mode and is_degraded flag
138
+ */
139
+ async getStorageInfo() {
140
+ const { type } = await getStore();
141
+ return {
142
+ storage_mode: type,
143
+ is_degraded: type === "json" // JSON mode is considered degraded
144
+ };
145
+ }
146
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * TaskService - Phase 2: Async Task Management
3
+ *
4
+ * Manages long-running operations with progress tracking,
5
+ * meeting traceability, and MCP-compatible status reporting.
6
+ */
7
+ import { getDatabase } from "./sqlite.js";
8
+ // Generate unique task ID
9
+ function generateTaskId() {
10
+ const timestamp = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
11
+ const random = Math.random().toString(36).substring(2, 6);
12
+ return `task_${timestamp}_${random}`;
13
+ }
14
+ /**
15
+ * Initialize the tasks table (run migrations)
16
+ */
17
+ export function initTasksTable() {
18
+ const db = getDatabase();
19
+ const TASKS_SCHEMA = `
20
+ CREATE TABLE IF NOT EXISTS tasks (
21
+ id TEXT PRIMARY KEY,
22
+ status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
23
+ progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
24
+ source_meeting_id TEXT,
25
+ metadata TEXT,
26
+ result_uri TEXT,
27
+ error_message TEXT,
28
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
29
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
30
+ ttl INTEGER,
31
+ FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
35
+ CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
36
+ `;
37
+ db.exec(TASKS_SCHEMA);
38
+ // Add trigger for auto-updating updated_at (separate exec to handle IF NOT EXISTS)
39
+ try {
40
+ db.exec(`
41
+ CREATE TRIGGER IF NOT EXISTS tasks_updated_at
42
+ AFTER UPDATE ON tasks
43
+ FOR EACH ROW
44
+ BEGIN
45
+ UPDATE tasks SET updated_at = datetime('now') WHERE id = OLD.id;
46
+ END;
47
+ `);
48
+ }
49
+ catch {
50
+ // Trigger may already exist in older SQLite versions without IF NOT EXISTS support
51
+ }
52
+ console.error("[Nexus] Tasks table initialized");
53
+ }
54
+ /**
55
+ * Create a new task
56
+ */
57
+ export function createTask(input = {}) {
58
+ const db = getDatabase();
59
+ const id = input.id || generateTaskId();
60
+ const now = new Date().toISOString();
61
+ const stmt = db.prepare(`
62
+ INSERT INTO tasks (id, status, progress, source_meeting_id, metadata, created_at, updated_at, ttl)
63
+ VALUES (?, 'pending', 0.0, ?, ?, ?, ?, ?)
64
+ `);
65
+ stmt.run(id, input.source_meeting_id || null, input.metadata ? JSON.stringify(input.metadata) : null, now, now, input.ttl || null);
66
+ return getTask(id);
67
+ }
68
+ /**
69
+ * Get a task by ID
70
+ */
71
+ export function getTask(id) {
72
+ const db = getDatabase();
73
+ const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
74
+ if (!row)
75
+ return null;
76
+ return {
77
+ id: row.id,
78
+ status: row.status,
79
+ progress: row.progress,
80
+ source_meeting_id: row.source_meeting_id || null,
81
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
82
+ result_uri: row.result_uri || null,
83
+ error_message: row.error_message || null,
84
+ created_at: row.created_at,
85
+ updated_at: row.updated_at,
86
+ ttl: row.ttl || null
87
+ };
88
+ }
89
+ /**
90
+ * Update task status and progress
91
+ */
92
+ export function updateTask(id, update) {
93
+ const db = getDatabase();
94
+ const now = new Date().toISOString();
95
+ const sets = ["updated_at = ?"];
96
+ const values = [now];
97
+ if (update.status !== undefined) {
98
+ sets.push("status = ?");
99
+ values.push(update.status);
100
+ }
101
+ if (update.progress !== undefined) {
102
+ sets.push("progress = ?");
103
+ values.push(Math.max(0, Math.min(1, update.progress)));
104
+ }
105
+ if (update.result_uri !== undefined) {
106
+ sets.push("result_uri = ?");
107
+ values.push(update.result_uri);
108
+ }
109
+ if (update.error_message !== undefined) {
110
+ sets.push("error_message = ?");
111
+ values.push(update.error_message);
112
+ }
113
+ if (update.metadata !== undefined) {
114
+ sets.push("metadata = ?");
115
+ values.push(JSON.stringify(update.metadata));
116
+ }
117
+ values.push(id);
118
+ const sql = `UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`;
119
+ db.prepare(sql).run(...values);
120
+ return getTask(id);
121
+ }
122
+ /**
123
+ * List tasks with optional status filter
124
+ */
125
+ export function listTasks(status, limit = 50) {
126
+ const db = getDatabase();
127
+ let sql = "SELECT * FROM tasks";
128
+ const params = [];
129
+ if (status) {
130
+ sql += " WHERE status = ?";
131
+ params.push(status);
132
+ }
133
+ sql += " ORDER BY created_at DESC LIMIT ?";
134
+ params.push(limit);
135
+ const rows = db.prepare(sql).all(...params);
136
+ return rows.map(row => ({
137
+ id: row.id,
138
+ status: row.status,
139
+ progress: row.progress,
140
+ source_meeting_id: row.source_meeting_id || null,
141
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
142
+ result_uri: row.result_uri || null,
143
+ error_message: row.error_message || null,
144
+ created_at: row.created_at,
145
+ updated_at: row.updated_at,
146
+ ttl: row.ttl || null
147
+ }));
148
+ }
149
+ /**
150
+ * Get tasks by meeting ID
151
+ */
152
+ export function getTasksByMeeting(meetingId) {
153
+ const db = getDatabase();
154
+ const rows = db.prepare(`
155
+ SELECT * FROM tasks WHERE source_meeting_id = ? ORDER BY created_at DESC
156
+ `).all(meetingId);
157
+ return rows.map(row => ({
158
+ id: row.id,
159
+ status: row.status,
160
+ progress: row.progress,
161
+ source_meeting_id: row.source_meeting_id || null,
162
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
163
+ result_uri: row.result_uri || null,
164
+ error_message: row.error_message || null,
165
+ created_at: row.created_at,
166
+ updated_at: row.updated_at,
167
+ ttl: row.ttl || null
168
+ }));
169
+ }
170
+ /**
171
+ * Cancel a pending or running task
172
+ */
173
+ export function cancelTask(id) {
174
+ const task = getTask(id);
175
+ if (!task)
176
+ return false;
177
+ if (task.status !== "pending" && task.status !== "running")
178
+ return false;
179
+ updateTask(id, { status: "cancelled" });
180
+ return true;
181
+ }
182
+ /**
183
+ * Delete completed/failed/cancelled tasks older than specified age
184
+ */
185
+ export function cleanupTasks(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
186
+ const db = getDatabase();
187
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
188
+ const result = db.prepare(`
189
+ DELETE FROM tasks
190
+ WHERE status IN ('completed', 'failed', 'cancelled')
191
+ AND updated_at < ?
192
+ `).run(cutoff);
193
+ return result.changes;
194
+ }
195
+ /**
196
+ * Get active (pending/running) task count
197
+ */
198
+ export function getActiveTaskCount() {
199
+ const db = getDatabase();
200
+ const row = db.prepare(`
201
+ SELECT COUNT(*) as count FROM tasks WHERE status IN ('pending', 'running')
202
+ `).get();
203
+ return row.count;
204
+ }