claude-discover 1.10.0 → 1.10.2
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 -124
- package/bin/update.mjs +33 -0
- package/out/main/main.js +155 -20
- package/out/preload/preload.cjs +3 -1
- package/out/renderer/assets/{index-x_-ZeUa3.js → index-BV8MRn7r.js} +831 -674
- package/out/renderer/assets/{index-DnYN00T3.css → index-CR3tCQfY.css} +192 -303
- package/out/renderer/assets/{vendors-D2DMOduT.js → vendors-CF1AEHsu.js} +479 -340
- package/out/renderer/assets/wave-D-wH63sc.svg +3 -0
- package/out/renderer/index.html +3 -3
- package/package.json +7 -3
|
@@ -1,124 +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, context usage, token totals, rate-limit windows:
|
|
4
|
-
// [Opus 4.8] Context: ▓▓▓░░░░░░░ 32% used (45.2k, 87% cached) | Tokens: 1.2M total (+45.2k, 3 turns) |
|
|
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,
|
|
13
|
-
limit: [0, 60, 90],
|
|
14
|
-
cache: [90, 60, 0],
|
|
15
|
-
tokens: [0, 250_000, 1_000_000],
|
|
16
|
-
session: [0,
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
d.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
return `${min}m`
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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/update.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
// Usage: node bin/update.mjs <pid> <version> [--no-reopen] (dev passes --no-reopen)
|
|
5
|
+
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
9
|
+
|
|
10
|
+
const [pid, version, noReopen] = process.argv.slice(2)
|
|
11
|
+
|
|
12
|
+
while (isRunning(pid)) await sleep(200)
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Into the helper's console window (@windows), where --loglevel=http lists each package as npm fetches it
|
|
16
|
+
const { status } = spawnSync(`npm i -g claude-discover@${version} --loglevel=http`, { shell: true, stdio: 'inherit' })
|
|
17
|
+
if (status !== 0) await sleep(5_000) // leaves npm's error readable before the window closes
|
|
18
|
+
} finally {
|
|
19
|
+
// Reopen the global install (the one just updated, or just created from an npx run), even when the update failed
|
|
20
|
+
if (!noReopen) {
|
|
21
|
+
const root = spawnSync('npm root -g', { shell: true, encoding: 'utf8' }).stdout.trim()
|
|
22
|
+
spawn(process.execPath, [path.join(root, 'claude-discover/bin/claude-discover.mjs')], { detached: true, stdio: 'ignore' }).unref()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isRunning(pid) {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(Number(pid), 0)
|
|
29
|
+
return true
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return err.code === 'EPERM' // alive, just not ours to signal
|
|
32
|
+
}
|
|
33
|
+
}
|
package/out/main/main.js
CHANGED
|
@@ -7,8 +7,10 @@ import { sync } from "write-file-atomic";
|
|
|
7
7
|
import fsp from "node:fs/promises";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
9
|
import which from "which";
|
|
10
|
-
import { execFile } from "node:child_process";
|
|
10
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
|
+
import latestVersion from "latest-version";
|
|
13
|
+
import semver from "semver";
|
|
12
14
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
13
15
|
import windowStateKeeper from "electron-window-state";
|
|
14
16
|
import contextMenu from "electron-context-menu";
|
|
@@ -35,9 +37,17 @@ const CLAUDE_DIR = local.claudeDir || process.env.CLAUDE_CONFIG_DIR || join(home
|
|
|
35
37
|
const RECENT_CLAUDE_DIRS = local.recents || [CLAUDE_DIR];
|
|
36
38
|
const CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
37
39
|
const CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json");
|
|
38
|
-
const IS_EPHEMERAL = import.meta.dirname.split(/[\\/]/).includes("_npx");
|
|
39
40
|
const STATUSLINE_PATH = join(import.meta.dirname, "../../bin/claude/statusline.mjs");
|
|
40
41
|
const PROXY_PATH = join(import.meta.dirname, "../../bin/proxy.mjs");
|
|
42
|
+
const UPDATE_PATH = join(import.meta.dirname, "../../bin/update.mjs");
|
|
43
|
+
const LAUNCH_MODES = { NPM_DEV: "npm-dev", NPM_START: "npm-start", NPX_TEMP: "npx-temp", NPM_GLOBAL: "npm-global" };
|
|
44
|
+
const LAUNCH_MODE = detectLaunchMode();
|
|
45
|
+
function detectLaunchMode() {
|
|
46
|
+
const dirs = import.meta.dirname.split(/[\\/]/);
|
|
47
|
+
if (dirs.includes("_npx")) return LAUNCH_MODES.NPX_TEMP;
|
|
48
|
+
if (dirs.includes("node_modules")) return LAUNCH_MODES.NPM_GLOBAL;
|
|
49
|
+
return LAUNCH_MODES.NPM_START;
|
|
50
|
+
}
|
|
41
51
|
const SCHEME = "claude-discover";
|
|
42
52
|
class DeepLink extends EventEmitter {
|
|
43
53
|
#pending = null;
|
|
@@ -51,6 +61,7 @@ class DeepLink extends EventEmitter {
|
|
|
51
61
|
});
|
|
52
62
|
this.#register();
|
|
53
63
|
this.#pending = findTarget(process.argv);
|
|
64
|
+
return this;
|
|
54
65
|
}
|
|
55
66
|
open(target) {
|
|
56
67
|
if (target) this.emit("open", target);
|
|
@@ -71,7 +82,7 @@ class DeepLink extends EventEmitter {
|
|
|
71
82
|
// @windows Rewritten on every launch — idempotent, and the last launched checkout wins.
|
|
72
83
|
#register() {
|
|
73
84
|
if (process.platform !== "win32") return;
|
|
74
|
-
if (
|
|
85
|
+
if (LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP) return;
|
|
75
86
|
app.setAsDefaultProtocolClient(SCHEME, process.execPath, [app.getAppPath()]);
|
|
76
87
|
}
|
|
77
88
|
}
|
|
@@ -638,6 +649,7 @@ class RequestFile {
|
|
|
638
649
|
const parser = new RequestParser();
|
|
639
650
|
const files = /* @__PURE__ */ new Map();
|
|
640
651
|
for (const rec of await this.read()) {
|
|
652
|
+
if (!rec.request) continue;
|
|
641
653
|
const record = (f) => ({ timestamp: rec.timestamp, ...f });
|
|
642
654
|
const sys = parser.systemPrompt(rec);
|
|
643
655
|
if (sys && !files.has(sys.hash)) files.set(sys.hash, record(sys));
|
|
@@ -693,6 +705,8 @@ function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileS
|
|
|
693
705
|
mtime,
|
|
694
706
|
startedAt: null,
|
|
695
707
|
lastActivityAt: null,
|
|
708
|
+
continuesFrom: null,
|
|
709
|
+
continuesTo: null,
|
|
696
710
|
entrypoint: null,
|
|
697
711
|
project: null,
|
|
698
712
|
worktree: null,
|
|
@@ -701,6 +715,7 @@ function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileS
|
|
|
701
715
|
version: null,
|
|
702
716
|
model: null,
|
|
703
717
|
models: [],
|
|
718
|
+
efforts: [],
|
|
704
719
|
serviceTier: null,
|
|
705
720
|
speed: null,
|
|
706
721
|
fastPricingUnknown: false,
|
|
@@ -737,6 +752,8 @@ class SessionParser {
|
|
|
737
752
|
this.prevMessage = null;
|
|
738
753
|
this.tokenTotal = 0;
|
|
739
754
|
this.lastSeenTs = null;
|
|
755
|
+
this.commandUuids = /* @__PURE__ */ new Set();
|
|
756
|
+
this.seenUuids = /* @__PURE__ */ new Set();
|
|
740
757
|
this.appliedById = /* @__PURE__ */ new Map();
|
|
741
758
|
}
|
|
742
759
|
// Returns true if the item passes `this.range`.
|
|
@@ -758,12 +775,23 @@ class SessionParser {
|
|
|
758
775
|
const inherited = parsed > 0 ? parsed : this.lastSeenTs;
|
|
759
776
|
const ts = inherited ?? meta.mtime;
|
|
760
777
|
if (parsed > 0) this.lastSeenTs = parsed;
|
|
761
|
-
if (this.range && (ts < this.range.start || ts > this.range.end))
|
|
778
|
+
if (this.range && (ts < this.range.start || ts > this.range.end)) {
|
|
779
|
+
if (inherited != null && ts < this.range.start && ts > meta.continuesFrom) meta.continuesFrom = ts;
|
|
780
|
+
if (inherited != null && ts > this.range.end && (meta.continuesTo == null || ts < meta.continuesTo)) meta.continuesTo = ts;
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
762
783
|
obj._ts = ts;
|
|
763
784
|
if (inherited != null) {
|
|
764
785
|
if (meta.startedAt == null || inherited < meta.startedAt) meta.startedAt = inherited;
|
|
765
786
|
if (inherited > meta.lastActivityAt) meta.lastActivityAt = inherited;
|
|
766
787
|
}
|
|
788
|
+
if (obj.uuid) {
|
|
789
|
+
if (this.seenUuids.has(obj.uuid)) {
|
|
790
|
+
obj._relogged = true;
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
this.seenUuids.add(obj.uuid);
|
|
794
|
+
}
|
|
767
795
|
if (t === "attachment") {
|
|
768
796
|
obj.isMeta = true;
|
|
769
797
|
if (obj.attachment?.type !== "queued_command") this.prevMessage = obj;
|
|
@@ -778,7 +806,7 @@ class SessionParser {
|
|
|
778
806
|
meta.activityPeriods.push({ start: ts, end: ts });
|
|
779
807
|
}
|
|
780
808
|
if (t === "user") {
|
|
781
|
-
if (obj.turnCompanion && !obj.sourceToolUseID) meta.skillCalls += 1;
|
|
809
|
+
if (obj.turnCompanion && !obj.sourceToolUseID && this.commandUuids.has(obj.parentUuid)) meta.skillCalls += 1;
|
|
782
810
|
this._feedUser(obj);
|
|
783
811
|
} else if (t === "assistant") {
|
|
784
812
|
const msg = obj.message;
|
|
@@ -787,6 +815,8 @@ class SessionParser {
|
|
|
787
815
|
if (!meta.model) meta.model = msg.model;
|
|
788
816
|
if (!meta.models.includes(msg.model)) meta.models.push(msg.model);
|
|
789
817
|
}
|
|
818
|
+
const effort = obj.perTurnEffort ?? obj.effort;
|
|
819
|
+
if (effort && !meta.efforts.includes(effort)) meta.efforts.push(effort);
|
|
790
820
|
if (Array.isArray(msg.content)) {
|
|
791
821
|
for (const b of msg.content) {
|
|
792
822
|
if (b?.type !== "tool_use") continue;
|
|
@@ -806,6 +836,7 @@ class SessionParser {
|
|
|
806
836
|
const text = extractText(obj.message?.content);
|
|
807
837
|
if (text.startsWith("<command-name>") || text.startsWith("<command-message>")) {
|
|
808
838
|
if (!meta.firstUserCommand) meta.firstUserCommand = text;
|
|
839
|
+
if (obj.uuid) this.commandUuids.add(obj.uuid);
|
|
809
840
|
obj.isMeta = true;
|
|
810
841
|
} else if (text.startsWith("<local-command-stdout>")) {
|
|
811
842
|
if (meta.firstUserCommand) meta.firstUserCommand += "\n" + text;
|
|
@@ -814,7 +845,7 @@ class SessionParser {
|
|
|
814
845
|
obj.isMeta = true;
|
|
815
846
|
} else if (text.startsWith("<scheduled-task")) {
|
|
816
847
|
meta.hasScheduledTask = true;
|
|
817
|
-
} else if (text && !obj.isMeta) {
|
|
848
|
+
} else if (text && !obj.isMeta && !obj.isCompactSummary) {
|
|
818
849
|
if (!meta.firstUserPrompt) meta.firstUserPrompt = text.slice(0, 300);
|
|
819
850
|
}
|
|
820
851
|
}
|
|
@@ -1513,7 +1544,7 @@ class WorkHours {
|
|
|
1513
1544
|
}
|
|
1514
1545
|
}
|
|
1515
1546
|
class AgentRunner {
|
|
1516
|
-
async run(text, sender
|
|
1547
|
+
async run(text, sender) {
|
|
1517
1548
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
1518
1549
|
const send = (chunk) => {
|
|
1519
1550
|
if (!sender.isDestroyed()) sender.send("agent:output", chunk);
|
|
@@ -1522,9 +1553,10 @@ class AgentRunner {
|
|
|
1522
1553
|
prompt: text,
|
|
1523
1554
|
options: {
|
|
1524
1555
|
includePartialMessages: true,
|
|
1525
|
-
env:
|
|
1526
|
-
|
|
1527
|
-
|
|
1556
|
+
env: { ...process.env, DISABLE_PROMPT_CACHING: "1" },
|
|
1557
|
+
// @macOS - process.env must be included
|
|
1558
|
+
tools: [],
|
|
1559
|
+
settingSources: []
|
|
1528
1560
|
}
|
|
1529
1561
|
});
|
|
1530
1562
|
for await (const message of response) {
|
|
@@ -1900,12 +1932,12 @@ class Switchers {
|
|
|
1900
1932
|
}
|
|
1901
1933
|
async status(name) {
|
|
1902
1934
|
const s = await this.#get(name).status();
|
|
1903
|
-
return name in this.#keepActive ? { ...s, keepActive: this.#keepActive[name], ephemeral:
|
|
1935
|
+
return name in this.#keepActive ? { ...s, keepActive: this.#keepActive[name], ephemeral: LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP } : s;
|
|
1904
1936
|
}
|
|
1905
1937
|
activate(name, ...args) {
|
|
1906
1938
|
return this.#attempt(name, async (s) => {
|
|
1907
1939
|
await s.activate(...args);
|
|
1908
|
-
this.#keepActive[name] &&=
|
|
1940
|
+
this.#keepActive[name] &&= LAUNCH_MODE !== LAUNCH_MODES.NPX_TEMP;
|
|
1909
1941
|
});
|
|
1910
1942
|
}
|
|
1911
1943
|
// an npx-cache run undoes what it activated
|
|
@@ -1936,6 +1968,30 @@ class Switchers {
|
|
|
1936
1968
|
}
|
|
1937
1969
|
}
|
|
1938
1970
|
}
|
|
1971
|
+
class AutoUpdate {
|
|
1972
|
+
#result = null;
|
|
1973
|
+
// Once per launch — a renderer reload reuses the answer
|
|
1974
|
+
check() {
|
|
1975
|
+
return this.#result ??= this.#fetchNewer().catch((err) => {
|
|
1976
|
+
console.warn("[auto-update] failed:", err.message);
|
|
1977
|
+
return null;
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
async install() {
|
|
1981
|
+
const update = await this.check();
|
|
1982
|
+
if (!update) return;
|
|
1983
|
+
const args = [UPDATE_PATH, process.pid, update.latest, ...LAUNCH_MODE === LAUNCH_MODES.NPM_DEV ? ["--no-reopen"] : []].map(String);
|
|
1984
|
+
const [command, commandArgs] = process.platform === "win32" ? ["cmd", ["/c", "start", "Updating Claude Discover", "node", ...args]] : ["node", args];
|
|
1985
|
+
spawn(command, commandArgs, { detached: true, stdio: ["ignore", "inherit", "inherit"], cwd: homedir() }).once("spawn", () => app.quit()).once("error", (err) => dialog.showErrorBox("Update failed", `Could not start node: ${err.message}`));
|
|
1986
|
+
}
|
|
1987
|
+
async #fetchNewer() {
|
|
1988
|
+
if (LAUNCH_MODE === LAUNCH_MODES.NPM_START) return null;
|
|
1989
|
+
const latest = await latestVersion("claude-discover");
|
|
1990
|
+
const current = app.getVersion();
|
|
1991
|
+
if (LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP) return { current, latest, fromNpx: true };
|
|
1992
|
+
return semver.gt(latest, current) ? { current, latest } : null;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1939
1995
|
const VIEWABLE_EXT = /* @__PURE__ */ new Set([".md", ".txt", ".json", ".jsonl", ".log", ".csv", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".pdf"]);
|
|
1940
1996
|
async function openLinkSafely(href, baseFile) {
|
|
1941
1997
|
if (/^(https?:|mailto:)/i.test(href)) return shell.openExternal(href);
|
|
@@ -2079,14 +2135,15 @@ class Application {
|
|
|
2079
2135
|
this.agentRunner = new AgentRunner();
|
|
2080
2136
|
this.workHours = new WorkHours();
|
|
2081
2137
|
this.sessionsService = new SessionsService();
|
|
2138
|
+
this.autoUpdate = new AutoUpdate();
|
|
2082
2139
|
this.switchers = new Switchers({ restart: () => this.restart() });
|
|
2083
2140
|
}
|
|
2084
2141
|
start() {
|
|
2085
|
-
const { deepLink, win, agentRunner, workHours, sessionsService, switchers } = this;
|
|
2142
|
+
const { deepLink, win, agentRunner, workHours, sessionsService, autoUpdate, switchers } = this;
|
|
2086
2143
|
Menu.setApplicationMenu(this.#buildMenu());
|
|
2087
2144
|
sessionsService.on("update", (sessions) => win.send("sessions:update", sessions));
|
|
2088
2145
|
sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
|
|
2089
|
-
deepLink
|
|
2146
|
+
deepLink?.on("open", (target) => {
|
|
2090
2147
|
win.focus();
|
|
2091
2148
|
win.send("deeplink:open-session", target);
|
|
2092
2149
|
});
|
|
@@ -2099,9 +2156,11 @@ class Application {
|
|
|
2099
2156
|
ipcMain.handle("switch:keep-active", (_e, name, value) => switchers.setKeepActive(name, value));
|
|
2100
2157
|
ipcMain.handle("work-hours:get", () => workHours.read());
|
|
2101
2158
|
ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
|
|
2102
|
-
ipcMain.handle("
|
|
2159
|
+
ipcMain.handle("auto-update:check", () => autoUpdate.check());
|
|
2160
|
+
ipcMain.handle("auto-update:install", () => autoUpdate.install());
|
|
2161
|
+
ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
|
|
2103
2162
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
2104
|
-
ipcMain.handle("deeplink:take-pending", () => deepLink
|
|
2163
|
+
ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
|
|
2105
2164
|
ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
|
|
2106
2165
|
ipcMain.on("find:stop", () => win.findBar?.stop());
|
|
2107
2166
|
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
@@ -2171,11 +2230,80 @@ class Application {
|
|
|
2171
2230
|
]);
|
|
2172
2231
|
}
|
|
2173
2232
|
}
|
|
2233
|
+
const APP_ID = "claude-discover";
|
|
2234
|
+
const GLOBAL = LAUNCH_MODE === LAUNCH_MODES.NPM_GLOBAL;
|
|
2235
|
+
const NAME = GLOBAL ? "Claude Discover" : "Claude Discover (local)";
|
|
2236
|
+
class Shortcuts {
|
|
2237
|
+
#os = process.platform === "win32" ? new WindowsLinks() : process.platform === "darwin" ? new MacBundle() : null;
|
|
2238
|
+
// Call before any window is created, so the window picks up the app id.
|
|
2239
|
+
activate() {
|
|
2240
|
+
if (process.platform === "win32") app.setAppUserModelId(APP_ID);
|
|
2241
|
+
if (!this.#os || LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP || LAUNCH_MODE === LAUNCH_MODES.NPM_DEV) return;
|
|
2242
|
+
const created = config.read().shortcuts ?? {};
|
|
2243
|
+
const install = fs.statSync(app.getAppPath()).birthtimeMs;
|
|
2244
|
+
const newInstall = created[NAME] !== install;
|
|
2245
|
+
for (const path2 of this.#os.paths()) {
|
|
2246
|
+
const exists = fs.existsSync(path2);
|
|
2247
|
+
if (!exists && !newInstall) continue;
|
|
2248
|
+
this.#os.write(path2, exists);
|
|
2249
|
+
}
|
|
2250
|
+
if (newInstall) config.save({ shortcuts: { ...created, [NAME]: install } });
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
class WindowsLinks {
|
|
2254
|
+
// @windows
|
|
2255
|
+
paths() {
|
|
2256
|
+
return [join(app.getPath("appData"), "Microsoft/Windows/Start Menu/Programs"), app.getPath("desktop")].map((dir) => join(dir, `${NAME}.lnk`));
|
|
2257
|
+
}
|
|
2258
|
+
write(path2, exists) {
|
|
2259
|
+
const written = shell.writeShortcutLink(path2, exists ? "replace" : "create", {
|
|
2260
|
+
target: process.execPath,
|
|
2261
|
+
args: `"${app.getAppPath()}"`,
|
|
2262
|
+
description: "Browse your local Claude Code sessions",
|
|
2263
|
+
icon: process.execPath,
|
|
2264
|
+
iconIndex: 0,
|
|
2265
|
+
appUserModelId: APP_ID
|
|
2266
|
+
});
|
|
2267
|
+
if (!written) throw new Error(`cannot write ${path2}`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
class MacBundle {
|
|
2271
|
+
// @macOS
|
|
2272
|
+
paths() {
|
|
2273
|
+
return [join(app.getPath("home"), "Applications", `${NAME}.app`)];
|
|
2274
|
+
}
|
|
2275
|
+
write(path2) {
|
|
2276
|
+
const electronApp = join(process.execPath, "../../..");
|
|
2277
|
+
const quote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
2278
|
+
fs.mkdirSync(join(path2, "Contents/MacOS"), { recursive: true });
|
|
2279
|
+
fs.mkdirSync(join(path2, "Contents/Resources"), { recursive: true });
|
|
2280
|
+
fs.copyFileSync(join(electronApp, "Contents/Resources/electron.icns"), join(path2, "Contents/Resources/icon.icns"));
|
|
2281
|
+
fs.writeFileSync(join(path2, "Contents/MacOS/launch"), [
|
|
2282
|
+
"#!/bin/sh",
|
|
2283
|
+
`exec open -n ${quote(electronApp)} --env ${quote(`PATH=${process.env.PATH}`)} --args ${quote(app.getAppPath())}`,
|
|
2284
|
+
""
|
|
2285
|
+
].join("\n"), { mode: 493 });
|
|
2286
|
+
fs.writeFileSync(join(path2, "Contents/Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
|
|
2287
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2288
|
+
<plist version="1.0">
|
|
2289
|
+
<dict>
|
|
2290
|
+
<key>CFBundleName</key><string>${NAME}</string>
|
|
2291
|
+
<key>CFBundleIdentifier</key><string>com.dichev.${APP_ID}${GLOBAL ? "" : ".local"}</string>
|
|
2292
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
2293
|
+
<key>CFBundleExecutable</key><string>launch</string>
|
|
2294
|
+
<key>CFBundleIconFile</key><string>icon</string>
|
|
2295
|
+
<key>LSUIElement</key><true/>
|
|
2296
|
+
</dict>
|
|
2297
|
+
</plist>
|
|
2298
|
+
`);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2174
2301
|
const isFirstInstance = app.requestSingleInstanceLock();
|
|
2175
2302
|
if (isFirstInstance) {
|
|
2176
|
-
let application;
|
|
2177
|
-
|
|
2178
|
-
deepLink.activate();
|
|
2303
|
+
let application = null;
|
|
2304
|
+
let deepLink = null;
|
|
2305
|
+
optional("deep links", () => deepLink = new DeepLink().activate());
|
|
2306
|
+
optional("shortcuts", () => new Shortcuts().activate());
|
|
2179
2307
|
app.whenReady().then(() => {
|
|
2180
2308
|
application = new Application({ deepLink });
|
|
2181
2309
|
application.start();
|
|
@@ -2185,7 +2313,7 @@ if (isFirstInstance) {
|
|
|
2185
2313
|
if (argv.includes("--restart")) {
|
|
2186
2314
|
application?.restart();
|
|
2187
2315
|
} else if (target) {
|
|
2188
|
-
deepLink
|
|
2316
|
+
deepLink?.open(target);
|
|
2189
2317
|
} else {
|
|
2190
2318
|
application?.win.focus();
|
|
2191
2319
|
}
|
|
@@ -2193,3 +2321,10 @@ if (isFirstInstance) {
|
|
|
2193
2321
|
} else {
|
|
2194
2322
|
app.exit(0);
|
|
2195
2323
|
}
|
|
2324
|
+
function optional(label, activate) {
|
|
2325
|
+
try {
|
|
2326
|
+
activate();
|
|
2327
|
+
} catch (err) {
|
|
2328
|
+
console.warn(`[${label}] skipped:`, err.message);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
package/out/preload/preload.cjs
CHANGED
|
@@ -20,8 +20,10 @@ electron.contextBridge.exposeInMainWorld("api", {
|
|
|
20
20
|
onScanProgress: subscribe("sessions:scan-progress"),
|
|
21
21
|
getWorkHours: () => electron.ipcRenderer.invoke("work-hours:get"),
|
|
22
22
|
setWorkHours: (data) => electron.ipcRenderer.invoke("work-hours:set", data),
|
|
23
|
-
runAgentPrompt: (text
|
|
23
|
+
runAgentPrompt: (text) => electron.ipcRenderer.invoke("agent:run", text),
|
|
24
24
|
onAgentOutput: subscribe("agent:output"),
|
|
25
|
+
checkUpdate: () => electron.ipcRenderer.invoke("auto-update:check"),
|
|
26
|
+
installUpdate: () => electron.ipcRenderer.invoke("auto-update:install"),
|
|
25
27
|
openLink: (href, baseFile) => electron.ipcRenderer.invoke("shell:open-link", href, baseFile),
|
|
26
28
|
onFindActive: subscribe("find:active"),
|
|
27
29
|
findClose: () => electron.ipcRenderer.send("find:close"),
|