@alyibrahim/claude-statusline 1.5.4 → 1.6.1
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 +39 -1
- package/bin/cli.js +75 -0
- package/package.json +6 -6
- package/scripts/check-version-alignment.js +6 -1
- package/scripts/config.js +42 -3
- package/scripts/history.js +1 -1
- package/scripts/plugin-autosetup.js +38 -23
- package/scripts/postinstall.js +11 -0
- package/scripts/setup.js +12 -0
- package/scripts/uninstall.js +3 -0
- package/statusline.js +154 -10
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ 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
|
|
37
|
+
**Plugin install (Claude Code marketplace):** Search for `claude-statusline` in the Claude Code plugin browser. The plugin auto-configures during installation. To get the native Rust binary after a plugin install, run:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
claude-statusline download-binary
|
|
@@ -64,6 +64,7 @@ claude-statusline setup
|
|
|
64
64
|
| Git branch | Detected automatically, silently absent if not a git repo |
|
|
65
65
|
| Session commits | Shows `+N` next to the branch for commits made during the current session |
|
|
66
66
|
| Directory label | Displays as `~/parent/dir` so you always know which project you're in |
|
|
67
|
+
| Terminal wrapping | Lines wrap to fit terminal width — reads `COLUMNS` or `stdout.columns` automatically |
|
|
67
68
|
|
|
68
69
|
---
|
|
69
70
|
|
|
@@ -162,6 +163,43 @@ Rows are color-coded by exit reason: green = normal, yellow = interrupted, orang
|
|
|
162
163
|
|
|
163
164
|
<div align="center">
|
|
164
165
|
|
|
166
|
+
## Realtime Renderer
|
|
167
|
+
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
An optional background process that maintains a persistent Unix socket per terminal, enabling terminal-resize awareness and faster state access. **Disabled by default.**
|
|
171
|
+
|
|
172
|
+
**Enable** by setting the environment variable (add to your shell profile to persist):
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
export CLAUDE_STATUSLINE_REALTIME=1
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Accepted values: `1`, `true`, `TRUE`.
|
|
179
|
+
|
|
180
|
+
When enabled, the native binary auto-spawns a renderer process the first time it runs in a terminal session. The renderer listens on a Unix socket and persists state to `~/.claude/statusline-state-{tty}.json`. It exits automatically on `session_end` or when explicitly stopped.
|
|
181
|
+
|
|
182
|
+
**Terminal identification**
|
|
183
|
+
|
|
184
|
+
Each terminal gets its own renderer, identified by a *TTY slug* derived in priority order from:
|
|
185
|
+
|
|
186
|
+
1. `CLAUDE_STATUSLINE_TTY` — set this explicitly for a stable, human-readable slug
|
|
187
|
+
2. `TERM_SESSION_ID` — used automatically by terminal emulators that set it
|
|
188
|
+
3. `pid-{PID}` — fallback, changes on each shell restart
|
|
189
|
+
|
|
190
|
+
**Commands:**
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
claude-statusline realtime-status # Show renderer state and paths for current terminal
|
|
194
|
+
claude-statusline realtime-stop # Request renderer shutdown for current terminal
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
> The realtime renderer is Unix-only. On Windows, the feature flag is silently ignored.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
<div align="center">
|
|
202
|
+
|
|
165
203
|
## Platform support
|
|
166
204
|
|
|
167
205
|
</div>
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const config = require('../scripts/config');
|
|
|
6
6
|
const { getSettingsPath } = config;
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
9
10
|
|
|
10
11
|
const USAGE = `
|
|
11
12
|
claude-statusline <command>
|
|
@@ -16,6 +17,8 @@ Commands:
|
|
|
16
17
|
download-binary Download the native binary for this platform
|
|
17
18
|
enable-history Enable tracking session analytics to JSONL (default on setup)
|
|
18
19
|
disable-history Remove history tracking hooks from Claude settings
|
|
20
|
+
realtime-status Show realtime renderer state for current terminal
|
|
21
|
+
realtime-stop Request realtime renderer shutdown for current terminal
|
|
19
22
|
history Open the session analytics dashboard
|
|
20
23
|
--mode web|terminal (persist dashboard mode preference)
|
|
21
24
|
`.trim();
|
|
@@ -100,6 +103,72 @@ function runHistory() {
|
|
|
100
103
|
process.exit(child.status || 0);
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function runRealtimeStatus() {
|
|
107
|
+
const paths = config.getRealtimePaths();
|
|
108
|
+
let registry = null;
|
|
109
|
+
let state = null;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (fs.existsSync(paths.registryPath)) {
|
|
113
|
+
registry = JSON.parse(fs.readFileSync(paths.registryPath, 'utf8'));
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(paths.statePath)) {
|
|
119
|
+
state = JSON.parse(fs.readFileSync(paths.statePath, 'utf8'));
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {}
|
|
122
|
+
|
|
123
|
+
const summary = {
|
|
124
|
+
ttySlug: paths.ttySlug,
|
|
125
|
+
registryPath: paths.registryPath,
|
|
126
|
+
statePath: paths.statePath,
|
|
127
|
+
socketPath: paths.socketPath,
|
|
128
|
+
hasRegistry: !!registry,
|
|
129
|
+
hasState: !!state,
|
|
130
|
+
registry,
|
|
131
|
+
stateEventType: state?.event_type || null,
|
|
132
|
+
stateUpdatedAt: state?.updated_at_ms || null,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runRealtimeStop() {
|
|
140
|
+
const net = require('net');
|
|
141
|
+
const paths = config.getRealtimePaths();
|
|
142
|
+
const ts = Date.now();
|
|
143
|
+
const event = {
|
|
144
|
+
version: 1,
|
|
145
|
+
event_type: 'shutdown',
|
|
146
|
+
tty_slug: paths.ttySlug,
|
|
147
|
+
updated_at_ms: ts,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
config.atomicWrite(paths.statePath, event);
|
|
152
|
+
} catch (e) {}
|
|
153
|
+
|
|
154
|
+
const done = () => {
|
|
155
|
+
console.log('✓ Realtime shutdown event sent');
|
|
156
|
+
process.exit(0);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (fs.existsSync(paths.socketPath)) {
|
|
160
|
+
const client = net.createConnection(paths.socketPath);
|
|
161
|
+
client.on('connect', () => {
|
|
162
|
+
client.write(JSON.stringify(event) + '\n');
|
|
163
|
+
client.end();
|
|
164
|
+
});
|
|
165
|
+
client.on('close', done);
|
|
166
|
+
client.on('error', done);
|
|
167
|
+
} else {
|
|
168
|
+
done();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
103
172
|
const cmd = process.argv[2];
|
|
104
173
|
|
|
105
174
|
if (cmd === 'setup') {
|
|
@@ -161,6 +230,12 @@ if (cmd === 'setup') {
|
|
|
161
230
|
} else if (cmd === 'history') {
|
|
162
231
|
runHistory();
|
|
163
232
|
|
|
233
|
+
} else if (cmd === 'realtime-status') {
|
|
234
|
+
runRealtimeStatus();
|
|
235
|
+
|
|
236
|
+
} else if (cmd === 'realtime-stop') {
|
|
237
|
+
runRealtimeStop();
|
|
238
|
+
|
|
164
239
|
} else if (cmd === undefined) {
|
|
165
240
|
console.log(USAGE);
|
|
166
241
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alyibrahim/claude-statusline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
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.
|
|
62
|
-
"@alyibrahim/claude-statusline-linux-arm64": "1.
|
|
63
|
-
"@alyibrahim/claude-statusline-darwin-x64": "1.
|
|
64
|
-
"@alyibrahim/claude-statusline-darwin-arm64": "1.
|
|
65
|
-
"@alyibrahim/claude-statusline-win32-x64": "1.
|
|
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"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"jest": "^29.0.0"
|
|
@@ -15,7 +15,7 @@ function readCargoVersion(cargoTomlPath) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function main() {
|
|
18
|
-
const root = path.resolve(__dirname, '..');
|
|
18
|
+
const root = process.env.CHECK_VERSIONS_ROOT || path.resolve(__dirname, '..');
|
|
19
19
|
const packageJsonPath = path.join(root, 'package.json');
|
|
20
20
|
const packageLockPath = path.join(root, 'package-lock.json');
|
|
21
21
|
const cargoTomlPath = path.join(root, 'Cargo.toml');
|
|
@@ -56,6 +56,11 @@ function main() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
for (const [name, version] of Object.entries(optionalDeps)) {
|
|
59
|
+
// Each platform package must be pinned to the root version — they are published together.
|
|
60
|
+
if (version !== expected) {
|
|
61
|
+
errors.push(`optionalDependencies[${name}] is pinned to ${version}, expected root version ${expected}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
const lockRootVersion = lock.packages && lock.packages[''] && lock.packages[''].optionalDependencies
|
|
60
65
|
? lock.packages[''].optionalDependencies[name]
|
|
61
66
|
: undefined;
|
package/scripts/config.js
CHANGED
|
@@ -3,11 +3,43 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
function getClaudeConfigDir() {
|
|
7
7
|
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
8
|
-
|
|
8
|
+
return (configDir && configDir.trim())
|
|
9
9
|
? configDir
|
|
10
10
|
: path.join(os.homedir(), '.claude');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sanitizeSlug(s) {
|
|
14
|
+
return String(s || '')
|
|
15
|
+
.replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
16
|
+
.replace(/-+/g, '-')
|
|
17
|
+
.replace(/^-|-$/g, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getRealtimeTtySlug() {
|
|
21
|
+
const preferred = [process.env.CLAUDE_STATUSLINE_TTY, process.env.TERM_SESSION_ID];
|
|
22
|
+
for (const raw of preferred) {
|
|
23
|
+
const slug = sanitizeSlug(raw || '');
|
|
24
|
+
if (slug) return slug;
|
|
25
|
+
}
|
|
26
|
+
return sanitizeSlug(`pid-${process.pid}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRealtimePaths() {
|
|
30
|
+
const claudeDir = getClaudeConfigDir();
|
|
31
|
+
const ttySlug = getRealtimeTtySlug();
|
|
32
|
+
return {
|
|
33
|
+
claudeDir,
|
|
34
|
+
ttySlug,
|
|
35
|
+
registryPath: path.join(claudeDir, `statusline-renderer-${ttySlug}.json`),
|
|
36
|
+
statePath: path.join(claudeDir, `statusline-state-${ttySlug}.json`),
|
|
37
|
+
socketPath: path.join(claudeDir, `statusline-rt-${ttySlug}.sock`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSettingsPath() {
|
|
42
|
+
const base = getClaudeConfigDir();
|
|
11
43
|
return path.join(base, 'settings.json');
|
|
12
44
|
}
|
|
13
45
|
|
|
@@ -40,4 +72,11 @@ function resolveBinary() {
|
|
|
40
72
|
return binaryPath;
|
|
41
73
|
}
|
|
42
74
|
|
|
43
|
-
module.exports = {
|
|
75
|
+
module.exports = {
|
|
76
|
+
getSettingsPath,
|
|
77
|
+
atomicWrite,
|
|
78
|
+
resolveBinary,
|
|
79
|
+
getClaudeConfigDir,
|
|
80
|
+
getRealtimeTtySlug,
|
|
81
|
+
getRealtimePaths,
|
|
82
|
+
};
|
package/scripts/history.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
-
const open = require('open');
|
|
6
5
|
const { normalizeProjectSlug } = require('./slug-utils');
|
|
7
6
|
|
|
8
7
|
const JSONL_PATH = path.join(
|
|
@@ -165,6 +164,7 @@ async function handleHistory() {
|
|
|
165
164
|
const tempPath = path.join(os.tmpdir(), 'claude-statusline-dashboard.html');
|
|
166
165
|
fs.writeFileSync(tempPath, html);
|
|
167
166
|
try {
|
|
167
|
+
const open = require('open');
|
|
168
168
|
await open.default(tempPath);
|
|
169
169
|
console.log(`Dashboard opened: ${tempPath}`);
|
|
170
170
|
} catch (e) {
|
|
@@ -2,34 +2,49 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
-
const { atomicWrite } = require('./config');
|
|
5
|
+
const { atomicWrite, resolveBinary } = require('./config');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (!pluginRoot)
|
|
7
|
+
function pluginAutoSetup(pluginRoot = process.env.CLAUDE_PLUGIN_ROOT) {
|
|
8
|
+
// Only meaningful in plugin context.
|
|
9
|
+
if (!pluginRoot) return { ok: true, configured: false };
|
|
10
10
|
|
|
11
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
12
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
11
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
12
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
13
13
|
|
|
14
|
-
let settings = {};
|
|
15
|
-
try {
|
|
16
|
-
|
|
17
|
-
} catch (e) {}
|
|
14
|
+
let settings = {};
|
|
15
|
+
try {
|
|
16
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
17
|
+
} catch (e) {}
|
|
18
18
|
|
|
19
|
-
// Already configured
|
|
20
|
-
if (settings.statusLine)
|
|
19
|
+
// Already configured; leave user config untouched.
|
|
20
|
+
if (settings.statusLine) return { ok: true, configured: false };
|
|
21
21
|
|
|
22
|
-
const script = path.join(pluginRoot, 'statusline.js');
|
|
23
|
-
|
|
22
|
+
const script = path.join(pluginRoot, 'statusline.js');
|
|
23
|
+
const binaryPath = resolveBinary();
|
|
24
|
+
if (!binaryPath && !fs.existsSync(script)) return { ok: true, configured: false };
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
26
|
+
const command = binaryPath
|
|
27
|
+
? `"${binaryPath}"`
|
|
28
|
+
: `"${process.execPath}" "${script}"`;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
30
|
+
settings.statusLine = {
|
|
31
|
+
type: 'command',
|
|
32
|
+
command,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
37
|
+
atomicWrite(settingsPath, settings);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return { ok: true, configured: false }; // Non-fatal.
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ok: true, configured: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { pluginAutoSetup };
|
|
46
|
+
|
|
47
|
+
if (require.main === module) {
|
|
48
|
+
pluginAutoSetup();
|
|
49
|
+
process.exit(0); // never fail plugin startup
|
|
35
50
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -3,8 +3,19 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { setup } = require('./setup');
|
|
6
|
+
const { pluginAutoSetup } = require('./plugin-autosetup');
|
|
6
7
|
const config = require('./config');
|
|
7
8
|
try {
|
|
9
|
+
// Plugin installs run npm scripts in plugin context; configure statusLine once at install time.
|
|
10
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
11
|
+
pluginAutoSetup(process.env.CLAUDE_PLUGIN_ROOT);
|
|
12
|
+
const pluginBinaryPath = config.resolveBinary();
|
|
13
|
+
if (pluginBinaryPath && process.platform !== 'win32') {
|
|
14
|
+
try { fs.chmodSync(pluginBinaryPath, 0o755); } catch (e) {}
|
|
15
|
+
}
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
const result = setup({ force: false });
|
|
9
20
|
if (result.settingsPath === null) process.exit(0); // non-global install, skip silently
|
|
10
21
|
|
package/scripts/setup.js
CHANGED
|
@@ -59,6 +59,9 @@ 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)) {
|
|
63
|
+
return { ok: false, error: 'settings.json does not contain a JSON object — fix manually then re-run.' };
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
const binaryPath = config.resolveBinary();
|
|
@@ -180,6 +183,9 @@ function toggleHistory(enable) {
|
|
|
180
183
|
} catch (e) {
|
|
181
184
|
return { ok: false, error: 'settings.json contains invalid JSON — fix manually then re-run.' };
|
|
182
185
|
}
|
|
186
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
187
|
+
return { ok: false, error: 'settings.json does not contain a JSON object — fix manually then re-run.' };
|
|
188
|
+
}
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
try {
|
|
@@ -209,6 +215,9 @@ function getDashboardMode() {
|
|
|
209
215
|
} catch (e) {
|
|
210
216
|
return { ok: false, error: 'settings.json contains invalid JSON - fix manually then re-run.' };
|
|
211
217
|
}
|
|
218
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
219
|
+
return { ok: false, error: 'settings.json does not contain a JSON object - fix manually then re-run.' };
|
|
220
|
+
}
|
|
212
221
|
|
|
213
222
|
const mode = settings.dashboardMode === 'terminal' ? 'terminal' : 'web';
|
|
214
223
|
return { ok: true, settingsPath, mode };
|
|
@@ -227,6 +236,9 @@ function setDashboardMode(mode) {
|
|
|
227
236
|
} catch (e) {
|
|
228
237
|
return { ok: false, error: 'settings.json contains invalid JSON - fix manually then re-run.' };
|
|
229
238
|
}
|
|
239
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
240
|
+
return { ok: false, error: 'settings.json does not contain a JSON object - fix manually then re-run.' };
|
|
241
|
+
}
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
settings.dashboardMode = mode;
|
package/scripts/uninstall.js
CHANGED
|
@@ -12,6 +12,9 @@ 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)) {
|
|
16
|
+
return { ok: false, error: 'settings.json does not contain a JSON object — cannot safely modify.' };
|
|
17
|
+
}
|
|
15
18
|
|
|
16
19
|
if (settings.statusLine) {
|
|
17
20
|
delete settings.statusLine;
|
package/statusline.js
CHANGED
|
@@ -8,6 +8,135 @@ const os = require('os');
|
|
|
8
8
|
const { execSync, execFileSync } = require('child_process');
|
|
9
9
|
const { normalizeProjectSlug } = require('./scripts/slug-utils');
|
|
10
10
|
|
|
11
|
+
function realtimeEnabled() {
|
|
12
|
+
const v = process.env.CLAUDE_STATUSLINE_REALTIME;
|
|
13
|
+
return v === '1' || v === 'true' || v === 'TRUE';
|
|
14
|
+
}
|
|
15
|
+
|
|
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
|
+
function emitRealtimeEvent(eventType, payload) {
|
|
29
|
+
if (!realtimeEnabled()) return;
|
|
30
|
+
try {
|
|
31
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
32
|
+
const ttySlug = realtimeTtySlug();
|
|
33
|
+
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
|
+
|
|
37
|
+
atomicWrite(registryPath, {
|
|
38
|
+
version: 1,
|
|
39
|
+
pid: process.pid,
|
|
40
|
+
tty_slug: ttySlug,
|
|
41
|
+
heartbeat_at_ms: now,
|
|
42
|
+
socket_path: path.join(claudeDir, `statusline-rt-${ttySlug}.sock`),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
atomicWrite(statePath, {
|
|
46
|
+
version: 1,
|
|
47
|
+
event_type: eventType,
|
|
48
|
+
tty_slug: ttySlug,
|
|
49
|
+
updated_at_ms: now,
|
|
50
|
+
payload: payload || {},
|
|
51
|
+
});
|
|
52
|
+
} catch (_) {
|
|
53
|
+
// Silent fail - never break statusline rendering
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripSgr(s) {
|
|
58
|
+
return String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function visibleLen(s) {
|
|
62
|
+
return [...stripSgr(String(s))].reduce((sum, ch) => sum + (ch.codePointAt(0) > 0xFFFF ? 2 : 1), 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function terminalColumns() {
|
|
66
|
+
const fromStdout = Number(process.stdout && process.stdout.columns);
|
|
67
|
+
if (Number.isFinite(fromStdout) && fromStdout > 0) return fromStdout;
|
|
68
|
+
const fromEnv = Number(process.env.COLUMNS);
|
|
69
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function truncateVisible(s, maxVisible) {
|
|
74
|
+
if (!maxVisible || maxVisible <= 0) return '';
|
|
75
|
+
if (visibleLen(s) <= maxVisible) return String(s);
|
|
76
|
+
|
|
77
|
+
const src = String(s);
|
|
78
|
+
let out = '';
|
|
79
|
+
let visible = 0;
|
|
80
|
+
for (let i = 0; i < src.length;) {
|
|
81
|
+
if (src[i] === '\x1b' && src[i + 1] === '[') {
|
|
82
|
+
let j = i + 2;
|
|
83
|
+
while (j < src.length && /[0-9;]/.test(src[j])) j++;
|
|
84
|
+
if (j < src.length && /[mGKHFABCDJ]/.test(src[j])) {
|
|
85
|
+
out += src.slice(i, j + 1);
|
|
86
|
+
i = j + 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const code = src.codePointAt(i);
|
|
92
|
+
const ch = String.fromCodePoint(code);
|
|
93
|
+
const width = code > 0xFFFF ? 2 : 1;
|
|
94
|
+
if (visible + width > maxVisible) break;
|
|
95
|
+
out += ch;
|
|
96
|
+
visible += width;
|
|
97
|
+
i += code > 0xFFFF ? 2 : 1;
|
|
98
|
+
}
|
|
99
|
+
return `${out}…\x1b[0m`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function wrapChunks(chunks, width, sep) {
|
|
103
|
+
if (!width) return [chunks.join(sep)];
|
|
104
|
+
if (width < 8) {
|
|
105
|
+
return chunks.map(c => truncateVisible(c, Math.max(1, width - 1)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sepLen = visibleLen(sep);
|
|
109
|
+
const lines = [];
|
|
110
|
+
let current = '';
|
|
111
|
+
let currentLen = 0;
|
|
112
|
+
|
|
113
|
+
for (const rawChunk of chunks) {
|
|
114
|
+
const chunk = visibleLen(rawChunk) > width
|
|
115
|
+
? truncateVisible(rawChunk, Math.max(1, width - 1))
|
|
116
|
+
: rawChunk;
|
|
117
|
+
const chunkLen = visibleLen(chunk);
|
|
118
|
+
|
|
119
|
+
if (!current) {
|
|
120
|
+
current = chunk;
|
|
121
|
+
currentLen = chunkLen;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const needed = currentLen + sepLen + chunkLen;
|
|
126
|
+
if (needed <= width) {
|
|
127
|
+
current += `${sep}${chunk}`;
|
|
128
|
+
currentLen = needed;
|
|
129
|
+
} else {
|
|
130
|
+
lines.push(current);
|
|
131
|
+
current = chunk;
|
|
132
|
+
currentLen = chunkLen;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (current) lines.push(current);
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
11
140
|
const cmd = process.argv[2];
|
|
12
141
|
if (cmd === 'history') {
|
|
13
142
|
require('./scripts/history').handleHistory().catch(e => {
|
|
@@ -18,12 +147,20 @@ if (cmd === 'history') {
|
|
|
18
147
|
} else if (cmd === 'hook') {
|
|
19
148
|
const hookcmd = process.argv[3];
|
|
20
149
|
if (hookcmd === 'start') {
|
|
150
|
+
emitRealtimeEvent('session_start', {});
|
|
21
151
|
require('./scripts/history').handleHookStart();
|
|
22
152
|
return;
|
|
23
153
|
} else if (hookcmd === 'end') {
|
|
154
|
+
emitRealtimeEvent('session_end', {});
|
|
24
155
|
require('./scripts/history').handleHookEnd();
|
|
25
156
|
return;
|
|
26
157
|
}
|
|
158
|
+
} else if (cmd === 'realtime') {
|
|
159
|
+
const subcmd = process.argv[3];
|
|
160
|
+
if (subcmd === 'shutdown') {
|
|
161
|
+
emitRealtimeEvent('shutdown', {});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
27
164
|
}
|
|
28
165
|
|
|
29
166
|
// Reads cumulative token totals from the session JSONL file, using a byte-offset
|
|
@@ -89,6 +226,7 @@ process.stdin.on('end', () => {
|
|
|
89
226
|
try {
|
|
90
227
|
const sanitize = s => String(s).replace(/\x1b\[[0-9;]*[mGKHFABCDJ]/g, '');
|
|
91
228
|
const data = JSON.parse(input);
|
|
229
|
+
emitRealtimeEvent('state_update', data);
|
|
92
230
|
const model = sanitize(data.model?.display_name || 'Claude');
|
|
93
231
|
const dir = data.workspace?.current_dir || process.cwd();
|
|
94
232
|
const session = data.session_id || '';
|
|
@@ -230,13 +368,14 @@ process.stdin.on('end', () => {
|
|
|
230
368
|
const fmt = n => n >= 1_000_000 ? (n % 1_000_000 === 0 ? `${n / 1_000_000}M` : `${(n / 1_000_000).toFixed(1)}M`)
|
|
231
369
|
: n >= 1000 ? `${(n / 1000).toFixed(1)}k`
|
|
232
370
|
: String(n);
|
|
233
|
-
tokenDisplay = `\x1b[
|
|
371
|
+
tokenDisplay = `\x1b[97m${fmt(totalIn ?? 0)}↓ ${fmt(totalOut ?? 0)}↑\x1b[0m`;
|
|
234
372
|
}
|
|
235
373
|
|
|
236
374
|
const usageContent = [u7d, u5h].filter(Boolean).join(' ');
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
375
|
+
const line2Chunks = ['\x1b[0m\x1b[32mUsage\x1b[0m'];
|
|
376
|
+
if (usageContent) line2Chunks.push(usageContent);
|
|
377
|
+
if (costDisplay) line2Chunks.push(costDisplay.trimStart());
|
|
378
|
+
if (tokenDisplay) line2Chunks.push(tokenDisplay);
|
|
240
379
|
// Effort level: read from env var first, then settings.json, then fall back to
|
|
241
380
|
// model-based default (sonnet-4/opus-4 default to "medium" in Claude Code).
|
|
242
381
|
const effortLetters = { low: 'L', medium: 'M', high: 'H' };
|
|
@@ -262,12 +401,17 @@ process.stdin.on('end', () => {
|
|
|
262
401
|
|
|
263
402
|
const agentDisplay = activeAgents > 0 ? ` \x1b[0m\x1b[36m↪ ${activeAgents}\x1b[0m` : '';
|
|
264
403
|
const modelDisplay = `\x1b[0m\x1b[94m${model}\x1b[0m` + effortSuffix + agentDisplay;
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
404
|
+
const line1Chunks = [modelDisplay];
|
|
405
|
+
if (task) line1Chunks.push(`\x1b[1m${task}\x1b[0m`);
|
|
406
|
+
line1Chunks.push(`${dirDisplay}${ctx}`);
|
|
407
|
+
|
|
408
|
+
const columns = terminalColumns();
|
|
409
|
+
const sepToken = ' \x1b[2m│\x1b[0m ';
|
|
410
|
+
const wrappedLine1 = wrapChunks(line1Chunks, columns, sepToken);
|
|
411
|
+
const sepLen = wrappedLine1.reduce((max, line) => Math.max(max, visibleLen(line)), 0);
|
|
412
|
+
const sep = `\x1b[2m${'─'.repeat(sepLen)}\x1b[0m`;
|
|
413
|
+
const wrappedLine2 = line2Chunks.length > 1 ? wrapChunks(line2Chunks, columns, sepToken) : [];
|
|
414
|
+
process.stdout.write(wrappedLine2.length ? `${wrappedLine1.join('\n')}\n${sep}\n${wrappedLine2.join('\n')}` : wrappedLine1.join('\n'));
|
|
271
415
|
} catch (e) {
|
|
272
416
|
// Silent fail - don't break statusline on parse errors
|
|
273
417
|
}
|