claude-discover 1.10.6 → 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 +113 -189
- package/out/preload/preload.cjs +1 -0
- package/out/renderer/assets/{index-BQftbGlQ.css → index-BB2ZzrWB.css} +36 -25
- package/out/renderer/assets/{index-CirBBtX2.js → index-DKilivAV.js} +205 -191
- package/out/renderer/assets/{vendors-BRqWyPqt.js → vendors-C1QIL8dC.js} +2970 -3650
- package/out/renderer/assets/{vendors-B4XMnGdr.css → vendors-CXTqG1IV.css} +1 -1
- package/out/renderer/index.html +4 -4
- package/package.json +4 -5
- 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`));
|
|
@@ -1696,13 +1703,13 @@ class ClaudeSettings {
|
|
|
1696
1703
|
}
|
|
1697
1704
|
}
|
|
1698
1705
|
}
|
|
1699
|
-
//
|
|
1700
|
-
get
|
|
1701
|
-
return this.cfg
|
|
1706
|
+
// Any top-level setting; undefined when unset, and setting undefined removes the key.
|
|
1707
|
+
get(key) {
|
|
1708
|
+
return this.cfg[key];
|
|
1702
1709
|
}
|
|
1703
|
-
set
|
|
1704
|
-
if (
|
|
1705
|
-
else this.cfg
|
|
1710
|
+
set(key, value) {
|
|
1711
|
+
if (value === void 0) delete this.cfg[key];
|
|
1712
|
+
else this.cfg[key] = value;
|
|
1706
1713
|
}
|
|
1707
1714
|
// Claude Code's status line config: { type: 'command', command }; undefined when unset.
|
|
1708
1715
|
get statusLine() {
|
|
@@ -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 });
|
|
@@ -1884,7 +1891,8 @@ class ProxySwitch {
|
|
|
1884
1891
|
} catch {
|
|
1885
1892
|
throw new Error("Cannot reach api.anthropic.com — not enabling capture. Check your network and try again.");
|
|
1886
1893
|
}
|
|
1887
|
-
await this.#exit()
|
|
1894
|
+
if (!await this.#exit())
|
|
1895
|
+
throw new Error(`The running proxy on ${PROXY_URL} did not exit — try again.`);
|
|
1888
1896
|
await this.#service.install();
|
|
1889
1897
|
if (!await this.#settle(true)) {
|
|
1890
1898
|
await this.#service.uninstall().catch(() => {
|
|
@@ -1904,17 +1912,17 @@ class ProxySwitch {
|
|
|
1904
1912
|
settings.save();
|
|
1905
1913
|
await new Promise((r) => setTimeout(r, SETTINGS_RELOAD_GRACE_MS));
|
|
1906
1914
|
}
|
|
1907
|
-
await this.#exit();
|
|
1915
|
+
const exited = await this.#exit();
|
|
1908
1916
|
await this.#service.uninstall();
|
|
1917
|
+
if (!exited) throw new Error(`The proxy on ${PROXY_URL} did not exit and still holds the port.`);
|
|
1909
1918
|
}
|
|
1910
|
-
// Ask a running instance to exit via its control route
|
|
1919
|
+
// Ask a running instance to exit via its control route; true once the port is free
|
|
1911
1920
|
async #exit() {
|
|
1912
1921
|
try {
|
|
1913
1922
|
await fetch(`${PROXY_URL}${EXIT_ROUTE}`, { method: "POST", signal: AbortSignal.timeout(1e3) });
|
|
1914
1923
|
} catch {
|
|
1915
|
-
return;
|
|
1916
1924
|
}
|
|
1917
|
-
|
|
1925
|
+
return this.#settle(false);
|
|
1918
1926
|
}
|
|
1919
1927
|
async #running() {
|
|
1920
1928
|
try {
|
|
@@ -1960,28 +1968,37 @@ class StatuslineSwitch {
|
|
|
1960
1968
|
}
|
|
1961
1969
|
}
|
|
1962
1970
|
}
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
|
|
1971
|
+
const SETTINGS = [
|
|
1972
|
+
{ key: "cleanupPeriodDays", value: 365, fallback: 30, isOn: (days) => days >= 365, oneWay: true },
|
|
1973
|
+
// this app can only browse transcripts Claude Code hasn't swept yet; lowering it would let the next sweep delete them for good
|
|
1974
|
+
{ key: "showThinkingSummaries", value: true, fallback: false }
|
|
1975
|
+
// otherwise transcripts hold only empty thinking blocks
|
|
1976
|
+
];
|
|
1977
|
+
const isOn = (s, settings) => {
|
|
1978
|
+
const value = settings.get(s.key) ?? s.fallback;
|
|
1979
|
+
return s.isOn ? s.isOn(value) : value === s.value;
|
|
1980
|
+
};
|
|
1981
|
+
class ClaudeSettingsSwitch {
|
|
1966
1982
|
status() {
|
|
1967
|
-
const
|
|
1968
|
-
return {
|
|
1983
|
+
const settings = new ClaudeSettings();
|
|
1984
|
+
return { applied: SETTINGS.filter((s) => isOn(s, settings)).length, total: SETTINGS.length };
|
|
1969
1985
|
}
|
|
1970
|
-
//
|
|
1986
|
+
// A value that already counts as on (e.g. a longer retention) is left as is
|
|
1971
1987
|
activate() {
|
|
1972
1988
|
const settings = new ClaudeSettings();
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
settings.
|
|
1989
|
+
const off = SETTINGS.filter((s) => !isOn(s, settings));
|
|
1990
|
+
for (const s of off) {
|
|
1991
|
+
settings.set(s.key, s.value);
|
|
1976
1992
|
}
|
|
1993
|
+
if (off.length) settings.save();
|
|
1977
1994
|
}
|
|
1978
|
-
// Remove the setting — back to Claude Code's default.
|
|
1979
1995
|
deactivate() {
|
|
1980
1996
|
const settings = new ClaudeSettings();
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
settings.
|
|
1997
|
+
const set = SETTINGS.filter((s) => !s.oneWay && settings.get(s.key) !== void 0);
|
|
1998
|
+
for (const s of set) {
|
|
1999
|
+
settings.set(s.key, void 0);
|
|
1984
2000
|
}
|
|
2001
|
+
if (set.length) settings.save();
|
|
1985
2002
|
}
|
|
1986
2003
|
}
|
|
1987
2004
|
class ClaudeDirSwitch {
|
|
@@ -2018,72 +2035,48 @@ class Switchers {
|
|
|
2018
2035
|
this.#switches = {
|
|
2019
2036
|
proxy: new ProxySwitch(),
|
|
2020
2037
|
statusline: new StatuslineSwitch(),
|
|
2021
|
-
|
|
2038
|
+
settings: new ClaudeSettingsSwitch(),
|
|
2022
2039
|
claudedir: new ClaudeDirSwitch({ restart })
|
|
2023
2040
|
};
|
|
2024
2041
|
}
|
|
2025
|
-
async status(
|
|
2026
|
-
const s = await this.#get(
|
|
2027
|
-
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;
|
|
2028
2045
|
}
|
|
2029
|
-
activate(
|
|
2030
|
-
return this.#attempt(
|
|
2046
|
+
activate(name2, ...args) {
|
|
2047
|
+
return this.#attempt(name2, async (s) => {
|
|
2031
2048
|
await s.activate(...args);
|
|
2032
|
-
this.#keepActive[
|
|
2049
|
+
this.#keepActive[name2] &&= LAUNCH_MODE !== LAUNCH_MODES.NPX_TEMP;
|
|
2033
2050
|
});
|
|
2034
2051
|
}
|
|
2035
2052
|
// an npx-cache run undoes what it activated
|
|
2036
|
-
deactivate(
|
|
2037
|
-
return this.#attempt(
|
|
2053
|
+
deactivate(name2) {
|
|
2054
|
+
return this.#attempt(name2, (s) => s.deactivate());
|
|
2038
2055
|
}
|
|
2039
|
-
setKeepActive(
|
|
2040
|
-
return this.#attempt(
|
|
2056
|
+
setKeepActive(name2, value) {
|
|
2057
|
+
return this.#attempt(name2, () => this.#keepActive[name2] = !!value);
|
|
2041
2058
|
}
|
|
2042
2059
|
// Deactivate every switch not marked keep-active
|
|
2043
2060
|
async deactivateOnQuit() {
|
|
2044
|
-
for (const
|
|
2045
|
-
if (!this.#keepActive[
|
|
2061
|
+
for (const name2 in this.#keepActive) {
|
|
2062
|
+
if (!this.#keepActive[name2]) await this.deactivate(name2);
|
|
2046
2063
|
}
|
|
2047
2064
|
}
|
|
2048
2065
|
// Resolved outside #attempt's try — an unknown name is a bug, thrown raw to IPC, not a toggle error
|
|
2049
|
-
#get(
|
|
2050
|
-
if (!this.#switches[
|
|
2051
|
-
return this.#switches[
|
|
2066
|
+
#get(name2) {
|
|
2067
|
+
if (!this.#switches[name2]) throw new Error(`Unknown switch: ${name2}`);
|
|
2068
|
+
return this.#switches[name2];
|
|
2052
2069
|
}
|
|
2053
|
-
async #attempt(
|
|
2054
|
-
const sw = this.#get(
|
|
2070
|
+
async #attempt(name2, fn) {
|
|
2071
|
+
const sw = this.#get(name2);
|
|
2055
2072
|
try {
|
|
2056
2073
|
await fn(sw);
|
|
2057
|
-
return this.status(
|
|
2074
|
+
return this.status(name2);
|
|
2058
2075
|
} catch (err) {
|
|
2059
|
-
return { ...await this.status(
|
|
2076
|
+
return { ...await this.status(name2), error: String(err?.message || err) };
|
|
2060
2077
|
}
|
|
2061
2078
|
}
|
|
2062
2079
|
}
|
|
2063
|
-
class AutoUpdate {
|
|
2064
|
-
#result = null;
|
|
2065
|
-
// Once per launch — a renderer reload reuses the answer
|
|
2066
|
-
check() {
|
|
2067
|
-
return this.#result ??= this.#fetchNewer().catch((err) => {
|
|
2068
|
-
console.warn("[auto-update] failed:", err.message);
|
|
2069
|
-
return null;
|
|
2070
|
-
});
|
|
2071
|
-
}
|
|
2072
|
-
async install() {
|
|
2073
|
-
const update = await this.check();
|
|
2074
|
-
if (!update) return;
|
|
2075
|
-
const args = [UPDATE_PATH, process.pid, update.latest, ...LAUNCH_MODE === LAUNCH_MODES.NPM_DEV ? ["--no-reopen"] : []].map(String);
|
|
2076
|
-
const [command, commandArgs] = process.platform === "win32" ? ["cmd", ["/c", "start", "Updating Claude Discover", "node", ...args]] : ["node", args];
|
|
2077
|
-
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}`));
|
|
2078
|
-
}
|
|
2079
|
-
async #fetchNewer() {
|
|
2080
|
-
if (LAUNCH_MODE === LAUNCH_MODES.NPM_START) return null;
|
|
2081
|
-
const latest = await latestVersion("claude-discover");
|
|
2082
|
-
const current = app.getVersion();
|
|
2083
|
-
if (LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP) return { current, latest, fromNpx: true };
|
|
2084
|
-
return semver.gt(latest, current) ? { current, latest } : null;
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
2080
|
const EVERY = 6e4;
|
|
2088
2081
|
const toWindow = (w) => w?.utilization == null ? null : {
|
|
2089
2082
|
utilization: w.utilization,
|
|
@@ -2193,6 +2186,10 @@ class MainWindow {
|
|
|
2193
2186
|
this.win.focus();
|
|
2194
2187
|
}
|
|
2195
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 });
|
|
2196
2193
|
class Application {
|
|
2197
2194
|
constructor({ deepLink }) {
|
|
2198
2195
|
this.deepLink = deepLink;
|
|
@@ -2200,12 +2197,11 @@ class Application {
|
|
|
2200
2197
|
this.agentRunner = new AgentRunner();
|
|
2201
2198
|
this.workHours = new WorkHours();
|
|
2202
2199
|
this.sessionsService = new SessionsService();
|
|
2203
|
-
this.autoUpdate = new AutoUpdate();
|
|
2204
2200
|
this.rateLimits = new RateLimits(this.agentRunner);
|
|
2205
2201
|
this.switchers = new Switchers({ restart: () => this.restart() });
|
|
2206
2202
|
}
|
|
2207
2203
|
start() {
|
|
2208
|
-
const { deepLink, win, agentRunner, workHours, sessionsService,
|
|
2204
|
+
const { deepLink, win, agentRunner, workHours, sessionsService, rateLimits, switchers } = this;
|
|
2209
2205
|
Menu.setApplicationMenu(this.#buildMenu());
|
|
2210
2206
|
sessionsService.on("update", (update) => win.send("sessions:update", update));
|
|
2211
2207
|
sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
|
|
@@ -2217,26 +2213,22 @@ class Application {
|
|
|
2217
2213
|
ipcMain.handle("sessions:list", (_e, date, granularity) => sessionsService.list(date, granularity));
|
|
2218
2214
|
ipcMain.handle("sessions:read", (_e, filePath, date, granularity) => sessionsService.readSession(filePath, date, granularity));
|
|
2219
2215
|
ipcMain.handle("sessions:read-requests", (_e, filePath, date, granularity) => sessionsService.readRequests(filePath, date, granularity));
|
|
2220
|
-
ipcMain.handle("
|
|
2221
|
-
ipcMain.handle("switch:
|
|
2222
|
-
ipcMain.handle("switch:
|
|
2223
|
-
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));
|
|
2224
2221
|
ipcMain.handle("work-hours:get", () => workHours.read());
|
|
2225
2222
|
ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
|
|
2226
|
-
ipcMain.handle("auto-update:check", () =>
|
|
2227
|
-
ipcMain.handle("auto-update:install", () =>
|
|
2223
|
+
ipcMain.handle("auto-update:check", () => updater.check());
|
|
2224
|
+
ipcMain.handle("auto-update:install", () => updater.install());
|
|
2228
2225
|
ipcMain.handle("rate-limits:get", (_e, opts) => rateLimits.get(opts));
|
|
2229
2226
|
ipcMain.handle("agent:run", (e, text) => agentRunner.run(text, e.sender));
|
|
2230
2227
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
2231
2228
|
ipcMain.handle("deeplink:take-pending", () => deepLink?.takePending() ?? null);
|
|
2232
2229
|
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
2233
2230
|
ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
|
|
2234
|
-
app.on("
|
|
2235
|
-
if (BrowserWindow.getAllWindows().length === 0) win.create();
|
|
2236
|
-
});
|
|
2237
|
-
app.on("window-all-closed", () => {
|
|
2238
|
-
if (process.platform !== "darwin") app.quit();
|
|
2239
|
-
});
|
|
2231
|
+
app.on("window-all-closed", () => app.quit());
|
|
2240
2232
|
app.on("will-quit", async (e) => {
|
|
2241
2233
|
e.preventDefault();
|
|
2242
2234
|
await switchers.deactivateOnQuit();
|
|
@@ -2298,80 +2290,12 @@ class Application {
|
|
|
2298
2290
|
]);
|
|
2299
2291
|
}
|
|
2300
2292
|
}
|
|
2301
|
-
const APP_ID = "claude-discover";
|
|
2302
|
-
const GLOBAL = LAUNCH_MODE === LAUNCH_MODES.NPM_GLOBAL;
|
|
2303
|
-
const NAME = GLOBAL ? "Claude Discover" : "Claude Discover (local)";
|
|
2304
|
-
class Shortcuts {
|
|
2305
|
-
#os = process.platform === "win32" ? new WindowsLinks() : process.platform === "darwin" ? new MacBundle() : null;
|
|
2306
|
-
// Call before any window is created, so the window picks up the app id.
|
|
2307
|
-
activate() {
|
|
2308
|
-
if (process.platform === "win32") app.setAppUserModelId(APP_ID);
|
|
2309
|
-
if (!this.#os || LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP || LAUNCH_MODE === LAUNCH_MODES.NPM_DEV) return;
|
|
2310
|
-
const created = config.read().shortcuts ?? {};
|
|
2311
|
-
const install = fs.statSync(app.getAppPath()).birthtimeMs;
|
|
2312
|
-
const newInstall = created[NAME] !== install;
|
|
2313
|
-
for (const path2 of this.#os.paths()) {
|
|
2314
|
-
const exists = fs.existsSync(path2);
|
|
2315
|
-
if (!exists && !newInstall) continue;
|
|
2316
|
-
this.#os.write(path2, exists);
|
|
2317
|
-
}
|
|
2318
|
-
if (newInstall) config.save({ shortcuts: { ...created, [NAME]: install } });
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
class WindowsLinks {
|
|
2322
|
-
// @windows
|
|
2323
|
-
paths() {
|
|
2324
|
-
return [join(app.getPath("appData"), "Microsoft/Windows/Start Menu/Programs"), app.getPath("desktop")].map((dir) => join(dir, `${NAME}.lnk`));
|
|
2325
|
-
}
|
|
2326
|
-
write(path2, exists) {
|
|
2327
|
-
const written = shell.writeShortcutLink(path2, exists ? "replace" : "create", {
|
|
2328
|
-
target: process.execPath,
|
|
2329
|
-
args: `"${app.getAppPath()}"`,
|
|
2330
|
-
description: "Browse your local Claude Code sessions",
|
|
2331
|
-
icon: process.execPath,
|
|
2332
|
-
iconIndex: 0,
|
|
2333
|
-
appUserModelId: APP_ID
|
|
2334
|
-
});
|
|
2335
|
-
if (!written) throw new Error(`cannot write ${path2}`);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
class MacBundle {
|
|
2339
|
-
// @macOS
|
|
2340
|
-
paths() {
|
|
2341
|
-
return [join(app.getPath("home"), "Applications", `${NAME}.app`)];
|
|
2342
|
-
}
|
|
2343
|
-
write(path2) {
|
|
2344
|
-
const electronApp = join(process.execPath, "../../..");
|
|
2345
|
-
const quote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
2346
|
-
fs.mkdirSync(join(path2, "Contents/MacOS"), { recursive: true });
|
|
2347
|
-
fs.mkdirSync(join(path2, "Contents/Resources"), { recursive: true });
|
|
2348
|
-
fs.copyFileSync(join(electronApp, "Contents/Resources/electron.icns"), join(path2, "Contents/Resources/icon.icns"));
|
|
2349
|
-
fs.writeFileSync(join(path2, "Contents/MacOS/launch"), [
|
|
2350
|
-
"#!/bin/sh",
|
|
2351
|
-
`exec open -n ${quote(electronApp)} --env ${quote(`PATH=${process.env.PATH}`)} --args ${quote(app.getAppPath())}`,
|
|
2352
|
-
""
|
|
2353
|
-
].join("\n"), { mode: 493 });
|
|
2354
|
-
fs.writeFileSync(join(path2, "Contents/Info.plist"), `<?xml version="1.0" encoding="UTF-8"?>
|
|
2355
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2356
|
-
<plist version="1.0">
|
|
2357
|
-
<dict>
|
|
2358
|
-
<key>CFBundleName</key><string>${NAME}</string>
|
|
2359
|
-
<key>CFBundleIdentifier</key><string>com.dichev.${APP_ID}${GLOBAL ? "" : ".local"}</string>
|
|
2360
|
-
<key>CFBundlePackageType</key><string>APPL</string>
|
|
2361
|
-
<key>CFBundleExecutable</key><string>launch</string>
|
|
2362
|
-
<key>CFBundleIconFile</key><string>icon</string>
|
|
2363
|
-
<key>LSUIElement</key><true/>
|
|
2364
|
-
</dict>
|
|
2365
|
-
</plist>
|
|
2366
|
-
`);
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
2293
|
const isFirstInstance = app.requestSingleInstanceLock();
|
|
2370
2294
|
if (isFirstInstance) {
|
|
2371
2295
|
let application = null;
|
|
2372
2296
|
let deepLink = null;
|
|
2373
2297
|
optional("deep links", () => deepLink = new DeepLink().activate());
|
|
2374
|
-
|
|
2298
|
+
updater.createShortcuts({ appId: "com.dichev.claude-discover", name: "Claude Discover", description: "Browse your local Claude Code sessions" });
|
|
2375
2299
|
app.whenReady().then(() => {
|
|
2376
2300
|
application = new Application({ deepLink });
|
|
2377
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"),
|