ccdeck 3.36.7 → 3.36.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli/leaving.js +54 -0
- package/bin/cli/uninstall.js +9 -0
- package/dist/web/assets/{BrowserWatchModal-Fzl8O2oN.js → BrowserWatchModal-BCrDvuug.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-BEWKpjC2.js → UsageHistoryModal-BtRCuo33.js} +1 -1
- package/dist/web/assets/index-tVBMXvDv.js +306 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/release-notes.json +3 -1
- package/src/server/feature-use.mjs +42 -0
- package/src/server/index.mjs +11 -0
- package/src/server/reports.mjs +42 -3
- package/src/server/usage-day.mjs +49 -6
- package/dist/web/assets/index-B1ngNkKH.js +0 -306
package/dist/web/index.html
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
|
|
65
65
|
})();
|
|
66
66
|
</script>
|
|
67
|
-
<script type="module" crossorigin src="/assets/index-
|
|
67
|
+
<script type="module" crossorigin src="/assets/index-tVBMXvDv.js"></script>
|
|
68
68
|
<link rel="stylesheet" crossorigin href="/assets/index-BkXTN4dZ.css">
|
|
69
69
|
</head>
|
|
70
70
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.36.
|
|
3
|
+
"version": "3.36.9",
|
|
4
4
|
"description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Which features got used today, for the "active" report's `features` (see
|
|
2
|
+
// usage-day.mjs for the list and reports.mjs for what leaves).
|
|
3
|
+
//
|
|
4
|
+
// Two doors in. The page names what it saw opened — a panel shown, a dialog
|
|
5
|
+
// opened — through POST /api/feature, once per name per day (feature-use.ts).
|
|
6
|
+
// The server names what a request did: the routes below are actions somebody
|
|
7
|
+
// takes on purpose, never something a page polls, so the request is the use.
|
|
8
|
+
// Either way only a name off FEATURES is kept, and only as "used today".
|
|
9
|
+
import { readBody, send } from "./http-io.mjs";
|
|
10
|
+
import { usageDay } from "./usage-day.mjs";
|
|
11
|
+
|
|
12
|
+
/** The requests that are a feature being used. Only actions: a read the page
|
|
13
|
+
* makes on its own (the quota, the roster, a radio probe) says nothing about
|
|
14
|
+
* what somebody chose to do. */
|
|
15
|
+
const ROUTE_FEATURES = new Map([
|
|
16
|
+
["POST /api/claude-accounts/switch", "account-switch"],
|
|
17
|
+
["POST /api/claude-accounts/admin", "accounts-manage"],
|
|
18
|
+
["POST /api/cswap-auto", "auto-switch"],
|
|
19
|
+
["POST /api/lan/invite", "lan-pairing"],
|
|
20
|
+
["POST /api/lan/peer", "lan-pairing"],
|
|
21
|
+
["POST /api/feedback", "feedback-sent"],
|
|
22
|
+
["POST /api/upgrade", "self-update"],
|
|
23
|
+
["POST /api/clear", "clear-board"],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Note the feature a request is, if it is one. Called for requests that got
|
|
27
|
+
* past the deck's own gates. */
|
|
28
|
+
export function noteRouteFeature(method, pathname) {
|
|
29
|
+
const name = ROUTE_FEATURES.get(`${method} ${pathname}`);
|
|
30
|
+
if (name) usageDay.noteFeature(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** POST /api/feature — the page saying it showed or opened something.
|
|
34
|
+
* `{ name }`; a name off the list is a 400, and nothing about it is kept. */
|
|
35
|
+
export async function handleFeature(req, res) {
|
|
36
|
+
const raw = await readBody(req).catch(() => null);
|
|
37
|
+
let body = null;
|
|
38
|
+
try { body = JSON.parse(raw ?? ""); } catch { /* handled below */ }
|
|
39
|
+
const name = typeof body?.name === "string" ? body.name : "";
|
|
40
|
+
if (!usageDay.noteFeature(name)) return send(res, 400, { ok: false, reason: "unknown_feature" });
|
|
41
|
+
return send(res, 200, { ok: true });
|
|
42
|
+
}
|
package/src/server/index.mjs
CHANGED
|
@@ -63,6 +63,8 @@ import { serveStatic } from "./static-serve.mjs";
|
|
|
63
63
|
// transcript watch between hook events, and POST /api/forget. See
|
|
64
64
|
// session-tracking.mjs.
|
|
65
65
|
import { handleForget, startOutputWatch } from "./session-tracking.mjs";
|
|
66
|
+
// Which features were used today, for the usage reports — see feature-use.mjs.
|
|
67
|
+
import { handleFeature, noteRouteFeature } from "./feature-use.mjs";
|
|
66
68
|
// Exported from this file before it moved, and still.
|
|
67
69
|
export { HARD_TRACKED_SESSIONS } from "./session-tracking.mjs";
|
|
68
70
|
// The desktop app's update as its window sees it — the state the app reports
|
|
@@ -285,6 +287,11 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
285
287
|
return send(res, 401, { error: "unauthenticated" });
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
// A request that is somebody using a feature on purpose — switching an
|
|
291
|
+
// account, pairing a deck — counts as that feature used today. Past the
|
|
292
|
+
// gates above, so only the deck's own callers count. See feature-use.mjs.
|
|
293
|
+
noteRouteFeature(req.method, url.pathname);
|
|
294
|
+
|
|
288
295
|
// `?persist=0` — another deck was elected to write this event to the log
|
|
289
296
|
// the two of them share. Absent, this deck writes it.
|
|
290
297
|
if (req.method === "POST" && url.pathname === "/api/event") return guard(handleEventIngest(req, res, url.searchParams.get("persist") !== "0"), res);
|
|
@@ -387,6 +394,10 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
387
394
|
// Above the 404 below, which is what every real route has to be.
|
|
388
395
|
if (req.method === "POST" && url.pathname === "/api/forget") return guard(handleForget(req, res), res);
|
|
389
396
|
|
|
397
|
+
// POST /api/feature — the page saying it showed a panel or opened a dialog,
|
|
398
|
+
// once per name per day. A name, never what was in it. See feature-use.mjs.
|
|
399
|
+
if (req.method === "POST" && url.pathname === "/api/feature") return guard(handleFeature(req, res), res);
|
|
400
|
+
|
|
390
401
|
// AN UNMATCHED /api/ PATH IS A 404, not the SPA shell.
|
|
391
402
|
//
|
|
392
403
|
// serveStatic falls back to index.html for a path it cannot find, which is
|
package/src/server/reports.mjs
CHANGED
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
// ccdeck gets counted without counting anything else about them, and which
|
|
14
14
|
// alone also carries coarse counts of how much: how many sessions,
|
|
15
15
|
// subagents and projects the deck heard from on the last day it was used,
|
|
16
|
-
// and that day's date — numbers, never their names (see usage-day.mjs)
|
|
16
|
+
// and that day's date — numbers, never their names (see usage-day.mjs) —
|
|
17
|
+
// and which of the deck's features were used that day, as fixed names
|
|
18
|
+
// ("usage-history", "account-switch") and nothing about what was done
|
|
19
|
+
// with them (feature-use.mjs),
|
|
20
|
+
// - an "uninstall" event when somebody runs `ccdeck --uninstall`, with the
|
|
21
|
+
// reason they picked from a short list if they picked one (bin/cli/leaving.js),
|
|
17
22
|
// - an "activated" event, once, when a new install's first session arrives:
|
|
18
23
|
// how long after the install it came, as a bucket ("5m", "1h", "1d", "7d",
|
|
19
24
|
// "later"), and whether it was a Claude or a Codex session (activation.mjs),
|
|
@@ -69,7 +74,7 @@ import { reportsVetoed } from "./deck-prefs.mjs";
|
|
|
69
74
|
import { isGitCheckout } from "./install-layout.mjs";
|
|
70
75
|
import { heldPrefs, prefsRead } from "./prefs-state.mjs";
|
|
71
76
|
import { RUNNING_VERSION } from "./running-version.mjs";
|
|
72
|
-
import { usageDay, utcDay } from "./usage-day.mjs";
|
|
77
|
+
import { FEATURES, usageDay, utcDay } from "./usage-day.mjs";
|
|
73
78
|
import { setupFacts, sinceInstallBucket, whenSetupKnown } from "./activation.mjs";
|
|
74
79
|
|
|
75
80
|
export const REPORTS_API = "https://api.ccdeck.dev";
|
|
@@ -89,6 +94,10 @@ const SAME_ERROR_QUIET_MS = 60 * 60 * 1000;
|
|
|
89
94
|
const MESSAGE_MAX = 500;
|
|
90
95
|
const STACK_MAX = 4000;
|
|
91
96
|
|
|
97
|
+
/** Why somebody uninstalled, when they said: the answers `ccdeck --uninstall`
|
|
98
|
+
* offers (bin/cli/leaving.js), and nothing else ever leaves as a reason. */
|
|
99
|
+
export const UNINSTALL_REASONS = Object.freeze(["not-useful", "too-noisy", "broken", "other-tool", "privacy", "other"]);
|
|
100
|
+
|
|
92
101
|
const EMPTY_REPORT = Object.freeze({
|
|
93
102
|
installId: "", lastVersion: "", lastActiveDay: "", forget: "", usage: null,
|
|
94
103
|
installedAt: "", firstSessionAt: "", firstProvider: "", activationSent: false,
|
|
@@ -394,6 +403,9 @@ export function createReporter({
|
|
|
394
403
|
addIf(out, "sessions", count(raw.sessions));
|
|
395
404
|
addIf(out, "subagents", count(raw.subagents));
|
|
396
405
|
addIf(out, "projects", count(raw.projects));
|
|
406
|
+
// The features used that day, as names off the fixed list — an empty list
|
|
407
|
+
// is a real answer (the deck ran, nothing was opened), not a missing one.
|
|
408
|
+
out.features = Array.isArray(raw.features) ? raw.features.filter(f => FEATURES.includes(f)) : [];
|
|
397
409
|
return out;
|
|
398
410
|
} catch {
|
|
399
411
|
return {};
|
|
@@ -537,6 +549,33 @@ export function createReporter({
|
|
|
537
549
|
if (await call("POST", "/v1/app/events", body)) await remember(r.installId, { activationSent: true });
|
|
538
550
|
}
|
|
539
551
|
|
|
552
|
+
/** Whether a report would go out right now: reports on, the machine not
|
|
553
|
+
* vetoing them, and an install id to send under. `ccdeck --uninstall` asks
|
|
554
|
+
* before it asks the person anything. Never throws. */
|
|
555
|
+
async function willReport() {
|
|
556
|
+
try {
|
|
557
|
+
await Promise.resolve(ready).catch(() => {});
|
|
558
|
+
const p = prefs.current();
|
|
559
|
+
return reportsOn(p, env) && Boolean(p.report.installId);
|
|
560
|
+
} catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** The install leaving: `ccdeck --uninstall`, with the reason picked off
|
|
566
|
+
* UNINSTALL_REASONS if one was. The id stays: a deck run again later is the
|
|
567
|
+
* same install coming back. Resolves to whether it got through; never throws. */
|
|
568
|
+
async function reportUninstall(reason) {
|
|
569
|
+
try {
|
|
570
|
+
if (!(await willReport())) return false;
|
|
571
|
+
const body = { installId: prefs.current().report.installId, kind: "uninstall", ...factsNow() };
|
|
572
|
+
addIf(body, "reason", UNINSTALL_REASONS.includes(reason) ? reason : undefined);
|
|
573
|
+
return await call("POST", "/v1/app/events", body);
|
|
574
|
+
} catch {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
540
579
|
/** The first session this run heard, sent the moment it lands rather than at
|
|
541
580
|
* the next check-in, hours later or on a next launch that may never come. */
|
|
542
581
|
async function onFirstSession() {
|
|
@@ -637,7 +676,7 @@ export function createReporter({
|
|
|
637
676
|
pingTimer = null;
|
|
638
677
|
}
|
|
639
678
|
|
|
640
|
-
return { checkIn, ping, setReports, reportError, restoreUsage, saveUsage, sendActivation, start, stop };
|
|
679
|
+
return { checkIn, ping, setReports, reportError, restoreUsage, saveUsage, sendActivation, willReport, reportUninstall, start, stop };
|
|
641
680
|
}
|
|
642
681
|
|
|
643
682
|
// ── the deck's own providers ─────────────────────────────────────────────────
|
package/src/server/usage-day.mjs
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
// WHAT IS KEPT. In memory, the session ids, the subagent ids and the folders the
|
|
13
13
|
// transcripts sit in, so a session heard from a hundred times counts once. They
|
|
14
14
|
// never leave this module: what is saved between runs (prefs.json, beside the
|
|
15
|
-
// install id) and what is sent are three counts
|
|
15
|
+
// install id) and what is sent are three counts, the day they belong to, and
|
|
16
|
+
// the names of the features used that day — fixed tokens off FEATURES, a yes
|
|
17
|
+
// for each, never what was done with them.
|
|
16
18
|
//
|
|
17
19
|
// A RESTART MID-DAY. The saved counts come back as a floor and the new run
|
|
18
20
|
// counts on top of them, so a session that was running across the restart is
|
|
@@ -24,6 +26,34 @@
|
|
|
24
26
|
// watches (its first scan skips every file's history). Replayed lines and the
|
|
25
27
|
// deck's own synthetic events are not use.
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The features a day can say were used — fixed tokens, never anything a caller
|
|
31
|
+
* makes up. The page names the ones it sees opened (POST /api/feature, through
|
|
32
|
+
* feature-use.ts), the server the ones a route or a session shows. A name not on
|
|
33
|
+
* this list is dropped wherever it comes from.
|
|
34
|
+
*/
|
|
35
|
+
export const FEATURES = Object.freeze([
|
|
36
|
+
// Panels the page showed that day (each can be closed, and the choice is kept).
|
|
37
|
+
"usage-panel", "machine-panel", "detail-panel", "accounts-panel",
|
|
38
|
+
// Dialogs somebody opened.
|
|
39
|
+
"tool-detail", "session-summary", "context-window", "usage-history", "browser-watch",
|
|
40
|
+
"feedback", "keyboard-help", "share-accounts", "account-projects", "add-account",
|
|
41
|
+
"process-list", "lan-setup", "claude-fm",
|
|
42
|
+
// What the server saw done.
|
|
43
|
+
"account-switch", "accounts-manage", "auto-switch", "lan-pairing", "feedback-sent",
|
|
44
|
+
"self-update", "clear-board",
|
|
45
|
+
// Which CLI the day's sessions came from.
|
|
46
|
+
"claude-sessions", "codex-sessions",
|
|
47
|
+
]);
|
|
48
|
+
const FEATURE_SET = new Set(FEATURES);
|
|
49
|
+
|
|
50
|
+
/** Only known feature names, once each, in the list's order. */
|
|
51
|
+
function knownFeatures(list) {
|
|
52
|
+
if (!Array.isArray(list)) return [];
|
|
53
|
+
const have = new Set(list.filter(f => FEATURE_SET.has(f)));
|
|
54
|
+
return FEATURES.filter(f => have.has(f));
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
/** The day as the reporter says it: the UTC date, "2026-10-01". */
|
|
28
58
|
export function utcDay(date) {
|
|
29
59
|
return date.toISOString().slice(0, 10);
|
|
@@ -36,10 +66,13 @@ function whole(n) {
|
|
|
36
66
|
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
37
67
|
}
|
|
38
68
|
|
|
39
|
-
/** Saved totals, coerced: a day and
|
|
69
|
+
/** Saved totals, coerced: a day, three counts and the features used, or null for anything else. */
|
|
40
70
|
function savedTotals(raw) {
|
|
41
71
|
if (!raw || typeof raw !== "object" || typeof raw.day !== "string" || !DAY.test(raw.day)) return null;
|
|
42
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
day: raw.day, sessions: whole(raw.sessions), subagents: whole(raw.subagents), projects: whole(raw.projects),
|
|
74
|
+
features: knownFeatures(raw.features),
|
|
75
|
+
};
|
|
43
76
|
}
|
|
44
77
|
|
|
45
78
|
/** What prefs.json keeps for this, coerced — deck-prefs.mjs normalises with it. */
|
|
@@ -79,10 +112,11 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
79
112
|
function fresh(day, floor = null) {
|
|
80
113
|
return {
|
|
81
114
|
day,
|
|
82
|
-
floor: floor ?? { sessions: 0, subagents: 0, projects: 0 },
|
|
115
|
+
floor: floor ?? { sessions: 0, subagents: 0, projects: 0, features: [] },
|
|
83
116
|
sessions: new Set(),
|
|
84
117
|
subagents: new Set(),
|
|
85
118
|
projects: new Set(),
|
|
119
|
+
features: new Set(),
|
|
86
120
|
};
|
|
87
121
|
}
|
|
88
122
|
|
|
@@ -92,6 +126,7 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
92
126
|
sessions: c.floor.sessions + c.sessions.size,
|
|
93
127
|
subagents: c.floor.subagents + c.subagents.size,
|
|
94
128
|
projects: c.floor.projects + c.projects.size,
|
|
129
|
+
features: knownFeatures([...c.floor.features, ...c.features]),
|
|
95
130
|
};
|
|
96
131
|
}
|
|
97
132
|
|
|
@@ -118,6 +153,7 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
118
153
|
const c = today();
|
|
119
154
|
add(c.sessions, sid);
|
|
120
155
|
if (typeof raw.agent_id === "string" && raw.agent_id) add(c.subagents, `${sid}\u0000${raw.agent_id}`);
|
|
156
|
+
add(c.features, raw.provider === "codex" ? "codex-sessions" : "claude-sessions");
|
|
121
157
|
if (!first) {
|
|
122
158
|
first = { at: now().toISOString(), provider: raw.provider === "codex" ? "codex" : "claude" };
|
|
123
159
|
for (const fn of firstListeners) {
|
|
@@ -133,6 +169,13 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
133
169
|
add(today().projects, folderOf(path));
|
|
134
170
|
}
|
|
135
171
|
|
|
172
|
+
/** A feature somebody used today — a name off FEATURES, or nothing. */
|
|
173
|
+
function noteFeature(name) {
|
|
174
|
+
if (!FEATURE_SET.has(name)) return false;
|
|
175
|
+
add(today().features, name);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
136
179
|
/** A Codex session's working directory, which is its project. */
|
|
137
180
|
function noteFolder(dir) {
|
|
138
181
|
if (!dir || typeof dir !== "string") return;
|
|
@@ -187,7 +230,7 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
187
230
|
if (!t) continue;
|
|
188
231
|
const c = today();
|
|
189
232
|
if (t.day === c.day) {
|
|
190
|
-
c.floor = { sessions: t.sessions, subagents: t.subagents, projects: t.projects };
|
|
233
|
+
c.floor = { sessions: t.sessions, subagents: t.subagents, projects: t.projects, features: t.features };
|
|
191
234
|
} else if (t.day < c.day && (!done || t.day > done.day)) {
|
|
192
235
|
done = t;
|
|
193
236
|
}
|
|
@@ -200,7 +243,7 @@ export function createUsageDay({ now = () => new Date() } = {}) {
|
|
|
200
243
|
return changes;
|
|
201
244
|
}
|
|
202
245
|
|
|
203
|
-
return { noteUse, noteProject, noteFolder, firstUse, onFirstUse, finished, markSent, saved, restore, version };
|
|
246
|
+
return { noteUse, noteProject, noteFolder, noteFeature, firstUse, onFirstUse, finished, markSent, saved, restore, version };
|
|
204
247
|
}
|
|
205
248
|
|
|
206
249
|
/** The deck's own tally, which the event pipeline feeds and the reporter reads. */
|