n2n-nexus 0.4.2

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,141 @@
1
+ import Database from "better-sqlite3";
2
+ import { createRequire } from "module";
3
+ import { NexusPaths } from "./paths.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
+ initiator TEXT,
15
+ participants TEXT DEFAULT '[]',
16
+ created_at TEXT NOT NULL,
17
+ closed_at TEXT,
18
+ summary TEXT
19
+ );
20
+
21
+ -- Messages table
22
+ CREATE TABLE IF NOT EXISTS messages (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ meeting_id TEXT NOT NULL,
25
+ sender TEXT NOT NULL,
26
+ text TEXT NOT NULL,
27
+ category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
28
+ timestamp TEXT NOT NULL,
29
+ FOREIGN KEY (meeting_id) REFERENCES meetings(id)
30
+ );
31
+
32
+ -- Decisions table (extracted from DECISION messages)
33
+ CREATE TABLE IF NOT EXISTS decisions (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ meeting_id TEXT NOT NULL,
36
+ content TEXT NOT NULL,
37
+ timestamp TEXT NOT NULL,
38
+ FOREIGN KEY (meeting_id) REFERENCES meetings(id)
39
+ );
40
+
41
+ -- Meeting state (Key-Value store)
42
+ CREATE TABLE IF NOT EXISTS meeting_state (
43
+ key TEXT PRIMARY KEY,
44
+ value TEXT NOT NULL
45
+ );
46
+
47
+ -- Indexes for performance
48
+ CREATE INDEX IF NOT EXISTS idx_messages_meeting ON messages(meeting_id);
49
+ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
50
+ CREATE INDEX IF NOT EXISTS idx_decisions_meeting ON decisions(meeting_id);
51
+ CREATE INDEX IF NOT EXISTS idx_meetings_status ON meetings(status);
52
+
53
+ -- Read cursors table (tracks each IDE's last read message per context)
54
+ CREATE TABLE IF NOT EXISTS read_cursors (
55
+ instance_id TEXT NOT NULL,
56
+ context_type TEXT NOT NULL CHECK(context_type IN ('meeting', 'global')),
57
+ context_id TEXT,
58
+ last_read_id INTEGER DEFAULT 0,
59
+ updated_at TEXT,
60
+ PRIMARY KEY (instance_id, context_type, context_id)
61
+ );
62
+ `;
63
+ /**
64
+ * Get the database file path
65
+ */
66
+ export function getDbPath() {
67
+ return NexusPaths.dbFile;
68
+ }
69
+ /**
70
+ * Initialize the SQLite database with WAL mode
71
+ */
72
+ export function initDatabase() {
73
+ if (db)
74
+ return db;
75
+ const dbPath = getDbPath();
76
+ db = new Database(dbPath);
77
+ // Enable WAL mode for better concurrent access
78
+ db.pragma("journal_mode = WAL");
79
+ // Initialize schema
80
+ db.exec(SCHEMA);
81
+ // Migration: Add initiator column if it doesn't exist (Upgrade from v0.1.7)
82
+ try {
83
+ const columns = db.prepare("PRAGMA table_info(meetings)").all();
84
+ const hasInitiator = columns.some(c => c.name === "initiator");
85
+ if (!hasInitiator) {
86
+ console.error("[Nexus] Migrating database: Adding 'initiator' column to 'meetings' table.");
87
+ db.exec("ALTER TABLE meetings ADD COLUMN initiator TEXT");
88
+ }
89
+ }
90
+ catch (e) {
91
+ console.error("[Nexus] Migration check failed:", e);
92
+ }
93
+ // Initialize default state if not exists
94
+ const stmt = db.prepare("INSERT OR IGNORE INTO meeting_state (key, value) VALUES (?, ?)");
95
+ stmt.run("active_meetings", "[]");
96
+ stmt.run("default_meeting", "");
97
+ console.error("[Nexus] SQLite database initialized at:", dbPath);
98
+ return db;
99
+ }
100
+ /**
101
+ * Get the database instance
102
+ */
103
+ export function getDatabase() {
104
+ if (!db) {
105
+ return initDatabase();
106
+ }
107
+ return db;
108
+ }
109
+ /**
110
+ * Close the database connection
111
+ */
112
+ export function closeDatabase() {
113
+ if (db) {
114
+ db.close();
115
+ db = null;
116
+ console.error("[Nexus] SQLite database closed");
117
+ }
118
+ }
119
+ export function isSqliteAvailable() {
120
+ const require = createRequire(import.meta.url);
121
+ try {
122
+ // Try to load better-sqlite3
123
+ require("better-sqlite3");
124
+ return true;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ // Cleanup on process exit
131
+ process.on("exit", () => {
132
+ closeDatabase();
133
+ });
134
+ process.on("SIGINT", () => {
135
+ closeDatabase();
136
+ process.exit(0);
137
+ });
138
+ process.on("SIGTERM", () => {
139
+ closeDatabase();
140
+ process.exit(0);
141
+ });
@@ -0,0 +1,153 @@
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 (incremental for SQLite with instanceId)
105
+ */
106
+ async getRecentMessages(count, meetingId, instanceId) {
107
+ const { type, store } = await getStore();
108
+ if (type === "sqlite") {
109
+ // SQLite supports incremental reads with cursor tracking
110
+ return store.getRecentMessages(count || 10, meetingId, instanceId);
111
+ }
112
+ else {
113
+ // JSON fallback - no incremental support
114
+ return store.getRecentMessages(count || 10, meetingId);
115
+ }
116
+ },
117
+ /**
118
+ * Get the current backend type
119
+ */
120
+ async getBackendType() {
121
+ const { type } = await getStore();
122
+ return type;
123
+ },
124
+ /**
125
+ * Get meeting state (SQLite only, returns empty for JSON)
126
+ */
127
+ async getState() {
128
+ const { type, store } = await getStore();
129
+ if (type === "sqlite") {
130
+ const sqliteStore = store;
131
+ const activeMeetings = JSON.parse(sqliteStore.getState("active_meetings") || "[]");
132
+ const defaultMeetingId = sqliteStore.getState("default_meeting") || null;
133
+ return { activeMeetings, defaultMeetingId };
134
+ }
135
+ else {
136
+ // JSON backend
137
+ const jsonStore = store;
138
+ const state = await jsonStore.getState();
139
+ return state;
140
+ }
141
+ },
142
+ /**
143
+ * Get storage info for status display
144
+ * @returns storage_mode and is_degraded flag
145
+ */
146
+ async getStorageInfo() {
147
+ const { type } = await getStore();
148
+ return {
149
+ storage_mode: type,
150
+ is_degraded: type === "json" // JSON mode is considered degraded
151
+ };
152
+ }
153
+ };
@@ -0,0 +1,212 @@
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
+ import { TASK_CLEANUP_MAX_AGE_MS } from "../constants.js";
9
+ // Generate unique task ID
10
+ function generateTaskId() {
11
+ const timestamp = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
12
+ const random = Math.random().toString(36).substring(2, 6);
13
+ return `task_${timestamp}_${random}`;
14
+ }
15
+ let initialized = false;
16
+ /**
17
+ * Initialize the tasks table (run migrations)
18
+ */
19
+ export function initTasksTable() {
20
+ if (initialized)
21
+ return;
22
+ const db = getDatabase();
23
+ const TASKS_SCHEMA = `
24
+ CREATE TABLE IF NOT EXISTS tasks (
25
+ id TEXT PRIMARY KEY,
26
+ status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
27
+ progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
28
+ source_meeting_id TEXT,
29
+ metadata TEXT,
30
+ result_uri TEXT,
31
+ error_message TEXT,
32
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
33
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
34
+ ttl INTEGER,
35
+ FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
39
+ CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
40
+ `;
41
+ db.exec(TASKS_SCHEMA);
42
+ // Add trigger for auto-updating updated_at (separate exec to handle IF NOT EXISTS)
43
+ try {
44
+ db.exec(`
45
+ CREATE TRIGGER IF NOT EXISTS tasks_updated_at
46
+ AFTER UPDATE ON tasks
47
+ FOR EACH ROW
48
+ BEGIN
49
+ UPDATE tasks SET updated_at = datetime('now') WHERE id = OLD.id;
50
+ END;
51
+ `);
52
+ }
53
+ catch {
54
+ // Trigger may already exist in older SQLite versions without IF NOT EXISTS support
55
+ }
56
+ console.error("[Nexus] Tasks table initialized");
57
+ initialized = true;
58
+ }
59
+ export function resetTasksInit() {
60
+ initialized = false;
61
+ }
62
+ /**
63
+ * Create a new task
64
+ */
65
+ export function createTask(input = {}) {
66
+ const db = getDatabase();
67
+ const id = input.id || generateTaskId();
68
+ const now = new Date().toISOString();
69
+ const stmt = db.prepare(`
70
+ INSERT INTO tasks (id, status, progress, source_meeting_id, metadata, created_at, updated_at, ttl)
71
+ VALUES (?, 'pending', 0.0, ?, ?, ?, ?, ?)
72
+ `);
73
+ stmt.run(id, input.source_meeting_id || null, input.metadata ? JSON.stringify(input.metadata) : null, now, now, input.ttl || null);
74
+ return getTask(id);
75
+ }
76
+ /**
77
+ * Get a task by ID
78
+ */
79
+ export function getTask(id) {
80
+ const db = getDatabase();
81
+ const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
82
+ if (!row)
83
+ return null;
84
+ return {
85
+ id: row.id,
86
+ status: row.status,
87
+ progress: row.progress,
88
+ source_meeting_id: row.source_meeting_id || null,
89
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
90
+ result_uri: row.result_uri || null,
91
+ error_message: row.error_message || null,
92
+ created_at: row.created_at,
93
+ updated_at: row.updated_at,
94
+ ttl: row.ttl || null
95
+ };
96
+ }
97
+ /**
98
+ * Update task status and progress
99
+ */
100
+ export function updateTask(id, update) {
101
+ const db = getDatabase();
102
+ const now = new Date().toISOString();
103
+ const sets = ["updated_at = ?"];
104
+ const values = [now];
105
+ if (update.status !== undefined) {
106
+ sets.push("status = ?");
107
+ values.push(update.status);
108
+ }
109
+ if (update.progress !== undefined) {
110
+ sets.push("progress = ?");
111
+ values.push(Math.max(0, Math.min(1, update.progress)));
112
+ }
113
+ if (update.result_uri !== undefined) {
114
+ sets.push("result_uri = ?");
115
+ values.push(update.result_uri);
116
+ }
117
+ if (update.error_message !== undefined) {
118
+ sets.push("error_message = ?");
119
+ values.push(update.error_message);
120
+ }
121
+ if (update.metadata !== undefined) {
122
+ sets.push("metadata = ?");
123
+ values.push(JSON.stringify(update.metadata));
124
+ }
125
+ values.push(id);
126
+ const sql = `UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`;
127
+ db.prepare(sql).run(...values);
128
+ return getTask(id);
129
+ }
130
+ /**
131
+ * List tasks with optional status filter
132
+ */
133
+ export function listTasks(status, limit = 50) {
134
+ const db = getDatabase();
135
+ let sql = "SELECT * FROM tasks";
136
+ const params = [];
137
+ if (status) {
138
+ sql += " WHERE status = ?";
139
+ params.push(status);
140
+ }
141
+ sql += " ORDER BY created_at DESC LIMIT ?";
142
+ params.push(limit);
143
+ const rows = db.prepare(sql).all(...params);
144
+ return rows.map(row => ({
145
+ id: row.id,
146
+ status: row.status,
147
+ progress: row.progress,
148
+ source_meeting_id: row.source_meeting_id || null,
149
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
150
+ result_uri: row.result_uri || null,
151
+ error_message: row.error_message || null,
152
+ created_at: row.created_at,
153
+ updated_at: row.updated_at,
154
+ ttl: row.ttl || null
155
+ }));
156
+ }
157
+ /**
158
+ * Get tasks by meeting ID
159
+ */
160
+ export function getTasksByMeeting(meetingId) {
161
+ const db = getDatabase();
162
+ const rows = db.prepare(`
163
+ SELECT * FROM tasks WHERE source_meeting_id = ? ORDER BY created_at DESC
164
+ `).all(meetingId);
165
+ return rows.map(row => ({
166
+ id: row.id,
167
+ status: row.status,
168
+ progress: row.progress,
169
+ source_meeting_id: row.source_meeting_id || null,
170
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
171
+ result_uri: row.result_uri || null,
172
+ error_message: row.error_message || null,
173
+ created_at: row.created_at,
174
+ updated_at: row.updated_at,
175
+ ttl: row.ttl || null
176
+ }));
177
+ }
178
+ /**
179
+ * Cancel a pending or running task
180
+ */
181
+ export function cancelTask(id) {
182
+ const task = getTask(id);
183
+ if (!task)
184
+ return false;
185
+ if (task.status !== "pending" && task.status !== "running")
186
+ return false;
187
+ updateTask(id, { status: "cancelled" });
188
+ return true;
189
+ }
190
+ /**
191
+ * Delete completed/failed/cancelled tasks older than specified age
192
+ */
193
+ export function cleanupTasks(maxAgeMs = TASK_CLEANUP_MAX_AGE_MS) {
194
+ const db = getDatabase();
195
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
196
+ const result = db.prepare(`
197
+ DELETE FROM tasks
198
+ WHERE status IN ('completed', 'failed', 'cancelled')
199
+ AND updated_at < ?
200
+ `).run(cutoff);
201
+ return result.changes;
202
+ }
203
+ /**
204
+ * Get active (pending/running) task count
205
+ */
206
+ export function getActiveTaskCount() {
207
+ const db = getDatabase();
208
+ const row = db.prepare(`
209
+ SELECT COUNT(*) as count FROM tasks WHERE status IN ('pending', 'running')
210
+ `).get();
211
+ return row.count;
212
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Simple mutex lock for preventing concurrent file writes.
3
+ * Ensures that only one write operation can happen at a time.
4
+ */
5
+ export class AsyncMutex {
6
+ locked = false;
7
+ queue = [];
8
+ async acquire() {
9
+ if (!this.locked) {
10
+ this.locked = true;
11
+ return;
12
+ }
13
+ return new Promise((resolve) => {
14
+ this.queue.push(resolve);
15
+ });
16
+ }
17
+ release() {
18
+ if (this.queue.length > 0) {
19
+ const next = this.queue.shift();
20
+ if (next)
21
+ next();
22
+ }
23
+ else {
24
+ this.locked = false;
25
+ }
26
+ }
27
+ async withLock(fn) {
28
+ await this.acquire();
29
+ try {
30
+ return await fn();
31
+ }
32
+ finally {
33
+ this.release();
34
+ }
35
+ }
36
+ }