@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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,142 @@
1
+ const express = require('express');
2
+ const WorkspaceManager = require('../services/workspace-manager');
3
+ const { prepare } = require('../db');
4
+
5
+ const router = express.Router();
6
+ const workspaceManager = new WorkspaceManager();
7
+
8
+ /**
9
+ * GET /api/v1/workspaces
10
+ * List all unique workspaces from sessions
11
+ */
12
+ router.get('/', async (req, res) => {
13
+ try {
14
+ const stmt = prepare(`
15
+ SELECT
16
+ workspace_path,
17
+ COUNT(*) as session_count,
18
+ MAX(last_used_at) as last_activity
19
+ FROM sessions
20
+ GROUP BY workspace_path
21
+ ORDER BY last_activity DESC
22
+ `);
23
+
24
+ const workspaces = stmt.all();
25
+
26
+ res.json({ workspaces });
27
+ } catch (error) {
28
+ console.error('[Workspaces] Error listing workspaces:', error);
29
+ res.status(500).json({ error: error.message });
30
+ }
31
+ });
32
+
33
+ /**
34
+ * POST /api/v1/workspaces/:path/mount
35
+ * Mount workspace (validate + index sessions)
36
+ */
37
+ router.post('/:path(*)/mount', async (req, res) => {
38
+ try {
39
+ // Replace __ back to / (avoid Apache encoding issues with slashes in paths)
40
+ let workspacePath = req.params.path.replace(/__/g, '/');
41
+ workspacePath = decodeURIComponent(workspacePath);
42
+
43
+ const result = await workspaceManager.mountWorkspace(workspacePath);
44
+
45
+ res.json(result);
46
+ } catch (error) {
47
+ console.error('[Workspaces] Mount error:', error);
48
+ res.status(400).json({ error: error.message });
49
+ }
50
+ });
51
+
52
+ /**
53
+ * GET /api/v1/workspaces/:path/sessions
54
+ * Get sessions in workspace (lightweight)
55
+ */
56
+ router.get('/:path(*)/sessions', async (req, res) => {
57
+ try {
58
+ // Replace __ back to / (avoid Apache encoding issues with slashes in paths)
59
+ let workspacePath = req.params.path.replace(/__/g, '/');
60
+ workspacePath = decodeURIComponent(workspacePath);
61
+ const { groupBy = 'date' } = req.query;
62
+
63
+ // Get sessions from DB
64
+ const stmt = prepare(`
65
+ SELECT
66
+ s.*,
67
+ ss.summary_short,
68
+ ss.updated_at as summary_updated_at
69
+ FROM sessions s
70
+ LEFT JOIN session_summaries ss ON s.id = ss.session_id
71
+ WHERE s.workspace_path = ?
72
+ ORDER BY s.last_used_at DESC
73
+ `);
74
+
75
+ const sessions = stmt.all(workspacePath);
76
+
77
+ // Parse metadata JSON and ensure title is not empty
78
+ sessions.forEach(session => {
79
+ if (session.metadata) {
80
+ try {
81
+ session.metadata = JSON.parse(session.metadata);
82
+ } catch (e) {
83
+ session.metadata = {};
84
+ }
85
+ }
86
+
87
+ // Ensure title is not empty (fallback to default if missing)
88
+ if (!session.title || session.title.trim() === '') {
89
+ console.warn(`[Workspaces] Session ${session.id} has empty title, using default`);
90
+ session.title = 'Untitled Session';
91
+ }
92
+ });
93
+
94
+ // Group by date if requested
95
+ if (groupBy === 'date') {
96
+ const grouped = groupSessionsByDate(sessions);
97
+ return res.json(grouped);
98
+ }
99
+
100
+ res.json({ sessions });
101
+ } catch (error) {
102
+ console.error('[Workspaces] Error getting sessions:', error);
103
+ res.status(500).json({ error: error.message });
104
+ }
105
+ });
106
+
107
+ /**
108
+ * Group sessions by date
109
+ */
110
+ function groupSessionsByDate(sessions) {
111
+ const now = Date.now();
112
+ const oneDayMs = 24 * 60 * 60 * 1000;
113
+
114
+ const grouped = {
115
+ pinned: [],
116
+ today: [],
117
+ yesterday: [],
118
+ last7days: [],
119
+ last30days: [],
120
+ older: []
121
+ };
122
+
123
+ sessions.forEach(session => {
124
+ // Pinned sessions go to separate group
125
+ if (session.pinned) {
126
+ grouped.pinned.push(session);
127
+ return;
128
+ }
129
+
130
+ const age = now - session.last_used_at;
131
+
132
+ if (age < oneDayMs) grouped.today.push(session);
133
+ else if (age < 2 * oneDayMs) grouped.yesterday.push(session);
134
+ else if (age < 7 * oneDayMs) grouped.last7days.push(session);
135
+ else if (age < 30 * oneDayMs) grouped.last30days.push(session);
136
+ else grouped.older.push(session);
137
+ });
138
+
139
+ return grouped;
140
+ }
141
+
142
+ module.exports = router;
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { initDb, getDb } = require('../db');
4
+ const Message = require('../models/Message');
5
+
6
+ /**
7
+ * Identify and clean up "ghost" conversations
8
+ * Ghost = conversation with 0 messages OR only 1 user message (no assistant reply)
9
+ */
10
+ async function cleanupGhostSessions() {
11
+ await initDb();
12
+ const db = getDb();
13
+
14
+ console.log('[Cleanup] Starting ghost session cleanup...');
15
+
16
+ const conversations = db.prepare('SELECT id, title, created_at FROM conversations').all();
17
+ console.log(`[Cleanup] Total conversations: ${conversations.length}`);
18
+
19
+ const ghosts = [];
20
+
21
+ for (const conv of conversations) {
22
+ // Only need first two messages to detect ghosts
23
+ const messages = Message.getByConversation(conv.id, 2);
24
+
25
+ if (messages.length === 0) {
26
+ ghosts.push({ ...conv, reason: 'NO_MESSAGES' });
27
+ } else if (messages.length === 1 && messages[0].role === 'user') {
28
+ ghosts.push({ ...conv, reason: 'NO_REPLY' });
29
+ }
30
+ }
31
+
32
+ console.log(`[Cleanup] Found ${ghosts.length} ghost conversations`);
33
+
34
+ if (ghosts.length === 0) {
35
+ console.log('[Cleanup] No ghosts found - database is clean!');
36
+ return;
37
+ }
38
+
39
+ console.log('\n[Cleanup] Ghost conversations:');
40
+ ghosts.forEach(g => {
41
+ console.log(` - ${g.id.substring(0, 8)}: "${g.title}" (${g.reason})`);
42
+ });
43
+
44
+ const shouldDelete = process.argv.includes('--force');
45
+
46
+ if (!shouldDelete) {
47
+ console.log('\n[Cleanup] Dry run complete. Use --force to delete.');
48
+ return;
49
+ }
50
+
51
+ const deleteConvo = db.prepare('DELETE FROM conversations WHERE id = ?');
52
+ const deleteMessages = db.prepare('DELETE FROM messages WHERE conversation_id = ?');
53
+
54
+ for (const ghost of ghosts) {
55
+ deleteMessages.run(ghost.id);
56
+ deleteConvo.run(ghost.id);
57
+ }
58
+
59
+ console.log(`\n[Cleanup] Deleted ${ghosts.length} ghost conversations`);
60
+ }
61
+
62
+ if (require.main === module) {
63
+ cleanupGhostSessions()
64
+ .then(() => process.exit(0))
65
+ .catch(err => {
66
+ console.error('[Cleanup] Error:', err);
67
+ process.exit(1);
68
+ });
69
+ }
70
+
71
+ module.exports = { cleanupGhostSessions };
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { initDb } = require('../db');
4
+ const User = require('../models/User');
5
+
6
+ async function seed() {
7
+ console.log('🌱 Seeding users...');
8
+
9
+ try {
10
+ // Initialize database first
11
+ await initDb();
12
+
13
+ // Check if user dag already exists
14
+ const existing = User.findByUsername('dag');
15
+
16
+ if (existing) {
17
+ console.log('✅ User "dag" already exists');
18
+ process.exit(0);
19
+ }
20
+
21
+ // Create admin user dag
22
+ const user = User.create('dag', 'myj0b1s', 'admin');
23
+
24
+ console.log('✅ Created admin user:');
25
+ console.log(` Username: ${user.username}`);
26
+ console.log(` Role: ${user.role}`);
27
+ console.log(` ID: ${user.id}`);
28
+
29
+ process.exit(0);
30
+ } catch (error) {
31
+ console.error('❌ Error seeding users:', error.message);
32
+ console.error(error.stack);
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ seed();
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function testHistoryAccess() {
7
+ const historyPath = path.join(process.env.HOME || '', '.claude', 'history.jsonl');
8
+
9
+ console.log(`[Test] Checking access to: ${historyPath}`);
10
+ console.log(`[Test] HOME env: ${process.env.HOME}`);
11
+ console.log(`[Test] USER env: ${process.env.USER}`);
12
+
13
+ if (!fs.existsSync(historyPath)) {
14
+ console.error('[Test] ❌ history.jsonl NOT FOUND');
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log('[Test] ✅ history.jsonl exists');
19
+
20
+ try {
21
+ fs.accessSync(historyPath, fs.constants.R_OK);
22
+ console.log('[Test] ✅ Read permission OK');
23
+ } catch (err) {
24
+ console.error('[Test] ❌ No read permission:', err.message);
25
+ process.exit(1);
26
+ }
27
+
28
+ try {
29
+ const content = fs.readFileSync(historyPath, 'utf8');
30
+ const lines = content.split('\n').filter(l => l.trim()).slice(0, 10);
31
+
32
+ console.log(`[Test] ✅ Read ${lines.length} lines successfully`);
33
+
34
+ if (lines.length > 0) {
35
+ const firstEntry = JSON.parse(lines[0]);
36
+ console.log('[Test] Sample entry:', {
37
+ sessionId: firstEntry.sessionId ? firstEntry.sessionId.substring(0, 8) : null,
38
+ project: firstEntry.project,
39
+ timestamp: firstEntry.timestamp ? new Date(firstEntry.timestamp).toISOString() : null
40
+ });
41
+ }
42
+ } catch (err) {
43
+ console.error('[Test] ❌ Failed to read/parse:', err.message);
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log('\n[Test] ✅ All checks passed!');
48
+ }
49
+
50
+ testHistoryAccess();
@@ -0,0 +1,227 @@
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const https = require('https');
6
+ const http = require('http');
7
+ const { initDb, prepare } = require('./db');
8
+ const User = require('./models/User');
9
+ const WorkspaceManager = require('./services/workspace-manager');
10
+ const pkg = require('../../package.json');
11
+
12
+ // Import middleware
13
+ const { authMiddleware } = require('./middleware/auth');
14
+
15
+ // Import routes
16
+ const authRouter = require('./routes/auth');
17
+ const conversationsRouter = require('./routes/conversations');
18
+ const messagesRouter = require('./routes/messages');
19
+ const jobsRouter = require('./routes/jobs');
20
+ const chatRouter = require('./routes/chat');
21
+ const codexRouter = require('./routes/codex');
22
+ const geminiRouter = require('./routes/gemini');
23
+ const modelsRouter = require('./routes/models');
24
+ const workspaceRouter = require('./routes/workspace');
25
+ const workspacesRouter = require('./routes/workspaces');
26
+ const sessionsRouter = require('./routes/sessions');
27
+ const wakeLockRouter = require('./routes/wake-lock');
28
+ const uploadRouter = require('./routes/upload');
29
+
30
+ const app = express();
31
+ const PORT = process.env.PORT || 41800;
32
+
33
+ // Middleware
34
+ app.use(cors());
35
+ app.use(express.json());
36
+
37
+ // Request logging
38
+ app.use((req, res, next) => {
39
+ console.log(`${req.method} ${req.path} (URL: ${req.url}, original: ${req.originalUrl})`);
40
+ next();
41
+ });
42
+
43
+ // Health check (public)
44
+ app.get('/health', (req, res) => {
45
+ res.json({
46
+ status: 'ok',
47
+ service: 'nexuscli-backend',
48
+ version: pkg.version,
49
+ engines: ['claude', 'codex', 'gemini'],
50
+ port: PORT,
51
+ timestamp: new Date().toISOString()
52
+ });
53
+ });
54
+
55
+ // Serve frontend static files (production)
56
+ // Path: lib/server -> ../../frontend/dist
57
+ const frontendDist = path.join(__dirname, '../../frontend/dist');
58
+ app.use(express.static(frontendDist));
59
+
60
+ // Public routes
61
+ app.use('/api/v1/auth', authRouter);
62
+ app.use('/api/v1/models', modelsRouter);
63
+ app.use('/api/v1/workspace', workspaceRouter);
64
+ app.use('/api/v1', wakeLockRouter); // Wake lock endpoints (public for app visibility handling)
65
+ app.use('/api/v1/workspaces', authMiddleware, workspacesRouter);
66
+ app.use('/api/v1/sessions', authMiddleware, sessionsRouter);
67
+
68
+ // Protected routes (require authentication)
69
+ app.use('/api/v1/conversations', authMiddleware, conversationsRouter);
70
+ app.use('/api/v1/conversations', authMiddleware, messagesRouter);
71
+ app.use('/api/v1/jobs', authMiddleware, jobsRouter);
72
+ app.use('/api/v1/chat', authMiddleware, chatRouter);
73
+ app.use('/api/v1/codex', authMiddleware, codexRouter);
74
+ app.use('/api/v1/gemini', authMiddleware, geminiRouter);
75
+ app.use('/api/v1/upload', authMiddleware, uploadRouter); // File upload
76
+
77
+ // Root endpoint
78
+ app.get('/', (req, res) => {
79
+ res.json({
80
+ service: 'NexusCLI Backend',
81
+ version: pkg.version,
82
+ engines: ['claude', 'codex', 'gemini'],
83
+ endpoints: {
84
+ health: '/health',
85
+ models: '/api/v1/models',
86
+ chat: '/api/v1/chat (Claude)',
87
+ codex: '/api/v1/codex (OpenAI)',
88
+ gemini: '/api/v1/gemini (Google)',
89
+ conversations: '/api/v1/conversations',
90
+ jobs: '/api/v1/jobs'
91
+ }
92
+ });
93
+ });
94
+
95
+ // SPA catch-all route (serve index.html for all non-API routes)
96
+ app.get('*', (req, res) => {
97
+ res.sendFile(path.join(frontendDist, 'index.html'));
98
+ });
99
+
100
+ // 404 handler (for API routes)
101
+ app.use((req, res) => {
102
+ res.status(404).json({
103
+ error: 'Not Found',
104
+ path: req.path
105
+ });
106
+ });
107
+
108
+ // Error handler
109
+ app.use((err, req, res, next) => {
110
+ console.error('Error:', err);
111
+ res.status(500).json({
112
+ error: 'Internal Server Error',
113
+ message: err.message
114
+ });
115
+ });
116
+
117
+ // Start server
118
+ async function start() {
119
+ // Initialize database first
120
+ await initDb();
121
+
122
+ // ⚡ SYNC DATABASE WITH FILESYSTEM (CRITICAL!)
123
+ // Database is only a CACHE - real truth is on disk
124
+ // Discover and index ALL workspaces from .claude/projects/
125
+ console.log('[Startup] Discovering and syncing all workspaces...');
126
+ const workspaceManager = new WorkspaceManager();
127
+ try {
128
+ const workspaces = await workspaceManager.discoverWorkspaces();
129
+ console.log(`[Startup] Found ${workspaces.length} workspaces with sessions`);
130
+
131
+ // Mount each workspace to index its sessions
132
+ for (const ws of workspaces) {
133
+ try {
134
+ await workspaceManager.mountWorkspace(ws.workspace_path);
135
+ console.log(`[Startup] ✅ Mounted ${ws.workspace_path} (${ws.session_count} sessions)`);
136
+ } catch (error) {
137
+ console.error(`[Startup] ⚠️ Failed to mount ${ws.workspace_path}:`, error.message);
138
+ }
139
+ }
140
+
141
+ console.log('[Startup] ✅ All workspaces synced to database');
142
+ } catch (error) {
143
+ console.error('[Startup] ⚠️ Failed to discover workspaces:', error.message);
144
+ // Continue anyway - database will sync on first workspace mount
145
+ }
146
+
147
+ // Auto-seed dev user on demand (Termux/local) if no users exist
148
+ const autoSeed = process.env.NEXUSCLI_AUTO_SEED === '1';
149
+ if (autoSeed) {
150
+ const countStmt = prepare('SELECT COUNT(*) as count FROM users');
151
+ const { count } = countStmt.get();
152
+ if (count === 0) {
153
+ const username = process.env.NEXUSCLI_SEED_USER || 'tux';
154
+ const password = process.env.NEXUSCLI_SEED_PASS || 'tux';
155
+ const role = 'admin';
156
+
157
+ const user = User.create(username, password, role);
158
+ console.log(`[AutoSeed] Created default user ${user.username} (${user.role})`);
159
+ }
160
+ }
161
+
162
+ // Check for HTTPS certificates
163
+ const certDir = path.join(process.env.HOME || '', '.nexuscli', 'certs');
164
+ const certPath = path.join(certDir, 'cert.pem');
165
+ const keyPath = path.join(certDir, 'key.pem');
166
+ const useHttps = fs.existsSync(certPath) && fs.existsSync(keyPath);
167
+
168
+ let server;
169
+ let protocol = 'http';
170
+
171
+ if (useHttps) {
172
+ try {
173
+ const httpsOptions = {
174
+ key: fs.readFileSync(keyPath),
175
+ cert: fs.readFileSync(certPath)
176
+ };
177
+ server = https.createServer(httpsOptions, app);
178
+ protocol = 'https';
179
+ console.log('[Startup] HTTPS enabled - certificates found');
180
+ } catch (err) {
181
+ console.warn('[Startup] Failed to load certificates, falling back to HTTP:', err.message);
182
+ server = http.createServer(app);
183
+ }
184
+ } else {
185
+ server = http.createServer(app);
186
+ console.log('[Startup] HTTP mode - no certificates found');
187
+ console.log('[Startup] Run: ./scripts/setup-https.sh to enable HTTPS');
188
+ }
189
+
190
+ server.listen(PORT, () => {
191
+ console.log('');
192
+ console.log('╔══════════════════════════════════════════════╗');
193
+ console.log('║ 🚀 NexusCLI Backend ║');
194
+ console.log('╚══════════════════════════════════════════════╝');
195
+ console.log('');
196
+ console.log(`✅ Server running on ${protocol}://localhost:${PORT}`);
197
+ if (useHttps) {
198
+ console.log(`🔒 HTTPS enabled - secure connection`);
199
+ }
200
+ console.log('');
201
+ console.log('Endpoints:');
202
+ console.log(` GET /health (public)`);
203
+ console.log(` POST /api/v1/auth/login (public)`);
204
+ console.log(` GET /api/v1/auth/me (protected)`);
205
+ console.log(` GET /api/v1/conversations (protected)`);
206
+ console.log(` POST /api/v1/conversations (protected)`);
207
+ console.log(` POST /api/v1/jobs (protected)`);
208
+ console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
209
+ console.log('');
210
+ });
211
+ }
212
+
213
+ start().catch(err => {
214
+ console.error('Failed to start server:', err);
215
+ process.exit(1);
216
+ });
217
+
218
+ // Graceful shutdown
219
+ process.on('SIGTERM', () => {
220
+ console.log('SIGTERM signal received: closing HTTP server');
221
+ process.exit(0);
222
+ });
223
+
224
+ process.on('SIGINT', () => {
225
+ console.log('\nSIGINT signal received: closing HTTP server');
226
+ process.exit(0);
227
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Cache Service - In-memory caching with TTL
3
+ *
4
+ * Uses node-cache for lightweight, Redis-free caching.
5
+ * Perfect for Termux environments with limited resources.
6
+ */
7
+
8
+ const NodeCache = require('node-cache');
9
+
10
+ // Cache configuration
11
+ const cache = new NodeCache({
12
+ stdTTL: 30, // Default 30 seconds TTL
13
+ checkperiod: 60, // Check for expired keys every 60 seconds
14
+ useClones: false, // Don't clone objects (faster for read-heavy workloads)
15
+ deleteOnExpire: true
16
+ });
17
+
18
+ // Cache keys
19
+ const KEYS = {
20
+ CONVERSATIONS_GROUPED: 'conversations:grouped',
21
+ CONVERSATIONS_WORKSPACE: (ws) => `conversations:workspace:${ws}`,
22
+ CONVERSATION: (id) => `conversation:${id}`,
23
+ };
24
+
25
+ /**
26
+ * Get cached value or execute callback and cache result
27
+ * @param {string} key - Cache key
28
+ * @param {Function} callback - Async function to get data if not cached
29
+ * @param {number} ttl - TTL in seconds (default: 30)
30
+ * @returns {Promise<any>}
31
+ */
32
+ async function getOrSet(key, callback, ttl = 30) {
33
+ let value = cache.get(key);
34
+
35
+ if (value !== undefined) {
36
+ return value;
37
+ }
38
+
39
+ // Execute callback and cache result
40
+ value = await callback();
41
+ cache.set(key, value, ttl);
42
+
43
+ return value;
44
+ }
45
+
46
+ /**
47
+ * Invalidate specific key
48
+ * @param {string} key
49
+ */
50
+ function invalidate(key) {
51
+ cache.del(key);
52
+ }
53
+
54
+ /**
55
+ * Invalidate all conversation-related caches
56
+ * Called when conversation is created, updated, or deleted
57
+ */
58
+ function invalidateConversations() {
59
+ // Get all keys and delete conversation-related ones
60
+ const keys = cache.keys();
61
+ const toDelete = keys.filter(k => k.startsWith('conversations:') || k.startsWith('conversation:'));
62
+ cache.del(toDelete);
63
+ console.log(`[Cache] Invalidated ${toDelete.length} conversation cache entries`);
64
+ }
65
+
66
+ /**
67
+ * Get cache statistics
68
+ */
69
+ function getStats() {
70
+ return {
71
+ keys: cache.keys().length,
72
+ hits: cache.getStats().hits,
73
+ misses: cache.getStats().misses,
74
+ hitRate: cache.getStats().hits / (cache.getStats().hits + cache.getStats().misses) || 0
75
+ };
76
+ }
77
+
78
+ module.exports = {
79
+ cache,
80
+ KEYS,
81
+ getOrSet,
82
+ invalidate,
83
+ invalidateConversations,
84
+ getStats
85
+ };