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
package/lib/web/usage.js DELETED
@@ -1,323 +0,0 @@
1
- const usageState = {
2
- source: null,
3
- scope: 'weekly',
4
- selectedPeriod: null,
5
- sources: [],
6
- requestSequence: 0
7
- };
8
-
9
- const usageModelColors = [
10
- '#0a84ff', '#30d158', '#bf5af2', '#ff9f0a', '#ff453a', '#64d2ff',
11
- '#ffd60a', '#5e5ce6', '#ff375f', '#66d4cf', '#ac8e68', '#8e8e93'
12
- ];
13
-
14
- function formatExactTokens(value) {
15
- return new Intl.NumberFormat().format(Number(value) || 0);
16
- }
17
-
18
- function formatCompactNumber(value) {
19
- const number = Number(value) || 0;
20
- if (number < 1000) return formatExactTokens(number);
21
- return new Intl.NumberFormat(undefined, {
22
- notation: 'compact',
23
- maximumFractionDigits: number >= 1000000 ? 2 : 1
24
- }).format(number);
25
- }
26
-
27
- function formatEstimatedCost(value, compact = false) {
28
- if (value === null || value === undefined) return '—';
29
- const amount = Number(value) || 0;
30
- return new Intl.NumberFormat(undefined, {
31
- style: 'currency',
32
- currency: 'USD',
33
- notation: compact && amount >= 1000 ? 'compact' : 'standard',
34
- minimumFractionDigits: compact ? 2 : (amount < 1 ? 3 : 2),
35
- maximumFractionDigits: compact ? 2 : (amount < 1 ? 4 : 2)
36
- }).format(amount);
37
- }
38
-
39
- function closeUsageSourceModal(event) {
40
- if (event && event.target.id !== 'usage-source-overlay') return;
41
- document.getElementById('usage-source-overlay').style.display = 'none';
42
- }
43
-
44
- async function showUsageSourceModal(options = {}) {
45
- const overlay = document.getElementById('usage-source-overlay');
46
- overlay.style.display = 'flex';
47
- const hasCachedSources = usageState.sources.length > 0;
48
- if (hasCachedSources && !options.refresh) {
49
- renderUsageSources();
50
- } else {
51
- document.getElementById('usage-sources-list').innerHTML = '<p class="usage-modal-state">Reading local usage data...</p>';
52
- }
53
- const list = document.getElementById('usage-sources-list');
54
- try {
55
- const suffix = options.refresh ? '?refresh=1' : '';
56
- const response = await fetchWithTimeout(`/api/usage/sources${suffix}`, {}, 60000);
57
- const data = await response.json();
58
- if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
59
- usageState.sources = Array.isArray(data.sources) ? data.sources : [];
60
- renderUsageSources();
61
- } catch (error) {
62
- if (hasCachedSources) return;
63
- list.innerHTML = `<div class="usage-modal-state">Unable to read usage data.<br>${escapeHtml(error.message)}<br><button class="btn-retry" type="button" onclick="showUsageSourceModal({ refresh: true })">Retry</button></div>`;
64
- }
65
- }
66
-
67
- function renderUsageSources() {
68
- const list = document.getElementById('usage-sources-list');
69
- if (!usageState.sources.length) {
70
- list.innerHTML = '<p class="usage-modal-state">No supported local CLI usage history was found.</p>';
71
- return;
72
- }
73
- list.innerHTML = usageState.sources.map(source => `
74
- <button class="usage-source-item" type="button" onclick="openUsageDashboard('${escapeHtml(source.id)}')">
75
- <span class="usage-source-badge">${escapeHtml(source.badge)}</span>
76
- <span class="usage-source-copy"><strong>${escapeHtml(source.label)}</strong><span>Local token history</span></span>
77
- <span class="usage-source-arrow">›</span>
78
- </button>`).join('');
79
- }
80
-
81
- async function openUsageDashboard(sourceId) {
82
- const source = usageState.sources.find(item => item.id === sourceId);
83
- usageState.source = source || { id: sourceId, label: sourceId };
84
- usageState.selectedPeriod = null;
85
- closeUsageSourceModal();
86
- document.querySelectorAll('.view').forEach(view => view.classList.remove('active'));
87
- document.getElementById('usage-view').classList.add('active');
88
- document.getElementById('usage-source-title').textContent = `${usageState.source.label} Usage`;
89
- await loadUsageDashboard();
90
- }
91
-
92
- async function setUsageScope(scope) {
93
- if (!['weekly', 'monthly'].includes(scope) || usageState.scope === scope) return;
94
- usageState.scope = scope;
95
- usageState.selectedPeriod = null;
96
- updateUsageScopeButtons();
97
- if (usageState.source) await loadUsageDashboard();
98
- }
99
-
100
- function updateUsageScopeButtons() {
101
- for (const scope of ['weekly', 'monthly']) {
102
- document.getElementById(`usage-scope-${scope}`).classList.toggle('active', usageState.scope === scope);
103
- }
104
- }
105
-
106
- async function selectUsagePeriod(period) {
107
- if (!period || usageState.selectedPeriod === period) return;
108
- usageState.selectedPeriod = period;
109
- await loadUsageDashboard();
110
- }
111
-
112
- async function refreshUsageDashboard() {
113
- if (!usageState.source) return;
114
- await loadUsageDashboard(true);
115
- }
116
-
117
- function setUsageLoading(loading, message = 'Loading usage data...') {
118
- const state = document.getElementById('usage-loading');
119
- const dashboard = document.getElementById('usage-dashboard');
120
- const refresh = document.getElementById('usage-refresh-button');
121
- state.classList.remove('error');
122
- state.textContent = message;
123
- state.hidden = !loading;
124
- dashboard.hidden = loading;
125
- refresh.classList.toggle('loading', loading);
126
- refresh.disabled = loading;
127
- }
128
-
129
- async function loadUsageDashboard(refresh = false) {
130
- const requestSequence = ++usageState.requestSequence;
131
- setUsageLoading(true);
132
- try {
133
- const query = new URLSearchParams({ source: usageState.source.id, scope: usageState.scope });
134
- if (usageState.selectedPeriod) query.set('period', usageState.selectedPeriod);
135
- if (refresh) query.set('refresh', '1');
136
- const response = await fetchWithTimeout(`/api/usage/report?${query}`, {}, 60000);
137
- const data = await response.json();
138
- if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
139
- if (requestSequence !== usageState.requestSequence) return;
140
- renderUsageDashboard(data);
141
- } catch (error) {
142
- if (requestSequence !== usageState.requestSequence) return;
143
- const state = document.getElementById('usage-loading');
144
- state.classList.add('error');
145
- state.innerHTML = `Unable to load usage: ${escapeHtml(error.message)}<br><button class="btn-retry" type="button" onclick="loadUsageDashboard(true)">Retry</button>`;
146
- state.hidden = false;
147
- document.getElementById('usage-dashboard').hidden = true;
148
- } finally {
149
- if (requestSequence !== usageState.requestSequence) return;
150
- const refreshButton = document.getElementById('usage-refresh-button');
151
- refreshButton.classList.remove('loading');
152
- refreshButton.disabled = false;
153
- }
154
- }
155
-
156
- function renderPeriodPicker(report) {
157
- usageState.selectedPeriod = report.selectedPeriod;
158
- const select = document.getElementById('usage-period-select');
159
- select.innerHTML = (report.availablePeriods || []).map(period =>
160
- `<option value="${escapeHtml(period)}"${period === report.selectedPeriod ? ' selected' : ''}>${escapeHtml(period)}</option>`
161
- ).join('');
162
- select.disabled = !report.availablePeriods || report.availablePeriods.length === 0;
163
- }
164
-
165
- function totalSummaryCard(label, value, cost = false) {
166
- const display = cost ? formatEstimatedCost(value, true) : formatCompactNumber(value);
167
- const exact = cost ? formatEstimatedCost(value) : `${formatExactTokens(value)} tokens`;
168
- return `<article class="usage-summary-card" title="${escapeHtml(exact)}">
169
- <span class="label"><i class="dot ${cost ? 'cost' : 'tokens'}"></i>${escapeHtml(label)}</span>
170
- <strong class="value">${escapeHtml(display)}</strong>
171
- <span class="exact">${escapeHtml(exact)}</span>
172
- </article>`;
173
- }
174
-
175
- function renderAllModelTotals(report) {
176
- const totals = report.summary && report.summary.totals ? report.summary.totals : {};
177
- const cards = [totalSummaryCard('All-model tokens', totals.totalTokens)];
178
- if (totals.estimatedCostUSD !== null && totals.estimatedCostUSD !== undefined) {
179
- cards.push(totalSummaryCard('All GPT cost', totals.estimatedCostUSD, true));
180
- }
181
- document.getElementById('usage-summary').innerHTML = cards.join('');
182
- }
183
-
184
- function renderModelSummary(report) {
185
- const models = report.summary && Array.isArray(report.summary.models) ? report.summary.models : [];
186
- const totals = report.summary && report.summary.totals ? report.summary.totals : {};
187
- const hasCost = totals.estimatedCostUSD !== null && totals.estimatedCostUSD !== undefined;
188
- const container = document.getElementById('usage-model-summary');
189
- if (!models.length) {
190
- container.innerHTML = '<div class="usage-empty">No usage in this period.</div>';
191
- return;
192
- }
193
- container.innerHTML = `<table class="usage-table">
194
- <thead><tr><th>Model</th><th>Uncached input</th><th>Cached input</th><th>Output</th><th>Total tokens</th>${hasCost ? '<th>Cost</th>' : ''}</tr></thead>
195
- <tbody>${models.map(model => `<tr>
196
- <td class="usage-models" title="${escapeHtml(model.modelName)}">${escapeHtml(model.modelName)}</td>
197
- <td>${escapeHtml(formatExactTokens(model.uncachedInputTokens))}</td>
198
- <td>${escapeHtml(formatExactTokens(model.cachedInputTokens))}</td>
199
- <td>${escapeHtml(formatExactTokens(model.outputTokens))}</td>
200
- <td>${escapeHtml(formatExactTokens(model.totalTokens))}</td>
201
- ${hasCost ? `<td>${escapeHtml(formatEstimatedCost(model.estimatedCostUSD))}</td>` : ''}
202
- </tr>`).join('')}</tbody>
203
- <tfoot><tr><td>All models</td><td>${escapeHtml(formatExactTokens(totals.uncachedInputTokens))}</td><td>${escapeHtml(formatExactTokens(totals.cachedInputTokens))}</td><td>${escapeHtml(formatExactTokens(totals.outputTokens))}</td><td>${escapeHtml(formatExactTokens(totals.totalTokens))}</td>${hasCost ? `<td>${escapeHtml(formatEstimatedCost(totals.estimatedCostUSD))}</td>` : ''}</tr></tfoot>
204
- </table>`;
205
- }
206
-
207
- function collectChartModels(days, metric) {
208
- const names = [];
209
- const seen = new Set();
210
- for (const day of days) {
211
- for (const model of day.models || []) {
212
- const value = model[metric];
213
- if ((value === null || value === undefined || Number(value) <= 0) || seen.has(model.modelName)) continue;
214
- seen.add(model.modelName);
215
- names.push(model.modelName);
216
- }
217
- }
218
- return names.sort((a, b) => a.localeCompare(b));
219
- }
220
-
221
- function modelColorMap(modelNames) {
222
- return new Map(modelNames.map((name, index) => [
223
- name,
224
- usageModelColors[index] || `hsl(${(index * 47) % 360} 75% 58%)`
225
- ]));
226
- }
227
-
228
- function renderModelLegend(elementId, modelNames, colors) {
229
- document.getElementById(elementId).innerHTML = modelNames.map(name =>
230
- `<span title="${escapeHtml(name)}"><i style="background:${colors.get(name)}"></i>${escapeHtml(name)}</span>`
231
- ).join('');
232
- }
233
-
234
- function renderStackedModelChart(report, options) {
235
- const days = report.days || [];
236
- const modelNames = collectChartModels(days, options.metric);
237
- const colors = modelColorMap(modelNames);
238
- const container = document.getElementById(options.containerId);
239
- renderModelLegend(options.legendId, modelNames, colors);
240
- if (!days.length || !modelNames.length) {
241
- container.innerHTML = `<div class="usage-empty">${escapeHtml(options.emptyText)}</div>`;
242
- return false;
243
- }
244
- const dayTotals = days.map(day => (day.models || []).reduce((sum, model) => {
245
- const value = model[options.metric];
246
- return sum + (value === null || value === undefined ? 0 : Number(value) || 0);
247
- }, 0));
248
- const maximum = Math.max(...dayTotals, 1);
249
- container.innerHTML = days.map((day, dayIndex) => {
250
- const segments = modelNames.map(name => {
251
- const model = (day.models || []).find(item => item.modelName === name);
252
- const value = model && model[options.metric] !== null ? Number(model[options.metric]) || 0 : 0;
253
- if (value <= 0) return '';
254
- const title = `${name}: ${options.formatExact(value)}`;
255
- return `<span title="${escapeHtml(title)}" style="width:${value / maximum * 100}%;background:${colors.get(name)}"></span>`;
256
- }).join('');
257
- return `<div class="usage-chart-row">
258
- <span class="usage-chart-label">${escapeHtml(day.period)}</span>
259
- <div class="usage-chart-track">${segments}</div>
260
- <span class="usage-chart-total">${escapeHtml(options.formatCompact(dayTotals[dayIndex]))}</span>
261
- </div>`;
262
- }).join('');
263
- return true;
264
- }
265
-
266
- function renderDailyTable(report) {
267
- const days = (report.days || []).slice().reverse();
268
- const hasCost = days.some(day => day.totals && day.totals.estimatedCostUSD !== null);
269
- const container = document.getElementById('usage-daily-table');
270
- if (!days.length) {
271
- container.innerHTML = '<div class="usage-empty">No daily usage in this period.</div>';
272
- return;
273
- }
274
- container.innerHTML = `<table class="usage-table">
275
- <thead><tr><th>Date</th><th>Total tokens</th>${hasCost ? '<th>Cost</th>' : ''}<th>Models</th></tr></thead>
276
- <tbody>${days.map(day => `<tr>
277
- <td>${escapeHtml(day.period)}</td>
278
- <td>${escapeHtml(formatExactTokens(day.totals.totalTokens))}</td>
279
- ${hasCost ? `<td>${escapeHtml(formatEstimatedCost(day.totals.estimatedCostUSD))}</td>` : ''}
280
- <td class="usage-models" title="${escapeHtml(day.models.map(model => model.modelName).join(', '))}">${escapeHtml(day.models.map(model => model.modelName).join(', ') || '—')}</td>
281
- </tr>`).join('')}</tbody>
282
- </table>`;
283
- }
284
-
285
- function renderEngineNote(report) {
286
- const engine = report.engine || { name: 'ccusage', version: 'unknown' };
287
- const pricingMode = engine.pricingMode === 'embedded' ? 'embedded pricing' : 'pricing';
288
- const parts = [`Statistics and ${pricingMode} calculated by ${engine.name} ${engine.version}`];
289
- if (report.cost) parts.push(report.cost.note);
290
- else parts.push('Cost is only shown for GPT models used by Codex.');
291
- document.getElementById('usage-engine-note').textContent = parts.join(' · ');
292
- }
293
-
294
- function renderUsageDashboard(report) {
295
- setUsageLoading(false);
296
- updateUsageScopeButtons();
297
- renderPeriodPicker(report);
298
- const updated = report.generatedAt ? new Date(report.generatedAt) : null;
299
- document.getElementById('usage-updated-at').textContent = updated && !Number.isNaN(updated.getTime())
300
- ? `Updated ${updated.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
301
- : '';
302
- renderAllModelTotals(report);
303
- renderModelSummary(report);
304
- renderStackedModelChart(report, {
305
- containerId: 'usage-token-chart',
306
- legendId: 'usage-token-legend',
307
- metric: 'totalTokens',
308
- emptyText: 'No token data in this period.',
309
- formatExact: value => `${formatExactTokens(value)} tokens`,
310
- formatCompact: formatCompactNumber
311
- });
312
- const hasCostChart = renderStackedModelChart(report, {
313
- containerId: 'usage-cost-chart',
314
- legendId: 'usage-cost-legend',
315
- metric: 'estimatedCostUSD',
316
- emptyText: 'No Codex GPT cost estimate in this period.',
317
- formatExact: formatEstimatedCost,
318
- formatCompact: value => formatEstimatedCost(value, true)
319
- });
320
- document.getElementById('usage-cost-panel').hidden = !hasCostChart;
321
- renderDailyTable(report);
322
- renderEngineNote(report);
323
- }
@@ -1,77 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- class WorkspaceService {
5
- constructor({ gitService } = {}) {
6
- this.gitService = gitService || null;
7
- }
8
-
9
- resolveInside(rootDir, targetPath = '') {
10
- const root = fs.realpathSync(path.resolve(rootDir));
11
- const requestedPath = path.resolve(root, String(targetPath || ''));
12
- const fullPath = fs.realpathSync(requestedPath);
13
- const relative = path.relative(root, fullPath);
14
-
15
- if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
16
- return fullPath;
17
- }
18
-
19
- const error = new Error('Access denied');
20
- error.statusCode = 403;
21
- throw error;
22
- }
23
-
24
- readFile(rootDir, filePath) {
25
- const fullPath = this.resolveInside(rootDir, filePath);
26
- return fs.readFileSync(fullPath, 'utf8');
27
- }
28
-
29
- async listDirectory(rootDir, dirPath = '') {
30
- const fullPath = this.resolveInside(rootDir, dirPath);
31
- const entries = fs.readdirSync(fullPath, { withFileTypes: true });
32
- let files = entries.map(entry => ({
33
- name: entry.name,
34
- isDirectory: entry.isDirectory()
35
- })).sort((a, b) => {
36
- if (a.isDirectory && !b.isDirectory) return -1;
37
- if (!a.isDirectory && b.isDirectory) return 1;
38
- return a.name.localeCompare(b.name);
39
- });
40
-
41
- if (!this.gitService) return files;
42
-
43
- try {
44
- const gitResult = await this.gitService.status(rootDir);
45
- if (!gitResult.success || !Array.isArray(gitResult.files)) return files;
46
-
47
- const gitMap = new Map();
48
- gitResult.files.forEach(entry => {
49
- gitMap.set(entry.path, entry.status);
50
- });
51
-
52
- files = files.map(file => {
53
- const relativePath = dirPath ? `${dirPath}/${file.name}` : file.name;
54
- let gitStatus = null;
55
-
56
- if (file.isDirectory) {
57
- for (const [gitFile] of gitMap.entries()) {
58
- if (gitFile.startsWith(relativePath + '/')) {
59
- gitStatus = 'M';
60
- break;
61
- }
62
- }
63
- } else if (gitMap.has(relativePath)) {
64
- gitStatus = gitMap.get(relativePath);
65
- }
66
-
67
- return { ...file, gitStatus };
68
- });
69
- } catch (_) {
70
- return files;
71
- }
72
-
73
- return files;
74
- }
75
- }
76
-
77
- module.exports = WorkspaceService;
@@ -1,26 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { spawnSync } = require('child_process');
4
-
5
- const projectRoot = path.resolve(__dirname, '..');
6
- const sourceRoots = ['bin', 'lib', 'scripts', 'tests'];
7
- const standaloneFiles = ['playwright.config.js'];
8
-
9
- function collectJavaScriptFiles(directory) {
10
- return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
11
- const fullPath = path.join(directory, entry.name);
12
- if (entry.isDirectory()) return collectJavaScriptFiles(fullPath);
13
- return entry.isFile() && entry.name.endsWith('.js') ? [fullPath] : [];
14
- });
15
- }
16
-
17
- const files = [
18
- ...sourceRoots.flatMap(sourceRoot => collectJavaScriptFiles(path.join(projectRoot, sourceRoot))),
19
- ...standaloneFiles.map(file => path.join(projectRoot, file))
20
- ];
21
- for (const file of files) {
22
- const result = spawnSync(process.execPath, ['--check', file], { stdio: 'inherit' });
23
- if (result.status !== 0) process.exit(result.status || 1);
24
- }
25
-
26
- console.log(`Checked ${files.length} JavaScript files.`);