glad-web 1.0.28 → 1.0.30
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/lib/claude/cli-usage.js +95 -0
- package/lib/claude/structured-session.js +223 -51
- package/lib/claude/transcript-repository.js +216 -0
- package/lib/codex/image-store.js +175 -0
- package/lib/codex/structured-session.js +18 -4
- package/lib/commands/web.js +52 -234
- package/lib/server/routes/providers.js +103 -0
- package/lib/server/routes/schedules.js +54 -0
- package/lib/server/routes/workspace.js +77 -0
- package/lib/session/session-manager.js +111 -340
- package/lib/web/claude.js +1074 -0
- package/lib/web/codex.js +475 -0
- package/lib/web/composer.js +230 -0
- package/lib/web/core.js +327 -0
- package/lib/web/git.js +533 -0
- package/lib/web/index.html +45 -3610
- package/lib/web/schedules.js +245 -0
- package/lib/web/session.js +351 -0
- package/lib/web/shell.js +56 -0
- package/lib/web/styles.css +377 -0
- package/lib/web/terminal-scroll.js +81 -0
- package/lib/web/timed-inputs.js +223 -0
- package/lib/workspace/service.js +3 -2
- package/package.json +10 -5
- package/scripts/check-syntax.js +26 -0
package/lib/commands/web.js
CHANGED
|
@@ -43,6 +43,9 @@ const { JobStore } = require('../schedule/job-store');
|
|
|
43
43
|
const JobRunner = require('../schedule/job-runner');
|
|
44
44
|
const SchedulerService = require('../schedule/scheduler-service');
|
|
45
45
|
const { getClaudeRuntimeConfig } = require('../claude/config');
|
|
46
|
+
const registerScheduleRoutes = require('../server/routes/schedules');
|
|
47
|
+
const registerWorkspaceRoutes = require('../server/routes/workspace');
|
|
48
|
+
const registerProviderRoutes = require('../server/routes/providers');
|
|
46
49
|
|
|
47
50
|
async function webCommand(options) {
|
|
48
51
|
const port = parseInt(options.port) || 3000;
|
|
@@ -124,65 +127,7 @@ async function webCommand(options) {
|
|
|
124
127
|
res.json({ defaultWorkingDirectory: baseDir });
|
|
125
128
|
});
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
app.get('/api/schedules', (req, res) => {
|
|
129
|
-
res.json(jobStore.list());
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
app.post('/api/schedules', (req, res) => {
|
|
133
|
-
try {
|
|
134
|
-
const job = jobStore.create(req.body || {});
|
|
135
|
-
res.json(job);
|
|
136
|
-
} catch (e) {
|
|
137
|
-
res.status(400).json({ error: e.message });
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
app.get('/api/schedules/:id', (req, res) => {
|
|
142
|
-
const job = jobStore.get(req.params.id);
|
|
143
|
-
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
144
|
-
res.json(job);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
app.patch('/api/schedules/:id', (req, res) => {
|
|
148
|
-
const job = jobStore.update(req.params.id, req.body || {});
|
|
149
|
-
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
150
|
-
res.json(job);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
app.patch('/api/schedules/:id/enabled', (req, res) => {
|
|
154
|
-
const job = jobStore.patchRuntime(req.params.id, { enabled: Boolean(req.body && req.body.enabled) });
|
|
155
|
-
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
156
|
-
res.json(job);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
app.delete('/api/schedules/:id', (req, res) => {
|
|
160
|
-
res.json({ success: jobStore.delete(req.params.id) });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
app.post('/api/schedules/:id/duplicate', (req, res) => {
|
|
164
|
-
const job = jobStore.duplicate(req.params.id);
|
|
165
|
-
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
166
|
-
res.json(job);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
app.post('/api/schedules/:id/run', async (req, res) => {
|
|
170
|
-
try {
|
|
171
|
-
const result = await jobRunner.run(req.params.id, { manual: false });
|
|
172
|
-
res.json(result);
|
|
173
|
-
} catch (e) {
|
|
174
|
-
res.status(400).json({ error: e.message });
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
app.post('/api/schedules/:id/simulate', async (req, res) => {
|
|
179
|
-
try {
|
|
180
|
-
const result = await jobRunner.run(req.params.id, { manual: true, background: true });
|
|
181
|
-
res.json(result);
|
|
182
|
-
} catch (e) {
|
|
183
|
-
res.status(400).json({ error: e.message });
|
|
184
|
-
}
|
|
185
|
-
});
|
|
130
|
+
registerScheduleRoutes(app, { jobStore, jobRunner });
|
|
186
131
|
|
|
187
132
|
// API: List all active sessions
|
|
188
133
|
app.get('/api/sessions', (req, res) => {
|
|
@@ -267,10 +212,10 @@ async function webCommand(options) {
|
|
|
267
212
|
});
|
|
268
213
|
|
|
269
214
|
// Browser images are stored only in a private, per-session temporary directory.
|
|
270
|
-
//
|
|
215
|
+
// Structured providers receive either a local path or validated base64 content.
|
|
271
216
|
app.post('/api/sessions/:id/attachments/images', express.raw({ type: () => true, limit: '50mb' }), async (req, res) => {
|
|
272
217
|
try {
|
|
273
|
-
const attachment = await sessionManager.
|
|
218
|
+
const attachment = await sessionManager.storeImageAttachment(req.params.id, req.body);
|
|
274
219
|
res.status(201).json({ success: true, attachment });
|
|
275
220
|
} catch (e) {
|
|
276
221
|
res.status(e.statusCode || 500).json({ error: e.message });
|
|
@@ -281,7 +226,7 @@ async function webCommand(options) {
|
|
|
281
226
|
// Small sequential chunks let the browser report progress from server receipts.
|
|
282
227
|
app.post('/api/sessions/:id/attachments/images/chunks', express.raw({ type: () => true, limit: '1mb' }), async (req, res) => {
|
|
283
228
|
try {
|
|
284
|
-
const result = await sessionManager.
|
|
229
|
+
const result = await sessionManager.appendImageChunk(req.params.id, {
|
|
285
230
|
uploadId: req.get('X-Glad-Upload-Id'),
|
|
286
231
|
chunkIndex: req.get('X-Glad-Chunk-Index'),
|
|
287
232
|
chunkTotal: req.get('X-Glad-Chunk-Total')
|
|
@@ -294,7 +239,7 @@ async function webCommand(options) {
|
|
|
294
239
|
|
|
295
240
|
app.delete('/api/sessions/:id/attachments/images/uploads/:uploadId', async (req, res) => {
|
|
296
241
|
try {
|
|
297
|
-
const removed = await sessionManager.
|
|
242
|
+
const removed = await sessionManager.discardImageUpload(req.params.id, req.params.uploadId);
|
|
298
243
|
res.json({ success: true, removed });
|
|
299
244
|
} catch (e) {
|
|
300
245
|
res.status(e.statusCode || 500).json({ error: e.message });
|
|
@@ -303,7 +248,7 @@ async function webCommand(options) {
|
|
|
303
248
|
|
|
304
249
|
app.delete('/api/sessions/:id/attachments/images/:attachmentId', async (req, res) => {
|
|
305
250
|
try {
|
|
306
|
-
const removed = await sessionManager.
|
|
251
|
+
const removed = await sessionManager.discardImageAttachment(req.params.id, req.params.attachmentId);
|
|
307
252
|
if (!removed) return res.status(404).json({ error: 'Image attachment not found' });
|
|
308
253
|
res.json({ success: true });
|
|
309
254
|
} catch (e) {
|
|
@@ -323,173 +268,13 @@ async function webCommand(options) {
|
|
|
323
268
|
res.json({ success: true, diagnostics });
|
|
324
269
|
});
|
|
325
270
|
|
|
326
|
-
app
|
|
327
|
-
const items = sessionManager.listClaudeResumeSessions(req.params.id);
|
|
328
|
-
if (!items) return res.status(404).json({ error: 'Claude session not found' });
|
|
329
|
-
res.json({ success: true, items });
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
app.patch('/api/sessions/:id/claude-settings', (req, res) => {
|
|
333
|
-
const state = sessionManager.updateClaudeSettings(req.params.id, req.body || {});
|
|
334
|
-
if (!state) return res.status(404).json({ error: 'Claude session not found' });
|
|
335
|
-
res.json({ success: true, state });
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
app.post('/api/sessions/:id/claude-abort', (req, res) => {
|
|
339
|
-
const success = sessionManager.abortClaude(req.params.id);
|
|
340
|
-
if (!success) return res.status(404).json({ error: 'Claude session not found or idle' });
|
|
341
|
-
res.json({ success: true });
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
app.post('/api/sessions/:id/claude-resume', (req, res) => {
|
|
345
|
-
const resumeSessionId = req.body && req.body.resumeSessionId;
|
|
346
|
-
if (!resumeSessionId) return res.status(400).json({ error: 'Missing resumeSessionId' });
|
|
347
|
-
const success = sessionManager.resumeClaude(req.params.id, resumeSessionId);
|
|
348
|
-
if (!success) return res.status(404).json({ error: 'Claude session not found' });
|
|
349
|
-
res.json({ success: true });
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
app.patch('/api/sessions/:id/codex-settings', async (req, res) => {
|
|
353
|
-
try {
|
|
354
|
-
const state = await sessionManager.updateCodexSettings(req.params.id, req.body || {});
|
|
355
|
-
if (!state) return res.status(404).json({ error: 'Codex session not found' });
|
|
356
|
-
res.json({ success: true, state });
|
|
357
|
-
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
app.get('/api/sessions/:id/codex-resume-threads', async (req, res) => {
|
|
361
|
-
try {
|
|
362
|
-
const items = await sessionManager.listCodexResumeThreads(req.params.id);
|
|
363
|
-
if (!items) return res.status(404).json({ error: 'Codex session not found' });
|
|
364
|
-
res.json({ success: true, items });
|
|
365
|
-
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
app.post('/api/sessions/:id/codex-abort', (req, res) => {
|
|
369
|
-
const success = sessionManager.abortCodex(req.params.id);
|
|
370
|
-
if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
|
|
371
|
-
res.json({ success: true });
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
app.post('/api/sessions/:id/codex-resume', async (req, res) => {
|
|
375
|
-
try {
|
|
376
|
-
const success = await sessionManager.resumeCodex(req.params.id, req.body && req.body.threadId);
|
|
377
|
-
if (!success) return res.status(409).json({ error: 'Codex session is busy or no thread is available' });
|
|
378
|
-
res.json({ success: true });
|
|
379
|
-
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
app.post('/api/sessions/:id/codex-fork', async (req, res) => {
|
|
383
|
-
try {
|
|
384
|
-
const session = await sessionManager.forkCodex(req.params.id, req.body && req.body.threadId);
|
|
385
|
-
if (!session) return res.status(404).json({ error: 'Codex session not found' });
|
|
386
|
-
res.json({ success: true, id: session.id, name: session.name, threadId: session.threadId });
|
|
387
|
-
} catch (e) { res.status(e.statusCode || 400).json({ error: e.message }); }
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
app.post('/api/sessions/:id/codex-presentation', async (req, res) => {
|
|
391
|
-
const presentation = req.body && req.body.presentation;
|
|
392
|
-
if (!['terminal', 'structured'].includes(presentation)) return res.status(400).json({ error: 'Invalid presentation' });
|
|
393
|
-
try {
|
|
394
|
-
const success = await sessionManager.switchCodexPresentation(req.params.id, presentation);
|
|
395
|
-
if (!success) return res.status(409).json({ error: 'Codex session cannot switch presentation now' });
|
|
396
|
-
res.json({ success: true });
|
|
397
|
-
} catch (e) { res.status(409).json({ error: e.message }); }
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
app.post('/api/debug/client-log', (req, res) => {
|
|
401
|
-
const { sessionId, event, payload } = req.body || {};
|
|
402
|
-
sessionManager.logClientDebug(sessionId, event, payload);
|
|
403
|
-
res.json({ success: true });
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// API: Git Show
|
|
407
|
-
app.get('/api/sessions/:id/git-show/:hash', async (req, res) => {
|
|
408
|
-
const session = sessionManager.get(req.params.id);
|
|
409
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
410
|
-
const hash = req.params.hash;
|
|
411
|
-
const result = await gitService.show(getSessionWorkingDirectory(session), hash);
|
|
412
|
-
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// API: Git Branch Name for Commit
|
|
416
|
-
app.get('/api/sessions/:id/git-branch/:hash', async (req, res) => {
|
|
417
|
-
const session = sessionManager.get(req.params.id);
|
|
418
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
419
|
-
const hash = req.params.hash;
|
|
420
|
-
const result = await gitService.nameRev(getSessionWorkingDirectory(session), hash);
|
|
421
|
-
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// API: Git Log
|
|
425
|
-
app.get('/api/sessions/:id/git-log', async (req, res) => {
|
|
426
|
-
const session = sessionManager.get(req.params.id);
|
|
427
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
428
|
-
const result = await gitService.log(getSessionWorkingDirectory(session), req.query.maxCount);
|
|
429
|
-
if (!result.success) {
|
|
430
|
-
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
431
|
-
}
|
|
432
|
-
res.json({ success: true, commits: result.commits });
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// API: Git Status
|
|
436
|
-
app.get('/api/sessions/:id/git-status', async (req, res) => {
|
|
437
|
-
const session = sessionManager.get(req.params.id);
|
|
438
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
439
|
-
const result = await gitService.status(getSessionWorkingDirectory(session));
|
|
440
|
-
if (!result.success) {
|
|
441
|
-
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
442
|
-
}
|
|
443
|
-
res.json({ success: true, files: result.files });
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// API: Git Diff Numstat (unstaged and staged)
|
|
447
|
-
app.get('/api/sessions/:id/git-diff-numstat', async (req, res) => {
|
|
448
|
-
const session = sessionManager.get(req.params.id);
|
|
449
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
450
|
-
const isStaged = req.query.staged === 'true';
|
|
451
|
-
const result = await gitService.diffNumstat(getSessionWorkingDirectory(session), isStaged);
|
|
452
|
-
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
// API: Git Diff File
|
|
456
|
-
app.get('/api/sessions/:id/git-diff-file', async (req, res) => {
|
|
457
|
-
const session = sessionManager.get(req.params.id);
|
|
458
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
459
|
-
const isStaged = req.query.staged === 'true';
|
|
460
|
-
const filePath = req.query.path;
|
|
461
|
-
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
462
|
-
const result = await gitService.diffFile(getSessionWorkingDirectory(session), filePath, isStaged);
|
|
463
|
-
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
// API: Get File Content
|
|
467
|
-
app.get('/api/sessions/:id/file', async (req, res) => {
|
|
468
|
-
const session = sessionManager.get(req.params.id);
|
|
469
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
470
|
-
const filePath = req.query.path || '';
|
|
471
|
-
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
472
|
-
const cwd = getSessionWorkingDirectory(session);
|
|
473
|
-
try {
|
|
474
|
-
const content = workspaceService.readFile(cwd, filePath);
|
|
475
|
-
res.json({ success: true, content });
|
|
476
|
-
} catch (e) {
|
|
477
|
-
res.status(e.statusCode || 200).json({ success: false, error: e.message });
|
|
478
|
-
}
|
|
479
|
-
});
|
|
271
|
+
registerProviderRoutes(app, { sessionManager });
|
|
480
272
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const cwd = getSessionWorkingDirectory(session);
|
|
487
|
-
try {
|
|
488
|
-
const files = await workspaceService.listDirectory(cwd, dirPath);
|
|
489
|
-
res.json({ success: true, files });
|
|
490
|
-
} catch (e) {
|
|
491
|
-
res.status(e.statusCode || 200).json({ success: false, error: e.message });
|
|
492
|
-
}
|
|
273
|
+
registerWorkspaceRoutes(app, {
|
|
274
|
+
sessionManager,
|
|
275
|
+
gitService,
|
|
276
|
+
workspaceService,
|
|
277
|
+
getWorkingDirectory: getSessionWorkingDirectory
|
|
493
278
|
});
|
|
494
279
|
|
|
495
280
|
|
|
@@ -555,7 +340,8 @@ async function webCommand(options) {
|
|
|
555
340
|
session.write(payload.data);
|
|
556
341
|
}
|
|
557
342
|
if (payload.type === 'claude-input') {
|
|
558
|
-
sessionManager.sendClaudeInput(sessionId, payload.text || '')
|
|
343
|
+
sessionManager.sendClaudeInput(sessionId, payload.text || '', payload.attachmentIds || [])
|
|
344
|
+
.catch(error => logger.error(`Claude input error: ${error.message}`));
|
|
559
345
|
}
|
|
560
346
|
if (payload.type === 'claude-permission') {
|
|
561
347
|
sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved), payload.action || null);
|
|
@@ -563,6 +349,12 @@ async function webCommand(options) {
|
|
|
563
349
|
if (payload.type === 'claude-settings') {
|
|
564
350
|
sessionManager.updateClaudeSettings(sessionId, payload.settings || {});
|
|
565
351
|
}
|
|
352
|
+
if (payload.type === 'claude-usage') {
|
|
353
|
+
sessionManager.showClaudeUsage(sessionId).catch(error => logger.error(`Claude usage error: ${error.message}`));
|
|
354
|
+
}
|
|
355
|
+
if (payload.type === 'claude-context') {
|
|
356
|
+
sessionManager.showClaudeContext(sessionId).catch(error => logger.error(`Claude context error: ${error.message}`));
|
|
357
|
+
}
|
|
566
358
|
if (payload.type === 'claude-abort') {
|
|
567
359
|
sessionManager.abortClaude(sessionId);
|
|
568
360
|
}
|
|
@@ -618,6 +410,9 @@ async function webCommand(options) {
|
|
|
618
410
|
// Frontend routes
|
|
619
411
|
const assetsDir = path.resolve(__dirname, '../../assets');
|
|
620
412
|
const webDir = path.join(__dirname, '../web');
|
|
413
|
+
const xtermScript = require.resolve('@xterm/xterm');
|
|
414
|
+
const xtermStyles = path.resolve(path.dirname(xtermScript), '../css/xterm.css');
|
|
415
|
+
const fitAddonScript = require.resolve('@xterm/addon-fit');
|
|
621
416
|
|
|
622
417
|
const sendLogo = (req, res) => {
|
|
623
418
|
res.sendFile('logo.svg', { root: assetsDir }, error => {
|
|
@@ -635,11 +430,34 @@ async function webCommand(options) {
|
|
|
635
430
|
}
|
|
636
431
|
});
|
|
637
432
|
|
|
638
|
-
|
|
639
|
-
res.sendFile(
|
|
433
|
+
const sendWebAsset = assetName => (req, res) => {
|
|
434
|
+
res.sendFile(assetName, { root: webDir }, error => {
|
|
640
435
|
if (error && !res.headersSent) res.status(404).send('Not found');
|
|
641
436
|
});
|
|
642
|
-
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const webAssets = [
|
|
440
|
+
'gitgraph.js',
|
|
441
|
+
'styles.css',
|
|
442
|
+
'core.js',
|
|
443
|
+
'claude.js',
|
|
444
|
+
'schedules.js',
|
|
445
|
+
'shell.js',
|
|
446
|
+
'codex.js',
|
|
447
|
+
'session.js',
|
|
448
|
+
'composer.js',
|
|
449
|
+
'timed-inputs.js',
|
|
450
|
+
'terminal-scroll.js',
|
|
451
|
+
'git.js'
|
|
452
|
+
];
|
|
453
|
+
for (const assetName of webAssets) {
|
|
454
|
+
const escapedName = assetName.replace('.', '\\.');
|
|
455
|
+
app.get([`/${assetName}`, new RegExp(`.*\\/${escapedName}$`)], sendWebAsset(assetName));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
app.get('/vendor/xterm.js', (req, res) => res.sendFile(xtermScript));
|
|
459
|
+
app.get('/vendor/xterm.css', (req, res) => res.sendFile(xtermStyles));
|
|
460
|
+
app.get('/vendor/xterm-addon-fit.js', (req, res) => res.sendFile(fitAddonScript));
|
|
643
461
|
|
|
644
462
|
app.get(['/logo.svg', /.*\/logo\.svg$/], sendLogo);
|
|
645
463
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function registerProviderRoutes(app, { sessionManager }) {
|
|
2
|
+
app.get('/api/sessions/:id/claude-resume-sessions', (req, res) => {
|
|
3
|
+
const items = sessionManager.listClaudeResumeSessions(req.params.id);
|
|
4
|
+
if (!items) return res.status(404).json({ error: 'Claude session not found' });
|
|
5
|
+
res.json({ success: true, items });
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
app.patch('/api/sessions/:id/claude-settings', (req, res) => {
|
|
9
|
+
const state = sessionManager.updateClaudeSettings(req.params.id, req.body || {});
|
|
10
|
+
if (!state) return res.status(404).json({ error: 'Claude session not found' });
|
|
11
|
+
res.json({ success: true, state });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.post('/api/sessions/:id/claude-abort', (req, res) => {
|
|
15
|
+
const success = sessionManager.abortClaude(req.params.id);
|
|
16
|
+
if (!success) return res.status(404).json({ error: 'Claude session not found or idle' });
|
|
17
|
+
res.json({ success: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.post('/api/sessions/:id/claude-resume', (req, res) => {
|
|
21
|
+
const resumeSessionId = req.body?.resumeSessionId;
|
|
22
|
+
if (!resumeSessionId) return res.status(400).json({ error: 'Missing resumeSessionId' });
|
|
23
|
+
const success = sessionManager.resumeClaude(req.params.id, resumeSessionId);
|
|
24
|
+
if (!success) return res.status(404).json({ error: 'Claude session not found' });
|
|
25
|
+
res.json({ success: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.post('/api/sessions/:id/claude-fork', async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const result = await sessionManager.forkClaude(req.params.id, req.body?.claudeSessionId);
|
|
31
|
+
if (!result) return res.status(404).json({ error: 'Claude session not found' });
|
|
32
|
+
res.json({ success: true, ...result });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
res.status(error.statusCode || 400).json({ error: error.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.patch('/api/sessions/:id/codex-settings', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const state = await sessionManager.updateCodexSettings(req.params.id, req.body || {});
|
|
41
|
+
if (!state) return res.status(404).json({ error: 'Codex session not found' });
|
|
42
|
+
res.json({ success: true, state });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
res.status(400).json({ error: error.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
app.get('/api/sessions/:id/codex-resume-threads', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const items = await sessionManager.listCodexResumeThreads(req.params.id);
|
|
51
|
+
if (!items) return res.status(404).json({ error: 'Codex session not found' });
|
|
52
|
+
res.json({ success: true, items });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
res.status(400).json({ error: error.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.post('/api/sessions/:id/codex-abort', (req, res) => {
|
|
59
|
+
const success = sessionManager.abortCodex(req.params.id);
|
|
60
|
+
if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
|
|
61
|
+
res.json({ success: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
app.post('/api/sessions/:id/codex-resume', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const success = await sessionManager.resumeCodex(req.params.id, req.body?.threadId);
|
|
67
|
+
if (!success) return res.status(409).json({ error: 'Codex session is busy or no thread is available' });
|
|
68
|
+
res.json({ success: true });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
res.status(400).json({ error: error.message });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
app.post('/api/sessions/:id/codex-fork', async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const session = await sessionManager.forkCodex(req.params.id, req.body?.threadId);
|
|
77
|
+
if (!session) return res.status(404).json({ error: 'Codex session not found' });
|
|
78
|
+
res.json({ success: true, id: session.id, name: session.name, threadId: session.threadId });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
res.status(error.statusCode || 400).json({ error: error.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.post('/api/sessions/:id/codex-presentation', async (req, res) => {
|
|
85
|
+
const presentation = req.body?.presentation;
|
|
86
|
+
if (!['terminal', 'structured'].includes(presentation)) return res.status(400).json({ error: 'Invalid presentation' });
|
|
87
|
+
try {
|
|
88
|
+
const success = await sessionManager.switchCodexPresentation(req.params.id, presentation);
|
|
89
|
+
if (!success) return res.status(409).json({ error: 'Codex session cannot switch presentation now' });
|
|
90
|
+
res.json({ success: true });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
res.status(409).json({ error: error.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.post('/api/debug/client-log', (req, res) => {
|
|
97
|
+
const { sessionId, event, payload } = req.body || {};
|
|
98
|
+
sessionManager.logClientDebug(sessionId, event, payload);
|
|
99
|
+
res.json({ success: true });
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = registerProviderRoutes;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function registerScheduleRoutes(app, { jobStore, jobRunner }) {
|
|
2
|
+
app.get('/api/schedules', (req, res) => {
|
|
3
|
+
res.json(jobStore.list());
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
app.post('/api/schedules', (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
res.json(jobStore.create(req.body || {}));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
res.status(400).json({ error: error.message });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.get('/api/schedules/:id', (req, res) => {
|
|
15
|
+
const job = jobStore.get(req.params.id);
|
|
16
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
17
|
+
res.json(job);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.patch('/api/schedules/:id', (req, res) => {
|
|
21
|
+
const job = jobStore.update(req.params.id, req.body || {});
|
|
22
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
23
|
+
res.json(job);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.patch('/api/schedules/:id/enabled', (req, res) => {
|
|
27
|
+
const job = jobStore.patchRuntime(req.params.id, { enabled: Boolean(req.body?.enabled) });
|
|
28
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
29
|
+
res.json(job);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.delete('/api/schedules/:id', (req, res) => {
|
|
33
|
+
res.json({ success: jobStore.delete(req.params.id) });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.post('/api/schedules/:id/duplicate', (req, res) => {
|
|
37
|
+
const job = jobStore.duplicate(req.params.id);
|
|
38
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
39
|
+
res.json(job);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const runJob = options => async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
res.json(await jobRunner.run(req.params.id, options));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
res.status(400).json({ error: error.message });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
app.post('/api/schedules/:id/run', runJob({ manual: false }));
|
|
51
|
+
app.post('/api/schedules/:id/simulate', runJob({ manual: true, background: true }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = registerScheduleRoutes;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function registerWorkspaceRoutes(app, { sessionManager, gitService, workspaceService, getWorkingDirectory }) {
|
|
2
|
+
function getSession(req, res) {
|
|
3
|
+
const session = sessionManager.get(req.params.id);
|
|
4
|
+
if (!session) res.status(404).json({ error: 'Session not found' });
|
|
5
|
+
return session;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
app.get('/api/sessions/:id/git-show/:hash', async (req, res) => {
|
|
9
|
+
const session = getSession(req, res);
|
|
10
|
+
if (!session) return;
|
|
11
|
+
const result = await gitService.show(getWorkingDirectory(session), req.params.hash);
|
|
12
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.get('/api/sessions/:id/git-branch/:hash', async (req, res) => {
|
|
16
|
+
const session = getSession(req, res);
|
|
17
|
+
if (!session) return;
|
|
18
|
+
const result = await gitService.nameRev(getWorkingDirectory(session), req.params.hash);
|
|
19
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.get('/api/sessions/:id/git-log', async (req, res) => {
|
|
23
|
+
const session = getSession(req, res);
|
|
24
|
+
if (!session) return;
|
|
25
|
+
const result = await gitService.log(getWorkingDirectory(session), req.query.maxCount);
|
|
26
|
+
if (!result.success) return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
27
|
+
res.json({ success: true, commits: result.commits });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get('/api/sessions/:id/git-status', async (req, res) => {
|
|
31
|
+
const session = getSession(req, res);
|
|
32
|
+
if (!session) return;
|
|
33
|
+
const result = await gitService.status(getWorkingDirectory(session));
|
|
34
|
+
if (!result.success) return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
35
|
+
res.json({ success: true, files: result.files });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
app.get('/api/sessions/:id/git-diff-numstat', async (req, res) => {
|
|
39
|
+
const session = getSession(req, res);
|
|
40
|
+
if (!session) return;
|
|
41
|
+
const result = await gitService.diffNumstat(getWorkingDirectory(session), req.query.staged === 'true');
|
|
42
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get('/api/sessions/:id/git-diff-file', async (req, res) => {
|
|
46
|
+
const session = getSession(req, res);
|
|
47
|
+
if (!session) return;
|
|
48
|
+
if (!req.query.path) return res.status(400).json({ error: 'Missing file path' });
|
|
49
|
+
const result = await gitService.diffFile(getWorkingDirectory(session), req.query.path, req.query.staged === 'true');
|
|
50
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
app.get('/api/sessions/:id/file', (req, res) => {
|
|
54
|
+
const session = getSession(req, res);
|
|
55
|
+
if (!session) return;
|
|
56
|
+
if (!req.query.path) return res.status(400).json({ error: 'Missing file path' });
|
|
57
|
+
try {
|
|
58
|
+
const content = workspaceService.readFile(getWorkingDirectory(session), req.query.path);
|
|
59
|
+
res.json({ success: true, content });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
res.status(error.statusCode || 200).json({ success: false, error: error.message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
app.get('/api/sessions/:id/fs/dir', async (req, res) => {
|
|
66
|
+
const session = getSession(req, res);
|
|
67
|
+
if (!session) return;
|
|
68
|
+
try {
|
|
69
|
+
const files = await workspaceService.listDirectory(getWorkingDirectory(session), req.query.path || '');
|
|
70
|
+
res.json({ success: true, files });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
res.status(error.statusCode || 200).json({ success: false, error: error.message });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = registerWorkspaceRoutes;
|