claude-discover 1.10.4 → 1.10.6

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.
@@ -1,125 +1,125 @@
1
- #!/usr/bin/env node
2
- // Claude Code status line, installed by StatuslineSwitch. Reads the hook JSON from stdin and
3
- // prints one colored line — model and effort, context usage, token totals, rate-limit windows:
4
- // [Opus 4.8 high] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) | Daily: 42% used (resets in 2h 30m)
5
-
6
- import { readFileSync } from 'node:fs'
7
- import { styleText } from 'node:util'
8
-
9
-
10
- // Formatting
11
- const THRESHOLDS = { // (white, yellow, red)
12
- context: [0, 100_000, 250_000],
13
- limit: [0, 60, 90],
14
- cache: [90, 60, 0],
15
- tokens: [0, 250_000, 1_000_000],
16
- session: [0, 3_000_000, 8_000_000],
17
- turns: [0, 10, 25],
18
- }
19
-
20
- const NO_COLOR = process.env.NO_COLOR !== undefined // standard opt-out; styleText can't honor it for piped output, so gate here
21
-
22
- function colorize(kind, text, value = 0) {
23
- const bounds = THRESHOLDS[kind]
24
- const n = bounds.filter(b => value >= b).length
25
- const TIERS = [null, 'yellow', 'red'] // indexed by severity tier
26
- const idx = bounds[0] < bounds.at(-1) ? n - 1 : bounds.length - n
27
- const color = TIERS[idx]
28
- // validateStream:false forces codes through the pipe (statusline output isn't a TTY)
29
- return color && !NO_COLOR ? styleText(color, text, { validateStream: false }) : text
30
- }
31
-
32
-
33
- // Collect stdin data
34
- const data = JSON.parse(readFileSync(0, 'utf-8')) // fd 0 = stdin
35
- const modelName = data.model?.display_name?.replace('Claude ', '') || '?'
36
- const effort = data.effort?.level // absent when the model doesn't support effort
37
- const parts = []
38
-
39
-
40
-
41
- // Context window
42
- const ctx = data.context_window || {}
43
- const total = ctx.context_window_size || 0
44
- if (total) {
45
- const usage = ctx.current_usage || {}
46
- const hit = usage.cache_read_input_tokens || 0
47
- const used = hit + (usage.cache_creation_input_tokens || 0) + (usage.input_tokens || 0)
48
- const pct = Math.round((used / total) * 100)
49
- const cpct = used ? Math.floor((hit * 100) / used) : 0
50
- const filled = Math.floor(pct / 10)
51
- const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled)
52
- const head = colorize('context', `${bar} ${pct}% used`, used)
53
- const tokens = colorize('context', `${(used / 1000).toFixed(1)}k`, used)
54
- const cached = used ? colorize('cache', `${cpct}% cached`, cpct) : `${cpct}% cached`
55
- parts.push(`Context: ${head} (${tokens}, ${cached})`)
56
- }
57
-
58
-
59
-
60
- // Total tokens (per session and per reply loop)
61
- function agentLoopUsage(transcriptPath) { // Read the transcript JSONL and sum API usage for the current agent loop and the whole session.
62
- const lines = readFileSync(transcriptPath, 'utf-8').split(/\r?\n/).filter(Boolean)
63
- let loop = { total: 0, turns: 0 } // last prompt-reply breakdown
64
- const session = { total: 0, turns: 0 } // whole-session breakdown
65
- let seen = new Set() // One API response spans multiple lines (one per content block), each repeating the usage block, so dedupe by message.id.
66
-
67
- for (const line of lines) {
68
- let e
69
- try { e = JSON.parse(line) } catch { continue } // the transcript may be mid-append — skip a half-written line
70
- const msg = e.message || {}
71
-
72
- if (e.type === 'user' && !e.isMeta) {
73
- const isToolReply = Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')
74
- if (!isToolReply) { // real user prompt = new loop
75
- loop = { total: 0, turns: 0 }
76
- seen = new Set()
77
- }
78
- } else if (e.type === 'assistant' && !seen.has(msg.id)) {
79
- seen.add(msg.id)
80
- const u = msg.usage || {}
81
- const tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
82
- for (const d of [loop, session]) {
83
- d.total += tokens
84
- d.turns += 1
85
- }
86
- }
87
- }
88
- return [loop, session]
89
- }
90
-
91
- if (data.transcript_path) {
92
- const [loop, session] = agentLoopUsage(data.transcript_path)
93
- const fmt = n => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${(n / 1000).toFixed(1)}k`
94
- const tokensS = colorize('tokens', fmt(loop.total), loop.total)
95
- const sessS = colorize('session', fmt(session.total), session.total)
96
- const turnsS = colorize('turns', `${loop.turns} turn${loop.turns !== 1 ? 's' : ''}`, loop.turns)
97
- parts.push(`Tokens: ${sessS} total (+${tokensS}, ${turnsS})`)
98
- }
99
-
100
-
101
-
102
- // Rate limits (5-hour and weekly windows)
103
- function fmtReset(resetsAt) { // Format remaining time until a unix-epoch reset, e.g. "2h 30m" (null if no timestamp).
104
- if (!resetsAt) return null
105
- const secs = Math.max(0, Math.floor(Number(resetsAt)) - Math.floor(Date.now() / 1000))
106
- const min = Math.floor(secs / 60) % 60
107
- const hrs = Math.floor(secs / 3600) % 24
108
- const days = Math.floor(secs / 86400)
109
- if (days) return `${days}d ${hrs}h`
110
- if (hrs) return `${hrs}h ${min}m`
111
- return `${min}m`
112
- }
113
-
114
- const rl = data.rate_limits || {}
115
- for (const [label, window] of [['Daily', rl.five_hour], ['Weekly', rl.seven_day]]) {
116
- if (!window) continue
117
- const pct = Math.floor(window.used_percentage || 0)
118
- const reset = fmtReset(window.resets_at)
119
- const resetStr = reset ? ` (resets in ${reset})` : ''
120
- parts.push(`${label}: ${colorize('limit', `${pct}% used${resetStr}`, pct)}`)
121
- }
122
-
123
-
124
- // Status line
125
- process.stdout.write(`[${modelName}${effort ? ` ${effort}` : ''}] ` + parts.join(' | ') + '\n')
1
+ #!/usr/bin/env node
2
+ // Claude Code status line, installed by StatuslineSwitch. Reads the hook JSON from stdin and
3
+ // prints one colored line — model and effort, context usage, token totals, rate-limit windows:
4
+ // [Opus 4.8 high] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) | Daily: 42% used (resets in 2h 30m)
5
+
6
+ import { readFileSync } from 'node:fs'
7
+ import { styleText } from 'node:util'
8
+
9
+
10
+ // Formatting
11
+ const THRESHOLDS = { // (white, yellow, red)
12
+ context: [0, 100_000, 250_000],
13
+ limit: [0, 60, 90], // keep in sync with src/renderer/utils/thresholds.js
14
+ cache: [90, 60, 0],
15
+ tokens: [0, 250_000, 1_000_000],
16
+ session: [0, 3_000_000, 8_000_000],
17
+ turns: [0, 10, 25],
18
+ }
19
+
20
+ const NO_COLOR = process.env.NO_COLOR !== undefined // standard opt-out; styleText can't honor it for piped output, so gate here
21
+
22
+ function colorize(kind, text, value = 0) {
23
+ const bounds = THRESHOLDS[kind]
24
+ const n = bounds.filter(b => value >= b).length
25
+ const TIERS = [null, 'yellow', 'red'] // indexed by severity tier
26
+ const idx = bounds[0] < bounds.at(-1) ? n - 1 : bounds.length - n
27
+ const color = TIERS[idx]
28
+ // validateStream:false forces codes through the pipe (statusline output isn't a TTY)
29
+ return color && !NO_COLOR ? styleText(color, text, { validateStream: false }) : text
30
+ }
31
+
32
+
33
+ // Collect stdin data
34
+ const data = JSON.parse(readFileSync(0, 'utf-8')) // fd 0 = stdin
35
+ const modelName = data.model?.display_name?.replace('Claude ', '') || '?'
36
+ const effort = data.effort?.level // absent when the model doesn't support effort
37
+ const parts = []
38
+
39
+
40
+
41
+ // Context window
42
+ const ctx = data.context_window || {}
43
+ const total = ctx.context_window_size || 0
44
+ if (total) {
45
+ const usage = ctx.current_usage || {}
46
+ const hit = usage.cache_read_input_tokens || 0
47
+ const used = hit + (usage.cache_creation_input_tokens || 0) + (usage.input_tokens || 0)
48
+ const pct = Math.round((used / total) * 100)
49
+ const cpct = used ? Math.floor((hit * 100) / used) : 0
50
+ const filled = Math.floor(pct / 10)
51
+ const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled)
52
+ const head = colorize('context', `${bar} ${pct}% used`, used)
53
+ const tokens = colorize('context', `${(used / 1000).toFixed(1)}k`, used)
54
+ const cached = used ? colorize('cache', `${cpct}% cached`, cpct) : `${cpct}% cached`
55
+ parts.push(`Context: ${head} (${tokens}, ${cached})`)
56
+ }
57
+
58
+
59
+
60
+ // Total tokens (per session and per reply loop)
61
+ function agentLoopUsage(transcriptPath) { // Read the transcript JSONL and sum API usage for the current agent loop and the whole session.
62
+ const lines = readFileSync(transcriptPath, 'utf-8').split(/\r?\n/).filter(Boolean)
63
+ let loop = { total: 0, turns: 0 } // last prompt-reply breakdown
64
+ const session = { total: 0, turns: 0 } // whole-session breakdown
65
+ let seen = new Set() // One API response spans multiple lines (one per content block), each repeating the usage block, so dedupe by message.id.
66
+
67
+ for (const line of lines) {
68
+ let e
69
+ try { e = JSON.parse(line) } catch { continue } // the transcript may be mid-append — skip a half-written line
70
+ const msg = e.message || {}
71
+
72
+ if (e.type === 'user' && !e.isMeta) {
73
+ const isToolReply = Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')
74
+ if (!isToolReply) { // real user prompt = new loop
75
+ loop = { total: 0, turns: 0 }
76
+ seen = new Set()
77
+ }
78
+ } else if (e.type === 'assistant' && !seen.has(msg.id)) {
79
+ seen.add(msg.id)
80
+ const u = msg.usage || {}
81
+ const tokens = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0)
82
+ for (const d of [loop, session]) {
83
+ d.total += tokens
84
+ d.turns += 1
85
+ }
86
+ }
87
+ }
88
+ return [loop, session]
89
+ }
90
+
91
+ if (data.transcript_path) {
92
+ const [loop, session] = agentLoopUsage(data.transcript_path)
93
+ const fmt = n => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${(n / 1000).toFixed(1)}k`
94
+ const tokensS = colorize('tokens', fmt(loop.total), loop.total)
95
+ const sessS = colorize('session', fmt(session.total), session.total)
96
+ const turnsS = colorize('turns', `${loop.turns} turn${loop.turns !== 1 ? 's' : ''}`, loop.turns)
97
+ parts.push(`Tokens: ${sessS} total (+${tokensS}, ${turnsS})`)
98
+ }
99
+
100
+
101
+
102
+ // Rate limits (5-hour and weekly windows)
103
+ function fmtReset(resetsAt) { // Format remaining time until a unix-epoch reset, e.g. "2h 30m" (null if no timestamp).
104
+ if (!resetsAt) return null
105
+ const secs = Math.max(0, Math.floor(Number(resetsAt)) - Math.floor(Date.now() / 1000))
106
+ const min = Math.floor(secs / 60) % 60
107
+ const hrs = Math.floor(secs / 3600) % 24
108
+ const days = Math.floor(secs / 86400)
109
+ if (days) return `${days}d ${hrs}h`
110
+ if (hrs) return `${hrs}h ${min}m`
111
+ return `${min}m`
112
+ }
113
+
114
+ const rl = data.rate_limits || {}
115
+ for (const [label, window] of [['Daily', rl.five_hour], ['Weekly', rl.seven_day]]) {
116
+ if (!window) continue
117
+ const pct = Math.floor(window.used_percentage || 0)
118
+ const reset = fmtReset(window.resets_at)
119
+ const resetStr = reset ? ` (resets in ${reset})` : ''
120
+ parts.push(`${label}: ${colorize('limit', `${pct}% used${resetStr}`, pct)}`)
121
+ }
122
+
123
+
124
+ // Status line
125
+ process.stdout.write(`[${modelName}${effort ? ` ${effort}` : ''}] ` + parts.join(' | ') + '\n')
@@ -3,8 +3,8 @@
3
3
  // Electron binary at the package root, whose package.json main is out/main/main.js.
