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,274 +0,0 @@
1
- class TextHistory {
2
- constructor(options = {}) {
3
- this.maxBytes = options.maxBytes || 5 * 1024 * 1024;
4
- this.debugLabel = options.debugLabel || 'session';
5
- this.lines = [''];
6
- this.row = 0;
7
- this.col = 0;
8
- this.updatedAt = Date.now();
9
- this.truncated = false;
10
- this.totalWrites = 0;
11
- this.totalBytes = 0;
12
- this.escapeCount = 0;
13
- this.clearEvents = 0;
14
- this.eraseLineEvents = 0;
15
- this.cursorMoveEvents = 0;
16
- this.trimEvents = 0;
17
- this.lastEvents = [];
18
- }
19
-
20
- write(data) {
21
- if (!data) return;
22
- const text = String(data);
23
- this.totalWrites += 1;
24
- this.totalBytes += Buffer.byteLength(text, 'utf8');
25
-
26
- for (let i = 0; i < text.length; i++) {
27
- const ch = text[i];
28
-
29
- if (ch === '\x1b') {
30
- this.escapeCount += 1;
31
- i = this.skipEscape(text, i);
32
- continue;
33
- }
34
-
35
- if (ch === '\r') {
36
- this.col = 0;
37
- continue;
38
- }
39
-
40
- if (ch === '\n') {
41
- this.newLine();
42
- continue;
43
- }
44
-
45
- if (ch === '\b') {
46
- this.col = Math.max(0, this.col - 1);
47
- continue;
48
- }
49
-
50
- if (ch === '\t') {
51
- const spaces = 4 - (this.col % 4);
52
- for (let j = 0; j < spaces; j++) this.writeChar(' ');
53
- continue;
54
- }
55
-
56
- if (ch >= ' ' || ch === '\u00a0') {
57
- this.writeChar(ch);
58
- }
59
- }
60
-
61
- this.updatedAt = Date.now();
62
- this.trim();
63
- }
64
-
65
- toJSON() {
66
- return {
67
- text: this.toString(),
68
- updatedAt: this.updatedAt,
69
- truncated: this.truncated,
70
- bytes: Buffer.byteLength(this.toString(), 'utf8'),
71
- lines: this.lines.length
72
- };
73
- }
74
-
75
- getDebugSnapshot(options = {}) {
76
- const tailLines = options.tailLines || 12;
77
- return {
78
- label: this.debugLabel,
79
- updatedAt: this.updatedAt,
80
- truncated: this.truncated,
81
- row: this.row,
82
- col: this.col,
83
- lines: this.lines.length,
84
- bytes: Buffer.byteLength(this.lines.join('\n'), 'utf8'),
85
- totalWrites: this.totalWrites,
86
- totalBytes: this.totalBytes,
87
- escapeCount: this.escapeCount,
88
- clearEvents: this.clearEvents,
89
- eraseLineEvents: this.eraseLineEvents,
90
- cursorMoveEvents: this.cursorMoveEvents,
91
- trimEvents: this.trimEvents,
92
- lastEvents: [...this.lastEvents],
93
- tailPreview: this.previewText(this.lines.slice(-tailLines).join('\n'))
94
- };
95
- }
96
-
97
- toString() {
98
- return this.lines.join('\n').replace(/\s+$/g, '');
99
- }
100
-
101
- writeChar(ch) {
102
- this.ensureRow();
103
- const line = this.lines[this.row] || '';
104
- const padded = line.length < this.col ? line + ' '.repeat(this.col - line.length) : line;
105
- this.lines[this.row] = padded.slice(0, this.col) + ch + padded.slice(this.col + 1);
106
- this.col += 1;
107
- }
108
-
109
- newLine() {
110
- this.row += 1;
111
- this.col = 0;
112
- this.ensureRow();
113
- }
114
-
115
- ensureRow() {
116
- while (this.row >= this.lines.length) {
117
- this.lines.push('');
118
- }
119
- }
120
-
121
- skipEscape(text, index) {
122
- const next = text[index + 1];
123
-
124
- if (next === ']') {
125
- return this.skipOsc(text, index + 2);
126
- }
127
-
128
- if (next === '[') {
129
- return this.handleCsi(text, index + 2);
130
- }
131
-
132
- return Math.min(index + 1, text.length - 1);
133
- }
134
-
135
- skipOsc(text, index) {
136
- for (let i = index; i < text.length; i++) {
137
- if (text[i] === '\x07') return i;
138
- if (text[i] === '\x1b' && text[i + 1] === '\\') return i + 1;
139
- }
140
- return text.length - 1;
141
- }
142
-
143
- handleCsi(text, index) {
144
- let i = index;
145
- while (i < text.length && !/[A-Za-z@`~]/.test(text[i])) {
146
- i++;
147
- }
148
- if (i >= text.length) return text.length - 1;
149
-
150
- const params = text.slice(index, i);
151
- const command = text[i];
152
- this.applyCsi(params, command);
153
- return i;
154
- }
155
-
156
- applyCsi(params, command) {
157
- const values = params
158
- .replace(/[?>!]/g, '')
159
- .split(';')
160
- .filter(Boolean)
161
- .map(value => Number.parseInt(value, 10))
162
- .map(value => Number.isFinite(value) ? value : 0);
163
- const first = values[0] || 0;
164
-
165
- if (command === 'K') {
166
- this.eraseLineEvents += 1;
167
- this.recordEvent(`CSI K(${first})`);
168
- this.eraseLine(first);
169
- return;
170
- }
171
-
172
- if (command === 'J') {
173
- if (first === 2 || first === 3) {
174
- this.clearEvents += 1;
175
- this.recordEvent(`CSI J(${first}) ignored-clear`);
176
- this.startFreshLineAfterClear();
177
- }
178
- return;
179
- }
180
-
181
- if (command === 'H' || command === 'f') {
182
- this.cursorMoveEvents += 1;
183
- this.recordEvent(`CSI ${command}(${params || ''}) ignored-cursor`);
184
- return;
185
- }
186
-
187
- if (command === 'A') {
188
- this.cursorMoveEvents += 1;
189
- this.recordEvent(`CSI A(${first || 1}) ignored-cursor`);
190
- return;
191
- }
192
-
193
- if (command === 'B') {
194
- this.cursorMoveEvents += 1;
195
- this.recordEvent(`CSI B(${first || 1}) ignored-cursor`);
196
- return;
197
- }
198
-
199
- if (command === 'C') {
200
- this.cursorMoveEvents += 1;
201
- this.recordEvent(`CSI C(${first || 1}) ignored-cursor`);
202
- return;
203
- }
204
-
205
- if (command === 'D') {
206
- this.cursorMoveEvents += 1;
207
- this.recordEvent(`CSI D(${first || 1}) ignored-cursor`);
208
- return;
209
- }
210
-
211
- if (command === 'G') {
212
- this.cursorMoveEvents += 1;
213
- this.recordEvent(`CSI G(${first || 1}) ignored-cursor`);
214
- }
215
- }
216
-
217
- eraseLine(mode) {
218
- this.ensureRow();
219
- const line = this.lines[this.row] || '';
220
- if (mode === 1) {
221
- this.lines[this.row] = ' '.repeat(Math.min(this.col, line.length)) + line.slice(this.col);
222
- } else if (mode === 2) {
223
- this.lines[this.row] = '';
224
- this.col = 0;
225
- } else {
226
- this.lines[this.row] = line.slice(0, this.col);
227
- }
228
- }
229
-
230
- startFreshLineAfterClear() {
231
- const hasContent = this.lines.some(line => line.length > 0);
232
- const currentLine = this.lines[this.row] || '';
233
- if (hasContent && currentLine.length > 0) this.newLine();
234
- this.row = this.lines.length - 1;
235
- this.col = 0;
236
- this.lines[this.row] = '';
237
- }
238
-
239
- trim() {
240
- let bytes = Buffer.byteLength(this.lines.join('\n'), 'utf8');
241
- while (bytes > this.maxBytes && this.lines.length > 1) {
242
- const removed = this.lines.shift();
243
- bytes -= Buffer.byteLength(removed, 'utf8') + 1;
244
- this.row = Math.max(0, this.row - 1);
245
- this.truncated = true;
246
- this.trimEvents += 1;
247
- }
248
-
249
- if (bytes > this.maxBytes && this.lines.length === 1) {
250
- const keepChars = Math.floor(this.maxBytes / 2);
251
- this.lines[0] = this.lines[0].slice(-keepChars);
252
- this.col = Math.min(this.col, this.lines[0].length);
253
- this.truncated = true;
254
- this.trimEvents += 1;
255
- }
256
- }
257
-
258
- recordEvent(message) {
259
- this.lastEvents.push(`${new Date().toISOString()} ${message}`);
260
- if (this.lastEvents.length > 25) this.lastEvents.shift();
261
- }
262
-
263
- previewText(text, maxChars = 400) {
264
- if (!text) return '';
265
- const normalized = String(text)
266
- .replace(/\r/g, '\\r')
267
- .replace(/\n/g, '\\n')
268
- .replace(/\t/g, '\\t')
269
- .replace(/\x1b/g, '\\x1b');
270
- return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
271
- }
272
- }
273
-
274
- module.exports = TextHistory;
@@ -1,128 +0,0 @@
1
- const { spawn } = require('child_process');
2
- const { chmodSync, statSync } = require('fs');
3
-
4
- const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
5
- const DEFAULT_TIMEOUT_MS = 45000;
6
-
7
- const NATIVE_PACKAGES = {
8
- 'darwin-arm64': '@ccusage/ccusage-darwin-arm64',
9
- 'darwin-x64': '@ccusage/ccusage-darwin-x64',
10
- 'linux-arm64': '@ccusage/ccusage-linux-arm64',
11
- 'linux-x64': '@ccusage/ccusage-linux-x64',
12
- 'win32-arm64': '@ccusage/ccusage-win32-arm64',
13
- 'win32-x64': '@ccusage/ccusage-win32-x64'
14
- };
15
-
16
- function resolveCcusageBinary(platform = process.platform, arch = process.arch) {
17
- const packageName = NATIVE_PACKAGES[`${platform}-${arch}`];
18
- if (!packageName) {
19
- throw new Error(`ccusage is not available for ${platform}-${arch}`);
20
- }
21
- const binaryName = platform === 'win32' ? 'ccusage.exe' : 'ccusage';
22
- try {
23
- return require.resolve(`${packageName}/bin/${binaryName}`);
24
- } catch (_error) {
25
- throw new Error(`ccusage native package is missing for ${platform}-${arch}`);
26
- }
27
- }
28
-
29
- function ensureCcusageBinaryExecutable(binaryPath, options = {}) {
30
- const platform = options.platform || process.platform;
31
- if (platform === 'win32') return binaryPath;
32
-
33
- const statPath = options.statPath || statSync;
34
- const chmodPath = options.chmodPath || chmodSync;
35
- try {
36
- // ccusage's platform packages can be installed without execute bits. Its JS
37
- // wrapper repairs them too, but this runner intentionally spawns the native binary.
38
- if ((statPath(binaryPath).mode & 0o111) === 0) chmodPath(binaryPath, 0o755);
39
- return binaryPath;
40
- } catch (error) {
41
- throw new Error(`ccusage native binary is not executable: ${error.message}`);
42
- }
43
- }
44
-
45
- function reportArgs(timezone) {
46
- return [
47
- 'daily',
48
- '--sections', 'daily,weekly,monthly',
49
- '--by-agent',
50
- '--json',
51
- '--offline',
52
- '--timezone', timezone
53
- ];
54
- }
55
-
56
- class CcusageRunner {
57
- constructor(options = {}) {
58
- this.binaryPath = options.binaryPath
59
- || ensureCcusageBinaryExecutable(resolveCcusageBinary());
60
- this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
61
- this.spawnProcess = options.spawnProcess || spawn;
62
- }
63
-
64
- loadAllPeriods(timezone) {
65
- return this.runJson(reportArgs(timezone));
66
- }
67
-
68
- runJson(args) {
69
- return new Promise((resolve, reject) => {
70
- const child = this.spawnProcess(this.binaryPath, args, {
71
- env: { ...process.env, NO_COLOR: '1' },
72
- shell: false,
73
- windowsHide: true
74
- });
75
- const stdout = [];
76
- const stderr = [];
77
- let outputBytes = 0;
78
- let stderrBytes = 0;
79
- let settled = false;
80
-
81
- const finish = callback => {
82
- if (settled) return;
83
- settled = true;
84
- clearTimeout(timer);
85
- callback();
86
- };
87
- const timer = setTimeout(() => {
88
- child.kill();
89
- finish(() => reject(new Error('ccusage timed out while reading local usage data')));
90
- }, this.timeoutMs);
91
-
92
- child.stdout.on('data', chunk => {
93
- outputBytes += chunk.length;
94
- if (outputBytes > MAX_OUTPUT_BYTES) {
95
- child.kill();
96
- finish(() => reject(new Error('ccusage report exceeded the safe output limit')));
97
- return;
98
- }
99
- stdout.push(chunk);
100
- });
101
- child.stderr.on('data', chunk => {
102
- if (stderrBytes >= 64 * 1024) return;
103
- stderr.push(chunk);
104
- stderrBytes += chunk.length;
105
- });
106
- child.on('error', error => finish(() => reject(new Error(`Unable to start ccusage: ${error.message}`))));
107
- child.on('close', code => finish(() => {
108
- const errorText = Buffer.concat(stderr).toString('utf8').trim();
109
- if (code !== 0) {
110
- reject(new Error(errorText || `ccusage exited with code ${code}`));
111
- return;
112
- }
113
- try {
114
- resolve(JSON.parse(Buffer.concat(stdout).toString('utf8')));
115
- } catch (_error) {
116
- reject(new Error('ccusage returned invalid JSON'));
117
- }
118
- }));
119
- });
120
- }
121
- }
122
-
123
- module.exports = {
124
- CcusageRunner,
125
- ensureCcusageBinaryExecutable,
126
- reportArgs,
127
- resolveCcusageBinary
128
- };
@@ -1,26 +0,0 @@
1
- const SOURCES = [
2
- { id: 'codex', label: 'Codex', badge: 'CX' },
3
- { id: 'claude', label: 'Claude', badge: 'CL' },
4
- { id: 'gemini', label: 'Gemini', badge: 'GE' },
5
- { id: 'opencode', label: 'OpenCode', badge: 'OC' },
6
- { id: 'copilot', label: 'Copilot', badge: 'CP' },
7
- { id: 'amp', label: 'Amp', badge: 'AM' },
8
- { id: 'droid', label: 'Droid', badge: 'DR' },
9
- { id: 'codebuff', label: 'Codebuff', badge: 'CB' },
10
- { id: 'hermes', label: 'Hermes', badge: 'HE' },
11
- { id: 'pi', label: 'Pi', badge: 'PI' },
12
- { id: 'goose', label: 'Goose', badge: 'GO' },
13
- { id: 'kilo', label: 'Kilo', badge: 'KI' },
14
- { id: 'kimi', label: 'Kimi', badge: 'KM' },
15
- { id: 'qwen', label: 'Qwen', badge: 'QW' },
16
- { id: 'openclaw', label: 'OpenClaw', badge: 'OA' },
17
- { id: 'grok', label: 'Grok', badge: 'GR' }
18
- ];
19
-
20
- const SOURCE_BY_ID = new Map(SOURCES.map(source => [source.id, source]));
21
-
22
- function getUsageSource(id) {
23
- return SOURCE_BY_ID.get(String(id || '').toLowerCase()) || null;
24
- }
25
-
26
- module.exports = { SOURCES, getUsageSource };
@@ -1,226 +0,0 @@
1
- const { CcusageRunner } = require('./ccusage-runner');
2
- const { SOURCES, getUsageSource } = require('./source-catalog');
3
-
4
- const SCOPES = new Set(['weekly', 'monthly']);
5
- const CACHE_TTL_MS = 5 * 60 * 1000;
6
-
7
- function defaultTimezone() {
8
- try {
9
- return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
10
- } catch (_error) {
11
- return 'UTC';
12
- }
13
- }
14
-
15
- function ccusageVersion() {
16
- try {
17
- return require('ccusage/package.json').version;
18
- } catch (_error) {
19
- return 'unknown';
20
- }
21
- }
22
-
23
- function positiveNumber(value) {
24
- const number = Number(value);
25
- return Number.isFinite(number) && number > 0 ? number : 0;
26
- }
27
-
28
- function isGptModel(modelName) {
29
- return /^gpt(?:-|$)/i.test(String(modelName || ''));
30
- }
31
-
32
- function modelCost(sourceId, modelName, cost) {
33
- if (sourceId !== 'codex' || !isGptModel(modelName)) return null;
34
- const value = positiveNumber(cost);
35
- return value > 0 ? value : null;
36
- }
37
-
38
- function normalizeModel(sourceId, breakdown) {
39
- const uncachedInputTokens = positiveNumber(breakdown.inputTokens)
40
- + positiveNumber(breakdown.cacheCreationTokens);
41
- const cachedInputTokens = positiveNumber(breakdown.cacheReadTokens);
42
- const outputTokens = positiveNumber(breakdown.outputTokens);
43
- return {
44
- modelName: String(breakdown.modelName || 'Unknown'),
45
- uncachedInputTokens,
46
- cachedInputTokens,
47
- outputTokens,
48
- totalTokens: uncachedInputTokens + cachedInputTokens + outputTokens,
49
- estimatedCostUSD: modelCost(sourceId, breakdown.modelName, breakdown.cost)
50
- };
51
- }
52
-
53
- function sumModels(models) {
54
- return models.reduce((totals, model) => {
55
- totals.uncachedInputTokens += model.uncachedInputTokens;
56
- totals.cachedInputTokens += model.cachedInputTokens;
57
- totals.outputTokens += model.outputTokens;
58
- totals.totalTokens += model.totalTokens;
59
- if (model.estimatedCostUSD !== null) {
60
- totals.estimatedCostUSD = (totals.estimatedCostUSD || 0) + model.estimatedCostUSD;
61
- }
62
- return totals;
63
- }, {
64
- uncachedInputTokens: 0,
65
- cachedInputTokens: 0,
66
- outputTokens: 0,
67
- totalTokens: 0,
68
- estimatedCostUSD: null
69
- });
70
- }
71
-
72
- function fallbackModel(sourceId, agentRow) {
73
- const names = Array.isArray(agentRow.modelsUsed) ? agentRow.modelsUsed : [];
74
- const modelName = names.length === 1 ? names[0] : names.length > 1 ? 'Multiple models' : 'Unknown';
75
- return normalizeModel(sourceId, {
76
- modelName,
77
- inputTokens: agentRow.inputTokens,
78
- cacheCreationTokens: agentRow.cacheCreationTokens,
79
- cacheReadTokens: agentRow.cacheReadTokens,
80
- outputTokens: agentRow.outputTokens,
81
- cost: names.length === 1 ? agentRow.totalCost : null
82
- });
83
- }
84
-
85
- function normalizeAgentRow(sourceId, period, agentRow) {
86
- const breakdowns = Array.isArray(agentRow.modelBreakdowns) ? agentRow.modelBreakdowns : [];
87
- const models = breakdowns.length
88
- ? breakdowns.map(item => normalizeModel(sourceId, item))
89
- : [fallbackModel(sourceId, agentRow)];
90
- models.sort((a, b) => b.totalTokens - a.totalTokens || a.modelName.localeCompare(b.modelName));
91
- return { period, models, totals: sumModels(models) };
92
- }
93
-
94
- function findAgentRow(row, sourceId) {
95
- return Array.isArray(row && row.agents)
96
- ? row.agents.find(agent => agent && agent.agent === sourceId) || null
97
- : null;
98
- }
99
-
100
- function availablePeriods(raw, scope, sourceId) {
101
- return (raw[scope] || [])
102
- .filter(row => findAgentRow(row, sourceId))
103
- .map(row => String(row.period || ''))
104
- .filter(Boolean)
105
- .sort()
106
- .reverse();
107
- }
108
-
109
- function dateInsideScope(date, scope, selectedPeriod) {
110
- if (scope === 'monthly') return date.startsWith(`${selectedPeriod}-`);
111
- const start = new Date(`${selectedPeriod}T00:00:00Z`);
112
- const candidate = new Date(`${date}T00:00:00Z`);
113
- if (Number.isNaN(start.getTime()) || Number.isNaN(candidate.getTime())) return false;
114
- const end = new Date(start);
115
- end.setUTCDate(end.getUTCDate() + 7);
116
- return candidate >= start && candidate < end;
117
- }
118
-
119
- function buildDashboard(raw, sourceId, scope, requestedPeriod) {
120
- const periods = availablePeriods(raw, scope, sourceId);
121
- const selectedPeriod = periods.includes(requestedPeriod) ? requestedPeriod : periods[0] || null;
122
- if (!selectedPeriod) {
123
- return { availablePeriods: [], selectedPeriod: null, summary: { models: [], totals: sumModels([]) }, days: [] };
124
- }
125
-
126
- const scopeRow = (raw[scope] || []).find(row => row.period === selectedPeriod);
127
- const summaryAgent = findAgentRow(scopeRow, sourceId);
128
- const summary = summaryAgent
129
- ? normalizeAgentRow(sourceId, selectedPeriod, summaryAgent)
130
- : { models: [], totals: sumModels([]) };
131
- const days = (raw.daily || [])
132
- .filter(row => dateInsideScope(String(row.period || ''), scope, selectedPeriod))
133
- .flatMap(row => {
134
- const agent = findAgentRow(row, sourceId);
135
- return agent ? [normalizeAgentRow(sourceId, String(row.period), agent)] : [];
136
- });
137
- return { availablePeriods: periods, selectedPeriod, summary, days };
138
- }
139
-
140
- class UsageService {
141
- constructor(options = {}) {
142
- this.runner = options.runner || null;
143
- this.timezone = options.timezone || defaultTimezone();
144
- this.cacheTtlMs = options.cacheTtlMs ?? CACHE_TTL_MS;
145
- this.logger = options.logger || { debug() {} };
146
- this.cached = null;
147
- this.loading = null;
148
- }
149
-
150
- async getSnapshot(refresh = false) {
151
- const fresh = this.cached && Date.now() - this.cached.loadedAt < this.cacheTtlMs;
152
- if (!refresh && fresh) return this.cached;
153
- if (!refresh && this.cached) {
154
- this.loadSnapshot().catch(error => this.logger.debug(`Background usage refresh failed: ${error.message}`));
155
- return this.cached;
156
- }
157
- return this.loadSnapshot();
158
- }
159
-
160
- async loadSnapshot() {
161
- if (this.loading) return this.loading;
162
- if (!this.runner) this.runner = new CcusageRunner();
163
- this.loading = this.runner.loadAllPeriods(this.timezone)
164
- .then(raw => {
165
- this.cached = { raw, loadedAt: Date.now(), generatedAt: new Date().toISOString() };
166
- return this.cached;
167
- })
168
- .finally(() => { this.loading = null; });
169
- return this.loading;
170
- }
171
-
172
- async listSources(refresh = false) {
173
- const snapshot = await this.getSnapshot(refresh);
174
- const present = new Set();
175
- for (const scope of ['daily', 'weekly', 'monthly']) {
176
- for (const row of snapshot.raw[scope] || []) {
177
- for (const agent of row.agents || []) present.add(agent.agent);
178
- }
179
- }
180
- return {
181
- sources: SOURCES.filter(source => present.has(source.id)),
182
- generatedAt: snapshot.generatedAt,
183
- timezone: this.timezone,
184
- engine: { name: 'ccusage', version: ccusageVersion(), pricingMode: 'embedded' }
185
- };
186
- }
187
-
188
- async getDashboard(sourceId, scope, selectedPeriod, refresh = false) {
189
- const source = getUsageSource(sourceId);
190
- if (!source) {
191
- const error = new Error('Unsupported usage source');
192
- error.statusCode = 400;
193
- throw error;
194
- }
195
- if (!SCOPES.has(scope)) {
196
- const error = new Error('Scope must be weekly or monthly');
197
- error.statusCode = 400;
198
- throw error;
199
- }
200
- const snapshot = await this.getSnapshot(refresh);
201
- return {
202
- source,
203
- scope,
204
- ...buildDashboard(snapshot.raw, source.id, scope, selectedPeriod),
205
- generatedAt: snapshot.generatedAt,
206
- timezone: this.timezone,
207
- engine: { name: 'ccusage', version: ccusageVersion(), pricingMode: 'embedded' },
208
- cost: source.id === 'codex' ? {
209
- basis: 'ccusage estimate for Codex GPT models',
210
- note: 'Estimated from ccusage model pricing; it is not an actual provider bill.'
211
- } : null
212
- };
213
- }
214
- }
215
-
216
- module.exports = {
217
- UsageService,
218
- availablePeriods,
219
- buildDashboard,
220
- dateInsideScope,
221
- isGptModel,
222
- modelCost,
223
- normalizeAgentRow,
224
- normalizeModel,
225
- sumModels
226
- };