@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,285 @@
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { prepare, saveDb } = require('../db');
5
+ const CliLoader = require('../services/cli-loader');
6
+ const SummaryGenerator = require('../services/summary-generator');
7
+
8
+ const router = express.Router();
9
+ const cliLoader = new CliLoader();
10
+ const summaryGenerator = new SummaryGenerator();
11
+
12
+ // Engine-specific session directories
13
+ const SESSION_DIRS = {
14
+ claude: path.join(process.env.HOME || '', '.claude', 'projects'),
15
+ codex: path.join(process.env.HOME || '', '.codex', 'sessions'),
16
+ gemini: path.join(process.env.HOME || '', '.gemini', 'sessions'),
17
+ };
18
+
19
+ /**
20
+ * GET /api/v1/sessions/:id
21
+ * Return session metadata from DB (sessions table)
22
+ */
23
+ router.get('/:id', async (req, res) => {
24
+ try {
25
+ const sessionId = req.params.id;
26
+ const stmt = prepare('SELECT * FROM sessions WHERE id = ?');
27
+ const session = stmt.get(sessionId);
28
+
29
+ if (!session) {
30
+ return res.status(404).json({ error: 'Session not found' });
31
+ }
32
+
33
+ if (session.metadata) {
34
+ try {
35
+ session.metadata = JSON.parse(session.metadata);
36
+ } catch (_) {
37
+ session.metadata = {};
38
+ }
39
+ }
40
+
41
+ res.json({ session });
42
+ } catch (error) {
43
+ console.error('[Sessions] Metadata fetch error:', error);
44
+ res.status(500).json({ error: error.message });
45
+ }
46
+ });
47
+
48
+ /**
49
+ * GET /api/v1/sessions/:id/messages
50
+ * Lazy load messages from CLI files (not from DB)
51
+ */
52
+ router.get('/:id/messages', async (req, res) => {
53
+ try {
54
+ const sessionId = req.params.id;
55
+ const limit = Math.min(Number(req.query.limit) || 30, 200);
56
+ const before = req.query.before ? Number(req.query.before) : undefined;
57
+ const mode = req.query.mode === 'desc' ? 'desc' : 'asc';
58
+
59
+ // Fetch session to get workspace path & engine
60
+ const sessionStmt = prepare('SELECT * FROM sessions WHERE id = ?');
61
+ const session = sessionStmt.get(sessionId);
62
+
63
+ if (!session) {
64
+ return res.status(404).json({ error: 'Session not found' });
65
+ }
66
+
67
+ const { messages, pagination } = await cliLoader.loadMessagesFromCLI({
68
+ sessionId,
69
+ engine: session.engine || 'claude-code',
70
+ workspacePath: session.workspace_path,
71
+ limit,
72
+ before,
73
+ mode
74
+ });
75
+
76
+ res.json({
77
+ session: {
78
+ id: session.id,
79
+ workspace_path: session.workspace_path,
80
+ title: session.title,
81
+ engine: session.engine,
82
+ last_used_at: session.last_used_at,
83
+ created_at: session.created_at,
84
+ message_count: session.message_count
85
+ },
86
+ messages,
87
+ pagination
88
+ });
89
+ } catch (error) {
90
+ console.error('[Sessions] Messages fetch error:', error);
91
+ res.status(500).json({ error: error.message });
92
+ }
93
+ });
94
+
95
+ /**
96
+ * GET /api/v1/sessions/:id/summary
97
+ * Placeholder for Phase 4 (summary generation)
98
+ */
99
+ router.get('/:id/summary', async (req, res) => {
100
+ try {
101
+ const sessionId = req.params.id;
102
+ const stmt = prepare(`
103
+ SELECT ss.*
104
+ FROM session_summaries ss
105
+ WHERE ss.session_id = ?
106
+ `);
107
+ const summaryRow = stmt.get(sessionId);
108
+
109
+ if (summaryRow) {
110
+ ['key_decisions', 'tools_used', 'files_modified'].forEach(key => {
111
+ if (summaryRow[key]) {
112
+ try {
113
+ summaryRow[key] = JSON.parse(summaryRow[key]);
114
+ } catch (_) {
115
+ summaryRow[key] = [];
116
+ }
117
+ } else {
118
+ summaryRow[key] = [];
119
+ }
120
+ });
121
+ }
122
+
123
+ res.json({ summary: summaryRow || null });
124
+ } catch (error) {
125
+ console.error('[Sessions] Summary fetch error:', error);
126
+ res.status(500).json({ error: error.message });
127
+ }
128
+ });
129
+
130
+ /**
131
+ * POST /api/v1/sessions/:id/summarize
132
+ * Generate or refresh summary using Claude Haiku
133
+ */
134
+ router.post('/:id/summarize', async (req, res) => {
135
+ try {
136
+ const sessionId = req.params.id;
137
+ const limit = Math.min(Number(req.body?.limit) || 120, 300);
138
+ const before = req.body?.before ? Number(req.body.before) : undefined;
139
+
140
+ // Fetch session metadata
141
+ const sessionStmt = prepare('SELECT * FROM sessions WHERE id = ?');
142
+ const session = sessionStmt.get(sessionId);
143
+ if (!session) {
144
+ return res.status(404).json({ error: 'Session not found' });
145
+ }
146
+
147
+ // Load messages from CLI history
148
+ const { messages } = await cliLoader.loadMessagesFromCLI({
149
+ sessionId,
150
+ engine: session.engine || 'claude-code',
151
+ workspacePath: session.workspace_path,
152
+ limit,
153
+ before,
154
+ mode: 'asc'
155
+ });
156
+
157
+ // Pull existing summary (if any)
158
+ const existingStmt = prepare('SELECT * FROM session_summaries WHERE session_id = ?');
159
+ const existingSummary = existingStmt.get(sessionId);
160
+
161
+ const summary = await summaryGenerator.generateSummary({
162
+ sessionId,
163
+ messages,
164
+ existingSummary
165
+ });
166
+
167
+ const updatedAt = Date.now();
168
+
169
+ // Upsert summary table
170
+ const upsertStmt = prepare(`
171
+ INSERT INTO session_summaries (
172
+ session_id, summary_short, summary_long, key_decisions,
173
+ tools_used, files_modified, updated_at, version
174
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1)
175
+ ON CONFLICT(session_id) DO UPDATE SET
176
+ summary_short = excluded.summary_short,
177
+ summary_long = excluded.summary_long,
178
+ key_decisions = excluded.key_decisions,
179
+ tools_used = excluded.tools_used,
180
+ files_modified = excluded.files_modified,
181
+ updated_at = excluded.updated_at,
182
+ version = session_summaries.version + 1
183
+ `);
184
+
185
+ upsertStmt.run(
186
+ sessionId,
187
+ summary.summary_short || '',
188
+ summary.summary_long || '',
189
+ JSON.stringify(summary.key_decisions || []),
190
+ JSON.stringify(summary.tools_used || []),
191
+ JSON.stringify(summary.files_modified || []),
192
+ updatedAt
193
+ );
194
+
195
+ res.json({
196
+ summary: {
197
+ session_id: sessionId,
198
+ ...summary,
199
+ updated_at: updatedAt
200
+ }
201
+ });
202
+ } catch (error) {
203
+ console.error('[Sessions] Summarize error:', error);
204
+ res.status(500).json({ error: error.message });
205
+ }
206
+ });
207
+
208
+ /**
209
+ * DELETE /api/v1/sessions/:id
210
+ * Delete session from DB AND the original .jsonl file (SYNC DELETE)
211
+ */
212
+ router.delete('/:id', async (req, res) => {
213
+ try {
214
+ const sessionId = req.params.id;
215
+ console.log(`[Sessions] Deleting session: ${sessionId}`);
216
+
217
+ // Fetch session to get workspace path & engine for file deletion
218
+ const sessionStmt = prepare('SELECT * FROM sessions WHERE id = ?');
219
+ const session = sessionStmt.get(sessionId);
220
+
221
+ if (!session) {
222
+ return res.status(404).json({ error: 'Session not found' });
223
+ }
224
+
225
+ // Delete the original .jsonl file (SYNC DELETE)
226
+ let fileDeleted = false;
227
+ const sessionFile = getSessionFilePath(sessionId, session.engine, session.workspace_path);
228
+ if (sessionFile && fs.existsSync(sessionFile)) {
229
+ try {
230
+ fs.unlinkSync(sessionFile);
231
+ fileDeleted = true;
232
+ console.log(`[Sessions] Deleted session file: ${sessionFile}`);
233
+ } catch (e) {
234
+ console.warn(`[Sessions] Failed to delete file ${sessionFile}: ${e.message}`);
235
+ }
236
+ }
237
+
238
+ // Delete from session_summaries (cascade should handle this, but be explicit)
239
+ const deleteSummaryStmt = prepare('DELETE FROM session_summaries WHERE session_id = ?');
240
+ deleteSummaryStmt.run(sessionId);
241
+
242
+ // Delete from sessions table
243
+ const deleteStmt = prepare('DELETE FROM sessions WHERE id = ?');
244
+ deleteStmt.run(sessionId);
245
+ saveDb();
246
+
247
+ console.log(`[Sessions] Session ${sessionId} deleted (file: ${fileDeleted})`);
248
+ res.json({ success: true, fileDeleted });
249
+ } catch (error) {
250
+ console.error('[Sessions] Delete error:', error);
251
+ res.status(500).json({ error: error.message });
252
+ }
253
+ });
254
+
255
+ /**
256
+ * Helper: Convert workspace path to slug (matches Claude Code behavior)
257
+ */
258
+ function pathToSlug(workspacePath) {
259
+ if (!workspacePath) return '-default';
260
+ return workspacePath.replace(/[\/\.]/g, '-');
261
+ }
262
+
263
+ /**
264
+ * Helper: Get the filesystem path for a session file
265
+ */
266
+ function getSessionFilePath(sessionId, engine, workspacePath) {
267
+ const normalizedEngine = engine?.toLowerCase().includes('claude') ? 'claude'
268
+ : engine?.toLowerCase().includes('codex') ? 'codex'
269
+ : engine?.toLowerCase().includes('gemini') ? 'gemini'
270
+ : 'claude';
271
+
272
+ switch (normalizedEngine) {
273
+ case 'claude':
274
+ const slug = pathToSlug(workspacePath);
275
+ return path.join(SESSION_DIRS.claude, slug, `${sessionId}.jsonl`);
276
+ case 'codex':
277
+ return path.join(SESSION_DIRS.codex, `${sessionId}.jsonl`);
278
+ case 'gemini':
279
+ return path.join(SESSION_DIRS.gemini, `${sessionId}.jsonl`);
280
+ default:
281
+ return null;
282
+ }
283
+ }
284
+
285
+ module.exports = router;
@@ -0,0 +1,134 @@
1
+ const express = require('express');
2
+ const multer = require('multer');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ const router = express.Router();
7
+
8
+ // Termux-only: attachments in ~/.nexuscli/attachments
9
+ const ATTACHMENTS_DIR = path.join(process.env.HOME, '.nexuscli', 'attachments');
10
+
11
+ // Ensure directory exists
12
+ if (!fs.existsSync(ATTACHMENTS_DIR)) {
13
+ fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true });
14
+ console.log(`[Upload] Created attachments directory: ${ATTACHMENTS_DIR}`);
15
+ }
16
+
17
+ // Multer storage config
18
+ const storage = multer.diskStorage({
19
+ destination: (req, file, cb) => {
20
+ cb(null, ATTACHMENTS_DIR);
21
+ },
22
+ filename: (req, file, cb) => {
23
+ // Unique filename: timestamp_originalname
24
+ const timestamp = Date.now();
25
+ const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
26
+ cb(null, `${timestamp}_${safeName}`);
27
+ }
28
+ });
29
+
30
+ // File filter - allow images and documents
31
+ const fileFilter = (req, file, cb) => {
32
+ const allowedMimes = [
33
+ // Images
34
+ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
35
+ // Documents
36
+ 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
37
+ 'application/json', 'application/xml', 'application/pdf',
38
+ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
39
+ 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40
+ // Code files (often sent as octet-stream)
41
+ 'application/octet-stream', 'text/x-python', 'text/javascript', 'application/javascript'
42
+ ];
43
+
44
+ // Also allow by extension for code files
45
+ const allowedExts = ['.js', '.ts', '.py', '.java', '.c', '.cpp', '.h', '.rb', '.php', '.go', '.rs', '.sh', '.bash', '.zsh', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sql', '.log'];
46
+ const ext = path.extname(file.originalname).toLowerCase();
47
+
48
+ if (allowedMimes.includes(file.mimetype) || allowedExts.includes(ext)) {
49
+ cb(null, true);
50
+ } else {
51
+ cb(new Error(`File type not allowed: ${file.mimetype} (${ext})`), false);
52
+ }
53
+ };
54
+
55
+ // Multer config - 50MB limit
56
+ const upload = multer({
57
+ storage,
58
+ fileFilter,
59
+ limits: { fileSize: 50 * 1024 * 1024 }
60
+ });
61
+
62
+ // POST /api/v1/upload - Single file upload
63
+ router.post('/', upload.single('file'), (req, res) => {
64
+ try {
65
+ if (!req.file) {
66
+ return res.status(400).json({ error: 'No file uploaded' });
67
+ }
68
+
69
+ const filePath = req.file.path;
70
+ const fileName = req.file.filename;
71
+ const originalName = req.file.originalname;
72
+ const mimeType = req.file.mimetype;
73
+ const size = req.file.size;
74
+
75
+ console.log(`[Upload] File saved: ${filePath} (${size} bytes, ${mimeType})`);
76
+
77
+ res.json({
78
+ success: true,
79
+ file: {
80
+ path: filePath,
81
+ name: fileName,
82
+ originalName,
83
+ mimeType,
84
+ size
85
+ }
86
+ });
87
+ } catch (error) {
88
+ console.error('[Upload] Error:', error);
89
+ res.status(500).json({ error: error.message });
90
+ }
91
+ });
92
+
93
+ // POST /api/v1/upload/multiple - Multiple files upload
94
+ router.post('/multiple', upload.array('files', 10), (req, res) => {
95
+ try {
96
+ if (!req.files || req.files.length === 0) {
97
+ return res.status(400).json({ error: 'No files uploaded' });
98
+ }
99
+
100
+ const files = req.files.map(f => ({
101
+ path: f.path,
102
+ name: f.filename,
103
+ originalName: f.originalname,
104
+ mimeType: f.mimetype,
105
+ size: f.size
106
+ }));
107
+
108
+ console.log(`[Upload] ${files.length} files saved`);
109
+
110
+ res.json({
111
+ success: true,
112
+ files
113
+ });
114
+ } catch (error) {
115
+ console.error('[Upload] Error:', error);
116
+ res.status(500).json({ error: error.message });
117
+ }
118
+ });
119
+
120
+ // Error handler for multer
121
+ router.use((error, req, res, next) => {
122
+ if (error instanceof multer.MulterError) {
123
+ if (error.code === 'LIMIT_FILE_SIZE') {
124
+ return res.status(400).json({ error: 'File too large (max 50MB)' });
125
+ }
126
+ return res.status(400).json({ error: error.message });
127
+ }
128
+ if (error) {
129
+ return res.status(400).json({ error: error.message });
130
+ }
131
+ next();
132
+ });
133
+
134
+ module.exports = router;
@@ -0,0 +1,95 @@
1
+ const express = require('express');
2
+ const { execSync } = require('child_process');
3
+ const router = express.Router();
4
+
5
+ // State to track wake lock status
6
+ let wakeLockAcquired = false;
7
+
8
+ /**
9
+ * POST /api/v1/wake-lock
10
+ * Acquire wake lock (prevent Android from killing process)
11
+ * On non-Termux systems, this is a no-op but returns success to avoid errors
12
+ */
13
+ router.post('/wake-lock', (req, res) => {
14
+ try {
15
+ console.log('[WakeLock] Acquiring wake lock...');
16
+
17
+ const isTermux = process.env.PREFIX?.includes('com.termux');
18
+
19
+ if (isTermux) {
20
+ // Only execute on Termux/Android
21
+ try {
22
+ execSync('termux-wake-lock', { stdio: 'ignore' });
23
+ } catch (e) {
24
+ console.warn('[WakeLock] termux-wake-lock command failed (may not be available)');
25
+ }
26
+ } else {
27
+ console.log('[WakeLock] Not on Termux - wake-lock is a no-op');
28
+ }
29
+
30
+ wakeLockAcquired = true;
31
+
32
+ console.log('[WakeLock] ✅ Wake lock acquired (or skipped on non-Termux)');
33
+
34
+ res.json({
35
+ status: 'ok',
36
+ message: 'Wake lock acquired',
37
+ acquired: wakeLockAcquired,
38
+ platform: isTermux ? 'termux' : 'linux'
39
+ });
40
+ } catch (err) {
41
+ console.error('[WakeLock] ❌ Unexpected error:', err.message);
42
+
43
+ // Still return success to avoid breaking the app
44
+ res.status(200).json({
45
+ status: 'ok',
46
+ message: 'Wake lock handler executed',
47
+ acquired: true,
48
+ error: err.message // Log the error but don't fail the request
49
+ });
50
+ }
51
+ });
52
+
53
+ /**
54
+ * DELETE /api/v1/wake-lock
55
+ * Release wake lock
56
+ */
57
+ router.delete('/wake-lock', (req, res) => {
58
+ try {
59
+ console.log('[WakeLock] Releasing wake lock...');
60
+
61
+ // Execute termux-wake-unlock
62
+ execSync('termux-wake-unlock', { stdio: 'ignore' });
63
+
64
+ wakeLockAcquired = false;
65
+
66
+ console.log('[WakeLock] ✅ Wake lock released');
67
+
68
+ res.json({
69
+ status: 'ok',
70
+ message: 'Wake lock released',
71
+ acquired: wakeLockAcquired
72
+ });
73
+ } catch (err) {
74
+ console.error('[WakeLock] ❌ Failed to release wake lock:', err.message);
75
+
76
+ res.status(500).json({
77
+ status: 'error',
78
+ message: 'Failed to release wake lock',
79
+ error: err.message
80
+ });
81
+ }
82
+ });
83
+
84
+ /**
85
+ * GET /api/v1/wake-lock
86
+ * Get wake lock status
87
+ */
88
+ router.get('/wake-lock', (req, res) => {
89
+ res.json({
90
+ status: 'ok',
91
+ acquired: wakeLockAcquired
92
+ });
93
+ });
94
+
95
+ module.exports = router;
@@ -0,0 +1,80 @@
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+
6
+ const router = express.Router();
7
+
8
+ // Config file path (same as lib/config/manager.js)
9
+ const CONFIG_DIR = path.join(os.homedir(), '.nexuscli');
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
+
12
+ /**
13
+ * Load config from file
14
+ */
15
+ function loadConfig() {
16
+ try {
17
+ if (fs.existsSync(CONFIG_FILE)) {
18
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
19
+ }
20
+ } catch (err) {
21
+ console.warn('[Workspace] Failed to load config:', err.message);
22
+ }
23
+ return null;
24
+ }
25
+
26
+ /**
27
+ * Expand ~ to home directory
28
+ */
29
+ function expandPath(p) {
30
+ if (!p) return p;
31
+ if (p.startsWith('~/')) {
32
+ return path.join(os.homedir(), p.slice(2));
33
+ }
34
+ return p;
35
+ }
36
+
37
+ /**
38
+ * GET /api/v1/workspace
39
+ * Returns configured default workspace and environment info
40
+ */
41
+ router.get('/', (req, res) => {
42
+ try {
43
+ const home = os.homedir();
44
+ const config = loadConfig();
45
+
46
+ // Get default workspace from config, fallback to ~/Dev or HOME
47
+ let defaultWorkspace = home;
48
+ if (config?.workspaces?.default) {
49
+ defaultWorkspace = expandPath(config.workspaces.default);
50
+ }
51
+
52
+ // CREATE the directory if it doesn't exist (user chose this path during init)
53
+ if (!fs.existsSync(defaultWorkspace)) {
54
+ try {
55
+ fs.mkdirSync(defaultWorkspace, { recursive: true });
56
+ console.log(`[Workspace] Created default workspace: ${defaultWorkspace}`);
57
+ } catch (err) {
58
+ console.warn(`[Workspace] Failed to create ${defaultWorkspace}: ${err.message}, using HOME`);
59
+ defaultWorkspace = home;
60
+ }
61
+ }
62
+
63
+ // Get configured workspace list
64
+ const configuredPaths = (config?.workspaces?.paths || []).map(expandPath);
65
+
66
+ res.json({
67
+ current: defaultWorkspace,
68
+ default: defaultWorkspace,
69
+ configured: configuredPaths,
70
+ home: home,
71
+ platform: process.platform,
72
+ user: process.env.USER || process.env.USERNAME || 'unknown'
73
+ });
74
+ } catch (error) {
75
+ console.error('[Workspace] Error fetching workspace info:', error);
76
+ res.status(500).json({ error: 'Failed to fetch workspace info' });
77
+ }
78
+ });
79
+
80
+ module.exports = router;