claude-discover 1.10.1 → 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 +129 -32
- package/out/preload/preload.cjs +3 -1
- package/out/renderer/assets/{index-FM6CSRHb.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 +80 -77
|
@@ -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;
|
|
@@ -72,7 +82,7 @@ class DeepLink extends EventEmitter {
|
|
|
72
82
|
// @windows Rewritten on every launch — idempotent, and the last launched checkout wins.
|
|
73
83
|
#register() {
|
|
74
84
|
if (process.platform !== "win32") return;
|
|
75
|
-
if (
|
|
85
|
+
if (LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP) return;
|
|
76
86
|
app.setAsDefaultProtocolClient(SCHEME, process.execPath, [app.getAppPath()]);
|
|
77
87
|
}
|
|
78
88
|
}
|
|
@@ -639,6 +649,7 @@ class RequestFile {
|
|
|
639
649
|
const parser = new RequestParser();
|
|
640
650
|
const files = /* @__PURE__ */ new Map();
|
|
641
651
|
for (const rec of await this.read()) {
|
|
652
|
+
if (!rec.request) continue;
|
|
642
653
|
const record = (f) => ({ timestamp: rec.timestamp, ...f });
|
|
643
654
|
const sys = parser.systemPrompt(rec);
|
|
644
655
|
if (sys && !files.has(sys.hash)) files.set(sys.hash, record(sys));
|
|
@@ -694,6 +705,8 @@ function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileS
|
|
|
694
705
|
mtime,
|
|
695
706
|
startedAt: null,
|
|
696
707
|
lastActivityAt: null,
|
|
708
|
+
continuesFrom: null,
|
|
709
|
+
continuesTo: null,
|
|
697
710
|
entrypoint: null,
|
|
698
711
|
project: null,
|
|
699
712
|
worktree: null,
|
|
@@ -702,6 +715,7 @@ function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileS
|
|
|
702
715
|
version: null,
|
|
703
716
|
model: null,
|
|
704
717
|
models: [],
|
|
718
|
+
efforts: [],
|
|
705
719
|
serviceTier: null,
|
|
706
720
|
speed: null,
|
|
707
721
|
fastPricingUnknown: false,
|
|
@@ -738,6 +752,8 @@ class SessionParser {
|
|
|
738
752
|
this.prevMessage = null;
|
|
739
753
|
this.tokenTotal = 0;
|
|
740
754
|
this.lastSeenTs = null;
|
|
755
|
+
this.commandUuids = /* @__PURE__ */ new Set();
|
|
756
|
+
this.seenUuids = /* @__PURE__ */ new Set();
|
|
741
757
|
this.appliedById = /* @__PURE__ */ new Map();
|
|
742
758
|
}
|
|
743
759
|
// Returns true if the item passes `this.range`.
|
|
@@ -759,12 +775,23 @@ class SessionParser {
|
|
|
759
775
|
const inherited = parsed > 0 ? parsed : this.lastSeenTs;
|
|
760
776
|
const ts = inherited ?? meta.mtime;
|
|
761
777
|
if (parsed > 0) this.lastSeenTs = parsed;
|
|
762
|
-
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
|
+
}
|
|
763
783
|
obj._ts = ts;
|
|
764
784
|
if (inherited != null) {
|
|
765
785
|
if (meta.startedAt == null || inherited < meta.startedAt) meta.startedAt = inherited;
|
|
766
786
|
if (inherited > meta.lastActivityAt) meta.lastActivityAt = inherited;
|
|
767
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
|
+
}
|
|
768
795
|
if (t === "attachment") {
|
|
769
796
|
obj.isMeta = true;
|
|
770
797
|
if (obj.attachment?.type !== "queued_command") this.prevMessage = obj;
|
|
@@ -779,7 +806,7 @@ class SessionParser {
|
|
|
779
806
|
meta.activityPeriods.push({ start: ts, end: ts });
|
|
780
807
|
}
|
|
781
808
|
if (t === "user") {
|
|
782
|
-
if (obj.turnCompanion && !obj.sourceToolUseID) meta.skillCalls += 1;
|
|
809
|
+
if (obj.turnCompanion && !obj.sourceToolUseID && this.commandUuids.has(obj.parentUuid)) meta.skillCalls += 1;
|
|
783
810
|
this._feedUser(obj);
|
|
784
811
|
} else if (t === "assistant") {
|
|
785
812
|
const msg = obj.message;
|
|
@@ -788,6 +815,8 @@ class SessionParser {
|
|
|
788
815
|
if (!meta.model) meta.model = msg.model;
|
|
789
816
|
if (!meta.models.includes(msg.model)) meta.models.push(msg.model);
|
|
790
817
|
}
|
|
818
|
+
const effort = obj.perTurnEffort ?? obj.effort;
|
|
819
|
+
if (effort && !meta.efforts.includes(effort)) meta.efforts.push(effort);
|
|
791
820
|
if (Array.isArray(msg.content)) {
|
|
792
821
|
for (const b of msg.content) {
|
|
793
822
|
if (b?.type !== "tool_use") continue;
|
|
@@ -807,6 +836,7 @@ class SessionParser {
|
|
|
807
836
|
const text = extractText(obj.message?.content);
|
|
808
837
|
if (text.startsWith("<command-name>") || text.startsWith("<command-message>")) {
|
|
809
838
|
if (!meta.firstUserCommand) meta.firstUserCommand = text;
|
|
839
|
+
if (obj.uuid) this.commandUuids.add(obj.uuid);
|
|
810
840
|
obj.isMeta = true;
|
|
811
841
|
} else if (text.startsWith("<local-command-stdout>")) {
|
|
812
842
|
if (meta.firstUserCommand) meta.firstUserCommand += "\n" + text;
|
|
@@ -815,7 +845,7 @@ class SessionParser {
|
|
|
815
845
|
obj.isMeta = true;
|
|
816
846
|
} else if (text.startsWith("<scheduled-task")) {
|
|
817
847
|
meta.hasScheduledTask = true;
|
|
818
|
-
} else if (text && !obj.isMeta) {
|
|
848
|
+
} else if (text && !obj.isMeta && !obj.isCompactSummary) {
|
|
819
849
|
if (!meta.firstUserPrompt) meta.firstUserPrompt = text.slice(0, 300);
|
|
820
850
|
}
|
|
821
851
|
}
|
|
@@ -1514,7 +1544,7 @@ class WorkHours {
|
|
|
1514
1544
|
}
|
|
1515
1545
|
}
|
|
1516
1546
|
class AgentRunner {
|
|
1517
|
-
async run(text, sender
|
|
1547
|
+
async run(text, sender) {
|
|
1518
1548
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
1519
1549
|
const send = (chunk) => {
|
|
1520
1550
|
if (!sender.isDestroyed()) sender.send("agent:output", chunk);
|
|
@@ -1523,9 +1553,10 @@ class AgentRunner {
|
|
|
1523
1553
|
prompt: text,
|
|
1524
1554
|
options: {
|
|
1525
1555
|
includePartialMessages: true,
|
|
1526
|
-
env:
|
|
1527
|
-
|
|
1528
|
-
|
|
1556
|
+
env: { ...process.env, DISABLE_PROMPT_CACHING: "1" },
|
|
1557
|
+
// @macOS - process.env must be included
|
|
1558
|
+
tools: [],
|
|
1559
|
+
settingSources: []
|
|
1529
1560
|
}
|
|
1530
1561
|
});
|
|
1531
1562
|
for await (const message of response) {
|
|
@@ -1901,12 +1932,12 @@ class Switchers {
|
|
|
1901
1932
|
}
|
|
1902
1933
|
async status(name) {
|
|
1903
1934
|
const s = await this.#get(name).status();
|
|
1904
|
-
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;
|
|
1905
1936
|
}
|
|
1906
1937
|
activate(name, ...args) {
|
|
1907
1938
|
return this.#attempt(name, async (s) => {
|
|
1908
1939
|
await s.activate(...args);
|
|
1909
|
-
this.#keepActive[name] &&=
|
|
1940
|
+
this.#keepActive[name] &&= LAUNCH_MODE !== LAUNCH_MODES.NPX_TEMP;
|
|
1910
1941
|
});
|
|
1911
1942
|
}
|
|
1912
1943
|
// an npx-cache run undoes what it activated
|
|
@@ -1937,6 +1968,30 @@ class Switchers {
|
|
|
1937
1968
|
}
|
|
1938
1969
|
}
|
|
1939
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
|
+
}
|
|
1940
1995
|
const VIEWABLE_EXT = /* @__PURE__ */ new Set([".md", ".txt", ".json", ".jsonl", ".log", ".csv", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".pdf"]);
|
|
1941
1996
|
async function openLinkSafely(href, baseFile) {
|
|
1942
1997
|
if (/^(https?:|mailto:)/i.test(href)) return shell.openExternal(href);
|
|
@@ -2080,10 +2135,11 @@ class Application {
|
|
|
2080
2135
|
this.agentRunner = new AgentRunner();
|
|
2081
2136
|
this.workHours = new WorkHours();
|
|
2082
2137
|
this.sessionsService = new SessionsService();
|
|
2138
|
+
this.autoUpdate = new AutoUpdate();
|
|
2083
2139
|
this.switchers = new Switchers({ restart: () => this.restart() });
|
|
2084
2140
|
}
|
|
2085
2141
|
start() {
|
|
2086
|
-
const { deepLink, win, agentRunner, workHours, sessionsService, switchers } = this;
|
|
2142
|
+
const { deepLink, win, agentRunner, workHours, sessionsService, autoUpdate, switchers } = this;
|
|
2087
2143
|
Menu.setApplicationMenu(this.#buildMenu());
|
|
2088
2144
|
sessionsService.on("update", (sessions) => win.send("sessions:update", sessions));
|
|
2089
2145
|
sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
|
|
@@ -2100,7 +2156,9 @@ class Application {
|
|
|
2100
2156
|
ipcMain.handle("switch:keep-active", (_e, name, value) => switchers.setKeepActive(name, value));
|
|
2101
2157
|
ipcMain.handle("work-hours:get", () => workHours.read());
|
|
2102
2158
|
ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
|
|
2103
|
-
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));
|
|
2104
2162
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
2105
2163
|
ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
|
|
2106
2164
|
ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
|
|
@@ -2173,32 +2231,71 @@ class Application {
|
|
|
2173
2231
|
}
|
|
2174
2232
|
}
|
|
2175
2233
|
const APP_ID = "claude-discover";
|
|
2234
|
+
const GLOBAL = LAUNCH_MODE === LAUNCH_MODES.NPM_GLOBAL;
|
|
2235
|
+
const NAME = GLOBAL ? "Claude Discover" : "Claude Discover (local)";
|
|
2176
2236
|
class Shortcuts {
|
|
2237
|
+
#os = process.platform === "win32" ? new WindowsLinks() : process.platform === "darwin" ? new MacBundle() : null;
|
|
2177
2238
|
// Call before any window is created, so the window picks up the app id.
|
|
2178
2239
|
activate() {
|
|
2179
|
-
if (process.platform
|
|
2180
|
-
|
|
2181
|
-
if (IS_EPHEMERAL || false) return;
|
|
2182
|
-
const installed = app.getAppPath().split(/[\\/]/).includes("node_modules");
|
|
2183
|
-
const name = installed ? "Claude Discover" : "Claude Discover (local)";
|
|
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;
|
|
2184
2242
|
const created = config.read().shortcuts ?? {};
|
|
2185
2243
|
const install = fs.statSync(app.getAppPath()).birthtimeMs;
|
|
2186
|
-
const newInstall = created[
|
|
2187
|
-
for (const
|
|
2188
|
-
const path2 = join(dir, `${name}.lnk`);
|
|
2244
|
+
const newInstall = created[NAME] !== install;
|
|
2245
|
+
for (const path2 of this.#os.paths()) {
|
|
2189
2246
|
const exists = fs.existsSync(path2);
|
|
2190
2247
|
if (!exists && !newInstall) continue;
|
|
2191
|
-
|
|
2192
|
-
target: process.execPath,
|
|
2193
|
-
args: `"${app.getAppPath()}"`,
|
|
2194
|
-
description: "Browse your local Claude Code sessions",
|
|
2195
|
-
icon: process.execPath,
|
|
2196
|
-
iconIndex: 0,
|
|
2197
|
-
appUserModelId: APP_ID
|
|
2198
|
-
});
|
|
2199
|
-
if (!written) throw new Error(`cannot write ${path2}`);
|
|
2248
|
+
this.#os.write(path2, exists);
|
|
2200
2249
|
}
|
|
2201
|
-
if (newInstall) config.save({ shortcuts: { ...created, [
|
|
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
|
+
`);
|
|
2202
2299
|
}
|
|
2203
2300
|
}
|
|
2204
2301
|
const isFirstInstance = app.requestSingleInstanceLock();
|
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"),
|