ccdeck 3.36.9 → 3.37.1
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 +15 -7
- package/bin/agent-dag.js +19 -6
- package/bin/cli/help.js +1 -0
- package/bin/cli/login-item.js +44 -20
- package/bin/cli/one-shot.js +15 -5
- package/bin/cli/screen.js +10 -5
- package/bin/cli/startup.js +6 -2
- package/bin/cli/uninstall.js +41 -5
- package/bin/deck.js +50 -8
- package/dist/web/assets/BrowserWatchModal-njN0zg93.js +1 -0
- package/dist/web/assets/UsageHistoryModal-DYSZZZEz.js +1 -0
- package/dist/web/assets/index-B0SOa-XB.css +1 -0
- package/dist/web/assets/index-DDxnWaTa.js +306 -0
- package/dist/web/index.html +2 -2
- package/hook/hook.js +80 -9
- package/package.json +1 -1
- package/release-notes.json +8 -1
- package/src/server/account-origins.mjs +8 -1
- package/src/server/account-projects.mjs +34 -9
- package/src/server/activation.mjs +39 -10
- package/src/server/app-host.mjs +7 -2
- package/src/server/args.mjs +3 -0
- package/src/server/browser-watch-routes.mjs +23 -16
- package/src/server/browser-watch.mjs +9 -3
- package/src/server/claude-accounts.mjs +27 -7
- package/src/server/claude-verdicts.mjs +12 -1
- package/src/server/cli-versions.mjs +6 -4
- package/src/server/codex-auth.mjs +29 -0
- package/src/server/codex-quota.mjs +57 -7
- package/src/server/deck-home.mjs +23 -1
- package/src/server/deck-prefs.mjs +70 -7
- package/src/server/depth-facts.mjs +178 -0
- package/src/server/detach.mjs +61 -33
- package/src/server/event-log.mjs +15 -0
- package/src/server/event-pipeline.mjs +5 -2
- package/src/server/event-routes.mjs +22 -0
- package/src/server/feedback-form.mjs +36 -13
- package/src/server/index.mjs +6 -2
- package/src/server/install-layout.mjs +35 -0
- package/src/server/install-ref.mjs +39 -0
- package/src/server/installer.mjs +20 -2
- package/src/server/lan-beacon.mjs +18 -3
- package/src/server/lan-deck.mjs +7 -1
- package/src/server/lan-engine.mjs +40 -21
- package/src/server/lan-manifest.mjs +9 -6
- package/src/server/lan-requests.mjs +18 -2
- package/src/server/lan-serve.mjs +4 -2
- package/src/server/lan-sync.mjs +97 -6
- package/src/server/lifecycle.mjs +18 -2
- package/src/server/log-replay.mjs +9 -3
- package/src/server/login-service.mjs +95 -12
- package/src/server/npm-upgrade.mjs +12 -4
- package/src/server/pinned-build.mjs +3 -0
- package/src/server/prefs-routes.mjs +5 -8
- package/src/server/prefs-state.mjs +9 -1
- package/src/server/quota-oauth.mjs +88 -13
- package/src/server/quota-shape.mjs +28 -0
- package/src/server/quota.mjs +68 -17
- package/src/server/rating.mjs +87 -0
- package/src/server/reports-routes.mjs +21 -2
- package/src/server/reports.mjs +672 -64
- package/src/server/ring-bounds.mjs +11 -0
- package/src/server/running-deck.mjs +48 -0
- package/src/server/self-update.mjs +1 -1
- package/src/server/session-enrichment.mjs +14 -6
- package/src/server/stable-node.mjs +68 -0
- package/src/server/static-serve.mjs +5 -0
- package/src/server/supervisor.mjs +21 -15
- package/src/server/tailscale.mjs +8 -4
- package/src/server/transcript-scan.mjs +7 -4
- package/src/server/usage-day.mjs +76 -11
- package/dist/web/assets/BrowserWatchModal-BCrDvuug.js +0 -1
- package/dist/web/assets/UsageHistoryModal-BtRCuo33.js +0 -1
- package/dist/web/assets/index-BkXTN4dZ.css +0 -1
- package/dist/web/assets/index-tVBMXvDv.js +0 -306
|
@@ -86,6 +86,17 @@ export function isEnrichment(raw) {
|
|
|
86
86
|
return raw != null && typeof raw === "object" && LAST_VALUE_WINS.has(raw.hook_event_name);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Whether an event name is one of the deck's own control markers — `__clear`,
|
|
91
|
+
* which handleClear sends, is the one in use — which only the server may send.
|
|
92
|
+
* Here beside isEnrichment for the same reason: the ingest that refuses one
|
|
93
|
+
* from a hook and the boot replay that honours one only from the server ask
|
|
94
|
+
* the same question.
|
|
95
|
+
*/
|
|
96
|
+
export function isReservedEventName(name) {
|
|
97
|
+
return typeof name === "string" && name.startsWith("__");
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
/**
|
|
90
101
|
* The ring's array-length backstop, distinct from its event budget.
|
|
91
102
|
*
|
|
@@ -58,10 +58,13 @@
|
|
|
58
58
|
// that imports two node builtins; index.mjs takes them from the same place and
|
|
59
59
|
// re-exports them, so there is still exactly one spelling in the package.
|
|
60
60
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
61
|
+
import { uptime } from "node:os";
|
|
61
62
|
import { join } from "node:path";
|
|
62
63
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
63
64
|
import { challengeDeck, isProcessAlive } from "./deck-probe.mjs";
|
|
64
65
|
import { sameCodexTree } from "./log-election.mjs";
|
|
66
|
+
import { run } from "./metrics-run.mjs";
|
|
67
|
+
import { elapsedSeconds } from "./process-list.mjs";
|
|
65
68
|
import { CRASH_WINDOW_MS } from "./supervisor.mjs";
|
|
66
69
|
|
|
67
70
|
/**
|
|
@@ -264,6 +267,16 @@ export async function liveDecks({
|
|
|
264
267
|
* recycled pid than a supervisor, and a signal sent there ends a stranger.
|
|
265
268
|
* The window is the supervisor's own, CRASH_WINDOW_MS.
|
|
266
269
|
*
|
|
270
|
+
* AND FRESH IS NOT ENOUGH ON ITS OWN. A deck that dies without its shutdown —
|
|
271
|
+
* a power cut, a hard reset, a console closed on Windows — leaves a record
|
|
272
|
+
* exactly as fresh, and after a quick reboot, or on a system that reuses pids
|
|
273
|
+
* quickly, its `parent` names whatever holds that number now. So the parent
|
|
274
|
+
* has to prove it is the supervisor by when it started: no later than the
|
|
275
|
+
* worker it supervises, whose record carries `startedAt`. A record stamped
|
|
276
|
+
* before this boot names no process that is running, and is never read. A
|
|
277
|
+
* parent whose start cannot be read is left alone; the cost is the behaviour
|
|
278
|
+
* before #1779, a `--stop` that misses a deck about to come back.
|
|
279
|
+
*
|
|
267
280
|
* One entry per supervisor, and none for a supervisor that already has a live
|
|
268
281
|
* worker registered: that deck is on the ordinary list, and stopping it ends
|
|
269
282
|
* the supervisor too. Never this process or its own supervisor. Marked
|
|
@@ -277,6 +290,8 @@ export async function restartingDecks({
|
|
|
277
290
|
alive = isProcessAlive,
|
|
278
291
|
now = Date.now(),
|
|
279
292
|
windowMs = CRASH_WINDOW_MS,
|
|
293
|
+
bootedAt = now - uptime() * 1000,
|
|
294
|
+
startedAt = processStartedAt,
|
|
280
295
|
} = {}) {
|
|
281
296
|
let names;
|
|
282
297
|
try { names = await fs.readdir(dir); } catch { return []; }
|
|
@@ -300,13 +315,46 @@ export async function restartingDecks({
|
|
|
300
315
|
if (up || !Number.isInteger(parent) || parent <= 0) continue;
|
|
301
316
|
if (parent === self || parent === selfParent || serving.has(parent)) continue;
|
|
302
317
|
if (!(Number.isFinite(mtime) && now - mtime >= 0 && now - mtime < windowMs)) continue;
|
|
318
|
+
if (mtime < bootedAt) continue;
|
|
303
319
|
if (!alive(parent)) continue;
|
|
320
|
+
const workerSince = Date.parse(d.startedAt ?? "");
|
|
321
|
+
if (!Number.isFinite(workerSince)) continue;
|
|
322
|
+
const parentSince = await startedAt(parent);
|
|
323
|
+
if (!Number.isFinite(parentSince) || parentSince > workerSince + START_SLACK_MS) continue;
|
|
304
324
|
serving.add(parent);
|
|
305
325
|
out.push({ ...d, restarting: true });
|
|
306
326
|
}
|
|
307
327
|
return out.sort((a, b) => a.port - b.port || a.pid - b.pid);
|
|
308
328
|
}
|
|
309
329
|
|
|
330
|
+
/** How much later than its worker a supervisor may seem to have started. `ps`
|
|
331
|
+
* reads elapsed time in whole seconds, so a start read that way can come out
|
|
332
|
+
* up to a second late, and the read itself takes a moment. */
|
|
333
|
+
const START_SLACK_MS = 2_000;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* When `pid` started, in ms since the epoch, or null when that cannot be read.
|
|
337
|
+
*
|
|
338
|
+
* `ps -o etime=` on both Unixes — the POSIX field, `[[DD-]HH:]MM:SS` — and
|
|
339
|
+
* Get-Process on Windows. Spawned only for a crashed worker's live parent,
|
|
340
|
+
* which `--stop` and `--status` meet a few times in a deck's life, so the cost
|
|
341
|
+
* of a child process is paid there and nowhere else. A pid that is gone, or
|
|
342
|
+
* that this account may not read, answers null.
|
|
343
|
+
*/
|
|
344
|
+
export async function processStartedAt(pid, { platform = process.platform, runFn = run, now = Date.now } = {}) {
|
|
345
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
346
|
+
if (platform === "win32") {
|
|
347
|
+
const out = await runFn("powershell.exe", [
|
|
348
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
349
|
+
`([DateTimeOffset](Get-Process -Id ${pid} -ErrorAction Stop).StartTime).ToUnixTimeMilliseconds()`,
|
|
350
|
+
], 6_000);
|
|
351
|
+
const ms = Number(String(out ?? "").trim());
|
|
352
|
+
return out && Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
353
|
+
}
|
|
354
|
+
const secs = elapsedSeconds(await runFn("ps", ["-o", "etime=", "-p", String(pid)], 2_000));
|
|
355
|
+
return secs == null ? null : now() - secs * 1000;
|
|
356
|
+
}
|
|
357
|
+
|
|
310
358
|
/**
|
|
311
359
|
* What to say about the version of the deck we attached to.
|
|
312
360
|
*
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
installedVersion, PUBLISHED_NAME, registryName, upgradeCommand, upgradeName,
|
|
33
33
|
} from "./install-layout.mjs";
|
|
34
34
|
export {
|
|
35
|
-
installedVersion, isNpxInstall, isGitCheckout, npxRoot, bareSpecName, npxSpecFromMeta,
|
|
35
|
+
installedVersion, isNpxInstall, isOneOffRun, oneOffRunner, isGitCheckout, npxRoot, bareSpecName, npxSpecFromMeta,
|
|
36
36
|
npxRestartSpec, ALIAS_PACKAGES, PUBLISHED_NAME, RETIRED_NAMES, currentName, installedName,
|
|
37
37
|
hostRoot, hostNameFromMeta, hostPackage, successorRoot, frozenNameInstall, upgradeName,
|
|
38
38
|
upgradeCommand, registryName,
|
|
@@ -58,7 +58,7 @@ export function transcriptTarget(payload) {
|
|
|
58
58
|
// agents created before the model was resolved. The cache is the emit
|
|
59
59
|
// filter, not a read filter: reading once per session meant a subagent
|
|
60
60
|
// model that only appears after the root is known was never picked up.
|
|
61
|
-
const modelBySession = new Map(); // sessionId -> { rootModel, subsSig }
|
|
61
|
+
const modelBySession = new Map(); // sessionId -> { rootModel, subsSig, subagentModels }
|
|
62
62
|
const MODEL_READ_THROTTLE_MS = 2500;
|
|
63
63
|
const modelReads = sessionReadGate(MODEL_READ_THROTTLE_MS);
|
|
64
64
|
|
|
@@ -71,10 +71,18 @@ export function cachedModelId(cached) {
|
|
|
71
71
|
return typeof rootModel === "string" && rootModel ? rootModel : null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/** The
|
|
75
|
-
* stamps on a payload that arrives without one.
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
/** The model already resolved for whoever this payload is about, or null —
|
|
75
|
+
* what pushEvent stamps on a payload that arrives without one. A payload that
|
|
76
|
+
* names a subagent (`agent_id`, or the legacy `parent_tool_use_id`) is that
|
|
77
|
+
* subagent's own traffic and gets its model from the transcript scan's map,
|
|
78
|
+
* or nothing: the root's model on it overwrote the subagent's on its card,
|
|
79
|
+
* and nothing ever sent the right one again. */
|
|
80
|
+
function knownModelId(sid, subagentKey = null) {
|
|
81
|
+
const cached = modelBySession.get(sid);
|
|
82
|
+
if (!subagentKey) return cachedModelId(cached);
|
|
83
|
+
const subs = cached?.subagentModels;
|
|
84
|
+
const model = subs && Object.hasOwn(subs, subagentKey) ? subs[subagentKey] : null;
|
|
85
|
+
return typeof model === "string" && model ? model : null;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/** Read the main session JSONL. Returns the root model and any
|
|
@@ -246,7 +254,7 @@ function maybeResolveModel(payload) {
|
|
|
246
254
|
const prev = modelBySession.get(sid);
|
|
247
255
|
const subsSig = JSON.stringify(subagentModels);
|
|
248
256
|
if (prev && prev.rootModel === rootModel && prev.subsSig === subsSig) return;
|
|
249
|
-
modelBySession.set(sid, { rootModel, subsSig });
|
|
257
|
+
modelBySession.set(sid, { rootModel, subsSig, subagentModels });
|
|
250
258
|
pushEvent({
|
|
251
259
|
hook_event_name: "ModelObserved",
|
|
252
260
|
session_id: sid,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The spelling of the running node that outlives an upgrade of the same install.
|
|
2
|
+
//
|
|
3
|
+
// Two things this deck writes name a node and are read long after it wrote
|
|
4
|
+
// them: the Claude Code hook command in settings.json, run on every tool call
|
|
5
|
+
// of every session, and the login item, run at every login. Both used
|
|
6
|
+
// `process.execPath`, and Node resolves symbolic links in it — on Linux it is
|
|
7
|
+
// /proc/self/exe, on macOS the realpath of the executable — so the path written
|
|
8
|
+
// is the one an upgrade deletes:
|
|
9
|
+
//
|
|
10
|
+
// Homebrew /opt/homebrew/Cellar/node/24.1.0/bin/node (`brew upgrade` removes the old keg)
|
|
11
|
+
// tarball ~/.local/share/node/node-v24.21.0-linux-x64/bin/node (behind ~/.local/bin/node)
|
|
12
|
+
//
|
|
13
|
+
// After that `sh -c` exits 127 on all ten hook events, Claude Code shows
|
|
14
|
+
// `<Event> hook error` on every tool call and the deck receives nothing — and
|
|
15
|
+
// the login item, naming the same binary, cannot start the deck whose next boot
|
|
16
|
+
// would have rewritten the entry.
|
|
17
|
+
//
|
|
18
|
+
// So the path is asked for the way the user reaches node: the first PATH entry
|
|
19
|
+
// whose real path IS this binary (/opt/homebrew/bin/node, ~/.local/bin/node,
|
|
20
|
+
// nodejs\ under nvm-windows), and failing that Homebrew's own stable link for a
|
|
21
|
+
// keg, <prefix>/opt/<formula>/bin/node. Only a spelling that resolves to this
|
|
22
|
+
// very file is taken — a different node earlier on PATH is a different
|
|
23
|
+
// program, with its own version and its own globals. Nothing found, and the
|
|
24
|
+
// running binary is the answer, as it always was: nvm, fnm and Volta have no
|
|
25
|
+
// such link, and a machine with nothing better loses nothing. For the hook,
|
|
26
|
+
// the command itself falls back to the node on PATH once that binary is gone
|
|
27
|
+
// (hookCommand in installer.mjs).
|
|
28
|
+
import { realpathSync } from "node:fs";
|
|
29
|
+
import { posix as posixPath, win32 as winPath } from "node:path";
|
|
30
|
+
|
|
31
|
+
/** Homebrew's keg path, mapped to the link it keeps pointing at the current
|
|
32
|
+
* keg of the same formula. Both separators, so the rule reads the same on any
|
|
33
|
+
* leg of the suite; null for anything that is not a keg. */
|
|
34
|
+
function brewOptPath(real) {
|
|
35
|
+
const m = /^(.*)([\\/])Cellar\2([^\\/]+)\2[^\\/]+\2bin\2node$/.exec(String(real));
|
|
36
|
+
return m ? [m[1], "opt", m[3], "bin", "node"].join(m[2]) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stableNodePath({
|
|
40
|
+
execPath = process.execPath,
|
|
41
|
+
env = process.env,
|
|
42
|
+
platform = process.platform,
|
|
43
|
+
realpath = realpathSync,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const win = platform === "win32";
|
|
46
|
+
// Windows paths compare without case; everywhere else a byte is a byte.
|
|
47
|
+
const key = (p) => (win ? p.toLowerCase() : p);
|
|
48
|
+
const resolved = (p) => { try { return key(realpath(p)); } catch { return null; } };
|
|
49
|
+
let own;
|
|
50
|
+
try { own = realpath(execPath); } catch { return execPath; }
|
|
51
|
+
const real = key(own);
|
|
52
|
+
const path = win ? winPath : posixPath;
|
|
53
|
+
const pathVar = env?.PATH ?? env?.Path ?? "";
|
|
54
|
+
for (const dir of String(pathVar).split(win ? ";" : ":")) {
|
|
55
|
+
// A relative entry would be resolved by Claude Code against each session's
|
|
56
|
+
// own cwd, and found in one directory and not in the next.
|
|
57
|
+
if (!dir || !path.isAbsolute(dir)) continue;
|
|
58
|
+
const candidate = path.join(dir, win ? "node.exe" : "node");
|
|
59
|
+
// The versioned directory itself on PATH is no more stable than execPath;
|
|
60
|
+
// a link later on PATH may still be.
|
|
61
|
+
if (key(candidate) !== real && resolved(candidate) === real) return candidate;
|
|
62
|
+
}
|
|
63
|
+
// From the path as given when it is a keg itself, so the prefix keeps the
|
|
64
|
+
// spelling it came with (/var rather than /private/var on a Mac).
|
|
65
|
+
const opt = brewOptPath(execPath) ?? brewOptPath(own);
|
|
66
|
+
if (opt && resolved(opt) === real) return opt;
|
|
67
|
+
return execPath;
|
|
68
|
+
}
|
|
@@ -62,6 +62,11 @@ async function serveStatic(req, res, url) {
|
|
|
62
62
|
});
|
|
63
63
|
res.end(body);
|
|
64
64
|
} catch {
|
|
65
|
+
// Nothing under assets/ is a client-side route: a name there that is not
|
|
66
|
+
// on disk is a bundle from before an upgrade asking for a chunk the new
|
|
67
|
+
// build no longer has. index.html in its place is a 200 the browser cannot
|
|
68
|
+
// run as a module, so it is a 404, which says what happened.
|
|
69
|
+
if (rel.startsWith("assets/")) return send(res, 404, { error: "not found" });
|
|
65
70
|
// SPA fallback to index.html for client-side routes
|
|
66
71
|
try {
|
|
67
72
|
const idx = await readFile(join(WEB_DIST, "index.html"));
|
|
@@ -202,9 +202,8 @@ export function signalExitAction(signal, platform = process.platform, numbers =
|
|
|
202
202
|
* The re-raise is the obvious half and on its own it does not work, which is
|
|
203
203
|
* the reason this is a function rather than one line at the call site. The
|
|
204
204
|
* supervisor traps SIGINT, SIGTERM and SIGHUP — and those are exactly the
|
|
205
|
-
* signals a worker is most likely to die of, since deck.js
|
|
206
|
-
*
|
|
207
|
-
* seconds. For all three the re-raise lands back in our own handler, which
|
|
205
|
+
* signals a worker is most likely to die of, since deck.js answers all three
|
|
206
|
+
* with its shutdown only after a boot that takes seconds. For all three the re-raise lands back in our own handler, which
|
|
208
207
|
* finds no child left and exits 0: `kill -HUP` on the deck was reported to the
|
|
209
208
|
* shell as a clean, successful stop. Dropping the handler first restores the
|
|
210
209
|
* default action, which is to die of the signal.
|
|
@@ -363,26 +362,33 @@ export function withoutPortAndOpen(args) {
|
|
|
363
362
|
return out;
|
|
364
363
|
}
|
|
365
364
|
|
|
365
|
+
/** `argv` with `ours` added at the end of its options: before the first bare
|
|
366
|
+
* `--`, which parseArgs reads as the end of options and drops everything
|
|
367
|
+
* after. Appended after it, the supervisor's own flags were never read. */
|
|
368
|
+
function withOptions(argv, ours) {
|
|
369
|
+
const end = argv.indexOf("--");
|
|
370
|
+
return end === -1 ? [...argv, ...ours] : [...argv.slice(0, end), ...ours, ...argv.slice(end)];
|
|
371
|
+
}
|
|
372
|
+
|
|
366
373
|
/** The worker's command line: the script and the user's own argv, and on a
|
|
367
|
-
* respawn the port the last worker actually bound.
|
|
368
|
-
* the worker's parser keeps the final --port. */
|
|
374
|
+
* respawn the port the last worker actually bound. Last among the options so
|
|
375
|
+
* it wins: the worker's parser keeps the final --port. */
|
|
369
376
|
export function workerArgs(worker, argv, { respawn = false, boundPort = null } = {}) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
return args;
|
|
377
|
+
const ours = respawn && boundPort != null ? ["--port", String(boundPort)] : [];
|
|
378
|
+
return [worker, ...withOptions(argv, ours)];
|
|
373
379
|
}
|
|
374
380
|
|
|
375
381
|
/** npx's command line for an upgrade: the spec, then the user's argv with the
|
|
376
|
-
* two flags this sets itself taken out. Ours are
|
|
377
|
-
* are dropped rather than left to be overridden —
|
|
378
|
-
* --port 4317 --no-open` works, but it is what the next
|
|
382
|
+
* two flags this sets itself taken out. Ours are added after the rest of the
|
|
383
|
+
* options, so the originals are dropped rather than left to be overridden —
|
|
384
|
+
* `--port 4317 --no-open --port 4317 --no-open` works, but it is what the next
|
|
385
|
+
* person reads in `ps`. */
|
|
379
386
|
export function npxRelaunchArgs(spec, argv, boundPort = null) {
|
|
380
|
-
const
|
|
381
|
-
if (boundPort != null) args.push("--port", String(boundPort));
|
|
387
|
+
const ours = boundPort != null ? ["--port", String(boundPort)] : [];
|
|
382
388
|
// The tab that asked for this is open and reconnecting; a second one would be
|
|
383
389
|
// the deck talking over itself.
|
|
384
|
-
|
|
385
|
-
return
|
|
390
|
+
ours.push("--no-open");
|
|
391
|
+
return ["-y", spec, ...withOptions(withoutPortAndOpen(argv), ours)];
|
|
386
392
|
}
|
|
387
393
|
|
|
388
394
|
/**
|
package/src/server/tailscale.mjs
CHANGED
|
@@ -179,9 +179,11 @@ export function beaconTargets(tailnet) {
|
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/** 100.64.0.0/10, the block Tailscale draws every IPv4 node address from.
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
/** 100.64.0.0/10, the block Tailscale draws every IPv4 node address from.
|
|
183
|
+
* Exported for the accept switches — see askToAccept in lan-requests.mjs,
|
|
184
|
+
* which asks it of an address whose route this file could not answer. */
|
|
185
|
+
export function inTailnetBlock(addr) {
|
|
186
|
+
const m = /^100\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(String(addr ?? "").replace(/^::ffff:/, ""));
|
|
185
187
|
return !!m && Number(m[1]) >= 64 && Number(m[1]) <= 127;
|
|
186
188
|
}
|
|
187
189
|
|
|
@@ -190,7 +192,9 @@ function inTailnetBlock(addr) {
|
|
|
190
192
|
*
|
|
191
193
|
* Null means the local network, which is every answer this deck gave before
|
|
192
194
|
* Tailscale was read — and still the answer on a machine where it is not
|
|
193
|
-
* running.
|
|
195
|
+
* running. It is an answer about routing, not about trust: the local accept
|
|
196
|
+
* switch still waits for a press on an address in Tailscale's block that this
|
|
197
|
+
* could not place — see askToAccept in lan-requests.mjs.
|
|
194
198
|
*
|
|
195
199
|
* WHAT MAKES THE ANSWER SAFE TO ACT ON is Tailscale, not the packet: WireGuard
|
|
196
200
|
* binds each node address to that node's key, and tailscaled drops a packet
|
|
@@ -654,10 +654,13 @@ function scanTranscript(path) {
|
|
|
654
654
|
});
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
-
// What session-enrichment.mjs calls, and the
|
|
658
|
-
// (account-projects.mjs) reads its own pass of the transcripts by
|
|
659
|
-
//
|
|
660
|
-
//
|
|
657
|
+
// What session-enrichment.mjs calls, and the rules the Projects report
|
|
658
|
+
// (account-projects.mjs) reads its own pass of the transcripts by: which lines
|
|
659
|
+
// billed anything, and the speed each was billed at with the cap on how many
|
|
660
|
+
// speeds one model keeps apart. Listed here rather than marked at each
|
|
661
|
+
// declaration so that every declaration above reads exactly as it did where it
|
|
662
|
+
// came from.
|
|
661
663
|
export {
|
|
662
664
|
scanTranscript, newUsageTotals, hasSpend, mergeUsageByModel, copyUsageBucket, repeatsRequestUsage,
|
|
665
|
+
billedSpeed, MAX_SPEEDS_PER_MODEL, UNRECOGNISED_SPEED,
|
|
663
666
|
};
|
package/src/server/usage-day.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// How much this deck was used on one UTC day: the distinct sessions, subagents
|
|
2
|
-
// and projects its hooks heard from
|
|
3
|
-
//
|
|
2
|
+
// and projects its hooks heard from — and how hard the deck worked for it: how
|
|
3
|
+
// many live events it took in, and its peak memory. The "active" report carries
|
|
4
|
+
// the totals of the last finished day (reports.mjs), the last two as buckets
|
|
5
|
+
// (depth-facts.mjs).
|
|
4
6
|
//
|
|
5
7
|
// WHY A DAY AND NOT A SNAPSHOT. The counts used to be read off the live state at
|
|
6
8
|
// the moment the "active" went out — and that moment is the first check-in of
|
|
@@ -12,15 +14,20 @@
|
|
|
12
14
|
// WHAT IS KEPT. In memory, the session ids, the subagent ids and the folders the
|
|
13
15
|
// transcripts sit in, so a session heard from a hundred times counts once. They
|
|
14
16
|
// never leave this module: what is saved between runs (prefs.json, beside the
|
|
15
|
-
// install id) and what is sent are
|
|
16
|
-
//
|
|
17
|
-
//
|
|
17
|
+
// install id) and what is sent are counts, the day they belong to, and the
|
|
18
|
+
// names of the features used that day — fixed tokens off FEATURES, a yes for
|
|
19
|
+
// each, never what was done with them.
|
|
18
20
|
//
|
|
19
21
|
// A RESTART MID-DAY. The saved counts come back as a floor and the new run
|
|
20
22
|
// counts on top of them, so a session that was running across the restart is
|
|
21
23
|
// counted twice. That is the price of keeping no ids on disk, and a restart is
|
|
22
24
|
// rare next to the day's sessions.
|
|
23
25
|
//
|
|
26
|
+
// DAYS USED. Beside the day, a running count of the days the deck heard any
|
|
27
|
+
// session at all, and the last of them so a restart does not count a day twice
|
|
28
|
+
// — a count and one date, never the list. It is what decides when the deck has
|
|
29
|
+
// been in use long enough to ask how useful it is (rating.mjs).
|
|
30
|
+
//
|
|
24
31
|
// Only live use counts: a Claude hook fires because somebody is running the
|
|
25
32
|
// CLI, and the Codex watcher emits only what a rollout gains while the deck
|
|
26
33
|
// watches (its first scan skips every file's history). Replayed lines and the
|
|
@@ -66,22 +73,29 @@ function whole(n) {
|
|
|
66
73
|
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
/** Saved totals, coerced: a day,
|
|
76
|
+
/** Saved totals, coerced: a day, its counts and the features used, or null for anything else.
|
|
77
|
+
* Events a save does not say — 3.36.x counted none — stay null, not 0: that
|
|
78
|
+
* day was not counted in full, and says nothing about them. */
|
|
70
79
|
function savedTotals(raw) {
|
|
71
80
|
if (!raw || typeof raw !== "object" || typeof raw.day !== "string" || !DAY.test(raw.day)) return null;
|
|
72
81
|
return {
|
|
73
82
|
day: raw.day, sessions: whole(raw.sessions), subagents: whole(raw.subagents), projects: whole(raw.projects),
|
|
74
83
|
features: knownFeatures(raw.features),
|
|
84
|
+
events: Number.isInteger(raw.events) && raw.events >= 0 ? raw.events : null,
|
|
85
|
+
peakMb: whole(raw.peakMb),
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
/** What prefs.json keeps for this, coerced — deck-prefs.mjs normalises with it. */
|
|
79
90
|
export function normaliseUsage(raw) {
|
|
80
91
|
const src = raw && typeof raw === "object" ? raw : {};
|
|
92
|
+
const day = v => (typeof v === "string" && DAY.test(v) ? v : "");
|
|
81
93
|
return {
|
|
82
94
|
current: savedTotals(src.current),
|
|
83
95
|
done: savedTotals(src.done),
|
|
84
|
-
sent:
|
|
96
|
+
sent: day(src.sent),
|
|
97
|
+
daysUsed: whole(src.daysUsed),
|
|
98
|
+
lastUsedDay: day(src.lastUsedDay),
|
|
85
99
|
};
|
|
86
100
|
}
|
|
87
101
|
|
|
@@ -105,6 +119,12 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
105
119
|
let sent = "";
|
|
106
120
|
/** Moves on every change, so the reporter saves only when there is something new. */
|
|
107
121
|
let changes = 0;
|
|
122
|
+
/** The days used before this run, as an earlier run saved them: a count and
|
|
123
|
+
* the last of them. */
|
|
124
|
+
let usedBefore = 0;
|
|
125
|
+
let lastUsedBefore = "";
|
|
126
|
+
/** The days this run heard a session on — one or two, a deck past midnight. */
|
|
127
|
+
const usedThisRun = new Set();
|
|
108
128
|
/** The first use this run heard, `{ at, provider }`, for activation.mjs. */
|
|
109
129
|
let first = null;
|
|
110
130
|
const firstListeners = [];
|
|
@@ -112,11 +132,13 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
112
132
|
function fresh(day, floor = null) {
|
|
113
133
|
return {
|
|
114
134
|
day,
|
|
115
|
-
floor: floor ?? { sessions: 0, subagents: 0, projects: 0, features: [] },
|
|
135
|
+
floor: floor ?? { sessions: 0, subagents: 0, projects: 0, features: [], events: 0, peakMb: 0 },
|
|
116
136
|
sessions: new Set(),
|
|
117
137
|
subagents: new Set(),
|
|
118
138
|
projects: new Set(),
|
|
119
139
|
features: new Set(),
|
|
140
|
+
events: 0,
|
|
141
|
+
peakMb: 0,
|
|
120
142
|
};
|
|
121
143
|
}
|
|
122
144
|
|
|
@@ -127,6 +149,10 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
127
149
|
subagents: c.floor.subagents + c.subagents.size,
|
|
128
150
|
projects: c.floor.projects + c.projects.size,
|
|
129
151
|
features: knownFeatures([...c.floor.features, ...c.features]),
|
|
152
|
+
// A floor with no events count makes the day's unknown too: this run's
|
|
153
|
+
// alone would undercount it.
|
|
154
|
+
events: c.floor.events === null ? null : c.floor.events + c.events,
|
|
155
|
+
peakMb: Math.max(c.floor.peakMb, c.peakMb),
|
|
130
156
|
};
|
|
131
157
|
}
|
|
132
158
|
|
|
@@ -151,6 +177,9 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
151
177
|
const sid = raw?.session_id;
|
|
152
178
|
if (!sid || typeof sid !== "string") return;
|
|
153
179
|
const c = today();
|
|
180
|
+
c.events++;
|
|
181
|
+
changes++;
|
|
182
|
+
add(usedThisRun, c.day);
|
|
154
183
|
add(c.sessions, sid);
|
|
155
184
|
if (typeof raw.agent_id === "string" && raw.agent_id) add(c.subagents, `${sid}\u0000${raw.agent_id}`);
|
|
156
185
|
add(c.features, raw.provider === "codex" ? "codex-sessions" : "claude-sessions");
|
|
@@ -162,6 +191,16 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
162
191
|
}
|
|
163
192
|
}
|
|
164
193
|
|
|
194
|
+
/** The deck's resident memory right now, in MB: the day keeps its highest. */
|
|
195
|
+
function notePeak(mb) {
|
|
196
|
+
if (!Number.isFinite(mb) || mb <= 0) return;
|
|
197
|
+
const c = today();
|
|
198
|
+
const rounded = Math.round(mb);
|
|
199
|
+
if (rounded <= c.peakMb) return;
|
|
200
|
+
c.peakMb = rounded;
|
|
201
|
+
changes++;
|
|
202
|
+
}
|
|
203
|
+
|
|
165
204
|
/** A transcript path a Claude hook carried, already accepted as Claude's:
|
|
166
205
|
* the project is the folder it sits in. */
|
|
167
206
|
function noteProject(path) {
|
|
@@ -182,6 +221,21 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
182
221
|
add(today().projects, dir);
|
|
183
222
|
}
|
|
184
223
|
|
|
224
|
+
/** How many days in all this deck heard a session on: the saved count, and
|
|
225
|
+
* the days of this run that came after the last of them. */
|
|
226
|
+
function daysUsed() {
|
|
227
|
+
let n = usedBefore;
|
|
228
|
+
for (const d of usedThisRun) if (d > lastUsedBefore) n++;
|
|
229
|
+
return n;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The last of those days, "" before the first. */
|
|
233
|
+
function lastUsedDay() {
|
|
234
|
+
let last = lastUsedBefore;
|
|
235
|
+
for (const d of usedThisRun) if (d > last) last = d;
|
|
236
|
+
return last;
|
|
237
|
+
}
|
|
238
|
+
|
|
185
239
|
/** The first use this run heard, or null. */
|
|
186
240
|
function firstUse() {
|
|
187
241
|
return first ? { ...first } : null;
|
|
@@ -214,7 +268,7 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
214
268
|
|
|
215
269
|
/** What survives a restart: counts and days, never an id or a path. */
|
|
216
270
|
function saved() {
|
|
217
|
-
return { current: current ? totals(current) : null, done, sent };
|
|
271
|
+
return { current: current ? totals(current) : null, done, sent, daysUsed: daysUsed(), lastUsedDay: lastUsedDay() };
|
|
218
272
|
}
|
|
219
273
|
|
|
220
274
|
/**
|
|
@@ -226,11 +280,19 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
226
280
|
function restore(raw) {
|
|
227
281
|
const s = normaliseUsage(raw);
|
|
228
282
|
if (s.sent > sent) sent = s.sent;
|
|
283
|
+
// The further-along of the two, so restoring the same save twice adds nothing.
|
|
284
|
+
if (s.daysUsed > usedBefore || (s.daysUsed === usedBefore && s.lastUsedDay > lastUsedBefore)) {
|
|
285
|
+
usedBefore = s.daysUsed;
|
|
286
|
+
lastUsedBefore = s.lastUsedDay;
|
|
287
|
+
}
|
|
229
288
|
for (const t of [s.done, s.current]) {
|
|
230
289
|
if (!t) continue;
|
|
231
290
|
const c = today();
|
|
232
291
|
if (t.day === c.day) {
|
|
233
|
-
c.floor = {
|
|
292
|
+
c.floor = {
|
|
293
|
+
sessions: t.sessions, subagents: t.subagents, projects: t.projects, features: t.features,
|
|
294
|
+
events: t.events, peakMb: t.peakMb,
|
|
295
|
+
};
|
|
234
296
|
} else if (t.day < c.day && (!done || t.day > done.day)) {
|
|
235
297
|
done = t;
|
|
236
298
|
}
|
|
@@ -243,7 +305,10 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
243
305
|
return changes;
|
|
244
306
|
}
|
|
245
307
|
|
|
246
|
-
return {
|
|
308
|
+
return {
|
|
309
|
+
noteUse, noteProject, noteFolder, noteFeature, notePeak, firstUse, onFirstUse, finished, markSent, saved, restore, version,
|
|
310
|
+
daysUsed,
|
|
311
|
+
};
|
|
247
312
|
}
|
|
248
313
|
|
|
249
314
|
/** The deck's own tally, which the event pipeline feeds and the reporter reads. */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{j as e,r as c,k as _,l as V,m as Y,n as z,s as X,u as Z,g as ee}from"./index-tVBMXvDv.js";function se({snap:s}){return e.jsxs("section",{className:"bw-feed",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Live activity",e.jsxs("span",{className:"bw-feed-count",children:[s.log.length," ",s.log.length===1?"event":"events"]})]}),e.jsx("p",{className:"bw-since bw-feed-note",children:"Pages you opened yourself — counted to run the rule, never written down. Addresses appear under Findings, and only for pages a program opened."}),e.jsx("div",{className:"bw-log","aria-live":"polite","aria-relevant":"additions",children:s.log.length===0?e.jsx("p",{className:"bw-note",children:"The deck writes a line here each time it looks at a profile."}):s.log.map(t=>e.jsxs("div",{className:`bw-log-line ${t.level}${t.parts?"":" sys"}`,children:[e.jsx("span",{className:"bw-log-time",children:new Date(t.atMs).toLocaleTimeString("en-GB",{hour12:!1})}),t.parts?e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"bw-log-browser",children:t.parts.browser}),e.jsx("span",{className:"bw-log-profile",title:t.parts.profile,children:t.parts.profile}),e.jsxs("span",{className:"bw-log-value",children:[t.parts.value,t.parts.flagged>0&&e.jsxs("span",{className:"bw-log-flag",children:[" · ",t.parts.flagged," flagged"]})]})]}):e.jsx("span",{className:"bw-log-sys",children:t.text})]},`${t.atMs}-${t.level}-${t.text}`))})]})}const E=s=>`${s.host}-${s.startMs}`;function te(s){const t=l=>new Date(l).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1});return s.count===1||s.endMs-s.startMs<6e4?t(s.startMs):`${t(s.startMs)} → ${t(s.endMs)}`}function ne(s){return new Date(s).toLocaleDateString(void 0,{weekday:"short",day:"numeric",month:"short"})}function U(s){const t=Math.round((s.endMs-s.startMs)/6e4);return t<1?"":`${t} min`}function ae({snap:s,dismiss:t,refreshRef:l}){const[r,h]=c.useState(null),o=c.useRef(new Map),a=c.useRef(null);c.useEffect(()=>{var i,w;const d=a.current;!d||s.episodes.some(m=>E(m)===d.id)||(a.current=null,_(((i=document.activeElement)==null?void 0:i.tagName)??null)&&((w=(d.next?o.current.get(d.next):void 0)??l.current)==null||w.focus()))},[s]);const n=d=>{a.current={id:E(d),next:V(s.episodes.map(E),E(d))},t(d)},u=c.useMemo(()=>{const d=[];for(const i of s.episodes){const w=ne(i.startMs),m=d[d.length-1];m&&m.label===w?m.episodes.push(i):d.push({label:w,episodes:[i]})}return d},[s]);return e.jsxs("section",{className:"bw-findings",children:[e.jsxs("h4",{className:"bw-sec-head bw-head-row",children:["Findings",s.episodes.length>0&&e.jsxs("span",{className:"bw-feed-count",children:[s.episodes.length," ",s.episodes.length===1?"episode":"episodes"]})]}),e.jsx("div",{className:"bw-eps",children:u.length===0?e.jsxs("div",{className:"bw-empty",children:[e.jsx("p",{className:"bw-empty-head",children:"Nothing found yet"}),e.jsxs("p",{className:"bw-empty-note",children:["An episode lands here when a program opens pages in a browser nobody has touched for"," ",s.settings.quietMinutes," ",s.settings.quietMinutes===1?"minute":"minutes",". On most machines that is rare, so an empty list is the ordinary result rather than a sign something is wrong."]}),!s.settings.enabled&&e.jsx("p",{className:"bw-empty-note",children:"Watching is off, so this shows only what this deck has seen since it started. Anything an earlier run recorded comes back when you switch it on."})]}):u.map(d=>e.jsxs("div",{className:"bw-day",children:[e.jsx("h4",{className:"bw-sec-head",children:d.label}),d.episodes.map(i=>{const w=E(i),m=r===w;return e.jsxs("div",{className:`bw-ep${m?" open":""}`,children:[e.jsxs("div",{className:"bw-ep-row",children:[e.jsxs("button",{className:"bw-ep-head",ref:b=>{b?o.current.set(w,b):o.current.delete(w)},onClick:()=>h(m?null:w),"aria-expanded":m,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:m?"▾":"▸"}),e.jsx("span",{className:"bw-ep-host",children:i.host}),e.jsxs("span",{className:"bw-ep-meta",children:[te(i),U(i)&&e.jsxs(e.Fragment,{children:[" · ",U(i)]}),i.provisional&&e.jsxs(e.Fragment,{children:[" · ",e.jsx("span",{title:"Its quiet window has not closed yet. If somebody used the browser in the minutes after it, it is withdrawn; until then it is not written down and no reaction runs.",children:"provisional"})]})]}),e.jsxs("span",{className:"bw-ep-count",children:[i.count," ",i.count===1?"page":"pages"]})]}),e.jsx("button",{className:"glyph-btn bw-ep-x",onClick:()=>n(i),"aria-label":`Dismiss ${i.host}`,title:"Dismiss — it leaves this list for good, and stays in the log file",children:"×"})]}),m&&e.jsx("ul",{className:"bw-urls",children:i.urls.map((b,y)=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-url-time",children:new Date(b.timeMs).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",hour12:!1})}),e.jsx("span",{className:"bw-url",children:b.url})]},`${b.url}-${b.timeMs}-${y}`))})]},w)})]},d.label))})]})}function re(s){const t=/^#?([0-9a-f]{6})$/i.exec((s??"").trim());if(!t)return!1;const l=parseInt(t[1],16),r=[l>>16&255,l>>8&255,l&255].map(h=>{const o=h/255;return o<=.03928?o/12.92:Math.pow((o+.055)/1.055,2.4)});return .2126*r[0]+.7152*r[1]+.0722*r[2]>.5}const G=1e4,ie=12e3;function oe({browsers:s,findings:t,watching:l,palette:r}){const h=c.useRef(null),o=c.useRef(null);return o.current={browsers:s,findings:t,watching:l},c.useEffect(()=>{var m;const a=h.current;if(!a)return;const n=a.getContext("2d");if(!n)return;const u=((m=window.matchMedia)==null?void 0:m.call(window,"(prefers-reduced-motion: reduce)").matches)??!1;let d=0,i=!1;const w=()=>{var q;if(i)return;const b=o.current;if(!b)return;const{browsers:y,findings:p,watching:P}=b,R=r["--accent"]||"#7dd3fc",f=r["--line"]||"#1f2229",j=r["--text-dim"]||"#6e727c",C=re(r["--bg"]),N=C?42:26,v=C?.85:.65,I=Date.now(),$=Math.min(window.devicePixelRatio||1,2),W=a.clientWidth,k=a.clientHeight;(a.width!==W*$||a.height!==k*$)&&(a.width=W*$,a.height=k*$),n.setTransform($,0,0,$,0,0),n.clearRect(0,0,W,k);const x=W/2,M=k/2,S=Math.min(W,k)/2-6;n.strokeStyle=f,n.lineWidth=1,n.globalAlpha=C?1:.9;for(const g of[.4,.7,1])n.beginPath(),n.arc(x,M,S*g,0,Math.PI*2),n.stroke();n.beginPath(),n.moveTo(x-S,M),n.lineTo(x+S,M),n.moveTo(x,M-S),n.lineTo(x,M+S),n.stroke(),n.globalAlpha=1;const A=(P&&!u?I%G/G:0)*Math.PI*2-Math.PI/2;if(P){const g=(q=n.createConicGradient)==null?void 0:q.call(n,A-.85,x,M);g&&(g.addColorStop(0,"transparent"),g.addColorStop(.85/(Math.PI*2),`color-mix(in srgb, ${R} ${N}%, transparent)`),g.addColorStop(1,"transparent"),n.fillStyle=g,n.beginPath(),n.moveTo(x,M),n.arc(x,M,S,A-.85,A),n.closePath(),n.fill()),n.strokeStyle=R,n.globalAlpha=v,n.beginPath(),n.moveTo(x,M),n.lineTo(x+Math.cos(A)*S,M+Math.sin(A)*S),n.stroke(),n.globalAlpha=1}y.forEach((g,T)=>{const B=T/Math.max(y.length,1)*Math.PI*2-Math.PI/2,D=g.lastReadMs===null?1:Math.min(1,(I-g.lastReadMs)/12e4),O=S*(.42+D*.5),F=x+Math.cos(B)*O,L=M+Math.sin(B)*O,Q=Math.abs((A-B+Math.PI*3)%(Math.PI*2)-Math.PI),K=P?Math.max(0,1-Q/.55):0;n.beginPath(),n.arc(F,L,3+K*1.6,0,Math.PI*2),n.fillStyle=g.running?R:j,n.globalAlpha=g.running?.55+K*.45:.35,n.fill(),n.globalAlpha=1});for(const g of p){const T=(I-g.atMs)/ie;if(T<0||T>1)continue;const D=Math.max(0,y.findIndex(L=>L.key===g.browser))/Math.max(y.length,1)*Math.PI*2-Math.PI/2,O=x+Math.cos(D)*S*.6,F=M+Math.sin(D)*S*.6;n.beginPath(),n.arc(O,F,4+T*22,0,Math.PI*2),n.strokeStyle=R,n.globalAlpha=(1-T)*.8,n.lineWidth=1.5,n.stroke(),n.globalAlpha=1}d=requestAnimationFrame(w)};return w(),()=>{i=!0,cancelAnimationFrame(d)}},[r]),e.jsx("canvas",{ref:h,className:"bw-radar",role:"img","aria-label":l?`Watching ${s.length} browser${s.length===1?"":"s"}`:"Not watching"})}function le(s,t,l){if(s===null)return null;const r=s+t-l;return r>0?r:null}function ce(s){const t=Math.ceil(s/1e3),l=Math.floor(t/60),r=t%60;return l>0?`${l}m ${r}s`:`${r}s`}function J(s){return(s??[]).filter(t=>t.installed&&t.profiles>0)}function de(s){return!Number.isFinite(s)||s<=0?"empty":Y(s)}function he(s,t){return s.map(l=>({key:l.key,name:l.name,running:l.running,visits:t.filter(r=>r.browser===l.key).reduce((r,h)=>r+h.programVisits,0)}))}function ue(s,t){if(s===null)return null;const l=Math.max(0,Math.round((t-s)/1e3));if(l<5)return"just now";if(l<60)return`${l} sec ago`;const r=Math.round(l/60);return r<60?`${r} min ago`:`${Math.round(r/60)} hr ago`}function we(s){const t=s.profiles.filter(r=>r.degraded);if(t.length>0){const r=t[0],h=t.length-1;return{kind:"unreadable",text:`${r.name}/${r.profile} could not be read — ${r.reason??"no reason given"}`+(h>0?`, and ${h} more`:"")+". The watch continues on the profiles it can read."}}const l=J(s.browsers);return l.length===0?{kind:"no-profiles",text:"No browser on this machine has a profile to read. Open one of the browsers below once and it will be watched from then on."}:l.some(r=>r.running)?null:{kind:"none-running",text:"No watched browser is running, so nothing can be navigated right now. Their history is still read, and anything found while they were open is still here."}}function me({snap:s,watching:t,palette:l}){const[r,h]=c.useState(!1),[o,a]=c.useState(()=>Date.now());c.useEffect(()=>{const u=setInterval(()=>a(Date.now()),1e3);return()=>clearInterval(u)},[]);const n=le(s.coverage.lastHumanMs,s.coverage.quietMs,o);return e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Activity overview",e.jsx("button",{className:"bw-help",onClick:()=>h(u=>!u),"aria-expanded":r,"aria-label":"What the sweep and the dots mean",children:"?"})]}),e.jsxs("div",{className:"bw-glance",children:[e.jsx(oe,{browsers:t.map(u=>{var d;return{key:u.key,name:u.name,running:u.running,lastReadMs:((d=s.profiles.find(i=>i.browser===u.key))==null?void 0:d.lastWrittenMs)??null}}),findings:s.episodes.slice(0,6).map(u=>({browser:u.browser??null,atMs:u.endMs})),watching:s.settings.enabled,palette:l}),e.jsx("ul",{className:"bw-counts",children:he(t,s.profiles).map(u=>e.jsxs("li",{children:[e.jsx("span",{className:"bw-count-n",children:u.visits.toLocaleString("en-US")}),e.jsx("span",{className:"bw-count-of",children:u.name})]},u.key))})]}),e.jsx("p",{className:"bw-since",children:"Pages a program opened, since this deck started"}),s.coverage.checkedMs>0&&e.jsxs("p",{className:"bw-since",children:["Last checked ",ue(s.coverage.checkedMs,o),s.coverage.checks>0&&e.jsxs(e.Fragment,{children:[" · ",s.coverage.checks.toLocaleString("en-US")," ",s.coverage.checks===1?"check":"checks"]})]}),n!==null&&e.jsxs("p",{className:"bw-gate",children:["You are browsing — a program page would count in ",ce(n)]}),r&&e.jsxs("dl",{className:"bw-key",children:[e.jsx("dt",{children:"Sweep"}),e.jsx("dd",{children:"one poll of every watched profile"}),e.jsx("dt",{children:"Dot"}),e.jsx("dd",{children:"a browser, further out the longer since it last wrote history"}),e.jsx("dt",{children:"Ring"}),e.jsx("dd",{children:"a finding, leaving the browser it came from"})]})]})}function be({watching:s,rest:t}){const[l,r]=c.useState(!1),[h,o]=c.useState(!1);return e.jsxs("section",{className:"bw-sec",children:[e.jsxs("h4",{className:"bw-sec-head",children:["Watched profiles",e.jsx("button",{className:"bw-help",onClick:()=>r(a=>!a),"aria-expanded":l,"aria-label":"What running, idle and connected to Anthropic mean",children:"?"})]}),l&&e.jsxs("dl",{className:"bw-key",children:[e.jsx("dt",{children:"Running"}),e.jsx("dd",{children:"the browser has a process on this machine right now"}),e.jsx("dt",{children:"Connected"}),e.jsx("dd",{children:"an open connection to an address Anthropic's relay uses — which it shares with claude.ai, so an open tab looks the same as an agent channel"})]}),e.jsx("ul",{className:"bw-profiles",children:s.map(a=>e.jsxs("li",{children:[e.jsx("span",{className:`bw-prof-dot${a.running?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-prof-name",children:a.name}),e.jsx("span",{className:"bw-prof-state",children:a.running?"running":"idle"}),a.running&&a.relay.state==="live"&&e.jsx("span",{className:"bw-relay",children:"connected to Anthropic"})]},a.key))}),t.length>0&&e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"bw-why bw-rest-head",onClick:()=>o(a=>!a),"aria-expanded":h,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:h?"▾":"▸"})," ",t.length," not watched"]}),h&&e.jsxs("p",{className:"bw-rest",children:[t.map(a=>a.name).join(", ")," — not installed, or installed and never opened, so there is no history to read."]})]})]})}function fe({snap:s,quiet:t,setQuiet:l,save:r,access:h,setAccess:o}){return e.jsxs("div",{className:"bw-settings",children:[e.jsxs("label",{title:"A program opening a page counts as a finding only if nobody had touched the browser for this long. Shorter catches more and reports more of your own work; longer is quieter.",children:[e.jsx("span",{children:"Nobody browsing for"}),e.jsxs("select",{value:t??s.settings.quietMinutes,onChange:a=>{l(Number(a.target.value)),r({quietMinutes:Number(a.target.value)})},children:[e.jsx("option",{value:1,children:"1 min"}),e.jsx("option",{value:5,children:"5 min"}),e.jsx("option",{value:15,children:"15 min"}),e.jsx("option",{value:30,children:"30 min"}),e.jsx("option",{value:60,children:"60 min"})]})]}),e.jsxs("label",{children:[e.jsx("span",{children:"When it finds one"}),e.jsx("select",{value:s.settings.reaction,onChange:a=>void r({reaction:a.target.value}),disabled:!s.settings.enabled,title:s.settings.enabled?"What to do besides writing it down.":"Turn watching on to arm a reaction.",children:(s.reactions??["notify"]).map(a=>e.jsx("option",{value:a,children:a==="notify"?"notify me":a==="close-tab"?"close the tab":"quit the browser"},a))})]}),e.jsxs("div",{className:"bw-settings-note",children:[e.jsxs("p",{children:["Chrome marks a navigation that came from an extension or a command rather than from a click. These are the ones that happened while nobody had touched the browser for the time above. ",e.jsx("strong",{children:"Usually that is your own agent doing what you asked."})]}),e.jsxs("button",{className:"bw-why",onClick:()=>o(a=>!a),"aria-expanded":h,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:h?"▾":"▸"})," What Browser Watch can access"]}),h&&e.jsxs("dl",{className:"bw-access",children:[e.jsx("dt",{children:"Reads"}),e.jsxs("dd",{children:["A copy of each browser's own history database — the live file is locked while the browser holds it. Only rows newer than the moment this deck started:"," ",new Date(s.coverage.startedMs).toLocaleString(),"."]}),e.jsx("dt",{children:"Keeps"}),e.jsxs("dd",{children:["Only while the switch is on, and only the episodes it flagged — never your ordinary browsing. In ",e.jsx("code",{className:"bw-path",children:s.coverage.logPath}),typeof s.coverage.logBytes=="number"?` (${de(s.coverage.logBytes)})`:"",", with every address written in full so you can check it yourself."]}),e.jsx("dt",{children:"Sends"}),e.jsx("dd",{children:"Nothing. No part of this reads or writes over the network; the deck serves on 127.0.0.1 and this panel talks only to it."}),e.jsx("dt",{children:"Never reads"}),e.jsx("dd",{children:"Anything from before this deck started, cookies, saved passwords, page contents, or any browser profile with no history file."})]})]})]})}function H(s,t){return t?{kind:"saving",word:"Saving"}:s.settings.enabled?{kind:"on",word:"Watching"}:{kind:"off",word:"Paused"}}function ge({snap:s,saving:t,save:l,why:r,setWhy:h}){return e.jsxs("footer",{className:"bw-status",children:[e.jsx("span",{className:`bw-mode-dot${s.settings.enabled?" on":""}`,"aria-hidden":!0}),e.jsx("span",{className:"bw-mode-word",children:H(s,t).word},H(s,t).kind),e.jsx("span",{className:"bw-status-text",children:s.coverage.archived===0?s.settings.enabled?"No episodes recorded yet":"No episodes on disk":`${s.coverage.archived} ${s.coverage.archived===1?"episode":"episodes"} on disk`}),e.jsx("button",{className:`glyph-btn bw-gear${r?" on":""}`,onClick:()=>h(o=>!o),"aria-expanded":r,"aria-label":"Settings",title:"Settings",children:e.jsxs("svg",{width:"13",height:"13",viewBox:"0 0 14 14",fill:"none",stroke:"currentColor",strokeWidth:"1.3","aria-hidden":!0,children:[e.jsx("circle",{cx:"7",cy:"7",r:"2.1"}),e.jsx("path",{d:"M7 1.2v1.6M7 11.2v1.6M1.2 7h1.6M11.2 7h1.6M2.9 2.9l1.1 1.1M10 10l1.1 1.1M11.1 2.9L10 4M4 10l-1.1 1.1"})]})}),e.jsxs("label",{className:"bw-switch",htmlFor:"bw-enabled",children:[e.jsx("span",{className:"bw-switch-label",id:"bw-enabled-label",children:"Watch browser activity"}),e.jsx("button",{id:"bw-enabled",type:"button",role:"switch","aria-checked":s.settings.enabled,"aria-labelledby":"bw-enabled-label",className:"switch",onClick:()=>void l({enabled:!s.settings.enabled}),title:s.settings.enabled?"On — every episode it finds is written down, so the list outlives the browsing history being cleared":"Off — showing only what this deck has seen since it started. Anything an earlier run archived is hidden until you switch back on, and nothing new is kept",children:e.jsx("span",{className:"switch-knob"})})]})]})}function pe({relay:s}){const[t,l]=c.useState(!1),[r,h]=c.useState(!1),o=s.killswitch.blocked?"unblock":"block",a=s.command[o],n=c.useCallback(async()=>{await z(a.command)&&(h(!0),window.setTimeout(()=>h(!1),1600))},[a.command]),u=s.profiles.filter(i=>{var w;return(w=i.report)==null?void 0:w.present}),d=s.profiles.filter(i=>i.report===null);return e.jsxs("section",{className:"bw-sec",children:[e.jsx("h4",{className:"bw-sec-head",children:"Remote control"}),e.jsx("p",{className:`bw-rc-verdict ${s.verdict}`,role:"status",children:s.verdict==="exposed"?"Claude in Chrome is live here, so any Claude Code session signed in to your Anthropic account can drive this browser — from any machine, with no prompt on this one.":s.verdict==="protected"?`The relay is black-holed in ${s.hostsPath}, so no new connection to it can be made from this machine.`:"Claude in Chrome is not installed and enabled in any profile here, so there is nothing for the relay to drive."}),s.killswitch.foreign.length>0&&e.jsxs("p",{className:"bw-rc-foreign",children:["Another entry for this host is already in ",s.hostsPath,", and ccdeck will not touch it:",s.killswitch.foreign.map(i=>e.jsx("code",{className:"bw-path",children:i},i))]}),e.jsxs("button",{className:"bw-why",onClick:()=>l(i=>!i),"aria-expanded":t,children:[e.jsx("span",{className:"bw-chev","aria-hidden":!0,children:t?"▾":"▸"})," ",s.verdict==="protected"?"How to undo the block":"What it can reach, and how to stop it"]}),t&&e.jsxs("div",{className:"bw-rc-body",children:[u.length>0&&e.jsx("dl",{className:"bw-key",children:u.map(i=>e.jsxs(c.Fragment,{children:[e.jsxs("dt",{children:[i.name,i.profile==="Default"?"":` · ${i.profile}`]}),e.jsxs("dd",{children:[i.report.enabled?"enabled":"installed but switched off",i.report.allUrls?", can act on every site":", limited to the sites it was granted",i.report.sensitiveApis.length>0&&e.jsxs(e.Fragment,{children:[" — ",i.report.sensitiveApis.join(", ")]})]})]},`${i.browser}/${i.profile}`))}),d.length>0&&e.jsxs("p",{className:"bw-rc-note",children:[d.map(i=>i.name).join(", ")," — its settings file could not be read, so nothing is claimed about it either way."]}),!s.hostsRead&&e.jsxs("p",{className:"bw-rc-note",children:[s.hostsPath," could not be read, so whether the relay is already blocked is unknown."]}),u.some(i=>i.report.allUrls)&&e.jsxs("p",{className:"bw-rc-note",children:["Narrowing the sites in ",e.jsx("code",{className:"bw-path",children:"chrome://extensions"})," does not change this: the grant is held at the browser level and the per-site list is enforced inside the extension, by the extension."]}),e.jsxs("div",{className:"bw-rc-cmd",children:[e.jsx("code",{children:a.command}),e.jsx("button",{className:"btn bw-rc-copy",onClick:n,children:r?"Copied":"Copy"})]}),e.jsx("p",{className:"bw-rc-note",children:a.note})]})]})}function xe(s){const[t,l]=c.useState(null),[r,h]=c.useState(null),[o,a]=c.useState(null),[n,u]=c.useState(!1),d=c.useRef(!1),[i,w]=c.useState(null),[m,b]=c.useState(!1),y=c.useRef(!1),p=c.useCallback(async f=>{var j,C;if(!(f&&!X(d.current))){f&&(d.current=!0,u(!0));try{const N=await fetch(`/api/browser-watch${f?"?refresh=1":""}`);if(!N.ok)throw new Error(`the deck answered ${N.status}`);const v=await N.json();l(v),y.current||w(((j=v==null?void 0:v.settings)==null?void 0:j.quietMinutes)??15),s(((C=v==null?void 0:v.settings)==null?void 0:C.enabled)===!0),h(null)}catch(N){h(N instanceof Error?N.message:String(N))}finally{f&&(d.current=!1,u(!1))}}},[s]);c.useEffect(()=>{p(!1)},[p]),c.useEffect(()=>{const f=setInterval(()=>{p(!1)},1e4);return()=>clearInterval(f)},[p]);const P=c.useCallback(async f=>{try{await fetch("/api/browser-watch/dismiss",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({host:f.host,startMs:f.startMs})})}finally{await p(!1)}},[p]),R=c.useCallback(async f=>{y.current=!0,b(!0);try{const j=await fetch("/api/browser-watch",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(f)});if(!j.ok)throw new Error(`the deck answered ${j.status}`);await p(!1),a(null)}catch(j){a(j instanceof Error?j.message:String(j))}finally{y.current=!1,b(!1)}},[p]);return{snap:t,error:r,writeError:o,setWriteError:a,busy:n,quiet:i,setQuiet:w,saving:m,load:p,dismiss:P,save:R}}function ye({onClose:s,onSeen:t,onWatching:l,palette:r}){const h=Z(s),{snap:o,error:a,writeError:n,setWriteError:u,busy:d,quiet:i,setQuiet:w,saving:m,load:b,dismiss:y,save:p}=xe(l),[P,R]=c.useState(!1),[f,j]=c.useState(!1),C=c.useRef(null),N=c.useRef(t);N.current=t,c.useEffect(()=>()=>N.current(Date.now()),[]);const v=J(o==null?void 0:o.browsers),I=((o==null?void 0:o.browsers)??[]).filter(k=>!(k.installed&&k.profiles>0)),$=o?we(o):null,W=v.reduce((k,x)=>k+x.profiles,0);return e.jsx("div",{className:"modal-backdrop",onClick:s,role:"presentation",children:e.jsxs("div",{ref:h,className:"modal bw-modal",onClick:k=>k.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":"bw-title",children:[e.jsxs("header",{className:"modal-head",children:[e.jsxs("div",{className:"modal-title",children:[e.jsx("span",{className:"modal-tool-name",id:"bw-title",children:"Browser watch"}),e.jsx("span",{className:"modal-tool-id",children:o?`${W} ${W===1?"profile":"profiles"} · local only`:"local only"})]}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{ref:C,className:"glyph-btn",onClick:()=>void b(!0),...ee(d),"aria-label":"Re-read every profile now",title:"Re-read every profile now",children:d?"…":"↻"}),e.jsx("button",{className:"glyph-btn",onClick:s,"aria-label":"Close (Esc)",title:"Close (Esc)",children:"×"})]})]}),e.jsxs("div",{className:"modal-body bw-body",children:[!o&&!a&&e.jsxs("div",{className:"bw-loading",children:[e.jsx("span",{className:"bw-loading-bar","aria-hidden":!0}),e.jsx("p",{children:"Reading each browser's history. The first look copies the file, which takes a moment."})]}),a&&e.jsx("div",{className:"bw-state",children:e.jsxs("div",{className:"bw-row err",children:[e.jsx("span",{className:"bw-dot","aria-hidden":!0}),e.jsx("span",{className:"bw-row-label",children:"Could not read"}),e.jsx("span",{className:"bw-row-detail",children:a})]})}),o&&$&&e.jsx("p",{className:`bw-trouble ${$.kind}`,role:"status",children:$.text}),o&&e.jsxs("div",{className:"bw-work",children:[e.jsxs("aside",{className:"bw-side",children:[e.jsx(me,{snap:o,watching:v,palette:r}),e.jsx(be,{watching:v,rest:I}),o.relay&&e.jsx(pe,{relay:o.relay})]}),e.jsxs("div",{className:"bw-main",children:[e.jsx(ae,{snap:o,dismiss:y,refreshRef:C}),e.jsx(se,{snap:o})]})]}),o&&P&&e.jsx(fe,{snap:o,quiet:i,setQuiet:w,save:p,access:f,setAccess:j}),n&&e.jsx("div",{className:"bw-state bw-write-err",role:"alert",children:e.jsxs("div",{className:"bw-row err",children:[e.jsx("span",{className:"bw-dot","aria-hidden":!0}),e.jsx("span",{className:"bw-row-label",children:"Could not save that setting"}),e.jsxs("span",{className:"bw-row-detail",children:[n," — the switches below show what is stored, not what you pressed."]}),e.jsx("button",{className:"glyph-btn bw-write-err-x",onClick:()=>u(null),"aria-label":"Dismiss",title:"Dismiss",children:"×"})]})}),o&&e.jsx(ge,{snap:o,saving:m,save:p,why:P,setWhy:R})]})]})})}export{ye as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as U,f as p,r as c,s as Z,p as _,j as a,b as g,c as O,u as D,d as ee,e as se,g as E,h as ae,i as te}from"./index-tVBMXvDv.js";function ne(s){return!s.answer||s.answer.range!==s.want?{phase:"busy",stale:!1}:s.answer.ok?{phase:s.answer.days>0?"chart":"empty",stale:s.loading}:{phase:"error",stale:s.loading}}function I(s){const e=new Map;for(const n of s)for(const o of n.agents??[]){if(!o||typeof o.agent!="string"||o.agent==="")continue;const r=e.get(o.agent)??{id:o.agent,cost:0,tokens:0};r.cost+=Number.isFinite(o.totalCost)?o.totalCost:0,r.tokens+=Number.isFinite(o.totalTokens)?o.totalTokens:0,e.set(o.agent,r)}return[...e.values()].sort((n,o)=>o.cost-n.cost||n.id.localeCompare(o.id))}function oe(s){const e=I([{agents:s}]);return e.length<2?null:e.map(n=>`${U(n.id)} ${p(n.cost)}`).join(" · ")}function F(s){switch(s.toLowerCase()){case"claude":return"var(--model-opus)";case"codex":return"var(--model-codex)";case"gemini":return"var(--model-gemini)";case"copilot":return"var(--model-haiku)";default:return"var(--model-other)"}}function le(s,e){if(!(e>0)||!(s>0))return"0%";const n=s/e*100;return n<.1?"<0.1%":`${n.toFixed(1)}%`}const u=s=>typeof s=="number"&&Number.isFinite(s)?s:0,M=s=>typeof s=="string"?s:"",P=s=>Array.isArray(s)?s.filter(e=>typeof e=="string"):[];function re(s){const e=s??{};return{modelName:M(e.modelName),cost:u(e.cost),inputTokens:u(e.inputTokens),outputTokens:u(e.outputTokens),cacheCreationTokens:u(e.cacheCreationTokens),cacheReadTokens:u(e.cacheReadTokens)}}function ce(s){const e=s??{},n=Array.isArray(e.agents)?e.agents.map(o=>{const r=o??{};return{agent:M(r.agent),totalCost:u(r.totalCost),totalTokens:u(r.totalTokens)}}):void 0;return{period:M(e.period),totalCost:u(e.totalCost),totalTokens:u(e.totalTokens),inputTokens:u(e.inputTokens),outputTokens:u(e.outputTokens),cacheCreationTokens:u(e.cacheCreationTokens),cacheReadTokens:u(e.cacheReadTokens),modelsUsed:P(e.modelsUsed),modelBreakdowns:Array.isArray(e.modelBreakdowns)?e.modelBreakdowns.map(re):[],...n?{agents:n}:{},...e.metadata&&typeof e.metadata=="object"?{metadata:{agents:P(e.metadata.agents)}}:{}}}function ie(s){const e=s??{};return{...e,ok:e.ok===!0,days:Array.isArray(e.days)?e.days.map(ce):void 0}}function T(s){const e=s.toLowerCase();return e.includes("opus")?"var(--model-opus)":e.includes("sonnet")?"var(--model-sonnet)":e.includes("haiku")?"var(--model-haiku)":e.includes("gpt-5")||e.includes("gpt5")?"var(--model-gpt5)":e.includes("gpt")?"var(--model-gpt)":e.includes("gemini")?"var(--model-gemini)":e.includes("codex")?"var(--model-codex)":e.includes("fable")||e.includes("mythos")?"var(--model-fable)":"var(--model-other)"}function ue(s){let e=0,n=0,o=0,r=0;const m=new Map;for(const l of s){e+=l.totalCost,n+=l.totalTokens,o+=l.inputTokens+l.outputTokens,r+=l.cacheReadTokens;for(const d of l.modelBreakdowns)m.set(d.modelName,(m.get(d.modelName)??0)+d.cost)}return{totalCost:e,totalTok:n,inOut:o,cacheRead:r,modelCosts:m}}function de(s){return Array.from(s.entries()).sort((e,n)=>n[1]-e[1])}function q(s){return s.slice().sort((e,n)=>n.cost-e.cost)}function C(s,e){return e>0?s/e*100:0}function he(s){var n,o;return oe(s.agents)??((o=(n=s.metadata)==null?void 0:n.agents)!=null&&o.length?s.metadata.agents.join(" · "):null)}function me(){let s=0;return{begin(){const e=++s;return()=>e===s},cancel(){s++}}}function pe(s){const[e,n]=c.useState(null),[o,r]=c.useState(!1),m=c.useState(me)[0],l=c.useRef(!1),d=(f=!1)=>{if(f&&!Z(l.current))return;const x=m.begin();l.current=f;const k=s;r(!0);const i=`/api/ccusage?since=${_(k)}${f?"&refresh=1":""}`;fetch(i).then(j=>j.json()).then(j=>{x()&&n({range:k,resp:ie(j)})}).catch(()=>{x()&&n({range:k,resp:{ok:!1,reason:"unreachable"}})}).finally(()=>{x()&&(l.current=!1,r(!1))})};return c.useEffect(()=>(d(!1),()=>m.cancel()),[s]),{landed:e,loading:o,reload:()=>d(!0)}}function ge({selectedDay:s}){const e=he(s);return a.jsxs("div",{className:"uh-detail",children:[a.jsxs("div",{className:"uh-detail-head",children:[a.jsx("span",{className:"uh-detail-date",children:s.period}),a.jsx("span",{className:"uh-detail-cost",children:p(s.totalCost)}),e&&a.jsx("span",{className:"uh-detail-agents",title:e,children:e})]}),a.jsxs("div",{className:"uh-detail-mini",children:[a.jsx(y,{label:"input",val:g(s.inputTokens)}),a.jsx(y,{label:"output",val:g(s.outputTokens)}),a.jsx(y,{label:"cache write",val:g(s.cacheCreationTokens)}),a.jsx(y,{label:"cache read",val:g(s.cacheReadTokens)})]}),a.jsx("div",{className:"uh-detail-models",children:q(s.modelBreakdowns).map(n=>{const o=C(n.cost,s.totalCost);return a.jsxs("div",{className:"uh-model-row",title:n.modelName,children:[a.jsxs("span",{className:"uh-model-name",children:[a.jsx("span",{className:"uh-legend-dot",style:{background:T(n.modelName)}}),a.jsx("span",{className:"uh-model-label",children:O(n.modelName)})]}),a.jsx("span",{className:"uh-model-bar",children:a.jsx("span",{className:"uh-model-bar-fill",style:{width:`${o}%`,background:T(n.modelName)}})}),a.jsx("span",{className:"uh-model-cost",children:p(n.cost)})]},n.modelName)})})]})}function y({label:s,val:e}){return a.jsxs("div",{className:"uh-ministat",children:[a.jsx("span",{className:"uh-ministat-val",children:e}),a.jsx("span",{className:"uh-ministat-label",children:s})]})}const fe=[7,14,30,90];function je({onClose:s,providers:e}){var B;const[n,o]=c.useState(30),[r,m]=c.useState(null),{landed:l,loading:d,reload:f}=pe(n),x=c.useRef(null),k=D(s,{focusRef:x}),h=ne({loading:d,want:n,answer:l&&{range:l.range,ok:l.resp.ok===!0,days:((B=l.resp.days)==null?void 0:B.length)??0}}),i=h.phase==="chart"?l.resp.days??[]:[],j=c.useRef(null),G=ee(h.phase!=="error",j),H=c.useMemo(()=>i.reduce((t,N)=>Math.max(t,N.totalCost),0),[i]),S=c.useRef(null);c.useLayoutEffect(()=>{const t=S.current;t&&(t.scrollLeft=t.scrollWidth)},[i]);const{totalCost:w,totalTok:V,inOut:W,cacheRead:z,modelCosts:A}=c.useMemo(()=>ue(i),[i]),J=c.useMemo(()=>de(A),[A]),b=c.useMemo(()=>I(i),[i]),K=b.length>1,L=r?i.find(t=>t.period===r)??null:null;return a.jsx("div",{className:"modal-backdrop",onClick:s,role:"presentation",children:a.jsxs("div",{ref:k,className:"modal uh-modal",onClick:t=>t.stopPropagation(),role:"dialog","aria-modal":"true","aria-label":"Usage history",children:[a.jsxs("header",{className:"uh-head",children:[a.jsxs("div",{className:"uh-titlewrap",children:[a.jsx("div",{className:"uh-title",children:"Usage history"}),a.jsx("div",{className:"uh-sub",children:h.stale?"running ccusage…":se(e,b.map(t=>t.id))})]}),a.jsx("div",{className:"uh-range",role:"group","aria-label":"Range",children:fe.map(t=>a.jsxs("button",{type:"button","aria-pressed":n===t,className:"uh-range-btn",onClick:()=>{o(t),m(null)},children:[t,"d"]},t))}),a.jsx("button",{ref:j,className:"glyph-btn uh-reload",onClick:f,...E(d),title:"Re-run ccusage","aria-label":"Reload",children:d?"…":"↻"}),a.jsx("button",{ref:x,className:"glyph-btn uh-close",onClick:s,"aria-label":"Close",children:"×"})]}),h.phase==="busy"?a.jsxs("div",{className:"uh-status uh-status-wait","aria-busy":"true",children:[a.jsx("span",{className:"uh-wait-word",children:l?"running ccusage…":"running ccusage… (first run downloads the package)"}),a.jsx("span",{className:"uh-wait-axis","aria-hidden":!0})]}):h.phase==="error"?a.jsxs("div",{className:"uh-status uh-err",title:te(l.resp)||void 0,children:[ae(l.resp,"ccusage did not report usage"),a.jsx("div",{children:a.jsx("button",{className:"btn uh-retry",onClick:()=>{G(),f()},...E(d),children:d?"trying…":"Try again"})})]}):h.phase==="empty"?a.jsx("div",{className:`uh-status${h.stale?" uh-stale":""}`,"aria-busy":h.stale||void 0,children:"no usage in this range"}):a.jsxs("div",{className:h.stale?"uh-stale":void 0,"aria-busy":h.stale||void 0,children:[a.jsxs("div",{className:"uh-totals",children:[a.jsx(v,{label:"total cost",val:p(w),accent:!0}),a.jsx(v,{label:"tokens",val:g(V)}),a.jsx(v,{label:"input+output",val:g(W)}),a.jsx(v,{label:"cache reads",val:g(z)})]}),K&&a.jsxs("div",{className:"uh-agents",role:"group","aria-label":"Cost by CLI",children:[a.jsx("div",{className:"uh-agent-bar","aria-hidden":"true",children:b.map(t=>a.jsx("span",{className:"uh-agent-seg",style:{width:`${C(t.cost,w)}%`,background:F(t.id)}},t.id))}),a.jsx("div",{className:"uh-agent-keys",children:b.map(t=>a.jsxs("span",{className:"uh-agent-key",title:`${t.id} · ${g(t.tokens)} tokens`,children:[a.jsx("span",{className:"uh-legend-dot",style:{background:F(t.id)}}),U(t.id),a.jsx("span",{className:"uh-agent-cost",children:p(t.cost)}),a.jsx("span",{className:"uh-agent-share",children:le(t.cost,w)})]},t.id))})]}),a.jsx("div",{ref:S,className:"uh-chart",role:"group","aria-label":"Daily cost by model",children:i.map((t,N)=>{const Q=C(t.totalCost,H),R=t.period===r,X=`${t.period}, ${p(t.totalCost)}`;return a.jsxs("button",{className:`uh-bar-col${R?" sel":""}`,onClick:()=>m(R?null:t.period),"aria-pressed":R,"aria-label":X,title:`${t.period} · ${p(t.totalCost)}`,style:{flexBasis:`${100/i.length}%`,"--uh-i":N,"--uh-step":`${Math.min(10,200/Math.max(i.length,1))}ms`},children:[a.jsx("div",{className:"uh-bar",style:{height:`${Math.max(Q,t.totalCost>0?2:0)}%`},children:q(t.modelBreakdowns).map($=>{const Y=C($.cost,t.totalCost);return a.jsx("div",{className:"uh-bar-seg",style:{height:`${Y}%`,background:T($.modelName)}},$.modelName)})}),a.jsx("span",{className:"uh-bar-label",children:t.period.slice(5)})]},t.period)})}),a.jsx("div",{className:"uh-legend",children:J.map(([t,N])=>a.jsxs("span",{className:"uh-legend-item",title:t,children:[a.jsx("span",{className:"uh-legend-dot",style:{background:T(t)}}),O(t)," ",a.jsx("span",{className:"uh-legend-cost",children:p(N)})]},t))}),L&&a.jsx(ge,{selectedDay:L})]})]})})}function v({label:s,val:e,accent:n}){return a.jsxs("div",{className:"uh-stat",children:[a.jsx("span",{className:`uh-stat-val${n?" accent":""}`,children:e}),a.jsx("span",{className:"uh-stat-label",children:s})]})}export{je as default};
|