@hasna/statusline 0.0.3 → 0.0.5
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 +25 -15
- package/dist/accounts.d.ts +4 -2
- package/dist/cli.js +71 -55
- package/dist/index.js +65 -49
- package/dist/mcp/index.js +6597 -19325
- package/dist/render.d.ts +5 -1
- package/dist/settings.d.ts +11 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Tiny, composable statusline CLI for AI coding agents. Pre-built segments you toggle
|
|
4
4
|
on and off — machine name, project + branch, model, context remaining, session cost,
|
|
5
|
-
and more — rendered as
|
|
5
|
+
and more — rendered as one or more rows in your agent's status bar.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
apple03 · statusline (main) · 12h · 1.2k · fable 5 [1m] · 90% left · $0.04
|
|
@@ -14,7 +14,7 @@ Built with [Bun](https://bun.sh) + TypeScript. The CLI renderer is dependency-li
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
bun install -g @hasna/statusline
|
|
17
|
-
statusline install claude # wires it into
|
|
17
|
+
statusline install claude # wires it into the active Claude config dir
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
That's it — Claude Code picks it up on the next status refresh (no restart needed).
|
|
@@ -22,7 +22,7 @@ That's it — Claude Code picks it up on the next status refresh (no restart nee
|
|
|
22
22
|
## Segments
|
|
23
23
|
|
|
24
24
|
`statusline list` shows a compact, enabled-first summary of your segments (use
|
|
25
|
-
`statusline list --all` for every row). The table below is the full catalog — all
|
|
25
|
+
`statusline list --all` for every row). The table below is the full catalog — all 26
|
|
26
26
|
segment ids from `src/segments/index.ts`.
|
|
27
27
|
|
|
28
28
|
`renderLine` walks your configured segment order, renders each one, and **drops**
|
|
@@ -39,7 +39,8 @@ 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
|
|
42
|
+
| `model-with-reasoning` | off | Model name with its reasoning effort | lowercased `model.display_name` (or friendly id) + `effort.level`; falls back to `thinking.enabled` when on | no model | `fable (xhigh)` |
|
|
43
|
+
| `fast-mode` | off | Fast-mode indicator | `fastMode` in this session's Claude config-dir `settings.json` | fast mode is off or settings are unavailable | `fast` |
|
|
43
44
|
| `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
45
|
| `auth-email` | off | Account email this session is logged in as | the agent's own login record for this config dir, else the registry | no login recorded | `dev@example.com` |
|
|
45
46
|
| `five-hour-limit` | off | Percentage of the 5-hour rate limit used | `rate_limits.five_hour.used_percentage` | host reports no limit data | `5h:42%` |
|
|
@@ -49,11 +50,12 @@ anything that returns `null` or throws. A failed segment never breaks the host U
|
|
|
49
50
|
| `context-remaining` | on | Percentage of context window remaining | transcript via `contextUsage` | transcript missing/unreadable | `90% left` |
|
|
50
51
|
| `used-tokens` | off | Total tokens in the context window | transcript usage block (input + output) | transcript missing/unreadable | `102k tok` |
|
|
51
52
|
| `cost` | on | Session cost in USD | `StatusContext.cost.totalCostUsd` | cost is zero or missing | `$1,234.50` |
|
|
52
|
-
| `duration` | off | Session wall-clock duration | `StatusContext.cost.totalDurationMs` | duration missing | `1h30m` |
|
|
53
|
+
| `duration` | off | Session wall-clock duration | `StatusContext.cost.totalDurationMs` | duration is zero or missing | `1h30m` |
|
|
53
54
|
| `lines-changed` | off | Lines added/removed this session | `cost.totalLinesAdded/Removed` | both zero | `+142/-18` |
|
|
54
55
|
| `output-style` | off | Active output style | `StatusContext.outputStyle` | style is `default` or missing | `concise` |
|
|
55
56
|
| `agent-version` | off | Host agent version | `StatusContext.version` | version missing | `v2.1.39` |
|
|
56
57
|
| `session-id` | off | Session identifier (short) | first UUID segment of `sessionId` | no session id | `abc12345` |
|
|
58
|
+
| `newline` | off | Row break | renderer starts a new output row | always (the segment emits no text itself) | line break |
|
|
57
59
|
|
|
58
60
|
### Context segments and transcripts
|
|
59
61
|
|
|
@@ -103,7 +105,7 @@ registry knows about always wins.
|
|
|
103
105
|
### Colours
|
|
104
106
|
|
|
105
107
|
Segments may declare a colour, applied by `renderLine` when colours are on
|
|
106
|
-
(the default). The rate-limit segments escalate from yellow to red
|
|
108
|
+
(the default). The rate-limit segments escalate from yellow to red at 80%.
|
|
107
109
|
|
|
108
110
|
```bash
|
|
109
111
|
statusline colors off # or set "colors": false in the config
|
|
@@ -117,7 +119,7 @@ statusline colors off # or set "colors": false in the config
|
|
|
117
119
|
enabled segments first, capped rows, and hints for the detail paths:
|
|
118
120
|
|
|
119
121
|
```
|
|
120
|
-
Segments: 7 enabled /
|
|
122
|
+
Segments: 7 enabled / 26 total (showing 12 of 26)
|
|
121
123
|
state default id
|
|
122
124
|
on yes machine
|
|
123
125
|
on yes project
|
|
@@ -130,7 +132,7 @@ off no project-name
|
|
|
130
132
|
off no git-branch
|
|
131
133
|
off no current-dir
|
|
132
134
|
off no model
|
|
133
|
-
off no
|
|
135
|
+
off no model-with-reasoning
|
|
134
136
|
Hint: use `--all` for all matching rows, `statusline show <id>` for details, `--verbose` for descriptions, `--json` for machines.
|
|
135
137
|
```
|
|
136
138
|
|
|
@@ -158,6 +160,8 @@ statusbar width.
|
|
|
158
160
|
|
|
159
161
|
```bash
|
|
160
162
|
statusline list # compact segment summary
|
|
163
|
+
statusline search token --json # search ids/descriptions; list flags apply
|
|
164
|
+
statusline show model-context # show one segment in detail
|
|
161
165
|
statusline enable used-tokens duration # turn segments on (appended at the end)
|
|
162
166
|
statusline disable loc # turn segments off
|
|
163
167
|
statusline colors off # drop ANSI colours
|
|
@@ -165,6 +169,8 @@ statusline order machine project model-context cost # exact order = enabled se
|
|
|
165
169
|
statusline separator " | " # change the separator
|
|
166
170
|
statusline preview # render a sample line from the current dir
|
|
167
171
|
statusline reset # back to defaults
|
|
172
|
+
statusline install claude # update the active Claude settings file
|
|
173
|
+
statusline version # print the package version
|
|
168
174
|
```
|
|
169
175
|
|
|
170
176
|
Config lives at `~/.config/statusline/config.json` (override with `$STATUSLINE_CONFIG`):
|
|
@@ -172,6 +178,7 @@ Config lives at `~/.config/statusline/config.json` (override with `$STATUSLINE_C
|
|
|
172
178
|
```json
|
|
173
179
|
{
|
|
174
180
|
"separator": " · ",
|
|
181
|
+
"colors": true,
|
|
175
182
|
"segments": ["machine", "project", "commit-age", "loc", "model-context", "context-remaining", "cost"]
|
|
176
183
|
}
|
|
177
184
|
```
|
|
@@ -197,7 +204,8 @@ ids to the end; `statusline disable` removes them.
|
|
|
197
204
|
|
|
198
205
|
Claude Code pipes a JSON payload (cwd, model, session cost, transcript path, …) to the
|
|
199
206
|
configured `statusLine` command on every status refresh. `statusline render` parses it,
|
|
200
|
-
renders each enabled segment, drops anything unavailable, and prints one
|
|
207
|
+
renders each enabled segment, drops anything unavailable, and prints one or more rows.
|
|
208
|
+
The `newline` segment starts a new row; empty rows are removed. Segments
|
|
201
209
|
that need more than the payload offers (e.g. `context-remaining`) read the session
|
|
202
210
|
transcript to compute it. A segment failure is never fatal — it's simply omitted.
|
|
203
211
|
|
|
@@ -220,9 +228,10 @@ const next = enableSegments(["duration"], config).config;
|
|
|
220
228
|
saveConfig(next);
|
|
221
229
|
```
|
|
222
230
|
|
|
223
|
-
Useful exports include `parseClaudeInput`, `renderLine`, `
|
|
224
|
-
`defaultConfig`, `loadConfig`, `saveConfig`, `configPath`,
|
|
225
|
-
`
|
|
231
|
+
Useful exports include `parseClaudeInput`, `renderLine`, `renderStatusline`, `segments`,
|
|
232
|
+
`getSegment`, `listSegments`, `defaultConfig`, `loadConfig`, `saveConfig`, `configPath`,
|
|
233
|
+
`previewStatusline`, `enableSegments`, `disableSegments`, `orderSegments`,
|
|
234
|
+
`setSeparator`, `setColors`, `installClaude`, and the account, context, format, and Git helpers.
|
|
226
235
|
|
|
227
236
|
## MCP
|
|
228
237
|
|
|
@@ -232,7 +241,7 @@ Useful exports include `parseClaudeInput`, `renderLine`, `segments`, `getSegment
|
|
|
232
241
|
statusline-mcp
|
|
233
242
|
```
|
|
234
243
|
|
|
235
|
-
It exposes safe tools for `render_statusline`, `preview_statusline`,
|
|
244
|
+
It exposes safe tools for `statusline_health`, `render_statusline`, `preview_statusline`,
|
|
236
245
|
`list_segments`, `get_config`, `update_config`, `enable_segments`,
|
|
237
246
|
`disable_segments`, `order_segments`, and `reset_config`. Config mutation tools
|
|
238
247
|
require `confirm_write: true`; every mutation tool supports `dry_run: true`.
|
|
@@ -250,8 +259,9 @@ bun install -g @hasna/statusline
|
|
|
250
259
|
statusline install claude
|
|
251
260
|
```
|
|
252
261
|
|
|
253
|
-
`installClaude()` writes to `$CLAUDE_CONFIG_DIR/settings.json
|
|
254
|
-
`~/.claude/settings.json`.
|
|
262
|
+
`installClaude()` writes to `$CLAUDE_CONFIG_DIR/settings.json` when the variable is
|
|
263
|
+
non-blank, falling back to `~/.claude/settings.json`. A leading `~/` in
|
|
264
|
+
`CLAUDE_CONFIG_DIR` is expanded against `$HOME`. This matters for multi-account setups: when a session is
|
|
255
265
|
bound to an isolated config dir, `~/.claude/settings.json` is never read, so
|
|
256
266
|
installing there wires up a statusline that silently never runs.
|
|
257
267
|
|
package/dist/accounts.d.ts
CHANGED
|
@@ -23,9 +23,11 @@ export interface SessionAccount {
|
|
|
23
23
|
/**
|
|
24
24
|
* How `profile` was resolved. `"layout"` means it was read off the dir path
|
|
25
25
|
* rather than the registry, which is authoritative about the dir but can lag
|
|
26
|
-
* a rename — see `layoutProfile`.
|
|
26
|
+
* a rename — see `layoutProfile`. `"switched"` means an in-place
|
|
27
|
+
* `switch-account` marker named another profile's account as the dir's
|
|
28
|
+
* current occupant. Surfaced for diagnostics, not for display.
|
|
27
29
|
*/
|
|
28
|
-
source: "registry" | "layout" | null;
|
|
30
|
+
source: "registry" | "layout" | "switched" | null;
|
|
29
31
|
}
|
|
30
32
|
type Env = Record<string, string | undefined>;
|
|
31
33
|
/** Config dir the current process is bound to, for settings and registry lookup. */
|
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);
|
|
@@ -273,6 +243,8 @@ var ACCOUNTS_HOME_ENV = "ACCOUNTS_HOME";
|
|
|
273
243
|
var DEFAULT_ACCOUNTS_HOME = [".hasna", "accounts"];
|
|
274
244
|
var STORE_FILE = "accounts.json";
|
|
275
245
|
var PROFILES_SUBDIR = "profiles";
|
|
246
|
+
var AUTH_STATE_SUBDIR = ".accounts-auth";
|
|
247
|
+
var SWITCHED_ACCOUNT_MARKER_FILE = "switched-account.json";
|
|
276
248
|
var STATE_FILE_PREFERRED = ".config.json";
|
|
277
249
|
var STATE_FILE_PREFIX = ".claude";
|
|
278
250
|
var STATE_FILE_SUFFIX = ".json";
|
|
@@ -359,6 +331,14 @@ function layoutProfile(profilesRoot, configDir, env) {
|
|
|
359
331
|
return null;
|
|
360
332
|
return { configDir, profile, tool, source: "layout" };
|
|
361
333
|
}
|
|
334
|
+
function switchedOccupant(configDir) {
|
|
335
|
+
const raw = readJson(join2(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
|
|
336
|
+
if (!raw)
|
|
337
|
+
return null;
|
|
338
|
+
const profile = str(raw.profile);
|
|
339
|
+
const email = str(raw.email);
|
|
340
|
+
return profile || email ? { profile, email } : null;
|
|
341
|
+
}
|
|
362
342
|
function sessionConfigDir(env = process.env) {
|
|
363
343
|
return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
|
|
364
344
|
}
|
|
@@ -382,9 +362,11 @@ async function sessionAccount(env = process.env, tool) {
|
|
|
382
362
|
const paths = await accountsPaths(env);
|
|
383
363
|
const entry = registryEntry(paths.store, configDir, env, tool);
|
|
384
364
|
const name = str(entry?.name);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
365
|
+
const owner = name ? { configDir, profile: name, tool: str(entry?.tool), source: "registry" } : layoutProfile(paths.profiles, configDir, env) ?? unresolved;
|
|
366
|
+
const occupant = switchedOccupant(configDir);
|
|
367
|
+
if (occupant)
|
|
368
|
+
return { configDir, profile: occupant.profile, tool: owner.tool, source: "switched" };
|
|
369
|
+
return owner;
|
|
388
370
|
} catch {
|
|
389
371
|
return unresolved;
|
|
390
372
|
}
|
|
@@ -395,13 +377,29 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
395
377
|
const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
|
|
396
378
|
if (live)
|
|
397
379
|
return live;
|
|
380
|
+
const configDir = sessionConfigDir(env);
|
|
381
|
+
const occupant = switchedOccupant(configDir);
|
|
382
|
+
if (occupant)
|
|
383
|
+
return occupant.email;
|
|
398
384
|
const paths = await accountsPaths(env);
|
|
399
|
-
return str(registryEntry(paths.store,
|
|
385
|
+
return str(registryEntry(paths.store, configDir, env, tool)?.email);
|
|
400
386
|
} catch {
|
|
401
387
|
return null;
|
|
402
388
|
}
|
|
403
389
|
}
|
|
404
390
|
|
|
391
|
+
// src/settings.ts
|
|
392
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
393
|
+
import { join as join3 } from "path";
|
|
394
|
+
function sessionFastMode(env = process.env) {
|
|
395
|
+
try {
|
|
396
|
+
const raw = readFileSync4(join3(sessionConfigDir(env), "settings.json"), "utf8");
|
|
397
|
+
return JSON.parse(raw)?.fastMode === true;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
405
403
|
// src/segments/index.ts
|
|
406
404
|
function friendlyModel(id) {
|
|
407
405
|
let tag = null;
|
|
@@ -421,7 +419,7 @@ function friendlyModel(id) {
|
|
|
421
419
|
var dir = (ctx) => ctx.cwd || process.cwd();
|
|
422
420
|
function modelName(ctx) {
|
|
423
421
|
if (ctx.model?.displayName)
|
|
424
|
-
return ctx.model.displayName;
|
|
422
|
+
return ctx.model.displayName.toLowerCase();
|
|
425
423
|
return ctx.model?.id ? friendlyModel(ctx.model.id).name : null;
|
|
426
424
|
}
|
|
427
425
|
function limitPercent(pct) {
|
|
@@ -515,12 +513,19 @@ var segments = [
|
|
|
515
513
|
if (!name)
|
|
516
514
|
return null;
|
|
517
515
|
if (ctx.effort)
|
|
518
|
-
return `${name} (${ctx.effort})`;
|
|
516
|
+
return `${name} (${ctx.effort.toLowerCase()})`;
|
|
519
517
|
if (ctx.thinking)
|
|
520
518
|
return `${name} (thinking)`;
|
|
521
519
|
return name;
|
|
522
520
|
}
|
|
523
521
|
},
|
|
522
|
+
{
|
|
523
|
+
id: "fast-mode",
|
|
524
|
+
description: `Shows "fast" when fast mode is on for this session's config dir`,
|
|
525
|
+
defaultEnabled: false,
|
|
526
|
+
color: "yellow",
|
|
527
|
+
render: () => sessionFastMode(process.env) ? "fast" : null
|
|
528
|
+
},
|
|
524
529
|
{
|
|
525
530
|
id: "auth-profile",
|
|
526
531
|
description: "Account profile this session runs as (omitted when unmanaged)",
|
|
@@ -648,6 +653,12 @@ var segments = [
|
|
|
648
653
|
description: "Session identifier (short)",
|
|
649
654
|
defaultEnabled: false,
|
|
650
655
|
render: (ctx) => ctx.sessionId ? ctx.sessionId.split("-")[0] : null
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: "newline",
|
|
659
|
+
description: "Row break \u2014 segments after it render on the next line",
|
|
660
|
+
defaultEnabled: false,
|
|
661
|
+
render: () => null
|
|
651
662
|
}
|
|
652
663
|
];
|
|
653
664
|
function getSegment(id) {
|
|
@@ -678,23 +689,28 @@ function paint(segment, text, ctx) {
|
|
|
678
689
|
}
|
|
679
690
|
async function renderLine(ctx, cfg) {
|
|
680
691
|
const colorize = colorsEnabled(cfg);
|
|
681
|
-
const
|
|
692
|
+
const rows = [[]];
|
|
682
693
|
for (const id of cfg.segments) {
|
|
694
|
+
if (id === "newline") {
|
|
695
|
+
rows.push([]);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
683
698
|
const segment = getSegment(id);
|
|
684
699
|
if (!segment)
|
|
685
700
|
continue;
|
|
686
701
|
try {
|
|
687
702
|
const out = await segment.render(ctx);
|
|
688
703
|
if (out)
|
|
689
|
-
|
|
704
|
+
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
|
|
690
705
|
} catch {}
|
|
691
706
|
}
|
|
692
|
-
return parts.join(cfg.separator)
|
|
707
|
+
return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
|
|
708
|
+
`);
|
|
693
709
|
}
|
|
694
710
|
// package.json
|
|
695
711
|
var package_default = {
|
|
696
712
|
name: "@hasna/statusline",
|
|
697
|
-
version: "0.0.
|
|
713
|
+
version: "0.0.5",
|
|
698
714
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
699
715
|
license: "Apache-2.0",
|
|
700
716
|
type: "module",
|
|
@@ -754,28 +770,28 @@ var package_default = {
|
|
|
754
770
|
bun: ">=1.0.0"
|
|
755
771
|
},
|
|
756
772
|
dependencies: {
|
|
757
|
-
"@modelcontextprotocol/sdk": "
|
|
758
|
-
zod: "
|
|
773
|
+
"@modelcontextprotocol/sdk": "1.12.1",
|
|
774
|
+
zod: "3.25.76"
|
|
759
775
|
},
|
|
760
776
|
devDependencies: {
|
|
761
777
|
"@types/bun": "^1.2.4",
|
|
762
|
-
typescript: "
|
|
778
|
+
typescript: "5.9.3"
|
|
763
779
|
}
|
|
764
780
|
};
|
|
765
781
|
// src/install.ts
|
|
766
|
-
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
782
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
767
783
|
import { homedir as homedir3 } from "os";
|
|
768
|
-
import { dirname as dirname2, join as
|
|
784
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
769
785
|
function defaultSettingsPath(env) {
|
|
770
|
-
return
|
|
786
|
+
return join4(env.HOME || homedir3(), ".claude", "settings.json");
|
|
771
787
|
}
|
|
772
788
|
function claudeSettingsPath(env = process.env) {
|
|
773
789
|
const home2 = env.HOME || homedir3();
|
|
774
790
|
const raw = env.CLAUDE_CONFIG_DIR?.trim();
|
|
775
791
|
if (!raw)
|
|
776
792
|
return defaultSettingsPath(env);
|
|
777
|
-
const configDir = raw === "~" || raw.startsWith("~/") ?
|
|
778
|
-
return
|
|
793
|
+
const configDir = raw === "~" || raw.startsWith("~/") ? join4(home2, raw.slice(1)) : raw;
|
|
794
|
+
return join4(configDir, "settings.json");
|
|
779
795
|
}
|
|
780
796
|
function installClaude(settingsPath, env = process.env) {
|
|
781
797
|
const target = settingsPath ?? claudeSettingsPath(env);
|
|
@@ -783,7 +799,7 @@ function installClaude(settingsPath, env = process.env) {
|
|
|
783
799
|
let settings = {};
|
|
784
800
|
if (existsSync4(target)) {
|
|
785
801
|
try {
|
|
786
|
-
settings = JSON.parse(
|
|
802
|
+
settings = JSON.parse(readFileSync5(target, "utf8"));
|
|
787
803
|
} catch (error) {
|
|
788
804
|
throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
|
|
789
805
|
}
|
|
@@ -906,11 +922,11 @@ run \`statusline list\` to see available segments`);
|
|
|
906
922
|
process.exitCode = 1;
|
|
907
923
|
}
|
|
908
924
|
function help() {
|
|
909
|
-
return `statusline ${statuslineVersion} \u2014 composable
|
|
925
|
+
return `statusline ${statuslineVersion} \u2014 composable status rows for AI coding agents
|
|
910
926
|
|
|
911
927
|
usage:
|
|
912
|
-
statusline render read agent JSON on stdin, print
|
|
913
|
-
statusline list
|
|
928
|
+
statusline render read agent JSON on stdin, print status rows (default)
|
|
929
|
+
statusline list [search text] compact segment list (default limit: ${DEFAULT_LIST_LIMIT})
|
|
914
930
|
statusline list --verbose include descriptions
|
|
915
931
|
statusline list --all show all rows
|
|
916
932
|
statusline list --json machine-readable segment list
|
|
@@ -918,8 +934,8 @@ usage:
|
|
|
918
934
|
statusline list --enabled show only enabled segments
|
|
919
935
|
statusline list --disabled show only disabled segments
|
|
920
936
|
statusline list --search text filter by id or description
|
|
921
|
-
statusline search <text> alias for list --search
|
|
922
|
-
statusline show <id> show one segment in detail
|
|
937
|
+
statusline search <text> alias for list --search (accepts list options)
|
|
938
|
+
statusline show <id> show one segment in detail (--json supported)
|
|
923
939
|
statusline show <id> --json machine-readable segment detail
|
|
924
940
|
statusline inspect <id> alias for show
|
|
925
941
|
statusline enable <id...> enable segments (appended to the end)
|
|
@@ -929,7 +945,7 @@ usage:
|
|
|
929
945
|
statusline colors <on|off> toggle segment colours (default on)
|
|
930
946
|
statusline preview render with a sample payload from the current directory
|
|
931
947
|
statusline reset restore default config
|
|
932
|
-
statusline install [claude] wire into Claude Code
|
|
948
|
+
statusline install [claude] wire into the active Claude Code settings file
|
|
933
949
|
statusline version print version
|
|
934
950
|
|
|
935
951
|
config: ${configPath()}`;
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
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);
|
|
33
3
|
var __returnValue = (v) => v;
|
|
34
4
|
function __exportSetter(name, newValue) {
|
|
35
5
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -272,6 +242,8 @@ var ACCOUNTS_HOME_ENV = "ACCOUNTS_HOME";
|
|
|
272
242
|
var DEFAULT_ACCOUNTS_HOME = [".hasna", "accounts"];
|
|
273
243
|
var STORE_FILE = "accounts.json";
|
|
274
244
|
var PROFILES_SUBDIR = "profiles";
|
|
245
|
+
var AUTH_STATE_SUBDIR = ".accounts-auth";
|
|
246
|
+
var SWITCHED_ACCOUNT_MARKER_FILE = "switched-account.json";
|
|
275
247
|
var STATE_FILE_PREFERRED = ".config.json";
|
|
276
248
|
var STATE_FILE_PREFIX = ".claude";
|
|
277
249
|
var STATE_FILE_SUFFIX = ".json";
|
|
@@ -358,6 +330,14 @@ function layoutProfile(profilesRoot, configDir, env) {
|
|
|
358
330
|
return null;
|
|
359
331
|
return { configDir, profile, tool, source: "layout" };
|
|
360
332
|
}
|
|
333
|
+
function switchedOccupant(configDir) {
|
|
334
|
+
const raw = readJson(join2(configDir, AUTH_STATE_SUBDIR, SWITCHED_ACCOUNT_MARKER_FILE));
|
|
335
|
+
if (!raw)
|
|
336
|
+
return null;
|
|
337
|
+
const profile = str(raw.profile);
|
|
338
|
+
const email = str(raw.email);
|
|
339
|
+
return profile || email ? { profile, email } : null;
|
|
340
|
+
}
|
|
361
341
|
function sessionConfigDir(env = process.env) {
|
|
362
342
|
return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
|
|
363
343
|
}
|
|
@@ -381,9 +361,11 @@ async function sessionAccount(env = process.env, tool) {
|
|
|
381
361
|
const paths = await accountsPaths(env);
|
|
382
362
|
const entry = registryEntry(paths.store, configDir, env, tool);
|
|
383
363
|
const name = str(entry?.name);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
364
|
+
const owner = name ? { configDir, profile: name, tool: str(entry?.tool), source: "registry" } : layoutProfile(paths.profiles, configDir, env) ?? unresolved;
|
|
365
|
+
const occupant = switchedOccupant(configDir);
|
|
366
|
+
if (occupant)
|
|
367
|
+
return { configDir, profile: occupant.profile, tool: owner.tool, source: "switched" };
|
|
368
|
+
return owner;
|
|
387
369
|
} catch {
|
|
388
370
|
return unresolved;
|
|
389
371
|
}
|
|
@@ -394,13 +376,29 @@ async function sessionAccountEmail(env = process.env, tool) {
|
|
|
394
376
|
const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
|
|
395
377
|
if (live)
|
|
396
378
|
return live;
|
|
379
|
+
const configDir = sessionConfigDir(env);
|
|
380
|
+
const occupant = switchedOccupant(configDir);
|
|
381
|
+
if (occupant)
|
|
382
|
+
return occupant.email;
|
|
397
383
|
const paths = await accountsPaths(env);
|
|
398
|
-
return str(registryEntry(paths.store,
|
|
384
|
+
return str(registryEntry(paths.store, configDir, env, tool)?.email);
|
|
399
385
|
} catch {
|
|
400
386
|
return null;
|
|
401
387
|
}
|
|
402
388
|
}
|
|
403
389
|
|
|
390
|
+
// src/settings.ts
|
|
391
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
392
|
+
import { join as join3 } from "path";
|
|
393
|
+
function sessionFastMode(env = process.env) {
|
|
394
|
+
try {
|
|
395
|
+
const raw = readFileSync4(join3(sessionConfigDir(env), "settings.json"), "utf8");
|
|
396
|
+
return JSON.parse(raw)?.fastMode === true;
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
404
402
|
// src/segments/index.ts
|
|
405
403
|
function friendlyModel(id) {
|
|
406
404
|
let tag = null;
|
|
@@ -420,7 +418,7 @@ function friendlyModel(id) {
|
|
|
420
418
|
var dir = (ctx) => ctx.cwd || process.cwd();
|
|
421
419
|
function modelName(ctx) {
|
|
422
420
|
if (ctx.model?.displayName)
|
|
423
|
-
return ctx.model.displayName;
|
|
421
|
+
return ctx.model.displayName.toLowerCase();
|
|
424
422
|
return ctx.model?.id ? friendlyModel(ctx.model.id).name : null;
|
|
425
423
|
}
|
|
426
424
|
function limitPercent(pct) {
|
|
@@ -514,12 +512,19 @@ var segments = [
|
|
|
514
512
|
if (!name)
|
|
515
513
|
return null;
|
|
516
514
|
if (ctx.effort)
|
|
517
|
-
return `${name} (${ctx.effort})`;
|
|
515
|
+
return `${name} (${ctx.effort.toLowerCase()})`;
|
|
518
516
|
if (ctx.thinking)
|
|
519
517
|
return `${name} (thinking)`;
|
|
520
518
|
return name;
|
|
521
519
|
}
|
|
522
520
|
},
|
|
521
|
+
{
|
|
522
|
+
id: "fast-mode",
|
|
523
|
+
description: `Shows "fast" when fast mode is on for this session's config dir`,
|
|
524
|
+
defaultEnabled: false,
|
|
525
|
+
color: "yellow",
|
|
526
|
+
render: () => sessionFastMode(process.env) ? "fast" : null
|
|
527
|
+
},
|
|
523
528
|
{
|
|
524
529
|
id: "auth-profile",
|
|
525
530
|
description: "Account profile this session runs as (omitted when unmanaged)",
|
|
@@ -647,6 +652,12 @@ var segments = [
|
|
|
647
652
|
description: "Session identifier (short)",
|
|
648
653
|
defaultEnabled: false,
|
|
649
654
|
render: (ctx) => ctx.sessionId ? ctx.sessionId.split("-")[0] : null
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
id: "newline",
|
|
658
|
+
description: "Row break \u2014 segments after it render on the next line",
|
|
659
|
+
defaultEnabled: false,
|
|
660
|
+
render: () => null
|
|
650
661
|
}
|
|
651
662
|
];
|
|
652
663
|
function getSegment(id) {
|
|
@@ -677,23 +688,28 @@ function paint(segment, text, ctx) {
|
|
|
677
688
|
}
|
|
678
689
|
async function renderLine(ctx, cfg) {
|
|
679
690
|
const colorize = colorsEnabled(cfg);
|
|
680
|
-
const
|
|
691
|
+
const rows = [[]];
|
|
681
692
|
for (const id of cfg.segments) {
|
|
693
|
+
if (id === "newline") {
|
|
694
|
+
rows.push([]);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
682
697
|
const segment = getSegment(id);
|
|
683
698
|
if (!segment)
|
|
684
699
|
continue;
|
|
685
700
|
try {
|
|
686
701
|
const out = await segment.render(ctx);
|
|
687
702
|
if (out)
|
|
688
|
-
|
|
703
|
+
rows[rows.length - 1].push(colorize ? paint(segment, out, ctx) : out);
|
|
689
704
|
} catch {}
|
|
690
705
|
}
|
|
691
|
-
return parts.join(cfg.separator)
|
|
706
|
+
return rows.map((parts) => parts.join(cfg.separator)).filter((row) => row.length > 0).join(`
|
|
707
|
+
`);
|
|
692
708
|
}
|
|
693
709
|
// package.json
|
|
694
710
|
var package_default = {
|
|
695
711
|
name: "@hasna/statusline",
|
|
696
|
-
version: "0.0.
|
|
712
|
+
version: "0.0.5",
|
|
697
713
|
description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
698
714
|
license: "Apache-2.0",
|
|
699
715
|
type: "module",
|
|
@@ -753,28 +769,28 @@ var package_default = {
|
|
|
753
769
|
bun: ">=1.0.0"
|
|
754
770
|
},
|
|
755
771
|
dependencies: {
|
|
756
|
-
"@modelcontextprotocol/sdk": "
|
|
757
|
-
zod: "
|
|
772
|
+
"@modelcontextprotocol/sdk": "1.12.1",
|
|
773
|
+
zod: "3.25.76"
|
|
758
774
|
},
|
|
759
775
|
devDependencies: {
|
|
760
776
|
"@types/bun": "^1.2.4",
|
|
761
|
-
typescript: "
|
|
777
|
+
typescript: "5.9.3"
|
|
762
778
|
}
|
|
763
779
|
};
|
|
764
780
|
// src/install.ts
|
|
765
|
-
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as
|
|
781
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
766
782
|
import { homedir as homedir3 } from "os";
|
|
767
|
-
import { dirname as dirname2, join as
|
|
783
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
768
784
|
function defaultSettingsPath(env) {
|
|
769
|
-
return
|
|
785
|
+
return join4(env.HOME || homedir3(), ".claude", "settings.json");
|
|
770
786
|
}
|
|
771
787
|
function claudeSettingsPath(env = process.env) {
|
|
772
788
|
const home2 = env.HOME || homedir3();
|
|
773
789
|
const raw = env.CLAUDE_CONFIG_DIR?.trim();
|
|
774
790
|
if (!raw)
|
|
775
791
|
return defaultSettingsPath(env);
|
|
776
|
-
const configDir = raw === "~" || raw.startsWith("~/") ?
|
|
777
|
-
return
|
|
792
|
+
const configDir = raw === "~" || raw.startsWith("~/") ? join4(home2, raw.slice(1)) : raw;
|
|
793
|
+
return join4(configDir, "settings.json");
|
|
778
794
|
}
|
|
779
795
|
function installClaude(settingsPath, env = process.env) {
|
|
780
796
|
const target = settingsPath ?? claudeSettingsPath(env);
|
|
@@ -782,7 +798,7 @@ function installClaude(settingsPath, env = process.env) {
|
|
|
782
798
|
let settings = {};
|
|
783
799
|
if (existsSync4(target)) {
|
|
784
800
|
try {
|
|
785
|
-
settings = JSON.parse(
|
|
801
|
+
settings = JSON.parse(readFileSync5(target, "utf8"));
|
|
786
802
|
} catch (error) {
|
|
787
803
|
throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
|
|
788
804
|
}
|