octocode-cli 1.3.0 → 1.5.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 (106) hide show
  1. package/README.md +129 -28
  2. package/out/chunks/chunk-7476PETK.js +309 -0
  3. package/out/chunks/chunk-CVNNNSMQ.js +26 -0
  4. package/out/chunks/chunk-OQBJTZWK.js +60 -0
  5. package/out/chunks/chunk-UCZCF3BQ.js +9 -0
  6. package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
  7. package/out/chunks/commands-XBFPLHSQ.js +8 -0
  8. package/out/chunks/help-P7TCOYAJ.js +10 -0
  9. package/out/chunks/main-help-ULF5PAQY.js +10 -0
  10. package/out/chunks/prompts-5E6VKRX5.js +8 -0
  11. package/out/chunks/spinner-URV2OX6O.js +8 -0
  12. package/out/chunks/tool-command-M6VA7P2F.js +8 -0
  13. package/out/octocode-cli.js +1 -1
  14. package/package.json +5 -3
  15. package/skills/README.md +60 -58
  16. package/skills/agentic-flow-best-practices/SKILL.md +280 -0
  17. package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
  18. package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
  19. package/skills/agentic-flow-best-practices/references/resources.md +112 -0
  20. package/skills/octocode-brainstorming/.env.example +11 -0
  21. package/skills/octocode-brainstorming/SKILL.md +262 -0
  22. package/skills/octocode-brainstorming/scripts/tavily-search.mjs +138 -0
  23. package/skills/octocode-chrome-devtools/README.md +541 -0
  24. package/skills/octocode-chrome-devtools/SKILL.md +197 -0
  25. package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
  26. package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
  27. package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
  28. package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
  29. package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
  30. package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
  31. package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
  32. package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
  33. package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
  34. package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
  35. package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
  36. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
  37. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
  38. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
  39. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
  40. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
  41. package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
  42. package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
  43. package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
  44. package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
  45. package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
  46. package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
  47. package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
  48. package/skills/octocode-design/README.md +2 -2
  49. package/skills/octocode-documentation-writer/README.md +1 -1
  50. package/skills/octocode-engineer/README.md +1 -1
  51. package/skills/octocode-engineer/SKILL.md +137 -306
  52. package/skills/octocode-engineer/references/cli-reference.md +13 -0
  53. package/skills/octocode-engineer/references/output-files.md +3 -3
  54. package/skills/octocode-engineer/scripts/run.js +146 -146
  55. package/skills/octocode-engineer/src/pipeline/main.ts +1 -17
  56. package/skills/octocode-engineer/src/pipeline/progress.ts +4 -0
  57. package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
  58. package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
  59. package/skills/octocode-install/SKILL.md +1 -1
  60. package/skills/octocode-pull-request-reviewer/README.md +5 -5
  61. package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
  62. package/skills/octocode-research/AGENTS.md +1 -1
  63. package/skills/octocode-research/README.md +2 -2
  64. package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
  65. package/skills/octocode-research/scripts/server.js +184 -239
  66. package/skills/octocode-research/src/routes/github.ts +4 -21
  67. package/skills/octocode-research/src/routes/local.ts +4 -21
  68. package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
  69. package/skills/octocode-search-skill/SKILL.md +337 -0
  70. package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
  71. package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
  72. package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
  73. package/skills/octocode-search-skill/references/install-reference.md +130 -0
  74. package/skills/octocode-search-skill/references/references-template.md +27 -0
  75. package/skills/octocode-search-skill/references/references.md +62 -0
  76. package/skills/octocode-slides/README.md +307 -0
  77. package/skills/octocode-slides/SKILL.md +410 -0
  78. package/skills/octocode-slides/references/01-brief.md +156 -0
  79. package/skills/octocode-slides/references/02-research.md +149 -0
  80. package/skills/octocode-slides/references/03-outline.md +172 -0
  81. package/skills/octocode-slides/references/04-design.md +301 -0
  82. package/skills/octocode-slides/references/05-implementation.md +213 -0
  83. package/skills/octocode-slides/references/06-review.md +258 -0
  84. package/skills/octocode-slides/references/animation.md +281 -0
  85. package/skills/octocode-slides/references/design-system.md +316 -0
  86. package/skills/octocode-slides/references/html-templates.md +673 -0
  87. package/skills/octocode-slides/references/image-generation.md +448 -0
  88. package/skills/octocode-slides/references/resources.md +840 -0
  89. package/skills/octocode-slides/references/slide-rules.md +541 -0
  90. package/skills/octocode-slides/references/wireframes.md +727 -0
  91. package/skills/octocode-slides/scripts/animation.js +182 -0
  92. package/skills/octocode-slides/scripts/base.css +353 -0
  93. package/skills/octocode-slides/scripts/base.html +655 -0
  94. package/skills/octocode-slides/scripts/generate_image.py +221 -0
  95. package/skills/octocode-slides/scripts/navbridge.js +79 -0
  96. package/skills/octocode-slides/scripts/presenter.js +316 -0
  97. package/skills/octocode-slides/scripts/slide.html +248 -0
  98. package/skills/octocode-stats/SKILL.md +73 -0
  99. package/skills/octocode-stats/assets/template.html +1332 -0
  100. package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
  101. package/out/chunks/chunk-LH4AZJPA.js +0 -389
  102. package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
  103. package/out/chunks/commands-M3QTWKWE.js +0 -51
  104. package/out/chunks/help-XPXP46ZT.js +0 -10
  105. package/out/chunks/main-help-HXFAFHPG.js +0 -10
  106. package/out/chunks/tool-command-VHFLPIHY.js +0 -8
