@hasna/statusline 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -29
- package/dist/accounts.d.ts +24 -0
- package/dist/cli.js +421 -51
- package/dist/context-window.d.ts +22 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +427 -52
- package/dist/mcp/index.js +19802 -6343
- package/dist/providers/codex.d.ts +19 -0
- package/dist/providers/opencode.d.ts +16 -0
- package/dist/providers/types.d.ts +1 -1
- package/package.json +10 -8
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
5
|
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
13
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
|
+
for (let key of __getOwnPropNames(mod))
|
|
24
|
+
if (!__hasOwnProp.call(to, key))
|
|
25
|
+
__defProp(to, key, {
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
27
|
+
enumerable: true
|
|
28
|
+
});
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
31
|
+
return to;
|
|
32
|
+
};
|
|
33
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
34
|
var __returnValue = (v) => v;
|
|
5
35
|
function __exportSetter(name, newValue) {
|
|
6
36
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -30,6 +60,7 @@ function defaultConfig() {
|
|
|
30
60
|
"loc",
|
|
31
61
|
"model-context",
|
|
32
62
|
"context-remaining",
|
|
63
|
+
"cache-rate",
|
|
33
64
|
"cost"
|
|
34
65
|
]
|
|
35
66
|
};
|
|
@@ -95,7 +126,7 @@ function numberOr(v) {
|
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
// src/segments/index.ts
|
|
98
|
-
import { hostname } from "os";
|
|
129
|
+
import { homedir as homedir5, hostname } from "os";
|
|
99
130
|
|
|
100
131
|
// src/format.ts
|
|
101
132
|
function compactAge(seconds) {
|
|
@@ -135,10 +166,10 @@ function compactDuration(ms) {
|
|
|
135
166
|
return `${s}s`;
|
|
136
167
|
if (s < 3600) {
|
|
137
168
|
const rem2 = s % 60;
|
|
138
|
-
return rem2 ? `${Math.floor(s / 60)}m${rem2}s` : `${Math.floor(s / 60)}m`;
|
|
169
|
+
return rem2 ? `${Math.floor(s / 60)}m ${rem2}s` : `${Math.floor(s / 60)}m`;
|
|
139
170
|
}
|
|
140
171
|
const rem = Math.floor(s % 3600 / 60);
|
|
141
|
-
return rem ? `${Math.floor(s / 3600)}h${rem}m` : `${Math.floor(s / 3600)}h`;
|
|
172
|
+
return rem ? `${Math.floor(s / 3600)}h ${rem}m` : `${Math.floor(s / 3600)}h`;
|
|
142
173
|
}
|
|
143
174
|
|
|
144
175
|
// src/git.ts
|
|
@@ -195,14 +226,145 @@ function trackedLineCount(cwd) {
|
|
|
195
226
|
}
|
|
196
227
|
|
|
197
228
|
// src/context-window.ts
|
|
198
|
-
import { existsSync as
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
230
|
+
|
|
231
|
+
// src/providers/codex.ts
|
|
232
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
233
|
+
import { homedir as homedir2 } from "os";
|
|
234
|
+
import { join as join2 } from "path";
|
|
235
|
+
function latestCodexSessionPath(home = homedir2()) {
|
|
236
|
+
const root = join2(home, ".codex", "sessions");
|
|
237
|
+
if (!existsSync2(root))
|
|
238
|
+
return null;
|
|
239
|
+
let newest = null;
|
|
240
|
+
let newestMtime = 0;
|
|
241
|
+
try {
|
|
242
|
+
for (const year of readdirSync(root)) {
|
|
243
|
+
const yearDir = join2(root, year);
|
|
244
|
+
if (!statSync(yearDir, { throwIfNoEntry: false })?.isDirectory())
|
|
245
|
+
continue;
|
|
246
|
+
for (const month of readdirSync(yearDir)) {
|
|
247
|
+
const monthDir = join2(yearDir, month);
|
|
248
|
+
if (!statSync(monthDir, { throwIfNoEntry: false })?.isDirectory())
|
|
249
|
+
continue;
|
|
250
|
+
for (const day of readdirSync(monthDir)) {
|
|
251
|
+
const dayDir = join2(monthDir, day);
|
|
252
|
+
if (!statSync(dayDir, { throwIfNoEntry: false })?.isDirectory())
|
|
253
|
+
continue;
|
|
254
|
+
for (const file of readdirSync(dayDir)) {
|
|
255
|
+
if (!file.startsWith("rollout-") || !file.endsWith(".jsonl"))
|
|
256
|
+
continue;
|
|
257
|
+
const full = join2(dayDir, file);
|
|
258
|
+
const mtime = statSync(full, { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
259
|
+
if (mtime > newestMtime) {
|
|
260
|
+
newestMtime = mtime;
|
|
261
|
+
newest = full;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return newest;
|
|
271
|
+
}
|
|
272
|
+
function codexCacheRate(sessionPath) {
|
|
273
|
+
const path = sessionPath ?? latestCodexSessionPath();
|
|
201
274
|
if (!path || !existsSync2(path))
|
|
202
275
|
return null;
|
|
203
276
|
let lines;
|
|
204
277
|
try {
|
|
205
278
|
lines = readFileSync2(path, "utf8").split(`
|
|
279
|
+
`);
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
let usage = null;
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
if (!line || !line.includes("token_count"))
|
|
286
|
+
continue;
|
|
287
|
+
try {
|
|
288
|
+
const entry = JSON.parse(line);
|
|
289
|
+
if (entry?.type !== "event_msg")
|
|
290
|
+
continue;
|
|
291
|
+
if (entry?.payload?.type !== "token_count")
|
|
292
|
+
continue;
|
|
293
|
+
const info = entry?.payload?.info?.total_token_usage;
|
|
294
|
+
if (info && typeof info === "object")
|
|
295
|
+
usage = info;
|
|
296
|
+
} catch {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!usage)
|
|
301
|
+
return null;
|
|
302
|
+
const read = usage.cached_input_tokens || 0;
|
|
303
|
+
const input = usage.input_tokens || 0;
|
|
304
|
+
const write = usage.cache_write_input_tokens || 0;
|
|
305
|
+
const divisor = input + read + write;
|
|
306
|
+
if (divisor <= 0)
|
|
307
|
+
return null;
|
|
308
|
+
return Math.min(1, Math.max(0, read / divisor));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/providers/opencode.ts
|
|
312
|
+
import { existsSync as existsSync3 } from "fs";
|
|
313
|
+
import { homedir as homedir3 } from "os";
|
|
314
|
+
import { join as join3 } from "path";
|
|
315
|
+
import { Database } from "bun:sqlite";
|
|
316
|
+
function opencodeCacheRate(opts = {}) {
|
|
317
|
+
const dbPath = opts.dbPath ?? join3(homedir3(), ".local", "share", "opencode", "opencode.db");
|
|
318
|
+
if (!existsSync3(dbPath))
|
|
319
|
+
return null;
|
|
320
|
+
let db;
|
|
321
|
+
try {
|
|
322
|
+
db = new Database(dbPath, { readonly: true });
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
let row = tokenRow(db, "session", opts.sessionId);
|
|
328
|
+
if (!row)
|
|
329
|
+
row = tokenRow(db, "session_v2", opts.sessionId);
|
|
330
|
+
if (!row)
|
|
331
|
+
return null;
|
|
332
|
+
const input = row.tokens_input ?? 0;
|
|
333
|
+
const read = row.tokens_cache_read ?? 0;
|
|
334
|
+
const write = row.tokens_cache_write ?? 0;
|
|
335
|
+
const divisor = input + read + write;
|
|
336
|
+
if (divisor <= 0)
|
|
337
|
+
return null;
|
|
338
|
+
return Math.min(1, Math.max(0, read / divisor));
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
} finally {
|
|
342
|
+
db.close();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function tokenRow(db, table, sessionId) {
|
|
346
|
+
try {
|
|
347
|
+
if (sessionId) {
|
|
348
|
+
return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
|
|
349
|
+
FROM ${table} WHERE id = ? LIMIT 1`).get(sessionId);
|
|
350
|
+
}
|
|
351
|
+
return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
|
|
352
|
+
FROM ${table}
|
|
353
|
+
WHERE tokens_input > 0 OR tokens_cache_read > 0 OR tokens_cache_write > 0
|
|
354
|
+
ORDER BY time_updated DESC LIMIT 1`).get();
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/context-window.ts
|
|
361
|
+
function lastUsageBlock(ctx) {
|
|
362
|
+
const path = ctx.transcriptPath;
|
|
363
|
+
if (!path || !existsSync4(path))
|
|
364
|
+
return null;
|
|
365
|
+
let lines;
|
|
366
|
+
try {
|
|
367
|
+
lines = readFileSync3(path, "utf8").split(`
|
|
206
368
|
`);
|
|
207
369
|
} catch {
|
|
208
370
|
return null;
|
|
@@ -216,26 +378,48 @@ function contextUsage(ctx) {
|
|
|
216
378
|
const usage = entry?.message?.usage;
|
|
217
379
|
if (!usage || typeof usage.input_tokens !== "number")
|
|
218
380
|
continue;
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
used,
|
|
222
|
-
output: usage.output_tokens || 0,
|
|
223
|
-
window: windowSize(ctx)
|
|
224
|
-
};
|
|
381
|
+
return usage;
|
|
225
382
|
} catch {
|
|
226
383
|
continue;
|
|
227
384
|
}
|
|
228
385
|
}
|
|
229
386
|
return null;
|
|
230
387
|
}
|
|
388
|
+
function contextUsage(ctx) {
|
|
389
|
+
const usage = lastUsageBlock(ctx);
|
|
390
|
+
if (!usage)
|
|
391
|
+
return null;
|
|
392
|
+
const used = usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
393
|
+
return {
|
|
394
|
+
used,
|
|
395
|
+
output: usage.output_tokens || 0,
|
|
396
|
+
window: windowSize(ctx)
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function cacheRate(ctx) {
|
|
400
|
+
if (ctx.provider === "codex")
|
|
401
|
+
return codexCacheRate();
|
|
402
|
+
if (ctx.provider === "opencode")
|
|
403
|
+
return opencodeCacheRate();
|
|
404
|
+
const usage = lastUsageBlock(ctx);
|
|
405
|
+
if (!usage)
|
|
406
|
+
return null;
|
|
407
|
+
const read = usage.cache_read_input_tokens || 0;
|
|
408
|
+
const creation = usage.cache_creation_input_tokens || 0;
|
|
409
|
+
const input = usage.input_tokens || 0;
|
|
410
|
+
const divisor = input + creation + read;
|
|
411
|
+
if (divisor <= 0)
|
|
412
|
+
return null;
|
|
413
|
+
return read / divisor;
|
|
414
|
+
}
|
|
231
415
|
function windowSize(ctx) {
|
|
232
416
|
return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
|
|
233
417
|
}
|
|
234
418
|
|
|
235
419
|
// src/accounts.ts
|
|
236
|
-
import { existsSync as
|
|
237
|
-
import { homedir as
|
|
238
|
-
import { join as
|
|
420
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
421
|
+
import { homedir as homedir4 } from "os";
|
|
422
|
+
import { join as join4, resolve, sep } from "path";
|
|
239
423
|
var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
|
|
240
424
|
var DEFAULT_CONFIG_SUBDIR = ".claude";
|
|
241
425
|
var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
|
|
@@ -250,14 +434,19 @@ var STATE_FILE_PREFIX = ".claude";
|
|
|
250
434
|
var STATE_FILE_SUFFIX = ".json";
|
|
251
435
|
var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
|
|
252
436
|
var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
|
|
437
|
+
var CACHE_SUBDIR = "cache";
|
|
438
|
+
var USAGE_CACHE_SUBDIR = "usage";
|
|
439
|
+
var USAGE_CACHE_MAX_AGE_MS = 15 * 60 * 1000;
|
|
440
|
+
var SESSION_WINDOW_MAX_MS = 5 * 60 * 60 * 1000;
|
|
441
|
+
var SAFE_UUID = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
|
|
253
442
|
function str(value) {
|
|
254
443
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
255
444
|
}
|
|
256
445
|
function home(env) {
|
|
257
|
-
return str(env.HOME) ??
|
|
446
|
+
return str(env.HOME) ?? homedir4();
|
|
258
447
|
}
|
|
259
448
|
function expandHome(path, env) {
|
|
260
|
-
return path === "~" || path.startsWith(`~${sep}`) ?
|
|
449
|
+
return path === "~" || path.startsWith(`~${sep}`) ? join4(home(env), path.slice(1)) : path;
|
|
261
450
|
}
|
|
262
451
|
function canonical(path, env) {
|
|
263
452
|
const abs = resolve(expandHome(path, env));
|
|
@@ -269,7 +458,7 @@ function canonical(path, env) {
|
|
|
269
458
|
}
|
|
270
459
|
function readJson(path) {
|
|
271
460
|
try {
|
|
272
|
-
return JSON.parse(
|
|
461
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
273
462
|
} catch {
|
|
274
463
|
return null;
|
|
275
464
|
}
|
|
@@ -290,14 +479,14 @@ async function accountsPaths(env) {
|
|
|
290
479
|
return { store: storage.storePath(), profiles: storage.profilesDir() };
|
|
291
480
|
} catch {}
|
|
292
481
|
}
|
|
293
|
-
const base = homeOverride ??
|
|
482
|
+
const base = homeOverride ?? join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
|
|
294
483
|
return {
|
|
295
|
-
store: storeOverride ??
|
|
296
|
-
profiles:
|
|
484
|
+
store: storeOverride ?? join4(base, STORE_FILE),
|
|
485
|
+
profiles: join4(base, PROFILES_SUBDIR)
|
|
297
486
|
};
|
|
298
487
|
}
|
|
299
488
|
function registryEntry(store, configDir, env, tool) {
|
|
300
|
-
if (!
|
|
489
|
+
if (!existsSync5(store))
|
|
301
490
|
return null;
|
|
302
491
|
const entries = readJson(store)?.profiles;
|
|
303
492
|
if (!Array.isArray(entries))
|
|
@@ -318,7 +507,7 @@ function registryEntry(store, configDir, env, tool) {
|
|
|
318
507
|
return tool ? untyped : first;
|
|
319
508
|
}
|
|
320
509
|
function layoutProfile(profilesRoot, configDir, env) {
|
|
321
|
-
if (!
|
|
510
|
+
if (!existsSync5(configDir))
|
|
322
511
|
return null;
|
|
323
512
|
const prefix = canonical(profilesRoot, env) + sep;
|
|
324
513
|
if (!configDir.startsWith(prefix))
|
|
@@ -332,7 +521,7 @@ function layoutProfile(profilesRoot, configDir, env) {
|
|
|
332
521
|
return { configDir, profile, tool, source: "layout" };
|
|
333
522
|
}
|
|
334
523
|
function switchedOccupant(configDir) {
|
|
335
|
-
const raw = readJson(
|
|
524
|
+
const raw = readJson(join4(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
|
|
336
525
|
if (!raw)
|
|
337
526
|
return null;
|
|
338
527
|
const profile = str(raw.profile);
|
|
@@ -340,17 +529,17 @@ function switchedOccupant(configDir) {
|
|
|
340
529
|
return profile || email ? { profile, email } : null;
|
|
341
530
|
}
|
|
342
531
|
function sessionConfigDir(env = process.env) {
|
|
343
|
-
return canonical(str(env[CONFIG_DIR_ENV]) ??
|
|
532
|
+
return canonical(str(env[CONFIG_DIR_ENV]) ?? join4(home(env), DEFAULT_CONFIG_SUBDIR), env);
|
|
344
533
|
}
|
|
345
534
|
function sessionStateFile(env = process.env) {
|
|
346
535
|
const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
|
|
347
|
-
const preferred =
|
|
348
|
-
if (
|
|
536
|
+
const preferred = join4(base, STATE_FILE_PREFERRED);
|
|
537
|
+
if (existsSync5(preferred))
|
|
349
538
|
return preferred;
|
|
350
539
|
const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
|
|
351
540
|
for (const variant of variants) {
|
|
352
|
-
const candidate =
|
|
353
|
-
if (
|
|
541
|
+
const candidate = join4(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
|
|
542
|
+
if (existsSync5(candidate))
|
|
354
543
|
return candidate;
|
|
355
544
|
}
|
|
356
545
|
return null;
|
|
@@ -396,13 +585,102 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
396
585
|
return null;
|
|
397
586
|
}
|
|
398
587
|
}
|
|
588
|
+
function accountsHomeDir(env) {
|
|
589
|
+
const override = str(env[ACCOUNTS_HOME_ENV]);
|
|
590
|
+
return override ? expandHome(override, env) : join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
|
|
591
|
+
}
|
|
592
|
+
function sessionAccountUuid(env = process.env) {
|
|
593
|
+
try {
|
|
594
|
+
const stateFile = sessionStateFile(env);
|
|
595
|
+
if (!stateFile)
|
|
596
|
+
return null;
|
|
597
|
+
const uuid = str(readJson(stateFile)?.oauthAccount?.accountUuid);
|
|
598
|
+
return uuid && SAFE_UUID.test(uuid) ? uuid : null;
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function classifyWindow(w, fetchedAt) {
|
|
604
|
+
if (w.scoped === true)
|
|
605
|
+
return "scoped";
|
|
606
|
+
const group = str(w.group);
|
|
607
|
+
if (group === "session" || group === "weekly")
|
|
608
|
+
return group;
|
|
609
|
+
const id = str(w.id);
|
|
610
|
+
if (id) {
|
|
611
|
+
if (id.includes("session"))
|
|
612
|
+
return "session";
|
|
613
|
+
if (id.includes("weekly"))
|
|
614
|
+
return "weekly";
|
|
615
|
+
}
|
|
616
|
+
const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
|
|
617
|
+
if (Number.isFinite(resetsAt) && fetchedAt !== null && resetsAt - fetchedAt > SESSION_WINDOW_MAX_MS) {
|
|
618
|
+
return "weekly";
|
|
619
|
+
}
|
|
620
|
+
return "unknown";
|
|
621
|
+
}
|
|
622
|
+
function windowHeadroom(w, now, fetchedAt) {
|
|
623
|
+
const resetsAt = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
|
|
624
|
+
const rolled = Number.isFinite(resetsAt) && resetsAt <= now && (fetchedAt === null || resetsAt > fetchedAt);
|
|
625
|
+
if (rolled)
|
|
626
|
+
return 100;
|
|
627
|
+
const util = typeof w.utilization === "number" && Number.isFinite(w.utilization) ? w.utilization : null;
|
|
628
|
+
if (util === null)
|
|
629
|
+
return null;
|
|
630
|
+
return Math.max(0, Math.min(100, Math.round(100 - util)));
|
|
631
|
+
}
|
|
632
|
+
function deriveUsage(usage, now) {
|
|
633
|
+
const u = usage ?? {};
|
|
634
|
+
const windows = Array.isArray(u.windows) ? u.windows : [];
|
|
635
|
+
const fetchedAtMs = typeof u.fetchedAt === "string" ? Date.parse(u.fetchedAt) : Number.NaN;
|
|
636
|
+
const fetchedAt = Number.isFinite(fetchedAtMs) ? fetchedAtMs : null;
|
|
637
|
+
let session = null;
|
|
638
|
+
let weekly = null;
|
|
639
|
+
let unknownMin = null;
|
|
640
|
+
for (const w of windows) {
|
|
641
|
+
const cls = classifyWindow(w, fetchedAt);
|
|
642
|
+
if (cls === "scoped")
|
|
643
|
+
continue;
|
|
644
|
+
const h = windowHeadroom(w, now, fetchedAt);
|
|
645
|
+
if (h === null)
|
|
646
|
+
continue;
|
|
647
|
+
if (cls === "session")
|
|
648
|
+
session = session === null ? h : Math.min(session, h);
|
|
649
|
+
else if (cls === "weekly")
|
|
650
|
+
weekly = weekly === null ? h : Math.min(weekly, h);
|
|
651
|
+
else
|
|
652
|
+
unknownMin = unknownMin === null ? h : Math.min(unknownMin, h);
|
|
653
|
+
}
|
|
654
|
+
const weeklyHeadroom = weekly !== null ? unknownMin !== null ? Math.min(weekly, unknownMin) : weekly : unknownMin;
|
|
655
|
+
return { sessionHeadroom: session, weeklyHeadroom };
|
|
656
|
+
}
|
|
657
|
+
function sessionUsage(env = process.env, now = new Date) {
|
|
658
|
+
const empty = { sessionHeadroom: null, weeklyHeadroom: null };
|
|
659
|
+
try {
|
|
660
|
+
const uuid = sessionAccountUuid(env);
|
|
661
|
+
if (!uuid)
|
|
662
|
+
return empty;
|
|
663
|
+
const path = join4(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
|
|
664
|
+
if (!existsSync5(path))
|
|
665
|
+
return empty;
|
|
666
|
+
const entry = readJson(path);
|
|
667
|
+
if (!entry || entry.accountUuid !== uuid)
|
|
668
|
+
return empty;
|
|
669
|
+
const fetchedAt = typeof entry.fetchedAt === "string" ? Date.parse(entry.fetchedAt) : Number.NaN;
|
|
670
|
+
if (!Number.isFinite(fetchedAt) || now.getTime() - fetchedAt > USAGE_CACHE_MAX_AGE_MS)
|
|
671
|
+
return empty;
|
|
672
|
+
return deriveUsage(entry.usage, now.getTime());
|
|
673
|
+
} catch {
|
|
674
|
+
return empty;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
399
677
|
|
|
400
678
|
// src/settings.ts
|
|
401
|
-
import { readFileSync as
|
|
402
|
-
import { join as
|
|
679
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
680
|
+
import { join as join5 } from "path";
|
|
403
681
|
function sessionFastMode(env = process.env) {
|
|
404
682
|
try {
|
|
405
|
-
const raw =
|
|
683
|
+
const raw = readFileSync5(join5(sessionConfigDir(env), "settings.json"), "utf8");
|
|
406
684
|
return JSON.parse(raw)?.fastMode === true;
|
|
407
685
|
} catch {
|
|
408
686
|
return false;
|
|
@@ -426,6 +704,29 @@ function friendlyModel(id) {
|
|
|
426
704
|
return { name: name || mid, tag };
|
|
427
705
|
}
|
|
428
706
|
var dir = (ctx) => ctx.cwd || process.cwd();
|
|
707
|
+
function abbreviateHome(path) {
|
|
708
|
+
const home2 = homedir5();
|
|
709
|
+
if (!home2 || home2 === "/")
|
|
710
|
+
return path;
|
|
711
|
+
if (path === home2)
|
|
712
|
+
return "~";
|
|
713
|
+
return path.startsWith(`${home2}/`) ? `~${path.slice(home2.length)}` : path;
|
|
714
|
+
}
|
|
715
|
+
var CWD_MAX = 40;
|
|
716
|
+
function truncateLeft(path, max) {
|
|
717
|
+
return path.length <= max ? path : `\u2026${path.slice(path.length - (max - 1))}`;
|
|
718
|
+
}
|
|
719
|
+
function contextColor(ctx) {
|
|
720
|
+
const u = contextUsage(ctx);
|
|
721
|
+
if (!u || !u.window)
|
|
722
|
+
return null;
|
|
723
|
+
const pct = Math.min(100, Math.round(u.used / u.window * 100));
|
|
724
|
+
if (pct >= 80)
|
|
725
|
+
return "red";
|
|
726
|
+
if (pct >= 50)
|
|
727
|
+
return "yellow";
|
|
728
|
+
return "green";
|
|
729
|
+
}
|
|
429
730
|
function modelName(ctx) {
|
|
430
731
|
if (ctx.model?.displayName)
|
|
431
732
|
return ctx.model.displayName.toLowerCase();
|
|
@@ -434,6 +735,24 @@ function modelName(ctx) {
|
|
|
434
735
|
function limitPercent(pct) {
|
|
435
736
|
return typeof pct === "number" ? pct : 0;
|
|
436
737
|
}
|
|
738
|
+
function anthropicSession(ctx) {
|
|
739
|
+
if (ctx.rateLimits)
|
|
740
|
+
return true;
|
|
741
|
+
const id = ctx.model?.id;
|
|
742
|
+
return typeof id === "string" && /claude/i.test(id);
|
|
743
|
+
}
|
|
744
|
+
function usageLabel(prefix, headroom) {
|
|
745
|
+
return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
|
|
746
|
+
}
|
|
747
|
+
function usageColor(headroom, binding = false) {
|
|
748
|
+
if (headroom === null)
|
|
749
|
+
return "dim";
|
|
750
|
+
if (headroom < 20)
|
|
751
|
+
return "red";
|
|
752
|
+
if (headroom < 50)
|
|
753
|
+
return "yellow";
|
|
754
|
+
return binding ? "green" : null;
|
|
755
|
+
}
|
|
437
756
|
var segments = [
|
|
438
757
|
{
|
|
439
758
|
id: "machine",
|
|
@@ -491,10 +810,23 @@ var segments = [
|
|
|
491
810
|
defaultEnabled: false,
|
|
492
811
|
render: (ctx) => dir(ctx).split("/").pop() || null
|
|
493
812
|
},
|
|
813
|
+
{
|
|
814
|
+
id: "cwd",
|
|
815
|
+
description: "Current working directory (full path)",
|
|
816
|
+
defaultEnabled: false,
|
|
817
|
+
color: "blue",
|
|
818
|
+
render(ctx) {
|
|
819
|
+
const path = ctx.cwd;
|
|
820
|
+
if (typeof path !== "string" || !path)
|
|
821
|
+
return null;
|
|
822
|
+
return truncateLeft(abbreviateHome(path), CWD_MAX);
|
|
823
|
+
}
|
|
824
|
+
},
|
|
494
825
|
{
|
|
495
826
|
id: "model",
|
|
496
827
|
description: "Current model name",
|
|
497
828
|
defaultEnabled: false,
|
|
829
|
+
color: "dim",
|
|
498
830
|
render(ctx) {
|
|
499
831
|
if (!ctx.model?.id)
|
|
500
832
|
return null;
|
|
@@ -505,6 +837,7 @@ var segments = [
|
|
|
505
837
|
id: "model-context",
|
|
506
838
|
description: "Model name with context-size tag (e.g. [1m])",
|
|
507
839
|
defaultEnabled: true,
|
|
840
|
+
color: "dim",
|
|
508
841
|
render(ctx) {
|
|
509
842
|
if (!ctx.model?.id)
|
|
510
843
|
return null;
|
|
@@ -516,7 +849,7 @@ var segments = [
|
|
|
516
849
|
id: "model-with-reasoning",
|
|
517
850
|
description: "Model name with its reasoning effort (or thinking, when on)",
|
|
518
851
|
defaultEnabled: false,
|
|
519
|
-
color: "
|
|
852
|
+
color: "dim",
|
|
520
853
|
render(ctx) {
|
|
521
854
|
const name = modelName(ctx);
|
|
522
855
|
if (!name)
|
|
@@ -572,6 +905,20 @@ var segments = [
|
|
|
572
905
|
return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
|
|
573
906
|
}
|
|
574
907
|
},
|
|
908
|
+
{
|
|
909
|
+
id: "usage-session",
|
|
910
|
+
description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
|
|
911
|
+
defaultEnabled: false,
|
|
912
|
+
color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).sessionHeadroom) : null,
|
|
913
|
+
render: (ctx) => anthropicSession(ctx) ? usageLabel("5h", sessionUsage(process.env).sessionHeadroom) : null
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: "usage-weekly",
|
|
917
|
+
description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
|
|
918
|
+
defaultEnabled: false,
|
|
919
|
+
color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).weeklyHeadroom, true) : null,
|
|
920
|
+
render: (ctx) => anthropicSession(ctx) ? usageLabel("7d", sessionUsage(process.env).weeklyHeadroom) : null
|
|
921
|
+
},
|
|
575
922
|
{
|
|
576
923
|
id: "thread-title",
|
|
577
924
|
description: "Thread title set with /rename (omitted until named)",
|
|
@@ -583,17 +930,19 @@ var segments = [
|
|
|
583
930
|
id: "context-used",
|
|
584
931
|
description: "Percentage of context window used (omitted when unknown)",
|
|
585
932
|
defaultEnabled: false,
|
|
933
|
+
color: contextColor,
|
|
586
934
|
render(ctx) {
|
|
587
935
|
const u = contextUsage(ctx);
|
|
588
936
|
if (!u)
|
|
589
937
|
return null;
|
|
590
|
-
return
|
|
938
|
+
return `ctx ${Math.min(100, Math.round(u.used / u.window * 100))}%`;
|
|
591
939
|
}
|
|
592
940
|
},
|
|
593
941
|
{
|
|
594
942
|
id: "context-remaining",
|
|
595
943
|
description: "Percentage of context window remaining (omitted when unknown)",
|
|
596
944
|
defaultEnabled: true,
|
|
945
|
+
color: contextColor,
|
|
597
946
|
render(ctx) {
|
|
598
947
|
const u = contextUsage(ctx);
|
|
599
948
|
if (!u)
|
|
@@ -612,10 +961,20 @@ var segments = [
|
|
|
612
961
|
return `${compactNum(u.used + u.output)} tok`;
|
|
613
962
|
}
|
|
614
963
|
},
|
|
964
|
+
{
|
|
965
|
+
id: "cache-rate",
|
|
966
|
+
description: "Percentage of input tokens served from cache (omitted when unknown)",
|
|
967
|
+
defaultEnabled: true,
|
|
968
|
+
render(ctx) {
|
|
969
|
+
const r = cacheRate(ctx);
|
|
970
|
+
return r === null ? null : `${Math.round(r * 100)}% cached`;
|
|
971
|
+
}
|
|
972
|
+
},
|
|
615
973
|
{
|
|
616
974
|
id: "cost",
|
|
617
975
|
description: "Session cost in USD (omitted when zero)",
|
|
618
976
|
defaultEnabled: true,
|
|
977
|
+
color: "dim",
|
|
619
978
|
render(ctx) {
|
|
620
979
|
const c = ctx.cost?.totalCostUsd;
|
|
621
980
|
return c ? money(c) : null;
|
|
@@ -625,6 +984,7 @@ var segments = [
|
|
|
625
984
|
id: "duration",
|
|
626
985
|
description: "Session wall-clock duration",
|
|
627
986
|
defaultEnabled: false,
|
|
987
|
+
color: "dim",
|
|
628
988
|
render(ctx) {
|
|
629
989
|
const ms = ctx.cost?.totalDurationMs;
|
|
630
990
|
return ms ? compactDuration(ms) : null;
|
|
@@ -719,7 +1079,7 @@ async function renderLine(ctx, cfg) {
|
|
|
719
1079
|
// package.json
|
|
720
1080
|
var package_default = {
|
|
721
1081
|
name: "@hasna/statusline",
|
|
722
|
-
version: "0.0.
|
|
1082
|
+
version: "0.0.8",
|
|
723
1083
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
724
1084
|
license: "Apache-2.0",
|
|
725
1085
|
type: "module",
|
|
@@ -758,11 +1118,12 @@ var package_default = {
|
|
|
758
1118
|
},
|
|
759
1119
|
repository: {
|
|
760
1120
|
type: "git",
|
|
761
|
-
url: "
|
|
1121
|
+
url: "https://github.com/hasna/apps.git",
|
|
1122
|
+
directory: "apps/statusline"
|
|
762
1123
|
},
|
|
763
|
-
homepage: "https://github.com/hasna/statusline#readme",
|
|
1124
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/statusline#readme",
|
|
764
1125
|
bugs: {
|
|
765
|
-
url: "https://github.com/hasna/
|
|
1126
|
+
url: "https://github.com/hasna/apps/issues"
|
|
766
1127
|
},
|
|
767
1128
|
keywords: [
|
|
768
1129
|
"statusline",
|
|
@@ -779,36 +1140,37 @@ var package_default = {
|
|
|
779
1140
|
bun: ">=1.0.0"
|
|
780
1141
|
},
|
|
781
1142
|
dependencies: {
|
|
782
|
-
"@modelcontextprotocol/sdk": "1.
|
|
1143
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
783
1144
|
zod: "3.25.76"
|
|
784
1145
|
},
|
|
785
1146
|
devDependencies: {
|
|
786
|
-
"@types/bun": "
|
|
787
|
-
typescript: "5.9.3"
|
|
1147
|
+
"@types/bun": "1.3.14",
|
|
1148
|
+
typescript: "5.9.3",
|
|
1149
|
+
"bun-types": "1.3.14"
|
|
788
1150
|
}
|
|
789
1151
|
};
|
|
790
1152
|
// src/install.ts
|
|
791
|
-
import { copyFileSync, existsSync as
|
|
792
|
-
import { homedir as
|
|
793
|
-
import { dirname as dirname2, join as
|
|
1153
|
+
import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
1154
|
+
import { homedir as homedir6 } from "os";
|
|
1155
|
+
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
794
1156
|
function defaultSettingsPath(env) {
|
|
795
|
-
return
|
|
1157
|
+
return join6(env.HOME || homedir6(), ".claude", "settings.json");
|
|
796
1158
|
}
|
|
797
1159
|
function claudeSettingsPath(env = process.env) {
|
|
798
|
-
const home2 = env.HOME ||
|
|
1160
|
+
const home2 = env.HOME || homedir6();
|
|
799
1161
|
const raw = env.CLAUDE_CONFIG_DIR?.trim();
|
|
800
1162
|
if (!raw)
|
|
801
1163
|
return defaultSettingsPath(env);
|
|
802
|
-
const configDir = raw === "~" || raw.startsWith("~/") ?
|
|
803
|
-
return
|
|
1164
|
+
const configDir = raw === "~" || raw.startsWith("~/") ? join6(home2, raw.slice(1)) : raw;
|
|
1165
|
+
return join6(configDir, "settings.json");
|
|
804
1166
|
}
|
|
805
1167
|
function installClaude(settingsPath, env = process.env) {
|
|
806
1168
|
const target = settingsPath ?? claudeSettingsPath(env);
|
|
807
1169
|
const binary = Bun.which("statusline") || "statusline";
|
|
808
1170
|
let settings = {};
|
|
809
|
-
if (
|
|
1171
|
+
if (existsSync6(target)) {
|
|
810
1172
|
try {
|
|
811
|
-
settings = JSON.parse(
|
|
1173
|
+
settings = JSON.parse(readFileSync6(target, "utf8"));
|
|
812
1174
|
} catch (error) {
|
|
813
1175
|
throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
|
|
814
1176
|
}
|
|
@@ -816,7 +1178,7 @@ function installClaude(settingsPath, env = process.env) {
|
|
|
816
1178
|
}
|
|
817
1179
|
settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
|
|
818
1180
|
const configDir = dirname2(target);
|
|
819
|
-
if (!
|
|
1181
|
+
if (!existsSync6(configDir)) {
|
|
820
1182
|
if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
|
|
821
1183
|
throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
|
|
822
1184
|
}
|
|
@@ -883,6 +1245,14 @@ function resetStatuslineConfig() {
|
|
|
883
1245
|
return defaultConfig();
|
|
884
1246
|
}
|
|
885
1247
|
async function renderStatusline(input = {}, config = loadConfig()) {
|
|
1248
|
+
const provider = process.env.STATUSLINE_PROVIDER;
|
|
1249
|
+
if (provider === "codex" || provider === "opencode") {
|
|
1250
|
+
const ctx = {
|
|
1251
|
+
provider,
|
|
1252
|
+
cwd: typeof input.cwd === "string" ? input.cwd : process.cwd()
|
|
1253
|
+
};
|
|
1254
|
+
return renderLine(ctx, config);
|
|
1255
|
+
}
|
|
886
1256
|
return renderLine(parseClaudeInput(input), config);
|
|
887
1257
|
}
|
|
888
1258
|
function sampleClaudeInput(cwd = process.cwd()) {
|