@alyibrahim/claude-statusline 1.5.0 → 1.5.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
@@ -7,6 +7,7 @@
7
7
 
8
8
  [![CI](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml/badge.svg)](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml)
9
9
  [![npm](https://img.shields.io/npm/v/@alyibrahim/claude-statusline)](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
10
+ [![Downloads](https://img.shields.io/npm/dt/@alyibrahim/claude-statusline)](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
12
 
12
13
  **A rich, fast statusline for [Claude Code](https://claude.ai/code)** — shows model, git branch, context usage, rate limits, session cost, and split input/output token counts after every response.
@@ -65,7 +66,7 @@ Done. The statusline configures itself automatically. Restart Claude Code to see
65
66
 
66
67
  Track token usage, cost, and duration across every Claude Code session. Choose between a **terminal-native TUI** or a **browser dashboard** — your preference is saved automatically.
67
68
 
68
- ![history dashboard](assets/dashboard-preview.png)
69
+ ![history dashboard](https://raw.githubusercontent.com/AlyIbrahim1/claude-statusline/main/.github/assets/dashboard-preview.png)
69
70
 
70
71
  Session history is **enabled by default** on setup. Each session records:
71
72
 
@@ -92,6 +93,18 @@ claude-statusline disable-history # Disable session tracking
92
93
 
93
94
  Data is stored at `~/.claude/statusline-history.jsonl`.
94
95
 
96
+ ### Claude Code slash commands
97
+
98
+ History commands are also available directly inside Claude Code as slash commands:
99
+
100
+ - `/history`
101
+ - `/history-enable`
102
+ - `/history-disable`
103
+ - `/history-mode <web|terminal>`
104
+
105
+ Project contributors get these from the repo at `.claude/commands/`.
106
+ Global npm installs copy them to `~/.claude/commands/` automatically.
107
+
95
108
  ### Terminal TUI
96
109
 
97
110
  `--mode terminal` opens an interactive full-screen dashboard directly in your terminal — no browser required. Requires the native Rust binary (falls back to web dashboard with a warning if unavailable).
@@ -194,6 +207,8 @@ npm uninstall -g @alyibrahim/claude-statusline
194
207
 
195
208
  > Always run `claude-statusline uninstall` first — it removes the `statusLine` entry from `~/.claude/settings.json` before the files are deleted.
196
209
 
210
+ `npm uninstall -g @alyibrahim/claude-statusline` also removes the four history slash command files installed by this package from `~/.claude/commands/`, without touching other custom commands.
211
+
197
212
  ---
198
213
 
199
214
  <div align="center">
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/statusline.js\" hook start"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionEnd": [
15
+ {
16
+ "matcher": "",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/statusline.js\" hook end"
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/scripts/plugin-autosetup.js\""
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alyibrahim/claude-statusline",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Rich statusline for Claude Code — model, context bar, real-time token tracking, git branch, rate limits, and session stats. Rust binary, ~5ms startup.",
5
5
  "keywords": [
6
6
  "claude",
@@ -49,6 +49,7 @@
49
49
  "statusline.js",
50
50
  "bin/",
51
51
  "scripts/",
52
+ "hooks/",
52
53
  "README.md"
53
54
  ],
54
55
  "license": "MIT",
@@ -56,11 +57,11 @@
56
57
  "open": "^10.1.0"
57
58
  },
58
59
  "optionalDependencies": {
59
- "@alyibrahim/claude-statusline-linux-x64": "1.5.0",
60
- "@alyibrahim/claude-statusline-linux-arm64": "1.5.0",
61
- "@alyibrahim/claude-statusline-darwin-x64": "1.5.0",
62
- "@alyibrahim/claude-statusline-darwin-arm64": "1.5.0",
63
- "@alyibrahim/claude-statusline-win32-x64": "1.5.0"
60
+ "@alyibrahim/claude-statusline-linux-x64": "1.5.2",
61
+ "@alyibrahim/claude-statusline-linux-arm64": "1.5.2",
62
+ "@alyibrahim/claude-statusline-darwin-x64": "1.5.2",
63
+ "@alyibrahim/claude-statusline-darwin-arm64": "1.5.2",
64
+ "@alyibrahim/claude-statusline-win32-x64": "1.5.2"
64
65
  },
65
66
  "devDependencies": {
66
67
  "jest": "^29.0.0"
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { atomicWrite } = require('./config');
6
+
7
+ // Only meaningful in plugin context — exit silently otherwise
8
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
9
+ if (!pluginRoot) process.exit(0);
10
+
11
+ const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
12
+ const settingsPath = path.join(configDir, 'settings.json');
13
+
14
+ let settings = {};
15
+ try {
16
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
17
+ } catch (e) {}
18
+
19
+ // Already configured — nothing to do
20
+ if (settings.statusLine) process.exit(0);
21
+
22
+ const script = path.join(pluginRoot, 'statusline.js');
23
+ if (!fs.existsSync(script)) process.exit(0);
24
+
25
+ settings.statusLine = {
26
+ type: 'command',
27
+ command: `"${process.execPath}" "${script}"`,
28
+ };
29
+
30
+ try {
31
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
32
+ atomicWrite(settingsPath, settings);
33
+ } catch (e) {
34
+ process.exit(0); // Non-fatal — user can run /claude-statusline:setup manually
35
+ }
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const fs = require('fs');
4
+ const path = require('path');
4
5
  const { setup } = require('./setup');
5
6
  const config = require('./config');
6
7
  try {
7
8
  const result = setup({ force: false });
8
9
  if (result.settingsPath === null) process.exit(0); // non-global install, skip silently
10
+
11
+ const sourceDir = path.join(__dirname, '..', '.claude', 'commands');
12
+ const commandsDir = path.join(path.dirname(result.settingsPath), 'commands');
13
+ fs.mkdirSync(commandsDir, { recursive: true });
14
+ for (const f of fs.readdirSync(sourceDir)) {
15
+ fs.copyFileSync(path.join(sourceDir, f), path.join(commandsDir, f));
16
+ }
17
+
9
18
  if (!result.ok) {
10
19
  console.warn('\n⚠ claude-statusline: auto-setup failed:', result.error);
11
20
  console.warn(' Run manually: claude-statusline setup\n');
@@ -1,5 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getSettingsPath } = require('./config');
3
6
  const { uninstall } = require('./uninstall');
4
- try { uninstall(); } catch (e) {} // fully silent — best-effort cleanup
7
+
8
+ const FILES = ['history.md', 'history-enable.md', 'history-disable.md', 'history-mode.md'];
9
+
10
+ try {
11
+ const commandsDir = path.join(path.dirname(getSettingsPath()), 'commands');
12
+ for (const f of FILES) {
13
+ const dest = path.join(commandsDir, f);
14
+ if (fs.existsSync(dest)) fs.unlinkSync(dest);
15
+ }
16
+ uninstall();
17
+ } catch (e) {} // fully silent — best-effort cleanup
5
18
  process.exit(0);
package/scripts/setup.js CHANGED
@@ -6,6 +6,35 @@ const { getSettingsPath, atomicWrite } = config;
6
6
 
7
7
  const UNSAFE_CHARS = /["`$!()\\]/;
8
8
 
9
+ function buildNodeExecCommand() {
10
+ return `"${process.execPath}"`;
11
+ }
12
+
13
+ function resolveHooksFromFile(filePath, replacements) {
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
17
+ } catch (err) {
18
+ const wrapped = new Error(`Hook configuration error in ${path.basename(filePath)}: ${err.message}`);
19
+ wrapped.code = 'HOOK_CONFIG_ERROR';
20
+ throw wrapped;
21
+ }
22
+
23
+ const hooks = parsed && parsed.hooks;
24
+ if (!hooks || typeof hooks !== 'object') {
25
+ const wrapped = new Error(`Hook configuration error in ${path.basename(filePath)}: missing hooks object`);
26
+ wrapped.code = 'HOOK_CONFIG_ERROR';
27
+ throw wrapped;
28
+ }
29
+
30
+ let serialized = JSON.stringify(hooks);
31
+ for (const [token, value] of Object.entries(replacements)) {
32
+ serialized = serialized.replace(new RegExp(`\\$\\{${token}\\}`, 'g'), value);
33
+ }
34
+
35
+ return JSON.parse(serialized);
36
+ }
37
+
9
38
  function setup({ force = false } = {}) {
10
39
  // CI guard: skip during local/CI npm installs unless forced (e.g. from CLI)
11
40
  if (!force && process.env.npm_config_global !== 'true') {
@@ -38,7 +67,11 @@ function setup({ force = false } = {}) {
38
67
  : `"${process.execPath}" "${scriptPath}"`;
39
68
  settings.statusLine = { type: 'command', command };
40
69
 
41
- updateHooks(settings, command, true);
70
+ try {
71
+ updateHooks(settings, true, { nodeExecCommand: buildNodeExecCommand() });
72
+ } catch (err) {
73
+ return { ok: false, error: err.message };
74
+ }
42
75
 
43
76
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
44
77
 
@@ -51,31 +84,74 @@ function setup({ force = false } = {}) {
51
84
  return { ok: true, settingsPath };
52
85
  }
53
86
 
54
- function updateHooks(settings, command, enable) {
87
+ function updateHooks(settings, enable, { nodeExecCommand = buildNodeExecCommand() } = {}) {
55
88
  if (!settings.hooks) settings.hooks = {};
56
-
57
- const startCmd = `${command} hook start`;
58
- const endCmd = `${command} hook end`;
59
-
60
- function toggleHook(hookName, cmdString) {
61
- if (!settings.hooks[hookName]) settings.hooks[hookName] = [];
62
- // Remove any existing statusline hook entries (both old and new format)
63
- settings.hooks[hookName] = settings.hooks[hookName].filter(h => {
64
- if (h.hooks) {
65
- return !h.hooks.some(inner => inner.command && (inner.command.endsWith(' hook start') || inner.command.endsWith(' hook end')));
66
- }
67
- return !(h.command && (h.command.endsWith(' hook start') || h.command.endsWith(' hook end')));
89
+
90
+ const packageRoot = path.resolve(__dirname, '..');
91
+ const escapedRoot = packageRoot.replace(/\\/g, '\\\\');
92
+ const escapedNodeExec = nodeExecCommand
93
+ .replace(/\\/g, '\\\\')
94
+ .replace(/"/g, '\\"');
95
+
96
+ // When installing, only add history hooks from hooks.json.
97
+ // When removing, read all our hooks files so every hook we may have written gets cleaned up.
98
+ const installFile = path.join(packageRoot, 'hooks', 'hooks.json');
99
+ const allFiles = [
100
+ installFile,
101
+ path.join(packageRoot, 'hooks', 'plugin-setup.json'),
102
+ ].filter(f => fs.existsSync(f));
103
+ const filesToProcess = enable ? [installFile] : allFiles;
104
+
105
+ // Collect every resolved command across all our hooks files for exact-match removal.
106
+ const ourCommands = new Set();
107
+ for (const f of allFiles) {
108
+ const resolved = resolveHooksFromFile(f, {
109
+ CLAUDE_PLUGIN_ROOT: escapedRoot,
110
+ CLAUDE_NODE_EXEC: escapedNodeExec,
68
111
  });
69
- if (enable) {
70
- settings.hooks[hookName].push({ matcher: '', hooks: [{ type: 'command', command: cmdString }] });
71
- }
72
- if (settings.hooks[hookName].length === 0) {
73
- delete settings.hooks[hookName];
112
+ for (const entries of Object.values(resolved)) {
113
+ for (const entry of entries) {
114
+ for (const hook of (entry.hooks || [])) {
115
+ if (hook.command) ourCommands.add(hook.command);
116
+ }
117
+ }
74
118
  }
75
119
  }
76
120
 
77
- toggleHook('SessionStart', startCmd);
78
- toggleHook('SessionEnd', endCmd);
121
+ for (const f of filesToProcess) {
122
+ const resolvedHooks = resolveHooksFromFile(f, {
123
+ CLAUDE_PLUGIN_ROOT: escapedRoot,
124
+ CLAUDE_NODE_EXEC: escapedNodeExec,
125
+ });
126
+
127
+ for (const [event, entries] of Object.entries(resolvedHooks)) {
128
+ if (!settings.hooks[event]) settings.hooks[event] = [];
129
+ settings.hooks[event] = settings.hooks[event].filter(h => {
130
+ const isLegacyAutosetup = cmd => {
131
+ if (!cmd) return false;
132
+ const hasScript = /scripts[\\/]+plugin-autosetup\.js/.test(cmd);
133
+ const hasOwnedMarker = /claude-statusline|CLAUDE_PLUGIN_ROOT/i.test(cmd);
134
+ return hasScript && hasOwnedMarker;
135
+ };
136
+ const isOurs = inner => inner.command && (
137
+ // Suffix match — catches hooks written by older package versions
138
+ inner.command.endsWith(' hook start') || inner.command.endsWith(' hook end') ||
139
+ // Exact match — catches current hooks including plugin-setup entries
140
+ ourCommands.has(inner.command) ||
141
+ // Legacy autosetup fallback — catches prior install roots
142
+ isLegacyAutosetup(inner.command)
143
+ );
144
+ if (h.hooks) return !h.hooks.some(isOurs);
145
+ return !isOurs(h);
146
+ });
147
+ if (enable) {
148
+ settings.hooks[event].push(...entries);
149
+ }
150
+ if (settings.hooks[event].length === 0) {
151
+ delete settings.hooks[event];
152
+ }
153
+ }
154
+ }
79
155
 
80
156
  if (Object.keys(settings.hooks).length === 0) {
81
157
  delete settings.hooks;
@@ -83,11 +159,6 @@ function updateHooks(settings, command, enable) {
83
159
  }
84
160
 
85
161
  function toggleHistory(enable) {
86
- const scriptPath = path.resolve(__dirname, '../statusline.js');
87
- const binaryPath = config.resolveBinary();
88
- const safeBinary = binaryPath && !UNSAFE_CHARS.test(binaryPath) ? binaryPath : null;
89
- const command = safeBinary ? `"${safeBinary}"` : `"${process.execPath}" "${scriptPath}"`;
90
-
91
162
  const settingsPath = getSettingsPath();
92
163
  let settings = {};
93
164
  if (fs.existsSync(settingsPath)) {
@@ -98,7 +169,11 @@ function toggleHistory(enable) {
98
169
  }
99
170
  }
100
171
 
101
- updateHooks(settings, command, enable);
172
+ try {
173
+ updateHooks(settings, enable, { nodeExecCommand: buildNodeExecCommand() });
174
+ } catch (err) {
175
+ return { ok: false, error: err.message };
176
+ }
102
177
 
103
178
  try {
104
179
  atomicWrite(settingsPath, settings);
@@ -19,7 +19,11 @@ function uninstall() {
19
19
 
20
20
  // Also strip our hooks if they exist
21
21
  const { updateHooks } = require('./setup');
22
- updateHooks(settings, '', false);
22
+ try {
23
+ updateHooks(settings, false);
24
+ } catch (err) {
25
+ return { ok: false, error: err.message };
26
+ }
23
27
 
24
28
  try {
25
29
  atomicWrite(settingsPath, settings);