claude-code-pulsify 1.2.1 → 1.3.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/bin/install.js CHANGED
@@ -4,6 +4,8 @@
4
4
  const fs = require('node:fs')
5
5
  const path = require('node:path')
6
6
 
7
+ const { isOurEntry, upsertHookArray, removeFromHookArray } = require('../lib/install')
8
+
7
9
  const PACKAGE_VERSION = require('../package.json').version
8
10
  const HOOKS_SOURCE = path.join(__dirname, '..', 'hooks')
9
11
  const HOOK_FILES = ['statusline.js', 'context-monitor.js', 'check-update.js', 'check-update-worker.js']
@@ -33,24 +35,6 @@ function writeJSON(filePath, data) {
33
35
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8')
34
36
  }
35
37
 
36
- function isOurEntry(entry) {
37
- const cmd = entry?.hooks?.[0]?.command || ''
38
- return cmd.includes('claude-code-pulsify')
39
- }
40
-
41
- function upsertHookArray(arr, newEntry) {
42
- const idx = arr.findIndex(isOurEntry)
43
- if (idx >= 0) {
44
- arr[idx] = newEntry
45
- } else {
46
- arr.push(newEntry)
47
- }
48
- }
49
-
50
- function removeFromHookArray(arr) {
51
- return arr.filter((entry) => !isOurEntry(entry))
52
- }
53
-
54
38
  // --- Install ---
55
39
 
56
40
  function install() {
@@ -8,21 +8,9 @@
8
8
  */
9
9
 
10
10
  const fs = require('node:fs')
11
- const path = require('node:path')
12
11
 
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
- // 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
- }
12
+ const { sanitizeId } = require('../lib/statusline')
13
+ const { BRIDGE_STALE_MS, getSeverity, shouldFireWarning } = require('../lib/context')
26
14
 
27
15
  // State file to track debounce across invocations
