@mmmbuto/nexuscli 0.5.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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,314 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ // Termux-only: all data in ~/.nexuscli
5
+ const dbDir = path.join(process.env.HOME, '.nexuscli');
6
+ const dbPath = path.join(dbDir, 'nexuscli.db');
7
+
8
+ // Ensure directory exists
9
+ if (!fs.existsSync(dbDir)) {
10
+ fs.mkdirSync(dbDir, { recursive: true });
11
+ console.log(`✅ Created database directory: ${dbDir}`);
12
+ }
13
+
14
+ // Termux-only: use sql.js (no native compilation needed)
15
+ const Driver = require('./drivers/sql-js');
16
+ let db = null;
17
+
18
+ console.log(`📦 Using sql.js driver (Termux-compatible)`);
19
+
20
+ // Initialize database
21
+ async function initDb(options = {}) {
22
+ const { skipMigrationCheck = false } = options;
23
+ const driver = new Driver(dbPath);
24
+ await driver.init();
25
+ db = driver;
26
+
27
+ console.log(`✅ Database ready: ${dbPath}`);
28
+
29
+ // Initialize schema
30
+ initSchema();
31
+
32
+ if (!skipMigrationCheck) {
33
+ const needsMigration = await checkMigrationNeeded();
34
+
35
+ if (needsMigration) {
36
+ console.log('[DB] Migration required - running migrations...');
37
+ const { runMigrations } = require('./migrate');
38
+ await runMigrations({ skipInit: true });
39
+ }
40
+ }
41
+
42
+ return db;
43
+ }
44
+
45
+ function initSchema() {
46
+ db.exec(`
47
+ -- Conversations table
48
+ CREATE TABLE IF NOT EXISTS conversations (
49
+ id TEXT PRIMARY KEY,
50
+ title TEXT NOT NULL,
51
+ created_at INTEGER NOT NULL,
52
+ updated_at INTEGER NOT NULL,
53
+ metadata TEXT
54
+ );
55
+
56
+ CREATE INDEX IF NOT EXISTS idx_conversations_updated_at
57
+ ON conversations(updated_at DESC);
58
+
59
+ -- Messages table
60
+ CREATE TABLE IF NOT EXISTS messages (
61
+ id TEXT PRIMARY KEY,
62
+ conversation_id TEXT NOT NULL,
63
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
64
+ content TEXT NOT NULL,
65
+ created_at INTEGER NOT NULL,
66
+ metadata TEXT,
67
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation_id
71
+ ON messages(conversation_id);
72
+
73
+ CREATE INDEX IF NOT EXISTS idx_messages_created_at
74
+ ON messages(created_at ASC);
75
+
76
+ -- Jobs table
77
+ CREATE TABLE IF NOT EXISTS jobs (
78
+ id TEXT PRIMARY KEY,
79
+ conversation_id TEXT,
80
+ message_id TEXT,
81
+ node_id TEXT NOT NULL,
82
+ tool TEXT NOT NULL,
83
+ command TEXT NOT NULL,
84
+ status TEXT NOT NULL CHECK(status IN ('queued', 'executing', 'completed', 'failed', 'cancelled')),
85
+ exit_code INTEGER,
86
+ stdout TEXT,
87
+ stderr TEXT,
88
+ duration INTEGER,
89
+ created_at INTEGER NOT NULL,
90
+ started_at INTEGER,
91
+ completed_at INTEGER,
92
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL,
93
+ FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_jobs_conversation_id
97
+ ON jobs(conversation_id);
98
+
99
+ CREATE INDEX IF NOT EXISTS idx_jobs_status
100
+ ON jobs(status);
101
+
102
+ CREATE INDEX IF NOT EXISTS idx_jobs_created_at
103
+ ON jobs(created_at DESC);
104
+
105
+ -- Users table
106
+ CREATE TABLE IF NOT EXISTS users (
107
+ id TEXT PRIMARY KEY,
108
+ username TEXT UNIQUE NOT NULL,
109
+ password_hash TEXT NOT NULL,
110
+ role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin', 'user')),
111
+ is_locked INTEGER NOT NULL DEFAULT 0,
112
+ failed_attempts INTEGER NOT NULL DEFAULT 0,
113
+ last_failed_attempt INTEGER,
114
+ locked_until INTEGER,
115
+ created_at INTEGER NOT NULL,
116
+ last_login INTEGER
117
+ );
118
+
119
+ CREATE INDEX IF NOT EXISTS idx_users_username
120
+ ON users(username);
121
+
122
+ -- Login attempts table
123
+ CREATE TABLE IF NOT EXISTS login_attempts (
124
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
125
+ ip_address TEXT NOT NULL,
126
+ username TEXT,
127
+ success INTEGER NOT NULL DEFAULT 0,
128
+ timestamp INTEGER NOT NULL
129
+ );
130
+
131
+ CREATE INDEX IF NOT EXISTS idx_login_attempts_ip
132
+ ON login_attempts(ip_address, timestamp DESC);
133
+
134
+ CREATE INDEX IF NOT EXISTS idx_login_attempts_timestamp
135
+ ON login_attempts(timestamp DESC);
136
+
137
+ -- Nodes table
138
+ CREATE TABLE IF NOT EXISTS nodes (
139
+ id TEXT PRIMARY KEY,
140
+ hostname TEXT NOT NULL,
141
+ ip_address TEXT,
142
+ status TEXT NOT NULL CHECK(status IN ('online', 'offline', 'error')),
143
+ capabilities TEXT,
144
+ last_heartbeat INTEGER,
145
+ created_at INTEGER NOT NULL
146
+ );
147
+
148
+ CREATE INDEX IF NOT EXISTS idx_nodes_status
149
+ ON nodes(status);
150
+
151
+ -- API Keys table (encrypted storage for provider keys)
152
+ CREATE TABLE IF NOT EXISTS api_keys (
153
+ provider TEXT PRIMARY KEY,
154
+ api_key TEXT NOT NULL,
155
+ created_at INTEGER NOT NULL,
156
+ updated_at INTEGER NOT NULL
157
+ );
158
+ `);
159
+
160
+ console.log('✅ Database schema initialized');
161
+ }
162
+
163
+ /**
164
+ * Get API key for a provider
165
+ * @param {string} provider - Provider name (e.g., 'deepseek', 'openai')
166
+ * @returns {string|null} API key or null if not found
167
+ */
168
+ function getApiKey(provider) {
169
+ if (!db) return null;
170
+ try {
171
+ const stmt = db.prepare('SELECT api_key FROM api_keys WHERE provider = ?');
172
+ const row = stmt.get(provider.toLowerCase());
173
+ return row?.api_key || null;
174
+ } catch (err) {
175
+ console.error(`[DB] Error getting API key for ${provider}:`, err.message);
176
+ return null;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Set API key for a provider
182
+ * @param {string} provider - Provider name
183
+ * @param {string} apiKey - API key value
184
+ * @returns {boolean} Success
185
+ */
186
+ function setApiKey(provider, apiKey) {
187
+ if (!db) return false;
188
+ try {
189
+ const now = Date.now();
190
+ const stmt = db.prepare(`
191
+ INSERT INTO api_keys (provider, api_key, created_at, updated_at)
192
+ VALUES (?, ?, ?, ?)
193
+ ON CONFLICT(provider) DO UPDATE SET api_key = ?, updated_at = ?
194
+ `);
195
+ stmt.run(provider.toLowerCase(), apiKey, now, now, apiKey, now);
196
+ db.save();
197
+ return true;
198
+ } catch (err) {
199
+ console.error(`[DB] Error setting API key for ${provider}:`, err.message);
200
+ return false;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Delete API key for a provider
206
+ * @param {string} provider - Provider name
207
+ * @returns {boolean} Success
208
+ */
209
+ function deleteApiKey(provider) {
210
+ if (!db) return false;
211
+ try {
212
+ const stmt = db.prepare('DELETE FROM api_keys WHERE provider = ?');
213
+ stmt.run(provider.toLowerCase());
214
+ db.save();
215
+ return true;
216
+ } catch (err) {
217
+ console.error(`[DB] Error deleting API key for ${provider}:`, err.message);
218
+ return false;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * List all configured API key providers (without exposing keys)
224
+ * @returns {Array} List of provider names
225
+ */
226
+ function listApiKeyProviders() {
227
+ if (!db) return [];
228
+ try {
229
+ const stmt = db.prepare('SELECT provider, created_at, updated_at FROM api_keys');
230
+ return stmt.all();
231
+ } catch (err) {
232
+ console.error('[DB] Error listing API key providers:', err.message);
233
+ return [];
234
+ }
235
+ }
236
+
237
+ async function checkMigrationNeeded() {
238
+ const currentDb = getDb();
239
+
240
+ if (!currentDb) {
241
+ console.error('[DB] Cannot check migration status - DB not initialized');
242
+ return false;
243
+ }
244
+
245
+ try {
246
+ const hasSessionsStmt = currentDb.prepare(`
247
+ SELECT name FROM sqlite_master
248
+ WHERE type='table' AND name='sessions'
249
+ `);
250
+ const hasSessions = hasSessionsStmt.get();
251
+
252
+ if (!hasSessions) {
253
+ console.log('[DB] sessions table not found - migration needed');
254
+ return true;
255
+ }
256
+
257
+ const conversationsCount = currentDb.prepare('SELECT COUNT(*) as count FROM conversations').get();
258
+ const sessionsCount = currentDb.prepare('SELECT COUNT(*) as count FROM sessions').get();
259
+
260
+ if (conversationsCount.count > sessionsCount.count) {
261
+ console.log('[DB] Unmigrated conversations found - migration needed');
262
+ return true;
263
+ }
264
+
265
+ console.log('[DB] No migration needed');
266
+ return false;
267
+ } catch (error) {
268
+ console.error('[DB] Error checking migration status:', error);
269
+ return false;
270
+ }
271
+ }
272
+
273
+ function getDb() {
274
+ return db;
275
+ }
276
+
277
+ function prepare(sql) {
278
+ return db.prepare(sql);
279
+ }
280
+
281
+ function saveDb() {
282
+ if (db) db.save();
283
+ }
284
+
285
+ // Graceful shutdown
286
+ process.on('exit', () => {
287
+ if (db) {
288
+ db.close();
289
+ console.log('✅ Database connection closed');
290
+ }
291
+ });
292
+
293
+ process.on('SIGINT', () => {
294
+ if (db) {
295
+ db.close();
296
+ console.log('✅ Database connection closed (SIGINT)');
297
+ }
298
+ process.exit(0);
299
+ });
300
+
301
+ process.on('SIGTERM', () => {
302
+ if (db) db.close();
303
+ });
304
+
305
+ module.exports = {
306
+ initDb,
307
+ getDb,
308
+ prepare,
309
+ saveDb,
310
+ getApiKey,
311
+ setApiKey,
312
+ deleteApiKey,
313
+ listApiKeyProviders
314
+ };
@@ -0,0 +1,38 @@
1
+ const Database = require('better-sqlite3');
2
+
3
+ class BetterSqlite3Driver {
4
+ constructor(dbPath) {
5
+ this.dbPath = dbPath;
6
+ this.db = null;
7
+ }
8
+
9
+ async init() {
10
+ this.db = new Database(this.dbPath, {
11
+ verbose: process.env.NODE_ENV === 'development' ? console.log : null
12
+ });
13
+
14
+ // Enable WAL mode for better concurrency
15
+ this.db.pragma('journal_mode = WAL');
16
+ this.db.pragma('foreign_keys = ON');
17
+ }
18
+
19
+ save() {
20
+ // better-sqlite3 auto-saves, no-op
21
+ }
22
+
23
+ exec(sql) {
24
+ return this.db.exec(sql);
25
+ }
26
+
27
+ prepare(sql) {
28
+ return this.db.prepare(sql);
29
+ }
30
+
31
+ close() {
32
+ if (this.db) {
33
+ this.db.close();
34
+ }
35
+ }
36
+ }
37
+
38
+ module.exports = BetterSqlite3Driver;
@@ -0,0 +1,75 @@
1
+ const initSqlJs = require('sql.js');
2
+ const fs = require('fs');
3
+
4
+ class SqlJsDriver {
5
+ constructor(dbPath) {
6
+ this.dbPath = dbPath;
7
+ this.db = null;
8
+ this.saveInterval = null;
9
+ }
10
+
11
+ async init() {
12
+ const SQL = await initSqlJs();
13
+
14
+ if (fs.existsSync(this.dbPath)) {
15
+ const buffer = fs.readFileSync(this.dbPath);
16
+ this.db = new SQL.Database(buffer);
17
+ } else {
18
+ this.db = new SQL.Database();
19
+ }
20
+
21
+ // Auto-save every 5 seconds
22
+ this.saveInterval = setInterval(() => this.save(), 5000);
23
+ }
24
+
25
+ save() {
26
+ if (!this.db) return;
27
+ const data = this.db.export();
28
+ const buffer = Buffer.from(data);
29
+ fs.writeFileSync(this.dbPath, buffer);
30
+ }
31
+
32
+ exec(sql) {
33
+ return this.db.run(sql);
34
+ }
35
+
36
+ prepare(sql) {
37
+ const stmt = this.db.prepare(sql);
38
+ return {
39
+ run: (...params) => {
40
+ stmt.bind(params);
41
+ stmt.step();
42
+ stmt.reset();
43
+ this.save();
44
+ },
45
+ get: (...params) => {
46
+ stmt.bind(params);
47
+ const result = stmt.step() ? stmt.getAsObject() : null;
48
+ stmt.reset();
49
+ return result;
50
+ },
51
+ all: (...params) => {
52
+ stmt.bind(params);
53
+ const results = [];
54
+ while (stmt.step()) {
55
+ results.push(stmt.getAsObject());
56
+ }
57
+ stmt.reset();
58
+ return results;
59
+ }
60
+ };
61
+ }
62
+
63
+ close() {
64
+ if (this.db) {
65
+ this.save();
66
+ this.db.close();
67
+ }
68
+ if (this.saveInterval) {
69
+ clearInterval(this.saveInterval);
70
+ this.saveInterval = null;
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = SqlJsDriver;
@@ -0,0 +1,174 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { initDb, getDb, saveDb } = require('./adapter');
4
+
5
+ const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
6
+
7
+ /**
8
+ * Get list of migration files in order
9
+ */
10
+ function getMigrationFiles() {
11
+ const files = fs.readdirSync(MIGRATIONS_DIR)
12
+ .filter(f => f.endsWith('.sql'))
13
+ .sort(); // Alphabetical order ensures numeric order (001_, 002_, etc.)
14
+ return files;
15
+ }
16
+
17
+ /**
18
+ * Normalize and split SQL into executable statements.
19
+ * Keeps CREATE/INSERT blocks while stripping comment-only lines.
20
+ */
21
+ function parseSqlStatements(sql) {
22
+ return sql
23
+ .replace(/\r\n/g, '\n')
24
+ .split(';')
25
+ .map(stmt => stmt.trim())
26
+ .map(stmt => {
27
+ const lines = stmt
28
+ .split('\n')
29
+ .map(line => line.trim())
30
+ .filter(line => line.length > 0 && !line.startsWith('--'));
31
+ return lines.join('\n').trim();
32
+ })
33
+ .filter(stmt => stmt.length > 0);
34
+ }
35
+
36
+ /**
37
+ * Get applied migrations from DB
38
+ */
39
+ function getAppliedMigrations(db) {
40
+ try {
41
+ // Ensure migrations table exists
42
+ db.exec(`
43
+ CREATE TABLE IF NOT EXISTS _migrations (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ name TEXT UNIQUE NOT NULL,
46
+ applied_at INTEGER NOT NULL
47
+ )
48
+ `);
49
+
50
+ const stmt = db.prepare('SELECT name FROM _migrations ORDER BY id');
51
+ return stmt.all().map(row => row.name);
52
+ } catch (error) {
53
+ console.error('[Migration] Error getting applied migrations:', error.message);
54
+ return [];
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Mark migration as applied
60
+ */
61
+ function markMigrationApplied(db, name) {
62
+ const stmt = db.prepare('INSERT INTO _migrations (name, applied_at) VALUES (?, ?)');
63
+ stmt.run(name, Date.now());
64
+ }
65
+
66
+ /**
67
+ * Run a single migration file
68
+ */
69
+ function runMigrationFile(db, filename) {
70
+ const filePath = path.join(MIGRATIONS_DIR, filename);
71
+ const migrationSql = fs.readFileSync(filePath, 'utf8');
72
+ const statements = parseSqlStatements(migrationSql);
73
+
74
+ console.log(`[Migration] Running ${filename} (${statements.length} statements)`);
75
+
76
+ for (let i = 0; i < statements.length; i++) {
77
+ const stmt = statements[i];
78
+
79
+ if (/^SELECT/i.test(stmt)) {
80
+ // Skip verification SELECT statements
81
+ continue;
82
+ }
83
+
84
+ try {
85
+ db.exec(stmt);
86
+ } catch (error) {
87
+ // Handle "duplicate column" errors gracefully
88
+ if (error.message.includes('duplicate column')) {
89
+ console.log(`[Migration] Column already exists (skipping): ${error.message}`);
90
+ continue;
91
+ }
92
+ console.error(`[Migration] Failed at statement ${i + 1}: ${error.message}`);
93
+ console.error('Statement preview:', stmt.substring(0, 200));
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ markMigrationApplied(db, filename);
99
+ console.log(`[Migration] ✅ ${filename} applied`);
100
+ }
101
+
102
+ /**
103
+ * Run database migrations.
104
+ * @param {Object} options
105
+ * @param {boolean} options.skipInit - skip initDb (adapter already initialized)
106
+ */
107
+ async function runMigrations(options = {}) {
108
+ const { skipInit = false } = options;
109
+
110
+ console.log('[Migration] Starting database migration...');
111
+
112
+ if (!skipInit && !getDb()) {
113
+ await initDb({ skipMigrationCheck: true });
114
+ }
115
+
116
+ const db = getDb();
117
+
118
+ if (!db) {
119
+ throw new Error('[Migration] Database instance not initialized');
120
+ }
121
+
122
+ const migrationFiles = getMigrationFiles();
123
+ const appliedMigrations = getAppliedMigrations(db);
124
+
125
+ console.log(`[Migration] Found ${migrationFiles.length} migration files`);
126
+ console.log(`[Migration] Already applied: ${appliedMigrations.length}`);
127
+
128
+ const pendingMigrations = migrationFiles.filter(f => !appliedMigrations.includes(f));
129
+
130
+ if (pendingMigrations.length === 0) {
131
+ console.log('[Migration] No pending migrations');
132
+ return;
133
+ }
134
+
135
+ console.log(`[Migration] Pending: ${pendingMigrations.join(', ')}`);
136
+
137
+ for (const file of pendingMigrations) {
138
+ runMigrationFile(db, file);
139
+ }
140
+
141
+ saveDb();
142
+
143
+ console.log('[Migration] All migrations completed');
144
+
145
+ // Verification counts
146
+ try {
147
+ const verifyStmt = db.prepare(`
148
+ SELECT 'conversations' as table_name, COUNT(*) as count FROM conversations
149
+ UNION ALL
150
+ SELECT 'sessions' as table_name, COUNT(*) as count FROM sessions
151
+ `);
152
+
153
+ const counts = verifyStmt.all();
154
+ console.log('[Migration] Verification:');
155
+ counts.forEach(row => console.log(` ${row.table_name}: ${row.count} rows`));
156
+ } catch (error) {
157
+ console.error('[Migration] Verification failed:', error.message);
158
+ }
159
+ }
160
+
161
+ // Run if called directly
162
+ if (require.main === module) {
163
+ runMigrations()
164
+ .then(() => {
165
+ console.log('[Migration] Complete');
166
+ process.exit(0);
167
+ })
168
+ .catch(error => {
169
+ console.error('[Migration] Failed:', error);
170
+ process.exit(1);
171
+ });
172
+ }
173
+
174
+ module.exports = { runMigrations };
@@ -0,0 +1,96 @@
1
+ -- ============================================================
2
+ -- SESSIONS: Lightweight index of all CLI sessions
3
+ -- ============================================================
4
+ CREATE TABLE IF NOT EXISTS sessions (
5
+ id TEXT PRIMARY KEY,
6
+ engine TEXT NOT NULL DEFAULT 'claude-code',
7
+ workspace_path TEXT NOT NULL,
8
+ session_path TEXT,
9
+ title TEXT NOT NULL,
10
+ last_used_at INTEGER NOT NULL,
11
+ created_at INTEGER NOT NULL,
12
+ pinned INTEGER DEFAULT 0,
13
+ importance INTEGER DEFAULT 0,
14
+ message_count INTEGER DEFAULT 0,
15
+ metadata TEXT
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_path);
19
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_used ON sessions(last_used_at DESC);
20
+ CREATE INDEX IF NOT EXISTS idx_sessions_pinned ON sessions(pinned, last_used_at DESC);
21
+
22
+ -- ============================================================
23
+ -- SESSION_SUMMARIES: Contextual memory for each session
24
+ -- ============================================================
25
+ CREATE TABLE IF NOT EXISTS session_summaries (
26
+ session_id TEXT PRIMARY KEY,
27
+ summary_short TEXT NOT NULL,
28
+ summary_long TEXT,
29
+ key_decisions TEXT,
30
+ tools_used TEXT,
31
+ files_modified TEXT,
32
+ updated_at INTEGER NOT NULL,
33
+ version INTEGER DEFAULT 1,
34
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS idx_summaries_updated ON session_summaries(updated_at DESC);
38
+
39
+ -- ============================================================
40
+ -- WORKSPACE_MEMORY: Project-level context (optional)
41
+ -- ============================================================
42
+ CREATE TABLE IF NOT EXISTS workspace_memory (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ workspace_path TEXT UNIQUE NOT NULL,
45
+ summary TEXT,
46
+ tech_stack TEXT,
47
+ architecture_notes TEXT,
48
+ important_files TEXT,
49
+ session_count INTEGER DEFAULT 0,
50
+ last_activity INTEGER,
51
+ updated_at INTEGER NOT NULL
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_workspace_memory_activity ON workspace_memory(last_activity DESC);
55
+
56
+ -- ============================================================
57
+ -- MIGRATION: Map old conversations to new sessions
58
+ -- ============================================================
59
+ INSERT INTO sessions (
60
+ id,
61
+ engine,
62
+ workspace_path,
63
+ session_path,
64
+ title,
65
+ last_used_at,
66
+ created_at,
67
+ pinned,
68
+ importance,
69
+ message_count,
70
+ metadata
71
+ )
72
+ SELECT
73
+ c.id,
74
+ 'claude-code' as engine,
75
+ COALESCE(
76
+ json_extract(c.metadata, '$.workspace'),
77
+ '/data/data/com.termux/files/home/Dev/NexusCLI/backend'
78
+ ) as workspace_path,
79
+ NULL as session_path,
80
+ c.title,
81
+ c.updated_at as last_used_at,
82
+ c.created_at,
83
+ COALESCE(json_extract(c.metadata, '$.bookmarked'), 0) as pinned,
84
+ 0 as importance,
85
+ (SELECT COUNT(*) FROM messages WHERE conversation_id = c.id) as message_count,
86
+ c.metadata
87
+ FROM conversations c
88
+ WHERE NOT EXISTS (SELECT 1 FROM sessions WHERE id = c.id);
89
+
90
+ -- ============================================================
91
+ -- VERIFICATION: Count check
92
+ -- ============================================================
93
+ -- This will be run separately for verification
94
+ -- SELECT 'OLD' as source, COUNT(*) as count FROM conversations
95
+ -- UNION ALL
96
+ -- SELECT 'NEW' as source, COUNT(*) as count FROM sessions;
@@ -0,0 +1,19 @@
1
+ -- ============================================================
2
+ -- MIGRATION 002: Add conversation_id mapping to sessions
3
+ -- Enables sync pattern: conversationId -> sessionId per engine
4
+ -- ============================================================
5
+
6
+ -- Add conversation_id column for frontend mapping
7
+ ALTER TABLE sessions ADD COLUMN conversation_id TEXT;
8
+
9
+ -- Index for fast lookup by conversation
10
+ CREATE INDEX IF NOT EXISTS idx_sessions_conversation ON sessions(conversation_id);
11
+
12
+ -- Composite index for engine + conversation lookup
13
+ CREATE INDEX IF NOT EXISTS idx_sessions_engine_conversation ON sessions(engine, conversation_id);
14
+
15
+ -- ============================================================
16
+ -- BACKFILL: Set conversation_id = id for existing sessions
17
+ -- (maintains backwards compatibility)
18
+ -- ============================================================
19
+ UPDATE sessions SET conversation_id = id WHERE conversation_id IS NULL;