@hasna/statusline 0.0.7 → 0.0.9

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) {
@@ -126,6 +157,23 @@ function compactNum(n) {
126
157
  return `${trimmed(n / 1000)}k`;
127
158
  return `${n}`;
128
159
  }
160
+ var BYTE_UNITS = ["K", "M", "G", "T", "P"];
161
+ function compactBytes(bytes) {
162
+ if (!Number.isFinite(bytes) || bytes < 0)
163
+ return "";
164
+ if (bytes < 1024)
165
+ return `${Math.floor(bytes)}B`;
166
+ let value = bytes / 1024;
167
+ let unit = 0;
168
+ while (unit < BYTE_UNITS.length - 1) {
169
+ const rounded = Number(value.toFixed(1));
170
+ if (rounded < 1024)
171
+ break;
172
+ value /= 1024;
173
+ unit += 1;
174
+ }
175
+ return `${trimmed(value)}${BYTE_UNITS[unit]}`;
176
+ }
129
177
  function money(n) {
130
178
  return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
131
179
  }
@@ -135,10 +183,10 @@ function compactDuration(ms) {
135
183
  return `${s}s`;
136
184
  if (s < 3600) {
137
185
  const rem2 = s % 60;
138
- return rem2 ? `${Math.floor(s / 60)}m${rem2}s` : `${Math.floor(s / 60)}m`;
186
+ return rem2 ? `${Math.floor(s / 60)}m ${rem2}s` : `${Math.floor(s / 60)}m`;
139
187
  }
140
188
  const rem = Math.floor(s % 3600 / 60);
141
- return rem ? `${Math.floor(s / 3600)}h${rem}m` : `${Math.floor(s / 3600)}h`;
189
+ return rem ? `${Math.floor(s / 3600)}h ${rem}m` : `${Math.floor(s / 3600)}h`;
142
190
  }
143
191
 
144
192
  // src/git.ts
@@ -195,14 +243,145 @@ function trackedLineCount(cwd) {
195
243
  }
196
244
 
197
245
  // src/context-window.ts
198
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
199
- function contextUsage(ctx) {
200
- const path = ctx.transcriptPath;
246
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
247
+
248
+ // src/providers/codex.ts
249
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
250
+ import { homedir as homedir2 } from "os";
251
+ import { join as join2 } from "path";
252
+ function latestCodexSessionPath(home = homedir2()) {
253
+ const root = join2(home, ".codex", "sessions");
254
+ if (!existsSync2(root))
255
+ return null;
256
+ let newest = null;
257
+ let newestMtime = 0;
258
+ try {
259
+ for (const year of readdirSync(root)) {
260
+ const yearDir = join2(root, year);
261
+ if (!statSync(yearDir, { throwIfNoEntry: false })?.isDirectory())
262
+ continue;
263
+ for (const month of readdirSync(yearDir)) {
264
+ const monthDir = join2(yearDir, month);
265
+ if (!statSync(monthDir, { throwIfNoEntry: false })?.isDirectory())
266
+ continue;
267
+ for (const day of readdirSync(monthDir)) {
268
+ const dayDir = join2(monthDir, day);
269
+ if (!statSync(dayDir, { throwIfNoEntry: false })?.isDirectory())
270
+ continue;
271
+ for (const file of readdirSync(dayDir)) {
272
+ if (!file.startsWith("rollout-") || !file.endsWith(".jsonl"))
273
+ continue;
274
+ const full = join2(dayDir, file);
275
+ const mtime = statSync(full, { throwIfNoEntry: false })?.mtimeMs ?? 0;
276
+ if (mtime > newestMtime) {
277
+ newestMtime = mtime;
278
+ newest = full;
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ } catch {
285
+ return null;
286
+ }
287
+ return newest;
288
+ }
289
+ function codexCacheRate(sessionPath) {
290
+ const path = sessionPath ?? latestCodexSessionPath();
201
291
  if (!path || !existsSync2(path))
202
292
  return null;
203
293
  let lines;
204
294
  try {
205
295
  lines = readFileSync2(path, "utf8").split(`
296
+ `);
297
+ } catch {
298
+ return null;
299
+ }
300
+ let usage = null;
301
+ for (const line of lines) {
302
+ if (!line || !line.includes("token_count"))
303
+ continue;
304
+ try {
305
+ const entry = JSON.parse(line);
306
+ if (entry?.type !== "event_msg")
307
+ continue;
308
+ if (entry?.payload?.type !== "token_count")
309
+ continue;
310
+ const info = entry?.payload?.info?.total_token_usage;
311
+ if (info && typeof info === "object")
312
+ usage = info;
313
+ } catch {
314
+ continue;
315
+ }
316
+ }
317
+ if (!usage)
318
+ return null;
319
+ const read = usage.cached_input_tokens || 0;
320
+ const input = usage.input_tokens || 0;
321
+ const write = usage.cache_write_input_tokens || 0;
322
+ const divisor = input + read + write;
323
+ if (divisor <= 0)
324
+ return null;
325
+ return Math.min(1, Math.max(0, read / divisor));
326
+ }
327
+
328
+ // src/providers/opencode.ts
329
+ import { existsSync as existsSync3 } from "fs";
330
+ import { homedir as homedir3 } from "os";
331
+ import { join as join3 } from "path";
332
+ import { Database } from "bun:sqlite";
333
+ function opencodeCacheRate(opts = {}) {
334
+ const dbPath = opts.dbPath ?? join3(homedir3(), ".local", "share", "opencode", "opencode.db");
335
+ if (!existsSync3(dbPath))
336
+ return null;
337
+ let db;
338
+ try {
339
+ db = new Database(dbPath, { readonly: true });
340
+ } catch {
341
+ return null;
342
+ }
343
+ try {
344
+ let row = tokenRow(db, "session", opts.sessionId);
345
+ if (!row)
346
+ row = tokenRow(db, "session_v2", opts.sessionId);
347
+ if (!row)
348
+ return null;
349
+ const input = row.tokens_input ?? 0;
350
+ const read = row.tokens_cache_read ?? 0;
351
+ const write = row.tokens_cache_write ?? 0;
352
+ const divisor = input + read + write;
353
+ if (divisor <= 0)
354
+ return null;
355
+ return Math.min(1, Math.max(0, read / divisor));
356
+ } catch {
357
+ return null;
358
+ } finally {
359
+ db.close();
360
+ }
361
+ }
362
+ function tokenRow(db, table, sessionId) {
363
+ try {
364
+ if (sessionId) {
365
+ return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
366
+ FROM ${table} WHERE id = ? LIMIT 1`).get(sessionId);
367
+ }
368
+ return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
369
+ FROM ${table}
370
+ WHERE tokens_input > 0 OR tokens_cache_read > 0 OR tokens_cache_write > 0
371
+ ORDER BY time_updated DESC LIMIT 1`).get();
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+
377
+ // src/context-window.ts
378
+ function lastUsageBlock(ctx) {
379
+ const path = ctx.transcriptPath;
380
+ if (!path || !existsSync4(path))
381
+ return null;
382
+ let lines;
383
+ try {
384
+ lines = readFileSync3(path, "utf8").split(`
206
385
  `);
207
386
  } catch {
208
387
  return null;
@@ -216,26 +395,137 @@ function contextUsage(ctx) {
216
395
  const usage = entry?.message?.usage;
217
396
  if (!usage || typeof usage.input_tokens !== "number")
218
397
  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
- };
398
+ return usage;
225
399
  } catch {
226
400
  continue;
227
401
  }
228
402
  }
229
403
  return null;
230
404
  }
405
+ function contextUsage(ctx) {
406
+ const usage = lastUsageBlock(ctx);
407
+ if (!usage)
408
+ return null;
409
+ const used = usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
410
+ return {
411
+ used,
412
+ output: usage.output_tokens || 0,
413
+ window: windowSize(ctx)
414
+ };
415
+ }
416
+ function cacheRate(ctx) {
417
+ if (ctx.provider === "codex")
418
+ return codexCacheRate();
419
+ if (ctx.provider === "opencode")
420
+ return opencodeCacheRate();
421
+ const usage = lastUsageBlock(ctx);
422
+ if (!usage)
423
+ return null;
424
+ const read = usage.cache_read_input_tokens || 0;
425
+ const creation = usage.cache_creation_input_tokens || 0;
426
+ const input = usage.input_tokens || 0;
427
+ const divisor = input + creation + read;
428
+ if (divisor <= 0)
429
+ return null;
430
+ return read / divisor;
431
+ }
231
432
  function windowSize(ctx) {
232
433
  return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
233
434
  }
234
435
 
436
+ // src/disk.ts
437
+ import { statfsSync } from "fs";
438
+ var DISK_MEMO = Symbol("statusline.disk-free");
439
+ var DISK_WARN_PERCENT = 20;
440
+ var DISK_CRITICAL_PERCENT = 10;
441
+ function finiteNonNegative(value) {
442
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
443
+ }
444
+ function readDiskStats(path) {
445
+ if (typeof statfsSync !== "function")
446
+ return null;
447
+ try {
448
+ const stats = statfsSync(path);
449
+ if (!stats)
450
+ return null;
451
+ return {
452
+ bsize: Number(stats.bsize),
453
+ blocks: Number(stats.blocks),
454
+ bavail: Number(stats.bavail)
455
+ };
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ function diskFree(path, reader = readDiskStats) {
461
+ let stats;
462
+ try {
463
+ stats = reader(path);
464
+ } catch {
465
+ return null;
466
+ }
467
+ if (!stats)
468
+ return null;
469
+ const bsize = finiteNonNegative(stats.bsize);
470
+ const blocks = finiteNonNegative(stats.blocks);
471
+ const bavail = finiteNonNegative(stats.bavail);
472
+ if (bsize === null || blocks === null || bavail === null)
473
+ return null;
474
+ if (bsize <= 0 || blocks <= 0)
475
+ return null;
476
+ const available = Math.min(bavail, blocks);
477
+ const totalBytes = bsize * blocks;
478
+ const freeBytes = bsize * available;
479
+ if (!Number.isFinite(totalBytes) || !Number.isFinite(freeBytes))
480
+ return null;
481
+ return {
482
+ freeBytes,
483
+ totalBytes,
484
+ freePercent: available / blocks * 100
485
+ };
486
+ }
487
+ function diskFreeFor(path, memo, reader = readDiskStats) {
488
+ if (!memo)
489
+ return diskFree(path, reader);
490
+ const hit = memo.get(DISK_MEMO);
491
+ if (hit && hit.path === path)
492
+ return hit.value;
493
+ const value = diskFree(path, reader);
494
+ memo.set(DISK_MEMO, { path, value });
495
+ return value;
496
+ }
497
+ function diskLevel(freePercent) {
498
+ if (!Number.isFinite(freePercent))
499
+ return null;
500
+ if (freePercent < DISK_CRITICAL_PERCENT)
501
+ return "critical";
502
+ if (freePercent < DISK_WARN_PERCENT)
503
+ return "low";
504
+ return null;
505
+ }
506
+ function formatDiskFree(disk) {
507
+ const value = compactBytes(disk.freeBytes);
508
+ if (!value)
509
+ return "";
510
+ return diskLevel(disk.freePercent) ? `disk ${value} low` : `disk ${value}`;
511
+ }
512
+ function diskColor(disk) {
513
+ if (!disk)
514
+ return null;
515
+ switch (diskLevel(disk.freePercent)) {
516
+ case "critical":
517
+ return "red";
518
+ case "low":
519
+ return "yellow";
520
+ default:
521
+ return "dim";
522
+ }
523
+ }
524
+
235
525
  // 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";
526
+ import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync } from "fs";
527
+ import { homedir as homedir4 } from "os";
528
+ import { join as join4, resolve, sep } from "path";
239
529
  var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
240
530
  var DEFAULT_CONFIG_SUBDIR = ".claude";
241
531
  var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
@@ -259,10 +549,10 @@ function str(value) {
259
549
  return typeof value === "string" && value.trim() ? value.trim() : null;
260
550
  }
261
551
  function home(env) {
262
- return str(env.HOME) ?? homedir2();
552
+ return str(env.HOME) ?? homedir4();
263
553
  }
264
554
  function expandHome(path, env) {
265
- return path === "~" || path.startsWith(`~${sep}`) ? join2(home(env), path.slice(1)) : path;
555
+ return path === "~" || path.startsWith(`~${sep}`) ? join4(home(env), path.slice(1)) : path;
266
556
  }
267
557
  function canonical(path, env) {
268
558
  const abs = resolve(expandHome(path, env));
@@ -274,7 +564,7 @@ function canonical(path, env) {
274
564
  }
275
565
  function readJson(path) {
276
566
  try {
277
- return JSON.parse(readFileSync3(path, "utf8"));
567
+ return JSON.parse(readFileSync4(path, "utf8"));
278
568
  } catch {
279
569
  return null;
280
570
  }
@@ -295,14 +585,14 @@ async function accountsPaths(env) {
295
585
  return { store: storage.storePath(), profiles: storage.profilesDir() };
296
586
  } catch {}
297
587
  }
298
- const base = homeOverride ?? join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
588
+ const base = homeOverride ?? join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
299
589
  return {
300
- store: storeOverride ?? join2(base, STORE_FILE),
301
- profiles: join2(base, PROFILES_SUBDIR)
590
+ store: storeOverride ?? join4(base, STORE_FILE),
591
+ profiles: join4(base, PROFILES_SUBDIR)
302
592
  };
303
593
  }
304
594
  function registryEntry(store, configDir, env, tool) {
305
- if (!existsSync3(store))
595
+ if (!existsSync5(store))
306
596
  return null;
307
597
  const entries = readJson(store)?.profiles;
308
598
  if (!Array.isArray(entries))
@@ -323,7 +613,7 @@ function registryEntry(store, configDir, env, tool) {
323
613
  return tool ? untyped : first;
324
614
  }
325
615
  function layoutProfile(profilesRoot, configDir, env) {
326
- if (!existsSync3(configDir))
616
+ if (!existsSync5(configDir))
327
617
  return null;
328
618
  const prefix = canonical(profilesRoot, env) + sep;
329
619
  if (!configDir.startsWith(prefix))
@@ -337,7 +627,7 @@ function layoutProfile(profilesRoot, configDir, env) {
337
627
  return { configDir, profile, tool, source: "layout" };
338
628
  }
339
629
  function switchedOccupant(configDir) {
340
- const raw = readJson(join2(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
630
+ const raw = readJson(join4(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
341
631
  if (!raw)
342
632
  return null;
343
633
  const profile = str(raw.profile);
@@ -345,17 +635,17 @@ function switchedOccupant(configDir) {
345
635
  return profile || email ? { profile, email } : null;
346
636
  }
347
637
  function sessionConfigDir(env = process.env) {
348
- return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
638
+ return canonical(str(env[CONFIG_DIR_ENV]) ?? join4(home(env), DEFAULT_CONFIG_SUBDIR), env);
349
639
  }
350
640
  function sessionStateFile(env = process.env) {
351
641
  const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
352
- const preferred = join2(base, STATE_FILE_PREFERRED);
353
- if (existsSync3(preferred))
642
+ const preferred = join4(base, STATE_FILE_PREFERRED);
643
+ if (existsSync5(preferred))
354
644
  return preferred;
355
645
  const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
356
646
  for (const variant of variants) {
357
- const candidate = join2(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
358
- if (existsSync3(candidate))
647
+ const candidate = join4(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
648
+ if (existsSync5(candidate))
359
649
  return candidate;
360
650
  }
361
651
  return null;
@@ -403,7 +693,7 @@ async function sessionAccountEmail(env = process.env, tool) {
403
693
  }
404
694
  function accountsHomeDir(env) {
405
695
  const override = str(env[ACCOUNTS_HOME_ENV]);
406
- return override ? expandHome(override, env) : join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
696
+ return override ? expandHome(override, env) : join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
407
697
  }
408
698
  function sessionAccountUuid(env = process.env) {
409
699
  try {
@@ -476,8 +766,8 @@ function sessionUsage(env = process.env, now = new Date) {
476
766
  const uuid = sessionAccountUuid(env);
477
767
  if (!uuid)
478
768
  return empty;
479
- const path = join2(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
480
- if (!existsSync3(path))
769
+ const path = join4(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
770
+ if (!existsSync5(path))
481
771
  return empty;
482
772
  const entry = readJson(path);
483
773
  if (!entry || entry.accountUuid !== uuid)
@@ -492,11 +782,11 @@ function sessionUsage(env = process.env, now = new Date) {
492
782
  }
493
783
 
494
784
  // src/settings.ts
495
- import { readFileSync as readFileSync4 } from "fs";
496
- import { join as join3 } from "path";
785
+ import { readFileSync as readFileSync5 } from "fs";
786
+ import { join as join5 } from "path";
497
787
  function sessionFastMode(env = process.env) {
498
788
  try {
499
- const raw = readFileSync4(join3(sessionConfigDir(env), "settings.json"), "utf8");
789
+ const raw = readFileSync5(join5(sessionConfigDir(env), "settings.json"), "utf8");
500
790
  return JSON.parse(raw)?.fastMode === true;
501
791
  } catch {
502
792
  return false;
@@ -520,6 +810,29 @@ function friendlyModel(id) {
520
810
  return { name: name || mid, tag };
521
811
  }
522
812
  var dir = (ctx) => ctx.cwd || process.cwd();
813
+ function abbreviateHome(path) {
814
+ const home2 = homedir5();
815
+ if (!home2 || home2 === "/")
816
+ return path;
817
+ if (path === home2)
818
+ return "~";
819
+ return path.startsWith(`${home2}/`) ? `~${path.slice(home2.length)}` : path;
820
+ }
821
+ var CWD_MAX = 40;
822
+ function truncateLeft(path, max) {
823
+ return path.length <= max ? path : `\u2026${path.slice(path.length - (max - 1))}`;
824
+ }
825
+ function contextColor(ctx) {
826
+ const u = contextUsage(ctx);
827
+ if (!u || !u.window)
828
+ return null;
829
+ const pct = Math.min(100, Math.round(u.used / u.window * 100));
830
+ if (pct >= 80)
831
+ return "red";
832
+ if (pct >= 50)
833
+ return "yellow";
834
+ return "green";
835
+ }
523
836
  function modelName(ctx) {
524
837
  if (ctx.model?.displayName)
525
838
  return ctx.model.displayName.toLowerCase();
@@ -528,6 +841,12 @@ function modelName(ctx) {
528
841
  function limitPercent(pct) {
529
842
  return typeof pct === "number" ? pct : 0;
530
843
  }
844
+ function anthropicSession(ctx) {
845
+ if (ctx.rateLimits)
846
+ return true;
847
+ const id = ctx.model?.id;
848
+ return typeof id === "string" && /claude/i.test(id);
849
+ }
531
850
  function usageLabel(prefix, headroom) {
532
851
  return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
533
852
  }
@@ -591,16 +910,39 @@ var segments = [
591
910
  return n ? compactNum(n) : null;
592
911
  }
593
912
  },
913
+ {
914
+ id: "disk-free",
915
+ description: "Free space on the volume holding the working directory",
916
+ defaultEnabled: false,
917
+ color: (ctx, memo) => diskColor(diskFreeFor(dir(ctx), memo)),
918
+ render(ctx, memo) {
919
+ const disk = diskFreeFor(dir(ctx), memo);
920
+ return disk ? formatDiskFree(disk) || null : null;
921
+ }
922
+ },
594
923
  {
595
924
  id: "current-dir",
596
925
  description: "Current working directory (basename)",
597
926
  defaultEnabled: false,
598
927
  render: (ctx) => dir(ctx).split("/").pop() || null
599
928
  },
929
+ {
930
+ id: "cwd",
931
+ description: "Current working directory (full path)",
932
+ defaultEnabled: false,
933
+ color: "blue",
934
+ render(ctx) {
935
+ const path = ctx.cwd;
936
+ if (typeof path !== "string" || !path)
937
+ return null;
938
+ return truncateLeft(abbreviateHome(path), CWD_MAX);
939
+ }
940
+ },
600
941
  {
601
942
  id: "model",
602
943
  description: "Current model name",
603
944
  defaultEnabled: false,
945
+ color: "dim",
604
946
  render(ctx) {
605
947
  if (!ctx.model?.id)
606
948
  return null;
@@ -611,6 +953,7 @@ var segments = [
611
953
  id: "model-context",
612
954
  description: "Model name with context-size tag (e.g. [1m])",
613
955
  defaultEnabled: true,
956
+ color: "dim",
614
957
  render(ctx) {
615
958
  if (!ctx.model?.id)
616
959
  return null;
@@ -622,7 +965,7 @@ var segments = [
622
965
  id: "model-with-reasoning",
623
966
  description: "Model name with its reasoning effort (or thinking, when on)",
624
967
  defaultEnabled: false,
625
- color: "cyan",
968
+ color: "dim",
626
969
  render(ctx) {
627
970
  const name = modelName(ctx);
628
971
  if (!name)
@@ -682,15 +1025,15 @@ var segments = [
682
1025
  id: "usage-session",
683
1026
  description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
684
1027
  defaultEnabled: false,
685
- color: () => usageColor(sessionUsage(process.env).sessionHeadroom),
686
- render: () => usageLabel("5h", sessionUsage(process.env).sessionHeadroom)
1028
+ color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).sessionHeadroom) : null,
1029
+ render: (ctx) => anthropicSession(ctx) ? usageLabel("5h", sessionUsage(process.env).sessionHeadroom) : null
687
1030
  },
688
1031
  {
689
1032
  id: "usage-weekly",
690
1033
  description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
691
1034
  defaultEnabled: false,
692
- color: () => usageColor(sessionUsage(process.env).weeklyHeadroom, true),
693
- render: () => usageLabel("7d", sessionUsage(process.env).weeklyHeadroom)
1035
+ color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).weeklyHeadroom, true) : null,
1036
+ render: (ctx) => anthropicSession(ctx) ? usageLabel("7d", sessionUsage(process.env).weeklyHeadroom) : null
694
1037
  },
695
1038
  {
696
1039
  id: "thread-title",
@@ -703,17 +1046,19 @@ var segments = [
703
1046
  id: "context-used",
704
1047
  description: "Percentage of context window used (omitted when unknown)",
705
1048
  defaultEnabled: false,
1049
+ color: contextColor,
706
1050
  render(ctx) {
707
1051
  const u = contextUsage(ctx);
708
1052
  if (!u)
709
1053
  return null;
710
- return `${Math.min(100, Math.round(u.used / u.window * 100))}%`;
1054
+ return `ctx ${Math.min(100, Math.round(u.used / u.window * 100))}%`;
711
1055
  }
712
1056
  },
713
1057
  {
714
1058
  id: "context-remaining",
715
1059
  description: "Percentage of context window remaining (omitted when unknown)",
716
1060
  defaultEnabled: true,
1061
+ color: contextColor,
717
1062
  render(ctx) {
718
1063
  const u = contextUsage(ctx);
719
1064
  if (!u)
@@ -732,10 +1077,20 @@ var segments = [
732
1077
  return `${compactNum(u.used + u.output)} tok`;
733
1078
  }
734
1079
  },
1080
+ {
1081
+ id: "cache-rate",
1082
+ description: "Percentage of input tokens served from cache (omitted when unknown)",
1083
+ defaultEnabled: true,
1084
+ render(ctx) {
1085
+ const r = cacheRate(ctx);
1086
+ return r === null ? null : `${Math.round(r * 100)}% cached`;
1087
+ }
1088
+ },
735
1089
  {
736
1090
  id: "cost",
737
1091
  description: "Session cost in USD (omitted when zero)",
738
1092
  defaultEnabled: true,
1093
+ color: "dim",
739
1094
  render(ctx) {
740
1095
  const c = ctx.cost?.totalCostUsd;
741
1096
  return c ? money(c) : null;
@@ -745,6 +1100,7 @@ var segments = [
745
1100
  id: "duration",
746
1101
  description: "Session wall-clock duration",
747
1102
  defaultEnabled: false,
1103
+ color: "dim",
748
1104
  render(ctx) {
749
1105
  const ms = ctx.cost?.totalDurationMs;
750
1106
  return ms ? compactDuration(ms) : null;
@@ -811,13 +1167,14 @@ function colorsEnabled(cfg, env = process.env) {
811
1167
  return false;
812
1168
  return cfg.colors !== false;
813
1169
  }
814
- function paint(segment, text, ctx) {
815
- const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
1170
+ function paint(segment, text, ctx, memo) {
1171
+ const color = typeof segment.color === "function" ? segment.color(ctx, memo) : segment.color;
816
1172
  const code = color ? ANSI[color] : undefined;
817
1173
  return code ? `${code}${text}${RESET}` : text;
818
1174
  }
819
1175
  async function renderLine(ctx, cfg) {
820
1176
  const colorize = colorsEnabled(cfg);
1177
+ const memo = new Map;
821
1178
  const rows = [[]];
822
1179
  for (const id of cfg.segments) {
823
1180
  if (id === "newline") {
@@ -828,9 +1185,9 @@ async function renderLine(ctx, cfg) {
828
1185
  if (!segment)
829
1186
  continue;
830
1187
  try {
831
- const out = await segment.render(ctx);
1188
+ const out = await segment.render(ctx, memo);
832
1189
  if (out)
833
- rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
1190
+ rows[rows.length - 1].push(colorize ? paint(segment, out, ctx, memo) : out);
834
1191
  } catch {}
835
1192
  }
836
1193
  return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
@@ -839,7 +1196,7 @@ async function renderLine(ctx, cfg) {
839
1196
  // package.json
840
1197
  var package_default = {
841
1198
  name: "@hasna/statusline",
842
- version: "0.0.7",
1199
+ version: "0.0.9",
843
1200
  description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
844
1201
  license: "Apache-2.0",
845
1202
  type: "module",
@@ -878,11 +1235,12 @@ var package_default = {
878
1235
  },
879
1236
  repository: {
880
1237
  type: "git",
881
- url: "git+https://github.com/hasna/statusline.git"
1238
+ url: "https://github.com/hasna/apps.git",
1239
+ directory: "apps/statusline"
882
1240
  },
883
- homepage: "https://github.com/hasna/statusline#readme",
1241
+ homepage: "https://github.com/hasna/apps/tree/main/apps/statusline#readme",
884
1242
  bugs: {
885
- url: "https://github.com/hasna/statusline/issues"
1243
+ url: "https://github.com/hasna/apps/issues"
886
1244
  },
887
1245
  keywords: [
888
1246
  "statusline",
@@ -899,36 +1257,37 @@ var package_default = {
899
1257
  bun: ">=1.0.0"
900
1258
  },
901
1259
  dependencies: {
902
- "@modelcontextprotocol/sdk": "1.12.1",
1260
+ "@modelcontextprotocol/sdk": "^1.26.0",
903
1261
  zod: "3.25.76"
904
1262
  },
905
1263
  devDependencies: {
906
- "@types/bun": "^1.2.4",
907
- typescript: "5.9.3"
1264
+ "@types/bun": "1.3.14",
1265
+ typescript: "5.9.3",
1266
+ "bun-types": "1.3.14"
908
1267
  }
909
1268
  };
910
1269
  // 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";
1270
+ import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1271
+ import { homedir as homedir6 } from "os";
1272
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
914
1273
  function defaultSettingsPath(env) {
915
- return join4(env.HOME || homedir3(), ".claude", "settings.json");
1274
+ return join6(env.HOME || homedir6(), ".claude", "settings.json");
916
1275
  }
917
1276
  function claudeSettingsPath(env = process.env) {
918
- const home2 = env.HOME || homedir3();
1277
+ const home2 = env.HOME || homedir6();
919
1278
  const raw = env.CLAUDE_CONFIG_DIR?.trim();
920
1279
  if (!raw)
921
1280
  return defaultSettingsPath(env);
922
- const configDir = raw === "~" || raw.startsWith("~/") ? join4(home2, raw.slice(1)) : raw;
923
- return join4(configDir, "settings.json");
1281
+ const configDir = raw === "~" || raw.startsWith("~/") ? join6(home2, raw.slice(1)) : raw;
1282
+ return join6(configDir, "settings.json");
924
1283
  }
925
1284
  function installClaude(settingsPath, env = process.env) {
926
1285
  const target = settingsPath ?? claudeSettingsPath(env);
927
1286
  const binary = Bun.which("statusline") || "statusline";
928
1287
  let settings = {};
929
- if (existsSync4(target)) {
1288
+ if (existsSync6(target)) {
930
1289
  try {
931
- settings = JSON.parse(readFileSync5(target, "utf8"));
1290
+ settings = JSON.parse(readFileSync6(target, "utf8"));
932
1291
  } catch (error) {
933
1292
  throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
934
1293
  }
@@ -936,7 +1295,7 @@ function installClaude(settingsPath, env = process.env) {
936
1295
  }
937
1296
  settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
938
1297
  const configDir = dirname2(target);
939
- if (!existsSync4(configDir)) {
1298
+ if (!existsSync6(configDir)) {
940
1299
  if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
941
1300
  throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
942
1301
  }
@@ -1003,6 +1362,14 @@ function resetStatuslineConfig() {
1003
1362
  return defaultConfig();
1004
1363
  }
1005
1364
  async function renderStatusline(input = {}, config = loadConfig()) {
1365
+ const provider = process.env.STATUSLINE_PROVIDER;
1366
+ if (provider === "codex" || provider === "opencode") {
1367
+ const ctx = {
1368
+ provider,
1369
+ cwd: typeof input.cwd === "string" ? input.cwd : process.cwd()
1370
+ };
1371
+ return renderLine(ctx, config);
1372
+ }
1006
1373
  return renderLine(parseClaudeInput(input), config);
1007
1374
  }
1008
1375
  function sampleClaudeInput(cwd = process.cwd()) {