@alyibrahim/claude-statusline 1.5.1 → 1.5.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 +2 -1
- package/bin/cli.js +11 -0
- package/hooks/hooks.json +26 -0
- package/hooks/plugin-setup.json +15 -0
- package/package.json +7 -6
- package/scripts/download-binary.js +60 -0
- package/scripts/plugin-autosetup.js +35 -0
- package/scripts/setup.js +102 -27
- package/scripts/uninstall.js +5 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
[](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml)
|
|
9
9
|
[](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
|
|
10
|
+
[](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
|
|
10
11
|
[](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
|
-

|
|
69
|
+

|
|
69
70
|
|
|
70
71
|
Session history is **enabled by default** on setup. Each session records:
|
|
71
72
|
|
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ claude-statusline <command>
|
|
|
13
13
|
Commands:
|
|
14
14
|
setup Configure ~/.claude/settings.json to use this statusline
|
|
15
15
|
uninstall Remove this statusline from ~/.claude/settings.json
|
|
16
|
+
download-binary Download the native binary for this platform
|
|
16
17
|
enable-history Enable tracking session analytics to JSONL (default on setup)
|
|
17
18
|
disable-history Remove history tracking hooks from Claude settings
|
|
18
19
|
history Open the session analytics dashboard
|
|
@@ -117,6 +118,16 @@ if (cmd === 'setup') {
|
|
|
117
118
|
}
|
|
118
119
|
console.log(`✓ Removed statusline from ${getSettingsPath()}`);
|
|
119
120
|
|
|
121
|
+
} else if (cmd === 'download-binary') {
|
|
122
|
+
const { downloadBinary } = require('../scripts/download-binary');
|
|
123
|
+
const result = downloadBinary();
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
console.error('Error:', result.error);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
console.log(`\n✓ Binary installed at ${result.binaryPath}`);
|
|
129
|
+
console.log(' Run claude-statusline setup to update your settings to use it.\n');
|
|
130
|
+
|
|
120
131
|
} else if (cmd === 'enable-history') {
|
|
121
132
|
const result = toggleHistory(true);
|
|
122
133
|
if (!result.ok) {
|
package/hooks/hooks.json
ADDED
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alyibrahim/claude-statusline",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
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.
|
|
60
|
-
"@alyibrahim/claude-statusline-linux-arm64": "1.5.
|
|
61
|
-
"@alyibrahim/claude-statusline-darwin-x64": "1.5.
|
|
62
|
-
"@alyibrahim/claude-statusline-darwin-arm64": "1.5.
|
|
63
|
-
"@alyibrahim/claude-statusline-win32-x64": "1.5.
|
|
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,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const SUPPORTED = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'win32-x64'];
|
|
7
|
+
|
|
8
|
+
function downloadBinary() {
|
|
9
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
10
|
+
if (!SUPPORTED.includes(platformKey)) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: `No pre-built binary for ${platformKey}. The JS fallback will be used.`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { version } = require('../package.json');
|
|
18
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
19
|
+
const packageSpec = `@alyibrahim/claude-statusline-${platformKey}@${version}`;
|
|
20
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
21
|
+
|
|
22
|
+
const installResult = spawnSync(npmCmd, ['install', '--no-save', packageSpec], {
|
|
23
|
+
cwd: packageRoot,
|
|
24
|
+
stdio: 'inherit'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (installResult.status !== 0) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: `npm install exited with code ${installResult.status}`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Clear cached modules so resolveBinary sees newly installed optional dependency.
|
|
35
|
+
const platformPkg = `claude-statusline-${platformKey}`;
|
|
36
|
+
Object.keys(require.cache).forEach((cacheKey) => {
|
|
37
|
+
if (cacheKey.includes(platformPkg)) {
|
|
38
|
+
delete require.cache[cacheKey];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const { resolveBinary } = require('./config');
|
|
43
|
+
const binaryPath = resolveBinary();
|
|
44
|
+
if (!binaryPath) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: 'Package installed but binary not found - unexpected layout'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.platform !== 'win32') {
|
|
52
|
+
try {
|
|
53
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
54
|
+
} catch (e) {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { ok: true, binaryPath };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { downloadBinary };
|
|
@@ -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
|
+
}
|
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
|
-
|
|
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,
|
|
87
|
+
function updateHooks(settings, enable, { nodeExecCommand = buildNodeExecCommand() } = {}) {
|
|
55
88
|
if (!settings.hooks) settings.hooks = {};
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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);
|
package/scripts/uninstall.js
CHANGED
|
@@ -19,7 +19,11 @@ function uninstall() {
|
|
|
19
19
|
|
|
20
20
|
// Also strip our hooks if they exist
|
|
21
21
|
const { updateHooks } = require('./setup');
|
|
22
|
-
|
|
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);
|