@hasna/statusline 0.0.7 → 0.0.9
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 +98 -31
- package/dist/cli.js +429 -62
- package/dist/context-window.d.ts +22 -0
- package/dist/disk.d.ts +51 -0
- package/dist/format.d.ts +7 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +435 -63
- package/dist/mcp/index.js +19939 -6483
- package/dist/providers/codex.d.ts +19 -0
- package/dist/providers/opencode.d.ts +16 -0
- package/dist/providers/types.d.ts +10 -3
- 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) {
|
|
@@ -126,6 +157,23 @@ function compactNum(n) {
|
|
|
126
157
|
return `${trimmed(n / 1000)}k`;
|
|
127
158
|
return `${n}`;
|
|
128
159
|
}
|
|
160
|
+
var BYTE_UNITS = ["K", "M", "G", "T", "P"];
|
|
161
|
+
function compactBytes(bytes) {
|
|
162
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
163
|
+
return "";
|
|
164
|
+
if (bytes < 1024)
|
|
165
|
+
return `${Math.floor(bytes)}B`;
|
|
166
|
+
let value = bytes / 1024;
|
|
167
|
+
let unit = 0;
|
|
168
|
+
while (unit < BYTE_UNITS.length - 1) {
|
|
169
|
+
const rounded = Number(value.toFixed(1));
|
|
170
|
+
if (rounded < 1024)
|
|
171
|
+
break;
|
|
172
|
+
value /= 1024;
|
|
173
|
+
unit += 1;
|
|
174
|
+
}
|
|
175
|
+
return `${trimmed(value)}${BYTE_UNITS[unit]}`;
|
|
176
|
+
}
|
|
129
177
|
function money(n) {
|
|
130
178
|
return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
131
179
|
}
|
|
@@ -135,10 +183,10 @@ function compactDuration(ms) {
|
|
|
135
183
|
return `${s}s`;
|
|
136
184
|
if (s < 3600) {
|
|
137
185
|
const rem2 = s % 60;
|
|
138
|
-
return rem2 ? `${Math.floor(s / 60)}m${rem2}s` : `${Math.floor(s / 60)}m`;
|
|
186
|
+
return rem2 ? `${Math.floor(s / 60)}m ${rem2}s` : `${Math.floor(s / 60)}m`;
|
|
139
187
|
}
|
|
140
188
|
const rem = Math.floor(s % 3600 / 60);
|
|
141
|
-
return rem ? `${Math.floor(s / 3600)}h${rem}m` : `${Math.floor(s / 3600)}h`;
|
|
189
|
+
return rem ? `${Math.floor(s / 3600)}h ${rem}m` : `${Math.floor(s / 3600)}h`;
|
|
142
190
|
}
|
|
143
191
|
|
|
144
192
|
// src/git.ts
|
|
@@ -195,14 +243,145 @@ function trackedLineCount(cwd) {
|
|
|
195
243
|
}
|
|
196
244
|
|
|
197
245
|
// src/context-window.ts
|
|
198
|
-
import { existsSync as
|
|
199
|
-
|
|
200
|
-
|
|
246
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
247
|
+
|
|
248
|
+
// src/providers/codex.ts
|
|
249
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
250
|
+
import { homedir as homedir2 } from "os";
|
|
251
|
+
import { join as join2 } from "path";
|
|
252
|
+
function latestCodexSessionPath(home = homedir2()) {
|
|
253
|
+
const root = join2(home, ".codex", "sessions");
|
|
254
|
+
if (!existsSync2(root))
|
|
255
|
+
return null;
|
|
256
|
+
let newest = null;
|
|
257
|
+
let newestMtime = 0;
|
|
258
|
+
try {
|
|
259
|
+
for (const year of readdirSync(root)) {
|
|
260
|
+
const yearDir = join2(root, year);
|
|
261
|
+
if (!statSync(yearDir, { throwIfNoEntry: false })?.isDirectory())
|
|
262
|
+
continue;
|
|
263
|
+
for (const month of readdirSync(yearDir)) {
|
|
264
|
+
const monthDir = join2(yearDir, month);
|
|
265
|
+
if (!statSync(monthDir, { throwIfNoEntry: false })?.isDirectory())
|
|
266
|
+
continue;
|
|
267
|
+
for (const day of readdirSync(monthDir)) {
|
|
268
|
+
const dayDir = join2(monthDir, day);
|
|
269
|
+
if (!statSync(dayDir, { throwIfNoEntry: false })?.isDirectory())
|
|
270
|
+
continue;
|
|
271
|
+
for (const file of readdirSync(dayDir)) {
|
|
272
|
+
if (!file.startsWith("rollout-") || !file.endsWith(".jsonl"))
|
|
273
|
+
continue;
|
|
274
|
+
const full = join2(dayDir, file);
|
|
275
|
+
const mtime = statSync(full, { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
276
|
+
if (mtime > newestMtime) {
|
|
277
|
+
newestMtime = mtime;
|
|
278
|
+
newest = full;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return newest;
|
|
288
|
+
}
|
|
289
|
+
function codexCacheRate(sessionPath) {
|
|
290
|
+
const path = sessionPath ?? latestCodexSessionPath();
|
|
201
291
|
if (!path || !existsSync2(path))
|
|
202
292
|
return null;
|
|
203
293
|
let lines;
|
|
204
294
|
try {
|
|
205
295
|
lines = readFileSync2(path, "utf8").split(`
|
|
296
|
+
`);
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
let usage = null;
|
|
301
|
+
for (const line of lines) {
|
|
302
|
+
if (!line || !line.includes("token_count"))
|
|
303
|
+
continue;
|
|
304
|
+
try {
|
|
305
|
+
const entry = JSON.parse(line);
|
|
306
|
+
if (entry?.type !== "event_msg")
|
|
307
|
+
continue;
|
|
308
|
+
if (entry?.payload?.type !== "token_count")
|
|
309
|
+
continue;
|
|
310
|
+
const info = entry?.payload?.info?.total_token_usage;
|
|
311
|
+
if (info && typeof info === "object")
|
|
312
|
+
usage = info;
|
|
313
|
+
} catch {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!usage)
|
|
318
|
+
return null;
|
|
319
|
+
const read = usage.cached_input_tokens || 0;
|
|
320
|
+
const input = usage.input_tokens || 0;
|
|
321
|
+
const write = usage.cache_write_input_tokens || 0;
|
|
322
|
+
const divisor = input + read + write;
|
|
323
|
+
if (divisor <= 0)
|
|
324
|
+
return null;
|
|
325
|
+
return Math.min(1, Math.max(0, read / divisor));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/providers/opencode.ts
|
|
329
|
+
import { existsSync as existsSync3 } from "fs";
|
|
330
|
+
import { homedir as homedir3 } from "os";
|
|
331
|
+
import { join as join3 } from "path";
|
|
332
|
+
import { Database } from "bun:sqlite";
|
|
333
|
+
function opencodeCacheRate(opts = {}) {
|
|
334
|
+
const dbPath = opts.dbPath ?? join3(homedir3(), ".local", "share", "opencode", "opencode.db");
|
|
335
|
+
if (!existsSync3(dbPath))
|
|
336
|
+
return null;
|
|
337
|
+
let db;
|
|
338
|
+
try {
|
|
339
|
+
db = new Database(dbPath, { readonly: true });
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
let row = tokenRow(db, "session", opts.sessionId);
|
|
345
|
+
if (!row)
|
|
346
|
+
row = tokenRow(db, "session_v2", opts.sessionId);
|
|
347
|
+
if (!row)
|
|
348
|
+
return null;
|
|
349
|
+
const input = row.tokens_input ?? 0;
|
|
350
|
+
const read = row.tokens_cache_read ?? 0;
|
|
351
|
+
const write = row.tokens_cache_write ?? 0;
|
|
352
|
+
const divisor = input + read + write;
|
|
353
|
+
if (divisor <= 0)
|
|
354
|
+
return null;
|
|
355
|
+
return Math.min(1, Math.max(0, read / divisor));
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
} finally {
|
|
359
|
+
db.close();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function tokenRow(db, table, sessionId) {
|
|
363
|
+
try {
|
|
364
|
+
if (sessionId) {
|
|
365
|
+
return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
|
|
366
|
+
FROM ${table} WHERE id = ? LIMIT 1`).get(sessionId);
|
|
367
|
+
}
|
|
368
|
+
return db.query(`SELECT tokens_input, tokens_cache_read, tokens_cache_write
|
|
369
|
+
FROM ${table}
|
|
370
|
+
WHERE tokens_input > 0 OR tokens_cache_read > 0 OR tokens_cache_write > 0
|
|
371
|
+
ORDER BY time_updated DESC LIMIT 1`).get();
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/context-window.ts
|
|
378
|
+
function lastUsageBlock(ctx) {
|
|
379
|
+
const path = ctx.transcriptPath;
|
|
380
|
+
if (!path || !existsSync4(path))
|
|
381
|
+
return null;
|
|
382
|
+
let lines;
|
|
383
|
+
try {
|
|
384
|
+
lines = readFileSync3(path, "utf8").split(`
|
|
206
385
|
`);
|
|
207
386
|
} catch {
|
|
208
387
|
return null;
|
|
@@ -216,26 +395,137 @@ function contextUsage(ctx) {
|
|
|
216
395
|
const usage = entry?.message?.usage;
|
|
217
396
|
if (!usage || typeof usage.input_tokens !== "number")
|
|
218
397
|
continue;
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
used,
|
|
222
|
-
output: usage.output_tokens || 0,
|
|
223
|
-
window: windowSize(ctx)
|
|
224
|
-
};
|
|
398
|
+
return usage;
|
|
225
399
|
} catch {
|
|
226
400
|
continue;
|
|
227
401
|
}
|
|
228
402
|
}
|
|
229
403
|
return null;
|
|
230
404
|
}
|
|
405
|
+
function contextUsage(ctx) {
|
|
406
|
+
const usage = lastUsageBlock(ctx);
|
|
407
|
+
if (!usage)
|
|
408
|
+
return null;
|
|
409
|
+
const used = usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
410
|
+
return {
|
|
411
|
+
used,
|
|
412
|
+
output: usage.output_tokens || 0,
|
|
413
|
+
window: windowSize(ctx)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function cacheRate(ctx) {
|
|
417
|
+
if (ctx.provider === "codex")
|
|
418
|
+
return codexCacheRate();
|
|
419
|
+
if (ctx.provider === "opencode")
|
|
420
|
+
return opencodeCacheRate();
|
|
421
|
+
const usage = lastUsageBlock(ctx);
|
|
422
|
+
if (!usage)
|
|
423
|
+
return null;
|
|
424
|
+
const read = usage.cache_read_input_tokens || 0;
|
|
425
|
+
const creation = usage.cache_creation_input_tokens || 0;
|
|
426
|
+
const input = usage.input_tokens || 0;
|
|
427
|
+
const divisor = input + creation + read;
|
|
428
|
+
if (divisor <= 0)
|
|
429
|
+
return null;
|
|
430
|
+
return read / divisor;
|
|
431
|
+
}
|
|
231
432
|
function windowSize(ctx) {
|
|
232
433
|
return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
|
|
233
434
|
}
|
|
234
435
|
|
|
436
|
+
// src/disk.ts
|
|
437
|
+
import { statfsSync } from "fs";
|
|
438
|
+
var DISK_MEMO = Symbol("statusline.disk-free");
|
|
439
|
+
var DISK_WARN_PERCENT = 20;
|
|
440
|
+
var DISK_CRITICAL_PERCENT = 10;
|
|
441
|
+
function finiteNonNegative(value) {
|
|
442
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
443
|
+
}
|
|
444
|
+
function readDiskStats(path) {
|
|
445
|
+
if (typeof statfsSync !== "function")
|
|
446
|
+
return null;
|
|
447
|
+
try {
|
|
448
|
+
const stats = statfsSync(path);
|
|
449
|
+
if (!stats)
|
|
450
|
+
return null;
|
|
451
|
+
return {
|
|
452
|
+
bsize: Number(stats.bsize),
|
|
453
|
+
blocks: Number(stats.blocks),
|
|
454
|
+
bavail: Number(stats.bavail)
|
|
455
|
+
};
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function diskFree(path, reader = readDiskStats) {
|
|
461
|
+
let stats;
|
|
462
|
+
try {
|
|
463
|
+
stats = reader(path);
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
if (!stats)
|
|
468
|
+
return null;
|
|
469
|
+
const bsize = finiteNonNegative(stats.bsize);
|
|
470
|
+
const blocks = finiteNonNegative(stats.blocks);
|
|
471
|
+
const bavail = finiteNonNegative(stats.bavail);
|
|
472
|
+
if (bsize === null || blocks === null || bavail === null)
|
|
473
|
+
return null;
|
|
474
|
+
if (bsize <= 0 || blocks <= 0)
|
|
475
|
+
return null;
|
|
476
|
+
const available = Math.min(bavail, blocks);
|
|
477
|
+
const totalBytes = bsize * blocks;
|
|
478
|
+
const freeBytes = bsize * available;
|
|
479
|
+
if (!Number.isFinite(totalBytes) || !Number.isFinite(freeBytes))
|
|
480
|
+
return null;
|
|
481
|
+
return {
|
|
482
|
+
freeBytes,
|
|
483
|
+
totalBytes,
|
|
484
|
+
freePercent: available / blocks * 100
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function diskFreeFor(path, memo, reader = readDiskStats) {
|
|
488
|
+
if (!memo)
|
|
489
|
+
return diskFree(path, reader);
|
|
490
|
+
const hit = memo.get(DISK_MEMO);
|
|
491
|
+
if (hit && hit.path === path)
|
|
492
|
+
return hit.value;
|
|
493
|
+
const value = diskFree(path, reader);
|
|
494
|
+
memo.set(DISK_MEMO, { path, value });
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
function diskLevel(freePercent) {
|
|
498
|
+
if (!Number.isFinite(freePercent))
|
|
499
|
+
return null;
|
|
500
|
+
if (freePercent < DISK_CRITICAL_PERCENT)
|
|
501
|
+
return "critical";
|
|
502
|
+
if (freePercent < DISK_WARN_PERCENT)
|
|
503
|
+
return "low";
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function formatDiskFree(disk) {
|
|
507
|
+
const value = compactBytes(disk.freeBytes);
|
|
508
|
+
if (!value)
|
|
509
|
+
return "";
|
|
510
|
+
return diskLevel(disk.freePercent) ? `disk ${value} low` : `disk ${value}`;
|
|
511
|
+
}
|
|
512
|
+
function diskColor(disk) {
|
|
513
|
+
if (!disk)
|
|
514
|
+
return null;
|
|
515
|
+
switch (diskLevel(disk.freePercent)) {
|
|
516
|
+
case "critical":
|
|
517
|
+
return "red";
|
|
518
|
+
case "low":
|
|
519
|
+
return "yellow";
|
|
520
|
+
default:
|
|
521
|
+
return "dim";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
235
525
|
// src/accounts.ts
|
|
236
|
-
import { existsSync as
|
|
237
|
-
import { homedir as
|
|
238
|
-
import { join as
|
|
526
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
527
|
+
import { homedir as homedir4 } from "os";
|
|
528
|
+
import { join as join4, resolve, sep } from "path";
|
|
239
529
|
var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
|
|
240
530
|
var DEFAULT_CONFIG_SUBDIR = ".claude";
|
|
241
531
|
var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
|
|
@@ -259,10 +549,10 @@ function str(value) {
|
|
|
259
549
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
260
550
|
}
|
|
261
551
|
function home(env) {
|
|
262
|
-
return str(env.HOME) ??
|
|
552
|
+
return str(env.HOME) ?? homedir4();
|
|
263
553
|
}
|
|
264
554
|
function expandHome(path, env) {
|
|
265
|
-
return path === "~" || path.startsWith(`~${sep}`) ?
|
|
555
|
+
return path === "~" || path.startsWith(`~${sep}`) ? join4(home(env), path.slice(1)) : path;
|
|
266
556
|
}
|
|
267
557
|
function canonical(path, env) {
|
|
268
558
|
const abs = resolve(expandHome(path, env));
|
|
@@ -274,7 +564,7 @@ function canonical(path, env) {
|
|
|
274
564
|
}
|
|
275
565
|
function readJson(path) {
|
|
276
566
|
try {
|
|
277
|
-
return JSON.parse(
|
|
567
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
278
568
|
} catch {
|
|
279
569
|
return null;
|
|
280
570
|
}
|
|
@@ -295,14 +585,14 @@ async function accountsPaths(env) {
|
|
|
295
585
|
return { store: storage.storePath(), profiles: storage.profilesDir() };
|
|
296
586
|
} catch {}
|
|
297
587
|
}
|
|
298
|
-
const base = homeOverride ??
|
|
588
|
+
const base = homeOverride ?? join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
|
|
299
589
|
return {
|
|
300
|
-
store: storeOverride ??
|
|
301
|
-
profiles:
|
|
590
|
+
store: storeOverride ?? join4(base, STORE_FILE),
|
|
591
|
+
profiles: join4(base, PROFILES_SUBDIR)
|
|
302
592
|
};
|
|
303
593
|
}
|
|
304
594
|
function registryEntry(store, configDir, env, tool) {
|
|
305
|
-
if (!
|
|
595
|
+
if (!existsSync5(store))
|
|
306
596
|
return null;
|
|
307
597
|
const entries = readJson(store)?.profiles;
|
|
308
598
|
if (!Array.isArray(entries))
|
|
@@ -323,7 +613,7 @@ function registryEntry(store, configDir, env, tool) {
|
|
|
323
613
|
return tool ? untyped : first;
|
|
324
614
|
}
|
|
325
615
|
function layoutProfile(profilesRoot, configDir, env) {
|
|
326
|
-
if (!
|
|
616
|
+
if (!existsSync5(configDir))
|
|
327
617
|
return null;
|
|
328
618
|
const prefix = canonical(profilesRoot, env) + sep;
|
|
329
619
|
if (!configDir.startsWith(prefix))
|
|
@@ -337,7 +627,7 @@ function layoutProfile(profilesRoot, configDir, env) {
|
|
|
337
627
|
return { configDir, profile, tool, source: "layout" };
|
|
338
628
|
}
|
|
339
629
|
function switchedOccupant(configDir) {
|
|
340
|
-
const raw = readJson(
|
|
630
|
+
const raw = readJson(join4(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
|
|
341
631
|
if (!raw)
|
|
342
632
|
return null;
|
|
343
633
|
const profile = str(raw.profile);
|
|
@@ -345,17 +635,17 @@ function switchedOccupant(configDir) {
|
|
|
345
635
|
return profile || email ? { profile, email } : null;
|
|
346
636
|
}
|
|
347
637
|
function sessionConfigDir(env = process.env) {
|
|
348
|
-
return canonical(str(env[CONFIG_DIR_ENV]) ??
|
|
638
|
+
return canonical(str(env[CONFIG_DIR_ENV]) ?? join4(home(env), DEFAULT_CONFIG_SUBDIR), env);
|
|
349
639
|
}
|
|
350
640
|
function sessionStateFile(env = process.env) {
|
|
351
641
|
const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
|
|
352
|
-
const preferred =
|
|
353
|
-
if (
|
|
642
|
+
const preferred = join4(base, STATE_FILE_PREFERRED);
|
|
643
|
+
if (existsSync5(preferred))
|
|
354
644
|
return preferred;
|
|
355
645
|
const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
|
|
356
646
|
for (const variant of variants) {
|
|
357
|
-
const candidate =
|
|
358
|
-
if (
|
|
647
|
+
const candidate = join4(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
|
|
648
|
+
if (existsSync5(candidate))
|
|
359
649
|
return candidate;
|
|
360
650
|
}
|
|
361
651
|
return null;
|
|
@@ -403,7 +693,7 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
403
693
|
}
|
|
404
694
|
function accountsHomeDir(env) {
|
|
405
695
|
const override = str(env[ACCOUNTS_HOME_ENV]);
|
|
406
|
-
return override ? expandHome(override, env) :
|
|
696
|
+
return override ? expandHome(override, env) : join4(home(env), ...DEFAULT_ACCOUNTS_HOME);
|
|
407
697
|
}
|
|
408
698
|
function sessionAccountUuid(env = process.env) {
|
|
409
699
|
try {
|
|
@@ -476,8 +766,8 @@ function sessionUsage(env = process.env, now = new Date) {
|
|
|
476
766
|
const uuid = sessionAccountUuid(env);
|
|
477
767
|
if (!uuid)
|
|
478
768
|
return empty;
|
|
479
|
-
const path =
|
|
480
|
-
if (!
|
|
769
|
+
const path = join4(accountsHomeDir(env), CACHE_SUBDIR, USAGE_CACHE_SUBDIR, `${uuid}.json`);
|
|
770
|
+
if (!existsSync5(path))
|
|
481
771
|
return empty;
|
|
482
772
|
const entry = readJson(path);
|
|
483
773
|
if (!entry || entry.accountUuid !== uuid)
|
|
@@ -492,11 +782,11 @@ function sessionUsage(env = process.env, now = new Date) {
|
|
|
492
782
|
}
|
|
493
783
|
|
|
494
784
|
// src/settings.ts
|
|
495
|
-
import { readFileSync as
|
|
496
|
-
import { join as
|
|
785
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
786
|
+
import { join as join5 } from "path";
|
|
497
787
|
function sessionFastMode(env = process.env) {
|
|
498
788
|
try {
|
|
499
|
-
const raw =
|
|
789
|
+
const raw = readFileSync5(join5(sessionConfigDir(env), "settings.json"), "utf8");
|
|
500
790
|
return JSON.parse(raw)?.fastMode === true;
|
|
501
791
|
} catch {
|
|
502
792
|
return false;
|
|
@@ -520,6 +810,29 @@ function friendlyModel(id) {
|
|
|
520
810
|
return { name: name || mid, tag };
|
|
521
811
|
}
|
|
522
812
|
var dir = (ctx) => ctx.cwd || process.cwd();
|
|
813
|
+
function abbreviateHome(path) {
|
|
814
|
+
const home2 = homedir5();
|
|
815
|
+
if (!home2 || home2 === "/")
|
|
816
|
+
return path;
|
|
817
|
+
if (path === home2)
|
|
818
|
+
return "~";
|
|
819
|
+
return path.startsWith(`${home2}/`) ? `~${path.slice(home2.length)}` : path;
|
|
820
|
+
}
|
|
821
|
+
var CWD_MAX = 40;
|
|
822
|
+
function truncateLeft(path, max) {
|
|
823
|
+
return path.length <= max ? path : `\u2026${path.slice(path.length - (max - 1))}`;
|
|
824
|
+
}
|
|
825
|
+
function contextColor(ctx) {
|
|
826
|
+
const u = contextUsage(ctx);
|
|
827
|
+
if (!u || !u.window)
|
|
828
|
+
return null;
|
|
829
|
+
const pct = Math.min(100, Math.round(u.used / u.window * 100));
|
|
830
|
+
if (pct >= 80)
|
|
831
|
+
return "red";
|
|
832
|
+
if (pct >= 50)
|
|
833
|
+
return "yellow";
|
|
834
|
+
return "green";
|
|
835
|
+
}
|
|
523
836
|
function modelName(ctx) {
|
|
524
837
|
if (ctx.model?.displayName)
|
|
525
838
|
return ctx.model.displayName.toLowerCase();
|
|
@@ -528,6 +841,12 @@ function modelName(ctx) {
|
|
|
528
841
|
function limitPercent(pct) {
|
|
529
842
|
return typeof pct === "number" ? pct : 0;
|
|
530
843
|
}
|
|
844
|
+
function anthropicSession(ctx) {
|
|
845
|
+
if (ctx.rateLimits)
|
|
846
|
+
return true;
|
|
847
|
+
const id = ctx.model?.id;
|
|
848
|
+
return typeof id === "string" && /claude/i.test(id);
|
|
849
|
+
}
|
|
531
850
|
function usageLabel(prefix, headroom) {
|
|
532
851
|
return headroom === null ? `${prefix} \u2014` : `${prefix} ${headroom}%`;
|
|
533
852
|
}
|
|
@@ -591,16 +910,39 @@ var segments = [
|
|
|
591
910
|
return n ? compactNum(n) : null;
|
|
592
911
|
}
|
|
593
912
|
},
|
|
913
|
+
{
|
|
914
|
+
id: "disk-free",
|
|
915
|
+
description: "Free space on the volume holding the working directory",
|
|
916
|
+
defaultEnabled: false,
|
|
917
|
+
color: (ctx, memo) => diskColor(diskFreeFor(dir(ctx), memo)),
|
|
918
|
+
render(ctx, memo) {
|
|
919
|
+
const disk = diskFreeFor(dir(ctx), memo);
|
|
920
|
+
return disk ? formatDiskFree(disk) || null : null;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
594
923
|
{
|
|
595
924
|
id: "current-dir",
|
|
596
925
|
description: "Current working directory (basename)",
|
|
597
926
|
defaultEnabled: false,
|
|
598
927
|
render: (ctx) => dir(ctx).split("/").pop() || null
|
|
599
928
|
},
|
|
929
|
+
{
|
|
930
|
+
id: "cwd",
|
|
931
|
+
description: "Current working directory (full path)",
|
|
932
|
+
defaultEnabled: false,
|
|
933
|
+
color: "blue",
|
|
934
|
+
render(ctx) {
|
|
935
|
+
const path = ctx.cwd;
|
|
936
|
+
if (typeof path !== "string" || !path)
|
|
937
|
+
return null;
|
|
938
|
+
return truncateLeft(abbreviateHome(path), CWD_MAX);
|
|
939
|
+
}
|
|
940
|
+
},
|
|
600
941
|
{
|
|
601
942
|
id: "model",
|
|
602
943
|
description: "Current model name",
|
|
603
944
|
defaultEnabled: false,
|
|
945
|
+
color: "dim",
|
|
604
946
|
render(ctx) {
|
|
605
947
|
if (!ctx.model?.id)
|
|
606
948
|
return null;
|
|
@@ -611,6 +953,7 @@ var segments = [
|
|
|
611
953
|
id: "model-context",
|
|
612
954
|
description: "Model name with context-size tag (e.g. [1m])",
|
|
613
955
|
defaultEnabled: true,
|
|
956
|
+
color: "dim",
|
|
614
957
|
render(ctx) {
|
|
615
958
|
if (!ctx.model?.id)
|
|
616
959
|
return null;
|
|
@@ -622,7 +965,7 @@ var segments = [
|
|
|
622
965
|
id: "model-with-reasoning",
|
|
623
966
|
description: "Model name with its reasoning effort (or thinking, when on)",
|
|
624
967
|
defaultEnabled: false,
|
|
625
|
-
color: "
|
|
968
|
+
color: "dim",
|
|
626
969
|
render(ctx) {
|
|
627
970
|
const name = modelName(ctx);
|
|
628
971
|
if (!name)
|
|
@@ -682,15 +1025,15 @@ var segments = [
|
|
|
682
1025
|
id: "usage-session",
|
|
683
1026
|
description: "Session (5-hour) usage remaining for this pane's own account, from the local accounts usage cache",
|
|
684
1027
|
defaultEnabled: false,
|
|
685
|
-
color: () => usageColor(sessionUsage(process.env).sessionHeadroom),
|
|
686
|
-
render: () => usageLabel("5h", sessionUsage(process.env).sessionHeadroom)
|
|
1028
|
+
color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).sessionHeadroom) : null,
|
|
1029
|
+
render: (ctx) => anthropicSession(ctx) ? usageLabel("5h", sessionUsage(process.env).sessionHeadroom) : null
|
|
687
1030
|
},
|
|
688
1031
|
{
|
|
689
1032
|
id: "usage-weekly",
|
|
690
1033
|
description: "Weekly usage remaining for this pane's own account \u2014 the binding limit \u2014 from the local accounts usage cache",
|
|
691
1034
|
defaultEnabled: false,
|
|
692
|
-
color: () => usageColor(sessionUsage(process.env).weeklyHeadroom, true),
|
|
693
|
-
render: () => usageLabel("7d", sessionUsage(process.env).weeklyHeadroom)
|
|
1035
|
+
color: (ctx) => anthropicSession(ctx) ? usageColor(sessionUsage(process.env).weeklyHeadroom, true) : null,
|
|
1036
|
+
render: (ctx) => anthropicSession(ctx) ? usageLabel("7d", sessionUsage(process.env).weeklyHeadroom) : null
|
|
694
1037
|
},
|
|
695
1038
|
{
|
|
696
1039
|
id: "thread-title",
|
|
@@ -703,17 +1046,19 @@ var segments = [
|
|
|
703
1046
|
id: "context-used",
|
|
704
1047
|
description: "Percentage of context window used (omitted when unknown)",
|
|
705
1048
|
defaultEnabled: false,
|
|
1049
|
+
color: contextColor,
|
|
706
1050
|
render(ctx) {
|
|
707
1051
|
const u = contextUsage(ctx);
|
|
708
1052
|
if (!u)
|
|
709
1053
|
return null;
|
|
710
|
-
return
|
|
1054
|
+
return `ctx ${Math.min(100, Math.round(u.used / u.window * 100))}%`;
|
|
711
1055
|
}
|
|
712
1056
|
},
|
|
713
1057
|
{
|
|
714
1058
|
id: "context-remaining",
|
|
715
1059
|
description: "Percentage of context window remaining (omitted when unknown)",
|
|
716
1060
|
defaultEnabled: true,
|
|
1061
|
+
color: contextColor,
|
|
717
1062
|
render(ctx) {
|
|
718
1063
|
const u = contextUsage(ctx);
|
|
719
1064
|
if (!u)
|
|
@@ -732,10 +1077,20 @@ var segments = [
|
|
|
732
1077
|
return `${compactNum(u.used + u.output)} tok`;
|
|
733
1078
|
}
|
|
734
1079
|
},
|
|
1080
|
+
{
|
|
1081
|
+
id: "cache-rate",
|
|
1082
|
+
description: "Percentage of input tokens served from cache (omitted when unknown)",
|
|
1083
|
+
defaultEnabled: true,
|
|
1084
|
+
render(ctx) {
|
|
1085
|
+
const r = cacheRate(ctx);
|
|
1086
|
+
return r === null ? null : `${Math.round(r * 100)}% cached`;
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
735
1089
|
{
|
|
736
1090
|
id: "cost",
|
|
737
1091
|
description: "Session cost in USD (omitted when zero)",
|
|
738
1092
|
defaultEnabled: true,
|
|
1093
|
+
color: "dim",
|
|
739
1094
|
render(ctx) {
|
|
740
1095
|
const c = ctx.cost?.totalCostUsd;
|
|
741
1096
|
return c ? money(c) : null;
|
|
@@ -745,6 +1100,7 @@ var segments = [
|
|
|
745
1100
|
id: "duration",
|
|
746
1101
|
description: "Session wall-clock duration",
|
|
747
1102
|
defaultEnabled: false,
|
|
1103
|
+
color: "dim",
|
|
748
1104
|
render(ctx) {
|
|
749
1105
|
const ms = ctx.cost?.totalDurationMs;
|
|
750
1106
|
return ms ? compactDuration(ms) : null;
|
|
@@ -811,13 +1167,14 @@ function colorsEnabled(cfg, env = process.env) {
|
|
|
811
1167
|
return false;
|
|
812
1168
|
return cfg.colors !== false;
|
|
813
1169
|
}
|
|
814
|
-
function paint(segment, text, ctx) {
|
|
815
|
-
const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
|
|
1170
|
+
function paint(segment, text, ctx, memo) {
|
|
1171
|
+
const color = typeof segment.color === "function" ? segment.color(ctx, memo) : segment.color;
|
|
816
1172
|
const code = color ? ANSI[color] : undefined;
|
|
817
1173
|
return code ? `${code}${text}${RESET}` : text;
|
|
818
1174
|
}
|
|
819
1175
|
async function renderLine(ctx, cfg) {
|
|
820
1176
|
const colorize = colorsEnabled(cfg);
|
|
1177
|
+
const memo = new Map;
|
|
821
1178
|
const rows = [[]];
|
|
822
1179
|
for (const id of cfg.segments) {
|
|
823
1180
|
if (id === "newline") {
|
|
@@ -828,9 +1185,9 @@ async function renderLine(ctx, cfg) {
|
|
|
828
1185
|
if (!segment)
|
|
829
1186
|
continue;
|
|
830
1187
|
try {
|
|
831
|
-
const out = await segment.render(ctx);
|
|
1188
|
+
const out = await segment.render(ctx, memo);
|
|
832
1189
|
if (out)
|
|
833
|
-
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
|
|
1190
|
+
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx, memo) : out);
|
|
834
1191
|
} catch {}
|
|
835
1192
|
}
|
|
836
1193
|
return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
|
|
@@ -839,7 +1196,7 @@ async function renderLine(ctx, cfg) {
|
|
|
839
1196
|
// package.json
|
|
840
1197
|
var package_default = {
|
|
841
1198
|
name: "@hasna/statusline",
|
|
842
|
-
version: "0.0.
|
|
1199
|
+
version: "0.0.9",
|
|
843
1200
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
844
1201
|
license: "Apache-2.0",
|
|
845
1202
|
type: "module",
|
|
@@ -878,11 +1235,12 @@ var package_default = {
|
|
|
878
1235
|
},
|
|
879
1236
|
repository: {
|
|
880
1237
|
type: "git",
|
|
881
|
-
url: "
|
|
1238
|
+
url: "https://github.com/hasna/apps.git",
|
|
1239
|
+
directory: "apps/statusline"
|
|
882
1240
|
},
|
|
883
|
-
homepage: "https://github.com/hasna/statusline#readme",
|
|
1241
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/statusline#readme",
|
|
884
1242
|
bugs: {
|
|
885
|
-
url: "https://github.com/hasna/
|
|
1243
|
+
url: "https://github.com/hasna/apps/issues"
|
|
886
1244
|
},
|
|
887
1245
|
keywords: [
|
|
888
1246
|
"statusline",
|
|
@@ -899,36 +1257,37 @@ var package_default = {
|
|
|
899
1257
|
bun: ">=1.0.0"
|
|
900
1258
|
},
|
|
901
1259
|
dependencies: {
|
|
902
|
-
"@modelcontextprotocol/sdk": "1.
|
|
1260
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
903
1261
|
zod: "3.25.76"
|
|
904
1262
|
},
|
|
905
1263
|
devDependencies: {
|
|
906
|
-
"@types/bun": "
|
|
907
|
-
typescript: "5.9.3"
|
|
1264
|
+
"@types/bun": "1.3.14",
|
|
1265
|
+
typescript: "5.9.3",
|
|
1266
|
+
"bun-types": "1.3.14"
|
|
908
1267
|
}
|
|
909
1268
|
};
|
|
910
1269
|
// src/install.ts
|
|
911
|
-
import { copyFileSync, existsSync as
|
|
912
|
-
import { homedir as
|
|
913
|
-
import { dirname as dirname2, join as
|
|
1270
|
+
import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
1271
|
+
import { homedir as homedir6 } from "os";
|
|
1272
|
+
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
914
1273
|
function defaultSettingsPath(env) {
|
|
915
|
-
return
|
|
1274
|
+
return join6(env.HOME || homedir6(), ".claude", "settings.json");
|
|
916
1275
|
}
|
|
917
1276
|
function claudeSettingsPath(env = process.env) {
|
|
918
|
-
const home2 = env.HOME ||
|
|
1277
|
+
const home2 = env.HOME || homedir6();
|
|
919
1278
|
const raw = env.CLAUDE_CONFIG_DIR?.trim();
|
|
920
1279
|
if (!raw)
|
|
921
1280
|
return defaultSettingsPath(env);
|
|
922
|
-
const configDir = raw === "~" || raw.startsWith("~/") ?
|
|
923
|
-
return
|
|
1281
|
+
const configDir = raw === "~" || raw.startsWith("~/") ? join6(home2, raw.slice(1)) : raw;
|
|
1282
|
+
return join6(configDir, "settings.json");
|
|
924
1283
|
}
|
|
925
1284
|
function installClaude(settingsPath, env = process.env) {
|
|
926
1285
|
const target = settingsPath ?? claudeSettingsPath(env);
|
|
927
1286
|
const binary = Bun.which("statusline") || "statusline";
|
|
928
1287
|
let settings = {};
|
|
929
|
-
if (
|
|
1288
|
+
if (existsSync6(target)) {
|
|
930
1289
|
try {
|
|
931
|
-
settings = JSON.parse(
|
|
1290
|
+
settings = JSON.parse(readFileSync6(target, "utf8"));
|
|
932
1291
|
} catch (error) {
|
|
933
1292
|
throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
|
|
934
1293
|
}
|
|
@@ -936,7 +1295,7 @@ function installClaude(settingsPath, env = process.env) {
|
|
|
936
1295
|
}
|
|
937
1296
|
settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
|
|
938
1297
|
const configDir = dirname2(target);
|
|
939
|
-
if (!
|
|
1298
|
+
if (!existsSync6(configDir)) {
|
|
940
1299
|
if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
|
|
941
1300
|
throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
|
|
942
1301
|
}
|
|
@@ -1003,6 +1362,14 @@ function resetStatuslineConfig() {
|
|
|
1003
1362
|
return defaultConfig();
|
|
1004
1363
|
}
|
|
1005
1364
|
async function renderStatusline(input = {}, config = loadConfig()) {
|
|
1365
|
+
const provider = process.env.STATUSLINE_PROVIDER;
|
|
1366
|
+
if (provider === "codex" || provider === "opencode") {
|
|
1367
|
+
const ctx = {
|
|
1368
|
+
provider,
|
|
1369
|
+
cwd: typeof input.cwd === "string" ? input.cwd : process.cwd()
|
|
1370
|
+
};
|
|
1371
|
+
return renderLine(ctx, config);
|
|
1372
|
+
}
|
|
1006
1373
|
return renderLine(parseClaudeInput(input), config);
|
|
1007
1374
|
}
|
|
1008
1375
|
function sampleClaudeInput(cwd = process.cwd()) {
|