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