claude-discover 1.10.3 → 1.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/proxy.mjs CHANGED
@@ -16,20 +16,25 @@ import { parseArgs } from 'node:util'
16
16
 
17
17
  import { HOST, PORT, PROXY_URL, UPSTREAM, EXIT_ROUTE, PING_ROUTE, PING_RESPONSE, REQUESTS_DIR, ERROR_LOG_PATH } from './proxy.config.js'
18
18
 
19
+ // How long an exiting proxy keeps serving in-flight responses — long replies stream for minutes
20
+ const DRAIN_TIMEOUT_MS = 10 * 60_000
21
+
19
22
 
20
23
  // ── 1. Generic tee proxy — knows nothing about Claude ────────────────────────
21
24
  // Forwards verbatim to `upstream` and pipes the response back while buffering both bodies.
22
25
  // `onExchange(exchange)` fires exactly once per request, after piping (its response fields are
23
- // undefined when upstream never responded); `errorBody(err)` shapes the 502 when it's unreachable.
26
+ // undefined when upstream never responded); `errorBody(err)` shapes the 502 when it's unreachable;
27
+ // `onExit()` runs once the exit route has answered.
24
28
 
25
- function createProxy({ upstream, onExchange, onError, errorBody }) {
29
+ function createProxy({ upstream, onExchange, onError, errorBody, onExit }) {
26
30
  const mod = upstream.protocol === 'http:' ? http : https
27
31
 
28
32
  return http.createServer(async (req, res) => {
29
33
  if (req.method === 'POST' && req.url === EXIT_ROUTE) {
30
34
  // Loopback-only control route. The restart client is a bare Node request; reject anything carrying browser fetch metadata so a web page can't CSRF the proxy into exiting.
31
35
  if (req.headers.origin || req.headers.referer || req.headers['sec-fetch-site']) return res.writeHead(403).end('forbidden')
32
- return res.end('bye', () => process.exit(0))
36
+ res.setHeader('connection', 'close')
37
+ return res.end('bye', onExit)
33
38
  }
34
39
  if (req.url === PING_ROUTE) return res.end(PING_RESPONSE)
35
40
  const started = Date.now()
@@ -246,7 +251,17 @@ if (isMain) {
246
251
  onExchange: logRequest,
247
252
  onError: logError,
248
253
  errorBody: err => JSON.stringify({ type: 'error', error: { type: 'api_error', message: `claude-discover proxy: upstream unreachable (${err.code || err.message})` } }),
254
+ onExit: () => drain(),
249
255
  })
256
+ // Stop listening at once (frees the port for a successor) but let in-flight responses finish — exiting outright would cut a reply mid-stream
257
+ let draining = false
258
+ const drain = () => {
259
+ if (draining) return
260
+ draining = true
261
+ server.close(() => process.exit(0))
262
+ setTimeout(() => process.exit(0), DRAIN_TIMEOUT_MS)
263
+ }
264
+ process.on('SIGTERM', drain) // @macOS @linux launchd/systemd stop the service this way, right after ProxySwitch's exit request
250
265
  if (args.values.restart) {
251
266
  // Ask a previous instance (the port is loopback-only and assumed ours) to exit before listening.
252
267
  const replaced = await new Promise(resolve => http.request(`${PROXY_URL}${EXIT_ROUTE}`, { method: 'POST' }, () => resolve(true)).on('error', () => resolve(false)).end())
package/out/main/main.js CHANGED
@@ -435,7 +435,7 @@ const MEMORY_LABELS = {
435
435
  "user's private project instructions, not checked in": "Local",
436
436
  "user's auto-memory, persists across conversations": "Auto"
437
437
  };
438
- const MEMORY_FILES = { User: "CLAUDE.md", Project: "CLAUDE.md", Local: "CLAUDE.local.md", Auto: "MEMORY.md" };
438
+ const MEMORY_FILES = { User: ["CLAUDE.md"], Project: ["CLAUDE.md", "AGENTS.md"], Local: ["CLAUDE.local.md"], Auto: ["MEMORY.md"] };
439
439
  function splitClaudeMd(text) {
440
440
  const marker = text.match(/\n# claudeMd\r?\n|Codebase and user instructions are shown below\./);
441
441
  if (!marker) return { files: [], rest: text };
@@ -449,7 +449,7 @@ function splitClaudeMd(text) {
449
449
  const base = h[1].split(/[\\/]/).pop();
450
450
  return {
451
451
  file_path: h[1],
452
- name: base === MEMORY_FILES[memory_type] ? base : h[1],
452
+ name: MEMORY_FILES[memory_type]?.includes(base) ? base : h[1],
453
453
  memory_type,
454
454
  content: region.slice(h.index + h[0].length, heads[i + 1]?.index ?? region.length).trim()
455
455
  };
@@ -1608,21 +1608,6 @@ class ClaudeSettings {
1608
1608
  }
1609
1609
  }
1610
1610
  }
1611
- // Removes every hook (any event) whose command matches `pattern`, dropping groups and events
1612
- // left empty. Returns the removed commands.
1613
- removeHooks(pattern) {
1614
- const matches = (h) => pattern.test(h.command ?? "");
1615
- const removed = [];
1616
- for (const [event, groups] of Object.entries(this.cfg.hooks ?? {})) {
1617
- const hit = groups.flatMap((g) => (g.hooks ?? []).filter(matches).map((h) => h.command));
1618
- if (!hit.length) continue;
1619
- removed.push(...hit);
1620
- for (const g of groups) g.hooks = (g.hooks ?? []).filter((h) => !matches(h));
1621
- this.cfg.hooks[event] = groups.filter((g) => g.hooks.length);
1622
- if (!this.cfg.hooks[event].length) delete this.cfg.hooks[event];
1623
- }
1624
- return removed;
1625
- }
1626
1611
  // Days Claude Code keeps transcripts before auto-deleting them; undefined when unset (its default is 30).
1627
1612
  get cleanupPeriodDays() {
1628
1613
  return this.cfg.cleanupPeriodDays;
@@ -1790,15 +1775,9 @@ WantedBy=default.target
1790
1775
  }
1791
1776
  }
1792
1777
  const NODE = await which("node", { nothrow: true }) ?? "node";
1778
+ const SETTINGS_RELOAD_GRACE_MS = 3e3;
1793
1779
  class ProxySwitch {
1794
1780
  #service = new LoginService({ name: "claude-discover-proxy", command: [NODE, PROXY_PATH] });
1795
- constructor() {
1796
- try {
1797
- const settings = new ClaudeSettings();
1798
- if (settings.removeHooks(/claude-discover/).length) settings.save();
1799
- } catch {
1800
- }
1801
- }
1802
1781
  async status() {
1803
1782
  return {
1804
1783
  running: await this.#running(),
@@ -1828,16 +1807,17 @@ class ProxySwitch {
1828
1807
  settings.setEnv("ENABLE_TOOL_SEARCH", "true");
1829
1808
  settings.save();
1830
1809
  }
1831
- // Stop the proxy, remove the service and our env keys — a foreign base URL is left alone
1810
+ // Remove our env keys, then stop the proxy and remove the service — a foreign base URL is left alone
1832
1811
  async deactivate() {
1833
- await this.#exit();
1834
- await this.#service.uninstall();
1835
1812
  const settings = new ClaudeSettings();
1836
1813
  if (settings.env?.ANTHROPIC_BASE_URL === PROXY_URL) {
1837
- settings.deleteEnv("ANTHROPIC_BASE_URL");
1814
+ settings.setEnv("ANTHROPIC_BASE_URL", "");
1838
1815
  settings.deleteEnv("ENABLE_TOOL_SEARCH");
1839
1816
  settings.save();
1817
+ await new Promise((r) => setTimeout(r, SETTINGS_RELOAD_GRACE_MS));
1840
1818
  }
1819
+ await this.#exit();
1820
+ await this.#service.uninstall();
1841
1821
  }
1842
1822
  // Ask a running instance to exit via its control route and wait for the port to free up
1843
1823
  async #exit() {
@@ -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.3") {
38
- const [MAJOR, MINOR, PATCH] = "1.10.3".split(".");
37
+ if (version !== "1.10.4") {
38
+ const [MAJOR, MINOR, PATCH] = "1.10.4".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.3");
43
+ localStorage.setItem(VERSION_KEY, "1.10.4");
44
44
  }
45
45
  }
46
46
  function useMouseFontScale(storageKey) {
@@ -1550,6 +1550,7 @@ function ConversationView({ items, instructions = [], filePath = null, expandAll
1550
1550
  "Continues from ",
1551
1551
  format(continuesFrom, "MMM d, yyyy HH:mm")
1552
1552
  ] }),
1553
+ instructions.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "conv-row conv-row-instruction", children: /* @__PURE__ */ jsxRuntimeExports.jsx(InstructionsMissing, {}) }),
1553
1554
  groups.map((g, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1554
1555
  i === branch?.index && branchDivider,
1555
1556
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `conv-row conv-row-${g.kind}`, children: [
@@ -1857,13 +1858,21 @@ function safeJson(x) {
1857
1858
  function JsonBlock({ value }) {
1858
1859
  return /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { className: "block-text", text: "```json\n" + safeJson(value) + "\n```" });
1859
1860
  }
1861
+ const ProxyHeader = ({ children }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "msg-header msg-proxy", children: [
1862
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "msg-icon" }),
1863
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-author", children: "Proxy" }),
1864
+ children
1865
+ ] });
1866
+ function InstructionsMissing() {
1867
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "instruction-missing", children: [
1868
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ProxyHeader, { children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-summary", children: "not captured" }) }),
1869
+ "Turn on the Capture proxy to see the system prompt, tools and memory files."
1870
+ ] });
1871
+ }
1860
1872
  function InstructionRun({ turns, model }) {
1861
1873
  const strips = turns.flatMap((t) => t.blocks).map(({ it }) => ({ ...it, tokens: countTokens(it.content, it.model ?? model) }));
1862
1874
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
1863
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "msg-header msg-proxy", children: [
1864
- /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "msg-icon" }),
1865
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "msg-author", children: "Proxy" })
1866
- ] }),
1875
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ProxyHeader, {}),
1867
1876
  groupInstructions(strips).map(([label, list]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
1868
1877
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "aux instruction-source", children: label }),
1869
1878
  list.map((it) => /* @__PURE__ */ jsxRuntimeExports.jsx(InstructionFile, { it, model, tokens: it.tokens }, `${it.hash ?? it.file_path}-${it.timestamp}`))
