@praeviso/code-env-switch 0.1.4 → 0.1.6

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