@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,317 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
const CliWrapper = require('../lib/cli-wrapper');
|
|
4
|
+
const db = require('../db');
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
// CLI wrapper instance
|
|
9
|
+
const cliWrapper = new CliWrapper({
|
|
10
|
+
workspaceDir: process.cwd()
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/v1/jobs
|
|
15
|
+
* Create and execute job
|
|
16
|
+
*/
|
|
17
|
+
router.post('/', async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const {
|
|
20
|
+
conversationId,
|
|
21
|
+
messageId,
|
|
22
|
+
nodeId = 'localhost',
|
|
23
|
+
tool = 'bash',
|
|
24
|
+
command,
|
|
25
|
+
workingDir,
|
|
26
|
+
timeout = 30000
|
|
27
|
+
} = req.body;
|
|
28
|
+
|
|
29
|
+
// Validate input
|
|
30
|
+
if (!command) {
|
|
31
|
+
return res.status(400).json({ error: 'Command is required' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create job ID
|
|
35
|
+
const jobId = uuidv4();
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
|
|
38
|
+
// Insert job into database
|
|
39
|
+
const stmt = db.prepare(`
|
|
40
|
+
INSERT INTO jobs (
|
|
41
|
+
id, conversation_id, message_id, node_id, tool, command,
|
|
42
|
+
status, created_at
|
|
43
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
stmt.run(
|
|
47
|
+
jobId,
|
|
48
|
+
conversationId || null,
|
|
49
|
+
messageId || null,
|
|
50
|
+
nodeId,
|
|
51
|
+
tool,
|
|
52
|
+
command,
|
|
53
|
+
'queued',
|
|
54
|
+
now
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log(`[Jobs] Created job ${jobId}: ${tool} - ${command.substring(0, 50)}`);
|
|
58
|
+
|
|
59
|
+
// Return job info with stream endpoint
|
|
60
|
+
res.status(201).json({
|
|
61
|
+
jobId,
|
|
62
|
+
nodeId,
|
|
63
|
+
tool,
|
|
64
|
+
command,
|
|
65
|
+
status: 'queued',
|
|
66
|
+
createdAt: now,
|
|
67
|
+
streamEndpoint: `/api/v1/jobs/${jobId}/stream`
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Execute job asynchronously (don't block response)
|
|
71
|
+
setImmediate(() => {
|
|
72
|
+
executeJob(jobId, { tool, command, workingDir, timeout });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('[Jobs] Create error:', error);
|
|
77
|
+
res.status(500).json({ error: 'Failed to create job' });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* GET /api/v1/jobs/:jobId/stream
|
|
83
|
+
* SSE stream for job execution
|
|
84
|
+
*/
|
|
85
|
+
router.get('/:jobId/stream', (req, res) => {
|
|
86
|
+
const { jobId } = req.params;
|
|
87
|
+
|
|
88
|
+
console.log(`[Jobs] SSE stream opened for job ${jobId}`);
|
|
89
|
+
|
|
90
|
+
// Set SSE headers (LibreChat/NexusChat pattern)
|
|
91
|
+
res.writeHead(200, {
|
|
92
|
+
'Content-Type': 'text/event-stream',
|
|
93
|
+
'Cache-Control': 'no-cache',
|
|
94
|
+
'Connection': 'keep-alive',
|
|
95
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Get job from database
|
|
99
|
+
const stmt = db.prepare('SELECT * FROM jobs WHERE id = ?');
|
|
100
|
+
const job = stmt.get(jobId);
|
|
101
|
+
|
|
102
|
+
if (!job) {
|
|
103
|
+
res.write(`data: ${JSON.stringify({
|
|
104
|
+
type: 'error',
|
|
105
|
+
error: 'Job not found'
|
|
106
|
+
})}\n\n`);
|
|
107
|
+
res.end();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If job already completed, send final result
|
|
112
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
113
|
+
res.write(`data: ${JSON.stringify({
|
|
114
|
+
type: 'response_done',
|
|
115
|
+
exitCode: job.exit_code,
|
|
116
|
+
duration: job.duration
|
|
117
|
+
})}\n\n`);
|
|
118
|
+
|
|
119
|
+
res.write(`data: ${JSON.stringify({
|
|
120
|
+
type: 'done'
|
|
121
|
+
})}\n\n`);
|
|
122
|
+
|
|
123
|
+
res.end();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Store SSE connection in a Map (keyed by jobId)
|
|
128
|
+
if (!global.sseConnections) {
|
|
129
|
+
global.sseConnections = new Map();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
global.sseConnections.set(jobId, res);
|
|
133
|
+
|
|
134
|
+
// Emit initial queued status
|
|
135
|
+
res.write(`data: ${JSON.stringify({
|
|
136
|
+
type: 'status',
|
|
137
|
+
category: 'queued',
|
|
138
|
+
nodeId: job.node_id,
|
|
139
|
+
message: `Queued on ${job.node_id}...`,
|
|
140
|
+
icon: '⏱️',
|
|
141
|
+
timestamp: new Date().toISOString()
|
|
142
|
+
})}\n\n`);
|
|
143
|
+
|
|
144
|
+
// Cleanup on client disconnect
|
|
145
|
+
req.on('close', () => {
|
|
146
|
+
console.log(`[Jobs] SSE stream closed for job ${jobId}`);
|
|
147
|
+
global.sseConnections.delete(jobId);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* GET /api/v1/jobs/:jobId
|
|
153
|
+
* Get job result
|
|
154
|
+
*/
|
|
155
|
+
router.get('/:jobId', (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const stmt = db.prepare('SELECT * FROM jobs WHERE id = ?');
|
|
158
|
+
const job = stmt.get(req.params.jobId);
|
|
159
|
+
|
|
160
|
+
if (!job) {
|
|
161
|
+
return res.status(404).json({ error: 'Job not found' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
res.json(job);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[Jobs] Get error:', error);
|
|
167
|
+
res.status(500).json({ error: 'Failed to get job' });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* DELETE /api/v1/jobs/:jobId
|
|
173
|
+
* Cancel running job
|
|
174
|
+
*/
|
|
175
|
+
router.delete('/:jobId', (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const { jobId } = req.params;
|
|
178
|
+
|
|
179
|
+
// Try to kill running process
|
|
180
|
+
const killed = cliWrapper.kill(jobId);
|
|
181
|
+
|
|
182
|
+
// Update job status
|
|
183
|
+
const stmt = db.prepare(`
|
|
184
|
+
UPDATE jobs
|
|
185
|
+
SET status = ?, completed_at = ?
|
|
186
|
+
WHERE id = ?
|
|
187
|
+
`);
|
|
188
|
+
|
|
189
|
+
stmt.run('cancelled', Date.now(), jobId);
|
|
190
|
+
|
|
191
|
+
// Emit cancelled event to SSE stream
|
|
192
|
+
emitSSE(jobId, {
|
|
193
|
+
type: 'status',
|
|
194
|
+
category: 'cancelled',
|
|
195
|
+
message: 'Job cancelled',
|
|
196
|
+
icon: '⛔'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
emitSSE(jobId, { type: 'done' });
|
|
200
|
+
|
|
201
|
+
res.json({ success: true, killed });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('[Jobs] Cancel error:', error);
|
|
204
|
+
res.status(500).json({ error: 'Failed to cancel job' });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Execute job asynchronously
|
|
210
|
+
*/
|
|
211
|
+
async function executeJob(jobId, { tool, command, workingDir, timeout }) {
|
|
212
|
+
console.log(`[Jobs] Executing job ${jobId}`);
|
|
213
|
+
|
|
214
|
+
// Update status to executing
|
|
215
|
+
let stmt = db.prepare(`
|
|
216
|
+
UPDATE jobs
|
|
217
|
+
SET status = ?, started_at = ?
|
|
218
|
+
WHERE id = ?
|
|
219
|
+
`);
|
|
220
|
+
stmt.run('executing', Date.now(), jobId);
|
|
221
|
+
|
|
222
|
+
// Emit executing status
|
|
223
|
+
emitSSE(jobId, {
|
|
224
|
+
type: 'status',
|
|
225
|
+
category: 'executing',
|
|
226
|
+
message: `Executing on localhost...`,
|
|
227
|
+
icon: '▶',
|
|
228
|
+
timestamp: new Date().toISOString()
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Execute command via CLI wrapper
|
|
233
|
+
const result = await cliWrapper.execute({
|
|
234
|
+
jobId,
|
|
235
|
+
tool,
|
|
236
|
+
command,
|
|
237
|
+
workingDir,
|
|
238
|
+
timeout,
|
|
239
|
+
onStatus: (event) => {
|
|
240
|
+
// Forward events to SSE stream
|
|
241
|
+
emitSSE(jobId, event);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Update job with result
|
|
246
|
+
stmt = db.prepare(`
|
|
247
|
+
UPDATE jobs
|
|
248
|
+
SET status = ?, exit_code = ?, stdout = ?, stderr = ?,
|
|
249
|
+
duration = ?, completed_at = ?
|
|
250
|
+
WHERE id = ?
|
|
251
|
+
`);
|
|
252
|
+
|
|
253
|
+
const status = result.exitCode === 0 ? 'completed' : 'failed';
|
|
254
|
+
|
|
255
|
+
stmt.run(
|
|
256
|
+
status,
|
|
257
|
+
result.exitCode,
|
|
258
|
+
result.stdout,
|
|
259
|
+
result.stderr,
|
|
260
|
+
result.duration,
|
|
261
|
+
Date.now(),
|
|
262
|
+
jobId
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
console.log(`[Jobs] Job ${jobId} ${status} (exit: ${result.exitCode}, ${result.duration}ms)`);
|
|
266
|
+
|
|
267
|
+
// Emit done events
|
|
268
|
+
emitSSE(jobId, {
|
|
269
|
+
type: 'response_done',
|
|
270
|
+
exitCode: result.exitCode,
|
|
271
|
+
duration: result.duration
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
emitSSE(jobId, {
|
|
275
|
+
type: 'done'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error(`[Jobs] Job ${jobId} error:`, error);
|
|
280
|
+
|
|
281
|
+
// Update job as failed
|
|
282
|
+
stmt = db.prepare(`
|
|
283
|
+
UPDATE jobs
|
|
284
|
+
SET status = ?, stderr = ?, completed_at = ?
|
|
285
|
+
WHERE id = ?
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
stmt.run('failed', error.message, Date.now(), jobId);
|
|
289
|
+
|
|
290
|
+
// Emit error event
|
|
291
|
+
emitSSE(jobId, {
|
|
292
|
+
type: 'error',
|
|
293
|
+
error: error.message
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
emitSSE(jobId, { type: 'done' });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Emit SSE event to connected clients
|
|
302
|
+
*/
|
|
303
|
+
function emitSSE(jobId, event) {
|
|
304
|
+
if (!global.sseConnections) return;
|
|
305
|
+
|
|
306
|
+
const res = global.sseConnections.get(jobId);
|
|
307
|
+
if (res) {
|
|
308
|
+
try {
|
|
309
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(`[Jobs] SSE write error for job ${jobId}:`, error);
|
|
312
|
+
global.sseConnections.delete(jobId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = router;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const Message = require('../models/Message');
|
|
3
|
+
const Conversation = require('../models/Conversation');
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST /api/v1/conversations/:conversationId/messages
|
|
9
|
+
* Add message to conversation
|
|
10
|
+
*/
|
|
11
|
+
router.post('/:conversationId/messages', (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const { conversationId } = req.params;
|
|
14
|
+
const { role, content, metadata } = req.body;
|
|
15
|
+
|
|
16
|
+
// Validate conversation exists
|
|
17
|
+
const conversation = Conversation.getById(conversationId);
|
|
18
|
+
if (!conversation) {
|
|
19
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validate input
|
|
23
|
+
if (!role || !content) {
|
|
24
|
+
return res.status(400).json({ error: 'Role and content are required' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!['user', 'assistant', 'system'].includes(role)) {
|
|
28
|
+
return res.status(400).json({ error: 'Invalid role' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create message
|
|
32
|
+
const message = Message.create(conversationId, role, content, metadata);
|
|
33
|
+
|
|
34
|
+
res.status(201).json(message);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[Messages] Create error:', error);
|
|
37
|
+
res.status(500).json({ error: 'Failed to create message' });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* GET /api/v1/conversations/:conversationId/messages
|
|
43
|
+
* Get messages for conversation
|
|
44
|
+
*/
|
|
45
|
+
router.get('/:conversationId/messages', (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const { conversationId } = req.params;
|
|
48
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
49
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
50
|
+
|
|
51
|
+
const messages = Message.getByConversation(conversationId, limit, offset);
|
|
52
|
+
|
|
53
|
+
res.json({ messages });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('[Messages] List error:', error);
|
|
56
|
+
res.status(500).json({ error: 'Failed to list messages' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
module.exports = router;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/v1/models
|
|
6
|
+
* Returns list of available CLI tools and their models
|
|
7
|
+
*
|
|
8
|
+
* TRI CLI v0.4.0:
|
|
9
|
+
* - Claude: Opus 4.5, Sonnet 4.5, Haiku 4.5
|
|
10
|
+
* - Codex: GPT-5.1 variants
|
|
11
|
+
* - Gemini: Gemini 3 Pro Preview
|
|
12
|
+
*/
|
|
13
|
+
router.get('/', (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const cliTools = {
|
|
16
|
+
// ============================================================
|
|
17
|
+
// CLAUDE - Anthropic Claude Code CLI
|
|
18
|
+
// ============================================================
|
|
19
|
+
'claude': {
|
|
20
|
+
name: 'Claude Code',
|
|
21
|
+
icon: 'Terminal',
|
|
22
|
+
enabled: true,
|
|
23
|
+
endpoint: '/api/v1/chat',
|
|
24
|
+
thinkModes: ['think', 'no-think'],
|
|
25
|
+
defaultThinkMode: 'think',
|
|
26
|
+
models: [
|
|
27
|
+
// === Claude Opus 4.5 (Most Intelligent) ===
|
|
28
|
+
{
|
|
29
|
+
id: 'claude-opus-4-5-20251101',
|
|
30
|
+
name: 'claude-opus-4-5-20251101',
|
|
31
|
+
label: 'Opus 4.5',
|
|
32
|
+
description: '🧠 Most Intelligent',
|
|
33
|
+
category: 'claude'
|
|
34
|
+
},
|
|
35
|
+
// === Claude Sonnet 4.5 (Best Balance) ===
|
|
36
|
+
{
|
|
37
|
+
id: 'claude-sonnet-4-5-20250929',
|
|
38
|
+
name: 'claude-sonnet-4-5-20250929',
|
|
39
|
+
label: 'Sonnet 4.5',
|
|
40
|
+
description: '🧠 Extended Thinking (default)',
|
|
41
|
+
category: 'claude',
|
|
42
|
+
default: true
|
|
43
|
+
},
|
|
44
|
+
// === Claude Haiku 4.5 (Fastest) ===
|
|
45
|
+
{
|
|
46
|
+
id: 'claude-haiku-4-5-20251001',
|
|
47
|
+
name: 'claude-haiku-4-5-20251001',
|
|
48
|
+
label: 'Haiku 4.5',
|
|
49
|
+
description: '⚡ Fast & Efficient',
|
|
50
|
+
category: 'claude'
|
|
51
|
+
},
|
|
52
|
+
// === DeepSeek (Alternative Models) ===
|
|
53
|
+
{
|
|
54
|
+
id: 'deepseek-reasoner',
|
|
55
|
+
name: 'deepseek-reasoner',
|
|
56
|
+
label: 'DeepSeek Reasoner',
|
|
57
|
+
description: '🧠 Deep Reasoning',
|
|
58
|
+
category: 'claude'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'deepseek-chat',
|
|
62
|
+
name: 'deepseek-chat',
|
|
63
|
+
label: 'DeepSeek Chat',
|
|
64
|
+
description: '💬 Fast Chat',
|
|
65
|
+
category: 'claude'
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// ============================================================
|
|
71
|
+
// CODEX - OpenAI Codex CLI
|
|
72
|
+
// ============================================================
|
|
73
|
+
'codex': {
|
|
74
|
+
name: 'Codex',
|
|
75
|
+
icon: 'Code2',
|
|
76
|
+
enabled: true,
|
|
77
|
+
endpoint: '/api/v1/codex',
|
|
78
|
+
models: [
|
|
79
|
+
{
|
|
80
|
+
id: 'gpt-5.1-codex-max',
|
|
81
|
+
name: 'gpt-5.1-codex-max',
|
|
82
|
+
label: 'GPT-5.1 Codex Max',
|
|
83
|
+
description: '💎 Extra High reasoning (best)',
|
|
84
|
+
category: 'codex',
|
|
85
|
+
reasoningEfforts: ['low', 'medium', 'high', 'xhigh'],
|
|
86
|
+
defaultReasoning: 'xhigh',
|
|
87
|
+
default: true
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'gpt-5.1-codex',
|
|
91
|
+
name: 'gpt-5.1-codex',
|
|
92
|
+
label: 'GPT-5.1 Codex',
|
|
93
|
+
description: '🧠 High reasoning',
|
|
94
|
+
category: 'codex',
|
|
95
|
+
reasoningEfforts: ['low', 'medium', 'high'],
|
|
96
|
+
defaultReasoning: 'high'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'gpt-5.1-codex-mini',
|
|
100
|
+
name: 'gpt-5.1-codex-mini',
|
|
101
|
+
label: 'GPT-5.1 Codex Mini',
|
|
102
|
+
description: '⚡ Compact & Fast',
|
|
103
|
+
category: 'codex',
|
|
104
|
+
reasoningEfforts: ['medium', 'high'],
|
|
105
|
+
defaultReasoning: 'high'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'gpt-5.1',
|
|
109
|
+
name: 'gpt-5.1',
|
|
110
|
+
label: 'GPT-5.1',
|
|
111
|
+
description: '🧠 General Purpose',
|
|
112
|
+
category: 'codex',
|
|
113
|
+
reasoningEfforts: ['low', 'medium', 'high'],
|
|
114
|
+
defaultReasoning: 'high'
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// GEMINI - Google Gemini CLI
|
|
121
|
+
// ============================================================
|
|
122
|
+
'gemini': {
|
|
123
|
+
name: 'Gemini',
|
|
124
|
+
icon: 'Sparkles',
|
|
125
|
+
enabled: true,
|
|
126
|
+
endpoint: '/api/v1/gemini',
|
|
127
|
+
models: [
|
|
128
|
+
{
|
|
129
|
+
id: 'gemini-3-pro-preview',
|
|
130
|
+
name: 'gemini-3-pro-preview',
|
|
131
|
+
label: 'Gemini 3 Pro',
|
|
132
|
+
description: '🚀 Latest Preview',
|
|
133
|
+
category: 'gemini',
|
|
134
|
+
default: true
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
res.json(cliTools);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('[Models] Error fetching models:', error);
|
|
143
|
+
res.status(500).json({ error: 'Failed to fetch models' });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* GET /api/v1/models/:engine
|
|
149
|
+
* Returns models for a specific engine
|
|
150
|
+
*/
|
|
151
|
+
router.get('/:engine', (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const { engine } = req.params;
|
|
154
|
+
|
|
155
|
+
// Normalize engine name
|
|
156
|
+
let normalizedEngine = engine.toLowerCase();
|
|
157
|
+
if (normalizedEngine.includes('claude')) normalizedEngine = 'claude';
|
|
158
|
+
if (normalizedEngine.includes('codex') || normalizedEngine.includes('openai')) normalizedEngine = 'codex';
|
|
159
|
+
if (normalizedEngine.includes('gemini') || normalizedEngine.includes('google')) normalizedEngine = 'gemini';
|
|
160
|
+
|
|
161
|
+
const cliTools = {
|
|
162
|
+
'claude': {
|
|
163
|
+
name: 'Claude Code',
|
|
164
|
+
models: [
|
|
165
|
+
{ id: 'claude-opus-4-5-20251101', label: 'Opus 4.5', description: '🧠 Most Intelligent' },
|
|
166
|
+
{ id: 'claude-sonnet-4-5-20250929', label: 'Sonnet 4.5', description: '🧠 Extended Thinking', default: true },
|
|
167
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', description: '⚡ Fast & Efficient' },
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
'codex': {
|
|
171
|
+
name: 'Codex',
|
|
172
|
+
models: [
|
|
173
|
+
{ id: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max', description: '💎 Best Quality', default: true },
|
|
174
|
+
{ id: 'gpt-5.1-codex', label: 'GPT-5.1 Codex', description: '🧠 High reasoning' },
|
|
175
|
+
{ id: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini', description: '⚡ Fast' },
|
|
176
|
+
{ id: 'gpt-5.1', label: 'GPT-5.1', description: '🧠 General Purpose' },
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
'gemini': {
|
|
180
|
+
name: 'Gemini',
|
|
181
|
+
models: [
|
|
182
|
+
{ id: 'gemini-3-pro-preview', label: 'Gemini 3 Pro', description: '🚀 Latest', default: true },
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (!cliTools[normalizedEngine]) {
|
|
188
|
+
return res.status(404).json({ error: `Engine not found: ${engine}` });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
res.json(cliTools[normalizedEngine]);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[Models] Error fetching engine models:', error);
|
|
194
|
+
res.status(500).json({ error: 'Failed to fetch models' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
module.exports = router;
|