@alyibrahim/claude-statusline 1.5.3 → 1.5.4

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
@@ -34,6 +34,13 @@ Done. The statusline configures itself automatically. Restart Claude Code to see
34
34
 
35
35
  > If auto-setup didn't run: `claude-statusline setup`
36
36
 
37
+ **Plugin install (Claude Code marketplace):** Search for `claude-statusline` in the Claude Code plugin browser. The plugin auto-configures on first session start. To get the native Rust binary after a plugin install, run:
38
+
39
+ ```bash
40
+ claude-statusline download-binary
41
+ claude-statusline setup
42
+ ```
43
+
37
44
  ---
38
45
 
39
46
  <div align="center">
@@ -95,15 +102,17 @@ Data is stored at `~/.claude/statusline-history.jsonl`.
95
102
 
96
103
  ### Claude Code slash commands
97
104
 
98
- History commands are also available directly inside Claude Code as slash commands:
105
+ Commands are also available directly inside Claude Code as slash commands:
99
106
 
100
107
  - `/history`
101
108
  - `/history-enable`
102
109
  - `/history-disable`
103
110
  - `/history-mode <web|terminal>`
111
+ - `/download-binary`
104
112
 
105
113
  Project contributors get these from the repo at `.claude/commands/`.
106
114
  Global npm installs copy them to `~/.claude/commands/` automatically.
115
+ Plugin installs include them via the plugin's `commands/` directory.
107
116
 
108
117
  ### Terminal TUI
109
118
 
@@ -159,6 +168,8 @@ Rows are color-coded by exit reason: green = normal, yellow = interrupted, orang
159
168
 
160
169
  Pre-built Rust binaries are available for **Linux x64/arm64, macOS x64/arm64, and Windows x64**. All Linux distributions (Ubuntu, Arch, Fedora, etc.) are supported. Any other platform falls back to the JS implementation automatically — no action needed.
161
170
 
171
+ Plugin installs skip `npm install`, so the binary is not downloaded automatically. Run `claude-statusline download-binary` once to get it.
172
+
162
173
  See [PLATFORMS.md](PLATFORMS.md) for the full compatibility guide, per-platform install instructions, and feature availability table.
163
174
 
164
175
  ---
package/bin/cli.js CHANGED
@@ -23,7 +23,7 @@ Commands:
23
23
  const TERMINAL_FALLBACK_WARNING = [
24
24
  '[claude-statusline] terminal mode requires the native binary.',
25
25
  'Falling back to web dashboard. To install the binary, run:',
26
- ' npm install -g @alyibrahim/claude-statusline'
26
+ ' claude-statusline download-binary'
27
27
  ].join('\n');
28
28
 
29
29
  function parseHistoryMode(args) {
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/statusline.js\" hook start"
9
+ "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/statusline.js\" hook start --marker=${HOOK_MARKER}"
10
10
  }
11
11
  ]
12
12
  }
@@ -17,7 +17,7 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/statusline.js\" hook end"
20
+ "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/statusline.js\" hook end --marker=${HOOK_MARKER}"
21
21
  }
22
22
  ]
23
23
  }
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/scripts/plugin-autosetup.js\""
9
+ "command": "${CLAUDE_NODE_EXEC} \"${CLAUDE_PLUGIN_ROOT}/scripts/plugin-autosetup.js\" --marker=${HOOK_MARKER}"
10
10
  }
11
11
  ]
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alyibrahim/claude-statusline",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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",
@@ -37,7 +37,8 @@
37
37
  "scripts": {
38
38
  "postinstall": "node scripts/postinstall.js",
39
39
  "preuninstall": "node scripts/preuninstall.js",
40
- "test": "jest"
40
+ "test": "jest",
41
+ "check-versions": "node scripts/check-version-alignment.js"
41
42
  },
