@hasna/statusline 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -45,6 +45,8 @@ anything that returns `null` or throws. A failed segment never breaks the host U
45
45
  | `auth-email` | off | Account email this session is logged in as | the agent's own login record for this config dir, else the registry | no login recorded | `dev@example.com` |
46
46
  | `five-hour-limit` | off | Percentage of the 5-hour rate limit used | `rate_limits.five_hour.used_percentage` | host reports no limit data | `5h:42%` |
47
47
  | `seven-day-limit` | off | Percentage of the 7-day rate limit used | `rate_limits.seven_day.used_percentage` | host reports no limit data | `7d:12%` |
48
+ | `usage-session` | off | Session (5-hour) usage **remaining** for this pane's own account | this process's `CLAUDE_CONFIG_DIR` account uuid → the accounts usage cache at `~/.hasna/accounts/cache/usage/<uuid>.json` (kept warm by `@hasna/accounts`; **no cloud call**) | never omitted — shows `5h —` when usage is unknown, missing, stale, or rate-limited | `5h 87%` |
49
+ | `usage-weekly` | off | Weekly usage **remaining** for this pane's own account (the binding limit) | same local usage cache as `usage-session` | never omitted — shows `7d —` when usage is unavailable | `7d 92%` |
48
50
  | `thread-title` | off | Thread title set with `/rename` | `session_name` | thread not renamed | `ship it` |
49
51
  | `context-used` | off | Percentage of context window used | session transcript JSONL (`contextUsage`) | transcript missing/unreadable | `10%` |
50
52
  | `context-remaining` | on | Percentage of context window remaining | transcript via `contextUsage` | transcript missing/unreadable | `90% left` |
@@ -49,4 +49,28 @@ export declare function sessionAccount(env?: Env, tool?: string): Promise<Sessio
49
49
  * blob, which is large enough to only touch when a segment asks for it.
50
50
  */
51
51
  export declare function sessionAccountEmail(env?: Env, tool?: string): Promise<string | null>;
52
+ /**
53
+ * The account uuid the host agent recorded as logged in for this config dir.
54
+ * Read from the same live state blob `sessionAccountEmail` prefers, so it names
55
+ * whoever is actually signed in now — the account whose usage the warmer
56
+ * cached — rather than a registry entry that can lag a re-login or a switch.
57
+ * Returns null when no record is present or the value is not a safe cache key.
58
+ */
59
+ export declare function sessionAccountUuid(env?: Env): string | null;
60
+ /** Usage headroom for this session's account, as percent still available. */
61
+ export interface SessionUsage {
62
+ /** The ~5-hour session window's remaining percent, or null when unknown. */
63
+ sessionHeadroom: number | null;
64
+ /** The weekly window's remaining percent — the binding limit — or null. */
65
+ weeklyHeadroom: number | null;
66
+ }
67
+ /**
68
+ * Session and weekly usage headroom for the account this pane is running as,
69
+ * read straight from the accounts usage cache — never the cloud API, which the
70
+ * launched, registry-stripped session cannot reach and could not afford on
71
+ * every render anyway. Returns nulls (so a segment shows a neutral marker)
72
+ * whenever the account is unknown, the cache is missing, stale, corrupt, or
73
+ * keyed to a different account. Never throws.
74
+ */
75
+ export declare function sessionUsage(env?: Env, now?: Date): SessionUsage;
52
76
  export {};
package/dist/cli.js CHANGED
@@ -250,6 +250,11 @@ var STATE_FILE_PREFIX = ".claude";
250
250
  var STATE_FILE_SUFFIX = ".json";
251
251
  var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
252
252
  var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
253
+ var CACHE_SUBDIR = "cache";
254
+ var USAGE_CACHE_SUBDIR = "usage";
255
+ var USAGE_CACHE_MAX_AGE_MS = 15 * 60 * 1000;
256
+ var SESSION_WINDOW_MAX_MS = 5 * 60 * 60 * 1000;
257
+ var SAFE_UUID = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
253
258
  function str(value) {
254
259
  return typeof value === "string" && value.trim() ? value.trim() : null;
255
260
  }
@@ -355,6 +360,16 @@ function sessionStateFile(env = process.env) {
355
360
  }
356
361
  return null;
357
362
  }
