@praeviso/code-env-switch 0.1.3 → 0.1.4
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/bin/statusline/debug.js +42 -0
- package/bin/statusline/format.js +60 -0
- package/bin/statusline/git.js +96 -0
- package/bin/statusline/index.js +67 -556
- package/bin/statusline/input.js +249 -0
- package/bin/statusline/style.js +22 -0
- package/bin/statusline/types.js +2 -0
- package/bin/statusline/usage.js +123 -0
- package/bin/statusline/utils.js +35 -0
- package/package.json +1 -1
- package/src/statusline/debug.ts +40 -0
- package/src/statusline/format.ts +68 -0
- package/src/statusline/git.ts +82 -0
- package/src/statusline/index.ts +72 -743
- package/src/statusline/input.ts +300 -0
- package/src/statusline/style.ts +19 -0
- package/src/statusline/types.ts +105 -0
- package/src/statusline/usage.ts +175 -0
- package/src/statusline/utils.ts +27 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { DEFAULT_PROFILE_TYPES } from "../constants";
|
|
3
|
+
import { normalizeType } from "../profile/type";
|
|
4
|
+
import type {
|
|
5
|
+
GitStatus,
|
|
6
|
+
StatuslineInput,
|
|
7
|
+
StatuslineInputProfile,
|
|
8
|
+
StatuslineInputUsage,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { coerceNumber, firstNonEmpty, firstNumber, isRecord } from "./utils";
|
|
11
|
+
|
|
12
|
+
export function readStdinJson(): StatuslineInput | null {
|
|
13
|
+
if (process.stdin.isTTY) return null;
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
if (!trimmed) return null;
|
|
18
|
+
const parsed = JSON.parse(trimmed);
|
|
19
|
+
if (!isRecord(parsed)) return null;
|
|
20
|
+
return parsed as StatuslineInput;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeTypeValue(value: string | null): string | null {
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
const normalized = normalizeType(value);
|
|
29
|
+
if (normalized) return normalized;
|
|
30
|
+
const trimmed = String(value).trim();
|
|
31
|
+
return trimmed ? trimmed : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function detectTypeFromEnv(): string | null {
|
|
35
|
+
const matches = DEFAULT_PROFILE_TYPES.filter((type) => {
|
|
36
|
+
const suffix = type.toUpperCase();
|
|
37
|
+
return (
|
|
38
|
+
process.env[`CODE_ENV_PROFILE_KEY_${suffix}`] ||
|
|
39
|
+
process.env[`CODE_ENV_PROFILE_NAME_${suffix}`]
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
if (matches.length === 1) return matches[0];
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveEnvProfile(
|
|
47
|
+
type: string | null
|
|
48
|
+
): { key: string | null; name: string | null } {
|
|
49
|
+
const genericKey = process.env.CODE_ENV_PROFILE_KEY || null;
|
|
50
|
+
const genericName = process.env.CODE_ENV_PROFILE_NAME || null;
|
|
51
|
+
if (!type) {
|
|
52
|
+
return { key: genericKey, name: genericName };
|
|
53
|
+
}
|
|
54
|
+
const suffix = type.toUpperCase();
|
|
55
|
+
const key = process.env[`CODE_ENV_PROFILE_KEY_${suffix}`] || genericKey;
|
|
56
|
+
const name = process.env[`CODE_ENV_PROFILE_NAME_${suffix}`] || genericName;
|
|
57
|
+
return { key: key || null, name: name || null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getModelFromInput(input: StatuslineInput | null): string | null {
|
|
61
|
+
if (!input) return null;
|
|
62
|
+
const raw = input.model;
|
|
63
|
+
if (!raw) return null;
|
|
64
|
+
if (typeof raw === "string") return raw;
|
|
65
|
+
if (isRecord(raw)) {
|
|
66
|
+
const displayName = raw.displayName || raw.display_name;
|
|
67
|
+
if (displayName) return String(displayName);
|
|
68
|
+
if (raw.id) return String(raw.id);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getModelProviderFromInput(input: StatuslineInput | null): string | null {
|
|
74
|
+
if (!input || !input.model_provider) return null;
|
|
75
|
+
const provider = String(input.model_provider).trim();
|
|
76
|
+
return provider ? provider : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getInputProfile(
|
|
80
|
+
input: StatuslineInput | null
|
|
81
|
+
): StatuslineInputProfile | null {
|
|
82
|
+
if (!input || !isRecord(input.profile)) return null;
|
|
83
|
+
return input.profile as StatuslineInputProfile;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getInputUsage(input: StatuslineInput | null): StatuslineInputUsage | null {
|
|
87
|
+
if (!input) return null;
|
|
88
|
+
if (isRecord(input.usage)) {
|
|
89
|
+
return input.usage as StatuslineInputUsage;
|
|
90
|
+
}
|
|
91
|
+
const tokenUsage = input.token_usage;
|
|
92
|
+
if (tokenUsage !== null && tokenUsage !== undefined) {
|
|
93
|
+
if (typeof tokenUsage === "number") {
|
|
94
|
+
return {
|
|
95
|
+
todayTokens: null,
|
|
96
|
+
totalTokens: coerceNumber(tokenUsage),
|
|
97
|
+
inputTokens: null,
|
|
98
|
+
outputTokens: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (isRecord(tokenUsage)) {
|
|
102
|
+
const record = tokenUsage as Record<string, unknown>;
|
|
103
|
+
const todayTokens =
|
|
104
|
+
firstNumber(
|
|
105
|
+
record.todayTokens,
|
|
106
|
+
record.today,
|
|
107
|
+
record.today_tokens,
|
|
108
|
+
record.daily,
|
|
109
|
+
record.daily_tokens
|
|
110
|
+
) ?? null;
|
|
111
|
+
const totalTokens =
|
|
112
|
+
firstNumber(
|
|
113
|
+
record.totalTokens,
|
|
114
|
+
record.total,
|
|
115
|
+
record.total_tokens
|
|
116
|
+
) ?? null;
|
|
117
|
+
const inputTokens =
|
|
118
|
+
firstNumber(
|
|
119
|
+
record.inputTokens,
|
|
120
|
+
record.input,
|
|
121
|
+
record.input_tokens
|
|
122
|
+
) ?? null;
|
|
123
|
+
const outputTokens =
|
|
124
|
+
firstNumber(
|
|
125
|
+
record.outputTokens,
|
|
126
|
+
record.output,
|
|
127
|
+
record.output_tokens
|
|
128
|
+
) ?? null;
|
|
129
|
+
const cacheRead =
|
|
130
|
+
firstNumber(
|
|
131
|
+
record.cache_read_input_tokens,
|
|
132
|
+
record.cacheReadInputTokens,
|
|
133
|
+
record.cache_read,
|
|
134
|
+
record.cacheRead
|
|
135
|
+
) ?? null;
|
|
136
|
+
const cacheWrite =
|
|
137
|
+
firstNumber(
|
|
138
|
+
record.cache_creation_input_tokens,
|
|
139
|
+
record.cacheCreationInputTokens,
|
|
140
|
+
record.cache_write_input_tokens,
|
|
141
|
+
record.cacheWriteInputTokens,
|
|
142
|
+
record.cache_write,
|
|
143
|
+
record.cacheWrite
|
|
144
|
+
) ?? null;
|
|
145
|
+
if (
|
|
146
|
+
todayTokens === null &&
|
|
147
|
+
totalTokens === null &&
|
|
148
|
+
inputTokens === null &&
|
|
149
|
+
outputTokens === null &&
|
|
150
|
+
cacheRead === null &&
|
|
151
|
+
cacheWrite === null
|
|
152
|
+
) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const hasCacheTokens = cacheRead !== null || cacheWrite !== null;
|
|
156
|
+
const computedTotal = hasCacheTokens
|
|
157
|
+
? (inputTokens || 0) +
|
|
158
|
+
(outputTokens || 0) +
|
|
159
|
+
(cacheRead || 0) +
|
|
160
|
+
(cacheWrite || 0)
|
|
161
|
+
: null;
|
|
162
|
+
const resolvedTodayTokens = hasCacheTokens
|
|
163
|
+
? todayTokens ?? totalTokens ?? computedTotal
|
|
164
|
+
: todayTokens;
|
|
165
|
+
return {
|
|
166
|
+
todayTokens: resolvedTodayTokens,
|
|
167
|
+
totalTokens: totalTokens ?? null,
|
|
168
|
+
inputTokens,
|
|
169
|
+
outputTokens,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const contextWindow = isRecord(input.context_window)
|
|
174
|
+
? (input.context_window as Record<string, unknown>)
|
|
175
|
+
: isRecord(input.contextWindow)
|
|
176
|
+
? (input.contextWindow as Record<string, unknown>)
|
|
177
|
+
: null;
|
|
178
|
+
if (!contextWindow) return null;
|
|
179
|
+
const totalInputTokens =
|
|
180
|
+
firstNumber(
|
|
181
|
+
contextWindow.total_input_tokens,
|
|
182
|
+
contextWindow.totalInputTokens
|
|
183
|
+
) ?? null;
|
|
184
|
+
const totalOutputTokens =
|
|
185
|
+
firstNumber(
|
|
186
|
+
contextWindow.total_output_tokens,
|
|
187
|
+
contextWindow.totalOutputTokens
|
|
188
|
+
) ?? null;
|
|
189
|
+
if (totalInputTokens !== null || totalOutputTokens !== null) {
|
|
190
|
+
return {
|
|
191
|
+
todayTokens: null,
|
|
192
|
+
totalTokens: null,
|
|
193
|
+
inputTokens: totalInputTokens,
|
|
194
|
+
outputTokens: totalOutputTokens,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const currentUsage = isRecord(contextWindow.current_usage)
|
|
198
|
+
? (contextWindow.current_usage as Record<string, unknown>)
|
|
199
|
+
: isRecord(contextWindow.currentUsage)
|
|
200
|
+
? (contextWindow.currentUsage as Record<string, unknown>)
|
|
201
|
+
: null;
|
|
202
|
+
if (!currentUsage) return null;
|
|
203
|
+
const inputTokens =
|
|
204
|
+
firstNumber(
|
|
205
|
+
currentUsage.input_tokens,
|
|
206
|
+
currentUsage.inputTokens
|
|
207
|
+
) ?? null;
|
|
208
|
+
const outputTokens =
|
|
209
|
+
firstNumber(
|
|
210
|
+
currentUsage.output_tokens,
|
|
211
|
+
currentUsage.outputTokens
|
|
212
|
+
) ?? null;
|
|
213
|
+
const cacheRead =
|
|
214
|
+
firstNumber(
|
|
215
|
+
currentUsage.cache_read_input_tokens,
|
|
216
|
+
currentUsage.cacheReadInputTokens
|
|
217
|
+
) ?? null;
|
|
218
|
+
const cacheWrite =
|
|
219
|
+
firstNumber(
|
|
220
|
+
currentUsage.cache_creation_input_tokens,
|
|
221
|
+
currentUsage.cacheCreationInputTokens
|
|
222
|
+
) ?? null;
|
|
223
|
+
if (
|
|
224
|
+
inputTokens === null &&
|
|
225
|
+
outputTokens === null &&
|
|
226
|
+
cacheRead === null &&
|
|
227
|
+
cacheWrite === null
|
|
228
|
+
) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const totalTokens =
|
|
232
|
+
(inputTokens || 0) +
|
|
233
|
+
(outputTokens || 0) +
|
|
234
|
+
(cacheRead || 0) +
|
|
235
|
+
(cacheWrite || 0);
|
|
236
|
+
return {
|
|
237
|
+
todayTokens: totalTokens,
|
|
238
|
+
totalTokens: null,
|
|
239
|
+
inputTokens,
|
|
240
|
+
outputTokens,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function getSessionId(input: StatuslineInput | null): string | null {
|
|
245
|
+
if (!input) return null;
|
|
246
|
+
return firstNonEmpty(input.session_id, input.sessionId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function getContextUsedTokens(input: StatuslineInput | null): number | null {
|
|
250
|
+
if (!input) return null;
|
|
251
|
+
return coerceNumber(input.context_window_used_tokens);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function getContextLeftPercent(
|
|
255
|
+
input: StatuslineInput | null,
|
|
256
|
+
type: string | null
|
|
257
|
+
): number | null {
|
|
258
|
+
if (!input) return null;
|
|
259
|
+
const raw = coerceNumber(input.context_window_percent);
|
|
260
|
+
if (raw === null || raw < 0) return null;
|
|
261
|
+
const percent = raw <= 1 ? raw * 100 : raw;
|
|
262
|
+
if (percent > 100) return null;
|
|
263
|
+
const usedTokens = getContextUsedTokens(input);
|
|
264
|
+
const normalizedType = normalizeTypeValue(type);
|
|
265
|
+
const preferRemaining =
|
|
266
|
+
normalizedType === "codex" ||
|
|
267
|
+
normalizedType === "claude" ||
|
|
268
|
+
usedTokens === null ||
|
|
269
|
+
(usedTokens <= 0 && percent >= 99);
|
|
270
|
+
const left = preferRemaining ? percent : 100 - percent;
|
|
271
|
+
return Math.max(0, Math.min(100, left));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getWorkspaceDir(input: StatuslineInput | null): string | null {
|
|
275
|
+
if (!input || !isRecord(input.workspace)) return null;
|
|
276
|
+
const currentDir = input.workspace.current_dir;
|
|
277
|
+
if (currentDir) {
|
|
278
|
+
const trimmed = String(currentDir).trim();
|
|
279
|
+
if (trimmed) return trimmed;
|
|
280
|
+
}
|
|
281
|
+
const projectDir = input.workspace.project_dir;
|
|
282
|
+
if (!projectDir) return null;
|
|
283
|
+
const trimmed = String(projectDir).trim();
|
|
284
|
+
return trimmed ? trimmed : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function getGitStatusFromInput(input: StatuslineInput | null): GitStatus | null {
|
|
288
|
+
if (!input || !input.git_branch) return null;
|
|
289
|
+
const branch = String(input.git_branch).trim();
|
|
290
|
+
if (!branch) return null;
|
|
291
|
+
return {
|
|
292
|
+
branch,
|
|
293
|
+
ahead: 0,
|
|
294
|
+
behind: 0,
|
|
295
|
+
staged: 0,
|
|
296
|
+
unstaged: 0,
|
|
297
|
+
untracked: 0,
|
|
298
|
+
conflicted: 0,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const COLOR_ENABLED = !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
2
|
+
const ANSI_RESET = "\x1b[0m";
|
|
3
|
+
|
|
4
|
+
export const ICON_GIT = "⎇";
|
|
5
|
+
export const ICON_PROFILE = "👤";
|
|
6
|
+
export const ICON_MODEL = "⚙";
|
|
7
|
+
export const ICON_USAGE = "⚡";
|
|
8
|
+
export const ICON_CONTEXT = "🧠";
|
|
9
|
+
export const ICON_REVIEW = "📝";
|
|
10
|
+
export const ICON_CWD = "📁";
|
|
11
|
+
|
|
12
|
+
export function colorize(text: string, colorCode: string): string {
|
|
13
|
+
if (!COLOR_ENABLED) return text;
|
|
14
|
+
return `\x1b[${colorCode}m${text}${ANSI_RESET}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function dim(text: string): string {
|
|
18
|
+
return colorize(text, "2");
|
|
19
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export interface StatuslineInputProfile {
|
|
2
|
+
key?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface StatuslineInputUsage {
|
|
8
|
+
todayTokens?: number;
|
|
9
|
+
totalTokens?: number;
|
|
10
|
+
inputTokens?: number;
|
|
11
|
+
outputTokens?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StatuslineInputContextWindowUsage {
|
|
15
|
+
input_tokens?: number;
|
|
16
|
+
output_tokens?: number;
|
|
17
|
+
cache_creation_input_tokens?: number;
|
|
18
|
+
cache_read_input_tokens?: number;
|
|
19
|
+
inputTokens?: number;
|
|
20
|
+
outputTokens?: number;
|
|
21
|
+
cacheCreationInputTokens?: number;
|
|
22
|
+
cacheReadInputTokens?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StatuslineInputContextWindow {
|
|
26
|
+
current_usage?: StatuslineInputContextWindowUsage | null;
|
|
27
|
+
total_input_tokens?: number;
|
|
28
|
+
total_output_tokens?: number;
|
|
29
|
+
context_window_size?: number;
|
|
30
|
+
currentUsage?: StatuslineInputContextWindowUsage | null;
|
|
31
|
+
totalInputTokens?: number;
|
|
32
|
+
totalOutputTokens?: number;
|
|
33
|
+
contextWindowSize?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StatuslineInputModel {
|
|
37
|
+
id?: string;
|
|
38
|
+
displayName?: string;
|
|
39
|
+
display_name?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StatuslineInput {
|
|
43
|
+
cwd?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
profile?: StatuslineInputProfile;
|
|
46
|
+
model?: string | StatuslineInputModel;
|
|
47
|
+
model_provider?: string;
|
|
48
|
+
usage?: StatuslineInputUsage;
|
|
49
|
+
token_usage?: StatuslineInputUsage | number | Record<string, unknown>;
|
|
50
|
+
git_branch?: string;
|
|
51
|
+
task_running?: boolean;
|
|
52
|
+
review_mode?: boolean;
|
|
53
|
+
context_window_percent?: number;
|
|
54
|
+
context_window_used_tokens?: number;
|
|
55
|
+
context_window?: StatuslineInputContextWindow | Record<string, unknown> | null;
|
|
56
|
+
contextWindow?: StatuslineInputContextWindow | Record<string, unknown> | null;
|
|
57
|
+
workspace?: {
|
|
58
|
+
current_dir?: string;
|
|
59
|
+
project_dir?: string;
|
|
60
|
+
};
|
|
61
|
+
cost?: Record<string, unknown>;
|
|
62
|
+
version?: string;
|
|
63
|
+
output_style?: { name?: string };
|
|
64
|
+
session_id?: string;
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
transcript_path?: string;
|
|
67
|
+
hook_event_name?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StatuslineUsage {
|
|
71
|
+
todayTokens: number | null;
|
|
72
|
+
totalTokens: number | null;
|
|
73
|
+
inputTokens: number | null;
|
|
74
|
+
outputTokens: number | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StatuslineUsageTotals {
|
|
78
|
+
inputTokens: number | null;
|
|
79
|
+
outputTokens: number | null;
|
|
80
|
+
totalTokens: number | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GitStatus {
|
|
84
|
+
branch: string | null;
|
|
85
|
+
ahead: number;
|
|
86
|
+
behind: number;
|
|
87
|
+
staged: number;
|
|
88
|
+
unstaged: number;
|
|
89
|
+
untracked: number;
|
|
90
|
+
conflicted: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface StatuslineJson {
|
|
94
|
+
cwd: string;
|
|
95
|
+
type: string | null;
|
|
96
|
+
profile: { key: string | null; name: string | null };
|
|
97
|
+
model: string | null;
|
|
98
|
+
usage: StatuslineUsage | null;
|
|
99
|
+
git: GitStatus | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface StatuslineResult {
|
|
103
|
+
text: string;
|
|
104
|
+
json: StatuslineJson;
|
|
105
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
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, 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
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getUsageTotalsFromInput(
|
|
73
|
+
input: StatuslineInput | null
|
|
74
|
+
): StatuslineUsageTotals | null {
|
|
75
|
+
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);
|
|
121
|
+
}
|
|
122
|
+
if (isRecord(input.usage)) {
|
|
123
|
+
return parseUsageTotalsRecord(input.usage as Record<string, unknown>);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeInputUsage(
|
|
129
|
+
inputUsage: StatuslineInputUsage | null
|
|
130
|
+
): StatuslineUsage | null {
|
|
131
|
+
if (!inputUsage) return null;
|
|
132
|
+
const usage: StatuslineUsage = {
|
|
133
|
+
todayTokens: coerceNumber(inputUsage.todayTokens),
|
|
134
|
+
totalTokens: coerceNumber(inputUsage.totalTokens),
|
|
135
|
+
inputTokens: coerceNumber(inputUsage.inputTokens),
|
|
136
|
+
outputTokens: coerceNumber(inputUsage.outputTokens),
|
|
137
|
+
};
|
|
138
|
+
const hasUsage =
|
|
139
|
+
usage.todayTokens !== null ||
|
|
140
|
+
usage.totalTokens !== null ||
|
|
141
|
+
usage.inputTokens !== null ||
|
|
142
|
+
usage.outputTokens !== null;
|
|
143
|
+
return hasUsage ? usage : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function resolveUsageFromRecords(
|
|
147
|
+
config: Config,
|
|
148
|
+
configPath: string | null,
|
|
149
|
+
type: string | null,
|
|
150
|
+
profileKey: string | null,
|
|
151
|
+
profileName: string | null,
|
|
152
|
+
syncUsage: boolean
|
|
153
|
+
): StatuslineUsage | null {
|
|
154
|
+
try {
|
|
155
|
+
const normalized = normalizeType(type || "");
|
|
156
|
+
if (!normalized || (!profileKey && !profileName)) return null;
|
|
157
|
+
const totals = readUsageTotalsIndex(config, configPath, syncUsage);
|
|
158
|
+
if (!totals) return null;
|
|
159
|
+
const usage = resolveUsageTotalsForProfile(
|
|
160
|
+
totals,
|
|
161
|
+
normalized,
|
|
162
|
+
profileKey,
|
|
163
|
+
profileName
|
|
164
|
+
);
|
|
165
|
+
if (!usage) return null;
|
|
166
|
+
return {
|
|
167
|
+
todayTokens: usage.today,
|
|
168
|
+
totalTokens: usage.total,
|
|
169
|
+
inputTokens: null,
|
|
170
|
+
outputTokens: null,
|
|
171
|
+
};
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -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
|
+
}
|