codeblog-mcp 2.6.1 → 2.8.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/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { registerSessionTools } from "./tools/sessions.js";
6
6
  import { registerPostingTools } from "./tools/posting.js";
7
7
  import { registerForumTools } from "./tools/forum.js";
8
8
  import { registerAgentTools } from "./tools/agents.js";
9
+ import { registerDailyReportTools } from "./tools/daily-report.js";
9
10
  function getVersion() {
10
11
  try {
11
12
  const req = createRequire(import.meta.url);
@@ -31,5 +32,6 @@ export function createServer(version) {
31
32
  registerPostingTools(server);
32
33
  registerForumTools(server);
33
34
  registerAgentTools(server);
35
+ registerDailyReportTools(server);
34
36
  return server;
35
37
  }
@@ -0,0 +1,35 @@
1
+ export interface ModelTokens {
2
+ inputTokens: number;
3
+ outputTokens: number;
4
+ cacheCreationTokens: number;
5
+ cacheReadTokens: number;
6
+ costUSD: number;
7
+ }
8
+ export interface ProjectStats {
9
+ name: string;
10
+ path: string;
11
+ sessionCount: number;
12
+ messageCount: number;
13
+ tokensUsed: number;
14
+ }
15
+ export interface IdeStats {
16
+ source: string;
17
+ sessionCount: number;
18
+ messageCount: number;
19
+ }
20
+ export interface DailyUsageStats {
21
+ date: string;
22
+ timezone: string;
23
+ totalSessions: number;
24
+ totalConversations: number;
25
+ totalMessages: number;
26
+ tokensByModel: Record<string, ModelTokens>;
27
+ totalTokens: number;
28
+ totalCostUSD: number;
29
+ projects: ProjectStats[];
30
+ ideBreakdown: IdeStats[];
31
+ hourlyActivity: Record<number, number>;
32
+ }
33
+ export declare function collectDailyUsage(targetDate: string, timezone?: string): DailyUsageStats;
34
+ export declare function formatTokens(n: number): string;
35
+ export declare function formatCost(usd: number): string;
@@ -0,0 +1,299 @@
1
+ import * as path from "path";
2
+ import { getHome } from "./platform.js";
3
+ import { listFiles, listDirs, readJsonl, decodeDirNameToPath } from "./fs-utils.js";
4
+ import { scanAll } from "./registry.js";
5
+ const FAMILY_PRICING = {
6
+ opus: { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.50 },
7
+ sonnet: { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.30 },
8
+ haiku: { input: 1, output: 5, cacheCreation: 1.25, cacheRead: 0.10 },
9
+ };
10
+ function getPricing(model) {
11
+ const lower = model.toLowerCase();
12
+ if (lower.includes("opus"))
13
+ return FAMILY_PRICING.opus;
14
+ if (lower.includes("haiku"))
15
+ return FAMILY_PRICING.haiku;
16
+ return FAMILY_PRICING.sonnet;
17
+ }
18
+ function calculateCost(model, input, output, cacheCreation, cacheRead) {
19
+ const p = getPricing(model);
20
+ return ((input * p.input +
21
+ output * p.output +
22
+ cacheCreation * p.cacheCreation +
23
+ cacheRead * p.cacheRead) /
24
+ 1_000_000);
25
+ }
26
+ // ─── Date helpers ────────────────────────────────────────────────────
27
+ function toLocalDate(isoTimestamp, timezone) {
28
+ try {
29
+ const d = new Date(isoTimestamp);
30
+ const parts = new Intl.DateTimeFormat("en-CA", {
31
+ timeZone: timezone,
32
+ year: "numeric",
33
+ month: "2-digit",
34
+ day: "2-digit",
35
+ }).formatToParts(d);
36
+ const y = parts.find((p) => p.type === "year")?.value;
37
+ const m = parts.find((p) => p.type === "month")?.value;
38
+ const dd = parts.find((p) => p.type === "day")?.value;
39
+ return `${y}-${m}-${dd}`;
40
+ }
41
+ catch {
42
+ return isoTimestamp.slice(0, 10);
43
+ }
44
+ }
45
+ function toLocalHour(isoTimestamp, timezone) {
46
+ try {
47
+ const d = new Date(isoTimestamp);
48
+ return parseInt(new Intl.DateTimeFormat("en-US", {
49
+ timeZone: timezone,
50
+ hour: "numeric",
51
+ hour12: false,
52
+ }).format(d));
53
+ }
54
+ catch {
55
+ return new Date(isoTimestamp).getHours();
56
+ }
57
+ }
58
+ // ─── Main collector ──────────────────────────────────────────────────
59
+ export function collectDailyUsage(targetDate, timezone) {
60
+ const tz = timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
61
+ const projectsDir = path.join(getHome(), ".claude", "projects");
62
+ const tokensByModel = {};
63
+ const projectMap = new Map();
64
+ const sessionIds = new Set();
65
+ let totalConversations = 0;
66
+ let totalMessages = 0;
67
+ const hourlyActivity = {};
68
+ const projectDirs = listDirs(projectsDir);
69
+ for (const projectDir of projectDirs) {
70
+ const dirName = path.basename(projectDir);
71
+ const files = listFiles(projectDir, [".jsonl"]);
72
+ for (const filePath of files) {
73
+ const entries = readJsonl(filePath);
74
+ if (entries.length === 0)
75
+ continue;
76
+ // Check if any entry in this file matches the target date
77
+ let hasMatchingDate = false;
78
+ let projectPath = "";
79
+ let projectName = dirName;
80
+ const sessionId = path.basename(filePath, ".jsonl");
81
+ for (const entry of entries) {
82
+ if (!entry.timestamp)
83
+ continue;
84
+ const entryDate = toLocalDate(entry.timestamp, tz);
85
+ if (entryDate !== targetDate)
86
+ continue;
87
+ hasMatchingDate = true;
88
+ // Extract project info from cwd
89
+ if (!projectPath && entry.cwd) {
90
+ projectPath = entry.cwd;
91
+ projectName = path.basename(projectPath);
92
+ }
93
+ // Count user messages as conversations
94
+ if (entry.type === "user") {
95
+ // Skip tool_result-only messages
96
+ const content = entry.message?.content;
97
+ const isToolResult = Array.isArray(content) &&
98
+ content.length > 0 &&
99
+ content.every((c) => c.type === "tool_result");
100
+ if (!isToolResult) {
101
+ totalConversations++;
102
+ totalMessages++;
103
+ const hour = toLocalHour(entry.timestamp, tz);
104
+ hourlyActivity[hour] = (hourlyActivity[hour] || 0) + 1;
105
+ }
106
+ }
107
+ // Extract usage from assistant messages
108
+ if (entry.type === "assistant" && entry.message?.usage) {
109
+ totalMessages++;
110
+ sessionIds.add(sessionId);
111
+ const usage = entry.message.usage;
112
+ const model = entry.message.model || "unknown";
113
+ // Skip synthetic/internal entries with no real tokens
114
+ if (model === "<synthetic>")
115
+ continue;
116
+ const input = usage.input_tokens || 0;
117
+ const output = usage.output_tokens || 0;
118
+ const cacheCreation = usage.cache_creation_input_tokens || 0;
119
+ const cacheRead = usage.cache_read_input_tokens || 0;
120
+ const tokens = input + output + cacheCreation + cacheRead;
121
+ // Use pre-calculated cost if available, otherwise calculate
122
+ const cost = entry.costUSD && entry.costUSD > 0
123
+ ? entry.costUSD
124
+ : calculateCost(model, input, output, cacheCreation, cacheRead);
125
+ // Accumulate by model
126
+ if (!tokensByModel[model]) {
127
+ tokensByModel[model] = {
128
+ inputTokens: 0,
129
+ outputTokens: 0,
130
+ cacheCreationTokens: 0,
131
+ cacheReadTokens: 0,
132
+ costUSD: 0,
133
+ };
134
+ }
135
+ tokensByModel[model].inputTokens += input;
136
+ tokensByModel[model].outputTokens += output;
137
+ tokensByModel[model].cacheCreationTokens += cacheCreation;
138
+ tokensByModel[model].cacheReadTokens += cacheRead;
139
+ tokensByModel[model].costUSD += cost;
140
+ // Accumulate by project
141
+ const pKey = projectPath || dirName;
142
+ if (!projectMap.has(pKey)) {
143
+ projectMap.set(pKey, {
144
+ path: projectPath || dirName,
145
+ sessions: new Set(),
146
+ messages: 0,
147
+ tokens: 0,
148
+ });
149
+ }
150
+ const proj = projectMap.get(pKey);
151
+ proj.sessions.add(sessionId);
152
+ proj.messages++;
153
+ proj.tokens += tokens;
154
+ }
155
+ }
156
+ // If no matching date entries, also try to decode project path for later
157
+ if (!hasMatchingDate)
158
+ continue;
159
+ // Ensure project path is decoded if we only have dir name
160
+ if (!projectPath && dirName.startsWith("-")) {
161
+ projectPath = decodeDirNameToPath(dirName) || "";
162
+ if (projectPath)
163
+ projectName = path.basename(projectPath);
164
+ }
165
+ }
166
+ }
167
+ // Build project stats
168
+ const projects = Array.from(projectMap.entries())
169
+ .map(([, v]) => ({
170
+ name: path.basename(v.path),
171
+ path: v.path,
172
+ sessionCount: v.sessions.size,
173
+ messageCount: v.messages,
174
+ tokensUsed: v.tokens,
175
+ }))
176
+ .sort((a, b) => b.tokensUsed - a.tokensUsed);
177
+ // Calculate totals
178
+ let totalTokens = 0;
179
+ let totalCostUSD = 0;
180
+ for (const m of Object.values(tokensByModel)) {
181
+ totalTokens +=
182
+ m.inputTokens +
183
+ m.outputTokens +
184
+ m.cacheCreationTokens +
185
+ m.cacheReadTokens;
186
+ totalCostUSD += m.costUSD;
187
+ }
188
+ totalCostUSD = Math.round(totalCostUSD * 10000) / 10000;
189
+ // Get IDE breakdown from existing scanners (also merges other IDE sessions/projects)
190
+ const { ideBreakdown, otherIdeSessions, otherIdeConversations, otherIdeProjects } = collectIdeBreakdown(targetDate, tz, sessionIds.size, totalConversations);
191
+ // Merge other IDE projects into project list
192
+ for (const op of otherIdeProjects) {
193
+ const existing = projects.find((p) => op.path && p.path ? p.path === op.path : p.name === op.name);
194
+ if (existing) {
195
+ existing.sessionCount += op.sessionCount;
196
+ existing.messageCount += op.messageCount;
197
+ }
198
+ else {
199
+ projects.push(op);
200
+ }
201
+ }
202
+ projects.sort((a, b) => b.sessionCount - a.sessionCount);
203
+ return {
204
+ date: targetDate,
205
+ timezone: tz,
206
+ totalSessions: sessionIds.size + otherIdeSessions,
207
+ totalConversations: totalConversations + otherIdeConversations,
208
+ totalMessages,
209
+ tokensByModel,
210
+ totalTokens,
211
+ totalCostUSD,
212
+ projects,
213
+ ideBreakdown,
214
+ hourlyActivity,
215
+ };
216
+ }
217
+ function collectIdeBreakdown(targetDate, timezone, claudeCodeSessions, claudeCodeConversations) {
218
+ const breakdown = [];
219
+ let otherIdeSessions = 0;
220
+ let otherIdeConversations = 0;
221
+ const otherIdeProjects = [];
222
+ // Claude Code stats come from our own JSONL parsing above
223
+ if (claudeCodeSessions > 0) {
224
+ breakdown.push({
225
+ source: "claude-code",
226
+ sessionCount: claudeCodeSessions,
227
+ messageCount: claudeCodeConversations,
228
+ });
229
+ }
230
+ // Scan other IDEs via the scanner registry
231
+ try {
232
+ const allSessions = scanAll(200);
233
+ const otherSources = new Map();
234
+ const otherProjectMap = new Map();
235
+ for (const session of allSessions) {
236
+ if (session.source === "claude-code")
237
+ continue;
238
+ // Check if session was modified on the target date
239
+ const sessionDate = toLocalDate(session.modifiedAt.toISOString(), timezone);
240
+ if (sessionDate !== targetDate)
241
+ continue;
242
+ // IDE stats
243
+ if (!otherSources.has(session.source)) {
244
+ otherSources.set(session.source, { sessions: 0, messages: 0 });
245
+ }
246
+ const s = otherSources.get(session.source);
247
+ s.sessions++;
248
+ s.messages += session.humanMessages;
249
+ // Project stats
250
+ const projPath = (session.project || "").trim() || "unknown-project";
251
+ const projName = path.basename(projPath) || projPath;
252
+ if (!otherProjectMap.has(projPath)) {
253
+ otherProjectMap.set(projPath, {
254
+ name: projName,
255
+ path: projPath,
256
+ sessions: new Set(),
257
+ messages: 0,
258
+ });
259
+ }
260
+ const p = otherProjectMap.get(projPath);
261
+ p.sessions.add(session.id);
262
+ p.messages += session.messageCount;
263
+ }
264
+ for (const [source, stats] of otherSources) {
265
+ breakdown.push({
266
+ source,
267
+ sessionCount: stats.sessions,
268
+ messageCount: stats.messages,
269
+ });
270
+ otherIdeSessions += stats.sessions;
271
+ otherIdeConversations += stats.messages;
272
+ }
273
+ for (const [, p] of otherProjectMap) {
274
+ otherIdeProjects.push({
275
+ name: p.name,
276
+ path: p.path,
277
+ sessionCount: p.sessions.size,
278
+ messageCount: p.messages,
279
+ tokensUsed: 0, // Other IDEs don't provide token data
280
+ });
281
+ }
282
+ }
283
+ catch {
284
+ // Scanner errors are non-critical
285
+ }
286
+ breakdown.sort((a, b) => b.sessionCount - a.sessionCount);
287
+ return { ideBreakdown: breakdown, otherIdeSessions, otherIdeConversations, otherIdeProjects };
288
+ }
289
+ // ─── Formatting helpers ──────────────────────────────────────────────
290
+ export function formatTokens(n) {
291
+ if (n >= 1_000_000)
292
+ return `${(n / 1_000_000).toFixed(1)}M`;
293
+ if (n >= 1_000)
294
+ return `${(n / 1_000).toFixed(1)}K`;
295
+ return String(n);
296
+ }
297
+ export function formatCost(usd) {
298
+ return `$${usd.toFixed(2)}`;
299
+ }