@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.
- package/README.md +17 -0
- package/README_zh.md +17 -0
- package/bin/commands/list.js +44 -2
- package/bin/statusline/debug.js +42 -0
- package/bin/statusline/format.js +57 -0
- package/bin/statusline/git.js +96 -0
- package/bin/statusline/index.js +107 -558
- package/bin/statusline/input.js +167 -0
- package/bin/statusline/style.js +22 -0
- package/bin/statusline/types.js +2 -0
- package/bin/statusline/usage/claude.js +181 -0
- package/bin/statusline/usage/codex.js +168 -0
- package/bin/statusline/usage.js +67 -0
- package/bin/statusline/utils.js +35 -0
- package/bin/usage/index.js +396 -41
- package/bin/usage/pricing.js +303 -0
- package/code-env.example.json +55 -0
- package/package.json +1 -1
- package/src/commands/list.ts +74 -4
- package/src/statusline/debug.ts +40 -0
- package/src/statusline/format.ts +67 -0
- package/src/statusline/git.ts +82 -0
- package/src/statusline/index.ts +143 -764
- package/src/statusline/input.ts +159 -0
- package/src/statusline/style.ts +19 -0
- package/src/statusline/types.ts +111 -0
- package/src/statusline/usage/claude.ts +299 -0
- package/src/statusline/usage/codex.ts +263 -0
- package/src/statusline/usage.ts +80 -0
- package/src/statusline/utils.ts +27 -0
- package/src/types.ts +23 -0
- package/src/usage/index.ts +519 -35
- package/src/usage/pricing.ts +323 -0
- package/PLAN.md +0 -33
|
@@ -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 {
|