@hasna/statusline 0.0.2 → 0.0.4
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 +68 -1
- package/dist/accounts.d.ts +50 -0
- package/dist/cli.js +329 -51
- package/dist/config.d.ts +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +326 -52
- package/dist/install.d.ts +12 -1
- package/dist/mcp/index.js +6518 -18984
- package/dist/providers/types.d.ts +22 -0
- package/dist/render.d.ts +5 -1
- package/dist/settings.d.ts +11 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -39,6 +39,12 @@ anything that returns `null` or throws. A failed segment never breaks the host U
|
|
|
39
39
|
| `current-dir` | off | Current working directory (basename) | `StatusContext.cwd` | never (always renders basename) | `open-statusline` |
|
|
40
40
|
| `model` | off | Current model name (no context tag) | `StatusContext.model.id` via friendly formatter | no model id | `fable 5` |
|
|
41
41
|
| `model-context` | on | Model name with context-size tag | same formatter, keeps `[tag]` suffix | no model id | `fable 5 [1m]` |
|
|
42
|
+
| `model-with-reasoning` | off | Model name with its reasoning effort | `model.display_name` (or friendly id) + `effort.level`; falls back to `thinking.enabled`, which the host defaults to on | no model | `Fable (xhigh)` |
|
|
43
|
+
| `auth-profile` | off | Account profile this session runs as | this process's `CLAUDE_CONFIG_DIR`, resolved against the accounts registry | config dir is unmanaged and has no login | `account001` |
|
|
44
|
+
| `auth-email` | off | Account email this session is logged in as | the agent's own login record for this config dir, else the registry | no login recorded | `dev@example.com` |
|
|
45
|
+
| `five-hour-limit` | off | Percentage of the 5-hour rate limit used | `rate_limits.five_hour.used_percentage` | host reports no limit data | `5h:42%` |
|
|
46
|
+
| `seven-day-limit` | off | Percentage of the 7-day rate limit used | `rate_limits.seven_day.used_percentage` | host reports no limit data | `7d:12%` |
|
|
47
|
+
| `thread-title` | off | Thread title set with `/rename` | `session_name` | thread not renamed | `ship it` |
|
|
42
48
|
| `context-used` | off | Percentage of context window used | session transcript JSONL (`contextUsage`) | transcript missing/unreadable | `10%` |
|
|
43
49
|
| `context-remaining` | on | Percentage of context window remaining | transcript via `contextUsage` | transcript missing/unreadable | `90% left` |
|
|
44
50
|
| `used-tokens` | off | Total tokens in the context window | transcript usage block (input + output) | transcript missing/unreadable | `102k tok` |
|
|
@@ -60,6 +66,51 @@ JSONL for the last assistant entry with a `usage` block and sums
|
|
|
60
66
|
With the test fixture transcript (100k input-side tokens on a 1m window), expect
|
|
61
67
|
`context-used` → `10%`, `context-remaining` → `90% left`, `used-tokens` → `102k tok`.
|
|
62
68
|
|
|
69
|
+
### Which account am I? (`auth-profile`)
|
|
70
|
+
|
|
71
|
+
Statusline payloads carry no account or profile field, so `auth-profile` reads the
|
|
72
|
+
config dir **this process** was handed — `CLAUDE_CONFIG_DIR`, which multi-account
|
|
73
|
+
launchers set per session — and resolves it in order:
|
|
74
|
+
|
|
75
|
+
1. the `@hasna/accounts` registry entry whose `dir` matches, preferring one that
|
|
76
|
+
belongs to the agent being rendered (a dir can be claimed by several tools;
|
|
77
|
+
another tool's entry is never adopted);
|
|
78
|
+
2. the managed layout `<accountsHome>/profiles/<tool>/<name>`, for dirs the local
|
|
79
|
+
registry has no entry for;
|
|
80
|
+
3. the account email the agent recorded in that dir, when neither names a profile.
|
|
81
|
+
|
|
82
|
+
It never consults an "active"/"current" profile pointer. Those are global — and,
|
|
83
|
+
against a shared registry, global across every machine: two sessions running under
|
|
84
|
+
different accounts would both report whichever profile was switched to last.
|
|
85
|
+
Resolving from the process's own config dir keeps concurrent sessions independent.
|
|
86
|
+
|
|
87
|
+
Where the agent's login record lives mirrors Claude Code's own rule: `.config.json`
|
|
88
|
+
in the config dir wins, otherwise `.claude<oauth-variant>.json`. Note the base dir
|
|
89
|
+
is the config dir **or the home dir** — with `CLAUDE_CONFIG_DIR` unset the file is
|
|
90
|
+
`~/.claude.json`, a sibling of `~/.claude` rather than something inside it.
|
|
91
|
+
|
|
92
|
+
Registry location comes from `@hasna/accounts` itself when it is installed
|
|
93
|
+
alongside this package (imported lazily, only when an auth segment renders);
|
|
94
|
+
otherwise from its documented overrides `ACCOUNTS_STORE_PATH`, then
|
|
95
|
+
`ACCOUNTS_HOME`, then `~/.hasna/accounts`. `@hasna/accounts` is not a dependency —
|
|
96
|
+
with none of it present the segment renders nothing rather than guessing.
|
|
97
|
+
|
|
98
|
+
**Known limitation.** Renaming a profile does not move its dir. For a profile the
|
|
99
|
+
local registry has no entry for, step 2 therefore reports the name the dir was
|
|
100
|
+
created with until the registry catches up. Step 1 is tried first so a profile the
|
|
101
|
+
registry knows about always wins.
|
|
102
|
+
|
|
103
|
+
### Colours
|
|
104
|
+
|
|
105
|
+
Segments may declare a colour, applied by `renderLine` when colours are on
|
|
106
|
+
(the default). The rate-limit segments escalate from yellow to red past 80%.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
statusline colors off # or set "colors": false in the config
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`NO_COLOR` in the environment always wins.
|
|
113
|
+
|
|
63
114
|
### Quick reference from `statusline list`
|
|
64
115
|
|
|
65
116
|
`statusline list` is compact by default so it stays friendly in agent terminals —
|
|
@@ -109,6 +160,7 @@ statusbar width.
|
|
|
109
160
|
statusline list # compact segment summary
|
|
110
161
|
statusline enable used-tokens duration # turn segments on (appended at the end)
|
|
111
162
|
statusline disable loc # turn segments off
|
|
163
|
+
statusline colors off # drop ANSI colours
|
|
112
164
|
statusline order machine project model-context cost # exact order = enabled set
|
|
113
165
|
statusline separator " | " # change the separator
|
|
114
166
|
statusline preview # render a sample line from the current dir
|
|
@@ -198,7 +250,17 @@ bun install -g @hasna/statusline
|
|
|
198
250
|
statusline install claude
|
|
199
251
|
```
|
|
200
252
|
|
|
201
|
-
`installClaude()` writes to
|
|
253
|
+
`installClaude()` writes to `$CLAUDE_CONFIG_DIR/settings.json`, falling back to
|
|
254
|
+
`~/.claude/settings.json`. This matters for multi-account setups: when a session is
|
|
255
|
+
bound to an isolated config dir, `~/.claude/settings.json` is never read, so
|
|
256
|
+
installing there wires up a statusline that silently never runs.
|
|
257
|
+
|
|
258
|
+
Only `~/.claude` is created on demand. Every other target — an isolated config
|
|
259
|
+
dir, or a path passed as `installClaude(path)` — must already exist: installing
|
|
260
|
+
there is a request to configure a profile someone else created, not to fabricate
|
|
261
|
+
one. The rule follows the target, not the shell you install from, so driving a
|
|
262
|
+
list of profiles gives the same answer from any session. Pass the environment to
|
|
263
|
+
resolve against as `installClaude(undefined, env)`.
|
|
202
264
|
|
|
203
265
|
```json
|
|
204
266
|
{
|
|
@@ -222,8 +284,13 @@ on every status refresh — **no restart needed**.
|
|
|
222
284
|
| `cost.total_lines_added/removed` | `cost.totalLinesAdded/Removed` |
|
|
223
285
|
| `transcript_path` | `transcriptPath` |
|
|
224
286
|
| `session_id` | `sessionId` |
|
|
287
|
+
| `session_name` | `sessionName` |
|
|
225
288
|
| `version` | `version` |
|
|
226
289
|
| `output_style.name` | `outputStyle` |
|
|
290
|
+
| `effort.level` | `effort` |
|
|
291
|
+
| `thinking.enabled` | `thinking` |
|
|
292
|
+
| `rate_limits.five_hour.used_percentage` | `rateLimits.fiveHour.usedPercentage` |
|
|
293
|
+
| `rate_limits.seven_day.used_percentage` | `rateLimits.sevenDay.usedPercentage` |
|
|
227
294
|
|
|
228
295
|
Every field is optional — partial or empty payloads still produce a usable context.
|
|
229
296
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which account is *this* session running as?
|
|
3
|
+
*
|
|
4
|
+
* The host agent's statusline payload carries no account or profile field, so
|
|
5
|
+
* the answer has to come from the process environment. Tools that manage
|
|
6
|
+
* several logins (`@hasna/accounts`, and Claude Code itself) isolate each
|
|
7
|
+
* login in its own config dir and point the child process at it via
|
|
8
|
+
* `CLAUDE_CONFIG_DIR`. That variable is per-process, so it is the only signal
|
|
9
|
+
* that stays correct when several sessions run under different accounts at the
|
|
10
|
+
* same time.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately NOT used: any "active"/"current"/"applied" profile pointer.
|
|
13
|
+
* Those are global — every concurrent session would report whichever profile
|
|
14
|
+
* was switched to last, which is the staleness this module exists to avoid.
|
|
15
|
+
*/
|
|
16
|
+
export interface SessionAccount {
|
|
17
|
+
/** Config dir this session is running against (canonicalized). */
|
|
18
|
+
configDir: string;
|
|
19
|
+
/** Managed profile name owning that dir, when it can be resolved. */
|
|
20
|
+
profile: string | null;
|
|
21
|
+
/** Tool id the profile belongs to (e.g. "claude"), when known. */
|
|
22
|
+
tool: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* How `profile` was resolved. `"layout"` means it was read off the dir path
|
|
25
|
+
* rather than the registry, which is authoritative about the dir but can lag
|
|
26
|
+
* a rename — see `layoutProfile`. Surfaced for diagnostics, not for display.
|
|
27
|
+
*/
|
|
28
|
+
source: "registry" | "layout" | null;
|
|
29
|
+
}
|
|
30
|
+
type Env = Record<string, string | undefined>;
|
|
31
|
+
/** Config dir the current process is bound to, for settings and registry lookup. */
|
|
32
|
+
export declare function sessionConfigDir(env?: Env): string;
|
|
33
|
+
/**
|
|
34
|
+
* Path of the host agent's account record for this session, or null when none
|
|
35
|
+
* of its known filenames are present.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sessionStateFile(env?: Env): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the profile owning this session. `tool` is the agent being rendered
|
|
40
|
+
* for, used to disambiguate a config dir claimed by several tools. Never throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sessionAccount(env?: Env, tool?: string): Promise<SessionAccount>;
|
|
43
|
+
/**
|
|
44
|
+
* Email this session is logged in as. The host agent's own record for this
|
|
45
|
+
* config dir wins over the registry's copy, which can lag behind a re-login.
|
|
46
|
+
* Read separately from `sessionAccount` because it parses the agent's state
|
|
47
|
+
* blob, which is large enough to only touch when a segment asks for it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionAccountEmail(env?: Env, tool?: string): Promise<string | null>;
|
|
50
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,36 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
5
3
|
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);
|
|
34
4
|
var __returnValue = (v) => v;
|
|
35
5
|
function __exportSetter(name, newValue) {
|
|
36
6
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -52,6 +22,7 @@ import { dirname, join } from "path";
|
|
|
52
22
|
function defaultConfig() {
|
|
53
23
|
return {
|
|
54
24
|
separator: " \xB7 ",
|
|
25
|
+
colors: true,
|
|
55
26
|
segments: [
|
|
56
27
|
"machine",
|
|
57
28
|
"project",
|
|
@@ -79,7 +50,8 @@ function loadConfig(path = configPath()) {
|
|
|
79
50
|
const fallback = defaultConfig();
|
|
80
51
|
return {
|
|
81
52
|
separator: typeof raw.separator === "string" ? raw.separator : fallback.separator,
|
|
82
|
-
segments: Array.isArray(raw.segments) ? raw.segments.filter((s) => typeof s === "string") : fallback.segments
|
|
53
|
+
segments: Array.isArray(raw.segments) ? raw.segments.filter((s) => typeof s === "string") : fallback.segments,
|
|
54
|
+
colors: typeof raw.colors === "boolean" ? raw.colors : fallback.colors
|
|
83
55
|
};
|
|
84
56
|
} catch {
|
|
85
57
|
return defaultConfig();
|
|
@@ -107,8 +79,15 @@ function parseClaudeInput(input) {
|
|
|
107
79
|
} : undefined,
|
|
108
80
|
transcriptPath: input?.transcript_path || undefined,
|
|
109
81
|
sessionId: input?.session_id || undefined,
|
|
82
|
+
sessionName: input?.session_name || undefined,
|
|
110
83
|
version: input?.version || undefined,
|
|
111
|
-
outputStyle: input?.output_style?.name || undefined
|
|
84
|
+
outputStyle: input?.output_style?.name || undefined,
|
|
85
|
+
effort: input?.effort?.level || undefined,
|
|
86
|
+
thinking: typeof input?.thinking?.enabled === "boolean" ? input.thinking.enabled : undefined,
|
|
87
|
+
rateLimits: input?.rate_limits ? {
|
|
88
|
+
fiveHour: { usedPercentage: numberOr(input.rate_limits.five_hour?.used_percentage) },
|
|
89
|
+
sevenDay: { usedPercentage: numberOr(input.rate_limits.seven_day?.used_percentage) }
|
|
90
|
+
} : undefined
|
|
112
91
|
};
|
|
113
92
|
}
|
|
114
93
|
function numberOr(v) {
|
|
@@ -253,6 +232,158 @@ function windowSize(ctx) {
|
|
|
253
232
|
return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
|
|
254
233
|
}
|
|
255
234
|
|
|
235
|
+
// src/accounts.ts
|
|
236
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
|
|
237
|
+
import { homedir as homedir2 } from "os";
|
|
238
|
+
import { join as join2, resolve, sep } from "path";
|
|
239
|
+
var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
|
|
240
|
+
var DEFAULT_CONFIG_SUBDIR = ".claude";
|
|
241
|
+
var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
|
|
242
|
+
var ACCOUNTS_HOME_ENV = "ACCOUNTS_HOME";
|
|
243
|
+
var DEFAULT_ACCOUNTS_HOME = [".hasna", "accounts"];
|
|
244
|
+
var STORE_FILE = "accounts.json";
|
|
245
|
+
var PROFILES_SUBDIR = "profiles";
|
|
246
|
+
var STATE_FILE_PREFERRED = ".config.json";
|
|
247
|
+
var STATE_FILE_PREFIX = ".claude";
|
|
248
|
+
var STATE_FILE_SUFFIX = ".json";
|
|
249
|
+
var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
|
|
250
|
+
var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
|
|
251
|
+
function str(value) {
|
|
252
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
253
|
+
}
|
|
254
|
+
function home(env) {
|
|
255
|
+
return str(env.HOME) ?? homedir2();
|
|
256
|
+
}
|
|
257
|
+
function expandHome(path, env) {
|
|
258
|
+
return path === "~" || path.startsWith(`~${sep}`) ? join2(home(env), path.slice(1)) : path;
|
|
259
|
+
}
|
|
260
|
+
function canonical(path, env) {
|
|
261
|
+
const abs = resolve(expandHome(path, env));
|
|
262
|
+
try {
|
|
263
|
+
return realpathSync(abs);
|
|
264
|
+
} catch {
|
|
265
|
+
return abs;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function readJson(path) {
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
var ACCOUNTS_STORAGE_MODULE = "@hasna/accounts/storage";
|
|
276
|
+
var storagePromise;
|
|
277
|
+
function accountsStorage() {
|
|
278
|
+
storagePromise ??= import(ACCOUNTS_STORAGE_MODULE).then((mod) => typeof mod?.storePath === "function" && typeof mod?.profilesDir === "function" ? mod : null, () => null);
|
|
279
|
+
return storagePromise;
|
|
280
|
+
}
|
|
281
|
+
async function accountsPaths(env) {
|
|
282
|
+
const homeOverride = str(env[ACCOUNTS_HOME_ENV]);
|
|
283
|
+
const storeOverride = str(env[STORE_PATH_ENV]);
|
|
284
|
+
if (!homeOverride && !storeOverride) {
|
|
285
|
+
const storage = await accountsStorage();
|
|
286
|
+
try {
|
|
287
|
+
if (storage)
|
|
288
|
+
return { store: storage.storePath(), profiles: storage.profilesDir() };
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
const base = homeOverride ?? join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
|
|
292
|
+
return {
|
|
293
|
+
store: storeOverride ?? join2(base, STORE_FILE),
|
|
294
|
+
profiles: join2(base, PROFILES_SUBDIR)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function registryEntry(store, configDir, env, tool) {
|
|
298
|
+
if (!existsSync3(store))
|
|
299
|
+
return null;
|
|
300
|
+
const entries = readJson(store)?.profiles;
|
|
301
|
+
if (!Array.isArray(entries))
|
|
302
|
+
return null;
|
|
303
|
+
let untyped = null;
|
|
304
|
+
let first = null;
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
const dir = str(entry?.dir);
|
|
307
|
+
if (!dir || canonical(dir, env) !== configDir)
|
|
308
|
+
continue;
|
|
309
|
+
const entryTool = str(entry?.tool);
|
|
310
|
+
if (tool && entryTool === tool)
|
|
311
|
+
return entry;
|
|
312
|
+
if (!entryTool)
|
|
313
|
+
untyped ??= entry;
|
|
314
|
+
first ??= entry;
|
|
315
|
+
}
|
|
316
|
+
return tool ? untyped : first;
|
|
317
|
+
}
|
|
318
|
+
function layoutProfile(profilesRoot, configDir, env) {
|
|
319
|
+
if (!existsSync3(configDir))
|
|
320
|
+
return null;
|
|
321
|
+
const prefix = canonical(profilesRoot, env) + sep;
|
|
322
|
+
if (!configDir.startsWith(prefix))
|
|
323
|
+
return null;
|
|
324
|
+
const parts = configDir.slice(prefix.length).split(sep);
|
|
325
|
+
if (parts.length !== 2)
|
|
326
|
+
return null;
|
|
327
|
+
const [tool, profile] = parts;
|
|
328
|
+
if (!tool || !profile)
|
|
329
|
+
return null;
|
|
330
|
+
return { configDir, profile, tool, source: "layout" };
|
|
331
|
+
}
|
|
332
|
+
function sessionConfigDir(env = process.env) {
|
|
333
|
+
return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
|
|
334
|
+
}
|
|
335
|
+
function sessionStateFile(env = process.env) {
|
|
336
|
+
const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
|
|
337
|
+
const preferred = join2(base, STATE_FILE_PREFERRED);
|
|
338
|
+
if (existsSync3(preferred))
|
|
339
|
+
return preferred;
|
|
340
|
+
const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
|
|
341
|
+
for (const variant of variants) {
|
|
342
|
+
const candidate = join2(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
|
|
343
|
+
if (existsSync3(candidate))
|
|
344
|
+
return candidate;
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
async function sessionAccount(env = process.env, tool) {
|
|
349
|
+
const configDir = sessionConfigDir(env);
|
|
350
|
+
const unresolved = { configDir, profile: null, tool: null, source: null };
|
|
351
|
+
try {
|
|
352
|
+
const paths = await accountsPaths(env);
|
|
353
|
+
const entry = registryEntry(paths.store, configDir, env, tool);
|
|
354
|
+
const name = str(entry?.name);
|
|
355
|
+
if (name)
|
|
356
|
+
return { configDir, profile: name, tool: str(entry?.tool), source: "registry" };
|
|
357
|
+
return layoutProfile(paths.profiles, configDir, env) ?? unresolved;
|
|
358
|
+
} catch {
|
|
359
|
+
return unresolved;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function sessionAccountEmail(env = process.env, tool) {
|
|
363
|
+
try {
|
|
364
|
+
const stateFile = sessionStateFile(env);
|
|
365
|
+
const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
|
|
366
|
+
if (live)
|
|
367
|
+
return live;
|
|
368
|
+
const paths = await accountsPaths(env);
|
|
369
|
+
return str(registryEntry(paths.store, sessionConfigDir(env), env, tool)?.email);
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/settings.ts
|
|
376
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
377
|
+
import { join as join3 } from "path";
|
|
378
|
+
function sessionFastMode(env = process.env) {
|
|
379
|
+
try {
|
|
380
|
+
const raw = readFileSync4(join3(sessionConfigDir(env), "settings.json"), "utf8");
|
|
381
|
+
return JSON.parse(raw)?.fastMode === true;
|
|
382
|
+
} catch {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
256
387
|
// src/segments/index.ts
|
|
257
388
|
function friendlyModel(id) {
|
|
258
389
|
let tag = null;
|
|
@@ -270,6 +401,14 @@ function friendlyModel(id) {
|
|
|
270
401
|
return { name: name || mid, tag };
|
|
271
402
|
}
|
|
272
403
|
var dir = (ctx) => ctx.cwd || process.cwd();
|
|
404
|
+
function modelName(ctx) {
|
|
405
|
+
if (ctx.model?.displayName)
|
|
406
|
+
return ctx.model.displayName.toLowerCase();
|
|
407
|
+
return ctx.model?.id ? friendlyModel(ctx.model.id).name : null;
|
|
408
|
+
}
|
|
409
|
+
function limitPercent(pct) {
|
|
410
|
+
return typeof pct === "number" ? pct : 0;
|
|
411
|
+
}
|
|
273
412
|
var segments = [
|
|
274
413
|
{
|
|
275
414
|
id: "machine",
|
|
@@ -348,6 +487,73 @@ var segments = [
|
|
|
348
487
|
return tag ? `${name} ${tag}` : name;
|
|
349
488
|
}
|
|
350
489
|
},
|
|
490
|
+
{
|
|
491
|
+
id: "model-with-reasoning",
|
|
492
|
+
description: "Model name with its reasoning effort (or thinking, when on)",
|
|
493
|
+
defaultEnabled: false,
|
|
494
|
+
color: "cyan",
|
|
495
|
+
render(ctx) {
|
|
496
|
+
const name = modelName(ctx);
|
|
497
|
+
if (!name)
|
|
498
|
+
return null;
|
|
499
|
+
if (ctx.effort)
|
|
500
|
+
return `${name} (${ctx.effort.toLowerCase()})`;
|
|
501
|
+
if (ctx.thinking)
|
|
502
|
+
return `${name} (thinking)`;
|
|
503
|
+
return name;
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: "fast-mode",
|
|
508
|
+
description: `Shows "fast" when fast mode is on for this session's config dir`,
|
|
509
|
+
defaultEnabled: false,
|
|
510
|
+
color: "yellow",
|
|
511
|
+
render: () => sessionFastMode(process.env) ? "fast" : null
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "auth-profile",
|
|
515
|
+
description: "Account profile this session runs as (omitted when unmanaged)",
|
|
516
|
+
defaultEnabled: false,
|
|
517
|
+
color: "magenta",
|
|
518
|
+
async render(ctx) {
|
|
519
|
+
const account = await sessionAccount(process.env, ctx.provider);
|
|
520
|
+
return account.profile ?? await sessionAccountEmail(process.env, ctx.provider);
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: "auth-email",
|
|
525
|
+
description: "Account email this session is logged in as (omitted when unknown)",
|
|
526
|
+
defaultEnabled: false,
|
|
527
|
+
color: "magenta",
|
|
528
|
+
render: (ctx) => sessionAccountEmail(process.env, ctx.provider)
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
id: "five-hour-limit",
|
|
532
|
+
description: "Percentage of the 5-hour rate limit used (omitted when unreported)",
|
|
533
|
+
defaultEnabled: false,
|
|
534
|
+
color: (ctx) => limitPercent(ctx.rateLimits?.fiveHour?.usedPercentage) >= 80 ? "red" : "yellow",
|
|
535
|
+
render(ctx) {
|
|
536
|
+
const pct = ctx.rateLimits?.fiveHour?.usedPercentage;
|
|
537
|
+
return typeof pct === "number" ? `5h:${Math.round(pct)}%` : null;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
id: "seven-day-limit",
|
|
542
|
+
description: "Percentage of the 7-day rate limit used (omitted when unreported)",
|
|
543
|
+
defaultEnabled: false,
|
|
544
|
+
color: (ctx) => limitPercent(ctx.rateLimits?.sevenDay?.usedPercentage) >= 80 ? "red" : "yellow",
|
|
545
|
+
render(ctx) {
|
|
546
|
+
const pct = ctx.rateLimits?.sevenDay?.usedPercentage;
|
|
547
|
+
return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: "thread-title",
|
|
552
|
+
description: "Thread title set with /rename (omitted until named)",
|
|
553
|
+
defaultEnabled: false,
|
|
554
|
+
color: "blue",
|
|
555
|
+
render: (ctx) => ctx.sessionName || null
|
|
556
|
+
},
|
|
351
557
|
{
|
|
352
558
|
id: "context-used",
|
|
353
559
|
description: "Percentage of context window used (omitted when unknown)",
|
|
@@ -431,6 +637,12 @@ var segments = [
|
|
|
431
637
|
description: "Session identifier (short)",
|
|
432
638
|
defaultEnabled: false,
|
|
433
639
|
render: (ctx) => ctx.sessionId ? ctx.sessionId.split("-")[0] : null
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "newline",
|
|
643
|
+
description: "Row break \u2014 segments after it render on the next line",
|
|
644
|
+
defaultEnabled: false,
|
|
645
|
+
render: () => null
|
|
434
646
|
}
|
|
435
647
|
];
|
|
436
648
|
function getSegment(id) {
|
|
@@ -438,24 +650,51 @@ function getSegment(id) {
|
|
|
438
650
|
}
|
|
439
651
|
|
|
440
652
|
// src/render.ts
|
|
653
|
+
var ANSI = {
|
|
654
|
+
cyan: "\x1B[36m",
|
|
655
|
+
magenta: "\x1B[35m",
|
|
656
|
+
blue: "\x1B[34m",
|
|
657
|
+
green: "\x1B[32m",
|
|
658
|
+
yellow: "\x1B[33m",
|
|
659
|
+
red: "\x1B[31m",
|
|
660
|
+
bold: "\x1B[1m",
|
|
661
|
+
dim: "\x1B[2m"
|
|
662
|
+
};
|
|
663
|
+
var RESET = "\x1B[0m";
|
|
664
|
+
function colorsEnabled(cfg, env = process.env) {
|
|
665
|
+
if (env.NO_COLOR)
|
|
666
|
+
return false;
|
|
667
|
+
return cfg.colors !== false;
|
|
668
|
+
}
|
|
669
|
+
function paint(segment, text, ctx) {
|
|
670
|
+
const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
|
|
671
|
+
const code = color ? ANSI[color] : undefined;
|
|
672
|
+
return code ? `${code}${text}${RESET}` : text;
|
|
673
|
+
}
|
|
441
674
|
async function renderLine(ctx, cfg) {
|
|
442
|
-
const
|
|
675
|
+
const colorize = colorsEnabled(cfg);
|
|
676
|
+
const rows = [[]];
|
|
443
677
|
for (const id of cfg.segments) {
|
|
678
|
+
if (id === "newline") {
|
|
679
|
+
rows.push([]);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
444
682
|
const segment = getSegment(id);
|
|
445
683
|
if (!segment)
|
|
446
684
|
continue;
|
|
447
685
|
try {
|
|
448
686
|
const out = await segment.render(ctx);
|
|
449
687
|
if (out)
|
|
450
|
-
|
|
688
|
+
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
|
|
451
689
|
} catch {}
|
|
452
690
|
}
|
|
453
|
-
return parts.join(cfg.separator)
|
|
691
|
+
return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
|
|
692
|
+
`);
|
|
454
693
|
}
|
|
455
694
|
// package.json
|
|
456
695
|
var package_default = {
|
|
457
696
|
name: "@hasna/statusline",
|
|
458
|
-
version: "0.0.
|
|
697
|
+
version: "0.0.4",
|
|
459
698
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
460
699
|
license: "Apache-2.0",
|
|
461
700
|
type: "module",
|
|
@@ -515,30 +754,52 @@ var package_default = {
|
|
|
515
754
|
bun: ">=1.0.0"
|
|
516
755
|
},
|
|
517
756
|
dependencies: {
|
|
518
|
-
"@modelcontextprotocol/sdk": "
|
|
519
|
-
zod: "
|
|
757
|
+
"@modelcontextprotocol/sdk": "1.12.1",
|
|
758
|
+
zod: "3.25.76"
|
|
520
759
|
},
|
|
521
760
|
devDependencies: {
|
|
522
761
|
"@types/bun": "^1.2.4",
|
|
523
|
-
typescript: "
|
|
762
|
+
typescript: "5.9.3"
|
|
524
763
|
}
|
|
525
764
|
};
|
|
526
765
|
// src/install.ts
|
|
527
|
-
import { copyFileSync, existsSync as
|
|
528
|
-
import { homedir as
|
|
529
|
-
import { dirname as dirname2, join as
|
|
530
|
-
function
|
|
766
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
767
|
+
import { homedir as homedir3 } from "os";
|
|
768
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
769
|
+
function defaultSettingsPath(env) {
|
|
770
|
+
return join4(env.HOME || homedir3(), ".claude", "settings.json");
|
|
771
|
+
}
|
|
772
|
+
function claudeSettingsPath(env = process.env) {
|
|
773
|
+
const home2 = env.HOME || homedir3();
|
|
774
|
+
const raw = env.CLAUDE_CONFIG_DIR?.trim();
|
|
775
|
+
if (!raw)
|
|
776
|
+
return defaultSettingsPath(env);
|
|
777
|
+
const configDir = raw === "~" || raw.startsWith("~/") ? join4(home2, raw.slice(1)) : raw;
|
|
778
|
+
return join4(configDir, "settings.json");
|
|
779
|
+
}
|
|
780
|
+
function installClaude(settingsPath, env = process.env) {
|
|
781
|
+
const target = settingsPath ?? claudeSettingsPath(env);
|
|
531
782
|
const binary = Bun.which("statusline") || "statusline";
|
|
532
783
|
let settings = {};
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
784
|
+
if (existsSync4(target)) {
|
|
785
|
+
try {
|
|
786
|
+
settings = JSON.parse(readFileSync5(target, "utf8"));
|
|
787
|
+
} catch (error) {
|
|
788
|
+
throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
|
|
789
|
+
}
|
|
790
|
+
copyFileSync(target, `${target}.bak-statusline`);
|
|
536
791
|
}
|
|
537
|
-
settings.statusLine = { type: "command", command: `${binary} render` };
|
|
538
|
-
|
|
539
|
-
|
|
792
|
+
settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
|
|
793
|
+
const configDir = dirname2(target);
|
|
794
|
+
if (!existsSync4(configDir)) {
|
|
795
|
+
if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
|
|
796
|
+
throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
|
|
797
|
+
}
|
|
798
|
+
mkdirSync2(configDir, { recursive: true });
|
|
799
|
+
}
|
|
800
|
+
writeFileSync2(target, JSON.stringify(settings, null, 2) + `
|
|
540
801
|
`);
|
|
541
|
-
return
|
|
802
|
+
return target;
|
|
542
803
|
}
|
|
543
804
|
|
|
544
805
|
// src/index.ts
|
|
@@ -590,6 +851,9 @@ function setSeparator(separator, config = loadConfig()) {
|
|
|
590
851
|
throw new Error("separator must be a non-empty string");
|
|
591
852
|
return { config: { ...config, separator }, changed: ["separator"] };
|
|
592
853
|
}
|
|
854
|
+
function setColors(colors, config = loadConfig()) {
|
|
855
|
+
return { config: { ...config, colors }, changed: ["colors"] };
|
|
856
|
+
}
|
|
593
857
|
function resetStatuslineConfig() {
|
|
594
858
|
return defaultConfig();
|
|
595
859
|
}
|
|
@@ -602,7 +866,11 @@ function sampleClaudeInput(cwd = process.cwd()) {
|
|
|
602
866
|
model: { id: "claude-fable-5[1m]", display_name: "Fable" },
|
|
603
867
|
cost: { total_cost_usd: 12.34, total_duration_ms: 5400000, total_lines_added: 42, total_lines_removed: 7 },
|
|
604
868
|
version: statuslineVersion,
|
|
605
|
-
session_id: "preview0-0000-0000-0000-000000000000"
|
|
869
|
+
session_id: "preview0-0000-0000-0000-000000000000",
|
|
870
|
+
session_name: "preview thread",
|
|
871
|
+
effort: { level: "high" },
|
|
872
|
+
thinking: { enabled: true },
|
|
873
|
+
rate_limits: { five_hour: { used_percentage: 42 } }
|
|
606
874
|
};
|
|
607
875
|
}
|
|
608
876
|
async function previewStatusline(config = loadConfig(), cwd = process.cwd()) {
|
|
@@ -658,6 +926,7 @@ usage:
|
|
|
658
926
|
statusline disable <id...> disable segments
|
|
659
927
|
statusline order <id...> set the exact segment order (also defines enabled set)
|
|
660
928
|
statusline separator <str> set the separator (default " \xB7 ")
|
|
929
|
+
statusline colors <on|off> toggle segment colours (default on)
|
|
661
930
|
statusline preview render with a sample payload from the current directory
|
|
662
931
|
statusline reset restore default config
|
|
663
932
|
statusline install [claude] wire into Claude Code (~/.claude/settings.json)
|
|
@@ -978,6 +1247,15 @@ order: ${result.config.segments.join(", ") || "(none)"}`);
|
|
|
978
1247
|
console.log(`separator: "${result.config.separator}"`);
|
|
979
1248
|
return;
|
|
980
1249
|
}
|
|
1250
|
+
case "colors": {
|
|
1251
|
+
const value = args[0];
|
|
1252
|
+
if (value !== "on" && value !== "off")
|
|
1253
|
+
throw new Error("usage: statusline colors <on|off>");
|
|
1254
|
+
const result = setColors(value === "on", loadConfig());
|
|
1255
|
+
saveConfig(result.config);
|
|
1256
|
+
console.log(`colors: ${value}`);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
981
1259
|
case "reset": {
|
|
982
1260
|
saveConfig(defaultConfig());
|
|
983
1261
|
console.log(`reset to defaults (${configPath()})`);
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export interface StatuslineConfig {
|
|
|
2
2
|
separator: string;
|
|
3
3
|
/** Ordered list of enabled segment ids. */
|
|
4
4
|
segments: string[];
|
|
5
|
+
/** Colourize segments that ask for it (default on; off when `NO_COLOR` is set). */
|
|
6
|
+
colors?: boolean;
|
|
5
7
|
}
|
|
6
8
|
export declare function defaultConfig(): StatuslineConfig;
|
|
7
9
|
export declare function configPath(path?: string): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type StatuslineConfig } from "./config.js";
|
|
2
|
+
export { sessionAccount, sessionAccountEmail, sessionConfigDir, sessionStateFile, type SessionAccount, } from "./accounts.js";
|
|
2
3
|
export { defaultConfig, loadConfig, saveConfig, configPath, type StatuslineConfig } from "./config.js";
|
|
3
4
|
export { contextUsage, type ContextUsage } from "./context-window.js";
|
|
4
5
|
export { compactAge, compactDuration, compactNum, money } from "./format.js";
|
|
5
6
|
export { gitBranch, gitProjectName, gitRoot, lastCommitEpoch, trackedLineCount } from "./git.js";
|
|
6
|
-
export { installClaude } from "./install.js";
|
|
7
|
+
export { claudeSettingsPath, installClaude } from "./install.js";
|
|
7
8
|
export { parseClaudeInput } from "./providers/claude.js";
|
|
8
|
-
export type { Segment, StatusContext } from "./providers/types.js";
|
|
9
|
+
export type { Segment, SegmentColor, StatusContext } from "./providers/types.js";
|
|
9
10
|
export { renderLine } from "./render.js";
|
|
10
11
|
export { getSegment, segments } from "./segments/index.js";
|
|
11
12
|
export declare const statuslineVersion: string;
|
|
@@ -25,6 +26,7 @@ export declare function enableSegments(ids: string[], config?: StatuslineConfig)
|
|
|
25
26
|
export declare function disableSegments(ids: string[], config?: StatuslineConfig): ConfigUpdateResult;
|
|
26
27
|
export declare function orderSegments(ids: string[], config?: StatuslineConfig): ConfigUpdateResult;
|
|
27
28
|
export declare function setSeparator(separator: string, config?: StatuslineConfig): ConfigUpdateResult;
|
|
29
|
+
export declare function setColors(colors: boolean, config?: StatuslineConfig): ConfigUpdateResult;
|
|
28
30
|
export declare function resetStatuslineConfig(): StatuslineConfig;
|
|
29
31
|
export declare function renderStatusline(input?: Record<string, unknown>, config?: StatuslineConfig): Promise<string>;
|
|
30
32
|
export declare function sampleClaudeInput(cwd?: string): Record<string, unknown>;
|