claude-code-pulsify 1.0.0
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 -0
- package/bin/install.js +167 -0
- package/hooks/check-update.js +67 -0
- package/hooks/context-monitor.js +115 -0
- package/hooks/statusline.js +163 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# claude-code-pulsify
|
|
2
|
+
|
|
3
|
+
Context-aware statusline and context monitor for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
4
|
+
|
|
5
|
+
Shows a live progress bar of context window usage, active task info, and warns you when context is running low.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx claude-code-pulsify@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This copies hooks to `~/.claude/hooks/claude-code-pulsify/` and patches `~/.claude/settings.json`. Restart Claude Code to activate.
|
|
14
|
+
|
|
15
|
+
## Update
|
|
16
|
+
|
|
17
|
+
Run the same command:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx claude-code-pulsify@latest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The statusline also shows an arrow indicator when an update is available.
|
|
24
|
+
|
|
25
|
+
## Uninstall
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx claude-code-pulsify@latest --uninstall
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
- **Statusline** -- Shows model name, working directory, context usage bar (color-coded), and active task
|
|
34
|
+
- **Context monitor** -- Warns at 65% and 75% context usage via PostToolUse hook
|
|
35
|
+
- **Update checker** -- Background version check on session start, non-blocking
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Respects the `CLAUDE_CONFIG_DIR` environment variable. Defaults to `~/.claude`.
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
const PACKAGE_VERSION = require('../package.json').version
|
|
8
|
+
const HOOKS_SOURCE = path.join(__dirname, '..', 'hooks')
|
|
9
|
+
const HOOK_FILES = ['statusline.js', 'context-monitor.js', 'check-update.js']
|
|
10
|
+
|
|
11
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude')
|
|
12
|
+
const hooksTarget = path.join(configDir, 'hooks', 'claude-code-pulsify')
|
|
13
|
+
const settingsPath = path.join(configDir, 'settings.json')
|
|
14
|
+
|
|
15
|
+
// --- Helpers ---
|
|
16
|
+
|
|
17
|
+
function readJSON(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
20
|
+
} catch {
|
|
21
|
+
return {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJSON(filePath, data) {
|
|
26
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isOurEntry(entry) {
|
|
30
|
+
const cmd = entry?.hooks?.[0]?.command || ''
|
|
31
|
+
return cmd.includes('claude-code-pulsify')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function upsertHookArray(arr, newEntry) {
|
|
35
|
+
const idx = arr.findIndex(isOurEntry)
|
|
36
|
+
if (idx >= 0) {
|
|
37
|
+
arr[idx] = newEntry
|
|
38
|
+
} else {
|
|
39
|
+
arr.push(newEntry)
|
|
40
|
+
}
|
|
41
|
+
return arr
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeFromHookArray(arr) {
|
|
45
|
+
return arr.filter((entry) => !isOurEntry(entry))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Install ---
|
|
49
|
+
|
|
50
|
+
function install() {
|
|
51
|
+
console.log(`\nInstalling claude-code-pulsify v${PACKAGE_VERSION}...\n`)
|
|
52
|
+
|
|
53
|
+
// 1. Copy hooks
|
|
54
|
+
fs.mkdirSync(hooksTarget, { recursive: true })
|
|
55
|
+
for (const file of HOOK_FILES) {
|
|
56
|
+
const src = path.join(HOOKS_SOURCE, file)
|
|
57
|
+
const dst = path.join(hooksTarget, file)
|
|
58
|
+
fs.copyFileSync(src, dst)
|
|
59
|
+
console.log(` Copied ${file}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Write VERSION
|
|
63
|
+
fs.writeFileSync(path.join(hooksTarget, 'VERSION'), PACKAGE_VERSION, 'utf8')
|
|
64
|
+
console.log(` Wrote VERSION (${PACKAGE_VERSION})`)
|
|
65
|
+
|
|
66
|
+
// 3. Patch settings.json
|
|
67
|
+
const settings = readJSON(settingsPath)
|
|
68
|
+
|
|
69
|
+
// statusLine
|
|
70
|
+
settings.statusLine = {
|
|
71
|
+
type: 'command',
|
|
72
|
+
command: `node ${path.join(hooksTarget, 'statusline.js')}`,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// hooks
|
|
76
|
+
if (!settings.hooks) settings.hooks = {}
|
|
77
|
+
|
|
78
|
+
// PostToolUse — context-monitor
|
|
79
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = []
|
|
80
|
+
upsertHookArray(settings.hooks.PostToolUse, {
|
|
81
|
+
hooks: [
|
|
82
|
+
{
|
|
83
|
+
type: 'command',
|
|
84
|
+
command: `node ${path.join(hooksTarget, 'context-monitor.js')}`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// SessionStart — check-update
|
|
90
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = []
|
|
91
|
+
upsertHookArray(settings.hooks.SessionStart, {
|
|
92
|
+
hooks: [
|
|
93
|
+
{
|
|
94
|
+
type: 'command',
|
|
95
|
+
command: `node ${path.join(hooksTarget, 'check-update.js')}`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
writeJSON(settingsPath, settings)
|
|
101
|
+
console.log(` Patched settings.json`)
|
|
102
|
+
|
|
103
|
+
console.log(`\nDone! Restart Claude Code to activate the statusline.\n`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Uninstall ---
|
|
107
|
+
|
|
108
|
+
function uninstall() {
|
|
109
|
+
console.log(`\nUninstalling claude-code-pulsify...\n`)
|
|
110
|
+
|
|
111
|
+
// 1. Remove hooks directory
|
|
112
|
+
if (fs.existsSync(hooksTarget)) {
|
|
113
|
+
fs.rmSync(hooksTarget, { recursive: true })
|
|
114
|
+
console.log(` Removed ${hooksTarget}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. Clean settings.json
|
|
118
|
+
if (fs.existsSync(settingsPath)) {
|
|
119
|
+
const settings = readJSON(settingsPath)
|
|
120
|
+
|
|
121
|
+
// Remove statusLine if it's ours
|
|
122
|
+
if (settings.statusLine?.command?.includes('claude-code-pulsify')) {
|
|
123
|
+
delete settings.statusLine
|
|
124
|
+
console.log(` Removed statusLine config`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Remove hook entries
|
|
128
|
+
if (settings.hooks) {
|
|
129
|
+
for (const event of ['PostToolUse', 'SessionStart']) {
|
|
130
|
+
if (Array.isArray(settings.hooks[event])) {
|
|
131
|
+
settings.hooks[event] = removeFromHookArray(settings.hooks[event])
|
|
132
|
+
if (settings.hooks[event].length === 0) {
|
|
133
|
+
delete settings.hooks[event]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
138
|
+
delete settings.hooks
|
|
139
|
+
}
|
|
140
|
+
console.log(` Cleaned hook entries`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
writeJSON(settingsPath, settings)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 3. Remove cache file
|
|
147
|
+
const cachePath = path.join(configDir, 'cache', 'claude-code-pulsify-update.json')
|
|
148
|
+
try {
|
|
149
|
+
fs.unlinkSync(cachePath)
|
|
150
|
+
} catch {
|
|
151
|
+
// Doesn't exist — fine
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`\nDone! claude-code-pulsify has been removed.\n`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Main ---
|
|
158
|
+
|
|
159
|
+
const args = process.argv.slice(2)
|
|
160
|
+
|
|
161
|
+
if (args.includes('--version')) {
|
|
162
|
+
console.log(PACKAGE_VERSION)
|
|
163
|
+
} else if (args.includes('--uninstall')) {
|
|
164
|
+
uninstall()
|
|
165
|
+
} else {
|
|
166
|
+
install()
|
|
167
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SessionStart hook -- background version check for claude-code-pulsify.
|
|
6
|
+
* Spawns a detached process to compare installed version vs npm latest,
|
|
7
|
+
* writes result to a cache file that statusline.js reads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process')
|
|
11
|
+
const fs = require('fs')
|
|
12
|
+
const path = require('path')
|
|
13
|
+
|
|
14
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude')
|
|
15
|
+
const hooksDir = path.join(configDir, 'hooks', 'claude-code-pulsify')
|
|
16
|
+
const cacheDir = path.join(configDir, 'cache')
|
|
17
|
+
const cachePath = path.join(cacheDir, 'claude-code-pulsify-update.json')
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
// Consume stdin (required by hook protocol)
|
|
21
|
+
let input = ''
|
|
22
|
+
for await (const chunk of process.stdin) {
|
|
23
|
+
input += chunk
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read installed version
|
|
27
|
+
const versionFile = path.join(hooksDir, 'VERSION')
|
|
28
|
+
let installed
|
|
29
|
+
try {
|
|
30
|
+
installed = fs.readFileSync(versionFile, 'utf8').trim()
|
|
31
|
+
} catch {
|
|
32
|
+
return // No VERSION file — not installed properly
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Spawn detached background check so we don't block session startup
|
|
36
|
+
const script = `
|
|
37
|
+
const { execSync } = require('child_process');
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const cachePath = ${JSON.stringify(cachePath)};
|
|
40
|
+
const cacheDir = ${JSON.stringify(cacheDir)};
|
|
41
|
+
const installed = ${JSON.stringify(installed)};
|
|
42
|
+
try {
|
|
43
|
+
const latest = execSync('npm view claude-code-pulsify version', {
|
|
44
|
+
timeout: 10000,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
47
|
+
}).trim();
|
|
48
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
50
|
+
installed,
|
|
51
|
+
latest,
|
|
52
|
+
updateAvailable: latest !== installed,
|
|
53
|
+
checkedAt: Date.now()
|
|
54
|
+
}));
|
|
55
|
+
} catch {
|
|
56
|
+
// Network error or npm not available — silently ignore
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
const child = spawn(process.execPath, ['-e', script], {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
})
|
|
64
|
+
child.unref()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch(() => process.exit(0))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code PostToolUse hook -- context monitor.
|
|
6
|
+
* Reads the bridge file written by statusline.js and injects
|
|
7
|
+
* warnings when context is running low.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
// Thresholds on "used percentage" (normalized, 0-100)
|
|
14
|
+
const WARNING_THRESHOLD = 65 // remaining ~35% usable
|
|
15
|
+
const CRITICAL_THRESHOLD = 75 // remaining ~25% usable
|
|
16
|
+
|
|
17
|
+
// Debounce: minimum tool calls between repeated warnings at the same severity
|
|
18
|
+
const DEBOUNCE_COUNT = 5
|
|
19
|
+
|
|
20
|
+
// State file to track debounce across invocations
|
|
21
|
+
function getStateFile(sessionId) {
|
|
22
|
+
return `/tmp/claude-ctx-monitor-state-${sessionId}.json`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readState(sessionId) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(getStateFile(sessionId), 'utf8'))
|
|
28
|
+
} catch {
|
|
29
|
+
return { lastSeverity: null, callsSinceWarning: 0 }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeState(sessionId, state) {
|
|
34
|
+
try {
|
|
35
|
+
fs.writeFileSync(getStateFile(sessionId), JSON.stringify(state), 'utf8')
|
|
36
|
+
} catch {
|
|
37
|
+
// Silently ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
let input = ''
|
|
43
|
+
for await (const chunk of process.stdin) {
|
|
44
|
+
input += chunk
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let hookData
|
|
48
|
+
try {
|
|
49
|
+
hookData = JSON.parse(input)
|
|
50
|
+
} catch {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sessionId = hookData.session?.id || hookData.session_id || null
|
|
55
|
+
if (!sessionId) return
|
|
56
|
+
|
|
57
|
+
// Read bridge file from statusline
|
|
58
|
+
const bridgePath = `/tmp/claude-ctx-${sessionId}.json`
|
|
59
|
+
let metrics
|
|
60
|
+
try {
|
|
61
|
+
metrics = JSON.parse(fs.readFileSync(bridgePath, 'utf8'))
|
|
62
|
+
} catch {
|
|
63
|
+
return // No bridge file yet -- statusline hasn't run
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check staleness (>60s old)
|
|
67
|
+
if (Date.now() - (metrics.timestamp || 0) > 60_000) return
|
|
68
|
+
|
|
69
|
+
const usedPct = metrics.used_percentage
|
|
70
|
+
if (usedPct == null) return
|
|
71
|
+
|
|
72
|
+
// Determine severity
|
|
73
|
+
let severity = null
|
|
74
|
+
if (usedPct >= CRITICAL_THRESHOLD) severity = 'CRITICAL'
|
|
75
|
+
else if (usedPct >= WARNING_THRESHOLD) severity = 'WARNING'
|
|
76
|
+
|
|
77
|
+
if (!severity) return // Context is fine
|
|
78
|
+
|
|
79
|
+
// Debounce logic
|
|
80
|
+
const state = readState(sessionId)
|
|
81
|
+
state.callsSinceWarning = (state.callsSinceWarning || 0) + 1
|
|
82
|
+
|
|
83
|
+
const isEscalation = severity === 'CRITICAL' && state.lastSeverity === 'WARNING'
|
|
84
|
+
const shouldWarn = isEscalation || state.callsSinceWarning >= DEBOUNCE_COUNT
|
|
85
|
+
|
|
86
|
+
if (!shouldWarn) {
|
|
87
|
+
writeState(sessionId, state)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Reset debounce counter
|
|
92
|
+
state.callsSinceWarning = 0
|
|
93
|
+
state.lastSeverity = severity
|
|
94
|
+
writeState(sessionId, state)
|
|
95
|
+
|
|
96
|
+
// Emit warning
|
|
97
|
+
const pct = Math.round(usedPct)
|
|
98
|
+
const remaining = 100 - pct
|
|
99
|
+
|
|
100
|
+
if (severity === 'CRITICAL') {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
`\n⚠️ CONTEXT CRITICAL (${pct}% used, ~${remaining}% remaining)\n` +
|
|
103
|
+
`Context window is nearly exhausted. Inform the user and ask how to proceed.\n` +
|
|
104
|
+
`Consider: summarize progress, commit work, or start a new session.\n`,
|
|
105
|
+
)
|
|
106
|
+
} else {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`\n⚡ CONTEXT WARNING (${pct}% used, ~${remaining}% remaining)\n` +
|
|
109
|
+
`Context is getting limited. Avoid starting new complex work.\n` +
|
|
110
|
+
`Focus on completing current tasks and wrapping up.\n`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch(() => process.exit(0))
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code statusline hook.
|
|
6
|
+
* Reads session JSON from stdin, outputs a formatted statusline,
|
|
7
|
+
* and writes a bridge file for the context monitor.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
// ANSI helpers
|
|
14
|
+
const ESC = '\x1b['
|
|
15
|
+
const RESET = `${ESC}0m`
|
|
16
|
+
const BOLD = `${ESC}1m`
|
|
17
|
+
const DIM = `${ESC}2m`
|
|
18
|
+
const FG = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`
|
|
19
|
+
|
|
20
|
+
// Colors
|
|
21
|
+
const GRAY = FG(100, 100, 100)
|
|
22
|
+
const WHITE = FG(200, 200, 200)
|
|
23
|
+
const GREEN = FG(80, 200, 80)
|
|
24
|
+
const YELLOW = FG(220, 200, 60)
|
|
25
|
+
const ORANGE = FG(230, 140, 40)
|
|
26
|
+
const RED = FG(220, 60, 60)
|
|
27
|
+
const CYAN = FG(80, 200, 220)
|
|
28
|
+
|
|
29
|
+
const SEPARATOR = `${GRAY}\u2502${RESET}`
|
|
30
|
+
|
|
31
|
+
// Autocompact kicks in around 16.5% remaining -- normalize so bar reflects usable context
|
|
32
|
+
const AUTOCOMPACT_BUFFER = 16.5
|
|
33
|
+
|
|
34
|
+
function normalizeUsage(remainingPct) {
|
|
35
|
+
// remaining_percentage goes from 100 (empty) to 0 (full)
|
|
36
|
+
// usable range is 100 down to ~16.5 (autocompact buffer)
|
|
37
|
+
const usableRange = 100 - AUTOCOMPACT_BUFFER
|
|
38
|
+
const usableRemaining = Math.max(0, remainingPct - AUTOCOMPACT_BUFFER)
|
|
39
|
+
const usedPct = ((usableRange - usableRemaining) / usableRange) * 100
|
|
40
|
+
return Math.min(100, Math.max(0, usedPct))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getBarColor(usedPct) {
|
|
44
|
+
if (usedPct < 50) return { fg: GREEN, label: GREEN }
|
|
45
|
+
if (usedPct < 65) return { fg: YELLOW, label: YELLOW }
|
|
46
|
+
if (usedPct < 80) return { fg: ORANGE, label: ORANGE }
|
|
47
|
+
return { fg: RED, label: RED }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildProgressBar(usedPct, segments = 15) {
|
|
51
|
+
const filled = Math.round((usedPct / 100) * segments)
|
|
52
|
+
const empty = segments - filled
|
|
53
|
+
const colors = getBarColor(usedPct)
|
|
54
|
+
const emphasis = usedPct >= 80 ? BOLD : ''
|
|
55
|
+
|
|
56
|
+
const filledBar = `${emphasis}${colors.fg}${'█'.repeat(filled)}${RESET}`
|
|
57
|
+
const emptyBar = `${GRAY}${'░'.repeat(empty)}${RESET}`
|
|
58
|
+
const pctLabel = `${colors.label}${BOLD}${Math.round(usedPct)}%${RESET}`
|
|
59
|
+
|
|
60
|
+
return `${filledBar}${emptyBar} ${pctLabel}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getActiveTask(data) {
|
|
64
|
+
const todos = data.todos || []
|
|
65
|
+
const active = todos.find((t) => t.status === 'in_progress') || todos.find((t) => t.status === 'pending')
|
|
66
|
+
if (!active) return null
|
|
67
|
+
const label = active.content || active.description || ''
|
|
68
|
+
return label.length > 40 ? label.slice(0, 37) + '...' : label
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getUpdateIndicator() {
|
|
72
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude')
|
|
73
|
+
const cachePath = path.join(configDir, 'cache', 'claude-code-pulsify-update.json')
|
|
74
|
+
try {
|
|
75
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'))
|
|
76
|
+
if (cache.updateAvailable && cache.latest) {
|
|
77
|
+
return ` ${CYAN}\u2191${cache.latest}${RESET}`
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// No cache file or invalid — no indicator
|
|
81
|
+
}
|
|
82
|
+
return ''
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getGitBranch(cwd) {
|
|
86
|
+
try {
|
|
87
|
+
const head = fs.readFileSync(path.join(cwd, '.git', 'HEAD'), 'utf8').trim()
|
|
88
|
+
const match = head.match(/^ref: refs\/heads\/(.+)$/)
|
|
89
|
+
return match ? match[1] : null
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeBridgeFile(sessionId, metrics) {
|
|
96
|
+
if (!sessionId) return
|
|
97
|
+
const bridgePath = `/tmp/claude-ctx-${sessionId}.json`
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(bridgePath, JSON.stringify({ ...metrics, timestamp: Date.now() }), 'utf8')
|
|
100
|
+
} catch {
|
|
101
|
+
// Silently ignore write errors
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function main() {
|
|
106
|
+
let input = ''
|
|
107
|
+
for await (const chunk of process.stdin) {
|
|
108
|
+
input += chunk
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let data
|
|
112
|
+
try {
|
|
113
|
+
data = JSON.parse(input)
|
|
114
|
+
} catch {
|
|
115
|
+
process.stdout.write(`${DIM}statusline: no data${RESET}`)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Debug payload dump
|
|
120
|
+
if (process.env.CLAUDE_PULSIFY_DEBUG) {
|
|
121
|
+
try {
|
|
122
|
+
fs.writeFileSync('/tmp/claude-pulsify-debug.json', JSON.stringify(data, null, 2), 'utf8')
|
|
123
|
+
} catch {
|
|
124
|
+
// Silently ignore write errors
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Extract fields
|
|
129
|
+
const model = data.model?.display_name || data.model?.name || 'unknown'
|
|
130
|
+
const cwd = data.workspace?.current_dir || process.cwd()
|
|
131
|
+
const dir = path.basename(cwd)
|
|
132
|
+
const remainingPct = data.context_window?.remaining_percentage ?? 100
|
|
133
|
+
const sessionId = data.session?.id || data.session_id || null
|
|
134
|
+
|
|
135
|
+
// Normalize and build bar
|
|
136
|
+
const usedPct = normalizeUsage(remainingPct)
|
|
137
|
+
const bar = buildProgressBar(usedPct)
|
|
138
|
+
|
|
139
|
+
// Git branch
|
|
140
|
+
const branch = getGitBranch(cwd)
|
|
141
|
+
const dirLabel = branch ? `${DIM}${dir} (${branch})${RESET}` : `${DIM}${dir}${RESET}`
|
|
142
|
+
|
|
143
|
+
// Active task
|
|
144
|
+
const task = getActiveTask(data)
|
|
145
|
+
const taskSegment = task ? ` ${SEPARATOR} ${DIM}${task}${RESET}` : ''
|
|
146
|
+
|
|
147
|
+
// Update indicator
|
|
148
|
+
const updateIndicator = getUpdateIndicator()
|
|
149
|
+
|
|
150
|
+
// Write bridge file for context-monitor
|
|
151
|
+
writeBridgeFile(sessionId, {
|
|
152
|
+
remaining_percentage: remainingPct,
|
|
153
|
+
used_percentage: usedPct,
|
|
154
|
+
session_id: sessionId,
|
|
155
|
+
model,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Output statusline
|
|
159
|
+
const line = `${WHITE}${model}${RESET} ${SEPARATOR} ${dirLabel} ${SEPARATOR} ${bar}${taskSegment}${updateIndicator}`
|
|
160
|
+
process.stdout.write(line)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
main().catch(() => process.exit(0))
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-pulsify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Context-aware statusline and context monitor for Claude Code",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-code-pulsify": "bin/install.js"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/SpitzN/claude-code-pulsify"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"hooks/"
|
|
22
|
+
]
|
|
23
|
+
}
|