@hasna/statusline 0.0.6 → 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 +2 -0
- package/dist/accounts.d.ts +24 -0
- package/dist/cli.js +121 -1
- package/dist/index.js +121 -1
- package/dist/mcp/index.js +121 -1
- package/package.json +1 -1
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` |
|
package/dist/accounts.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -396,6 +401,95 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
396
401
|
return null;
|
|
397
402
|
}
|
|
398
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
|
+
}
|
|
399
493
|
|
|
400
494
|
// src/settings.ts
|
|
401
495
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -434,6 +528,18 @@ function modelName(ctx) {
|
|
|
434
528
|
function limitPercent(pct) {
|
|
435
529
|
return typeof pct === "number" ? pct : 0;
|
|
436
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
|
+
}
|
|
437
543
|
var segments = [
|
|
438
544
|
{
|
|
439
545
|
id: "machine",
|
|
@@ -572,6 +678,20 @@ var segments = [
|
|
|
572
678
|
return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
|
|
573
679
|
}
|
|
574
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
|
+
},
|
|
575
695
|
{
|
|
576
696
|
id: "thread-title",
|
|
577
697
|
description: "Thread title set with /rename (omitted until named)",
|
|
@@ -719,7 +839,7 @@ async function renderLine(ctx, cfg) {
|
|
|
719
839
|
// package.json
|
|
720
840
|
var package_default = {
|
|
721
841
|
name: "@hasna/statusline",
|
|
722
|
-
version: "0.0.
|
|
842
|
+
version: "0.0.7",
|
|
723
843
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
724
844
|
license: "Apache-2.0",
|
|
725
845
|
type: "module",
|
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
|
}
|
|
@@ -395,6 +400,95 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
395
400
|
return null;
|
|
396
401
|
}
|
|
397
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
|
+
}
|
|
398
492
|
|
|
399
493
|
// src/settings.ts
|
|
400
494
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -433,6 +527,18 @@ function modelName(ctx) {
|
|
|
433
527
|
function limitPercent(pct) {
|
|
434
528
|
return typeof pct === "number" ? pct : 0;
|
|
435
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
|
+
}
|
|
436
542
|
var segments = [
|
|
437
543
|
{
|
|
438
544
|
id: "machine",
|
|
@@ -571,6 +677,20 @@ var segments = [
|
|
|
571
677
|
return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
|
|
572
678
|
}
|
|
573
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
|
+
},
|
|
574
694
|
{
|
|
575
695
|
id: "thread-title",
|
|
576
696
|
description: "Thread title set with /rename (omitted until named)",
|
|
@@ -718,7 +838,7 @@ async function renderLine(ctx, cfg) {
|
|
|
718
838
|
// package.json
|
|
719
839
|
var package_default = {
|
|
720
840
|
name: "@hasna/statusline",
|
|
721
|
-
version: "0.0.
|
|
841
|
+
version: "0.0.7",
|
|
722
842
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
723
843
|
license: "Apache-2.0",
|
|
724
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
|
}
|
|
@@ -396,6 +401,95 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
396
401
|
return null;
|
|
397
402
|
}
|
|
398
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
|
+
}
|
|
399
493
|
|
|
400
494
|
// src/settings.ts
|
|
401
495
|
import { readFileSync as readFileSync4 } from "fs";
|
|
@@ -434,6 +528,18 @@ function modelName(ctx) {
|
|
|
434
528
|
function limitPercent(pct) {
|
|
435
529
|
return typeof pct === "number" ? pct : 0;
|
|
436
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
|
+
}
|
|
437
543
|
var segments = [
|
|
438
544
|
{
|
|
439
545
|
id: "machine",
|
|
@@ -572,6 +678,20 @@ var segments = [
|
|
|
572
678
|
return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
|
|
573
679
|
}
|
|
574
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
|
+
},
|
|
575
695
|
{
|
|
576
696
|
id: "thread-title",
|
|
577
697
|
description: "Thread title set with /rename (omitted until named)",
|
|
@@ -719,7 +839,7 @@ async function renderLine(ctx, cfg) {
|
|
|
719
839
|
// package.json
|
|
720
840
|
var package_default = {
|
|
721
841
|
name: "@hasna/statusline",
|
|
722
|
-
version: "0.0.
|
|
842
|
+
version: "0.0.7",
|
|
723
843
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
724
844
|
license: "Apache-2.0",
|
|
725
845
|
type: "module",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/statusline",
|
|
3
|
-
"version": "0.0.
|
|
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",
|