@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- 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;
|