enigma-cli 1.15.8 → 1.15.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 CHANGED
@@ -8,18 +8,29 @@ committed.
8
8
 
9
9
  ## Install
10
10
 
11
- One command, no global install, no prompts - deploy the skills to every supported
12
- agent at user level:
11
+ Recommended - install the `enigma` command globally, then run the interactive hub:
13
12
 
14
13
  ```bash
15
- npx enigma-cli@latest install --all --yes
14
+ npm install -g enigma-cli@latest # provides the `enigma` command
15
+ enigma # interactive hub: pick what to set up
16
+ ```
17
+
18
+ Or via the install script (clears the npm cache, installs the latest version, then
19
+ runs `enigma install` for you - handy where npm `postinstall` scripts are disabled):
20
+
21
+ ```bash
22
+ # macOS / Linux
23
+ curl -fsSL https://raw.githubusercontent.com/FJRG2007/enigma/main/install.sh | sh
24
+
25
+ # Windows (PowerShell)
26
+ irm https://raw.githubusercontent.com/FJRG2007/enigma/main/install.ps1 | iex
16
27
  ```
17
28
 
18
- Or install the command and pick interactively:
29
+ Or one-shot, no global install, no prompts - deploy the skills to every supported
30
+ agent at user level:
19
31
 
20
32
  ```bash
21
- npm install -g enigma-cli # provides the `enigma` command
22
- enigma # interactive hub: pick what to set up
33
+ npx enigma-cli@latest install --all --yes
23
34
  ```
24
35
 
25
36
  That first install is the only one you ever need to run by hand: afterwards,
@@ -72,8 +83,11 @@ enigma account ... Manage per-tool accounts (list/add/use/login/remove)
72
83
  enigma profile ... Group one account per tool (list/add/use/set/unset/remove)
73
84
  enigma skills ... List skills and manage discards (list/discard/restore)
74
85
  enigma compress [file] Compress JSON/logs/text to fewer tokens (reversible via CCR);
75
- --retrieve <hash> restores, --stats shows total savings
86
+ --retrieve <hash> restores, --stats shows total savings,
87
+ --clear wipes all dashboard data (stats/history/cache)
76
88
  enigma mcp Run the context-compression MCP server over stdio
