@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 +2 -0
- package/dist/accounts.d.ts +24 -0
- package/dist/cli.js +137 -4
- package/dist/index.js +133 -4
- package/dist/mcp/index.js +133 -4
- 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
|
}
|
|
@@ -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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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",
|