claude-code-pulsify 1.0.2 → 1.1.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 +12 -0
- package/bin/install.js +44 -8
- package/hooks/check-update-worker.js +45 -0
- package/hooks/check-update.js +20 -32
- package/hooks/context-monitor.js +19 -7
- package/hooks/statusline.js +80 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,18 @@ npx claude-code-pulsify@latest --uninstall
|
|
|
34
34
|
- **Context monitor** -- Warns at 65% and 75% context usage via PostToolUse hook
|
|
35
35
|
- **Update checker** -- Background version check on session start, non-blocking
|
|
36
36
|
|
|
37
|
+
## Checking your installed version
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx claude-code-pulsify --status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This shows the currently installed version, hooks location, and whether an update is available.
|
|
44
|
+
|
|
45
|
+
You can also check manually:
|
|
46
|
+
- **VERSION file:** `~/.claude/hooks/claude-code-pulsify/VERSION`
|
|
47
|
+
- **Update cache:** `~/.claude/cache/claude-code-pulsify-update.json` — written on each session start, contains `installed`, `latest`, and `updateAvailable` fields
|
|
48
|
+
|
|
37
49
|
## Configuration
|
|
38
50
|
|
|
39
51
|
Respects the `CLAUDE_CONFIG_DIR` environment variable. Defaults to `~/.claude`.
|
package/bin/install.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
6
|
|
|
7
7
|
const PACKAGE_VERSION = require('../package.json').version
|
|
8
8
|
const HOOKS_SOURCE = path.join(__dirname, '..', 'hooks')
|
|
9
|
-
const HOOK_FILES = ['statusline.js', 'context-monitor.js', 'check-update.js']
|
|
9
|
+
const HOOK_FILES = ['statusline.js', 'context-monitor.js', 'check-update.js', 'check-update-worker.js']
|
|
10
10
|
|
|
11
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude')
|
|
11
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('node:os').homedir(), '.claude')
|
|
12
12
|
const hooksTarget = path.join(configDir, 'hooks', 'claude-code-pulsify')
|
|
13
13
|
const settingsPath = path.join(configDir, 'settings.json')
|
|
14
14
|
|
|
@@ -17,8 +17,15 @@ const settingsPath = path.join(configDir, 'settings.json')
|
|
|
17
17
|
function readJSON(filePath) {
|
|
18
18
|
try {
|
|
19
19
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
20
|
-
} catch {
|
|
21
|
-
return {}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === 'ENOENT') return {}
|
|
22
|
+
if (err instanceof SyntaxError) {
|
|
23
|
+
const backupPath = `${filePath}.backup-${Date.now()}`
|
|
24
|
+
console.warn(` ⚠ Parse error in ${filePath} — backing up to ${backupPath}`)
|
|
25
|
+
fs.copyFileSync(filePath, backupPath)
|
|
26
|
+
return {}
|
|
27
|
+
}
|
|
28
|
+
throw err
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -38,7 +45,6 @@ function upsertHookArray(arr, newEntry) {
|
|
|
38
45
|
} else {
|
|
39
46
|
arr.push(newEntry)
|
|
40
47
|
}
|
|
41
|
-
return arr
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
function removeFromHookArray(arr) {
|
|
@@ -63,7 +69,17 @@ function install() {
|
|
|
63
69
|
fs.writeFileSync(path.join(hooksTarget, 'VERSION'), PACKAGE_VERSION, 'utf8')
|
|
64
70
|
console.log(` Wrote VERSION (${PACKAGE_VERSION})`)
|
|
65
71
|
|
|
66
|
-
// 3.
|
|
72
|
+
// 3. Clear update cache so stale "update available" indicators don't persist after install.
|
|
73
|
+
// The background worker will refresh it on next session.
|
|
74
|
+
const cachePath = path.join(configDir, 'cache', 'claude-code-pulsify-update.json')
|
|
75
|
+
try {
|
|
76
|
+
fs.unlinkSync(cachePath)
|
|
77
|
+
console.log(` Cleared update cache`)
|
|
78
|
+
} catch {
|
|
79
|
+
// Doesn't exist — fine
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 4. Patch settings.json
|
|
67
83
|
const settings = readJSON(settingsPath)
|
|
68
84
|
|
|
69
85
|
// statusLine
|
|
@@ -160,6 +176,26 @@ const args = process.argv.slice(2)
|
|
|
160
176
|
|
|
161
177
|
if (args.includes('--version')) {
|
|
162
178
|
console.log(PACKAGE_VERSION)
|
|
179
|
+
} else if (args.includes('--status')) {
|
|
180
|
+
const versionFile = path.join(hooksTarget, 'VERSION')
|
|
181
|
+
const cachePath = path.join(configDir, 'cache', 'claude-code-pulsify-update.json')
|
|
182
|
+
const installed = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : null
|
|
183
|
+
if (!installed) {
|
|
184
|
+
console.log('claude-code-pulsify is not installed.')
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
console.log(`Installed: v${installed}`)
|
|
188
|
+
console.log(`Hooks: ${hooksTarget}`)
|
|
189
|
+
try {
|
|
190
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'))
|
|
191
|
+
if (cache.updateAvailable && cache.latest) {
|
|
192
|
+
console.log(`Latest: v${cache.latest} (update available)`)
|
|
193
|
+
} else {
|
|
194
|
+
console.log(`Latest: v${cache.latest || installed} (up to date)`)
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
console.log(`Latest: unknown (run a Claude Code session to check)`)
|
|
198
|
+
}
|
|
163
199
|
} else if (args.includes('--uninstall')) {
|
|
164
200
|
uninstall()
|
|
165
201
|
} else {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Background worker for checking npm updates.
|
|
6
|
+
* Invoked as a detached process by check-update.js.
|
|
7
|
+
*
|
|
8
|
+
* Expected environment variables:
|
|
9
|
+
* PULSIFY_CACHE_PATH — path to write the update cache JSON
|
|
10
|
+
* PULSIFY_CACHE_DIR — parent directory (created if missing)
|
|
11
|
+
* PULSIFY_INSTALLED — currently installed version string
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('node:child_process')
|
|
15
|
+
const fs = require('node:fs')
|
|
16
|
+
|
|
17
|
+
const NPM_VIEW_TIMEOUT_MS = 10000
|
|
18
|
+
|
|
19
|
+
const cachePath = process.env.PULSIFY_CACHE_PATH
|
|
20
|
+
const cacheDir = process.env.PULSIFY_CACHE_DIR
|
|
21
|
+
const installed = process.env.PULSIFY_INSTALLED
|
|
22
|
+
|
|
23
|
+
if (!cachePath || !cacheDir || !installed) {
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const latest = execSync('npm view claude-code-pulsify version', {
|
|
29
|
+
timeout: NPM_VIEW_TIMEOUT_MS,
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
32
|
+
}).trim()
|
|
33
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
cachePath,
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
installed,
|
|
38
|
+
latest,
|
|
39
|
+
updateAvailable: latest.localeCompare(installed, undefined, { numeric: true }) > 0,
|
|
40
|
+
checkedAt: Date.now(),
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
} catch {
|
|
44
|
+
// Network error or npm not available — silently ignore
|
|
45
|
+
}
|
package/hooks/check-update.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* SessionStart hook -- background version check for claude-code-pulsify.
|
|
6
|
-
* Spawns a detached process to compare installed version vs npm latest,
|
|
6
|
+
* Spawns a detached worker process to compare installed version vs npm latest,
|
|
7
7
|
* writes result to a cache file that statusline.js reads.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { spawn } = require('child_process')
|
|
11
|
-
const fs = require('fs')
|
|
12
|
-
const path = require('path')
|
|
10
|
+
const { spawn } = require('node:child_process')
|
|
11
|
+
const fs = require('node:fs')
|
|
12
|
+
const path = require('node:path')
|
|
13
13
|
|
|
14
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude')
|
|
14
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('node:os').homedir(), '.claude')
|
|
15
15
|
const hooksDir = path.join(configDir, 'hooks', 'claude-code-pulsify')
|
|
16
16
|
const cacheDir = path.join(configDir, 'cache')
|
|
17
17
|
const cachePath = path.join(cacheDir, 'claude-code-pulsify-update.json')
|
|
@@ -32,36 +32,24 @@ async function main() {
|
|
|
32
32
|
return // No VERSION file — not installed properly
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Spawn detached background
|
|
36
|
-
const
|
|
37
|
-
|
|
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.localeCompare(installed, undefined, { numeric: true }) > 0,
|
|
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], {
|
|
35
|
+
// Spawn detached background worker so we don't block session startup
|
|
36
|
+
const workerPath = path.join(__dirname, 'check-update-worker.js')
|
|
37
|
+
const child = spawn(process.execPath, [workerPath], {
|
|
61
38
|
detached: true,
|
|
62
39
|
stdio: 'ignore',
|
|
40
|
+
env: {
|
|
41
|
+
...process.env,
|
|
42
|
+
PULSIFY_CACHE_PATH: cachePath,
|
|
43
|
+
PULSIFY_CACHE_DIR: cacheDir,
|
|
44
|
+
PULSIFY_INSTALLED: installed,
|
|
45
|
+
},
|
|
63
46
|
})
|
|
64
47
|
child.unref()
|
|
65
48
|
}
|
|
66
49
|
|
|
67
|
-
main().catch(() =>
|
|
50
|
+
main().catch((err) => {
|
|
51
|
+
if (process.env.CLAUDE_PULSIFY_DEBUG) {
|
|
52
|
+
process.stderr.write(`[pulsify:check-update] ${err.message}\n`)
|
|
53
|
+
}
|
|
54
|
+
process.exit(0)
|
|
55
|
+
})
|
package/hooks/context-monitor.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* warnings when context is running low.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const fs = require('fs')
|
|
11
|
-
const path = require('path')
|
|
10
|
+
const fs = require('node:fs')
|
|
11
|
+
const path = require('node:path')
|
|
12
12
|
|
|
13
13
|
// Thresholds on "used percentage" (normalized, 0-100)
|
|
14
14
|
const WARNING_THRESHOLD = 65 // remaining ~35% usable
|
|
@@ -17,9 +17,16 @@ const CRITICAL_THRESHOLD = 75 // remaining ~25% usable
|
|
|
17
17
|
// Debounce: minimum tool calls between repeated warnings at the same severity
|
|
18
18
|
const DEBOUNCE_COUNT = 5
|
|
19
19
|
|
|
20
|
+
// Bridge data older than this is considered stale and ignored
|
|
21
|
+
const BRIDGE_STALE_MS = 60_000
|
|
22
|
+
|
|
23
|
+
function sanitizeId(id) {
|
|
24
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
// State file to track debounce across invocations
|
|
21
28
|
function getStateFile(sessionId) {
|
|
22
|
-
return `/tmp/claude-ctx-monitor-state-${sessionId}.json`
|
|
29
|
+
return `/tmp/claude-ctx-monitor-state-${sanitizeId(sessionId)}.json`
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
function readState(sessionId) {
|
|
@@ -55,7 +62,7 @@ async function main() {
|
|
|
55
62
|
if (!sessionId) return
|
|
56
63
|
|
|
57
64
|
// Read bridge file from statusline
|
|
58
|
-
const bridgePath = `/tmp/claude-ctx-${sessionId}.json`
|
|
65
|
+
const bridgePath = `/tmp/claude-ctx-${sanitizeId(sessionId)}.json`
|
|
59
66
|
let metrics
|
|
60
67
|
try {
|
|
61
68
|
metrics = JSON.parse(fs.readFileSync(bridgePath, 'utf8'))
|
|
@@ -63,8 +70,8 @@ async function main() {
|
|
|
63
70
|
return // No bridge file yet -- statusline hasn't run
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
// Check staleness
|
|
67
|
-
if (Date.now() - (metrics.timestamp || 0) >
|
|
73
|
+
// Check staleness
|
|
74
|
+
if (Date.now() - (metrics.timestamp || 0) > BRIDGE_STALE_MS) return
|
|
68
75
|
|
|
69
76
|
const usedPct = metrics.used_percentage
|
|
70
77
|
if (usedPct == null) return
|
|
@@ -112,4 +119,9 @@ async function main() {
|
|
|
112
119
|
}
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
main().catch(() =>
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
if (process.env.CLAUDE_PULSIFY_DEBUG) {
|
|
124
|
+
process.stderr.write(`[pulsify:context-monitor] ${err.message}\n`)
|
|
125
|
+
}
|
|
126
|
+
process.exit(0)
|
|
127
|
+
})
|
package/hooks/statusline.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* and writes a bridge file for the context monitor.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const fs = require('fs')
|
|
11
|
-
const path = require('path')
|
|
10
|
+
const fs = require('node:fs')
|
|
11
|
+
const path = require('node:path')
|
|
12
12
|
|
|
13
13
|
// ANSI helpers
|
|
14
14
|
const ESC = '\x1b['
|
|
@@ -28,9 +28,18 @@ const CYAN = FG(80, 200, 220)
|
|
|
28
28
|
|
|
29
29
|
const SEPARATOR = `${GRAY}\u2502${RESET}`
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const PROGRESS_BAR_SEGMENTS = 15
|
|
32
|
+
const MAX_TASK_LABEL_LENGTH = 40
|
|
33
|
+
|
|
34
|
+
// Claude's autocompact kicks in around 16.5% remaining context.
|
|
35
|
+
// We normalize the bar so 0% = empty, 100% = autocompact threshold,
|
|
36
|
+
// giving users a view of their *usable* context rather than total.
|
|
32
37
|
const AUTOCOMPACT_BUFFER = 16.5
|
|
33
38
|
|
|
39
|
+
function sanitizeId(id) {
|
|
40
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
function normalizeUsage(remainingPct) {
|
|
35
44
|
// remaining_percentage goes from 100 (empty) to 0 (full)
|
|
36
45
|
// usable range is 100 down to ~16.5 (autocompact buffer)
|
|
@@ -41,21 +50,21 @@ function normalizeUsage(remainingPct) {
|
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
function getBarColor(usedPct) {
|
|
44
|
-
if (usedPct < 50) return
|
|
45
|
-
if (usedPct < 65) return
|
|
46
|
-
if (usedPct < 80) return
|
|
47
|
-
return
|
|
53
|
+
if (usedPct < 50) return GREEN
|
|
54
|
+
if (usedPct < 65) return YELLOW
|
|
55
|
+
if (usedPct < 80) return ORANGE
|
|
56
|
+
return RED
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
function buildProgressBar(usedPct
|
|
51
|
-
const filled = Math.round((usedPct / 100) *
|
|
52
|
-
const empty =
|
|
53
|
-
const
|
|
59
|
+
function buildProgressBar(usedPct) {
|
|
60
|
+
const filled = Math.round((usedPct / 100) * PROGRESS_BAR_SEGMENTS)
|
|
61
|
+
const empty = PROGRESS_BAR_SEGMENTS - filled
|
|
62
|
+
const color = getBarColor(usedPct)
|
|
54
63
|
const emphasis = usedPct >= 80 ? BOLD : ''
|
|
55
64
|
|
|
56
|
-
const filledBar = `${emphasis}${
|
|
65
|
+
const filledBar = `${emphasis}${color}${'█'.repeat(filled)}${RESET}`
|
|
57
66
|
const emptyBar = `${GRAY}${'░'.repeat(empty)}${RESET}`
|
|
58
|
-
const pctLabel = `${
|
|
67
|
+
const pctLabel = `${color}${BOLD}${Math.round(usedPct)}%${RESET}`
|
|
59
68
|
|
|
60
69
|
return `${filledBar}${emptyBar} ${pctLabel}`
|
|
61
70
|
}
|
|
@@ -65,11 +74,11 @@ function getActiveTask(data) {
|
|
|
65
74
|
const active = todos.find((t) => t.status === 'in_progress') || todos.find((t) => t.status === 'pending')
|
|
66
75
|
if (!active) return null
|
|
67
76
|
const label = active.content || active.description || ''
|
|
68
|
-
return label.length >
|
|
77
|
+
return label.length > MAX_TASK_LABEL_LENGTH ? label.slice(0, MAX_TASK_LABEL_LENGTH - 3) + '...' : label
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
function getUpdateIndicator() {
|
|
72
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('os').homedir(), '.claude')
|
|
81
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('node:os').homedir(), '.claude')
|
|
73
82
|
const cachePath = path.join(configDir, 'cache', 'claude-code-pulsify-update.json')
|
|
74
83
|
try {
|
|
75
84
|
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'))
|
|
@@ -94,14 +103,41 @@ function getGitBranch(cwd) {
|
|
|
94
103
|
|
|
95
104
|
function writeBridgeFile(sessionId, metrics) {
|
|
96
105
|
if (!sessionId) return
|
|
97
|
-
const
|
|
106
|
+
const safeId = sanitizeId(sessionId)
|
|
107
|
+
const bridgePath = `/tmp/claude-ctx-${safeId}.json`
|
|
108
|
+
const tmpPath = `${bridgePath}.tmp`
|
|
98
109
|
try {
|
|
99
|
-
fs.writeFileSync(
|
|
110
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ ...metrics, timestamp: Date.now() }), 'utf8')
|
|
111
|
+
fs.renameSync(tmpPath, bridgePath)
|
|
100
112
|
} catch {
|
|
101
113
|
// Silently ignore write errors
|
|
102
114
|
}
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
function formatCost(costUsd) {
|
|
118
|
+
if (!costUsd) return ''
|
|
119
|
+
if (costUsd < 1) return `${WHITE}$${costUsd.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')}${RESET}`
|
|
120
|
+
const str = costUsd.toFixed(1).replace(/\.0$/, '')
|
|
121
|
+
return `${WHITE}$${str}${RESET}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatLinesChanged(added, removed) {
|
|
125
|
+
const parts = []
|
|
126
|
+
if (added) parts.push(`${GREEN}+${added}${RESET}`)
|
|
127
|
+
if (removed) parts.push(`${RED}-${removed}${RESET}`)
|
|
128
|
+
return parts.join(' ')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatTokenCount(input, output) {
|
|
132
|
+
const total = (input || 0) + (output || 0)
|
|
133
|
+
if (!total) return ''
|
|
134
|
+
let compact
|
|
135
|
+
if (total >= 1e6) compact = `${(total / 1e6).toFixed(1)}M`
|
|
136
|
+
else if (total >= 1e3) compact = `${(total / 1e3).toFixed(1)}k`
|
|
137
|
+
else compact = `${total}`
|
|
138
|
+
return `${DIM}${compact} tok${RESET}`
|
|
139
|
+
}
|
|
140
|
+
|
|
105
141
|
async function main() {
|
|
106
142
|
let input = ''
|
|
107
143
|
for await (const chunk of process.stdin) {
|
|
@@ -141,6 +177,11 @@ async function main() {
|
|
|
141
177
|
const dir = path.basename(cwd)
|
|
142
178
|
const remainingPct = data.context_window?.remaining_percentage ?? 100
|
|
143
179
|
const sessionId = data.session?.id || data.session_id || null
|
|
180
|
+
const costUsd = data.cost?.total_cost_usd ?? 0
|
|
181
|
+
const linesAdded = data.cost?.total_lines_added ?? 0
|
|
182
|
+
const linesRemoved = data.cost?.total_lines_removed ?? 0
|
|
183
|
+
const totalInputTokens = data.context_window?.total_input_tokens ?? 0
|
|
184
|
+
const totalOutputTokens = data.context_window?.total_output_tokens ?? 0
|
|
144
185
|
|
|
145
186
|
// Normalize and build bar
|
|
146
187
|
const usedPct = normalizeUsage(remainingPct)
|
|
@@ -165,9 +206,29 @@ async function main() {
|
|
|
165
206
|
model,
|
|
166
207
|
})
|
|
167
208
|
|
|
209
|
+
// Format new segments
|
|
210
|
+
const cost = formatCost(costUsd)
|
|
211
|
+
const lines = formatLinesChanged(linesAdded, linesRemoved)
|
|
212
|
+
const tokenCount = formatTokenCount(totalInputTokens, totalOutputTokens)
|
|
213
|
+
|
|
214
|
+
// Build segments array (only include non-empty optional segments)
|
|
215
|
+
const segments = [
|
|
216
|
+
`${WHITE}${model}${RESET}`,
|
|
217
|
+
dirLabel,
|
|
218
|
+
]
|
|
219
|
+
if (cost) segments.push(cost)
|
|
220
|
+
if (lines) segments.push(lines)
|
|
221
|
+
const barWithTokens = tokenCount ? `${bar} ${tokenCount}` : bar
|
|
222
|
+
segments.push(barWithTokens)
|
|
223
|
+
|
|
168
224
|
// Output statusline
|
|
169
|
-
const line =
|
|
225
|
+
const line = segments.join(` ${SEPARATOR} `) + taskSegment + updateIndicator
|
|
170
226
|
process.stdout.write(line)
|
|
171
227
|
}
|
|
172
228
|
|
|
173
|
-
main().catch(() =>
|
|
229
|
+
main().catch((err) => {
|
|
230
|
+
if (process.env.CLAUDE_PULSIFY_DEBUG) {
|
|
231
|
+
process.stderr.write(`[pulsify:statusline] ${err.message}\n`)
|
|
232
|
+
}
|
|
233
|
+
process.exit(0)
|
|
234
|
+
})
|