@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.
- package/README.md +34 -16
- package/README_zh.md +179 -0
- package/build/index.js +38 -22
- package/build/resources/index.js +60 -11
- package/build/storage/index.js +9 -35
- package/build/storage/meetings.js +275 -0
- package/build/storage/sqlite-meeting.js +281 -0
- package/build/storage/sqlite.js +132 -0
- package/build/storage/store.js +146 -0
- package/build/storage/tasks.js +204 -0
- package/build/tools/handlers.js +459 -152
- package/build/tools/index.js +12 -1
- package/build/tools/schemas.js +275 -0
- package/build/utils/async-mutex.js +36 -0
- package/build/utils/auth.js +11 -0
- package/build/utils/error.js +15 -0
- package/docs/ASSISTANT_GUIDE.md +56 -0
- package/docs/CHANGELOG.md +170 -0
- package/docs/CHANGELOG_zh.md +170 -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 +14 -9
- package/build/tools/definitions.js +0 -228
|
@@ -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
|
+
}
|