claude-discover 1.9.4 → 1.9.5
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/out/main/main.js
CHANGED
|
@@ -1,18 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { app, dialog, BrowserWindow, shell, WebContentsView, Menu, ipcMain } from "electron";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import os, { homedir } from "node:os";
|
|
4
|
+
import path, { join, dirname, basename } from "node:path";
|
|
3
5
|
import fs from "node:fs";
|
|
6
|
+
import { sync } from "write-file-atomic";
|
|
4
7
|
import fsp from "node:fs/promises";
|
|
5
|
-
import path, { join, dirname, basename } from "node:path";
|
|
6
|
-
import os, { homedir } from "node:os";
|
|
7
8
|
import crypto from "node:crypto";
|
|
8
9
|
import chokidar from "chokidar";
|
|
9
|
-
import { sync } from "write-file-atomic";
|
|
10
|
-
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
11
|
-
import windowStateKeeper from "electron-window-state";
|
|
12
|
-
import contextMenu from "electron-context-menu";
|
|
13
10
|
import which from "which";
|
|
14
11
|
import { execFile } from "node:child_process";
|
|
15
12
|
import { promisify } from "node:util";
|
|
13
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
14
|
+
import windowStateKeeper from "electron-window-state";
|
|
15
|
+
import contextMenu from "electron-context-menu";
|
|
16
|
+
class ConfigFile {
|
|
17
|
+
dataDir = join(homedir(), ".claude-discover");
|
|
18
|
+
configFile = join(this.dataDir, "config.json");
|
|
19
|
+
read() {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(this.configFile, "utf8"));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code !== "ENOENT") console.warn(`Ignoring unreadable ${this.configFile}: ${err.message}`);
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
save(data) {
|
|
28
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
29
|
+
sync(this.configFile, JSON.stringify({ ...this.read(), ...data }, null, 2));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const config = new ConfigFile();
|
|
33
|
+
const local = config.read();
|
|
34
|
+
const DATA_DIR$1 = config.dataDir;
|
|
35
|
+
const CLAUDE_DIR = local.claudeDir || process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
36
|
+
const RECENT_CLAUDE_DIRS = local.recents || [CLAUDE_DIR];
|
|
37
|
+
const CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
38
|
+
const CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json");
|
|
39
|
+
const IS_EPHEMERAL = import.meta.dirname.split(/[\\/]/).includes("_npx");
|
|
40
|
+
const STATUSLINE_PATH = join(import.meta.dirname, "../../bin/claude/statusline.mjs");
|
|
41
|
+
const PROXY_PATH = join(import.meta.dirname, "../../bin/proxy.mjs");
|
|
42
|
+
const SCHEME = "claude-discover";
|
|
43
|
+
class DeepLink extends EventEmitter {
|
|
44
|
+
#pending = null;
|
|
45
|
+
#live = false;
|
|
46
|
+
// the renderer has pulled the cold-start link, so later links can be pushed to it
|
|
47
|
+
// Call in the first instance before app.whenReady(), so cold-start URLs can be parked.
|
|
48
|
+
activate() {
|
|
49
|
+
app.on("open-url", (e, url) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
this.#deliver(findTarget([url]));
|
|
52
|
+
});
|
|
53
|
+
this.#register();
|
|
54
|
+
this.#pending = findTarget(process.argv);
|
|
55
|
+
}
|
|
56
|
+
open(target) {
|
|
57
|
+
if (target) this.emit("open", target);
|
|
58
|
+
}
|
|
59
|
+
takePending() {
|
|
60
|
+
this.#live = true;
|
|
61
|
+
const target = this.#pending;
|
|
62
|
+
this.#pending = null;
|
|
63
|
+
return target;
|
|
64
|
+
}
|
|
65
|
+
// @macOS Launch Services reuses the running bundle instead of launching a second process, so links
|
|
66
|
+
// come as events — and a cold-start one fires before the renderer exists, hence the park.
|
|
67
|
+
#deliver(target) {
|
|
68
|
+
if (!target) return;
|
|
69
|
+
if (this.#live) this.emit("open", target);
|
|
70
|
+
else this.#pending = target;
|
|
71
|
+
}
|
|
72
|
+
// @windows Rewritten on every launch — idempotent, and the last launched checkout wins.
|
|
73
|
+
#register() {
|
|
74
|
+
if (process.platform !== "win32") return;
|
|
75
|
+
if (IS_EPHEMERAL) return;
|
|
76
|
+
app.setAsDefaultProtocolClient(SCHEME, process.execPath, [app.getAppPath()]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function findTarget(argv) {
|
|
80
|
+
const link = URL.parse(argv.find((a) => a.startsWith(`${SCHEME}://`)) ?? "");
|
|
81
|
+
const id = link?.searchParams.get("id");
|
|
82
|
+
return id ? { id, date: link.searchParams.get("date") } : null;
|
|
83
|
+
}
|
|
16
84
|
const millisecondsInMinute = 6e4;
|
|
17
85
|
const millisecondsInHour = 36e5;
|
|
18
86
|
const constructFromSymbol = /* @__PURE__ */ Symbol.for("constructDateFrom");
|
|
@@ -250,7 +318,9 @@ class SessionFile {
|
|
|
250
318
|
constructor(filePath) {
|
|
251
319
|
this.filePath = filePath;
|
|
252
320
|
const parts = path.dirname(filePath).split(path.sep);
|
|
253
|
-
|
|
321
|
+
const i = parts.indexOf("subagents");
|
|
322
|
+
this.parentSessionId = i > 0 ? parts[i - 1] : null;
|
|
323
|
+
this.parentFilePath = i > 0 ? parts.slice(0, i).join(path.sep) + ".jsonl" : null;
|
|
254
324
|
const base = path.basename(filePath, ".jsonl");
|
|
255
325
|
this.sessionId = base === "journal" ? `${parts.at(-1)}-journal` : base;
|
|
256
326
|
}
|
|
@@ -296,37 +366,10 @@ const UPSTREAM = process.env.CLAUDE_DISCOVER_UPSTREAM || "https://api.anthropic.
|
|
|
296
366
|
const EXIT_ROUTE = "/claude-discover/exit";
|
|
297
367
|
const PING_ROUTE = "/claude-discover/ping";
|
|
298
368
|
const PING_RESPONSE = "claude-discover-proxy";
|
|
299
|
-
const DATA_DIR
|
|
300
|
-
const REQUESTS_DIR = path.join(DATA_DIR
|
|
301
|
-
const ERROR_LOG_PATH = path.join(DATA_DIR
|
|
302
|
-
const
|
|
303
|
-
const contentHash = (s) => crypto.createHash("sha256").update(s).digest("base64url").slice(0, 16);
|
|
304
|
-
const isMcp = (name) => name.startsWith("mcp__");
|
|
305
|
-
const MEMORY_LABELS = {
|
|
306
|
-
"user's private global instructions for all projects": "User",
|
|
307
|
-
"project instructions, checked into the codebase": "Project",
|
|
308
|
-
"user's private project instructions, not checked in": "Local",
|
|
309
|
-
"user's auto-memory, persists across conversations": "Auto"
|
|
310
|
-
};
|
|
311
|
-
const MEMORY_FILES = { User: "CLAUDE.md", Project: "CLAUDE.md", Local: "CLAUDE.local.md", Auto: "MEMORY.md" };
|
|
312
|
-
function parseClaudeMd(text) {
|
|
313
|
-
const marker = text.match(/\n# claudeMd\r?\n|Codebase and user instructions are shown below\./);
|
|
314
|
-
if (!marker) return [];
|
|
315
|
-
let region = text.slice(marker.index + marker[0].length);
|
|
316
|
-
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);
|
|
317
|
-
if (ends.length) region = region.slice(0, Math.min(...ends));
|
|
318
|
-
const heads = [...region.matchAll(/^Contents of (.+?) \(([^)]*)\):\r?\n/gm)];
|
|
319
|
-
return heads.map((h, i) => {
|
|
320
|
-
const memory_type = MEMORY_LABELS[h[2]] ?? h[2];
|
|
321
|
-
const base = h[1].split(/[\\/]/).pop();
|
|
322
|
-
return {
|
|
323
|
-
file_path: h[1],
|
|
324
|
-
name: base === MEMORY_FILES[memory_type] ? base : h[1],
|
|
325
|
-
memory_type,
|
|
326
|
-
content: region.slice(h.index + h[0].length, heads[i + 1]?.index ?? region.length).trim()
|
|
327
|
-
};
|
|
328
|
-
});
|
|
329
|
-
}
|
|
369
|
+
const DATA_DIR = process.env.CLAUDE_DISCOVER_DIR || path.join(os.homedir(), ".claude-discover");
|
|
370
|
+
const REQUESTS_DIR = path.join(DATA_DIR, "requests");
|
|
371
|
+
const ERROR_LOG_PATH = path.join(DATA_DIR, "proxy.error.log");
|
|
372
|
+
const headline = (line) => line.length > 34 ? line.slice(0, 33).trimEnd() + "…" : line;
|
|
330
373
|
const classifyRequest = (req, url) => {
|
|
331
374
|
if (url?.includes("/count_tokens")) return ["count", "Token count"];
|
|
332
375
|
if (!req) return null;
|
|
@@ -340,24 +383,80 @@ const classifyRequest = (req, url) => {
|
|
|
340
383
|
const tail = (blocks?.findLast((b) => b?.type === "text")?.text ?? user).slice(0, 600);
|
|
341
384
|
if (req.max_tokens === 1 || user.trim() === "quota") return ["quota", "Quota probe"];
|
|
342
385
|
if (sysText.includes("You are a security monitor") || user.startsWith("<transcript>")) return ["security", "Auto-mode check"];
|
|
343
|
-
if (sysText.includes("Generate a concise, sentence-case title")) return ["title", "Session title"];
|
|
386
|
+
if (sysText.includes("Generate a concise, sentence-case title") || sysText.includes("You are naming a coding session")) return ["title", "Session title"];
|
|
344
387
|
if (sysText.includes("performing a web search")) return ["web", "Web search"];
|
|
345
388
|
if (user.startsWith("[SUGGESTION MODE")) return ["suggest", "Suggestions"];
|
|
346
389
|
if (user.startsWith("Web page content")) return ["web", "Web fetch"];
|
|
347
390
|
if (user.startsWith("Describe your most recent action")) return ["status", "Status blurb"];
|
|
348
391
|
if (tail.includes("detailed summary of the conversation")) return ["compact", "Compact"];
|
|
349
392
|
if (sysText.includes("built on Anthropic's Claude Agent SDK")) return ["agent", "Subagent"];
|
|
393
|
+
const tools = un(req.tools);
|
|
394
|
+
if (sysText && Array.isArray(tools) && !tools.length) {
|
|
395
|
+
const line = sysText.split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("x-anthropic-billing-header") && !l.startsWith("You are Claude Code"));
|
|
396
|
+
return ["aux", line ? headline(line) : "Side channel"];
|
|
397
|
+
}
|
|
350
398
|
if (!sysText.includes("You are Claude Code")) return null;
|
|
351
399
|
const continues = !last || blocks?.some((b) => b?.type === "tool_result");
|
|
352
400
|
return continues ? ["tools", "Tool turn"] : ["main", "User message"];
|
|
353
401
|
};
|
|
402
|
+
const REMINDER_LABELS = [
|
|
403
|
+
["As you answer the user", "Session Context"],
|
|
404
|
+
["# Environment", "Environment"],
|
|
405
|
+
["Attribution for git commits", "Attribution"],
|
|
406
|
+
["Available agent types", "Agent Types"],
|
|
407
|
+
["The following skills are available", "Skills"],
|
|
408
|
+
["# MCP Server Instructions", "MCP Instructions"],
|
|
409
|
+
["You are powered by the model", "Model"],
|
|
410
|
+
["Today's date", "Date"],
|
|
411
|
+
["While auto mode is active", "Auto Mode"],
|
|
412
|
+
["## Auto Mode Active", "Auto Mode"],
|
|
413
|
+
["[SYSTEM NOTIFICATION", "Notification"],
|
|
414
|
+
["Other agents active", "Agents"],
|
|
415
|
+
["<total_tokens>", "Token Budget"]
|
|
416
|
+
];
|
|
417
|
+
const reminderLabel = (text) => {
|
|
418
|
+
const line = text.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
|
|
419
|
+
return REMINDER_LABELS.find(([open]) => line.startsWith(open))?.[1] ?? headline(line);
|
|
420
|
+
};
|
|
421
|
+
const DEFERRED_INTRO = /deferred tools are now available[^\n]*:\r?\n/;
|
|
422
|
+
const MEMORY_LABELS = {
|
|
423
|
+
"user's private global instructions for all projects": "User",
|
|
424
|
+
"project instructions, checked into the codebase": "Project",
|
|
425
|
+
"user's private project instructions, not checked in": "Local",
|
|
426
|
+
"user's auto-memory, persists across conversations": "Auto"
|
|
427
|
+
};
|
|
428
|
+
const MEMORY_FILES = { User: "CLAUDE.md", Project: "CLAUDE.md", Local: "CLAUDE.local.md", Auto: "MEMORY.md" };
|
|
429
|
+
function splitClaudeMd(text) {
|
|
430
|
+
const marker = text.match(/\n# claudeMd\r?\n|Codebase and user instructions are shown below\./);
|
|
431
|
+
if (!marker) return { files: [], rest: text };
|
|
432
|
+
const start = marker.index + marker[0].length;
|
|
433
|
+
const ends = [/\n# [a-z][a-zA-Z0-9]*\r?\n/, /\n\s*IMPORTANT: this context/, /<\/system-reminder>/].map((re) => text.slice(start).search(re)).filter((i) => i !== -1);
|
|
434
|
+
const end = ends.length ? start + Math.min(...ends) : text.length;
|
|
435
|
+
const region = text.slice(start, end);
|
|
436
|
+
const heads = [...region.matchAll(/^Contents of (.+?) \(([^)]*)\):\r?\n/gm)];
|
|
437
|
+
const files = heads.map((h, i) => {
|
|
438
|
+
const memory_type = MEMORY_LABELS[h[2]] ?? h[2];
|
|
439
|
+
const base = h[1].split(/[\\/]/).pop();
|
|
440
|
+
return {
|
|
441
|
+
file_path: h[1],
|
|
442
|
+
name: base === MEMORY_FILES[memory_type] ? base : h[1],
|
|
443
|
+
memory_type,
|
|
444
|
+
content: region.slice(h.index + h[0].length, heads[i + 1]?.index ?? region.length).trim()
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
return { files, rest: (text.slice(0, marker.index) + text.slice(end)).trimEnd() };
|
|
448
|
+
}
|
|
449
|
+
const jsonBytes = (x) => x == null ? 0 : Buffer.byteLength(JSON.stringify(x));
|
|
450
|
+
const contentHash = (s) => crypto.createHash("sha256").update(s).digest("base64url").slice(0, 16);
|
|
451
|
+
const isMcp = (name) => name.startsWith("mcp__");
|
|
354
452
|
const stripKind = (req) => {
|
|
355
453
|
const kind = classifyRequest(req);
|
|
356
454
|
return !kind || kind[0] === "main" || kind[0] === "tools" ? ["main", null] : kind;
|
|
357
455
|
};
|
|
358
|
-
const toStrip = (rec, file_path, label, content) => {
|
|
456
|
+
const toStrip = (rec, source, file_path, label, content) => {
|
|
359
457
|
const [kind, kindLabel] = stripKind(rec.request);
|
|
360
458
|
return {
|
|
459
|
+
source,
|
|
361
460
|
file_path,
|
|
362
461
|
memory_type: [kindLabel, label].filter(Boolean).join(", "),
|
|
363
462
|
model: rec.request.model,
|
|
@@ -366,6 +465,8 @@ const toStrip = (rec, file_path, label, content) => {
|
|
|
366
465
|
kind
|
|
367
466
|
};
|
|
368
467
|
};
|
|
468
|
+
const texts = (c) => (Array.isArray(c) ? c.map((p) => p?.text) : [c]).filter((t) => typeof t === "string");
|
|
469
|
+
const injectedTexts = (req) => (req?.messages || []).filter((m) => m?.role !== "assistant").flatMap((m) => texts(m.content));
|
|
369
470
|
class RequestParser {
|
|
370
471
|
constructor() {
|
|
371
472
|
this.hashes = /* @__PURE__ */ new Map();
|
|
@@ -409,12 +510,13 @@ class RequestParser {
|
|
|
409
510
|
return rec;
|
|
410
511
|
}
|
|
411
512
|
// The request's system prompt, from a resolveRefs'd record — a plain string or an array of
|
|
412
|
-
// text blocks
|
|
513
|
+
// text blocks, minus any CLAUDE.md section (those files are strips of their own, see memoryFiles).
|
|
514
|
+
// The content hash is the dedup key: repeats collapse in the caller's map.
|
|
413
515
|
systemPrompt(rec) {
|
|
414
516
|
const sys = rec.request?.system;
|
|
415
517
|
if (sys == null || sys.$ref) return null;
|
|
416
|
-
const content = typeof sys === "string" ? sys : sys.map((b) => b?.text ?? "").join("\n\n");
|
|
417
|
-
return toStrip(rec, "System Prompt", "", content);
|
|
518
|
+
const content = typeof sys === "string" ? splitClaudeMd(sys).rest : sys.map((b) => splitClaudeMd(b?.text ?? "").rest).join("\n\n");
|
|
519
|
+
return toStrip(rec, "system", "System Prompt", "", content);
|
|
418
520
|
}
|
|
419
521
|
// The deferred-tool roster Claude Code announces in the environment block of the first user
|
|
420
522
|
// message: names it can call but whose schemas are left out of `tools` until ToolSearch pulls one
|
|
@@ -427,7 +529,7 @@ class RequestParser {
|
|
|
427
529
|
for (const part of Array.isArray(content) ? content : [{ text: content }]) {
|
|
428
530
|
const text = part?.text;
|
|
429
531
|
if (typeof text !== "string") continue;
|
|
430
|
-
const intro = text.match(
|
|
532
|
+
const intro = text.match(DEFERRED_INTRO);
|
|
431
533
|
if (!intro) continue;
|
|
432
534
|
const names = [];
|
|
433
535
|
for (const line of text.slice(intro.index + intro[0].length).split(/\r?\n/)) {
|
|
@@ -449,7 +551,7 @@ class RequestParser {
|
|
|
449
551
|
const mcp = names.filter(isMcp).length;
|
|
450
552
|
const count = (n, what) => n && `${n} deferred ${what} tool${n === 1 ? "" : "s"}`;
|
|
451
553
|
const label = [count(names.length - mcp, "system"), count(mcp, "MCP")].filter(Boolean).join(", ");
|
|
452
|
-
return toStrip(rec, "Deferred Tools", label, "```\n" + names.join("\n") + "\n```");
|
|
554
|
+
return toStrip(rec, "message", "Deferred Tools", label, "```\n" + names.join("\n") + "\n```");
|
|
453
555
|
}
|
|
454
556
|
// The request's tool definitions, from a resolveRefs'd record, as a native and an MCP strip —
|
|
455
557
|
// MCP schemas are often the bulk of a prompt, so they are worth costing on their own. The array
|
|
@@ -465,23 +567,33 @@ class RequestParser {
|
|
|
465
567
|
const list = fresh.filter((t) => pick(t.name));
|
|
466
568
|
if (!list.length) return null;
|
|
467
569
|
const content = list.map((t) => [`## ${t.name}`, t.description || "", schema(t)].filter(Boolean).join("\n\n")).join("\n\n");
|
|
468
|
-
return toStrip(rec, file_path, `${list.length} tool${list.length === 1 ? "" : "s"}`, content);
|
|
570
|
+
return toStrip(rec, "tools", file_path, `${list.length} tool${list.length === 1 ? "" : "s"}`, content);
|
|
469
571
|
};
|
|
470
572
|
return [strip("System Tools", (n) => !isMcp(n)), strip("MCP Tools", isMcp)].filter(Boolean);
|
|
471
573
|
}
|
|
472
|
-
// Memory files (CLAUDE.md / MEMORY.md / …) carried by a resolveRefs'd record
|
|
473
|
-
//
|
|
574
|
+
// Memory files (CLAUDE.md / MEMORY.md / …) carried by a resolveRefs'd record — one
|
|
575
|
+
// { source, file_path, memory_type, content } per file listed. Claude Code ships them in a user
|
|
576
|
+
// message's system-reminder; an Agent SDK app can append the same section to the system prompt.
|
|
474
577
|
memoryFiles(rec) {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
578
|
+
const req = rec.request;
|
|
579
|
+
const reminders = injectedTexts(req).filter((t) => t.includes("<system-reminder>"));
|
|
580
|
+
const from = (source, list) => list.flatMap((t) => splitClaudeMd(t).files).map((f) => ({ source, ...f }));
|
|
581
|
+
return [...from("system", texts(req?.system)), ...from("message", reminders)];
|
|
582
|
+
}
|
|
583
|
+
// Every other <system-reminder> the harness injected into the messages — session context, git
|
|
584
|
+
// status, environment, attribution rules, … — one strip each, minus what is already a strip of
|
|
585
|
+
// its own: the CLAUDE.md files (cut out, which empties the reminder that only wraps them) and
|
|
586
|
+
// the deferred-tool roster. Nothing the model was sent stays hidden.
|
|
587
|
+
reminderStrips(rec) {
|
|
588
|
+
const strips = [];
|
|
589
|
+
for (const text of injectedTexts(rec.request)) {
|
|
590
|
+
for (const [, inner] of text.matchAll(/<system-reminder>([\s\S]*?)<\/system-reminder>/g)) {
|
|
591
|
+
if (DEFERRED_INTRO.test(inner)) continue;
|
|
592
|
+
const content = splitClaudeMd(inner).rest.trim();
|
|
593
|
+
if (content) strips.push(toStrip(rec, "message", reminderLabel(content), "system-reminder", content));
|
|
482
594
|
}
|
|
483
595
|
}
|
|
484
|
-
return
|
|
596
|
+
return strips;
|
|
485
597
|
}
|
|
486
598
|
}
|
|
487
599
|
const agentKey = (id) => id?.includes("@") ? id.slice(0, id.indexOf("@")) : id?.replace(/^a/, "").replace(/-[0-9a-f]{16}$/, "") ?? null;
|
|
@@ -519,9 +631,10 @@ class RequestFile {
|
|
|
519
631
|
return t >= range.start && t <= range.end;
|
|
520
632
|
});
|
|
521
633
|
}
|
|
522
|
-
// System prompts, tool definitions
|
|
523
|
-
// message's instructions system-reminder)
|
|
524
|
-
// log is the only source. Returns one record per
|
|
634
|
+
// System prompts, tool definitions, memory files (CLAUDE.md / MEMORY.md / … from a user
|
|
635
|
+
// message's instructions system-reminder) and every other injected system-reminder — none of
|
|
636
|
+
// which the transcript records, so the request log is the only source. Returns one record per
|
|
637
|
+
// unique prompt / tool batch / file_path / reminder.
|
|
525
638
|
async readInstructions() {
|
|
526
639
|
const parser = new RequestParser();
|
|
527
640
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -535,6 +648,9 @@ class RequestFile {
|
|
|
535
648
|
for (const f of parser.memoryFiles(rec)) {
|
|
536
649
|
if (!files.has(f.file_path)) files.set(f.file_path, record(f));
|
|
537
650
|
}
|
|
651
|
+
for (const r of parser.reminderStrips(rec)) {
|
|
652
|
+
if (!files.has(r.hash)) files.set(r.hash, record(r));
|
|
653
|
+
}
|
|
538
654
|
}
|
|
539
655
|
return [...files.values()];
|
|
540
656
|
}
|
|
@@ -568,11 +684,12 @@ function classifySource(meta) {
|
|
|
568
684
|
if (meta.hasScheduledTask) return "scheduled";
|
|
569
685
|
return (meta.entrypoint || "").toLowerCase() || "other";
|
|
570
686
|
}
|
|
571
|
-
function freshMeta({ sessionId, parentSessionId, filePath, fileSize, mtime }) {
|
|
687
|
+
function freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileSize, mtime }) {
|
|
572
688
|
return {
|
|
573
689
|
sessionId,
|
|
574
690
|
parentSessionId,
|
|
575
691
|
filePath,
|
|
692
|
+
parentFilePath,
|
|
576
693
|
fileSize,
|
|
577
694
|
mtime,
|
|
578
695
|
startedAt: null,
|
|
@@ -599,6 +716,7 @@ function freshMeta({ sessionId, parentSessionId, filePath, fileSize, mtime }) {
|
|
|
599
716
|
messageCount: 0,
|
|
600
717
|
workflowAgents: 0,
|
|
601
718
|
toolCalls: 0,
|
|
719
|
+
skillCalls: 0,
|
|
602
720
|
tokens: emptyBucket(),
|
|
603
721
|
tokensByModel: {},
|
|
604
722
|
tokensByModelFast: {},
|
|
@@ -611,11 +729,11 @@ function freshMeta({ sessionId, parentSessionId, filePath, fileSize, mtime }) {
|
|
|
611
729
|
};
|
|
612
730
|
}
|
|
613
731
|
class SessionParser {
|
|
614
|
-
constructor({ sessionId, parentSessionId, filePath, fileSize = 0, mtime = 0, pricing = null, range = null, excludeIds = null }) {
|
|
732
|
+
constructor({ sessionId, parentSessionId, filePath, parentFilePath = null, fileSize = 0, mtime = 0, pricing = null, range = null, excludeIds = null }) {
|
|
615
733
|
this.range = range;
|
|
616
734
|
this.excludeIds = excludeIds;
|
|
617
735
|
this.pricing = pricing;
|
|
618
|
-
this.meta = freshMeta({ sessionId, parentSessionId, filePath, fileSize, mtime });
|
|
736
|
+
this.meta = freshMeta({ sessionId, parentSessionId, filePath, parentFilePath, fileSize, mtime });
|
|
619
737
|
this.prevMessage = null;
|
|
620
738
|
this.tokenTotal = 0;
|
|
621
739
|
this.lastSeenTs = null;
|
|
@@ -659,6 +777,7 @@ class SessionParser {
|
|
|
659
777
|
meta.activityPeriods.push({ start: ts, end: ts });
|
|
660
778
|
}
|
|
661
779
|
if (t === "user") {
|
|
780
|
+
if (obj.turnCompanion && !obj.sourceToolUseID) meta.skillCalls += 1;
|
|
662
781
|
this._feedUser(obj);
|
|
663
782
|
} else if (t === "assistant") {
|
|
664
783
|
const msg = obj.message;
|
|
@@ -668,7 +787,11 @@ class SessionParser {
|
|
|
668
787
|
if (!meta.models.includes(msg.model)) meta.models.push(msg.model);
|
|
669
788
|
}
|
|
670
789
|
if (Array.isArray(msg.content)) {
|
|
671
|
-
for (const b of msg.content)
|
|
790
|
+
for (const b of msg.content) {
|
|
791
|
+
if (b?.type !== "tool_use") continue;
|
|
792
|
+
meta.toolCalls += 1;
|
|
793
|
+
if (b.name === "Skill") meta.skillCalls += 1;
|
|
794
|
+
}
|
|
672
795
|
}
|
|
673
796
|
this._recordUsage(obj);
|
|
674
797
|
}
|
|
@@ -690,7 +813,7 @@ class SessionParser {
|
|
|
690
813
|
obj.isMeta = true;
|
|
691
814
|
} else if (text.startsWith("<scheduled-task")) {
|
|
692
815
|
meta.hasScheduledTask = true;
|
|
693
|
-
} else if (text) {
|
|
816
|
+
} else if (text && !obj.isMeta) {
|
|
694
817
|
if (!meta.firstUserPrompt) meta.firstUserPrompt = text.slice(0, 300);
|
|
695
818
|
}
|
|
696
819
|
}
|
|
@@ -789,32 +912,6 @@ class SessionParser {
|
|
|
789
912
|
return meta;
|
|
790
913
|
}
|
|
791
914
|
}
|
|
792
|
-
class ConfigFile {
|
|
793
|
-
dataDir = join(homedir(), ".claude-discover");
|
|
794
|
-
configFile = join(this.dataDir, "config.json");
|
|
795
|
-
read() {
|
|
796
|
-
try {
|
|
797
|
-
return JSON.parse(fs.readFileSync(this.configFile, "utf8"));
|
|
798
|
-
} catch (err) {
|
|
799
|
-
if (err.code !== "ENOENT") console.warn(`Ignoring unreadable ${this.configFile}: ${err.message}`);
|
|
800
|
-
return {};
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
save(data) {
|
|
804
|
-
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
805
|
-
sync(this.configFile, JSON.stringify({ ...this.read(), ...data }, null, 2));
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
const config = new ConfigFile();
|
|
809
|
-
const local = config.read();
|
|
810
|
-
const DATA_DIR = config.dataDir;
|
|
811
|
-
const CLAUDE_DIR = local.claudeDir || process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
812
|
-
const RECENT_CLAUDE_DIRS = local.recents || [CLAUDE_DIR];
|
|
813
|
-
const CLAUDE_PROJECTS_DIR = join(CLAUDE_DIR, "projects");
|
|
814
|
-
const CLAUDE_SETTINGS = join(CLAUDE_DIR, "settings.json");
|
|
815
|
-
const IS_EPHEMERAL = import.meta.dirname.split(/[\\/]/).includes("_npx");
|
|
816
|
-
const STATUSLINE_PATH = join(import.meta.dirname, "../../bin/claude/statusline.mjs");
|
|
817
|
-
const PROXY_PATH = join(import.meta.dirname, "../../bin/proxy.mjs");
|
|
818
915
|
class StatCache {
|
|
819
916
|
constructor() {
|
|
820
917
|
this.stats = /* @__PURE__ */ new Map();
|
|
@@ -961,9 +1058,6 @@ class MetaCache {
|
|
|
961
1058
|
get(filePath) {
|
|
962
1059
|
return this.periodMetas.get(filePath);
|
|
963
1060
|
}
|
|
964
|
-
byId(sessionId) {
|
|
965
|
-
return [...this.periodMetas.values()].find((m) => m.sessionId === sessionId);
|
|
966
|
-
}
|
|
967
1061
|
values() {
|
|
968
1062
|
return this.periodMetas.values();
|
|
969
1063
|
}
|
|
@@ -1024,7 +1118,7 @@ const seed = {
|
|
|
1024
1118
|
"claude-3-sonnet": { "input": 3, "output": 15, "cacheRead": 0.3, "cacheWrite": 3.75 },
|
|
1025
1119
|
"claude-3-haiku": { "input": 0.25, "output": 1.25, "cacheRead": 0.03, "cacheWrite": 0.3, "cacheWrite1h": 0.5 }
|
|
1026
1120
|
};
|
|
1027
|
-
const CURRENT_PATH = path.join(DATA_DIR, "prices.current.json");
|
|
1121
|
+
const CURRENT_PATH = path.join(DATA_DIR$1, "prices.current.json");
|
|
1028
1122
|
const LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
1029
1123
|
const DAY_MS = 24 * 3600 * 1e3;
|
|
1030
1124
|
const MILLION = 1e6;
|
|
@@ -1113,7 +1207,7 @@ class Pricing extends EventEmitter {
|
|
|
1113
1207
|
if (typeof fast === "number") out["claude-" + name].fastModeMplr = fast;
|
|
1114
1208
|
}
|
|
1115
1209
|
if (Object.keys(out).length) {
|
|
1116
|
-
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
1210
|
+
fs.mkdirSync(DATA_DIR$1, { recursive: true });
|
|
1117
1211
|
fs.writeFileSync(CURRENT_PATH, JSON.stringify(out, null, 2) + "\n");
|
|
1118
1212
|
const before = JSON.stringify(this.#prices);
|
|
1119
1213
|
this.load();
|
|
@@ -1150,8 +1244,10 @@ class SessionsService extends EventEmitter {
|
|
|
1150
1244
|
}
|
|
1151
1245
|
// Always parses the whole file from 0: the parser's running token totals only stamp
|
|
1152
1246
|
// correctly from a fresh start, so live updates re-read rather than resume at an offset.
|
|
1153
|
-
|
|
1154
|
-
|
|
1247
|
+
// Keyed by file path, not session id: Claude Code reuses one id across cwds, writing a
|
|
1248
|
+
// separate transcript per project dir.
|
|
1249
|
+
async readSession(filePath, date = null, granularity = "day") {
|
|
1250
|
+
const meta = this.metaCache.get(filePath);
|
|
1155
1251
|
if (!meta) return null;
|
|
1156
1252
|
const range = date ? periodBounds(date, granularity) : null;
|
|
1157
1253
|
const reader = new SessionFile(meta.filePath);
|
|
@@ -1166,10 +1262,11 @@ class SessionsService extends EventEmitter {
|
|
|
1166
1262
|
return { items, instructions };
|
|
1167
1263
|
}
|
|
1168
1264
|
// Captured API requests for a session (see bin/proxy.mjs), period-filtered like readSession.
|
|
1169
|
-
async readRequests(
|
|
1170
|
-
|
|
1265
|
+
async readRequests(filePath, date = null, granularity = "day") {
|
|
1266
|
+
const meta = this.metaCache.get(filePath);
|
|
1267
|
+
if (!meta || !/^[\w-]+$/.test(meta.sessionId)) return [];
|
|
1171
1268
|
const range = date ? periodBounds(date, granularity) : null;
|
|
1172
|
-
return this.#requestFile(sessionId,
|
|
1269
|
+
return this.#requestFile(meta.sessionId, meta.parentSessionId).read(range);
|
|
1173
1270
|
}
|
|
1174
1271
|
// Subagents send the parent's session id, so the proxy logs their requests into the parent's
|
|
1175
1272
|
// file, tagged x-claude-code-agent-id — route their reads there. The transcript filename is
|
|
@@ -1233,6 +1330,7 @@ class SessionsService extends EventEmitter {
|
|
|
1233
1330
|
sessionId: reader.sessionId,
|
|
1234
1331
|
parentSessionId: reader.parentSessionId,
|
|
1235
1332
|
filePath: reader.filePath,
|
|
1333
|
+
parentFilePath: reader.parentFilePath,
|
|
1236
1334
|
fileSize: stat.size,
|
|
1237
1335
|
mtime: stat.mtimeMs,
|
|
1238
1336
|
range,
|
|
@@ -1255,10 +1353,10 @@ class SessionsService extends EventEmitter {
|
|
|
1255
1353
|
if (globalSeen.has(id)) (overlap ||= /* @__PURE__ */ new Set()).add(id);
|
|
1256
1354
|
globalSeen.add(id);
|
|
1257
1355
|
}
|
|
1258
|
-
if (overlap) rescans.set(m.
|
|
1356
|
+
if (overlap) rescans.set(m.filePath, this._scanSession(new SessionFile(m.filePath), { range, excludeIds: overlap }));
|
|
1259
1357
|
}
|
|
1260
1358
|
const sessions = await Promise.all(metas.map(async (m) => {
|
|
1261
|
-
const rescanned = await rescans.get(m.
|
|
1359
|
+
const rescanned = await rescans.get(m.filePath);
|
|
1262
1360
|
return stripInternal(rescanned ? { ...m, ...rescanned } : m);
|
|
1263
1361
|
}));
|
|
1264
1362
|
const labels = suffixLabels(sessions.map((m) => m.project));
|
|
@@ -1347,303 +1445,88 @@ class AgentRunner {
|
|
|
1347
1445
|
return { code: 0 };
|
|
1348
1446
|
}
|
|
1349
1447
|
}
|
|
1350
|
-
const
|
|
1351
|
-
class
|
|
1352
|
-
|
|
1353
|
-
|
|
1448
|
+
const mtimeOf = (path2) => fs.statSync(path2, { throwIfNoEntry: false })?.mtimeMs;
|
|
1449
|
+
class ClaudeSettings {
|
|
1450
|
+
constructor() {
|
|
1451
|
+
this.cfg = {};
|
|
1452
|
+
this.mtime = mtimeOf(CLAUDE_SETTINGS);
|
|
1453
|
+
if (this.mtime !== void 0) {
|
|
1454
|
+
try {
|
|
1455
|
+
this.cfg = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, "utf8"));
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
console.warn(`Failed to read ${CLAUDE_SETTINGS}: ${err.message}`);
|
|
1458
|
+
this.loadFailed = true;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1354
1461
|
}
|
|
1355
|
-
//
|
|
1356
|
-
|
|
1357
|
-
|
|
1462
|
+
// Removes every hook (any event) whose command matches `pattern`, dropping groups and events
|
|
1463
|
+
// left empty. Returns the removed commands.
|
|
1464
|
+
removeHooks(pattern) {
|
|
1465
|
+
const matches = (h) => pattern.test(h.command ?? "");
|
|
1466
|
+
const removed = [];
|
|
1467
|
+
for (const [event, groups] of Object.entries(this.cfg.hooks ?? {})) {
|
|
1468
|
+
const hit = groups.flatMap((g) => (g.hooks ?? []).filter(matches).map((h) => h.command));
|
|
1469
|
+
if (!hit.length) continue;
|
|
1470
|
+
removed.push(...hit);
|
|
1471
|
+
for (const g of groups) g.hooks = (g.hooks ?? []).filter((h) => !matches(h));
|
|
1472
|
+
this.cfg.hooks[event] = groups.filter((g) => g.hooks.length);
|
|
1473
|
+
if (!this.cfg.hooks[event].length) delete this.cfg.hooks[event];
|
|
1474
|
+
}
|
|
1475
|
+
return removed;
|
|
1358
1476
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1477
|
+
// Days Claude Code keeps transcripts before auto-deleting them; undefined when unset (its default is 30).
|
|
1478
|
+
get cleanupPeriodDays() {
|
|
1479
|
+
return this.cfg.cleanupPeriodDays;
|
|
1361
1480
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
title: "Select Claude directory",
|
|
1366
|
-
defaultPath: CLAUDE_DIR,
|
|
1367
|
-
properties: ["openDirectory"]
|
|
1368
|
-
});
|
|
1369
|
-
if (!r.canceled) {
|
|
1370
|
-
switchToClaudeDir(r.filePaths[0]);
|
|
1481
|
+
set cleanupPeriodDays(days) {
|
|
1482
|
+
if (days === void 0) delete this.cfg.cleanupPeriodDays;
|
|
1483
|
+
else this.cfg.cleanupPeriodDays = days;
|
|
1371
1484
|
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1485
|
+
// Claude Code's status line config: { type: 'command', command }; undefined when unset.
|
|
1486
|
+
get statusLine() {
|
|
1487
|
+
return this.cfg.statusLine;
|
|
1488
|
+
}
|
|
1489
|
+
set statusLine(value) {
|
|
1490
|
+
if (value === void 0) delete this.cfg.statusLine;
|
|
1491
|
+
else this.cfg.statusLine = value;
|
|
1492
|
+
}
|
|
1493
|
+
// Environment variables Claude Code applies to every session; undefined when unset.
|
|
1494
|
+
get env() {
|
|
1495
|
+
return this.cfg.env;
|
|
1496
|
+
}
|
|
1497
|
+
setEnv(name, value) {
|
|
1498
|
+
this.cfg.env ??= {};
|
|
1499
|
+
this.cfg.env[name] = value;
|
|
1500
|
+
}
|
|
1501
|
+
deleteEnv(name) {
|
|
1502
|
+
delete this.cfg.env?.[name];
|
|
1503
|
+
}
|
|
1504
|
+
save() {
|
|
1505
|
+
if (this.loadFailed) throw new Error(`Refusing to save ${CLAUDE_SETTINGS}: settings did not load cleanly`);
|
|
1506
|
+
if (mtimeOf(CLAUDE_SETTINGS) !== this.mtime) throw new Error(`Refusing to save ${CLAUDE_SETTINGS}: file changed on disk since load`);
|
|
1507
|
+
fs.mkdirSync(dirname(CLAUDE_SETTINGS), { recursive: true });
|
|
1508
|
+
sync(CLAUDE_SETTINGS, JSON.stringify(this.cfg, null, 2));
|
|
1383
1509
|
}
|
|
1384
1510
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
{
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
click: () => switchToClaudeDir(p)
|
|
1399
|
-
})),
|
|
1400
|
-
{ type: "separator" },
|
|
1401
|
-
{ role: "quit" }
|
|
1402
|
-
]
|
|
1403
|
-
},
|
|
1404
|
-
{
|
|
1405
|
-
// @macOS Cmd+C/V/A have no key equivalents without an Edit menu; Windows/Linux get these from Chromium
|
|
1406
|
-
label: "Edit",
|
|
1407
|
-
submenu: [
|
|
1408
|
-
{ role: "copy" },
|
|
1409
|
-
{ role: "paste" },
|
|
1410
|
-
{ role: "selectAll" },
|
|
1411
|
-
{ label: "Find…", accelerator: "CmdOrCtrl+F", click: () => onFind?.() },
|
|
1412
|
-
{ label: "Deselect", accelerator: "Escape", click: (_i, win) => onEscape?.(win) }
|
|
1413
|
-
]
|
|
1414
|
-
},
|
|
1415
|
-
{
|
|
1416
|
-
label: "View",
|
|
1417
|
-
submenu: [
|
|
1418
|
-
{ role: "reload" },
|
|
1419
|
-
{ role: "forceReload" },
|
|
1420
|
-
{ role: "toggleDevTools" }
|
|
1421
|
-
]
|
|
1422
|
-
}
|
|
1423
|
-
]);
|
|
1511
|
+
class LoginService {
|
|
1512
|
+
#service;
|
|
1513
|
+
constructor({ name, command }) {
|
|
1514
|
+
if (process.platform === "win32") this.#service = new TaskScheduler({ name, command });
|
|
1515
|
+
else if (process.platform === "darwin") this.#service = new LaunchAgent({ name, command });
|
|
1516
|
+
else this.#service = new SystemdUnit({ name, command });
|
|
1517
|
+
}
|
|
1518
|
+
install() {
|
|
1519
|
+
return this.#service.install();
|
|
1520
|
+
}
|
|
1521
|
+
uninstall() {
|
|
1522
|
+
return this.#service.uninstall();
|
|
1523
|
+
}
|
|
1424
1524
|
}
|
|
1425
|
-
|
|
1426
|
-
async function openLinkSafely(href, baseFile) {
|
|
1427
|
-
if (/^(https?:|mailto:)/i.test(href)) return shell.openExternal(href);
|
|
1428
|
-
if (!baseFile) return;
|
|
1429
|
-
let decoded;
|
|
1525
|
+
async function exec(file, args) {
|
|
1430
1526
|
try {
|
|
1431
|
-
|
|
1432
|
-
} catch {
|
|
1433
|
-
|
|
1434
|
-
}
|
|
1435
|
-
const baseDir = path.dirname(path.resolve(baseFile));
|
|
1436
|
-
const abs = path.resolve(baseDir, decoded);
|
|
1437
|
-
const rel = path.relative(baseDir, abs);
|
|
1438
|
-
if (rel.startsWith("..") || path.isAbsolute(rel)) return;
|
|
1439
|
-
if (!VIEWABLE_EXT.has(path.extname(abs).toLowerCase())) return shell.showItemInFolder(abs);
|
|
1440
|
-
const err = await shell.openPath(abs);
|
|
1441
|
-
if (err) shell.showItemInFolder(abs);
|
|
1442
|
-
return err;
|
|
1443
|
-
}
|
|
1444
|
-
const DEV_ORIGIN = process.env.ELECTRON_RENDERER_URL && new URL(process.env.ELECTRON_RENDERER_URL).origin;
|
|
1445
|
-
const APP_PAGES = pathToFileURL(path.join(import.meta.dirname, "../renderer/")).href;
|
|
1446
|
-
function lockNavigation(contents) {
|
|
1447
|
-
const toBrowser = (url) => /^(https?|mailto):/i.test(url) && shell.openExternal(url);
|
|
1448
|
-
const block = (e, url) => {
|
|
1449
|
-
const { href, origin } = new URL(url);
|
|
1450
|
-
if (href.startsWith(APP_PAGES) || origin === DEV_ORIGIN) return;
|
|
1451
|
-
e.preventDefault();
|
|
1452
|
-
toBrowser(url);
|
|
1453
|
-
};
|
|
1454
|
-
contents.setWindowOpenHandler(({ url }) => {
|
|
1455
|
-
toBrowser(url);
|
|
1456
|
-
return { action: "deny" };
|
|
1457
|
-
});
|
|
1458
|
-
contents.on("will-navigate", block);
|
|
1459
|
-
contents.on("will-redirect", block);
|
|
1460
|
-
}
|
|
1461
|
-
const __dirname$2 = path.dirname(fileURLToPath(import.meta.url));
|
|
1462
|
-
const DEV_URL$1 = process.env.ELECTRON_RENDERER_URL;
|
|
1463
|
-
const WIDTH = 320, HEIGHT = 56, MARGIN = 6;
|
|
1464
|
-
class FindBar {
|
|
1465
|
-
constructor(win) {
|
|
1466
|
-
this.win = win;
|
|
1467
|
-
this.view = new WebContentsView({
|
|
1468
|
-
webPreferences: {
|
|
1469
|
-
preload: path.join(__dirname$2, "../preload/findPreload.cjs"),
|
|
1470
|
-
contextIsolation: true,
|
|
1471
|
-
nodeIntegration: false,
|
|
1472
|
-
sandbox: true
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
this.view.setBackgroundColor("#00000000");
|
|
1476
|
-
lockNavigation(this.view.webContents);
|
|
1477
|
-
win.contentView.addChildView(this.view);
|
|
1478
|
-
this.view.setVisible(false);
|
|
1479
|
-
if (DEV_URL$1) this.view.webContents.loadURL(`${DEV_URL$1}/find/find.html`);
|
|
1480
|
-
else this.view.webContents.loadFile(path.join(__dirname$2, "../renderer/find/find.html"));
|
|
1481
|
-
win.webContents.on("found-in-page", (_e, r) => {
|
|
1482
|
-
return this.view.webContents.send("find:result", { active: r.activeMatchOrdinal, total: r.matches, id: r.requestId });
|
|
1483
|
-
});
|
|
1484
|
-
win.on("resize", () => {
|
|
1485
|
-
if (this.visible) this.layout();
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
get visible() {
|
|
1489
|
-
return this.view.getVisible();
|
|
1490
|
-
}
|
|
1491
|
-
layout() {
|
|
1492
|
-
const [w] = this.win.getContentSize();
|
|
1493
|
-
this.view.setBounds({ x: Math.max(0, w - WIDTH - MARGIN), y: MARGIN, width: WIDTH, height: HEIGHT });
|
|
1494
|
-
}
|
|
1495
|
-
query(text, options) {
|
|
1496
|
-
this.win.webContents.findInPage(text, options);
|
|
1497
|
-
}
|
|
1498
|
-
stop() {
|
|
1499
|
-
this.win.webContents.stopFindInPage("clearSelection");
|
|
1500
|
-
}
|
|
1501
|
-
show() {
|
|
1502
|
-
this.layout();
|
|
1503
|
-
this.view.setVisible(true);
|
|
1504
|
-
this.view.webContents.focus();
|
|
1505
|
-
this.view.webContents.send("find:open");
|
|
1506
|
-
this.win.webContents.send("find:active", true);
|
|
1507
|
-
}
|
|
1508
|
-
hide() {
|
|
1509
|
-
if (!this.visible) return;
|
|
1510
|
-
this.view.setVisible(false);
|
|
1511
|
-
this.stop();
|
|
1512
|
-
this.win.webContents.focus();
|
|
1513
|
-
this.win.webContents.send("find:active", false);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
1517
|
-
const DEV_URL = process.env.ELECTRON_RENDERER_URL;
|
|
1518
|
-
class MainWindow {
|
|
1519
|
-
constructor() {
|
|
1520
|
-
this.win = null;
|
|
1521
|
-
}
|
|
1522
|
-
create() {
|
|
1523
|
-
Menu.setApplicationMenu(buildAppMenu({
|
|
1524
|
-
onFind: () => this.findBar?.show(),
|
|
1525
|
-
onEscape: (win) => this.findBar?.visible ? this.findBar.hide() : win?.webContents.unselect()
|
|
1526
|
-
}));
|
|
1527
|
-
const state = windowStateKeeper({ defaultWidth: 1500, defaultHeight: 900 });
|
|
1528
|
-
this.win = new BrowserWindow({
|
|
1529
|
-
x: state.x,
|
|
1530
|
-
y: state.y,
|
|
1531
|
-
width: state.width,
|
|
1532
|
-
height: state.height,
|
|
1533
|
-
backgroundColor: "#0b0d12",
|
|
1534
|
-
autoHideMenuBar: true,
|
|
1535
|
-
title: `Claude Discover v${app.getVersion()}`,
|
|
1536
|
-
webPreferences: {
|
|
1537
|
-
preload: path.join(__dirname$1, "../preload/preload.cjs"),
|
|
1538
|
-
contextIsolation: true,
|
|
1539
|
-
nodeIntegration: false,
|
|
1540
|
-
sandbox: true
|
|
1541
|
-
}
|
|
1542
|
-
});
|
|
1543
|
-
state.manage(this.win);
|
|
1544
|
-
this.win.on("page-title-updated", (e) => e.preventDefault());
|
|
1545
|
-
lockNavigation(this.win.webContents);
|
|
1546
|
-
contextMenu({ window: this.win, showSelectAll: true, showCopyImage: true, showCopyLink: true, showInspectElement: true });
|
|
1547
|
-
if (DEV_URL) {
|
|
1548
|
-
this.win.loadURL(DEV_URL);
|
|
1549
|
-
} else {
|
|
1550
|
-
this.win.loadFile(path.join(__dirname$1, "../renderer/index.html"));
|
|
1551
|
-
}
|
|
1552
|
-
this.findBar = new FindBar(this.win);
|
|
1553
|
-
return this.win;
|
|
1554
|
-
}
|
|
1555
|
-
send(channel, payload) {
|
|
1556
|
-
if (this.win && !this.win.isDestroyed()) this.win.webContents.send(channel, payload);
|
|
1557
|
-
}
|
|
1558
|
-
focus() {
|
|
1559
|
-
if (!this.win || this.win.isDestroyed()) return;
|
|
1560
|
-
if (this.win.isMinimized()) this.win.restore();
|
|
1561
|
-
this.win.show();
|
|
1562
|
-
this.win.focus();
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
const mtimeOf = (path2) => fs.statSync(path2, { throwIfNoEntry: false })?.mtimeMs;
|
|
1566
|
-
class ClaudeSettings {
|
|
1567
|
-
constructor() {
|
|
1568
|
-
this.cfg = {};
|
|
1569
|
-
this.mtime = mtimeOf(CLAUDE_SETTINGS);
|
|
1570
|
-
if (this.mtime !== void 0) {
|
|
1571
|
-
try {
|
|
1572
|
-
this.cfg = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, "utf8"));
|
|
1573
|
-
} catch (err) {
|
|
1574
|
-
console.warn(`Failed to read ${CLAUDE_SETTINGS}: ${err.message}`);
|
|
1575
|
-
this.loadFailed = true;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
// Removes every hook (any event) whose command matches `pattern`, dropping groups and events
|
|
1580
|
-
// left empty. Returns the removed commands.
|
|
1581
|
-
removeHooks(pattern) {
|
|
1582
|
-
const matches = (h) => pattern.test(h.command ?? "");
|
|
1583
|
-
const removed = [];
|
|
1584
|
-
for (const [event, groups] of Object.entries(this.cfg.hooks ?? {})) {
|
|
1585
|
-
const hit = groups.flatMap((g) => (g.hooks ?? []).filter(matches).map((h) => h.command));
|
|
1586
|
-
if (!hit.length) continue;
|
|
1587
|
-
removed.push(...hit);
|
|
1588
|
-
for (const g of groups) g.hooks = (g.hooks ?? []).filter((h) => !matches(h));
|
|
1589
|
-
this.cfg.hooks[event] = groups.filter((g) => g.hooks.length);
|
|
1590
|
-
if (!this.cfg.hooks[event].length) delete this.cfg.hooks[event];
|
|
1591
|
-
}
|
|
1592
|
-
return removed;
|
|
1593
|
-
}
|
|
1594
|
-
// Days Claude Code keeps transcripts before auto-deleting them; undefined when unset (its default is 30).
|
|
1595
|
-
get cleanupPeriodDays() {
|
|
1596
|
-
return this.cfg.cleanupPeriodDays;
|
|
1597
|
-
}
|
|
1598
|
-
set cleanupPeriodDays(days) {
|
|
1599
|
-
if (days === void 0) delete this.cfg.cleanupPeriodDays;
|
|
1600
|
-
else this.cfg.cleanupPeriodDays = days;
|
|
1601
|
-
}
|
|
1602
|
-
// Claude Code's status line config: { type: 'command', command }; undefined when unset.
|
|
1603
|
-
get statusLine() {
|
|
1604
|
-
return this.cfg.statusLine;
|
|
1605
|
-
}
|
|
1606
|
-
set statusLine(value) {
|
|
1607
|
-
if (value === void 0) delete this.cfg.statusLine;
|
|
1608
|
-
else this.cfg.statusLine = value;
|
|
1609
|
-
}
|
|
1610
|
-
// Environment variables Claude Code applies to every session; undefined when unset.
|
|
1611
|
-
get env() {
|
|
1612
|
-
return this.cfg.env;
|
|
1613
|
-
}
|
|
1614
|
-
setEnv(name, value) {
|
|
1615
|
-
this.cfg.env ??= {};
|
|
1616
|
-
this.cfg.env[name] = value;
|
|
1617
|
-
}
|
|
1618
|
-
deleteEnv(name) {
|
|
1619
|
-
delete this.cfg.env?.[name];
|
|
1620
|
-
}
|
|
1621
|
-
save() {
|
|
1622
|
-
if (this.loadFailed) throw new Error(`Refusing to save ${CLAUDE_SETTINGS}: settings did not load cleanly`);
|
|
1623
|
-
if (mtimeOf(CLAUDE_SETTINGS) !== this.mtime) throw new Error(`Refusing to save ${CLAUDE_SETTINGS}: file changed on disk since load`);
|
|
1624
|
-
fs.mkdirSync(dirname(CLAUDE_SETTINGS), { recursive: true });
|
|
1625
|
-
sync(CLAUDE_SETTINGS, JSON.stringify(this.cfg, null, 2));
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
class LoginService {
|
|
1629
|
-
#service;
|
|
1630
|
-
constructor({ name, command }) {
|
|
1631
|
-
if (process.platform === "win32") this.#service = new TaskScheduler({ name, command });
|
|
1632
|
-
else if (process.platform === "darwin") this.#service = new LaunchAgent({ name, command });
|
|
1633
|
-
else this.#service = new SystemdUnit({ name, command });
|
|
1634
|
-
}
|
|
1635
|
-
install() {
|
|
1636
|
-
return this.#service.install();
|
|
1637
|
-
}
|
|
1638
|
-
uninstall() {
|
|
1639
|
-
return this.#service.uninstall();
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
async function exec(file, args) {
|
|
1643
|
-
try {
|
|
1644
|
-
return await promisify(execFile)(file, args, { windowsHide: true });
|
|
1645
|
-
} catch (err) {
|
|
1646
|
-
throw new Error(err.stderr?.trim() || err.message);
|
|
1527
|
+
return await promisify(execFile)(file, args, { windowsHide: true });
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
throw new Error(err.stderr?.trim() || err.message);
|
|
1647
1530
|
}
|
|
1648
1531
|
}
|
|
1649
1532
|
class TaskScheduler {
|
|
@@ -1884,22 +1767,51 @@ class RetentionSwitch {
|
|
|
1884
1767
|
}
|
|
1885
1768
|
}
|
|
1886
1769
|
}
|
|
1770
|
+
class ClaudeDirSwitch {
|
|
1771
|
+
constructor({ restart }) {
|
|
1772
|
+
this.restart = restart;
|
|
1773
|
+
}
|
|
1774
|
+
status() {
|
|
1775
|
+
return { dir: CLAUDE_DIR };
|
|
1776
|
+
}
|
|
1777
|
+
// Switch to `claudeDir`, or browse for one when none is given; the app restarts on it
|
|
1778
|
+
async activate(claudeDir) {
|
|
1779
|
+
claudeDir ??= await browseForClaudeDir();
|
|
1780
|
+
if (!claudeDir || claudeDir === CLAUDE_DIR) return;
|
|
1781
|
+
config.save({ claudeDir, recents: [.../* @__PURE__ */ new Set([...RECENT_CLAUDE_DIRS, claudeDir])] });
|
|
1782
|
+
this.restart();
|
|
1783
|
+
}
|
|
1784
|
+
deactivate() {
|
|
1785
|
+
throw new Error("Nothing to deactivate");
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async function browseForClaudeDir() {
|
|
1789
|
+
const r = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
|
|
1790
|
+
title: "Select Claude directory",
|
|
1791
|
+
defaultPath: CLAUDE_DIR,
|
|
1792
|
+
properties: ["openDirectory"]
|
|
1793
|
+
});
|
|
1794
|
+
return r.canceled ? null : r.filePaths[0];
|
|
1795
|
+
}
|
|
1887
1796
|
class Switchers {
|
|
1888
|
-
#switches
|
|
1889
|
-
proxy: new ProxySwitch(),
|
|
1890
|
-
statusline: new StatuslineSwitch(),
|
|
1891
|
-
retention: new RetentionSwitch(),
|
|
1892
|
-
claudedir: new ClaudeDirSwitch()
|
|
1893
|
-
};
|
|
1797
|
+
#switches;
|
|
1894
1798
|
#keepActive = { proxy: true, statusline: true };
|
|
1895
1799
|
// toggle feature persisting on app quit — starts on, so a switch this run didn't activate is never undone by it
|
|
1800
|
+
constructor({ restart }) {
|
|
1801
|
+
this.#switches = {
|
|
1802
|
+
proxy: new ProxySwitch(),
|
|
1803
|
+
statusline: new StatuslineSwitch(),
|
|
1804
|
+
retention: new RetentionSwitch(),
|
|
1805
|
+
claudedir: new ClaudeDirSwitch({ restart })
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1896
1808
|
async status(name) {
|
|
1897
1809
|
const s = await this.#get(name).status();
|
|
1898
1810
|
return name in this.#keepActive ? { ...s, keepActive: this.#keepActive[name], ephemeral: IS_EPHEMERAL } : s;
|
|
1899
1811
|
}
|
|
1900
|
-
activate(name) {
|
|
1812
|
+
activate(name, ...args) {
|
|
1901
1813
|
return this.#attempt(name, async (s) => {
|
|
1902
|
-
await s.activate();
|
|
1814
|
+
await s.activate(...args);
|
|
1903
1815
|
this.#keepActive[name] &&= !IS_EPHEMERAL;
|
|
1904
1816
|
});
|
|
1905
1817
|
}
|
|
@@ -1931,95 +1843,260 @@ class Switchers {
|
|
|
1931
1843
|
}
|
|
1932
1844
|
}
|
|
1933
1845
|
}
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1846
|
+
const VIEWABLE_EXT = /* @__PURE__ */ new Set([".md", ".txt", ".json", ".jsonl", ".log", ".csv", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".pdf"]);
|
|
1847
|
+
async function openLinkSafely(href, baseFile) {
|
|
1848
|
+
if (/^(https?:|mailto:)/i.test(href)) return shell.openExternal(href);
|
|
1849
|
+
if (!baseFile) return;
|
|
1850
|
+
let decoded;
|
|
1851
|
+
try {
|
|
1852
|
+
decoded = decodeURIComponent(href);
|
|
1853
|
+
} catch {
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const baseDir = path.dirname(path.resolve(baseFile));
|
|
1857
|
+
const abs = path.resolve(baseDir, decoded);
|
|
1858
|
+
const rel = path.relative(baseDir, abs);
|
|
1859
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return;
|
|
1860
|
+
if (!VIEWABLE_EXT.has(path.extname(abs).toLowerCase())) return shell.showItemInFolder(abs);
|
|
1861
|
+
const err = await shell.openPath(abs);
|
|
1862
|
+
if (err) shell.showItemInFolder(abs);
|
|
1863
|
+
return err;
|
|
1864
|
+
}
|
|
1865
|
+
const DEV_ORIGIN = process.env.ELECTRON_RENDERER_URL && new URL(process.env.ELECTRON_RENDERER_URL).origin;
|
|
1866
|
+
const APP_PAGES = pathToFileURL(path.join(import.meta.dirname, "../renderer/")).href;
|
|
1867
|
+
function lockNavigation(contents) {
|
|
1868
|
+
const toBrowser = (url) => /^(https?|mailto):/i.test(url) && shell.openExternal(url);
|
|
1869
|
+
const block = (e, url) => {
|
|
1870
|
+
const { href, origin } = new URL(url);
|
|
1871
|
+
if (href.startsWith(APP_PAGES) || origin === DEV_ORIGIN) return;
|
|
1872
|
+
e.preventDefault();
|
|
1873
|
+
toBrowser(url);
|
|
1874
|
+
};
|
|
1875
|
+
contents.setWindowOpenHandler(({ url }) => {
|
|
1876
|
+
toBrowser(url);
|
|
1877
|
+
return { action: "deny" };
|
|
1878
|
+
});
|
|
1879
|
+
contents.on("will-navigate", block);
|
|
1880
|
+
contents.on("will-redirect", block);
|
|
1881
|
+
}
|
|
1882
|
+
const __dirname$2 = path.dirname(fileURLToPath(import.meta.url));
|
|
1883
|
+
const DEV_URL$1 = process.env.ELECTRON_RENDERER_URL;
|
|
1884
|
+
const WIDTH = 320, HEIGHT = 56, MARGIN = 6;
|
|
1885
|
+
class FindBar {
|
|
1886
|
+
constructor(win) {
|
|
1887
|
+
this.win = win;
|
|
1888
|
+
this.view = new WebContentsView({
|
|
1889
|
+
webPreferences: {
|
|
1890
|
+
preload: path.join(__dirname$2, "../preload/findPreload.cjs"),
|
|
1891
|
+
contextIsolation: true,
|
|
1892
|
+
nodeIntegration: false,
|
|
1893
|
+
sandbox: true
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
this.view.setBackgroundColor("#00000000");
|
|
1897
|
+
lockNavigation(this.view.webContents);
|
|
1898
|
+
win.contentView.addChildView(this.view);
|
|
1899
|
+
this.view.setVisible(false);
|
|
1900
|
+
if (DEV_URL$1) this.view.webContents.loadURL(`${DEV_URL$1}/find/find.html`);
|
|
1901
|
+
else this.view.webContents.loadFile(path.join(__dirname$2, "../renderer/find/find.html"));
|
|
1902
|
+
win.webContents.on("found-in-page", (_e, r) => {
|
|
1903
|
+
return this.view.webContents.send("find:result", { active: r.activeMatchOrdinal, total: r.matches, id: r.requestId });
|
|
1904
|
+
});
|
|
1905
|
+
win.on("resize", () => {
|
|
1906
|
+
if (this.visible) this.layout();
|
|
1947
1907
|
});
|
|
1948
|
-
this.#register();
|
|
1949
|
-
this.#pending = findTarget(process.argv);
|
|
1950
|
-
return true;
|
|
1951
1908
|
}
|
|
1952
|
-
|
|
1953
|
-
this
|
|
1954
|
-
const target = this.#pending;
|
|
1955
|
-
this.#pending = null;
|
|
1956
|
-
return target;
|
|
1909
|
+
get visible() {
|
|
1910
|
+
return this.view.getVisible();
|
|
1957
1911
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
if (!target) return;
|
|
1962
|
-
if (this.#live) this.emit("open", target);
|
|
1963
|
-
else this.#pending = target;
|
|
1912
|
+
layout() {
|
|
1913
|
+
const [w] = this.win.getContentSize();
|
|
1914
|
+
this.view.setBounds({ x: Math.max(0, w - WIDTH - MARGIN), y: MARGIN, width: WIDTH, height: HEIGHT });
|
|
1964
1915
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1916
|
+
query(text, options) {
|
|
1917
|
+
this.win.webContents.findInPage(text, options);
|
|
1918
|
+
}
|
|
1919
|
+
stop() {
|
|
1920
|
+
this.win.webContents.stopFindInPage("clearSelection");
|
|
1921
|
+
}
|
|
1922
|
+
show() {
|
|
1923
|
+
this.layout();
|
|
1924
|
+
this.view.setVisible(true);
|
|
1925
|
+
this.view.webContents.focus();
|
|
1926
|
+
this.view.webContents.send("find:open");
|
|
1927
|
+
this.win.webContents.send("find:active", true);
|
|
1928
|
+
}
|
|
1929
|
+
hide() {
|
|
1930
|
+
if (!this.visible) return;
|
|
1931
|
+
this.view.setVisible(false);
|
|
1932
|
+
this.stop();
|
|
1933
|
+
this.win.webContents.focus();
|
|
1934
|
+
this.win.webContents.send("find:active", false);
|
|
1970
1935
|
}
|
|
1971
1936
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1937
|
+
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
1938
|
+
const DEV_URL = process.env.ELECTRON_RENDERER_URL;
|
|
1939
|
+
class MainWindow {
|
|
1940
|
+
constructor() {
|
|
1941
|
+
this.win = null;
|
|
1942
|
+
}
|
|
1943
|
+
create() {
|
|
1944
|
+
const state = windowStateKeeper({ defaultWidth: 1500, defaultHeight: 900 });
|
|
1945
|
+
this.win = new BrowserWindow({
|
|
1946
|
+
x: state.x,
|
|
1947
|
+
y: state.y,
|
|
1948
|
+
width: state.width,
|
|
1949
|
+
height: state.height,
|
|
1950
|
+
backgroundColor: "#0b0d12",
|
|
1951
|
+
autoHideMenuBar: true,
|
|
1952
|
+
title: `Claude Discover v${app.getVersion()}`,
|
|
1953
|
+
webPreferences: {
|
|
1954
|
+
preload: path.join(__dirname$1, "../preload/preload.cjs"),
|
|
1955
|
+
contextIsolation: true,
|
|
1956
|
+
nodeIntegration: false,
|
|
1957
|
+
sandbox: true
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
state.manage(this.win);
|
|
1961
|
+
this.win.on("page-title-updated", (e) => e.preventDefault());
|
|
1962
|
+
lockNavigation(this.win.webContents);
|
|
1963
|
+
contextMenu({ window: this.win, showSelectAll: true, showCopyImage: true, showCopyLink: true, showInspectElement: true });
|
|
1964
|
+
if (DEV_URL) {
|
|
1965
|
+
this.win.loadURL(DEV_URL);
|
|
1966
|
+
} else {
|
|
1967
|
+
this.win.loadFile(path.join(__dirname$1, "../renderer/index.html"));
|
|
1968
|
+
}
|
|
1969
|
+
this.findBar = new FindBar(this.win);
|
|
1970
|
+
return this.win;
|
|
1971
|
+
}
|
|
1972
|
+
send(channel, payload) {
|
|
1973
|
+
if (this.win && !this.win.isDestroyed()) this.win.webContents.send(channel, payload);
|
|
1974
|
+
}
|
|
1975
|
+
focus() {
|
|
1976
|
+
if (!this.win || this.win.isDestroyed()) return;
|
|
1977
|
+
if (this.win.isMinimized()) this.win.restore();
|
|
1978
|
+
this.win.show();
|
|
1979
|
+
this.win.focus();
|
|
1980
|
+
}
|
|
1976
1981
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
win
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1982
|
+
class Application {
|
|
1983
|
+
constructor({ deepLink }) {
|
|
1984
|
+
this.deepLink = deepLink;
|
|
1985
|
+
this.win = new MainWindow();
|
|
1986
|
+
this.agentRunner = new AgentRunner();
|
|
1987
|
+
this.workHours = new WorkHours();
|
|
1988
|
+
this.sessionsService = new SessionsService();
|
|
1989
|
+
this.switchers = new Switchers({ restart: () => this.restart() });
|
|
1990
|
+
}
|
|
1991
|
+
start() {
|
|
1992
|
+
const { deepLink, win, agentRunner, workHours, sessionsService, switchers } = this;
|
|
1993
|
+
Menu.setApplicationMenu(this.#buildMenu());
|
|
1994
|
+
sessionsService.on("update", (sessions) => win.send("sessions:update", sessions));
|
|
1995
|
+
sessionsService.on("progress", (p) => win.send("sessions:scan-progress", p));
|
|
1996
|
+
deepLink.on("open", (target) => {
|
|
1997
|
+
win.focus();
|
|
1998
|
+
win.send("deeplink:open-session", target);
|
|
1999
|
+
});
|
|
2000
|
+
ipcMain.handle("sessions:list", (_e, date, granularity) => sessionsService.list(date, granularity));
|
|
2001
|
+
ipcMain.handle("sessions:read", (_e, filePath, date, granularity) => sessionsService.readSession(filePath, date, granularity));
|
|
2002
|
+
ipcMain.handle("sessions:read-requests", (_e, filePath, date, granularity) => sessionsService.readRequests(filePath, date, granularity));
|
|
2003
|
+
ipcMain.handle("switch:status", (_e, name) => switchers.status(name));
|
|
2004
|
+
ipcMain.handle("switch:activate", (_e, name) => switchers.activate(name));
|
|
2005
|
+
ipcMain.handle("switch:deactivate", (_e, name) => switchers.deactivate(name));
|
|
2006
|
+
ipcMain.handle("switch:keep-active", (_e, name, value) => switchers.setKeepActive(name, value));
|
|
2007
|
+
ipcMain.handle("work-hours:get", () => workHours.read());
|
|
2008
|
+
ipcMain.handle("work-hours:set", (_e, data) => workHours.write(data));
|
|
2009
|
+
ipcMain.handle("agent:run", (e, text, systemTools, cache) => agentRunner.run(text, e.sender, systemTools, cache));
|
|
2010
|
+
ipcMain.handle("shell:open-link", (_e, href, baseFile) => openLinkSafely(href, baseFile));
|
|
2011
|
+
ipcMain.handle("deeplink:take-pending", () => deepLink.takePending());
|
|
2012
|
+
ipcMain.on("find:query", (_e, text, options) => win.findBar?.query(text, options));
|
|
2013
|
+
ipcMain.on("find:stop", () => win.findBar?.stop());
|
|
2014
|
+
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
2015
|
+
ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
|
|
2016
|
+
app.on("activate", () => {
|
|
2017
|
+
if (BrowserWindow.getAllWindows().length === 0) win.create();
|
|
2018
|
+
});
|
|
2019
|
+
app.on("window-all-closed", () => {
|
|
2020
|
+
if (process.platform !== "darwin") app.quit();
|
|
2021
|
+
});
|
|
2022
|
+
app.on("will-quit", async (e) => {
|
|
2023
|
+
e.preventDefault();
|
|
2024
|
+
await switchers.deactivateOnQuit();
|
|
2025
|
+
app.exit();
|
|
2026
|
+
});
|
|
2027
|
+
sessionsService.start();
|
|
2028
|
+
win.create();
|
|
2029
|
+
}
|
|
2030
|
+
// app.exit skips will-quit, so switches stay active across the restart
|
|
2031
|
+
restart() {
|
|
2032
|
+
{
|
|
2033
|
+
app.relaunch();
|
|
2034
|
+
}
|
|
2035
|
+
BrowserWindow.getAllWindows().forEach((w) => w.close());
|
|
2036
|
+
app.exit(0);
|
|
2037
|
+
}
|
|
2038
|
+
#buildMenu() {
|
|
2039
|
+
const { win, switchers } = this;
|
|
2040
|
+
const changeDir = (dir) => switchers.activate("claudedir", dir);
|
|
2041
|
+
return Menu.buildFromTemplate([
|
|
2042
|
+
...process.platform === "darwin" ? [{ role: "appMenu" }] : [],
|
|
2043
|
+
// @macOS
|
|
2044
|
+
{
|
|
2045
|
+
label: "File",
|
|
2046
|
+
submenu: [
|
|
2047
|
+
{ label: "Change directory…", click: () => changeDir() },
|
|
2048
|
+
{ type: "separator" },
|
|
2049
|
+
...RECENT_CLAUDE_DIRS.map((p) => ({
|
|
2050
|
+
label: p,
|
|
2051
|
+
type: "checkbox",
|
|
2052
|
+
checked: p === CLAUDE_DIR,
|
|
2053
|
+
click: () => changeDir(p)
|
|
2054
|
+
})),
|
|
2055
|
+
{ type: "separator" },
|
|
2056
|
+
{ role: "quit" }
|
|
2057
|
+
]
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
// @macOS Cmd+C/V/A have no key equivalents without an Edit menu; Windows/Linux get these from Chromium
|
|
2061
|
+
label: "Edit",
|
|
2062
|
+
submenu: [
|
|
2063
|
+
{ role: "copy" },
|
|
2064
|
+
{ role: "paste" },
|
|
2065
|
+
{ role: "selectAll" },
|
|
2066
|
+
{ label: "Find…", accelerator: "CmdOrCtrl+F", click: () => win.findBar?.show() },
|
|
2067
|
+
{ label: "Deselect", accelerator: "Escape", click: (_i, w) => win.findBar?.visible ? win.findBar.hide() : w?.webContents.unselect() }
|
|
2068
|
+
]
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
label: "View",
|
|
2072
|
+
submenu: [
|
|
2073
|
+
{ role: "reload" },
|
|
2074
|
+
{ role: "forceReload" },
|
|
2075
|
+
{ role: "toggleDevTools" }
|
|
2076
|
+
]
|
|
2077
|
+
}
|
|
2078
|
+
]);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const isFirstInstance = app.requestSingleInstanceLock();
|
|
2082
|
+
if (isFirstInstance) {
|
|
2083
|
+
let application;
|
|
2084
|
+
const deepLink = new DeepLink();
|
|
2085
|
+
deepLink.activate();
|
|
2086
|
+
app.whenReady().then(() => {
|
|
2087
|
+
application = new Application({ deepLink });
|
|
2088
|
+
application.start();
|
|
1993
2089
|
});
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
ipcMain.on("find:stop", () => win.findBar?.stop());
|
|
2004
|
-
ipcMain.on("find:close", () => win.findBar?.hide());
|
|
2005
|
-
ipcMain.on("claude-settings:get", (e) => e.returnValue = { claudeDir: CLAUDE_DIR });
|
|
2006
|
-
const switchers = new Switchers();
|
|
2007
|
-
app.on("will-quit", async (e) => {
|
|
2008
|
-
e.preventDefault();
|
|
2009
|
-
await switchers.deactivateOnQuit();
|
|
2010
|
-
app.exit();
|
|
2090
|
+
app.on("second-instance", (_e, argv) => {
|
|
2091
|
+
const target = findTarget(argv);
|
|
2092
|
+
if (argv.includes("--restart")) {
|
|
2093
|
+
application?.restart();
|
|
2094
|
+
} else if (target) {
|
|
2095
|
+
deepLink.open(target);
|
|
2096
|
+
} else {
|
|
2097
|
+
application?.win.focus();
|
|
2098
|
+
}
|
|
2011
2099
|
});
|
|
2012
|
-
ipcMain.handle("switch:status", (_e, name) => switchers.status(name));
|
|
2013
|
-
ipcMain.handle("switch:activate", (_e, name) => switchers.activate(name));
|
|
2014
|
-
ipcMain.handle("switch:deactivate", (_e, name) => switchers.deactivate(name));
|
|
2015
|
-
ipcMain.handle("switch:keep-active", (_e, name, value) => switchers.setKeepActive(name, value));
|
|
2016
|
-
}
|
|
2017
|
-
app.on("window-all-closed", () => {
|
|
2018
|
-
if (process.platform !== "darwin") app.quit();
|
|
2019
|
-
});
|
|
2020
|
-
const isFirstInstance = deepLink.requestLock();
|
|
2021
|
-
if (isFirstInstance) {
|
|
2022
|
-
app.whenReady().then(start);
|
|
2023
2100
|
} else {
|
|
2024
2101
|
app.exit(0);
|
|
2025
2102
|
}
|