ccdeck 3.29.11 → 3.31.0

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.
Files changed (178) hide show
  1. package/README.md +4 -3
  2. package/bin/agent-dag.js +67 -118
  3. package/bin/cli/help.js +67 -0
  4. package/bin/cli/login-item.js +182 -0
  5. package/bin/cli/one-shot.js +254 -0
  6. package/bin/cli/package.js +39 -0
  7. package/bin/cli/pulse.js +98 -0
  8. package/bin/cli/restart.js +179 -0
  9. package/bin/cli/screen.js +166 -0
  10. package/bin/cli/second-start.js +120 -0
  11. package/bin/cli/startup.js +459 -0
  12. package/bin/cli/uninstall.js +169 -0
  13. package/bin/deck.js +112 -1559
  14. package/dist/web/assets/BrowserWatchModal-n63GjLhv.js +1 -0
  15. package/dist/web/assets/UsageHistoryModal-DEwBX4Fk.js +1 -0
  16. package/dist/web/assets/index-CZJf1Cpp.js +299 -0
  17. package/dist/web/assets/index-D4q1ElXU.css +1 -0
  18. package/dist/web/index.html +2 -2
  19. package/hook/hook.js +15 -14
  20. package/package.json +1 -1
  21. package/release-notes.json +40 -0
  22. package/src/server/account-projects.mjs +25 -11
  23. package/src/server/account-routes.mjs +254 -0
  24. package/src/server/activity.mjs +3 -3
  25. package/src/server/append-budget.mjs +185 -0
  26. package/src/server/append-failures.mjs +147 -0
  27. package/src/server/args.mjs +37 -5
  28. package/src/server/atomic-write.mjs +300 -0
  29. package/src/server/block-notify.mjs +0 -5
  30. package/src/server/boot-lock.mjs +1 -1
  31. package/src/server/brand.mjs +7 -6
  32. package/src/server/browser-react.mjs +10 -2
  33. package/src/server/browser-watch-decks.mjs +146 -0
  34. package/src/server/browser-watch-feed.mjs +69 -0
  35. package/src/server/browser-watch-log.mjs +173 -0
  36. package/src/server/browser-watch-mtime.mjs +18 -0
  37. package/src/server/browser-watch-relay.mjs +112 -0
  38. package/src/server/browser-watch-routes.mjs +172 -0
  39. package/src/server/browser-watch-seen.mjs +127 -0
  40. package/src/server/browser-watch-store.mjs +14 -143
  41. package/src/server/browser-watch.mjs +278 -626
  42. package/src/server/canonical-path.mjs +150 -0
  43. package/src/server/ccusage-failure.mjs +64 -0
  44. package/src/server/ccusage-install.mjs +459 -0
  45. package/src/server/ccusage-runner.mjs +271 -0
  46. package/src/server/ccusage.mjs +21 -756
  47. package/src/server/claude-accounts.mjs +220 -464
  48. package/src/server/claude-collector.mjs +189 -0
  49. package/src/server/claude-dir.mjs +7 -3
  50. package/src/server/claude-identity.mjs +5 -4
  51. package/src/server/claude-reset-credits.mjs +130 -0
  52. package/src/server/claude-verdicts.mjs +174 -0
  53. package/src/server/clear-route.mjs +191 -0
  54. package/src/server/codex-auth.mjs +4 -4
  55. package/src/server/codex-dir.mjs +52 -8
  56. package/src/server/codex-enrichment.mjs +310 -0
  57. package/src/server/codex-quota.mjs +5 -3
  58. package/src/server/codex-translate.mjs +407 -0
  59. package/src/server/codex-usage.mjs +8 -5
  60. package/src/server/codex-watch.mjs +496 -0
  61. package/src/server/cswap-auto-loop.mjs +342 -0
  62. package/src/server/cswap-auto-readers.mjs +303 -0
  63. package/src/server/cswap-auto.mjs +51 -609
  64. package/src/server/cswap-install.mjs +69 -265
  65. package/src/server/cswap-layout.mjs +242 -0
  66. package/src/server/deck-home.mjs +2 -2
  67. package/src/server/deck-prefs.mjs +58 -184
  68. package/src/server/deck-scope.mjs +52 -0
  69. package/src/server/desktop-update-routes.mjs +75 -0
  70. package/src/server/detach.mjs +78 -0
  71. package/src/server/discovery.mjs +298 -0
  72. package/src/server/event-log.mjs +328 -0
  73. package/src/server/event-pipeline.mjs +235 -0
  74. package/src/server/event-ring.mjs +214 -0
  75. package/src/server/event-routes.mjs +352 -0
  76. package/src/server/event-sink.mjs +26 -0
  77. package/src/server/exec-children.mjs +128 -0
  78. package/src/server/exec-not-found.mjs +213 -0
  79. package/src/server/exec-output.mjs +23 -0
  80. package/src/server/exec-spec.mjs +341 -0
  81. package/src/server/exec.mjs +74 -661
  82. package/src/server/health-route.mjs +42 -0
  83. package/src/server/hook-challenge.mjs +37 -0
  84. package/src/server/http-io.mjs +104 -0
  85. package/src/server/index.mjs +174 -7620
  86. package/src/server/install-layout.mjs +506 -0
  87. package/src/server/installer.mjs +79 -608
  88. package/src/server/jsonl-chunks.mjs +139 -0
  89. package/src/server/lan-accounts.mjs +27 -0
  90. package/src/server/lan-beacon.mjs +457 -0
  91. package/src/server/lan-call.mjs +239 -0
  92. package/src/server/lan-copies.mjs +270 -0
  93. package/src/server/lan-deck.mjs +583 -0
  94. package/src/server/lan-dials.mjs +161 -0
  95. package/src/server/lan-engine.mjs +475 -1024
  96. package/src/server/lan-hearing.mjs +49 -0
  97. package/src/server/lan-inbound.mjs +100 -0
  98. package/src/server/lan-invite-offer.mjs +77 -0
  99. package/src/server/lan-invite.mjs +207 -0
  100. package/src/server/lan-lines.mjs +73 -0
  101. package/src/server/lan-manifest.mjs +74 -0
  102. package/src/server/lan-reach.mjs +82 -69
  103. package/src/server/lan-requests.mjs +249 -0
  104. package/src/server/lan-round-record.mjs +45 -0
  105. package/src/server/lan-round-timer.mjs +62 -0
  106. package/src/server/lan-routes.mjs +143 -0
  107. package/src/server/lan-serve.mjs +112 -0
  108. package/src/server/lan-socket.mjs +243 -925
  109. package/src/server/lan-sync.mjs +58 -1026
  110. package/src/server/lan-tailnet-poll.mjs +57 -0
  111. package/src/server/lan-turns.mjs +80 -0
  112. package/src/server/lan-wire.mjs +620 -0
  113. package/src/server/lifecycle.mjs +421 -0
  114. package/src/server/listen.mjs +110 -0
  115. package/src/server/live-decks.mjs +138 -0
  116. package/src/server/load-average.mjs +32 -0
  117. package/src/server/log-election.mjs +235 -0
  118. package/src/server/log-replay.mjs +374 -0
  119. package/src/server/log-writer.mjs +72 -557
  120. package/src/server/login-service.mjs +3 -2
  121. package/src/server/macmon.mjs +1 -1
  122. package/src/server/memory-files.mjs +163 -0
  123. package/src/server/memory-metrics.mjs +178 -0
  124. package/src/server/metrics-history.mjs +94 -0
  125. package/src/server/metrics-run.mjs +100 -0
  126. package/src/server/music-routes.mjs +134 -0
  127. package/src/server/network-metrics.mjs +5 -3
  128. package/src/server/network-sampler.mjs +256 -0
  129. package/src/server/npm-latest.mjs +385 -0
  130. package/src/server/npm-upgrade.mjs +352 -0
  131. package/src/server/npx.mjs +41 -7
  132. package/src/server/output-watch.mjs +5 -4
  133. package/src/server/pinned-build.mjs +110 -0
  134. package/src/server/prefs-give-back.mjs +206 -0
  135. package/src/server/prefs-path.mjs +14 -0
  136. package/src/server/prefs-refusal.mjs +86 -0
  137. package/src/server/prefs-routes.mjs +99 -0
  138. package/src/server/prefs-set-aside.mjs +86 -0
  139. package/src/server/prefs-state.mjs +70 -0
  140. package/src/server/process-command.mjs +104 -0
  141. package/src/server/process-list.mjs +547 -0
  142. package/src/server/provider-status.mjs +307 -0
  143. package/src/server/pulse-line.mjs +132 -0
  144. package/src/server/purge-key.mjs +7 -7
  145. package/src/server/quota-cli.mjs +174 -0
  146. package/src/server/quota-oauth.mjs +190 -0
  147. package/src/server/quota-shape.mjs +229 -0
  148. package/src/server/quota-store-resets.mjs +206 -0
  149. package/src/server/quota.mjs +123 -509
  150. package/src/server/relay-guard.mjs +3 -3
  151. package/src/server/request-gates.mjs +441 -0
  152. package/src/server/restart-note.mjs +180 -0
  153. package/src/server/retire-sound-hook.mjs +3 -3
  154. package/src/server/ring-bounds.mjs +215 -0
  155. package/src/server/running-deck.mjs +1 -1
  156. package/src/server/running-version.mjs +24 -0
  157. package/src/server/self-update.mjs +39 -1371
  158. package/src/server/session-enrichment.mjs +622 -0
  159. package/src/server/session-read-gate.mjs +49 -0
  160. package/src/server/session-tracking.mjs +242 -0
  161. package/src/server/sse-clients.mjs +222 -0
  162. package/src/server/stale-discovery.mjs +30 -0
  163. package/src/server/static-serve.mjs +81 -0
  164. package/src/server/supervisor.mjs +73 -12
  165. package/src/server/system-metrics.mjs +169 -1935
  166. package/src/server/term.mjs +11 -249
  167. package/src/server/thermal-metrics.mjs +520 -0
  168. package/src/server/thermal-sampler.mjs +204 -0
  169. package/src/server/token-redact.mjs +123 -0
  170. package/src/server/transcript-gate.mjs +107 -0
  171. package/src/server/transcript-scan.mjs +563 -0
  172. package/src/server/usage-routes.mjs +108 -0
  173. package/src/server/uv-bootstrap.mjs +1 -1
  174. package/src/server/wordmark.mjs +127 -0
  175. package/dist/web/assets/BrowserWatchModal-CyAFTvAY.js +0 -1
  176. package/dist/web/assets/UsageHistoryModal-KJfli0oa.js +0 -1
  177. package/dist/web/assets/index-Ck2RAla3.css +0 -1
  178. package/dist/web/assets/index-jmrcEsCj.js +0 -298