@@ -0,0 +1,1332 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Octocode MCP — Stats Dashboard</title>
7
+ <meta name="theme-color" content="#6366f1" />
8
+ <link rel="icon" type="image/png" href="https://octocode.ai/apple-touch-icon.png" />
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
13
+ rel="stylesheet"
14
+ />
15
+ <script src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.0/dist/apexcharts.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/countup.js@2.8.0/dist/countUp.umd.min.js"></script>
17
+ <script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
18
+ <style>
19
+ /* ============================================================
20
+ OCTOCODE STATS — design tokens
21
+ Adapted from octocode.ai's OKLCH shadcn-style theme.
22
+ ============================================================ */
23
+ :root[data-theme='dark'] {
24
+ --background: oklch(14% .02 260);
25
+ --foreground: oklch(95% .01 250);
26
+ --card: oklch(18% .025 260);
27
+ --card-elevated: oklch(20% .028 260);
28
+ --card-foreground: oklch(95% .01 250);
29
+ --muted: oklch(18% .015 260);
30
+ --muted-foreground: oklch(70% .01 250);
31
+ --border: oklch(24% .02 260);
32
+ --border-strong: oklch(30% .025 260);
33
+ --input: oklch(24% .02 260);
34
+ --primary: oklch(70% .18 265); /* indigo */
35
+ --primary-foreground: oklch(14% .02 260);
36
+ --secondary: oklch(68% .18 295); /* purple */
37
+ --accent: oklch(68% .14 200); /* cyan/teal */
38
+ --info: oklch(68% .2 245); /* bright blue */
39
+ --warn: oklch(78% .15 80); /* warm amber */
40
+ --destructive: oklch(68% .22 25); /* red */
41
+ --ring: oklch(70% .18 265);
42
+ --grad-hero: linear-gradient(135deg,
43
+ oklch(70% .18 265) 0%,
44
+ oklch(68% .18 295) 50%,
45
+ oklch(68% .14 200) 100%);
46
+ --grad-saved: linear-gradient(90deg,
47
+ oklch(68% .14 200), oklch(70% .18 265));
48
+ --grad-response: linear-gradient(90deg,
49
+ oklch(68% .2 245), oklch(70% .18 265));
50
+ --bg-noise: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
51
+ --bg-base: linear-gradient(135deg,#01030c,#030513,#040915,#030310,#02060f);
52
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
53
+ --shadow: 0 10px 40px -12px rgba(0, 0, 0, 0.6);
54
+ --shadow-glow: 0 0 0 1px oklch(70% .18 265 / .25),
55
+ 0 20px 60px -20px oklch(70% .18 265 / .35);
56
+ --ring-shadow: 0 0 0 3px oklch(70% .18 265 / .35);
57
+ --duration-fast: .15s;
58
+ --duration-normal: .3s;
59
+ --duration-slow: .5s;
60
+ --ease-out: cubic-bezier(0, 0, .2, 1);
61
+ --ease-bounce: cubic-bezier(.68, -.55, .265, 1.55);
62
+ --radius: .75rem;
63
+ --radius-sm: .5rem;
64
+ --radius-lg: 1rem;
65
+ }
66
+ :root[data-theme='light'] {
67
+ --background: oklch(97.5% .006 260);
68
+ --foreground: oklch(18% .03 260);
69
+ --card: oklch(100% 0 0);
70
+ --card-elevated: oklch(99% .002 260);
71
+ --card-foreground: oklch(20% .03 260);
72
+ --muted: oklch(96% .005 260);
73
+ --muted-foreground: oklch(48% .015 260);
74
+ --border: oklch(90% .008 260);
75
+ --border-strong: oklch(82% .012 260);
76
+ --input: oklch(90% .008 260);
77
+ --primary: oklch(55% .2 265);
78
+ --primary-foreground: oklch(100% 0 0);
79
+ --secondary: oklch(52% .18 290);
80
+ --accent: oklch(52% .14 215);
81
+ --info: oklch(62% .2 245);
82
+ --warn: oklch(60% .14 80);
83
+ --destructive: oklch(58% .22 25);
84
+ --ring: oklch(55% .2 265);
85
+ --grad-hero: linear-gradient(135deg,
86
+ oklch(55% .2 265), oklch(52% .18 290), oklch(52% .14 215));
87
+ --grad-saved: linear-gradient(90deg,
88
+ oklch(52% .14 215), oklch(55% .2 265));
89
+ --grad-response: linear-gradient(90deg,
90
+ oklch(62% .2 245), oklch(55% .2 265));
91
+ --bg-noise: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.012'/%3E%3C/svg%3E");
92
+ --bg-base: linear-gradient(180deg,#f8fafe,#f4f7fc 40%,#f6f9fc);
93
+ --shadow: 0 10px 40px -16px oklch(48% .015 260 / .25);
94
+ --shadow-glow: 0 0 0 1px oklch(55% .2 265 / .2),
95
+ 0 20px 60px -20px oklch(55% .2 265 / .3);
96
+ }
97
+
98
+ /* ============================================================
99
+ Base
100
+ ============================================================ */
101
+ * { box-sizing: border-box; }
102
+ html, body { margin: 0; padding: 0; }
103
+ html { scroll-behavior: smooth; scroll-padding-top: 86px; }
104
+ body {
105
+ font-family: 'Inter', system-ui, -apple-system, Segoe UI, sans-serif;
106
+ color: var(--foreground);
107
+ background: var(--bg-noise), var(--bg-base);
108
+ background-attachment: fixed;
109
+ min-height: 100vh;
110
+ -webkit-font-smoothing: antialiased;
111
+ letter-spacing: -0.005em;
112
+ line-height: 1.5;
113
+ }
114
+ a { color: var(--primary); text-decoration: none; transition: opacity var(--duration-fast); }
115
+ a:hover { opacity: 0.8; }
116
+ h1, h2, h3, .display, .num {
117
+ font-family: 'Outfit', 'Inter', sans-serif;
118
+ letter-spacing: -0.025em;
119
+ }
120
+ code, .mono {
121
+ font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
122
+ }
123
+ button { font-family: inherit; }
124
+ ::selection { background: oklch(70% .18 265 / .35); }
125
+
126
+ .container { max-width: 1320px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 32px); }
127
+
128
+ /* ============================================================
129
+ Top nav
130
+ ============================================================ */
131
+ nav.topnav {
132
+ position: sticky; top: 0; z-index: 50;
133
+ backdrop-filter: blur(18px) saturate(160%);
134
+ -webkit-backdrop-filter: blur(18px) saturate(160%);
135
+ background: oklch(from var(--background) l c h / 0.7);
136
+ border-bottom: 1px solid var(--border);
137
+ }
138
+ .nav-inner {
139
+ display: flex; align-items: center; gap: 18px;
140
+ padding: 14px 0; flex-wrap: wrap;
141
+ }
142
+ .brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
143
+ .brand-mark {
144
+ width: 38px; height: 38px; border-radius: 12px;
145
+ overflow: hidden; flex-shrink: 0;
146
+ background: oklch(from var(--primary) l c h / .12);
147
+ border: 1px solid oklch(from var(--primary) l c h / .25);
148
+ display: grid; place-items: center;
149
+ box-shadow: 0 0 24px -6px oklch(from var(--primary) l c h / .5);
150
+ }
151
+ .brand-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }
152
+ .brand-name {
153
+ font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 16px;
154
+ letter-spacing: -0.02em; line-height: 1.1;
155
+ }
156
+ .brand-sub {
157
+ font-size: 10px; color: var(--muted-foreground);
158
+ letter-spacing: 0.14em; text-transform: uppercase; margin-top: 2px;
159
+ }
160
+ .nav-links { display: flex; gap: 2px; margin-left: 8px; flex-wrap: wrap; }
161
+ .nav-link {
162
+ padding: 8px 13px; border-radius: 10px; font-size: 13px; font-weight: 500;
163
+ color: var(--muted-foreground); transition: all var(--duration-fast);
164
+ display: inline-flex; align-items: center; gap: 7px;
165
+ }
166
+ .nav-link:hover { background: var(--muted); color: var(--foreground); }
167
+ .nav-link.active { background: oklch(from var(--primary) l c h / .12); color: var(--foreground); }
168
+ .nav-link svg { width: 14px; height: 14px; }
169
+
170
+ .nav-spacer { flex: 1; }
171
+ .nav-actions { display: flex; align-items: center; gap: 8px; }
172
+ .pill {
173
+ display: inline-flex; align-items: center; gap: 7px;
174
+ padding: 6px 12px; border-radius: 999px;
175
+ background: var(--card); border: 1px solid var(--border);
176
+ font-size: 12px; color: var(--muted-foreground); font-weight: 500;
177
+ }
178
+ .pill svg { width: 12px; height: 12px; }
179
+ .pill .dot {
180
+ width: 6px; height: 6px; border-radius: 50%;
181
+ background: var(--accent); box-shadow: 0 0 8px var(--accent);
182
+ animation: pulse 2s ease-in-out infinite;
183
+ }
184
+ @keyframes pulse {
185
+ 0%, 100% { opacity: 1; transform: scale(1); }
186
+ 50% { opacity: .5; transform: scale(.85); }
187
+ }
188
+ .icon-btn {
189
+ width: 36px; height: 36px; border-radius: 10px;
190
+ border: 1px solid var(--border); background: var(--card);
191
+ color: var(--muted-foreground); cursor: pointer;
192
+ display: grid; place-items: center;
193
+ transition: all var(--duration-fast);
194
+ }
195
+ .icon-btn:hover {
196
+ color: var(--foreground); border-color: var(--border-strong);
197
+ background: var(--card-elevated);
198
+ }
199
+ .icon-btn svg { width: 16px; height: 16px; }
200
+
201
+ /* ============================================================
202
+ Hero
203
+ ============================================================ */
204
+ .hero {
205
+ position: relative; margin-top: 32px;
206
+ padding: 44px clamp(24px, 4vw, 44px);
207
+ border-radius: var(--radius-lg);
208
+ background:
209
+ radial-gradient(ellipse 80% 50% at 50% 0%, oklch(from var(--primary) l c h / .12) 0%, transparent 50%),
210
+ radial-gradient(circle at 10% 40%, oklch(from var(--secondary) l c h / .08) 0%, transparent 35%),
211
+ radial-gradient(circle at 90% 60%, oklch(from var(--accent) l c h / .08) 0%, transparent 35%),
212
+ var(--card);
213
+ border: 1px solid var(--border);
214
+ box-shadow: var(--shadow);
215
+ display: grid; gap: 36px; grid-template-columns: 1fr;
216
+ animation: fade-up .6s .05s both;
217
+ overflow: hidden;
218
+ }
219
+ @media (min-width: 920px) { .hero { grid-template-columns: 1.5fr 1fr; align-items: center; } }
220
+ @keyframes fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
221
+
222
+ .hero-left { display: flex; flex-direction: column; gap: 18px; }
223
+ .hero-brand-row { display: flex; align-items: center; gap: 14px; }
224
+ .hero-mascot {
225
+ width: 72px; height: 72px; border-radius: 18px; overflow: hidden;
226
+ background: oklch(from var(--primary) l c h / .15);
227
+ border: 1px solid oklch(from var(--primary) l c h / .3);
228
+ box-shadow:
229
+ 0 0 0 4px oklch(from var(--primary) l c h / .08),
230
+ 0 12px 28px -12px oklch(from var(--primary) l c h / .55);
231
+ flex-shrink: 0;
232
+ animation: float 6s ease-in-out infinite;
233
+ }
234
+ .hero-mascot img { width: 100%; height: 100%; object-fit: cover; display: block; }
235
+ @keyframes float {
236
+ 0%, 100% { transform: translateY(0); }
237
+ 50% { transform: translateY(-4px); }
238
+ }
239
+
240
+ .eyebrow {
241
+ display: inline-flex; align-items: center; gap: 8px;
242
+ font-family: 'Outfit', sans-serif;
243
+ font-size: 11px; font-weight: 600;
244
+ letter-spacing: 0.16em; text-transform: uppercase;
245
+ color: var(--accent);
246
+ }
247
+ .eyebrow svg { width: 14px; height: 14px; }
248
+
249
+ .hero-title {
250
+ font-family: 'Outfit', sans-serif;
251
+ font-size: clamp(40px, 6.5vw, 76px);
252
+ line-height: 0.95; margin: 0; font-weight: 800;
253
+ letter-spacing: -0.04em;
254
+ }
255
+ .hero-title .approx { color: var(--muted-foreground); font-weight: 400; margin-right: 4px; }
256
+ .hero-title .num {
257
+ background: var(--grad-hero);
258
+ -webkit-background-clip: text; background-clip: text;
259
+ -webkit-text-fill-color: transparent;
260
+ font-feature-settings: 'tnum';
261
+ }
262
+ .hero-title .unit {
263
+ font-size: 0.42em; color: var(--muted-foreground);
264
+ font-weight: 500; margin-left: 10px; letter-spacing: -0.02em;
265
+ }
266
+
267
+ .hero-disclaimer {
268
+ display: inline-flex; align-items: center; gap: 8px;
269
+ padding: 6px 11px; border-radius: 999px;
270
+ background: var(--muted); border: 1px dashed var(--border-strong);
271
+ font-size: 11px; color: var(--muted-foreground); font-weight: 500;
272
+ align-self: flex-start;
273
+ }
274
+ .hero-disclaimer svg { width: 12px; height: 12px; color: var(--info); }
275
+
276
+ .hero-sub { color: var(--muted-foreground); font-size: 16px; line-height: 1.6; max-width: 580px; margin: 0; }
277
+ .hero-sub strong { color: var(--foreground); font-weight: 600; }
278
+
279
+ .hero-meta { display: flex; gap: 10px; flex-wrap: wrap; }
280
+ .hero-chip {
281
+ display: inline-flex; align-items: center; gap: 9px;
282
+ padding: 10px 14px; border-radius: 12px;
283
+ background: var(--muted); border: 1px solid var(--border);
284
+ font-size: 13px; color: var(--muted-foreground);
285
+ }
286
+ .hero-chip strong {
287
+ color: var(--foreground); font-weight: 600; font-variant-numeric: tabular-nums;
288
+ font-family: 'JetBrains Mono', monospace; font-size: 12px;
289
+ }
290
+ .hero-chip svg { width: 14px; height: 14px; color: var(--accent); }
291
+
292
+ .ring-wrap {
293
+ position: relative; display: grid; place-items: center;
294
+ min-height: 260px;
295
+ }
296
+ .ring-center {
297
+ position: absolute; inset: 0;
298
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
299
+ pointer-events: none;
300
+ }
301
+ .ring-pct {
302
+ font-family: 'Outfit', sans-serif; font-size: 48px; font-weight: 700;
303
+ letter-spacing: -0.04em; font-variant-numeric: tabular-nums;
304
+ }
305
+ .ring-label {
306
+ font-family: 'Outfit', sans-serif;
307
+ font-size: 11px; color: var(--muted-foreground);
308
+ letter-spacing: 0.16em; text-transform: uppercase;
309
+ margin-top: 4px; font-weight: 600;
310
+ }
311
+
312
+ /* ============================================================
313
+ KPI strip
314
+ ============================================================ */
315
+ .kpi-strip {
316
+ display: grid; gap: 16px; margin-top: 24px;
317
+ grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
318
+ }
319
+ .kpi {
320
+ position: relative; padding: 22px;
321
+ border-radius: var(--radius); overflow: hidden;
322
+ background: var(--card);
323
+ border: 1px solid var(--border); box-shadow: var(--shadow);
324
+ transition: transform var(--duration-fast), box-shadow var(--duration-normal), border-color var(--duration-fast);
325
+ animation: fade-up .5s both;
326
+ }
327
+ .kpi:nth-child(1) { animation-delay: .1s; }
328
+ .kpi:nth-child(2) { animation-delay: .15s; }
329
+ .kpi:nth-child(3) { animation-delay: .2s; }
330
+ .kpi:nth-child(4) { animation-delay: .25s; }
331
+ .kpi:hover {
332
+ transform: translateY(-2px);
333
+ border-color: var(--border-strong);
334
+ box-shadow: var(--shadow-glow);
335
+ }
336
+ .kpi-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
337
+ .kpi-icon {
338
+ width: 38px; height: 38px; border-radius: 11px;
339
+ display: grid; place-items: center;
340
+ background: var(--muted); border: 1px solid var(--border);
341
+ }
342
+ .kpi-icon svg { width: 18px; height: 18px; }
343
+ .kpi.primary .kpi-icon { background: oklch(from var(--primary) l c h / .15); border-color: oklch(from var(--primary) l c h / .3); color: var(--primary); }
344
+ .kpi.accent .kpi-icon { background: oklch(from var(--accent) l c h / .15); border-color: oklch(from var(--accent) l c h / .3); color: var(--accent); }
345
+ .kpi.info .kpi-icon { background: oklch(from var(--info) l c h / .15); border-color: oklch(from var(--info) l c h / .3); color: var(--info); }
346
+ .kpi.destr .kpi-icon { background: oklch(from var(--destructive) l c h / .15); border-color: oklch(from var(--destructive) l c h / .3); color: var(--destructive); }
347
+ .kpi-label {
348
+ font-family: 'Outfit', sans-serif;
349
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em;
350
+ color: var(--muted-foreground); font-weight: 600;
351
+ }
352
+ .kpi-value {
353
+ font-family: 'Outfit', sans-serif;
354
+ font-size: 36px; font-weight: 800;
355
+ letter-spacing: -0.035em; margin: 10px 0 6px;
356
+ font-variant-numeric: tabular-nums; line-height: 1;
357
+ }
358
+ .kpi-sub {
359
+ font-size: 12px; color: var(--muted-foreground);
360
+ display: flex; align-items: center; gap: 6px;
361
+ }
362
+ .kpi-sub svg { width: 12px; height: 12px; }
363
+ .kpi-bar {
364
+ position: absolute; top: 0; left: 0; right: 0; height: 3px;
365
+ background: linear-gradient(90deg, transparent, var(--primary), transparent);
366
+ opacity: 0.7;
367
+ }
368
+ .kpi.accent .kpi-bar { background: linear-gradient(90deg, transparent, var(--accent), transparent); }
369
+ .kpi.info .kpi-bar { background: linear-gradient(90deg, transparent, var(--info), transparent); }
370
+ .kpi.destr .kpi-bar { background: linear-gradient(90deg, transparent, var(--destructive), transparent); }
371
+
372
+ /* ============================================================
373
+ Sections
374
+ ============================================================ */
375
+ section.panel {
376
+ margin-top: 32px;
377
+ background: var(--card);
378
+ border: 1px solid var(--border);
379
+ border-radius: var(--radius-lg);
380
+ padding: clamp(20px, 3vw, 30px);
381
+ box-shadow: var(--shadow);
382
+ scroll-margin-top: 86px;
383
+ overflow: hidden;
384
+ }
385
+ .section-head {
386
+ display: flex; align-items: flex-start; justify-content: space-between;
387
+ gap: 16px; margin-bottom: 8px; flex-wrap: wrap;
388
+ }
389
+ .section-title { display: flex; align-items: center; gap: 12px; min-width: 0; }
390
+ .section-title h2 {
391
+ font-family: 'Outfit', sans-serif;
392
+ font-size: 20px; margin: 0; font-weight: 700; letter-spacing: -0.02em;
393
+ line-height: 1.2;
394
+ }
395
+ .section-actions {
396
+ display: flex; align-items: center; justify-content: flex-end;
397
+ gap: 10px; flex-wrap: wrap; margin-left: auto;
398
+ }
399
+ .section-icon {
400
+ width: 32px; height: 32px; border-radius: 10px;
401
+ display: grid; place-items: center;
402
+ background: oklch(from var(--primary) l c h / .12);
403
+ border: 1px solid oklch(from var(--primary) l c h / .25);
404
+ color: var(--primary);
405
+ }
406
+ .section-icon svg { width: 16px; height: 16px; }
407
+ .section-desc {
408
+ font-size: 14px; color: var(--muted-foreground);
409
+ margin: 0 0 24px; max-width: 760px; line-height: 1.6;
410
+ }
411
+ .badge-count {
412
+ display: inline-flex; align-items: center; gap: 6px;
413
+ padding: 5px 11px; border-radius: 999px;
414
+ background: var(--muted); border: 1px solid var(--border);
415
+ font-size: 11px; color: var(--muted-foreground);
416
+ font-variant-numeric: tabular-nums; font-weight: 500;
417
+ letter-spacing: 0.04em;
418
+ }
419
+
420
+ /* ============================================================
421
+ Savings meter
422
+ ============================================================ */
423
+ .savings-grid { display: grid; gap: 28px; grid-template-columns: 1fr; }
424
+ @media (min-width: 920px) { .savings-grid { grid-template-columns: 1.4fr 1fr; align-items: center; } }
425
+ .meter-block { display: flex; flex-direction: column; gap: 14px; }
426
+ .meter-row {
427
+ display: flex; justify-content: space-between; gap: 12px; align-items: baseline;
428
+ }
429
+ .meter-row .label { color: var(--muted-foreground); font-size: 13px; font-weight: 500; }
430
+ .meter-row .val {
431
+ font-family: 'JetBrains Mono', monospace;
432
+ color: var(--foreground); font-size: 13px; font-weight: 500;
433
+ }
434
+ .meter {
435
+ position: relative; height: 20px; border-radius: 12px;
436
+ background: var(--muted); overflow: hidden; display: flex;
437
+ border: 1px solid var(--border);
438
+ }
439
+ .meter-seg { height: 100%; transition: width 1.2s var(--ease-out); position: relative; }
440
+ .meter-seg.response { background: var(--grad-response); }
441
+ .meter-seg.saved { background: var(--grad-saved); position: relative; overflow: hidden; }
442
+ .meter-seg.saved::after {
443
+ content: ''; position: absolute; inset: 0;
444
+ background: linear-gradient(90deg, transparent, oklch(100% 0 0 / .35), transparent);
445
+ animation: shimmer 2.6s infinite;
446
+ }
447
+ @keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
448
+ .legend { display: flex; gap: 18px; font-size: 12px; color: var(--muted-foreground); flex-wrap: wrap; margin-top: 6px; }
449
+ .legend-item { display: inline-flex; align-items: center; gap: 7px; }
450
+ .legend-dot { width: 11px; height: 11px; border-radius: 4px; }
451
+ .legend-item strong {
452
+ color: var(--foreground); font-family: 'JetBrains Mono', monospace; font-weight: 500;
453
+ }
454
+
455
+ /* ============================================================
456
+ Tables
457
+ ============================================================ */
458
+ .table-wrap {
459
+ overflow-x: auto;
460
+ margin: 0;
461
+ border: 1px solid var(--border);
462
+ border-radius: var(--radius);
463
+ background: var(--card-elevated);
464
+ box-shadow: inset 0 1px 0 oklch(100% 0 0 / .03);
465
+ }
466
+ .table-wrap.spaced { margin-top: 24px; }
467
+ table.data {
468
+ width: 100%; min-width: 640px;
469
+ border-collapse: separate; border-spacing: 0;
470
+ font-size: 13px; table-layout: fixed;
471
+ }
472
+ #tools-table { min-width: 1040px; }
473
+ #cache-table { min-width: 420px; }
474
+ table.data th, table.data td {
475
+ text-align: left; padding: 14px 16px;
476
+ border-bottom: 1px solid var(--border);
477
+ vertical-align: middle;
478
+ }
479
+ table.data th {
480
+ font-family: 'Outfit', sans-serif;
481
+ color: var(--muted-foreground); font-weight: 700; text-transform: uppercase;
482
+ font-size: 10px; letter-spacing: 0.11em;
483
+ position: sticky; top: 0; z-index: 1; background: var(--card-elevated);
484
+ cursor: pointer; user-select: none;
485
+ transition: color var(--duration-fast), background var(--duration-fast);
486
+ white-space: nowrap;
487
+ }
488
+ #tools-table th:first-child, #tools-table td:first-child { width: 24%; }
489
+ #tools-table th:nth-child(2), #tools-table td:nth-child(2) { width: 150px; }
490
+ #cache-table th:last-child, #cache-table td:last-child { width: 110px; }
491
+ table.data th.num, table.data td.num {
492
+ text-align: right;
493
+ font-variant-numeric: tabular-nums;
494
+ font-family: 'JetBrains Mono', monospace;
495
+ }
496
+ table.data th.num { color: var(--muted-foreground); }
497
+ table.data td { color: var(--foreground); }
498
+ table.data td.num { color: var(--muted-foreground); }
499
+ .muted-cell { color: var(--muted-foreground); }
500
+ table.data th:hover { color: var(--foreground); background: var(--muted); }
501
+ table.data th .sort-ind { opacity: .4; margin-left: 4px; font-size: 10px; }
502
+ table.data th.sorted { color: var(--primary); }
503
+ table.data th.sorted .sort-ind { opacity: 1; color: var(--primary); }
504
+ table.data td.num.strong { color: var(--foreground); font-weight: 600; }
505
+ table.data tr:last-child td { border-bottom: none; }
506
+ table.data tbody tr { transition: background var(--duration-fast); }
507
+ table.data tbody tr:hover { background: var(--muted); }
508
+ table.data .tool-cell, table.data .endpoint-cell {
509
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
510
+ }
511
+ table.data .tool-cell .chip { max-width: 100%; }
512
+
513
+ .search-box {
514
+ display: inline-flex; align-items: center; gap: 8px;
515
+ padding: 9px 13px; border-radius: 10px;
516
+ background: var(--muted); border: 1px solid var(--border);
517
+ transition: border-color var(--duration-fast);
518
+ }
519
+ .search-box:focus-within { border-color: var(--primary); box-shadow: var(--ring-shadow); }
520
+ .search-box svg { width: 14px; height: 14px; color: var(--muted-foreground); }
521
+ .search-box input {
522
+ background: transparent; border: none; outline: none;
523
+ color: var(--foreground); font-size: 13px; font-family: inherit; width: 200px;
524
+ }
525
+ .search-box input::placeholder { color: var(--muted-foreground); }
526
+
527
+ /* ============================================================
528
+ Chips
529
+ ============================================================ */
530
+ .chip {
531
+ display: inline-flex; align-items: center; gap: 6px;
532
+ padding: 4px 10px; border-radius: 999px;
533
+ font-size: 11px; font-weight: 500;
534
+ background: var(--muted); color: var(--muted-foreground);
535
+ border: 1px solid var(--border);
536
+ max-width: 100%; min-width: 0;
537
+ }
538
+ .chip svg { width: 11px; height: 11px; flex-shrink: 0; }
539
+ .chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
540
+ .chip.primary { background: oklch(from var(--primary) l c h / .12); color: var(--primary); border-color: oklch(from var(--primary) l c h / .3); }
541
+ .chip.accent { background: oklch(from var(--accent) l c h / .12); color: var(--accent); border-color: oklch(from var(--accent) l c h / .3); }
542
+ .chip.info { background: oklch(from var(--info) l c h / .12); color: var(--info); border-color: oklch(from var(--info) l c h / .3); }
543
+ .chip.warn { background: oklch(from var(--warn) l c h / .12); color: var(--warn); border-color: oklch(from var(--warn) l c h / .3); }
544
+ .chip.destr { background: oklch(from var(--destructive) l c h / .12); color: var(--destructive); border-color: oklch(from var(--destructive) l c h / .3); }
545
+
546
+ /* ============================================================
547
+ Callout
548
+ ============================================================ */
549
+ .callout {
550
+ display: flex; align-items: center; gap: 14px;
551
+ padding: 18px 20px; border-radius: var(--radius);
552
+ background:
553
+ linear-gradient(135deg,
554
+ oklch(from var(--accent) l c h / .08),
555
+ oklch(from var(--primary) l c h / .08));
556
+ border: 1px solid oklch(from var(--accent) l c h / .25);
557
+ }
558
+ .callout-icon {
559
+ width: 40px; height: 40px; border-radius: 11px;
560
+ background: var(--grad-hero); display: grid; place-items: center;
561
+ color: oklch(14% .02 260); flex-shrink: 0;
562
+ box-shadow: 0 0 0 4px oklch(from var(--accent) l c h / .12);
563
+ }
564
+ .callout-icon svg { width: 20px; height: 20px; }
565
+ .callout-title {
566
+ font-family: 'Outfit', sans-serif;
567
+ font-weight: 700; font-size: 15px; margin-bottom: 2px;
568
+ }
569
+ .callout-sub { font-size: 13px; color: var(--muted-foreground); }
570
+ .cache-compact .section-desc { margin-bottom: 12px; }
571
+ .cache-compact .callout { padding: 13px 15px; gap: 12px; }
572
+ .cache-compact .callout-icon { width: 34px; height: 34px; border-radius: 9px; }
573
+ .cache-compact .callout-title { font-size: 14px; }
574
+ .cache-compact .callout-sub { font-size: 12px; }
575
+ .cache-compact table.data th,
576
+ .cache-compact table.data td { padding: 9px 10px; }
577
+
578
+ /* ============================================================
579
+ Mini KPIs (in row)
580
+ ============================================================ */
581
+ .row { display: flex; gap: 14px; flex-wrap: wrap; }
582
+ .mini-kpi {
583
+ flex: 1; min-width: 140px; padding: 16px 18px; border-radius: var(--radius);
584
+ background: var(--muted); border: 1px solid var(--border);
585
+ }
586
+ .mini-kpi .label {
587
+ font-family: 'Outfit', sans-serif;
588
+ font-size: 11px; color: var(--muted-foreground);
589
+ text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600;
590
+ }
591
+ .mini-kpi .value {
592
+ font-family: 'Outfit', sans-serif;
593
+ font-size: 24px; font-weight: 700; margin-top: 6px;
594
+ font-variant-numeric: tabular-nums; letter-spacing: -0.025em;
595
+ }
596
+ .mini-kpi .value.primary { color: var(--primary); }
597
+ .mini-kpi .value.accent { color: var(--accent); }
598
+ .mini-kpi .value.info { color: var(--info); }
599
+ .mini-kpi .value.warn { color: var(--warn); }
600
+ .mini-kpi .value.destr { color: var(--destructive); }
601
+
602
+ /* ============================================================
603
+ Charts
604
+ ============================================================ */
605
+ .chart-wrap { position: relative; min-height: 280px; }
606
+ .apexcharts-tooltip {
607
+ background: var(--card-elevated) !important;
608
+ border: 1px solid var(--border-strong) !important;
609
+ box-shadow: var(--shadow) !important;
610
+ color: var(--foreground) !important;
611
+ border-radius: var(--radius-sm) !important;
612
+ }
613
+ .apexcharts-tooltip-title {
614
+ background: var(--muted) !important;
615
+ border-bottom: 1px solid var(--border) !important;
616
+ }
617
+
618
+ /* ============================================================
619
+ Empty + utility
620
+ ============================================================ */
621
+ .empty-list {
622
+ padding: 32px 16px; text-align: center; color: var(--muted-foreground);
623
+ font-size: 13px; background: var(--muted); border-radius: var(--radius-sm);
624
+ border: 1px dashed var(--border);
625
+ }
626
+ .empty-state-card {
627
+ padding: 44px 28px; text-align: center;
628
+ background: var(--card); border: 1px dashed var(--border-strong);
629
+ border-radius: var(--radius); margin-top: 24px;
630
+ }
631
+ .empty-state-card .mascot-empty {
632
+ width: 88px; height: 88px; margin: 0 auto 16px;
633
+ border-radius: 22px; overflow: hidden;
634
+ background: oklch(from var(--primary) l c h / .1);
635
+ border: 1px solid oklch(from var(--primary) l c h / .25);
636
+ opacity: 0.7;
637
+ }
638
+ .empty-state-card .mascot-empty img { width: 100%; height: 100%; object-fit: cover; }
639
+ .empty-state-card strong {
640
+ font-family: 'Outfit', sans-serif;
641
+ color: var(--foreground); display: block;
642
+ margin-bottom: 6px; font-size: 16px; font-weight: 600;
643
+ }
644
+
645
+ .grid-2 { display: grid; gap: 24px; grid-template-columns: minmax(0, 1fr); align-items: start; }
646
+ @media (min-width: 920px) { .grid-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
647
+ .grid-2.uneven { grid-template-columns: minmax(0, 1fr); }
648
+ @media (min-width: 920px) { .grid-2.uneven { grid-template-columns: minmax(0, 1.4fr) minmax(320px, 1fr); } }
649
+ @media (max-width: 720px) {
650
+ .section-actions { width: 100%; justify-content: stretch; }
651
+ .section-actions .badge-count, .section-actions .search-box { flex: 1 1 auto; }
652
+ .search-box input { width: min(100%, 220px); }
653
+ #tools-table { min-width: 920px; }
654
+ #cache-table { min-width: 360px; }
655
+ table.data th, table.data td { padding: 12px 14px; }
656
+ }
657
+
658
+ /* ============================================================
659
+ Footer
660
+ ============================================================ */
661
+ footer.page {
662
+ margin: 44px 0 60px;
663
+ display: flex; gap: 16px; flex-wrap: wrap; justify-content: space-between;
664
+ align-items: center; font-size: 12px; color: var(--muted-foreground);
665
+ }
666
+ .footer-disclaimer {
667
+ display: flex; align-items: center; gap: 10px;
668
+ padding: 12px 16px; border-radius: var(--radius);
669
+ background: var(--muted); border: 1px solid var(--border);
670
+ flex: 1; min-width: 280px; max-width: 560px;
671
+ font-size: 12px; color: var(--muted-foreground); line-height: 1.5;
672
+ }
673
+ .footer-disclaimer svg { width: 16px; height: 16px; color: var(--info); flex-shrink: 0; }
674
+ .copy-row {
675
+ display: inline-flex; align-items: center; gap: 8px;
676
+ padding: 7px 12px; border-radius: 10px;
677
+ background: var(--card); border: 1px solid var(--border);
678
+ }
679
+ .copy-row code { font-size: 11px; color: var(--muted-foreground); }
680
+ .copy-btn {
681
+ background: none; border: none; cursor: pointer;
682
+ color: var(--muted-foreground); padding: 2px; border-radius: 4px;
683
+ transition: color var(--duration-fast);
684
+ }
685
+ .copy-btn:hover { color: var(--primary); }
686
+ .copy-btn.copied { color: var(--accent); }
687
+ .copy-btn svg { width: 14px; height: 14px; display: block; }
688
+
689
+ @media print {
690
+ nav.topnav, .icon-btn { display: none !important; }
691
+ body { background: white; color: black; }
692
+ .kpi, .panel, .hero { box-shadow: none; border: 1px solid #ddd; background: white; }
693
+ }
694
+ </style>
695
+ </head>
696
+ <body>
697
+ <nav class="topnav">
698
+ <div class="container nav-inner">
699
+ <a class="brand" href="https://octocode.ai/" target="_blank" rel="noopener" style="color: inherit;">
700
+ <div class="brand-mark"><img src="https://octocode.ai/apple-touch-icon.png" alt="Octocode" /></div>
701
+ <div>
702
+ <div class="brand-name">Octocode Stats</div>
703
+ <div class="brand-sub">MCP usage dashboard</div>
704
+ </div>
705
+ </a>
706
+ <div class="nav-links" id="nav-links">
707
+ <a class="nav-link" href="#overview"><i data-lucide="layout-dashboard"></i> Overview</a>
708
+ <a class="nav-link" href="#tools"><i data-lucide="wrench"></i> Tools</a>
709
+ <a class="nav-link" href="#cache"><i data-lucide="database"></i> Cache</a>
710
+ <a class="nav-link" href="#errors"><i data-lucide="shield-alert"></i> Health</a>
711
+ </div>
712
+ <div class="nav-spacer"></div>
713
+ <div class="nav-actions">
714
+ <span class="pill" id="session-pill" hidden><span class="dot"></span> <span id="session-pill-text">Active session</span></span>
715
+ <span class="pill"><i data-lucide="clock"></i> <span id="generated-pill">—</span></span>
716
+ <button class="icon-btn" id="theme-toggle" title="Toggle theme"><i data-lucide="moon"></i></button>
717
+ </div>
718
+ </div>
719
+ </nav>
720
+
721
+ <main class="container" id="overview">
722
+ <section class="hero">
723
+ <div class="hero-left">
724
+ <div class="hero-brand-row">
725
+ <div class="hero-mascot"><img src="https://octocode.ai/apple-touch-icon.png" alt="Octocode mascot" /></div>
726
+ <div>
727
+ <div class="eyebrow"><i data-lucide="sparkles"></i> Octocode saved you</div>
728
+ <div style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--muted-foreground); margin-top: 4px;" id="hero-session-line">—</div>
729
+ </div>
730
+ </div>
731
+ <h1 class="hero-title">
732
+ <span class="approx">≈</span><span class="num" id="hero-tokens">0</span><span class="unit">tokens</span>
733
+ </h1>
734
+ <span class="hero-disclaimer">
735
+ <i data-lucide="info"></i>
736
+ Estimate · <strong id="hero-disclaimer-ratio">1 token ≈ 4 chars</strong> · actual usage varies by model
737
+ </span>
738
+ <p class="hero-sub" id="hero-sub">—</p>
739
+ <div class="hero-meta" id="hero-meta"></div>
740
+ </div>
741
+ <div class="ring-wrap">
742
+ <div id="ring-chart"></div>
743
+ <div class="ring-center">
744
+ <div class="ring-pct" id="ring-pct">0%</div>
745
+ <div class="ring-label">savings rate</div>
746
+ </div>
747
+ </div>
748
+ </section>
749
+
750
+ <div id="empty-state" class="empty-state-card" hidden>
751
+ <div class="mascot-empty"><img src="https://octocode.ai/apple-touch-icon.png" alt="" /></div>
752
+ <strong>No stats yet</strong>
753
+ Run any Octocode MCP tool, then regenerate this dashboard.
754
+ </div>
755
+
756
+ <div class="kpi-strip" id="kpi-strip"></div>
757
+ </main>
758
+
759
+ <main class="container">
760
+ <section class="panel">
761
+ <div class="section-head">
762
+ <div class="section-title">
763
+ <div class="section-icon"><i data-lucide="zap"></i></div>
764
+ <h2>Token Savings</h2>
765
+ </div>
766
+ <span class="badge-count" id="savings-calls-badge">— calls</span>
767
+ </div>
768
+ <p class="section-desc">How much response payload Octocode trimmed before sending it back to the agent. Saved bytes never reach the model — that's budget you keep.</p>
769
+ <div class="savings-grid">
770
+ <div class="meter-block">
771
+ <div class="meter-row">
772
+ <span class="label">Raw → Response → Saved</span>
773
+ <span class="val" id="raw-chars">—</span>
774
+ </div>
775
+ <div class="meter" id="savings-meter">
776
+ <div class="meter-seg response" style="width: 0%;"></div>
777
+ <div class="meter-seg saved" style="width: 0%;"></div>
778
+ </div>
779
+ <div class="legend">
780
+ <span class="legend-item"><span class="legend-dot" style="background: var(--info);"></span> Response sent <strong id="legend-response">—</strong></span>
781
+ <span class="legend-item"><span class="legend-dot" style="background: var(--accent);"></span> Saved <strong id="legend-saved">—</strong></span>
782
+ <span class="legend-item"><span class="legend-dot" style="background: var(--muted); border: 1px solid var(--border);"></span> Raw total <strong id="legend-raw">—</strong></span>
783
+ </div>
784
+ </div>
785
+ <div>
786
+ <div class="row">
787
+ <div class="mini-kpi">
788
+ <div class="label">Avg saved / call</div>
789
+ <div class="value accent" id="avg-savings">—</div>
790
+ </div>
791
+ <div class="mini-kpi">
792
+ <div class="label">Saving tools</div>
793
+ <div class="value primary" id="unique-tools">—</div>
794
+ </div>
795
+ </div>
796
+ </div>
797
+ </div>
798
+ </section>
799
+
800
+ <section class="panel" id="tools">
801
+ <div class="section-head">
802
+ <div class="section-title">
803
+ <div class="section-icon"><i data-lucide="wrench"></i></div>
804
+ <h2>Tool Response Savings</h2>
805
+ </div>
806
+ <div class="section-actions">
807
+ <span class="badge-count" id="tools-count-badge">— tools</span>
808
+ <div class="search-box">
809
+ <i data-lucide="search"></i>
810
+ <input id="tools-search" type="text" placeholder="Filter tools…" />
811
+ </div>
812
+ </div>
813
+ </div>
814
+ <p class="section-desc">Based on <code>charsSavedByTool</code>: measured tool responses with raw/response/saved character counts. GitHub and <code>packageSearch</code> provider payload details are merged here instead of repeated in a separate table.</p>
815
+ <div class="chart-wrap" style="min-height: 340px;"><div id="tools-treemap"></div></div>
816
+ <div class="table-wrap spaced">
817
+ <table class="data" id="tools-table">
818
+ <thead>
819
+ <tr>
820
+ <th data-key="name">Tool <span class="sort-ind">↕</span></th>
821
+ <th data-key="source">Source <span class="sort-ind">↕</span></th>
822
+ <th class="num" data-key="calls">Measured <span class="sort-ind">↕</span></th>
823
+ <th class="num" data-key="rawChars">Raw <span class="sort-ind">↕</span></th>
824
+ <th class="num" data-key="responseChars">Response <span class="sort-ind">↕</span></th>
825
+ <th class="num" data-key="savedChars">Saved <span class="sort-ind">↕</span></th>
826
+ <th class="num" data-key="deltaChars">Delta <span class="sort-ind">↕</span></th>
827
+ <th class="num" data-key="savingsPct">Saved % <span class="sort-ind">↕</span></th>
828
+ <th class="num" data-key="sentPctOfRaw">Sent % <span class="sort-ind">↕</span></th>
829
+ <th class="num" data-key="estimatedTokensSaved">≈ Tokens <span class="sort-ind">↕</span></th>
830
+ </tr>
831
+ </thead>
832
+ <tbody></tbody>
833
+ </table>
834
+ </div>
835
+ </section>
836
+
837
+ <section class="panel cache-compact" id="cache">
838
+ <div class="section-head">
839
+ <div class="section-title">
840
+ <div class="section-icon"><i data-lucide="database"></i></div>
841
+ <h2>GitHub Cache</h2>
842
+ </div>
843
+ <span class="badge-count" id="cache-count-badge">— endpoints</span>
844
+ </div>
845
+ <p class="section-desc">Cached GitHub reads served locally instead of spending API quota.</p>
846
+ <div class="callout" id="cache-callout" style="margin-bottom: 16px;">
847
+ <div class="callout-icon"><i data-lucide="shield-check"></i></div>
848
+ <div>
849
+ <div class="callout-title" id="cache-callout-title">—</div>
850
+ <div class="callout-sub" id="cache-callout-sub">—</div>
851
+ </div>
852
+ </div>
853
+ <div class="grid-2 uneven">
854
+ <div class="chart-wrap" style="min-height: 180px;"><div id="cache-bar"></div></div>
855
+ <div class="table-wrap">
856
+ <table class="data" id="cache-table">
857
+ <thead><tr><th>Endpoint</th><th class="num">Hits</th></tr></thead>
858
+ <tbody></tbody>
859
+ </table>
860
+ </div>
861
+ </div>
862
+ </section>
863
+
864
+ <section class="panel" id="errors">
865
+ <div class="section-head">
866
+ <div class="section-title">
867
+ <div class="section-icon"><i data-lucide="shield-alert"></i></div>
868
+ <h2>Health · Errors &amp; Rate Limits</h2>
869
+ </div>
870
+ <span class="badge-count" id="errors-count-badge">— events</span>
871
+ </div>
872
+ <p class="section-desc">Operational health: failures and provider throttling encountered during your session.</p>
873
+ <div class="row" style="margin-bottom: 24px;">
874
+ <div class="mini-kpi"><div class="label">Total errors</div><div class="value destr" id="errors-total">—</div></div>
875
+ <div class="mini-kpi"><div class="label">Rate limits hit</div><div class="value warn" id="rl-total">—</div></div>
876
+ <div class="mini-kpi"><div class="label">Package failures</div><div class="value destr" id="pkg-fail-total">—</div></div>
877
+ </div>
878
+ </section>
879
+
880
+ <footer class="page">
881
+ <div class="footer-disclaimer">
882
+ <i data-lucide="info"></i>
883
+ <span id="footer-disclaimer-text">—</span>
884
+ </div>
885
+ <div style="display: flex; align-items: center; gap: 14px; flex-wrap: wrap;">
886
+ <div class="copy-row">
887
+ <i data-lucide="file-json" style="width: 14px; height: 14px; color: var(--muted-foreground);"></i>
888
+ <code id="stats-path">—</code>
889
+ <button class="copy-btn" id="copy-btn" title="Copy path"><i data-lucide="copy"></i></button>
890
+ </div>
891
+ <span>Schema v<span id="schema-version">1</span> · <a href="https://octocode.ai/" target="_blank" rel="noopener">octocode.ai</a> · <a href="https://github.com/bgauryy/octocode-mcp/blob/main/packages/octocode-shared/docs/SESSION_PERSISTENCE.md" target="_blank" rel="noopener">docs</a></span>
892
+ </div>
893
+ </footer>
894
+ </main>
895
+
896
+ <script id="octocode-stats-data" type="application/json">__OCTOCODE_STATS_DATA__</script>
897
+
898
+ <script>
899
+ (function () {
900
+ const DATA = JSON.parse(document.getElementById('octocode-stats-data').textContent);
901
+ const t = DATA.totals;
902
+ const tokenRatio = DATA.tokenCharRatio || 4;
903
+
904
+ const fmt = (n) => Number(n || 0).toLocaleString();
905
+ const fmtPct = (n) => `${Number(n || 0).toFixed(1)}%`;
906
+ const fmtBytes = (n) => {
907
+ const v = Number(n || 0);
908
+ if (v < 1000) return `${v}`;
909
+ if (v < 1_000_000) return `${(v / 1000).toFixed(1)}K`;
910
+ if (v < 1_000_000_000) return `${(v / 1_000_000).toFixed(2)}M`;
911
+ return `${(v / 1_000_000_000).toFixed(2)}B`;
912
+ };
913
+ const fmtAge = (ms) => {
914
+ if (!ms || ms < 0) return null;
915
+ const s = Math.floor(ms / 1000);
916
+ const d = Math.floor(s / 86400);
917
+ const h = Math.floor((s % 86400) / 3600);
918
+ const m = Math.floor((s % 3600) / 60);
919
+ if (d > 0) return `${d}d ${h}h`;
920
+ if (h > 0) return `${h}h ${m}m`;
921
+ if (m > 0) return `${m}m`;
922
+ return `${s}s`;
923
+ };
924
+ const cssVar = (v) =>
925
+ getComputedStyle(document.documentElement).getPropertyValue(v).trim();
926
+ const animateNum = (el, end, { duration = 1.2, decimals = 0, suffix = '' } = {}) => {
927
+ if (typeof countUp === 'undefined' || !window.countUp) {
928
+ el.textContent = fmt(end) + suffix;
929
+ return;
930
+ }
931
+ const c = new countUp.CountUp(el, end, {
932
+ duration, decimals, suffix, separator: ',', useEasing: true,
933
+ });
934
+ if (!c.error) c.start(); else el.textContent = fmt(end) + suffix;
935
+ };
936
+
937
+ const renderIcons = () => { if (window.lucide) window.lucide.createIcons(); };
938
+ renderIcons();
939
+
940
+ // ---------- theme toggle ----------
941
+ const root = document.documentElement;
942
+ const themeBtn = document.getElementById('theme-toggle');
943
+ const setTheme = (mode) => {
944
+ root.setAttribute('data-theme', mode);
945
+ themeBtn.innerHTML = mode === 'dark' ? '<i data-lucide="sun"></i>' : '<i data-lucide="moon"></i>';
946
+ renderIcons();
947
+ try { localStorage.setItem('octocode-stats-theme', mode); } catch {}
948
+ rebuildCharts();
949
+ };
950
+ try {
951
+ const stored = localStorage.getItem('octocode-stats-theme');
952
+ if (stored === 'light' || stored === 'dark') setTheme(stored);
953
+ } catch {}
954
+ themeBtn.addEventListener('click', () => {
955
+ setTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
956
+ });
957
+
958
+ // ---------- header meta ----------
959
+ const dt = new Date(DATA.generatedAt);
960
+ document.getElementById('generated-pill').textContent =
961
+ dt.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
962
+ const sessionPill = document.getElementById('session-pill');
963
+ if (DATA.session && DATA.session.sessionAgeMs != null) {
964
+ const age = fmtAge(DATA.session.sessionAgeMs);
965
+ if (age) {
966
+ document.getElementById('session-pill-text').textContent = `Session ${age}`;
967
+ sessionPill.hidden = false;
968
+ }
969
+ }
970
+ document.getElementById('stats-path').textContent = DATA.statsPath;
971
+ if (DATA.schemaVersion)
972
+ document.getElementById('schema-version').textContent = String(DATA.schemaVersion);
973
+
974
+ const emptyState = document.getElementById('empty-state');
975
+ if (!DATA.hasStats || t.toolCalls === 0) emptyState.hidden = false;
976
+
977
+ // ---------- hero ----------
978
+ animateNum(document.getElementById('hero-tokens'), t.estimatedTokensSaved, { duration: 1.8 });
979
+ document.getElementById('hero-disclaimer-ratio').textContent = `1 token ≈ ${tokenRatio} chars`;
980
+ document.getElementById('footer-disclaimer-text').textContent = DATA.tokenEstimateNote;
981
+
982
+ // Session line under eyebrow
983
+ const sessionLineEl = document.getElementById('hero-session-line');
984
+ if (DATA.session?.createdAt) {
985
+ const since = new Date(DATA.session.createdAt).toLocaleDateString(undefined, { dateStyle: 'medium' });
986
+ const ageStr = fmtAge(DATA.session.sessionAgeMs);
987
+ sessionLineEl.textContent = ageStr ? `Session active ${ageStr} · since ${since}` : `Session since ${since}`;
988
+ } else {
989
+ sessionLineEl.textContent = `Generated ${dt.toLocaleString()}`;
990
+ }
991
+
992
+ // Hero sub — fact-only, no fabricated comparisons
993
+ const subParts = [];
994
+ if (t.savedChars > 0) {
995
+ subParts.push(`<strong>${fmt(t.savedChars)}</strong> response characters trimmed before reaching the model.`);
996
+ subParts.push(`That's a <strong>${fmtPct(t.savingsPct)}</strong> reduction across <strong>${fmt(t.charSavingsCalls)}</strong> tool responses.`);
997
+ } else {
998
+ subParts.push('No measurable response trimming yet — start using Octocode tools and your savings will appear here.');
999
+ }
1000
+ document.getElementById('hero-sub').innerHTML = subParts.join(' ');
1001
+
1002
+ const heroMeta = document.getElementById('hero-meta');
1003
+ const heroChips = [
1004
+ { icon: 'activity', label: 'Tool calls', value: fmt(t.toolCalls) },
1005
+ { icon: 'shield-check', label: 'Rate limits avoided', value: fmt(t.githubCacheRateLimits) },
1006
+ { icon: 'database', label: 'Cache hits', value: fmt(t.githubCacheHits) },
1007
+ ];
1008
+ heroMeta.innerHTML = heroChips
1009
+ .map((c) => `<span class="hero-chip"><i data-lucide="${c.icon}"></i><span>${c.label}</span> <strong>${c.value}</strong></span>`)
1010
+ .join('');
1011
+ renderIcons();
1012
+
1013
+ // ---------- KPI strip ----------
1014
+ const kpis = [
1015
+ { icon: 'activity', label: 'Tool calls', value: t.toolCalls, sub: `${fmt(t.promptCalls)} prompts`, subIcon: 'message-square', cls: 'info' },
1016
+ { icon: 'zap', label: 'Tokens saved', value: t.estimatedTokensSaved, sub: `${fmtPct(t.savingsPct)} of raw bytes (≈)`, subIcon: 'trending-down', cls: 'accent' },
1017
+ { icon: 'database', label: 'Cache hits', value: t.githubCacheHits, sub: `${fmt(t.githubCacheRateLimits)} limits avoided`, subIcon: 'shield-check', cls: 'primary' },
1018
+ { icon: t.errors > 0 ? 'shield-alert' : 'shield-check', label: 'Errors', value: t.errors, sub: `${fmt(t.rateLimits)} rate limits hit`, subIcon: 'alert-octagon', cls: t.errors > 0 ? 'destr' : '' },
1019
+ ];
1020
+ const kpiStrip = document.getElementById('kpi-strip');
1021
+ kpiStrip.innerHTML = kpis.map((k, i) => `
1022
+ <div class="kpi ${k.cls}">
1023
+ <div class="kpi-bar"></div>
1024
+ <div class="kpi-head">
1025
+ <div class="kpi-label">${k.label}</div>
1026
+ <div class="kpi-icon"><i data-lucide="${k.icon}"></i></div>
1027
+ </div>
1028
+ <div class="kpi-value" data-kpi="${i}" data-value="${k.value}">0</div>
1029
+ <div class="kpi-sub"><i data-lucide="${k.subIcon}"></i> ${k.sub}</div>
1030
+ </div>
1031
+ `).join('');
1032
+ renderIcons();
1033
+ document.querySelectorAll('[data-kpi]').forEach((el, idx) => {
1034
+ animateNum(el, Number(el.getAttribute('data-value')), { duration: 1.2 + idx * 0.1 });
1035
+ });
1036
+
1037
+ // ---------- savings meter ----------
1038
+ const meter = document.getElementById('savings-meter');
1039
+ const rawTotal = Math.max(t.rawChars, t.responseChars + t.savedChars, 1);
1040
+ const respPct = (t.responseChars / rawTotal) * 100;
1041
+ const savedPct = (t.savedChars / rawTotal) * 100;
1042
+ requestAnimationFrame(() => {
1043
+ meter.children[0].style.width = `${respPct.toFixed(2)}%`;
1044
+ meter.children[1].style.width = `${savedPct.toFixed(2)}%`;
1045
+ });
1046
+ document.getElementById('raw-chars').textContent = `${fmtBytes(t.rawChars)} chars total`;
1047
+ document.getElementById('legend-response').textContent = fmtBytes(t.responseChars);
1048
+ document.getElementById('legend-saved').textContent = fmtBytes(t.savedChars);
1049
+ document.getElementById('legend-raw').textContent = fmtBytes(t.rawChars);
1050
+ document.getElementById('savings-calls-badge').textContent = `${fmt(t.charSavingsCalls)} calls measured`;
1051
+ animateNum(document.getElementById('avg-savings'), t.avgCharsSavedPerCall);
1052
+ animateNum(document.getElementById('unique-tools'), t.uniqueTools);
1053
+
1054
+ // ---------- charts ----------
1055
+ let charts = [];
1056
+ const destroyCharts = () => {
1057
+ charts.forEach((c) => { try { c.destroy(); } catch {} });
1058
+ charts = [];
1059
+ };
1060
+
1061
+ function brandColors() {
1062
+ return {
1063
+ primary: cssVar('--primary'),
1064
+ secondary: cssVar('--secondary'),
1065
+ accent: cssVar('--accent'),
1066
+ info: cssVar('--info'),
1067
+ warn: cssVar('--warn'),
1068
+ destr: cssVar('--destructive'),
1069
+ text: cssVar('--foreground'),
1070
+ dim: cssVar('--muted-foreground'),
1071
+ border: cssVar('--border'),
1072
+ muted: cssVar('--muted'),
1073
+ };
1074
+ }
1075
+
1076
+ function buildRingChart() {
1077
+ const c = brandColors();
1078
+ const pct = Math.max(0, Math.min(100, t.savingsPct));
1079
+ const ring = new ApexCharts(document.getElementById('ring-chart'), {
1080
+ chart: { type: 'radialBar', height: 260, sparkline: { enabled: true }, animations: { enabled: true, speed: 1400, easing: 'easeOutCubic' } },
1081
+ series: [pct],
1082
+ colors: [c.accent],
1083
+ fill: {
1084
+ type: 'gradient',
1085
+ gradient: { shade: 'dark', type: 'horizontal', gradientToColors: [c.primary, c.secondary], stops: [0, 50, 100] },
1086
+ },
1087
+ plotOptions: {
1088
+ radialBar: {
1089
+ startAngle: -135, endAngle: 135, hollow: { size: '64%' },
1090
+ track: { background: c.muted, strokeWidth: '100%' },
1091
+ dataLabels: { show: false },
1092
+ },
1093
+ },
1094
+ stroke: { lineCap: 'round' },
1095
+ });
1096
+ ring.render();
1097
+ charts.push(ring);
1098
+ animateNum(document.getElementById('ring-pct'), pct, { duration: 1.6, decimals: 1, suffix: '%' });
1099
+ }
1100
+
1101
+ function buildTreemap() {
1102
+ const c = brandColors();
1103
+ const data = DATA.tools
1104
+ .filter((tool) => tool.calls > 0)
1105
+ .map((tool) => ({ x: tool.name, y: tool.calls, savingsPct: tool.savingsPct, saved: tool.savedChars }));
1106
+ const el = document.getElementById('tools-treemap');
1107
+ el.innerHTML = '';
1108
+ if (!data.length) { el.innerHTML = '<div class="empty-list">No measured tool response savings recorded yet.</div>'; return; }
1109
+ const treemap = new ApexCharts(el, {
1110
+ chart: { type: 'treemap', height: 340, toolbar: { show: false }, fontFamily: 'Inter, sans-serif', background: 'transparent' },
1111
+ series: [{ data }],
1112
+ legend: { show: false },
1113
+ plotOptions: {
1114
+ treemap: {
1115
+ enableShades: false, distributed: true,
1116
+ colorScale: {
1117
+ ranges: [
1118
+ { from: 0, to: 19, color: c.info },
1119
+ { from: 20, to: 39, color: c.primary },
1120
+ { from: 40, to: 59, color: c.secondary },
1121
+ { from: 60, to: 79, color: c.accent },
1122
+ { from: 80, to: 100, color: c.accent },
1123
+ ],
1124
+ },
1125
+ },
1126
+ },
1127
+ dataLabels: {
1128
+ enabled: true,
1129
+ style: { fontSize: '13px', fontWeight: 700, fontFamily: 'Outfit, sans-serif', colors: ['#0a0e1a'] },
1130
+ formatter: (text, op) => [text, `${op.value} measured`],
1131
+ offsetY: -4,
1132
+ },
1133
+ tooltip: {
1134
+ theme: root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light',
1135
+ custom: ({ seriesIndex, dataPointIndex, w }) => {
1136
+ const d = w.config.series[seriesIndex].data[dataPointIndex];
1137
+ return `<div style="padding:10px 12px; font-family:Inter;">
1138
+ <div style="font-weight:600; margin-bottom:4px;">${d.x}</div>
1139
+ <div style="font-size:12px; opacity:0.8;">${d.y} measured responses · ${d.savingsPct.toFixed(1)}% saved · ${fmtBytes(d.saved)} chars</div>
1140
+ </div>`;
1141
+ },
1142
+ },
1143
+ });
1144
+ treemap.render();
1145
+ charts.push(treemap);
1146
+ }
1147
+
1148
+ function buildCacheBar() {
1149
+ const c = brandColors();
1150
+ const top = DATA.githubCacheHits.slice(0, 6);
1151
+ const el = document.getElementById('cache-bar');
1152
+ el.innerHTML = '';
1153
+ if (!top.length) { el.innerHTML = '<div class="empty-list">No GitHub cache activity yet.</div>'; return; }
1154
+ const bar = new ApexCharts(el, {
1155
+ chart: { type: 'bar', height: Math.max(160, top.length * 28), toolbar: { show: false }, fontFamily: 'Inter, sans-serif', background: 'transparent' },
1156
+ series: [{ name: 'Hits', data: top.map((x) => x.count) }],
1157
+ xaxis: {
1158
+ categories: top.map((x) => x.endpoint),
1159
+ labels: { style: { colors: c.dim, fontSize: '11px' } },
1160
+ axisBorder: { show: false }, axisTicks: { show: false },
1161
+ },
1162
+ yaxis: { labels: { style: { colors: c.dim, fontSize: '11px' } } },
1163
+ grid: { borderColor: c.border, strokeDashArray: 3 },
1164
+ plotOptions: { bar: { horizontal: true, barHeight: '56%', borderRadius: 7, borderRadiusApplication: 'end' } },
1165
+ colors: [c.primary],
1166
+ fill: {
1167
+ type: 'gradient',
1168
+ gradient: { shade: 'dark', type: 'horizontal', gradientToColors: [c.accent], stops: [0, 100] },
1169
+ },
1170
+ dataLabels: {
1171
+ enabled: true,
1172
+ style: { colors: ['#0a0e1a'], fontWeight: 700, fontSize: '11px', fontFamily: 'JetBrains Mono, monospace' },
1173
+ formatter: (v) => fmt(v),
1174
+ },
1175
+ tooltip: { theme: root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light' },
1176
+ });
1177
+ bar.render();
1178
+ charts.push(bar);
1179
+ }
1180
+
1181
+ function rebuildCharts() {
1182
+ destroyCharts();
1183
+ buildRingChart();
1184
+ buildTreemap();
1185
+ buildCacheBar();
1186
+ }
1187
+ rebuildCharts();
1188
+
1189
+ // ---------- tools table ----------
1190
+ const toolsTbody = document.querySelector('#tools-table tbody');
1191
+ const remoteByName = new Map((DATA.remoteTools || []).map((tool) => [tool.name, tool]));
1192
+ const mergedTools = DATA.tools.map((tool) => {
1193
+ const remote = remoteByName.get(tool.name);
1194
+ return {
1195
+ ...tool,
1196
+ source: remote?.source || '',
1197
+ outcome: remote?.outcome || '',
1198
+ deltaChars: remote?.deltaChars ?? null,
1199
+ sentPctOfRaw: remote?.sentPctOfRaw ?? null,
1200
+ };
1201
+ });
1202
+ const renderToolsTable = (rows) => {
1203
+ toolsTbody.innerHTML = rows.length
1204
+ ? rows.map((tool) => {
1205
+ const chipClass = tool.outcome === 'expanded' ? 'warn' : tool.outcome === 'reduced' ? 'accent' : tool.outcome === 'same' ? 'info' : '';
1206
+ const sourceCell = tool.source
1207
+ ? `<span class="chip ${chipClass}"><span class="chip-label">${tool.source}</span></span>`
1208
+ : '<span class="muted-cell">—</span>';
1209
+ const deltaLabel = tool.deltaChars === null
1210
+ ? '—'
1211
+ : tool.deltaChars >= 0
1212
+ ? `-${fmtBytes(tool.deltaChars)}`
1213
+ : `+${fmtBytes(Math.abs(tool.deltaChars))}`;
1214
+ return `
1215
+ <tr>
1216
+ <td class="tool-cell"><span class="chip primary"><i data-lucide="wrench"></i><span class="chip-label">${tool.name}</span></span></td>
1217
+ <td>${sourceCell}</td>
1218
+ <td class="num strong">${fmt(tool.calls)}</td>
1219
+ <td class="num">${fmtBytes(tool.rawChars)}</td>
1220
+ <td class="num">${fmtBytes(tool.responseChars)}</td>
1221
+ <td class="num">${fmtBytes(tool.savedChars)}</td>
1222
+ <td class="num strong">${deltaLabel}</td>
1223
+ <td class="num">${fmtPct(tool.savingsPct)}</td>
1224
+ <td class="num">${tool.sentPctOfRaw === null ? '—' : fmtPct(tool.sentPctOfRaw)}</td>
1225
+ <td class="num strong">${fmt(tool.estimatedTokensSaved)}</td>
1226
+ </tr>`;
1227
+ }).join('')
1228
+ : '<tr><td colspan="10" class="empty-list" style="border: none;">No measured tools match your filter.</td></tr>';
1229
+ renderIcons();
1230
+ };
1231
+ let toolsSort = { key: 'calls', dir: 'desc' };
1232
+ const applyToolsTable = () => {
1233
+ const q = (document.getElementById('tools-search').value || '').toLowerCase();
1234
+ const rows = mergedTools
1235
+ .filter((tool) => !q || `${tool.name} ${tool.source}`.toLowerCase().includes(q))
1236
+ .sort((a, b) => {
1237
+ const av = a[toolsSort.key], bv = b[toolsSort.key];
1238
+ const cmp = typeof av === 'string'
1239
+ ? av.localeCompare(bv)
1240
+ : ((av ?? -1) - (bv ?? -1));
1241
+ return toolsSort.dir === 'asc' ? cmp : -cmp;
1242
+ });
1243
+ renderToolsTable(rows);
1244
+ document.querySelectorAll('#tools-table th').forEach((th) => {
1245
+ const isSorted = th.getAttribute('data-key') === toolsSort.key;
1246
+ th.classList.toggle('sorted', isSorted);
1247
+ const ind = th.querySelector('.sort-ind');
1248
+ if (ind) ind.textContent = isSorted ? (toolsSort.dir === 'asc' ? '↑' : '↓') : '↕';
1249
+ });
1250
+ };
1251
+ document.querySelectorAll('#tools-table th').forEach((th) => {
1252
+ th.addEventListener('click', () => {
1253
+ const key = th.getAttribute('data-key');
1254
+ if (toolsSort.key === key) { toolsSort.dir = toolsSort.dir === 'asc' ? 'desc' : 'asc'; }
1255
+ else { toolsSort = { key, dir: key === 'name' || key === 'source' ? 'asc' : 'desc' }; }
1256
+ applyToolsTable();
1257
+ });
1258
+ });
1259
+ document.getElementById('tools-search').addEventListener('input', applyToolsTable);
1260
+ document.getElementById('tools-count-badge').textContent = `${DATA.tools.length} tools · ${remoteByName.size} remote`;
1261
+ applyToolsTable();
1262
+
1263
+ // ---------- cache ----------
1264
+ document.getElementById('cache-count-badge').textContent = `${DATA.githubCacheHits.length} endpoints`;
1265
+ document.getElementById('cache-callout-title').textContent =
1266
+ t.githubCacheRateLimits > 0
1267
+ ? `${fmt(t.githubCacheRateLimits)} rate limits avoided by cache`
1268
+ : `${fmt(t.githubCacheHits)} requests served from cache`;
1269
+ document.getElementById('cache-callout-sub').textContent =
1270
+ t.githubCacheRateLimits > 0
1271
+ ? `Each cached hit dodged a GitHub API request. Your token kept its budget.`
1272
+ : `Repeated reads served locally — no GitHub API quota consumed.`;
1273
+ const cacheTbody = document.querySelector('#cache-table tbody');
1274
+ cacheTbody.innerHTML = DATA.githubCacheHits.length
1275
+ ? DATA.githubCacheHits.slice(0, 8).map((c) => `
1276
+ <tr>
1277
+ <td class="endpoint-cell"><span class="mono" style="font-size: 11px;">${c.endpoint}</span></td>
1278
+ <td class="num strong">${fmt(c.count)}</td>
1279
+ </tr>
1280
+ `).join('')
1281
+ : '<tr><td colspan="2" class="empty-list" style="border: none;">No cache hits recorded.</td></tr>';
1282
+
1283
+ // ---------- errors ----------
1284
+ const errEvents = t.errors + t.rateLimits + t.packageRegistryFailures;
1285
+ document.getElementById('errors-count-badge').textContent = `${fmt(errEvents)} events`;
1286
+ animateNum(document.getElementById('errors-total'), t.errors);
1287
+ animateNum(document.getElementById('rl-total'), t.rateLimits);
1288
+ animateNum(document.getElementById('pkg-fail-total'), t.packageRegistryFailures);
1289
+
1290
+
1291
+ renderIcons();
1292
+
1293
+ // ---------- copy path ----------
1294
+ const copyBtn = document.getElementById('copy-btn');
1295
+ copyBtn.addEventListener('click', async () => {
1296
+ try {
1297
+ await navigator.clipboard.writeText(DATA.statsPath);
1298
+ copyBtn.classList.add('copied');
1299
+ copyBtn.innerHTML = '<i data-lucide="check"></i>';
1300
+ renderIcons();
1301
+ setTimeout(() => {
1302
+ copyBtn.classList.remove('copied');
1303
+ copyBtn.innerHTML = '<i data-lucide="copy"></i>';
1304
+ renderIcons();
1305
+ }, 1400);
1306
+ } catch {}
1307
+ });
1308
+
1309
+ // ---------- active section nav ----------
1310
+ const sectionIds = ['overview', 'tools', 'cache', 'errors'];
1311
+ const navLinks = Array.from(document.querySelectorAll('#nav-links .nav-link'));
1312
+ if ('IntersectionObserver' in window) {
1313
+ const obs = new IntersectionObserver(
1314
+ (entries) => {
1315
+ entries.forEach((e) => {
1316
+ if (e.isIntersecting) {
1317
+ const id = e.target.id;
1318
+ navLinks.forEach((l) => l.classList.toggle('active', l.getAttribute('href') === `#${id}`));
1319
+ }
1320
+ });
1321
+ },
1322
+ { rootMargin: '-40% 0px -55% 0px' }
1323
+ );
1324
+ sectionIds.forEach((id) => {
1325
+ const el = document.getElementById(id);
1326
+ if (el) obs.observe(el);
1327
+ });
1328
+ }
1329
+ })();
1330
+ </script>
1331
+ </body>
1332
+ </html>