@alyibrahim/claude-statusline 1.5.1 → 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 +2 -1
- package/hooks/hooks.json +26 -0
- package/hooks/plugin-setup.json +15 -0
- package/package.json +7 -6
- 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/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.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.
|
|
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,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);
|