42
43
  "jest": {
43
44
  "testPathIgnorePatterns": [
@@ -57,11 +58,11 @@
57
58
  "open": "^10.1.0"
58
59
  },
59
60
  "optionalDependencies": {
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"
61
+ "@alyibrahim/claude-statusline-linux-x64": "1.5.4",
62
+ "@alyibrahim/claude-statusline-linux-arm64": "1.5.4",
63
+ "@alyibrahim/claude-statusline-darwin-x64": "1.5.4",
64
+ "@alyibrahim/claude-statusline-darwin-arm64": "1.5.4",
65
+ "@alyibrahim/claude-statusline-win32-x64": "1.5.4"
65
66
  },
66
67
  "devDependencies": {
67
68
  "jest": "^29.0.0"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ function readJson(filePath) {
8
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
9
+ }
10
+
11
+ function readCargoVersion(cargoTomlPath) {
12
+ const content = fs.readFileSync(cargoTomlPath, 'utf8');
13
+ const match = content.match(/^version\s*=\s*"([^"]+)"/m);
14
+ return match ? match[1] : null;
15
+ }
16
+
17
+ function main() {
18
+ const root = path.resolve(__dirname, '..');
19
+ const packageJsonPath = path.join(root, 'package.json');
20
+ const packageLockPath = path.join(root, 'package-lock.json');
21
+ const cargoTomlPath = path.join(root, 'Cargo.toml');
22
+ const pluginJsonPath = path.join(root, '.claude-plugin', 'plugin.json');
23
+ const marketplaceJsonPath = path.join(root, '.claude-plugin', 'marketplace.json');
24
+
25
+ const rootPkg = readJson(packageJsonPath);
26
+ const lock = readJson(packageLockPath);
27
+ const cargoVersion = readCargoVersion(cargoTomlPath);
28
+ const pluginJson = readJson(pluginJsonPath);
29
+ const marketplaceJson = readJson(marketplaceJsonPath);
30
+ const optionalDeps = rootPkg.optionalDependencies || {};
31
+
32
+ const errors = [];
33
+ const expected = rootPkg.version;
34
+
35
+ if (!expected) {
36
+ errors.push('package.json is missing a version');
37
+ }
38
+
39
+ if (!cargoVersion) {
40
+ errors.push('Cargo.toml is missing a version');
41
+ } else if (cargoVersion !== expected) {
42
+ errors.push(`Cargo.toml version ${cargoVersion} does not match package.json version ${expected}`);
43
+ }
44
+
45
+ if (!pluginJson.version) {
46
+ errors.push('.claude-plugin/plugin.json is missing a version');
47
+ } else if (pluginJson.version !== expected) {
48
+ errors.push(`.claude-plugin/plugin.json version ${pluginJson.version} does not match package.json version ${expected}`);
49
+ }
50
+
51
+ const marketplaceVersion = marketplaceJson.plugins && marketplaceJson.plugins[0] && marketplaceJson.plugins[0].version;
52
+ if (!marketplaceVersion) {
53
+ errors.push('.claude-plugin/marketplace.json is missing plugins[0].version');
54
+ } else if (marketplaceVersion !== expected) {
55
+ errors.push(`.claude-plugin/marketplace.json plugins[0].version ${marketplaceVersion} does not match package.json version ${expected}`);
56
+ }
57
+
58
+ for (const [name, version] of Object.entries(optionalDeps)) {
59
+ const lockRootVersion = lock.packages && lock.packages[''] && lock.packages[''].optionalDependencies
60
+ ? lock.packages[''].optionalDependencies[name]
61
+ : undefined;
62
+ if (lockRootVersion !== version) {
63
+ errors.push(`package-lock.json root optionalDependencies[${name}] is ${lockRootVersion}, expected ${version}`);
64
+ }
65
+ // node_modules entries are only resolved once the package exists on npm.
66
+ // Pre-publish they contain only { optional: true } — skip the version check in that case.
67
+ const lockPkg = lock.packages && lock.packages[`node_modules/${name}`];
68
+ if (lockPkg && lockPkg.version !== undefined && lockPkg.version !== version) {
69
+ errors.push(`package-lock.json node_modules entry for ${name} is ${lockPkg.version}, expected ${version}`);
70
+ }
71
+ }
72
+
73
+ if (errors.length > 0) {
74
+ console.error('[check-versions] Failed version alignment checks:');
75
+ for (const err of errors) {
76
+ console.error(`- ${err}`);
77
+ }
78
+ process.exit(1);
79
+ }
80
+
81
+ console.log(`[check-versions] OK (version ${expected})`);
82
+ }
83
+
84
+ main();
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
  const open = require('open');
6
+ const { normalizeProjectSlug } = require('./slug-utils');
6
7
 
7
8
  const JSONL_PATH = path.join(
8
9
  process.env.HOME || process.env.USERPROFILE || os.homedir(),
@@ -65,7 +66,7 @@ function handleHookEnd() {
65
66
 
66
67
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
67
68
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
68
- const slug = projectDir.replace(/[/\\]/g, '-');
69
+ const slug = normalizeProjectSlug(projectDir);
69
70
  const projectsDir = path.join(home, '.claude', 'projects', slug);
70
71
 
71
72
  // Read session stats from the most recently modified JSONL in the project dir
package/scripts/setup.js CHANGED
@@ -5,6 +5,7 @@ const config = require('./config');
5
5
  const { getSettingsPath, atomicWrite } = config;
6
6
 
7
7
  const UNSAFE_CHARS = /["`$!()\\]/;
8
+ const HOOK_MARKER = 'claude-statusline-owned-v1';
8
9
 
9
10
  function buildNodeExecCommand() {
10
11
  return `"${process.execPath}"`;
@@ -108,6 +109,7 @@ function updateHooks(settings, enable, { nodeExecCommand = buildNodeExecCommand(
108
109
  const resolved = resolveHooksFromFile(f, {
109
110
  CLAUDE_PLUGIN_ROOT: escapedRoot,
110
111
  CLAUDE_NODE_EXEC: escapedNodeExec,
112
+ HOOK_MARKER,
111
113
  });
112
114
  for (const entries of Object.values(resolved)) {
113
115
  for (const entry of entries) {
@@ -122,6 +124,7 @@ function updateHooks(settings, enable, { nodeExecCommand = buildNodeExecCommand(
122
124
  const resolvedHooks = resolveHooksFromFile(f, {
123
125
  CLAUDE_PLUGIN_ROOT: escapedRoot,
124
126
  CLAUDE_NODE_EXEC: escapedNodeExec,
127
+ HOOK_MARKER,
125
128
  });
126
129
 
127
130
  for (const [event, entries] of Object.entries(resolvedHooks)) {
@@ -133,11 +136,21 @@ function updateHooks(settings, enable, { nodeExecCommand = buildNodeExecCommand(
133
136
  const hasOwnedMarker = /claude-statusline|CLAUDE_PLUGIN_ROOT/i.test(cmd);
134
137
  return hasScript && hasOwnedMarker;
135
138
  };
139
+ const hasOwnedMarker = cmd => cmd && cmd.includes(`--marker=${HOOK_MARKER}`);
140
+ const isLegacyStatuslineHook = cmd => {
141
+ if (!cmd) return false;
142
+ const isHookSuffix = cmd.endsWith(' hook start') || cmd.endsWith(' hook end');
143
+ if (!isHookSuffix) return false;
144
+ // Keep backward compatibility with older commands while avoiding broad suffix-only matches.
145
+ return /(?:^|\s)(?:statusline|claude-statusline)(?:\s|$)/i.test(cmd);
146
+ };
136
147
  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') ||
148
+ // Marker match — canonical ownership check for new installs.
149
+ hasOwnedMarker(inner.command) ||
139
150
  // Exact match — catches current hooks including plugin-setup entries
140
151
  ourCommands.has(inner.command) ||
152
+ // Backward-compatible statusline suffix match for older package versions.
153
+ isLegacyStatuslineHook(inner.command) ||
141
154
  // Legacy autosetup fallback — catches prior install roots
142
155
  isLegacyAutosetup(inner.command)
143
156
  );
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ function normalizeProjectSlug(projectPath) {
4
+ return String(projectPath || '').replace(/[/\\]/g, '-');
5
+ }
6
+
7
+ module.exports = { normalizeProjectSlug };
package/statusline.js CHANGED
@@ -6,6 +6,7 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
8
  const { execSync, execFileSync } = require('child_process');
9
+ const { normalizeProjectSlug } = require('./scripts/slug-utils');
9
10
 
10
11
  const cmd = process.argv[2];
11
12
  if (cmd === 'history') {
@@ -30,7 +31,7 @@ if (cmd === 'history') {
30
31
  // Returns { totalIn, totalOut } or null on any error.
31
32
  function readSessionTokens(claudeDir, session, absDir) {
32
33
  if (!session) return null;
33
- const slug = absDir.replace(/\//g, '-');
34
+ const slug = normalizeProjectSlug(absDir);
34
35
  const jsonlPath = path.join(claudeDir, 'projects', slug, `${session}.jsonl`);
35
36
  const cachePath = path.join(claudeDir, `statusline-tokcache-${session}.json`);
36
37
  try {