@hasna/statusline 0.0.1 → 0.0.3

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 CHANGED
@@ -8,7 +8,7 @@ and more — rendered as a single line for your agent's status bar.
8
8
  apple03 · statusline (main) · 12h · 1.2k · fable 5 [1m] · 90% left · $0.04
9
9
  ```
10
10
 
11
- Zero runtime dependencies. Built with [Bun](https://bun.sh) + TypeScript. Apache-2.0.
11
+ Built with [Bun](https://bun.sh) + TypeScript. The CLI renderer is dependency-light; the optional MCP server uses the Model Context Protocol SDK. Apache-2.0.
12
12
 
13
13
  ## Install
14
14
 
@@ -21,34 +21,146 @@ That's it — Claude Code picks it up on the next status refresh (no restart nee
21
21
 
22
22
  ## Segments
23
23
 
24
- `statusline list` shows everything available:
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 18
26
+ segment ids from `src/segments/index.ts`.
25
27
 
28
+ `renderLine` walks your configured segment order, renders each one, and **drops**
29
+ anything that returns `null` or throws. A failed segment never breaks the host UI.
30
+
31
+ | Segment | Default | Description | Data source | Omit when | Example |
32
+ |---------|---------|-------------|-------------|-----------|---------|
33
+ | `machine` | on | Machine hostname (short) | `os.hostname()` — first label before `.` | never (always renders) | `apple03` |
34
+ | `project` | on | Project name with current Git branch | `gitProjectName(cwd)` + `gitBranch(cwd)`; falls back to cwd basename | no project name resolvable | `statusline (main)` |
35
+ | `project-name` | off | Project name only | `gitProjectName(cwd)` or cwd basename | outside a git repo with no name | `statusline` |
36
+ | `git-branch` | off | Current Git branch | `git branch --show-current` in `cwd` | outside git or detached | `main` |
37
+ | `commit-age` | on | Time since last commit (compact) | `git log -1` epoch vs now | no commits / not a repo | `12h` |
38
+ | `loc` | on | Lines of code tracked by Git (compact) | `git ls-files` + `wc -l` in repo root | outside git or zero lines | `1.2k` |
39
+ | `current-dir` | off | Current working directory (basename) | `StatusContext.cwd` | never (always renders basename) | `open-statusline` |
40
+ | `model` | off | Current model name (no context tag) | `StatusContext.model.id` via friendly formatter | no model id | `fable 5` |
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` |
48
+ | `context-used` | off | Percentage of context window used | session transcript JSONL (`contextUsage`) | transcript missing/unreadable | `10%` |
49
+ | `context-remaining` | on | Percentage of context window remaining | transcript via `contextUsage` | transcript missing/unreadable | `90% left` |
50
+ | `used-tokens` | off | Total tokens in the context window | transcript usage block (input + output) | transcript missing/unreadable | `102k tok` |
51
+ | `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
+ | `lines-changed` | off | Lines added/removed this session | `cost.totalLinesAdded/Removed` | both zero | `+142/-18` |
54
+ | `output-style` | off | Active output style | `StatusContext.outputStyle` | style is `default` or missing | `concise` |
55
+ | `agent-version` | off | Host agent version | `StatusContext.version` | version missing | `v2.1.39` |
56
+ | `session-id` | off | Session identifier (short) | first UUID segment of `sessionId` | no session id | `abc12345` |
57
+
58
+ ### Context segments and transcripts
59
+
60
+ Segments that report context usage (`context-used`, `context-remaining`, `used-tokens`)
61
+ read the session transcript path from the provider payload. `contextUsage` scans the
62
+ JSONL for the last assistant entry with a `usage` block and sums
63
+ `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. Window size is
64
+ **1,000,000** when the model id contains `[1m]`, otherwise **200,000**.
65
+
66
+ With the test fixture transcript (100k input-side tokens on a 1m window), expect
67
+ `context-used` → `10%`, `context-remaining` → `90% left`, `used-tokens` → `102k tok`.
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
26
110
  ```
27
- [x] machine Machine hostname (short)
28
- [x] project Project name with current Git branch
29
- [ ] project-name Project name (omitted when unavailable)
30
- [ ] git-branch Current Git branch (omitted when unavailable)
31
- [x] commit-age Time since last commit (compact)
32
- [x] loc Lines of code tracked by Git (compact)
33
- [ ] current-dir Current working directory (basename)
34
- [ ] model Current model name
35
- [x] model-context Model name with context-size tag (e.g. [1m])
36
- [ ] context-used Percentage of context window used (omitted when unknown)
37
- [x] context-remaining Percentage of context window remaining (omitted when unknown)
38
- [ ] used-tokens Total tokens in the context window (omitted when unknown)
39
- [x] cost Session cost in USD (omitted when zero)
40
- [ ] duration Session wall-clock duration
41
- [ ] lines-changed Lines added/removed this session (omitted when zero)
42
- [ ] output-style Active output style (omitted when default)
43
- [ ] agent-version Host agent version (e.g. Claude Code version)
44
- [ ] session-id Session identifier (short)
111
+
112
+ `NO_COLOR` in the environment always wins.
113
+
114
+ ### Quick reference from `statusline list`
115
+
116
+ `statusline list` is compact by default so it stays friendly in agent terminals —
117
+ enabled segments first, capped rows, and hints for the detail paths:
118
+
119
+ ```
120
+ Segments: 7 enabled / 18 total (showing 12 of 18)
121
+ state default id
122
+ on yes machine
123
+ on yes project
124
+ on yes commit-age
125
+ on yes loc
126
+ on yes model-context
127
+ on yes context-remaining
128
+ on yes cost
129
+ off no project-name
130
+ off no git-branch
131
+ off no current-dir
132
+ off no model
133
+ off no context-used
134
+ Hint: use `--all` for all matching rows, `statusline show <id>` for details, `--verbose` for descriptions, `--json` for machines.
45
135
  ```
46
136
 
137
+ Use gradual disclosure when you need more:
138
+
139
+ ```bash
140
+ statusline list --verbose # include descriptions, still capped
141
+ statusline list --all --verbose # include every row with descriptions
142
+ statusline list --all # show every segment row
143
+ statusline list --limit 5 # cap rows explicitly
144
+ statusline list --enabled # only enabled segments
145
+ statusline list --disabled # only disabled segments
146
+ statusline list --search token # filter by id or description
147
+ statusline list --json # structured output for tooling
148
+ statusline show model-context # full details for one segment
149
+ statusline inspect used-tokens --json # JSON detail path
150
+ ```
151
+
152
+ `statusline list --json` returns `{ total, matching, showing, limited, segments, next }`.
153
+ Each segment has `{ id, description, enabled, defaultEnabled }`; `next` is `null`
154
+ unless the result is limited. Column padding is display-only — it does not affect
155
+ statusbar width.
156
+
47
157
  ## Usage
48
158
 
49
159
  ```bash
