@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.
@@ -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-DFYYfeuX.js"></script>
63
- <link rel="stylesheet" crossorigin href="/assets/index-Dl9FJBOB.css">
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>
@@ -1,5 +1,5 @@
1
1
  // NexusCLI Service Worker
2
- const CACHE_VERSION = 'nexuscli-v1';
2
+ const CACHE_VERSION = 'nexuscli-v1766155108128';
3
3
  const STATIC_CACHE = `${CACHE_VERSION}-static`;
4
4
  const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
5
5
 
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
- "build": "vite build",
8
+ "build": "vite build && node scripts/inject-sw-version.js",
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
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 ${versionDisplay}`;
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
@@ -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
  }
@@ -14,7 +14,7 @@ PORT=41800
14
14
  NODE_ENV=production
15
15
 
16
16
  # Workspace directory for CLI execution
17
- WORKSPACE_DIR=/var/www/cli.wellanet.dev
17
+ WORKSPACE_DIR=/home/user/myproject
18
18
 
19
19
  # Timeout for CLI commands (ms)
20
20
  DEFAULT_TIMEOUT=30000
@@ -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 {
@@ -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 - detect DeepSeek models and configure API accordingly
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
- ptyProcess = pty.spawn(command, spawnArgs, {
250
- name: 'xterm-color',
251
- cols: 80,
252
- rows: 30,
253
- cwd: cwd, // Use session-specific workspace
254
- env: spawnEnv, // Use configured env (includes DeepSeek API if needed)
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
- // Timeout after 10 minutes (same as Codex wrapper)
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('[ClaudeWrapper] Timeout after 10 minutes');
371
+ console.error(`[ClaudeWrapper] Timeout after ${timeoutLabel}`);
273
372
  if (onStatus) {
274
- onStatus({ type: 'error', category: 'timeout', message: 'Claude CLI timeout after 10 minutes' });
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('Claude CLI timeout after 10 minutes'));
382
+ reject(new Error(`Claude CLI timeout after ${timeoutLabel}`));
284
383
  }
285
- }, 600000);
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
- return this.usage;
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.8.9",
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
  },
@@ -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
- };