@@ -2112,7 +2121,7 @@ const Pane = React.memo(function Pane2({ value, headers, seen, expandAll }) {
2112
2121
  ] })
2113
2122
  ] });
2114
2123
  });
2115
- function RequestsView({ filePath, date, granularity = "day", fileSize = 0, expandAll = null }) {
2124
+ function RequestsView({ filePath, items, date, granularity = "day", fileSize = 0, expandAll = null }) {
2116
2125
  const [records, setRecords] = reactExports.useState(null);
2117
2126
  const [selected, setSelected] = reactExports.useState(0);
2118
2127
  const [tab, setTab] = reactExports.useState("request");
@@ -2143,21 +2152,49 @@ function RequestsView({ filePath, date, granularity = "day", fileSize = 0, expan
2143
2152
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: error ? /* @__PURE__ */ jsxRuntimeExports.jsx(ErrorMessage, { title: "Couldn't load the captured requests", error }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-empty", children: "Loading…" }) });
2144
2153
  }
2145
2154
  if (!records.length) {
2146
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-empty", children: "No captured requests for this session. Requests are recorded only while the capture proxy is running." }) });
2155
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-view", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-pitch", children: [
2156
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { children: [
2157
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "requests-pitch-icon" }),
2158
+ "No API requests captured"
2159
+ ] }),
2160
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "See the system prompt, tools and hidden calls behind every reply." }),
2161
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "requests-pitch-hint", children: [
2162
+ "Turn on ",
2163
+ /* @__PURE__ */ jsxRuntimeExports.jsx("b", { children: "Capture proxy" }),
2164
+ " in the status bar."
2165
+ ] })
2166
+ ] }) });
2147
2167
  }
