glad-web 1.0.45 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -58
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1578
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -586
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -89
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/usage.js +0 -23
  37. package/lib/server/routes/workspace.js +0 -77
  38. package/lib/session/buffer.js +0 -102
  39. package/lib/session/file-attachment-store.js +0 -168
  40. package/lib/session/pty-manager.js +0 -255
  41. package/lib/session/rendered-history.js +0 -225
  42. package/lib/session/session-manager.js +0 -1001
  43. package/lib/session/text-history.js +0 -274
  44. package/lib/usage/ccusage-runner.js +0 -128
  45. package/lib/usage/source-catalog.js +0 -26
  46. package/lib/usage/usage-service.js +0 -226
  47. package/lib/utils/logger.js +0 -74
  48. package/lib/utils/pid.js +0 -67
  49. package/lib/utils/validation.js +0 -53
  50. package/lib/web/claude.js +0 -1129
  51. package/lib/web/codex.js +0 -1042
  52. package/lib/web/composer.js +0 -463
  53. package/lib/web/core.js +0 -373
  54. package/lib/web/git.js +0 -535
  55. package/lib/web/gitgraph.js +0 -293
  56. package/lib/web/index.html +0 -516
  57. package/lib/web/layout.js +0 -72
  58. package/lib/web/notifications.js +0 -163
  59. package/lib/web/schedules.js +0 -245
  60. package/lib/web/session.js +0 -360
  61. package/lib/web/shell.js +0 -59
  62. package/lib/web/styles.css +0 -905
  63. package/lib/web/terminal-scroll.js +0 -81
  64. package/lib/web/theme.js +0 -60
  65. package/lib/web/timed-inputs.js +0 -216
  66. package/lib/web/usage.js +0 -323
  67. package/lib/workspace/service.js +0 -77
  68. package/scripts/check-syntax.js +0 -26
