Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,33 +459,56 @@ Control hook behavior with these environment variables:

### Real-Time Session Monitoring

Track token savings in real-time using the built-in monitoring tools:
**To view your actual token SAVINGS**, use the `get_session_stats` tool:

```typescript
// View current session statistics
// View current session statistics with token savings breakdown
await get_session_stats({});
```

**Output includes:**
- Total tokens saved (input + output)
- Token reduction percentage
- Cache hit rate
- Breakdown by tool (Read, Grep, Glob, etc.)
- Top 10 most optimized operations
- **Total tokens saved** (this is the actual savings amount!)
- **Token reduction percentage** (e.g., "60% reduction")
- **Cache hit rate** and **compression ratios**
- **Breakdown by tool** (Read, Grep, Glob, etc.)
- **Top 10 most optimized operations** with before/after comparison

**Example Output:**
```json
{
"sessionId": "abc-123",
"totalTokensSaved": 125430, // ← THIS is your savings!
"tokenReductionPercent": 68.2,
"originalTokens": 184000,
"optimizedTokens": 58570,
"cacheHitRate": 0.72,
"byTool": {
"smart_read": { "saved": 45000, "percent": 80 },
"smart_grep": { "saved": 32000, "percent": 75 }
}
}
```

### Session Logs
### Session Tracking Files

All operations are automatically tracked in session data files:

**Location**: `~/.claude-global/hooks/data/current-session.txt`

**Tracked Data**:
- `sessionId` - Unique identifier for the session
- `totalOperations` - Number of operations performed
- `totalTokens` - Cumulative token count
- `lastOptimized` - Timestamp of last optimization
**IMPORTANT**: This file shows operation tracking, NOT token savings:

```json
{
"sessionId": "abc-123",
"totalOperations": 1250, // ← Number of operations
"totalTokens": 184000, // ← Cumulative token COUNT (not savings!)
"lastOptimized": 1698765432
}
```

**To see actual token SAVINGS**, you MUST use `get_session_stats()` - the session file only tracks operation counts, not optimization results.

**Note**: Detailed CSV logging is planned for a future release.
**Note**: Detailed CSV logging with per-operation savings is planned for a future release.

### Project-Wide Analysis

Expand Down
26 changes: 8 additions & 18 deletions hooks/helpers/invoke-mcp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,21 @@ function Invoke-MCP {
param(
[string]$Server,
[string]$Tool,
$Args # Accept any type, will validate internally
$ToolArguments # FIXED: Renamed from $Args to avoid collision with PowerShell's automatic $args variable
)

try {
Write-Log "Invoking MCP: $Server -> $Tool" "DEBUG"

# Ensure Args is a proper object for JSON serialization
# ConvertTo-Json can serialize hashtables as arrays in some cases
# Force conversion to PSCustomObject to ensure object serialization
if ($null -eq $Args) {
$Args = @{}
# Ensure ToolArguments is a proper object for JSON serialization
if ($null -eq $ToolArguments) {
$ToolArguments = @{}
}

# Build MCP protocol request
# CRITICAL FIX: Explicitly convert nested Hashtable to PSCustomObject
# When a Hashtable is nested inside another Hashtable and then converted to JSON,
# PowerShell treats it as an enumerable collection, resulting in [] empty array
# instead of {} JSON object. This fix ensures proper JSON object serialization.
$jsonArguments = if ($Args -is [hashtable] -and $Args.Count -gt 0) {
[PSCustomObject]$Args
} elseif ($null -eq $Args -or ($Args -is [hashtable] -and $Args.Count -eq 0)) {
[PSCustomObject]@{}
} else {
$Args
}
# PowerShell Hashtables serialize correctly to JSON objects without casting
# No [PSCustomObject] conversion needed - let ConvertTo-Json handle it natively
$jsonArguments = $ToolArguments

$request = @{
jsonrpc = "2.0"
Expand Down Expand Up @@ -148,7 +138,7 @@ Write-Log "Arguments type: $($Arguments.GetType().FullName)" "DEBUG"
Write-Log "Arguments content: $($Arguments | ConvertTo-Json -Compress)" "DEBUG"

# Execute the MCP call
$result = Invoke-MCP -Server $ServerName -Tool $Tool -Args $Arguments
$result = Invoke-MCP -Server $ServerName -Tool $Tool -ToolArguments $Arguments

# Output result as JSON for caller
if ($result) {
Expand Down
14 changes: 11 additions & 3 deletions src/core/token-counter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export class TokenCounter {
// Auto-detect model from environment or use provided model
// Claude Code sets CLAUDE_MODEL env var with the active model
// Falls back to GPT-4 as universal approximation
this.model = model || process.env.CLAUDE_MODEL || process.env.ANTHROPIC_MODEL || 'gpt-4';
this.model =
model ||
process.env.CLAUDE_MODEL ||
process.env.ANTHROPIC_MODEL ||
'gpt-4';

// Map Claude models to closest tiktoken equivalent
// Claude uses similar tokenization to GPT-4, so it's a good approximation
Expand All @@ -31,8 +35,12 @@ export class TokenCounter {
const lowerModel = model.toLowerCase();

// Claude models use GPT-4 tokenizer as closest approximation
if (lowerModel.includes('claude') || lowerModel.includes('sonnet') ||
lowerModel.includes('opus') || lowerModel.includes('haiku')) {
if (
lowerModel.includes('claude') ||
lowerModel.includes('sonnet') ||
lowerModel.includes('opus') ||
lowerModel.includes('haiku')
) {
return 'gpt-4';
}

Expand Down
7 changes: 5 additions & 2 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,18 +630,21 @@
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;

// @ts-ignore - validatedArgs ensures validation but original args used for type compatibility
// @ts-expect-error - validatedArgs ensures validation but original args used for type compatibility
// Validate tool arguments using Zod schemas
let validatedArgs: any;

Check warning on line 635 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
try {
validatedArgs = validateToolArgs(name, args || {});

Check warning on line 637 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

'validatedArgs' is assigned a value but never used. Allowed unused vars must match /^_/u
} catch (validationError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: validationError instanceof Error ? validationError.message : String(validationError),
error:
validationError instanceof Error
? validationError.message
: String(validationError),
}),
},
],
Expand Down Expand Up @@ -1373,7 +1376,7 @@
}

case 'predictive_cache': {
const options = args as any;

Check warning on line 1379 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
const result = await predictiveCache.run(options);

return {
Expand All @@ -1387,7 +1390,7 @@
}

case 'cache_warmup': {
const options = args as any;

Check warning on line 1393 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
const result = await cacheWarmup.run(options);

return {
Expand All @@ -1402,7 +1405,7 @@

// Code analysis tools
case 'smart_ast_grep': {
const options = args as any;

Check warning on line 1408 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
const result = await smartAstGrep.grep(options.pattern, options);
return {
content: [
Expand All @@ -1415,7 +1418,7 @@
}

case 'cache_analytics': {
const options = args as any;

Check warning on line 1421 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
const result = await cacheAnalytics.run(options);

return {
Expand All @@ -1429,7 +1432,7 @@
}

case 'cache_benchmark': {
const options = args as any;

Check warning on line 1435 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
const result = await runCacheBenchmark(
options,
cache,
Expand All @@ -1448,7 +1451,7 @@
}

case 'cache_compression': {
const options = args as any;

Check warning on line 1454 in src/server/index.ts

View workflow job for this annotation

GitHub Actions / Lint and Format Check

Unexpected any. Specify a different type
const result = await runCacheCompression(options);

return {
Expand Down
Loading
Loading