claude-discover 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/proxy.mjs +14 -1
- package/out/main/main.js +371 -65
- package/out/preload/preload.cjs +3 -1
- package/out/renderer/assets/{index-0bBLuYQv.css → index-03LlBvFr.css} +27 -1
- package/out/renderer/assets/{index-BLwpO6Wp.js → index-BugPzqD2.js} +161 -37
- package/out/renderer/assets/vendors-S1fS5xz9.js +52139 -0
- package/out/renderer/index.html +3 -3
- package/package.json +5 -3
- package/out/renderer/assets/vendors-D6iOvAG3.js +0 -49248
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Ever wondered what Claude Code is **actually** doing all day?
|
|
5
5
|
|
|
6
|
-
<img width="1660"
|
|
6
|
+
<img width="1660" alt="claude-discover-dimmed-screenshot" src="https://github.com/user-attachments/assets/708d05c7-8769-47d8-b5bd-14337d1d6037" />
|
|
7
7
|
<br><br>
|
|
8
8
|
|
|
9
9
|
**Claude Discover** is a desktop tool that shows you what **Claude Code** is actually doing. It collects all your sessions (from the CLI, Desktop, or SDK) and puts them on one timeline, where you can read every conversation, peek at the system prompts, memories, and tools, inspect the raw API traffic behind it all, and see exactly where your tokens - and your money - went. (Spoiler: mostly on re-sending tool definitions.)
|
package/bin/proxy.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'node:fs'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import http from 'node:http'
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
12
13
|
import https from 'node:https'
|
|
13
14
|
import zlib from 'node:zlib'
|
|
14
15
|
import crypto from 'node:crypto'
|
|
@@ -227,7 +228,19 @@ function logError(err, context) {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
230
|
-
if (
|
|
231
|
+
if (process.parentPort) {
|
|
232
|
+
// Forked as an Electron utility process (ProxySwitch): Chromium gives those a clean handle
|
|
233
|
+
// table, unlike the app's main process whose children inherit the dev CDP server's socket on
|
|
234
|
+
// Windows and keep its port bound after the app quits. Utility processes die with the app,
|
|
235
|
+
// so re-spawn detached from here (as node — no system `node` required) and relay an early
|
|
236
|
+
// exit code back as a message until the app kills this relay.
|
|
237
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
238
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
239
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
|
240
|
+
})
|
|
241
|
+
child.once('exit', code => { process.parentPort.postMessage(code); process.exit(0) })
|
|
242
|
+
}
|
|
243
|
+
else if (isMain) {
|
|
231
244
|
const args = parseArgs({
|
|
232
245
|
options: {
|
|
233
246
|
restart: { type: 'boolean' }, // replace an already-running instance instead of exiting
|
package/out/main/main.js
CHANGED
|
@@ -1,21 +1,248 @@
|
|
|
1
|
-
import { BrowserWindow, dialog, app, Menu, shell, WebContentsView, ipcMain } from "electron";
|
|
1
|
+
import { BrowserWindow, dialog, app, Menu, shell, WebContentsView, utilityProcess, ipcMain } from "electron";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { parseISO, endOfWeek, startOfWeek, endOfMonth, startOfMonth, endOfDay, startOfDay } from "date-fns";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import fsp from "node:fs/promises";
|
|
6
5
|
import path, { join, dirname, basename } from "node:path";
|
|
7
6
|
import os, { homedir } from "node:os";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
8
|
import chokidar from "chokidar";
|
|
9
9
|
import { sync } from "write-file-atomic";
|
|
10
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
10
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
12
11
|
import windowStateKeeper from "electron-window-state";
|
|
13
12
|
import contextMenu from "electron-context-menu";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const millisecondsInMinute = 6e4;
|
|
14
|
+
const millisecondsInHour = 36e5;
|
|
15
|
+
const constructFromSymbol = /* @__PURE__ */ Symbol.for("constructDateFrom");
|
|
16
|
+
function constructFrom(date, value) {
|
|
17
|
+
if (typeof date === "function") return date(value);
|
|
18
|
+
if (date && typeof date === "object" && constructFromSymbol in date)
|
|
19
|
+
return date[constructFromSymbol](value);
|
|
20
|
+
if (date instanceof Date) return new date.constructor(value);
|
|
21
|
+
return new Date(value);
|
|
22
|
+
}
|
|
23
|
+
function toDate(argument, context) {
|
|
24
|
+
return constructFrom(context || argument, argument);
|
|
25
|
+
}
|
|
26
|
+
let defaultOptions = {};
|
|
27
|
+
function getDefaultOptions() {
|
|
28
|
+
return defaultOptions;
|
|
29
|
+
}
|
|
30
|
+
function startOfWeek(date, options) {
|
|
31
|
+
const defaultOptions2 = getDefaultOptions();
|
|
32
|
+
const weekStartsOn = options?.weekStartsOn ?? options?.locale?.options?.weekStartsOn ?? defaultOptions2.weekStartsOn ?? defaultOptions2.locale?.options?.weekStartsOn ?? 0;
|
|
33
|
+
const _date = toDate(date, options?.in);
|
|
34
|
+
const day = _date.getDay();
|
|
35
|
+
const diff = (day < weekStartsOn ? 7 : 0) + day - weekStartsOn;
|
|
36
|
+
_date.setDate(_date.getDate() - diff);
|
|
37
|
+
_date.setHours(0, 0, 0, 0);
|
|
38
|
+
return _date;
|
|
39
|
+
}
|
|
40
|
+
function startOfDay(date, options) {
|
|
41
|
+
const _date = toDate(date, options?.in);
|
|
42
|
+
_date.setHours(0, 0, 0, 0);
|
|
43
|
+
return _date;
|
|
44
|
+
}
|
|
45
|
+
function endOfDay(date, options) {
|
|
46
|
+
const _date = toDate(date, options?.in);
|
|
47
|
+
_date.setHours(23, 59, 59, 999);
|
|
48
|
+
return _date;
|
|
49
|
+
}
|
|
50
|
+
function endOfMonth(date, options) {
|
|
51
|
+
const _date = toDate(date, options?.in);
|
|
52
|
+
const month = _date.getMonth();
|
|
53
|
+
_date.setFullYear(_date.getFullYear(), month + 1, 0);
|
|
54
|
+
_date.setHours(23, 59, 59, 999);
|
|
55
|
+
return _date;
|
|
56
|
+
}
|
|
57
|
+
function startOfMonth(date, options) {
|
|
58
|
+
const _date = toDate(date, options?.in);
|
|
59
|
+
_date.setDate(1);
|
|
60
|
+
_date.setHours(0, 0, 0, 0);
|
|
61
|
+
return _date;
|
|
62
|
+
}
|
|
63
|
+
function endOfWeek(date, options) {
|
|
64
|
+
const defaultOptions2 = getDefaultOptions();
|
|
65
|
+
const weekStartsOn = options?.weekStartsOn ?? options?.locale?.options?.weekStartsOn ?? defaultOptions2.weekStartsOn ?? defaultOptions2.locale?.options?.weekStartsOn ?? 0;
|
|
66
|
+
const _date = toDate(date, options?.in);
|
|
67
|
+
const day = _date.getDay();
|
|
68
|
+
const diff = (day < weekStartsOn ? -7 : 0) + 6 - (day - weekStartsOn);
|
|
69
|
+
_date.setDate(_date.getDate() + diff);
|
|
70
|
+
_date.setHours(23, 59, 59, 999);
|
|
71
|
+
return _date;
|
|
72
|
+
}
|
|
73
|
+
function parseISO(argument, options) {
|
|
74
|
+
const invalidDate = () => constructFrom(options?.in, NaN);
|
|
75
|
+
const additionalDigits = options?.additionalDigits ?? 2;
|
|
76
|
+
const dateStrings = splitDateString(argument);
|
|
77
|
+
let date;
|
|
78
|
+
if (dateStrings.date) {
|
|
79
|
+
const parseYearResult = parseYear(dateStrings.date, additionalDigits);
|
|
80
|
+
date = parseDate(parseYearResult.restDateString, parseYearResult.year);
|
|
81
|
+
}
|
|
82
|
+
if (!date || isNaN(+date)) return invalidDate();
|
|
83
|
+
const timestamp = +date;
|
|
84
|
+
let time = 0;
|
|
85
|
+
let offset;
|
|
86
|
+
if (dateStrings.time) {
|
|
87
|
+
time = parseTime(dateStrings.time);
|
|
88
|
+
if (isNaN(time)) return invalidDate();
|
|
89
|
+
}
|
|
90
|
+
if (dateStrings.timezone) {
|
|
91
|
+
offset = parseTimezone(dateStrings.timezone);
|
|
92
|
+
if (isNaN(offset)) return invalidDate();
|
|
93
|
+
} else {
|
|
94
|
+
const tmpDate = new Date(timestamp + time);
|
|
95
|
+
const result = toDate(0, options?.in);
|
|
96
|
+
result.setFullYear(
|
|
97
|
+
tmpDate.getUTCFullYear(),
|
|
98
|
+
tmpDate.getUTCMonth(),
|
|
99
|
+
tmpDate.getUTCDate()
|
|
100
|
+
);
|
|
101
|
+
result.setHours(
|
|
102
|
+
tmpDate.getUTCHours(),
|
|
103
|
+
tmpDate.getUTCMinutes(),
|
|
104
|
+
tmpDate.getUTCSeconds(),
|
|
105
|
+
tmpDate.getUTCMilliseconds()
|
|
106
|
+
);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
return toDate(timestamp + time + offset, options?.in);
|
|
110
|
+
}
|
|
111
|
+
const patterns = {
|
|
112
|
+
dateTimeDelimiter: /[T ]/,
|
|
113
|
+
timeZoneDelimiter: /[Z ]/i,
|
|
114
|
+
timezone: /([Z+-].*)$/
|
|
115
|
+
};
|
|
116
|
+
const dateRegex = /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/;
|
|
117
|
+
const timeRegex = /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/;
|
|
118
|
+
const timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/;
|
|
119
|
+
function splitDateString(dateString) {
|
|
120
|
+
const dateStrings = {};
|
|
121
|
+
const array = dateString.split(patterns.dateTimeDelimiter);
|
|
122
|
+
let timeString;
|
|
123
|
+
if (array.length > 2) {
|
|
124
|
+
return dateStrings;
|
|
125
|
+
}
|
|
126
|
+
if (/:/.test(array[0])) {
|
|
127
|
+
timeString = array[0];
|
|
128
|
+
} else {
|
|
129
|
+
dateStrings.date = array[0];
|
|
130
|
+
timeString = array[1];
|
|
131
|
+
if (patterns.timeZoneDelimiter.test(dateStrings.date)) {
|
|
132
|
+
dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0];
|
|
133
|
+
timeString = dateString.substr(
|
|
134
|
+
dateStrings.date.length,
|
|
135
|
+
dateString.length
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (timeString) {
|
|
140
|
+
const token = patterns.timezone.exec(timeString);
|
|
141
|
+
if (token) {
|
|
142
|
+
dateStrings.time = timeString.replace(token[1], "");
|
|
143
|
+
dateStrings.timezone = token[1];
|
|
144
|
+
} else {
|
|
145
|
+
dateStrings.time = timeString;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return dateStrings;
|
|
149
|
+
}
|
|
150
|
+
function parseYear(dateString, additionalDigits) {
|
|
151
|
+
const regex = new RegExp(
|
|
152
|
+
"^(?:(\\d{4}|[+-]\\d{" + (4 + additionalDigits) + "})|(\\d{2}|[+-]\\d{" + (2 + additionalDigits) + "})$)"
|
|
153
|
+
);
|
|
154
|
+
const captures = dateString.match(regex);
|
|
155
|
+
if (!captures) return { year: NaN, restDateString: "" };
|
|
156
|
+
const year = captures[1] ? parseInt(captures[1]) : null;
|
|
157
|
+
const century = captures[2] ? parseInt(captures[2]) : null;
|
|
158
|
+
return {
|
|
159
|
+
year: century === null ? year : century * 100,
|
|
160
|
+
restDateString: dateString.slice((captures[1] || captures[2]).length)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function parseDate(dateString, year) {
|
|
164
|
+
if (year === null) return /* @__PURE__ */ new Date(NaN);
|
|
165
|
+
const captures = dateString.match(dateRegex);
|
|
166
|
+
if (!captures) return /* @__PURE__ */ new Date(NaN);
|
|
167
|
+
const isWeekDate = !!captures[4];
|
|
168
|
+
const dayOfYear = parseDateUnit(captures[1]);
|
|
169
|
+
const month = parseDateUnit(captures[2]) - 1;
|
|
170
|
+
const day = parseDateUnit(captures[3]);
|
|
171
|
+
const week = parseDateUnit(captures[4]);
|
|
172
|
+
const dayOfWeek = parseDateUnit(captures[5]) - 1;
|
|
173
|
+
if (isWeekDate) {
|
|
174
|
+
if (!validateWeekDate(year, week, dayOfWeek)) {
|
|
175
|
+
return /* @__PURE__ */ new Date(NaN);
|
|
176
|
+
}
|
|
177
|
+
return dayOfISOWeekYear(year, week, dayOfWeek);
|
|
178
|
+
} else {
|
|
179
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
180
|
+
if (!validateDate(year, month, day) || !validateDayOfYearDate(year, dayOfYear)) {
|
|
181
|
+
return /* @__PURE__ */ new Date(NaN);
|
|
182
|
+
}
|
|
183
|
+
date.setUTCFullYear(year, month, Math.max(dayOfYear, day));
|
|
184
|
+
return date;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function parseDateUnit(value) {
|
|
188
|
+
return value ? parseInt(value) : 1;
|
|
189
|
+
}
|
|
190
|
+
function parseTime(timeString) {
|
|
191
|
+
const captures = timeString.match(timeRegex);
|
|
192
|
+
if (!captures) return NaN;
|
|
193
|
+
const hours = parseTimeUnit(captures[1]);
|
|
194
|
+
const minutes = parseTimeUnit(captures[2]);
|
|
195
|
+
const seconds = parseTimeUnit(captures[3]);
|
|
196
|
+
if (!validateTime(hours, minutes, seconds)) {
|
|
197
|
+
return NaN;
|
|
198
|
+
}
|
|
199
|
+
return hours * millisecondsInHour + minutes * millisecondsInMinute + seconds * 1e3;
|
|
200
|
+
}
|
|
201
|
+
function parseTimeUnit(value) {
|
|
202
|
+
return value && parseFloat(value.replace(",", ".")) || 0;
|
|
203
|
+
}
|
|
204
|
+
function parseTimezone(timezoneString) {
|
|
205
|
+
if (timezoneString === "Z") return 0;
|
|
206
|
+
const captures = timezoneString.match(timezoneRegex);
|
|
207
|
+
if (!captures) return 0;
|
|
208
|
+
const sign = captures[1] === "+" ? -1 : 1;
|
|
209
|
+
const hours = parseInt(captures[2]);
|
|
210
|
+
const minutes = captures[3] && parseInt(captures[3]) || 0;
|
|
211
|
+
if (!validateTimezone(hours, minutes)) {
|
|
212
|
+
return NaN;
|
|
213
|
+
}
|
|
214
|
+
return sign * (hours * millisecondsInHour + minutes * millisecondsInMinute);
|
|
215
|
+
}
|
|
216
|
+
function dayOfISOWeekYear(isoWeekYear, week, day) {
|
|
217
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
218
|
+
date.setUTCFullYear(isoWeekYear, 0, 4);
|
|
219
|
+
const fourthOfJanuaryDay = date.getUTCDay() || 7;
|
|
220
|
+
const diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay;
|
|
221
|
+
date.setUTCDate(date.getUTCDate() + diff);
|
|
222
|
+
return date;
|
|
223
|
+
}
|
|
224
|
+
const daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
225
|
+
function isLeapYearIndex(year) {
|
|
226
|
+
return year % 400 === 0 || year % 4 === 0 && year % 100 !== 0;
|
|
227
|
+
}
|
|
228
|
+
function validateDate(year, month, date) {
|
|
229
|
+
return month >= 0 && month <= 11 && date >= 1 && date <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28));
|
|
230
|
+
}
|
|
231
|
+
function validateDayOfYearDate(year, dayOfYear) {
|
|
232
|
+
return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365);
|
|
233
|
+
}
|
|
234
|
+
function validateWeekDate(_year, week, day) {
|
|
235
|
+
return week >= 1 && week <= 53 && day >= 0 && day <= 6;
|
|
236
|
+
}
|
|
237
|
+
function validateTime(hours, minutes, seconds) {
|
|
238
|
+
if (hours === 24) {
|
|
239
|
+
return minutes === 0 && seconds === 0;
|
|
240
|
+
}
|
|
241
|
+
return seconds >= 0 && seconds < 60 && minutes >= 0 && minutes < 60 && hours >= 0 && hours < 25;
|
|
242
|
+
}
|
|
243
|
+
function validateTimezone(_hours, minutes) {
|
|
244
|
+
return minutes >= 0 && minutes <= 59;
|
|
245
|
+
}
|
|
19
246
|
class SessionFile {
|
|
20
247
|
constructor(filePath) {
|
|
21
248
|
this.filePath = filePath;
|
|
@@ -70,6 +297,7 @@ const DATA_DIR$1 = process.env.CLAUDE_DISCOVER_DIR || path.join(os.homedir(), ".
|
|
|
70
297
|
const REQUESTS_DIR = path.join(DATA_DIR$1, "requests");
|
|
71
298
|
const ERROR_LOG_PATH = path.join(DATA_DIR$1, "proxy.error.log");
|
|
72
299
|
const jsonBytes = (x) => x == null ? 0 : Buffer.byteLength(JSON.stringify(x));
|
|
300
|
+
const contentHash = (s) => crypto.createHash("sha256").update(s).digest("base64url").slice(0, 16);
|
|
73
301
|
const MEMORY_LABELS = {
|
|
74
302
|
"user's private global instructions for all projects": "User",
|
|
75
303
|
"project instructions, checked into the codebase": "Project",
|
|
@@ -79,9 +307,9 @@ const MEMORY_LABELS = {
|
|
|
79
307
|
const MEMORY_FILES = { User: "CLAUDE.md", Project: "CLAUDE.md", Local: "CLAUDE.local.md", Auto: "MEMORY.md" };
|
|
80
308
|
function parseClaudeMd(text) {
|
|
81
309
|
const marker = "\n# claudeMd\n";
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
let region = text.slice(
|
|
310
|
+
const start2 = text.indexOf(marker);
|
|
311
|
+
if (start2 === -1) return [];
|
|
312
|
+
let region = text.slice(start2 + marker.length);
|
|
85
313
|
const ends = [/\n# [a-z][a-zA-Z0-9]*\r?\n/, /\n\s*IMPORTANT: this context/, /<\/system-reminder>/].map((re) => region.search(re)).filter((i) => i !== -1);
|
|
86
314
|
if (ends.length) region = region.slice(0, Math.min(...ends));
|
|
87
315
|
const heads = [...region.matchAll(/^Contents of (.+?) \(([^)]*)\):\r?\n/gm)];
|
|
@@ -108,7 +336,7 @@ const classifyRequest = (req, url) => {
|
|
|
108
336
|
const user = (typeof content === "string" ? content : blocks?.find((b) => b?.type === "text")?.text ?? "").trimStart().slice(0, 300);
|
|
109
337
|
const tail = (blocks?.findLast((b) => b?.type === "text")?.text ?? user).slice(0, 600);
|
|
110
338
|
if (req.max_tokens === 1 || user.trim() === "quota") return ["quota", "Quota probe"];
|
|
111
|
-
if (sysText.includes("You are a security monitor") || user.startsWith("<transcript>")) return ["security", "
|
|
339
|
+
if (sysText.includes("You are a security monitor") || user.startsWith("<transcript>")) return ["security", "Auto-mode check"];
|
|
112
340
|
if (sysText.includes("Generate a concise, sentence-case title")) return ["title", "Session title"];
|
|
113
341
|
if (sysText.includes("performing a web search")) return ["web", "Web search"];
|
|
114
342
|
if (user.startsWith("[SUGGESTION MODE")) return ["suggest", "Suggestions"];
|
|
@@ -165,40 +393,38 @@ class RequestParser {
|
|
|
165
393
|
rec.resSize = jsonBytes(rec.response);
|
|
166
394
|
return rec;
|
|
167
395
|
}
|
|
168
|
-
// The request's system prompt,
|
|
169
|
-
//
|
|
396
|
+
// The request's system prompt, from a resolveRefs'd record — a plain string or an array of
|
|
397
|
+
// text blocks. The content hash is the dedup key: repeats collapse in the caller's map.
|
|
170
398
|
systemPrompt(rec) {
|
|
171
399
|
const sys = rec.request?.system;
|
|
172
|
-
if (
|
|
173
|
-
const
|
|
174
|
-
const content = typeof value === "string" ? value : value.map((b) => b?.text ?? "").join("\n\n");
|
|
400
|
+
if (sys == null || sys.$ref) return null;
|
|
401
|
+
const content = typeof sys === "string" ? sys : sys.map((b) => b?.text ?? "").join("\n\n");
|
|
175
402
|
const [kind, kindLabel] = stripKind(rec.request);
|
|
176
|
-
return { file_path: "System Prompt", memory_type: kindLabel || "", model: rec.request.model, content, hash:
|
|
403
|
+
return { file_path: "System Prompt", memory_type: kindLabel || "", model: rec.request.model, content, hash: contentHash(content), kind };
|
|
177
404
|
}
|
|
178
|
-
// The request's tool definitions,
|
|
179
|
-
//
|
|
180
|
-
//
|
|
405
|
+
// The request's tool definitions, from a resolveRefs'd record. The array grows as deferred
|
|
406
|
+
// tools load mid-session, so only tools this parser hasn't seen yet are returned — the first
|
|
407
|
+
// request yields the full set, later arrays just the additions.
|
|
181
408
|
systemTools(rec) {
|
|
182
409
|
const tools = rec.request?.tools;
|
|
183
|
-
if (!tools
|
|
184
|
-
const fresh =
|
|
410
|
+
if (!Array.isArray(tools)) return null;
|
|
411
|
+
const fresh = tools.filter((t) => t?.name && !this.seenTools.has(t.name));
|
|
185
412
|
if (!fresh.length) return null;
|
|
186
413
|
for (const t of fresh) this.seenTools.add(t.name);
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
${t.description || ""}`.trim()).join("\n\n");
|
|
414
|
+
const schema = (t) => t.input_schema ? "```json\n" + JSON.stringify(t.input_schema, null, 2) + "\n```" : "";
|
|
415
|
+
const content = fresh.map((t) => [`## ${t.name}`, t.description || "", schema(t)].filter(Boolean).join("\n\n")).join("\n\n");
|
|
190
416
|
const [kind, kindLabel] = stripKind(rec.request);
|
|
191
417
|
const label = [kindLabel, `${fresh.length} tool${fresh.length === 1 ? "" : "s"}`].filter(Boolean).join(", ");
|
|
192
|
-
return { file_path: "System Tools", memory_type: label, model: rec.request.model, content, hash:
|
|
418
|
+
return { file_path: "System Tools", memory_type: label, model: rec.request.model, content, hash: contentHash(content), kind };
|
|
193
419
|
}
|
|
194
|
-
// Memory files (CLAUDE.md / MEMORY.md / …) carried by
|
|
195
|
-
// system-reminders — one { file_path, memory_type, content } per file listed.
|
|
420
|
+
// Memory files (CLAUDE.md / MEMORY.md / …) carried by a resolveRefs'd record's messages inside
|
|
421
|
+
// `# claudeMd` system-reminders — one { file_path, memory_type, content } per file listed.
|
|
196
422
|
memoryFiles(rec) {
|
|
197
423
|
const files = [];
|
|
198
424
|
for (const m of rec.request?.messages || []) {
|
|
199
|
-
const content =
|
|
425
|
+
const content = m?.content;
|
|
200
426
|
for (const part of Array.isArray(content) ? content : [{ text: content }]) {
|
|
201
|
-
const text =
|
|
427
|
+
const text = part?.text;
|
|
202
428
|
if (typeof text !== "string" || !text.includes("<system-reminder>")) continue;
|
|
203
429
|
files.push(...parseClaudeMd(text));
|
|
204
430
|
}
|
|
@@ -206,17 +432,22 @@ ${t.description || ""}`.trim()).join("\n\n");
|
|
|
206
432
|
return files;
|
|
207
433
|
}
|
|
208
434
|
}
|
|
435
|
+
const agentKey = (id) => id?.includes("@") ? id.slice(0, id.indexOf("@")) : id?.replace(/^a/, "").replace(/-[0-9a-f]{16}$/, "") ?? null;
|
|
209
436
|
class RequestFile {
|
|
210
|
-
constructor(sessionId, dir = REQUESTS_DIR) {
|
|
437
|
+
constructor(sessionId, { dir = REQUESTS_DIR, agentId = null } = {}) {
|
|
211
438
|
this.filePath = join(dir, `${sessionId}.requests.jsonl`);
|
|
439
|
+
this.agentKey = agentKey(agentId);
|
|
440
|
+
}
|
|
441
|
+
#mine(rec) {
|
|
442
|
+
return agentKey(rec.requestHeaders?.["x-claude-code-agent-id"] ?? null) === this.agentKey;
|
|
212
443
|
}
|
|
213
444
|
async lines() {
|
|
214
445
|
const raw = await fsp.readFile(this.filePath, "utf8").catch(() => "");
|
|
215
446
|
return raw.split("\n").filter(Boolean);
|
|
216
447
|
}
|
|
217
|
-
// `range` is { start, end } epoch ms. Refs are resolved over the whole file before the
|
|
218
|
-
//
|
|
219
|
-
// so callers see every request exactly as it was sent to the API.
|
|
448
|
+
// `range` is { start, end } epoch ms. Refs are resolved over the whole file before the range and
|
|
449
|
+
// agent filters — an out-of-range (or another agent's) record may hold the $hash an in-range
|
|
450
|
+
// record refs — so callers see every request exactly as it was sent to the API.
|
|
220
451
|
async read(range = null) {
|
|
221
452
|
const parser = new RequestParser();
|
|
222
453
|
const records = [];
|
|
@@ -229,8 +460,9 @@ class RequestFile {
|
|
|
229
460
|
}
|
|
230
461
|
records.push(parser.resolveRefs(rec));
|
|
231
462
|
}
|
|
232
|
-
if (!range) return records;
|
|
233
463
|
return records.filter((r) => {
|
|
464
|
+
if (!this.#mine(r)) return false;
|
|
465
|
+
if (!range) return true;
|
|
234
466
|
const t = Date.parse(r.timestamp);
|
|
235
467
|
return t >= range.start && t <= range.end;
|
|
236
468
|
});
|
|
@@ -238,25 +470,17 @@ class RequestFile {
|
|
|
238
470
|
// System prompts (request.system), tool definitions (request.tools) and memory files
|
|
239
471
|
// (CLAUDE.md / MEMORY.md / … inside a user message's `# claudeMd` system-reminder) — none of
|
|
240
472
|
// which Claude Code records in the session transcript, so the request log is the only source.
|
|
473
|
+
// Works on read()'s output, so dedup wrappers and the agent scope are already handled.
|
|
241
474
|
// Returns one record per unique system prompt / tool batch / file_path; readSession ships them
|
|
242
475
|
// alongside the transcript items.
|
|
243
476
|
async readInstructions() {
|
|
244
477
|
const parser = new RequestParser();
|
|
245
478
|
const files = /* @__PURE__ */ new Map();
|
|
246
|
-
for (const
|
|
247
|
-
const hasSystem = line.includes('"system":{"$hash"');
|
|
248
|
-
const hasTools = line.includes('"tools":{"$hash"');
|
|
249
|
-
if (!hasSystem && !hasTools && !line.includes("# claudeMd")) continue;
|
|
250
|
-
let rec;
|
|
251
|
-
try {
|
|
252
|
-
rec = JSON.parse(line);
|
|
253
|
-
} catch {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
479
|
+
for (const rec of await this.read()) {
|
|
256
480
|
const record = (f) => ({ timestamp: rec.timestamp, ...f });
|
|
257
|
-
const sys =
|
|
481
|
+
const sys = parser.systemPrompt(rec);
|
|
258
482
|
if (sys && !files.has(sys.hash)) files.set(sys.hash, record(sys));
|
|
259
|
-
const tools =
|
|
483
|
+
const tools = parser.systemTools(rec);
|
|
260
484
|
if (tools) files.set(tools.hash, record(tools));
|
|
261
485
|
for (const f of parser.memoryFiles(rec)) {
|
|
262
486
|
if (!files.has(f.file_path)) files.set(f.file_path, record(f));
|
|
@@ -373,7 +597,7 @@ class SessionParser {
|
|
|
373
597
|
}
|
|
374
598
|
if (t === "attachment") {
|
|
375
599
|
obj.isMeta = true;
|
|
376
|
-
this.prevMessage = obj;
|
|
600
|
+
if (obj.attachment?.type !== "queued_command") this.prevMessage = obj;
|
|
377
601
|
return true;
|
|
378
602
|
}
|
|
379
603
|
if (t !== "user" && t !== "assistant") return true;
|
|
@@ -582,6 +806,8 @@ class SessionsScanner {
|
|
|
582
806
|
constructor({ root = CLAUDE_PROJECTS_DIR } = {}) {
|
|
583
807
|
this.root = root;
|
|
584
808
|
this.watcher = null;
|
|
809
|
+
this.watchWanted = false;
|
|
810
|
+
this.walkDone = Promise.withResolvers();
|
|
585
811
|
this.statCache = new StatCache();
|
|
586
812
|
}
|
|
587
813
|
async _listDirs(p) {
|
|
@@ -608,8 +834,17 @@ class SessionsScanner {
|
|
|
608
834
|
// truthy — lets callers flush UI updates incrementally. An aborted `signal` (a superseded
|
|
609
835
|
// scan) stops the walk: no further stats, onFile calls or emits. Once a walk has completed
|
|
610
836
|
// with the watcher live, the StatCache mirrors the disk and serves later scans in memory.
|
|
611
|
-
async scan(day,
|
|
612
|
-
if (this.statCache.complete && this.watcher)
|
|
837
|
+
async scan(day, opts = {}) {
|
|
838
|
+
if (this.statCache.complete && this.watcher) {
|
|
839
|
+
return this.statCache.scan((stat) => this._inPeriod(stat, day), opts);
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
await this._walk(day, opts);
|
|
843
|
+
} finally {
|
|
844
|
+
this.walkDone.resolve();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async _walk(day, { onFile, onBatchDone, onProgress, signal } = {}) {
|
|
613
848
|
const projects = await this._listDirs(this.root);
|
|
614
849
|
let done = 0;
|
|
615
850
|
const progress = () => onProgress?.({ done, total: projects.length, scanning: done < projects.length });
|
|
@@ -631,7 +866,14 @@ class SessionsScanner {
|
|
|
631
866
|
if (onBatchDone) await onBatchDone();
|
|
632
867
|
progress();
|
|
633
868
|
}
|
|
634
|
-
|
|
869
|
+
// Starts chokidar once a scan has finished, and resolves when it's live. Its setup walks the whole
|
|
870
|
+
// projects dir to register watchers, and on a cold disk those stats are the bulk of startup, so it
|
|
871
|
+
// waits instead of racing our walk. The gap is self-healing: `ignoreInitial` skips files present at
|
|
872
|
+
// startup anyway, and a session still being written emits on its next append.
|
|
873
|
+
async watch({ onChange, onUnlink } = {}) {
|
|
874
|
+
this.watchWanted = true;
|
|
875
|
+
await this.walkDone.promise;
|
|
876
|
+
if (!this.watchWanted) return;
|
|
635
877
|
const isUNC = this.root.startsWith("\\\\") || this.root.startsWith("//");
|
|
636
878
|
this.watcher = chokidar.watch(this.root, {
|
|
637
879
|
ignoreInitial: true,
|
|
@@ -655,6 +897,7 @@ class SessionsScanner {
|
|
|
655
897
|
this.watcher.on("unlink", removed);
|
|
656
898
|
}
|
|
657
899
|
stop() {
|
|
900
|
+
this.watchWanted = false;
|
|
658
901
|
if (this.watcher) {
|
|
659
902
|
this.watcher.close();
|
|
660
903
|
this.watcher = null;
|
|
@@ -873,14 +1116,21 @@ class SessionsService extends EventEmitter {
|
|
|
873
1116
|
});
|
|
874
1117
|
items.sort((a, b) => a._ts - b._ts);
|
|
875
1118
|
for (const o of items) delete o._ts;
|
|
876
|
-
const instructions = await
|
|
1119
|
+
const instructions = await this.#requestFile(reader.sessionId, reader.parentSessionId).readInstructions();
|
|
877
1120
|
return { items, instructions };
|
|
878
1121
|
}
|
|
879
1122
|
// Captured API requests for a session (see bin/proxy.mjs), period-filtered like readSession.
|
|
880
1123
|
async readRequests(sessionId, date = null, granularity = "day") {
|
|
881
1124
|
if (!/^[\w-]+$/.test(sessionId)) return [];
|
|
882
1125
|
const range = date ? periodBounds(date, granularity) : null;
|
|
883
|
-
return
|
|
1126
|
+
return this.#requestFile(sessionId, this.metaCache.byId(sessionId)?.parentSessionId).read(range);
|
|
1127
|
+
}
|
|
1128
|
+
// Subagents send the parent's session id, so the proxy logs their requests into the parent's
|
|
1129
|
+
// file, tagged x-claude-code-agent-id — route their reads there. The transcript filename is
|
|
1130
|
+
// `agent-<id>`; RequestFile normalizes that id against the header's own spelling.
|
|
1131
|
+
#requestFile(sessionId, parentSessionId) {
|
|
1132
|
+
const agentId = parentSessionId && sessionId.startsWith("agent-") ? sessionId.slice("agent-".length) : null;
|
|
1133
|
+
return agentId ? new RequestFile(parentSessionId, { agentId }) : new RequestFile(sessionId);
|
|
884
1134
|
}
|
|
885
1135
|
// Scan `period` ({ start, end } epoch ms, optional `key`/`date`; see periodBounds). watch:true (UI) streams 'update' events per dir batch and returns the current cache immediately; watch:false (CLIs/tests) awaits completion and returns the deduped sessions with no emits.
|
|
886
1136
|
async scan(period, { watch = false } = {}) {
|
|
@@ -1027,6 +1277,7 @@ class WorkHours {
|
|
|
1027
1277
|
}
|
|
1028
1278
|
class AgentRunner {
|
|
1029
1279
|
async run(text, sender, systemTools = false, cache = false) {
|
|
1280
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
1030
1281
|
const send = (chunk) => {
|
|
1031
1282
|
if (!sender.isDestroyed()) sender.send("agent:output", chunk);
|
|
1032
1283
|
};
|
|
@@ -1258,6 +1509,12 @@ class MainWindow {
|
|
|
1258
1509
|
send(channel, payload) {
|
|
1259
1510
|
if (this.win && !this.win.isDestroyed()) this.win.webContents.send(channel, payload);
|
|
1260
1511
|
}
|
|
1512
|
+
focus() {
|
|
1513
|
+
if (!this.win || this.win.isDestroyed()) return;
|
|
1514
|
+
if (this.win.isMinimized()) this.win.restore();
|
|
1515
|
+
this.win.show();
|
|
1516
|
+
this.win.focus();
|
|
1517
|
+
}
|
|
1261
1518
|
}
|
|
1262
1519
|
const mtimeOf = (path2) => fs.statSync(path2, { throwIfNoEntry: false })?.mtimeMs;
|
|
1263
1520
|
class ClaudeSettings {
|
|
@@ -1349,15 +1606,9 @@ class ProxySwitch {
|
|
|
1349
1606
|
const baseUrl = settings.env?.ANTHROPIC_BASE_URL;
|
|
1350
1607
|
if (baseUrl && baseUrl !== PROXY_URL)
|
|
1351
1608
|
throw new Error(`Leaving your existing env.ANTHROPIC_BASE_URL in place (${baseUrl}) — remove it from settings.json to enable capture.`);
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
windowsHide: true,
|
|
1356
|
-
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" }
|
|
1357
|
-
});
|
|
1358
|
-
const exited = new Promise((resolve) => child.once("exit", resolve));
|
|
1359
|
-
child.unref();
|
|
1360
|
-
const outcome = await Promise.race([exited, this.#settle(true).then((ok) => ok ? "up" : "unresponsive")]);
|
|
1609
|
+
const launcher = utilityProcess.fork(PROXY_PATH, ["--restart"], { stdio: "ignore" });
|
|
1610
|
+
const exited = new Promise((resolve) => launcher.once("message", resolve));
|
|
1611
|
+
const outcome = await Promise.race([exited, this.#settle(true).then((ok) => ok ? "up" : "unresponsive")]).finally(() => launcher.kill());
|
|
1361
1612
|
if (outcome !== "up")
|
|
1362
1613
|
throw new Error(outcome === 2 ? "Cannot reach api.anthropic.com — not enabling capture. Check your network and try again." : `Proxy did not start — see ${ERROR_LOG_PATH}`);
|
|
1363
1614
|
const hook = settings.findHook("SessionStart", HOOK_BASENAME);
|
|
@@ -1503,7 +1754,51 @@ class Switchers {
|
|
|
1503
1754
|
}
|
|
1504
1755
|
}
|
|
1505
1756
|
}
|
|
1506
|
-
|
|
1757
|
+
const SCHEME = "claude-discover";
|
|
1758
|
+
class DeepLink extends EventEmitter {
|
|
1759
|
+
#pending = null;
|
|
1760
|
+
#live = false;
|
|
1761
|
+
// the renderer has pulled the cold-start link, so later links can be pushed to it
|
|
1762
|
+
// Call at startup, before app.whenReady(). Returns false when another instance already holds the
|
|
1763
|
+
// lock — the caller must then quit, our argv has been forwarded to the window that owns it.
|
|
1764
|
+
requestLock() {
|
|
1765
|
+
if (!app.requestSingleInstanceLock()) return false;
|
|
1766
|
+
app.on("second-instance", (_e, argv) => this.emit("open", findTarget(argv)));
|
|
1767
|
+
app.on("open-url", (e, url) => {
|
|
1768
|
+
e.preventDefault();
|
|
1769
|
+
this.#deliver(findTarget([url]));
|
|
1770
|
+
});
|
|
1771
|
+
this.#register();
|
|
1772
|
+
this.#pending = findTarget(process.argv);
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
takePending() {
|
|
1776
|
+
this.#live = true;
|
|
1777
|
+
const target = this.#pending;
|
|
1778
|
+
this.#pending = null;
|
|
1779
|
+
return target;
|
|
1780
|
+
}
|
|
1781
|
+
// @macOS Launch Services reuses the running bundle instead of launching a second process, so links
|
|
1782
|
+
// come as events — and a cold-start one fires before the renderer exists, hence the park.
|
|
1783
|
+
#deliver(target) {
|
|
1784
|
+
if (!target) return;
|
|
1785
|
+
if (this.#live) this.emit("open", target);
|
|
1786
|
+
else this.#pending = target;
|
|
1787
|
+
}
|
|
1788
|
+
// @windows Rewritten on every launch — idempotent, and the last launched checkout wins.
|
|
1789
|
+
#register() {
|
|
1790
|
+
if (process.platform !== "win32") return;
|
|
1791
|
+
if (IS_EPHEMERAL) return;
|
|
1792
|
+
app.setAsDefaultProtocolClient(SCHEME, process.execPath, [app.getAppPath()]);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function findTarget(argv) {
|
|
1796
|
+
const link = URL.parse(argv.find((a) => a.startsWith(`${SCHEME}://`)) ?? "");
|
|
1797
|
+
const id = link?.searchParams.get("id");
|
|
1798
|
+
return id ? { id, date: link.searchParams.get("date") } : null;
|
|
1799
|
+
}
|
|
1800
|
+
const deepLink = new DeepLink();
|
|
1801
|
+
function start() {
|
|
1507
1802
|
const win = new MainWindow();
|
|
1508
1803
|
const agentRunner = new AgentRunner();
|
|
1509
1804
|
const workHours = new WorkHours();
|
|
@@ -1511,6 +1806,10 @@ app.whenReady().then(() => {
|
|
|
1511
1806
|
sessionsService.on("update", (sessions) => win.send("sessions:update", sessions));
|
|
1512
1807
|
sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
|
|
1513
1808
|
sessionsService.start();
|
|
1809
|
+
deepLink.on("open", (target) => {
|
|
1810
|
+
win.focus();
|
|
1811
|
+
if (target) win.send("deeplink:open-session", target);
|
|
1812
|
+
});
|
|
1514
1813
|
win.create();
|
|
1515
1814
|
app.on("activate", () => {
|
|
1516
1815
|
if (BrowserWindow.getAllWindows().length === 0) win.create();
|
|
@@ -1522,6 +1821,7 @@ app.whenReady().then(() => {
|
|
|
1522
1821
|
ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
|
|
1523
1822
|
ipcMain.handle("agent:run", (e, text, systemTools, cache) => agentRunner.run(text, e.sender, systemTools, cache));
|
|
1524
1823
|
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
1824
|
+
ipcMain.handle("deeplink:take-pending", () => deepLink.takePending());
|
|
1525
1825
|
ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
|
|
1526
1826
|
ipcMain.on("find:stop", () => win.findBar?.stop());
|
|
1527
1827
|
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
@@ -1536,7 +1836,13 @@ app.whenReady().then(() => {
|
|
|
1536
1836
|
ipcMain.handle("switch:activate", (_e, name) => switchers.activate(name));
|
|
1537
1837
|
ipcMain.handle("switch:deactivate", (_e, name) => switchers.deactivate(name));
|
|
1538
1838
|
ipcMain.handle("switch:keep-active", (_e, name, value) => switchers.setKeepActive(name, value));
|
|
1539
|
-
}
|
|
1839
|
+
}
|
|
1540
1840
|
app.on("window-all-closed", () => {
|
|
1541
1841
|
if (process.platform !== "darwin") app.quit();
|
|
1542
1842
|
});
|
|
1843
|
+
const isFirstInstance = deepLink.requestLock();
|
|
1844
|
+
if (isFirstInstance) {
|
|
1845
|
+
app.whenReady().then(start);
|
|
1846
|
+
} else {
|
|
1847
|
+
app.exit(0);
|
|
1848
|
+
}
|
package/out/preload/preload.cjs
CHANGED
|
@@ -24,5 +24,7 @@ electron.contextBridge.exposeInMainWorld("api", {
|
|
|
24
24
|
onAgentOutput: subscribe("agent:output"),
|
|
25
25
|
openLink: (href, baseFile) => electron.ipcRenderer.invoke("shell:open-link", href, baseFile),
|
|
26
26
|
onFindActive: subscribe("find:active"),
|
|
27
|
-
findClose: () => electron.ipcRenderer.send("find:close")
|
|
27
|
+
findClose: () => electron.ipcRenderer.send("find:close"),
|
|
28
|
+
onDeepLink: subscribe("deeplink:open-session"),
|
|
29
|
+
takeDeepLink: () => electron.ipcRenderer.invoke("deeplink:take-pending")
|
|
28
30
|
});
|