@@ -69,7 +69,7 @@ import { existsSync } from "node:fs";
69
69
  import { join } from "node:path";
70
70
  import { homedir } from "node:os";
71
71
  import { claudeConfigDir } from "./claude-dir.mjs";
72
- import { readSettingsForWrite, writeFileAtomic } from "./installer.mjs";
72
+ import { readSettingsForWrite, writeFileAtomic } from "./atomic-write.mjs";
73
73
 
74
74
  const CLAUDE_DIR = claudeConfigDir();
75
75
  const SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
@@ -122,8 +122,8 @@ const commandsOf = (entry) =>
122
122
  * The last two segments are fixed whatever the prefix is — the install
123
123
  * directory is always `<config dir>/agent-dag` — and they contain no character
124
124
  * any quoting rewrites. Case is folded where the filesystem folds it, which is
125
- * the other half: `exec.mjs`'s own `sameCommand` lowercases "because Windows
126
- * paths are", and this comparison did not.
125
+ * the other half: exec-not-found.mjs's own `sameCommand` lowercases "because
126
+ * Windows paths are", and this comparison did not.
127
127
  */
128
128
  const SCRIPT_TAILS = ["agent-dag/notify.mjs", "agent-dag/notify.js"];
129
129
 
@@ -0,0 +1,215 @@
1
+ // How much the event ring may hold, and what one event is charged against it.
2
+ //
3
+ // These lived in src/server/index.mjs at the head of the ring buffer section.
4
+ // The ring itself is event-ring.mjs's — the array, the running totals that
5
+ // have to move with it, and the eviction in admitEvent that keeps these bounds
6
+ // — and the boot replay, which sizes its read against the same two numbers,
7
+ // imports them from here as well. The numbers and the walk are unchanged.
8
+
9
+ // MAX_BUFFER is how many events a late SSE subscriber can be replayed.
10
+ // MAX_BUFFER_CHARS is how much they are allowed to weigh, and until #625 there
11
+ // was no such thing — which is the whole defect, because a count is not a bound
12
+ // on memory when the thing being counted has no size of its own. `POST
13
+ // /api/event` admits a body of 5,000,000 characters and nothing between that
14
+ // door and this array shrinks it: hook/hook.js forwards the payload whole, and
15
+ // log-writer.mjs already says in as many words that a PostToolUse carrying a
16
+ // large Read or Bash response is routinely a good fraction of that. So the real
17
+ // ceiling was MAX_BUFFER multiplied by the largest event ingest accepts.
18
+ //
19
+ // Measured on Node 22.14 / macOS, posting 4,900,000-character bodies to a real
20
+ // server on loopback and reading process.memoryUsage() after a forced GC:
21
+ //
22
+ // after 20 events: heapUsed 107MB rss 292MB per-event 4.94MB
23
+ // after 100 events: heapUsed 481MB rss 739MB per-event 4.73MB
24
+ // after 200 events: heapUsed 947MB rss 1231MB per-event 4.69MB
25
+ //
26
+ // 4.69 MB retained per buffered event, flat as the ring fills. A full ring of
27
+ // 2000 of those is 9.4 GB against the 4144 MB heap limit V8 picks on a 32 GB
28
+ // machine, so the count cap was not reachable on any developer machine — and
29
+ // what happened instead of reaching it was not degradation. The same harness
30
+ // under `--max-old-space-size=2048`, roughly the heap an 8 GB laptop gives
31
+ // itself:
32
+ //
33
+ // after 420 events: heapUsed 1975MB rss 2247MB
34
+ // FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
35
+ //
36
+ // 429 events in, 1571 short of the cap. That abort is not catchable, so the SSE
37
+ // stream, the hook ingest and the log all stop together — and `/api/event` is a
38
+ // deliberate OPEN_MUTATION, so about 430 posts from a local process holding no
39
+ // credential at all end the deck. The comment at OPEN_MUTATIONS says the worst
40
+ // a caller does with that route is draw a session that is not there; this is
41
+ // the sentence that made it false.
42
+ //
43
+ // Both bounds are now enforced on every push, evicting oldest-first until each
44
+ // holds. See the eviction in admitEvent for why it is one splice and why the
45
+ // newest event is never the one evicted.
46
+ export const MAX_BUFFER = 2000; // recent HOOK events kept for late SSE subscribers
47
+
48
+ /**
49
+ * The enrichment this deck derives rather than receives, where only the newest
50
+ * one per session has ever been worth anything.
51
+ *
52
+ * These four are last-value-wins STATE, not history: a session's model, its
53
+ * token usage, its context fill and its name. Replaying an older UsageObserved
54
+ * on top of a newer one changes nothing a reader can see, which is precisely
55
+ * why they do not deserve a slot in a window measured in events.
56
+ *
57
+ * MEASURED, at one hook event per session every 3s — an ordinary tool-call
58
+ * cadence, well under every documented cap. Four scanners fire per hook event,
59
+ * throttled at 2.5s, 2.5s and 4s:
60
+ *
61
+ * sessions=10 posted=60 seq_delta=160 amplification=2.67
62
+ * sessions=200 posted=2000 seq_delta=5190 amplification=2.60
63
+ *
64
+ * Flat, near enough: every hook event costs about 2.6 ring slots. At 200
65
+ * sessions the 2000-entry ring held
66
+ * {"PostToolUse":800,"UsageObserved":800,"ContextObserved":400} and spanned
67
+ * 9.5 SECONDS — 60% of the resume window spent on state that is superseded
68
+ * moments later. Close the lid, switch networks, or let a tab sleep for fifteen
69
+ * seconds, and the Last-Event-ID is older than the ring's head: handleSse
70
+ * replays what is left and the client steps lastSeq over the gap, so the tool
71
+ * calls made in those seconds are never drawn and nothing reports the hole.
72
+ *
73
+ * `OutputObserved` is deliberately NOT here. It says something landed at a
74
+ * given moment, which is history — two of them are two events, not one value
75
+ * twice.
76
+ */
77
+ const LAST_VALUE_WINS = new Set(["ModelObserved", "UsageObserved", "ContextObserved", "SessionNamed"]);
78
+
79
+ /**
80
+ * The ring's array-length backstop, distinct from its event budget.
81
+ *
82
+ * MAX_BUFFER now counts hook events only, so the array holds those plus
83
+ * whatever enrichment is interleaved among them — about 2.6x the budget at the
84
+ * amplification measured above. This is the ceiling for a ratio nobody has
85
+ * measured yet: a future scanner, or a cadence that makes enrichment denser
86
+ * still, must not be able to grow the array without bound. It is not the
87
+ * working limit and should never be the binding one; MAX_BUFFER_CHARS remains
88
+ * the memory bound.
89
+ */
90
+ export const MAX_RING_ENTRIES = MAX_BUFFER * 4;
91
+
92
+ // The byte budget, counted in CHARACTERS — the unit the server already measures
93
+ // payloads in, and the unit MAX_CLIENT_BUFFER_BYTES in sse-clients.mjs is
94
+ // really written in despite its name (there is a long note there about why,
95
+ // which applies here unchanged: a character is one byte of heap while the
96
+ // string stays one-byte and two once it does not).
97
+ //
98
+ // 128 MiB, and the three readings that pick it:
99
+ //
100
+ // - It is 26 times the largest single event ingest can admit (5,000,000
101
+ // characters plus this deck's envelope), so a burst of maximum-size tool
102
+ // responses — eight subagents each returning a big Read — is held whole
103
+ // rather than collapsing the ring to nothing.
104
+ // - It is thirteen times a completely FULL 2000-event ring of ordinary
105
+ // traffic. The mean serialized event is about 5 KB, measured over 4.7k real
106
+ // payloads in a 21 MB events.jsonl (the same sample redactDeckToken's note
107
+ // quotes), so 2000 of them are 10 MB. Ordinary traffic therefore never
108
+ // meets this bound at all and keeps the full count-based replay depth; only
109
+ // the traffic that used to kill the process ever sees it.
110
+ // - Its worst case is a bounded fraction of the heap rather than a multiple
111
+ // of it. 128 MiB of charged characters is at most about 320 MiB of retained
112
+ // heap — the charge below tracks real retention within 2.5x across every
113
+ // payload shape measured — which is 16% of the 2 GB heap an 8 GB laptop
114
+ // picks, against the 9.4 GB the count alone permitted.
115
+ //
116
+ // Exported for the same reason MAX_CLIENT_BUFFER_BYTES is: a bound whose only
117
+ // observable failure is the process running out of memory is a bound no test
118
+ // can assert. See event-ring-byte-cap.test.ts, which pins all three readings.
119
+ export const MAX_BUFFER_CHARS = 128 * 1024 * 1024;
120
+
121
+ // What one envelope costs on top of its payload — seq, epoch, receivedAt,
122
+ // source and the JSON around them. Measured at 127 characters, the same figure
123
+ // MAX_CLIENT_BUFFER_BYTES is sized against; 128 here so that an event with an
124
+ // empty payload still costs something and a flood of them cannot be free.
125
+ const ENVELOPE_CHARS = 128;
126
+
127
+ /**
128
+ * What this payload will cost the ring, charged in characters.
129
+ *
130
+ * Not `JSON.stringify(raw).length`, which is the obvious answer and is wrong
131
+ * here twice over. It allocates a full copy of a payload that can be five
132
+ * megabytes, on the hottest path in the process; and it would defeat the
133
+ * deliberate optimization in pushEvent that skips serializing ENTIRELY when
134
+ * nothing is subscribed and nothing is being logged — a headless deck, and the
135
+ * boot replay of a log that only rotates at 50 MB. That optimization is pinned
136
+ * by sse-serialize-once.test.ts, so serializing here would fail the suite as
137
+ * well as the machine.
138
+ *
139
+ * So it is a walk, allocating nothing, in the same iterative shape and for the
140
+ * same stack-overflow reason as redactDeckToken (token-redact.mjs): a body from JSON.parse
141
+ * is free to nest as deeply as it likes and a recursive scan would be a new way
142
+ * to blow the stack inside the request listener, where nothing catches it.
143
+ *
144
+ * WHAT IT CHARGES, and why it is not just the string lengths. Measured on
145
+ * Node 22.14 by parsing twenty copies of a 4.9M-character body of each shape
146
+ * and reading heapUsed after a forced GC — retained per copy, against what
147
+ * string characters alone would have charged:
148
+ *
149
+ * one long string 4.67MB retained 4.67M charged 1.0x
150
+ * array of 8-char strings 3.91MB retained 3.40M charged 1.2x
151
+ * array of small numbers 4.67MB retained 0.00M charged ∞
152
+ * array of tiny objects 10.20MB retained 0.85M charged 12.0x
153
+ * object of distinct keys 12.38MB retained 2.86M charged 4.3x
154
+ *
155
+ * A payload of numbers is INVISIBLE to a string-length charge while retaining
156
+ * 4.67 MB, and an array of small objects is under-charged twelvefold — so a
157
+ * ring bounded that way would have been the same OOM behind a different
158
+ * payload shape. Adding 8 characters per value (V8 spends a tagged slot on
159
+ * each, and small objects and packed arrays measured at 8–46 bytes an entry)
160
+ * and the length of every key brings the same five shapes to 1.0x, 0.6x, 1.0x,
161
+ * 1.7x and 2.5x — i.e. never blind, and never more than 2.5x under the truth.
162
+ * That 2.5x is what MAX_BUFFER_CHARS is sized against. The one direction it
163
+ * over-charges is arrays of short strings, which shortens replay depth and
164
+ * never the other way.
165
+ *
166
+ * Two-byte strings cost twice what they are charged, exactly as they do for
167
+ * MAX_CLIENT_BUFFER_BYTES: a 4.8M-character CJK payload retains 9.35 MB and is
168
+ * charged 4.67M. Folded into the same 2.5x.
169
+ *
170
+ * Cost, measured against the JSON.parse the same event already pays for:
171
+ * 0.21 µs for a realistic 5 KB hook payload, 0.04 µs for a 4.9M-character
172
+ * single string (it is one node), and 22.6 ms for the pathological 222k-tiny-
173
+ * object body — against 96.4 ms to JSON.parse that same body. So the walk is a
174
+ * fifth of a parse the ingest path is paying anyway, and cheaper than
175
+ * redactDeckToken's walk, which does substring searches this one does not.
176
+ *
177
+ * Exported so the charge itself can be asserted rather than inferred from the
178
+ * ring's behaviour.
179
+ */
180
+ export function payloadChars(raw) {
181
+ if (typeof raw === "string") return raw.length;
182
+ // A top-level primitive is a legal body for `POST /api/event`, same as it is
183
+ // for redactDeckToken. One slot's worth.
184
+ if (raw === null || typeof raw !== "object") return 8;
185
+
186
+ let n = 0;
187
+ const stack = [raw];
188
+ while (stack.length > 0) {
189
+ const node = stack.pop();
190
+ // Arrays and objects walked separately for the reason redactDeckToken gives:
191
+ // an indexed loop is markedly cheaper than `for…in`, and a single
192
+ // PostToolUse response can be an array of thousands.
193
+ if (Array.isArray(node)) {
194
+ n += 8 * node.length;
195
+ for (let i = 0; i < node.length; i++) {
196
+ const v = node[i];
197
+ if (typeof v === "string") n += v.length;
198
+ else if (v !== null && typeof v === "object") stack.push(v);
199
+ }
200
+ } else {
201
+ for (const k in node) {
202
+ const v = node[k];
203
+ n += 8 + k.length;
204
+ if (typeof v === "string") n += v.length;
205
+ else if (v !== null && typeof v === "object") stack.push(v);
206
+ }
207
+ }
208
+ }
209
+ return n;
210
+ }
211
+
212
+ // What index.mjs reads besides the four exported above. Listed rather than
213
+ // marked at each declaration, so the declarations read as they did where they
214
+ // came from.
215
+ export { ENVELOPE_CHARS, LAST_VALUE_WINS };
@@ -61,7 +61,7 @@ import { readdir, readFile } from "node:fs/promises";
61
61
  import { join } from "node:path";
