costhawk 1.2.2 → 1.3.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/README.md +22 -0
- package/dist/codex-parser.d.ts +47 -0
- package/dist/codex-parser.d.ts.map +1 -0
- package/dist/codex-parser.js +275 -0
- package/dist/codex-parser.js.map +1 -0
- package/dist/index.js +529 -51
- package/dist/index.js.map +1 -1
- package/dist/transcript-parser.d.ts +12 -0
- package/dist/transcript-parser.d.ts.map +1 -1
- package/dist/transcript-parser.js +68 -10
- package/dist/transcript-parser.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,17 +3,20 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
// Claude Code local transcript parsing
|
|
6
|
-
import { claudeCodeDirectoryExists, discoverTranscripts,
|
|
6
|
+
import { claudeCodeDirectoryExists, discoverTranscripts, parseAllTranscriptsDetailed, } from "./transcript-parser.js";
|
|
7
|
+
import { codexDirectoryExists, discoverCodexSessions, parseAllCodexSessionsDetailed, } from "./codex-parser.js";
|
|
7
8
|
import { calculateCost, formatCost, formatTokens, calculateSavings, CLAUDE_SUBSCRIPTIONS, } from "./pricing-constants.js";
|
|
8
9
|
// Constants
|
|
9
10
|
const CHARACTER_LIMIT = 25000;
|
|
10
11
|
const REQUEST_TIMEOUT_MS = 30000; // 30 seconds
|
|
11
12
|
const API_BASE_URL = process.env.COSTHAWK_API_URL || "https://costhawk.ai";
|
|
12
13
|
const DEFAULT_API_KEY = process.env.COSTHAWK_API_KEY;
|
|
14
|
+
const CLIENT_VERSION = "1.3.1";
|
|
13
15
|
// Auto-sync configuration
|
|
14
16
|
const AUTO_SYNC_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
15
17
|
const AUTO_SYNC_MAX_AGE_HOURS = 24; // Sync sessions from last 24 hours
|
|
16
18
|
let autoSyncIntervalId = null;
|
|
19
|
+
const CODEX_AUTO_SYNC_ENABLED = process.env.COSTHAWK_CODEX_AUTO_SYNC === "true";
|
|
17
20
|
// AlertType enum values from backend
|
|
18
21
|
const ALERT_TYPE_VALUES = [
|
|
19
22
|
"BUDGET_EXCEEDED",
|
|
@@ -352,6 +355,12 @@ function formatClaudeCodeSyncResultMarkdown(data) {
|
|
|
352
355
|
output += `| Metric | Value |\n|--------|-------|\n`;
|
|
353
356
|
output += `| New Sessions | ${data.sessionsNew} |\n`;
|
|
354
357
|
output += `| Updated Sessions | ${data.sessionsUpdated} |\n`;
|
|
358
|
+
if (data.sessionsWithDailyUsage !== undefined) {
|
|
359
|
+
output += `| Sessions w/ Daily Usage | ${data.sessionsWithDailyUsage} |\n`;
|
|
360
|
+
}
|
|
361
|
+
if (data.dailyUsageRowsUpserted !== undefined) {
|
|
362
|
+
output += `| Daily Usage Rows Upserted | ${data.dailyUsageRowsUpserted} |\n`;
|
|
363
|
+
}
|
|
355
364
|
output += `| Total Tokens | ${formatTokens(data.totalTokens)} |\n`;
|
|
356
365
|
output += `| Estimated Cost | ${formatCost(data.estimatedCost)} |\n`;
|
|
357
366
|
return truncateResponse(output);
|
|
@@ -372,10 +381,67 @@ function formatClaudeCodeSessionsMarkdown(sessions) {
|
|
|
372
381
|
}
|
|
373
382
|
return truncateResponse(output);
|
|
374
383
|
}
|
|
384
|
+
function formatCodexLocalUsageMarkdown(data) {
|
|
385
|
+
let output = `# Codex Local Usage\n\n`;
|
|
386
|
+
output += `## Summary\n`;
|
|
387
|
+
output += `| Metric | Value |\n|--------|-------|\n`;
|
|
388
|
+
output += `| Sessions | ${data.sessions} |\n`;
|
|
389
|
+
output += `| Total Tokens | ${formatTokens(data.tokens.total)} |\n`;
|
|
390
|
+
output += `| Input Tokens | ${formatTokens(data.tokens.input)} |\n`;
|
|
391
|
+
output += `| Output Tokens | ${formatTokens(data.tokens.output)} |\n`;
|
|
392
|
+
output += `| Cache Write Tokens | ${formatTokens(data.tokens.cacheCreation)} |\n`;
|
|
393
|
+
output += `| Cache Read Tokens | ${formatTokens(data.tokens.cacheRead)} |\n\n`;
|
|
394
|
+
if (data.sessionDetails.length > 0) {
|
|
395
|
+
output += `## Recent Sessions\n`;
|
|
396
|
+
output += `| Session | Model | Tokens |\n|---------|-------|--------|\n`;
|
|
397
|
+
for (const session of data.sessionDetails.slice(0, 10)) {
|
|
398
|
+
const shortId = session.sessionId.slice(0, 8);
|
|
399
|
+
output += `| ${shortId}... | ${session.model} | ${formatTokens(session.tokens)} |\n`;
|
|
400
|
+
}
|
|
401
|
+
if (data.sessionDetails.length > 10) {
|
|
402
|
+
output += `\n*...and ${data.sessionDetails.length - 10} more sessions*\n`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return truncateResponse(output);
|
|
406
|
+
}
|
|
407
|
+
function formatCodexSyncResultMarkdown(data) {
|
|
408
|
+
const statusEmoji = data.success ? "✅" : "❌";
|
|
409
|
+
let output = `# Codex Sync Result\n\n`;
|
|
410
|
+
output += `${statusEmoji} ${data.message}\n\n`;
|
|
411
|
+
output += `| Metric | Value |\n|--------|-------|\n`;
|
|
412
|
+
output += `| New Sessions | ${data.sessionsNew} |\n`;
|
|
413
|
+
output += `| Updated Sessions | ${data.sessionsUpdated} |\n`;
|
|
414
|
+
if (data.sessionsWithDailyUsage !== undefined) {
|
|
415
|
+
output += `| Sessions w/ Daily Usage | ${data.sessionsWithDailyUsage} |\n`;
|
|
416
|
+
}
|
|
417
|
+
if (data.dailyUsageRowsUpserted !== undefined) {
|
|
418
|
+
output += `| Daily Usage Rows Upserted | ${data.dailyUsageRowsUpserted} |\n`;
|
|
419
|
+
}
|
|
420
|
+
output += `| Total Tokens | ${formatTokens(data.totalTokens)} |\n`;
|
|
421
|
+
if (data.estimatedCost !== undefined) {
|
|
422
|
+
output += `| Estimated Cost | ${formatCost(data.estimatedCost)} |\n`;
|
|
423
|
+
}
|
|
424
|
+
return truncateResponse(output);
|
|
425
|
+
}
|
|
426
|
+
function formatCodexSessionsMarkdown(sessions) {
|
|
427
|
+
if (sessions.length === 0) {
|
|
428
|
+
return "# Codex Sessions\n\nNo sessions found. Make sure you have used Codex recently.";
|
|
429
|
+
}
|
|
430
|
+
let output = `# Codex Sessions\n\n`;
|
|
431
|
+
output += `Found ${sessions.length} session(s)\n\n`;
|
|
432
|
+
output += `| Session ID | Last Modified | Size |\n|------------|---------------|------|\n`;
|
|
433
|
+
for (const session of sessions) {
|
|
434
|
+
const shortId = session.sessionId.slice(0, 12);
|
|
435
|
+
const date = session.lastModified.toISOString().split("T")[0];
|
|
436
|
+
const sizeKB = (session.size / 1024).toFixed(1);
|
|
437
|
+
output += `| ${shortId}... | ${date} | ${sizeKB} KB |\n`;
|
|
438
|
+
}
|
|
439
|
+
return truncateResponse(output);
|
|
440
|
+
}
|
|
375
441
|
// Create MCP server
|
|
376
442
|
const server = new McpServer({
|
|
377
443
|
name: "costhawk-mcp-server",
|
|
378
|
-
version: "1.
|
|
444
|
+
version: "1.3.1",
|
|
379
445
|
});
|
|
380
446
|
// Tool annotations for MCP best practices
|
|
381
447
|
const READ_ONLY_ANNOTATIONS = {
|
|
@@ -560,6 +626,41 @@ const ListClaudeCodeSessionsSchema = {
|
|
|
560
626
|
limit: z.number().min(1).max(100).optional().default(20).describe("Maximum number of sessions to show"),
|
|
561
627
|
format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
|
|
562
628
|
};
|
|
629
|
+
const SyncCodexUsageSchema = {
|
|
630
|
+
apiKey: z
|
|
631
|
+
.string()
|
|
632
|
+
.optional()
|
|
633
|
+
.describe("Your CostHawk API key. If not provided, uses COSTHAWK_API_KEY environment variable."),
|
|
634
|
+
maxAgeHours: z
|
|
635
|
+
.number()
|
|
636
|
+
.min(1)
|
|
637
|
+
.max(720)
|
|
638
|
+
.optional()
|
|
639
|
+
.default(24)
|
|
640
|
+
.describe("Only sync sessions modified within this many hours (1-720, default 24)"),
|
|
641
|
+
format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
|
|
642
|
+
};
|
|
643
|
+
const GetLocalCodexUsageSchema = {
|
|
644
|
+
maxAgeHours: z
|
|
645
|
+
.number()
|
|
646
|
+
.min(1)
|
|
647
|
+
.max(720)
|
|
648
|
+
.optional()
|
|
649
|
+
.default(24)
|
|
650
|
+
.describe("Only include sessions modified within this many hours (1-720, default 24)"),
|
|
651
|
+
format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
|
|
652
|
+
};
|
|
653
|
+
const ListCodexSessionsSchema = {
|
|
654
|
+
maxAgeHours: z
|
|
655
|
+
.number()
|
|
656
|
+
.min(1)
|
|
657
|
+
.max(720)
|
|
658
|
+
.optional()
|
|
659
|
+
.default(168)
|
|
660
|
+
.describe("Only include sessions modified within this many hours (1-720, default 168 = 7 days)"),
|
|
661
|
+
limit: z.number().min(1).max(100).optional().default(20).describe("Maximum number of sessions to show"),
|
|
662
|
+
format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
|
|
663
|
+
};
|
|
563
664
|
// Register tools using registerTool (recommended API)
|
|
564
665
|
server.registerTool("costhawk_get_usage_summary", {
|
|
565
666
|
description: `Get a summary of your AI API usage and costs over a time period. Returns total costs, API calls, and token usage broken down by provider and model. Supports preset periods (last_24h, today, yesterday, last_7d, last_30d) or custom date ranges.`,
|
|
@@ -985,7 +1086,7 @@ server.registerTool("costhawk_sync_claude_code_usage", {
|
|
|
985
1086
|
try {
|
|
986
1087
|
// Parse all transcripts within age limit
|
|
987
1088
|
const maxAgeHours = args.maxAgeHours ?? 24;
|
|
988
|
-
const sessions =
|
|
1089
|
+
const sessions = parseAllTranscriptsDetailed(maxAgeHours);
|
|
989
1090
|
if (sessions.length === 0) {
|
|
990
1091
|
return {
|
|
991
1092
|
content: [
|
|
@@ -997,18 +1098,28 @@ server.registerTool("costhawk_sync_claude_code_usage", {
|
|
|
997
1098
|
};
|
|
998
1099
|
}
|
|
999
1100
|
// Transform sessions to API payload format
|
|
1000
|
-
const allSessions = sessions.map((session) =>
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1101
|
+
const allSessions = sessions.map((session) => {
|
|
1102
|
+
const dailyUsage = Object.entries(session.dailyUsage).map(([date, tokens]) => ({
|
|
1103
|
+
date,
|
|
1104
|
+
inputTokens: tokens.inputTokens,
|
|
1105
|
+
outputTokens: tokens.outputTokens,
|
|
1106
|
+
cacheCreationTokens: tokens.cacheCreationTokens,
|
|
1107
|
+
cacheReadTokens: tokens.cacheReadTokens,
|
|
1108
|
+
}));
|
|
1109
|
+
return {
|
|
1110
|
+
sessionId: session.sessionId,
|
|
1111
|
+
projectHash: session.projectHash,
|
|
1112
|
+
model: session.model,
|
|
1113
|
+
inputTokens: session.tokens.inputTokens,
|
|
1114
|
+
outputTokens: session.tokens.outputTokens,
|
|
1115
|
+
cacheCreationTokens: session.tokens.cacheCreationTokens,
|
|
1116
|
+
cacheReadTokens: session.tokens.cacheReadTokens,
|
|
1117
|
+
messageCount: session.messageCount,
|
|
1118
|
+
startTime: session.startTime,
|
|
1119
|
+
endTime: session.endTime,
|
|
1120
|
+
dailyUsage: dailyUsage.length > 0 ? dailyUsage : undefined,
|
|
1121
|
+
};
|
|
1122
|
+
});
|
|
1012
1123
|
// Batch sessions in chunks of 100 (API limit)
|
|
1013
1124
|
const BATCH_SIZE = 100;
|
|
1014
1125
|
const batches = [];
|
|
@@ -1023,7 +1134,7 @@ server.registerTool("costhawk_sync_claude_code_usage", {
|
|
|
1023
1134
|
for (const batch of batches) {
|
|
1024
1135
|
const payload = {
|
|
1025
1136
|
sessions: batch,
|
|
1026
|
-
clientVersion:
|
|
1137
|
+
clientVersion: CLIENT_VERSION,
|
|
1027
1138
|
syncedAt: new Date().toISOString(),
|
|
1028
1139
|
};
|
|
1029
1140
|
const result = await apiRequest("/api/mcp/usage/claude-code", {
|
|
@@ -1076,8 +1187,29 @@ server.registerTool("costhawk_get_local_claude_code_usage", {
|
|
|
1076
1187
|
}
|
|
1077
1188
|
try {
|
|
1078
1189
|
const maxAgeHours = args.maxAgeHours ?? 24;
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1190
|
+
const rangeEnd = new Date();
|
|
1191
|
+
const rangeStart = new Date(rangeEnd.getTime() - maxAgeHours * 60 * 60 * 1000);
|
|
1192
|
+
const sessions = parseAllTranscriptsDetailed(maxAgeHours, { rangeStart, rangeEnd });
|
|
1193
|
+
const sessionsInRange = sessions
|
|
1194
|
+
.map((session) => {
|
|
1195
|
+
const rangeTokens = session.rangeUsage || {
|
|
1196
|
+
inputTokens: 0,
|
|
1197
|
+
outputTokens: 0,
|
|
1198
|
+
cacheCreationTokens: 0,
|
|
1199
|
+
cacheReadTokens: 0,
|
|
1200
|
+
};
|
|
1201
|
+
const totalTokens = rangeTokens.inputTokens +
|
|
1202
|
+
rangeTokens.outputTokens +
|
|
1203
|
+
rangeTokens.cacheCreationTokens +
|
|
1204
|
+
rangeTokens.cacheReadTokens;
|
|
1205
|
+
return {
|
|
1206
|
+
session,
|
|
1207
|
+
rangeTokens,
|
|
1208
|
+
totalTokens,
|
|
1209
|
+
};
|
|
1210
|
+
})
|
|
1211
|
+
.filter((entry) => entry.totalTokens > 0);
|
|
1212
|
+
if (sessionsInRange.length === 0) {
|
|
1081
1213
|
return {
|
|
1082
1214
|
content: [
|
|
1083
1215
|
{
|
|
@@ -1087,23 +1219,33 @@ server.registerTool("costhawk_get_local_claude_code_usage", {
|
|
|
1087
1219
|
],
|
|
1088
1220
|
};
|
|
1089
1221
|
}
|
|
1090
|
-
// Aggregate usage
|
|
1091
|
-
const
|
|
1222
|
+
// Aggregate usage for the requested window
|
|
1223
|
+
const aggregatedTokens = sessionsInRange.reduce((acc, entry) => ({
|
|
1224
|
+
inputTokens: acc.inputTokens + entry.rangeTokens.inputTokens,
|
|
1225
|
+
outputTokens: acc.outputTokens + entry.rangeTokens.outputTokens,
|
|
1226
|
+
cacheCreationTokens: acc.cacheCreationTokens + entry.rangeTokens.cacheCreationTokens,
|
|
1227
|
+
cacheReadTokens: acc.cacheReadTokens + entry.rangeTokens.cacheReadTokens,
|
|
1228
|
+
}), {
|
|
1229
|
+
inputTokens: 0,
|
|
1230
|
+
outputTokens: 0,
|
|
1231
|
+
cacheCreationTokens: 0,
|
|
1232
|
+
cacheReadTokens: 0,
|
|
1233
|
+
});
|
|
1234
|
+
const modelCounts = {};
|
|
1235
|
+
for (const entry of sessionsInRange) {
|
|
1236
|
+
modelCounts[entry.session.model] = (modelCounts[entry.session.model] || 0) + 1;
|
|
1237
|
+
}
|
|
1092
1238
|
// Calculate total cost using dominant model pricing
|
|
1093
|
-
const dominantModel = Object.entries(
|
|
1239
|
+
const dominantModel = Object.entries(modelCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || "unknown";
|
|
1094
1240
|
const cost = calculateCost({
|
|
1095
|
-
inputTokens:
|
|
1096
|
-
outputTokens:
|
|
1097
|
-
cacheCreationTokens:
|
|
1098
|
-
cacheReadTokens:
|
|
1241
|
+
inputTokens: aggregatedTokens.inputTokens,
|
|
1242
|
+
outputTokens: aggregatedTokens.outputTokens,
|
|
1243
|
+
cacheCreationTokens: aggregatedTokens.cacheCreationTokens,
|
|
1244
|
+
cacheReadTokens: aggregatedTokens.cacheReadTokens,
|
|
1099
1245
|
}, dominantModel);
|
|
1100
1246
|
// Build session details
|
|
1101
|
-
const sessionDetails =
|
|
1102
|
-
const sessionCost = calculateCost(
|
|
1103
|
-
const totalTokens = session.tokens.inputTokens +
|
|
1104
|
-
session.tokens.outputTokens +
|
|
1105
|
-
session.tokens.cacheCreationTokens +
|
|
1106
|
-
session.tokens.cacheReadTokens;
|
|
1247
|
+
const sessionDetails = sessionsInRange.map(({ session, rangeTokens, totalTokens }) => {
|
|
1248
|
+
const sessionCost = calculateCost(rangeTokens, session.model);
|
|
1107
1249
|
return {
|
|
1108
1250
|
sessionId: session.sessionId,
|
|
1109
1251
|
projectHash: session.projectHash,
|
|
@@ -1116,13 +1258,16 @@ server.registerTool("costhawk_get_local_claude_code_usage", {
|
|
|
1116
1258
|
});
|
|
1117
1259
|
// Build response
|
|
1118
1260
|
const response = {
|
|
1119
|
-
sessions:
|
|
1261
|
+
sessions: sessionsInRange.length,
|
|
1120
1262
|
tokens: {
|
|
1121
|
-
input:
|
|
1122
|
-
output:
|
|
1123
|
-
cacheCreation:
|
|
1124
|
-
cacheRead:
|
|
1125
|
-
total:
|
|
1263
|
+
input: aggregatedTokens.inputTokens,
|
|
1264
|
+
output: aggregatedTokens.outputTokens,
|
|
1265
|
+
cacheCreation: aggregatedTokens.cacheCreationTokens,
|
|
1266
|
+
cacheRead: aggregatedTokens.cacheReadTokens,
|
|
1267
|
+
total: aggregatedTokens.inputTokens +
|
|
1268
|
+
aggregatedTokens.outputTokens +
|
|
1269
|
+
aggregatedTokens.cacheCreationTokens +
|
|
1270
|
+
aggregatedTokens.cacheReadTokens,
|
|
1126
1271
|
},
|
|
1127
1272
|
cost,
|
|
1128
1273
|
sessionDetails,
|
|
@@ -1192,6 +1337,254 @@ server.registerTool("costhawk_list_claude_code_sessions", {
|
|
|
1192
1337
|
};
|
|
1193
1338
|
}
|
|
1194
1339
|
});
|
|
1340
|
+
server.registerTool("costhawk_sync_codex_usage", {
|
|
1341
|
+
description: `Sync your local Codex usage to CostHawk. Parses session logs from ~/.codex/sessions/ and uploads token counts to your CostHawk dashboard. Requires API key.`,
|
|
1342
|
+
inputSchema: SyncCodexUsageSchema,
|
|
1343
|
+
annotations: WRITE_ANNOTATIONS,
|
|
1344
|
+
}, async (args, _extra) => {
|
|
1345
|
+
const apiKey = getApiKey(args.apiKey);
|
|
1346
|
+
if (!apiKey) {
|
|
1347
|
+
return {
|
|
1348
|
+
content: [
|
|
1349
|
+
{
|
|
1350
|
+
type: "text",
|
|
1351
|
+
text: "Error: No API key provided. You must first sign up at https://costhawk.ai to get an API key. Once approved, go to Settings > Developer > Create Token, then set COSTHAWK_API_KEY in your MCP configuration.",
|
|
1352
|
+
},
|
|
1353
|
+
],
|
|
1354
|
+
isError: true,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
if (!codexDirectoryExists()) {
|
|
1358
|
+
return {
|
|
1359
|
+
content: [
|
|
1360
|
+
{
|
|
1361
|
+
type: "text",
|
|
1362
|
+
text: "Error: Codex sessions directory not found at ~/.codex/sessions/. Make sure you have used Codex at least once.",
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
isError: true,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const maxAgeHours = args.maxAgeHours ?? 24;
|
|
1370
|
+
const sessions = parseAllCodexSessionsDetailed(maxAgeHours);
|
|
1371
|
+
if (sessions.length === 0) {
|
|
1372
|
+
return {
|
|
1373
|
+
content: [
|
|
1374
|
+
{
|
|
1375
|
+
type: "text",
|
|
1376
|
+
text: `No Codex sessions found within the last ${maxAgeHours} hours. Try increasing maxAgeHours or use Codex first.`,
|
|
1377
|
+
},
|
|
1378
|
+
],
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
const allSessions = sessions.map((session) => {
|
|
1382
|
+
const dailyUsage = Object.entries(session.dailyUsage).map(([date, tokens]) => ({
|
|
1383
|
+
date,
|
|
1384
|
+
inputTokens: tokens.inputTokens,
|
|
1385
|
+
outputTokens: tokens.outputTokens,
|
|
1386
|
+
cacheCreationTokens: tokens.cacheCreationTokens,
|
|
1387
|
+
cacheReadTokens: tokens.cacheReadTokens,
|
|
1388
|
+
}));
|
|
1389
|
+
return {
|
|
1390
|
+
sessionId: session.sessionId,
|
|
1391
|
+
projectHash: session.projectHash,
|
|
1392
|
+
model: session.model,
|
|
1393
|
+
inputTokens: session.tokens.inputTokens,
|
|
1394
|
+
outputTokens: session.tokens.outputTokens,
|
|
1395
|
+
cacheCreationTokens: session.tokens.cacheCreationTokens,
|
|
1396
|
+
cacheReadTokens: session.tokens.cacheReadTokens,
|
|
1397
|
+
messageCount: session.messageCount,
|
|
1398
|
+
startTime: session.startTime,
|
|
1399
|
+
endTime: session.endTime,
|
|
1400
|
+
dailyUsage: dailyUsage.length > 0 ? dailyUsage : undefined,
|
|
1401
|
+
source: "codex",
|
|
1402
|
+
};
|
|
1403
|
+
});
|
|
1404
|
+
const BATCH_SIZE = 100;
|
|
1405
|
+
const batches = [];
|
|
1406
|
+
for (let i = 0; i < allSessions.length; i += BATCH_SIZE) {
|
|
1407
|
+
batches.push(allSessions.slice(i, i + BATCH_SIZE));
|
|
1408
|
+
}
|
|
1409
|
+
let totalNew = 0;
|
|
1410
|
+
let totalUpdated = 0;
|
|
1411
|
+
let totalTokens = 0;
|
|
1412
|
+
let totalCost = 0;
|
|
1413
|
+
let sessionsWithDailyUsage = 0;
|
|
1414
|
+
let dailyUsageRowsUpserted = 0;
|
|
1415
|
+
for (const batch of batches) {
|
|
1416
|
+
const payload = {
|
|
1417
|
+
sessions: batch,
|
|
1418
|
+
clientVersion: CLIENT_VERSION,
|
|
1419
|
+
syncedAt: new Date().toISOString(),
|
|
1420
|
+
};
|
|
1421
|
+
const result = await apiRequest("/api/mcp/usage/codex", {
|
|
1422
|
+
method: "POST",
|
|
1423
|
+
apiKey,
|
|
1424
|
+
body: payload,
|
|
1425
|
+
});
|
|
1426
|
+
totalNew += result.sessionsNew;
|
|
1427
|
+
totalUpdated += result.sessionsUpdated;
|
|
1428
|
+
totalTokens += result.totalTokens;
|
|
1429
|
+
totalCost += result.estimatedCost || 0;
|
|
1430
|
+
sessionsWithDailyUsage += result.sessionsWithDailyUsage || 0;
|
|
1431
|
+
dailyUsageRowsUpserted += result.dailyUsageRowsUpserted || 0;
|
|
1432
|
+
}
|
|
1433
|
+
const result = {
|
|
1434
|
+
success: true,
|
|
1435
|
+
sessionsNew: totalNew,
|
|
1436
|
+
sessionsUpdated: totalUpdated,
|
|
1437
|
+
totalTokens,
|
|
1438
|
+
estimatedCost: totalCost > 0 ? totalCost : undefined,
|
|
1439
|
+
sessionsWithDailyUsage: sessionsWithDailyUsage || undefined,
|
|
1440
|
+
dailyUsageRowsUpserted: dailyUsageRowsUpserted || undefined,
|
|
1441
|
+
message: `Synced ${totalNew} new and ${totalUpdated} updated sessions`,
|
|
1442
|
+
};
|
|
1443
|
+
const text = args.format === "json" ? JSON.stringify(result, null, 2) : formatCodexSyncResultMarkdown(result);
|
|
1444
|
+
return {
|
|
1445
|
+
content: [{ type: "text", text }],
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
return {
|
|
1450
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
|
|
1451
|
+
isError: true,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
server.registerTool("costhawk_get_local_codex_usage", {
|
|
1456
|
+
description: `Get Codex usage from local sessions WITHOUT uploading to CostHawk. Works offline. Shows token counts by type.`,
|
|
1457
|
+
inputSchema: GetLocalCodexUsageSchema,
|
|
1458
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
1459
|
+
}, async (args, _extra) => {
|
|
1460
|
+
if (!codexDirectoryExists()) {
|
|
1461
|
+
return {
|
|
1462
|
+
content: [
|
|
1463
|
+
{
|
|
1464
|
+
type: "text",
|
|
1465
|
+
text: "Error: Codex sessions directory not found at ~/.codex/sessions/. Make sure you have used Codex at least once.",
|
|
1466
|
+
},
|
|
1467
|
+
],
|
|
1468
|
+
isError: true,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
try {
|
|
1472
|
+
const maxAgeHours = args.maxAgeHours ?? 24;
|
|
1473
|
+
const rangeEnd = new Date();
|
|
1474
|
+
const rangeStart = new Date(rangeEnd.getTime() - maxAgeHours * 60 * 60 * 1000);
|
|
1475
|
+
const sessions = parseAllCodexSessionsDetailed(maxAgeHours, { rangeStart, rangeEnd });
|
|
1476
|
+
const sessionsInRange = sessions
|
|
1477
|
+
.map((session) => {
|
|
1478
|
+
const rangeTokens = session.rangeUsage || {
|
|
1479
|
+
inputTokens: 0,
|
|
1480
|
+
outputTokens: 0,
|
|
1481
|
+
cacheCreationTokens: 0,
|
|
1482
|
+
cacheReadTokens: 0,
|
|
1483
|
+
};
|
|
1484
|
+
const totalTokens = rangeTokens.inputTokens +
|
|
1485
|
+
rangeTokens.outputTokens +
|
|
1486
|
+
rangeTokens.cacheCreationTokens +
|
|
1487
|
+
rangeTokens.cacheReadTokens;
|
|
1488
|
+
return {
|
|
1489
|
+
session,
|
|
1490
|
+
rangeTokens,
|
|
1491
|
+
totalTokens,
|
|
1492
|
+
};
|
|
1493
|
+
})
|
|
1494
|
+
.filter((entry) => entry.totalTokens > 0);
|
|
1495
|
+
if (sessionsInRange.length === 0) {
|
|
1496
|
+
return {
|
|
1497
|
+
content: [
|
|
1498
|
+
{
|
|
1499
|
+
type: "text",
|
|
1500
|
+
text: `No Codex sessions found within the last ${maxAgeHours} hours. Try increasing maxAgeHours or use Codex first.`,
|
|
1501
|
+
},
|
|
1502
|
+
],
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
const aggregatedTokens = sessionsInRange.reduce((acc, entry) => ({
|
|
1506
|
+
inputTokens: acc.inputTokens + entry.rangeTokens.inputTokens,
|
|
1507
|
+
outputTokens: acc.outputTokens + entry.rangeTokens.outputTokens,
|
|
1508
|
+
cacheCreationTokens: acc.cacheCreationTokens + entry.rangeTokens.cacheCreationTokens,
|
|
1509
|
+
cacheReadTokens: acc.cacheReadTokens + entry.rangeTokens.cacheReadTokens,
|
|
1510
|
+
}), {
|
|
1511
|
+
inputTokens: 0,
|
|
1512
|
+
outputTokens: 0,
|
|
1513
|
+
cacheCreationTokens: 0,
|
|
1514
|
+
cacheReadTokens: 0,
|
|
1515
|
+
});
|
|
1516
|
+
const sessionDetails = sessionsInRange.map(({ session, totalTokens }) => ({
|
|
1517
|
+
sessionId: session.sessionId,
|
|
1518
|
+
model: session.model,
|
|
1519
|
+
tokens: totalTokens,
|
|
1520
|
+
startTime: session.startTime,
|
|
1521
|
+
endTime: session.endTime,
|
|
1522
|
+
}));
|
|
1523
|
+
const response = {
|
|
1524
|
+
sessions: sessionsInRange.length,
|
|
1525
|
+
tokens: {
|
|
1526
|
+
input: aggregatedTokens.inputTokens,
|
|
1527
|
+
output: aggregatedTokens.outputTokens,
|
|
1528
|
+
cacheCreation: aggregatedTokens.cacheCreationTokens,
|
|
1529
|
+
cacheRead: aggregatedTokens.cacheReadTokens,
|
|
1530
|
+
total: aggregatedTokens.inputTokens +
|
|
1531
|
+
aggregatedTokens.outputTokens +
|
|
1532
|
+
aggregatedTokens.cacheCreationTokens +
|
|
1533
|
+
aggregatedTokens.cacheReadTokens,
|
|
1534
|
+
},
|
|
1535
|
+
sessionDetails,
|
|
1536
|
+
};
|
|
1537
|
+
const text = args.format === "json" ? JSON.stringify(response, null, 2) : formatCodexLocalUsageMarkdown(response);
|
|
1538
|
+
return {
|
|
1539
|
+
content: [{ type: "text", text }],
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) {
|
|
1543
|
+
return {
|
|
1544
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
|
|
1545
|
+
isError: true,
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
server.registerTool("costhawk_list_codex_sessions", {
|
|
1550
|
+
description: `List available Codex sessions from local logs. Shows session IDs, modification dates, and file sizes.`,
|
|
1551
|
+
inputSchema: ListCodexSessionsSchema,
|
|
1552
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
1553
|
+
}, async (args, _extra) => {
|
|
1554
|
+
if (!codexDirectoryExists()) {
|
|
1555
|
+
return {
|
|
1556
|
+
content: [
|
|
1557
|
+
{
|
|
1558
|
+
type: "text",
|
|
1559
|
+
text: "Error: Codex sessions directory not found at ~/.codex/sessions/. Make sure you have used Codex at least once.",
|
|
1560
|
+
},
|
|
1561
|
+
],
|
|
1562
|
+
isError: true,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const maxAgeHours = args.maxAgeHours ?? 168;
|
|
1567
|
+
const limit = args.limit ?? 20;
|
|
1568
|
+
const sessions = discoverCodexSessions(maxAgeHours).slice(0, limit);
|
|
1569
|
+
const text = args.format === "json"
|
|
1570
|
+
? JSON.stringify(sessions.map((s) => ({
|
|
1571
|
+
sessionId: s.sessionId,
|
|
1572
|
+
lastModified: s.lastModified.toISOString(),
|
|
1573
|
+
size: s.size,
|
|
1574
|
+
path: s.path,
|
|
1575
|
+
})), null, 2)
|
|
1576
|
+
: formatCodexSessionsMarkdown(sessions);
|
|
1577
|
+
return {
|
|
1578
|
+
content: [{ type: "text", text }],
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
catch (error) {
|
|
1582
|
+
return {
|
|
1583
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
|
|
1584
|
+
isError: true,
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1195
1588
|
// Auto-sync function for Claude Code usage
|
|
1196
1589
|
async function performAutoSync() {
|
|
1197
1590
|
const apiKey = DEFAULT_API_KEY;
|
|
@@ -1204,24 +1597,34 @@ async function performAutoSync() {
|
|
|
1204
1597
|
return;
|
|
1205
1598
|
}
|
|
1206
1599
|
try {
|
|
1207
|
-
const sessions =
|
|
1600
|
+
const sessions = parseAllTranscriptsDetailed(AUTO_SYNC_MAX_AGE_HOURS);
|
|
1208
1601
|
if (sessions.length === 0) {
|
|
1209
1602
|
console.error("[Auto-sync] No sessions to sync");
|
|
1210
1603
|
return;
|
|
1211
1604
|
}
|
|
1212
1605
|
// Transform sessions to API payload format
|
|
1213
|
-
const allSessions = sessions.map((session) =>
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1606
|
+
const allSessions = sessions.map((session) => {
|
|
1607
|
+
const dailyUsage = Object.entries(session.dailyUsage).map(([date, tokens]) => ({
|
|
1608
|
+
date,
|
|
1609
|
+
inputTokens: tokens.inputTokens,
|
|
1610
|
+
outputTokens: tokens.outputTokens,
|
|
1611
|
+
cacheCreationTokens: tokens.cacheCreationTokens,
|
|
1612
|
+
cacheReadTokens: tokens.cacheReadTokens,
|
|
1613
|
+
}));
|
|
1614
|
+
return {
|
|
1615
|
+
sessionId: session.sessionId,
|
|
1616
|
+
projectHash: session.projectHash,
|
|
1617
|
+
model: session.model,
|
|
1618
|
+
inputTokens: session.tokens.inputTokens,
|
|
1619
|
+
outputTokens: session.tokens.outputTokens,
|
|
1620
|
+
cacheCreationTokens: session.tokens.cacheCreationTokens,
|
|
1621
|
+
cacheReadTokens: session.tokens.cacheReadTokens,
|
|
1622
|
+
messageCount: session.messageCount,
|
|
1623
|
+
startTime: session.startTime,
|
|
1624
|
+
endTime: session.endTime,
|
|
1625
|
+
dailyUsage: dailyUsage.length > 0 ? dailyUsage : undefined,
|
|
1626
|
+
};
|
|
1627
|
+
});
|
|
1225
1628
|
// Batch sessions in chunks of 100 (API limit)
|
|
1226
1629
|
const BATCH_SIZE = 100;
|
|
1227
1630
|
const batches = [];
|
|
@@ -1235,7 +1638,7 @@ async function performAutoSync() {
|
|
|
1235
1638
|
for (const batch of batches) {
|
|
1236
1639
|
const payload = {
|
|
1237
1640
|
sessions: batch,
|
|
1238
|
-
clientVersion:
|
|
1641
|
+
clientVersion: CLIENT_VERSION,
|
|
1239
1642
|
syncedAt: new Date().toISOString(),
|
|
1240
1643
|
autoSync: true,
|
|
1241
1644
|
};
|
|
@@ -1254,6 +1657,79 @@ async function performAutoSync() {
|
|
|
1254
1657
|
console.error(`[Auto-sync] Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1255
1658
|
}
|
|
1256
1659
|
}
|
|
1660
|
+
// Auto-sync function for Codex usage (optional)
|
|
1661
|
+
async function performCodexAutoSync() {
|
|
1662
|
+
if (!CODEX_AUTO_SYNC_ENABLED) {
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const apiKey = DEFAULT_API_KEY;
|
|
1666
|
+
if (!apiKey) {
|
|
1667
|
+
console.error("[Codex Auto-sync] Skipped: No COSTHAWK_API_KEY configured");
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (!codexDirectoryExists()) {
|
|
1671
|
+
console.error("[Codex Auto-sync] Skipped: Codex sessions directory not found");
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const sessions = parseAllCodexSessionsDetailed(AUTO_SYNC_MAX_AGE_HOURS);
|
|
1676
|
+
if (sessions.length === 0) {
|
|
1677
|
+
console.error("[Codex Auto-sync] No sessions to sync");
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
const allSessions = sessions.map((session) => {
|
|
1681
|
+
const dailyUsage = Object.entries(session.dailyUsage).map(([date, tokens]) => ({
|
|
1682
|
+
date,
|
|
1683
|
+
inputTokens: tokens.inputTokens,
|
|
1684
|
+
outputTokens: tokens.outputTokens,
|
|
1685
|
+
cacheCreationTokens: tokens.cacheCreationTokens,
|
|
1686
|
+
cacheReadTokens: tokens.cacheReadTokens,
|
|
1687
|
+
}));
|
|
1688
|
+
return {
|
|
1689
|
+
sessionId: session.sessionId,
|
|
1690
|
+
projectHash: session.projectHash,
|
|
1691
|
+
model: session.model,
|
|
1692
|
+
inputTokens: session.tokens.inputTokens,
|
|
1693
|
+
outputTokens: session.tokens.outputTokens,
|
|
1694
|
+
cacheCreationTokens: session.tokens.cacheCreationTokens,
|
|
1695
|
+
cacheReadTokens: session.tokens.cacheReadTokens,
|
|
1696
|
+
messageCount: session.messageCount,
|
|
1697
|
+
startTime: session.startTime,
|
|
1698
|
+
endTime: session.endTime,
|
|
1699
|
+
dailyUsage: dailyUsage.length > 0 ? dailyUsage : undefined,
|
|
1700
|
+
source: "codex",
|
|
1701
|
+
};
|
|
1702
|
+
});
|
|
1703
|
+
const BATCH_SIZE = 100;
|
|
1704
|
+
const batches = [];
|
|
1705
|
+
for (let i = 0; i < allSessions.length; i += BATCH_SIZE) {
|
|
1706
|
+
batches.push(allSessions.slice(i, i + BATCH_SIZE));
|
|
1707
|
+
}
|
|
1708
|
+
let totalNew = 0;
|
|
1709
|
+
let totalUpdated = 0;
|
|
1710
|
+
let totalTokens = 0;
|
|
1711
|
+
for (const batch of batches) {
|
|
1712
|
+
const payload = {
|
|
1713
|
+
sessions: batch,
|
|
1714
|
+
clientVersion: CLIENT_VERSION,
|
|
1715
|
+
syncedAt: new Date().toISOString(),
|
|
1716
|
+
autoSync: true,
|
|
1717
|
+
};
|
|
1718
|
+
const result = await apiRequest("/api/mcp/usage/codex", {
|
|
1719
|
+
method: "POST",
|
|
1720
|
+
apiKey,
|
|
1721
|
+
body: payload,
|
|
1722
|
+
});
|
|
1723
|
+
totalNew += result.sessionsNew;
|
|
1724
|
+
totalUpdated += result.sessionsUpdated;
|
|
1725
|
+
totalTokens += result.totalTokens;
|
|
1726
|
+
}
|
|
1727
|
+
console.error(`[Codex Auto-sync] Success: ${totalNew} new, ${totalUpdated} updated, ${formatTokens(totalTokens)} tokens (${batches.length} batch${batches.length > 1 ? "es" : ""})`);
|
|
1728
|
+
}
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
console.error(`[Codex Auto-sync] Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1257
1733
|
// Start auto-sync interval
|
|
1258
1734
|
function startAutoSync() {
|
|
1259
1735
|
if (!DEFAULT_API_KEY) {
|
|
@@ -1263,10 +1739,12 @@ function startAutoSync() {
|
|
|
1263
1739
|
// Perform initial sync after 30 seconds (give server time to fully start)
|
|
1264
1740
|
setTimeout(() => {
|
|
1265
1741
|
performAutoSync();
|
|
1742
|
+
performCodexAutoSync();
|
|
1266
1743
|
}, 30 * 1000);
|
|
1267
1744
|
// Then sync every 15 minutes
|
|
1268
1745
|
autoSyncIntervalId = setInterval(() => {
|
|
1269
1746
|
performAutoSync();
|
|
1747
|
+
performCodexAutoSync();
|
|
1270
1748
|
}, AUTO_SYNC_INTERVAL_MS);
|
|
1271
1749
|
console.error(`[Auto-sync] Enabled: syncing every ${AUTO_SYNC_INTERVAL_MS / 60000} minutes`);
|
|
1272
1750
|
}
|