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 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`));
@@ -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 });
@@ -2032,68 +2039,44 @@ class Switchers {
2032
2039
  claudedir: new ClaudeDirSwitch({ restart })
2033
2040
  };
2034
2041
  }
2035
- async status(name) {
2036
- const s = await this.#get(name).status();
2037
- 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;
2038
2045
  }
2039
- activate(name, ...args) {
2040
- return this.#attempt(name, async (s) => {
2046
+ activate(name2, ...args) {
2047
+ return this.#attempt(name2, async (s) => {
2041
2048
  await s.activate(...args);
2042
- this.#keepActive[name] &&= LAUNCH_MODE !== LAUNCH_MODES.NPX_TEMP;
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(name) {
2047
- return this.#attempt(name, (s) => s.deactivate());
2053
+ deactivate(name2) {
2054
+ return this.#attempt(name2, (s) => s.deactivate());
2048
2055
  }
2049
- setKeepActive(name, value) {
2050
- return this.#attempt(name, () => this.#keepActive[name] = !!value);
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 name in this.#keepActive) {
2055
- if (!this.#keepActive[name]) await this.deactivate(name);
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(name) {
2060
- if (!this.#switches[name]) throw new Error(`Unknown switch: ${name}`);
2061
- return this.#switches[name];
2066
+ #get(name2) {
2067
+ if (!this.#switches[name2]) throw new Error(`Unknown switch: ${name2}`);
2068
+ return this.#switches[name2];
2062
2069
  }
2063
- async #attempt(name, fn) {
2064
- const sw = this.#get(name);
2070
+ async #attempt(name2, fn) {
2071
+ const sw = this.#get(name2);
2065
2072
  try {
2066
2073
  await fn(sw);
2067
- return this.status(name);
2074
+ return this.status(name2);
2068
2075
  } catch (err) {
2069
- return { ...await this.status(name), error: String(err?.message || err) };
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, autoUpdate, rateLimits, switchers } = this;
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("switch:status", (_e, name) => switchers.status(name));
2231
- ipcMain.handle("switch:activate", (_e, name) => switchers.activate(name));
2232
- ipcMain.handle("switch:deactivate", (_e, name) => switchers.deactivate(name));
2233
- 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));
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", () => autoUpdate.check());
2237
- ipcMain.handle("auto-update:install", () => autoUpdate.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
- optional("shortcuts", () => new Shortcuts().activate());
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();
@@ -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.7") {
38
- const [MAJOR, MINOR, PATCH] = "1.10.7".split(".");
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.7");
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?.fromNpx && /* @__PURE__ */ jsxRuntimeExports.jsxs(StatusSwitch, { service: updater, button: "Install globally", className: "statusbar-update", tooltip: npxTooltip, changes: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `npm i -g claude-discover@${update.latest}` }), changesTitle: "Runs after the app closes", children: [
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
- " Update available"
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: [
@@ -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-BnOl_kG3.js"></script>
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-DEYU-s3r.css">
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.7",
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()