ccjk 13.3.15 → 13.3.16
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/dist/chunks/commit.mjs +2 -0
- package/dist/chunks/completion.mjs +1 -0
- package/dist/chunks/help.mjs +1 -0
- package/dist/chunks/impact.mjs +651 -0
- package/dist/chunks/package.mjs +1 -1
- package/dist/chunks/persistence.mjs +38 -0
- package/dist/chunks/skills-sync.mjs +4 -0
- package/dist/chunks/stats.mjs +4 -223
- package/dist/cli.mjs +22 -0
- package/dist/shared/ccjk.Bb9Mpi2O.mjs +19 -0
- package/dist/shared/ccjk.OTnevPNE.mjs +225 -0
- package/package.json +68 -65
package/dist/chunks/commit.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import a from './index2.mjs';
|
|
2
2
|
import { i as inquirer } from './index3.mjs';
|
|
3
3
|
import { exec as q } from './main.mjs';
|
|
4
|
+
import { s as showImpactReminder } from '../shared/ccjk.Bb9Mpi2O.mjs';
|
|
4
5
|
import '../shared/ccjk.BAGoDD49.mjs';
|
|
5
6
|
import 'node:readline';
|
|
6
7
|
import 'stream';
|
|
@@ -129,6 +130,7 @@ async function commit(options = {}) {
|
|
|
129
130
|
try {
|
|
130
131
|
await commitChanges(message);
|
|
131
132
|
console.log(a.green("\n\u2713 Changes committed"));
|
|
133
|
+
showImpactReminder("commit");
|
|
132
134
|
} catch (error) {
|
|
133
135
|
console.log(a.red(`
|
|
134
136
|
\u2717 Commit failed: ${error}`));
|
|
@@ -290,6 +290,7 @@ const COMPLETION_COMMANDS = [
|
|
|
290
290
|
// Other Commands
|
|
291
291
|
{ name: "ccr", description: "Configure Claude Code Router" },
|
|
292
292
|
{ name: "ccu", description: "Claude Code usage analysis" },
|
|
293
|
+
{ name: "impact", description: "Usage impact page with daily token trends", aliases: ["gain"] },
|
|
293
294
|
{ name: "vim", description: "Vim mode configuration" },
|
|
294
295
|
{ name: "workflows", description: "Manage workflows", aliases: ["wf"] },
|
|
295
296
|
{ name: "stats", description: "Usage statistics" },
|
package/dist/chunks/help.mjs
CHANGED
|
@@ -25,6 +25,7 @@ const COMMAND_REFERENCE = [
|
|
|
25
25
|
{ name: "ccjk workflows", alias: "wf", description: "\u7BA1\u7406\u5DE5\u4F5C\u6D41", category: "other", examples: ["ccjk wf"] },
|
|
26
26
|
{ name: "ccjk ccr", description: "CCR \u4EE3\u7406\u7BA1\u7406", category: "other", examples: ["ccjk ccr"] },
|
|
27
27
|
{ name: "ccjk ccu", description: "\u4F7F\u7528\u91CF\u7EDF\u8BA1", category: "other", examples: ["ccjk ccu"] },
|
|
28
|
+
{ name: "ccjk impact", alias: "gain", description: "\u6BCF\u65E5 Token \u6548\u679C\u9875", category: "other", examples: ["ccjk impact", "ccjk impact --days 30"] },
|
|
28
29
|
{ name: "ccjk uninstall", description: "\u5378\u8F7D\u914D\u7F6E", category: "other", examples: ["ccjk uninstall"] }
|
|
29
30
|
];
|
|
30
31
|
const HELP_TOPICS = [
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import a from './index2.mjs';
|
|
4
|
+
import { getContextPersistence } from './persistence.mjs';
|
|
5
|
+
import { g as getStatsStorage } from '../shared/ccjk.OTnevPNE.mjs';
|
|
6
|
+
import { j as join, d as dirname } from '../shared/ccjk.bQ7Dh1g4.mjs';
|
|
7
|
+
import '../shared/ccjk.BAGoDD49.mjs';
|
|
8
|
+
import 'better-sqlite3';
|
|
9
|
+
|
|
10
|
+
async function impactCommand(options = {}) {
|
|
11
|
+
const report = collectImpactReport(options.days);
|
|
12
|
+
if (options.json) {
|
|
13
|
+
console.log(JSON.stringify(report, null, 2));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
renderImpactSummary(report);
|
|
17
|
+
const outputPath = options.output || getDefaultImpactReportPath();
|
|
18
|
+
writeImpactHtmlReport(report, outputPath);
|
|
19
|
+
console.log(a.dim(`HTML report: ${outputPath}`));
|
|
20
|
+
}
|
|
21
|
+
function collectImpactReport(daysInput) {
|
|
22
|
+
const days = normalizeDays(daysInput);
|
|
23
|
+
const storage = getStatsStorage();
|
|
24
|
+
const { startDate, endDate } = getDateRange(days);
|
|
25
|
+
const records = storage.getRecordsByDateRange(startDate, endDate);
|
|
26
|
+
const allUsageDates = storage.getAvailableDates();
|
|
27
|
+
const allUsageStartDate = allUsageDates[0];
|
|
28
|
+
const usageDailyMap = buildUsageDailyMap(records);
|
|
29
|
+
const windowDates = enumerateDates(startDate, endDate);
|
|
30
|
+
let compressionMetrics = [];
|
|
31
|
+
let topProjects = [];
|
|
32
|
+
let compressionNotes = [];
|
|
33
|
+
try {
|
|
34
|
+
const persistence = getContextPersistence();
|
|
35
|
+
compressionMetrics = persistence.getCompressionMetrics(void 0, {
|
|
36
|
+
startTime: toStartTimestamp(startDate),
|
|
37
|
+
endTime: toEndTimestamp(endDate),
|
|
38
|
+
sortOrder: "asc"
|
|
39
|
+
});
|
|
40
|
+
topProjects = persistence.listProjects().sort((a, b) => (b.total_tokens || 0) - (a.total_tokens || 0)).slice(0, 5).map((project) => ({
|
|
41
|
+
name: project.name || project.path || project.hash,
|
|
42
|
+
contexts: project.context_count || 0,
|
|
43
|
+
tokens: project.total_tokens || 0,
|
|
44
|
+
updatedAt: project.updated_at ? new Date(project.updated_at).toISOString() : void 0
|
|
45
|
+
}));
|
|
46
|
+
} catch {
|
|
47
|
+
compressionNotes.push("Context compression history is unavailable, so savings may be under-reported.");
|
|
48
|
+
}
|
|
49
|
+
const compressionDailyMap = buildCompressionDailyMap(compressionMetrics);
|
|
50
|
+
const daily = windowDates.map((date) => {
|
|
51
|
+
const usage = usageDailyMap.get(date);
|
|
52
|
+
const compression = compressionDailyMap.get(date);
|
|
53
|
+
return {
|
|
54
|
+
date,
|
|
55
|
+
requests: usage?.totalRequests || 0,
|
|
56
|
+
tokens: usage?.totalTokens || 0,
|
|
57
|
+
cost: usage?.totalCost || 0,
|
|
58
|
+
savedTokens: compression?.savedTokens || 0,
|
|
59
|
+
savedCost: compression?.savedCost || 0
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const activeDays = daily.filter((day) => day.requests > 0 || day.savedTokens > 0).length;
|
|
63
|
+
const totalRequests = daily.reduce((sum, day) => sum + day.requests, 0);
|
|
64
|
+
const totalTokens = daily.reduce((sum, day) => sum + day.tokens, 0);
|
|
65
|
+
const totalCost = daily.reduce((sum, day) => sum + day.cost, 0);
|
|
66
|
+
const totalSavedTokens = daily.reduce((sum, day) => sum + day.savedTokens, 0);
|
|
67
|
+
const totalSavedCost = daily.reduce((sum, day) => sum + day.savedCost, 0);
|
|
68
|
+
const totalOriginalTokens = compressionMetrics.reduce((sum, metric) => sum + metric.originalTokens, 0);
|
|
69
|
+
const averageCompressionRatio = totalOriginalTokens > 0 ? totalSavedTokens / totalOriginalTokens : 0;
|
|
70
|
+
const averageCompressionTimeMs = compressionMetrics.length > 0 ? compressionMetrics.reduce((sum, metric) => sum + metric.timeTakenMs, 0) / compressionMetrics.length : 0;
|
|
71
|
+
const today = daily[daily.length - 1] || emptyDailyPoint(endDate);
|
|
72
|
+
const yesterday = daily[daily.length - 2] || emptyDailyPoint(endDate);
|
|
73
|
+
const comparison = buildComparison(daily);
|
|
74
|
+
const modelSummary = summarizeModels(records);
|
|
75
|
+
const providerSummary = summarizeProviders(records);
|
|
76
|
+
const algorithmSummary = summarizeAlgorithms(compressionMetrics);
|
|
77
|
+
const notes = buildNotes({
|
|
78
|
+
allUsageStartDate,
|
|
79
|
+
daily,
|
|
80
|
+
comparison,
|
|
81
|
+
hasCompressionData: compressionMetrics.length > 0,
|
|
82
|
+
compressionNotes
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
86
|
+
rangeDays: days,
|
|
87
|
+
trackingStartDate: allUsageStartDate || daily.find((day) => day.savedTokens > 0)?.date,
|
|
88
|
+
baselineMethod: comparison ? "Comparison uses the earliest tracked window versus the most recent window." : "More history is needed before a baseline comparison is meaningful.",
|
|
89
|
+
today: {
|
|
90
|
+
...today,
|
|
91
|
+
deltaTokensVsYesterday: today.tokens - yesterday.tokens,
|
|
92
|
+
deltaSavedTokensVsYesterday: today.savedTokens - yesterday.savedTokens
|
|
93
|
+
},
|
|
94
|
+
totals: {
|
|
95
|
+
activeDays,
|
|
96
|
+
totalRequests,
|
|
97
|
+
totalTokens,
|
|
98
|
+
totalCost,
|
|
99
|
+
totalSavedTokens,
|
|
100
|
+
totalSavedCost,
|
|
101
|
+
averageDailyTokens: activeDays > 0 ? totalTokens / activeDays : 0,
|
|
102
|
+
averageDailySavedTokens: activeDays > 0 ? totalSavedTokens / activeDays : 0,
|
|
103
|
+
averageCompressionRatio,
|
|
104
|
+
totalCompressions: compressionMetrics.length,
|
|
105
|
+
averageCompressionTimeMs
|
|
106
|
+
},
|
|
107
|
+
comparison,
|
|
108
|
+
topProviders: providerSummary,
|
|
109
|
+
topModels: modelSummary,
|
|
110
|
+
topProjects,
|
|
111
|
+
topAlgorithms: algorithmSummary,
|
|
112
|
+
daily,
|
|
113
|
+
notes
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function normalizeDays(daysInput) {
|
|
117
|
+
if (!daysInput || Number.isNaN(daysInput)) {
|
|
118
|
+
return 14;
|
|
119
|
+
}
|
|
120
|
+
return Math.max(7, Math.min(90, Math.floor(daysInput)));
|
|
121
|
+
}
|
|
122
|
+
function getDateRange(days) {
|
|
123
|
+
const now = /* @__PURE__ */ new Date();
|
|
124
|
+
const end = formatDate(now);
|
|
125
|
+
const startTime = toStartTimestamp(end) - (days - 1) * 24 * 60 * 60 * 1e3;
|
|
126
|
+
const start = formatDate(new Date(startTime));
|
|
127
|
+
return { startDate: start, endDate: end };
|
|
128
|
+
}
|
|
129
|
+
function buildUsageDailyMap(records) {
|
|
130
|
+
const map = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const record of records) {
|
|
132
|
+
const date = formatDate(new Date(record.timestamp));
|
|
133
|
+
const existing = map.get(date) || {
|
|
134
|
+
date,
|
|
135
|
+
totalRequests: 0,
|
|
136
|
+
successfulRequests: 0,
|
|
137
|
+
failedRequests: 0,
|
|
138
|
+
totalInputTokens: 0,
|
|
139
|
+
totalOutputTokens: 0,
|
|
140
|
+
totalTokens: 0,
|
|
141
|
+
totalCost: 0,
|
|
142
|
+
averageLatency: 0,
|
|
143
|
+
providerStats: {}
|
|
144
|
+
};
|
|
145
|
+
existing.totalRequests++;
|
|
146
|
+
if (record.success) {
|
|
147
|
+
existing.successfulRequests++;
|
|
148
|
+
} else {
|
|
149
|
+
existing.failedRequests++;
|
|
150
|
+
}
|
|
151
|
+
existing.totalInputTokens += record.inputTokens || 0;
|
|
152
|
+
existing.totalOutputTokens += record.outputTokens || 0;
|
|
153
|
+
existing.totalTokens += record.totalTokens || 0;
|
|
154
|
+
existing.totalCost += record.cost || 0;
|
|
155
|
+
map.set(date, existing);
|
|
156
|
+
}
|
|
157
|
+
return map;
|
|
158
|
+
}
|
|
159
|
+
function buildCompressionDailyMap(metrics) {
|
|
160
|
+
const map = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const metric of metrics) {
|
|
162
|
+
const date = formatDate(new Date(metric.timestamp));
|
|
163
|
+
const existing = map.get(date) || { savedTokens: 0, savedCost: 0 };
|
|
164
|
+
const savedTokens = metric.originalTokens - metric.compressedTokens;
|
|
165
|
+
existing.savedTokens += savedTokens;
|
|
166
|
+
existing.savedCost += estimateCompressionCost(savedTokens);
|
|
167
|
+
map.set(date, existing);
|
|
168
|
+
}
|
|
169
|
+
return map;
|
|
170
|
+
}
|
|
171
|
+
function summarizeProviders(records) {
|
|
172
|
+
const map = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const record of records) {
|
|
174
|
+
const key = record.provider || "unknown";
|
|
175
|
+
const current = map.get(key) || { requests: 0, tokens: 0, cost: 0 };
|
|
176
|
+
current.requests++;
|
|
177
|
+
current.tokens += record.totalTokens || 0;
|
|
178
|
+
current.cost += record.cost || 0;
|
|
179
|
+
map.set(key, current);
|
|
180
|
+
}
|
|
181
|
+
return Array.from(map.entries()).map(([provider, value]) => ({ provider, ...value })).sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
182
|
+
}
|
|
183
|
+
function summarizeModels(records) {
|
|
184
|
+
const map = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const record of records) {
|
|
186
|
+
if (!record.model) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const current = map.get(record.model) || { requests: 0, tokens: 0 };
|
|
190
|
+
current.requests++;
|
|
191
|
+
current.tokens += record.totalTokens || 0;
|
|
192
|
+
map.set(record.model, current);
|
|
193
|
+
}
|
|
194
|
+
return Array.from(map.entries()).map(([model, value]) => ({ model, ...value })).sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
195
|
+
}
|
|
196
|
+
function summarizeAlgorithms(metrics) {
|
|
197
|
+
const map = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const metric of metrics) {
|
|
199
|
+
const current = map.get(metric.algorithm) || { count: 0, savedTokens: 0 };
|
|
200
|
+
current.count++;
|
|
201
|
+
current.savedTokens += metric.originalTokens - metric.compressedTokens;
|
|
202
|
+
map.set(metric.algorithm, current);
|
|
203
|
+
}
|
|
204
|
+
return Array.from(map.entries()).map(([algorithm, value]) => ({ algorithm, ...value })).sort((a, b) => b.savedTokens - a.savedTokens).slice(0, 5);
|
|
205
|
+
}
|
|
206
|
+
function buildComparison(daily) {
|
|
207
|
+
const activeDays = daily.filter((day) => day.requests > 0 || day.savedTokens > 0);
|
|
208
|
+
if (activeDays.length < 4) {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
const windowSize = Math.min(7, Math.floor(activeDays.length / 2));
|
|
212
|
+
const baselineDays = activeDays.slice(0, windowSize);
|
|
213
|
+
const recentDays = activeDays.slice(-windowSize);
|
|
214
|
+
const baseline = summarizeWindow(baselineDays);
|
|
215
|
+
const recent = summarizeWindow(recentDays);
|
|
216
|
+
return {
|
|
217
|
+
baseline,
|
|
218
|
+
recent,
|
|
219
|
+
tokenDeltaPercent: calculatePercentDelta(baseline.avgDailyTokens, recent.avgDailyTokens),
|
|
220
|
+
costDeltaPercent: calculatePercentDelta(baseline.avgDailyCost, recent.avgDailyCost),
|
|
221
|
+
savedTokensDeltaPercent: calculatePercentDelta(baseline.avgDailySavedTokens, recent.avgDailySavedTokens)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function summarizeWindow(days) {
|
|
225
|
+
const dayCount = Math.max(days.length, 1);
|
|
226
|
+
return {
|
|
227
|
+
startDate: days[0].date,
|
|
228
|
+
endDate: days[days.length - 1].date,
|
|
229
|
+
avgDailyTokens: days.reduce((sum, day) => sum + day.tokens, 0) / dayCount,
|
|
230
|
+
avgDailyCost: days.reduce((sum, day) => sum + day.cost, 0) / dayCount,
|
|
231
|
+
avgDailySavedTokens: days.reduce((sum, day) => sum + day.savedTokens, 0) / dayCount
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function calculatePercentDelta(baseline, current) {
|
|
235
|
+
if (baseline === 0) {
|
|
236
|
+
return current === 0 ? 0 : 100;
|
|
237
|
+
}
|
|
238
|
+
return (current - baseline) / baseline * 100;
|
|
239
|
+
}
|
|
240
|
+
function buildNotes(input) {
|
|
241
|
+
const notes = [...input.compressionNotes];
|
|
242
|
+
const usageDays = input.daily.filter((day) => day.requests > 0).length;
|
|
243
|
+
if (!input.allUsageStartDate) {
|
|
244
|
+
notes.push("No API usage history was found under ~/.ccjk/stats yet. The report is currently driven by compression history only.");
|
|
245
|
+
}
|
|
246
|
+
if (!input.hasCompressionData) {
|
|
247
|
+
notes.push("Compression savings data is not available yet, so the page can only show raw usage where records exist.");
|
|
248
|
+
}
|
|
249
|
+
if (!input.comparison) {
|
|
250
|
+
notes.push("At least four active days are needed before before/after comparison cards become meaningful.");
|
|
251
|
+
}
|
|
252
|
+
if (usageDays > 0 && input.allUsageStartDate) {
|
|
253
|
+
notes.push(`Tracking currently starts at ${input.allUsageStartDate}. Until an explicit install marker exists, the baseline uses earliest tracked data.`);
|
|
254
|
+
}
|
|
255
|
+
return notes;
|
|
256
|
+
}
|
|
257
|
+
function renderImpactSummary(report) {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(a.cyan.bold("CCJK Usage Impact"));
|
|
260
|
+
console.log(a.gray("=".repeat(72)));
|
|
261
|
+
console.log(a.dim(`${report.rangeDays}-day view ending ${report.today.date}`));
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log(a.yellow("Today"));
|
|
264
|
+
console.log(` Tokens: ${a.white.bold(formatInteger(report.today.tokens))} ${formatSigned(report.today.deltaTokensVsYesterday, "vs yesterday")}`);
|
|
265
|
+
console.log(` Cost: ${a.white.bold(formatCurrency(report.today.cost))}`);
|
|
266
|
+
console.log(` Saved: ${a.green.bold(formatInteger(report.today.savedTokens))} ${formatSigned(report.today.deltaSavedTokensVsYesterday, "vs yesterday")}`);
|
|
267
|
+
console.log(` Requests: ${a.white.bold(formatInteger(report.today.requests))}`);
|
|
268
|
+
console.log("");
|
|
269
|
+
console.log(a.yellow("Range Summary"));
|
|
270
|
+
console.log(` Total tokens: ${a.white.bold(formatInteger(report.totals.totalTokens))}`);
|
|
271
|
+
console.log(` Total cost: ${a.white.bold(formatCurrency(report.totals.totalCost))}`);
|
|
272
|
+
console.log(` Saved tokens: ${a.green.bold(formatInteger(report.totals.totalSavedTokens))}`);
|
|
273
|
+
console.log(` Saved cost: ${a.green.bold(formatCurrency(report.totals.totalSavedCost))}`);
|
|
274
|
+
console.log(` Compression runs: ${a.white.bold(formatInteger(report.totals.totalCompressions))}`);
|
|
275
|
+
console.log(` Avg compression: ${a.white.bold(formatPercent(report.totals.averageCompressionRatio))}`);
|
|
276
|
+
console.log(` Active days: ${a.white.bold(formatInteger(report.totals.activeDays))}`);
|
|
277
|
+
console.log("");
|
|
278
|
+
if (report.comparison) {
|
|
279
|
+
console.log(a.yellow("Before / After"));
|
|
280
|
+
console.log(` Baseline: ${report.comparison.baseline.startDate} -> ${report.comparison.baseline.endDate}`);
|
|
281
|
+
console.log(` Recent: ${report.comparison.recent.startDate} -> ${report.comparison.recent.endDate}`);
|
|
282
|
+
console.log(` Avg daily tokens: ${a.white.bold(formatInteger(report.comparison.recent.avgDailyTokens))} (${formatSigned(report.comparison.tokenDeltaPercent, "vs baseline", true)})`);
|
|
283
|
+
console.log(` Avg daily cost: ${a.white.bold(formatCurrency(report.comparison.recent.avgDailyCost))} (${formatSigned(report.comparison.costDeltaPercent, "vs baseline", true)})`);
|
|
284
|
+
console.log(` Avg daily saved: ${a.green.bold(formatInteger(report.comparison.recent.avgDailySavedTokens))} (${formatSigned(report.comparison.savedTokensDeltaPercent, "vs baseline", true)})`);
|
|
285
|
+
console.log("");
|
|
286
|
+
}
|
|
287
|
+
if (report.topProviders.length > 0) {
|
|
288
|
+
console.log(a.yellow("Provider Summary"));
|
|
289
|
+
for (const provider of report.topProviders) {
|
|
290
|
+
console.log(` ${provider.provider.padEnd(18)} ${formatInteger(provider.tokens).padStart(10)} tokens ${formatCurrency(provider.cost).padStart(10)}`);
|
|
291
|
+
}
|
|
292
|
+
console.log("");
|
|
293
|
+
}
|
|
294
|
+
if (report.topModels.length > 0) {
|
|
295
|
+
console.log(a.yellow("Model Summary"));
|
|
296
|
+
for (const model of report.topModels) {
|
|
297
|
+
console.log(` ${truncate(model.model, 30).padEnd(32)} ${formatInteger(model.tokens).padStart(10)} tokens`);
|
|
298
|
+
}
|
|
299
|
+
console.log("");
|
|
300
|
+
}
|
|
301
|
+
if (report.topProjects.length > 0) {
|
|
302
|
+
console.log(a.yellow("Code Summary"));
|
|
303
|
+
for (const project of report.topProjects) {
|
|
304
|
+
console.log(` ${truncate(project.name, 30).padEnd(32)} ${formatInteger(project.tokens).padStart(10)} tokens ${formatInteger(project.contexts).padStart(6)} ctx`);
|
|
305
|
+
}
|
|
306
|
+
console.log("");
|
|
307
|
+
}
|
|
308
|
+
if (report.topAlgorithms.length > 0) {
|
|
309
|
+
console.log(a.yellow("Optimization Summary"));
|
|
310
|
+
for (const algorithm of report.topAlgorithms) {
|
|
311
|
+
console.log(` ${algorithm.algorithm.padEnd(18)} ${formatInteger(algorithm.savedTokens).padStart(10)} saved ${formatInteger(algorithm.count).padStart(6)} runs`);
|
|
312
|
+
}
|
|
313
|
+
console.log("");
|
|
314
|
+
}
|
|
315
|
+
if (report.notes.length > 0) {
|
|
316
|
+
console.log(a.yellow("Notes"));
|
|
317
|
+
for (const note of report.notes) {
|
|
318
|
+
console.log(` - ${note}`);
|
|
319
|
+
}
|
|
320
|
+
console.log("");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function writeImpactHtmlReport(report, outputPath) {
|
|
324
|
+
const outputDir = dirname(outputPath);
|
|
325
|
+
if (!existsSync(outputDir)) {
|
|
326
|
+
mkdirSync(outputDir, { recursive: true });
|
|
327
|
+
}
|
|
328
|
+
writeFileSync(outputPath, generateImpactHtml(report), "utf-8");
|
|
329
|
+
}
|
|
330
|
+
function generateImpactHtml(report) {
|
|
331
|
+
const maxUsage = Math.max(...report.daily.map((day) => day.tokens), 1);
|
|
332
|
+
const maxSaved = Math.max(...report.daily.map((day) => day.savedTokens), 1);
|
|
333
|
+
return `<!doctype html>
|
|
334
|
+
<html lang="en">
|
|
335
|
+
<head>
|
|
336
|
+
<meta charset="UTF-8" />
|
|
337
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
338
|
+
<title>CCJK Usage Impact</title>
|
|
339
|
+
<style>
|
|
340
|
+
:root {
|
|
341
|
+
--bg: #f6f1e8;
|
|
342
|
+
--panel: rgba(255, 252, 246, 0.86);
|
|
343
|
+
--ink: #1f2933;
|
|
344
|
+
--muted: #5b6770;
|
|
345
|
+
--accent: #0f766e;
|
|
346
|
+
--accent-soft: #99f6e4;
|
|
347
|
+
--usage: #d97706;
|
|
348
|
+
--saved: #0f766e;
|
|
349
|
+
--border: rgba(31, 41, 51, 0.12);
|
|
350
|
+
}
|
|
351
|
+
* { box-sizing: border-box; }
|
|
352
|
+
body {
|
|
353
|
+
margin: 0;
|
|
354
|
+
color: var(--ink);
|
|
355
|
+
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
|
|
356
|
+
background:
|
|
357
|
+
radial-gradient(circle at top left, rgba(217, 119, 6, 0.18), transparent 30%),
|
|
358
|
+
radial-gradient(circle at top right, rgba(15, 118, 110, 0.2), transparent 28%),
|
|
359
|
+
linear-gradient(180deg, #fffaf1 0%, var(--bg) 100%);
|
|
360
|
+
}
|
|
361
|
+
.shell {
|
|
362
|
+
max-width: 1180px;
|
|
363
|
+
margin: 0 auto;
|
|
364
|
+
padding: 32px 20px 48px;
|
|
365
|
+
}
|
|
366
|
+
.hero, .panel {
|
|
367
|
+
background: var(--panel);
|
|
368
|
+
backdrop-filter: blur(10px);
|
|
369
|
+
border: 1px solid var(--border);
|
|
370
|
+
border-radius: 24px;
|
|
371
|
+
box-shadow: 0 20px 60px rgba(31, 41, 51, 0.08);
|
|
372
|
+
}
|
|
373
|
+
.hero {
|
|
374
|
+
padding: 28px;
|
|
375
|
+
margin-bottom: 20px;
|
|
376
|
+
}
|
|
377
|
+
.eyebrow {
|
|
378
|
+
text-transform: uppercase;
|
|
379
|
+
letter-spacing: 0.12em;
|
|
380
|
+
color: var(--accent);
|
|
381
|
+
font-size: 12px;
|
|
382
|
+
font-weight: 700;
|
|
383
|
+
}
|
|
384
|
+
h1 {
|
|
385
|
+
margin: 10px 0 8px;
|
|
386
|
+
font-size: clamp(34px, 5vw, 56px);
|
|
387
|
+
line-height: 0.98;
|
|
388
|
+
}
|
|
389
|
+
.subtle {
|
|
390
|
+
color: var(--muted);
|
|
391
|
+
max-width: 760px;
|
|
392
|
+
font-size: 15px;
|
|
393
|
+
}
|
|
394
|
+
.stats {
|
|
395
|
+
display: grid;
|
|
396
|
+
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
397
|
+
gap: 14px;
|
|
398
|
+
margin-top: 24px;
|
|
399
|
+
}
|
|
400
|
+
.stat {
|
|
401
|
+
padding: 18px;
|
|
402
|
+
border-radius: 18px;
|
|
403
|
+
background: rgba(255,255,255,0.68);
|
|
404
|
+
border: 1px solid rgba(31, 41, 51, 0.08);
|
|
405
|
+
}
|
|
406
|
+
.stat .label {
|
|
407
|
+
font-size: 12px;
|
|
408
|
+
text-transform: uppercase;
|
|
409
|
+
letter-spacing: 0.08em;
|
|
410
|
+
color: var(--muted);
|
|
411
|
+
}
|
|
412
|
+
.stat .value {
|
|
413
|
+
margin-top: 8px;
|
|
414
|
+
font-size: 28px;
|
|
415
|
+
font-weight: 800;
|
|
416
|
+
}
|
|
417
|
+
.grid {
|
|
418
|
+
display: grid;
|
|
419
|
+
grid-template-columns: 1.4fr 1fr;
|
|
420
|
+
gap: 20px;
|
|
421
|
+
margin-top: 20px;
|
|
422
|
+
}
|
|
423
|
+
.panel {
|
|
424
|
+
padding: 22px;
|
|
425
|
+
}
|
|
426
|
+
.panel h2 {
|
|
427
|
+
margin: 0 0 14px;
|
|
428
|
+
font-size: 20px;
|
|
429
|
+
}
|
|
430
|
+
.chart {
|
|
431
|
+
display: grid;
|
|
432
|
+
gap: 10px;
|
|
433
|
+
}
|
|
434
|
+
.bar-row {
|
|
435
|
+
display: grid;
|
|
436
|
+
grid-template-columns: 86px 1fr 64px 64px;
|
|
437
|
+
gap: 10px;
|
|
438
|
+
align-items: center;
|
|
439
|
+
}
|
|
440
|
+
.bar-track {
|
|
441
|
+
position: relative;
|
|
442
|
+
height: 10px;
|
|
443
|
+
border-radius: 999px;
|
|
444
|
+
overflow: hidden;
|
|
445
|
+
background: rgba(31, 41, 51, 0.08);
|
|
446
|
+
}
|
|
447
|
+
.bar-usage, .bar-saved {
|
|
448
|
+
position: absolute;
|
|
449
|
+
top: 0;
|
|
450
|
+
left: 0;
|
|
451
|
+
bottom: 0;
|
|
452
|
+
border-radius: 999px;
|
|
453
|
+
}
|
|
454
|
+
.bar-usage { background: linear-gradient(90deg, #fb923c, var(--usage)); }
|
|
455
|
+
.bar-saved { background: linear-gradient(90deg, #5eead4, var(--saved)); opacity: 0.9; }
|
|
456
|
+
.mini {
|
|
457
|
+
color: var(--muted);
|
|
458
|
+
font-size: 12px;
|
|
459
|
+
}
|
|
460
|
+
table {
|
|
461
|
+
width: 100%;
|
|
462
|
+
border-collapse: collapse;
|
|
463
|
+
}
|
|
464
|
+
th, td {
|
|
465
|
+
padding: 10px 0;
|
|
466
|
+
text-align: left;
|
|
467
|
+
border-bottom: 1px solid rgba(31, 41, 51, 0.08);
|
|
468
|
+
font-size: 14px;
|
|
469
|
+
}
|
|
470
|
+
th {
|
|
471
|
+
color: var(--muted);
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
}
|
|
474
|
+
.note-list {
|
|
475
|
+
margin: 0;
|
|
476
|
+
padding-left: 18px;
|
|
477
|
+
color: var(--muted);
|
|
478
|
+
}
|
|
479
|
+
@media (max-width: 900px) {
|
|
480
|
+
.grid { grid-template-columns: 1fr; }
|
|
481
|
+
.bar-row { grid-template-columns: 72px 1fr 56px 56px; }
|
|
482
|
+
}
|
|
483
|
+
</style>
|
|
484
|
+
</head>
|
|
485
|
+
<body>
|
|
486
|
+
<div class="shell">
|
|
487
|
+
<section class="hero">
|
|
488
|
+
<div class="eyebrow">Daily proof of value</div>
|
|
489
|
+
<h1>CCJK Usage Impact</h1>
|
|
490
|
+
<p class="subtle">A simple answer to the product question: is CCJK reducing usage and increasing token efficiency over time?</p>
|
|
491
|
+
<div class="stats">
|
|
492
|
+
${statCard("Today tokens", formatInteger(report.today.tokens))}
|
|
493
|
+
${statCard("Today saved", formatInteger(report.today.savedTokens))}
|
|
494
|
+
${statCard("Today cost", formatCurrency(report.today.cost))}
|
|
495
|
+
${statCard(`${report.rangeDays}d saved`, formatInteger(report.totals.totalSavedTokens))}
|
|
496
|
+
${statCard("Compression runs", formatInteger(report.totals.totalCompressions))}
|
|
497
|
+
${statCard("Avg compression", formatPercent(report.totals.averageCompressionRatio))}
|
|
498
|
+
</div>
|
|
499
|
+
</section>
|
|
500
|
+
|
|
501
|
+
<div class="grid">
|
|
502
|
+
<section class="panel">
|
|
503
|
+
<h2>Daily effect</h2>
|
|
504
|
+
<div class="chart">
|
|
505
|
+
${report.daily.map((day) => `
|
|
506
|
+
<div class="bar-row">
|
|
507
|
+
<div class="mini">${day.date.slice(5)}</div>
|
|
508
|
+
<div class="bar-track">
|
|
509
|
+
<div class="bar-usage" style="width:${day.tokens / maxUsage * 100}%"></div>
|
|
510
|
+
<div class="bar-saved" style="width:${day.savedTokens / maxSaved * 100}%"></div>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="mini">${compactNumber(day.tokens)}</div>
|
|
513
|
+
<div class="mini">${compactNumber(day.savedTokens)}</div>
|
|
514
|
+
</div>
|
|
515
|
+
`).join("")}
|
|
516
|
+
</div>
|
|
517
|
+
<p class="mini">Orange shows used tokens. Teal shows tokens saved through compression and context optimization.</p>
|
|
518
|
+
</section>
|
|
519
|
+
|
|
520
|
+
<section class="panel">
|
|
521
|
+
<h2>Before / after</h2>
|
|
522
|
+
${report.comparison ? `
|
|
523
|
+
<table>
|
|
524
|
+
<tr><th>Window</th><th>Avg tokens/day</th><th>Avg saved/day</th></tr>
|
|
525
|
+
<tr><td>${report.comparison.baseline.startDate} \u2192 ${report.comparison.baseline.endDate}</td><td>${formatInteger(report.comparison.baseline.avgDailyTokens)}</td><td>${formatInteger(report.comparison.baseline.avgDailySavedTokens)}</td></tr>
|
|
526
|
+
<tr><td>${report.comparison.recent.startDate} \u2192 ${report.comparison.recent.endDate}</td><td>${formatInteger(report.comparison.recent.avgDailyTokens)}</td><td>${formatInteger(report.comparison.recent.avgDailySavedTokens)}</td></tr>
|
|
527
|
+
</table>
|
|
528
|
+
<p class="mini">Avg daily tokens delta: ${escapeHtml(formatSignedPlain(report.comparison.tokenDeltaPercent, "vs baseline", true))}</p>
|
|
529
|
+
<p class="mini">Avg daily saved tokens delta: ${escapeHtml(formatSignedPlain(report.comparison.savedTokensDeltaPercent, "vs baseline", true))}</p>
|
|
530
|
+
` : `<p class="mini">Need more active days before baseline comparison becomes trustworthy.</p>`}
|
|
531
|
+
</section>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div class="grid">
|
|
535
|
+
<section class="panel">
|
|
536
|
+
<h2>Provider and model summary</h2>
|
|
537
|
+
<table>
|
|
538
|
+
<tr><th>Provider</th><th>Requests</th><th>Tokens</th><th>Cost</th></tr>
|
|
539
|
+
${report.topProviders.map((provider) => `<tr><td>${escapeHtml(provider.provider)}</td><td>${formatInteger(provider.requests)}</td><td>${formatInteger(provider.tokens)}</td><td>${formatCurrency(provider.cost)}</td></tr>`).join("") || '<tr><td colspan="4">No provider history yet</td></tr>'}
|
|
540
|
+
</table>
|
|
541
|
+
<br />
|
|
542
|
+
<table>
|
|
543
|
+
<tr><th>Model</th><th>Requests</th><th>Tokens</th></tr>
|
|
544
|
+
${report.topModels.map((model) => `<tr><td>${escapeHtml(model.model)}</td><td>${formatInteger(model.requests)}</td><td>${formatInteger(model.tokens)}</td></tr>`).join("") || '<tr><td colspan="3">No model history yet</td></tr>'}
|
|
545
|
+
</table>
|
|
546
|
+
</section>
|
|
547
|
+
|
|
548
|
+
<section class="panel">
|
|
549
|
+
<h2>Code and optimization summary</h2>
|
|
550
|
+
<table>
|
|
551
|
+
<tr><th>Project</th><th>Contexts</th><th>Tokens</th></tr>
|
|
552
|
+
${report.topProjects.map((project) => `<tr><td>${escapeHtml(project.name)}</td><td>${formatInteger(project.contexts)}</td><td>${formatInteger(project.tokens)}</td></tr>`).join("") || '<tr><td colspan="3">No project history yet</td></tr>'}
|
|
553
|
+
</table>
|
|
554
|
+
<br />
|
|
555
|
+
<table>
|
|
556
|
+
<tr><th>Algorithm</th><th>Runs</th><th>Saved</th></tr>
|
|
557
|
+
${report.topAlgorithms.map((algorithm) => `<tr><td>${escapeHtml(algorithm.algorithm)}</td><td>${formatInteger(algorithm.count)}</td><td>${formatInteger(algorithm.savedTokens)}</td></tr>`).join("") || '<tr><td colspan="3">No optimization history yet</td></tr>'}
|
|
558
|
+
</table>
|
|
559
|
+
</section>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<section class="panel" style="margin-top:20px">
|
|
563
|
+
<h2>Notes</h2>
|
|
564
|
+
<ul class="note-list">
|
|
565
|
+
${report.notes.map((note) => `<li>${escapeHtml(note)}</li>`).join("")}
|
|
566
|
+
</ul>
|
|
567
|
+
<p class="mini">Generated at ${escapeHtml(report.generatedAt)} from local CCJK tracking data.</p>
|
|
568
|
+
</section>
|
|
569
|
+
</div>
|
|
570
|
+
</body>
|
|
571
|
+
</html>`;
|
|
572
|
+
}
|
|
573
|
+
function statCard(label, value) {
|
|
574
|
+
return `<div class="stat"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(value)}</div></div>`;
|
|
575
|
+
}
|
|
576
|
+
function getDefaultImpactReportPath() {
|
|
577
|
+
return join(homedir(), ".ccjk", "reports", "impact-latest.html");
|
|
578
|
+
}
|
|
579
|
+
function enumerateDates(startDate, endDate) {
|
|
580
|
+
const dates = [];
|
|
581
|
+
let cursor = toStartTimestamp(startDate);
|
|
582
|
+
const end = toStartTimestamp(endDate);
|
|
583
|
+
while (cursor <= end) {
|
|
584
|
+
dates.push(formatDate(new Date(cursor)));
|
|
585
|
+
cursor += 24 * 60 * 60 * 1e3;
|
|
586
|
+
}
|
|
587
|
+
return dates;
|
|
588
|
+
}
|
|
589
|
+
function toStartTimestamp(date) {
|
|
590
|
+
return (/* @__PURE__ */ new Date(`${date}T00:00:00`)).getTime();
|
|
591
|
+
}
|
|
592
|
+
function toEndTimestamp(date) {
|
|
593
|
+
return (/* @__PURE__ */ new Date(`${date}T23:59:59.999`)).getTime();
|
|
594
|
+
}
|
|
595
|
+
function emptyDailyPoint(date) {
|
|
596
|
+
return {
|
|
597
|
+
date,
|
|
598
|
+
requests: 0,
|
|
599
|
+
tokens: 0,
|
|
600
|
+
cost: 0,
|
|
601
|
+
savedTokens: 0,
|
|
602
|
+
savedCost: 0
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function estimateCompressionCost(tokens) {
|
|
606
|
+
return tokens / 1e3 * 0.015;
|
|
607
|
+
}
|
|
608
|
+
function formatDate(date) {
|
|
609
|
+
const year = date.getFullYear();
|
|
610
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
611
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
612
|
+
return `${year}-${month}-${day}`;
|
|
613
|
+
}
|
|
614
|
+
function formatInteger(value) {
|
|
615
|
+
return Math.round(value).toLocaleString();
|
|
616
|
+
}
|
|
617
|
+
function formatCurrency(value) {
|
|
618
|
+
return `$${value.toFixed(2)}`;
|
|
619
|
+
}
|
|
620
|
+
function formatPercent(value) {
|
|
621
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
622
|
+
}
|
|
623
|
+
function formatSigned(value, label, isPercent = false) {
|
|
624
|
+
const abs = isPercent ? `${Math.abs(value).toFixed(1)}%` : formatInteger(Math.abs(value));
|
|
625
|
+
const sign = value > 0 ? "+" : value < 0 ? "-" : "0";
|
|
626
|
+
const tone = value > 0 ? a.yellow : value < 0 ? a.green : a.gray;
|
|
627
|
+
const rendered = value === 0 ? `${abs} ${label}` : `${sign}${abs} ${label}`;
|
|
628
|
+
return tone(rendered);
|
|
629
|
+
}
|
|
630
|
+
function formatSignedPlain(value, label, isPercent = false) {
|
|
631
|
+
const abs = isPercent ? `${Math.abs(value).toFixed(1)}%` : formatInteger(Math.abs(value));
|
|
632
|
+
if (value === 0) {
|
|
633
|
+
return `${abs} ${label}`;
|
|
634
|
+
}
|
|
635
|
+
const sign = value > 0 ? "+" : "-";
|
|
636
|
+
return `${sign}${abs} ${label}`;
|
|
637
|
+
}
|
|
638
|
+
function compactNumber(value) {
|
|
639
|
+
return Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value);
|
|
640
|
+
}
|
|
641
|
+
function truncate(value, maxLength) {
|
|
642
|
+
if (value.length <= maxLength) {
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
return `${value.slice(0, maxLength - 1)}\u2026`;
|
|
646
|
+
}
|
|
647
|
+
function escapeHtml(value) {
|
|
648
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export { buildComparison, collectImpactReport, generateImpactHtml, impactCommand };
|
package/dist/chunks/package.mjs
CHANGED