@alyibrahim/claude-statusline 1.6.1 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alyibrahim/claude-statusline",
3
- "version": "1.6.1",
3
+ "version": "1.6.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",
@@ -58,11 +58,11 @@
58
58
  "open": "^10.1.0"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@alyibrahim/claude-statusline-linux-x64": "1.6.1",
62
- "@alyibrahim/claude-statusline-linux-arm64": "1.6.1",
63
- "@alyibrahim/claude-statusline-darwin-x64": "1.6.1",
64
- "@alyibrahim/claude-statusline-darwin-arm64": "1.6.1",
65
- "@alyibrahim/claude-statusline-win32-x64": "1.6.1"
61
+ "@alyibrahim/claude-statusline-linux-x64": "1.6.2",
62
+ "@alyibrahim/claude-statusline-linux-arm64": "1.6.2",
63
+ "@alyibrahim/claude-statusline-darwin-x64": "1.6.2",
64
+ "@alyibrahim/claude-statusline-darwin-arm64": "1.6.2",
65
+ "@alyibrahim/claude-statusline-win32-x64": "1.6.2"
66
66
  },
67
67
  "devDependencies": {
68
68
  "jest": "^29.0.0"
package/scripts/config.js CHANGED
@@ -3,6 +3,10 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
 
6
+ function getHomeDir() {
7
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
8
+ }
9
+
6
10
  function getClaudeConfigDir() {
7
11
  const configDir = process.env.CLAUDE_CONFIG_DIR;
8
12
  return (configDir && configDir.trim())
@@ -10,6 +14,10 @@ function getClaudeConfigDir() {
10
14
  : path.join(os.homedir(), '.claude');
11
15
  }
12
16
 
17
+ function isPlainObject(value) {
18
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
13
21
  function sanitizeSlug(s) {
14
22
  return String(s || '')
15
23
  .replace(/[^a-zA-Z0-9_-]/g, '-')
@@ -45,6 +53,7 @@ function getSettingsPath() {
45
53
 
46
54
  function atomicWrite(filePath, obj) {
47
55
  const tmpPath = filePath + '.tmp';
56
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
48
57
  if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
49
58
  fs.writeFileSync(tmpPath, JSON.stringify(obj, null, 2));
50
59
  try {
@@ -76,7 +85,9 @@ module.exports = {
76
85
  getSettingsPath,
77
86
  atomicWrite,
78
87
  resolveBinary,
88
+ getHomeDir,
79
89
  getClaudeConfigDir,
90
+ isPlainObject,
80
91
  getRealtimeTtySlug,
81
92
  getRealtimePaths,
82
93
  };
@@ -3,13 +3,22 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
  const { normalizeProjectSlug } = require('./slug-utils');
6
+ const { getHomeDir } = require('./config');
6
7
 
7
8
  const JSONL_PATH = path.join(
8
- process.env.HOME || process.env.USERPROFILE || os.homedir(),
9
+ getHomeDir(),
9
10
  '.claude',
10
11
  'statusline-history.jsonl'
11
12
  );
12
13
 
14
+ function safeJsonParse(value, fallback = null) {
15
+ try {
16
+ return JSON.parse(value);
17
+ } catch (e) {
18
+ return fallback;
19
+ }
20
+ }
21
+
13
22
  function now() {
14
23
  return new Date().toISOString().replace('T', ' ').slice(0, 19);
15
24
  }
@@ -60,11 +69,10 @@ function handleHookEnd() {
60
69
  process.stdin.setEncoding('utf8');
61
70
  process.stdin.on('data', chunk => input += chunk);
62
71
  process.stdin.on('end', () => {
63
- let reason = 'unknown';
64
- try { reason = JSON.parse(input).reason || 'unknown'; } catch (e) {}
72
+ const reason = safeJsonParse(input, {}).reason || 'unknown';
65
73
 
66
74
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
67
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
75
+ const home = getHomeDir();
68
76
  const slug = normalizeProjectSlug(projectDir);
69
77
  const projectsDir = path.join(home, '.claude', 'projects', slug);
70
78
 
@@ -86,21 +94,20 @@ function handleHookEnd() {
86
94
  const lines = fs.readFileSync(newestFile, 'utf8').split('\n');
87
95
  for (const line of lines) {
88
96
  if (!line.trim()) continue;
89
- try {
90
- const entry = JSON.parse(line);
91
- if (entry.type === 'assistant' && entry.message?.usage) {
92
- const u = entry.message.usage;
93
- totalIn += (u.input_tokens || 0)
94
- + Math.round((u.cache_read_input_tokens || 0) * 0.1)
95
- + (u.cache_creation_input_tokens || 0);
96
- totalOut += (u.output_tokens || 0);
97
- if (!model) model = entry.message.model || '';
98
- } else if (entry.type === 'cost') {
99
- cost += (entry.cost_usd || 0);
100
- } else if (entry.type === 'message_start' && !model) {
101
- model = entry.message?.model || '';
102
- }
103
- } catch (e) {}
97
+ const entry = safeJsonParse(line);
98
+ if (!entry) continue;
99
+ if (entry.type === 'assistant' && entry.message?.usage) {
100
+ const u = entry.message.usage;
101
+ totalIn += (u.input_tokens || 0)
102
+ + Math.round((u.cache_read_input_tokens || 0) * 0.1)
103
+ + (u.cache_creation_input_tokens || 0);
104
+ totalOut += (u.output_tokens || 0);
105
+ if (!model) model = entry.message.model || '';
106
+ } else if (entry.type === 'cost') {
107
+ cost += (entry.cost_usd || 0);
108
+ } else if (entry.type === 'message_start' && !model) {
109
+ model = entry.message?.model || '';
110
+ }
104
111
  }
105
112
  }
106
113
  } catch (e) {}
package/scripts/setup.js CHANGED
@@ -2,7 +2,7 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const config = require('./config');
5
- const { getSettingsPath, atomicWrite } = config;
5
+ const { getSettingsPath, atomicWrite, isPlainObject } = config;
6
6
 
7
7
  const UNSAFE_CHARS = /["`$!()\\]/;
8
8
  const HOOK_MARKER = 'claude-statusline-owned-v1';
@@ -59,7 +59,7 @@ function setup({ force = false } = {}) {
59
59
  } catch (e) {
60
60
  return { ok: false, error: 'settings.json contains invalid JSON — fix manually then re-run.' };
61
61
  }
62
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
62
+ if (!isPlainObject(settings)) {
63
63
  return { ok: false, error: 'settings.json does not contain a JSON object — fix manually then re-run.' };
64
64
  }
65
65
  }
@@ -183,7 +183,7 @@ function toggleHistory(enable) {
183
183
  } catch (e) {
184
184
  return { ok: false, error: 'settings.json contains invalid JSON — fix manually then re-run.' };
185
185
  }
186
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
186
+ if (!isPlainObject(settings)) {
187
187
  return { ok: false, error: 'settings.json does not contain a JSON object — fix manually then re-run.' };
188
188
  }
189
189
  }
@@ -215,7 +215,7 @@ function getDashboardMode() {
215
215
  } catch (e) {
216
216
  return { ok: false, error: 'settings.json contains invalid JSON - fix manually then re-run.' };
217
217
  }
218
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
218
+ if (!isPlainObject(settings)) {
219
219
  return { ok: false, error: 'settings.json does not contain a JSON object - fix manually then re-run.' };
220
220
  }
221
221
 
@@ -236,7 +236,7 @@ function setDashboardMode(mode) {
236
236
  } catch (e) {
237
237
  return { ok: false, error: 'settings.json contains invalid JSON - fix manually then re-run.' };
238
238
  }
239
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
239
+ if (!isPlainObject(settings)) {
240
240
  return { ok: false, error: 'settings.json does not contain a JSON object - fix manually then re-run.' };
241
241
  }
242
242
  }
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  const fs = require('fs');
3
- const { getSettingsPath, atomicWrite } = require('./config');
3
+ const { getSettingsPath, atomicWrite, isPlainObject } = require('./config');
4
4
 
5
5
  function uninstall() {
6
6
  const settingsPath = getSettingsPath();
@@ -12,7 +12,7 @@ function uninstall() {
12
12
  } catch (e) {
13
13
  return { ok: false, error: 'settings.json contains invalid JSON — cannot safely modify.' };
14
14
  }
15
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
15
+ if (!isPlainObject(settings)) {
16
16
  return { ok: false, error: 'settings.json does not contain a JSON object — cannot safely modify.' };
17
17
  }
18
18
 
package/statusline.js CHANGED
@@ -4,45 +4,34 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const os = require('os');
8
7
  const { execSync, execFileSync } = require('child_process');
9
8
  const { normalizeProjectSlug } = require('./scripts/slug-utils');
9
+ const {
10
+ getHomeDir,
11
+ getRealtimePaths,
12
+ atomicWrite: atomicWriteJson,
13
+ } = require('./scripts/config');
10
14
 
11
15
  function realtimeEnabled() {
12
16
  const v = process.env.CLAUDE_STATUSLINE_REALTIME;
13
17
  return v === '1' || v === 'true' || v === 'TRUE';
14
18
  }
15
19
 
16
- function realtimeTtySlug() {
17
- const raw = (process.env.CLAUDE_STATUSLINE_TTY || process.env.TERM_SESSION_ID || `pid-${process.pid}`).trim();
18
- return raw.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
19
- }
20
-
21
- function atomicWrite(filePath, obj) {
22
- const tmp = `${filePath}.tmp`;
23
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
- fs.writeFileSync(tmp, JSON.stringify(obj));
25
- fs.renameSync(tmp, filePath);
26
- }
27
-
28
20
  function emitRealtimeEvent(eventType, payload) {
29
21
  if (!realtimeEnabled()) return;
30
22
  try {
31
- const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
32
- const ttySlug = realtimeTtySlug();
23
+ const { claudeDir, ttySlug, statePath, registryPath, socketPath } = getRealtimePaths();
33
24
  const now = Date.now();
34
- const statePath = path.join(claudeDir, `statusline-state-${ttySlug}.json`);
35
- const registryPath = path.join(claudeDir, `statusline-renderer-${ttySlug}.json`);
36
25
 
37
- atomicWrite(registryPath, {
26
+ atomicWriteJson(registryPath, {
38
27
  version: 1,
39
28
  pid: process.pid,
40
29
  tty_slug: ttySlug,
41
30
  heartbeat_at_ms: now,
42
- socket_path: path.join(claudeDir, `statusline-rt-${ttySlug}.sock`),
31
+ socket_path: socketPath,
43
32
  });
44
33
 
45
- atomicWrite(statePath, {
34
+ atomicWriteJson(statePath, {
46
35
  version: 1,
47
36
  event_type: eventType,
48
37
  tty_slug: ttySlug,
@@ -261,7 +250,7 @@ process.stdin.on('end', () => {
261
250
  // Current task from todos
262
251
  let task = '';
263
252
  let activeAgents = 0;
264
- const homeDir = os.homedir();
253
+ const homeDir = getHomeDir();
265
254
  const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
266
255
  const todosDir = path.join(claudeDir, 'todos');
267
256
  if (session && fs.existsSync(todosDir)) {