@nutthead/cc-statusline 0.2.1 → 0.2.2

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
@@ -1,4 +1,17 @@
1
- # Claude Code Status Line
1
+ # statusline
2
+
3
+ Custom status line for Claude Code.
4
+
5
+ ## Preview
6
+
7
+ The default theme renders a two-row status line:
8
+
9
+ ```
10
+ 🗂️ ~/C/b/statusline ⋮ 🌿 master
11
+ ⏣ opus-4-6 ⋮ 📝 fc9bbbee-...
12
+ ```
13
+
14
+ Row 1 shows the abbreviated working directory and git branch. Row 2 shows the model and session ID.
2
15
 
3
16
  ## Install
4
17
 
@@ -6,11 +19,7 @@
6
19
  bunx @nutthead/cc-statusline install
7
20
  ```
8
21
 
9
- Use `--overwrite` to replace an existing installation.
10
-
11
- ## Configure
12
-
13
- Add to `~/.claude/settings.json`:
22
+ Then add to `~/.claude/settings.json`:
14
23
 
15
24
  ```json
16
25
  {
@@ -21,45 +30,55 @@ Add to `~/.claude/settings.json`:
21
30
  }
22
31
  ```
23
32
 
24
- ### Custom Theme
33
+ Use `--overwrite` to replace an existing installation.
34
+
35
+ ## Custom Themes
25
36
 
26
- 1. Create a directory for the custom theme:
37
+ Create a JS file that default-exports a theme function (e.g. `~/.config/cc-statusline/theme.js`):
27
38
 
28
- ```bash
29
- mkdir ~/.config/cc-statusline
30
- ```
39
+ ```js
40
+ export default function theme(input) {
41
+ if (!input) return "";
31
42
 
32
- 2. Write a custom theme (e.g. in `~/.config/cc-statusline/theme.js`)
43
+ const status = JSON.parse(input);
44
+ const dir = status.workspace.current_dir;
45
+ const model = status.model.display_name;
46
+ const ctx = status.context_window.used_percentage ?? 0;
33
47
 
34
- ```js
35
- export default function theme(input) {
36
- if (input) {
37
- // parse input
38
- const json = JSON.parse(input);
48
+ return `${model} | ${dir} | ctx: ${Math.round(ctx)}%`;
49
+ }
50
+ ```
51
+
52
+ Then point to it in `~/.claude/settings.json`:
53
+
54
+ ```json
55
+ {
56
+ "statusLine": {
57
+ "type": "command",
58
+ "command": "~/.claude/statusline --theme ~/.config/cc-statusline/theme.js"
59
+ }
60
+ }
61
+ ```
39
62
 
40
- // construct status line
41
- const statusLine = "...";
63
+ ## Available Fields
42
64
 
43
- // return status line
44
- return statusLine;
45
- } else {
46
- return "";
47
- }
48
- }
49
- ```
65
+ The JSON object passed to your theme function contains these fields:
50
66
 
51
- 3. Configure Claude Code
67
+ | Field | Example |
68
+ | -------------------------------- | ------------------------ |
69
+ | `session_id` | `"f9abcdef-1a2b-..."` |
70
+ | `version` | `"2.1.39"` |
71
+ | `model.id` | `"claude-opus-4-6"` |
72
+ | `model.display_name` | `"Claude Opus 4.6"` |
73
+ | `workspace.current_dir` | `"/home/user/project"` |
74
+ | `workspace.project_dir` | `"/home/user/project"` |
75
+ | `context_window.used_percentage` | `42.5` |
76
+ | `context_window.vim.mode` | `"INSERT"` or `"NORMAL"` |
77
+ | `context_window.agent.name` | `"claude-code"` |
52
78
 
53
- ```json
54
- {
55
- "statusLine": {
56
- "type": "command",
57
- "command": "~/.claude/statusline --theme ~/.config/cc-statusline/theme.js"
58
- }
59
- }
60
- ```
79
+ See [`src/statusLineSchema.ts`](src/statusLineSchema.ts) for the full schema.
61
80
 
62
- ## Logs
81
+ ## Troubleshooting
63
82
 
64
83
  Execution logs are stored in `~/.local/state/statusline/app.log`.
65
84
 
@@ -9408,8 +9408,8 @@ var meow = (helpText, options = {}) => {
9408
9408
  };
9409
9409
 
9410
9410
  // src/cli.ts
9411
- import { execSync } from "node:child_process";
9412
- import { existsSync, mkdirSync, copyFileSync, unlinkSync } from "node:fs";
9411
+ import { spawnSync } from "node:child_process";
9412
+ import { access, mkdir, copyFile, unlink } from "node:fs/promises";
9413
9413
  import { homedir } from "node:os";
9414
9414
  import { join } from "node:path";
9415
9415
  var BINARY_NAME = "statusline";
@@ -9437,53 +9437,78 @@ var cli = meow(`
9437
9437
  }
