@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,323 @@
1
+ import type { Config, Profile, TokenPricing } from "../types";
2
+
3
+ const TOKENS_PER_MILLION = 1_000_000;
4
+
5
+ export const DEFAULT_MODEL_PRICING: Record<string, TokenPricing> = {
6
+ "Claude Sonnet 4.5": {
7
+ input: 3.0,
8
+ output: 15.0,
9
+ cacheWrite: 3.75,
10
+ cacheRead: 0.3,
11
+ description: "Balanced performance and speed for daily use.",
12
+ },
13
+ "Sonnet 4.5": {
14
+ input: 3.0,
15
+ output: 15.0,
16
+ cacheWrite: 3.75,
17
+ cacheRead: 0.3,
18
+ description: "Balanced performance and speed for daily use.",
19
+ },
20
+ "Claude Opus 4.5": {
21
+ input: 5.0,
22
+ output: 25.0,
23
+ cacheWrite: 6.25,
24
+ cacheRead: 0.5,
25
+ description: "Most capable model for agents and coding.",
26
+ },
27
+ "Opus 4.5": {
28
+ input: 5.0,
29
+ output: 25.0,
30
+ cacheWrite: 6.25,
31
+ cacheRead: 0.5,
32
+ description: "Most capable model for agents and coding.",
33
+ },
34
+ "Claude Haiku 4.5": {
35
+ input: 1.0,
36
+ output: 5.0,
37
+ cacheWrite: 1.25,
38
+ cacheRead: 0.1,
39
+ description: "Fast responses for lightweight tasks.",
40
+ },
41
+ "Haiku 4.5": {
42
+ input: 1.0,
43
+ output: 5.0,
44
+ cacheWrite: 1.25,
45
+ cacheRead: 0.1,
46
+ description: "Fast responses for lightweight tasks.",
47
+ },
48
+ "claude-opus-4-5-20251101": {
49
+ input: 5.0,
50
+ output: 25.0,
51
+ cacheWrite: 6.25,
52
+ cacheRead: 0.5,
53
+ description: "Most capable model for agents and coding.",
54
+ },
55
+ "claude-sonnet-4-5-20251022": {
56
+ input: 3.0,
57
+ output: 15.0,
58
+ cacheWrite: 3.75,
59
+ cacheRead: 0.3,
60
+ description: "Balanced performance and speed for daily use.",
61
+ },
62
+ "claude-haiku-4-5-20251022": {
63
+ input: 1.0,
64
+ output: 5.0,
65
+ cacheWrite: 1.25,
66
+ cacheRead: 0.1,
67
+ description: "Fast responses for lightweight tasks.",
68
+ },
69
+ "gpt-5.1": {
70
+ input: 1.25,
71
+ output: 10.0,
72
+ cacheRead: 0.125,
73
+ description: "Base model for daily development work.",
74
+ },
75
+ "gpt-5.1-codex": {
76
+ input: 1.25,
77
+ output: 10.0,
78
+ cacheRead: 0.125,
79
+ description: "Code-focused model for programming workflows.",
80
+ },
81
+ "gpt-5.1-codex-max": {
82
+ input: 1.25,
83
+ output: 10.0,
84
+ cacheRead: 0.125,
85
+ description: "Flagship code model for complex projects.",
86
+ },
87
+ "gpt-5.2": {
88
+ input: 1.75,
89
+ output: 14.0,
90
+ cacheRead: 0.175,
91
+ description: "Latest flagship model with improved performance.",
92
+ },
93
+ "gpt-5.2-codex": {
94
+ input: 1.75,
95
+ output: 14.0,
96
+ cacheRead: 0.175,
97
+ description: "Latest flagship code model.",
98
+ },
99
+ };
100
+
101
+ export interface UsageTokenBreakdown {
102
+ inputTokens?: number | null;
103
+ outputTokens?: number | null;
104
+ cacheReadTokens?: number | null;
105
+ cacheWriteTokens?: number | null;
106
+ todayTokens?: number | null;
107
+ totalTokens?: number | null;
108
+ }
109
+
110
+ function normalizeModelKey(value: string): string {
111
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
112
+ }
113
+
114
+ function parsePriceValue(value: unknown): number | null {
115
+ if (value === null || value === undefined) return null;
116
+ if (typeof value === "number" && Number.isFinite(value)) return value;
117
+ if (typeof value === "string") {
118
+ const match = value.replace(/,/g, "").match(/-?\d+(?:\.\d+)?/);
119
+ if (match) {
120
+ const parsed = Number(match[0]);
121
+ return Number.isFinite(parsed) ? parsed : null;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ function compactPricing(value: TokenPricing | null | undefined): TokenPricing | null {
128
+ if (!value || typeof value !== "object") return null;
129
+ const input = parsePriceValue(value.input);
130
+ const output = parsePriceValue(value.output);
131
+ const cacheRead = parsePriceValue(value.cacheRead);
132
+ const cacheWrite = parsePriceValue(value.cacheWrite);
133
+ const description =
134
+ typeof value.description === "string" && value.description.trim()
135
+ ? value.description.trim()
136
+ : undefined;
137
+ const pricing: TokenPricing = {};
138
+ let hasNumber = false;
139
+ if (input !== null) {
140
+ pricing.input = input;
141
+ hasNumber = true;
142
+ }
143
+ if (output !== null) {
144
+ pricing.output = output;
145
+ hasNumber = true;
146
+ }
147
+ if (cacheRead !== null) {
148
+ pricing.cacheRead = cacheRead;
149
+ hasNumber = true;
150
+ }
151
+ if (cacheWrite !== null) {
152
+ pricing.cacheWrite = cacheWrite;
153
+ hasNumber = true;
154
+ }
155
+ if (description) pricing.description = description;
156
+ return hasNumber ? pricing : null;
157
+ }
158
+
159
+ function resolveMultiplier(value: unknown): number | null {
160
+ const parsed = parsePriceValue(value);
161
+ if (parsed === null) return null;
162
+ if (parsed < 0) return null;
163
+ return parsed;
164
+ }
165
+
166
+ function applyMultiplier(
167
+ pricing: TokenPricing | null,
168
+ multiplier: number | null
169
+ ): TokenPricing | null {
170
+ if (!pricing) return null;
171
+ if (multiplier === null) return pricing;
172
+ const scaled: TokenPricing = {};
173
+ if (pricing.input !== undefined) scaled.input = pricing.input * multiplier;
174
+ if (pricing.output !== undefined) scaled.output = pricing.output * multiplier;
175
+ if (pricing.cacheRead !== undefined) scaled.cacheRead = pricing.cacheRead * multiplier;
176
+ if (pricing.cacheWrite !== undefined) scaled.cacheWrite = pricing.cacheWrite * multiplier;
177
+ if (pricing.description) scaled.description = pricing.description;
178
+ return scaled;
179
+ }
180
+
181
+ function buildModelIndex(
182
+ models: Record<string, TokenPricing> | undefined
183
+ ): Map<string, { model: string; pricing: TokenPricing }> {
184
+ const index = new Map<string, { model: string; pricing: TokenPricing }>();
185
+ if (!models) return index;
186
+ for (const [model, pricing] of Object.entries(models)) {
187
+ const key = normalizeModelKey(model);
188
+ if (!key) continue;
189
+ const cleaned = compactPricing(pricing);
190
+ if (!cleaned) continue;
191
+ index.set(key, { model, pricing: cleaned });
192
+ }
193
+ return index;
194
+ }
195
+
196
+ function resolveModelPricing(
197
+ config: Config,
198
+ model: string | null
199
+ ): { model: string; pricing: TokenPricing } | null {
200
+ if (!model) return null;
201
+ const key = normalizeModelKey(model);
202
+ if (!key) return null;
203
+ const configIndex = buildModelIndex(config.pricing?.models);
204
+ const fromConfig = configIndex.get(key);
205
+ if (fromConfig) return fromConfig;
206
+ const defaultsIndex = buildModelIndex(DEFAULT_MODEL_PRICING);
207
+ return defaultsIndex.get(key) || null;
208
+ }
209
+
210
+ function mergePricing(
211
+ base: TokenPricing | null,
212
+ override: TokenPricing | null
213
+ ): TokenPricing | null {
214
+ if (!base && !override) return null;
215
+ return compactPricing({ ...(base || {}), ...(override || {}) });
216
+ }
217
+
218
+ function getProfilePricing(profile: Profile | null): {
219
+ model: string | null;
220
+ pricing: TokenPricing | null;
221
+ multiplier: unknown;
222
+ } {
223
+ if (!profile || !profile.pricing || typeof profile.pricing !== "object") {
224
+ return { model: null, pricing: null, multiplier: null };
225
+ }
226
+ const raw = profile.pricing as TokenPricing & {
227
+ model?: unknown;
228
+ multiplier?: unknown;
229
+ };
230
+ const model =
231
+ typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : null;
232
+ return {
233
+ model,
234
+ pricing: compactPricing(raw),
235
+ multiplier: raw.multiplier ?? null,
236
+ };
237
+ }
238
+
239
+ export function resolvePricingForProfile(
240
+ config: Config,
241
+ profile: Profile | null,
242
+ model: string | null
243
+ ): TokenPricing | null {
244
+ const profilePricing = getProfilePricing(profile);
245
+ const baseFromProfileModel = profilePricing.model
246
+ ? resolveModelPricing(config, profilePricing.model)
247
+ : null;
248
+ const mergedProfile = mergePricing(
249
+ baseFromProfileModel ? baseFromProfileModel.pricing : null,
250
+ profilePricing.pricing
251
+ );
252
+ const resolvedPricing =
253
+ mergedProfile ||
254
+ (baseFromProfileModel ? baseFromProfileModel.pricing : null) ||
255
+ (resolveModelPricing(config, model)?.pricing ?? null);
256
+ if (!resolvedPricing) return null;
257
+ const multiplier = resolveMultiplier(profilePricing.multiplier);
258
+ return applyMultiplier(resolvedPricing, multiplier);
259
+ }
260
+
261
+ function toFiniteNumber(value: number | null | undefined): number | null {
262
+ if (value === null || value === undefined) return null;
263
+ const num = Number(value);
264
+ if (!Number.isFinite(num)) return null;
265
+ return num;
266
+ }
267
+
268
+ export function calculateUsageCost(
269
+ usage: UsageTokenBreakdown | null,
270
+ pricing: TokenPricing | null
271
+ ): number | null {
272
+ if (!usage || !pricing) return null;
273
+ const inputTokens = toFiniteNumber(usage.inputTokens);
274
+ const outputTokens = toFiniteNumber(usage.outputTokens);
275
+ const cacheReadTokens = toFiniteNumber(usage.cacheReadTokens);
276
+ const cacheWriteTokens = toFiniteNumber(usage.cacheWriteTokens);
277
+ if (
278
+ inputTokens === null &&
279
+ outputTokens === null &&
280
+ cacheReadTokens === null &&
281
+ cacheWriteTokens === null
282
+ ) {
283
+ return null;
284
+ }
285
+ const tokens = {
286
+ input: Math.max(0, inputTokens || 0),
287
+ output: Math.max(0, outputTokens || 0),
288
+ cacheRead: Math.max(0, cacheReadTokens || 0),
289
+ cacheWrite: Math.max(0, cacheWriteTokens || 0),
290
+ };
291
+ const knownTotal = toFiniteNumber(
292
+ usage.todayTokens ?? usage.totalTokens ?? null
293
+ );
294
+ const breakdownTotal =
295
+ tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite;
296
+ if (breakdownTotal === 0 && knownTotal !== null && knownTotal > 0) {
297
+ return null;
298
+ }
299
+ if (tokens.input > 0 && pricing.input === undefined) return null;
300
+ if (tokens.output > 0 && pricing.output === undefined) return null;
301
+ if (tokens.cacheRead > 0 && pricing.cacheRead === undefined) return null;
302
+ if (tokens.cacheWrite > 0 && pricing.cacheWrite === undefined) return null;
303
+ const total =
304
+ (tokens.input * (pricing.input ?? 0) +
305
+ tokens.output * (pricing.output ?? 0) +
306
+ tokens.cacheRead * (pricing.cacheRead ?? 0) +
307
+ tokens.cacheWrite * (pricing.cacheWrite ?? 0)) /
308
+ TOKENS_PER_MILLION;
309
+ return Number.isFinite(total) ? total : null;
310
+ }
311
+
312
+ export function formatUsdAmount(amount: number | null): string {
313
+ if (amount === null || !Number.isFinite(amount)) return "-";
314
+ const normalized = Math.abs(amount) < 1e-12 ? 0 : amount;
315
+ const abs = Math.abs(normalized);
316
+ let decimals = 2;
317
+ if (abs < 1) decimals = 4;
318
+ if (abs < 0.1) decimals = 5;
319
+ if (abs < 0.01) decimals = 6;
320
+ let text = normalized.toFixed(decimals);
321
+ text = text.replace(/\.?0+$/, "");
322
+ return `$${text}`;
323
+ }
package/PLAN.md DELETED
@@ -1,33 +0,0 @@
1
- # Statusline Plan (Simplified)
2
-
3
- ## Goals
4
- - Provide a host-agnostic statusline outputter for Codex CLI and Claude Code.
5
- - Show Git status, model label, and usage in a single line.
6
- - Auto-enable a bottom statusline when launching via `codenv`.
7
-
8
- ## Done (implemented)
9
- - `codenv statusline` outputs text/JSON with Git + model + usage.
10
- - ANSI bottom-bar renderer added with forced redraw support (for Codex UI repaint).
11
- - `codenv launch codex` auto-starts the renderer.
12
- - `codenv launch claude` ensures `.claude/settings.json` uses a `statusLine` command object.
13
- - Env knobs added for enable/disable, interval, offset/reserve, and force redraw.
14
-
15
- ## Next plan (overall)
16
- ### Phase 1 — Stabilize behavior
17
- - Verify Codex overlay behavior across terminals; tune default interval/offset as needed.
18
- - Add a simple “compatibility fallback” note for terminals that don’t support scroll regions.
19
- - Confirm Claude statusLine object format across versions (no string form).
20
-
21
- ### Phase 2 — Data quality
22
- - Optional: resolve model from session logs if not provided by env/stdin.
23
- - Clarify usage sync strategy (per-session vs aggregate) and align with `src/usage/`.
24
-
25
- ### Phase 3 — Integrations & ergonomics
26
- - Provide generic wrapper example for other CLIs (stdin JSON contract).
27
- - Optional tmux statusline snippet as alternative for Codex.
28
- - Add minimal config knobs to `code-env.example.json` if needed.
29
-
30
- ### Phase 4 — QA & polish
31
- - Manual test checklist (bash/zsh/fish, macOS/Linux).
32
- - Performance check target (<50ms typical render).
33
- - Harden error handling and safe fallbacks.