Skip to content

Commit b032d4c

Browse files
committed
refactor(telemetry): improve type safety in telemetry parsers
- Replace 'any' with 'unknown' and add proper type guards - Update eslint config to use tseslint.config - Rename CCR engine display name to 'Claude Code Router'
1 parent 230ceaa commit b032d4c

File tree

9 files changed

+120
-66
lines changed

9 files changed

+120
-66
lines changed

eslint.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import js from '@eslint/js';
22
import eslintConfigPrettier from 'eslint-config-prettier';
33
import importPlugin from 'eslint-plugin-import';
4-
import { defineConfig } from 'typescript-eslint';
54
import tseslint from 'typescript-eslint';
65

76
const typescriptFiles = ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'];
87

9-
export default defineConfig(
8+
export default tseslint.config(
109
{
1110
ignores: ['node_modules', 'dist', 'coverage'],
1211
},

src/infra/engines/providers/ccr/telemetryParser.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ interface CapturedTelemetry {
1313
*
1414
* CCR uses Claude API format with "result" events
1515
*/
16-
export function parseTelemetry(json: any): CapturedTelemetry | null {
17-
// CCR format: same as Claude - type: 'result' with detailed usage
18-
if (json.type === 'result' && json.usage) {
16+
export function parseTelemetry(json: unknown): CapturedTelemetry | null {
17+
// Type guard to check if json is an object with required properties
18+
if (
19+
typeof json === 'object' &&
20+
json !== null &&
21+
'type' in json &&
22+
(json as Record<string, unknown>).type === 'result' &&
23+
'usage' in json
24+
) {
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
const data = json as Record<string, any>;
1927
// Calculate cached tokens from both cache_read_input_tokens and cache_creation_input_tokens
20-
const cachedTokens = (json.usage.cache_read_input_tokens || 0) + (json.usage.cache_creation_input_tokens || 0);
28+
const cachedTokens = (data.usage.cache_read_input_tokens || 0) + (data.usage.cache_creation_input_tokens || 0);
2129

2230
return {
23-
duration: json.duration_ms,
24-
cost: json.total_cost_usd,
31+
duration: data.duration_ms,
32+
cost: data.total_cost_usd,
2533
tokens: {
26-
input: json.usage.input_tokens,
27-
output: json.usage.output_tokens,
34+
input: data.usage.input_tokens,
35+
output: data.usage.output_tokens,
2836
cached: cachedTokens > 0 ? cachedTokens : undefined,
2937
},
3038
};

src/infra/engines/providers/claude/telemetryParser.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ interface CapturedTelemetry {
1313
*
1414
* Claude emits telemetry in "result" events with full usage data
1515
*/
16-
export function parseTelemetry(json: any): CapturedTelemetry | null {
17-
// Claude format: type: 'result' with detailed usage
18-
if (json.type === 'result' && json.usage) {
16+
export function parseTelemetry(json: unknown): CapturedTelemetry | null {
17+
// Type guard to check if json is an object with required properties
18+
if (
19+
typeof json === 'object' &&
20+
json !== null &&
21+
'type' in json &&
22+
(json as Record<string, unknown>).type === 'result' &&
23+
'usage' in json
24+
) {
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
const data = json as Record<string, any>;
1927
// Calculate cached tokens from both cache_read_input_tokens and cache_creation_input_tokens
20-
const cachedTokens = (json.usage.cache_read_input_tokens || 0) + (json.usage.cache_creation_input_tokens || 0);
28+
const cachedTokens = (data.usage.cache_read_input_tokens || 0) + (data.usage.cache_creation_input_tokens || 0);
2129

2230
return {
23-
duration: json.duration_ms,
24-
cost: json.total_cost_usd,
31+
duration: data.duration_ms,
32+
cost: data.total_cost_usd,
2533
tokens: {
26-
input: json.usage.input_tokens,
27-
output: json.usage.output_tokens,
34+
input: data.usage.input_tokens,
35+
output: data.usage.output_tokens,
2836
cached: cachedTokens > 0 ? cachedTokens : undefined,
2937
},
3038
};

src/infra/engines/providers/codex/telemetryParser.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@ interface CapturedTelemetry {
1313
*
1414
* Codex emits telemetry in "turn.completed" events with usage data
1515
*/
16-
export function parseTelemetry(json: any): CapturedTelemetry | null {
17-
// Codex format: type: 'turn.completed' with usage
18-
if (json.type === 'turn.completed' && json.usage) {
16+
export function parseTelemetry(json: unknown): CapturedTelemetry | null {
17+
// Type guard to check if json is an object with required properties
18+
if (
19+
typeof json === 'object' &&
20+
json !== null &&
21+
'type' in json &&
22+
(json as Record<string, unknown>).type === 'turn.completed' &&
23+
'usage' in json
24+
) {
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
const data = json as Record<string, any>;
1927
return {
2028
tokens: {
21-
input: json.usage.input_tokens,
22-
output: json.usage.output_tokens,
23-
cached: json.usage.cached_input_tokens,
29+
input: data.usage.input_tokens,
30+
output: data.usage.output_tokens,
31+
cached: data.usage.cached_input_tokens,
2432
},
2533
};
2634
}

src/infra/engines/providers/cursor/telemetryParser.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ interface CapturedTelemetry {
1313
*
1414
* Cursor uses Claude API format with "result" events
1515
*/
16-
export function parseTelemetry(json: any): CapturedTelemetry | null {
17-
// Cursor format: same as Claude - type: 'result' with detailed usage
18-
if (json.type === 'result' && json.usage) {
16+
export function parseTelemetry(json: unknown): CapturedTelemetry | null {
17+
// Type guard to check if json is an object with required properties
18+
if (
19+
typeof json === 'object' &&
20+
json !== null &&
21+
'type' in json &&
22+
(json as Record<string, unknown>).type === 'result' &&
23+
'usage' in json
24+
) {
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
const data = json as Record<string, any>;
1927
// Calculate cached tokens from both cache_read_input_tokens and cache_creation_input_tokens
20-
const cachedTokens = (json.usage.cache_read_input_tokens || 0) + (json.usage.cache_creation_input_tokens || 0);
28+
const cachedTokens = (data.usage.cache_read_input_tokens || 0) + (data.usage.cache_creation_input_tokens || 0);
2129

2230
return {
23-
duration: json.duration_ms,
24-
cost: json.total_cost_usd,
31+
duration: data.duration_ms,
32+
cost: data.total_cost_usd,
2533
tokens: {
26-
input: json.usage.input_tokens,
27-
output: json.usage.output_tokens,
34+
input: data.usage.input_tokens,
35+
output: data.usage.output_tokens,
2836
cached: cachedTokens > 0 ? cachedTokens : undefined,
2937
},
3038
};

src/infra/engines/providers/opencode/execution/runner.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ function cleanAnsi(text: string, plainLogs: boolean): string {
7676
return text.replace(ANSI_ESCAPE_SEQUENCE, '');
7777
}
7878

79-
function formatToolUse(part: any, plainLogs: boolean): string {
80-
const tool = part?.tool ?? 'tool';
79+
function formatToolUse(part: unknown, plainLogs: boolean): string {
80+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
81+
const partObj = (typeof part === 'object' && part !== null ? part : {}) as Record<string, any>;
82+
const tool = partObj?.tool ?? 'tool';
8183
const base = formatCommand(tool, 'success');
82-
const state = part?.state ?? {};
84+
const state = partObj?.state ?? {};
8385

8486
if (tool === 'bash') {
8587
const outputRaw =
@@ -108,15 +110,17 @@ function formatToolUse(part: any, plainLogs: boolean): string {
108110
return base;
109111
}
110112

111-
function formatStepEvent(type: string, part: any): string | null {
112-
const reason = typeof part?.reason === 'string' ? part.reason : undefined;
113+
function formatStepEvent(type: string, part: unknown): string | null {
114+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
115+
const partObj = (typeof part === 'object' && part !== null ? part : {}) as Record<string, any>;
116+
const reason = typeof partObj?.reason === 'string' ? partObj.reason : undefined;
113117

114118
// Only show final step (reason: 'stop'), skip intermediate steps (reason: 'tool-calls')
115119
if (reason !== 'stop') {
116120
return null;
117121
}
118122

119-
const tokens = part?.tokens;
123+
const tokens = partObj?.tokens;
120124
if (!tokens) {
121125
return null;
122126
}
@@ -128,14 +132,16 @@ function formatStepEvent(type: string, part: any): string | null {
128132
return tokenSummary;
129133
}
130134

131-
function formatErrorEvent(error: any, plainLogs: boolean): string {
135+
function formatErrorEvent(error: unknown, plainLogs: boolean): string {
136+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
137+
const errorObj = (typeof error === 'object' && error !== null ? error : {}) as Record<string, any>;
132138
const dataMessage =
133-
typeof error?.data?.message === 'string'
134-
? error.data.message
135-
: typeof error?.message === 'string'
136-
? error.message
137-
: typeof error?.name === 'string'
138-
? error.name
139+
typeof errorObj?.data?.message === 'string'
140+
? errorObj.data.message
141+
: typeof errorObj?.message === 'string'
142+
? errorObj.message
143+
: typeof errorObj?.name === 'string'
144+
? errorObj.name
139145
: 'OpenCode reported an unknown error';
140146

141147
const cleaned = cleanAnsi(dataMessage, plainLogs);
@@ -184,7 +190,7 @@ export async function runOpenCode(options: RunOpenCodeOptions): Promise<RunOpenC
184190
return;
185191
}
186192

187-
let parsed: any;
193+
let parsed: unknown;
188194
try {
189195
parsed = JSON.parse(line);
190196
} catch {
@@ -208,10 +214,17 @@ export async function runOpenCode(options: RunOpenCodeOptions): Promise<RunOpenC
208214
}
209215
}
210216

217+
// Type guard for parsed JSON
218+
if (typeof parsed !== 'object' || parsed === null) {
219+
return;
220+
}
221+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
222+
const parsedObj = parsed as Record<string, any>;
223+
211224
let formatted: string | null = null;
212-
switch (parsed.type) {
225+
switch (parsedObj.type) {
213226
case 'tool_use':
214-
formatted = formatToolUse(parsed.part, plainLogs);
227+
formatted = formatToolUse(parsedObj.part, plainLogs);
215228
break;
216229
case 'step_start':
217230
if (isFirstStep) {
@@ -221,10 +234,10 @@ export async function runOpenCode(options: RunOpenCodeOptions): Promise<RunOpenC
221234
// Subsequent step_start events are silent
222235
break;
223236
case 'step_finish':
224-
formatted = formatStepEvent(parsed.type, parsed.part);
237+
formatted = formatStepEvent(parsedObj.type, parsedObj.part);
225238
break;
226239
case 'text': {
227-
const textPart = parsed.part;
240+
const textPart = parsedObj.part;
228241
const textValue =
229242
typeof textPart?.text === 'string'
230243
? cleanAnsi(textPart.text, plainLogs)
@@ -233,7 +246,7 @@ export async function runOpenCode(options: RunOpenCodeOptions): Promise<RunOpenC
233246
break;
234247
}
235248
case 'error':
236-
formatted = formatErrorEvent(parsed.error, plainLogs);
249+
formatted = formatErrorEvent(parsedObj.error, plainLogs);
237250
break;
238251
default:
239252
break;

src/infra/engines/providers/opencode/telemetryParser.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,30 @@ interface CapturedTelemetry {
1313
*
1414
* OpenCode emits telemetry in "step_finish" events with nested tokens in part.tokens
1515
*/
16-
export function parseTelemetry(json: any): CapturedTelemetry | null {
17-
// OpenCode format: type: 'step_finish' with part.tokens
18-
if (json.type === 'step_finish' && json.part?.tokens) {
19-
const tokens = json.part.tokens;
20-
const cache = (tokens.cache?.read || 0) + (tokens.cache?.write || 0);
16+
export function parseTelemetry(json: unknown): CapturedTelemetry | null {
17+
// Type guard to check if json is an object with required properties
18+
if (
19+
typeof json === 'object' &&
20+
json !== null &&
21+
'type' in json &&
22+
(json as Record<string, unknown>).type === 'step_finish' &&
23+
'part' in json
24+
) {
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
const data = json as Record<string, any>;
27+
if (data.part?.tokens) {
28+
const tokens = data.part.tokens;
29+
const cache = (tokens.cache?.read || 0) + (tokens.cache?.write || 0);
2130

22-
return {
23-
tokens: {
24-
input: tokens.input,
25-
output: tokens.output,
26-
cached: cache > 0 ? cache : undefined,
27-
},
28-
cost: json.part.cost,
29-
};
31+
return {
32+
tokens: {
33+
input: tokens.input,
34+
output: tokens.output,
35+
cached: cache > 0 ? cache : undefined,
36+
},
37+
cost: data.part.cost,
38+
};
39+
}
3040
}
3141

3242
return null;

src/shared/telemetry/capture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface CapturedTelemetry {
1616
};
1717
}
1818

19-
type TelemetryParser = (json: any) => CapturedTelemetry | null;
19+
type TelemetryParser = (json: unknown) => CapturedTelemetry | null;
2020

2121
/**
2222
* Engine-specific telemetry parsers

tests/unit/infra/ccr-registry.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('CCR Engine Registry Integration', () => {
1313
expect(ccrEngine).toBeDefined();
1414
expect(ccrEngine?.metadata).toEqual({
1515
id: 'ccr',
16-
name: 'CCR',
16+
name: 'Claude Code Router',
1717
description: 'Authenticate with Claude Code Router',
1818
cliCommand: 'ccr',
1919
cliBinary: 'ccr',

0 commit comments

Comments
 (0)