@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 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 a single line for your agent's status bar.
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 ~/.claude/settings.json
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 18
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`, which the host defaults to on | no model | `Fable (xhigh)` |
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 past 80%.
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 / 18 total (showing 12 of 18)
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 context-used
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 line. Segments
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`, `segments`, `getSegment`,
224
- `defaultConfig`, `loadConfig`, `saveConfig`, `configPath`, `previewStatusline`,
225
- `orderSegments`, `disableSegments`, `setSeparator`, and `installClaude`.
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`, falling back to
254
- `~/.claude/settings.json`. This matters for multi-account setups: when a session is
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
 
@@ -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`. Surfaced for diagnostics, not for display.
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
- if (name)
386
- return { configDir, profile: name, tool: str(entry?.tool), source: "registry" };
387
- return layoutProfile(paths.profiles, configDir, env) ?? unresolved;
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, sessionConfigDir(env), env, tool)?.email);
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 parts = [];
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
- parts.push(colorize ? paint(segment, out, ctx) : out);
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.3",
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": "^1.12.1",
758
- zod: "^3.25.76"
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: "^5.7.3"
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 readFileSync4, writeFileSync as writeFileSync2 } from "fs";
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 join3, resolve as resolve2 } from "path";
784
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
769
785
  function defaultSettingsPath(env) {
770
- return join3(env.HOME || homedir3(), ".claude", "settings.json");
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("~/") ? join3(home2, raw.slice(1)) : raw;
778
- return join3(configDir, "settings.json");
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(readFileSync4(target, "utf8"));
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 statusline for AI coding agents
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 the statusline (default)
913
- statusline list compact segment list (default limit: ${DEFAULT_LIST_LIMIT})
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 (~/.claude/settings.json)
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
- if (name)
385
- return { configDir, profile: name, tool: str(entry?.tool), source: "registry" };
386
- return layoutProfile(paths.profiles, configDir, env) ?? unresolved;
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, sessionConfigDir(env), env, tool)?.email);
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 parts = [];
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
- parts.push(colorize ? paint(segment, out, ctx) : out);
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.3",
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": "^1.12.1",
757
- zod: "^3.25.76"
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: "^5.7.3"
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 readFileSync4, writeFileSync as writeFileSync2 } from "fs";
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 join3, resolve as resolve2 } from "path";
783
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
768
784
  function defaultSettingsPath(env) {
769
- return join3(env.HOME || homedir3(), ".claude", "settings.json");
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("~/") ? join3(home2, raw.slice(1)) : raw;
777
- return join3(configDir, "settings.json");
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(readFileSync4(target, "utf8"));
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
  }