@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- 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;
|