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