2168
+ const captured = new Set(records.map((r) => r.responseHeaders?.["request-id"]));
2169
+ const missed = items?.filter((o) => o.requestId && !captured.has(o.requestId)) ?? [];
2170
+ const gapRow = (from = "", to) => {
2171
+ const n = new Set(missed.filter((o) => o.timestamp >= from && (!to || o.timestamp < to)).map((o) => o.requestId)).size;
2172
+ return n > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: "requests-missed", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Divider, { children: [
2173
+ n,
2174
+ " ",
2175
+ n === 1 ? "reply" : "replies",
2176
+ " not captured (proxy was off)"
2177
+ ] }) });
2178
+ };
2148
2179
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-view", children: [
2149
- /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "requests-list", children: records.map((r, i) => {
2150
- const [method, path] = splitUrl(r.url);
2151
- return /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: i > 0 && r.kind?.[0] === "main" ? "new-turn" : "", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: i === selected ? "active" : "", onClick: () => setSelected(i), children: [
2152
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-method ${method.toLowerCase()}`, children: method }),
2153
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-url", children: path }),
2154
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-status ${!r.status || r.status >= 400 ? "error" : "ok"}`, children: r.status ?? "—" }),
2155
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta", children: time(r.timestamp) }),
2156
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-duration", children: duration(r.durationMs) }),
2157
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-kind ${r.kind ? `kind-${r.kind[0]}` : ""}`, children: r.kind?.[1] }),
2158
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-tokens", title: "total tokens (input + output + cache)", children: tokens(totalTokens(r)) })
2159
- ] }) }, i);
2160
- }) }),
2180
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "requests-list", children: [
2181
+ records.map((r, i) => {
2182
+ const [method, path] = splitUrl(r.url);
2183
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [
2184
+ gapRow(records[i - 1]?.timestamp, r.timestamp),
2185
+ /* @__PURE__ */ jsxRuntimeExports.jsx("li", { className: i > 0 && r.kind?.[0] === "main" ? "new-turn" : "", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { type: "button", className: i === selected ? "active" : "", onClick: () => setSelected(i), children: [
2186
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-method ${method.toLowerCase()}`, children: method }),
2187
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-url", children: path }),
2188
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-status ${!r.status || r.status >= 400 ? "error" : "ok"}`, children: r.status ?? "—" }),
2189
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta", children: time(r.timestamp) }),
2190
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-duration", children: duration(r.durationMs) }),
2191
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `requests-kind ${r.kind ? `kind-${r.kind[0]}` : ""}`, children: r.kind?.[1] }),
2192
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "requests-meta requests-tokens", title: "total tokens (input + output + cache)", children: tokens(totalTokens(r)) })
2193
+ ] }) })
2194
+ ] }, i);
2195
+ }),
2196
+ gapRow(records.at(-1).timestamp)
2197
+ ] }),
2161
2198
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "requests-detail", children: [
2162
2199
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "requests-tabs", children: ["request", "response"].map((t) => /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: t === tab ? "active" : "", onClick: () => setTab(t), children: t }, t)) }),
2163
2200
  /* @__PURE__ */ jsxRuntimeExports.jsx(Pane, { value: body, headers, seen, expandAll })
@@ -2594,7 +2631,7 @@ function Session({ meta, missing, date, granularity = "day", onShowPeriodOf }) {
2594
2631
  }
2595
2632
  )
2596
2633
  ] }),
2597
- mode === "conversation" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tab-pane-content", children: items ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationView, { items, instructions, filePath: meta.parentFilePath ?? meta.filePath, expandAll, continuesFrom: meta.continuesFrom, continuesTo: meta.continuesTo, onShowPeriodOf }) : loadError || /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "empty", children: "Loading conversation…" }) }) : mode === "jsonl" ? !items && loadError || /* @__PURE__ */ jsxRuntimeExports.jsx(JsonlView, { items, expandAll }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RequestsView, { filePath, date, granularity, fileSize, expandAll })
2634
+ mode === "conversation" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "view-tab-pane-content", children: items ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationView, { items, instructions, filePath: meta.parentFilePath ?? meta.filePath, expandAll, continuesFrom: meta.continuesFrom, continuesTo: meta.continuesTo, onShowPeriodOf }) : loadError || /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "empty", children: "Loading conversation…" }) }) : mode === "jsonl" ? !items && loadError || /* @__PURE__ */ jsxRuntimeExports.jsx(JsonlView, { items, expandAll }) : /* @__PURE__ */ jsxRuntimeExports.jsx(RequestsView, { filePath, items, date, granularity, fileSize, expandAll })
2598
2635
  ] }),
2599
2636
  /* @__PURE__ */ jsxRuntimeExports.jsx(SessionSummary, { meta, items, instructions, agent, granularity })
2600
2637
  ] }) }) }) }) });
@@ -2663,7 +2700,7 @@ function StatusSwitch({ service, on, warn, button, className = "", tooltip, chan
2663
2700
  }
2664
2701
  const proxyTooltip = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2665
2702
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Captures Claude Code's raw API traffic - system prompt, tool definitions, injected reminders, responses." }),
2666
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Applies only to newly started Claude Code sessions." })
2703
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "Applies to running Claude Code sessions too." })
2667
2704
  ] });
2668
2705
  const proxyChanges = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2669
2706
  /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: `"env": {
@@ -1085,12 +1085,19 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
1085
1085
  /* Instruction files (system prompt / CLAUDE.md / memory) extracted from the captured API requests,
1086
1086
  and the label over each group — one unbroken strip down the whole run. */
1087
1087
  .instruction-file,
1088
- .instruction-source {
1088
+ .instruction-source,
1089
+ .instruction-missing {
1089
1090
  border-left: 2px solid #a78bfa;
1090
1091
  padding-left: 8px;
1091
1092
  margin-left: -10px;
1092
1093
  }
1093
1094
 
1095
+ /* Stands in for the strips when nothing was captured (see InstructionsMissing). */
1096
+ .instruction-missing {
1097
+ color: var(--text-faint);
1098
+ font-size: calc(11px * var(--font-scale, 1));
1099
+ }
1100
+
1094
1101
  /* Group label over the strips read from one part of the request (system prompt / tools / user message). */
1095
1102
  .instruction-source {
1096
1103
  font-family: var(--font-mono), monospace;
@@ -1254,223 +1261,256 @@ button.msg-summary:hover { color: var(--text); background: var(--bg-elev-2); bor
1254
1261
  button { flex: none; padding: 0 6px; border: none; background: transparent; color: var(--text-faint); font-size: 16px; line-height: 1; }
1255
1262
  button:hover { color: var(--text); background: transparent; }
1256
1263
  }
1257
- .requests-view {
1258
- flex: 1;
1259
- display: flex;
1260
- min-height: 0;
1261
- background: var(--bg);
1262
- font-size: 12px;
1263
- }
1264
-
1265
- .requests-list {
1266
- flex: 0 0 550px;
1267
- overflow: auto;
1268
- border-right: 1px solid var(--border);
1269
- margin: 0;
1270
- padding: 20px 10px;
1271
- list-style: none;
1272
- }
1273
-
1274
- .requests-list li { margin: 0; }
1275
-
1276
- /* separator above the request that starts a new user turn */
1277
- .requests-list li.new-turn {
1278
- border-top: 1px solid var(--border);
1279
- margin-top: 6px;
1280
- padding-top: 6px;
1281
- }
1282
-
1283
- .requests-list button {
1284
- display: flex;
1285
- align-items: center;
1286
- gap: 8px;
1287
- width: 100%;
1288
- background: transparent;
1289
- border: 0;
1290
- color: var(--text);
1291
- text-align: left;
1292
- padding: 4px 10px;
1293
- font: inherit;
1294
- cursor: pointer;
1295
-
1296
- &:hover { background: var(--hover, rgba(255, 255, 255, 0.06)); }
1297
- &.active { background: var(--hover, rgba(255, 255, 255, 0.1)); }
1298
- }
1299
-
1300
- .requests-url {
1301
- flex: 1;
1302
- color: var(--text-faint);
1303
- overflow: hidden;
1304
- text-overflow: ellipsis;
1305
- white-space: nowrap;
1306
- }
1307
-
1308
- /* category label, last column — fixed width so the labels line up left-aligned,
1309
- set off from the metrics by a full-height vertical rule; colors come from the
1310
- shared .kind-* palette in styles.css */
1311
- .requests-kind {
1312
- flex: 0 0 106px;
1313
- align-self: stretch;
1314
- display: flex;
1315
- align-items: center;
1316
- padding: 0 8px;
1317
- border-left: 1px solid var(--border);
1318
- overflow: hidden;
1319
- white-space: nowrap;
1320
- }
1321
-
1322
- /* Postman-style method colors — muted, kept local so they don't track the app palette */
1323
- .requests-method {
1324
- flex: 0 0 34px;
1325
- font-weight: 600;
1326
-
1327
- &.get { color: #6ea882; }
1328
- &.post { color: #6b93c4; }
1329
- &.put { color: #c2835f; }
1330
- &.delete { color: #c47b7b; }
1331
- }
1332
-
1333
- .requests-status {
1334
- font-variant-numeric: tabular-nums;
1335
-
1336
- &.ok { color: var(--text-faint); }
1337
- &.error { color: var(--danger); font-weight: bold; }
1338
- }
1339
-
1340
- .requests-meta {
1341
- color: var(--text-faint);
1342
- font-variant-numeric: tabular-nums;
1343
- }
1344
-
1345
- .requests-tokens {
1346
- min-width: 42px;
1347
- text-align: right;
1348
- }
1349
-
1350
- .requests-duration {
1351
- min-width: 44px;
1352
- text-align: right;
1353
- }
1354
-
1355
- .requests-detail {
1356
- flex: 1;
1357
- min-width: 0;
1358
- display: flex;
1359
- flex-direction: column;
1360
- }
1361
-
1362
- .requests-tabs {
1363
- display: flex;
1364
- gap: 14px;
1365
- padding: 5px 12px;
1366
- border-bottom: 1px solid var(--border);
1367
-
1368
- & button {
1369
- background: transparent;
1370
- border: 0;
1371
- padding: 2px 0;
1372
- color: var(--text-faint);
1373
- text-transform: uppercase;
1374
- font: inherit;
1375
- font-size: 11px;
1376
- letter-spacing: 0.5px;
1377
- cursor: pointer;
1378
-
1379
- &:hover { color: var(--text); }
1380
- &.active { color: var(--text); }
1381
- }
1382
- }
1383
-
1384
- .requests-pane {
1385
- flex: 1;
1386
- min-width: 0;
1387
- overflow: auto;
1388
- padding: 8px 12px 12px;
1389
-
1390
- /* Shift/Ctrl+wheel zoom (useMouseFontScale): font sizes below multiply this scale. */
1391
- & .w-json-view-container {
1392
- --w-rjv-background-color: rgb(6 6 6) !important;
1393
- font-size: calc(12px * var(--font-scale, 1)) !important;
1394
- line-height: 1.2 !important;
1395
- }
1396
- }
1397
-
1398
- /* the hand-made braces / `"messages": [ … ]` lines stitching the body tree and the per-message
1399
- trees into one document, styled to blend in with JsonView (theme vars come from the wrapper's
1400
- inline style; indents mirror its KeyValues: 6px margin + 15px padding + guide line) */
1401
- .requests-doc {
1402
- font-family: var(--w-rjv-font-family, monospace);
1403
- font-size: calc(12px * var(--font-scale, 1));
1404
- line-height: 1.2;
1405
-
1406
- & .key { color: var(--w-rjv-key-string); }
1407
- & .colon { color: var(--w-rjv-colon-color); }
1408
- & .bracket { color: var(--w-rjv-brackets-color); }
1409
-
1410
- /* the root braces are ours, so hide the body tree's own open row (arrow + brace) and closing
1411
- brace; the tree is the wrapper's direct child, unlike the nested per-message trees.
1412
- !important beats the open row's inline display:inline-flex */
1413
- & > .w-json-view-container > :is(span:first-child, div:not(.w-rjv-wrap)) { display: none !important; }
1414
-
1415
- & .requests-msgs,
1416
- & .requests-msgs .items {
1417
- margin-left: 6px;
1418
- padding-left: 15px;
1419
- border-left: 1px solid var(--w-rjv-line-color);
1420
- }
1421
-
1422
- & .w-rjv-value {
1423
- color: var(--w-rjv-type-string-color)
1424
- }
1425
- }
1426
-
1427
- /* summary shown inside a collapsed node in place of the '...' ellipsis */
1428
- .requests-hint {
1429
- cursor: pointer;
1430
- user-select: none;
1431
- font-style: italic;
1432
- color: var(--text-faint);
1433
- padding: 0 3px;
1434
- white-space: pre; /* keeps the role's alignment padding */
1435
- }
1436
-
1437
- /* parts already sent in an earlier request ($refs in the log) fade so what's new stands out */
1438
- .requests-seen,
1439
- .requests-pane :is(.w-rjv-inner, .w-rjv-line):has(> span .requests-seen-key) {
1440
- opacity: 0.6;
1441
- }
1442
-
1443
- .requests-headers {
1444
- padding-bottom: 8px;
1445
- margin-bottom: 8px;
1446
- border-bottom: 1px solid var(--border);
1447
- }
1448
-
1449
- .requests-label {
1450
- display: flex;
1451
- justify-content: space-between;
1452
- color: var(--text-faint);
1453
- text-transform: uppercase;
1454
- font-size: calc(11px * var(--font-scale, 1));
1455
- letter-spacing: 0.5px;
1456
- padding: 2px 0 6px;
1457
- }
1458
-
1459
- /* explains the seen-part fading to first-time viewers */
1460
- .requests-label-hint {
1461
- text-transform: none;
1462
- letter-spacing: normal;
1463
- font-style: italic;
1464
- }
1465
-
1466
- .requests-pane-empty,
1467
- .requests-empty {
1468
- color: var(--text-faint);
1469
- }
1470
-
1471
- .requests-empty {
1472
- padding: 10px 20px;
1473
- }
1264
+ .requests-view {
1265
+ flex: 1;
1266
+ display: flex;
1267
+ min-height: 0;
1268
+ background: var(--bg);
1269
+ font-size: 12px;
1270
+ }
1271
+
1272
+ .requests-list {
1273
+ flex: 0 0 550px;
1274
+ overflow: auto;
1275
+ border-right: 1px solid var(--border);
1276
+ margin: 0;
1277
+ padding: 20px 10px;
1278
+ list-style: none;
1279
+ }
1280
+
1281
+ .requests-list li { margin: 0; }
1282
+
1283
+ /* separator above the request that starts a new user turn */
1284
+ .requests-list li.new-turn {
1285
+ border-top: 1px solid var(--border);
1286
+ margin-top: 6px;
1287
+ padding-top: 6px;
1288
+ }
1289
+
1290
+ .requests-list li.requests-missed {
1291
+ margin: 18px 0;
1292
+ padding: 6px 10px;
1293
+ background: var(--bg-elev);
1294
+ border-radius: 4px;
1295
+ }
1296
+
1297
+ /* the wavy divider already separates the turns */
1298
+ .requests-list li.requests-missed + li.new-turn {
1299
+ border-top: 0;
1300
+ margin-top: 0;
1301
+ padding-top: 0;
1302
+ }
1303
+
1304
+ .requests-list button {
1305
+ display: flex;
1306
+ align-items: center;
1307
+ gap: 8px;
1308
+ width: 100%;
1309
+ background: transparent;
1310
+ border: 0;
1311
+ color: var(--text);
1312
+ text-align: left;
1313
+ padding: 4px 10px;
1314
+ font: inherit;
1315
+ cursor: pointer;
1316
+
1317
+ &:hover { background: var(--hover, rgba(255, 255, 255, 0.06)); }
1318
+ &.active { background: var(--hover, rgba(255, 255, 255, 0.1)); }
1319
+ }
1320
+
1321
+ .requests-url {
1322
+ flex: 1;
1323
+ color: var(--text-faint);
1324
+ overflow: hidden;
1325
+ text-overflow: ellipsis;
1326
+ white-space: nowrap;
1327
+ }
1328
+
1329
+ /* category label, last column — fixed width so the labels line up left-aligned,
1330
+ set off from the metrics by a full-height vertical rule; colors come from the
1331
+ shared .kind-* palette in styles.css */
1332
+ .requests-kind {
1333
+ flex: 0 0 106px;
1334
+ align-self: stretch;
1335
+ display: flex;
1336
+ align-items: center;
1337
+ padding: 0 8px;
1338
+ border-left: 1px solid var(--border);
1339
+ overflow: hidden;
1340
+ white-space: nowrap;
1341
+ }
1342
+
1343
+ /* Postman-style method colors — muted, kept local so they don't track the app palette */
1344
+ .requests-method {
1345
+ flex: 0 0 34px;
1346
+ font-weight: 600;
1347
+
1348
+ &.get { color: #6ea882; }
1349
+ &.post { color: #6b93c4; }
1350
+ &.put { color: #c2835f; }
1351
+ &.delete { color: #c47b7b; }
1352
+ }
1353
+
1354
+ .requests-status {
1355
+ font-variant-numeric: tabular-nums;
1356
+
1357
+ &.ok { color: var(--text-faint); }
1358
+ &.error { color: var(--danger); font-weight: bold; }
1359
+ }
1360
+
1361
+ .requests-meta {
1362
+ color: var(--text-faint);
1363
+ font-variant-numeric: tabular-nums;
1364
+ }
1365
+
1366
+ .requests-tokens {
1367
+ min-width: 42px;
1368
+ text-align: right;
1369
+ }
1370
+
1371
+ .requests-duration {
1372
+ min-width: 44px;
1373
+ text-align: right;
1374
+ }
1375
+
1376
+ .requests-detail {
1377
+ flex: 1;
1378
+ min-width: 0;
1379
+ display: flex;
1380
+ flex-direction: column;
1381
+ }
1382
+
1383
+ .requests-tabs {
1384
+ display: flex;
1385
+ gap: 14px;
1386
+ padding: 5px 12px;
1387
+ border-bottom: 1px solid var(--border);
1388
+
1389
+ & button {
1390
+ background: transparent;
1391
+ border: 0;
1392
+ padding: 2px 0;
1393
+ color: var(--text-faint);
1394
+ text-transform: uppercase;
1395
+ font: inherit;
1396
+ font-size: 11px;
1397
+ letter-spacing: 0.5px;
1398
+ cursor: pointer;
1399
+
1400
+ &:hover { color: var(--text); }
1401
+ &.active { color: var(--text); }
1402
+ }
1403
+ }
1404
+
1405
+ .requests-pane {
1406
+ flex: 1;
1407
+ min-width: 0;
1408
+ overflow: auto;
1409
+ padding: 8px 12px 12px;
1410
+
1411
+ /* Shift/Ctrl+wheel zoom (useMouseFontScale): font sizes below multiply this scale. */
1412
+ & .w-json-view-container {
1413
+ --w-rjv-background-color: rgb(6 6 6) !important;
1414
+ font-size: calc(12px * var(--font-scale, 1)) !important;
1415
+ line-height: 1.2 !important;
1416
+ }
1417
+ }
1418
+
1419
+ /* the hand-made braces / `"messages": [ … ]` lines stitching the body tree and the per-message
1420
+ trees into one document, styled to blend in with JsonView (theme vars come from the wrapper's
1421
+ inline style; indents mirror its KeyValues: 6px margin + 15px padding + guide line) */
1422
+ .requests-doc {
1423
+ font-family: var(--w-rjv-font-family, monospace);
1424
+ font-size: calc(12px * var(--font-scale, 1));
1425
+ line-height: 1.2;
1426
+
1427
+ & .key { color: var(--w-rjv-key-string); }
1428
+ & .colon { color: var(--w-rjv-colon-color); }
1429
+ & .bracket { color: var(--w-rjv-brackets-color); }
1430
+
1431
+ /* the root braces are ours, so hide the body tree's own open row (arrow + brace) and closing
1432
+ brace; the tree is the wrapper's direct child, unlike the nested per-message trees.
1433
+ !important beats the open row's inline display:inline-flex */
1434
+ & > .w-json-view-container > :is(span:first-child, div:not(.w-rjv-wrap)) { display: none !important; }
1435
+
1436
+ & .requests-msgs,
1437
+ & .requests-msgs .items {
1438
+ margin-left: 6px;
1439
+ padding-left: 15px;
1440
+ border-left: 1px solid var(--w-rjv-line-color);
1441
+ }
1442
+
1443
+ & .w-rjv-value {
1444
+ color: var(--w-rjv-type-string-color)
1445
+ }
1446
+ }
1447
+
1448
+ /* summary shown inside a collapsed node in place of the '...' ellipsis */
1449
+ .requests-hint {
1450
+ cursor: pointer;
1451
+ user-select: none;
1452
+ font-style: italic;
1453
+ color: var(--text-faint);
1454
+ padding: 0 3px;
1455
+ white-space: pre; /* keeps the role's alignment padding */
1456
+ }
1457
+
1458
+ /* parts already sent in an earlier request ($refs in the log) fade so what's new stands out */
1459
+ .requests-seen,
1460
+ .requests-pane :is(.w-rjv-inner, .w-rjv-line):has(> span .requests-seen-key) {
1461
+ opacity: 0.6;
1462
+ }
1463
+
1464
+ .requests-headers {
1465
+ padding-bottom: 8px;
1466
+ margin-bottom: 8px;
1467
+ border-bottom: 1px solid var(--border);
1468
+ }
1469
+
1470
+ .requests-label {
1471
+ display: flex;
1472
+ justify-content: space-between;
1473
+ color: var(--text-faint);
1474
+ text-transform: uppercase;
1475
+ font-size: calc(11px * var(--font-scale, 1));
1476
+ letter-spacing: 0.5px;
1477
+ padding: 2px 0 6px;
1478
+ }
1479
+
1480
+ /* explains the seen-part fading to first-time viewers */
1481
+ .requests-label-hint {
1482
+ text-transform: none;
1483
+ letter-spacing: normal;
1484
+ font-style: italic;
1485
+ }
1486
+
1487
+ .requests-pane-empty,
1488
+ .requests-empty {
1489
+ color: var(--text-faint);
1490
+ }
1491
+
1492
+ .requests-empty {
1493
+ padding: 10px 20px;
1494
+ }
1495
+
1496
+ /* Empty state: sells the Capture proxy instead of just saying nothing was recorded. */
1497
+ .requests-pitch {
1498
+ margin: 60px auto auto;
1499
+ max-width: 480px;
1500
+ text-align: center;
1501
+ padding: 24px 28px;
1502
+ border: 1px solid rgba(167, 139, 250, 0.35);
1503
+ border-radius: 8px;
1504
+ background: rgba(167, 139, 250, 0.06);
1505
+ color: var(--text-dim);
1506
+ line-height: 1.6;
1507
+
1508
+ & h3 { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 0 0 8px; color: var(--text); font-size: 17px; }
1509
+ & p { margin: 0 0 6px; }
1510
+ & b { color: #a78bfa; }
1511
+ }
1512
+ .requests-pitch-icon { width: 22px; height: 22px; color: #a78bfa; }
1513
+ .requests-pitch-hint { color: var(--text); }
1474
1514
  .agent-prompt-typing {
1475
1515
  display: inline-flex;
1476
1516
  align-items: center;
@@ -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-By2RDJFe.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-B6LVyX-b.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="./assets/vendors-CpMM-Zb0.js">
11
11
  <link rel="stylesheet" crossorigin href="./assets/vendors-B4XMnGdr.css">
12
- <link rel="stylesheet" crossorigin href="./assets/index-CTIYCpXd.css">
12
+ <link rel="stylesheet" crossorigin href="./assets/index-DsnXk_cj.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/package.json CHANGED
@@ -1,79 +1,81 @@
1
- {
2
- "name": "claude-discover",
3
- "version": "1.10.3",
4
- "type": "module",
5
- "description": "Claude Discover — a desktop tool to browse your local Claude Code sessions: conversations, token costs, and captured API traffic",
6
- "license": "MIT",
7
- "author": "dichev <dichev.lab@gmail.com>",
8
- "repository": "github:dichev/claude-discover",
9
- "engines": {
10
- "node": ">=22"
11
- },
12
- "main": "out/main/main.js",
13
- "bin": {
14
- "claude-discover": "bin/claude-discover.mjs"
15
- },
16
- "files": [
17
- "out",
18
- "bin"
19
- ],
20
- "scripts": {
21
- "dev": "electron-vite dev",
22
- "build": "electron-vite build",
23
- "start": "electron .",
24
- "test": "vitest run --reporter=tree",
25
- "proxy": "node bin/proxy.mjs --restart",
26
- "readme": "gh markdown-preview",
27
- "smoke:local": "npm pack && npx -y ./claude-discover-${npm_package_version}.tgz",
28
- "smoke:global": "npm pack && npm i -g ./claude-discover-${npm_package_version}.tgz && claude-discover",
29
- "prepack": "npm run build",
30
- "prepublishOnly": "npm whoami && npm test"
31
- },
32
- "dependencies": {
33
- "@anthropic-ai/claude-agent-sdk": "^0.3.284",
34
- "electron": "44.4.5",
35
- "electron-context-menu": "^5.1.0",
36
- "electron-window-state": "^5.0.3",
37
- "latest-version": "^9.0.0",
38
- "semver": "^7.8.5",
39
- "which": "^7.0.0",
40
- "write-file-atomic": "^8.0.0"
41
- },
42
- "devDependencies": {
43
- "@date-fns/tz": "^1.5.0",
44
- "@fontsource/inter": "^5.3.0",
45
- "@fontsource/jetbrains-mono": "^5.3.0",
46
- "@tomjs/electron-devtools-installer": "^4.0.1",
47
- "@uiw/react-json-view": "^2.0.0-alpha.43",
48
- "@vitejs/plugin-react": "^5.2.0",
49
- "date-fns": "^4.4.0",
50
- "electron-vite": "^5.0.0",
51
- "gpt-tokenizer": "^4.0.0",
52
- "highlight.js": "^11.12.0",
53
- "lucide-react": "^1.43.0",
54
- "react": "^19.3.0",
55
- "react-dom": "^19.3.0",
56
- "react-error-boundary": "^6.1.6",
57
- "react-markdown": "^10.1.0",
58
- "react-resizable-panels": "^4.12.4",
59
- "rehype-highlight": "^7.0.2",
60
- "remark-gfm": "^4.0.1",
61
- "tippy.js": "^6.3.7",
62
- "vite": "^7.3.6",
63
- "vitest": "^5.0.2"
64
- },
65
- "keywords": [
66
- "claude",
67
- "claude-code",
68
- "claude-code-proxy",
69
- "claude-code-inspect",
70
- "system-prompt",
71
- "session-browser",
72
- "session-profiler",
73
- "transcript-viewer",
74
- "token-usage",
75
- "token-cost",
76
- "usage-dashboard",
77
- "api-inspector"
78
- ]
79
- }
1
+ {
2
+ "name": "claude-discover",
3
+ "version": "1.10.4",
4
+ "type": "module",
5
+ "description": "Claude Discover — a desktop tool to browse your local Claude Code sessions: conversations, token costs, and captured API traffic",
6
+ "license": "MIT",
7
+ "author": "dichev <dichev.lab@gmail.com>",
8
+ "repository": "github:dichev/claude-discover",
9
+ "engines": {
10
+ "node": ">=22"
11
+ },
12
+ "main": "out/main/main.js",
13
+ "bin": {
14
+ "claude-discover": "bin/claude-discover.mjs"
15
+ },
16
+ "files": [
17
+ "out",
18
+ "bin"
19
+ ],
20
+ "scripts": {
21
+ "predev": "kill-port 5555 9333",
22
+ "dev": "electron-vite dev",
23
+ "build": "electron-vite build",
24
+ "start": "electron .",
25
+ "test": "vitest run --reporter=tree",
26
+ "proxy": "node bin/proxy.mjs --restart",
27
+ "readme": "gh markdown-preview",
28
+ "smoke:local": "npm pack && npx -y ./claude-discover-${npm_package_version}.tgz",
29
+ "smoke:global": "npm pack && npm i -g ./claude-discover-${npm_package_version}.tgz && claude-discover",
30
+ "prepack": "npm run build",
31
+ "prepublishOnly": "npm whoami && npm test"
32
+ },
33
+ "dependencies": {
34
+ "@anthropic-ai/claude-agent-sdk": "^0.3.284",
35
+ "electron": "44.4.5",
36
+ "electron-context-menu": "^5.1.0",
37
+ "electron-window-state": "^5.0.3",
38
+ "latest-version": "^9.0.0",
39
+ "semver": "^7.8.5",
40
+ "which": "^7.0.0",
41
+ "write-file-atomic": "^8.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@date-fns/tz": "^1.5.0",
45
+ "@fontsource/inter": "^5.3.0",
46
+ "@fontsource/jetbrains-mono": "^5.3.0",
47
+ "@tomjs/electron-devtools-installer": "^4.0.1",
48
+ "@uiw/react-json-view": "^2.0.0-alpha.43",
49
+ "@vitejs/plugin-react": "^5.2.0",
50
+ "date-fns": "^4.4.0",
51
+ "electron-vite": "^5.0.0",
52
+ "gpt-tokenizer": "^4.0.0",
53
+ "highlight.js": "^11.12.0",
54
+ "kill-port": "^2.0.1",
55
+ "lucide-react": "^1.43.0",
56
+ "react": "^19.3.0",
57
+ "react-dom": "^19.3.0",
58
+ "react-error-boundary": "^6.1.6",
59
+ "react-markdown": "^10.1.0",
60
+ "react-resizable-panels": "^4.12.4",
61
+ "rehype-highlight": "^7.0.2",
62
+ "remark-gfm": "^4.0.1",
63
+ "tippy.js": "^6.3.7",
64
+ "vite": "^7.3.6",
65
+ "vitest": "^5.0.2"
66
+ },
67
+ "keywords": [
68
+ "claude",
69
+ "claude-code",
70
+ "claude-code-proxy",
71
+ "claude-code-inspect",
72
+ "system-prompt",
73
+ "session-browser",
74
+ "session-profiler",
75
+ "transcript-viewer",
76
+ "token-usage",
77
+ "token-cost",
78
+ "usage-dashboard",
79
+ "api-inspector"
80
+ ]
81
+ }