@@ -1,586 +0,0 @@
1
- const express = require('express');
2
- const http = require('http');
3
- const { WebSocketServer } = require('ws');
4
- const path = require('path');
5
- const fs = require('fs');
6
- const os = require('os');
7
- const zlib = require('zlib');
8
- const chalk = require('chalk');
9
- const { getAllTools } = require('../ai-tools/registry');
10
- const { GitService } = require('../git/service');
11
- const WorkspaceService = require('../workspace/service');
12
- const SessionManager = require('../session/session-manager');
13
-
14
- function sendCompressedJson(req, res, payload) {
15
- const body = Buffer.from(JSON.stringify(payload), 'utf8');
16
- const acceptEncoding = req.headers['accept-encoding'] || '';
17
-
18
- if (/\bgzip\b/.test(acceptEncoding)) {
19
- zlib.gzip(body, { level: 6 }, (error, compressed) => {
20
- if (error) {
21
- res.type('application/json').send(body);
22
- return;
23
- }
24
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
25
- res.setHeader('Content-Encoding', 'gzip');
26
- res.setHeader('Vary', 'Accept-Encoding');
27
- res.setHeader('Content-Length', compressed.length);
28
- res.send(compressed);
29
- });
30
- return;
31
- }
32
-
33
- res.type('application/json').send(body);
34
- }
35
-
36
- function getSessionWorkingDirectory(session) {
37
- return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || process.cwd();
38
- }
39
-
40
- const { detectInstalledTools } = require('../ai-tools/detector');
41
- const logger = require('../utils/logger');
42
- const { JobStore } = require('../schedule/job-store');
43
- const JobRunner = require('../schedule/job-runner');
44
- const SchedulerService = require('../schedule/scheduler-service');
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');
49
- const registerNotificationRoutes = require('../server/routes/notifications');
50
- const registerUsageRoutes = require('../server/routes/usage');
51
- const { UsageService } = require('../usage/usage-service');
52
- const { ServerChanSettingsStore } = require('../notifications/serverchan-settings-store');
53
- const ServerChanClient = require('../notifications/serverchan-client');
54
- const NotificationService = require('../notifications/notification-service');
55
-
56
- async function webCommand(options) {
57
- const port = parseInt(options.port) || 3000;
58
- const debugHistoryEnabled = process.env.DEBUG_SESSION_HISTORY === '1';
59
- const defaultRenderedTools = getAllTools().map(tool => tool.key).join(',');
60
- const renderHistoryTools = new Set(
61
- String(process.env.HISTORY_RENDER_TOOLS || defaultRenderedTools)
62
- .split(',')
63
- .map(value => value.trim().toLowerCase())
64
- .filter(Boolean)
65
- );
66
- const app = express();
67
- app.use((req, res, next) => {
68
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
69
- res.setHeader('Pragma', 'no-cache');
70
- res.setHeader('Expires', '0');
71
- next();
72
- });
73
- app.use(express.json());
74
- const server = http.createServer(app);
75
- const wss = new WebSocketServer({
76
- server,
77
- perMessageDeflate: {
78
- threshold: 1024,
79
- zlibDeflateOptions: { level: 3 },
80
- zlibInflateOptions: {},
81
- clientNoContextTakeover: true,
82
- serverNoContextTakeover: true
83
- }
84
- });
85
-
86
- // Use directory from options if provided, otherwise default to current working directory
87
- const baseDir = options.directory ? path.resolve(process.cwd(), options.directory) : process.cwd();
88
-
89
- const jobStore = new JobStore();
90
- const gitService = new GitService();
91
- const workspaceService = new WorkspaceService({ gitService });
92
- const sessionManager = new SessionManager({
93
- baseDir,
94
- renderHistoryTools,
95
- debugHistoryEnabled,
96
- logger,
97
- hasConnectedSessionClient
98
- });
99
- const serverChanSettings = new ServerChanSettingsStore();
100
- const notificationService = new NotificationService({
101
- sessionManager,
102
- settingsStore: serverChanSettings,
103
- channel: new ServerChanClient(),
104
- logger
105
- });
106
- const usageService = new UsageService({ logger });
107
- sessionManager.on('output', ({ sessionId, data }) => {
108
- broadcastToSession(sessionId, { type: 'output', data });
109
- });
110
- sessionManager.on('claude-event', ({ sessionId, event }) => {
111
- broadcastToSession(sessionId, { type: 'claude-event', event });
112
- });
113
- sessionManager.on('codex-event', ({ sessionId, event }) => {
114
- broadcastToSession(sessionId, { type: 'codex-event', event });
115
- });
116
- sessionManager.on('exit', ({ sessionId }) => {
117
- broadcastToSession(sessionId, { type: 'exit' });
118
- });
119
-
120
- const jobRunner = new JobRunner({
121
- createSession: input => sessionManager.create(input),
122
- getJob: id => jobStore.get(id),
123
- updateJob: (id, patch) => jobStore.patchRuntime(id, patch),
124
- logger
125
- });
126
- const schedulerService = new SchedulerService({ jobStore, jobRunner, logger });
127
- schedulerService.start();
128
-
129
- // API: Get all supported and installed tools
130
- app.get('/api/tools', async (req, res) => {
131
- try {
132
- const tools = await detectInstalledTools();
133
- res.json(tools);
134
- } catch (e) {
135
- res.status(500).json({ error: 'Failed to detect tools' });
136
- }
137
- });
138
-
139
- // API: Get web UI runtime configuration
140
- app.get('/api/config', (req, res) => {
141
- res.json({ defaultWorkingDirectory: baseDir });
142
- });
143
-
144
- registerScheduleRoutes(app, { jobStore, jobRunner });
145
- registerNotificationRoutes(app, {
146
- settingsStore: serverChanSettings,
147
- notificationService
148
- });
149
- registerUsageRoutes(app, { usageService, sendJson: sendCompressedJson });
150
-
151
- // API: List all active sessions
152
- app.get('/api/sessions', (req, res) => {
153
- logger.debug('API: GET /api/sessions');
154
- res.json(sessionManager.list());
155
- });
156
-
157
- app.get('/api/claude-config', (req, res) => {
158
- res.json({ success: true, config: getClaudeRuntimeConfig(process.env) });
159
- });
160
-
161
- // API: Create a new PTY session
162
- app.post('/api/sessions', async (req, res) => {
163
- logger.debug(`API: POST /api/sessions - ${JSON.stringify(req.body)}`);
164
- try {
165
- const { toolKey, workingDirectory, claudeOptions } = req.body;
166
- const session = sessionManager.create({ toolKey, workingDirectory, claudeOptions });
167
- res.json({ id: session.id });
168
- } catch (e) {
169
- logger.error(`API: POST /api/sessions failed: ${e.message}`);
170
- res.status(e.statusCode || 500).json({ error: e.message });
171
- }
172
- });
173
-
174
- // API: Plain text terminal history for mobile-friendly reading
175
- app.get('/api/sessions/:id/history', (req, res) => {
176
- const history = sessionManager.getHistory(req.params.id);
177
- if (!history) return res.status(404).json({ error: 'Session not found' });
178
- sessionManager.logHistoryRequest(req.params.id, req);
179
- sendCompressedJson(req, res, history);
180
- });
181
-
182
- // API: Rename session
183
- app.patch('/api/sessions/:id', (req, res) => {
184
- const session = sessionManager.rename(req.params.id, req.body.name);
185
- if (session) {
186
- res.json({ success: true, name: session.name });
187
- } else {
188
- res.status(404).json({ error: 'Session not found' });
189
- }
190
- });
191
-
192
- // API: Mark a session completion indicator as read
193
- app.post('/api/sessions/:id/completion/read', (req, res) => {
194
- const session = sessionManager.markCompletionRead(req.params.id);
195
- if (!session) return res.status(404).json({ error: 'Session not found' });
196
- res.json({ success: true });
197
- });
198
-
199
- app.get('/api/sessions/:id/timed-inputs', (req, res) => {
200
- const items = sessionManager.listTimedInputs(req.params.id);
201
- if (!items) return res.status(404).json({ error: 'Session not found' });
202
- res.json({ success: true, items });
203
- });
204
-
205
- app.post('/api/sessions/:id/timed-inputs', (req, res) => {
206
- try {
207
- const item = sessionManager.scheduleTimedInput(req.params.id, req.body || {});
208
- if (!item) return res.status(404).json({ error: 'Session not found' });
209
- res.json({ success: true, item });
210
- } catch (e) {
211
- res.status(e.statusCode || 500).json({ error: e.message });
212
- }
213
- });
214
-
215
- app.patch('/api/sessions/:id/timed-inputs/:inputId', (req, res) => {
216
- try {
217
- const item = sessionManager.updateTimedInput(req.params.id, req.params.inputId, req.body || {});
218
- if (item === null) return res.status(404).json({ error: 'Session not found' });
219
- if (!item) return res.status(404).json({ error: 'Timed input not found' });
220
- res.json({ success: true, item });
221
- } catch (e) {
222
- res.status(e.statusCode || 500).json({ error: e.message });
223
- }
224
- });
225
-
226
- app.delete('/api/sessions/:id/timed-inputs/:inputId', (req, res) => {
227
- const cancelled = sessionManager.cancelTimedInput(req.params.id, req.params.inputId);
228
- if (cancelled === null) return res.status(404).json({ error: 'Session not found' });
229
- if (!cancelled) return res.status(404).json({ error: 'Timed input not found' });
230
- res.json({ success: true });
231
- });
232
-
233
- // Browser images are stored only in a private, per-session temporary directory.
234
- // Structured providers receive either a local path or validated base64 content.
235
- app.post('/api/sessions/:id/attachments/images', express.raw({ type: () => true, limit: '50mb' }), async (req, res) => {
236
- try {
237
- const attachment = await sessionManager.storeImageAttachment(req.params.id, req.body);
238
- res.status(201).json({ success: true, attachment });
239
- } catch (e) {
240
- res.status(e.statusCode || 500).json({ error: e.message });
241
- }
242
- });
243
-
244
- // Mobile Safari can coalesce progress events for a single large request.
245
- // Small sequential chunks let the browser report progress from server receipts.
246
- app.post('/api/sessions/:id/attachments/images/chunks', express.raw({ type: () => true, limit: '1mb' }), async (req, res) => {
247
- try {
248
- const result = await sessionManager.appendImageChunk(req.params.id, {
249
- uploadId: req.get('X-Glad-Upload-Id'),
250
- chunkIndex: req.get('X-Glad-Chunk-Index'),
251
- chunkTotal: req.get('X-Glad-Chunk-Total')
252
- }, req.body);
253
- res.json({ success: true, ...result });
254
- } catch (e) {
255
- res.status(e.statusCode || 500).json({ error: e.message });
256
- }
257
- });
258
-
259
- app.delete('/api/sessions/:id/attachments/images/uploads/:uploadId', async (req, res) => {
260
- try {
261
- const removed = await sessionManager.discardImageUpload(req.params.id, req.params.uploadId);
262
- res.json({ success: true, removed });
263
- } catch (e) {
264
- res.status(e.statusCode || 500).json({ error: e.message });
265
- }
266
- });
267
-
268
- app.delete('/api/sessions/:id/attachments/images/:attachmentId', async (req, res) => {
269
- try {
270
- const removed = await sessionManager.discardImageAttachment(req.params.id, req.params.attachmentId);
271
- if (!removed) return res.status(404).json({ error: 'Image attachment not found' });
272
- res.json({ success: true });
273
- } catch (e) {
274
- res.status(e.statusCode || 500).json({ error: e.message });
275
- }
276
- });
277
-
278
- app.post('/api/sessions/:id/attachments/files/chunks', express.raw({ type: () => true, limit: '1mb' }), async (req, res) => {
279
- try {
280
- const result = await sessionManager.appendFileChunk(req.params.id, {
281
- uploadId: req.get('X-Glad-Upload-Id'),
282
- chunkIndex: req.get('X-Glad-Chunk-Index'),
283
- chunkTotal: req.get('X-Glad-Chunk-Total'),
284
- name: req.get('X-Glad-File-Name')
285
- }, req.body);
286
- res.json({ success: true, ...result });
287
- } catch (e) {
288
- res.status(e.statusCode || 500).json({ error: e.message });
289
- }
290
- });
291
-
292
- app.delete('/api/sessions/:id/attachments/files/uploads/:uploadId', async (req, res) => {
293
- try {
294
- const removed = await sessionManager.discardFileUpload(req.params.id, req.params.uploadId);
295
- res.json({ success: true, removed });
296
- } catch (e) {
297
- res.status(e.statusCode || 500).json({ error: e.message });
298
- }
299
- });
300
-
301
- app.delete('/api/sessions/:id/attachments/files/:attachmentId', async (req, res) => {
302
- try {
303
- const removed = await sessionManager.discardFileAttachment(req.params.id, req.params.attachmentId);
304
- if (!removed) return res.status(404).json({ error: 'File attachment not found' });
305
- res.json({ success: true });
306
- } catch (e) {
307
- res.status(e.statusCode || 500).json({ error: e.message });
308
- }
309
- });
310
-
311
- // API: Delete/Kill session
312
- app.delete('/api/sessions/:id', async (req, res) => {
313
- await sessionManager.kill(req.params.id);
314
- res.json({ success: true });
315
- });
316
-
317
- app.get('/api/sessions/:id/debug', (req, res) => {
318
- const diagnostics = sessionManager.getDiagnostics(req.params.id);
319
- if (!diagnostics) return res.status(404).json({ error: 'Session not found' });
320
- res.json({ success: true, diagnostics });
321
- });
322
-
323
- registerProviderRoutes(app, { sessionManager });
324
-
325
- registerWorkspaceRoutes(app, {
326
- sessionManager,
327
- gitService,
328
- workspaceService,
329
- getWorkingDirectory: getSessionWorkingDirectory
330
- });
331
-
332
-
333
- function broadcastToSession(sessionId, message) {
334
- const msgStr = JSON.stringify(message);
335
- wss.clients.forEach(client => {
336
- if (client.readyState === 1 && client.sessionId === sessionId) {
337
- client.send(msgStr);
338
- }
339
- });
340
- }
341
-
342
- function hasConnectedSessionClient(sessionId) {
343
- for (const client of wss.clients) {
344
- if (client.readyState === 1 && client.sessionId === sessionId) return true;
345
- }
346
- return false;
347
- }
348
-
349
- // WebSocket: Terminal I/O
350
- wss.on('connection', (ws, req) => {
351
- const url = new URL(req.url, 'http://' + req.headers.host);
352
- const sessionId = url.searchParams.get('sessionId');
353
-
354
- if (!sessionId || !sessionManager.has(sessionId)) {
355
- ws.close(4001, 'Invalid Session ID');
356
- return;
357
- }
358
-
359
- ws.sessionId = sessionId;
360
- const session = sessionManager.get(sessionId);
361
- const isReconnect = session.hasConnectedWebClient;
362
- session.hasConnectedWebClient = true;
363
- if (!session.resizeOwner) {
364
- session.resizeOwner = ws;
365
- }
366
- sessionManager.logWsConnected(sessionId, req);
367
-
368
- if (session.kind === 'claude-structured') {
369
- ws.send(JSON.stringify({ type: 'claude-snapshot', snapshot: sessionManager.getClaudeSnapshot(sessionId) }));
370
- }
371
- if (session.kind === 'codex-structured') {
372
- ws.send(JSON.stringify({ type: 'codex-snapshot', snapshot: sessionManager.getCodexSnapshot(sessionId) }));
373
- }
374
-
375
- // Send catchup output. TUI tools may skip the raw circular buffer, so fall
376
- // back to the rendered/text history snapshot instead of reconnecting blank.
377
- const catchup = sessionManager.getCatchupOutput(sessionId);
378
- ws.needsTuiRedraw = !['claude-structured', 'codex-structured'].includes(session.kind)
379
- && ['antigravity', 'claude-code', 'codex'].includes(session.tool.key)
380
- && (isReconnect || (catchup && catchup.source === 'rendered-history'));
381
- if (ws.needsTuiRedraw) {
382
- ws.send(JSON.stringify({ type: 'reset' }));
383
- } else if (!['claude-structured', 'codex-structured'].includes(session.kind) && catchup && catchup.data) {
384
- sessionManager.logWsCatchupOutput(sessionId, catchup);
385
- ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
386
- }
387
-
388
- ws.on('message', (message) => {
389
- try {
390
- const payload = JSON.parse(message);
391
- if (payload.type === 'input') {
392
- session.write(payload.data);
393
- }
394
- if (payload.type === 'file-input') {
395
- sessionManager.sendTerminalFileInput(sessionId, payload.text || '', payload.fileAttachmentIds || []);
396
- }
397
- if (payload.type === 'claude-input') {
398
- sessionManager.sendClaudeInput(sessionId, payload.text || '', payload.attachmentIds || [], payload.fileAttachmentIds || [])
399
- .catch(error => logger.error(`Claude input error: ${error.message}`));
400
- }
401
- if (payload.type === 'claude-permission') {
402
- sessionManager.respondClaudePermission(sessionId, payload.id, Boolean(payload.approved), payload.action || null);
403
- }
404
- if (payload.type === 'claude-settings') {
405
- sessionManager.updateClaudeSettings(sessionId, payload.settings || {});
406
- }
407
- if (payload.type === 'claude-usage') {
408
- sessionManager.showClaudeUsage(sessionId).catch(error => logger.error(`Claude usage error: ${error.message}`));
409
- }
410
- if (payload.type === 'claude-context') {
411
- sessionManager.showClaudeContext(sessionId).catch(error => logger.error(`Claude context error: ${error.message}`));
412
- }
413
- if (payload.type === 'claude-abort') {
414
- sessionManager.abortClaude(sessionId);
415
- }
416
- if (payload.type === 'codex-input') {
417
- sessionManager.sendCodexInput(sessionId, payload.text || '', payload.attachmentIds || [], payload.skills || [], payload.fileAttachmentIds || [])
418
- .catch(error => logger.error(`Codex input error: ${error.message}`));
419
- }
420
- if (payload.type === 'codex-permission') {
421
- const codex = sessionManager.get(sessionId);
422
- if (codex && codex.kind === 'codex-structured') {
423
- codex.respondPermission(payload.id, payload.decision || Boolean(payload.approved));
424
- }
425
- }
426
- if (payload.type === 'codex-settings') {
427
- sessionManager.updateCodexSettings(sessionId, payload.settings || {}).catch(error => logger.error(`Codex settings error: ${error.message}`));
428
- }
429
- if (payload.type === 'codex-status') {
430
- sessionManager.showCodexStatus(sessionId).catch(error => logger.error(`Codex status error: ${error.message}`));
431
- }
432
- if (payload.type === 'codex-compact') {
433
- sessionManager.compactCodexContext(sessionId).catch(error => logger.error(`Codex compact error: ${error.message}`));
434
- }
435
- if (payload.type === 'codex-detail-request') {
436
- const codex = sessionManager.get(sessionId);
437
- if (codex && codex.kind === 'codex-structured') {
438
- ws.send(JSON.stringify({
439
- type: 'codex-detail-response',
440
- requestId: payload.requestId || null,
441
- detail: codex.getMessageDetails({ ids: payload.ids, threadId: payload.threadId })
442
- }));
443
- }
444
- }
445
- if (payload.type === 'codex-abort') {
446
- sessionManager.abortCodex(sessionId);
447
- }
448
- if (payload.type === 'claude-resume') {
449
- sessionManager.resumeClaude(sessionId, payload.resumeSessionId || '');
450
- }
451
- if (payload.type === 'resize' && session.resizeOwner === ws) {
452
- sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
453
- if (ws.needsTuiRedraw) {
454
- ws.needsTuiRedraw = false;
455
- sessionManager.redraw(sessionId, payload.cols, payload.rows);
456
- } else {
457
- sessionManager.resize(sessionId, payload.cols, payload.rows);
458
- }
459
- }
460
- } catch (e) {
461
- logger.error('WS Message Error: ' + e.message);
462
- }
463
- });
464
-
465
- ws.on('close', () => {
466
- sessionManager.logWsClosed(sessionId);
467
- if (session.resizeOwner !== ws) return;
468
- session.resizeOwner = null;
469
- for (const client of wss.clients) {
470
- if (client.readyState === 1 && client.sessionId === sessionId) {
471
- session.resizeOwner = client;
472
- break;
473
- }
474
- }
475
- });
476
- });
477
-
478
- // Frontend routes
479
- const assetsDir = path.resolve(__dirname, '../../assets');
480
- const webDir = path.join(__dirname, '../web');
481
- const xtermScript = require.resolve('@xterm/xterm');
482
- const xtermStyles = path.resolve(path.dirname(xtermScript), '../css/xterm.css');
483
- const fitAddonScript = require.resolve('@xterm/addon-fit');
484
-
485
- const sendLogo = (req, res) => {
486
- res.sendFile('logo.svg', { root: assetsDir }, error => {
487
- if (error && !res.headersSent) res.status(404).send('Not found');
488
- });
489
- };
490
-
491
- app.get('/', (req, res) => {
492
- try {
493
- const htmlPath = path.join(__dirname, '../web/index.html');
494
- const html = fs.readFileSync(htmlPath, 'utf8');
495
- res.send(html);
496
- } catch (e) {
497
- res.status(500).send('UI not found');
498
- }
499
- });
500
-
501
- const sendWebAsset = assetName => (req, res) => {
502
- res.sendFile(assetName, { root: webDir }, error => {
503
- if (error && !res.headersSent) res.status(404).send('Not found');
504
- });
505
- };
506
-
507
- const sendDependencyAsset = assetPath => (req, res) => {
508
- res.sendFile(path.basename(assetPath), { root: path.dirname(assetPath) });
509
- };
510
-
511
- const webAssets = [
512
- 'gitgraph.js',
513
- 'styles.css',
514
- 'theme.js',
515
- 'core.js',
516
- 'layout.js',
517
- 'notifications.js',
518
- 'claude.js',
519
- 'schedules.js',
520
- 'shell.js',
521
- 'codex.js',
522
- 'session.js',
523
- 'composer.js',
524
- 'timed-inputs.js',
525
- 'terminal-scroll.js',
526
- 'git.js',
527
- 'usage.js'
528
- ];
529
- for (const assetName of webAssets) {
530
- const escapedName = assetName.replace('.', '\\.');
531
- app.get([`/${assetName}`, new RegExp(`.*\\/${escapedName}$`)], sendWebAsset(assetName));
532
- }
533
-
534
- app.get('/vendor/xterm.js', sendDependencyAsset(xtermScript));
535
- app.get('/vendor/xterm.css', sendDependencyAsset(xtermStyles));
536
- app.get('/vendor/xterm-addon-fit.js', sendDependencyAsset(fitAddonScript));
537
-
538
- app.get(['/logo.svg', /.*\/logo\.svg$/], sendLogo);
539
-
540
- app.get(['/favicon.ico', /.*\/favicon\.ico$/], (req, res) => {
541
- res.type('image/svg+xml');
542
- sendLogo(req, res);
543
- });
544
-
545
- app.get(['/manifest.json', /.*\/manifest\.json$/], (req, res) => res.json({
546
- name: "Glad Web",
547
- short_name: "Glad",
548
- start_url: ".",
549
- display: "standalone",
550
- background_color: "#000000",
551
- theme_color: "#007aff",
552
- icons: [
553
- {
554
- src: "logo.svg",
555
- sizes: "any",
556
- type: "image/svg+xml"
557
- }
558
- ]
559
- }));
560
-
561
- server.listen(port, '0.0.0.0', () => {
562
- const interfaces = os.networkInterfaces();
563
- let networkInfo = '';
564
- for (const name of Object.keys(interfaces)) {
565
- for (const iface of interfaces[name]) {
566
- if (iface.family === 'IPv4' && !iface.internal) {
567
- networkInfo += `\n āžœ Network: http://${iface.address}:${port}`;
568
- }
569
- }
570
- }
571
- console.log(chalk.green(`\nšŸš€ Glad Web Server is running!`));
572
- console.log(chalk.cyan(` āžœ Local: http://localhost:${port}${networkInfo}\n`));
573
- console.log(chalk.gray(` āžœ Project: ${baseDir}\n`));
574
- console.log(chalk.gray(` āžœ History Render Tools: ${Array.from(renderHistoryTools).join(', ') || '(none)'}\n`));
575
- console.log(chalk.gray(`Tips: Access from your phone via the Network URL above.\n`));
576
- });
577
-
578
- process.on('SIGINT', () => {
579
- schedulerService.stop();
580
- notificationService.stop();
581
- sessionManager.killAll();
582
- process.exit(0);
583
- });
584
- }
585
-
586
- module.exports = webCommand;
@@ -1,17 +0,0 @@
1
- /**
2
- * Application constants
3
- *
4
- * All magic numbers and configuration values should be defined here.
5
- */
6
-
7
- module.exports = {
8
- // Network & WebSocket
9
- HEARTBEAT_TIMEOUT: 13000, // 13s - detect network loss (server pings every ~5s)
10
- CLI_IDLE_THRESHOLD: 15000, // 15s - no PTY output = CLI is idle
11
-
12
- // Buffer
13
- DEFAULT_BUFFER_SIZE: 100000, // 100KB - circular buffer max size
14
-
15
- // Reconnection
16
- MAX_RECONNECT_ATTEMPTS: 10, // Max WebSocket reconnection attempts
17
- };
@@ -1,89 +0,0 @@
1
- const Conf = require('conf');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- // Config schema - only user preferences, no environment config
6
- const schema = {
7
- defaultAI: {
8
- type: 'string',
9
- default: ''
10
- },
11
- serverChan: {
12
- type: 'object',
13
- properties: {
14
- sendKey: {
15
- type: 'string',
16
- default: ''
17
- },
18
- clientType: {
19
- type: 'string',
20
- enum: ['wechat', 'pushdeer'],
21
- default: 'wechat'
22
- }
23
- },
24
- default: {
25
- sendKey: '',
26
- clientType: 'wechat'
27
- }
28
- },
29
- version: {
30
- type: 'string',
31
- default: '1.0.0'
32
- },
33
- lastUpdated: {
34
- type: 'string',
35
- default: ''
36
- }
37
- };
38
-
39
- // Create config instance
40
- const config = new Conf({
41
- projectName: 'glad',
42
- cwd: path.join(os.homedir(), '.glad'),
43
- configName: 'config',
44
- schema
45
- });
46
-
47
- // Get config value
48
- function getConfig(key) {
49
- if (key) {
50
- return config.get(key);
51
- }
52
- return config.store;
53
- }
54
-
55
- // Set config value
56
- function setConfig(key, value) {
57
- config.set(key, value);
58
- config.set('lastUpdated', new Date().toISOString());
59
- }
60
-
61
- // Get default AI tool
62
- function getDefaultAI() {
63
- const value = config.get('defaultAI');
64
- return value || null;
65
- }
66
-
67
- // Set default AI tool
68
- function setDefaultAI(tool) {
69
- setConfig('defaultAI', tool);
70
- }
71
-
72
- // Get config file path
73
- function getConfigPath() {
74
- return config.path;
75
- }
76
-
77
- // Reset config to defaults
78
- function resetConfig() {
79
- config.clear();
80
- }
81
-
82
- module.exports = {
83
- getConfig,
84
- setConfig,
85
- getDefaultAI,
86
- setDefaultAI,
87
- getConfigPath,
88
- resetConfig
89
- };