claude-code-watch 0.1.5 → 0.2.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/package.json +1 -1
- package/public/css/app.css +500 -0
- package/public/index.html +39 -2512
- package/public/js/app.js +500 -0
- package/public/js/shared.js +245 -0
- package/public/js/stream.js +1076 -0
- package/public/js/token.js +458 -0
- package/src/scanner/scanner.js +18 -9
- package/src/server/server.js +87 -14
- package/src/watcher/watcher.js +103 -65
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// shared.js — Common utilities and shared state
|
|
3
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
// ── LRU Cache ──
|
|
6
|
+
class LRUCache {
|
|
7
|
+
constructor(max) { this.max = max; this.map = new Map(); }
|
|
8
|
+
has(key) { if (!this.map.has(key)) return false; const v = this.map.get(key); this.map.delete(key); this.map.set(key, v); return true; }
|
|
9
|
+
get(key) { if (!this.map.has(key)) return undefined; const v = this.map.get(key); this.map.delete(key); this.map.set(key, v); return v; }
|
|
10
|
+
set(key, val) { if (this.map.has(key)) this.map.delete(key); this.map.set(key, val); if (this.map.size > this.max) { const oldest = this.map.keys().next().value; this.map.delete(oldest); } }
|
|
11
|
+
delete(key) { return this.map.delete(key); }
|
|
12
|
+
keys() { return this.map.keys(); }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Shared State ──
|
|
16
|
+
const sessions = [];
|
|
17
|
+
const sessionsMap = new Map();
|
|
18
|
+
const treeNodes = [];
|
|
19
|
+
let treeCursor = 0;
|
|
20
|
+
const folderCollapsed = {};
|
|
21
|
+
const streamItems = [];
|
|
22
|
+
let visibleItems = [];
|
|
23
|
+
let visibleDirty = true;
|
|
24
|
+
const filters = new Map();
|
|
25
|
+
let visibleFilterCount = 0;
|
|
26
|
+
let contextData = {};
|
|
27
|
+
let autoDiscovery = true;
|
|
28
|
+
let renderPending = false;
|
|
29
|
+
|
|
30
|
+
// LRU cache instances
|
|
31
|
+
const MAX_DESC_STORE = 200;
|
|
32
|
+
const seenToolIDs = new LRUCache(20000);
|
|
33
|
+
const toolNameMap = new LRUCache(2000);
|
|
34
|
+
const agentActivity = new LRUCache(500);
|
|
35
|
+
const taskDescriptions = new LRUCache(2000);
|
|
36
|
+
|
|
37
|
+
// Hidden sessions
|
|
38
|
+
const HIDDEN_KEY = 'claude-watch-hidden';
|
|
39
|
+
const hiddenSessionIDs = new Set();
|
|
40
|
+
|
|
41
|
+
function loadHiddenSessions() {
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(localStorage.getItem(HIDDEN_KEY) || '{}');
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
for (const [id, ts] of Object.entries(data)) {
|
|
46
|
+
if (now - ts < 24 * 60 * 60 * 1000) hiddenSessionIDs.add(id);
|
|
47
|
+
}
|
|
48
|
+
_saveHiddenSessions();
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _saveHiddenSessions() {
|
|
53
|
+
const data = {};
|
|
54
|
+
for (const id of hiddenSessionIDs) data[id] = Date.now();
|
|
55
|
+
localStorage.setItem(HIDDEN_KEY, JSON.stringify(data));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
loadHiddenSessions();
|
|
59
|
+
|
|
60
|
+
// ── Model Colors ──
|
|
61
|
+
const MODEL_COLORS = {
|
|
62
|
+
'claude-opus-4-7': '#e74c3c', 'claude-opus-4-6': '#c0392b', 'claude-opus-4-8': '#e67e22',
|
|
63
|
+
'claude-sonnet-4-6': '#3498db', 'claude-sonnet-4-5': '#2980b9',
|
|
64
|
+
'claude-haiku-4-5': '#5dade2', 'claude-haiku-4': '#1abc9c',
|
|
65
|
+
'glm-5.1': '#2980b9', 'glm-5': '#3498db', 'glm-4.7': '#5dade2',
|
|
66
|
+
'qwen3.7-max': '#55efc4', 'qwen3.6-plus': '#2ecc71', 'qwen3.5-plus': '#27ae60',
|
|
67
|
+
'qwen3-max': '#1abc9c',
|
|
68
|
+
'deepseek-v4-pro': '#9b59b6',
|
|
69
|
+
'kimi-k2.5': '#f39c12', 'kimi-k2.6': '#d35400', 'kimi-k2-thinking': '#d4a017',
|
|
70
|
+
'MiniMax-M2.5': '#1abc9c',
|
|
71
|
+
};
|
|
72
|
+
let _modelColorIdx = 0;
|
|
73
|
+
|
|
74
|
+
function modelColor(name) {
|
|
75
|
+
if (MODEL_COLORS[name]) return MODEL_COLORS[name];
|
|
76
|
+
const fallback = ['#e74c3c','#3498db','#2ecc71','#9b59b6','#f39c12','#1abc9c','#e67e22','#c0392b','#5dade2','#d35400','#55efc4','#d4a017'];
|
|
77
|
+
return fallback[_modelColorIdx++ % fallback.length];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Utility Functions ──
|
|
81
|
+
|
|
82
|
+
const _escMap = {'&':'&','<':'<','>':'>','"':'"',"'":''','\\':'\'};
|
|
83
|
+
function esc(s) {
|
|
84
|
+
return (s ?? '').replace(/[&<>"'\\]/g, c => _escMap[c]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fmtTok(n) {
|
|
88
|
+
if (!n) return '0';
|
|
89
|
+
if (n < 1000) return String(n);
|
|
90
|
+
if (n < 1000000) return (n / 1000).toFixed(2) + 'k';
|
|
91
|
+
return (n / 1000000).toFixed(2) + 'm';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fmtTS(n) {
|
|
95
|
+
if (!n) return '0';
|
|
96
|
+
return n.toLocaleString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fmtDur(ms) {
|
|
100
|
+
if (!ms || ms <= 0) return '';
|
|
101
|
+
if (ms < 1000) return `(${ms}ms)`;
|
|
102
|
+
if (ms < 60000) return `(${(ms / 1000).toFixed(1)}s)`;
|
|
103
|
+
return `(${(ms / 60000).toFixed(1)}m)`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fmtTimestamp(ts) {
|
|
107
|
+
if (!ts) return '';
|
|
108
|
+
const d = ts instanceof Date ? ts : new Date(ts);
|
|
109
|
+
if (isNaN(d.getTime())) return '';
|
|
110
|
+
const pad = (n, len) => String(n).padStart(len, '0');
|
|
111
|
+
const ms = pad(d.getMilliseconds(), 3);
|
|
112
|
+
return `${pad(d.getFullYear(),4)}-${pad(d.getMonth()+1,2)}-${pad(d.getDate(),2)} ${pad(d.getHours(),2)}:${pad(d.getMinutes(),2)}:${pad(d.getSeconds(),2)}.${ms}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatTime(ms) {
|
|
116
|
+
if (!ms) return '';
|
|
117
|
+
const d = new Date(ms);
|
|
118
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
119
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fmtDateISO(d) {
|
|
123
|
+
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function folderName(projectPath) {
|
|
127
|
+
if (!projectPath) return '';
|
|
128
|
+
const parts = projectPath.split('/');
|
|
129
|
+
return parts[parts.length - 1] || projectPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function idColor(rank) {
|
|
133
|
+
const hue = (rank * 137.508) % 360;
|
|
134
|
+
return `hsl(${hue}, 75%, 60%)`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function itemTime(item) {
|
|
138
|
+
if (item && item.timestamp) {
|
|
139
|
+
const ts = item.timestamp instanceof Date ? item.timestamp : new Date(item.timestamp);
|
|
140
|
+
if (!isNaN(ts.getTime())) return ts.getTime();
|
|
141
|
+
}
|
|
142
|
+
return Date.now();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function agentDisplayName(id, type) {
|
|
146
|
+
if (type) {
|
|
147
|
+
const idx = type.lastIndexOf(':');
|
|
148
|
+
if (idx >= 0 && idx < type.length - 1) return type.slice(idx + 1);
|
|
149
|
+
return type;
|
|
150
|
+
}
|
|
151
|
+
if (!id) return 'Main';
|
|
152
|
+
return 'Agent-' + id.slice(0, 7);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const agentIdDisplayLen = new Map();
|
|
156
|
+
function computeAgentIdDisplayLengths() {
|
|
157
|
+
agentIdDisplayLen.clear();
|
|
158
|
+
for (const s of sessions) {
|
|
159
|
+
const agentIds = s.agents.filter(a => a.id).map(a => a.id);
|
|
160
|
+
if (agentIds.length === 0) continue;
|
|
161
|
+
let minLen = 7;
|
|
162
|
+
while (minLen < 21) {
|
|
163
|
+
const prefixes = agentIds.map(id => id.slice(0, minLen));
|
|
164
|
+
const unique = new Set(prefixes);
|
|
165
|
+
if (unique.size === agentIds.length) break;
|
|
166
|
+
minLen++;
|
|
167
|
+
}
|
|
168
|
+
for (const id of agentIds) {
|
|
169
|
+
agentIdDisplayLen.set(s.id + ':' + id, minLen);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Token Computation ──
|
|
175
|
+
|
|
176
|
+
let totalInput = 0, totalOutput = 0, totalCacheCreate = 0, totalCacheRead = 0;
|
|
177
|
+
|
|
178
|
+
function computeTokensFromContext() {
|
|
179
|
+
totalInput = 0; totalOutput = 0; totalCacheCreate = 0; totalCacheRead = 0;
|
|
180
|
+
for (const ctx of Object.values(contextData)) {
|
|
181
|
+
totalInput += ctx.inputTokens || 0;
|
|
182
|
+
totalOutput += ctx.outputTokens || 0;
|
|
183
|
+
totalCacheCreate += ctx.cacheCreation || 0;
|
|
184
|
+
totalCacheRead += ctx.cacheRead || 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Weekly/Monthly Aggregation ──
|
|
189
|
+
|
|
190
|
+
function getWeekKey(d) {
|
|
191
|
+
const dayNum = d.getDay() || 7;
|
|
192
|
+
const thursday = new Date(d);
|
|
193
|
+
thursday.setDate(d.getDate() + 4 - dayNum);
|
|
194
|
+
const year = thursday.getFullYear();
|
|
195
|
+
const jan1 = new Date(year, 0, 1);
|
|
196
|
+
const wk = Math.ceil(((thursday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
|
|
197
|
+
return year + '-W' + String(wk).padStart(2, '0');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function aggregateWeekly(dailyKeys, daily) {
|
|
201
|
+
const result = {};
|
|
202
|
+
for (const k of dailyKeys) {
|
|
203
|
+
const d = new Date(k);
|
|
204
|
+
const wk = getWeekKey(d);
|
|
205
|
+
if (!result[wk]) result[wk] = { messages: 0, input: 0, output: 0, cacheCreation: 0, cacheRead: 0, models: {}, dateRange: k };
|
|
206
|
+
else result[wk].dateRange += ' ~ ' + k;
|
|
207
|
+
const day = daily[k];
|
|
208
|
+
result[wk].messages += day.messages;
|
|
209
|
+
result[wk].input += day.input;
|
|
210
|
+
result[wk].output += day.output;
|
|
211
|
+
result[wk].cacheCreation += day.cacheCreation;
|
|
212
|
+
result[wk].cacheRead += day.cacheRead;
|
|
213
|
+
for (const [mn, m] of Object.entries(day.models)) {
|
|
214
|
+
if (!result[wk].models[mn]) result[wk].models[mn] = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
215
|
+
result[wk].models[mn].input += m.input;
|
|
216
|
+
result[wk].models[mn].output += m.output;
|
|
217
|
+
result[wk].models[mn].cacheCreation += m.cacheCreation;
|
|
218
|
+
result[wk].models[mn].cacheRead += m.cacheRead;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function aggregateMonthly(dailyKeys, daily) {
|
|
225
|
+
const result = {};
|
|
226
|
+
for (const k of dailyKeys) {
|
|
227
|
+
const mk = k.slice(0, 7);
|
|
228
|
+
if (!result[mk]) result[mk] = { messages: 0, input: 0, output: 0, cacheCreation: 0, cacheRead: 0, models: {}, dateRange: k };
|
|
229
|
+
else result[mk].dateRange += ' ~ ' + k.slice(5);
|
|
230
|
+
const day = daily[k];
|
|
231
|
+
result[mk].messages += day.messages;
|
|
232
|
+
result[mk].input += day.input;
|
|
233
|
+
result[mk].output += day.output;
|
|
234
|
+
result[mk].cacheCreation += day.cacheCreation;
|
|
235
|
+
result[mk].cacheRead += day.cacheRead;
|
|
236
|
+
for (const [mn, m] of Object.entries(day.models)) {
|
|
237
|
+
if (!result[mk].models[mn]) result[mk].models[mn] = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
238
|
+
result[mk].models[mn].input += m.input;
|
|
239
|
+
result[mk].models[mn].output += m.output;
|
|
240
|
+
result[mk].models[mn].cacheCreation += m.cacheCreation;
|
|
241
|
+
result[mk].models[mn].cacheRead += m.cacheRead;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|