@praeviso/code-env-switch 0.1.2 → 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.
@@ -40,9 +40,20 @@ interface UsageStateEntry {
40
40
  cwd: string | null;
41
41
  }
42
42
 
43
+ interface UsageSessionEntry {
44
+ type: ProfileType;
45
+ inputTokens: number;
46
+ outputTokens: number;
47
+ totalTokens: number;
48
+ startTs: string | null;
49
+ endTs: string | null;
50
+ cwd: string | null;
51
+ }
52
+
43
53
  interface UsageStateFile {
44
54
  version: number;
45
55
  files: Record<string, UsageStateEntry>;
56
+ sessions?: Record<string, UsageSessionEntry>;
46
57
  }
47
58
 
48
59
  interface ProfileLogEntry {
@@ -78,6 +89,12 @@ interface SessionStats {
78
89
  sessionId: string | null;
79
90
  }
80
91
 
92
+ interface UsageTotalsInput {
93
+ inputTokens: number | null;
94
+ outputTokens: number | null;
95
+ totalTokens: number | null;
96
+ }
97
+
81
98
  function resolveDefaultConfigDir(configPath: string | null): string {
82
99
  if (configPath) return path.dirname(configPath);
83
100
  return path.join(os.homedir(), ".config", "code-env");
@@ -172,6 +189,18 @@ function normalizeUsageType(type: string | null | undefined): string | null {
172
189
  return trimmed ? trimmed : null;
173
190
  }
174
191
 
192
+ function buildSessionKey(type: ProfileType | null, sessionId: string): string {
193
+ const normalized = normalizeUsageType(type || "");
194
+ return normalized ? `${normalized}::${sessionId}` : sessionId;
195
+ }
196
+
197
+ function toFiniteNumber(value: number | null | undefined): number | null {
198
+ if (value === null || value === undefined) return null;
199
+ const num = Number(value);
200
+ if (!Number.isFinite(num)) return null;
201
+ return num;
202
+ }
203
+
175
204
  function buildUsageLookupKey(
176
205
  type: string | null | undefined,
177
206
  profileId: string | null | undefined
@@ -212,6 +241,81 @@ export function resolveUsageTotalsForProfile(
212
241
  );
213
242
  }
214
243
 
244
+ export function syncUsageFromStatuslineInput(
245
+ config: Config,
246
+ configPath: string | null,
247
+ type: ProfileType | null,
248
+ profileKey: string | null,
249
+ profileName: string | null,
250
+ sessionId: string | null,
251
+ totals: UsageTotalsInput | null,
252
+ cwd: string | null
253
+ ): void {
254
+ if (!sessionId) return;
255
+ if (!totals) return;
256
+ if (!profileKey && !profileName) return;
257
+ const normalizedType = normalizeType(type || "");
258
+ if (!normalizedType) return;
259
+ const usagePath = getUsagePath(config, configPath);
260
+ if (!usagePath) return;
261
+ const inputTokens = toFiniteNumber(totals.inputTokens) ?? 0;
262
+ const outputTokens = toFiniteNumber(totals.outputTokens) ?? 0;
263
+ const totalTokens =
264
+ toFiniteNumber(totals.totalTokens) ?? inputTokens + outputTokens;
265
+ if (!Number.isFinite(totalTokens)) return;
266
+
267
+ const statePath = getUsageStatePath(usagePath, config);
268
+ const lockPath = `${statePath}.lock`;
269
+ const lockFd = acquireLock(lockPath);
270
+ if (lockFd === null) return;
271
+ try {
272
+ const state = readUsageState(statePath);
273
+ const sessions = state.sessions || {};
274
+ const key = buildSessionKey(normalizedType, sessionId);
275
+ const prev = sessions[key];
276
+ const prevInput = prev ? prev.inputTokens : 0;
277
+ const prevOutput = prev ? prev.outputTokens : 0;
278
+ const prevTotal = prev ? prev.totalTokens : 0;
279
+
280
+ let deltaInput = inputTokens - prevInput;
281
+ let deltaOutput = outputTokens - prevOutput;
282
+ let deltaTotal = totalTokens - prevTotal;
283
+ if (deltaTotal < 0 || deltaInput < 0 || deltaOutput < 0) {
284
+ deltaInput = inputTokens;
285
+ deltaOutput = outputTokens;
286
+ deltaTotal = totalTokens;
287
+ }
288
+
289
+ if (deltaTotal > 0) {
290
+ const record: UsageRecord = {
291
+ ts: new Date().toISOString(),
292
+ type: normalizedType,
293
+ profileKey: profileKey || null,
294
+ profileName: profileName || null,
295
+ inputTokens: deltaInput,
296
+ outputTokens: deltaOutput,
297
+ totalTokens: deltaTotal,
298
+ };
299
+ appendUsageRecord(usagePath, record);
300
+ }
301
+
302
+ const now = new Date().toISOString();
303
+ sessions[key] = {
304
+ type: normalizedType,
305
+ inputTokens,
306
+ outputTokens,
307
+ totalTokens,
308
+ startTs: prev ? prev.startTs : now,
309
+ endTs: now,
310
+ cwd: cwd || (prev ? prev.cwd : null),
311
+ };
312
+ state.sessions = sessions;
313
+ writeUsageState(statePath, state);
314
+ } finally {
315
+ releaseLock(lockPath, lockFd);
316
+ }
317
+ }
318
+
215
319
  export function logProfileUse(
216
320
  config: Config,
217
321
  configPath: string | null,
@@ -424,19 +528,21 @@ function resolveProfileForSession(
424
528
 
425
529
  function readUsageState(statePath: string): UsageStateFile {
426
530
  if (!statePath || !fs.existsSync(statePath)) {
427
- return { version: 1, files: {} };
531
+ return { version: 1, files: {}, sessions: {} };
428
532
  }
429
533
  try {
430
534
  const raw = fs.readFileSync(statePath, "utf8");
431
535
  const parsed = JSON.parse(raw);
432
536
  if (!parsed || typeof parsed !== "object") {
433
- return { version: 1, files: {} };
537
+ return { version: 1, files: {}, sessions: {} };
434
538
  }
435
539
  const files =
436
540
  parsed.files && typeof parsed.files === "object" ? parsed.files : {};
437
- return { version: 1, files };
541
+ const sessions =
542
+ parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {};
543
+ return { version: 1, files, sessions };
438
544
  } catch {
439
- return { version: 1, files: {} };
545
+ return { version: 1, files: {}, sessions: {} };
440
546
  }
441
547
  }
442
548
 
@@ -754,6 +860,7 @@ export function syncUsageFromSessions(
754
860
 
755
861
  const state = readUsageState(statePath);
756
862
  const files = state.files || {};
863
+ const sessions = state.sessions || {};
757
864
  const codexFiles = collectSessionFiles(getCodexSessionsPath(config));
758
865
  const claudeFiles = collectSessionFiles(getClaudeSessionsPath(config));
759
866
 
@@ -786,16 +893,34 @@ export function syncUsageFromSessions(
786
893
  stats.sessionId
787
894
  );
788
895
  if (!resolved.match) return;
896
+ const sessionKey =
897
+ stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
898
+ const sessionPrev = sessionKey ? sessions[sessionKey] : null;
789
899
  const prevInput = prev ? prev.inputTokens : 0;
790
900
  const prevOutput = prev ? prev.outputTokens : 0;
791
901
  const prevTotal = prev ? prev.totalTokens : 0;
792
- let deltaInput = stats.inputTokens - prevInput;
793
- let deltaOutput = stats.outputTokens - prevOutput;
794
- let deltaTotal = stats.totalTokens - prevTotal;
902
+ const prevInputMax = sessionPrev
903
+ ? Math.max(prevInput, sessionPrev.inputTokens)
904
+ : prevInput;
905
+ const prevOutputMax = sessionPrev
906
+ ? Math.max(prevOutput, sessionPrev.outputTokens)
907
+ : prevOutput;
908
+ const prevTotalMax = sessionPrev
909
+ ? Math.max(prevTotal, sessionPrev.totalTokens)
910
+ : prevTotal;
911
+ let deltaInput = stats.inputTokens - prevInputMax;
912
+ let deltaOutput = stats.outputTokens - prevOutputMax;
913
+ let deltaTotal = stats.totalTokens - prevTotalMax;
795
914
  if (deltaTotal < 0 || deltaInput < 0 || deltaOutput < 0) {
796
- deltaInput = stats.inputTokens;
797
- deltaOutput = stats.outputTokens;
798
- deltaTotal = stats.totalTokens;
915
+ if (sessionPrev) {
916
+ deltaInput = 0;
917
+ deltaOutput = 0;
918
+ deltaTotal = 0;
919
+ } else {
920
+ deltaInput = stats.inputTokens;
921
+ deltaOutput = stats.outputTokens;
922
+ deltaTotal = stats.totalTokens;
923
+ }
799
924
  }
800
925
  if (deltaTotal > 0) {
801
926
  const record: UsageRecord = {
@@ -809,6 +934,26 @@ export function syncUsageFromSessions(
809
934
  };
810
935
  appendUsageRecord(usagePath, record);
811
936
  }
937
+ if (sessionKey) {
938
+ const nextInput = sessionPrev
939
+ ? Math.max(sessionPrev.inputTokens, stats.inputTokens)
940
+ : stats.inputTokens;
941
+ const nextOutput = sessionPrev
942
+ ? Math.max(sessionPrev.outputTokens, stats.outputTokens)
943
+ : stats.outputTokens;
944
+ const nextTotal = sessionPrev
945
+ ? Math.max(sessionPrev.totalTokens, stats.totalTokens)
946
+ : stats.totalTokens;
947
+ sessions[sessionKey] = {
948
+ type,
949
+ inputTokens: nextInput,
950
+ outputTokens: nextOutput,
951
+ totalTokens: nextTotal,
952
+ startTs: sessionPrev ? sessionPrev.startTs : stats.startTs,
953
+ endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
954
+ cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
955
+ };
956
+ }
812
957
  files[filePath] = {
813
958
  mtimeMs: stat.mtimeMs,
814
959
  size: stat.size,
@@ -826,6 +971,7 @@ export function syncUsageFromSessions(
826
971
  for (const filePath of claudeFiles) processFile(filePath, "claude");
827
972
 
828
973
  state.files = files;
974
+ state.sessions = sessions;
829
975
  writeUsageState(statePath, state);
830
976
  } finally {
831
977
  releaseLock(lockPath, lockFd);