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.
@@ -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();