@mmmbuto/nexuscli 0.8.9 โ 0.9.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 +9 -8
- package/lib/cli/status.js +1 -19
- package/lib/config/models.js +7 -0
- package/lib/server/.env.example +1 -1
- package/lib/server/routes/models.js +1 -1
- package/lib/server/server.js +0 -2
- package/package.json +1 -2
- package/lib/server/db.js.old +0 -225
- package/lib/server/docs/API_WRAPPER_CONTRACT.md +0 -682
- package/lib/server/docs/ARCHITECTURE.md +0 -441
- package/lib/server/docs/DATABASE_SCHEMA.md +0 -783
- package/lib/server/docs/DESIGN_PRINCIPLES.md +0 -598
- package/lib/server/docs/NEXUSCHAT_ANALYSIS.md +0 -488
- package/lib/server/docs/PIPELINE_INTEGRATION.md +0 -636
- package/lib/server/docs/README.md +0 -272
- package/lib/server/docs/UI_DESIGN.md +0 -916
- package/lib/server/routes/system.js +0 -29
- package/lib/server/services/version-manager.js +0 -170
|
@@ -1,783 +0,0 @@
|
|
|
1
|
-
# NexusCLI Database Schema - SQLite
|
|
2
|
-
|
|
3
|
-
**Version**: 0.1.0
|
|
4
|
-
**Created**: 2025-11-17
|
|
5
|
-
**Database**: SQLite (better-sqlite3)
|
|
6
|
-
**Philosophy**: Minimal, portable, chat-friendly
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## ๐ฏ Why SQLite?
|
|
11
|
-
|
|
12
|
-
### Alignment with Three Pillars
|
|
13
|
-
|
|
14
|
-
**1. Lightweight** โ
|
|
15
|
-
- Single dependency: `better-sqlite3` (~500KB native)
|
|
16
|
-
- No server process (embedded)
|
|
17
|
-
- Single file database (< 10MB for 1000s of messages)
|
|
18
|
-
|
|
19
|
-
**2. Portable (Linux + Termux)** โ
|
|
20
|
-
- Works on x86_64 and aarch64 (Termux)
|
|
21
|
-
- Pre-compiled binaries for Android
|
|
22
|
-
- Single `.db` file (easy backup)
|
|
23
|
-
|
|
24
|
-
**3. ChatGPT UI** โ
|
|
25
|
-
- Perfect for conversation history
|
|
26
|
-
- Fast full-text search (`FTS5`)
|
|
27
|
-
- Efficient pagination
|
|
28
|
-
|
|
29
|
-
### Alternatives Considered
|
|
30
|
-
|
|
31
|
-
| Option | Pros | Cons | Verdict |
|
|
32
|
-
|--------|------|------|---------|
|
|
33
|
-
| **SQLite** | Lightweight, portable, SQL | Native dependency | โ
**CHOSEN** |
|
|
34
|
-
| JSON files | Zero deps, simple | Slow search, no relations | โ Not scalable |
|
|
35
|
-
| MongoDB | Powerful | Heavy, not Termux-friendly | โ Too heavy |
|
|
36
|
-
| LevelDB | Fast K-V store | No SQL, harder queries | โ Not chat-friendly |
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## ๐ฆ Installation
|
|
41
|
-
|
|
42
|
-
### Dependency
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
// backend/package.json
|
|
46
|
-
{
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"express": "^4.18.2",
|
|
49
|
-
"cors": "^2.8.5",
|
|
50
|
-
"uuid": "^9.0.0",
|
|
51
|
-
"better-sqlite3": "^9.2.2" // โ Only new dependency
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Total dependencies**: 4 packages (still < 10 โ
)
|
|
57
|
-
|
|
58
|
-
### Setup (Linux)
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
cd backend
|
|
62
|
-
npm install better-sqlite3
|
|
63
|
-
# Native compilation (uses node-gyp)
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Setup (Termux)
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
# Termux requires build tools
|
|
70
|
-
pkg install clang make python
|
|
71
|
-
|
|
72
|
-
# Install better-sqlite3 (compiles for aarch64)
|
|
73
|
-
npm install better-sqlite3
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**Note**: Pre-built binaries usually available, compilation is fallback.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## ๐๏ธ Schema Design
|
|
81
|
-
|
|
82
|
-
### Database File Location
|
|
83
|
-
|
|
84
|
-
**Linux**:
|
|
85
|
-
```
|
|
86
|
-
/var/lib/nexuscli/nexuscli.db
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**Termux**:
|
|
90
|
-
```
|
|
91
|
-
/data/data/com.termux/files/home/.nexuscli/nexuscli.db
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Code**:
|
|
95
|
-
```javascript
|
|
96
|
-
// config.js
|
|
97
|
-
const isTermux = process.env.PREFIX?.includes('com.termux');
|
|
98
|
-
|
|
99
|
-
const DB_PATH = isTermux
|
|
100
|
-
? `${process.env.HOME}/.nexuscli/nexuscli.db`
|
|
101
|
-
: '/var/lib/nexuscli/nexuscli.db';
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## ๐ Tables
|
|
107
|
-
|
|
108
|
-
### 1. `conversations`
|
|
109
|
-
|
|
110
|
-
Tracks chat sessions (like ChatGPT conversations).
|
|
111
|
-
|
|
112
|
-
```sql
|
|
113
|
-
CREATE TABLE conversations (
|
|
114
|
-
id TEXT PRIMARY KEY, -- UUID
|
|
115
|
-
title TEXT NOT NULL, -- "Deploy to Production"
|
|
116
|
-
created_at INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
117
|
-
updated_at INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
118
|
-
metadata TEXT, -- JSON: {"tags": ["prod"], "archived": false}
|
|
119
|
-
|
|
120
|
-
INDEX idx_conversations_updated_at ON conversations(updated_at DESC)
|
|
121
|
-
);
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Example Row**:
|
|
125
|
-
```json
|
|
126
|
-
{
|
|
127
|
-
"id": "conv-abc123",
|
|
128
|
-
"title": "Deploy NexusCLI to Production",
|
|
129
|
-
"created_at": 1700000000000,
|
|
130
|
-
"updated_at": 1700001000000,
|
|
131
|
-
"metadata": "{\"tags\":[\"prod\",\"deploy\"],\"archived\":false}"
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
### 2. `messages`
|
|
138
|
-
|
|
139
|
-
Individual messages in conversations (user prompts + assistant responses).
|
|
140
|
-
|
|
141
|
-
```sql
|
|
142
|
-
CREATE TABLE messages (
|
|
143
|
-
id TEXT PRIMARY KEY, -- UUID
|
|
144
|
-
conversation_id TEXT NOT NULL, -- FK to conversations.id
|
|
145
|
-
role TEXT NOT NULL, -- 'user' | 'assistant' | 'system'
|
|
146
|
-
content TEXT NOT NULL, -- Markdown text
|
|
147
|
-
created_at INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
148
|
-
metadata TEXT, -- JSON: {"streaming": true, "tokens": 123}
|
|
149
|
-
|
|
150
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
|
|
151
|
-
INDEX idx_messages_conversation_id ON messages(conversation_id),
|
|
152
|
-
INDEX idx_messages_created_at ON messages(created_at ASC)
|
|
153
|
-
);
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**Example Rows**:
|
|
157
|
-
```json
|
|
158
|
-
// User message
|
|
159
|
-
{
|
|
160
|
-
"id": "msg-001",
|
|
161
|
-
"conversation_id": "conv-abc123",
|
|
162
|
-
"role": "user",
|
|
163
|
-
"content": "Deploy to all production nodes",
|
|
164
|
-
"created_at": 1700000000000,
|
|
165
|
-
"metadata": null
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Assistant message
|
|
169
|
-
{
|
|
170
|
-
"id": "msg-002",
|
|
171
|
-
"conversation_id": "conv-abc123",
|
|
172
|
-
"role": "assistant",
|
|
173
|
-
"content": "Deploying to 3 nodes:\n- prod-001 โ
\n- prod-002 โ
\n- prod-003 โ ๏ธ",
|
|
174
|
-
"created_at": 1700000001000,
|
|
175
|
-
"metadata": "{\"tokens\":45,\"duration\":1234}"
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
### 3. `jobs`
|
|
182
|
-
|
|
183
|
-
Job execution records (linked to messages).
|
|
184
|
-
|
|
185
|
-
```sql
|
|
186
|
-
CREATE TABLE jobs (
|
|
187
|
-
id TEXT PRIMARY KEY, -- Job ID (UUID)
|
|
188
|
-
conversation_id TEXT, -- FK to conversations.id (optional)
|
|
189
|
-
message_id TEXT, -- FK to messages.id (optional)
|
|
190
|
-
node_id TEXT NOT NULL, -- Target node
|
|
191
|
-
tool TEXT NOT NULL, -- 'bash' | 'git' | 'docker'
|
|
192
|
-
command TEXT NOT NULL, -- Command executed
|
|
193
|
-
status TEXT NOT NULL, -- 'queued' | 'executing' | 'completed' | 'failed'
|
|
194
|
-
exit_code INTEGER, -- Exit code (NULL if not completed)
|
|
195
|
-
stdout TEXT, -- Command stdout
|
|
196
|
-
stderr TEXT, -- Command stderr
|
|
197
|
-
duration INTEGER, -- Execution time (ms)
|
|
198
|
-
created_at INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
199
|
-
started_at INTEGER, -- Unix timestamp (ms)
|
|
200
|
-
completed_at INTEGER, -- Unix timestamp (ms)
|
|
201
|
-
|
|
202
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL,
|
|
203
|
-
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL,
|
|
204
|
-
INDEX idx_jobs_conversation_id ON jobs(conversation_id),
|
|
205
|
-
INDEX idx_jobs_status ON jobs(status),
|
|
206
|
-
INDEX idx_jobs_created_at ON jobs(created_at DESC)
|
|
207
|
-
);
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Example Row**:
|
|
211
|
-
```json
|
|
212
|
-
{
|
|
213
|
-
"id": "job-xyz",
|
|
214
|
-
"conversation_id": "conv-abc123",
|
|
215
|
-
"message_id": "msg-002",
|
|
216
|
-
"node_id": "prod-001",
|
|
217
|
-
"tool": "bash",
|
|
218
|
-
"command": "systemctl restart nginx",
|
|
219
|
-
"status": "completed",
|
|
220
|
-
"exit_code": 0,
|
|
221
|
-
"stdout": "nginx.service restarted",
|
|
222
|
-
"stderr": "",
|
|
223
|
-
"duration": 1234,
|
|
224
|
-
"created_at": 1700000000000,
|
|
225
|
-
"started_at": 1700000000100,
|
|
226
|
-
"completed_at": 1700000001334
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
### 4. `nodes` (Optional - for multi-node setups)
|
|
233
|
-
|
|
234
|
-
Registered nodes.
|
|
235
|
-
|
|
236
|
-
```sql
|
|
237
|
-
CREATE TABLE nodes (
|
|
238
|
-
id TEXT PRIMARY KEY, -- Node ID
|
|
239
|
-
hostname TEXT NOT NULL, -- Node hostname
|
|
240
|
-
ip_address TEXT, -- Node IP
|
|
241
|
-
status TEXT NOT NULL, -- 'online' | 'offline'
|
|
242
|
-
capabilities TEXT, -- JSON: {"tools": ["bash", "git"]}
|
|
243
|
-
last_heartbeat INTEGER, -- Unix timestamp (ms)
|
|
244
|
-
created_at INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
245
|
-
|
|
246
|
-
INDEX idx_nodes_status ON nodes(status)
|
|
247
|
-
);
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## ๐ Full-Text Search (Optional Enhancement)
|
|
253
|
-
|
|
254
|
-
For searching chat history.
|
|
255
|
-
|
|
256
|
-
```sql
|
|
257
|
-
-- Virtual FTS5 table for messages
|
|
258
|
-
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
259
|
-
content,
|
|
260
|
-
conversation_id UNINDEXED,
|
|
261
|
-
content='messages',
|
|
262
|
-
content_rowid='rowid'
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
-- Triggers to keep FTS index synced
|
|
266
|
-
CREATE TRIGGER messages_fts_insert AFTER INSERT ON messages BEGIN
|
|
267
|
-
INSERT INTO messages_fts(rowid, content, conversation_id)
|
|
268
|
-
VALUES (new.rowid, new.content, new.conversation_id);
|
|
269
|
-
END;
|
|
270
|
-
|
|
271
|
-
CREATE TRIGGER messages_fts_delete AFTER DELETE ON messages BEGIN
|
|
272
|
-
DELETE FROM messages_fts WHERE rowid = old.rowid;
|
|
273
|
-
END;
|
|
274
|
-
|
|
275
|
-
CREATE TRIGGER messages_fts_update AFTER UPDATE ON messages BEGIN
|
|
276
|
-
UPDATE messages_fts SET content = new.content WHERE rowid = new.rowid;
|
|
277
|
-
END;
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
**Usage**:
|
|
281
|
-
```sql
|
|
282
|
-
-- Search messages for "nginx"
|
|
283
|
-
SELECT m.* FROM messages m
|
|
284
|
-
JOIN messages_fts fts ON fts.rowid = m.rowid
|
|
285
|
-
WHERE messages_fts MATCH 'nginx'
|
|
286
|
-
ORDER BY m.created_at DESC
|
|
287
|
-
LIMIT 20;
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
## ๐พ Database Wrapper
|
|
293
|
-
|
|
294
|
-
### Initialization
|
|
295
|
-
|
|
296
|
-
```javascript
|
|
297
|
-
// backend/db.js
|
|
298
|
-
const Database = require('better-sqlite3');
|
|
299
|
-
const path = require('path');
|
|
300
|
-
const fs = require('fs');
|
|
301
|
-
|
|
302
|
-
const isTermux = process.env.PREFIX?.includes('com.termux');
|
|
303
|
-
const dbDir = isTermux
|
|
304
|
-
? path.join(process.env.HOME, '.nexuscli')
|
|
305
|
-
: '/var/lib/nexuscli';
|
|
306
|
-
|
|
307
|
-
const dbPath = path.join(dbDir, 'nexuscli.db');
|
|
308
|
-
|
|
309
|
-
// Ensure directory exists
|
|
310
|
-
if (!fs.existsSync(dbDir)) {
|
|
311
|
-
fs.mkdirSync(dbDir, { recursive: true });
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Open database (creates if doesn't exist)
|
|
315
|
-
const db = new Database(dbPath);
|
|
316
|
-
|
|
317
|
-
// Enable WAL mode for better concurrency
|
|
318
|
-
db.pragma('journal_mode = WAL');
|
|
319
|
-
|
|
320
|
-
// Enable foreign keys
|
|
321
|
-
db.pragma('foreign_keys = ON');
|
|
322
|
-
|
|
323
|
-
// Initialize schema
|
|
324
|
-
function initSchema() {
|
|
325
|
-
db.exec(`
|
|
326
|
-
CREATE TABLE IF NOT EXISTS conversations (
|
|
327
|
-
id TEXT PRIMARY KEY,
|
|
328
|
-
title TEXT NOT NULL,
|
|
329
|
-
created_at INTEGER NOT NULL,
|
|
330
|
-
updated_at INTEGER NOT NULL,
|
|
331
|
-
metadata TEXT
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at
|
|
335
|
-
ON conversations(updated_at DESC);
|
|
336
|
-
|
|
337
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
338
|
-
id TEXT PRIMARY KEY,
|
|
339
|
-
conversation_id TEXT NOT NULL,
|
|
340
|
-
role TEXT NOT NULL,
|
|
341
|
-
content TEXT NOT NULL,
|
|
342
|
-
created_at INTEGER NOT NULL,
|
|
343
|
-
metadata TEXT,
|
|
344
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id
|
|
348
|
-
ON messages(conversation_id);
|
|
349
|
-
|
|
350
|
-
CREATE INDEX IF NOT EXISTS idx_messages_created_at
|
|
351
|
-
ON messages(created_at ASC);
|
|
352
|
-
|
|
353
|
-
CREATE TABLE IF NOT EXISTS jobs (
|
|
354
|
-
id TEXT PRIMARY KEY,
|
|
355
|
-
conversation_id TEXT,
|
|
356
|
-
message_id TEXT,
|
|
357
|
-
node_id TEXT NOT NULL,
|
|
358
|
-
tool TEXT NOT NULL,
|
|
359
|
-
command TEXT NOT NULL,
|
|
360
|
-
status TEXT NOT NULL,
|
|
361
|
-
exit_code INTEGER,
|
|
362
|
-
stdout TEXT,
|
|
363
|
-
stderr TEXT,
|
|
364
|
-
duration INTEGER,
|
|
365
|
-
created_at INTEGER NOT NULL,
|
|
366
|
-
started_at INTEGER,
|
|
367
|
-
completed_at INTEGER,
|
|
368
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL,
|
|
369
|
-
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_conversation_id
|
|
373
|
-
ON jobs(conversation_id);
|
|
374
|
-
|
|
375
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_status
|
|
376
|
-
ON jobs(status);
|
|
377
|
-
|
|
378
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_created_at
|
|
379
|
-
ON jobs(created_at DESC);
|
|
380
|
-
`);
|
|
381
|
-
|
|
382
|
-
console.log(`โ
Database initialized: ${dbPath}`);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
initSchema();
|
|
386
|
-
|
|
387
|
-
module.exports = db;
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
---
|
|
391
|
-
|
|
392
|
-
### CRUD Operations
|
|
393
|
-
|
|
394
|
-
```javascript
|
|
395
|
-
// backend/models/conversation.js
|
|
396
|
-
const db = require('../db');
|
|
397
|
-
const { v4: uuidv4 } = require('uuid');
|
|
398
|
-
|
|
399
|
-
class Conversation {
|
|
400
|
-
// Create new conversation
|
|
401
|
-
static create(title) {
|
|
402
|
-
const id = uuidv4();
|
|
403
|
-
const now = Date.now();
|
|
404
|
-
|
|
405
|
-
const stmt = db.prepare(`
|
|
406
|
-
INSERT INTO conversations (id, title, created_at, updated_at)
|
|
407
|
-
VALUES (?, ?, ?, ?)
|
|
408
|
-
`);
|
|
409
|
-
|
|
410
|
-
stmt.run(id, title, now, now);
|
|
411
|
-
|
|
412
|
-
return { id, title, created_at: now, updated_at: now };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Get conversation by ID
|
|
416
|
-
static getById(id) {
|
|
417
|
-
const stmt = db.prepare('SELECT * FROM conversations WHERE id = ?');
|
|
418
|
-
return stmt.get(id);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// List recent conversations
|
|
422
|
-
static listRecent(limit = 20) {
|
|
423
|
-
const stmt = db.prepare(`
|
|
424
|
-
SELECT * FROM conversations
|
|
425
|
-
ORDER BY updated_at DESC
|
|
426
|
-
LIMIT ?
|
|
427
|
-
`);
|
|
428
|
-
return stmt.all(limit);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Update conversation title
|
|
432
|
-
static updateTitle(id, title) {
|
|
433
|
-
const stmt = db.prepare(`
|
|
434
|
-
UPDATE conversations
|
|
435
|
-
SET title = ?, updated_at = ?
|
|
436
|
-
WHERE id = ?
|
|
437
|
-
`);
|
|
438
|
-
stmt.run(title, Date.now(), id);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Delete conversation (cascade deletes messages)
|
|
442
|
-
static delete(id) {
|
|
443
|
-
const stmt = db.prepare('DELETE FROM conversations WHERE id = ?');
|
|
444
|
-
stmt.run(id);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
module.exports = Conversation;
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
```javascript
|
|
452
|
-
// backend/models/message.js
|
|
453
|
-
const db = require('../db');
|
|
454
|
-
const { v4: uuidv4 } = require('uuid');
|
|
455
|
-
|
|
456
|
-
class Message {
|
|
457
|
-
// Add message to conversation
|
|
458
|
-
static create(conversationId, role, content, metadata = null) {
|
|
459
|
-
const id = uuidv4();
|
|
460
|
-
const now = Date.now();
|
|
461
|
-
|
|
462
|
-
const stmt = db.prepare(`
|
|
463
|
-
INSERT INTO messages (id, conversation_id, role, content, created_at, metadata)
|
|
464
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
465
|
-
`);
|
|
466
|
-
|
|
467
|
-
stmt.run(id, conversationId, role, content, now,
|
|
468
|
-
metadata ? JSON.stringify(metadata) : null);
|
|
469
|
-
|
|
470
|
-
// Update conversation updated_at
|
|
471
|
-
const updateConv = db.prepare(`
|
|
472
|
-
UPDATE conversations SET updated_at = ? WHERE id = ?
|
|
473
|
-
`);
|
|
474
|
-
updateConv.run(now, conversationId);
|
|
475
|
-
|
|
476
|
-
return { id, conversation_id: conversationId, role, content, created_at: now };
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Get messages for conversation
|
|
480
|
-
static getByConversation(conversationId, limit = 100) {
|
|
481
|
-
const stmt = db.prepare(`
|
|
482
|
-
SELECT * FROM messages
|
|
483
|
-
WHERE conversation_id = ?
|
|
484
|
-
ORDER BY created_at ASC
|
|
485
|
-
LIMIT ?
|
|
486
|
-
`);
|
|
487
|
-
return stmt.all(conversationId, limit);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Search messages (if FTS enabled)
|
|
491
|
-
static search(query, limit = 20) {
|
|
492
|
-
const stmt = db.prepare(`
|
|
493
|
-
SELECT m.* FROM messages m
|
|
494
|
-
JOIN messages_fts fts ON fts.rowid = m.rowid
|
|
495
|
-
WHERE messages_fts MATCH ?
|
|
496
|
-
ORDER BY m.created_at DESC
|
|
497
|
-
LIMIT ?
|
|
498
|
-
`);
|
|
499
|
-
return stmt.all(query, limit);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
module.exports = Message;
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
---
|
|
507
|
-
|
|
508
|
-
## ๐ Migration Strategy
|
|
509
|
-
|
|
510
|
-
### Initial Setup (v0.1.0)
|
|
511
|
-
|
|
512
|
-
```javascript
|
|
513
|
-
// backend/migrations/001-initial.js
|
|
514
|
-
module.exports = {
|
|
515
|
-
up(db) {
|
|
516
|
-
db.exec(`
|
|
517
|
-
CREATE TABLE conversations (...);
|
|
518
|
-
CREATE TABLE messages (...);
|
|
519
|
-
CREATE TABLE jobs (...);
|
|
520
|
-
`);
|
|
521
|
-
},
|
|
522
|
-
|
|
523
|
-
down(db) {
|
|
524
|
-
db.exec(`
|
|
525
|
-
DROP TABLE IF EXISTS jobs;
|
|
526
|
-
DROP TABLE IF EXISTS messages;
|
|
527
|
-
DROP TABLE IF EXISTS conversations;
|
|
528
|
-
`);
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Migration Runner
|
|
534
|
-
|
|
535
|
-
```javascript
|
|
536
|
-
// backend/migrate.js
|
|
537
|
-
const db = require('./db');
|
|
538
|
-
const fs = require('fs');
|
|
539
|
-
const path = require('path');
|
|
540
|
-
|
|
541
|
-
function getCurrentVersion() {
|
|
542
|
-
try {
|
|
543
|
-
const row = db.prepare('SELECT value FROM schema_version WHERE key = "version"').get();
|
|
544
|
-
return parseInt(row.value);
|
|
545
|
-
} catch {
|
|
546
|
-
// First run
|
|
547
|
-
db.exec('CREATE TABLE IF NOT EXISTS schema_version (key TEXT PRIMARY KEY, value TEXT)');
|
|
548
|
-
db.prepare('INSERT INTO schema_version (key, value) VALUES ("version", "0")').run();
|
|
549
|
-
return 0;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function runMigrations() {
|
|
554
|
-
const currentVersion = getCurrentVersion();
|
|
555
|
-
const migrationsDir = path.join(__dirname, 'migrations');
|
|
556
|
-
|
|
557
|
-
const migrations = fs.readdirSync(migrationsDir)
|
|
558
|
-
.filter(f => f.endsWith('.js'))
|
|
559
|
-
.sort();
|
|
560
|
-
|
|
561
|
-
for (const file of migrations) {
|
|
562
|
-
const migration = require(path.join(migrationsDir, file));
|
|
563
|
-
const version = parseInt(file.split('-')[0]);
|
|
564
|
-
|
|
565
|
-
if (version > currentVersion) {
|
|
566
|
-
console.log(`Running migration ${file}...`);
|
|
567
|
-
migration.up(db);
|
|
568
|
-
db.prepare('UPDATE schema_version SET value = ? WHERE key = "version"').run(version);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
console.log('โ
Migrations complete');
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
runMigrations();
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
---
|
|
579
|
-
|
|
580
|
-
## ๐ Performance Optimization
|
|
581
|
-
|
|
582
|
-
### WAL Mode (Write-Ahead Logging)
|
|
583
|
-
|
|
584
|
-
```javascript
|
|
585
|
-
// Already enabled in db.js
|
|
586
|
-
db.pragma('journal_mode = WAL');
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
**Benefits**:
|
|
590
|
-
- Concurrent reads while writing
|
|
591
|
-
- Faster writes
|
|
592
|
-
- Better crash recovery
|
|
593
|
-
|
|
594
|
-
### Prepared Statements
|
|
595
|
-
|
|
596
|
-
```javascript
|
|
597
|
-
// โ
GOOD: Reuse prepared statement
|
|
598
|
-
const stmt = db.prepare('SELECT * FROM messages WHERE conversation_id = ?');
|
|
599
|
-
const messages = stmt.all(conversationId);
|
|
600
|
-
|
|
601
|
-
// โ BAD: Parse SQL every time
|
|
602
|
-
db.prepare('SELECT * FROM messages WHERE conversation_id = ?').all(conversationId);
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
### Indexes
|
|
606
|
-
|
|
607
|
-
All critical queries have indexes:
|
|
608
|
-
- `conversations.updated_at` (recent list)
|
|
609
|
-
- `messages.conversation_id` (get messages)
|
|
610
|
-
- `messages.created_at` (chronological order)
|
|
611
|
-
- `jobs.status` (filter by status)
|
|
612
|
-
|
|
613
|
-
---
|
|
614
|
-
|
|
615
|
-
## ๐พ Backup & Recovery
|
|
616
|
-
|
|
617
|
-
### Backup Script
|
|
618
|
-
|
|
619
|
-
```bash
|
|
620
|
-
#!/bin/bash
|
|
621
|
-
# backup-db.sh
|
|
622
|
-
|
|
623
|
-
DB_PATH="/var/lib/nexuscli/nexuscli.db"
|
|
624
|
-
BACKUP_DIR="/var/backups/nexuscli"
|
|
625
|
-
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
626
|
-
|
|
627
|
-
mkdir -p "$BACKUP_DIR"
|
|
628
|
-
|
|
629
|
-
# SQLite backup (safe even with active connections)
|
|
630
|
-
sqlite3 "$DB_PATH" ".backup $BACKUP_DIR/nexuscli-$TIMESTAMP.db"
|
|
631
|
-
|
|
632
|
-
# Keep last 30 days
|
|
633
|
-
find "$BACKUP_DIR" -name "nexuscli-*.db" -mtime +30 -delete
|
|
634
|
-
|
|
635
|
-
echo "โ
Backup complete: $BACKUP_DIR/nexuscli-$TIMESTAMP.db"
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
### Restore
|
|
639
|
-
|
|
640
|
-
```bash
|
|
641
|
-
# Stop NexusCLI
|
|
642
|
-
systemctl stop nexuscli
|
|
643
|
-
|
|
644
|
-
# Restore backup
|
|
645
|
-
cp /var/backups/nexuscli/nexuscli-20251117.db /var/lib/nexuscli/nexuscli.db
|
|
646
|
-
|
|
647
|
-
# Start NexusCLI
|
|
648
|
-
systemctl start nexuscli
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
---
|
|
652
|
-
|
|
653
|
-
## ๐ฏ API Integration
|
|
654
|
-
|
|
655
|
-
### REST Endpoints
|
|
656
|
-
|
|
657
|
-
```javascript
|
|
658
|
-
// backend/routes/conversations.js
|
|
659
|
-
const express = require('express');
|
|
660
|
-
const Conversation = require('../models/conversation');
|
|
661
|
-
const Message = require('../models/message');
|
|
662
|
-
|
|
663
|
-
const router = express.Router();
|
|
664
|
-
|
|
665
|
-
// GET /api/v1/conversations - List recent
|
|
666
|
-
router.get('/', (req, res) => {
|
|
667
|
-
const conversations = Conversation.listRecent(20);
|
|
668
|
-
res.json({ conversations });
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
// POST /api/v1/conversations - Create new
|
|
672
|
-
router.post('/', (req, res) => {
|
|
673
|
-
const { title } = req.body;
|
|
674
|
-
const conversation = Conversation.create(title || 'New Conversation');
|
|
675
|
-
res.status(201).json(conversation);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// GET /api/v1/conversations/:id - Get conversation with messages
|
|
679
|
-
router.get('/:id', (req, res) => {
|
|
680
|
-
const conversation = Conversation.getById(req.params.id);
|
|
681
|
-
if (!conversation) {
|
|
682
|
-
return res.status(404).json({ error: 'Conversation not found' });
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const messages = Message.getByConversation(req.params.id);
|
|
686
|
-
res.json({ ...conversation, messages });
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
// POST /api/v1/conversations/:id/messages - Add message
|
|
690
|
-
router.post('/:id/messages', (req, res) => {
|
|
691
|
-
const { role, content } = req.body;
|
|
692
|
-
const message = Message.create(req.params.id, role, content);
|
|
693
|
-
res.status(201).json(message);
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// DELETE /api/v1/conversations/:id - Delete conversation
|
|
697
|
-
router.delete('/:id', (req, res) => {
|
|
698
|
-
Conversation.delete(req.params.id);
|
|
699
|
-
res.json({ success: true });
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
module.exports = router;
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
---
|
|
706
|
-
|
|
707
|
-
## ๐งช Testing
|
|
708
|
-
|
|
709
|
-
### Unit Tests (SQLite in-memory)
|
|
710
|
-
|
|
711
|
-
```javascript
|
|
712
|
-
// backend/tests/conversation.test.js
|
|
713
|
-
const Database = require('better-sqlite3');
|
|
714
|
-
const Conversation = require('../models/conversation');
|
|
715
|
-
|
|
716
|
-
// Use in-memory DB for tests
|
|
717
|
-
const testDb = new Database(':memory:');
|
|
718
|
-
|
|
719
|
-
beforeEach(() => {
|
|
720
|
-
// Initialize schema
|
|
721
|
-
testDb.exec(`CREATE TABLE conversations (...)`);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
test('create conversation', () => {
|
|
725
|
-
const conv = Conversation.create('Test Conversation');
|
|
726
|
-
expect(conv.title).toBe('Test Conversation');
|
|
727
|
-
expect(conv.id).toBeTruthy();
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
test('list recent conversations', () => {
|
|
731
|
-
Conversation.create('Conv 1');
|
|
732
|
-
Conversation.create('Conv 2');
|
|
733
|
-
|
|
734
|
-
const list = Conversation.listRecent();
|
|
735
|
-
expect(list.length).toBe(2);
|
|
736
|
-
expect(list[0].title).toBe('Conv 2'); // Most recent first
|
|
737
|
-
});
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
---
|
|
741
|
-
|
|
742
|
-
## ๐ Size Estimates
|
|
743
|
-
|
|
744
|
-
| Data | Rows | Size per Row | Total Size |
|
|
745
|
-
|------|------|--------------|------------|
|
|
746
|
-
| Conversations | 100 | ~200 bytes | ~20 KB |
|
|
747
|
-
| Messages | 10,000 | ~500 bytes | ~5 MB |
|
|
748
|
-
| Jobs | 5,000 | ~1 KB | ~5 MB |
|
|
749
|
-
| **Total** | - | - | **~10 MB** |
|
|
750
|
-
|
|
751
|
-
**Conclusion**: Even with heavy usage, DB stays < 50MB (lightweight โ
)
|
|
752
|
-
|
|
753
|
-
---
|
|
754
|
-
|
|
755
|
-
## ๐ฏ Alignment with Three Pillars
|
|
756
|
-
|
|
757
|
-
### 1. Lightweight โ
|
|
758
|
-
- Single dependency: `better-sqlite3`
|
|
759
|
-
- Embedded DB (no server)
|
|
760
|
-
- WAL mode (< 10MB typical size)
|
|
761
|
-
|
|
762
|
-
### 2. Portable โ
|
|
763
|
-
- Works on Linux x86_64
|
|
764
|
-
- Works on Termux aarch64
|
|
765
|
-
- Single `.db` file (easy sync)
|
|
766
|
-
|
|
767
|
-
### 3. ChatGPT UI โ
|
|
768
|
-
- Perfect for conversation history
|
|
769
|
-
- Fast message retrieval
|
|
770
|
-
- Full-text search support
|
|
771
|
-
|
|
772
|
-
---
|
|
773
|
-
|
|
774
|
-
## ๐ Related Documents
|
|
775
|
-
|
|
776
|
-
- [Design Principles](./DESIGN_PRINCIPLES.md) - Three pillars
|
|
777
|
-
- [Architecture Overview](./ARCHITECTURE.md) - System design
|
|
778
|
-
- [API Contract](./API_WRAPPER_CONTRACT.md) - REST API spec
|
|
779
|
-
|
|
780
|
-
---
|
|
781
|
-
|
|
782
|
-
_Database design for NexusCLI - Lightweight, Portable, Chat-Friendly_
|
|
783
|
-
_Generated by Claude Code (Sonnet 4.5) - 2025-11-17_
|