89
+ enigma dashboard|dash Open the local savings dashboard in your browser (http://enigma,
90
+ or http://localhost:24282 if :80/hosts is unavailable)
77
91
  enigma seal Maintenance: (re)compute skill content hashes
78
92
  enigma check Integrity gate: verify skills are well-formed and sealed
79
93
  enigma help | version
@@ -462,8 +476,24 @@ cat tool-output.json | enigma compress # compressed to stdout, savings to st
462
476
  enigma compress big.log # compress a file
463
477
  enigma compress --retrieve <hash> # restore a cached original
464
478
  enigma compress --stats # cumulative token savings
479
+ enigma compress --clear # wipe all dashboard data (stats/history/cache)
465
480
  ```
466
481
 
482
+ ### Savings dashboard
483
+
484
+ `enigma dashboard` (alias `dash`) serves a local, loopback-only browser dashboard
485
+ of the token savings - totals, **estimated money saved**, average/best per call, a
486
+ per-day graph (toggle tokens/$ and daily/cumulative), breakdowns by the app that
487
+ requested each compression (Claude Code, OpenCode, Codex, or the CLI) and by content
488
+ type (JSON/logs/text...), a daily/weekly/monthly savings history, a recent-compressions
489
+ table, and reversible-cache (CCR) stats. It runs only while open; `enigma config
490
+ dashboard always` keeps a lightweight background daemon, and `enigma compress --clear`
491
+ resets the data.
492
+
493
+ The money figure is an estimate: enigma isn't a proxy, so it can't see the model -
494
+ it prices saved tokens per source with sensible defaults. Override the rate with
495
+ `enigma config token-price <usd-per-1M-input-tokens>` (0 restores the defaults).
496
+
467
497
  ### As an MCP server
468
498
 
469
499
  `enigma mcp` runs a stdio MCP server exposing three tools - `enigma_compress`,
@@ -20,10 +20,10 @@
20
20
  .wordmark { font-size: 20px; font-weight: 700; letter-spacing: 0.5px; }
21
21
  .wordmark span { color: var(--accent); }
22
22
  .sub { color: var(--muted); font-size: 13px; }
23
- .live { margin-left: auto; display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; }
23
+ .live { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; }
24
24
  .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); }
25
25
  .dot.stale { background: var(--muted); box-shadow: none; }
26
- .grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
26
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 20px; }
27
27
  .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
28
28
  .card .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px; }
29
29
  .card .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
@@ -34,7 +34,8 @@
34
34
  .panel-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
35
35
  .panel h2 { font-size: 14px; font-weight: 600; margin: 0; color: var(--text); }
36
36
  .panel h2 small { color: var(--muted); font-weight: 400; margin-left: 8px; }
37
- .ranges { margin-left: auto; display: flex; gap: 4px; }
37
+ .ctrls { margin-left: auto; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
38
+ .ranges { display: flex; gap: 4px; }
38
39
  .ranges button {
39
40
  background: var(--surface2); color: var(--muted); border: 1px solid var(--border);
40
41
  border-radius: 7px; padding: 4px 10px; font-size: 12px; cursor: pointer; font-family: inherit;
@@ -42,6 +43,13 @@
42
43
  .ranges button:hover { color: var(--text); }
43
44
  .ranges button.active { background: var(--accent); color: #1a1206; border-color: var(--accent); font-weight: 600; }
44
45
  .chartwrap { position: relative; width: 100%; height: 280px; }
46
+ /* Suppress the charting library's attribution logo. */
47
+ #tv-attr-logo { display: none !important; }
48
+ .nav { margin-left: auto; display: flex; align-items: center; gap: 18px; }
49
+ .links { display: flex; gap: 14px; align-items: center; }
50
+ .links a { color: var(--muted); display: inline-flex; transition: color 0.15s; }
51
+ .links a:hover { color: var(--accent); }
52
+ .links svg { width: 19px; height: 19px; display: block; }
45
53
  .tip {
46
54
  position: absolute; pointer-events: none; z-index: 5; background: var(--surface2);
47
55
  border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 12px;
@@ -54,7 +62,18 @@
54
62
  .bar-track { flex: 1; height: 14px; background: var(--surface2); border-radius: 7px; overflow: hidden; }
55
63
  .bar-fill { height: 100%; border-radius: 7px; }
56
64
  .bar-row .num { width: 90px; text-align: right; font-variant-numeric: tabular-nums; }
65
+ .bar-row .money { width: 78px; text-align: right; color: var(--good); font-variant-numeric: tabular-nums; }
57
66
  .empty { color: var(--muted); font-size: 14px; text-align: center; padding: 40px 0; }
67
+ .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
68
+ .tbl th { text-align: left; color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 10px; border-bottom: 1px solid var(--border); }
69
+ .tbl td { padding: 7px 10px; border-bottom: 1px solid var(--surface2); font-variant-numeric: tabular-nums; }
70
+ .tbl tr:last-child td { border-bottom: none; }
71
+ .tbl .r { text-align: right; }
72
+ .tbl .good { color: var(--good); }
73
+ .tbl-wrap { max-height: 320px; overflow-y: auto; }
74
+ .stat-line { display: flex; gap: 24px; margin-bottom: 12px; flex-wrap: wrap; }
75
+ .stat-line div { font-size: 13px; color: var(--muted); }
76
+ .stat-line b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
58
77
  footer { color: var(--muted); font-size: 12px; line-height: 1.6; margin-top: 8px; }
59
78
  code { background: var(--surface2); padding: 1px 6px; border-radius: 5px; color: var(--accent2); }
60
79
  @media (max-width: 720px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@@ -66,24 +85,49 @@
66
85
  <div class="wordmark">Enigma<span>.</span></div>
67
86
  <div class="sub">Context Compression Savings</div>
68
87
  </div>
69
- <div class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></div>
88
+ <div class="nav">
89
+ <span class="live"><span id="dot" class="dot stale"></span><span id="updated">Connecting...</span></span>
90
+ <nav class="links">
91
+ <a href="https://github.com/FJRG2007/enigma" target="_blank" rel="noopener noreferrer" title="GitHub repository" aria-label="GitHub repository">
92
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.52 11.52 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.014 2.898-.014 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
93
+ </a>
94
+ <a href="https://tpe.li/dsc" target="_blank" rel="noopener noreferrer" title="Discord" aria-label="Discord">
95
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9461 2.4189-2.1568 2.4189Z"/></svg>
96
+ </a>
97
+ <a href="https://ko-fi.com/fjrg2007" target="_blank" rel="noopener noreferrer" title="Support on Ko-fi" aria-label="Support on Ko-fi">
98
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg>
99
+ </a>
100
+ </nav>
101
+ </div>
70
102
  </header>
71
103
 
72
104
  <div class="grid">
73
- <div class="card"><div class="label">Tokens Saved</div><div id="saved" class="value good">-</div><div id="savedFoot" class="foot">Across all compressions</div></div>
105
+ <div class="card"><div class="label">Money Saved</div><div id="money" class="value good">-</div><div id="moneyFoot" class="foot">Estimated, by source</div></div>
106
+ <div class="card"><div class="label">Tokens Saved</div><div id="saved" class="value">-</div><div id="savedFoot" class="foot">Across all compressions</div></div>
74
107
  <div class="card"><div class="label">Savings Rate</div><div id="rate" class="value accent">-</div><div class="foot">Of input tokens removed</div></div>
75
108
  <div class="card"><div class="label">Compressions</div><div id="calls" class="value">-</div><div class="foot">Reversible via CCR</div></div>
76
- <div class="card"><div class="label">In &rarr; Out</div><div id="inout" class="value" style="font-size:20px">-</div><div class="foot">Total tokens</div></div>
109
+ <div class="card"><div class="label">Avg / Compression</div><div id="avg" class="value">-</div><div class="foot">Tokens saved per call</div></div>
110
+ <div class="card"><div class="label">Best Compression</div><div id="best" class="value">-</div><div class="foot">Most saved in one call</div></div>
77
111
  </div>
78
112
 
79
113
  <div class="panel">
80
114
  <div class="panel-head">
81
- <h2>Tokens Saved Per Day <small id="rangeLabel"></small></h2>
82
- <div class="ranges" id="ranges">
83
- <button type="button" data-d="7">7D</button>
84
- <button type="button" data-d="30" class="active">30D</button>
85
- <button type="button" data-d="90">90D</button>
86
- <button type="button" data-d="0">All</button>
115
+ <h2>Savings Per Day <small id="rangeLabel"></small></h2>
116
+ <div class="ctrls">
117
+ <div class="ranges" id="metricToggle">
118
+ <button type="button" data-m="tokens" class="active">Tokens</button>
119
+ <button type="button" data-m="usd">$</button>
120
+ </div>
121
+ <div class="ranges" id="modeToggle">
122
+ <button type="button" data-o="daily" class="active">Daily</button>
123
+ <button type="button" data-o="cumulative">Cumulative</button>
124
+ </div>
125
+ <div class="ranges" id="ranges">
126
+ <button type="button" data-d="7">7D</button>
127
+ <button type="button" data-d="30" class="active">30D</button>
128
+ <button type="button" data-d="90">90D</button>
129
+ <button type="button" data-d="0">All</button>
130
+ </div>
87
131
  </div>
88
132
  </div>
89
133
  <div id="chartWrap" class="chartwrap"></div>
@@ -96,17 +140,47 @@
96
140
  </div>
97
141
 
98
142
  <div class="panel">
99
- <h2 style="margin-bottom:14px">Before vs. After <small>cumulative tokens</small></h2>
143
+ <h2 style="margin-bottom:14px">Savings By Content Type <small>what enigma compressed</small></h2>
144
+ <div id="types"></div>
145
+ </div>
146
+
147
+ <div class="panel">
148
+ <h2 style="margin-bottom:14px">Before vs. After <small id="ratioLabel">cumulative tokens</small></h2>
100
149
  <div class="bar-row"><span class="name">Before</span><div class="bar-track"><div id="barBefore" class="bar-fill" style="background:var(--accent2)"></div></div><span id="numBefore" class="num">-</span></div>
101
150
  <div class="bar-row"><span class="name">After</span><div class="bar-track"><div id="barAfter" class="bar-fill" style="background:var(--good)"></div></div><span id="numAfter" class="num">-</span></div>
151
+ <div class="stat-line" style="margin:14px 0 0"><div>Removed <b id="removed">-</b></div><div>Reduction <b id="reduction">-</b></div><div>Compression ratio <b id="ratio">-</b></div></div>
152
+ </div>
153
+
154
+ <div class="panel">
155
+ <div class="panel-head">
156
+ <h2>Savings History <small id="histStats"></small></h2>
157
+ <div class="ctrls">
158
+ <div class="ranges" id="histToggle">
159
+ <button type="button" data-p="day" class="active">Daily</button>
160
+ <button type="button" data-p="week">Weekly</button>
161
+ <button type="button" data-p="month">Monthly</button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ <div class="tbl-wrap"><div id="histBody"></div></div>
166
+ </div>
167
+
168
+ <div class="panel">
169
+ <h2 style="margin-bottom:14px">Recent Compressions <small>newest first</small></h2>
170
+ <div class="tbl-wrap"><div id="recent"></div></div>
171
+ </div>
172
+
173
+ <div class="panel">
174
+ <h2 style="margin-bottom:14px">Reversible Cache (CCR) <small>recoverable originals on disk</small></h2>
175
+ <div id="cache"></div>
102
176
  </div>
103
177
 
104
178
  <footer>
105
179
  Polling pauses automatically when this tab is hidden, and the server caches each snapshot - so an open dashboard costs almost nothing.
106
- Data source: <code>~/.enigma/ccr/stats.json</code> + <code>history.jsonl</code>. Enigma <span id="ver"></span>
180
+ Data source: <code>~/.enigma/ccr/stats.json</code> + <code>history.jsonl</code> - reset it with <code>enigma compress --clear</code>. Enigma <span id="ver"></span>
107
181
  </footer>
108
182
 
109
- <script src="/lib/lightweight-charts.standalone.production.js"></script>
183
+ <script src="/lib/chart.min.js"></script>
110
184
  <script>
111
185
  const POLL_MS = 5000;
112
186
  const DAY = 86400000;
@@ -119,32 +193,56 @@
119
193
  if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
120
194
  return String(Math.round(n));
121
195
  }
196
+ function fmtUsd(n) {
197
+ n = Number(n) || 0;
198
+ if (n >= 1e3) return "$" + (n / 1e3).toFixed(2) + "k";
199
+ if (n >= 1) return "$" + n.toFixed(2);
200
+ return "$" + n.toFixed(n >= 0.1 ? 2 : 3);
201
+ }
202
+ function fmtBytes(n) {
203
+ n = Number(n) || 0;
204
+ if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
205
+ if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
206
+ return n + " B";
207
+ }
122
208
 
123
- // Friendly names for known MCP clients; unknown sources show title-cased raw.
209
+ // Estimated price per 1M INPUT tokens, by source. enigma is not a proxy, so it
210
+ // can't know the real model - these are sensible defaults, overridable globally
211
+ // with `enigma config token-price <usd>` (priceOverride from the payload).
212
+ const PRICE_1M = {
213
+ "claude-code": 3, "claude": 3, "claude-desktop": 3,
214
+ "opencode": 3, "codex": 2.5, "codex-cli": 2.5, "cli": 3, "unknown": 3,
215
+ };
216
+ const DEFAULT_PRICE_1M = 3;
217
+ let priceOverride = 0;
218
+ function priceFor(source) { return priceOverride > 0 ? priceOverride : (PRICE_1M[source] ?? DEFAULT_PRICE_1M); }
219
+ function moneyOf(source, savedTokens) { return savedTokens * priceFor(source) / 1e6; }
220
+
221
+ // Friendly names for known MCP clients / content types; unknown -> title-cased raw.
124
222
  const SOURCE_LABELS = {
125
223
  "claude-code": "Claude Code", "claude": "Claude Code", "claude-desktop": "Claude Desktop",
126
224
  "opencode": "OpenCode", "codex": "Codex", "codex-cli": "Codex",
127
225
  "cli": "CLI (enigma compress)", "unknown": "Unattributed",
128
226
  };
129
- function prettySource(k) {
130
- return SOURCE_LABELS[k] || String(k).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
131
- }
227
+ function prettySource(k) { return SOURCE_LABELS[k] || String(k).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); }
228
+ const TYPE_LABELS = { json: "JSON", log: "Logs", text: "Text", code: "Code", diff: "Diffs", markdown: "Markdown", unknown: "Other" };
229
+ function prettyType(k) { return TYPE_LABELS[k] || String(k).replace(/\b\w/g, (c) => c.toUpperCase()); }
132
230
 
133
- // One bar per origin (the calling app or the CLI), sorted by tokens saved.
134
- function renderSources(bySource) {
135
- const el = $("sources");
136
- const entries = Object.entries(bySource || {}).sort((a, b) => (b[1].tokensSaved || 0) - (a[1].tokensSaved || 0));
231
+ // One bar per key (source or type), sorted by tokens saved, with an optional $ column.
232
+ function renderBreakdown(el, map, label, money) {
233
+ const entries = Object.entries(map || {}).sort((a, b) => (b[1].tokensSaved || 0) - (a[1].tokensSaved || 0));
137
234
  if (!entries.length) {
138
- el.innerHTML = '<div class="empty" style="padding:20px 0">No per-source data yet. Attribution starts with the next compression - the MCP tags the calling app (Claude Code, OpenCode, Codex...), the CLI tags itself.</div>';
235
+ el.innerHTML = '<div class="empty" style="padding:20px 0">No data yet - this fills in as compressions are recorded.</div>';
139
236
  return;
140
237
  }
141
238
  const max = Math.max(1, ...entries.map(([, v]) => v.tokensSaved || 0));
142
239
  el.innerHTML = entries.map(([k, v]) => {
143
240
  const saved = v.tokensSaved || 0, pct = Math.round(saved / max * 100);
144
- return '<div class="bar-row"><span class="name" style="width:140px">' + prettySource(k)
241
+ const m = money ? '<span class="money">' + fmtUsd(moneyOf(k, saved)) + '</span>' : '';
242
+ return '<div class="bar-row"><span class="name" style="width:140px">' + label(k)
145
243
  + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--accent)"></div></div>'
146
- + '<span class="num">' + fmt(saved) + '</span>'
147
- + '<span class="foot" style="width:70px;text-align:right;margin:0">' + fmt(v.calls || 0) + ' calls</span></div>';
244
+ + '<span class="num">' + fmt(saved) + '</span>' + m
245
+ + '<span class="foot" style="width:64px;text-align:right;margin:0">' + fmt(v.calls || 0) + ' calls</span></div>';
148
246
  }).join("");
149
247
  }
150
248
 
@@ -152,19 +250,99 @@
152
250
  // chart's time scale expects (matches the server's UTC-day history buckets).
153
251
  function dayKey(ms) { return new Date(ms).toISOString().slice(0, 10); }
154
252
 
155
- // Aggregate raw history points into one ascending {time, value} per day of saved
156
- // tokens. Lightweight Charts requires unique, ascending times - so we sum per day.
253
+ const savedOf = (p) => Math.max(0, (p.b || 0) - (p.a || 0));
254
+
255
+ // Recent compressions table (newest first), one row per history point.
256
+ function renderRecent(history) {
257
+ const rows = (history || []).slice(-20).reverse();
258
+ const el = $("recent");
259
+ if (!rows.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No compressions recorded yet.</div>'; return; }
260
+ el.innerHTML = '<table class="tbl"><thead><tr><th>Time</th><th>Type</th><th>Source</th>'
261
+ + '<th class="r">Before</th><th class="r">After</th><th class="r">Saved</th></tr></thead><tbody>'
262
+ + rows.map((p) => {
263
+ const saved = savedOf(p), pct = p.b ? Math.round(saved / p.b * 100) : 0;
264
+ return '<tr><td>' + new Date(p.t).toLocaleString() + '</td><td>' + prettyType(p.c || "unknown")
265
+ + '</td><td>' + prettySource(p.s || "unknown") + '</td><td class="r">' + fmt(p.b || 0)
266
+ + '</td><td class="r">' + fmt(p.a || 0) + '</td><td class="r good">' + fmt(saved) + " (" + pct + "%)</td></tr>";
267
+ }).join("") + "</tbody></table>";
268
+ }
269
+
270
+ // ISO-ish period key for grouping (day = YYYY-MM-DD, week = YYYY-Www, month = YYYY-MM).
271
+ function periodKey(t, period) {
272
+ const d = new Date(t);
273
+ if (period === "month") return d.toISOString().slice(0, 7);
274
+ if (period === "week") {
275
+ const onejan = Date.UTC(d.getUTCFullYear(), 0, 1);
276
+ const week = Math.ceil(((d.getTime() - onejan) / DAY + 1) / 7);
277
+ return d.getUTCFullYear() + "-W" + String(week).padStart(2, "0");
278
+ }
279
+ return dayKey(t);
280
+ }
281
+
282
+ let histPeriod = "day";
283
+ function renderHistory(history) {
284
+ const hist = history || [];
285
+ const days = new Set(hist.map((p) => dayKey(p.t))).size;
286
+ const total = hist.reduce((s, p) => s + savedOf(p), 0);
287
+ $("histStats").textContent = days ? days + " active day" + (days === 1 ? "" : "s") + " · avg " + fmt(Math.round(total / days)) + "/day" : "";
288
+ const byP = new Map();
289
+ for (const p of hist) {
290
+ const k = periodKey(p.t, histPeriod);
291
+ const cur = byP.get(k) || { tokens: 0, usd: 0, calls: 0 };
292
+ cur.tokens += savedOf(p);
293
+ cur.usd += moneyOf(p.s || "unknown", savedOf(p));
294
+ cur.calls++;
295
+ byP.set(k, cur);
296
+ }
297
+ const rows = [...byP.entries()].sort((a, b) => a[0] < b[0] ? 1 : -1);
298
+ const el = $("histBody");
299
+ if (!rows.length) { el.innerHTML = '<div class="empty" style="padding:24px 0">No history yet.</div>'; return; }
300
+ el.innerHTML = '<table class="tbl"><thead><tr><th>Period</th><th class="r">Compressions</th>'
301
+ + '<th class="r">Tokens Saved</th><th class="r">$ Saved</th></tr></thead><tbody>'
302
+ + rows.map(([k, v]) => '<tr><td>' + k + '</td><td class="r">' + v.calls + '</td><td class="r good">'
303
+ + fmt(v.tokens) + '</td><td class="r">' + fmtUsd(v.usd) + "</td></tr>").join("") + "</tbody></table>";
304
+ }
305
+
306
+ function renderCache(c) {
307
+ const el = $("cache");
308
+ if (!c) { el.innerHTML = '<div class="empty" style="padding:20px 0">No cache data.</div>'; return; }
309
+ const pct = c.cap ? Math.round((c.count || 0) / c.cap * 100) : 0;
310
+ el.innerHTML = '<div class="bar-row"><span class="name" style="width:170px">Cached originals</span>'
311
+ + '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--accent)"></div></div>'
312
+ + '<span class="num">' + fmt(c.count || 0) + " / " + fmt(c.cap || 0) + '</span></div>'
313
+ + '<div class="stat-line" style="margin-top:6px"><div>On disk <b>' + fmtBytes(c.bytes || 0) + '</b></div>'
314
+ + '<div>Retention <b>newest ' + fmt(c.cap || 0) + '</b></div>'
315
+ + '<div>Restore <b>enigma compress --retrieve &lt;hash&gt;</b></div></div>';
316
+ }
317
+
318
+ // Aggregate raw history points into one {time, tokens, usd} per day (ascending,
319
+ // unique times). $ per point uses that point's recorded source price.
157
320
  function dailySeries(history) {
158
321
  const byDay = new Map();
159
322
  for (const p of history || []) {
160
323
  const saved = Math.max(0, (p.b || 0) - (p.a || 0));
161
324
  const key = dayKey(p.t);
162
- byDay.set(key, (byDay.get(key) || 0) + saved);
325
+ const cur = byDay.get(key) || { tokens: 0, usd: 0 };
326
+ cur.tokens += saved;
327
+ cur.usd += moneyOf(p.s || "unknown", saved);
328
+ byDay.set(key, cur);
163
329
  }
164
- return [...byDay.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1).map(([time, value]) => ({ time, value }));
330
+ return [...byDay.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)
331
+ .map(([time, v]) => ({ time, tokens: v.tokens, usd: v.usd }));
165
332
  }
166
333
 
167
- let chart = null, series = null, fullData = [], currentRange = 30;
334
+ let chart = null, series = null, dailyData = [], lastHistory = [], currentRange = 30, metric = "tokens", mode = "daily";
335
+ const metricFmt = (v) => metric === "usd" ? fmtUsd(v) : fmt(v);
336
+
337
+ // Map the per-day data to LC {time, value} for the active metric and mode.
338
+ function seriesValues() {
339
+ let cum = 0;
340
+ return dailyData.map((d) => {
341
+ const base = metric === "usd" ? d.usd : d.tokens;
342
+ if (mode === "cumulative") { cum += base; return { time: d.time, value: cum }; }
343
+ return { time: d.time, value: base };
344
+ });
345
+ }
168
346
 
169
347
  function initChart() {
170
348
  if (!window.LightweightCharts) return false;
@@ -185,20 +363,24 @@
185
363
  series = chart.addSeries(LC.AreaSeries, {
186
364
  lineColor: "#e0a458", topColor: "rgba(224,164,88,0.35)", bottomColor: "rgba(224,164,88,0.02)",
187
365
  lineWidth: 2, priceLineVisible: false, lastValueVisible: false,
188
- priceFormat: { type: "custom", minMove: 1, formatter: fmt },
366
+ priceFormat: { type: "custom", minMove: 1, formatter: metricFmt },
189
367
  });
190
368
  setupTooltip(el);
191
- $("ranges").addEventListener("click", (e) => {
369
+ // Button groups: select one, restyle siblings, run the picker.
370
+ const group = (id, onPick) => $(id).addEventListener("click", (e) => {
192
371
  const b = e.target.closest("button");
193
372
  if (!b) return;
194
- for (const x of $("ranges").children) x.classList.toggle("active", x === b);
195
- currentRange = Number(b.dataset.d);
196
- applyRange();
373
+ for (const x of $(id).children) x.classList.toggle("active", x === b);
374
+ onPick(b);
197
375
  });
376
+ group("ranges", (b) => { currentRange = Number(b.dataset.d); applyRange(); });
377
+ group("metricToggle", (b) => { metric = b.dataset.m; redraw(); });
378
+ group("modeToggle", (b) => { mode = b.dataset.o; redraw(); });
379
+ group("histToggle", (b) => { histPeriod = b.dataset.p; renderHistory(lastHistory); });
198
380
  return true;
199
381
  }
200
382
 
201
- // Floating crosshair tooltip: shows the hovered date and that day's saved tokens.
383
+ // Floating crosshair tooltip: hovered date + that point's value (tokens or $).
202
384
  function setupTooltip(container) {
203
385
  const tip = document.createElement("div");
204
386
  tip.className = "tip";
@@ -208,47 +390,77 @@
208
390
  if (!param.time || !param.point || param.point.x < 0 || param.point.y < 0) { tip.style.display = "none"; return; }
209
391
  const d = param.seriesData.get(series);
210
392
  if (!d) { tip.style.display = "none"; return; }
211
- tip.innerHTML = '<div class="tip-d">' + param.time + '</div><div class="tip-v">' + fmt(d.value) + " tokens saved</div>";
393
+ const suffix = (metric === "usd" ? " saved" : " tokens saved") + (mode === "cumulative" ? " (total)" : "");
394
+ tip.innerHTML = '<div class="tip-d">' + param.time + '</div><div class="tip-v">' + metricFmt(d.value) + suffix + "</div>";
212
395
  tip.style.display = "block";
213
- const left = Math.max(0, Math.min(param.point.x + 14, container.clientWidth - 150));
396
+ const left = Math.max(0, Math.min(param.point.x + 14, container.clientWidth - 160));
214
397
  tip.style.left = left + "px";
215
398
  tip.style.top = Math.max(0, param.point.y - 10) + "px";
216
399
  });
217
400
  }
218
401
 
402
+ // Push the active metric/mode data into the series and reformat the axis.
403
+ function redraw() {
404
+ if (!series) return;
405
+ series.applyOptions({ priceFormat: { type: "custom", minMove: metric === "usd" ? 0.0001 : 1, formatter: metricFmt } });
406
+ const has = dailyData.length > 0;
407
+ $("chartEmpty").style.display = has ? "none" : "block";
408
+ $("chartWrap").style.display = has ? "block" : "none";
409
+ if (has) { series.setData(seriesValues()); applyRange(); }
410
+ }
411
+
219
412
  // Constrain the visible window to the selected range (All = fit everything).
220
413
  function applyRange() {
221
- if (!chart || !fullData.length) return;
414
+ if (!chart || !dailyData.length) return;
222
415
  const ts = chart.timeScale();
223
416
  if (currentRange === 0) { ts.fitContent(); $("rangeLabel").textContent = "all time"; return; }
224
- const to = fullData[fullData.length - 1].time;
417
+ const to = dailyData[dailyData.length - 1].time;
225
418
  const from = dayKey(Date.now() - currentRange * DAY);
226
419
  try { ts.setVisibleRange({ from, to }); }
227
420
  catch { ts.fitContent(); }
228
421
  $("rangeLabel").textContent = "last " + currentRange + " days";
229
422
  }
230
423
 
424
+ // Total estimated $ saved: sum each source's saved tokens at its price (legacy
425
+ // data with no attribution falls back to the default price).
426
+ function totalMoney(s) {
427
+ const bs = s.bySource || {};
428
+ const keys = Object.keys(bs);
429
+ if (keys.length) return keys.reduce((sum, k) => sum + moneyOf(k, bs[k].tokensSaved || 0), 0);
430
+ return moneyOf("unknown", s.tokensSaved || 0);
431
+ }
432
+
231
433
  function render(data) {
434
+ priceOverride = Number(data.priceOverride) || 0;
232
435
  const s = data.stats || {};
233
- const before = s.tokensBefore || 0, after = s.tokensAfter || 0, saved = s.tokensSaved || 0;
436
+ const before = s.tokensBefore || 0, after = s.tokensAfter || 0, saved = s.tokensSaved || 0, calls = s.calls || 0;
234
437
  const rate = before ? Math.round((saved / before) * 100) : 0;
438
+ $("money").textContent = fmtUsd(totalMoney(s));
439
+ $("moneyFoot").textContent = priceOverride > 0 ? "~$" + priceOverride + "/1M tokens" : "Estimated, by source";
235
440
  $("saved").textContent = fmt(saved);
236
441
  $("savedFoot").textContent = saved ? "~" + saved.toLocaleString() + " tokens" : "Across all compressions";
237
442
  $("rate").textContent = rate + "%";
238
- $("calls").textContent = fmt(s.calls || 0);
239
- $("inout").textContent = fmt(before) + " " + fmt(after);
443
+ $("calls").textContent = fmt(calls);
444
+ $("avg").textContent = calls ? fmt(Math.round(saved / calls)) : "0";
445
+ $("best").textContent = fmt(s.best || 0);
240
446
  $("numBefore").textContent = fmt(before);
241
447
  $("numAfter").textContent = fmt(after);
242
448
  const max = Math.max(1, before);
243
449
  $("barBefore").style.width = (before / max * 100) + "%";
244
450
  $("barAfter").style.width = (after / max * 100) + "%";
245
- renderSources(s.bySource);
451
+ $("removed").textContent = fmt(saved) + " (" + rate + "%)";
452
+ $("reduction").textContent = before ? (100 - rate) + "% of input sent" : "-";
453
+ $("ratio").textContent = after ? "~" + (before / after).toFixed(1) + ":1" : "-";
454
+ renderBreakdown($("sources"), s.bySource, prettySource, true);
455
+ renderBreakdown($("types"), s.byType, prettyType, false);
246
456
 
247
- fullData = dailySeries(data.history);
248
- const has = fullData.length > 0 && !!series;
249
- $("chartEmpty").style.display = has ? "none" : "block";
250
- $("chartWrap").style.display = has ? "block" : "none";
251
- if (has) { series.setData(fullData); applyRange(); }
457
+ lastHistory = data.history || [];
458
+ renderRecent(lastHistory);
459
+ renderHistory(lastHistory);
460
+ renderCache(data.cache);
461
+
462
+ dailyData = dailySeries(data.history);
463
+ redraw();
252
464
 
253
465
  $("ver").textContent = data.version ? "v" + data.version : "";
254
466
  $("dot").classList.remove("stale");
@@ -28,6 +28,7 @@ higher one and move on. The first lazy solution that works is the right one.
28
28
  ## Rules
29
29
 
30
30
  - No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
31
+ - No passthrough indirection: a lookup table whose keys equal its values (`{ win32: "win32" }`), a wrapper that only forwards its arguments, a constant aliasing another constant, a variable used once. If a construct maps a thing to itself, delete it - use the value directly, or a `Set` when the real intent is an allowlist/membership test.
31
32
  - No boilerplate or scaffolding "for later"; later can scaffold for itself.
32
33
  - Deletion over addition. Boring over clever - clever is what someone decodes at 3am.
33
34
  - Fewest files possible. The shortest working diff wins.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "anti-overengineering-policy",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Minimal-code discipline - YAGNI ladder, deletion over addition, no unrequested abstractions, and the enigma: shortcut-marking convention; intensity via the minimal-code setting.",
6
- "cliVersion": "1.15.8",
7
- "sha": "297c5d9a0573a319a6f8476f5b1da8cb93e6f3297f13b76ac25d91ce10c7b523"
6
+ "cliVersion": "1.15.9",
7
+ "sha": "3469a7e554b63a5ae3b1a4a127a0765d760649511fa9dfa9cdbcb04b4431fd64"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, API and request optimization, server-side caching (Redis), and Zod boundary validation.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "a46c3cd00aa5f47adb1e7907f1d2bc6f5562f7a272890dee9b1121976ac04ae1"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.1",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Ciphera code style conventions (formatting, naming, imports, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "74f638aec13e8c93257fe1ad604c28b07e9a7c456796a4ceefcc99217d9e7039"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.9.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "82d246faa4770248a36f143926fbf06bd3ffc082959014184548b57193770fab"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "2883bcecb3202683ae6f81b073c3d6a9cec9c55029e011bdd06ba7ac3537297e"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "431645565aec8a4ab4cd17247139f78c377a9bcfcfc4ee1d6f252199c332d86f"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.3.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Frontend architecture: reusable components, abstraction thresholds, state management, no-op save detection, large-list rendering (infinite scroll/pagination, virtualization, skeletons, progressive loading), and optimistic UI with rollback.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "26962db5a8b607e95098a6d88977aee59a10bd14e03afe97245f2b013fd21a9a"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.3.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "73a978ef11f4def067f9252fc3e2e6aa737a08b6d58ad8a2a45cbcd4b02813f6"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "9971e9d9127397d0152e89d24aad3191e2935e55a8483db7fd15f5d4d7a60e7a"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "699586cce82ec0a5458288b598ee7e5ebdddb3dfcf19db354d8bc5e85e47c1c7"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "6e3facba307eb2b55cefbab2e4b2a346a2b82f93c3ef47e11ebeb78c3c9453a8"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.2.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
6
- "cliVersion": "1.15.8",
6
+ "cliVersion": "1.15.9",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "03ad3c401ed5b3b87b771b370d7c2a052e21b1990d04b1e600d4e9d293cd2331",
3
- "enigma-linux-arm64": "a5c37de6365d999ce82c07eff1e75160c1b3f91d50f8c0a1def931f89beffa3b",
4
- "enigma-linux-x64": "39cd51e75a493bf370f8cf88e3f82978a33631a87123c4fef7973c3822e74f0f",
5
- "enigma-win32-x64.exe": "ca8a3b990385ab8299bc86a14a4b1516ff4f080323b7650202a9bda11196f23a"
2
+ "enigma-darwin-arm64": "500c3c44723b5b545d3174ac7cb8c47cd4faa01fb794fe331e0e1e5a5b09930c",
3
+ "enigma-linux-arm64": "b47ebfeb8acd6c6636107d26a71e81f1041993e670935a86bb0a1f0ac4b011c5",
4
+ "enigma-linux-x64": "a0a0bd7f418124fc4868b66423cf552a1c1df78c200722dd3e6c1fc910c6ab86",
5
+ "enigma-win32-x64.exe": "917a4fe077b3897f9f128a384c955452717df9ab394b8280fd59ef2449befcdf"
6
6
  }
package/bin/platform.mjs CHANGED
@@ -16,12 +16,15 @@ import { fileURLToPath } from "node:url";
16
16
  import { dirname, join } from "node:path";
17
17
  import { existsSync, readFileSync } from "node:fs";
18
18
 
19
- // npm/Node "os" identifiers (win32, not "windows"); kept stable across the toolchain.
20
- const PLATFORMS = { win32: "win32", darwin: "darwin", linux: "linux" };
21
- const ARCHES = { x64: "x64", arm64: "arm64" };
19
+ // Allowlists of the npm/Node "os" identifiers we publish binaries for (win32, not
20
+ // "windows"). An unsupported host resolves to undefined, which platformKeys() turns
21
+ // into "no binary for this host" rather than guessing a non-existent asset name.
22
+ const SUPPORTED_PLATFORMS = new Set(["win32", "darwin", "linux"]);
23
+ const SUPPORTED_ARCHES = new Set(["x64", "arm64"]);
24
+ const allowed = (set, value) => (set.has(value) ? value : undefined);
22
25
 
23
- export const PLATFORM = PLATFORMS[os.platform()];
24
- export const ARCH = ARCHES[os.arch()];
26
+ export const PLATFORM = allowed(SUPPORTED_PLATFORMS, os.platform());
27
+ export const ARCH = allowed(SUPPORTED_ARCHES, os.arch());
25
28
  export const isWindows = PLATFORM === "win32";
26
29
 
27
30
  /** This package's root (the directory holding package.json), i.e. <pkg>/bin/.. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.15.8",
3
+ "version": "1.15.9",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {