@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.
@@ -1,751 +1,44 @@
1
1
  /**
2
2
  * Statusline builder
3
3
  */
4
- import * as fs from "fs";
5
- import * as path from "path";
6
- import { spawnSync } from "child_process";
7
4
  import type { Config, StatuslineArgs } from "../types";
8
- import { DEFAULT_PROFILE_TYPES } from "../constants";
9
5
  import { normalizeType, inferProfileType, getProfileDisplayName } from "../profile/type";
10
6
  import {
11
- formatTokenCount,
12
- readUsageTotalsIndex,
13
- resolveUsageTotalsForProfile,
7
+ readUsageCostIndex,
8
+ readUsageSessionCost,
9
+ resolveUsageCostForProfile,
14
10
  syncUsageFromStatuslineInput,
15
11
  } from "../usage";
16
-
17
- interface StatuslineInputProfile {
18
- key?: string;
19
- name?: string;
20
- type?: string;
21
- }
22
-
23
- interface StatuslineInputUsage {
24
- todayTokens?: number;
25
- totalTokens?: number;
26
- inputTokens?: number;
27
- outputTokens?: number;
28
- }
29
-
30
- interface StatuslineInputContextWindowUsage {
31
- input_tokens?: number;
32
- output_tokens?: number;
33
- cache_creation_input_tokens?: number;
34
- cache_read_input_tokens?: number;
35
- inputTokens?: number;
36
- outputTokens?: number;
37
- cacheCreationInputTokens?: number;
38
- cacheReadInputTokens?: number;
39
- }
40
-
41
- interface StatuslineInputContextWindow {
42
- current_usage?: StatuslineInputContextWindowUsage | null;
43
- total_input_tokens?: number;
44
- total_output_tokens?: number;
45
- context_window_size?: number;
46
- currentUsage?: StatuslineInputContextWindowUsage | null;
47
- totalInputTokens?: number;
48
- totalOutputTokens?: number;
49
- contextWindowSize?: number;
50
- }
51
-
52
- interface StatuslineInputModel {
53
- id?: string;
54
- displayName?: string;
55
- display_name?: string;
56
- }
57
-
58
- interface StatuslineInput {
59
- cwd?: string;
60
- type?: string;
61
- profile?: StatuslineInputProfile;
62
- model?: string | StatuslineInputModel;
63
- model_provider?: string;
64
- usage?: StatuslineInputUsage;
65
- token_usage?: StatuslineInputUsage | number | Record<string, unknown>;
66
- git_branch?: string;
67
- task_running?: boolean;
68
- review_mode?: boolean;
69
- context_window_percent?: number;
70
- context_window_used_tokens?: number;
71
- context_window?: StatuslineInputContextWindow | Record<string, unknown> | null;
72
- contextWindow?: StatuslineInputContextWindow | Record<string, unknown> | null;
73
- workspace?: {
74
- current_dir?: string;
75
- project_dir?: string;
76
- };
77
- cost?: Record<string, unknown>;
78
- version?: string;
79
- output_style?: { name?: string };
80
- session_id?: string;
81
- sessionId?: string;
82
- transcript_path?: string;
83
- hook_event_name?: string;
84
- }
85
-
86
- interface StatuslineUsage {
87
- todayTokens: number | null;
88
- totalTokens: number | null;
89
- inputTokens: number | null;
90
- outputTokens: number | null;
91
- }
92
-
93
- interface StatuslineUsageTotals {
94
- inputTokens: number | null;
95
- outputTokens: number | null;
96
- totalTokens: number | null;
97
- }
98
-
99
- interface GitStatus {
100
- branch: string | null;
101
- ahead: number;
102
- behind: number;
103
- staged: number;
104
- unstaged: number;
105
- untracked: number;
106
- conflicted: number;
107
- }
108
-
109
- const COLOR_ENABLED = !process.env.NO_COLOR && process.env.TERM !== "dumb";
110
- const ANSI_RESET = "\x1b[0m";
111
- const ICON_GIT = "⎇";
112
- const ICON_PROFILE = "👤";
113
- const ICON_MODEL = "⚙";
114
- const ICON_USAGE = "⚡";
115
- const ICON_CONTEXT = "🧠";
116
- const ICON_REVIEW = "📝";
117
- const ICON_CWD = "📁";
118
-
119
- function colorize(text: string, colorCode: string): string {
120
- if (!COLOR_ENABLED) return text;
121
- return `\x1b[${colorCode}m${text}${ANSI_RESET}`;
122
- }
123
-
124
- function dim(text: string): string {
125
- return colorize(text, "2");
126
- }
127
-
128
- function getCwdSegment(cwd: string): string | null {
129
- if (!cwd) return null;
130
- const base = path.basename(cwd) || cwd;
131
- const segment = `${ICON_CWD} ${base}`;
132
- return dim(segment);
133
- }
134
-
135
- export interface StatuslineJson {
136
- cwd: string;
137
- type: string | null;
138
- profile: { key: string | null; name: string | null };
139
- model: string | null;
140
- usage: StatuslineUsage | null;
141
- git: GitStatus | null;
142
- }
143
-
144
- export interface StatuslineResult {
145
- text: string;
146
- json: StatuslineJson;
147
- }
148
-
149
- function isRecord(value: unknown): value is Record<string, unknown> {
150
- return typeof value === "object" && value !== null && !Array.isArray(value);
151
- }
152
-
153
- function readStdinJson(): StatuslineInput | null {
154
- if (process.stdin.isTTY) return null;
155
- try {
156
- const raw = fs.readFileSync(0, "utf8");
157
- const trimmed = raw.trim();
158
- if (!trimmed) return null;
159
- const parsed = JSON.parse(trimmed);
160
- if (!isRecord(parsed)) return null;
161
- return parsed as StatuslineInput;
162
- } catch {
163
- return null;
164
- }
165
- }
166
-
167
- function firstNonEmpty(...values: Array<string | null | undefined>): string | null {
168
- for (const value of values) {
169
- if (value === null || value === undefined) continue;
170
- const text = String(value).trim();
171
- if (text) return text;
172
- }
173
- return null;
174
- }
175
-
176
- function coerceNumber(value: unknown): number | null {
177
- if (value === null || value === undefined || value === "") return null;
178
- const num = Number(value);
179
- if (!Number.isFinite(num)) return null;
180
- return num;
181
- }
182
-
183
- function firstNumber(...values: Array<unknown>): number | null {
184
- for (const value of values) {
185
- const num = coerceNumber(value);
186
- if (num !== null) return num;
187
- }
188
- return null;
189
- }
190
-
191
- function normalizeTypeValue(value: string | null): string | null {
192
- if (!value) return null;
193
- const normalized = normalizeType(value);
194
- if (normalized) return normalized;
195
- const trimmed = String(value).trim();
196
- return trimmed ? trimmed : null;
197
- }
198
-
199
- function detectTypeFromEnv(): string | null {
200
- const matches = DEFAULT_PROFILE_TYPES.filter((type) => {
201
- const suffix = type.toUpperCase();
202
- return (
203
- process.env[`CODE_ENV_PROFILE_KEY_${suffix}`] ||
204
- process.env[`CODE_ENV_PROFILE_NAME_${suffix}`]
205
- );
206
- });
207
- if (matches.length === 1) return matches[0];
208
- return null;
209
- }
210
-
211
- function resolveEnvProfile(type: string | null): { key: string | null; name: string | null } {
212
- const genericKey = process.env.CODE_ENV_PROFILE_KEY || null;
213
- const genericName = process.env.CODE_ENV_PROFILE_NAME || null;
214
- if (!type) {
215
- return { key: genericKey, name: genericName };
216
- }
217
- const suffix = type.toUpperCase();
218
- const key = process.env[`CODE_ENV_PROFILE_KEY_${suffix}`] || genericKey;
219
- const name = process.env[`CODE_ENV_PROFILE_NAME_${suffix}`] || genericName;
220
- return { key: key || null, name: name || null };
221
- }
222
-
223
- function getModelFromInput(input: StatuslineInput | null): string | null {
224
- if (!input) return null;
225
- const raw = input.model;
226
- if (!raw) return null;
227
- if (typeof raw === "string") return raw;
228
- if (isRecord(raw)) {
229
- const displayName = raw.displayName || raw.display_name;
230
- if (displayName) return String(displayName);
231
- if (raw.id) return String(raw.id);
232
- }
233
- return null;
234
- }
235
-
236
- function getModelProviderFromInput(input: StatuslineInput | null): string | null {
237
- if (!input || !input.model_provider) return null;
238
- const provider = String(input.model_provider).trim();
239
- return provider ? provider : null;
240
- }
241
-
242
- function getInputProfile(input: StatuslineInput | null): StatuslineInputProfile | null {
243
- if (!input || !isRecord(input.profile)) return null;
244
- return input.profile as StatuslineInputProfile;
245
- }
246
-
247
- function getInputUsage(input: StatuslineInput | null): StatuslineInputUsage | null {
248
- if (!input) return null;
249
- if (isRecord(input.usage)) {
250
- return input.usage as StatuslineInputUsage;
251
- }
252
- const tokenUsage = input.token_usage;
253
- if (tokenUsage !== null && tokenUsage !== undefined) {
254
- if (typeof tokenUsage === "number") {
255
- return {
256
- todayTokens: null,
257
- totalTokens: coerceNumber(tokenUsage),
258
- inputTokens: null,
259
- outputTokens: null,
260
- };
261
- }
262
- if (isRecord(tokenUsage)) {
263
- const record = tokenUsage as Record<string, unknown>;
264
- const todayTokens =
265
- firstNumber(
266
- record.todayTokens,
267
- record.today,
268
- record.today_tokens,
269
- record.daily,
270
- record.daily_tokens
271
- ) ?? null;
272
- const totalTokens =
273
- firstNumber(
274
- record.totalTokens,
275
- record.total,
276
- record.total_tokens
277
- ) ?? null;
278
- const inputTokens =
279
- firstNumber(
280
- record.inputTokens,
281
- record.input,
282
- record.input_tokens
283
- ) ?? null;
284
- const outputTokens =
285
- firstNumber(
286
- record.outputTokens,
287
- record.output,
288
- record.output_tokens
289
- ) ?? null;
290
- const cacheRead =
291
- firstNumber(
292
- record.cache_read_input_tokens,
293
- record.cacheReadInputTokens,
294
- record.cache_read,
295
- record.cacheRead
296
- ) ?? null;
297
- const cacheWrite =
298
- firstNumber(
299
- record.cache_creation_input_tokens,
300
- record.cacheCreationInputTokens,
301
- record.cache_write_input_tokens,
302
- record.cacheWriteInputTokens,
303
- record.cache_write,
304
- record.cacheWrite
305
- ) ?? null;
306
- if (
307
- todayTokens === null &&
308
- totalTokens === null &&
309
- inputTokens === null &&
310
- outputTokens === null &&
311
- cacheRead === null &&
312
- cacheWrite === null
313
- ) {
314
- return null;
315
- }
316
- const hasCacheTokens = cacheRead !== null || cacheWrite !== null;
317
- const computedTotal = hasCacheTokens
318
- ? (inputTokens || 0) +
319
- (outputTokens || 0) +
320
- (cacheRead || 0) +
321
- (cacheWrite || 0)
322
- : null;
323
- const resolvedTodayTokens = hasCacheTokens
324
- ? todayTokens ?? totalTokens ?? computedTotal
325
- : todayTokens;
326
- return {
327
- todayTokens: resolvedTodayTokens,
328
- totalTokens: totalTokens ?? null,
329
- inputTokens,
330
- outputTokens,
331
- };
332
- }
333
- }
334
- const contextWindow = isRecord(input.context_window)
335
- ? (input.context_window as Record<string, unknown>)
336
- : isRecord(input.contextWindow)
337
- ? (input.contextWindow as Record<string, unknown>)
338
- : null;
339
- if (!contextWindow) return null;
340
- const totalInputTokens =
341
- firstNumber(
342
- contextWindow.total_input_tokens,
343
- contextWindow.totalInputTokens
344
- ) ?? null;
345
- const totalOutputTokens =
346
- firstNumber(
347
- contextWindow.total_output_tokens,
348
- contextWindow.totalOutputTokens
349
- ) ?? null;
350
- if (totalInputTokens !== null || totalOutputTokens !== null) {
351
- return {
352
- todayTokens: null,
353
- totalTokens: null,
354
- inputTokens: totalInputTokens,
355
- outputTokens: totalOutputTokens,
356
- };
357
- }
358
- const currentUsage = isRecord(contextWindow.current_usage)
359
- ? (contextWindow.current_usage as Record<string, unknown>)
360
- : isRecord(contextWindow.currentUsage)
361
- ? (contextWindow.currentUsage as Record<string, unknown>)
362
- : null;
363
- if (!currentUsage) return null;
364
- const inputTokens =
365
- firstNumber(
366
- currentUsage.input_tokens,
367
- currentUsage.inputTokens
368
- ) ?? null;
369
- const outputTokens =
370
- firstNumber(
371
- currentUsage.output_tokens,
372
- currentUsage.outputTokens
373
- ) ?? null;
374
- const cacheRead =
375
- firstNumber(
376
- currentUsage.cache_read_input_tokens,
377
- currentUsage.cacheReadInputTokens
378
- ) ?? null;
379
- const cacheWrite =
380
- firstNumber(
381
- currentUsage.cache_creation_input_tokens,
382
- currentUsage.cacheCreationInputTokens
383
- ) ?? null;
384
- if (
385
- inputTokens === null &&
386
- outputTokens === null &&
387
- cacheRead === null &&
388
- cacheWrite === null
389
- ) {
390
- return null;
391
- }
392
- const totalTokens =
393
- (inputTokens || 0) +
394
- (outputTokens || 0) +
395
- (cacheRead || 0) +
396
- (cacheWrite || 0);
397
- return {
398
- todayTokens: totalTokens,
399
- totalTokens: null,
400
- inputTokens,
401
- outputTokens,
402
- };
403
- }
404
-
405
- function getSessionId(input: StatuslineInput | null): string | null {
406
- if (!input) return null;
407
- return firstNonEmpty(input.session_id, input.sessionId);
408
- }
409
-
410
- function parseUsageTotalsRecord(
411
- record: Record<string, unknown>
412
- ): StatuslineUsageTotals | null {
413
- const inputTokens =
414
- firstNumber(
415
- record.inputTokens,
416
- record.input,
417
- record.input_tokens
418
- ) ?? null;
419
- const outputTokens =
420
- firstNumber(
421
- record.outputTokens,
422
- record.output,
423
- record.output_tokens
424
- ) ?? null;
425
- const totalTokens =
426
- firstNumber(
427
- record.totalTokens,
428
- record.total,
429
- record.total_tokens
430
- ) ?? null;
431
- const cacheRead =
432
- firstNumber(
433
- record.cache_read_input_tokens,
434
- record.cacheReadInputTokens,
435
- record.cache_read,
436
- record.cacheRead
437
- ) ?? null;
438
- const cacheWrite =
439
- firstNumber(
440
- record.cache_creation_input_tokens,
441
- record.cacheCreationInputTokens,
442
- record.cache_write_input_tokens,
443
- record.cacheWriteInputTokens,
444
- record.cache_write,
445
- record.cacheWrite
446
- ) ?? null;
447
- let computedTotal: number | null = null;
448
- if (
449
- inputTokens !== null ||
450
- outputTokens !== null ||
451
- cacheRead !== null ||
452
- cacheWrite !== null
453
- ) {
454
- computedTotal =
455
- (inputTokens || 0) +
456
- (outputTokens || 0) +
457
- (cacheRead || 0) +
458
- (cacheWrite || 0);
459
- }
460
- const resolvedTotal = totalTokens ?? computedTotal;
461
- if (
462
- inputTokens === null &&
463
- outputTokens === null &&
464
- resolvedTotal === null
465
- ) {
466
- return null;
467
- }
468
- return {
469
- inputTokens,
470
- outputTokens,
471
- totalTokens: resolvedTotal,
472
- };
473
- }
474
-
475
- function getUsageTotalsFromInput(
476
- input: StatuslineInput | null
477
- ): StatuslineUsageTotals | null {
478
- if (!input) return null;
479
- const contextWindow = isRecord(input.context_window)
480
- ? (input.context_window as Record<string, unknown>)
481
- : isRecord(input.contextWindow)
482
- ? (input.contextWindow as Record<string, unknown>)
483
- : null;
484
- if (contextWindow) {
485
- const totalInputTokens =
486
- firstNumber(
487
- contextWindow.total_input_tokens,
488
- contextWindow.totalInputTokens
489
- ) ?? null;
490
- const totalOutputTokens =
491
- firstNumber(
492
- contextWindow.total_output_tokens,
493
- contextWindow.totalOutputTokens
494
- ) ?? null;
495
- if (totalInputTokens !== null || totalOutputTokens !== null) {
496
- return {
497
- inputTokens: totalInputTokens,
498
- outputTokens: totalOutputTokens,
499
- totalTokens: (totalInputTokens || 0) + (totalOutputTokens || 0),
500
- };
501
- }
502
- }
503
- if (typeof input.token_usage === "number") {
504
- return {
505
- inputTokens: null,
506
- outputTokens: null,
507
- totalTokens: coerceNumber(input.token_usage),
508
- };
509
- }
510
- if (isRecord(input.token_usage)) {
511
- return parseUsageTotalsRecord(input.token_usage as Record<string, unknown>);
512
- }
513
- if (isRecord(input.usage)) {
514
- return parseUsageTotalsRecord(input.usage as Record<string, unknown>);
515
- }
516
- return null;
517
- }
518
-
519
- function getContextUsedTokens(input: StatuslineInput | null): number | null {
520
- if (!input) return null;
521
- return coerceNumber(input.context_window_used_tokens);
522
- }
523
-
524
- function normalizeInputUsage(
525
- inputUsage: StatuslineInputUsage | null
526
- ): StatuslineUsage | null {
527
- if (!inputUsage) return null;
528
- const usage: StatuslineUsage = {
529
- todayTokens: coerceNumber(inputUsage.todayTokens),
530
- totalTokens: coerceNumber(inputUsage.totalTokens),
531
- inputTokens: coerceNumber(inputUsage.inputTokens),
532
- outputTokens: coerceNumber(inputUsage.outputTokens),
533
- };
534
- const hasUsage =
535
- usage.todayTokens !== null ||
536
- usage.totalTokens !== null ||
537
- usage.inputTokens !== null ||
538
- usage.outputTokens !== null;
539
- return hasUsage ? usage : null;
540
- }
541
-
542
- function getContextLeftPercent(
543
- input: StatuslineInput | null,
544
- type: string | null
545
- ): number | null {
546
- if (!input) return null;
547
- const raw = coerceNumber(input.context_window_percent);
548
- if (raw === null || raw < 0) return null;
549
- const percent = raw <= 1 ? raw * 100 : raw;
550
- if (percent > 100) return null;
551
- const usedTokens = getContextUsedTokens(input);
552
- const normalizedType = normalizeTypeValue(type);
553
- // Prefer treating the percent as "remaining" for codex/claude and when usage is absent.
554
- const preferRemaining =
555
- normalizedType === "codex" ||
556
- normalizedType === "claude" ||
557
- usedTokens === null ||
558
- (usedTokens <= 0 && percent >= 99);
559
- const left = preferRemaining ? percent : 100 - percent;
560
- return Math.max(0, Math.min(100, left));
561
- }
562
-
563
- function getWorkspaceDir(input: StatuslineInput | null): string | null {
564
- if (!input || !isRecord(input.workspace)) return null;
565
- const currentDir = input.workspace.current_dir;
566
- if (currentDir) {
567
- const trimmed = String(currentDir).trim();
568
- if (trimmed) return trimmed;
569
- }
570
- const projectDir = input.workspace.project_dir;
571
- if (!projectDir) return null;
572
- const trimmed = String(projectDir).trim();
573
- return trimmed ? trimmed : null;
574
- }
575
-
576
- function getGitStatusFromInput(
577
- input: StatuslineInput | null
578
- ): GitStatus | null {
579
- if (!input || !input.git_branch) return null;
580
- const branch = String(input.git_branch).trim();
581
- if (!branch) return null;
582
- return {
583
- branch,
584
- ahead: 0,
585
- behind: 0,
586
- staged: 0,
587
- unstaged: 0,
588
- untracked: 0,
589
- conflicted: 0,
590
- };
591
- }
592
-
593
- function getGitStatus(cwd: string): GitStatus | null {
594
- if (!cwd) return null;
595
- const result = spawnSync("git", ["-C", cwd, "status", "--porcelain=v2", "-b"], {
596
- encoding: "utf8",
597
- stdio: ["ignore", "pipe", "ignore"],
598
- });
599
- if (result.status !== 0 || !result.stdout) return null;
600
- const status: GitStatus = {
601
- branch: null,
602
- ahead: 0,
603
- behind: 0,
604
- staged: 0,
605
- unstaged: 0,
606
- untracked: 0,
607
- conflicted: 0,
608
- };
609
-
610
- const lines = result.stdout.split(/\r?\n/);
611
- for (const line of lines) {
612
- if (!line) continue;
613
- if (line.startsWith("# branch.head ")) {
614
- status.branch = line.slice("# branch.head ".length).trim();
615
- continue;
616
- }
617
- if (line.startsWith("# branch.ab ")) {
618
- const parts = line
619
- .slice("# branch.ab ".length)
620
- .trim()
621
- .split(/\s+/);
622
- for (const part of parts) {
623
- if (part.startsWith("+")) status.ahead = Number(part.slice(1)) || 0;
624
- if (part.startsWith("-")) status.behind = Number(part.slice(1)) || 0;
625
- }
626
- continue;
627
- }
628
- if (line.startsWith("? ")) {
629
- status.untracked += 1;
630
- continue;
631
- }
632
- if (line.startsWith("u ")) {
633
- status.conflicted += 1;
634
- continue;
635
- }
636
- if (line.startsWith("1 ") || line.startsWith("2 ")) {
637
- const parts = line.split(/\s+/);
638
- const xy = parts[1] || "";
639
- const staged = xy[0];
640
- const unstaged = xy[1];
641
- if (staged && staged !== ".") status.staged += 1;
642
- if (unstaged && unstaged !== ".") status.unstaged += 1;
643
- continue;
644
- }
645
- }
646
-
647
- if (!status.branch) {
648
- status.branch = "HEAD";
649
- }
650
- return status;
651
- }
652
-
653
- function formatGitSegment(status: GitStatus | null): string | null {
654
- if (!status || !status.branch) return null;
655
- const meta: string[] = [];
656
- const dirtyCount = status.staged + status.unstaged + status.untracked;
657
- if (status.ahead > 0) meta.push(`↑${status.ahead}`);
658
- if (status.behind > 0) meta.push(`↓${status.behind}`);
659
- if (status.conflicted > 0) meta.push(`✖${status.conflicted}`);
660
- if (dirtyCount > 0) meta.push(`+${dirtyCount}`);
661
- const suffix = meta.length > 0 ? ` [${meta.join("")}]` : "";
662
- const text = `${ICON_GIT} ${status.branch}${suffix}`;
663
- const hasConflicts = status.conflicted > 0;
664
- const isDirty = dirtyCount > 0;
665
- if (hasConflicts) return colorize(text, "31");
666
- if (isDirty) return colorize(text, "33");
667
- if (status.ahead > 0 || status.behind > 0) return colorize(text, "36");
668
- return colorize(text, "32");
669
- }
670
-
671
- function resolveUsageFromRecords(
672
- config: Config,
673
- configPath: string | null,
674
- type: string | null,
675
- profileKey: string | null,
676
- profileName: string | null,
677
- syncUsage: boolean
678
- ): StatuslineUsage | null {
679
- try {
680
- const normalized = normalizeType(type || "");
681
- if (!normalized || (!profileKey && !profileName)) return null;
682
- const totals = readUsageTotalsIndex(config, configPath, syncUsage);
683
- if (!totals) return null;
684
- const usage = resolveUsageTotalsForProfile(
685
- totals,
686
- normalized,
687
- profileKey,
688
- profileName
689
- );
690
- if (!usage) return null;
691
- return {
692
- todayTokens: usage.today,
693
- totalTokens: usage.total,
694
- inputTokens: null,
695
- outputTokens: null,
696
- };
697
- } catch {
698
- return null;
699
- }
700
- }
701
-
702
- function formatUsageSegment(usage: StatuslineUsage | null): string | null {
703
- if (!usage) return null;
704
- const today =
705
- usage.todayTokens ??
706
- (usage.inputTokens !== null || usage.outputTokens !== null
707
- ? (usage.inputTokens || 0) + (usage.outputTokens || 0)
708
- : usage.totalTokens);
709
- if (today === null) return null;
710
- const text = `Today ${formatTokenCount(today)}`;
711
- return colorize(`${ICON_USAGE} ${text}`, "33");
712
- }
713
-
714
- function formatModelSegment(
715
- model: string | null,
716
- provider: string | null
717
- ): string | null {
718
- if (!model) return null;
719
- const providerLabel = provider ? `${provider}:${model}` : model;
720
- return colorize(`${ICON_MODEL} ${providerLabel}`, "35");
721
- }
722
-
723
- function formatProfileSegment(
724
- type: string | null,
725
- profileKey: string | null,
726
- profileName: string | null
727
- ): string | null {
728
- const name = profileName || profileKey;
729
- if (!name) return null;
730
- const label = type ? `${type}:${name}` : name;
731
- return colorize(`${ICON_PROFILE} ${label}`, "37");
732
- }
733
-
734
- function formatContextSegment(contextLeft: number | null): string | null {
735
- if (contextLeft === null) return null;
736
- const left = Math.max(0, Math.min(100, Math.round(contextLeft)));
737
- return colorize(`${ICON_CONTEXT} ${left}% left`, "36");
738
- }
739
-
740
- function formatContextUsedSegment(usedTokens: number | null): string | null {
741
- if (usedTokens === null) return null;
742
- return colorize(`${ICON_CONTEXT} ${formatTokenCount(usedTokens)} used`, "36");
743
- }
744
-
745
- function formatModeSegment(reviewMode: boolean): string | null {
746
- if (!reviewMode) return null;
747
- return colorize(`${ICON_REVIEW} review`, "34");
748
- }
12
+ import { calculateUsageCost, resolvePricingForProfile } from "../usage/pricing";
13
+ import { appendStatuslineDebug } from "./debug";
14
+ import {
15
+ formatContextSegment,
16
+ formatContextUsedSegment,
17
+ formatModeSegment,
18
+ formatModelSegment,
19
+ formatProfileSegment,
20
+ formatUsageSegment,
21
+ getCwdSegment,
22
+ } from "./format";
23
+ import { formatGitSegment, getGitStatus } from "./git";
24
+ import {
25
+ detectTypeFromEnv,
26
+ getContextLeftPercent,
27
+ getContextUsedTokens,
28
+ getGitStatusFromInput,
29
+ getInputProfile,
30
+ getInputUsage,
31
+ getModelFromInput,
32
+ getModelProviderFromInput,
33
+ getSessionId,
34
+ getWorkspaceDir,
35
+ normalizeTypeValue,
36
+ readStdinJson,
37
+ resolveEnvProfile,
38
+ } from "./input";
39
+ import { getUsageTotalsFromInput, normalizeInputUsage, resolveUsageFromRecords } from "./usage";
40
+ import { firstNonEmpty, firstNumber } from "./utils";
41
+ import type { StatuslineResult, StatuslineUsage } from "./types";
749
42
 