4
4
  import { spawn } from 'node:child_process'
5
5
  import { existsSync } from 'node:fs'
6
+ import { access, constants } from 'node:fs/promises'
6
7
  import { fileURLToPath } from 'node:url'
7
- import electron from 'electron' // under plain node the electron package exports its binary path
8
8
 
9
9
  const appDir = fileURLToPath(new URL('..', import.meta.url))
10
10
  if (!existsSync(new URL('../out/main/main.js', import.meta.url))) {
@@ -12,5 +12,36 @@ if (!existsSync(new URL('../out/main/main.js', import.meta.url))) {
12
12
  process.exit(1)
13
13
  }
14
14
 
15
+
16
+ if (process.platform === 'linux') { // @linux
17
+ // Importing electron downloads its binary on first run, which can't unzip into a root-owned `sudo npm i -g` install,
18
+ // so it's imported dynamically, only after this check (a static import is hoisted and would run first)
19
+
20
+ const electronDir = new URL('.', import.meta.resolve('electron'))
21
+ const isWritable = await access(electronDir, constants.W_OK).then(() => true, () => false)
22
+ if (!existsSync(new URL('path.txt', electronDir)) && !isWritable) {
23
+ console.error(`
24
+ Electron can't finish installing — its folder isn't writable by your user.
25
+
26
+ Global npm packages shouldn't need sudo. The recommended setup is Node from a version
27
+ manager like nvm, which keeps global npm packages in your home folder:
28
+
29
+ sudo npm uninstall -g claude-discover
30
+ wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
31
+
32
+ Then open a new terminal and run:
33
+
34
+ nvm install --lts
35
+ npm install -g claude-discover
36
+ claude-discover
37
+
38
+ See https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally`)
39
+ process.exit(1)
40
+ }
41
+ }
42
+
43
+
44
+ const { default: electron } = await import('electron') // under plain node the electron package exports its binary path
45
+
15
46
  spawn(electron, [appDir, ...process.argv.slice(2)], { stdio: 'inherit' })
16
47
  .on('close', code => process.exit(code ?? 0))
package/bin/update.mjs CHANGED
@@ -1,42 +1,42 @@
1
- // Updates an `npm i -g` install from outside it. AutoUpdate starts it detached as the app quits:
2
- // Windows locks the app's running electron.exe, so npm can only replace the package once the app is gone.
3
-
4
- import { spawn, spawnSync } from 'node:child_process'
5
- import path from 'node:path'
6
- import { setTimeout as sleep } from 'node:timers/promises'
7
- import { parseArgs } from 'node:util'
8
-
9
-
10
- // ── 1. CLI, then wait for the app to exit ────────────────────────────────────
11
-
12
- const { values, positionals: [pid, version] } = parseArgs({
13
- allowPositionals: true,
14
- options: {
15
- 'no-reopen': { type: 'boolean' },
16
- 'help': { type: 'boolean', short: 'h' },
17
- },
18
- })
19
-
20
- if (values.help || !version) {
21
- console.error('Usage: node bin/update.mjs <pid> <version> [--no-reopen]')
22
- process.exit(1)
23
- }
24
-
25
- while (true) {
26
- try {
27
- process.kill(Number(pid), 0)
28
- } catch (err) {
29
- if (err.code !== 'EPERM') break // EPERM: alive, just not ours to signal
30
- }
31
- await sleep(200)
32
- }
33
-
34
-
35
- // ── 2. Update and reopen ─────────────────────────────────────────────────────
36
-
37
- const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
38
- if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
39
-
40
- // Reopen even after a failed update; dev passes --no-reopen, as its Vite renderer died with the app
41
- const globalBin = path.join(spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim(), 'claude-discover/bin/claude-discover.mjs')
42
- if (!values['no-reopen']) spawn(process.execPath, [globalBin], { detached: true, stdio: 'ignore' }).unref()
1
+ // Updates an `npm i -g` install from outside it. AutoUpdate starts it detached as the app quits:
2
+ // Windows locks the app's running electron.exe, so npm can only replace the package once the app is gone.
3
+
4
+ import { spawn, spawnSync } from 'node:child_process'
5
+ import path from 'node:path'
6
+ import { setTimeout as sleep } from 'node:timers/promises'
7
+ import { parseArgs } from 'node:util'
8
+
9
+
10
+ // ── 1. CLI, then wait for the app to exit ────────────────────────────────────
11
+
12
+ const { values, positionals: [pid, version] } = parseArgs({
13
+ allowPositionals: true,
14
+ options: {
15
+ 'no-reopen': { type: 'boolean' },
16
+ 'help': { type: 'boolean', short: 'h' },
17
+ },
18
+ })
19
+
20
+ if (values.help || !version) {
21
+ console.error('Usage: node bin/update.mjs <pid> <version> [--no-reopen]')
22
+ process.exit(1)
23
+ }
24
+
25
+ while (true) {
26
+ try {
27
+ process.kill(Number(pid), 0)
28
+ } catch (err) {
29
+ if (err.code !== 'EPERM') break // EPERM: alive, just not ours to signal
30
+ }
31
+ await sleep(200)
32
+ }
33
+
34
+
35
+ // ── 2. Update and reopen ─────────────────────────────────────────────────────
36
+
37
+ const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
38
+ if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
39
+
40
+ // Reopen even after a failed update; dev passes --no-reopen, as its Vite renderer died with the app
41
+ const globalBin = path.join(spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim(), 'claude-discover/bin/claude-discover.mjs')
42
+ if (!values['no-reopen']) spawn(process.execPath, [globalBin], { detached: true, stdio: 'ignore' }).unref()