costhawk 1.1.7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,140 +1,168 @@
1
1
  # CostHawk MCP Server
2
2
 
3
- Model Context Protocol (MCP) server that enables AI assistants like Claude to query your CostHawk data — track your spend, see your stats, and monitor your AI API costs directly from the command line.
3
+ Official MCP server for [CostHawk](https://costhawk.ai) - AI API cost monitoring and optimization platform.
4
4
 
5
- ## State of the Project
5
+ > **Early Alpha** - CostHawk is currently in early alpha testing. [Join the waitlist](https://costhawk.ai) to get early access.
6
6
 
7
- **We are in active development and currently in early access.**
7
+ ## Overview
8
8
 
9
- As of **v1.1.7**, the following features are live:
10
- - Usage summary and cost tracking across providers
11
- - Savings calculations for flat-rate subscriptions (Claude Max, OpenAI Pro, etc.)
12
- - Anomaly detection and cost spike alerts
13
- - Webhook integrations (Slack, Discord, Teams, PagerDuty)
14
- - Model pricing lookup with context window info
9
+ CostHawk helps teams track, analyze, and optimize their AI API spending across providers like Anthropic, OpenAI, and Google.
15
10
 
16
- **Versions prior to v1.1.4 were non-functional early builds.** If you installed an earlier version, please update:
11
+ **Key Features:**
12
+ - Real-time usage tracking and cost analytics
13
+ - **Claude Code local usage tracking** (NEW in v1.2.0)
14
+ - Savings analysis for flat-rate subscriptions (Claude Pro/Max, OpenAI Pro)
15
+ - Budget alerts and anomaly detection
16
+ - Webhook notifications (Slack, Discord, PagerDuty)
17
+
18
+ ## Quick Install
17
19
 
18
20
  ```bash
19
- npm update -g costhawk
21
+ # Global installation (all projects)
22
+ claude mcp add -s user -e COSTHAWK_API_KEY=YOUR_TOKEN_HERE costhawk -- npx -y costhawk
23
+
24
+ # Project-specific installation
25
+ claude mcp add -e COSTHAWK_API_KEY=YOUR_TOKEN_HERE costhawk -- npx -y costhawk
20
26
  ```
21
27
 
22
- ## Getting Started
28
+ Get your access token from [Settings → Developer](https://costhawk.ai/dashboard/settings) in your CostHawk dashboard (requires approved account).
23
29
 
24
- ### Step 1: Join the Waitlist
30
+ ## Available Tools
25
31
 
26
- **You must first sign up at [costhawk.ai](https://costhawk.ai) to use this package.**
32
+ ### Usage Tracking
27
33
 
28
- We're in early access and onboarding new users weekly. Join the waitlist and we'll notify you when your account is ready.
34
+ | Tool | Description |
35
+ |------|-------------|
36
+ | `costhawk_get_usage_summary` | Get usage and costs over a time period (by provider/model) |
37
+ | `costhawk_get_usage_by_tag` | Get usage grouped by custom tags (user_id, feature, etc.) |
38
+ | `costhawk_detect_anomalies` | Check for cost anomalies and unusual usage patterns |
29
39
 
30
- ### Step 2: Get Your API Key
40
+ ### Claude Code Local Tracking (NEW in v1.2.0)
31
41
 
32
- Once approved:
33
- 1. Log into your [CostHawk dashboard](https://costhawk.ai/dashboard)
34
- 2. Go to **Settings → API Keys**
35
- 3. Create a new key for MCP access
36
- 4. Copy the key (you'll only see it once)
42
+ These tools parse your local Claude Code transcripts from `~/.claude/projects/` to track token usage - including the 4 token types Claude Code uses:
37
43
 
38
- ### Step 3: Install the Package
44
+ - `input_tokens` - Regular input
45
+ - `output_tokens` - Regular output
46
+ - `cache_creation_input_tokens` - Writing to prompt cache
47
+ - `cache_read_input_tokens` - Reading from cache (10x cheaper!)
39
48
 
40
- ```bash
41
- npm install -g costhawk
42
- ```
49
+ | Tool | Description |
50
+ |------|-------------|
51
+ | `costhawk_sync_claude_code_usage` | Sync local usage to CostHawk dashboard for savings analysis |
52
+ | `costhawk_get_local_claude_code_usage` | View local usage offline with cost breakdown |
53
+ | `costhawk_list_claude_code_sessions` | List available Claude Code sessions |
43
54
 
44
- ### Step 4: Configure Your MCP Client
55
+ **Example: Check local usage offline**
56
+ ```
57
+ Use costhawk_get_local_claude_code_usage with subscriptionPlan="max_5x"
58
+ ```
45
59
 
46
- #### For Claude Code
60
+ This shows your token usage, costs at retail rates, and whether you're saving money vs your subscription.
47
61
 
48
- Add to `~/.claude/.mcp.json`:
62
+ ### Savings Analysis
49
63
 
50
- ```json
51
- {
52
- "mcpServers": {
53
- "costhawk": {
54
- "command": "npx",
55
- "args": ["-y", "costhawk"],
56
- "env": {
57
- "COSTHAWK_API_KEY": "your-api-key-here"
58
- }
59
- }
60
- }
61
- }
62
- ```
64
+ | Tool | Description |
65
+ |------|-------------|
66
+ | `costhawk_get_savings` | Compare retail costs vs subscription costs |
67
+ | `costhawk_get_savings_breakdown` | Per-model breakdown of usage and costs |
68
+ | `costhawk_list_subscriptions` | List your active flat-rate subscriptions |
63
69
 
64
- Then restart Claude Code.
65
-
66
- #### For Claude Desktop
67
-
68
- Edit your config file:
69
- - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
70
- - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
71
-
72
- ```json
73
- {
74
- "mcpServers": {
75
- "costhawk": {
76
- "command": "npx",
77
- "args": ["-y", "costhawk"],
78
- "env": {
79
- "COSTHAWK_API_KEY": "your-api-key-here"
80
- }
81
- }
82
- }
83
- }
84
- ```
70
+ ### Pricing & Alerts
85
71
 
86
- Then restart Claude Desktop.
72
+ | Tool | Description |
73
+ |------|-------------|
74
+ | `costhawk_get_model_pricing` | Get current AI model pricing (input/output per 1M tokens) |
75
+ | `costhawk_list_alerts` | List budget warnings, cost spikes, and anomaly alerts |
87
76
 
88
- ## Available Tools
77
+ ### Webhooks
89
78
 
90
79
  | Tool | Description |
91
80
  |------|-------------|
92
- | `costhawk_get_usage_summary` | Get usage and cost summary for a date range |
93
- | `costhawk_get_usage_by_tag` | Break down costs by metadata tags (project, environment, etc.) |
94
- | `costhawk_detect_anomalies` | Find cost spikes and unusual activity patterns |
95
- | `costhawk_list_webhooks` | List your configured alert webhooks |
96
- | `costhawk_create_webhook` | Set up new webhooks for Slack, Discord, Teams, or custom URLs |
97
- | `costhawk_get_model_pricing` | Look up current model pricing by provider |
98
- | `costhawk_list_alerts` | View alerts and notifications |
99
- | `costhawk_get_savings` | Show savings vs retail pricing for flat-rate subscriptions |
100
- | `costhawk_list_subscriptions` | List active flat-rate subscriptions |
101
- | `costhawk_get_savings_breakdown` | Show per-model usage and retail cost breakdown |
102
-
103
- ## Example Prompts
104
-
105
- Once configured, you can ask Claude:
106
-
107
- - "What's my AI API usage this month?"
108
- - "Show me costs broken down by project tag"
109
- - "Are there any cost anomalies I should know about?"
110
- - "What's the current pricing for Claude Opus?"
111
- - "Create a Slack webhook for budget alerts"
112
- - "List my unread alerts"
113
- - "Show my savings vs retail this month"
114
- - "List my flat-rate subscriptions"
115
- - "Break down my savings by model"
81
+ | `costhawk_list_webhooks` | List configured webhook endpoints |
82
+ | `costhawk_create_webhook` | Create webhook for Slack, Discord, Teams, PagerDuty |
83
+
84
+ ## Claude Code Token Types Explained
85
+
86
+ Claude Code uses caching extensively, which significantly affects your costs:
87
+
88
+ | Token Type | Description | Sonnet 4 Pricing |
89
+ |------------|-------------|------------------|
90
+ | Input | Regular input tokens | $3/1M |
91
+ | Output | Regular output tokens | $15/1M |
92
+ | Cache Write | Writing to prompt cache | $3.75/1M |
93
+ | Cache Read | Reading from cache | $0.30/1M (10x cheaper!) |
94
+
95
+ The cache read savings are significant - CostHawk tracks all 4 types to give you accurate cost calculations.
116
96
 
117
97
  ## Environment Variables
118
98
 
119
- | Variable | Required | Default | Description |
120
- |----------|----------|---------|-------------|
121
- | `COSTHAWK_API_KEY` | Yes | - | Your CostHawk API key |
122
- | `COSTHAWK_API_URL` | No | `https://costhawk.ai` | API base URL (for self-hosted) |
99
+ | Variable | Required | Description |
100
+ |----------|----------|-------------|
101
+ | `COSTHAWK_API_KEY` | Yes* | Your CostHawk API token |
102
+ | `COSTHAWK_API_URL` | No | API base URL (defaults to https://costhawk.ai) |
103
+
104
+ *Required for most tools. Local Claude Code tools work offline without an API key.
105
+
106
+ ## Getting Started
107
+
108
+ 1. **Get Early Access**: [Join the waitlist](https://costhawk.ai) and wait for approval
109
+ 2. **Create API Token**: Go to Settings → Developer → Create Token
110
+ 3. **Install MCP Server**: Use the quick install command above
111
+ 4. **Start Tracking**: Use the tools in Claude Code or Claude Desktop
112
+
113
+ ## Example Workflows
114
+
115
+ ### Check if your Claude Max subscription is worth it
116
+
117
+ ```
118
+ 1. costhawk_list_claude_code_sessions (see what's available)
119
+ 2. costhawk_get_local_claude_code_usage with subscriptionPlan="max_5x"
120
+ 3. Review the savings breakdown
121
+ ```
122
+
123
+ ### Sync usage to dashboard for team visibility
124
+
125
+ ```
126
+ 1. costhawk_sync_claude_code_usage (uploads to CostHawk)
127
+ 2. View detailed analytics at costhawk.ai/dashboard
128
+ ```
129
+
130
+ ### Set up cost spike alerts
131
+
132
+ ```
133
+ 1. costhawk_create_webhook with type="slack" and events=["cost_spike", "budget_alert"]
134
+ 2. Get notified when costs exceed thresholds
135
+ ```
136
+
137
+ ## Changelog
138
+
139
+ ### v1.2.0 (January 2026)
140
+ **Claude Code Local Tracking** - Major new feature release
141
+
142
+ - **New:** `costhawk_sync_claude_code_usage` - Sync local Claude Code transcripts to CostHawk
143
+ - **New:** `costhawk_get_local_claude_code_usage` - View usage offline with cost breakdown
144
+ - **New:** `costhawk_list_claude_code_sessions` - List available local sessions
145
+ - **New:** Full support for all 4 Claude Code token types (input, output, cache write, cache read)
146
+ - **New:** Offline cost calculation with embedded pricing
147
+ - **New:** Savings comparison vs Claude Pro/Max subscriptions
123
148
 
124
- ## Troubleshooting
149
+ ### v1.1.x
150
+ - Savings analysis tools (`costhawk_get_savings`, `costhawk_get_savings_breakdown`)
151
+ - Subscription management (`costhawk_list_subscriptions`)
152
+ - Webhook support for Slack, Discord, Teams, PagerDuty
153
+ - Usage tracking and anomaly detection
125
154
 
126
- **"Tool not found" errors:**
127
- - Ensure Claude was restarted after config changes
128
- - Verify the config file syntax is valid JSON
155
+ ### v1.0.x
156
+ - Initial release
157
+ - Basic usage summary and cost tracking
158
+ - Model pricing lookup
159
+ - Alert notifications
129
160
 
130
- **"Authentication failed" errors:**
131
- - Verify your API key is correct
132
- - Check the key is active in CostHawk dashboard
133
- - Make sure you've been approved from the waitlist
161
+ ## Links
134
162
 
135
- **"Connection refused" errors:**
136
- - Ensure `COSTHAWK_API_URL` is accessible
137
- - Check your network/firewall settings
163
+ - [Join Waitlist](https://costhawk.ai)
164
+ - [Documentation](https://docs.costhawk.ai)
165
+ - [npm Package](https://www.npmjs.com/package/costhawk)
138
166
 
139
167
  ## License
140
168
 
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@
2
2
  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
+ // Claude Code local transcript parsing
6
+ import { claudeCodeDirectoryExists, discoverTranscripts, parseAllTranscripts, aggregateUsage, } from "./transcript-parser.js";
7
+ import { calculateCost, formatCost, formatTokens, calculateSavings, CLAUDE_SUBSCRIPTIONS, } from "./pricing-constants.js";
5
8
  // Constants
6
9
  const CHARACTER_LIMIT = 25000;
7
10
  const REQUEST_TIMEOUT_MS = 30000; // 30 seconds
@@ -296,6 +299,75 @@ function formatSavingsBreakdownMarkdown(data) {
296
299
  }
297
300
  return truncateResponse(output);
298
301
  }
302
+ // Claude Code local usage formatters
303
+ function formatClaudeCodeLocalUsageMarkdown(data) {
304
+ let output = `# Claude Code Local Usage\n\n`;
305
+ output += `## Summary\n`;
306
+ output += `| Metric | Value |\n|--------|-------|\n`;
307
+ output += `| Sessions | ${data.sessions} |\n`;
308
+ output += `| Total Tokens | ${formatTokens(data.tokens.total)} |\n`;
309
+ output += `| Input Tokens | ${formatTokens(data.tokens.input)} |\n`;
310
+ output += `| Output Tokens | ${formatTokens(data.tokens.output)} |\n`;
311
+ output += `| Cache Write Tokens | ${formatTokens(data.tokens.cacheCreation)} |\n`;
312
+ output += `| Cache Read Tokens | ${formatTokens(data.tokens.cacheRead)} |\n\n`;
313
+ output += `## Cost Breakdown (Retail Rates)\n`;
314
+ output += `| Category | Cost |\n|----------|------|\n`;
315
+ output += `| Input | ${formatCost(data.cost.inputCost)} |\n`;
316
+ output += `| Output | ${formatCost(data.cost.outputCost)} |\n`;
317
+ output += `| Cache Write | ${formatCost(data.cost.cacheWriteCost)} |\n`;
318
+ output += `| Cache Read | ${formatCost(data.cost.cacheReadCost)} |\n`;
319
+ output += `| **Total** | **${formatCost(data.cost.totalCost)}** |\n\n`;
320
+ if (data.savings) {
321
+ const statusEmoji = data.savings.status === "saving" ? "✅" : data.savings.status === "losing" ? "⚠️" : "➖";
322
+ output += `## Subscription Savings\n`;
323
+ output += `| Metric | Value |\n|--------|-------|\n`;
324
+ output += `| Subscription | ${data.savings.subscriptionPlan} |\n`;
325
+ output += `| Monthly Cost | ${formatCost(data.savings.subscriptionCost)} |\n`;
326
+ output += `| Retail Value | ${formatCost(data.cost.totalCost)} |\n`;
327
+ output += `| Savings | ${formatCost(data.savings.savingsAmount)} |\n`;
328
+ output += `| Savings Rate | ${data.savings.savingsPercentage.toFixed(1)}% |\n`;
329
+ output += `| Status | ${statusEmoji} ${data.savings.status} |\n\n`;
330
+ }
331
+ if (data.sessionDetails.length > 0) {
332
+ output += `## Recent Sessions\n`;
333
+ output += `| Session | Model | Tokens | Cost |\n|---------|-------|--------|------|\n`;
334
+ for (const session of data.sessionDetails.slice(0, 10)) {
335
+ const shortId = session.sessionId.slice(0, 8);
336
+ output += `| ${shortId}... | ${session.model} | ${formatTokens(session.tokens)} | ${formatCost(session.cost)} |\n`;
337
+ }
338
+ if (data.sessionDetails.length > 10) {
339
+ output += `\n*...and ${data.sessionDetails.length - 10} more sessions*\n`;
340
+ }
341
+ }
342
+ return truncateResponse(output);
343
+ }
344
+ function formatClaudeCodeSyncResultMarkdown(data) {
345
+ const statusEmoji = data.success ? "✅" : "❌";
346
+ let output = `# Claude Code Sync Result\n\n`;
347
+ output += `${statusEmoji} ${data.message}\n\n`;
348
+ output += `| Metric | Value |\n|--------|-------|\n`;
349
+ output += `| New Sessions | ${data.sessionsNew} |\n`;
350
+ output += `| Updated Sessions | ${data.sessionsUpdated} |\n`;
351
+ output += `| Total Tokens | ${formatTokens(data.totalTokens)} |\n`;
352
+ output += `| Estimated Cost | ${formatCost(data.estimatedCost)} |\n`;
353
+ return truncateResponse(output);
354
+ }
355
+ function formatClaudeCodeSessionsMarkdown(sessions) {
356
+ if (sessions.length === 0) {
357
+ return "# Claude Code Sessions\n\nNo sessions found. Make sure you have used Claude Code recently.";
358
+ }
359
+ let output = `# Claude Code Sessions\n\n`;
360
+ output += `Found ${sessions.length} session(s)\n\n`;
361
+ output += `| Session ID | Project | Last Modified | Size |\n|------------|---------|---------------|------|\n`;
362
+ for (const session of sessions) {
363
+ const shortId = session.sessionId.slice(0, 12);
364
+ const shortProject = session.projectHash.slice(0, 8);
365
+ const date = session.lastModified.toISOString().split("T")[0];
366
+ const sizeKB = (session.size / 1024).toFixed(1);
367
+ output += `| ${shortId}... | ${shortProject}... | ${date} | ${sizeKB} KB |\n`;
368
+ }
369
+ return truncateResponse(output);
370
+ }
299
371
  // Create MCP server
300
372
  const server = new McpServer({
301
373
  name: "costhawk-mcp-server",
@@ -444,6 +516,46 @@ const GetSavingsBreakdownSchema = {
444
516
  orgId: z.string().optional().describe("Optional organization ID to scope the breakdown"),
445
517
  format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
446
518
  };
519
+ // Claude Code local transcript schemas
520
+ const SyncClaudeCodeUsageSchema = {
521
+ apiKey: z
522
+ .string()
523
+ .optional()
524
+ .describe("Your CostHawk API key. If not provided, uses COSTHAWK_API_KEY environment variable."),
525
+ maxAgeHours: z
526
+ .number()
527
+ .min(1)
528
+ .max(720)
529
+ .optional()
530
+ .default(24)
531
+ .describe("Only sync sessions modified within this many hours (1-720, default 24)"),
532
+ format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
533
+ };
534
+ const GetLocalClaudeCodeUsageSchema = {
535
+ maxAgeHours: z
536
+ .number()
537
+ .min(1)
538
+ .max(720)
539
+ .optional()
540
+ .default(24)
541
+ .describe("Only include sessions modified within this many hours (1-720, default 24)"),
542
+ subscriptionPlan: z
543
+ .enum(["pro", "max_5x", "max_20x"])
544
+ .optional()
545
+ .describe("Your Claude subscription plan for savings calculation"),
546
+ format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
547
+ };
548
+ const ListClaudeCodeSessionsSchema = {
549
+ maxAgeHours: z
550
+ .number()
551
+ .min(1)
552
+ .max(720)
553
+ .optional()
554
+ .default(168)
555
+ .describe("Only include sessions modified within this many hours (1-720, default 168 = 7 days)"),
556
+ limit: z.number().min(1).max(100).optional().default(20).describe("Maximum number of sessions to show"),
557
+ format: z.enum(["markdown", "json"]).optional().default("markdown").describe("Response format"),
558
+ };
447
559
  // Register tools using registerTool (recommended API)
448
560
  server.registerTool("costhawk_get_usage_summary", {
449
561
  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.`,
@@ -832,6 +944,224 @@ server.registerTool("costhawk_list_alerts", {
832
944
  };
833
945
  }
834
946
  });
947
+ // ============================================================================
948
+ // CLAUDE CODE LOCAL TRANSCRIPT TOOLS
949
+ // ============================================================================
950
+ // These tools parse local Claude Code transcripts from ~/.claude/projects/
951
+ // to track token usage and calculate savings vs subscription costs.
952
+ server.registerTool("costhawk_sync_claude_code_usage", {
953
+ description: `Sync your local Claude Code usage to CostHawk. Parses transcript files from ~/.claude/projects/ and uploads token counts to your CostHawk dashboard for savings analysis. Requires API key.`,
954
+ inputSchema: SyncClaudeCodeUsageSchema,
955
+ annotations: WRITE_ANNOTATIONS,
956
+ }, async (args, _extra) => {
957
+ const apiKey = getApiKey(args.apiKey);
958
+ if (!apiKey) {
959
+ return {
960
+ content: [
961
+ {
962
+ type: "text",
963
+ 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.",
964
+ },
965
+ ],
966
+ isError: true,
967
+ };
968
+ }
969
+ // Check if Claude Code directory exists
970
+ if (!claudeCodeDirectoryExists()) {
971
+ return {
972
+ content: [
973
+ {
974
+ type: "text",
975
+ text: "Error: Claude Code directory not found at ~/.claude/projects/. Make sure you have used Claude Code at least once.",
976
+ },
977
+ ],
978
+ isError: true,
979
+ };
980
+ }
981
+ try {
982
+ // Parse all transcripts within age limit
983
+ const maxAgeHours = args.maxAgeHours ?? 24;
984
+ const sessions = parseAllTranscripts(maxAgeHours);
985
+ if (sessions.length === 0) {
986
+ return {
987
+ content: [
988
+ {
989
+ type: "text",
990
+ text: `No Claude Code sessions found within the last ${maxAgeHours} hours. Try increasing maxAgeHours or use Claude Code first.`,
991
+ },
992
+ ],
993
+ };
994
+ }
995
+ // Transform sessions to API payload format
996
+ const payload = {
997
+ sessions: sessions.map((session) => ({
998
+ sessionId: session.sessionId,
999
+ projectHash: session.projectHash,
1000
+ model: session.model,
1001
+ inputTokens: session.tokens.inputTokens,
1002
+ outputTokens: session.tokens.outputTokens,
1003
+ cacheCreationTokens: session.tokens.cacheCreationTokens,
1004
+ cacheReadTokens: session.tokens.cacheReadTokens,
1005
+ messageCount: session.messageCount,
1006
+ startTime: session.startTime,
1007
+ endTime: session.endTime,
1008
+ })),
1009
+ clientVersion: "1.2.0",
1010
+ syncedAt: new Date().toISOString(),
1011
+ };
1012
+ // Send to CostHawk API
1013
+ const result = await apiRequest("/api/mcp/usage/claude-code", {
1014
+ method: "POST",
1015
+ apiKey,
1016
+ body: payload,
1017
+ });
1018
+ const text = args.format === "json" ? JSON.stringify(result, null, 2) : formatClaudeCodeSyncResultMarkdown(result);
1019
+ return {
1020
+ content: [{ type: "text", text }],
1021
+ };
1022
+ }
1023
+ catch (error) {
1024
+ return {
1025
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
1026
+ isError: true,
1027
+ };
1028
+ }
1029
+ });
1030
+ server.registerTool("costhawk_get_local_claude_code_usage", {
1031
+ description: `Get Claude Code usage from local transcripts WITHOUT uploading to CostHawk. Works offline. Shows token counts, costs at retail rates, and optional savings calculation if you provide your subscription plan.`,
1032
+ inputSchema: GetLocalClaudeCodeUsageSchema,
1033
+ annotations: READ_ONLY_ANNOTATIONS,
1034
+ }, async (args, _extra) => {
1035
+ // Check if Claude Code directory exists
1036
+ if (!claudeCodeDirectoryExists()) {
1037
+ return {
1038
+ content: [
1039
+ {
1040
+ type: "text",
1041
+ text: "Error: Claude Code directory not found at ~/.claude/projects/. Make sure you have used Claude Code at least once.",
1042
+ },
1043
+ ],
1044
+ isError: true,
1045
+ };
1046
+ }
1047
+ try {
1048
+ const maxAgeHours = args.maxAgeHours ?? 24;
1049
+ const sessions = parseAllTranscripts(maxAgeHours);
1050
+ if (sessions.length === 0) {
1051
+ return {
1052
+ content: [
1053
+ {
1054
+ type: "text",
1055
+ text: `No Claude Code sessions found within the last ${maxAgeHours} hours. Try increasing maxAgeHours or use Claude Code first.`,
1056
+ },
1057
+ ],
1058
+ };
1059
+ }
1060
+ // Aggregate usage
1061
+ const aggregated = aggregateUsage(sessions);
1062
+ // Calculate total cost using dominant model pricing
1063
+ const dominantModel = Object.entries(aggregated.models).sort((a, b) => b[1] - a[1])[0]?.[0] || "unknown";
1064
+ const cost = calculateCost({
1065
+ inputTokens: aggregated.tokens.inputTokens,
1066
+ outputTokens: aggregated.tokens.outputTokens,
1067
+ cacheCreationTokens: aggregated.tokens.cacheCreationTokens,
1068
+ cacheReadTokens: aggregated.tokens.cacheReadTokens,
1069
+ }, dominantModel);
1070
+ // Build session details
1071
+ const sessionDetails = sessions.map((session) => {
1072
+ const sessionCost = calculateCost(session.tokens, session.model);
1073
+ const totalTokens = session.tokens.inputTokens +
1074
+ session.tokens.outputTokens +
1075
+ session.tokens.cacheCreationTokens +
1076
+ session.tokens.cacheReadTokens;
1077
+ return {
1078
+ sessionId: session.sessionId,
1079
+ projectHash: session.projectHash,
1080
+ model: session.model,
1081
+ tokens: totalTokens,
1082
+ cost: sessionCost.totalCost,
1083
+ startTime: session.startTime,
1084
+ endTime: session.endTime,
1085
+ };
1086
+ });
1087
+ // Build response
1088
+ const response = {
1089
+ sessions: aggregated.totalSessions,
1090
+ tokens: {
1091
+ input: aggregated.tokens.inputTokens,
1092
+ output: aggregated.tokens.outputTokens,
1093
+ cacheCreation: aggregated.tokens.cacheCreationTokens,
1094
+ cacheRead: aggregated.tokens.cacheReadTokens,
1095
+ total: aggregated.totalTokens,
1096
+ },
1097
+ cost,
1098
+ sessionDetails,
1099
+ };
1100
+ // Add savings calculation if subscription plan provided
1101
+ if (args.subscriptionPlan) {
1102
+ const subscription = CLAUDE_SUBSCRIPTIONS[args.subscriptionPlan];
1103
+ const savings = calculateSavings(cost.totalCost, subscription.monthlyFee);
1104
+ response.savings = {
1105
+ subscriptionPlan: subscription.name,
1106
+ subscriptionCost: subscription.monthlyFee,
1107
+ savingsAmount: savings.savings,
1108
+ savingsPercentage: savings.savingsPercentage,
1109
+ status: savings.status,
1110
+ };
1111
+ }
1112
+ const text = args.format === "json" ? JSON.stringify(response, null, 2) : formatClaudeCodeLocalUsageMarkdown(response);
1113
+ return {
1114
+ content: [{ type: "text", text }],
1115
+ };
1116
+ }
1117
+ catch (error) {
1118
+ return {
1119
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
1120
+ isError: true,
1121
+ };
1122
+ }
1123
+ });
1124
+ server.registerTool("costhawk_list_claude_code_sessions", {
1125
+ description: `List available Claude Code sessions from local transcripts. Shows session IDs, project hashes, modification dates, and file sizes. Use this to see what sessions are available before syncing.`,
1126
+ inputSchema: ListClaudeCodeSessionsSchema,
1127
+ annotations: READ_ONLY_ANNOTATIONS,
1128
+ }, async (args, _extra) => {
1129
+ // Check if Claude Code directory exists
1130
+ if (!claudeCodeDirectoryExists()) {
1131
+ return {
1132
+ content: [
1133
+ {
1134
+ type: "text",
1135
+ text: "Error: Claude Code directory not found at ~/.claude/projects/. Make sure you have used Claude Code at least once.",
1136
+ },
1137
+ ],
1138
+ isError: true,
1139
+ };
1140
+ }
1141
+ try {
1142
+ const maxAgeHours = args.maxAgeHours ?? 168;
1143
+ const limit = args.limit ?? 20;
1144
+ const transcripts = discoverTranscripts(maxAgeHours).slice(0, limit);
1145
+ const text = args.format === "json"
1146
+ ? JSON.stringify(transcripts.map((t) => ({
1147
+ sessionId: t.sessionId,
1148
+ projectHash: t.projectHash,
1149
+ lastModified: t.lastModified.toISOString(),
1150
+ size: t.size,
1151
+ path: t.path,
1152
+ })), null, 2)
1153
+ : formatClaudeCodeSessionsMarkdown(transcripts);
1154
+ return {
1155
+ content: [{ type: "text", text }],
1156
+ };
1157
+ }
1158
+ catch (error) {
1159
+ return {
1160
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
1161
+ isError: true,
1162
+ };
1163
+ }
1164
+ });
835
1165
  // Start server
836
1166
  async function main() {
837
1167
  const transport = new StdioServerTransport();