@mmmbuto/nexuscli 0.8.9 → 0.9.1
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/frontend/dist/assets/{index-Dl9FJBOB.css → index-Ci39i_2l.css} +1 -1
- package/frontend/dist/assets/{index-DFYYfeuX.js → index-x6Jl2qtq.js} +1697 -1697
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +1 -1
- package/frontend/package.json +1 -1
- package/lib/cli/api.js +6 -0
- package/lib/cli/status.js +1 -19
- package/lib/config/models.js +15 -0
- package/lib/server/.env.example +1 -1
- package/lib/server/lib/pty-adapter.js +2 -0
- package/lib/server/routes/models.js +1 -1
- package/lib/server/server.js +0 -2
- package/lib/server/services/claude-wrapper.js +115 -16
- package/lib/server/services/output-parser.js +11 -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
package/frontend/dist/index.html
CHANGED
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
|
|
60
60
|
<!-- Prevent Scaling on iOS -->
|
|
61
61
|
<meta name="format-detection" content="telephone=no" />
|
|
62
|
-
<script type="module" crossorigin src="/assets/index-
|
|
63
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
62
|
+
<script type="module" crossorigin src="/assets/index-x6Jl2qtq.js"></script>
|
|
63
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Ci39i_2l.css">
|
|
64
64
|
</head>
|
|
65
65
|
<body>
|
|
66
66
|
<div id="root"></div>
|
package/frontend/dist/sw.js
CHANGED
package/frontend/package.json
CHANGED
package/lib/cli/api.js
CHANGED
|
@@ -32,6 +32,12 @@ const SUPPORTED_PROVIDERS = {
|
|
|
32
32
|
description: 'Multi-provider gateway',
|
|
33
33
|
keyFormat: 'sk-or-*',
|
|
34
34
|
url: 'https://openrouter.ai/keys'
|
|
35
|
+
},
|
|
36
|
+
zai: {
|
|
37
|
+
name: 'Z.ai',
|
|
38
|
+
description: 'GLM-4.6 (Chinese/English Multilingual)',
|
|
39
|
+
keyFormat: 'starts with alphanumeric + dot',
|
|
40
|
+
url: 'https://z.ai'
|
|
35
41
|
}
|
|
36
42
|
};
|
|
37
43
|
|
package/lib/cli/status.js
CHANGED
|
@@ -10,7 +10,6 @@ const { execSync } = require('child_process');
|
|
|
10
10
|
const { isInitialized, getConfig } = require('../config/manager');
|
|
11
11
|
const { PATHS, HOME } = require('../utils/paths');
|
|
12
12
|
const { isTermux, isTermuxApiWorking } = require('../utils/termux');
|
|
13
|
-
const versionManager = require('../server/services/version-manager');
|
|
14
13
|
|
|
15
14
|
// Get version from package.json
|
|
16
15
|
const packageJson = require('../../package.json');
|
|
@@ -107,28 +106,11 @@ async function status() {
|
|
|
107
106
|
const geminiStatus = getEngineStatus('gemini');
|
|
108
107
|
const workspaceCount = countWorkspaces();
|
|
109
108
|
|
|
110
|
-
// Version Check
|
|
111
|
-
const updateInfo = await versionManager.checkUpdate();
|
|
112
|
-
const vColor = updateInfo.updateAvailable ? chalk.yellow : chalk.green;
|
|
113
|
-
const versionDisplay = `v${updateInfo.current}`;
|
|
114
|
-
|
|
115
109
|
// Header
|
|
116
|
-
const header = `NexusCLI Status ${
|
|
110
|
+
const header = `NexusCLI Status v${VERSION}`;
|
|
117
111
|
const padding = ' '.repeat(Math.max(0, 41 - header.length));
|
|
118
112
|
console.log(chalk.bold('╔═══════════════════════════════════════════╗'));
|
|
119
113
|
console.log(chalk.bold(`║ ${header}${padding}║`));
|
|
120
|
-
|
|
121
|
-
if (updateInfo.updateAvailable) {
|
|
122
|
-
console.log(chalk.bold('╠═══════════════════════════════════════════╣'));
|
|
123
|
-
console.log(chalk.bold(`║ ${chalk.yellow('UPDATE AVAILABLE')} ║`));
|
|
124
|
-
const latestLabel = `Latest: ${updateInfo.latest || 'unknown'}`;
|
|
125
|
-
const runLabel = `Run: ${updateInfo.updateCommand || 'n/a'}`;
|
|
126
|
-
console.log(chalk.bold(`║ ${chalk.green(latestLabel.padEnd(38))}║`));
|
|
127
|
-
console.log(chalk.bold(`║ ${chalk.cyan(runLabel.padEnd(38))}║`));
|
|
128
|
-
} else if (updateInfo.status === 'error') {
|
|
129
|
-
// keep quiet on errors to avoid noisy offline output
|
|
130
|
-
}
|
|
131
|
-
|
|
132
114
|
console.log(chalk.bold('╠═══════════════════════════════════════════╣'));
|
|
133
115
|
|
|
134
116
|
// Server
|
package/lib/config/models.js
CHANGED
|
@@ -59,6 +59,14 @@ function getCliTools() {
|
|
|
59
59
|
label: 'DeepSeek Chat',
|
|
60
60
|
description: '💬 Fast Chat',
|
|
61
61
|
category: 'claude'
|
|
62
|
+
},
|
|
63
|
+
// === GLM-4.6 (Z.ai) ===
|
|
64
|
+
{
|
|
65
|
+
id: 'glm-4-6',
|
|
66
|
+
name: 'glm-4-6',
|
|
67
|
+
label: 'GLM 4.6',
|
|
68
|
+
description: '🌍 Advanced Chinese/English Multilingual',
|
|
69
|
+
category: 'claude'
|
|
62
70
|
}
|
|
63
71
|
]
|
|
64
72
|
},
|
|
@@ -137,6 +145,13 @@ function getCliTools() {
|
|
|
137
145
|
description: '🚀 Latest Preview',
|
|
138
146
|
category: 'gemini',
|
|
139
147
|
default: true
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'gemini-3-flash-preview',
|
|
151
|
+
name: 'gemini-3-flash-preview',
|
|
152
|
+
label: 'Gemini 3 Flash',
|
|
153
|
+
description: '⚡ Fastest Gemini 3 (preview)',
|
|
154
|
+
category: 'gemini'
|
|
140
155
|
}
|
|
141
156
|
]
|
|
142
157
|
}
|
package/lib/server/.env.example
CHANGED
|
@@ -29,11 +29,13 @@ function spawn(command, args, options = {}) {
|
|
|
29
29
|
|
|
30
30
|
proc.stdout.on('data', (buf) => {
|
|
31
31
|
const data = buf.toString();
|
|
32
|
+
console.log('[PTY-Adapter] stdout:', data.substring(0, 200));
|
|
32
33
|
dataHandlers.forEach((fn) => fn(data));
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
proc.stderr.on('data', (buf) => {
|
|
36
37
|
const data = buf.toString();
|
|
38
|
+
console.log('[PTY-Adapter] stderr:', data.substring(0, 200));
|
|
37
39
|
dataHandlers.forEach((fn) => fn(data));
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -9,7 +9,7 @@ const { getCliTools } = require('../../config/models');
|
|
|
9
9
|
* TRI CLI v0.4.0:
|
|
10
10
|
* - Claude: Opus 4.5, Sonnet 4.5, Haiku 4.5
|
|
11
11
|
* - Codex: GPT-5.1 variants
|
|
12
|
-
* - Gemini: Gemini 3 Pro Preview
|
|
12
|
+
* - Gemini: Gemini 3 Pro Preview, Gemini 3 Flash Preview
|
|
13
13
|
*/
|
|
14
14
|
router.get('/', (req, res) => {
|
|
15
15
|
try {
|
package/lib/server/server.js
CHANGED
|
@@ -30,7 +30,6 @@ const uploadRouter = require('./routes/upload');
|
|
|
30
30
|
const keysRouter = require('./routes/keys');
|
|
31
31
|
const speechRouter = require('./routes/speech');
|
|
32
32
|
const configRouter = require('./routes/config');
|
|
33
|
-
const systemRouter = require('./routes/system'); // System/Version
|
|
34
33
|
|
|
35
34
|
const app = express();
|
|
36
35
|
const PORT = process.env.PORT || 41800;
|
|
@@ -67,7 +66,6 @@ app.use('/api/v1/auth', authRouter);
|
|
|
67
66
|
app.use('/api/v1/models', modelsRouter);
|
|
68
67
|
app.use('/api/v1/config', configRouter);
|
|
69
68
|
app.use('/api/v1/workspace', workspaceRouter);
|
|
70
|
-
app.use('/api/v1/system', systemRouter); // System status (public for CLI check)
|
|
71
69
|
app.use('/api/v1', wakeLockRouter); // Wake lock endpoints (public for app visibility handling)
|
|
72
70
|
app.use('/api/v1/workspaces', authMiddleware, workspacesRouter);
|
|
73
71
|
app.use('/api/v1/sessions', authMiddleware, sessionsRouter);
|
|
@@ -163,15 +163,25 @@ class ClaudeWrapper extends BaseCliWrapper {
|
|
|
163
163
|
// Check if this is an existing session (DB is source of truth)
|
|
164
164
|
const isExistingSession = this.isExistingSession(conversationId);
|
|
165
165
|
|
|
166
|
+
// Detect alternative models early (needed for args construction)
|
|
167
|
+
const isDeepSeek = model.startsWith('deepseek-');
|
|
168
|
+
const isGLM = model === 'glm-4-6';
|
|
169
|
+
const isAlternativeModel = isDeepSeek || isGLM;
|
|
170
|
+
|
|
166
171
|
// Build Claude Code CLI args
|
|
167
172
|
const args = [
|
|
168
173
|
'--dangerously-skip-permissions', // Auto-approve all tool use
|
|
169
|
-
'--model', model,
|
|
170
174
|
'--print', // Non-interactive mode
|
|
171
175
|
'--verbose', // Enable detailed output
|
|
172
176
|
'--output-format', 'stream-json', // JSON streaming events
|
|
173
177
|
];
|
|
174
178
|
|
|
179
|
+
// Only pass --model for native Claude models
|
|
180
|
+
// Alternative models (DeepSeek, GLM) use ANTHROPIC_MODEL env var
|
|
181
|
+
if (!isAlternativeModel) {
|
|
182
|
+
args.push('--model', model);
|
|
183
|
+
}
|
|
184
|
+
|
|
175
185
|
// Session management: -r (resume) or --session-id (new)
|
|
176
186
|
if (isExistingSession) {
|
|
177
187
|
args.push('-r', conversationId); // Resume with full history
|
|
@@ -187,9 +197,8 @@ class ClaudeWrapper extends BaseCliWrapper {
|
|
|
187
197
|
// Termux compatibility: make sure ripgrep path exists before spawn
|
|
188
198
|
this.ensureRipgrepForTermux();
|
|
189
199
|
|
|
190
|
-
// Build environment -
|
|
200
|
+
// Build environment - configure API for alternative models
|
|
191
201
|
const spawnEnv = { ...process.env };
|
|
192
|
-
const isDeepSeek = model.startsWith('deepseek-');
|
|
193
202
|
|
|
194
203
|
if (isDeepSeek) {
|
|
195
204
|
// Get API key from database (priority) or fallback to env var
|
|
@@ -217,10 +226,40 @@ class ClaudeWrapper extends BaseCliWrapper {
|
|
|
217
226
|
// DeepSeek uses Anthropic-compatible API at different endpoint
|
|
218
227
|
spawnEnv.ANTHROPIC_BASE_URL = 'https://api.deepseek.com/anthropic';
|
|
219
228
|
spawnEnv.ANTHROPIC_AUTH_TOKEN = deepseekKey;
|
|
229
|
+
spawnEnv.ANTHROPIC_MODEL = model; // Pass model name to API
|
|
220
230
|
console.log(`[ClaudeWrapper] DeepSeek detected - using api.deepseek.com/anthropic`);
|
|
231
|
+
} else if (isGLM) {
|
|
232
|
+
// Get API key from database (priority) or fallback to env var
|
|
233
|
+
const glmKey = getApiKey('zai') || process.env.ZAI_API_KEY;
|
|
234
|
+
|
|
235
|
+
if (!glmKey) {
|
|
236
|
+
const errorMsg = `Z.ai API key not configured for GLM-4.6!\n\n` +
|
|
237
|
+
`Run this command to add your API key:\n` +
|
|
238
|
+
` nexuscli api set zai YOUR_API_KEY\n\n` +
|
|
239
|
+
`Get your key at: https://z.ai`;
|
|
240
|
+
|
|
241
|
+
console.error(`[ClaudeWrapper] ❌ ${errorMsg}`);
|
|
242
|
+
|
|
243
|
+
if (onStatus) {
|
|
244
|
+
onStatus({
|
|
245
|
+
type: 'error',
|
|
246
|
+
category: 'config',
|
|
247
|
+
message: errorMsg
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return reject(new Error(errorMsg));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// GLM-4.6 uses Z.ai Anthropic-compatible API
|
|
255
|
+
spawnEnv.ANTHROPIC_BASE_URL = 'https://api.z.ai/api/anthropic';
|
|
256
|
+
spawnEnv.ANTHROPIC_AUTH_TOKEN = glmKey;
|
|
257
|
+
spawnEnv.ANTHROPIC_MODEL = 'GLM-4.6'; // Z.ai model name
|
|
258
|
+
spawnEnv.API_TIMEOUT_MS = '3000000'; // 50 minutes timeout
|
|
259
|
+
console.log(`[ClaudeWrapper] GLM-4.6 detected - using Z.ai API with extended timeout`);
|
|
221
260
|
}
|
|
222
261
|
|
|
223
|
-
console.log(`[ClaudeWrapper] Model: ${model}${isDeepSeek ? ' (DeepSeek API)' : ''}`);
|
|
262
|
+
console.log(`[ClaudeWrapper] Model: ${model}${isDeepSeek ? ' (DeepSeek API)' : isGLM ? ' (Z.ai API)' : ''}`);
|
|
224
263
|
console.log(`[ClaudeWrapper] Session: ${conversationId} (${isExistingSession ? 'RESUME' : 'NEW'})`);
|
|
225
264
|
console.log(`[ClaudeWrapper] Working dir: ${cwd}`);
|
|
226
265
|
|
|
@@ -246,13 +285,63 @@ class ClaudeWrapper extends BaseCliWrapper {
|
|
|
246
285
|
|
|
247
286
|
let ptyProcess;
|
|
248
287
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
288
|
+
// Try direct spawn for Termux compatibility
|
|
289
|
+
const { spawn } = require('child_process');
|
|
290
|
+
ptyProcess = spawn(command, spawnArgs, {
|
|
291
|
+
cwd: cwd,
|
|
292
|
+
env: spawnEnv,
|
|
293
|
+
shell: false,
|
|
294
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Wrap to PTY interface
|
|
298
|
+
const onDataHandlers = [];
|
|
299
|
+
const onExitHandlers = [];
|
|
300
|
+
const onErrorHandlers = [];
|
|
301
|
+
const killProc = ptyProcess.kill.bind(ptyProcess);
|
|
302
|
+
|
|
303
|
+
ptyProcess.stdout.on('data', (data) => {
|
|
304
|
+
const text = data.toString();
|
|
305
|
+
console.log('[ClaudeWrapper] Direct stdout:', text.substring(0, 200));
|
|
306
|
+
onDataHandlers.forEach(fn => fn(text));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
ptyProcess.stderr.on('data', (data) => {
|
|
310
|
+
const text = data.toString();
|
|
311
|
+
console.log('[ClaudeWrapper] Direct stderr:', text.substring(0, 200));
|
|
312
|
+
onDataHandlers.forEach(fn => fn(text));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
ptyProcess.on('close', (code) => {
|
|
316
|
+
console.log('[ClaudeWrapper] Process closed with code:', code);
|
|
317
|
+
onExitHandlers.forEach(fn => fn({ exitCode: code ?? 0 }));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
ptyProcess.on('error', (err) => {
|
|
321
|
+
console.error('[ClaudeWrapper] Process error:', err.message);
|
|
322
|
+
onErrorHandlers.forEach(fn => fn(err));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// PTY-compatible wrapper
|
|
326
|
+
ptyProcess.onData = (fn) => onDataHandlers.push(fn);
|
|
327
|
+
ptyProcess.onExit = (fn) => onExitHandlers.push(fn);
|
|
328
|
+
ptyProcess.onError = (fn) => onErrorHandlers.push(fn);
|
|
329
|
+
ptyProcess.write = (data) => ptyProcess.stdin?.write(data);
|
|
330
|
+
ptyProcess.sendEsc = () => {
|
|
331
|
+
if (ptyProcess.stdin?.writable) {
|
|
332
|
+
ptyProcess.stdin.write('\x1b');
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
};
|
|
337
|
+
ptyProcess.kill = (signal) => killProc(signal);
|
|
338
|
+
ptyProcess.pid = ptyProcess.pid;
|
|
339
|
+
|
|
340
|
+
// Claude Code blocks waiting for stdin EOF when stdin is not a TTY.
|
|
341
|
+
// In --print mode we don't need stdin, so close it to avoid hanging.
|
|
342
|
+
if (ptyProcess.stdin && !ptyProcess.stdin.destroyed) {
|
|
343
|
+
ptyProcess.stdin.end();
|
|
344
|
+
}
|
|
256
345
|
} catch (err) {
|
|
257
346
|
const msg = `Failed to spawn Claude CLI: ${err.message}`;
|
|
258
347
|
console.error('[ClaudeWrapper]', msg);
|
|
@@ -267,11 +356,21 @@ class ClaudeWrapper extends BaseCliWrapper {
|
|
|
267
356
|
|
|
268
357
|
let stdout = '';
|
|
269
358
|
|
|
270
|
-
//
|
|
359
|
+
// Dynamic timeout based on model
|
|
360
|
+
let timeoutMs = 600000; // 10 minutes default
|
|
361
|
+
let timeoutLabel = '10 minutes';
|
|
362
|
+
if (isGLM) {
|
|
363
|
+
timeoutMs = 3600000; // 60 minutes for GLM-4.6 (slow responses)
|
|
364
|
+
timeoutLabel = '60 minutes';
|
|
365
|
+
} else if (isDeepSeek) {
|
|
366
|
+
timeoutMs = 900000; // 15 minutes for DeepSeek
|
|
367
|
+
timeoutLabel = '15 minutes';
|
|
368
|
+
}
|
|
369
|
+
|
|
271
370
|
const timeout = setTimeout(() => {
|
|
272
|
-
console.error(
|
|
371
|
+
console.error(`[ClaudeWrapper] Timeout after ${timeoutLabel}`);
|
|
273
372
|
if (onStatus) {
|
|
274
|
-
onStatus({ type: 'error', category: 'timeout', message:
|
|
373
|
+
onStatus({ type: 'error', category: 'timeout', message: `Claude CLI timeout after ${timeoutLabel}` });
|
|
275
374
|
}
|
|
276
375
|
try {
|
|
277
376
|
ptyProcess.kill();
|
|
@@ -280,9 +379,9 @@ class ClaudeWrapper extends BaseCliWrapper {
|
|
|
280
379
|
}
|
|
281
380
|
if (!promiseSettled) {
|
|
282
381
|
promiseSettled = true;
|
|
283
|
-
reject(new Error(
|
|
382
|
+
reject(new Error(`Claude CLI timeout after ${timeoutLabel}`));
|
|
284
383
|
}
|
|
285
|
-
},
|
|
384
|
+
}, timeoutMs);
|
|
286
385
|
|
|
287
386
|
// Process output chunks
|
|
288
387
|
ptyProcess.onData((data) => {
|
|
@@ -404,10 +404,19 @@ class OutputParser {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
/**
|
|
407
|
-
* Get usage statistics
|
|
407
|
+
* Get usage statistics (normalized for different API formats)
|
|
408
|
+
* Supports both Claude naming (input_tokens) and OpenAI naming (prompt_tokens)
|
|
408
409
|
*/
|
|
409
410
|
getUsage() {
|
|
410
|
-
|
|
411
|
+
if (!this.usage) return null;
|
|
412
|
+
|
|
413
|
+
// Normalize field names for different API providers (Claude, DeepSeek, GLM)
|
|
414
|
+
return {
|
|
415
|
+
input_tokens: this.usage.input_tokens || this.usage.prompt_tokens || 0,
|
|
416
|
+
output_tokens: this.usage.output_tokens || this.usage.completion_tokens || 0,
|
|
417
|
+
cache_creation_input_tokens: this.usage.cache_creation_input_tokens || 0,
|
|
418
|
+
cache_read_input_tokens: this.usage.cache_read_input_tokens || 0,
|
|
419
|
+
};
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmmbuto/nexuscli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
|
|
5
5
|
"main": "lib/server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,6 @@
|
|
|
70
70
|
"multer": "^2.0.2",
|
|
71
71
|
"node-cache": "^5.1.2",
|
|
72
72
|
"ora": "^5.4.1",
|
|
73
|
-
"semver": "^7.7.3",
|
|
74
73
|
"sql.js": "^1.13.0",
|
|
75
74
|
"uuid": "^9.0.0"
|
|
76
75
|
},
|
package/lib/server/db.js.old
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
const initSqlJs = require('sql.js');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
|
|
5
|
-
// Detect environment (Linux vs Termux)
|
|
6
|
-
const isTermux = process.env.PREFIX?.includes('com.termux');
|
|
7
|
-
|
|
8
|
-
// Database directory
|
|
9
|
-
const dbDir = isTermux
|
|
10
|
-
? path.join(process.env.HOME, '.nexuscli')
|
|
11
|
-
: process.env.NEXUSCLI_DB_DIR || '/var/lib/nexuscli';
|
|
12
|
-
|
|
13
|
-
// Database file path
|
|
14
|
-
const dbPath = path.join(dbDir, 'nexuscli.db');
|
|
15
|
-
|
|
16
|
-
// Ensure directory exists
|
|
17
|
-
if (!fs.existsSync(dbDir)) {
|
|
18
|
-
fs.mkdirSync(dbDir, { recursive: true });
|
|
19
|
-
console.log(`✅ Created database directory: ${dbDir}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let db = null;
|
|
23
|
-
|
|
24
|
-
// Initialize database
|
|
25
|
-
async function initDb() {
|
|
26
|
-
const SQL = await initSqlJs();
|
|
27
|
-
|
|
28
|
-
// Load existing database or create new
|
|
29
|
-
if (fs.existsSync(dbPath)) {
|
|
30
|
-
const buffer = fs.readFileSync(dbPath);
|
|
31
|
-
db = new SQL.Database(buffer);
|
|
32
|
-
console.log(`✅ Database loaded: ${dbPath}`);
|
|
33
|
-
} else {
|
|
34
|
-
db = new SQL.Database();
|
|
35
|
-
console.log(`✅ Database created: ${dbPath}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Initialize schema
|
|
39
|
-
initSchema();
|
|
40
|
-
|
|
41
|
-
// Auto-save every 5 seconds
|
|
42
|
-
setInterval(saveDb, 5000);
|
|
43
|
-
|
|
44
|
-
return db;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Save database to file
|
|
48
|
-
function saveDb() {
|
|
49
|
-
if (!db) return;
|
|
50
|
-
const data = db.export();
|
|
51
|
-
const buffer = Buffer.from(data);
|
|
52
|
-
fs.writeFileSync(dbPath, buffer);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Initialize schema
|
|
56
|
-
function initSchema() {
|
|
57
|
-
db.run(`
|
|
58
|
-
-- Conversations table
|
|
59
|
-
CREATE TABLE IF NOT EXISTS conversations (
|
|
60
|
-
id TEXT PRIMARY KEY,
|
|
61
|
-
title TEXT NOT NULL,
|
|
62
|
-
created_at INTEGER NOT NULL,
|
|
63
|
-
updated_at INTEGER NOT NULL,
|
|
64
|
-
metadata TEXT
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at
|
|
68
|
-
ON conversations(updated_at DESC);
|
|
69
|
-
|
|
70
|
-
-- Messages table
|
|
71
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
72
|
-
id TEXT PRIMARY KEY,
|
|
73
|
-
conversation_id TEXT NOT NULL,
|
|
74
|
-
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
|
75
|
-
content TEXT NOT NULL,
|
|
76
|
-
created_at INTEGER NOT NULL,
|
|
77
|
-
metadata TEXT,
|
|
78
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id
|
|
82
|
-
ON messages(conversation_id);
|
|
83
|
-
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_messages_created_at
|
|
85
|
-
ON messages(created_at ASC);
|
|
86
|
-
|
|
87
|
-
-- Jobs table
|
|
88
|
-
CREATE TABLE IF NOT EXISTS jobs (
|
|
89
|
-
id TEXT PRIMARY KEY,
|
|
90
|
-
conversation_id TEXT,
|
|
91
|
-
message_id TEXT,
|
|
92
|
-
node_id TEXT NOT NULL,
|
|
93
|
-
tool TEXT NOT NULL,
|
|
94
|
-
command TEXT NOT NULL,
|
|
95
|
-
status TEXT NOT NULL CHECK(status IN ('queued', 'executing', 'completed', 'failed', 'cancelled')),
|
|
96
|
-
exit_code INTEGER,
|
|
97
|
-
stdout TEXT,
|
|
98
|
-
stderr TEXT,
|
|
99
|
-
duration INTEGER,
|
|
100
|
-
created_at INTEGER NOT NULL,
|
|
101
|
-
started_at INTEGER,
|
|
102
|
-
completed_at INTEGER,
|
|
103
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL,
|
|
104
|
-
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_conversation_id
|
|
108
|
-
ON jobs(conversation_id);
|
|
109
|
-
|
|
110
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_status
|
|
111
|
-
ON jobs(status);
|
|
112
|
-
|
|
113
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_created_at
|
|
114
|
-
ON jobs(created_at DESC);
|
|
115
|
-
|
|
116
|
-
-- Users table
|
|
117
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
118
|
-
id TEXT PRIMARY KEY,
|
|
119
|
-
username TEXT UNIQUE NOT NULL,
|
|
120
|
-
password_hash TEXT NOT NULL,
|
|
121
|
-
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin', 'user')),
|
|
122
|
-
is_locked INTEGER NOT NULL DEFAULT 0,
|
|
123
|
-
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
124
|
-
last_failed_attempt INTEGER,
|
|
125
|
-
locked_until INTEGER,
|
|
126
|
-
created_at INTEGER NOT NULL,
|
|
127
|
-
last_login INTEGER
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
CREATE INDEX IF NOT EXISTS idx_users_username
|
|
131
|
-
ON users(username);
|
|
132
|
-
|
|
133
|
-
-- Login attempts table (for rate limiting)
|
|
134
|
-
CREATE TABLE IF NOT EXISTS login_attempts (
|
|
135
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
136
|
-
ip_address TEXT NOT NULL,
|
|
137
|
-
username TEXT,
|
|
138
|
-
success INTEGER NOT NULL DEFAULT 0,
|
|
139
|
-
timestamp INTEGER NOT NULL
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
CREATE INDEX IF NOT EXISTS idx_login_attempts_ip
|
|
143
|
-
ON login_attempts(ip_address, timestamp DESC);
|
|
144
|
-
|
|
145
|
-
CREATE INDEX IF NOT EXISTS idx_login_attempts_timestamp
|
|
146
|
-
ON login_attempts(timestamp DESC);
|
|
147
|
-
|
|
148
|
-
-- Nodes table (optional - for multi-node setups)
|
|
149
|
-
CREATE TABLE IF NOT EXISTS nodes (
|
|
150
|
-
id TEXT PRIMARY KEY,
|
|
151
|
-
hostname TEXT NOT NULL,
|
|
152
|
-
ip_address TEXT,
|
|
153
|
-
status TEXT NOT NULL CHECK(status IN ('online', 'offline', 'error')),
|
|
154
|
-
capabilities TEXT,
|
|
155
|
-
last_heartbeat INTEGER,
|
|
156
|
-
created_at INTEGER NOT NULL
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
CREATE INDEX IF NOT EXISTS idx_nodes_status
|
|
160
|
-
ON nodes(status);
|
|
161
|
-
`);
|
|
162
|
-
|
|
163
|
-
console.log('✅ Database schema initialized');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Prepare statement (sql.js uses db.prepare)
|
|
167
|
-
function prepare(sql) {
|
|
168
|
-
const stmt = db.prepare(sql);
|
|
169
|
-
return {
|
|
170
|
-
run: (...params) => {
|
|
171
|
-
stmt.bind(params);
|
|
172
|
-
stmt.step();
|
|
173
|
-
stmt.reset();
|
|
174
|
-
saveDb(); // Auto-save on write
|
|
175
|
-
},
|
|
176
|
-
get: (...params) => {
|
|
177
|
-
stmt.bind(params);
|
|
178
|
-
const result = stmt.step() ? stmt.getAsObject() : null;
|
|
179
|
-
stmt.reset();
|
|
180
|
-
return result;
|
|
181
|
-
},
|
|
182
|
-
all: (...params) => {
|
|
183
|
-
stmt.bind(params);
|
|
184
|
-
const results = [];
|
|
185
|
-
while (stmt.step()) {
|
|
186
|
-
results.push(stmt.getAsObject());
|
|
187
|
-
}
|
|
188
|
-
stmt.reset();
|
|
189
|
-
return results;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Graceful shutdown
|
|
195
|
-
process.on('exit', () => {
|
|
196
|
-
if (db) {
|
|
197
|
-
saveDb();
|
|
198
|
-
db.close();
|
|
199
|
-
console.log('✅ Database connection closed');
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
process.on('SIGINT', () => {
|
|
204
|
-
if (db) {
|
|
205
|
-
saveDb();
|
|
206
|
-
db.close();
|
|
207
|
-
console.log('✅ Database connection closed (SIGINT)');
|
|
208
|
-
}
|
|
209
|
-
process.exit(0);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
process.on('SIGTERM', () => {
|
|
213
|
-
if (db) {
|
|
214
|
-
saveDb();
|
|
215
|
-
db.close();
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Export db object (initialized async)
|
|
220
|
-
module.exports = {
|
|
221
|
-
initDb,
|
|
222
|
-
getDb: () => db,
|
|
223
|
-
prepare,
|
|
224
|
-
saveDb
|
|
225
|
-
};
|