@mmmbuto/nexuscli 0.8.8 → 0.8.9
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 +6 -6
- package/lib/cli/status.js +19 -1
- package/lib/server/.env.example +1 -1
- package/lib/server/db.js.old +225 -0
- package/lib/server/docs/API_WRAPPER_CONTRACT.md +682 -0
- package/lib/server/docs/ARCHITECTURE.md +441 -0
- package/lib/server/docs/DATABASE_SCHEMA.md +783 -0
- package/lib/server/docs/DESIGN_PRINCIPLES.md +598 -0
- package/lib/server/docs/NEXUSCHAT_ANALYSIS.md +488 -0
- package/lib/server/docs/PIPELINE_INTEGRATION.md +636 -0
- package/lib/server/docs/README.md +272 -0
- package/lib/server/docs/UI_DESIGN.md +916 -0
- package/lib/server/routes/system.js +29 -0
- package/lib/server/server.js +2 -0
- package/lib/server/services/version-manager.js +170 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -27,13 +27,13 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
## Highlights (v0.8.
|
|
30
|
+
## Highlights (v0.8.9)
|
|
31
31
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- Voice input ready: Whisper STT + auto HTTPS
|
|
32
|
+
- Hardened update checks: npm registry fetch uses 3s wall-clock timeout + semver coercion (prefissi `v` inclusi), git path usa upstream reale (`@{u}`) e segnala update solo se il branch è dietro.
|
|
33
|
+
- Offline-friendly output: CLI `nexuscli status` mantiene box puliti e silenzia errori in assenza di rete; API `/api/v1/system/version` rispetta lo shape anche in fallback.
|
|
34
|
+
- Stable mobile layout: `100dvh` viewport, overscroll disabled, pinned input bar con safe-area padding per Android browsers.
|
|
35
|
+
- Resilient chats: pre-flight `/health` ping + 60s client timeout con messaggistica chiara per evitare freeze.
|
|
36
|
+
- Voice input ready: Whisper STT + auto HTTPS per microfono remoto; stop button interrompe Claude/Codex/Gemini in modo affidabile.
|
|
37
37
|
|
|
38
38
|
## Features
|
|
39
39
|
|
package/lib/cli/status.js
CHANGED
|
@@ -10,6 +10,7 @@ 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');
|
|
13
14
|
|
|
14
15
|
// Get version from package.json
|
|
15
16
|
const packageJson = require('../../package.json');
|
|
@@ -106,11 +107,28 @@ async function status() {
|
|
|
106
107
|
const geminiStatus = getEngineStatus('gemini');
|
|
107
108
|
const workspaceCount = countWorkspaces();
|
|
108
109
|
|
|
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
|
+
|
|
109
115
|
// Header
|
|
110
|
-
const header = `NexusCLI Status
|
|
116
|
+
const header = `NexusCLI Status ${versionDisplay}`;
|
|
111
117
|
const padding = ' '.repeat(Math.max(0, 41 - header.length));
|
|
112
118
|
console.log(chalk.bold('╔═══════════════════════════════════════════╗'));
|
|
113
119
|
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
|
+
|
|
114
132
|
console.log(chalk.bold('╠═══════════════════════════════════════════╣'));
|
|
115
133
|
|
|
116
134
|
// Server
|
package/lib/server/.env.example
CHANGED
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
};
|