363
+ function liveAccountEmail(env) {
364
+ const stateFile = sessionStateFile(env);
365
+ return stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
366
+ }
367
+ function occupantMatchesLiveLogin(occupant, env) {
368
+ const live = liveAccountEmail(env);
369
+ if (!live)
370
+ return true;
371
+ return occupant.email != null && occupant.email === live;
372
+ }
358
373
  async function sessionAccount(env = process.env, tool) {
359
374
  const configDir = sessionConfigDir(env);
360
375
  const unresolved = { configDir, profile: null, tool: null, source: null };
@@ -364,7 +379,7 @@ async function sessionAccount(env = process.env, tool) {
364
379
  const name = str(entry?.name);
365
380
  const owner = name ? { configDir, profile: name, tool: str(entry?.tool), source: "registry" } : layoutProfile(paths.profiles, configDir, env) ?? unresolved;
366
381
  const occupant = switchedOccupant(configDir);
367
- if (occupant)
382
+ if (occupant && occupantMatchesLiveLogin(occupant, env))
368
383
  return { configDir, profile: occupant.profile, tool: owner.tool, source: "switched" };
369
384
  return owner;
370
385
  } catch {
@@ -373,8 +388,7 @@ async function sessionAccount(env = process.env, tool) {
373
388
  }
374
389
  async function sessionAccountEmail(env = process.env, tool) {
375
390
  try {
376
- const stateFile = sessionStateFile(env);
377
- const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
391
+ const live = liveAccountEmail(env);
378
392
  if (live)
379
393
  return live;
380
394
  const configDir = sessionConfigDir(env);
@@ -387,6 +401,95 @@ async function sessionAccountEmail(env = process.env, tool) {
387
401
  return null;
388
402
  }
389
403
  }
404
+ function accountsHomeDir(env) {
405
+ const override = str(env[ACCOUNTS_HOME_ENV]);
406
+ return override ? expandHome(override, env) : join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
407
+ }
408
+ function sessionAccountUuid(env = process.env) {
409
+ try {
410
+ const stateFile = sessionStateFile(env);
411
+ if (!stateFile)
412
+ return null;
413
+ const uuid = str(readJson(stateFile)?.oauthAccount?.accountUuid);
414
+ return uuid && SAFE_UUID.test(uuid) ? uuid : null;
415
+ } catch {
416
+ return null;
417
+ }
418
+ }
419
+ function classifyWindow(w, fetchedAt) {
420
+ if (w.scoped === true)
421
+ return "scoped";
422
+ const group = str(w.group);
423
+ if (group === "session" || group === "weekly")
424
+ return group;
425
+ const id = str(w.id);
426
+ if (id) {
427
+ if (id.includes("session"))
428
+ return "session";
429
+ if (id.includes("weekly"))
430
+ return "weekly";
431
+ }
432
+ const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
433
+ if (Number.isFinite(resetsAt) && fetchedAt !== null && resetsAt - fetchedAt > SESSION_WINDOW_MAX_MS) {
434
+ return "weekly";
435
+ }
436
+ return "unknown";
437
+ }
438
+ function windowHeadroom(w, now, fetchedAt) {
439
+ const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
440
+ const rolled = Number.isFinite(resetsAt) && resetsAt <= now && (fetchedAt === null || resetsAt > fetchedAt);
441
+ if (rolled)
442
+ return 100;
443
+ const util = typeof w.utilization === "number" && Number.isFinite(w.utilization) ? w.utilization : null;
444
+ if (util === null)
445
+ return null;
446
+ return Math.max(0, Math.min(100, Math.round(100 - util)));
447
+ }
448
+ function deriveUsage(usage, now) {
449
+ const u = usage ?? {};
450
+ const windows = Array.isArray(u.windows) ? u.windows : [];
451
+ const fetchedAtMs = typeof u.fetchedAt === "string" ? Date.parse(u.fetchedAt) : Number.NaN;
452
+ const fetchedAt = Number.isFinite(fetchedAtMs) ? fetchedAtMs : null;
453
+ let session = null;
454
+ let weekly = null;
455
+ let unknownMin = null;
456
+ for (const w of windows) {
457
+ const cls = classifyWindow(w, fetchedAt);
458
+ if (cls === "scoped")
459
+ continue;
460
+ const h = windowHeadroom(w, now, fetchedAt);
461
+ if (h === null)
462
+ continue;
463
+ if (cls === "session")
464
+ session = session === null ? h : Math.min(session, h);
465
+ else if (cls === "weekly")
466
+ weekly = weekly === null ? h : Math.min(weekly, h);
467
+ else
468
+ unknownMin = unknownMin === null ? h : Math.min(unknownMin, h);
469
+ }
470
+ const weeklyHeadroom = weekly !== null ? unknownMin !== null ? Math.min(weekly, unknownMin) : weekly : unknownMin;
471
+ return { sessionHeadroom: session, weeklyHeadroom };
472
+ }
473
+ function sessionUsage(env = process.env, now = new Date) {
474
+ const empty = { sessionHeadroom: null, weeklyHeadroom: null };
475
+ try {
476
+ const uuid = sessionAccountUuid(env);
477
+ if (!uuid)
478
+ return empty;
479
+ const path = join2(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
480
+ if (!existsSync3(path))
481
+ return empty;
482
+ const entry = readJson(path);
483
+ if (!entry || entry.accountUuid !== uuid)
484
+ return empty;
485
+ const fetchedAt = typeof entry.fetchedAt === "string" ? Date.parse(entry.fetchedAt) : Number.NaN;
486
+ if (!Number.isFinite(fetchedAt) || now.getTime() - fetchedAt > USAGE_CACHE_MAX_AGE_MS)
487
+ return empty;
488
+ return deriveUsage(entry.usage, now.getTime());
489
+ } catch {
490
+ return empty;
491
+ }
492
+ }
390
493
 
391
494
  // src/settings.ts
392
495
  import { readFileSync as readFileSync4 } from "fs";
@@ -425,6 +528,18 @@ function modelName(ctx) {
425
528
  function limitPercent(pct) {
426
529
  return typeof pct === "number" ? pct : 0;
427
530
  }
531
+ function usageLabel(prefix, headroom) {
532
+ return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
533
+ }
534
+ function usageColor(headroom, binding = false) {
535
+ if (headroom === null)
536
+ return "dim";
537
+ if (headroom < 20)
538
+ return "red";
539
+ if (headroom < 50)
540
+ return "yellow";
541
+ return binding ? "green" : null;
542
+ }
428
543
  var segments = [
429
544
  {
430
545
  id: "machine",
@@ -563,6 +678,20 @@ var segments = [
563
678
  return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
564
679
  }
565
680
  },
681
+ {
682
+ id: "usage-session",
683
+ description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
684
+ defaultEnabled: false,
685
+ color: () => usageColor(sessionUsage(process.env).sessionHeadroom),
686
+ render: () => usageLabel("5h", sessionUsage(process.env).sessionHeadroom)
687
+ },
688
+ {
689
+ id: "usage-weekly",
690
+ description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
691
+ defaultEnabled: false,
692
+ color: () => usageColor(sessionUsage(process.env).weeklyHeadroom, true),
693
+ render: () => usageLabel("7d", sessionUsage(process.env).weeklyHeadroom)
694
+ },
566
695
  {
567
696
  id: "thread-title",
568
697
  description: "Thread title set with /rename (omitted until named)",
@@ -710,7 +839,7 @@ async function renderLine(ctx, cfg) {
710
839
  // package.json
711
840
  var package_default = {
712
841
  name: "@hasna/statusline",
713
- version: "0.0.5",
842
+ version: "0.0.7",
714
843
  description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
715
844
  license: "Apache-2.0",
716
845
  type: "module",
@@ -1216,6 +1345,8 @@ async function runCli(argv = process.argv.slice(2)) {
1216
1345
  return;
1217
1346
  }
1218
1347
  case "enable": {
1348
+ if (!args.length)
1349
+ throw new Error("usage: statusline enable <segment> [segment...]");
1219
1350
  let result;
1220
1351
  try {
1221
1352
  result = enableSegments(args, loadConfig());
@@ -1229,6 +1360,8 @@ order: ${result.config.segments.join(", ")}`);
1229
1360
  return;
1230
1361
  }
1231
1362
  case "disable": {
1363
+ if (!args.length)
1364
+ throw new Error("usage: statusline disable <segment> [segment...]");
1232
1365
  let result;
1233
1366
  try {
1234
1367
  result = disableSegments(args, loadConfig());
package/dist/index.js CHANGED
@@ -249,6 +249,11 @@ var STATE_FILE_PREFIX = ".claude";
249
249
  var STATE_FILE_SUFFIX = ".json";
250
250
  var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
251
251
  var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
252
+ var CACHE_SUBDIR = "cache";
253
+ var USAGE_CACHE_SUBDIR = "usage";
254
+ var USAGE_CACHE_MAX_AGE_MS = 15 * 60 * 1000;
255
+ var SESSION_WINDOW_MAX_MS = 5 * 60 * 60 * 1000;
256
+ var SAFE_UUID = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
252
257
  function str(value) {
253
258
  return typeof value === "string" && value.trim() ? value.trim() : null;
254
259
  }
@@ -354,6 +359,16 @@ function sessionStateFile(env = process.env) {
354
359
  }
355
360
  return null;
356
361
  }
362
+ function liveAccountEmail(env) {
363
+ const stateFile = sessionStateFile(env);
364
+ return stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
365
+ }
366
+ function occupantMatchesLiveLogin(occupant, env) {
367
+ const live = liveAccountEmail(env);
368
+ if (!live)
369
+ return true;
370
+ return occupant.email != null && occupant.email === live;
371
+ }
357
372
  async function sessionAccount(env = process.env, tool) {
358
373
  const configDir = sessionConfigDir(env);
359
374
  const unresolved = { configDir, profile: null, tool: null, source: null };
@@ -363,7 +378,7 @@ async function sessionAccount(env = process.env, tool) {
363
378
  const name = str(entry?.name);
364
379
  const owner = name ? { configDir, profile: name, tool: str(entry?.tool), source: "registry" } : layoutProfile(paths.profiles, configDir, env) ?? unresolved;
365
380
  const occupant = switchedOccupant(configDir);
366
- if (occupant)
381
+ if (occupant && occupantMatchesLiveLogin(occupant, env))
367
382
  return { configDir, profile: occupant.profile, tool: owner.tool, source: "switched" };
368
383
  return owner;
369
384
  } catch {
@@ -372,8 +387,7 @@ async function sessionAccount(env = process.env, tool) {
372
387
  }
373
388
  async function sessionAccountEmail(env = process.env, tool) {
374
389
  try {
375
- const stateFile = sessionStateFile(env);
376
- const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
390
+ const live = liveAccountEmail(env);
377
391
  if (live)
378
392
  return live;
379
393
  const configDir = sessionConfigDir(env);
@@ -386,6 +400,95 @@ async function sessionAccountEmail(env = process.env, tool) {
386
400
  return null;
387
401
  }
388
402
  }
403
+ function accountsHomeDir(env) {
404
+ const override = str(env[ACCOUNTS_HOME_ENV]);
405
+ return override ? expandHome(override, env) : join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
406
+ }
407
+ function sessionAccountUuid(env = process.env) {
408
+ try {
409
+ const stateFile = sessionStateFile(env);
410
+ if (!stateFile)
411
+ return null;
412
+ const uuid = str(readJson(stateFile)?.oauthAccount?.accountUuid);
413
+ return uuid && SAFE_UUID.test(uuid) ? uuid : null;
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+ function classifyWindow(w, fetchedAt) {
419
+ if (w.scoped === true)
420
+ return "scoped";
421
+ const group = str(w.group);
422
+ if (group === "session" || group === "weekly")
423
+ return group;
424
+ const id = str(w.id);
425
+ if (id) {
426
+ if (id.includes("session"))
427
+ return "session";
428
+ if (id.includes("weekly"))
429
+ return "weekly";
430
+ }
431
+ const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
432
+ if (Number.isFinite(resetsAt) && fetchedAt !== null && resetsAt - fetchedAt > SESSION_WINDOW_MAX_MS) {
433
+ return "weekly";
434
+ }
435
+ return "unknown";
436
+ }
437
+ function windowHeadroom(w, now, fetchedAt) {
438
+ const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
439
+ const rolled = Number.isFinite(resetsAt) && resetsAt <= now && (fetchedAt === null || resetsAt > fetchedAt);
440
+ if (rolled)
441
+ return 100;
442
+ const util = typeof w.utilization === "number" && Number.isFinite(w.utilization) ? w.utilization : null;
443
+ if (util === null)
444
+ return null;
445
+ return Math.max(0, Math.min(100, Math.round(100 - util)));
446
+ }
447
+ function deriveUsage(usage, now) {
448
+ const u = usage ?? {};
449
+ const windows = Array.isArray(u.windows) ? u.windows : [];
450
+ const fetchedAtMs = typeof u.fetchedAt === "string" ? Date.parse(u.fetchedAt) : Number.NaN;
451
+ const fetchedAt = Number.isFinite(fetchedAtMs) ? fetchedAtMs : null;
452
+ let session = null;
453
+ let weekly = null;
454
+ let unknownMin = null;
455
+ for (const w of windows) {
456
+ const cls = classifyWindow(w, fetchedAt);
457
+ if (cls === "scoped")
458
+ continue;
459
+ const h = windowHeadroom(w, now, fetchedAt);
460
+ if (h === null)
461
+ continue;
462
+ if (cls === "session")
463
+ session = session === null ? h : Math.min(session, h);
464
+ else if (cls === "weekly")
465
+ weekly = weekly === null ? h : Math.min(weekly, h);
466
+ else
467
+ unknownMin = unknownMin === null ? h : Math.min(unknownMin, h);
468
+ }
469
+ const weeklyHeadroom = weekly !== null ? unknownMin !== null ? Math.min(weekly, unknownMin) : weekly : unknownMin;
470
+ return { sessionHeadroom: session, weeklyHeadroom };
471
+ }
472
+ function sessionUsage(env = process.env, now = new Date) {
473
+ const empty = { sessionHeadroom: null, weeklyHeadroom: null };
474
+ try {
475
+ const uuid = sessionAccountUuid(env);
476
+ if (!uuid)
477
+ return empty;
478
+ const path = join2(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
479
+ if (!existsSync3(path))
480
+ return empty;
481
+ const entry = readJson(path);
482
+ if (!entry || entry.accountUuid !== uuid)
483
+ return empty;
484
+ const fetchedAt = typeof entry.fetchedAt === "string" ? Date.parse(entry.fetchedAt) : Number.NaN;
485
+ if (!Number.isFinite(fetchedAt) || now.getTime() - fetchedAt > USAGE_CACHE_MAX_AGE_MS)
486
+ return empty;
487
+ return deriveUsage(entry.usage, now.getTime());
488
+ } catch {
489
+ return empty;
490
+ }
491
+ }
389
492
 
390
493
  // src/settings.ts
391
494
  import { readFileSync as readFileSync4 } from "fs";
@@ -424,6 +527,18 @@ function modelName(ctx) {
424
527
  function limitPercent(pct) {
425
528
  return typeof pct === "number" ? pct : 0;
426
529
  }
530
+ function usageLabel(prefix, headroom) {
531
+ return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
532
+ }
533
+ function usageColor(headroom, binding = false) {
534
+ if (headroom === null)
535
+ return "dim";
536
+ if (headroom < 20)
537
+ return "red";
538
+ if (headroom < 50)
539
+ return "yellow";
540
+ return binding ? "green" : null;
541
+ }
427
542
  var segments = [
428
543
  {
429
544
  id: "machine",
@@ -562,6 +677,20 @@ var segments = [
562
677
  return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
563
678
  }
564
679
  },
680
+ {
681
+ id: "usage-session",
682
+ description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
683
+ defaultEnabled: false,
684
+ color: () => usageColor(sessionUsage(process.env).sessionHeadroom),
685
+ render: () => usageLabel("5h", sessionUsage(process.env).sessionHeadroom)
686
+ },
687
+ {
688
+ id: "usage-weekly",
689
+ description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
690
+ defaultEnabled: false,
691
+ color: () => usageColor(sessionUsage(process.env).weeklyHeadroom, true),
692
+ render: () => usageLabel("7d", sessionUsage(process.env).weeklyHeadroom)
693
+ },
565
694
  {
566
695
  id: "thread-title",
567
696
  description: "Thread title set with /rename (omitted until named)",
@@ -709,7 +838,7 @@ async function renderLine(ctx, cfg) {
709
838
  // package.json
710
839
  var package_default = {
711
840
  name: "@hasna/statusline",
712
- version: "0.0.5",
841
+ version: "0.0.7",
713
842
  description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
714
843
  license: "Apache-2.0",
715
844
  type: "module",
package/dist/mcp/index.js CHANGED
@@ -250,6 +250,11 @@ var STATE_FILE_PREFIX = ".claude";
250
250
  var STATE_FILE_SUFFIX = ".json";
251
251
  var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
252
252
  var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
253
+ var CACHE_SUBDIR = "cache";
254
+ var USAGE_CACHE_SUBDIR = "usage";
255
+ var USAGE_CACHE_MAX_AGE_MS = 15 * 60 * 1000;
256
+ var SESSION_WINDOW_MAX_MS = 5 * 60 * 60 * 1000;
257
+ var SAFE_UUID = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
253
258
  function str(value) {
254
259
  return typeof value === "string" && value.trim() ? value.trim() : null;
255
260
  }
@@ -355,6 +360,16 @@ function sessionStateFile(env = process.env) {
355
360
  }
356
361
  return null;
357
362
  }
363
+ function liveAccountEmail(env) {
364
+ const stateFile = sessionStateFile(env);
365
+ return stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
366
+ }
367
+ function occupantMatchesLiveLogin(occupant, env) {
368
+ const live = liveAccountEmail(env);
369
+ if (!live)
370
+ return true;
371
+ return occupant.email != null && occupant.email === live;
372
+ }
358
373
  async function sessionAccount(env = process.env, tool) {
359
374
  const configDir = sessionConfigDir(env);
360
375
  const unresolved = { configDir, profile: null, tool: null, source: null };
@@ -364,7 +379,7 @@ async function sessionAccount(env = process.env, tool) {
364
379
  const name = str(entry?.name);
365
380
  const owner = name ? { configDir, profile: name, tool: str(entry?.tool), source: "registry" } : layoutProfile(paths.profiles, configDir, env) ?? unresolved;
366
381
  const occupant = switchedOccupant(configDir);
367
- if (occupant)
382
+ if (occupant && occupantMatchesLiveLogin(occupant, env))
368
383
  return { configDir, profile: occupant.profile, tool: owner.tool, source: "switched" };
369
384
  return owner;
370
385
  } catch {
@@ -373,8 +388,7 @@ async function sessionAccount(env = process.env, tool) {
373
388
  }
374
389
  async function sessionAccountEmail(env = process.env, tool) {
375
390
  try {
376
- const stateFile = sessionStateFile(env);
377
- const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
391
+ const live = liveAccountEmail(env);
378
392
  if (live)
379
393
  return live;
380
394
  const configDir = sessionConfigDir(env);
@@ -387,6 +401,95 @@ async function sessionAccountEmail(env = process.env, tool) {
387
401
  return null;
388
402
  }
389
403
  }
404
+ function accountsHomeDir(env) {
405
+ const override = str(env[ACCOUNTS_HOME_ENV]);
406
+ return override ? expandHome(override, env) : join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
407
+ }
408
+ function sessionAccountUuid(env = process.env) {
409
+ try {
410
+ const stateFile = sessionStateFile(env);
411
+ if (!stateFile)
412
+ return null;
413
+ const uuid = str(readJson(stateFile)?.oauthAccount?.accountUuid);
414
+ return uuid && SAFE_UUID.test(uuid) ? uuid : null;
415
+ } catch {
416
+ return null;
417
+ }
418
+ }
419
+ function classifyWindow(w, fetchedAt) {
420
+ if (w.scoped === true)
421
+ return "scoped";
422
+ const group = str(w.group);
423
+ if (group === "session" || group === "weekly")
424
+ return group;
425
+ const id = str(w.id);
426
+ if (id) {
427
+ if (id.includes("session"))
428
+ return "session";
429
+ if (id.includes("weekly"))
430
+ return "weekly";
431
+ }
432
+ const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
433
+ if (Number.isFinite(resetsAt) && fetchedAt !== null && resetsAt - fetchedAt > SESSION_WINDOW_MAX_MS) {
434
+ return "weekly";
435
+ }
436
+ return "unknown";
437
+ }
438
+ function windowHeadroom(w, now, fetchedAt) {
439
+ const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
440
+ const rolled = Number.isFinite(resetsAt) && resetsAt <= now && (fetchedAt === null || resetsAt > fetchedAt);
441
+ if (rolled)
442
+ return 100;
443
+ const util = typeof w.utilization === "number" && Number.isFinite(w.utilization) ? w.utilization : null;
444
+ if (util === null)
445
+ return null;
446
+ return Math.max(0, Math.min(100, Math.round(100 - util)));
447
+ }
448
+ function deriveUsage(usage, now) {
449
+ const u = usage ?? {};
450
+ const windows = Array.isArray(u.windows) ? u.windows : [];
451
+ const fetchedAtMs = typeof u.fetchedAt === "string" ? Date.parse(u.fetchedAt) : Number.NaN;
452
+ const fetchedAt = Number.isFinite(fetchedAtMs) ? fetchedAtMs : null;
453
+ let session = null;
454
+ let weekly = null;
455
+ let unknownMin = null;
456
+ for (const w of windows) {
457
+ const cls = classifyWindow(w, fetchedAt);
458
+ if (cls === "scoped")
459
+ continue;
460
+ const h = windowHeadroom(w, now, fetchedAt);
461
+ if (h === null)
462
+ continue;
463
+ if (cls === "session")
464
+ session = session === null ? h : Math.min(session, h);
465
+ else if (cls === "weekly")
466
+ weekly = weekly === null ? h : Math.min(weekly, h);
467
+ else
468
+ unknownMin = unknownMin === null ? h : Math.min(unknownMin, h);
469
+ }
470
+ const weeklyHeadroom = weekly !== null ? unknownMin !== null ? Math.min(weekly, unknownMin) : weekly : unknownMin;
471
+ return { sessionHeadroom: session, weeklyHeadroom };
472
+ }
473
+ function sessionUsage(env = process.env, now = new Date) {
474
+ const empty = { sessionHeadroom: null, weeklyHeadroom: null };
475
+ try {
476
+ const uuid = sessionAccountUuid(env);
477
+ if (!uuid)
478
+ return empty;
479
+ const path = join2(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
480
+ if (!existsSync3(path))
481
+ return empty;
482
+ const entry = readJson(path);
483
+ if (!entry || entry.accountUuid !== uuid)
484
+ return empty;
485
+ const fetchedAt = typeof entry.fetchedAt === "string" ? Date.parse(entry.fetchedAt) : Number.NaN;
486
+ if (!Number.isFinite(fetchedAt) || now.getTime() - fetchedAt > USAGE_CACHE_MAX_AGE_MS)
487
+ return empty;
488
+ return deriveUsage(entry.usage, now.getTime());
489
+ } catch {
490
+ return empty;
491
+ }
492
+ }
390
493
 
391
494
  // src/settings.ts
392
495
  import { readFileSync as readFileSync4 } from "fs";
@@ -425,6 +528,18 @@ function modelName(ctx) {
425
528
  function limitPercent(pct) {
426
529
  return typeof pct === "number" ? pct : 0;
427
530
  }
531
+ function usageLabel(prefix, headroom) {
532
+ return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
533
+ }
534
+ function usageColor(headroom, binding = false) {
535
+ if (headroom === null)
536
+ return "dim";
537
+ if (headroom < 20)
538
+ return "red";
539
+ if (headroom < 50)
540
+ return "yellow";
541
+ return binding ? "green" : null;
542
+ }
428
543
  var segments = [
429
544
  {
430
545
  id: "machine",
@@ -563,6 +678,20 @@ var segments = [
563
678
  return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
564
679
  }
565
680
  },
681
+ {
682
+ id: "usage-session",
683
+ description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
684
+ defaultEnabled: false,
685
+ color: () => usageColor(sessionUsage(process.env).sessionHeadroom),
686
+ render: () => usageLabel("5h", sessionUsage(process.env).sessionHeadroom)
687
+ },
688
+ {
689
+ id: "usage-weekly",
690
+ description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
691
+ defaultEnabled: false,
692
+ color: () => usageColor(sessionUsage(process.env).weeklyHeadroom, true),
693
+ render: () => usageLabel("7d", sessionUsage(process.env).weeklyHeadroom)
694
+ },
566
695
  {
567
696
  id: "thread-title",
568
697
  description: "Thread title set with /rename (omitted until named)",
@@ -710,7 +839,7 @@ async function renderLine(ctx, cfg) {
710
839
  // package.json
711
840
  var package_default = {
712
841
  name: "@hasna/statusline",
713
- version: "0.0.5",
842
+ version: "0.0.7",
714
843
  description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
715
844
  license: "Apache-2.0",
716
845
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/statusline",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",