claude-discover 1.10.7 → 1.10.8
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/README.md +1 -1
- package/out/main/main.js +77 -158
- package/out/preload/preload.cjs +1 -0
- package/out/renderer/assets/{index-DEYU-s3r.css → index-BB2ZzrWB.css} +9 -0
- package/out/renderer/assets/{index-BnOl_kG3.js → index-DKilivAV.js} +16 -9
- package/out/renderer/index.html +2 -2
- package/package.json +2 -3
- package/bin/update.mjs +0 -42
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The tool works on Windows, macOS, and Linux. If you have Node.js 22+, just try i
|
|
|
17
17
|
npx claude-discover@latest
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Like it and want to keep it around
|
|
20
|
+
Like it and want to keep it around, install it:
|
|
21
21
|
```
|
|
22
22
|
npm install -g claude-discover
|
|
23
23
|
claude-discover
|
package/out/main/main.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { app, shell, dialog, BrowserWindow, nativeTheme, Menu, ipcMain } from "electron";
|
|
1
|
+
import { app, nativeImage, shell, dialog, BrowserWindow, 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";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import { sync } from "write-file-atomic";
|
|
7
|
+
import { launchMode, LAUNCH_MODES } from "electron-update-from-npm/launch-mode";
|
|
7
8
|
import fsp from "node:fs/promises";
|
|
8
9
|
import crypto from "node:crypto";
|
|
9
10
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
10
11
|
import which from "which";
|
|
11
|
-
import { execFile
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
|
-
import latestVersion from "latest-version";
|
|
14
|
-
import semver from "semver";
|
|
15
14
|
import windowStateKeeper from "electron-window-state";
|
|
16
15
|
import contextMenu from "electron-context-menu";
|
|
17
16
|
import { FindOverlay } from "electron-find-overlay";
|
|
17
|
+
import { NpmUpdater } from "electron-update-from-npm";
|
|
18
18
|
class ConfigFile {
|
|
19
19
|
dataDir = join(homedir(), ".claude-discover");
|
|
20
20
|
configFile = join(this.dataDir, "config.json");
|
|
@@ -40,15 +40,7 @@ const CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
|
40
40
|
const CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json");
|
|
41
41
|
const STATUSLINE_PATH = join(import.meta.dirname, "../../bin/claude/statusline.mjs");
|
|
42
42
|
const PROXY_PATH = join(import.meta.dirname, "../../bin/proxy.mjs");
|
|
43
|
-
const
|
|
44
|
-
const LAUNCH_MODES = { NPM_DEV: "npm-dev", NPM_START: "npm-start", NPX_TEMP: "npx-temp", NPM_GLOBAL: "npm-global" };
|
|
45
|
-
const LAUNCH_MODE = detectLaunchMode();
|
|
46
|
-
function detectLaunchMode() {
|
|
47
|
-
const dirs = import.meta.dirname.split(/[\\/]/);
|
|
48
|
-
if (dirs.includes("_npx")) return LAUNCH_MODES.NPX_TEMP;
|
|
49
|
-
if (dirs.includes("node_modules")) return LAUNCH_MODES.NPM_GLOBAL;
|
|
50
|
-
return LAUNCH_MODES.NPM_START;
|
|
51
|
-
}
|
|
43
|
+
const LAUNCH_MODE = launchMode(import.meta.dirname, { dev: false });
|
|
52
44
|
const SCHEME = "claude-discover";
|
|
53
45
|
class DeepLink extends EventEmitter {
|
|
54
46
|
#pending = null;
|
|
@@ -459,7 +451,7 @@ function splitClaudeMd(text) {
|
|
|
459
451
|
}
|
|
460
452
|
const jsonBytes = (x) => x == null ? 0 : Buffer.byteLength(JSON.stringify(x));
|
|
461
453
|
const contentHash = (s) => crypto.createHash("sha256").update(s).digest("base64url").slice(0, 16);
|
|
462
|
-
const isMcp = (
|
|
454
|
+
const isMcp = (name2) => name2.startsWith("mcp__");
|
|
463
455
|
const stripKind = (req) => {
|
|
464
456
|
const kind = classifyRequest(req);
|
|
465
457
|
return !kind || kind[0] === "main" || kind[0] === "tools" ? ["main", null] : kind;
|
|
@@ -1013,9 +1005,9 @@ class EventFileWatcher {
|
|
|
1013
1005
|
// Throws if the OS refuses the watch, e.g. on \\wsl.localhost\...
|
|
1014
1006
|
constructor(root, opts) {
|
|
1015
1007
|
this.#opts = { settleMs: 100, ...opts };
|
|
1016
|
-
this.#handle = fs.watch(root, { recursive: true }, (_type,
|
|
1017
|
-
if (!
|
|
1018
|
-
if (this.#opts.filter(
|
|
1008
|
+
this.#handle = fs.watch(root, { recursive: true }, (_type, name2) => {
|
|
1009
|
+
if (!name2) return this.#opts.onDrop();
|
|
1010
|
+
if (this.#opts.filter(name2)) this.#settle(path.join(root, name2));
|
|
1019
1011
|
});
|
|
1020
1012
|
this.#handle.on("error", (err) => {
|
|
1021
1013
|
this.close();
|
|
@@ -1243,6 +1235,12 @@ class ProjectResolver {
|
|
|
1243
1235
|
}
|
|
1244
1236
|
return out;
|
|
1245
1237
|
}
|
|
1238
|
+
// An avatar.png in the session's cwd is shown beside it in the session list (e.g. a teamflows role's folder)
|
|
1239
|
+
async avatar(cwd) {
|
|
1240
|
+
if (!LOCAL_DIR.test(cwd ?? "")) return null;
|
|
1241
|
+
const file = path.join(cwd, "avatar.png");
|
|
1242
|
+
return await fsp.stat(file).catch(() => null) && file;
|
|
1243
|
+
}
|
|
1246
1244
|
// The nearest ancestor (or dir itself) holding a .git — a dir, or a file in worktrees/submodules — else null
|
|
1247
1245
|
repoRoot(dir) {
|
|
1248
1246
|
if (!LOCAL_DIR.test(dir ?? "")) return Promise.resolve(null);
|
|
@@ -1361,10 +1359,10 @@ class Pricing extends EventEmitter {
|
|
|
1361
1359
|
const anthropic = collect("anthropic.claude-");
|
|
1362
1360
|
const models = /* @__PURE__ */ new Set([...Object.keys(anthropic), ...Object.keys(claude)]);
|
|
1363
1361
|
const out = {};
|
|
1364
|
-
for (const
|
|
1365
|
-
const m = Object.assign({}, anthropic[
|
|
1362
|
+
for (const name2 of models) {
|
|
1363
|
+
const m = Object.assign({}, anthropic[name2], claude[name2]);
|
|
1366
1364
|
const per = (c) => Number.isFinite(c) ? Number((c * MILLION).toPrecision(12)) : void 0;
|
|
1367
|
-
out["claude-" +
|
|
1365
|
+
out["claude-" + name2] = {
|
|
1368
1366
|
input: per(m.input_cost_per_token),
|
|
1369
1367
|
output: per(m.output_cost_per_token),
|
|
1370
1368
|
cacheRead: per(m.cache_read_input_token_cost),
|
|
@@ -1372,7 +1370,7 @@ class Pricing extends EventEmitter {
|
|
|
1372
1370
|
cacheWrite1h: per(m.cache_creation_input_token_cost_above_1hr)
|
|
1373
1371
|
};
|
|
1374
1372
|
const fast = m.provider_specific_entry?.fast;
|
|
1375
|
-
if (typeof fast === "number") out["claude-" +
|
|
1373
|
+
if (typeof fast === "number") out["claude-" + name2].fastModeMplr = fast;
|
|
1376
1374
|
}
|
|
1377
1375
|
if (Object.keys(out).length) {
|
|
1378
1376
|
fs.mkdirSync(DATA_DIR$1, { recursive: true });
|
|
@@ -1439,6 +1437,10 @@ class SessionsService extends EventEmitter {
|
|
|
1439
1437
|
const range = date ? periodBounds(date, granularity) : null;
|
|
1440
1438
|
return this.#requestFile(meta.sessionId, meta.parentSessionId).read(range);
|
|
1441
1439
|
}
|
|
1440
|
+
// Looked up by session, so only avatar paths are readable over IPC
|
|
1441
|
+
avatarPath(filePath) {
|
|
1442
|
+
return this.metaCache.get(filePath)?.avatar ?? null;
|
|
1443
|
+
}
|
|
1442
1444
|
// Subagents send the parent's session id, so the proxy logs their requests into the parent's
|
|
1443
1445
|
// file, tagged x-claude-code-agent-id — route their reads there. The transcript filename is
|
|
1444
1446
|
// `agent-<id>`; RequestFile normalizes that id against the header's own spelling.
|
|
@@ -1513,7 +1515,8 @@ class SessionsService extends EventEmitter {
|
|
|
1513
1515
|
});
|
|
1514
1516
|
await reader.stream((obj) => parser.feed(obj));
|
|
1515
1517
|
const meta = parser.finalize(stat.mtimeMs);
|
|
1516
|
-
|
|
1518
|
+
const [project, avatar] = await Promise.all([this.projects.resolve(meta.project), this.projects.avatar(meta.project)]);
|
|
1519
|
+
return Object.assign(meta, project, { avatar });
|
|
1517
1520
|
}
|
|
1518
1521
|
// Resume/fork copies prior message.ids verbatim. Walk earliest-first; if a
|
|
1519
1522
|
// session's ids overlap one already counted, rescan it with the overlap
|
|
@@ -1636,6 +1639,10 @@ function lockNavigation(contents) {
|
|
|
1636
1639
|
contents.on("will-navigate", block);
|
|
1637
1640
|
contents.on("will-redirect", block);
|
|
1638
1641
|
}
|
|
1642
|
+
function imageDataUrl(file, height) {
|
|
1643
|
+
const image = file && nativeImage.createFromPath(file);
|
|
1644
|
+
return image && !image.isEmpty() ? image.resize({ height, quality: "best" }).toDataURL() : null;
|
|
1645
|
+
}
|
|
1639
1646
|
async function withTimeout(promise, ms, cleanup) {
|
|
1640
1647
|
const { promise: timeout, reject } = Promise.withResolvers();
|
|
1641
1648
|
const timer = setTimeout(reject, ms, new Error(`timed out after ${ms / 1e3}s`));
|
|
@@ -1716,12 +1723,12 @@ class ClaudeSettings {
|
|
|
1716
1723
|
get env() {
|
|
1717
1724
|
return this.cfg.env;
|
|
1718
1725
|
}
|
|
1719
|
-
setEnv(
|
|
1726
|
+
setEnv(name2, value) {
|
|
1720
1727
|
this.cfg.env ??= {};
|
|
1721
|
-
this.cfg.env[
|
|
1728
|
+
this.cfg.env[name2] = value;
|
|
1722
1729
|
}
|
|
1723
|
-
deleteEnv(
|
|
1724
|
-
delete this.cfg.env?.[
|
|
1730
|
+
deleteEnv(name2) {
|
|
1731
|
+
delete this.cfg.env?.[name2];
|
|
1725
1732
|
}
|
|
1726
1733
|
save() {
|
|
1727
1734
|
if (this.loadFailed) throw new Error(`Refusing to save ${CLAUDE_SETTINGS}: settings did not load cleanly`);
|
|
@@ -1732,10 +1739,10 @@ class ClaudeSettings {
|
|
|
1732
1739
|
}
|
|
1733
1740
|
class LoginService {
|
|
1734
1741
|
#service;
|
|
1735
|
-
constructor({ name, command }) {
|
|
1736
|
-
if (process.platform === "win32") this.#service = new TaskScheduler({ name, command });
|
|
1737
|
-
else if (process.platform === "darwin") this.#service = new LaunchAgent({ name, command });
|
|
1738
|
-
else this.#service = new SystemdUnit({ name, command });
|
|
1742
|
+
constructor({ name: name2, command }) {
|
|
1743
|
+
if (process.platform === "win32") this.#service = new TaskScheduler({ name: name2, command });
|
|
1744
|
+
else if (process.platform === "darwin") this.#service = new LaunchAgent({ name: name2, command });
|
|
1745
|
+
else this.#service = new SystemdUnit({ name: name2, command });
|
|
1739
1746
|
}
|
|
1740
1747
|
install() {
|
|
1741
1748
|
return this.#service.install();
|
|
@@ -1755,8 +1762,8 @@ class TaskScheduler {
|
|
|
1755
1762
|
static #SYSTEM32 = path.join(process.env.SystemRoot ?? "C:\\Windows", "System32");
|
|
1756
1763
|
#name;
|
|
1757
1764
|
#command;
|
|
1758
|
-
constructor({ name, command }) {
|
|
1759
|
-
this.#name =
|
|
1765
|
+
constructor({ name: name2, command }) {
|
|
1766
|
+
this.#name = name2;
|
|
1760
1767
|
this.#command = command;
|
|
1761
1768
|
}
|
|
1762
1769
|
// Without admin rights only an interactive task can be registered, and that would open a console
|
|
@@ -1797,10 +1804,10 @@ class LaunchAgent {
|
|
|
1797
1804
|
#command;
|
|
1798
1805
|
#plist;
|
|
1799
1806
|
#domain = `gui/${process.getuid()}`;
|
|
1800
|
-
constructor({ name, command }) {
|
|
1801
|
-
this.#name =
|
|
1807
|
+
constructor({ name: name2, command }) {
|
|
1808
|
+
this.#name = name2;
|
|
1802
1809
|
this.#command = command;
|
|
1803
|
-
this.#plist = path.join(os.homedir(), "Library", "LaunchAgents", `${
|
|
1810
|
+
this.#plist = path.join(os.homedir(), "Library", "LaunchAgents", `${name2}.plist`);
|
|
1804
1811
|
}
|
|
1805
1812
|
// RunAtLoad starts it right away; KeepAlive restarts it only after a crash, so a clean exit stays stopped
|
|
1806
1813
|
async install() {
|
|
@@ -1832,10 +1839,10 @@ class SystemdUnit {
|
|
|
1832
1839
|
#name;
|
|
1833
1840
|
#command;
|
|
1834
1841
|
#unit;
|
|
1835
|
-
constructor({ name, command }) {
|
|
1836
|
-
this.#name =
|
|
1842
|
+
constructor({ name: name2, command }) {
|
|
1843
|
+
this.#name = name2;
|
|
1837
1844
|
this.#command = command;
|
|
1838
|
-
this.#unit = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "systemd", "user", `${
|
|
1845
|
+
this.#unit = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "systemd", "user", `${name2}.service`);
|
|
1839
1846
|
}
|
|
1840
1847
|
async install() {
|
|
1841
1848
|
fs.mkdirSync(path.dirname(this.#unit), { recursive: true });
|
|
@@ -2032,68 +2039,44 @@ class Switchers {
|
|
|
2032
2039
|
claudedir: new ClaudeDirSwitch({ restart })
|
|
2033
2040
|
};
|
|
2034
2041
|
}
|
|
2035
|
-
async status(
|
|
2036
|
-
const s = await this.#get(
|
|
2037
|
-
return
|
|
2042
|
+
async status(name2) {
|
|
2043
|
+
const s = await this.#get(name2).status();
|
|
2044
|
+
return name2 in this.#keepActive ? { ...s, keepActive: this.#keepActive[name2], ephemeral: LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP } : s;
|
|
2038
2045
|
}
|
|
2039
|
-
activate(
|
|
2040
|
-
return this.#attempt(
|
|
2046
|
+
activate(name2, ...args) {
|
|
2047
|
+
return this.#attempt(name2, async (s) => {
|
|
2041
2048
|
await s.activate(...args);
|
|
2042
|
-
this.#keepActive[
|
|
2049
|
+
this.#keepActive[name2] &&= LAUNCH_MODE !== LAUNCH_MODES.NPX_TEMP;
|
|
2043
2050
|
});
|
|
2044
2051
|
}
|
|
2045
2052
|
// an npx-cache run undoes what it activated
|
|
2046
|
-
deactivate(
|
|
2047
|
-
return this.#attempt(
|
|
2053
|
+
deactivate(name2) {
|
|
2054
|
+
return this.#attempt(name2, (s) => s.deactivate());
|
|
2048
2055
|
}
|
|
2049
|
-
setKeepActive(
|
|
2050
|
-
return this.#attempt(
|
|
2056
|
+
setKeepActive(name2, value) {
|
|
2057
|
+
return this.#attempt(name2, () => this.#keepActive[name2] = !!value);
|
|
2051
2058
|
}
|
|
2052
2059
|
// Deactivate every switch not marked keep-active
|
|
2053
2060
|
async deactivateOnQuit() {
|
|
2054
|
-
for (const
|
|
2055
|
-
if (!this.#keepActive[
|
|
2061
|
+
for (const name2 in this.#keepActive) {
|
|
2062
|
+
if (!this.#keepActive[name2]) await this.deactivate(name2);
|
|
2056
2063
|
}
|
|
2057
2064
|
}
|
|
2058
2065
|
// Resolved outside #attempt's try — an unknown name is a bug, thrown raw to IPC, not a toggle error
|
|
2059
|
-
#get(
|
|
2060
|
-
if (!this.#switches[
|
|
2061
|
-
return this.#switches[
|
|
2066
|
+
#get(name2) {
|
|
2067
|
+
if (!this.#switches[name2]) throw new Error(`Unknown switch: ${name2}`);
|
|
2068
|
+
return this.#switches[name2];
|
|
2062
2069
|
}
|
|
2063
|
-
async #attempt(
|
|
2064
|
-
const sw = this.#get(
|
|
2070
|
+
async #attempt(name2, fn) {
|
|
2071
|
+
const sw = this.#get(name2);
|
|
2065
2072
|
try {
|
|
2066
2073
|
await fn(sw);
|
|
2067
|
-
return this.status(
|
|
2074
|
+
return this.status(name2);
|
|
2068
2075
|
} catch (err) {
|
|
2069
|
-
return { ...await this.status(
|
|
2076
|
+
return { ...await this.status(name2), error: String(err?.message || err) };
|
|
2070
2077
|
}
|
|
2071
2078
|
}
|
|
2072
2079
|
}
|
|
2073
|
-
class AutoUpdate {
|
|
2074
|
-
#result = null;
|
|
2075
|
-
// Once per launch — a renderer reload reuses the answer
|
|
2076
|
-
check() {
|
|
2077
|
-
return this.#result ??= this.#fetchNewer().catch((err) => {
|
|
2078
|
-
console.warn("[auto-update] failed:", err.message);
|
|
2079
|
-
return null;
|
|
2080
|
-
});
|
|
2081
|
-
}
|
|
2082
|
-
async install() {
|
|
2083
|
-
const update = await this.check();
|
|
2084
|
-
if (!update) return;
|
|
2085
|
-
const args = [UPDATE_PATH, process.pid, update.latest, ...LAUNCH_MODE === LAUNCH_MODES.NPM_DEV ? ["--no-reopen"] : []].map(String);
|
|
2086
|
-
const [command, commandArgs] = process.platform === "win32" ? ["cmd", ["/c", "start", "Updating Claude Discover", "node", ...args]] : ["node", args];
|
|
2087
|
-
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}`));
|
|
2088
|
-
}
|
|
2089
|
-
async #fetchNewer() {
|
|
2090
|
-
if (LAUNCH_MODE === LAUNCH_MODES.NPM_START) return null;
|
|
2091
|
-
const latest = await latestVersion("claude-discover");
|
|
2092
|
-
const current = app.getVersion();
|
|
2093
|
-
if (LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP) return { current, latest, fromNpx: true };
|
|
2094
|
-
return semver.gt(latest, current) ? { current, latest } : null;
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
2080
|
const EVERY = 6e4;
|
|
2098
2081
|
const toWindow = (w) => w?.utilization == null ? null : {
|
|
2099
2082
|
utilization: w.utilization,
|
|
@@ -2203,6 +2186,10 @@ class MainWindow {
|
|
|
2203
2186
|
this.win.focus();
|
|
2204
2187
|
}
|
|
2205
2188
|
}
|
|
2189
|
+
const name = "claude-discover";
|
|
2190
|
+
const version = "1.10.8";
|
|
2191
|
+
const bin = { "claude-discover": "bin/claude-discover.mjs" };
|
|
2192
|
+
const updater = new NpmUpdater({ name, version, bin }, { dev: false });
|
|
2206
2193
|
class Application {
|
|
2207
2194
|
constructor({ deepLink }) {
|
|
2208
2195
|
this.deepLink = deepLink;
|
|
@@ -2210,12 +2197,11 @@ class Application {
|
|
|
2210
2197
|
this.agentRunner = new AgentRunner();
|
|
2211
2198
|
this.workHours = new WorkHours();
|
|
2212
2199
|
this.sessionsService = new SessionsService();
|
|
2213
|
-
this.autoUpdate = new AutoUpdate();
|
|
2214
2200
|
this.rateLimits = new RateLimits(this.agentRunner);
|
|
2215
2201
|
this.switchers = new Switchers({ restart: () => this.restart() });
|
|
2216
2202
|
}
|
|
2217
2203
|
start() {
|
|
2218
|
-
const { deepLink, win, agentRunner, workHours, sessionsService,
|
|
2204
|
+
const { deepLink, win, agentRunner, workHours, sessionsService, rateLimits, switchers } = this;
|
|
2219
2205
|
Menu.setApplicationMenu(this.#buildMenu());
|
|
2220
2206
|
sessionsService.on("update", (update) => win.send("sessions:update", update));
|
|
2221
2207
|
sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
|
|
@@ -2227,14 +2213,15 @@ class Application {
|
|
|
2227
2213
|
ipcMain.handle("sessions:list", (_e, date, granularity) => sessionsService.list(date, granularity));
|
|
2228
2214
|
ipcMain.handle("sessions:read", (_e, filePath, date, granularity) => sessionsService.readSession(filePath, date, granularity));
|
|
2229
2215
|
ipcMain.handle("sessions:read-requests", (_e, filePath, date, granularity) => sessionsService.readRequests(filePath, date, granularity));
|
|
2230
|
-
ipcMain.handle("
|
|
2231
|
-
ipcMain.handle("switch:
|
|
2232
|
-
ipcMain.handle("switch:
|
|
2233
|
-
ipcMain.handle("switch:
|
|
2216
|
+
ipcMain.handle("sessions:read-avatar", (_e, filePath) => imageDataUrl(sessionsService.avatarPath(filePath), 56));
|
|
2217
|
+
ipcMain.handle("switch:status", (_e, name2) => switchers.status(name2));
|
|
2218
|
+
ipcMain.handle("switch:activate", (_e, name2) => switchers.activate(name2));
|
|
2219
|
+
ipcMain.handle("switch:deactivate", (_e, name2) => switchers.deactivate(name2));
|
|
2220
|
+
ipcMain.handle("switch:keep-active", (_e, name2, value) => switchers.setKeepActive(name2, value));
|
|
2234
2221
|
ipcMain.handle("work-hours:get", () => workHours.read());
|
|
2235
2222
|
ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
|
|
2236
|
-
ipcMain.handle("auto-update:check", () =>
|
|
2237
|
-
ipcMain.handle("auto-update:install", () =>
|
|
2223
|
+
ipcMain.handle("auto-update:check", () => updater.check());
|
|
2224
|
+
ipcMain.handle("auto-update:install", () => updater.install());
|
|
2238
2225
|
ipcMain.handle("rate-limits:get", (_e, opts) => rateLimits.get(opts));
|
|
2239
2226
|
ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
|
|
2240
2227
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
@@ -2303,80 +2290,12 @@ class Application {
|
|
|
2303
2290
|
]);
|
|
2304
2291
|
}
|
|
2305
2292
|
}
|
|
2306
|
-
const APP_ID = "claude-discover";
|
|
2307
|
-
const GLOBAL = LAUNCH_MODE === LAUNCH_MODES.NPM_GLOBAL;
|
|
2308
|
-
const NAME = GLOBAL ? "Claude Discover" : "Claude Discover (local)";
|
|
2309
|
-
class Shortcuts {
|
|
2310
|
-
#os = process.platform === "win32" ? new WindowsLinks() : process.platform === "darwin" ? new MacBundle() : null;
|
|
2311
|
-
// Call before any window is created, so the window picks up the app id.
|
|
2312
|
-
activate() {
|
|
2313
|
-
if (process.platform === "win32") app.setAppUserModelId(APP_ID);
|
|
2314
|
-
if (!this.#os || LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP || LAUNCH_MODE === LAUNCH_MODES.NPM_DEV) return;
|
|
2315
|
-
const created = config.read().shortcuts ?? {};
|
|
2316
|
-
const install = fs.statSync(app.getAppPath()).birthtimeMs;
|
|
2317
|
-
const newInstall = created[NAME] !== install;
|
|
2318
|
-
for (const path2 of this.#os.paths()) {
|
|
2319
|
-
const exists = fs.existsSync(path2);
|
|
2320
|
-
if (!exists && !newInstall) continue;
|
|
2321
|
-
this.#os.write(path2, exists);
|
|
2322
|
-
}
|
|
2323
|
-
if (newInstall) config.save({ shortcuts: { ...created, [NAME]: install } });
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
class WindowsLinks {
|
|
2327
|
-
// @windows
|
|
2328
|
-
paths() {
|
|
2329
|
-
return [join(app.getPath("appData"), "Microsoft/Windows/Start Menu/Programs"), app.getPath("desktop")].map((dir) => join(dir, `${NAME}.lnk`));
|
|
2330
|
-
}
|
|
2331
|
-
write(path2, exists) {
|
|
2332
|
-
const written = shell.writeShortcutLink(path2, exists ? "replace" : "create", {
|
|
2333
|
-
target: process.execPath,
|
|
2334
|
-
args: `"${app.getAppPath()}"`,
|
|
2335
|
-
description: "Browse your local Claude Code sessions",
|
|
2336
|
-
icon: process.execPath,
|
|
2337
|
-
iconIndex: 0,
|
|
2338
|
-
appUserModelId: APP_ID
|
|
2339
|
-
});
|
|
2340
|
-
if (!written) throw new Error(`cannot write ${path2}`);
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
class MacBundle {
|
|
2344
|
-
// @macOS
|
|
2345
|
-
paths() {
|
|
2346
|
-
return [join(app.getPath("home"), "Applications", `${NAME}.app`)];
|
|
2347
|
-
}
|
|
2348
|
-
write(path2) {
|
|
2349
|
-
const electronApp = join(process.execPath, "../../..");
|
|
2350
|
-
const quote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
2351
|
-
fs.mkdirSync(join(path2, "Contents/MacOS"), { recursive: true });
|
|
2352
|
-
fs.mkdirSync(join(path2, "Contents/Resources"), { recursive: true });
|
|
2353
|
-
fs.copyFileSync(join(electronApp, "Contents/Resources/electron.icns"), join(path2, "Contents/Resources/icon.icns"));
|
|
2354
|
-
fs.writeFileSync(join(path2, "Contents/MacOS/launch"), [
|
|
2355
|
-
"#!/bin/sh",
|
|
2356
|
-
`exec open -n ${quote(electronApp)} --env ${quote(`PATH=${process.env.PATH}`)} --args ${quote(app.getAppPath())}`,
|
|
2357
|
-
""
|
|
2358
|
-
].join("\n"), { mode: 493 });
|
|
2359
|
-
fs.writeFileSync(join(path2, "Contents/Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
|
|
2360
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2361
|
-
<plist version="1.0">
|
|
2362
|
-
<dict>
|
|
2363
|
-
<key>CFBundleName</key><string>${NAME}</string>
|
|
2364
|
-
<key>CFBundleIdentifier</key><string>com.dichev.${APP_ID}${GLOBAL ? "" : ".local"}</string>
|
|
2365
|
-
<key>CFBundlePackageType</key><string>APPL</string>
|
|
2366
|
-
<key>CFBundleExecutable</key><string>launch</string>
|
|
2367
|
-
<key>CFBundleIconFile</key><string>icon</string>
|
|
2368
|
-
<key>LSUIElement</key><true/>
|
|
2369
|
-
</dict>
|
|
2370
|
-
</plist>
|
|
2371
|
-
`);
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
2293
|
const isFirstInstance = app.requestSingleInstanceLock();
|
|
2375
2294
|
if (isFirstInstance) {
|
|
2376
2295
|
let application = null;
|
|
2377
2296
|
let deepLink = null;
|
|
2378
2297
|
optional("deep links", () => deepLink = new DeepLink().activate());
|
|
2379
|
-
|
|
2298
|
+
updater.createShortcuts({ appId: "com.dichev.claude-discover", name: "Claude Discover", description: "Browse your local Claude Code sessions" });
|
|
2380
2299
|
app.whenReady().then(() => {
|
|
2381
2300
|
application = new Application({ deepLink });
|
|
2382
2301
|
application.start();
|
package/out/preload/preload.cjs
CHANGED
|
@@ -16,6 +16,7 @@ electron.contextBridge.exposeInMainWorld("api", {
|
|
|
16
16
|
listSessions: (date, granularity = "day") => electron.ipcRenderer.invoke("sessions:list", date, granularity),
|
|
17
17
|
readSession: (filePath, date = null, granularity = "day") => electron.ipcRenderer.invoke("sessions:read", filePath, date, granularity),
|
|
18
18
|
readRequests: (filePath, date = null, granularity = "day") => electron.ipcRenderer.invoke("sessions:read-requests", filePath, date, granularity),
|
|
19
|
+
readAvatar: (filePath) => electron.ipcRenderer.invoke("sessions:read-avatar", filePath),
|
|
19
20
|
onSessionsUpdate: subscribe("sessions:update"),
|
|
20
21
|
onScanProgress: subscribe("sessions:scan-progress"),
|
|
21
22
|
getWorkHours: () => electron.ipcRenderer.invoke("work-hours:get"),
|
|
@@ -728,6 +728,15 @@
|
|
|
728
728
|
|
|
729
729
|
.session-command { color: var(--accent); }
|
|
730
730
|
|
|
731
|
+
.session-avatar {
|
|
732
|
+
flex: none;
|
|
733
|
+
align-self: center;
|
|
734
|
+
width: 28px;
|
|
735
|
+
height: 28px;
|
|
736
|
+
border-radius: 50%;
|
|
737
|
+
object-fit: cover;
|
|
738
|
+
}
|
|
739
|
+
|
|
731
740
|
.subagent-tag,
|
|
732
741
|
.worktree-tag {
|
|
733
742
|
font-family: var(--font-mono), monospace;
|
|
@@ -34,13 +34,13 @@ function useLocalStorage(key, initial) {
|
|
|
34
34
|
}
|
|
35
35
|
function clearOutdatedLocalStorage() {
|
|
36
36
|
const version = localStorage.getItem(VERSION_KEY);
|
|
37
|
-
if (version !== "1.10.
|
|
38
|
-
const [MAJOR, MINOR, PATCH] = "1.10.
|
|
37
|
+
if (version !== "1.10.8") {
|
|
38
|
+
const [MAJOR, MINOR, PATCH] = "1.10.8".split(".");
|
|
39
39
|
const [major, minor, patch] = (version ?? "").split(".");
|
|
40
40
|
if (MAJOR !== major || MINOR !== minor) {
|
|
41
41
|
localStorage.clear();
|
|
42
42
|
}
|
|
43
|
-
localStorage.setItem(VERSION_KEY, "1.10.
|
|
43
|
+
localStorage.setItem(VERSION_KEY, "1.10.8");
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function useMouseFontScale(storageKey) {
|
|
@@ -1059,6 +1059,12 @@ const TIPS = {
|
|
|
1059
1059
|
};
|
|
1060
1060
|
const EFFORT_BADGE = { xhigh: "warn-badge", max: "danger-badge" };
|
|
1061
1061
|
const Badge = ({ className, tip, icon: Icon }) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className, role: "img", "aria-label": tip, "data-tooltip-content": tip, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { size: 11 }) });
|
|
1062
|
+
const avatars = /* @__PURE__ */ new Map();
|
|
1063
|
+
const Avatar = ({ session }) => {
|
|
1064
|
+
if (!avatars.has(session.avatar)) avatars.set(session.avatar, window.api.readAvatar(session.filePath));
|
|
1065
|
+
const src = reactExports.use(avatars.get(session.avatar));
|
|
1066
|
+
return src && /* @__PURE__ */ jsxRuntimeExports.jsx("img", { className: "session-avatar", src, alt: "" });
|
|
1067
|
+
};
|
|
1062
1068
|
function SessionList({ sessions, selectedId, deepLink, onSelect }) {
|
|
1063
1069
|
const selectedRef = reactExports.useRef(null);
|
|
1064
1070
|
const [sortBy, setSortBy] = useLocalStorage("session-list.sort", "time");
|
|
@@ -1151,6 +1157,7 @@ claude-discover://session?id=${deepLink.id}${deepLink.date ? `&date=${deepLink.d
|
|
|
1151
1157
|
onClick: () => onSelect(selectedId === s.filePath ? null : s.filePath),
|
|
1152
1158
|
style: { borderLeftColor: SOURCE_COLORS[s.source] || SOURCE_COLORS.other },
|
|
1153
1159
|
children: [
|
|
1160
|
+
s.avatar && /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(Avatar, { session: s }) }),
|
|
1154
1161
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-row-main", children: [
|
|
1155
1162
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "session-label", children: [
|
|
1156
1163
|
s.tokens?.cacheCreation1h > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { className: "warn-badge", tip: `Used 1h extended cache (${fmtNum(s.tokens.cacheCreation1h)} tokens)
|
|
@@ -3028,12 +3035,15 @@ function StatusBar({ progress, sessionCount = 0 }) {
|
|
|
3028
3035
|
const limits = useRateLimits();
|
|
3029
3036
|
reactExports.useEffect(() => {
|
|
3030
3037
|
window.api.checkUpdate().then(setUpdate);
|
|
3038
|
+
const timer = setInterval(() => window.api.checkUpdate().then(setUpdate), 6 * 60 * 6e4);
|
|
3039
|
+
return () => clearInterval(timer);
|
|
3031
3040
|
}, []);
|
|
3032
3041
|
const updater = { status: update, busy: updating, toggle: async () => {
|
|
3033
3042
|
setUpdating(true);
|
|
3034
3043
|
await window.api.installUpdate();
|
|
3035
3044
|
setUpdating(false);
|
|
3036
3045
|
} };
|
|
3046
|
+
const npx = update?.type === "install";
|
|
3037
3047
|
const proxyOn = proxy.status?.running && proxy.status.configured;
|
|
3038
3048
|
const proxyDown = proxy.status?.configured && proxy.status.running === false;
|
|
3039
3049
|
const settingsOn = settings.status && settings.status.applied === settings.status.total;
|
|
@@ -3051,13 +3061,10 @@ function StatusBar({ progress, sessionCount = 0 }) {
|
|
|
3051
3061
|
] }),
|
|
3052
3062
|
scanning && /* @__PURE__ */ jsxRuntimeExports.jsx(ProgressBar, { pct })
|
|
3053
3063
|
] }),
|
|
3054
|
-
update
|
|
3055
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx(ArrowDownToLine, { size: 12 }),
|
|
3056
|
-
" Install this app"
|
|
3057
|
-
] }),
|
|
3058
|
-
update && !update.fromNpx && /* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: updater, button: `Update to v${update.latest}`, className: "statusbar-update", tooltip: updateTooltip(update), changes: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `npm i -g claude-discover@${update.latest}` }), changesTitle: "Runs after the app closes", children: [
|
|
3064
|
+
update && /* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: updater, button: npx ? "Install globally" : `Update to v${update.latest}`, className: "statusbar-update", tooltip: npx ? npxTooltip : updateTooltip(update), changes: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `npm i -g ${update.spec}` }), changesTitle: "Runs after the app closes", children: [
|
|
3059
3065
|
/* @__PURE__ */ jsxRuntimeExports.jsx(ArrowDownToLine, { size: 12 }),
|
|
3060
|
-
"
|
|
3066
|
+
" ",
|
|
3067
|
+
npx ? "Install this app" : "Update available"
|
|
3061
3068
|
] }),
|
|
3062
3069
|
/* @__PURE__ */ jsxRuntimeExports.jsx(UsageLimits, { limits }),
|
|
3063
3070
|
/* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: settings, on: settingsOn, warn: !!(settings.status && !settingsOn), tooltip: settingsTooltip, changes: settingsChanges, children: [
|
package/out/renderer/index.html
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<meta charset="UTF-8" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>Discover</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-DKilivAV.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="./assets/vendors-C1QIL8dC.js">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="./assets/vendors-CXTqG1IV.css">
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BB2ZzrWB.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-discover",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Claude Discover — a desktop tool to browse your local Claude Code sessions: conversations, token costs, and captured API traffic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,9 +35,8 @@
|
|
|
35
35
|
"electron": "44.4.5",
|
|
36
36
|
"electron-context-menu": "^4.1.2",
|
|
37
37
|
"electron-find-overlay": "^1.0.1",
|
|
38
|
+
"electron-update-from-npm": "^1.0.3",
|
|
38
39
|
"electron-window-state": "^5.0.3",
|
|
39
|
-
"latest-version": "^9.0.0",
|
|
40
|
-
"semver": "^7.8.5",
|
|
41
40
|
"which": "^6.0.1",
|
|
42
41
|
"write-file-atomic": "^7.0.1"
|
|
43
42
|
},
|
package/bin/update.mjs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
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()
|