fix: coerce status JSON numeric strings and bump version

Coerce numeric strings in StatusJSON parsing to keep status line rendering resilient with third-party payload tweaks.

Closes #192
This commit is contained in:
Matthew Breedlove
2026-03-03 18:30:04 -05:00
parent da7eaffe03
commit b13ea14a8c
3 changed files with 91 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "2.1.4",
"version": "2.1.5",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
+29 -15
View File
@@ -1,5 +1,19 @@
import { z } from 'zod';
const CoercedNumberSchema = z.preprocess((value) => {
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
if (trimmed.length === 0) {
return value;
}
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : value;
}, z.number());
export const StatusJSONSchema = z.looseObject({
hook_event_name: z.string().optional(),
session_id: z.string().optional(),
@@ -19,27 +33,27 @@ export const StatusJSONSchema = z.looseObject({
version: z.string().optional(),
output_style: z.object({ name: z.string().optional() }).optional(),
cost: z.object({
total_cost_usd: z.number().optional(),
total_duration_ms: z.number().optional(),
total_api_duration_ms: z.number().optional(),
total_lines_added: z.number().optional(),
total_lines_removed: z.number().optional()
total_cost_usd: CoercedNumberSchema.optional(),
total_duration_ms: CoercedNumberSchema.optional(),
total_api_duration_ms: CoercedNumberSchema.optional(),
total_lines_added: CoercedNumberSchema.optional(),
total_lines_removed: CoercedNumberSchema.optional()
}).optional(),
context_window: z.object({
context_window_size: z.number().nullable().optional(),
total_input_tokens: z.number().nullable().optional(),
total_output_tokens: z.number().nullable().optional(),
context_window_size: CoercedNumberSchema.nullable().optional(),
total_input_tokens: CoercedNumberSchema.nullable().optional(),
total_output_tokens: CoercedNumberSchema.nullable().optional(),
current_usage: z.union([
z.number(),
CoercedNumberSchema,
z.object({
input_tokens: z.number().optional(),
output_tokens: z.number().optional(),
cache_creation_input_tokens: z.number().optional(),
cache_read_input_tokens: z.number().optional()
input_tokens: CoercedNumberSchema.optional(),
output_tokens: CoercedNumberSchema.optional(),
cache_creation_input_tokens: CoercedNumberSchema.optional(),
cache_read_input_tokens: CoercedNumberSchema.optional()
})
]).nullable().optional(),
used_percentage: z.number().nullable().optional(),
remaining_percentage: z.number().nullable().optional()
used_percentage: CoercedNumberSchema.nullable().optional(),
remaining_percentage: CoercedNumberSchema.nullable().optional()
}).nullable().optional()
});
+61
View File
@@ -0,0 +1,61 @@
import {
describe,
expect,
it
} from 'vitest';
import { StatusJSONSchema } from '../StatusJSON';
describe('StatusJSONSchema numeric coercion', () => {
it('coerces numeric strings to numbers', () => {
const result = StatusJSONSchema.safeParse({
cost: {
total_cost_usd: '1.25',
total_duration_ms: '12345',
total_api_duration_ms: '2345',
total_lines_added: '12',
total_lines_removed: '3'
},
context_window: {
context_window_size: '200000',
total_input_tokens: '1200',
total_output_tokens: '340',
current_usage: {
input_tokens: '100',
output_tokens: '50',
cache_creation_input_tokens: '20',
cache_read_input_tokens: '10'
},
used_percentage: '9.3',
remaining_percentage: '90.7'
}
});
expect(result.success).toBe(true);
if (!result.success) {
return;
}
expect(result.data.cost?.total_duration_ms).toBe(12345);
expect(result.data.context_window?.context_window_size).toBe(200000);
expect(result.data.context_window?.current_usage).toEqual({
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10
});
expect(result.data.context_window?.used_percentage).toBe(9.3);
});
it('keeps invalid numeric strings rejected', () => {
const result = StatusJSONSchema.safeParse({ context_window: { context_window_size: 'not-a-number' } });
expect(result.success).toBe(false);
});
it('keeps empty numeric strings rejected', () => {
const result = StatusJSONSchema.safeParse({ context_window: { context_window_size: '' } });
expect(result.success).toBe(false);
});
});