@kylindc/ccxray 1.2.0
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/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.ja.md +144 -0
- package/README.md +145 -0
- package/README.zh-TW.md +144 -0
- package/package.json +46 -0
- package/public/app.js +99 -0
- package/public/cost-budget-ui.js +305 -0
- package/public/entry-rendering.js +535 -0
- package/public/index.html +119 -0
- package/public/intercept-ui.js +335 -0
- package/public/keyboard-nav.js +208 -0
- package/public/messages.js +750 -0
- package/public/miller-columns.js +1686 -0
- package/public/quota-ticker.js +67 -0
- package/public/style.css +431 -0
- package/public/system-prompt-ui.js +327 -0
- package/server/auth.js +34 -0
- package/server/bedrock-credentials.js +141 -0
- package/server/config.js +190 -0
- package/server/cost-budget.js +220 -0
- package/server/cost-worker.js +110 -0
- package/server/eventstream.js +148 -0
- package/server/forward.js +683 -0
- package/server/helpers.js +393 -0
- package/server/hub.js +418 -0
- package/server/index.js +551 -0
- package/server/pricing.js +133 -0
- package/server/restore.js +141 -0
- package/server/routes/api.js +123 -0
- package/server/routes/costs.js +124 -0
- package/server/routes/intercept.js +89 -0
- package/server/routes/sse.js +44 -0
- package/server/sigv4.js +104 -0
- package/server/sse-broadcast.js +71 -0
- package/server/storage/index.js +36 -0
- package/server/storage/interface.js +26 -0
- package/server/storage/local.js +79 -0
- package/server/storage/s3.js +91 -0
- package/server/store.js +108 -0
- package/server/system-prompt.js +150 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// ── Cost Budget: Client-side ──────────────────────────────────────────
|
|
2
|
+
let _row2Block = null, _row2Monthly = null;
|
|
3
|
+
function updateRow2Usage(block, monthly) {
|
|
4
|
+
if (block) _row2Block = block;
|
|
5
|
+
if (monthly) _row2Monthly = monthly;
|
|
6
|
+
const el = document.getElementById('row2-usage-summary');
|
|
7
|
+
if (!el) return;
|
|
8
|
+
const parts = [];
|
|
9
|
+
if (_row2Block && _row2Block.active) {
|
|
10
|
+
parts.push('Window: ' + (_row2Block.percentUsed || 0).toFixed(0) + '% used');
|
|
11
|
+
}
|
|
12
|
+
if (_row2Monthly && _row2Monthly.currentMonth) {
|
|
13
|
+
parts.push('$' + (_row2Monthly.currentMonth.costUSD || 0).toFixed(2) + ' this month');
|
|
14
|
+
}
|
|
15
|
+
el.textContent = parts.join(' · ') || 'Loading…';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function showCostPage() {
|
|
19
|
+
switchTab('usage');
|
|
20
|
+
}
|
|
21
|
+
function hideCostPage() {
|
|
22
|
+
switchTab('dashboard');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadCostPage() {
|
|
26
|
+
const z1 = document.getElementById('cp-z1-content');
|
|
27
|
+
const z2 = document.getElementById('cp-z2-content');
|
|
28
|
+
const z3 = document.getElementById('cp-z3-content');
|
|
29
|
+
const spinner = '<div style="color:var(--dim);padding:20px;text-align:center">Loading usage data…</div>';
|
|
30
|
+
z1.innerHTML = spinner;
|
|
31
|
+
z2.innerHTML = spinner;
|
|
32
|
+
z3.innerHTML = spinner;
|
|
33
|
+
|
|
34
|
+
// Poll until data is ready (server returns 202 while computing)
|
|
35
|
+
async function fetchWithRetry(url, fallback, maxRetries = 20) {
|
|
36
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
37
|
+
try {
|
|
38
|
+
const r = await fetch(url);
|
|
39
|
+
const data = await r.json();
|
|
40
|
+
if (data && data.loading) {
|
|
41
|
+
await new Promise(ok => setTimeout(ok, 2000));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
} catch { return fallback; }
|
|
46
|
+
}
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Each zone fetches and renders independently — no Promise.all
|
|
51
|
+
fetchWithRetry('/_api/costs/current-block', { active: false })
|
|
52
|
+
.then(blockData => { renderZone1(blockData); updateRow2Usage(blockData, null); });
|
|
53
|
+
|
|
54
|
+
const dailyPromise = fetchWithRetry('/_api/costs/daily', []);
|
|
55
|
+
const monthlyPromise = fetchWithRetry('/_api/costs/monthly', { monthly: [], currentMonth: { costUSD: 0 } });
|
|
56
|
+
|
|
57
|
+
// Zone 2 needs both monthly and daily data
|
|
58
|
+
Promise.all([monthlyPromise, dailyPromise])
|
|
59
|
+
.then(([monthlyData, dailyData]) => { renderZone2(monthlyData, dailyData); updateRow2Usage(null, monthlyData); });
|
|
60
|
+
|
|
61
|
+
// Zone 3 only needs daily data
|
|
62
|
+
dailyPromise.then(dailyData => renderZone3(dailyData));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderZone1(block) {
|
|
66
|
+
const el = document.getElementById('cp-z1-content');
|
|
67
|
+
if (!block.active) {
|
|
68
|
+
const lb = block.lastBlock;
|
|
69
|
+
if (lb) {
|
|
70
|
+
const agoH = Math.floor(lb.minutesAgo / 60), agoM = lb.minutesAgo % 60;
|
|
71
|
+
const agoStr = agoH > 0 ? agoH + 'h ' + agoM + 'min ago' : agoM + 'min ago';
|
|
72
|
+
el.innerHTML = `
|
|
73
|
+
<div style="color:var(--dim);font-size:12px;margin-bottom:6px">No active window · last ended ${agoStr}</div>
|
|
74
|
+
<div style="font-size:11px;color:var(--dim)">
|
|
75
|
+
${lb.totalTokens.toLocaleString()} tokens · $${lb.costUSD} · ${lb.models.slice(0,2).map(m=>m.split('-')[1]).join('/')}
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
} else {
|
|
79
|
+
el.innerHTML = '<div style="color:var(--dim);font-size:12px">No history data</div>';
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const pct = block.percentUsed || 0;
|
|
85
|
+
const timePct = block.timePct || 0;
|
|
86
|
+
const tokenColor = pct < 60 ? 'var(--green)' : pct < 85 ? 'var(--yellow)' : 'var(--red)';
|
|
87
|
+
const paceRatio = timePct > 0 ? pct / timePct : 0;
|
|
88
|
+
let statusDot, statusMsg, statusColor;
|
|
89
|
+
if (paceRatio > 1.3) {
|
|
90
|
+
statusDot = '🔴'; statusMsg = 'Burning faster than time'; statusColor = 'var(--red)';
|
|
91
|
+
} else if (paceRatio > 1.1) {
|
|
92
|
+
statusDot = '🟡'; statusMsg = 'Slightly fast, watch it'; statusColor = 'var(--yellow)';
|
|
93
|
+
} else if (paceRatio < 0.7 && timePct > 20) {
|
|
94
|
+
statusDot = '🟢'; statusMsg = 'Quota comfortable'; statusColor = 'var(--green)';
|
|
95
|
+
} else {
|
|
96
|
+
statusDot = '🟢'; statusMsg = 'Rate normal'; statusColor = 'var(--green)';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const minRemaining = block.minutesRemaining || 0;
|
|
100
|
+
const remainH = Math.floor(minRemaining / 60), remainM = minRemaining % 60;
|
|
101
|
+
const remainStr = remainH > 0 ? remainH + 'h ' + remainM + 'm' : remainM + 'm';
|
|
102
|
+
const br = block.burnRate;
|
|
103
|
+
const proj = block.projection;
|
|
104
|
+
|
|
105
|
+
el.innerHTML = `
|
|
106
|
+
<div style="font-size:12px;font-weight:600;color:${statusColor};margin-bottom:10px">${statusDot} ${statusMsg}</div>
|
|
107
|
+
<div style="margin-bottom:8px">
|
|
108
|
+
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--dim);margin-bottom:2px">
|
|
109
|
+
<span>TOKENS</span><span style="color:${tokenColor}">${pct.toFixed(1)}% · ${(block.totalTokens/1000).toFixed(0)}k / ${((block.tokenLimit||220000)/1000).toFixed(0)}k${pct>100?' ⚠️':''}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div style="height:7px;background:var(--border);border-radius:3px;overflow:hidden;margin-bottom:6px">
|
|
112
|
+
<div style="height:100%;width:${Math.min(pct,100)}%;background:${tokenColor};border-radius:3px;transition:width 0.5s"></div>
|
|
113
|
+
</div>
|
|
114
|
+
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--dim);margin-bottom:2px">
|
|
115
|
+
<span>TIME</span><span>${timePct.toFixed(1)}% · ${remainStr} left</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div style="height:7px;background:var(--border);border-radius:3px;overflow:hidden">
|
|
118
|
+
<div style="height:100%;width:${Math.min(timePct,100)}%;background:var(--dim);border-radius:3px;opacity:0.5;transition:width 0.5s"></div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
${br ? `<div style="font-size:10px;color:var(--dim);border-top:1px solid var(--border);padding-top:7px;display:flex;gap:12px;flex-wrap:wrap">
|
|
122
|
+
<span>${br.tokensPerMinute.toLocaleString()} tok/min</span>
|
|
123
|
+
<span>$${br.costPerHour}/hr</span>
|
|
124
|
+
${proj ? `<span>proj ${(proj.totalTokens/1000).toFixed(0)}k ($${proj.totalCost})</span>` : ''}
|
|
125
|
+
<span style="margin-left:auto">$${block.costUSD} equiv.</span>
|
|
126
|
+
</div>` : `<div style="font-size:10px;color:var(--dim)">$${block.costUSD} equiv. API cost</div>`}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const PLAN_OPTIONS = [
|
|
131
|
+
{ label: 'Pro $20/mo', price: 20 },
|
|
132
|
+
{ label: 'Max 5x $100/mo', price: 100 },
|
|
133
|
+
{ label: 'Max 20x $200/mo', price: 200 },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
function getSelectedPlanPrice() {
|
|
137
|
+
return parseInt(localStorage.getItem('planPrice')) || 200;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderZone2(monthlyData, dailyData) {
|
|
141
|
+
const el = document.getElementById('cp-z2-content');
|
|
142
|
+
const currentCost = monthlyData.currentMonth?.costUSD || 0;
|
|
143
|
+
const planPrice = getSelectedPlanPrice();
|
|
144
|
+
const roi = (currentCost / planPrice).toFixed(1);
|
|
145
|
+
const saved = (currentCost - planPrice).toFixed(0);
|
|
146
|
+
const roiColor = parseFloat(roi) >= 1 ? 'var(--green)' : 'var(--yellow)';
|
|
147
|
+
|
|
148
|
+
const optionsHtml = PLAN_OPTIONS.map(p =>
|
|
149
|
+
`<option value="${p.price}"${p.price === planPrice ? ' selected' : ''}>${p.label}</option>`
|
|
150
|
+
).join('');
|
|
151
|
+
|
|
152
|
+
// Render plan selector in card label
|
|
153
|
+
const labelEl = document.getElementById('cp-z2-label');
|
|
154
|
+
if (labelEl) {
|
|
155
|
+
labelEl.innerHTML = `ROI & Plan Fit <select onchange="localStorage.setItem('planPrice',this.value);renderZone2(window._zone2Monthly,window._zone2Daily)" style="background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:3px;font-size:10px;padding:1px 4px;cursor:pointer">${optionsHtml}</select>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
el.innerHTML = `
|
|
159
|
+
<div style="display:flex;gap:20px;flex-wrap:wrap">
|
|
160
|
+
<div>
|
|
161
|
+
<div style="font-size:10px;color:var(--dim);margin-bottom:2px">Equiv. API cost this month (not actual spend)</div>
|
|
162
|
+
<div style="font-size:20px;font-weight:700;color:${roiColor}">$${currentCost.toFixed(2)}</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<div style="font-size:10px;color:var(--dim);margin-bottom:2px">Monthly ROI</div>
|
|
166
|
+
<div style="font-size:20px;font-weight:700;color:${roiColor}">${roi}x</div>
|
|
167
|
+
</div>
|
|
168
|
+
${parseFloat(saved) > 0 ? `<div>
|
|
169
|
+
<div style="font-size:10px;color:var(--dim);margin-bottom:2px">Saved</div>
|
|
170
|
+
<div style="font-size:20px;font-weight:700;color:var(--green)">$${saved}</div>
|
|
171
|
+
</div>` : ''}
|
|
172
|
+
</div>
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
// Store data for re-render on plan change
|
|
176
|
+
window._zone2Monthly = monthlyData;
|
|
177
|
+
window._zone2Daily = dailyData;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let zone3Metric = 'sessions'; // 'sessions' | 'tokens'
|
|
181
|
+
|
|
182
|
+
function renderZone3(dailyData) {
|
|
183
|
+
const container = document.getElementById('cp-z3-content');
|
|
184
|
+
if (!dailyData || !dailyData.length) {
|
|
185
|
+
container.innerHTML = '<div style="color:var(--dim);font-size:12px">No data</div>';
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
window._zone3Data = dailyData; // store for toggle
|
|
189
|
+
|
|
190
|
+
// Summary totals
|
|
191
|
+
const totalSessions = dailyData.reduce((s, d) => s + (d.sessionCount || 0), 0);
|
|
192
|
+
const totalTokens = dailyData.reduce((s, d) => s + (d.totalTokens || 0), 0);
|
|
193
|
+
const tokStr = totalTokens >= 1e9 ? (totalTokens/1e9).toFixed(1)+'B'
|
|
194
|
+
: totalTokens >= 1e6 ? (totalTokens/1e6).toFixed(1)+'M'
|
|
195
|
+
: totalTokens >= 1e3 ? (totalTokens/1e3).toFixed(0)+'k'
|
|
196
|
+
: String(totalTokens);
|
|
197
|
+
|
|
198
|
+
const isSessions = zone3Metric === 'sessions';
|
|
199
|
+
const summaryHtml = `
|
|
200
|
+
<div style="display:flex;gap:24px;margin-bottom:10px">
|
|
201
|
+
<div style="cursor:pointer" onclick="zone3Metric='sessions';renderZone3(window._zone3Data)">
|
|
202
|
+
<div style="font-size:15px;font-weight:700;color:${isSessions?'var(--text)':'var(--dim)'}">${totalSessions.toLocaleString()} sessions</div>
|
|
203
|
+
<div style="height:2px;background:${isSessions?'var(--text)':'transparent'};border-radius:1px;margin-top:2px"></div>
|
|
204
|
+
</div>
|
|
205
|
+
<div style="cursor:pointer" onclick="zone3Metric='tokens';renderZone3(window._zone3Data)">
|
|
206
|
+
<div style="font-size:15px;font-weight:700;color:${!isSessions?'var(--text)':'var(--dim)'}">${tokStr} tokens</div>
|
|
207
|
+
<div style="height:2px;background:${!isSessions?'var(--text)':'transparent'};border-radius:1px;margin-top:2px"></div>
|
|
208
|
+
</div>
|
|
209
|
+
<div style="font-size:11px;color:var(--dim);align-self:flex-end;margin-bottom:3px">Last 3 months</div>
|
|
210
|
+
</div>
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
// Build a map of date → value
|
|
214
|
+
const costByDate = {};
|
|
215
|
+
for (const d of dailyData) costByDate[d.date] = isSessions ? (d.sessionCount || 0) : (d.totalTokens || 0);
|
|
216
|
+
|
|
217
|
+
// 6 levels via p17/p33/p50/p67/p83 of non-zero days — thresholds follow current metric
|
|
218
|
+
const metricValues = dailyData.map(d => isSessions ? (d.sessionCount || 0) : (d.totalTokens || 0));
|
|
219
|
+
const nonZero = metricValues.filter(c => c > 0).sort((a,b)=>a-b);
|
|
220
|
+
const p = (arr, pct) => arr[Math.max(0, Math.floor(arr.length * pct) - 1)] || 0;
|
|
221
|
+
const [t1,t2,t3,t4,t5] = [0.17,0.33,0.50,0.67,0.83].map(q => p(nonZero, q));
|
|
222
|
+
const cs = getComputedStyle(document.documentElement);
|
|
223
|
+
const PALETTE = [0,1,2,3,4,5].map(i => cs.getPropertyValue('--heatmap-' + i).trim());
|
|
224
|
+
const HEATMAP_EMPTY = cs.getPropertyValue('--heatmap-empty').trim();
|
|
225
|
+
function cellColor(cost) {
|
|
226
|
+
if (cost <= 0) return 'var(--border)';
|
|
227
|
+
if (cost < t1) return PALETTE[0];
|
|
228
|
+
if (cost < t2) return PALETTE[1];
|
|
229
|
+
if (cost < t3) return PALETTE[2];
|
|
230
|
+
if (cost < t4) return PALETTE[3];
|
|
231
|
+
if (cost < t5) return PALETTE[4];
|
|
232
|
+
return PALETTE[5];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Grid aligned to Sunday (row 0 = Sun, row 6 = Sat)
|
|
236
|
+
const today = new Date();
|
|
237
|
+
const thisSunday = new Date(today);
|
|
238
|
+
thisSunday.setDate(today.getDate() - today.getDay()); // getDay(): 0=Sun
|
|
239
|
+
thisSunday.setHours(0,0,0,0);
|
|
240
|
+
|
|
241
|
+
const WEEKS = 26; // ~6 months
|
|
242
|
+
const gridStart = new Date(thisSunday);
|
|
243
|
+
gridStart.setDate(thisSunday.getDate() - (WEEKS - 1) * 7);
|
|
244
|
+
|
|
245
|
+
const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
246
|
+
const CS = 11, GAP = 2, LW = 26; // cell size, gap, label width
|
|
247
|
+
const cellStyle = `width:${CS}px;height:${CS}px;border-radius:3px;display:inline-block;flex-shrink:0;`;
|
|
248
|
+
|
|
249
|
+
// Month labels: first week of each month
|
|
250
|
+
const monthLabels = [];
|
|
251
|
+
let lastMonth = -1;
|
|
252
|
+
for (let w = 0; w < WEEKS; w++) {
|
|
253
|
+
const d = new Date(gridStart);
|
|
254
|
+
d.setDate(gridStart.getDate() + w * 7);
|
|
255
|
+
if (d.getMonth() !== lastMonth) {
|
|
256
|
+
monthLabels.push({ w, label: d.toLocaleString('en', { month: 'short' }) });
|
|
257
|
+
lastMonth = d.getMonth();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let html = summaryHtml + `<div style="overflow-x:auto;overflow-y:hidden">`;
|
|
262
|
+
|
|
263
|
+
// Month header — GitHub style: label above first week of each month
|
|
264
|
+
html += `<div style="position:relative;height:14px;margin-left:${LW}px;margin-bottom:3px">`;
|
|
265
|
+
for (const { w, label } of monthLabels) {
|
|
266
|
+
html += `<div style="position:absolute;left:${w*(CS+GAP)}px;font-size:10px;color:var(--text);opacity:0.7;white-space:nowrap">${label}</div>`;
|
|
267
|
+
}
|
|
268
|
+
html += '</div>';
|
|
269
|
+
|
|
270
|
+
// Day rows: Sun(0) top → Sat(6) bottom; GitHub labels Mon/Wed/Fri only
|
|
271
|
+
const SHOW_LABEL = new Set([1, 3, 5]); // Mon, Wed, Fri
|
|
272
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
273
|
+
html += `<div style="display:flex;align-items:center;gap:${GAP}px;margin-bottom:${GAP}px">`;
|
|
274
|
+
const rowLabel = SHOW_LABEL.has(dow) ? dayNames[dow] : '';
|
|
275
|
+
html += `<div style="width:${LW}px;font-size:9px;color:var(--dim);text-align:right;padding-right:5px;flex-shrink:0">${rowLabel}</div>`;
|
|
276
|
+
for (let w = 0; w < WEEKS; w++) {
|
|
277
|
+
const d = new Date(gridStart);
|
|
278
|
+
d.setDate(gridStart.getDate() + w * 7 + dow);
|
|
279
|
+
const dateStr = d.toLocaleDateString('sv-SE');
|
|
280
|
+
const cost = costByDate[dateStr] || 0;
|
|
281
|
+
const isFuture = d > today;
|
|
282
|
+
const bg = isFuture ? 'var(--border)' : (cost === 0 ? '${HEATMAP_EMPTY}' : cellColor(cost));
|
|
283
|
+
const isWeekend = dow === 0 || dow === 6;
|
|
284
|
+
const opacity = (isWeekend && cost === 0 && !isFuture) ? '0.4' : '1';
|
|
285
|
+
const valStr = isSessions ? cost + ' sessions' : (cost >= 1000 ? (cost/1000).toFixed(1)+'k' : cost) + ' tokens';
|
|
286
|
+
const title = isFuture ? dateStr : `${dateStr} ${valStr}`;
|
|
287
|
+
html += `<div style="${cellStyle}background:${bg};opacity:${opacity}" title="${title}"></div>`;
|
|
288
|
+
}
|
|
289
|
+
html += '</div>';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Legend — GitHub style: Less ◻◻◻◻◻ More, right-aligned
|
|
293
|
+
html += `<div style="display:flex;gap:4px;align-items:center;margin-top:8px;margin-left:${LW}px;font-size:10px;color:var(--dim);justify-content:flex-end">
|
|
294
|
+
<span>Less</span>
|
|
295
|
+
<div style="${cellStyle}background:${HEATMAP_EMPTY}"></div>
|
|
296
|
+
${PALETTE.map(c=>`<div style="${cellStyle}background:${c}"></div>`).join('')}
|
|
297
|
+
<span>More</span>
|
|
298
|
+
</div>`;
|
|
299
|
+
html += '</div>';
|
|
300
|
+
|
|
301
|
+
container.innerHTML = html;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
// Escape handler moved to unified fullscreen-page listener in app.js
|