@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,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
|
+
};
|