750
43
  export function buildStatuslineResult(
751
44
  args: StatuslineArgs,
@@ -804,9 +97,46 @@ export function buildStatuslineResult(
804
97
  )!;
805
98
 
806
99
  const sessionId = getSessionId(stdinInput);
807
- const stdinUsageTotals = getUsageTotalsFromInput(stdinInput);
100
+ const usageType = normalizeType(type || "");
101
+ const stdinUsageTotals = getUsageTotalsFromInput(stdinInput, usageType);
102
+ const model = firstNonEmpty(
103
+ args.model,
104
+ process.env.CODE_ENV_MODEL,
105
+ getModelFromInput(stdinInput)
106
+ );
107
+ const modelProvider = firstNonEmpty(
108
+ process.env.CODE_ENV_MODEL_PROVIDER,
109
+ getModelProviderFromInput(stdinInput)
110
+ );
111
+ appendStatuslineDebug(configPath, {
112
+ timestamp: new Date().toISOString(),
113
+ typeCandidate,
114
+ resolvedType: type,
115
+ usageType,
116
+ profile: { key: profileKey, name: profileName },
117
+ sessionId,
118
+ stdinUsageTotals,
119
+ cwd,
120
+ args: {
121
+ type: args.type,
122
+ profileKey: args.profileKey,
123
+ profileName: args.profileName,
124
+ usageToday: args.usageToday,
125
+ usageTotal: args.usageTotal,
126
+ usageInput: args.usageInput,
127
+ usageOutput: args.usageOutput,
128
+ syncUsage: args.syncUsage,
129
+ },
130
+ env: {
131
+ CODE_ENV_TYPE: process.env.CODE_ENV_TYPE,
132
+ CODE_ENV_PROFILE_KEY: process.env.CODE_ENV_PROFILE_KEY,
133
+ CODE_ENV_PROFILE_NAME: process.env.CODE_ENV_PROFILE_NAME,
134
+ CODE_ENV_CWD: process.env.CODE_ENV_CWD,
135
+ CODE_ENV_STATUSLINE: process.env.CODE_ENV_STATUSLINE,
136
+ },
137
+ input: stdinInput,
138
+ });
808
139
  if (args.syncUsage && sessionId && stdinUsageTotals) {
809
- const usageType = normalizeType(type || "");
810
140
  syncUsageFromStatuslineInput(
811
141
  config,
812
142
  configPath,
@@ -815,20 +145,11 @@ export function buildStatuslineResult(
815
145
  profileName,
816
146
  sessionId,
817
147
  stdinUsageTotals,
818
- cwd
148
+ cwd,
149
+ model
819
150
  );
820
151
  }
821
152
 
822
- const model = firstNonEmpty(
823
- args.model,
824
- process.env.CODE_ENV_MODEL,
825
- getModelFromInput(stdinInput)
826
- );
827
- const modelProvider = firstNonEmpty(
828
- process.env.CODE_ENV_MODEL_PROVIDER,
829
- getModelProviderFromInput(stdinInput)
830
- );
831
-
832
153
  const usage: StatuslineUsage = {
833
154
  todayTokens: firstNumber(
834
155
  args.usageToday,
@@ -846,6 +167,8 @@ export function buildStatuslineResult(
846
167
  args.usageOutput,
847
168
  process.env.CODE_ENV_USAGE_OUTPUT
848
169
  ),
170
+ cacheReadTokens: null,
171
+ cacheWriteTokens: null,
849
172
  };
850
173
 
851
174
  const hasExplicitUsage =
@@ -854,21 +177,25 @@ export function buildStatuslineResult(
854
177
  usage.inputTokens !== null ||
855
178
  usage.outputTokens !== null;
856
179
 
857
- const stdinUsage = normalizeInputUsage(getInputUsage(stdinInput));
180
+ const stdinUsage = normalizeInputUsage(getInputUsage(stdinInput, usageType));
181
+ const recordsUsage = resolveUsageFromRecords(
182
+ config,
183
+ configPath,
184
+ type,
185
+ profileKey,
186
+ profileName,
187
+ args.syncUsage
188
+ );
858
189
 
859
190
  let finalUsage: StatuslineUsage | null = hasExplicitUsage ? usage : null;
191
+ if (!finalUsage && args.syncUsage && recordsUsage) {
192
+ finalUsage = recordsUsage;
193
+ }
860
194
  if (!finalUsage) {
861
195
  finalUsage = stdinUsage;
862
196
  }
863
- if (!finalUsage) {
864
- finalUsage = resolveUsageFromRecords(
865
- config,
866
- configPath,
867
- type,
868
- profileKey,
869
- profileName,
870
- args.syncUsage
871
- );
197
+ if (!finalUsage && recordsUsage) {
198
+ finalUsage = recordsUsage;
872
199
  }
873
200
 
874
201
  let gitStatus = getGitStatus(cwd);
@@ -883,7 +210,45 @@ export function buildStatuslineResult(
883
210
  const gitSegment = formatGitSegment(gitStatus);
884
211
  const profileSegment = formatProfileSegment(type, profileKey, profileName);
885
212
  const modelSegment = formatModelSegment(model, modelProvider);
886
- const usageSegment = formatUsageSegment(finalUsage);
213
+ let profile = profileKey && config.profiles ? config.profiles[profileKey] : null;
214
+ if (!profile && profileName && config.profiles) {
215
+ const matches = Object.entries(config.profiles).find(([key, entry]) => {
216
+ const displayName = getProfileDisplayName(key, entry);
217
+ return (
218
+ displayName === profileName ||
219
+ entry.name === profileName ||
220
+ key === profileName
221
+ );
222
+ });
223
+ if (matches) profile = matches[1];
224
+ }
225
+ const sessionUsage = hasExplicitUsage ? usage : stdinUsage;
226
+ const pricing = resolvePricingForProfile(config, profile || null, model);
227
+ let sessionCost: number | null = null;
228
+ if (hasExplicitUsage) {
229
+ sessionCost = sessionUsage
230
+ ? calculateUsageCost(sessionUsage, pricing)
231
+ : null;
232
+ } else {
233
+ const sessionCostFromRecords = sessionId
234
+ ? readUsageSessionCost(
235
+ config,
236
+ configPath,
237
+ type,
238
+ sessionId,
239
+ args.syncUsage
240
+ )
241
+ : null;
242
+ sessionCost =
243
+ sessionCostFromRecords ??
244
+ (sessionUsage ? calculateUsageCost(sessionUsage, pricing) : null);
245
+ }
246
+ const costIndex = readUsageCostIndex(config, configPath, args.syncUsage);
247
+ const costTotals = costIndex
248
+ ? resolveUsageCostForProfile(costIndex, type, profileKey, profileName)
249
+ : null;
250
+ const todayCost = costTotals ? costTotals.today : null;
251
+ const usageSegment = formatUsageSegment(todayCost, sessionCost);
887
252
  const contextLeft = getContextLeftPercent(stdinInput, type);
888
253
  const contextSegment = formatContextSegment(contextLeft);
889
254
  const contextUsedTokens = getContextUsedTokens(stdinInput);
@@ -918,3 +283,17 @@ export function buildStatuslineResult(
918
283
  },
919
284
  };
920
285
  }
286
+
287
+ export type {
288
+ GitStatus,
289
+ StatuslineInput,
290
+ StatuslineInputContextWindow,
291
+ StatuslineInputContextWindowUsage,
292
+ StatuslineInputModel,
293
+ StatuslineInputProfile,
294
+ StatuslineInputUsage,
295
+ StatuslineJson,
296
+ StatuslineResult,
297
+ StatuslineUsage,
298
+ StatuslineUsageTotals,
299
+ } from "./types";