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