62
62
  import { claudeConfigDir } from "./claude-dir.mjs";
63
63
  import { challengeDeck, isProcessAlive } from "./deck-probe.mjs";
64
- import { sameCodexTree } from "./log-writer.mjs";
64
+ import { sameCodexTree } from "./log-election.mjs";
65
65
 
66
66
  /**
67
67
  * Where every deck on this machine registers itself.
@@ -0,0 +1,24 @@
1
+ // The version this process is running, read once, at import.
2
+ //
3
+ // Two modules report it and must agree: index.mjs, which answers /api/version
4
+ // with it and hands it to the away-update, and lan-deck.mjs, whose card tells
5
+ // paired decks what this one runs. It was a constant in index.mjs until the
6
+ // LAN engine moved out, and a second read in the second file would be a second
7
+ // answer to "what is running", taken at a slightly different moment. So it is
8
+ // read here, and both import it; index.mjs imports this statically, so the
9
+ // read still happens at boot.
10
+ import { readFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ // Resolved the way pinned-build.mjs resolves it, from a file in the same
15
+ // directory.
16
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
17
+
18
+ // Read at import — i.e. at boot, before an upgrade can overwrite these files.
19
+ // Reading it later would report whatever npm has since installed and hide the
20
+ // exact drift /api/version exists to expose. See src/server/self-update.mjs.
21
+ export const RUNNING_VERSION = (() => {
22
+ try { return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"))?.version ?? null; }
23
+ catch { return null; }
24
+ })();