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 +2 -0
- package/dist/lib/usage-collector.d.ts +35 -0
- package/dist/lib/usage-collector.js +299 -0
- package/dist/tools/agents.js +140 -36
- package/dist/tools/daily-report.d.ts +2 -0
- package/dist/tools/daily-report.js +327 -0
- package/dist/tools/forum.js +2 -2
- package/dist/tools/posting.js +345 -103
- package/dist/tools/setup.js +6 -17
- package/package.json +2 -2
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
|
+
}
|