@hasna/statusline 0.0.7 → 0.0.8

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/dist/cli.js CHANGED
@@ -1,6 +1,36 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
3
5
  var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
34
  var __returnValue = (v) => v;
5
35
  function __exportSetter(name, newValue) {
6
36
  this[name] = __returnValue.bind(null, newValue);
@@ -30,6 +60,7 @@ function defaultConfig() {
30
60
  "loc",
31
61
  "model-context",
32
62
  "context-remaining",
63
+ "cache-rate",
33
64
  "cost"
34
65
  ]
35
66
  };
@@ -95,7 +126,7 @@ function numberOr(v) {
95
126
  }
96
127
 
97
128
  // src/segments/index.ts
98
- import { hostname } from "os";
129
+ import { homedir as homedir5, hostname } from "os";
99
130
 
100
131
  // src/format.ts
101
132
  function compactAge(seconds) {
@@ -135,10 +166,10 @@ function compactDuration(ms) {
135
166
  return `${s}s`;
136
167
  if (s < 3600) {
137
168
  const rem2 = s % 60;
138
- return rem2 ? `${Math.floor(s / 60)}m${rem2}s` : `${Math.floor(s / 60)}m`;
169
+ return rem2 ? `${Math.floor(s / 60)}m ${rem2}s` : `${Math.floor(s / 60)}m`;
139
170
  }
140
171
  const rem = Math.floor(s % 3600 / 60);
141
- return rem ? `${Math.floor(s / 3600)}h${rem}m` : `${Math.floor(s / 3600)}h`;
172
+ return rem ? `${Math.floor(s / 3600)}h ${rem}m` : `${Math.floor(s / 3600)}h`;
142
173
  }
143
174
 
144
175
  // src/git.ts
@@ -195,14 +226,145 @@ function trackedLineCount(cwd) {
195
226
  }
196
227
 
197
228
  // src/context-window.ts
198
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
199
- function contextUsage(ctx) {
200
- const path = ctx.transcriptPath;
229
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
230
+
231
+ // src/providers/codex.ts
232
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
233
+ import { homedir as homedir2 } from "os";
234
+ import { join as join2 } from "path";
235
+ function latestCodexSessionPath(home = homedir2()) {
236
+ const root = join2(home, ".codex", "sessions");
237
+ if (!existsSync2(root))
238
+ return null;
239
+ let newest = null;
240
+ let newestMtime = 0;
241
+ try {
242
+ for (const year of readdirSync(root)) {
243
+ const yearDir = join2(root, year);
244
+ if (!statSync(yearDir, { throwIfNoEntry: false })?.isDirectory())
245
+ continue;
246
+ for (const month of readdirSync(yearDir)) {
247
+ const monthDir = join2(yearDir, month);
248
+ if (!statSync(monthDir, { throwIfNoEntry: false })?.isDirectory())
249
+ continue;
250
+ for (const day of readdirSync(monthDir)) {
251
+ const dayDir = join2(monthDir, day);
252
+ if (!statSync(dayDir, { throwIfNoEntry: false })?.isDirectory())
253
+ continue;
254
+ for (const file of readdirSync(dayDir)) {
255
+ if (!file.startsWith("rollout-") || !file.endsWith(".jsonl"))
256
+ continue;
257
+ const full = join2(dayDir, file);
258
+ const mtime = statSync(full, { throwIfNoEntry: false })?.mtimeMs ?? 0;
259
+ if (mtime > newestMtime) {
260
+ newestMtime = mtime;
261
+ newest = full;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ } catch {
268
+ return null;
269
+ }
270
+ return newest;
271
+ }
272
+ function codexCacheRate(sessionPath) {
273
+ const path = sessionPath ?? latestCodexSessionPath();
201
274
  if (!path || !existsSync2(path))
202
275
  return null;
203
276
  let lines;
204
277
  try {
205
278
  lines = readFileSync2(path, "utf8").split(`
279
+ `);
280
+ } catch {
281
+ return null;
282
+ }
283
+ let usage = null;
284
+ for (const line of lines) {
285
+ if (!line || !line.includes("token_count"))
286
+ continue;
287
+ try {
288
+ const entry = JSON.parse(line);
289
+ if (entry?.type !== "event_msg")
290
+ continue;
291
+ if (entry?.payload?.type !== "token_count")
292
+ continue;
293
+ const info = entry?.payload?.info?.total_token_usage;
294
+ if (info && typeof info === "object")
295
+ usage = info;
296
+ } catch {
297
+ continue;
298
+ }
299
+ }
300
+ if (!usage)
301
+ return null;
302
+ const read = usage.cached_input_tokens || 0;
303
+ const input = usage.input_tokens || 0;
304
+ const write = usage.cache_write_input_tokens || 0;
305
+ const divisor = input + read + write;
306
+ if (divisor <= 0)
307
+ return null;
308
+ return Math.min(1, Math.max(0, read / divisor));
309
+ }
310
+
311
+ // src/providers/opencode.ts
312
+ import { existsSync as existsSync3 } from "fs";
313
+ import { homedir as homedir3 } from "os";
314
+ import { join as join3 } from "path";
315
+ import { Database } from "bun:sqlite";
316
+ function opencodeCacheRate(opts = {}) {
317
+ const dbPath = opts.dbPath ?? join3(homedir3(), ".local", "share", "opencode", "opencode.db");
318
+ if (!existsSync3(dbPath))
319
+ return null;
320
+ let db;
321
+ try {
322
+ db = new Database(dbPath, { readonly: true });
323
+ } catch {
324
+ return null;
325
+ }
326
+ try {
327
+ let row = tokenRow(db, "session", opts.sessionId);
328
+ if (!row)
329
+ row = tokenRow(db, "session_v2", opts.sessionId);
330
+ if (!row)
331
+ return null;
332
+ const input = row.tokens_input ?? 0;
333
+ const read = row.tokens_cache_read ?? 0;
334
+ const write = row.tokens_cache_write ?? 0;
335
+ const divisor = input + read + write;
336
+ if (divisor <= 0)
337
+ return null;
338
+ return Math.min(1, Math.max(0, read / divisor));
339
+ } catch {
340
+ return null;
341
+ } finally {
342
+ db.close();
343
+ }
344
+ }
345
+ function tokenRow(db, table, sessionId) {
346
+ try {
347
+ if (sessionId) {
348
+ return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
349
+ FROM ${table} WHERE id = ? LIMIT 1`).get(sessionId);
350
+ }
351
+ return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
352
+ FROM ${table}
353
+ WHERE tokens_input > 0 OR tokens_cache_read > 0 OR tokens_cache_write > 0
354
+ ORDER BY time_updated DESC LIMIT 1`).get();
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+
360
+ // src/context-window.ts
361
+ function lastUsageBlock(ctx) {
362
+ const path = ctx.transcriptPath;
363
+ if (!path || !existsSync4(path))
364
+ return null;
365
+ let lines;
366
+ try {
367
+ lines = readFileSync3(path, "utf8").split(`
206
368
  `);
207
369
  } catch {
208
370
  return null;
@@ -216,26 +378,48 @@ function contextUsage(ctx) {
216
378
  const usage = entry?.message?.usage;
217
379
  if (!usage || typeof usage.input_tokens !== "number")
218
380
  continue;
219
- const used = usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
220
- return {
221
- used,
222
- output: usage.output_tokens || 0,
223
- window: windowSize(ctx)
224
- };
381
+ return usage;
225
382
  } catch {
226
383
  continue;
227
384
  }
228
385
  }
229
386
  return null;
230
387
  }
388
+ function contextUsage(ctx) {
389
+ const usage = lastUsageBlock(ctx);
390
+ if (!usage)
391
+ return null;
392
+ const used = usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
393
+ return {
394
+ used,
395
+ output: usage.output_tokens || 0,
396
+ window: windowSize(ctx)
397
+ };
398
+ }
399
+ function cacheRate(ctx) {
400
+ if (ctx.provider === "codex")
401
+ return codexCacheRate();
402
+ if (ctx.provider === "opencode")
403
+ return opencodeCacheRate();
404
+ const usage = lastUsageBlock(ctx);
405
+ if (!usage)
406
+ return null;
407
+ const read = usage.cache_read_input_tokens || 0;
408
+ const creation = usage.cache_creation_input_tokens || 0;
409
+ const input = usage.input_tokens || 0;
410
+ const divisor = input + creation + read;
411
+ if (divisor <= 0)
412
+ return null;
413
+ return read / divisor;
414
+ }
231
415
  function windowSize(ctx) {
232
416
  return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
233
417
  }
234
418
 
235
419
  // src/accounts.ts
236
- import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
237
- import { homedir as homedir2 } from "os";
238
- import { join as join2, resolve, sep } from "path";
420
+ import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync } from "fs";
421
+ import { homedir as homedir4 } from "os";
422
+ import { join as join4, resolve, sep } from "path";
239
423
  var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
240
424
  var DEFAULT_CONFIG_SUBDIR = ".claude";
241
425
  var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
@@ -259,10 +443,10 @@ function str(value) {
259
443
  return typeof value === "string" && value.trim() ? value.trim() : null;
260
444
  }
261
445
  function home(env) {
262
- return str(env.HOME) ?? homedir2();
446
+ return str(env.HOME) ?? homedir4();
263
447
  }
264
448
  function expandHome(path, env) {
265
- return path === "~" || path.startsWith(`~${sep}`) ? join2(home(env), path.slice(1)) : path;
449
+ return path === "~" || path.startsWith(`~${sep}`) ? join4(home(env), path.slice(1)) : path;
266
450
  }
267
451
  function canonical(path, env) {
268
452
  const abs = resolve(expandHome(path, env));
@@ -274,7 +458,7 @@ function canonical(path, env) {
274
458
  }
275
459
  function readJson(path) {
276
460
  try {
277
- return JSON.parse(readFileSync3(path, "utf8"));
461
+ return JSON.parse(readFileSync4(path, "utf8"));
278
462
  } catch {
279
463
  return null;
280
464
  }
@@ -295,14 +479,14 @@ async function accountsPaths(env) {
295
479
  return { store: storage.storePath(), profiles: storage.profilesDir() };
296
480
  } catch {}
297
481
  }
298
- const base = homeOverride ?? join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
482
+ const base = homeOverride ?? join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
299
483
  return {
300
- store: storeOverride ?? join2(base, STORE_FILE),
301
- profiles: join2(base, PROFILES_SUBDIR)
484
+ store: storeOverride ?? join4(base, STORE_FILE),
485
+ profiles: join4(base, PROFILES_SUBDIR)
302
486
  };
303
487
  }
304
488
  function registryEntry(store, configDir, env, tool) {
305
- if (!existsSync3(store))
489
+ if (!existsSync5(store))
306
490
  return null;
307
491
  const entries = readJson(store)?.profiles;
308
492
  if (!Array.isArray(entries))
@@ -323,7 +507,7 @@ function registryEntry(store, configDir, env, tool) {
323
507
  return tool ? untyped : first;
324
508
  }
325
509
  function layoutProfile(profilesRoot, configDir, env) {
326
- if (!existsSync3(configDir))
510
+ if (!existsSync5(configDir))
327
511
  return null;
328
512
  const prefix = canonical(profilesRoot, env) + sep;
329
513
  if (!configDir.startsWith(prefix))
@@ -337,7 +521,7 @@ function layoutProfile(profilesRoot, configDir, env) {
337
521
  return { configDir, profile, tool, source: "layout" };
338
522
  }
339
523
  function switchedOccupant(configDir) {
340
- const raw = readJson(join2(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
524
+ const raw = readJson(join4(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
341
525
  if (!raw)
342
526
  return null;
343
527
  const profile = str(raw.profile);
@@ -345,17 +529,17 @@ function switchedOccupant(configDir) {
345
529
  return profile || email ? { profile, email } : null;
346
530
  }
347
531
  function sessionConfigDir(env = process.env) {
348
- return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
532
+ return canonical(str(env[CONFIG_DIR_ENV]) ?? join4(home(env), DEFAULT_CONFIG_SUBDIR), env);
349
533
  }
350
534
  function sessionStateFile(env = process.env) {
351
535
  const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
352
- const preferred = join2(base, STATE_FILE_PREFERRED);
353
- if (existsSync3(preferred))
536
+ const preferred = join4(base, STATE_FILE_PREFERRED);
537
+ if (existsSync5(preferred))
354
538
  return preferred;
355
539
  const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
356
540
  for (const variant of variants) {
357
- const candidate = join2(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
358
- if (existsSync3(candidate))
541
+ const candidate = join4(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
542
+ if (existsSync5(candidate))
359
543
  return candidate;
360
544
  }
361
545
  return null;
@@ -403,7 +587,7 @@ async function sessionAccountEmail(env = process.env, tool) {
403
587
  }
404
588
  function accountsHomeDir(env) {
405
589
  const override = str(env[ACCOUNTS_HOME_ENV]);
406
- return override ? expandHome(override, env) : join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
590
+ return override ? expandHome(override, env) : join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
407
591
  }
408
592
  function sessionAccountUuid(env = process.env) {
409
593
  try {
@@ -476,8 +660,8 @@ function sessionUsage(env = process.env, now = new Date) {
476
660
  const uuid = sessionAccountUuid(env);
477
661
  if (!uuid)
478
662
  return empty;
479
- const path = join2(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
480
- if (!existsSync3(path))
663
+ const path = join4(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
664
+ if (!existsSync5(path))
481
665
  return empty;
482
666
  const entry = readJson(path);
483
667
  if (!entry || entry.accountUuid !== uuid)
@@ -492,11 +676,11 @@ function sessionUsage(env = process.env, now = new Date) {
492
676
  }
493
677
 
494
678
  // src/settings.ts
495
- import { readFileSync as readFileSync4 } from "fs";
496
- import { join as join3 } from "path";
679
+ import { readFileSync as readFileSync5 } from "fs";
680
+ import { join as join5 } from "path";
497
681
  function sessionFastMode(env = process.env) {
498
682
  try {
499
- const raw = readFileSync4(join3(sessionConfigDir(env), "settings.json"), "utf8");
683
+ const raw = readFileSync5(join5(sessionConfigDir(env), "settings.json"), "utf8");
500
684
  return JSON.parse(raw)?.fastMode === true;
501
685
  } catch {
502
686
  return false;
@@ -520,6 +704,29 @@ function friendlyModel(id) {
520
704
  return { name: name || mid, tag };
521
705
  }
522
706
  var dir = (ctx) => ctx.cwd || process.cwd();
707
+ function abbreviateHome(path) {
708
+ const home2 = homedir5();
709
+ if (!home2 || home2 === "/")
710
+ return path;
711
+ if (path === home2)
712
+ return "~";
713
+ return path.startsWith(`${home2}/`) ? `~${path.slice(home2.length)}` : path;
714
+ }
715
+ var CWD_MAX = 40;
716
+ function truncateLeft(path, max) {
717
+ return path.length <= max ? path : `\u2026${path.slice(path.length - (max - 1))}`;
718
+ }
719
+ function contextColor(ctx) {
720
+ const u = contextUsage(ctx);
721
+ if (!u || !u.window)
722
+ return null;
723
+ const pct = Math.min(100, Math.round(u.used / u.window * 100));
724
+ if (pct >= 80)
725
+ return "red";
726
+ if (pct >= 50)
727
+ return "yellow";
728
+ return "green";
729
+ }
523
730
  function modelName(ctx) {
524
731
  if (ctx.model?.displayName)
525
732
  return ctx.model.displayName.toLowerCase();
@@ -528,6 +735,12 @@ function modelName(ctx) {
528
735
  function limitPercent(pct) {
529
736
  return typeof pct === "number" ? pct : 0;
530
737
  }
738
+ function anthropicSession(ctx) {
739
+ if (ctx.rateLimits)
740
+ return true;
741
+ const id = ctx.model?.id;
742
+ return typeof id === "string" && /claude/i.test(id);
743
+ }
531
744
  function usageLabel(prefix, headroom) {
532
745
  return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
533
746
  }
@@ -597,10 +810,23 @@ var segments = [
597
810
  defaultEnabled: false,
598
811
  render: (ctx) => dir(ctx).split("/").pop() || null
599
812
  },
813
+ {
814
+ id: "cwd",
815
+ description: "Current working directory (full path)",
816
+ defaultEnabled: false,
817
+ color: "blue",
818
+ render(ctx) {
819
+ const path = ctx.cwd;
820
+ if (typeof path !== "string" || !path)
821
+ return null;
822
+ return truncateLeft(abbreviateHome(path), CWD_MAX);
823
+ }
824
+ },
600
825
  {
601
826
  id: "model",
602
827
  description: "Current model name",
603
828
  defaultEnabled: false,
829
+ color: "dim",
604
830
  render(ctx) {
605
831
  if (!ctx.model?.id)
606
832
  return null;
@@ -611,6 +837,7 @@ var segments = [
611
837
  id: "model-context",
612
838
  description: "Model name with context-size tag (e.g. [1m])",
613
839
  defaultEnabled: true,
840
+ color: "dim",
614
841
  render(ctx) {
615
842
  if (!ctx.model?.id)
616
843
  return null;
@@ -622,7 +849,7 @@ var segments = [
622
849
  id: "model-with-reasoning",
623
850
  description: "Model name with its reasoning effort (or thinking, when on)",
624
851
  defaultEnabled: false,
625
- color: "cyan",
852
+ color: "dim",
626
853
  render(ctx) {
627
854
  const name = modelName(ctx);
628
855
  if (!name)
@@ -682,15 +909,15 @@ var segments = [
682
909
  id: "usage-session",
683
910
  description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
684
911
  defaultEnabled: false,
685
- color: () => usageColor(sessionUsage(process.env).sessionHeadroom),
686
- render: () => usageLabel("5h", sessionUsage(process.env).sessionHeadroom)
912
+ color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).sessionHeadroom) : null,
913
+ render: (ctx) => anthropicSession(ctx) ? usageLabel("5h", sessionUsage(process.env).sessionHeadroom) : null
687
914
  },
688
915
  {
689
916
  id: "usage-weekly",
690
917
  description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
691
918
  defaultEnabled: false,
692
- color: () => usageColor(sessionUsage(process.env).weeklyHeadroom, true),
693
- render: () => usageLabel("7d", sessionUsage(process.env).weeklyHeadroom)
919
+ color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).weeklyHeadroom, true) : null,
920
+ render: (ctx) => anthropicSession(ctx) ? usageLabel("7d", sessionUsage(process.env).weeklyHeadroom) : null
694
921
  },
695
922
  {
696
923
  id: "thread-title",
@@ -703,17 +930,19 @@ var segments = [
703
930
  id: "context-used",
704
931
  description: "Percentage of context window used (omitted when unknown)",
705
932
  defaultEnabled: false,
933
+ color: contextColor,
706
934
  render(ctx) {
707
935
  const u = contextUsage(ctx);
708
936
  if (!u)
709
937
  return null;
710
- return `${Math.min(100, Math.round(u.used / u.window * 100))}%`;
938
+ return `ctx ${Math.min(100, Math.round(u.used / u.window * 100))}%`;
711
939
  }
712
940
  },
713
941
  {
714
942
  id: "context-remaining",
715
943
  description: "Percentage of context window remaining (omitted when unknown)",
716
944
  defaultEnabled: true,
945
+ color: contextColor,
717
946
  render(ctx) {
718
947
  const u = contextUsage(ctx);
719
948
  if (!u)
@@ -732,10 +961,20 @@ var segments = [
732
961
  return `${compactNum(u.used + u.output)} tok`;
733
962
  }
734
963
  },
964
+ {
965
+ id: "cache-rate",
966
+ description: "Percentage of input tokens served from cache (omitted when unknown)",
967
+ defaultEnabled: true,
968
+ render(ctx) {
969
+ const r = cacheRate(ctx);
970
+ return r === null ? null : `${Math.round(r * 100)}% cached`;
971
+ }
972
+ },
735
973
  {
736
974
  id: "cost",
737
975
  description: "Session cost in USD (omitted when zero)",
738
976
  defaultEnabled: true,
977
+ color: "dim",
739
978
  render(ctx) {
740
979
  const c = ctx.cost?.totalCostUsd;
741
980
  return c ? money(c) : null;
@@ -745,6 +984,7 @@ var segments = [
745
984
  id: "duration",
746
985
  description: "Session wall-clock duration",
747
986
  defaultEnabled: false,
987
+ color: "dim",
748
988
  render(ctx) {
749
989
  const ms = ctx.cost?.totalDurationMs;
750
990
  return ms ? compactDuration(ms) : null;
@@ -839,7 +1079,7 @@ async function renderLine(ctx, cfg) {
839
1079
  // package.json
840
1080
  var package_default = {
841
1081
  name: "@hasna/statusline",
842
- version: "0.0.7",
1082
+ version: "0.0.8",
843
1083
  description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
844
1084
  license: "Apache-2.0",
845
1085
  type: "module",
@@ -878,11 +1118,12 @@ var package_default = {
878
1118
  },
879
1119
  repository: {
880
1120
  type: "git",
881
- url: "git+https://github.com/hasna/statusline.git"
1121
+ url: "https://github.com/hasna/apps.git",
1122
+ directory: "apps/statusline"
882
1123
  },
883
- homepage: "https://github.com/hasna/statusline#readme",
1124
+ homepage: "https://github.com/hasna/apps/tree/main/apps/statusline#readme",
884
1125
  bugs: {
885
- url: "https://github.com/hasna/statusline/issues"
1126
+ url: "https://github.com/hasna/apps/issues"
886
1127
  },
887
1128
  keywords: [
888
1129
  "statusline",
@@ -899,36 +1140,37 @@ var package_default = {
899
1140
  bun: ">=1.0.0"
900
1141
  },
901
1142
  dependencies: {
902
- "@modelcontextprotocol/sdk": "1.12.1",
1143
+ "@modelcontextprotocol/sdk": "^1.26.0",
903
1144
  zod: "3.25.76"
904
1145
  },
905
1146
  devDependencies: {
906
- "@types/bun": "^1.2.4",
907
- typescript: "5.9.3"
1147
+ "@types/bun": "1.3.14",
1148
+ typescript: "5.9.3",
1149
+ "bun-types": "1.3.14"
908
1150
  }
909
1151
  };
910
1152
  // src/install.ts
911
- import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
912
- import { homedir as homedir3 } from "os";
913
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
1153
+ import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1154
+ import { homedir as homedir6 } from "os";
1155
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
914
1156
  function defaultSettingsPath(env) {
915
- return join4(env.HOME || homedir3(), ".claude", "settings.json");
1157
+ return join6(env.HOME || homedir6(), ".claude", "settings.json");
916
1158
  }
917
1159
  function claudeSettingsPath(env = process.env) {
918
- const home2 = env.HOME || homedir3();
1160
+ const home2 = env.HOME || homedir6();
919
1161
  const raw = env.CLAUDE_CONFIG_DIR?.trim();
920
1162
  if (!raw)
921
1163
  return defaultSettingsPath(env);
922
- const configDir = raw === "~" || raw.startsWith("~/") ? join4(home2, raw.slice(1)) : raw;
923
- return join4(configDir, "settings.json");
1164
+ const configDir = raw === "~" || raw.startsWith("~/") ? join6(home2, raw.slice(1)) : raw;
1165
+ return join6(configDir, "settings.json");
924
1166
  }
925
1167
  function installClaude(settingsPath, env = process.env) {
926
1168
  const target = settingsPath ?? claudeSettingsPath(env);
927
1169
  const binary = Bun.which("statusline") || "statusline";
928
1170
  let settings = {};
929
- if (existsSync4(target)) {
1171
+ if (existsSync6(target)) {
930
1172
  try {
931
- settings = JSON.parse(readFileSync5(target, "utf8"));
1173
+ settings = JSON.parse(readFileSync6(target, "utf8"));
932
1174
  } catch (error) {
933
1175
  throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
934
1176
  }
@@ -936,7 +1178,7 @@ function installClaude(settingsPath, env = process.env) {
936
1178
  }
937
1179
  settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
938
1180
  const configDir = dirname2(target);
939
- if (!existsSync4(configDir)) {
1181
+ if (!existsSync6(configDir)) {
940
1182
  if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
941
1183
  throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
942
1184
  }
@@ -1003,6 +1245,14 @@ function resetStatuslineConfig() {
1003
1245
  return defaultConfig();
1004
1246
  }
1005
1247
  async function renderStatusline(input = {}, config = loadConfig()) {
1248
+ const provider = process.env.STATUSLINE_PROVIDER;
1249
+ if (provider === "codex" || provider === "opencode") {
1250
+ const ctx = {
1251
+ provider,
1252
+ cwd: typeof input.cwd === "string" ? input.cwd : process.cwd()
1253
+ };
1254
+ return renderLine(ctx, config);
1255
+ }
1006
1256
  return renderLine(parseClaudeInput(input), config);
1007
1257
  }
1008
1258
  function sampleClaudeInput(cwd = process.cwd()) {
@@ -7,8 +7,30 @@ export interface ContextUsage {
7
7
  /** Context window size in tokens. */
8
8
  window: number;
9
9
  }
10
+ /** The raw `message.usage` object of the last assistant entry that carries one. */
11
+ export interface UsageBlock {
12
+ input_tokens: number;
13
+ cache_creation_input_tokens?: number;
14
+ cache_read_input_tokens?: number;
15
+ output_tokens?: number;
16
+ }
17
+ /**
18
+ * Scan the session transcript (JSONL) from the end and return the raw usage
19
+ * block of the last assistant entry that carries one — the block that
20
+ * reflects what the context window currently holds. Lines that are malformed
21
+ * or whose usage block lacks a numeric `input_tokens` are skipped, exactly
22
+ * like `contextUsage` used to skip them inline.
23
+ */
24
+ export declare function lastUsageBlock(ctx: StatusContext): UsageBlock | null;
10
25
  /**
11
26
  * Derive context usage from the session transcript (JSONL). The last
12
27
  * assistant entry's usage block reflects what the context window holds.
13
28
  */
14
29
  export declare function contextUsage(ctx: StatusContext): ContextUsage | null;
30
+ /**
31
+ * Fraction of input-side tokens served from cache, dispatched on the
32
+ * context's provider: the Claude transcript usage block, the Codex session
33
+ * rollout's latest token_count, or the OpenCode sessions DB. Null when the
34
+ * source is absent or the divisor is zero — never a wrong number.
35
+ */
36
+ export declare function cacheRate(ctx: StatusContext): number | null;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { type StatuslineConfig } from "./config.js";
2
2
  export { sessionAccount, sessionAccountEmail, sessionConfigDir, sessionStateFile, type SessionAccount, } from "./accounts.js";
3
3
  export { defaultConfig, loadConfig, saveConfig, configPath, type StatuslineConfig } from "./config.js";
4
- export { contextUsage, type ContextUsage } from "./context-window.js";
4
+ export { cacheRate, contextUsage, lastUsageBlock, type ContextUsage, type UsageBlock } from "./context-window.js";
5
5
  export { compactAge, compactDuration, compactNum, money } from "./format.js";
6
6
  export { gitBranch, gitProjectName, gitRoot, lastCommitEpoch, trackedLineCount } from "./git.js";
7
7
  export { claudeSettingsPath, installClaude } from "./install.js";
8
+ export { codexCacheRate, latestCodexSessionPath, type TotalTokenUsage } from "./providers/codex.js";
9
+ export { opencodeCacheRate, type OpenCodeCacheRateOptions } from "./providers/opencode.js";
8
10
  export { parseClaudeInput } from "./providers/claude.js";
9
11
  export type { Segment, SegmentColor, StatusContext } from "./providers/types.js";
10
12
  export { renderLine } from "./render.js";