28
16
  function getStateFile(sessionId) {
@@ -78,20 +66,14 @@ async function main() {
78
66
  if (usedPct == null) return
79
67
 
80
68
  // Determine severity
81
- let severity = null
82
- if (usedPct >= CRITICAL_THRESHOLD) severity = 'CRITICAL'
83
- else if (usedPct >= WARNING_THRESHOLD) severity = 'WARNING'
84
-
69
+ const severity = getSeverity(usedPct)
85
70
  if (!severity) return // Context is fine
86
71
 
87
72
  // Debounce logic
88
73
  const state = readState(sessionId)
89
74
  state.callsSinceWarning = (state.callsSinceWarning || 0) + 1
90
75
 
91
- const isEscalation = severity === 'CRITICAL' && state.lastSeverity === 'WARNING'
92
- const shouldWarn = isEscalation || state.callsSinceWarning >= DEBOUNCE_COUNT
93
-
94
- if (!shouldWarn) {
76
+ if (!shouldFireWarning(state.callsSinceWarning, severity, state.lastSeverity)) {
95
77
  writeState(sessionId, state)
96
78
  return
97
79
  }
@@ -10,72 +10,10 @@
10
10
  const fs = require('node:fs')
11
11
  const path = require('node:path')
12
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
- 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.
37
- const AUTOCOMPACT_BUFFER = 16.5
38
-
39
- function sanitizeId(id) {
40
- return id.replace(/[^a-zA-Z0-9_-]/g, '')
41
- }
42
-
43
- function normalizeUsage(remainingPct) {
44
- // remaining_percentage goes from 100 (empty) to 0 (full)
45
- // usable range is 100 down to ~16.5 (autocompact buffer)
46
- const usableRange = 100 - AUTOCOMPACT_BUFFER
47
- const usableRemaining = Math.max(0, remainingPct - AUTOCOMPACT_BUFFER)
48
- const usedPct = ((usableRange - usableRemaining) / usableRange) * 100
49
- return Math.min(100, Math.max(0, usedPct))
50
- }
51
-
52
- function getBarColor(usedPct) {
53
- if (usedPct < 50) return GREEN
54
- if (usedPct < 65) return YELLOW
55
- if (usedPct < 80) return ORANGE
56
- return RED
57
- }
58
-
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)
63
- const emphasis = usedPct >= 80 ? BOLD : ''
64
-
65
- const filledBar = `${emphasis}${color}${'█'.repeat(filled)}${RESET}`
66
- const emptyBar = `${GRAY}${'░'.repeat(empty)}${RESET}`
67
- const pctLabel = `${color}${BOLD}${Math.round(usedPct)}%${RESET}`
68
-
69
- return `${filledBar}${emptyBar} ${pctLabel}`
70
- }
71
-
72
- function getActiveTask(data) {
73
- const todos = data.todos || []
74
- const active = todos.find((t) => t.status === 'in_progress') || todos.find((t) => t.status === 'pending')
75
- if (!active) return null
76
- const label = active.content || active.description || ''
77
- return label.length > MAX_TASK_LABEL_LENGTH ? label.slice(0, MAX_TASK_LABEL_LENGTH - 3) + '...' : label
78
- }
13
+ const {
14
+ RESET, DIM, WHITE, CYAN, SEPARATOR,
15
+ sanitizeId, normalizeUsage, buildProgressBar, getActiveTask, formatCost,
16
+ } = require('../lib/statusline')
79
17
 
80
18
  function getUpdateIndicator() {
81
19
  const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(require('node:os').homedir(), '.claude')
@@ -114,14 +52,6 @@ function writeBridgeFile(sessionId, metrics) {
114
52
  }
115
53
  }
116
54
 
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
-
125
55
  async function main() {
126
56
  let input = ''
127
57
  await Promise.race([
package/lib/context.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ // Thresholds on "used percentage" (normalized, 0-100)
4
+ const WARNING_THRESHOLD = 65 // remaining ~35% usable
5
+ const CRITICAL_THRESHOLD = 75 // remaining ~25% usable
6
+
7
+ // Debounce: minimum tool calls between repeated warnings at the same severity
8
+ const DEBOUNCE_COUNT = 5
9
+
10
+ // Bridge data older than this is considered stale and ignored
11
+ const BRIDGE_STALE_MS = 60_000
12
+
13
+ function getSeverity(usedPct) {
14
+ if (usedPct >= CRITICAL_THRESHOLD) return 'CRITICAL'
15
+ if (usedPct >= WARNING_THRESHOLD) return 'WARNING'
16
+ return null
17
+ }
18
+
19
+ function shouldFireWarning(callsSinceWarning, severity, lastSeverity) {
20
+ const isEscalation = severity === 'CRITICAL' && lastSeverity === 'WARNING'
21
+ return isEscalation || callsSinceWarning >= DEBOUNCE_COUNT
22
+ }
23
+
24
+ module.exports = {
25
+ WARNING_THRESHOLD,
26
+ CRITICAL_THRESHOLD,
27
+ DEBOUNCE_COUNT,
28
+ BRIDGE_STALE_MS,
29
+ getSeverity,
30
+ shouldFireWarning,
31
+ }
package/lib/install.js ADDED
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ function isOurEntry(entry) {
4
+ const cmd = entry?.hooks?.[0]?.command || ''
5
+ return cmd.includes('claude-code-pulsify')
6
+ }
7
+
8
+ function upsertHookArray(arr, newEntry) {
9
+ const idx = arr.findIndex(isOurEntry)
10
+ if (idx >= 0) {
11
+ arr[idx] = newEntry
12
+ } else {
13
+ arr.push(newEntry)
14
+ }
15
+ }
16
+
17
+ function removeFromHookArray(arr) {
18
+ return arr.filter((entry) => !isOurEntry(entry))
19
+ }
20
+
21
+ module.exports = { isOurEntry, upsertHookArray, removeFromHookArray }
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ // ANSI helpers
4
+ const ESC = '\x1b['
5
+ const RESET = `${ESC}0m`
6
+ const BOLD = `${ESC}1m`
7
+ const DIM = `${ESC}2m`
8
+ const FG = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`
9
+
10
+ // Colors
11
+ const GRAY = FG(100, 100, 100)
12
+ const WHITE = FG(200, 200, 200)
13
+ const GREEN = FG(80, 200, 80)
14
+ const YELLOW = FG(220, 200, 60)
15
+ const ORANGE = FG(230, 140, 40)
16
+ const RED = FG(220, 60, 60)
17
+ const CYAN = FG(80, 200, 220)
18
+
19
+ const SEPARATOR = `${GRAY}\u2502${RESET}`
20
+
21
+ const PROGRESS_BAR_SEGMENTS = 15
22
+ const MAX_TASK_LABEL_LENGTH = 40
23
+
24
+ // Claude's autocompact kicks in around 16.5% remaining context.
25
+ // We normalize the bar so 0% = empty, 100% = autocompact threshold,
26
+ // giving users a view of their *usable* context rather than total.
27
+ const AUTOCOMPACT_BUFFER = 16.5
28
+
29
+ function sanitizeId(id) {
30
+ return id.replace(/[^a-zA-Z0-9_-]/g, '')
31
+ }
32
+
33
+ function normalizeUsage(remainingPct) {
34
+ // remaining_percentage goes from 100 (empty) to 0 (full)
35
+ // usable range is 100 down to ~16.5 (autocompact buffer)
36
+ const usableRange = 100 - AUTOCOMPACT_BUFFER
37
+ const usableRemaining = Math.max(0, remainingPct - AUTOCOMPACT_BUFFER)
38
+ const usedPct = ((usableRange - usableRemaining) / usableRange) * 100
39
+ return Math.min(100, Math.max(0, usedPct))
40
+ }
41
+
42
+ function getBarColor(usedPct) {
43
+ if (usedPct < 50) return GREEN
44
+ if (usedPct < 65) return YELLOW
45
+ if (usedPct < 80) return ORANGE
46
+ return RED
47
+ }
48
+
49
+ function buildProgressBar(usedPct) {
50
+ const filled = Math.round((usedPct / 100) * PROGRESS_BAR_SEGMENTS)
51
+ const empty = PROGRESS_BAR_SEGMENTS - filled
52
+ const color = getBarColor(usedPct)
53
+ const emphasis = usedPct >= 80 ? BOLD : ''
54
+
55
+ const filledBar = `${emphasis}${color}${'█'.repeat(filled)}${RESET}`
56
+ const emptyBar = `${GRAY}${'░'.repeat(empty)}${RESET}`
57
+ const pctLabel = `${color}${BOLD}${Math.round(usedPct)}%${RESET}`
58
+
59
+ return `${filledBar}${emptyBar} ${pctLabel}`
60
+ }
61
+
62
+ function getActiveTask(data) {
63
+ const todos = data.todos || []
64
+ const active = todos.find((t) => t.status === 'in_progress') || todos.find((t) => t.status === 'pending')
65
+ if (!active) return null
66
+ const label = active.content || active.description || ''
67
+ return label.length > MAX_TASK_LABEL_LENGTH ? label.slice(0, MAX_TASK_LABEL_LENGTH - 3) + '...' : label
68
+ }
69
+
70
+ function formatCost(costUsd) {
71
+ if (!costUsd) return ''
72
+ if (costUsd < 1) return `${WHITE}$${costUsd.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')}${RESET}`
73
+ const str = costUsd.toFixed(1).replace(/\.0$/, '')
74
+ return `${WHITE}$${str}${RESET}`
75
+ }
76
+
77
+ module.exports = {
78
+ // ANSI helpers
79
+ RESET, DIM, WHITE, CYAN,
80
+ SEPARATOR,
81
+ // Functions
82
+ sanitizeId, normalizeUsage, getBarColor, buildProgressBar, getActiveTask, formatCost,
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-pulsify",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Context-aware statusline and context monitor for Claude Code",
5
5
  "bin": {
6
6
  "claude-code-pulsify": "bin/install.js"
@@ -12,12 +12,16 @@
12
12
  "engines": {
13
13
  "node": ">=20"
14
14
  },
15
+ "scripts": {
16
+ "test": "node --test 'tests/*.test.js'"
17
+ },
15
18
  "license": "MIT",
16
19
  "publishConfig": {
17
20
  "access": "public"
18
21
  },
19
22
  "files": [
20
23
  "bin/",
21
- "hooks/"
24
+ "hooks/",
25
+ "lib/"
22
26
  ]
23
27
  }