@hasna/statusline 0.0.2 → 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/dist/index.js CHANGED
@@ -51,6 +51,7 @@ import { dirname, join } from "path";
51
51
  function defaultConfig() {
52
52
  return {
53
53
  separator: " \xB7 ",
54
+ colors: true,
54
55
  segments: [
55
56
  "machine",
56
57
  "project",
@@ -78,7 +79,8 @@ function loadConfig(path = configPath()) {
78
79
  const fallback = defaultConfig();
79
80
  return {
80
81
  separator: typeof raw.separator === "string" ? raw.separator : fallback.separator,
81
- segments: Array.isArray(raw.segments) ? raw.segments.filter((s) => typeof s === "string") : fallback.segments
82
+ segments: Array.isArray(raw.segments) ? raw.segments.filter((s) => typeof s === "string") : fallback.segments,
83
+ colors: typeof raw.colors === "boolean" ? raw.colors : fallback.colors
82
84
  };
83
85
  } catch {
84
86
  return defaultConfig();
@@ -106,8 +108,15 @@ function parseClaudeInput(input) {
106
108
  } : undefined,
107
109
  transcriptPath: input?.transcript_path || undefined,
108
110
  sessionId: input?.session_id || undefined,
111
+ sessionName: input?.session_name || undefined,
109
112
  version: input?.version || undefined,
110
- outputStyle: input?.output_style?.name || undefined
113
+ outputStyle: input?.output_style?.name || undefined,
114
+ effort: input?.effort?.level || undefined,
115
+ thinking: typeof input?.thinking?.enabled === "boolean" ? input.thinking.enabled : undefined,
116
+ rateLimits: input?.rate_limits ? {
117
+ fiveHour: { usedPercentage: numberOr(input.rate_limits.five_hour?.used_percentage) },
118
+ sevenDay: { usedPercentage: numberOr(input.rate_limits.seven_day?.used_percentage) }
119
+ } : undefined
111
120
  };
112
121
  }
113
122
  function numberOr(v) {
@@ -252,6 +261,146 @@ function windowSize(ctx) {
252
261
  return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
253
262
  }
254
263
 
264
+ // src/accounts.ts
265
+ import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
266
+ import { homedir as homedir2 } from "os";
267
+ import { join as join2, resolve, sep } from "path";
268
+ var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
269
+ var DEFAULT_CONFIG_SUBDIR = ".claude";
270
+ var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
271
+ var ACCOUNTS_HOME_ENV = "ACCOUNTS_HOME";
272
+ var DEFAULT_ACCOUNTS_HOME = [".hasna", "accounts"];
273
+ var STORE_FILE = "accounts.json";
274
+ var PROFILES_SUBDIR = "profiles";
275
+ var STATE_FILE_PREFERRED = ".config.json";
276
+ var STATE_FILE_PREFIX = ".claude";
277
+ var STATE_FILE_SUFFIX = ".json";
278
+ var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
279
+ var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
280
+ function str(value) {
281
+ return typeof value === "string" && value.trim() ? value.trim() : null;
282
+ }
283
+ function home(env) {
284
+ return str(env.HOME) ?? homedir2();
285
+ }
286
+ function expandHome(path, env) {
287
+ return path === "~" || path.startsWith(`~${sep}`) ? join2(home(env), path.slice(1)) : path;
288
+ }
289
+ function canonical(path, env) {
290
+ const abs = resolve(expandHome(path, env));
291
+ try {
292
+ return realpathSync(abs);
293
+ } catch {
294
+ return abs;
295
+ }
296
+ }
297
+ function readJson(path) {
298
+ try {
299
+ return JSON.parse(readFileSync3(path, "utf8"));
300
+ } catch {
301
+ return null;
302
+ }
303
+ }
304
+ var ACCOUNTS_STORAGE_MODULE = "@hasna/accounts/storage";
305
+ var storagePromise;
306
+ function accountsStorage() {
307
+ storagePromise ??= import(ACCOUNTS_STORAGE_MODULE).then((mod) => typeof mod?.storePath === "function" && typeof mod?.profilesDir === "function" ? mod : null, () => null);
308
+ return storagePromise;
309
+ }
310
+ async function accountsPaths(env) {
311
+ const homeOverride = str(env[ACCOUNTS_HOME_ENV]);
312
+ const storeOverride = str(env[STORE_PATH_ENV]);
313
+ if (!homeOverride && !storeOverride) {
314
+ const storage = await accountsStorage();
315
+ try {
316
+ if (storage)
317
+ return { store: storage.storePath(), profiles: storage.profilesDir() };
318
+ } catch {}
319
+ }
320
+ const base = homeOverride ?? join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
321
+ return {
322
+ store: storeOverride ?? join2(base, STORE_FILE),
323
+ profiles: join2(base, PROFILES_SUBDIR)
324
+ };
325
+ }
326
+ function registryEntry(store, configDir, env, tool) {
327
+ if (!existsSync3(store))
328
+ return null;
329
+ const entries = readJson(store)?.profiles;
330
+ if (!Array.isArray(entries))
331
+ return null;
332
+ let untyped = null;
333
+ let first = null;
334
+ for (const entry of entries) {
335
+ const dir = str(entry?.dir);
336
+ if (!dir || canonical(dir, env) !== configDir)
337
+ continue;
338
+ const entryTool = str(entry?.tool);
339
+ if (tool && entryTool === tool)
340
+ return entry;
341
+ if (!entryTool)
342
+ untyped ??= entry;
343
+ first ??= entry;
344
+ }
345
+ return tool ? untyped : first;
346
+ }
347
+ function layoutProfile(profilesRoot, configDir, env) {
348
+ if (!existsSync3(configDir))
349
+ return null;
350
+ const prefix = canonical(profilesRoot, env) + sep;
351
+ if (!configDir.startsWith(prefix))
352
+ return null;
353
+ const parts = configDir.slice(prefix.length).split(sep);
354
+ if (parts.length !== 2)
355
+ return null;
356
+ const [tool, profile] = parts;
357
+ if (!tool || !profile)
358
+ return null;
359
+ return { configDir, profile, tool, source: "layout" };
360
+ }
361
+ function sessionConfigDir(env = process.env) {
362
+ return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
363
+ }
364
+ function sessionStateFile(env = process.env) {
365
+ const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
366
+ const preferred = join2(base, STATE_FILE_PREFERRED);
367
+ if (existsSync3(preferred))
368
+ return preferred;
369
+ const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
370
+ for (const variant of variants) {
371
+ const candidate = join2(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
372
+ if (existsSync3(candidate))
373
+ return candidate;
374
+ }
375
+ return null;
376
+ }
377
+ async function sessionAccount(env = process.env, tool) {
378
+ const configDir = sessionConfigDir(env);
379
+ const unresolved = { configDir, profile: null, tool: null, source: null };
380
+ try {
381
+ const paths = await accountsPaths(env);
382
+ const entry = registryEntry(paths.store, configDir, env, tool);
383
+ 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;
387
+ } catch {
388
+ return unresolved;
389
+ }
390
+ }
391
+ async function sessionAccountEmail(env = process.env, tool) {
392
+ try {
393
+ const stateFile = sessionStateFile(env);
394
+ const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
395
+ if (live)
396
+ return live;
397
+ const paths = await accountsPaths(env);
398
+ return str(registryEntry(paths.store, sessionConfigDir(env), env, tool)?.email);
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+
255
404
  // src/segments/index.ts
256
405
  function friendlyModel(id) {
257
406
  let tag = null;
@@ -269,6 +418,14 @@ function friendlyModel(id) {
269
418
  return { name: name || mid, tag };
270
419
  }
271
420
  var dir = (ctx) => ctx.cwd || process.cwd();
421
+ function modelName(ctx) {
422
+ if (ctx.model?.displayName)
423
+ return ctx.model.displayName;
424
+ return ctx.model?.id ? friendlyModel(ctx.model.id).name : null;
425
+ }
426
+ function limitPercent(pct) {
427
+ return typeof pct === "number" ? pct : 0;
428
+ }
272
429
  var segments = [
273
430
  {
274
431
  id: "machine",
@@ -347,6 +504,66 @@ var segments = [
347
504
  return tag ? `${name} ${tag}` : name;
348
505
  }
349
506
  },
507
+ {
508
+ id: "model-with-reasoning",
509
+ description: "Model name with its reasoning effort (or thinking, when on)",
510
+ defaultEnabled: false,
511
+ color: "cyan",
512
+ render(ctx) {
513
+ const name = modelName(ctx);
514
+ if (!name)
515
+ return null;
516
+ if (ctx.effort)
517
+ return `${name} (${ctx.effort})`;
518
+ if (ctx.thinking)
519
+ return `${name} (thinking)`;
520
+ return name;
521
+ }
522
+ },
523
+ {
524
+ id: "auth-profile",
525
+ description: "Account profile this session runs as (omitted when unmanaged)",
526
+ defaultEnabled: false,
527
+ color: "magenta",
528
+ async render(ctx) {
529
+ const account = await sessionAccount(process.env, ctx.provider);
530
+ return account.profile ?? await sessionAccountEmail(process.env, ctx.provider);
531
+ }
532
+ },
533
+ {
534
+ id: "auth-email",
535
+ description: "Account email this session is logged in as (omitted when unknown)",
536
+ defaultEnabled: false,
537
+ color: "magenta",
538
+ render: (ctx) => sessionAccountEmail(process.env, ctx.provider)
539
+ },
540
+ {
541
+ id: "five-hour-limit",
542
+ description: "Percentage of the 5-hour rate limit used (omitted when unreported)",
543
+ defaultEnabled: false,
544
+ color: (ctx) => limitPercent(ctx.rateLimits?.fiveHour?.usedPercentage) >= 80 ? "red" : "yellow",
545
+ render(ctx) {
546
+ const pct = ctx.rateLimits?.fiveHour?.usedPercentage;
547
+ return typeof pct === "number" ? `5h:${Math.round(pct)}%` : null;
548
+ }
549
+ },
550
+ {
551
+ id: "seven-day-limit",
552
+ description: "Percentage of the 7-day rate limit used (omitted when unreported)",
553
+ defaultEnabled: false,
554
+ color: (ctx) => limitPercent(ctx.rateLimits?.sevenDay?.usedPercentage) >= 80 ? "red" : "yellow",
555
+ render(ctx) {
556
+ const pct = ctx.rateLimits?.sevenDay?.usedPercentage;
557
+ return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
558
+ }
559
+ },
560
+ {
561
+ id: "thread-title",
562
+ description: "Thread title set with /rename (omitted until named)",
563
+ defaultEnabled: false,
564
+ color: "blue",
565
+ render: (ctx) => ctx.sessionName || null
566
+ },
350
567
  {
351
568
  id: "context-used",
352
569
  description: "Percentage of context window used (omitted when unknown)",
@@ -437,7 +654,29 @@ function getSegment(id) {
437
654
  }
438
655
 
439
656
  // src/render.ts
657
+ var ANSI = {
658
+ cyan: "\x1B[36m",
659
+ magenta: "\x1B[35m",
660
+ blue: "\x1B[34m",
661
+ green: "\x1B[32m",
662
+ yellow: "\x1B[33m",
663
+ red: "\x1B[31m",
664
+ bold: "\x1B[1m",
665
+ dim: "\x1B[2m"
666
+ };
667
+ var RESET = "\x1B[0m";
668
+ function colorsEnabled(cfg, env = process.env) {
669
+ if (env.NO_COLOR)
670
+ return false;
671
+ return cfg.colors !== false;
672
+ }
673
+ function paint(segment, text, ctx) {
674
+ const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
675
+ const code = color ? ANSI[color] : undefined;
676
+ return code ? `${code}${text}${RESET}` : text;
677
+ }
440
678
  async function renderLine(ctx, cfg) {
679
+ const colorize = colorsEnabled(cfg);
441
680
  const parts = [];
442
681
  for (const id of cfg.segments) {
443
682
  const segment = getSegment(id);
@@ -446,7 +685,7 @@ async function renderLine(ctx, cfg) {
446
685
  try {
447
686
  const out = await segment.render(ctx);
448
687
  if (out)
449
- parts.push(out);
688
+ parts.push(colorize ? paint(segment, out, ctx) : out);
450
689
  } catch {}
451
690
  }
452
691
  return parts.join(cfg.separator);
@@ -454,7 +693,7 @@ async function renderLine(ctx, cfg) {
454
693
  // package.json
455
694
  var package_default = {
456
695
  name: "@hasna/statusline",
457
- version: "0.0.2",
696
+ version: "0.0.3",
458
697
  description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
459
698
  license: "Apache-2.0",
460
699
  type: "module",
@@ -523,21 +762,43 @@ var package_default = {
523
762
  }
524
763
  };
525
764
  // src/install.ts
526
- import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
527
- import { homedir as homedir2 } from "os";
528
- import { dirname as dirname2, join as join2 } from "path";
529
- function installClaude(settingsPath = join2(homedir2(), ".claude", "settings.json")) {
765
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
766
+ import { homedir as homedir3 } from "os";
767
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
768
+ function defaultSettingsPath(env) {
769
+ return join3(env.HOME || homedir3(), ".claude", "settings.json");
770
+ }
771
+ function claudeSettingsPath(env = process.env) {
772
+ const home2 = env.HOME || homedir3();
773
+ const raw = env.CLAUDE_CONFIG_DIR?.trim();
774
+ if (!raw)
775
+ return defaultSettingsPath(env);
776
+ const configDir = raw === "~" || raw.startsWith("~/") ? join3(home2, raw.slice(1)) : raw;
777
+ return join3(configDir, "settings.json");
778
+ }
779
+ function installClaude(settingsPath, env = process.env) {
780
+ const target = settingsPath ?? claudeSettingsPath(env);
530
781
  const binary = Bun.which("statusline") || "statusline";
531
782
  let settings = {};
532
- if (existsSync3(settingsPath)) {
533
- settings = JSON.parse(readFileSync3(settingsPath, "utf8"));
534
- copyFileSync(settingsPath, `${settingsPath}.bak-statusline`);
783
+ if (existsSync4(target)) {
784
+ try {
785
+ settings = JSON.parse(readFileSync4(target, "utf8"));
786
+ } catch (error) {
787
+ throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
788
+ }
789
+ copyFileSync(target, `${target}.bak-statusline`);
790
+ }
791
+ settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
792
+ const configDir = dirname2(target);
793
+ if (!existsSync4(configDir)) {
794
+ if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
795
+ throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
796
+ }
797
+ mkdirSync2(configDir, { recursive: true });
535
798
  }
536
- settings.statusLine = { type: "command", command: `${binary} render` };
537
- mkdirSync2(dirname2(settingsPath), { recursive: true });
538
- writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + `
799
+ writeFileSync2(target, JSON.stringify(settings, null, 2) + `
539
800
  `);
540
- return settingsPath;
801
+ return target;
541
802
  }
542
803
 
543
804
  // src/index.ts
@@ -589,6 +850,9 @@ function setSeparator(separator, config = loadConfig()) {
589
850
  throw new Error("separator must be a non-empty string");
590
851
  return { config: { ...config, separator }, changed: ["separator"] };
591
852
  }
853
+ function setColors(colors, config = loadConfig()) {
854
+ return { config: { ...config, colors }, changed: ["colors"] };
855
+ }
592
856
  function resetStatuslineConfig() {
593
857
  return defaultConfig();
594
858
  }
@@ -601,7 +865,11 @@ function sampleClaudeInput(cwd = process.cwd()) {
601
865
  model: { id: "claude-fable-5[1m]", display_name: "Fable" },
602
866
  cost: { total_cost_usd: 12.34, total_duration_ms: 5400000, total_lines_added: 42, total_lines_removed: 7 },
603
867
  version: statuslineVersion,
604
- session_id: "preview0-0000-0000-0000-000000000000"
868
+ session_id: "preview0-0000-0000-0000-000000000000",
869
+ session_name: "preview thread",
870
+ effort: { level: "high" },
871
+ thinking: { enabled: true },
872
+ rate_limits: { five_hour: { used_percentage: 42 } }
605
873
  };
606
874
  }
607
875
  async function previewStatusline(config = loadConfig(), cwd = process.cwd()) {
@@ -615,6 +883,11 @@ export {
615
883
  trackedLineCount,
616
884
  statuslineVersion,
617
885
  setSeparator,
886
+ setColors,
887
+ sessionStateFile,
888
+ sessionConfigDir,
889
+ sessionAccountEmail,
890
+ sessionAccount,
618
891
  segments,
619
892
  saveUpdatedConfig,
620
893
  saveConfig,
@@ -642,5 +915,6 @@ export {
642
915
  configPath,
643
916
  compactNum,
644
917
  compactDuration,
645
- compactAge
918
+ compactAge,
919
+ claudeSettingsPath
646
920
  };
package/dist/install.d.ts CHANGED
@@ -1,5 +1,16 @@
1
+ /**
2
+ * Settings file the running agent actually reads. When a session is bound to
3
+ * an isolated config dir (multi-account setups export `CLAUDE_CONFIG_DIR`),
4
+ * `~/.claude/settings.json` is never loaded — installing there wires up a
5
+ * statusline that silently never runs.
6
+ */
7
+ export declare function claudeSettingsPath(env?: Record<string, string | undefined>): string;
1
8
  /**
2
9
  * Wire the statusline into Claude Code's user settings. Existing settings
3
10
  * are preserved; the previous file is backed up alongside it.
11
+ *
12
+ * `env` resolves the default target and decides whether a missing dir may be
13
+ * created; pass the same object you would pass to `claudeSettingsPath()` so a
14
+ * caller driving several profiles is not steered by its own shell.
4
15
  */
5
- export declare function installClaude(settingsPath?: string): string;
16
+ export declare function installClaude(settingsPath?: string, env?: Record<string, string | undefined>): string;