160
+ statusline list # compact segment summary
50
161
  statusline enable used-tokens duration # turn segments on (appended at the end)
51
162
  statusline disable loc # turn segments off
163
+ statusline colors off # drop ANSI colours
52
164
  statusline order machine project model-context cost # exact order = enabled set
53
165
  statusline separator " | " # change the separator
54
166
  statusline preview # render a sample line from the current dir
@@ -64,6 +176,23 @@ Config lives at `~/.config/statusline/config.json` (override with `$STATUSLINE_C
64
176
  }
65
177
  ```
66
178
 
179
+ ## Theming and layout
180
+
181
+ There is no fixed width knob — the rendered line grows with enabled segments. Control
182
+ length practically:
183
+
184
+ 1. **Fewer segments** — `statusline disable` segments you do not need, or `statusline order` to pick a compact set.
185
+ 2. **Shorter separator** — `statusline separator "·"` or `" | "` (default is `" · "`).
186
+ 3. **Split project info** — use `project-name` + `git-branch` instead of combined `project` if you want to omit the branch when unknown.
187
+ 4. **Rely on omit-when-null** — segments like `cost`, `context-remaining`, and `lines-changed` disappear when data is unavailable instead of showing placeholders.
188
+
189
+ **Segment order matters.** `statusline order a b c` sets both the display order and the
190
+ exact enabled set — segments not listed are disabled. `statusline enable` appends new
191
+ ids to the end; `statusline disable` removes them.
192
+
193
+ **Config path:** `~/.config/statusline/config.json`, overridable with `$STATUSLINE_CONFIG`.
194
+ `statusline reset` restores `defaultConfig()` (separator + default segment list above).
195
+
67
196
  ## How it works
68
197
 
69
198
  Claude Code pipes a JSON payload (cwd, model, session cost, transcript path, …) to the
@@ -72,11 +201,118 @@ renders each enabled segment, drops anything unavailable, and prints one line. S
72
201
  that need more than the payload offers (e.g. `context-remaining`) read the session
73
202
  transcript to compute it. A segment failure is never fatal — it's simply omitted.
74
203
 
204
+ ## SDK
205
+
206
+ The package root is a side-effect-free SDK surface:
207
+
208
+ ```ts
209
+ import {
210
+ renderStatusline,
211
+ listSegments,
212
+ enableSegments,
213
+ saveConfig,
214
+ loadConfig,
215
+ } from "@hasna/statusline";
216
+
217
+ const config = loadConfig();
218
+ const line = await renderStatusline({ cwd: process.cwd() }, config);
219
+ const next = enableSegments(["duration"], config).config;
220
+ saveConfig(next);
221
+ ```
222
+
223
+ Useful exports include `parseClaudeInput`, `renderLine`, `segments`, `getSegment`,
224
+ `defaultConfig`, `loadConfig`, `saveConfig`, `configPath`, `previewStatusline`,
225
+ `orderSegments`, `disableSegments`, `setSeparator`, and `installClaude`.
226
+
227
+ ## MCP
228
+
229
+ `statusline-mcp` starts a stdio MCP server:
230
+
231
+ ```bash
232
+ statusline-mcp
233
+ ```
234
+
235
+ It exposes safe tools for `render_statusline`, `preview_statusline`,
236
+ `list_segments`, `get_config`, `update_config`, `enable_segments`,
237
+ `disable_segments`, `order_segments`, and `reset_config`. Config mutation tools
238
+ require `confirm_write: true`; every mutation tool supports `dry_run: true`.
239
+
240
+ The MCP server intentionally does not mutate Claude settings. Use
241
+ `statusline install claude` or the `installClaude()` SDK helper when you
242
+ explicitly want to wire the CLI into Claude Code.
243
+
75
244
  ## Providers
76
245
 
246
+ ### Claude Code (implemented)
247
+
248
+ ```bash
249
+ bun install -g @hasna/statusline
250
+ statusline install claude
251
+ ```
252
+
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)`.
264
+
265
+ ```json
266
+ {
267
+ "statusLine": { "type": "command", "command": "statusline render" }
268
+ }
269
+ ```
270
+
271
+ Existing settings are preserved; the previous file is backed up as
272
+ `settings.json.bak-statusline`. Claude Code pipes JSON on stdin to `statusline render`
273
+ on every status refresh — **no restart needed**.
274
+
275
+ **stdin fields** parsed by `parseClaudeInput` (`src/providers/claude.ts`):
276
+
277
+ | Payload field | Maps to `StatusContext` |
278
+ |---------------|-------------------------|
279
+ | `cwd` / `workspace.current_dir` | `cwd` |
280
+ | `workspace.project_dir` | `projectDir` |
281
+ | `model.id`, `model.display_name` | `model.id`, `model.displayName` |
282
+ | `cost.total_cost_usd` | `cost.totalCostUsd` |
283
+ | `cost.total_duration_ms` | `cost.totalDurationMs` |
284
+ | `cost.total_lines_added/removed` | `cost.totalLinesAdded/Removed` |
285
+ | `transcript_path` | `transcriptPath` |
286
+ | `session_id` | `sessionId` |
287
+ | `session_name` | `sessionName` |
288
+ | `version` | `version` |
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` |
294
+
295
+ Every field is optional — partial or empty payloads still produce a usable context.
296
+
297
+ ### Codewith (not implemented)
298
+
299
+ `statusline install` only accepts `claude` today. Running
300
+ `statusline install codewith` errors with *unsupported target* (`src/cli.ts`).
301
+
302
+ The renderer is provider-agnostic: a future `parseCodewithInput` would map Codewith's
303
+ status payload into `StatusContext` (`src/providers/types.ts`) and reuse the same
304
+ segment registry. No install wiring exists yet (tracked in STA-00003).
305
+
306
+ ### Cursor (not implemented)
307
+
308
+ Same gap as Codewith — no `statusline install cursor` subcommand and no Cursor-specific
309
+ parser. A future adapter would implement `parseCursorInput` → `StatusContext` and hook
310
+ into Cursor's status bar the same way Claude does (tracked in STA-00004).
311
+
312
+ ### Other agents
313
+
77
314
  | Agent | Status |
78
315
  |-------|--------|
79
- | Claude Code | ✅ `statusline install claude` |
80
316
  | OpenCode | ⏳ no statusline/footer hook in its config as of v1.3.x — the renderer is provider-agnostic (`src/providers/`), so an adapter slots in the moment one exists |
81
317
  | Codex CLI | Codex ships its own built-in segment picker; no external command hook |
82
318
 
@@ -87,8 +323,16 @@ Adding a provider means one small parser: agent payload → `StatusContext`
87
323
 
88
324
  ```bash
89
325
  bun install
326
+ bun run typecheck
90
327
  bun test
91
- echo '{"cwd":"'$PWD'","model":{"id":"claude-fable-5[1m]"}}' | bun src/index.ts render
328
+ bun run build
329
+ echo '{"cwd":"'$PWD'","model":{"id":"claude-fable-5[1m]"}}' | bun src/cli.ts render
330
+ ```
331
+
332
+ Spot-check fixture output:
333
+
334
+ ```bash
335
+ cat test/fixtures/claude-input.json | bun src/cli.ts render
92
336
  ```
93
337
 
94
338
  ## License
@@ -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.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export declare function runCli(argv?: string[]): Promise<void>;