@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.
@@ -0,0 +1,393 @@
1
+ 'use strict';
2
+
3
+ const { countTokens } = require('@anthropic-ai/tokenizer');
4
+
5
+ // ── Helpers ─────────────────────────────────────────────────────────
6
+ function timestamp() {
7
+ const d = new Date();
8
+ const parts = d.toLocaleString('sv-SE', { timeZone: 'Asia/Taipei' }).replace(' ', 'T');
9
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
10
+ return (parts + '-' + ms).replace(/[:.]/g, '-');
11
+ }
12
+
13
+ function taipeiTime() {
14
+ return new Date().toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour12: false });
15
+ }
16
+
17
+ function printSeparator() {
18
+ console.log('\x1b[33m' + '═'.repeat(60) + '\x1b[0m');
19
+ }
20
+
21
+ function safeCountTokens(text) {
22
+ if (!text) return 0;
23
+ try { return countTokens(text); } catch { return 0; }
24
+ }
25
+
26
+ // ── Context Breakdown Analysis ───────────────────────────────────────
27
+ const TOOL_CATEGORIES = {
28
+ core: ['Bash','Read','Write','Edit','Glob','Grep','WebFetch','WebSearch','NotebookEdit','ToolSearch'],
29
+ agent: ['Agent','Skill','TaskOutput','TaskStop','AskUserQuestion','EnterPlanMode','ExitPlanMode'],
30
+ task: ['TaskCreate','TaskGet','TaskUpdate','TaskList'],
31
+ team: ['EnterWorktree','TeamCreate','TeamDelete','SendMessage'],
32
+ cron: ['CronCreate','CronDelete','CronList'],
33
+ };
34
+
35
+ function parseSystemBlocks(system) {
36
+ const result = {
37
+ billingHeader: 0, coreIdentity: 0, coreInstructions: 0,
38
+ customSkills: 0, customAgents: 0, pluginSkills: 0,
39
+ mcpServersList: 0, settingsJson: 0, envAndGit: 0, autoMemory: 0,
40
+ };
41
+ if (!system || !Array.isArray(system)) return result;
42
+
43
+ if (system[0]) result.billingHeader = safeCountTokens(system[0].text || '');
44
+ if (system[1]) result.coreIdentity = safeCountTokens(system[1].text || '');
45
+
46
+ const mainText = system.slice(2).map(b => b.text || '').join('\n');
47
+ if (!mainText) return result;
48
+
49
+ const markerDefs = [
50
+ { key: 'autoMemory', pattern: /# auto memory\n|You have a persistent, file-based memory/ },
51
+ { key: 'customSkills', pattern: /# User'?s Current Configuration/ },
52
+ { key: 'customAgents', pattern: /\*\*Available custom agents/ },
53
+ { key: 'mcpServersList', pattern: /\*\*Configured MCP servers/ },
54
+ { key: 'pluginSkills', pattern: /\*\*Available plugin skills/ },
55
+ { key: 'settingsJson', pattern: /\*\*User's settings\.json/ },
56
+ { key: 'envAndGit', pattern: /# Environment\n|<env>/ },
57
+ ];
58
+ const positions = [];
59
+ for (const m of markerDefs) {
60
+ const match = m.pattern.exec(mainText);
61
+ if (match) positions.push({ key: m.key, index: match.index });
62
+ }
63
+ positions.sort((a, b) => a.index - b.index);
64
+
65
+ const firstPos = positions.length > 0 ? positions[0].index : mainText.length;
66
+ result.coreInstructions = safeCountTokens(mainText.slice(0, firstPos));
67
+ for (let i = 0; i < positions.length; i++) {
68
+ const start = positions[i].index;
69
+ const end = i + 1 < positions.length ? positions[i + 1].index : mainText.length;
70
+ result[positions[i].key] = safeCountTokens(mainText.slice(start, end));
71
+ }
72
+ return result;
73
+ }
74
+
75
+ function parseClaudeMdFromMessages(messages) {
76
+ const result = { globalClaudeMd: 0, projectClaudeMd: 0 };
77
+ if (!messages || !messages.length) return result;
78
+
79
+ const firstUser = messages.find(m => m.role === 'user');
80
+ if (!firstUser) return result;
81
+ const text = typeof firstUser.content === 'string' ? firstUser.content : JSON.stringify(firstUser.content);
82
+
83
+ const re = /Contents of ([^\n]+CLAUDE\.md)[^\n]*:\n([\s\S]*?)(?=Contents of [^\n]+CLAUDE\.md|$)/g;
84
+ let match;
85
+ while ((match = re.exec(text)) !== null) {
86
+ const filePath = match[1];
87
+ const content = match[2];
88
+ if (filePath.includes('/.claude/CLAUDE.md') || /~\/\.claude/.test(filePath)) {
89
+ result.globalClaudeMd += safeCountTokens(content);
90
+ } else {
91
+ result.projectClaudeMd += safeCountTokens(content);
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function categorizeTools(tools) {
98
+ if (!tools || !tools.length) return { byCategory: {}, mcpPlugins: [], counts: {} };
99
+
100
+ const byCategory = { core: [], agent: [], task: [], team: [], cron: [], mcp: [], other: [] };
101
+ const mcpPluginsMap = {};
102
+
103
+ for (const tool of tools) {
104
+ const name = tool.name || '';
105
+ if (name.startsWith('mcp__')) {
106
+ byCategory.mcp.push(tool);
107
+ const plugin = name.split('__')[1] || 'unknown';
108
+ if (!mcpPluginsMap[plugin]) mcpPluginsMap[plugin] = [];
109
+ mcpPluginsMap[plugin].push(tool);
110
+ } else {
111
+ let placed = false;
112
+ for (const [cat, names] of Object.entries(TOOL_CATEGORIES)) {
113
+ if (names.includes(name)) { byCategory[cat].push(tool); placed = true; break; }
114
+ }
115
+ if (!placed) byCategory.other.push(tool);
116
+ }
117
+ }
118
+
119
+ const mcpPlugins = Object.entries(mcpPluginsMap).map(([plugin, pluginTools]) => ({
120
+ plugin, count: pluginTools.length,
121
+ tokens: safeCountTokens(JSON.stringify(pluginTools)),
122
+ }));
123
+ const counts = Object.fromEntries(Object.entries(byCategory).map(([k, v]) => [k, v.length]));
124
+ return { byCategory, mcpPlugins, counts };
125
+ }
126
+
127
+ function parseSkillsFromMessages(messages) {
128
+ if (!messages) return [];
129
+ // Skills are injected via system-reminder in user messages:
130
+ // "The following skills are available for use with the Skill tool:\n- name: desc\n..."
131
+ for (const m of messages) {
132
+ if (m.role !== 'user') continue;
133
+ const blocks = Array.isArray(m.content) ? m.content : [{ type: 'text', text: m.content || '' }];
134
+ for (const block of blocks) {
135
+ const txt = block.type === 'text' ? (block.text || '') : '';
136
+ const idx = txt.indexOf('The following skills are available for use with the Skill tool:');
137
+ if (idx < 0) continue;
138
+ const section = txt.slice(idx);
139
+ const end = section.indexOf('</system-reminder>');
140
+ const body = end >= 0 ? section.slice(0, end) : section;
141
+ const seen = new Set();
142
+ for (const line of body.split('\n')) {
143
+ // Support both "- skill-name: description" and "- skill-name" (no description)
144
+ // Skill names may contain ":" (e.g. "sourceatlas:flow"), so split on ": " (colon-space)
145
+ const m2 = line.match(/^- (.+?)(?:: .+)?$/);
146
+ if (m2) { const n = m2[1].trim(); if (n) seen.add(n); }
147
+ }
148
+ if (seen.size > 0) return [...seen];
149
+ }
150
+ }
151
+ return [];
152
+ }
153
+
154
+ function analyzeContext(body) {
155
+ if (!body) return null;
156
+ const systemBreakdown = parseSystemBlocks(body.system);
157
+ const claudeMd = parseClaudeMdFromMessages(body.messages);
158
+ const loadedSkills = parseSkillsFromMessages(body.messages);
159
+ const toolsCat = categorizeTools(body.tools);
160
+ const toolTokens = {};
161
+ for (const [cat, arr] of Object.entries(toolsCat.byCategory)) {
162
+ toolTokens[cat] = arr.length > 0 ? safeCountTokens(JSON.stringify(arr)) : 0;
163
+ }
164
+ let messageTokens = 0;
165
+ if (body.messages) {
166
+ for (const m of body.messages) {
167
+ if (typeof m.content === 'string') {
168
+ messageTokens += safeCountTokens(m.content);
169
+ } else if (Array.isArray(m.content)) {
170
+ for (const block of m.content) {
171
+ if (block.type === 'text') messageTokens += safeCountTokens(block.text || '');
172
+ else if (block.type === 'tool_use') messageTokens += safeCountTokens(JSON.stringify(block.input || {}));
173
+ else if (block.type === 'tool_result') {
174
+ const c = block.content;
175
+ if (typeof c === 'string') messageTokens += safeCountTokens(c);
176
+ else if (Array.isArray(c)) messageTokens += c.reduce((s, b) => s + safeCountTokens(b.text || ''), 0);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ return {
183
+ systemBreakdown, claudeMd, messageTokens, loadedSkills,
184
+ toolsBreakdown: { mcpPlugins: toolsCat.mcpPlugins, counts: toolsCat.counts, toolTokens },
185
+ };
186
+ }
187
+
188
+ function tokenizeRequest(body) {
189
+ if (!body) return null;
190
+ const breakdown = {};
191
+ if (body.system) {
192
+ const text = typeof body.system === 'string' ? body.system : JSON.stringify(body.system);
193
+ breakdown.system = safeCountTokens(text);
194
+ }
195
+ if (body.tools && body.tools.length) {
196
+ breakdown.tools = safeCountTokens(JSON.stringify(body.tools));
197
+ }
198
+ if (body.messages && body.messages.length) {
199
+ let total = 0;
200
+ breakdown.perMessage = body.messages.map(m => {
201
+ let tokens = 0;
202
+ const blocks = [];
203
+ if (typeof m.content === 'string') {
204
+ const t = safeCountTokens(m.content);
205
+ tokens = t;
206
+ if (t > 0) blocks.push({ type: 'text', tokens: t });
207
+ } else if (Array.isArray(m.content)) {
208
+ for (const block of m.content) {
209
+ if (block.type === 'text') {
210
+ const t = safeCountTokens(block.text || '');
211
+ tokens += t;
212
+ if (t > 0) blocks.push({ type: 'text', tokens: t });
213
+ } else if (block.type === 'thinking') {
214
+ const t = safeCountTokens(block.thinking || '');
215
+ tokens += t;
216
+ if (t > 0) blocks.push({ type: 'thinking', tokens: t });
217
+ } else if (block.type === 'tool_use') {
218
+ const t = safeCountTokens(JSON.stringify(block.input || {}));
219
+ tokens += t;
220
+ if (t > 0) blocks.push({ type: 'tool_use', name: block.name || null, tokens: t });
221
+ } else if (block.type === 'tool_result') {
222
+ const c = block.content;
223
+ let t = 0;
224
+ if (typeof c === 'string') t = safeCountTokens(c);
225
+ else if (Array.isArray(c)) t = c.reduce((s, b) => s + safeCountTokens(b.text || ''), 0);
226
+ tokens += t;
227
+ if (t > 0) blocks.push({ type: 'tool_result', name: block.name || null, tokens: t });
228
+ }
229
+ }
230
+ }
231
+ total += tokens;
232
+ return { role: m.role, tokens, blocks };
233
+ });
234
+ breakdown.messages = total;
235
+ }
236
+ breakdown.total = (breakdown.system || 0) + (breakdown.tools || 0) + (breakdown.messages || 0);
237
+ breakdown.contextBreakdown = analyzeContext(body);
238
+ return breakdown;
239
+ }
240
+
241
+ function extractUsage(resData) {
242
+ if (!Array.isArray(resData)) return null;
243
+ const msgStart = resData.find(e => e.type === 'message_start');
244
+ const msgDelta = resData.find(e => e.type === 'message_delta');
245
+ const u = msgStart?.message?.usage || {};
246
+ return {
247
+ input_tokens: u.input_tokens || 0,
248
+ output_tokens: msgDelta?.usage?.output_tokens || u.output_tokens || 0,
249
+ cache_creation_input_tokens: u.cache_creation_input_tokens || 0,
250
+ cache_read_input_tokens: u.cache_read_input_tokens || 0,
251
+ };
252
+ }
253
+
254
+ function summarizeRequest(body) {
255
+ const lines = [];
256
+ lines.push(` Model: ${body.model || '?'}`);
257
+ if (body.system) {
258
+ const text = typeof body.system === 'string' ? body.system : JSON.stringify(body.system);
259
+ lines.push(` System: ${safeCountTokens(text).toLocaleString()} tokens`);
260
+ }
261
+ if (body.tools && body.tools.length > 0) {
262
+ const names = body.tools.map(t => t.name);
263
+ const preview = names.slice(0, 7).join(', ');
264
+ const suffix = names.length > 7 ? `, … (${names.length} total)` : '';
265
+ lines.push(` Tools: ${names.length} [${preview}${suffix}]`);
266
+ }
267
+ if (body.messages && body.messages.length > 0) {
268
+ const msgs = body.messages;
269
+ const userCount = msgs.filter(m => m.role === 'user').length;
270
+ const asstCount = msgs.filter(m => m.role === 'assistant').length;
271
+ lines.push(` Messages: ${msgs.length} (${userCount} user, ${asstCount} assistant)`);
272
+ }
273
+ return lines.join('\n');
274
+ }
275
+
276
+ function totalContextTokens(usage) {
277
+ if (!usage) return 0;
278
+ return (usage.input_tokens || 0)
279
+ + (usage.cache_creation_input_tokens || 0)
280
+ + (usage.cache_read_input_tokens || 0);
281
+ }
282
+
283
+ function printContextBar(usage, model, system) {
284
+ const { getMaxContext } = require('./config');
285
+ if (!usage) return;
286
+ const maxCtx = getMaxContext(model, system);
287
+ const used = totalContextTokens(usage);
288
+ if (!used) return;
289
+ const pct = Math.min(100, (used / maxCtx) * 100);
290
+ const barWidth = 40;
291
+ const filled = Math.round(barWidth * pct / 100);
292
+ const empty = barWidth - filled;
293
+ const color = pct > 90 ? '\x1b[31m' : pct > 70 ? '\x1b[33m' : '\x1b[32m';
294
+ const bar = color + '█'.repeat(filled) + '\x1b[90m' + '░'.repeat(empty) + '\x1b[0m';
295
+ console.log(` Context ${bar} ${pct.toFixed(0)}% (${used.toLocaleString()} / ${maxCtx.toLocaleString()})`);
296
+ const parts = [];
297
+ if (usage.cache_read_input_tokens) parts.push(`cache:${usage.cache_read_input_tokens.toLocaleString()}↩`);
298
+ if (usage.cache_creation_input_tokens) parts.push(`${usage.cache_creation_input_tokens.toLocaleString()}↗`);
299
+ if (parts.length) console.log(` ${parts.join(' ')}`);
300
+ }
301
+
302
+ function computeThinkingDuration(events) {
303
+ let start = null, end = null;
304
+ for (const ev of events) {
305
+ if (!ev._ts) continue;
306
+ if (ev.type === 'content_block_start' && ev.content_block?.type === 'thinking') start = ev._ts;
307
+ else if (ev.type === 'content_block_stop' && start && !end) end = ev._ts;
308
+ }
309
+ return (start && end) ? (end - start) / 1000 : null;
310
+ }
311
+
312
+ function parseSSEEvents(raw) {
313
+ const events = [];
314
+ for (const line of raw.split('\n')) {
315
+ if (line.startsWith('data: ')) {
316
+ const data = line.slice(6).trim();
317
+ if (data === '[DONE]') continue;
318
+ try { events.push(JSON.parse(data)); } catch {}
319
+ }
320
+ }
321
+ return events;
322
+ }
323
+
324
+ // ── Turn title extraction ─────────────────────────────────────────
325
+ function extractResponseTitle(res) {
326
+ if (!res) return null;
327
+ let text = '';
328
+ if (Array.isArray(res)) {
329
+ text = res
330
+ .filter(ev => ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta')
331
+ .map(ev => ev.delta.text).join('');
332
+ } else if (res.content) {
333
+ text = (res.content || []).filter(b => b.type === 'text').map(b => b.text).join('');
334
+ }
335
+ text = text.trim().replace(/\s+/g, ' ');
336
+ if (!text) return null;
337
+ const firstSentence = text.split(/[.\n]/)[0].trim();
338
+ return (firstSentence || text).slice(0, 80) || null;
339
+ }
340
+
341
+ // ── Tool usage extraction ────────────────────────────────────────────
342
+ function extractToolCalls(messages) {
343
+ const counts = {};
344
+ (messages || []).forEach(m => {
345
+ if (!Array.isArray(m.content)) return;
346
+ m.content.forEach(b => {
347
+ if (b.type === 'tool_use' && b.name) counts[b.name] = (counts[b.name] || 0) + 1;
348
+ });
349
+ });
350
+ return counts;
351
+ }
352
+
353
+ function extractDuplicateToolCalls(messages) {
354
+ const seen = {}; // key → { name, count }
355
+ (messages || []).forEach(m => {
356
+ if (!Array.isArray(m.content)) return;
357
+ m.content.forEach(b => {
358
+ if (b.type !== 'tool_use' || !b.name) return;
359
+ const inputStr = JSON.stringify(b.input || {});
360
+ // Large inputs (>10KB): truncated key (may over-count, acceptable tradeoff)
361
+ const key = b.name + '\0' + (inputStr.length > 10240 ? inputStr.slice(0, 200) : inputStr);
362
+ if (!seen[key]) seen[key] = { name: b.name, count: 0 };
363
+ seen[key].count++;
364
+ });
365
+ });
366
+ const dupes = {};
367
+ for (const { name, count } of Object.values(seen)) {
368
+ if (count > 1) dupes[name] = (dupes[name] || 0) + (count - 1);
369
+ }
370
+ return Object.keys(dupes).length > 0 ? dupes : null;
371
+ }
372
+
373
+ module.exports = {
374
+ timestamp,
375
+ taipeiTime,
376
+ printSeparator,
377
+ safeCountTokens,
378
+ TOOL_CATEGORIES,
379
+ parseSystemBlocks,
380
+ parseClaudeMdFromMessages,
381
+ categorizeTools,
382
+ analyzeContext,
383
+ tokenizeRequest,
384
+ extractUsage,
385
+ summarizeRequest,
386
+ totalContextTokens,
387
+ printContextBar,
388
+ computeThinkingDuration,
389
+ parseSSEEvents,
390
+ extractResponseTitle,
391
+ extractToolCalls,
392
+ extractDuplicateToolCalls,
393
+ };