9438
9438
  }
9439
9439
  });
9440
- function build() {
9440
+ async function fileExists(path3) {
9441
+ try {
9442
+ await access(path3);
9443
+ return true;
9444
+ } catch {
9445
+ return false;
9446
+ }
9447
+ }
9448
+ var defaultFs = {
9449
+ exists: fileExists,
9450
+ mkdir: async (path3, options) => {
9451
+ await mkdir(path3, options);
9452
+ },
9453
+ copy: async (src, dest) => {
9454
+ await copyFile(src, dest);
9455
+ },
9456
+ remove: async (path3) => {
9457
+ await unlink(path3);
9458
+ }
9459
+ };
9460
+ async function build() {
9441
9461
  console.log("Building statusline binary...");
9442
- execSync("mkdir -p target && bun build --compile ./index.ts --outfile target/statusline", { stdio: "inherit" });
9462
+ await mkdir("target", { recursive: true });
9463
+ const result = spawnSync("bun", ["build", "--compile", "./index.ts", "--outfile", "target/statusline"], { stdio: "inherit" });
9464
+ if (result.error) {
9465
+ throw result.error;
9466
+ }
9467
+ if (result.status !== 0) {
9468
+ throw new Error(`Build failed with exit code ${result.status}`);
9469
+ }
9443
9470
  console.log("Build complete.");
9444
9471
  }
9445
- var defaultDeps = {
9446
- claudeDir: CLAUDE_DIR,
9447
- targetPath: TARGET_PATH,
9448
- sourcePath: join(process.cwd(), "target", BINARY_NAME),
9449
- doBuild: build,
9450
- existsSync,
9451
- mkdirSync,
9452
- copyFileSync,
9453
- unlinkSync
9454
- };
9455
- function installBinary(overwrite, deps = defaultDeps) {
9456
- deps.doBuild();
9457
- if (!deps.existsSync(deps.claudeDir)) {
9458
- deps.mkdirSync(deps.claudeDir, { recursive: true });
9459
- }
9460
- if (deps.existsSync(deps.targetPath)) {
9461
- if (!overwrite) {
9462
- console.error(`Error: ${deps.targetPath} already exists.`);
9463
- console.error("Use --overwrite to replace the existing file.");
9464
- process.exit(1);
9465
- }
9466
- console.log(`Overwriting existing file at ${deps.targetPath}...`);
9467
- deps.unlinkSync(deps.targetPath);
9468
- }
9469
- deps.copyFileSync(deps.sourcePath, deps.targetPath);
9470
- console.log(`Installed statusline to ${deps.targetPath}`);
9472
+ async function installBinary(options, deps) {
9473
+ await deps.build();
9474
+ if (!await deps.fs.exists(options.claudeDir)) {
9475
+ await deps.fs.mkdir(options.claudeDir, { recursive: true });
9476
+ }
9477
+ if (await deps.fs.exists(options.targetPath)) {
9478
+ if (!options.overwrite) {
9479
+ throw new Error(`${options.targetPath} already exists. Use --overwrite to replace the existing file.`);
9480
+ }
9481
+ console.log(`Overwriting existing file at ${options.targetPath}...`);
9482
+ await deps.fs.remove(options.targetPath);
9483
+ }
9484
+ await deps.fs.copy(options.sourcePath, options.targetPath);
9485
+ console.log(`Installed statusline to ${options.targetPath}`);
9471
9486
  }
9472
- function install(overwrite) {
9473
- installBinary(overwrite);
9487
+ async function install(overwrite) {
9488
+ await installBinary({
9489
+ overwrite,
9490
+ claudeDir: CLAUDE_DIR,
9491
+ targetPath: TARGET_PATH,
9492
+ sourcePath: join(process.cwd(), "target", BINARY_NAME)
9493
+ }, { fs: defaultFs, build });
9474
9494
  }
9475
- function main() {
9476
- const command = cli.input[0];
9477
- switch (command) {
9478
- case "install":
9479
- install(cli.flags.overwrite);
9480
- break;
9481
- case undefined:
9482
- cli.showHelp();
9483
- break;
9484
- default:
9485
- console.error(`Unknown command: ${command}`);
9486
- cli.showHelp(1);
9495
+ async function main() {
9496
+ try {
9497
+ const command = cli.input[0];
9498
+ switch (command) {
9499
+ case "install":
9500
+ await install(cli.flags.overwrite);
9501
+ break;
9502
+ case undefined:
9503
+ cli.showHelp();
9504
+ break;
9505
+ default:
9506
+ console.error(`Unknown command: ${command}`);
9507
+ cli.showHelp(1);
9508
+ }
9509
+ } catch (error) {
9510
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
9511
+ process.exit(1);
9487
9512
  }
9488
9513
  }
9489
9514
  if (__require.main == __require.module) {
package/bunfig.toml ADDED
@@ -0,0 +1,2 @@
1
+ [test]
2
+ preload = ["./test/setup.ts"]
package/index.ts CHANGED
@@ -1,23 +1,21 @@
1
1
  import meow from "meow";
2
2
  import { configure } from "@logtape/logtape";
3
3
  import { log, logtapeConfig } from "./src/logging";
4
- import { defaultTheme } from "./src/defaultTheme";
4
+ import { defaultTheme } from "./src/themes/defaultTheme";
5
5
  import { loadTheme } from "./src/theme/loadTheme";
6
6
 
7
7
  await configure(logtapeConfig);
8
8
 
9
9
  const cli = meow(
10
10
  `
11
- Usage
12
- $ cc-statusline
11
+ Usage
12
+ $ cc-statusline
13
13
 
14
- Options
15
- --theme, -t Use a custom theme
14
+ Options
15
+ --theme, -t Use a custom theme
16
16
 
17
- Examples
18
- $ cc-statusline --rainbow
17
+ Examples
19
18
  $ cc-statusline --theme ~/.config/cc-statusline/basic.js
20
-
21
19
  `,
22
20
  {
23
21
  importMeta: import.meta, // This is required
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nutthead/cc-statusline",
3
3
  "description": "Status Line for Claude Code",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cc-statusline": "bin/cc-statusline.js"
package/src/cli.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import meow from "meow";
4
- import { execSync } from "node:child_process";
5
- import { existsSync, mkdirSync, copyFileSync, unlinkSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import { access, mkdir, copyFile, unlink } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
 
@@ -36,81 +36,130 @@ const cli = meow(
36
36
  },
37
37
  );
38
38
 
39
- export function build(): void {
40
- console.log("Building statusline binary...");
41
- execSync(
42
- "mkdir -p target && bun build --compile ./index.ts --outfile target/statusline",
43
- { stdio: "inherit" },
44
- );
45
- console.log("Build complete.");
39
+ interface FileSystem {
40
+ exists(path: string): Promise<boolean>;
41
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
42
+ copy(src: string, dest: string): Promise<void>;
43
+ remove(path: string): Promise<void>;
46
44
  }
47
45
 
48
- export interface InstallDeps {
46
+ interface InstallOptions {
47
+ overwrite: boolean;
49
48
  claudeDir: string;
50
49
  targetPath: string;
51
50
  sourcePath: string;
52
- doBuild: () => void;
53
- existsSync: (path: string) => boolean;
54
- mkdirSync: (path: string, options?: { recursive?: boolean }) => void;
55
- copyFileSync: (src: string, dest: string) => void;
56
- unlinkSync: (path: string) => void;
57
51
  }
58
52
 
59
- const defaultDeps: InstallDeps = {
60
- claudeDir: CLAUDE_DIR,
61
- targetPath: TARGET_PATH,
62
- sourcePath: join(process.cwd(), "target", BINARY_NAME),
63
- doBuild: build,
64
- existsSync,
65
- mkdirSync,
66
- copyFileSync,
67
- unlinkSync,
53
+ interface InstallDeps {
54
+ fs: FileSystem;
55
+ build: () => Promise<void>;
56
+ }
57
+
58
+ async function fileExists(path: string): Promise<boolean> {
59
+ try {
60
+ await access(path);
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ const defaultFs: FileSystem = {
68
+ exists: fileExists,
69
+ mkdir: async (path, options) => {
70
+ await mkdir(path, options);
71
+ },
72
+ copy: async (src, dest) => {
73
+ await copyFile(src, dest);
74
+ },
75
+ remove: async (path) => {
76
+ await unlink(path);
77
+ },
68
78
  };
69
79
 
70
- export function installBinary(
71
- overwrite: boolean,
72
- deps: InstallDeps = defaultDeps,
73
- ): void {
74
- deps.doBuild();
80
+ async function build(): Promise<void> {
81
+ console.log("Building statusline binary...");
82
+ await mkdir("target", { recursive: true });
83
+ const result = spawnSync(
84
+ "bun",
85
+ ["build", "--compile", "./index.ts", "--outfile", "target/statusline"],
86
+ { stdio: "inherit" },
87
+ );
88
+ if (result.error) {
89
+ throw result.error;
90
+ }
91
+ if (result.status !== 0) {
92
+ throw new Error(`Build failed with exit code ${result.status}`);
93
+ }
94
+ console.log("Build complete.");
95
+ }
75
96
 
76
- if (!deps.existsSync(deps.claudeDir)) {
77
- deps.mkdirSync(deps.claudeDir, { recursive: true });
97
+ async function installBinary(
98
+ options: InstallOptions,
99
+ deps: InstallDeps,
100
+ ): Promise<void> {
101
+ await deps.build();
102
+
103
+ if (!(await deps.fs.exists(options.claudeDir))) {
104
+ await deps.fs.mkdir(options.claudeDir, { recursive: true });
78
105
  }
79
106
 
80
- if (deps.existsSync(deps.targetPath)) {
81
- if (!overwrite) {
82
- console.error(`Error: ${deps.targetPath} already exists.`);
83
- console.error("Use --overwrite to replace the existing file.");
84
- process.exit(1);
107
+ if (await deps.fs.exists(options.targetPath)) {
108
+ if (!options.overwrite) {
109
+ throw new Error(
110
+ `${options.targetPath} already exists. Use --overwrite to replace the existing file.`,
111
+ );
85
112
  }
86
- console.log(`Overwriting existing file at ${deps.targetPath}...`);
87
- deps.unlinkSync(deps.targetPath);
113
+ console.log(`Overwriting existing file at ${options.targetPath}...`);
114
+ await deps.fs.remove(options.targetPath);
88
115
  }
89
116
 
90
- deps.copyFileSync(deps.sourcePath, deps.targetPath);
91
- console.log(`Installed statusline to ${deps.targetPath}`);
117
+ await deps.fs.copy(options.sourcePath, options.targetPath);
118
+ console.log(`Installed statusline to ${options.targetPath}`);
92
119
  }
93
120
 
94
- export function install(overwrite: boolean): void {
95
- installBinary(overwrite);
121
+ async function install(overwrite: boolean): Promise<void> {
122
+ await installBinary(
123
+ {
124
+ overwrite,
125
+ claudeDir: CLAUDE_DIR,
126
+ targetPath: TARGET_PATH,
127
+ sourcePath: join(process.cwd(), "target", BINARY_NAME),
128
+ },
129
+ { fs: defaultFs, build },
130
+ );
96
131
  }
97
132
 
98
- function main(): void {
99
- const command = cli.input[0];
100
-
101
- switch (command) {
102
- case "install":
103
- install(cli.flags.overwrite);
104
- break;
105
- case undefined:
106
- cli.showHelp();
107
- break;
108
- default:
109
- console.error(`Unknown command: ${command}`);
110
- cli.showHelp(1);
133
+ async function main(): Promise<void> {
134
+ try {
135
+ const command = cli.input[0];
136
+
137
+ switch (command) {
138
+ case "install":
139
+ await install(cli.flags.overwrite);
140
+ break;
141
+ case undefined:
142
+ cli.showHelp();
143
+ break;
144
+ default:
145
+ console.error(`Unknown command: ${command}`);
146
+ cli.showHelp(1);
147
+ }
148
+ } catch (error) {
149
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
150
+ process.exit(1);
111
151
  }
112
152
  }
113
153
 
114
154
  if (import.meta.main) {
115
155
  main();
116
156
  }
157
+
158
+ export {
159
+ build,
160
+ install,
161
+ installBinary,
162
+ type FileSystem,
163
+ type InstallDeps,
164
+ type InstallOptions,
165
+ };
package/src/logging.ts CHANGED
@@ -4,7 +4,9 @@ import { homedir } from "node:os";
4
4
 
5
5
  const logtapeConfig: Config<"file", string> = {
6
6
  sinks: {
7
- file: getFileSink(`${homedir()}/.local/state/statusline/app.log`),
7
+ file: getFileSink(`${homedir()}/.local/state/statusline/app.log`, {
8
+ lazy: true,
9
+ }),
8
10
  },
9
11
  loggers: [
10
12
  {
@@ -1,7 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const statusSchema = z.object({
4
- hook_event_name: z.string().optional(),
3
+ const statusSchema = z.object({
5
4
  session_id: z.string(),
6
5
  transcript_path: z.string(),
7
6
  cwd: z.string(),
@@ -14,26 +13,33 @@ export const statusSchema = z.object({
14
13
  project_dir: z.string(),
15
14
  }),
16
15
  version: z.string(),
17
- output_style: z
18
- .object({
16
+ output_style: z.object({
17
+ name: z.string(),
18
+ }),
19
+ context_window: z.object({
20
+ total_input_tokens: z.number(),
21
+ total_output_tokens: z.number(),
22
+ context_window_size: z.number(),
23
+ current_usage: z
24
+ .object({
25
+ input_tokens: z.number(),
26
+ output_tokens: z.number(),
27
+ cache_creation_input_tokens: z.number(),
28
+ cache_read_input_tokens: z.number(),
29
+ })
30
+ .nullable(),
31
+ used_percentage: z.number().nullable(),
32
+ remaining_percentage: z.number().nullable(),
33
+ vim: z.object({
34
+ mode: z.enum(["INSERT", "NORMAL"]),
35
+ }),
36
+ agent: z.object({
19
37
  name: z.string(),
20
- })
21
- .optional(),
22
- cost: z.object({
23
- total_cost_usd: z.number(),
24
- total_duration_ms: z.number(),
25
- total_api_duration_ms: z.number(),
26
- total_lines_added: z.number(),
27
- total_lines_removed: z.number(),
38
+ type: z.string(),
39
+ }),
28
40
  }),
29
- context_window: z
30
- .object({
31
- input_tokens: z.number().optional(),
32
- output_tokens: z.number().optional(),
33
- cache_creation_input_tokens: z.number().optional(),
34
- cache_read_input_tokens: z.number().optional(),
35
- })
36
- .nullable(),
37
41
  });
38
42
 
39
- export type Status = z.infer<typeof statusSchema>;
43
+ type Status = z.infer<typeof statusSchema>;
44
+
45
+ export { statusSchema, type Status };
@@ -1,12 +1,12 @@
1
1
  import { z } from "zod";
2
- import { log } from "./logging";
3
- import { statusSchema } from "./statusLineSchema";
2
+ import { log } from "../logging";
3
+ import { statusSchema } from "../statusLineSchema";
4
4
  import {
5
5
  currentDirStatus,
6
6
  currentGitStatus,
7
7
  currentModelStatus,
8
8
  currentSessionId,
9
- } from "./utils";
9
+ } from "../utils";
10
10
 
11
11
  import c from "ansi-colors";
12
12
 
@@ -18,15 +18,15 @@ async function defaultTheme(input?: string): Promise<string> {
18
18
 
19
19
  if (result.success) {
20
20
  const status = result.data;
21
- const dirStatus = c.blue(currentDirStatus(status));
22
- const gitStatus = c.green(await currentGitStatus());
23
- const modelStatus = c.magenta(currentModelStatus(status));
21
+ const currentDir = c.blue(currentDirStatus(status));
22
+ const git = c.green(await currentGitStatus());
23
+ const model = c.magenta(currentModelStatus(status));
24
24
  const sessionId = c.blue(currentSessionId(status));
25
25
  const separator = c.bold.gray(" ⋮ ");
26
26
 
27
27
  statusLine = [
28
- [dirStatus, gitStatus],
29
- [modelStatus, sessionId],
28
+ [currentDir, git],
29
+ [model, sessionId],
30
30
  ]
31
31
  .map((row) => row.join(separator))
32
32
  .join("\n");