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 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? Install it:
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, spawn } from "node:child_process";
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 UPDATE_PATH = join(import.meta.dirname, "../../bin/update.mjs");
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 = (name) => name.startsWith("mcp__");
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, name) => {
1017
- if (!name) return this.#opts.onDrop();
1018
- if (this.#opts.filter(name)) this.#settle(path.join(root, name));
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 name of models) {
1365
- const m = Object.assign({}, anthropic[name], claude[name]);
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-" + name] = {
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-" + name].fastModeMplr = fast;
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
- return Object.assign(meta, await this.projects.resolve(meta.project));
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
- // Days Claude Code keeps transcripts before auto-deleting them; undefined when unset (its default is 30).
1700
- get cleanupPeriodDays() {
1701
- return this.cfg.cleanupPeriodDays;
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 cleanupPeriodDays(days) {
1704
- if (days === void 0) delete this.cfg.cleanupPeriodDays;
1705
- else this.cfg.cleanupPeriodDays = days;
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(name, value) {
1726
+ setEnv(name2, value) {
1720
1727
  this.cfg.env ??= {};
1721
- this.cfg.env[name] = value;
1728
+ this.cfg.env[name2] = value;
1722
1729
  }
1723
- deleteEnv(name) {
1724
- delete this.cfg.env?.[name];
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 = 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 = name;
1807
+ constructor({ name: name2, command }) {
1808
+ this.#name = name2;
1802
1809
  this.#command = command;
1803
- this.#plist = path.join(os.homedir(), "Library", "LaunchAgents", `${name}.plist`);
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 = 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", `${name}.service`);
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 and wait for the port to free up
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
- await this.#settle(false);
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 DEFAULT_DAYS = 30;
1964
- const RETAIN_DAYS = 365;
1965
- class RetentionSwitch {
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 days = new ClaudeSettings().cleanupPeriodDays ?? DEFAULT_DAYS;
1968
- return { days, raised: days >= RETAIN_DAYS };
1983
+ const settings = new ClaudeSettings();
1984
+ return { applied: SETTINGS.filter((s) => isOn(s, settings)).length, total: SETTINGS.length };
1969
1985
  }
1970
- // Raise retention to RETAIN_DAYS; an already-higher value is left as is.
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
- if ((settings.cleanupPeriodDays ?? DEFAULT_DAYS) < RETAIN_DAYS) {
1974
- settings.cleanupPeriodDays = RETAIN_DAYS;
1975
- settings.save();
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
- if (settings.cleanupPeriodDays !== void 0) {
1982
- settings.cleanupPeriodDays = void 0;
1983
- settings.save();
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
- retention: new RetentionSwitch(),
2038
+ settings: new ClaudeSettingsSwitch(),
2022
2039
  claudedir: new ClaudeDirSwitch({ restart })
2023
2040
  };
2024
2041
  }
2025
- async status(name) {
2026
- const s = await this.#get(name).status();
2027
- return name in this.#keepActive ? { ...s, keepActive: this.#keepActive[name], ephemeral: LAUNCH_MODE === LAUNCH_MODES.NPX_TEMP } : s;
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(name, ...args) {
2030
- return this.#attempt(name, async (s) => {
2046
+ activate(name2, ...args) {
2047
+ return this.#attempt(name2, async (s) => {
2031
2048
  await s.activate(...args);
2032
- this.#keepActive[name] &&= LAUNCH_MODE !== LAUNCH_MODES.NPX_TEMP;
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(name) {
2037
- return this.#attempt(name, (s) => s.deactivate());
2053
+ deactivate(name2) {
2054
+ return this.#attempt(name2, (s) => s.deactivate());
2038
2055
  }
2039
- setKeepActive(name, value) {
2040
- return this.#attempt(name, () => this.#keepActive[name] = !!value);
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 name in this.#keepActive) {
2045
- if (!this.#keepActive[name]) await this.deactivate(name);
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(name) {
2050
- if (!this.#switches[name]) throw new Error(`Unknown switch: ${name}`);
2051
- return this.#switches[name];
2066
+ #get(name2) {
2067
+ if (!this.#switches[name2]) throw new Error(`Unknown switch: ${name2}`);
2068
+ return this.#switches[name2];
2052
2069
  }
2053
- async #attempt(name, fn) {
2054
- const sw = this.#get(name);
2070
+ async #attempt(name2, fn) {
2071
+ const sw = this.#get(name2);
2055
2072
  try {
2056
2073
  await fn(sw);
2057
- return this.status(name);
2074
+ return this.status(name2);
2058
2075
  } catch (err) {
2059
- return { ...await this.status(name), error: String(err?.message || err) };
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, autoUpdate, rateLimits, switchers } = this;
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("switch:status", (_e, name) => switchers.status(name));
2221
- ipcMain.handle("switch:activate", (_e, name) => switchers.activate(name));
2222
- ipcMain.handle("switch:deactivate", (_e, name) => switchers.deactivate(name));
2223
- ipcMain.handle("switch:keep-active", (_e, name, value) => switchers.setKeepActive(name, value));
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", () => autoUpdate.check());
2227
- ipcMain.handle("auto-update:install", () => autoUpdate.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("activate", () => {
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
- optional("shortcuts", () => new Shortcuts().activate());
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();
@@ -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"),