glad-web 1.0.46 → 2.0.2

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 (74) 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 -61
  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 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  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/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. package/scripts/check-syntax.js +0 -26
@@ -1,104 +0,0 @@
1
- const { v4: uuidv4 } = require('uuid');
2
-
3
- function statusCode(error) {
4
- return Number(error?.statusCode) || 500;
5
- }
6
-
7
- function errorBody(error) {
8
- return {
9
- error: error?.message || 'SkillHub 操作失败',
10
- ...(error?.code ? { code: error.code } : {})
11
- };
12
- }
13
-
14
- function registerSkillHubRoutes(app, {
15
- settingsStore,
16
- client,
17
- installer,
18
- sessionManager
19
- }) {
20
- app.get('/api/skillhub/status', (_req, res) => {
21
- res.json({ available: installer.available === true });
22
- });
23
-
24
- app.get('/api/skillhub/settings', (_req, res) => {
25
- try { res.json(settingsStore.getPublic()); }
26
- catch (error) { res.status(statusCode(error)).json(errorBody(error)); }
27
- });
28
-
29
- app.put('/api/skillhub/settings', async (req, res) => {
30
- try {
31
- const resolved = settingsStore.resolve(req.body || {});
32
- const user = await client.test(resolved);
33
- const settings = settingsStore.save(resolved);
34
- res.json({ success: true, settings, user });
35
- } catch (error) {
36
- res.status(statusCode(error)).json(errorBody(error));
37
- }
38
- });
39
-
40
- app.delete('/api/skillhub/settings', (_req, res) => {
41
- try { res.json({ success: true, settings: settingsStore.clear() }); }
42
- catch (error) { res.status(statusCode(error)).json(errorBody(error)); }
43
- });
44
-
45
- app.post('/api/skillhub/settings/test', async (req, res) => {
46
- try {
47
- const settings = settingsStore.resolve(req.body || {});
48
- const user = await client.test(settings);
49
- res.json({ success: true, user });
50
- } catch (error) {
51
- res.status(statusCode(error)).json(errorBody(error));
52
- }
53
- });
54
-
55
- app.get('/api/skillhub/skills', async (_req, res) => {
56
- try {
57
- installer.assertAvailable();
58
- const skills = await client.listSkills();
59
- res.json({ success: true, skills });
60
- } catch (error) {
61
- res.status(statusCode(error)).json(errorBody(error));
62
- }
63
- });
64
-
65
- app.post('/api/skillhub/sessions', async (req, res) => {
66
- const sessionId = uuidv4();
67
- let created = false;
68
- try {
69
- if (req.body?.toolKey !== 'codex') {
70
- const error = new Error('SkillHub Session 当前只支持 Codex');
71
- error.statusCode = 400;
72
- error.code = 'SKILLHUB_CODEX_ONLY';
73
- throw error;
74
- }
75
- const activeSkill = await installer.prepare(sessionId, req.body?.skill || {});
76
- const session = sessionManager.create({
77
- id: sessionId,
78
- toolKey: 'codex',
79
- workingDirectory: req.body?.workingDirectory,
80
- name: activeSkill.name,
81
- codexOptions: {
82
- activeSkill,
83
- extraSkillRoots: [activeSkill.skillsRoot]
84
- },
85
- disposeResources: () => installer.cleanupSync(sessionId)
86
- });
87
- created = true;
88
- const defaultPrompt = activeSkill.defaultPrompt
89
- || '请先用中文介绍这个 Skill 能完成什么、适合哪些任务,以及用户接下来应该如何使用。这一轮只做使用引导。';
90
- const intro = `$${activeSkill.name}\n\n${defaultPrompt}`;
91
- const started = await sessionManager.sendCodexInput(session.id, intro, [], [activeSkill], []);
92
- if (!started) throw new Error('Codex Skill 引导会话启动失败');
93
- res.status(201).json({ id: session.id, name: session.name });
94
- } catch (error) {
95
- if (created) await sessionManager.kill(sessionId).catch(() => {});
96
- else {
97
- try { installer.cleanupSync(sessionId); } catch (_) { /* 目录可能尚未创建 */ }
98
- }
99
- res.status(statusCode(error)).json(errorBody(error));
100
- }
101
- });
102
- }
103
-
104
- module.exports = registerSkillHubRoutes;
@@ -1,23 +0,0 @@
1
- module.exports = function registerUsageRoutes(app, { usageService, sendJson = (_req, res, payload) => res.json(payload) }) {
2
- app.get('/api/usage/sources', async (req, res) => {
3
- try {
4
- res.json(await usageService.listSources(req.query.refresh === '1'));
5
- } catch (error) {
6
- res.status(error.statusCode || 500).json({ error: error.message || 'Failed to load usage sources' });
7
- }
8
- });
9
-
10
- app.get('/api/usage/report', async (req, res) => {
11
- try {
12
- const report = await usageService.getDashboard(
13
- req.query.source,
14
- req.query.scope || 'weekly',
15
- req.query.period,
16
- req.query.refresh === '1'
17
- );
18
- sendJson(req, res, report);
19
- } catch (error) {
20
- res.status(error.statusCode || 500).json({ error: error.message || 'Failed to load usage report' });
21
- }
22
- });
23
- };
@@ -1,77 +0,0 @@
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;
@@ -1,102 +0,0 @@
1
- const logger = require('../utils/logger');
2
-
3
- class CircularBuffer {
4
- constructor(maxSize = 100000) {
5
- this.maxSize = maxSize; // 100KB default
6
- this.buffer = [];
7
- this.totalSize = 0;
8
- this.currentSeq = 0;
9
- }
10
-
11
- previewText(text, maxChars = 240) {
12
- if (!text) return '';
13
- const normalized = String(text)
14
- .replace(/\r/g, '\\r')
15
- .replace(/\n/g, '\\n')
16
- .replace(/\t/g, '\\t')
17
- .replace(/\x1b/g, '\\x1b');
18
- return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
19
- }
20
-
21
- // Calculate byte size of data
22
- getByteSize(data) {
23
- return Buffer.byteLength(data, 'utf8');
24
- }
25
-
26
- // Append data to buffer
27
- append(data) {
28
- const timestamp = new Date().toISOString();
29
- const seq = ++this.currentSeq;
30
- const byteSize = this.getByteSize(data);
31
-
32
- const item = {
33
- seq,
34
- data,
35
- timestamp,
36
- size: byteSize
37
- };
38
-
39
- this.buffer.push(item);
40
- this.totalSize += byteSize;
41
-
42
- // Remove old items if buffer exceeds max size
43
- while (this.totalSize > this.maxSize && this.buffer.length > 0) {
44
- const removed = this.buffer.shift();
45
- this.totalSize -= removed.size;
46
- }
47
-
48
- return item;
49
- }
50
-
51
- // Get all items after a specific sequence number
52
- getAfter(seq) {
53
- return this.buffer.filter(item => item.seq > seq);
54
- }
55
-
56
- // Get all items
57
- getAll() {
58
- return [...this.buffer];
59
- }
60
-
61
- // Get current sequence number
62
- getCurrentSeq() {
63
- return this.currentSeq;
64
- }
65
-
66
- // Clear buffer
67
- clear() {
68
- logger.debug('Buffer: cleared');
69
- this.buffer = [];
70
- this.totalSize = 0;
71
- }
72
-
73
- // Get buffer stats
74
- getStats() {
75
- return {
76
- items: this.buffer.length,
77
- totalSize: this.totalSize,
78
- maxSize: this.maxSize,
79
- currentSeq: this.currentSeq,
80
- oldestSeq: this.buffer.length > 0 ? this.buffer[0].seq : null,
81
- newestSeq: this.buffer.length > 0 ? this.buffer[this.buffer.length - 1].seq : null
82
- };
83
- }
84
-
85
- getDebugSnapshot(maxItems = 5, maxChars = 240) {
86
- return {
87
- ...this.getStats(),
88
- tailItems: this.buffer.slice(-maxItems).map(item => ({
89
- seq: item.seq,
90
- size: item.size,
91
- timestamp: item.timestamp,
92
- preview: this.previewText(item.data, maxChars)
93
- })),
94
- combinedTailPreview: this.previewText(
95
- this.buffer.slice(-maxItems).map(item => item.data).join(''),
96
- maxChars
97
- )
98
- };
99
- }
100
- }
101
-
102
- module.exports = CircularBuffer;
@@ -1,168 +0,0 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const { v4: uuidv4 } = require('uuid');
5
-
6
- const MAX_BYTES = 50 * 1024 * 1024;
7
- const MAX_PER_SESSION = 8;
8
- const MAX_CHUNKS = 128;
9
- const CLEANUP_DELAY_MS = 30 * 60 * 1000;
10
-
11
- function inputError(message, statusCode = 400) {
12
- const error = new Error(message);
13
- error.statusCode = statusCode;
14
- return error;
15
- }
16
-
17
- function safeUploadId(value) {
18
- return typeof value === 'string' && /^[a-zA-Z0-9-]{8,100}$/.test(value);
19
- }
20
-
21
- function safeFileName(value) {
22
- let decoded = String(value || 'attachment.bin');
23
- try { decoded = decodeURIComponent(decoded); } catch (_) {}
24
- const base = decoded.replace(/\\/g, '/').split('/').pop() || 'attachment.bin';
25
- const cleaned = base.replace(/[\u0000-\u001f\u007f<>:"|?*]/g, '_').trim().slice(0, 160);
26
- return cleaned || 'attachment.bin';
27
- }
28
-
29
- class FileAttachmentStore {
30
- constructor({ logger, root, uploadRoot } = {}) {
31
- this.logger = logger || console;
32
- this.root = root || path.join(os.tmpdir(), 'glad', 'session-files');
33
- this.uploadRoot = uploadRoot || path.join(os.tmpdir(), 'glad', 'session-file-uploads');
34
- }
35
-
36
- assertSession(session) {
37
- if (!session) throw inputError('Session not found', 404);
38
- if (!session.fileAttachments || !session.fileUploads) throw inputError('File attachments are unavailable for this session');
39
- }
40
-
41
- async appendChunk(session, input = {}, bytes) {
42
- this.assertSession(session);
43
- if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw inputError('File chunk is required');
44
- const uploadId = String(input.uploadId || '');
45
- const chunkIndex = Number(input.chunkIndex);
46
- const chunkTotal = Number(input.chunkTotal);
47
- const name = safeFileName(input.name);
48
- if (!safeUploadId(uploadId)) throw inputError('Invalid file upload id');
49
- if (!Number.isInteger(chunkIndex) || !Number.isInteger(chunkTotal) || chunkIndex < 0 || chunkTotal < 1 || chunkTotal > MAX_CHUNKS || chunkIndex >= chunkTotal) {
50
- throw inputError('Invalid file chunk metadata');
51
- }
52
-
53
- let upload = session.fileUploads.get(uploadId);
54
- if (!upload) {
55
- if (chunkIndex !== 0) throw inputError('File upload must start with the first chunk');
56
- const directory = path.join(this.uploadRoot, session.id);
57
- await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
58
- upload = { id: uploadId, name, path: path.join(directory, `${uploadId}.part`), chunkTotal, nextChunkIndex: 0, bytes: 0 };
59
- session.fileUploads.set(uploadId, upload);
60
- }
61
- if (upload.name !== name || upload.chunkTotal !== chunkTotal || upload.nextChunkIndex !== chunkIndex) {
62
- throw inputError('File chunks arrived out of order');
63
- }
64
- if (upload.bytes + bytes.length > MAX_BYTES) {
65
- await this.discardUpload(session, uploadId);
66
- throw inputError('File must be 50 MB or smaller');
67
- }
68
-
69
- if (chunkIndex === 0) await fs.promises.writeFile(upload.path, bytes, { mode: 0o600, flag: 'wx' });
70
- else await fs.promises.appendFile(upload.path, bytes, { mode: 0o600 });
71
- upload.bytes += bytes.length;
72
- upload.nextChunkIndex += 1;
73
- if (upload.nextChunkIndex < upload.chunkTotal) {
74
- return { complete: false, receivedChunks: upload.nextChunkIndex, size: upload.bytes };
75
- }
76
-
77
- const pendingAttachmentCount = Array.from(session.fileAttachments.values()).filter(item => !item.sent).length;
78
- if (pendingAttachmentCount >= MAX_PER_SESSION) {
79
- await this.discardUpload(session, uploadId);
80
- throw inputError(`You can attach at most ${MAX_PER_SESSION} files at a time`);
81
- }
82
- const directory = path.join(this.root, session.id);
83
- await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
84
- const attachment = {
85
- id: uuidv4(),
86
- name: upload.name,
87
- path: path.join(directory, `${uuidv4()}-${upload.name}`),
88
- size: upload.bytes,
89
- createdAt: Date.now(),
90
- cleanupTimer: null
91
- };
92
- session.fileUploads.delete(uploadId);
93
- try {
94
- await fs.promises.rename(upload.path, attachment.path);
95
- await fs.promises.chmod(attachment.path, 0o600);
96
- session.fileAttachments.set(attachment.id, attachment);
97
- return { complete: true, attachment: { id: attachment.id, name: attachment.name, size: attachment.size, kind: 'file' } };
98
- } catch (error) {
99
- await fs.promises.rm(upload.path, { force: true });
100
- throw error;
101
- }
102
- }
103
-
104
- async discardUpload(session, uploadId) {
105
- if (!session?.fileUploads || !safeUploadId(uploadId)) return false;
106
- const upload = session.fileUploads.get(uploadId);
107
- if (!upload) return false;
108
- session.fileUploads.delete(uploadId);
109
- await fs.promises.rm(upload.path, { force: true });
110
- return true;
111
- }
112
-
113
- async discardAttachment(session, attachmentId) {
114
- if (!session?.fileAttachments) return false;
115
- const attachment = session.fileAttachments.get(attachmentId);
116
- if (!attachment) return false;
117
- clearTimeout(attachment.cleanupTimer);
118
- session.fileAttachments.delete(attachmentId);
119
- await fs.promises.rm(attachment.path, { force: true });
120
- return true;
121
- }
122
-
123
- resolve(session, attachmentIds = []) {
124
- const ids = Array.isArray(attachmentIds) ? attachmentIds : [];
125
- if (!session) throw inputError('Session not found', 404);
126
- if (ids.length === 0) return [];
127
- this.assertSession(session);
128
- if (ids.length > MAX_PER_SESSION) throw inputError(`You can attach at most ${MAX_PER_SESSION} files at a time`);
129
- const uniqueIds = [...new Set(ids.map(String))];
130
- if (uniqueIds.length !== ids.length) throw inputError('Duplicate file attachment');
131
- return uniqueIds.map(attachmentId => {
132
- const attachment = session.fileAttachments.get(attachmentId);
133
- if (!attachment) throw inputError('File attachment is no longer available');
134
- return attachment;
135
- });
136
- }
137
-
138
- scheduleCleanup(session, attachmentIds) {
139
- if (!session?.fileAttachments) return;
140
- for (const attachmentId of attachmentIds) {
141
- const attachment = session.fileAttachments.get(attachmentId);
142
- if (!attachment) continue;
143
- attachment.sent = true;
144
- clearTimeout(attachment.cleanupTimer);
145
- attachment.cleanupTimer = setTimeout(() => {
146
- this.discardAttachment(session, attachmentId).catch(error => {
147
- this.logger.debugInfo?.(`[file-attachment] cleanup failed: ${error.message}`);
148
- });
149
- }, CLEANUP_DELAY_MS);
150
- attachment.cleanupTimer.unref?.();
151
- }
152
- }
153
-
154
- clear(session) {
155
- if (!session) return;
156
- for (const attachment of session.fileAttachments?.values?.() || []) clearTimeout(attachment.cleanupTimer);
157
- session.fileAttachments?.clear?.();
158
- session.fileUploads?.clear?.();
159
- fs.promises.rm(path.join(this.root, session.id), { recursive: true, force: true }).catch(error => {
160
- this.logger.debugInfo?.(`[file-attachment] cleanup failed: ${error.message}`);
161
- });
162
- fs.promises.rm(path.join(this.uploadRoot, session.id), { recursive: true, force: true }).catch(error => {
163
- this.logger.debugInfo?.(`[file-upload] cleanup failed: ${error.message}`);
164
- });
165
- }
166
- }
167
-
168
- module.exports = FileAttachmentStore;