@praeviso/code-env-switch 0.1.3 → 0.1.5

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.
@@ -0,0 +1,263 @@
1
+ import type {
2
+ StatuslineInput,
3
+ StatuslineInputUsage,
4
+ StatuslineUsageTotals,
5
+ } from "../types";
6
+ import { coerceNumber, firstNumber, isRecord } from "../utils";
7
+
8
+ function resolveOutputTokens(record: Record<string, unknown>): number | null {
9
+ const outputTokens =
10
+ firstNumber(
11
+ record.outputTokens,
12
+ record.output,
13
+ record.output_tokens
14
+ ) ?? null;
15
+ const reasoningTokens =
16
+ firstNumber(
17
+ record.reasoning_output_tokens,
18
+ record.reasoningOutputTokens,
19
+ record.reasoning_output
20
+ ) ?? null;
21
+ if (outputTokens === null && reasoningTokens === null) return null;
22
+ if (reasoningTokens === null) return outputTokens;
23
+ return (outputTokens || 0) + reasoningTokens;
24
+ }
25
+
26
+ function parseCodexUsageTotalsRecord(
27
+ record: Record<string, unknown>
28
+ ): StatuslineUsageTotals | null {
29
+ const inputTokens =
30
+ firstNumber(
31
+ record.inputTokens,
32
+ record.input,
33
+ record.input_tokens
34
+ ) ?? null;
35
+ const outputTokens = resolveOutputTokens(record);
36
+ const cacheRead =
37
+ firstNumber(
38
+ record.cached_input_tokens,
39
+ record.cachedInputTokens,
40
+ record.cache_read_input_tokens,
41
+ record.cacheReadInputTokens,
42
+ record.cache_read,
43
+ record.cacheRead
44
+ ) ?? null;
45
+ const cacheWrite =
46
+ firstNumber(
47
+ record.cache_creation_input_tokens,
48
+ record.cacheCreationInputTokens,
49
+ record.cache_write_input_tokens,
50
+ record.cacheWriteInputTokens,
51
+ record.cache_write,
52
+ record.cacheWrite
53
+ ) ?? null;
54
+ const totalTokens =
55
+ firstNumber(
56
+ record.totalTokens,
57
+ record.total,
58
+ record.total_tokens
59
+ ) ?? null;
60
+ let computedTotal: number | null = null;
61
+ if (
62
+ inputTokens !== null ||
63
+ outputTokens !== null ||
64
+ cacheRead !== null ||
65
+ cacheWrite !== null
66
+ ) {
67
+ computedTotal =
68
+ (inputTokens || 0) +
69
+ (outputTokens || 0) +
70
+ (cacheRead || 0) +
71
+ (cacheWrite || 0);
72
+ }
73
+ const resolvedTotal = totalTokens ?? computedTotal;
74
+ if (
75
+ inputTokens === null &&
76
+ outputTokens === null &&
77
+ cacheRead === null &&
78
+ cacheWrite === null &&
79
+ resolvedTotal === null
80
+ ) {
81
+ return null;
82
+ }
83
+ return {
84
+ inputTokens,
85
+ outputTokens,
86
+ cacheReadTokens: cacheRead,
87
+ cacheWriteTokens: cacheWrite,
88
+ totalTokens: resolvedTotal,
89
+ };
90
+ }
91
+
92
+ function parseCodexInputUsageRecord(
93
+ record: Record<string, unknown>
94
+ ): StatuslineInputUsage | null {
95
+ const todayTokens =
96
+ firstNumber(
97
+ record.todayTokens,
98
+ record.today,
99
+ record.today_tokens,
100
+ record.daily,
101
+ record.daily_tokens
102
+ ) ?? null;
103
+ const totalTokens =
104
+ firstNumber(
105
+ record.totalTokens,
106
+ record.total,
107
+ record.total_tokens
108
+ ) ?? null;
109
+ const inputTokens =
110
+ firstNumber(
111
+ record.inputTokens,
112
+ record.input,
113
+ record.input_tokens
114
+ ) ?? null;
115
+ const outputTokens = resolveOutputTokens(record);
116
+ const cacheRead =
117
+ firstNumber(
118
+ record.cached_input_tokens,
119
+ record.cachedInputTokens,
120
+ record.cache_read_input_tokens,
121
+ record.cacheReadInputTokens,
122
+ record.cache_read,
123
+ record.cacheRead
124
+ ) ?? null;
125
+ const cacheWrite =
126
+ firstNumber(
127
+ record.cache_creation_input_tokens,
128
+ record.cacheCreationInputTokens,
129
+ record.cache_write_input_tokens,
130
+ record.cacheWriteInputTokens,
131
+ record.cache_write,
132
+ record.cacheWrite
133
+ ) ?? null;
134
+ if (
135
+ todayTokens === null &&
136
+ totalTokens === null &&
137
+ inputTokens === null &&
138
+ outputTokens === null &&
139
+ cacheRead === null &&
140
+ cacheWrite === null
141
+ ) {
142
+ return null;
143
+ }
144
+ const hasCacheTokens = cacheRead !== null || cacheWrite !== null;
145
+ const computedTotal = hasCacheTokens
146
+ ? (inputTokens || 0) +
147
+ (outputTokens || 0) +
148
+ (cacheRead || 0) +
149
+ (cacheWrite || 0)
150
+ : null;
151
+ const resolvedTodayTokens = hasCacheTokens
152
+ ? todayTokens ?? totalTokens ?? computedTotal
153
+ : todayTokens;
154
+ return {
155
+ todayTokens: resolvedTodayTokens,
156
+ totalTokens: totalTokens ?? null,
157
+ inputTokens,
158
+ outputTokens,
159
+ cacheReadTokens: cacheRead,
160
+ cacheWriteTokens: cacheWrite,
161
+ };
162
+ }
163
+
164
+ function resolveNestedRecord(
165
+ record: Record<string, unknown>,
166
+ ...keys: string[]
167
+ ): Record<string, unknown> | null {
168
+ for (const key of keys) {
169
+ if (isRecord(record[key])) {
170
+ return record[key] as Record<string, unknown>;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ export function getCodexUsageTotalsFromInput(
177
+ input: StatuslineInput | null
178
+ ): StatuslineUsageTotals | null {
179
+ if (!input) return null;
180
+ const tokenUsage = input.token_usage;
181
+ if (typeof tokenUsage === "number") {
182
+ return {
183
+ inputTokens: null,
184
+ outputTokens: null,
185
+ cacheReadTokens: null,
186
+ cacheWriteTokens: null,
187
+ totalTokens: coerceNumber(tokenUsage),
188
+ };
189
+ }
190
+ if (isRecord(tokenUsage)) {
191
+ const totalUsage = resolveNestedRecord(
192
+ tokenUsage,
193
+ "total_token_usage",
194
+ "totalTokenUsage"
195
+ );
196
+ if (totalUsage) {
197
+ const parsed = parseCodexUsageTotalsRecord(totalUsage);
198
+ if (parsed) return parsed;
199
+ }
200
+ const lastUsage = resolveNestedRecord(
201
+ tokenUsage,
202
+ "last_token_usage",
203
+ "lastTokenUsage"
204
+ );
205
+ if (lastUsage) {
206
+ const parsed = parseCodexUsageTotalsRecord(lastUsage);
207
+ if (parsed) return parsed;
208
+ }
209
+ const parsed = parseCodexUsageTotalsRecord(tokenUsage as Record<string, unknown>);
210
+ if (parsed) return parsed;
211
+ }
212
+ if (isRecord(input.usage)) {
213
+ return parseCodexUsageTotalsRecord(input.usage as Record<string, unknown>);
214
+ }
215
+ return null;
216
+ }
217
+
218
+ export function getCodexInputUsage(
219
+ input: StatuslineInput | null
220
+ ): StatuslineInputUsage | null {
221
+ if (!input) return null;
222
+ if (isRecord(input.usage)) {
223
+ const parsed = parseCodexInputUsageRecord(input.usage as Record<string, unknown>);
224
+ if (parsed) return parsed;
225
+ return input.usage as StatuslineInputUsage;
226
+ }
227
+ const tokenUsage = input.token_usage;
228
+ if (tokenUsage !== null && tokenUsage !== undefined) {
229
+ if (typeof tokenUsage === "number") {
230
+ return {
231
+ todayTokens: null,
232
+ totalTokens: coerceNumber(tokenUsage),
233
+ inputTokens: null,
234
+ outputTokens: null,
235
+ cacheReadTokens: null,
236
+ cacheWriteTokens: null,
237
+ };
238
+ }
239
+ if (isRecord(tokenUsage)) {
240
+ const totalUsage = resolveNestedRecord(
241
+ tokenUsage,
242
+ "total_token_usage",
243
+ "totalTokenUsage"
244
+ );
245
+ if (totalUsage) {
246
+ const parsed = parseCodexInputUsageRecord(totalUsage);
247
+ if (parsed) return parsed;
248
+ }
249
+ const lastUsage = resolveNestedRecord(
250
+ tokenUsage,
251
+ "last_token_usage",
252
+ "lastTokenUsage"
253
+ );
254
+ if (lastUsage) {
255
+ const parsed = parseCodexInputUsageRecord(lastUsage);
256
+ if (parsed) return parsed;
257
+ }
258
+ const parsed = parseCodexInputUsageRecord(tokenUsage as Record<string, unknown>);
259
+ if (parsed) return parsed;
260
+ }
261
+ }
262
+ return null;
263
+ }
@@ -0,0 +1,80 @@
1
+ import type { Config } from "../types";
2
+ import { normalizeType } from "../profile/type";
3
+ import { readUsageTotalsIndex, resolveUsageTotalsForProfile } from "../usage";
4
+ import type { StatuslineInput, StatuslineInputUsage, StatuslineUsage, StatuslineUsageTotals } from "./types";
5
+ import { coerceNumber } from "./utils";
6
+ import { getClaudeUsageTotalsFromInput } from "./usage/claude";
7
+ import { getCodexUsageTotalsFromInput } from "./usage/codex";
8
+
9
+ export function getUsageTotalsFromInput(
10
+ input: StatuslineInput | null,
11
+ type: string | null
12
+ ): StatuslineUsageTotals | null {
13
+ if (!input) return null;
14
+ const normalized = normalizeType(type || "");
15
+ if (normalized === "codex") {
16
+ return getCodexUsageTotalsFromInput(input);
17
+ }
18
+ if (normalized === "claude") {
19
+ return getClaudeUsageTotalsFromInput(input);
20
+ }
21
+ return (
22
+ getCodexUsageTotalsFromInput(input) ||
23
+ getClaudeUsageTotalsFromInput(input)
24
+ );
25
+ }
26
+
27
+ export function normalizeInputUsage(
28
+ inputUsage: StatuslineInputUsage | null
29
+ ): StatuslineUsage | null {
30
+ if (!inputUsage) return null;
31
+ const usage: StatuslineUsage = {
32
+ todayTokens: coerceNumber(inputUsage.todayTokens),
33
+ totalTokens: coerceNumber(inputUsage.totalTokens),
34
+ inputTokens: coerceNumber(inputUsage.inputTokens),
35
+ outputTokens: coerceNumber(inputUsage.outputTokens),
36
+ cacheReadTokens: coerceNumber(inputUsage.cacheReadTokens),
37
+ cacheWriteTokens: coerceNumber(inputUsage.cacheWriteTokens),
38
+ };
39
+ const hasUsage =
40
+ usage.todayTokens !== null ||
41
+ usage.totalTokens !== null ||
42
+ usage.inputTokens !== null ||
43
+ usage.outputTokens !== null ||
44
+ usage.cacheReadTokens !== null ||
45
+ usage.cacheWriteTokens !== null;
46
+ return hasUsage ? usage : null;
47
+ }
48
+
49
+ export function resolveUsageFromRecords(
50
+ config: Config,
51
+ configPath: string | null,
52
+ type: string | null,
53
+ profileKey: string | null,
54
+ profileName: string | null,
55
+ syncUsage: boolean
56
+ ): StatuslineUsage | null {
57
+ try {
58
+ const normalized = normalizeType(type || "");
59
+ if (!normalized || (!profileKey && !profileName)) return null;
60
+ const totals = readUsageTotalsIndex(config, configPath, syncUsage);
61
+ if (!totals) return null;
62
+ const usage = resolveUsageTotalsForProfile(
63
+ totals,
64
+ normalized,
65
+ profileKey,
66
+ profileName
67
+ );
68
+ if (!usage) return null;
69
+ return {
70
+ todayTokens: usage.today,
71
+ totalTokens: usage.total,
72
+ inputTokens: usage.todayInput ?? null,
73
+ outputTokens: usage.todayOutput ?? null,
74
+ cacheReadTokens: usage.todayCacheRead ?? null,
75
+ cacheWriteTokens: usage.todayCacheWrite ?? null,
76
+ };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
@@ -0,0 +1,27 @@
1
+ export function isRecord(value: unknown): value is Record<string, unknown> {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+
5
+ export function firstNonEmpty(...values: Array<string | null | undefined>): string | null {
6
+ for (const value of values) {
7
+ if (value === null || value === undefined) continue;
8
+ const text = String(value).trim();
9
+ if (text) return text;
10
+ }
11
+ return null;
12
+ }
13
+
14
+ export function coerceNumber(value: unknown): number | null {
15
+ if (value === null || value === undefined || value === "") return null;
16
+ const num = Number(value);
17
+ if (!Number.isFinite(num)) return null;
18
+ return num;
19
+ }
20
+
21
+ export function firstNumber(...values: Array<unknown>): number | null {
22
+ for (const value of values) {
23
+ const num = coerceNumber(value);
24
+ if (num !== null) return num;
25
+ }
26
+ return null;
27
+ }
package/src/types.ts CHANGED
@@ -15,6 +15,7 @@ export interface Profile {
15
15
  env?: Record<string, EnvValue>;
16
16
  removeFiles?: string[];
17
17
  commands?: string[];
18
+ pricing?: ProfilePricing;
18
19
  }
19
20
 
20
21
  export interface CodexStatuslineConfig {
@@ -43,6 +44,24 @@ export interface Config {
43
44
  claudeSessionsPath?: string;
44
45
  codexStatusline?: CodexStatuslineConfig;
45
46
  claudeStatusline?: ClaudeStatuslineConfig;
47
+ pricing?: PricingConfig;
48
+ }
49
+
50
+ export interface TokenPricing {
51
+ input?: number;
52
+ output?: number;
53
+ cacheRead?: number;
54
+ cacheWrite?: number;
55
+ description?: string;
56
+ }
57
+
58
+ export interface PricingConfig {
59
+ models?: Record<string, TokenPricing>;
60
+ }
61
+
62
+ export interface ProfilePricing extends TokenPricing {
63
+ model?: string;
64
+ multiplier?: number;
46
65
  }
47
66
 
48
67
  export interface ListRow {
@@ -54,6 +73,10 @@ export interface ListRow {
54
73
  usageType?: ProfileType | null;
55
74
  todayTokens?: number;
56
75
  totalTokens?: number;
76
+ todayCost?: number;
77
+ totalCost?: number;
78
+ todayBilledTokens?: number;
79
+ totalBilledTokens?: number;
57
80
  }
58
81
 
59
82
  export interface ParsedArgs {