claude-discover 1.10.4 → 1.10.5
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/claude/statusline.mjs +125 -125
- package/bin/claude-discover.mjs +32 -1
- package/bin/update.mjs +42 -42
- package/out/main/main.js +12 -62
- package/out/renderer/assets/{index-DsnXk_cj.css → index-BMh5255F.css} +250 -250
- package/out/renderer/assets/{index-B6LVyX-b.js → index-CHnuzPLr.js} +20 -13
- package/out/renderer/index.html +2 -2
- package/package.json +85 -81
- package/out/preload/findPreload.cjs +0 -16
- package/out/renderer/assets/find-CxcUmpSm.css +0 -88
- package/out/renderer/assets/find-DSEw-KLs.js +0 -59
- package/out/renderer/find/find.html +0 -26
|
@@ -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],
|
|
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')
|
package/bin/claude-discover.mjs
CHANGED
|
@@ -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()
|
package/out/main/main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, dialog, BrowserWindow, shell,
|
|
1
|
+
import { app, dialog, BrowserWindow, shell, nativeTheme, Menu, ipcMain } from "electron";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import os, { homedir } from "node:os";
|
|
4
4
|
import path, { join, dirname, basename } from "node:path";
|
|
@@ -14,6 +14,7 @@ import semver from "semver";
|
|
|
14
14
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
15
15
|
import windowStateKeeper from "electron-window-state";
|
|
16
16
|
import contextMenu from "electron-context-menu";
|
|
17
|
+
import { FindOverlay } from "electron-find-overlay";
|
|
17
18
|
class ConfigFile {
|
|
18
19
|
dataDir = join(homedir(), ".claude-discover");
|
|
19
20
|
configFile = join(this.dataDir, "config.json");
|
|
@@ -759,6 +760,7 @@ class SessionParser {
|
|
|
759
760
|
this.prevMessage = null;
|
|
760
761
|
this.tokenTotal = 0;
|
|
761
762
|
this.lastSeenTs = null;
|
|
763
|
+
this.modelId = null;
|
|
762
764
|
this.commandUuids = /* @__PURE__ */ new Set();
|
|
763
765
|
this.seenUuids = /* @__PURE__ */ new Set();
|
|
764
766
|
this.appliedById = /* @__PURE__ */ new Map();
|
|
@@ -774,6 +776,7 @@ class SessionParser {
|
|
|
774
776
|
if (t === "tag") meta.tag = obj.tag || null;
|
|
775
777
|
if (t === "queue-operation" && obj.content?.includes("<scheduled-task")) meta.hasScheduledTask = true;
|
|
776
778
|
if (t === "started" && obj.key && obj.agentId) meta.workflowAgents += 1;
|
|
779
|
+
if (t === "attachment" && obj.attachment?.type === "model") this.modelId = obj.attachment.identity?.modelId;
|
|
777
780
|
if (obj.cwd && !meta.project) meta.project = obj.cwd;
|
|
778
781
|
if (obj.gitBranch && !meta.gitBranch) meta.gitBranch = obj.gitBranch;
|
|
779
782
|
if (obj.entrypoint && !meta.entrypoint) meta.entrypoint = obj.entrypoint;
|
|
@@ -818,9 +821,10 @@ class SessionParser {
|
|
|
818
821
|
} else if (t === "assistant") {
|
|
819
822
|
const msg = obj.message;
|
|
820
823
|
if (!msg || msg.model === "<synthetic>") return true;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (!meta.
|
|
824
|
+
const model = this.modelId?.replace(/\[.*\]$/, "") === msg.model ? this.modelId : msg.model;
|
|
825
|
+
if (model) {
|
|
826
|
+
if (!meta.model) meta.model = model;
|
|
827
|
+
if (!meta.models.includes(model)) meta.models.push(model);
|
|
824
828
|
}
|
|
825
829
|
const effort = obj.perTurnEffort ?? obj.effort;
|
|
826
830
|
if (effort && !meta.efforts.includes(effort)) meta.efforts.push(effort);
|
|
@@ -2032,61 +2036,6 @@ function lockNavigation(contents) {
|
|
|
2032
2036
|
contents.on("will-navigate", block);
|
|
2033
2037
|
contents.on("will-redirect", block);
|
|
2034
2038
|
}
|
|
2035
|
-
const __dirname$2 = path.dirname(fileURLToPath(import.meta.url));
|
|
2036
|
-
const DEV_URL$1 = process.env.ELECTRON_RENDERER_URL;
|
|
2037
|
-
const WIDTH = 320, HEIGHT = 56, MARGIN = 6;
|
|
2038
|
-
class FindBar {
|
|
2039
|
-
constructor(win) {
|
|
2040
|
-
this.win = win;
|
|
2041
|
-
this.view = new WebContentsView({
|
|
2042
|
-
webPreferences: {
|
|
2043
|
-
preload: path.join(__dirname$2, "../preload/findPreload.cjs"),
|
|
2044
|
-
contextIsolation: true,
|
|
2045
|
-
nodeIntegration: false,
|
|
2046
|
-
sandbox: true
|
|
2047
|
-
}
|
|
2048
|
-
});
|
|
2049
|
-
this.view.setBackgroundColor("#00000000");
|
|
2050
|
-
lockNavigation(this.view.webContents);
|
|
2051
|
-
win.contentView.addChildView(this.view);
|
|
2052
|
-
this.view.setVisible(false);
|
|
2053
|
-
if (DEV_URL$1) this.view.webContents.loadURL(`${DEV_URL$1}/find/find.html`);
|
|
2054
|
-
else this.view.webContents.loadFile(path.join(__dirname$2, "../renderer/find/find.html"));
|
|
2055
|
-
win.webContents.on("found-in-page", (_e, r) => {
|
|
2056
|
-
return this.view.webContents.send("find:result", { active: r.activeMatchOrdinal, total: r.matches, id: r.requestId });
|
|
2057
|
-
});
|
|
2058
|
-
win.on("resize", () => {
|
|
2059
|
-
if (this.visible) this.layout();
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
get visible() {
|
|
2063
|
-
return this.view.getVisible();
|
|
2064
|
-
}
|
|
2065
|
-
layout() {
|
|
2066
|
-
const [w] = this.win.getContentSize();
|
|
2067
|
-
this.view.setBounds({ x: Math.max(0, w - WIDTH - MARGIN), y: MARGIN, width: WIDTH, height: HEIGHT });
|
|
2068
|
-
}
|
|
2069
|
-
query(text, options) {
|
|
2070
|
-
this.win.webContents.findInPage(text, options);
|
|
2071
|
-
}
|
|
2072
|
-
stop() {
|
|
2073
|
-
this.win.webContents.stopFindInPage("clearSelection");
|
|
2074
|
-
}
|
|
2075
|
-
show() {
|
|
2076
|
-
this.layout();
|
|
2077
|
-
this.view.setVisible(true);
|
|
2078
|
-
this.view.webContents.focus();
|
|
2079
|
-
this.view.webContents.send("find:open");
|
|
2080
|
-
this.win.webContents.send("find:active", true);
|
|
2081
|
-
}
|
|
2082
|
-
hide() {
|
|
2083
|
-
if (!this.visible) return;
|
|
2084
|
-
this.view.setVisible(false);
|
|
2085
|
-
this.stop();
|
|
2086
|
-
this.win.webContents.focus();
|
|
2087
|
-
this.win.webContents.send("find:active", false);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
2039
|
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
2091
2040
|
const DEV_URL = process.env.ELECTRON_RENDERER_URL;
|
|
2092
2041
|
class MainWindow {
|
|
@@ -2094,6 +2043,7 @@ class MainWindow {
|
|
|
2094
2043
|
this.win = null;
|
|
2095
2044
|
}
|
|
2096
2045
|
create() {
|
|
2046
|
+
nativeTheme.themeSource = "dark";
|
|
2097
2047
|
const state = windowStateKeeper({ defaultWidth: 1500, defaultHeight: 900 });
|
|
2098
2048
|
this.win = new BrowserWindow({
|
|
2099
2049
|
x: state.x,
|
|
@@ -2119,7 +2069,9 @@ class MainWindow {
|
|
|
2119
2069
|
} else {
|
|
2120
2070
|
this.win.loadFile(path.join(__dirname$1, "../renderer/index.html"));
|
|
2121
2071
|
}
|
|
2122
|
-
this.findBar = new
|
|
2072
|
+
this.findBar = new FindOverlay(this.win);
|
|
2073
|
+
this.findBar.on("show", () => this.send("find:active", true));
|
|
2074
|
+
this.findBar.on("hide", () => this.send("find:active", false));
|
|
2123
2075
|
return this.win;
|
|
2124
2076
|
}
|
|
2125
2077
|
send(channel, payload) {
|
|
@@ -2165,8 +2117,6 @@ class Application {
|
|
|
2165
2117
|
ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
|
|
2166
2118
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
2167
2119
|
ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
|
|
2168
|
-
ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
|
|
2169
|
-
ipcMain.on("find:stop", () => win.findBar?.stop());
|
|
2170
2120
|
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
2171
2121
|
ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
|
|
2172
2122
|
app.on("activate", () => {
|