ccdeck 3.36.5 → 3.36.7
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/deck.js +5 -0
- package/dist/web/assets/{BrowserWatchModal-LYnUxuAL.js → BrowserWatchModal-Fzl8O2oN.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-B0qdsys5.js → UsageHistoryModal-BEWKpjC2.js} +1 -1
- package/dist/web/assets/{index-B18vwZ9I.js → index-B1ngNkKH.js} +13 -13
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/release-notes.json +3 -1
- package/src/server/activation.mjs +76 -0
- package/src/server/deck-prefs.mjs +18 -3
- package/src/server/event-pipeline.mjs +13 -1
- package/src/server/output-watch.mjs +1 -14
- package/src/server/reports.mjs +143 -49
- package/src/server/session-enrichment.mjs +0 -15
- package/src/server/session-tracking.mjs +1 -18
- package/src/server/usage-day.mjs +207 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// How much this deck was used on one UTC day: the distinct sessions, subagents
|
|
2
|
+
// and projects its hooks heard from. The "active" report carries the totals of
|
|
3
|
+
// the last finished day (reports.mjs).
|
|
4
|
+
//
|
|
5
|
+
// WHY A DAY AND NOT A SNAPSHOT. The counts used to be read off the live state at
|
|
6
|
+
// the moment the "active" went out — and that moment is the first check-in of
|
|
7
|
+
// the day, which runs at launch, before anybody has done anything. Production
|
|
8
|
+
// on 2026-10-01: of 33 "active" reports carrying counts, the median session
|
|
9
|
+
// count was 0, and 24 said zero sessions. A day's totals, sent the next day,
|
|
10
|
+
// are the number the snapshot was trying to be.
|
|
11
|
+
//
|
|
12
|
+
// WHAT IS KEPT. In memory, the session ids, the subagent ids and the folders the
|
|
13
|
+
// transcripts sit in, so a session heard from a hundred times counts once. They
|
|
14
|
+
// never leave this module: what is saved between runs (prefs.json, beside the
|
|
15
|
+
// install id) and what is sent are three counts and the day they belong to.
|
|
16
|
+
//
|
|
17
|
+
// A RESTART MID-DAY. The saved counts come back as a floor and the new run
|
|
18
|
+
// counts on top of them, so a session that was running across the restart is
|
|
19
|
+
// counted twice. That is the price of keeping no ids on disk, and a restart is
|
|
20
|
+
// rare next to the day's sessions.
|
|
21
|
+
//
|
|
22
|
+
// Only live use counts: a Claude hook fires because somebody is running the
|
|
23
|
+
// CLI, and the Codex watcher emits only what a rollout gains while the deck
|
|
24
|
+
// watches (its first scan skips every file's history). Replayed lines and the
|
|
25
|
+
// deck's own synthetic events are not use.
|
|
26
|
+
|
|
27
|
+
/** The day as the reporter says it: the UTC date, "2026-10-01". */
|
|
28
|
+
export function utcDay(date) {
|
|
29
|
+
return date.toISOString().slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
33
|
+
|
|
34
|
+
/** A whole count of zero or more, else 0. */
|
|
35
|
+
function whole(n) {
|
|
36
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Saved totals, coerced: a day and three counts, or null for anything else. */
|
|
40
|
+
function savedTotals(raw) {
|
|
41
|
+
if (!raw || typeof raw !== "object" || typeof raw.day !== "string" || !DAY.test(raw.day)) return null;
|
|
42
|
+
return { day: raw.day, sessions: whole(raw.sessions), subagents: whole(raw.subagents), projects: whole(raw.projects) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What prefs.json keeps for this, coerced — deck-prefs.mjs normalises with it. */
|
|
46
|
+
export function normaliseUsage(raw) {
|
|
47
|
+
const src = raw && typeof raw === "object" ? raw : {};
|
|
48
|
+
return {
|
|
49
|
+
current: savedTotals(src.current),
|
|
50
|
+
done: savedTotals(src.done),
|
|
51
|
+
sent: typeof src.sent === "string" && DAY.test(src.sent) ? src.sent : "",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The folder a transcript sits in, which is the project it belongs to:
|
|
56
|
+
* `~/.claude/projects/<project>/<session>.jsonl`. */
|
|
57
|
+
function folderOf(path) {
|
|
58
|
+
const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
59
|
+
return cut > 0 ? path.slice(0, cut) : path;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The tally. `now` is injectable so the suite can walk it across midnight.
|
|
64
|
+
*/
|
|
65
|
+
export function createUsageDay({ now = () => new Date() } = {}) {
|
|
66
|
+
/** The day being counted: saved counts from an earlier run as a floor, and
|
|
67
|
+
* this run's distinct ids on top. */
|
|
68
|
+
let current = null;
|
|
69
|
+
/** The last finished day whose totals have not gone out yet. */
|
|
70
|
+
let done = null;
|
|
71
|
+
/** The last day whose totals did go out. */
|
|
72
|
+
let sent = "";
|
|
73
|
+
/** Moves on every change, so the reporter saves only when there is something new. */
|
|
74
|
+
let changes = 0;
|
|
75
|
+
/** The first use this run heard, `{ at, provider }`, for activation.mjs. */
|
|
76
|
+
let first = null;
|
|
77
|
+
const firstListeners = [];
|
|
78
|
+
|
|
79
|
+
function fresh(day, floor = null) {
|
|
80
|
+
return {
|
|
81
|
+
day,
|
|
82
|
+
floor: floor ?? { sessions: 0, subagents: 0, projects: 0 },
|
|
83
|
+
sessions: new Set(),
|
|
84
|
+
subagents: new Set(),
|
|
85
|
+
projects: new Set(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function totals(c) {
|
|
90
|
+
return {
|
|
91
|
+
day: c.day,
|
|
92
|
+
sessions: c.floor.sessions + c.sessions.size,
|
|
93
|
+
subagents: c.floor.subagents + c.subagents.size,
|
|
94
|
+
projects: c.floor.projects + c.projects.size,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The day being counted, rolled over first if the clock has passed it. */
|
|
99
|
+
function today() {
|
|
100
|
+
const day = utcDay(now());
|
|
101
|
+
if (current?.day === day) return current;
|
|
102
|
+
if (current && current.day < day) done = totals(current);
|
|
103
|
+
current = fresh(day);
|
|
104
|
+
changes++;
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function add(set, key) {
|
|
109
|
+
if (set.has(key)) return;
|
|
110
|
+
set.add(key);
|
|
111
|
+
changes++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A live payload: its session, and the subagent it came from if any. */
|
|
115
|
+
function noteUse(raw) {
|
|
116
|
+
const sid = raw?.session_id;
|
|
117
|
+
if (!sid || typeof sid !== "string") return;
|
|
118
|
+
const c = today();
|
|
119
|
+
add(c.sessions, sid);
|
|
120
|
+
if (typeof raw.agent_id === "string" && raw.agent_id) add(c.subagents, `${sid}\u0000${raw.agent_id}`);
|
|
121
|
+
if (!first) {
|
|
122
|
+
first = { at: now().toISOString(), provider: raw.provider === "codex" ? "codex" : "claude" };
|
|
123
|
+
for (const fn of firstListeners) {
|
|
124
|
+
try { fn(first); } catch { /* a listener's failure is its own */ }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A transcript path a Claude hook carried, already accepted as Claude's:
|
|
130
|
+
* the project is the folder it sits in. */
|
|
131
|
+
function noteProject(path) {
|
|
132
|
+
if (!path || typeof path !== "string") return;
|
|
133
|
+
add(today().projects, folderOf(path));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A Codex session's working directory, which is its project. */
|
|
137
|
+
function noteFolder(dir) {
|
|
138
|
+
if (!dir || typeof dir !== "string") return;
|
|
139
|
+
add(today().projects, dir);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The first use this run heard, or null. */
|
|
143
|
+
function firstUse() {
|
|
144
|
+
return first ? { ...first } : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Call `fn` with the first use, once, when it happens — or now, if it has. */
|
|
148
|
+
function onFirstUse(fn) {
|
|
149
|
+
if (first) {
|
|
150
|
+
try { fn(first); } catch { /* as above */ }
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
firstListeners.push(fn);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The totals of the last finished day before `day`, if they have not gone
|
|
157
|
+
* out yet; null when there is none. Rolls over first, so a deck that ran past
|
|
158
|
+
* midnight with nothing said since still has yesterday finished. */
|
|
159
|
+
function finished(day) {
|
|
160
|
+
today();
|
|
161
|
+
const c = current && current.day < day ? totals(current) : done;
|
|
162
|
+
return c && c.day < day && c.day > sent ? c : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Those totals went out: never send them again. */
|
|
166
|
+
function markSent(day) {
|
|
167
|
+
if (day > sent) sent = day;
|
|
168
|
+
if (done && done.day <= sent) done = null;
|
|
169
|
+
changes++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** What survives a restart: counts and days, never an id or a path. */
|
|
173
|
+
function saved() {
|
|
174
|
+
return { current: current ? totals(current) : null, done, sent };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* An earlier run's `saved()`, merged into this one. This run may already have
|
|
179
|
+
* heard from sessions — the prefs are read after the first hooks can land —
|
|
180
|
+
* so nothing it counted is dropped: the saved day becomes the floor of the
|
|
181
|
+
* same day, or the finished day of an earlier one.
|
|
182
|
+
*/
|
|
183
|
+
function restore(raw) {
|
|
184
|
+
const s = normaliseUsage(raw);
|
|
185
|
+
if (s.sent > sent) sent = s.sent;
|
|
186
|
+
for (const t of [s.done, s.current]) {
|
|
187
|
+
if (!t) continue;
|
|
188
|
+
const c = today();
|
|
189
|
+
if (t.day === c.day) {
|
|
190
|
+
c.floor = { sessions: t.sessions, subagents: t.subagents, projects: t.projects };
|
|
191
|
+
} else if (t.day < c.day && (!done || t.day > done.day)) {
|
|
192
|
+
done = t;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (done && done.day <= sent) done = null;
|
|
196
|
+
changes++;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function version() {
|
|
200
|
+
return changes;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { noteUse, noteProject, noteFolder, firstUse, onFirstUse, finished, markSent, saved, restore, version };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The deck's own tally, which the event pipeline feeds and the reporter reads. */
|
|
207
|
+
export const usageDay = createUsageDay();
|