kodelyth-ecc 1.5.10 → 1.7.1

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 (273) hide show
  1. package/.github/workflows/publish.yml +19 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +475 -0
  4. package/CLAUDE.md +48 -16
  5. package/README.md +285 -81
  6. package/VERSION +1 -1
  7. package/actions/ecc-review/README.md +243 -0
  8. package/actions/ecc-review/action.yml +158 -0
  9. package/actions/ecc-review/post-comment.js +171 -0
  10. package/actions/ecc-review/run-review.js +285 -0
  11. package/agents/backdoor-hunter.md +260 -0
  12. package/agents/chaos-engineer.md +251 -0
  13. package/agents/code-stealer-detector.md +228 -0
  14. package/agents/jailbreak-tester.md +222 -0
  15. package/agents/license-violation-finder.md +212 -0
  16. package/agents/prompt-injection-hunter.md +126 -0
  17. package/agents/secret-hunter.md +209 -0
  18. package/agents/supply-chain-auditor.md +195 -0
  19. package/bin/kodelyth-ecc.js +875 -1
  20. package/bundles/enterprise.md +172 -0
  21. package/bundles/indie-hacker.md +106 -0
  22. package/bundles/red-team.md +138 -0
  23. package/cat +0 -0
  24. package/commands/dashboard.md +67 -0
  25. package/commands/devil-mode.md +121 -0
  26. package/commands/memory-evolve.md +71 -0
  27. package/commands/replay.md +61 -0
  28. package/commands/route-model.md +48 -0
  29. package/commands/swarm.md +68 -0
  30. package/commands/verify-supply-chain.md +59 -0
  31. package/docs/dashboard.md +211 -0
  32. package/docs/evolve.md +303 -0
  33. package/docs/mcp-clients.md +167 -0
  34. package/docs/mcp.md +178 -0
  35. package/docs/replay.md +244 -0
  36. package/docs/supply-chain.md +207 -0
  37. package/docs/swarm.md +243 -0
  38. package/hooks/hooks.json +52 -0
  39. package/hooks/memory/auto-recall.js +29 -1
  40. package/hooks/safety/README.md +124 -0
  41. package/hooks/safety/lib/patterns.js +179 -0
  42. package/hooks/safety/prompt-injection-guard.js +179 -0
  43. package/hooks/safety/token-budget.js +229 -0
  44. package/install.ps1 +199 -2
  45. package/install.sh +285 -3
  46. package/package.json +30 -4
  47. package/rules/common/agent-intent-routing.md +182 -0
  48. package/rules/common/cost-aware-model-routing.md +152 -0
  49. package/scripts/dashboard/data.js +353 -0
  50. package/scripts/dashboard/server.js +324 -0
  51. package/scripts/dashboard/static/index.html +645 -0
  52. package/scripts/evolve/analyze.js +303 -0
  53. package/scripts/evolve/proposals.js +162 -0
  54. package/scripts/evolve/stats.js +219 -0
  55. package/scripts/mcp/catalog.js +244 -0
  56. package/scripts/mcp/client.js +181 -0
  57. package/scripts/mcp/prompts.js +133 -0
  58. package/scripts/mcp/resources.js +94 -0
  59. package/scripts/mcp/server.js +153 -0
  60. package/scripts/mcp/tools.js +465 -0
  61. package/scripts/replay/bundle.js +191 -0
  62. package/scripts/replay/replay.js +107 -0
  63. package/scripts/router/classify.js +232 -0
  64. package/scripts/supply-chain/manifest.js +155 -0
  65. package/scripts/supply-chain/sbom.js +202 -0
  66. package/scripts/supply-chain/verify.js +102 -0
  67. package/scripts/swarm/build-plan.js +193 -0
  68. package/skills/cost-aware-model-routing/SKILL.md +153 -0
  69. package/skills/kodelyth-quickstart/SKILL.md +7 -0
  70. package/skills/observability-dashboard/SKILL.md +119 -0
  71. package/skills/self-evolving-memory/SKILL.md +175 -0
  72. package/skills/session-replay/SKILL.md +199 -0
  73. package/skills/supply-chain-verification/SKILL.md +201 -0
  74. package/skills/swarm-orchestrator/SKILL.md +177 -0
  75. package/social/card-install.svg +1 -1
  76. package/social/facebook-group/POST.md +121 -0
  77. package/social/facebook-group/fb-1-3am-debug.png +0 -0
  78. package/social/facebook-group/fb-1-3am-debug.svg +97 -0
  79. package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
  80. package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
  81. package/social/facebook-group/fb-3-before-after.png +0 -0
  82. package/social/facebook-group/fb-3-before-after.svg +94 -0
  83. package/social/facebook-v150.svg +6 -6
  84. package/social/github-social-preview.svg +119 -100
  85. package/social/readme-hero.svg +12 -12
  86. package/social/section-agents.svg +57 -0
  87. package/social/section-author.svg +54 -0
  88. package/social/section-dashboard.svg +59 -0
  89. package/social/section-devil.svg +54 -0
  90. package/social/section-hooks.svg +51 -0
  91. package/social/section-install.svg +42 -0
  92. package/social/section-learning.svg +52 -0
  93. package/social/section-mcp.svg +46 -0
  94. package/social/section-memory.svg +57 -0
  95. package/social/section-parallel.svg +72 -0
  96. package/social/section-routing.svg +51 -0
  97. package/social/x-card-agents-grid.svg +6 -6
  98. package/tests/dashboard/data.test.js +235 -0
  99. package/tests/dashboard/server.test.js +240 -0
  100. package/tests/evolve/analyze.test.js +169 -0
  101. package/tests/evolve/proposals.test.js +173 -0
  102. package/tests/evolve/stats.test.js +167 -0
  103. package/tests/mcp/catalog.test.js +98 -0
  104. package/tests/mcp/client.test.js +113 -0
  105. package/tests/mcp/resources-prompts.test.js +70 -0
  106. package/tests/mcp/tools.test.js +159 -0
  107. package/tests/replay/bundle.test.js +181 -0
  108. package/tests/replay/replay.test.js +131 -0
  109. package/tests/router/classify.test.js +164 -0
  110. package/tests/safety/patterns.test.js +94 -0
  111. package/tests/safety/prompt-injection-guard.test.js +111 -0
  112. package/tests/safety/token-budget.test.js +119 -0
  113. package/tests/supply-chain/manifest.test.js +147 -0
  114. package/tests/supply-chain/sbom.test.js +170 -0
  115. package/tests/supply-chain/verify.test.js +146 -0
  116. package/tests/swarm/build-plan.test.js +188 -0
  117. package/wiki/Agent-Reference.md +58 -7
  118. package/wiki/FAQ.md +204 -7
  119. package/wiki/Home.md +104 -29
  120. package/wiki/Hook-Reference.md +1 -1
  121. package/wiki/Installation-Guide.md +109 -6
  122. package/wiki/Platform-Support.md +189 -72
  123. package/wiki/Skill-Reference.md +101 -6
  124. package/.agent/rules/README.md +0 -108
  125. package/.agent/rules/agents.md +0 -94
  126. package/.agent/rules/code-review.md +0 -124
  127. package/.agent/rules/coding-style.md +0 -122
  128. package/.agent/rules/design-quality.md +0 -63
  129. package/.agent/rules/development-workflow.md +0 -44
  130. package/.agent/rules/git-mastery.md +0 -366
  131. package/.agent/rules/git-workflow.md +0 -24
  132. package/.agent/rules/hooks.md +0 -25
  133. package/.agent/rules/kodelyth-always-on.md +0 -63
  134. package/.agent/rules/kodelyth-quickstart.md +0 -207
  135. package/.agent/rules/observability.md +0 -390
  136. package/.agent/rules/patterns.md +0 -50
  137. package/.agent/rules/performance.md +0 -178
  138. package/.agent/rules/security.md +0 -58
  139. package/.agent/rules/smart-debug.md +0 -241
  140. package/.agent/rules/soul.md +0 -32
  141. package/.agent/rules/testing.md +0 -46
  142. package/.agent/skills/api-guardian.md +0 -279
  143. package/.agent/skills/architect.md +0 -211
  144. package/.agent/skills/build-error-resolver.md +0 -114
  145. package/.agent/skills/chief-of-staff.md +0 -151
  146. package/.agent/skills/code-architect.md +0 -71
  147. package/.agent/skills/code-explorer.md +0 -69
  148. package/.agent/skills/code-reviewer.md +0 -237
  149. package/.agent/skills/code-simplifier.md +0 -47
  150. package/.agent/skills/comment-analyzer.md +0 -45
  151. package/.agent/skills/conversation-analyzer.md +0 -52
  152. package/.agent/skills/cpp-build-resolver.md +0 -90
  153. package/.agent/skills/cpp-reviewer.md +0 -72
  154. package/.agent/skills/csharp-reviewer.md +0 -101
  155. package/.agent/skills/dart-build-resolver.md +0 -201
  156. package/.agent/skills/database-reviewer.md +0 -91
  157. package/.agent/skills/debug-detective.md +0 -409
  158. package/.agent/skills/doc-updater.md +0 -107
  159. package/.agent/skills/docs-lookup.md +0 -68
  160. package/.agent/skills/e2e-runner.md +0 -107
  161. package/.agent/skills/flutter-reviewer.md +0 -243
  162. package/.agent/skills/gan-evaluator.md +0 -209
  163. package/.agent/skills/gan-generator.md +0 -131
  164. package/.agent/skills/gan-planner.md +0 -99
  165. package/.agent/skills/go-build-resolver.md +0 -94
  166. package/.agent/skills/go-reviewer.md +0 -76
  167. package/.agent/skills/harness-optimizer.md +0 -35
  168. package/.agent/skills/healthcare-reviewer.md +0 -83
  169. package/.agent/skills/java-build-resolver.md +0 -153
  170. package/.agent/skills/java-reviewer.md +0 -92
  171. package/.agent/skills/kodelyth-advisor.md +0 -172
  172. package/.agent/skills/kotlin-build-resolver.md +0 -118
  173. package/.agent/skills/kotlin-reviewer.md +0 -159
  174. package/.agent/skills/loop-operator.md +0 -36
  175. package/.agent/skills/migration-guide.md +0 -368
  176. package/.agent/skills/opensource-forker.md +0 -198
  177. package/.agent/skills/opensource-packager.md +0 -249
  178. package/.agent/skills/opensource-sanitizer.md +0 -188
  179. package/.agent/skills/pair-programmer.md +0 -260
  180. package/.agent/skills/performance-optimizer.md +0 -446
  181. package/.agent/skills/planner.md +0 -212
  182. package/.agent/skills/pr-test-analyzer.md +0 -45
  183. package/.agent/skills/python-reviewer.md +0 -98
  184. package/.agent/skills/pytorch-build-resolver.md +0 -120
  185. package/.agent/skills/refactor-cleaner.md +0 -85
  186. package/.agent/skills/rust-build-resolver.md +0 -148
  187. package/.agent/skills/rust-reviewer.md +0 -94
  188. package/.agent/skills/security-reviewer.md +0 -108
  189. package/.agent/skills/seo-specialist.md +0 -62
  190. package/.agent/skills/silent-failure-hunter.md +0 -50
  191. package/.agent/skills/tdd-guide.md +0 -91
  192. package/.agent/skills/type-design-analyzer.md +0 -41
  193. package/.agent/skills/typescript-reviewer.md +0 -112
  194. package/.agent/skills/ux-reviewer.md +0 -483
  195. package/.agent/workflows/agent-sort.md +0 -23
  196. package/.agent/workflows/aside.md +0 -164
  197. package/.agent/workflows/build-fix.md +0 -62
  198. package/.agent/workflows/checkpoint.md +0 -74
  199. package/.agent/workflows/claw.md +0 -23
  200. package/.agent/workflows/code-review.md +0 -289
  201. package/.agent/workflows/context-budget.md +0 -23
  202. package/.agent/workflows/cpp-build.md +0 -173
  203. package/.agent/workflows/cpp-review.md +0 -132
  204. package/.agent/workflows/cpp-test.md +0 -251
  205. package/.agent/workflows/devfleet.md +0 -23
  206. package/.agent/workflows/docs.md +0 -23
  207. package/.agent/workflows/e2e.md +0 -268
  208. package/.agent/workflows/eval.md +0 -23
  209. package/.agent/workflows/evolve.md +0 -178
  210. package/.agent/workflows/feature-dev.md +0 -49
  211. package/.agent/workflows/flutter-build.md +0 -164
  212. package/.agent/workflows/flutter-review.md +0 -116
  213. package/.agent/workflows/flutter-test.md +0 -144
  214. package/.agent/workflows/gan-build.md +0 -99
  215. package/.agent/workflows/gan-design.md +0 -35
  216. package/.agent/workflows/go-build.md +0 -183
  217. package/.agent/workflows/go-review.md +0 -148
  218. package/.agent/workflows/go-test.md +0 -268
  219. package/.agent/workflows/gradle-build.md +0 -70
  220. package/.agent/workflows/harness-audit.md +0 -73
  221. package/.agent/workflows/hookify-configure.md +0 -14
  222. package/.agent/workflows/hookify-help.md +0 -46
  223. package/.agent/workflows/hookify-list.md +0 -21
  224. package/.agent/workflows/hookify.md +0 -50
  225. package/.agent/workflows/instinct-export.md +0 -66
  226. package/.agent/workflows/instinct-import.md +0 -114
  227. package/.agent/workflows/instinct-status.md +0 -59
  228. package/.agent/workflows/jira.md +0 -106
  229. package/.agent/workflows/kotlin-build.md +0 -174
  230. package/.agent/workflows/kotlin-review.md +0 -140
  231. package/.agent/workflows/kotlin-test.md +0 -312
  232. package/.agent/workflows/learn-eval.md +0 -116
  233. package/.agent/workflows/learn.md +0 -70
  234. package/.agent/workflows/loop-start.md +0 -32
  235. package/.agent/workflows/loop-status.md +0 -24
  236. package/.agent/workflows/model-route.md +0 -26
  237. package/.agent/workflows/multi-backend.md +0 -158
  238. package/.agent/workflows/multi-execute.md +0 -315
  239. package/.agent/workflows/multi-frontend.md +0 -158
  240. package/.agent/workflows/multi-plan.md +0 -268
  241. package/.agent/workflows/multi-workflow.md +0 -191
  242. package/.agent/workflows/orchestrate.md +0 -135
  243. package/.agent/workflows/plan.md +0 -117
  244. package/.agent/workflows/pm2.md +0 -272
  245. package/.agent/workflows/projects.md +0 -39
  246. package/.agent/workflows/promote.md +0 -41
  247. package/.agent/workflows/prompt-optimize.md +0 -23
  248. package/.agent/workflows/prp-commit.md +0 -112
  249. package/.agent/workflows/prp-implement.md +0 -385
  250. package/.agent/workflows/prp-plan.md +0 -502
  251. package/.agent/workflows/prp-pr.md +0 -184
  252. package/.agent/workflows/prp-prd.md +0 -447
  253. package/.agent/workflows/prune.md +0 -31
  254. package/.agent/workflows/python-review.md +0 -297
  255. package/.agent/workflows/quality-gate.md +0 -29
  256. package/.agent/workflows/refactor-clean.md +0 -80
  257. package/.agent/workflows/resume-session.md +0 -156
  258. package/.agent/workflows/review-pr.md +0 -37
  259. package/.agent/workflows/rules-distill.md +0 -20
  260. package/.agent/workflows/rust-build.md +0 -187
  261. package/.agent/workflows/rust-review.md +0 -142
  262. package/.agent/workflows/rust-test.md +0 -308
  263. package/.agent/workflows/santa-loop.md +0 -175
  264. package/.agent/workflows/save-session.md +0 -275
  265. package/.agent/workflows/sessions.md +0 -333
  266. package/.agent/workflows/setup-pm.md +0 -80
  267. package/.agent/workflows/skill-create.md +0 -174
  268. package/.agent/workflows/skill-health.md +0 -54
  269. package/.agent/workflows/tdd.md +0 -231
  270. package/.agent/workflows/test-coverage.md +0 -69
  271. package/.agent/workflows/update-codemaps.md +0 -72
  272. package/.agent/workflows/update-docs.md +0 -84
  273. package/.agent/workflows/verify.md +0 -23
@@ -0,0 +1,645 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Kodelyth ECC — Dashboard</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='%23111a2e'/><stop offset='1' stop-color='%230b1220'/></linearGradient></defs><rect width='32' height='32' rx='7' fill='url(%23g)' stroke='%231f2a44' stroke-width='1'/><path d='M10.5 7.5 L10.5 24.5 M10.5 16 L21 7.5 M14 13.5 L21.5 24.5' stroke='%23e5e7eb' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' fill='none'/><circle cx='25' cy='7' r='3' fill='%2334d399'/></svg>">
8
+ <style>
9
+ /* ───── reset + base ───── */
10
+ *, *::before, *::after { box-sizing: border-box; }
11
+ html, body { margin: 0; padding: 0; height: 100%; }
12
+ body {
13
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
14
+ color: #e5e7eb;
15
+ background: radial-gradient(1200px 600px at 0% 0%, #1e293b 0%, #0b1220 70%, #060912 100%) fixed;
16
+ min-height: 100vh;
17
+ }
18
+ a { color: #93c5fd; text-decoration: none; }
19
+ a:hover { text-decoration: underline; }
20
+ code, pre, kbd, samp {
21
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
22
+ font-size: 12.5px;
23
+ }
24
+ pre {
25
+ margin: 0;
26
+ padding: 12px 14px;
27
+ background: #0a0f1a;
28
+ border: 1px solid #1f2a44;
29
+ border-radius: 8px;
30
+ overflow: auto;
31
+ color: #cbd5e1;
32
+ }
33
+
34
+ /* ───── layout ───── */
35
+ .app { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
36
+ header.top {
37
+ padding: 18px 28px;
38
+ border-bottom: 1px solid #1f2a44;
39
+ background: rgba(10,15,26,0.7);
40
+ backdrop-filter: blur(10px);
41
+ display: flex; align-items: center; justify-content: space-between; gap: 16px;
42
+ position: sticky; top: 0; z-index: 50;
43
+ flex-wrap: wrap;
44
+ }
45
+ .brand { display: flex; align-items: center; gap: 10px; }
46
+ .brand .dot {
47
+ width: 10px; height: 10px; border-radius: 999px;
48
+ background: #34d399; box-shadow: 0 0 12px #34d399;
49
+ transition: background 300ms, box-shadow 300ms;
50
+ }
51
+ .brand h1 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: 0.2px; }
52
+ .brand .sub { font-size: 12px; color: #94a3b8; }
53
+
54
+ .header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
55
+
56
+ nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
57
+ nav.tabs button {
58
+ background: transparent; color: #94a3b8;
59
+ border: 1px solid #1f2a44; border-radius: 8px;
60
+ padding: 8px 14px; font: inherit; cursor: pointer;
61
+ transition: 120ms;
62
+ }
63
+ nav.tabs button:hover { color: #e5e7eb; border-color: #334155; }
64
+ nav.tabs button.active {
65
+ color: #e5e7eb; background: #111a2e; border-color: #3b82f6;
66
+ }
67
+
68
+ .btn-ghost {
69
+ background: transparent; color: #94a3b8;
70
+ border: 1px solid #1f2a44; border-radius: 8px;
71
+ padding: 6px 12px; font: inherit; cursor: pointer; font-size: 12px;
72
+ transition: 120ms;
73
+ }
74
+ .btn-ghost:hover { color: #e5e7eb; border-color: #334155; }
75
+
76
+ main {
77
+ padding: 24px 28px 80px;
78
+ max-width: 1500px; width: 100%; margin: 0 auto;
79
+ }
80
+
81
+ /* ───── cards ───── */
82
+ .grid {
83
+ display: grid; gap: 16px;
84
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
85
+ }
86
+ .card {
87
+ background: rgba(15,23,42,0.7);
88
+ border: 1px solid #1f2a44;
89
+ border-radius: 12px;
90
+ padding: 18px 20px;
91
+ }
92
+ .card h2 {
93
+ margin: 0 0 14px; font-size: 13px; font-weight: 600;
94
+ color: #94a3b8; text-transform: uppercase; letter-spacing: 0.06em;
95
+ }
96
+ .stat .num {
97
+ font-size: 32px; font-weight: 700; color: #e5e7eb; line-height: 1;
98
+ }
99
+ .stat .label { color: #94a3b8; margin-top: 4px; font-size: 13px; }
100
+ .stat .micro { color: #64748b; margin-top: 6px; font-size: 11.5px; }
101
+
102
+ /* ───── tables ───── */
103
+ .table-wrap { overflow: auto; border-radius: 10px; border: 1px solid #1f2a44; }
104
+ table { width: 100%; border-collapse: collapse; }
105
+ th, td {
106
+ padding: 10px 12px; text-align: left; vertical-align: top;
107
+ border-bottom: 1px solid #1a2238; font-size: 13px;
108
+ }
109
+ th {
110
+ color: #94a3b8; background: #0a1322;
111
+ font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
112
+ }
113
+ tbody tr:hover { background: rgba(59,130,246,0.05); }
114
+ tbody tr:last-child td { border-bottom: 0; }
115
+
116
+ .pill {
117
+ display: inline-block; padding: 2px 8px; border-radius: 999px;
118
+ font-size: 11px; background: #1e293b; color: #cbd5e1;
119
+ border: 1px solid #334155;
120
+ }
121
+ .pill.ok { background: rgba(34,197,94,0.12); color: #86efac; border-color: rgba(34,197,94,0.4); }
122
+ .pill.pending { background: rgba(234,179,8,0.12); color: #fde68a; border-color: rgba(234,179,8,0.4); }
123
+ .pill.rejected { background: rgba(239,68,68,0.12); color: #fca5a5; border-color: rgba(239,68,68,0.4); }
124
+ .pill.applied { background: rgba(168,85,247,0.12); color: #d8b4fe; border-color: rgba(168,85,247,0.4); }
125
+
126
+ /* ───── search ───── */
127
+ .search {
128
+ display: flex; gap: 8px; margin: 0 0 16px;
129
+ }
130
+ .search input, .search select {
131
+ flex: 1; min-width: 0;
132
+ padding: 10px 14px;
133
+ background: #0a1322; color: #e5e7eb;
134
+ border: 1px solid #1f2a44; border-radius: 8px;
135
+ font: inherit; outline: none;
136
+ transition: border-color 120ms, box-shadow 120ms;
137
+ }
138
+ .search input:focus, .search select:focus {
139
+ border-color: #3b82f6;
140
+ box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
141
+ }
142
+
143
+ /* Custom select: kill native chevron, paint our own */
144
+ .search select {
145
+ appearance: none;
146
+ -webkit-appearance: none;
147
+ -moz-appearance: none;
148
+ flex: 0 0 180px;
149
+ padding-right: 38px;
150
+ cursor: pointer;
151
+ background-image:
152
+ url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
153
+ background-repeat: no-repeat;
154
+ background-position: right 14px center;
155
+ background-size: 14px 14px;
156
+ }
157
+ .search select:hover { border-color: #334155; }
158
+ .search select::-ms-expand { display: none; }
159
+ .search select option {
160
+ background: #0a1322;
161
+ color: #e5e7eb;
162
+ padding: 8px 12px;
163
+ }
164
+ .search select option:checked,
165
+ .search select option:hover {
166
+ background: #1e3a8a;
167
+ color: #ffffff;
168
+ }
169
+ .search button {
170
+ padding: 10px 16px;
171
+ background: #3b82f6; color: white; border: 0; border-radius: 8px;
172
+ cursor: pointer; font: inherit; font-weight: 600;
173
+ }
174
+ .search button:hover { background: #2563eb; }
175
+
176
+ /* ───── copy button ───── */
177
+ .copy-btn {
178
+ float: right; margin-left: 8px; font-size: 11px; padding: 2px 8px;
179
+ background: #1e293b; color: #93c5fd; border: 1px solid #334155;
180
+ border-radius: 6px; cursor: pointer; font-family: inherit; line-height: 1.4;
181
+ vertical-align: middle;
182
+ }
183
+ .copy-btn:hover { background: #1e3a5f; }
184
+
185
+ /* ───── small stuff ───── */
186
+ .row { display: flex; gap: 12px; flex-wrap: wrap; }
187
+ .row > * { flex: 1 1 220px; }
188
+ .muted { color: #94a3b8; }
189
+ .dim { color: #64748b; }
190
+ .strong { color: #e5e7eb; font-weight: 600; }
191
+
192
+ details { border: 1px solid #1f2a44; border-radius: 8px; padding: 8px 12px; background: #0a1322; }
193
+ details + details { margin-top: 8px; }
194
+ details summary { cursor: pointer; padding: 4px 0; }
195
+ details[open] summary { margin-bottom: 8px; color: #93c5fd; }
196
+
197
+ .empty {
198
+ padding: 32px; text-align: center; color: #64748b; border: 1px dashed #1f2a44; border-radius: 12px;
199
+ }
200
+
201
+ footer.bot {
202
+ padding: 16px 28px; border-top: 1px solid #1f2a44;
203
+ color: #64748b; font-size: 12px;
204
+ display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
205
+ }
206
+
207
+ hr.sep { border: 0; border-top: 1px solid #1f2a44; margin: 24px 0; }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <div class="app">
212
+ <header class="top">
213
+ <div class="brand">
214
+ <span class="dot" id="liveDot"></span>
215
+ <h1>Kodelyth ECC</h1>
216
+ <span class="sub" id="hostLabel">— dashboard v1.7.0 · localhost</span>
217
+ </div>
218
+ <div class="header-right">
219
+ <span id="connBadge" class="pill ok" title="Real-time SSE connection status">LIVE</span>
220
+ <button id="refreshBtn" class="btn-ghost" title="Refresh active tab (R key)">Refresh</button>
221
+ <nav class="tabs">
222
+ <button data-tab="overview" class="active">Overview</button>
223
+ <button data-tab="memory">Memory</button>
224
+ <button data-tab="evolve">Evolve</button>
225
+ <button data-tab="catalog">Catalog</button>
226
+ <button data-tab="sessions">Sessions</button>
227
+ </nav>
228
+ </div>
229
+ </header>
230
+
231
+ <main>
232
+ <!-- ───────── OVERVIEW ───────── -->
233
+ <section data-panel="overview">
234
+ <div class="grid" id="overviewCards"></div>
235
+ <hr class="sep">
236
+ <div class="row">
237
+ <div class="card">
238
+ <h2>Storage</h2>
239
+ <pre id="storagePre">loading…</pre>
240
+ </div>
241
+ <div class="card">
242
+ <h2>Token budget snapshot</h2>
243
+ <div id="budgetSnapshot">loading…</div>
244
+ </div>
245
+ </div>
246
+ </section>
247
+
248
+ <!-- ───────── MEMORY ───────── -->
249
+ <section data-panel="memory" hidden>
250
+ <div class="grid" id="memoryCards"></div>
251
+ <hr class="sep">
252
+ <div class="card">
253
+ <h2>BM25 search</h2>
254
+ <div class="search">
255
+ <input id="memQuery" placeholder='try "tailwind v4 migration"'>
256
+ <button id="memSearchBtn">Search</button>
257
+ </div>
258
+ <div id="memSearchResults" class="muted">Type a query and hit search.</div>
259
+ </div>
260
+ <hr class="sep">
261
+ <div class="card">
262
+ <h2>Recent memories</h2>
263
+ <div id="recentMemoriesTable">loading…</div>
264
+ </div>
265
+ </section>
266
+
267
+ <!-- ───────── EVOLVE ───────── -->
268
+ <section data-panel="evolve" hidden>
269
+ <div class="grid" id="evolveCards"></div>
270
+ <hr class="sep">
271
+ <div class="row">
272
+ <div class="card">
273
+ <h2>Top reused memories</h2>
274
+ <div id="reuseList">loading…</div>
275
+ </div>
276
+ <div class="card">
277
+ <h2>Top routing-miss clusters</h2>
278
+ <div id="missList">loading…</div>
279
+ </div>
280
+ </div>
281
+ <hr class="sep">
282
+ <div class="card">
283
+ <h2>Proposals</h2>
284
+ <div id="proposalsTable">loading…</div>
285
+ <p class="muted" style="margin-top: 12px; font-size: 12.5px;">
286
+ Run <code>kodelyth-ecc evolve accept &lt;id&gt;</code> in your terminal to write a draft to disk.
287
+ The dashboard NEVER auto-applies anything.
288
+ </p>
289
+ </div>
290
+ </section>
291
+
292
+ <!-- ───────── CATALOG ───────── -->
293
+ <section data-panel="catalog" hidden>
294
+ <div class="card">
295
+ <h2>Catalog browser</h2>
296
+ <div class="search">
297
+ <select id="catKind">
298
+ <option value="skills">skills</option>
299
+ <option value="agents">agents</option>
300
+ <option value="commands">commands</option>
301
+ <option value="rules">rules</option>
302
+ <option value="bundles">bundles</option>
303
+ </select>
304
+ <input id="catQuery" placeholder="filter by name, description, tag">
305
+ <button id="catRefreshBtn">Refresh</button>
306
+ </div>
307
+ <div id="catalogResults">loading…</div>
308
+ </div>
309
+ </section>
310
+
311
+ <!-- ───────── SESSIONS ───────── -->
312
+ <section data-panel="sessions" hidden>
313
+ <div class="card">
314
+ <h2>Orchestration / swarm sessions</h2>
315
+ <div id="sessionsList">loading…</div>
316
+ </div>
317
+ </section>
318
+ </main>
319
+
320
+ <footer class="bot">
321
+ <div>
322
+ Localhost only · zero telemetry · all data stays on this machine.
323
+ &nbsp;|&nbsp; Keys <kbd>1</kbd>–<kbd>5</kbd> switch tabs · <kbd>R</kbd> to refresh.
324
+ &nbsp;|&nbsp; Built by <a href="https://github.com/sifxprime" target="_blank" rel="noopener">Shofiqul Islam</a>
325
+ &nbsp;·&nbsp; <a href="https://github.com/sifxprime/kodelyth-ecc" target="_blank" rel="noopener">GitHub</a>
326
+ &nbsp;·&nbsp; <a href="https://www.npmjs.com/package/kodelyth-ecc" target="_blank" rel="noopener">npm</a>
327
+ </div>
328
+ <div id="lastUpdated">last refreshed: —</div>
329
+ </footer>
330
+ </div>
331
+
332
+ <script>
333
+ // ───── tiny helpers ─────
334
+ const $ = sel => document.querySelector(sel);
335
+ const $$ = sel => Array.from(document.querySelectorAll(sel));
336
+ const fmtDate = s => s ? new Date(s).toLocaleString() : '—';
337
+ const escapeHtml = s => String(s == null ? '' : s)
338
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
339
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
340
+
341
+ async function api(path) {
342
+ const r = await fetch(path, { headers: { Accept: 'application/json' } });
343
+ if (!r.ok) throw new Error(`${path} → ${r.status}`);
344
+ return r.json();
345
+ }
346
+
347
+ function setLastUpdated() {
348
+ $('#lastUpdated').textContent = `last refreshed: ${new Date().toLocaleTimeString()}`;
349
+ }
350
+
351
+ // ───── tab state + switching ─────
352
+ let currentTab = 'overview';
353
+
354
+ function switchTab(name) {
355
+ $$('.tabs button').forEach(x => x.classList.toggle('active', x.dataset.tab === name));
356
+ $$('section[data-panel]').forEach(s => s.hidden = s.dataset.panel !== name);
357
+ currentTab = name;
358
+ onTabShown(name);
359
+ }
360
+
361
+ $$('.tabs button').forEach(b => b.addEventListener('click', () => switchTab(b.dataset.tab)));
362
+
363
+ // ───── overview ─────
364
+ function statCard(num, label, micro = '') {
365
+ return `<div class="card stat">
366
+ <div class="num">${num}</div>
367
+ <div class="label">${escapeHtml(label)}</div>
368
+ ${micro ? `<div class="micro">${escapeHtml(micro)}</div>` : ''}
369
+ </div>`;
370
+ }
371
+
372
+ async function loadOverview() {
373
+ try {
374
+ const o = await api('/api/overview');
375
+ $('#hostLabel').textContent = `— dashboard v1.7.0 · ${location.host} · ${o.package_root.split('/').slice(-2).join('/')}`;
376
+ $('#overviewCards').innerHTML = [
377
+ statCard(o.catalog.agents, 'Agents'),
378
+ statCard(o.catalog.skills, 'Skills'),
379
+ statCard(o.catalog.commands, 'Commands'),
380
+ statCard(o.catalog.rules, 'Rules'),
381
+ statCard(o.catalog.bundles, 'Bundles'),
382
+ statCard(o.memory.total, 'Memories captured', `${o.memory.projects} projects`),
383
+ statCard(o.evolve.surfaces_total, 'Memory surfaces', 'cumulative across sessions'),
384
+ statCard(o.evolve.routing_misses, 'Routing misses', 'substantive prompts w/ no recall'),
385
+ statCard(o.evolve.proposals.pending, 'Pending proposals', `${o.evolve.proposals.total} total tracked`),
386
+ statCard(o.swarm.sessions, 'Swarm sessions', 'in this repo'),
387
+ ].join('');
388
+ $('#storagePre').textContent = JSON.stringify(o.storage, null, 2);
389
+
390
+ const tb = await api('/api/token-budget');
391
+ if (!tb.sessions.length) {
392
+ $('#budgetSnapshot').innerHTML = `<div class="empty">No token-budget hook activity recorded yet.</div>`;
393
+ } else {
394
+ $('#budgetSnapshot').innerHTML = `
395
+ <div class="stat" style="display:flex;gap:12px;align-items:baseline;">
396
+ <span class="num">${tb.total_tokens.toLocaleString()}</span>
397
+ <span class="label">tokens across ${tb.sessions.length} sessions</span>
398
+ </div>
399
+ <div class="muted" style="margin-top:8px;font-size:12px;">
400
+ most-recent: ${escapeHtml(tb.sessions[0]?.session_id || '—')} · last seen ${fmtDate(tb.sessions[0]?.last_event)}
401
+ </div>`;
402
+ }
403
+ setLastUpdated();
404
+ } catch (e) {
405
+ $('#overviewCards').innerHTML = `<div class="empty">failed to load overview: ${escapeHtml(e.message)}</div>`;
406
+ }
407
+ }
408
+
409
+ // ───── memory ─────
410
+ async function loadMemory() {
411
+ try {
412
+ const m = await api('/api/memory');
413
+ $('#memoryCards').innerHTML = [
414
+ statCard(m.stats.total || 0, 'Total memories'),
415
+ statCard(m.stats.projects || 0, 'Projects'),
416
+ statCard(Object.keys(m.stats.byLanguage || {}).length, 'Languages'),
417
+ statCard((m.stats.topTags || []).length, 'Tag classes'),
418
+ ].join('');
419
+
420
+ if (!m.recent.length) {
421
+ $('#recentMemoriesTable').innerHTML = `<div class="empty">No captures yet. Use <code>/memory remember</code> or the MCP <code>capture_memory</code> tool.</div>`;
422
+ } else {
423
+ $('#recentMemoriesTable').innerHTML = `<div class="table-wrap"><table>
424
+ <thead><tr><th>Captured</th><th>Problem</th><th>Tags</th><th>Lang</th><th>Source</th></tr></thead>
425
+ <tbody>${m.recent.map(r => `<tr>
426
+ <td class="muted">${fmtDate(r.captured_at)}</td>
427
+ <td><div class="strong">${escapeHtml(r.problem)}</div><div class="dim" style="font-size:12px;margin-top:4px;">${escapeHtml(r.approach.slice(0,180))}${r.approach.length > 180 ? '…' : ''}</div></td>
428
+ <td>${(r.tags || []).map(t => `<span class="pill">${escapeHtml(t)}</span>`).join(' ')}</td>
429
+ <td>${escapeHtml(r.language || '—')}</td>
430
+ <td class="muted">${escapeHtml(r.source || '—')}</td>
431
+ </tr>`).join('')}</tbody></table></div>`;
432
+ }
433
+ setLastUpdated();
434
+ } catch (e) {
435
+ $('#memoryCards').innerHTML = `<div class="empty">failed to load memory: ${escapeHtml(e.message)}</div>`;
436
+ }
437
+ }
438
+
439
+ let _searchInFlight = false;
440
+ async function runMemorySearch() {
441
+ if (_searchInFlight) return; // prevent key-repeat burst
442
+ const q = $('#memQuery').value.trim();
443
+ if (!q) return;
444
+ _searchInFlight = true;
445
+ $('#memSearchResults').textContent = `searching for "${q}"…`;
446
+ try {
447
+ const r = await api('/api/memory/search?q=' + encodeURIComponent(q));
448
+ if (!r.results.length) {
449
+ $('#memSearchResults').innerHTML = `<div class="empty">No matches.</div>`;
450
+ return;
451
+ }
452
+ $('#memSearchResults').innerHTML = r.results.map(m => `
453
+ <details>
454
+ <summary>
455
+ <span class="pill">score ${m.score}</span> ${escapeHtml(m.problem)}
456
+ <button class="copy-btn" data-copy="${escapeHtml(JSON.stringify({ problem: m.problem, approach: m.approach, tags: m.tags }))}" onclick="event.stopPropagation();navigator.clipboard?.writeText(this.dataset.copy).then(()=>{this.textContent='copied';setTimeout(()=>this.textContent='copy',1500)})">copy</button>
457
+ </summary>
458
+ <pre>${escapeHtml(m.approach)}</pre>
459
+ <div class="muted" style="margin-top:6px;font-size:12px;">${(m.tags||[]).map(t=>`<span class="pill">${escapeHtml(t)}</span>`).join(' ')} · ${escapeHtml(m.language || '—')}</div>
460
+ </details>`).join('');
461
+ } catch (e) {
462
+ $('#memSearchResults').innerHTML = `<div class="empty">search failed: ${escapeHtml(e.message)}</div>`;
463
+ } finally {
464
+ _searchInFlight = false;
465
+ }
466
+ }
467
+
468
+ // ───── evolve ─────
469
+ async function loadEvolve() {
470
+ try {
471
+ const e = await api('/api/evolve');
472
+ $('#evolveCards').innerHTML = [
473
+ statCard(e.reuse.total_memories_tracked, 'Memories tracked'),
474
+ statCard(e.reuse.total_surfaces, 'Surfaces'),
475
+ statCard(e.miss.total_misses, 'Routing misses'),
476
+ statCard(e.miss.unique_prompts, 'Unique miss prompts'),
477
+ statCard(e.proposals.length, 'Proposals'),
478
+ ].join('');
479
+
480
+ $('#reuseList').innerHTML = e.reuse.entries.length
481
+ ? e.reuse.entries.slice(0, 10).map(r => `<div style="margin-bottom:10px;">
482
+ <div class="strong">${escapeHtml(r.id)}</div>
483
+ <div class="muted" style="font-size:12px;">${r.count} surfaces · ${(r.sessions||[]).length} sessions · last ${fmtDate(r.lastSurfaced)}</div>
484
+ </div>`).join('')
485
+ : `<div class="empty">No reuse signals yet.</div>`;
486
+
487
+ $('#missList').innerHTML = e.miss.entries.length
488
+ ? e.miss.entries.slice(0, 10).map(m => `<div style="margin-bottom:10px;">
489
+ <div class="strong">${(m.tokens || []).slice(0,4).map(t=>`<span class="pill">${escapeHtml(t)}</span>`).join(' ')}</div>
490
+ <div class="muted" style="font-size:12px;margin-top:4px;">${m.count} occurrence${m.count===1?'':'s'} · last ${fmtDate(m.lastSeen)}</div>
491
+ ${(m.samples || [])[0] ? `<div class="dim" style="font-size:12px;margin-top:4px;">"${escapeHtml(m.samples[0])}"</div>` : ''}
492
+ </div>`).join('')
493
+ : `<div class="empty">No routing-miss clusters yet.</div>`;
494
+
495
+ $('#proposalsTable').innerHTML = e.proposals.length
496
+ ? `<div class="table-wrap"><table>
497
+ <thead><tr><th>ID</th><th>Type</th><th>Status</th><th>Target</th><th>Why</th></tr></thead>
498
+ <tbody>${e.proposals.map(p => `<tr>
499
+ <td><code>${escapeHtml(p.id)}</code></td>
500
+ <td>${escapeHtml(p.type)}</td>
501
+ <td><span class="pill ${escapeHtml(p.status||'')}">${escapeHtml(p.status||'?')}</span></td>
502
+ <td class="muted">${escapeHtml(p.target_path || '—')}</td>
503
+ <td>${escapeHtml(p.rationale || '—')}</td>
504
+ </tr>`).join('')}</tbody></table></div>`
505
+ : `<div class="empty">No proposals yet. Run <code>kodelyth-ecc evolve analyze</code>.</div>`;
506
+ setLastUpdated();
507
+ } catch (err) {
508
+ $('#evolveCards').innerHTML = `<div class="empty">failed to load evolve: ${escapeHtml(err.message)}</div>`;
509
+ }
510
+ }
511
+
512
+ // ───── catalog ─────
513
+ async function loadCatalog() {
514
+ const kind = $('#catKind').value;
515
+ const q = $('#catQuery').value.trim();
516
+ $('#catalogResults').textContent = 'loading…';
517
+ try {
518
+ const r = await api(`/api/catalog?kind=${encodeURIComponent(kind)}&q=${encodeURIComponent(q)}&limit=200`);
519
+ if (!r.items.length) {
520
+ $('#catalogResults').innerHTML = `<div class="empty">no matches</div>`;
521
+ return;
522
+ }
523
+ $('#catalogResults').innerHTML = `<div class="muted" style="margin:8px 0 12px;">showing ${r.items.length} of ${r.counts[kind] || 0} ${kind}</div>
524
+ <div class="table-wrap"><table>
525
+ <thead><tr><th>Name</th><th>Description</th><th>Tags</th></tr></thead>
526
+ <tbody>${r.items.map(it => `<tr>
527
+ <td><span class="strong">${escapeHtml(it.name || '')}</span></td>
528
+ <td>${escapeHtml(it.description || '')}</td>
529
+ <td>${(it.tags || []).slice(0, 6).map(t => `<span class="pill">${escapeHtml(t)}</span>`).join(' ')}</td>
530
+ </tr>`).join('')}</tbody></table></div>`;
531
+ setLastUpdated();
532
+ } catch (e) {
533
+ $('#catalogResults').innerHTML = `<div class="empty">failed: ${escapeHtml(e.message)}</div>`;
534
+ }
535
+ }
536
+
537
+ // ───── sessions ─────
538
+ async function loadSessions() {
539
+ try {
540
+ const r = await api('/api/sessions');
541
+ if (!r.sessions.length) {
542
+ $('#sessionsList').innerHTML = `<div class="empty">No swarm sessions in <code>.orchestration/</code>. Try <code>kodelyth-ecc swarm --task "…"</code>.</div>`;
543
+ return;
544
+ }
545
+ $('#sessionsList').innerHTML = r.sessions.map(s => `<details>
546
+ <summary><span class="strong">${escapeHtml(s.session)}</span> <span class="muted">· ${s.worker_count} workers · ${fmtDate(s.modified)}</span></summary>
547
+ <div class="muted" style="font-size:12px;margin-bottom:10px;">${escapeHtml(s.path)}</div>
548
+ <div>${s.workers.map(w => `<span class="pill" style="margin-right:6px;">${escapeHtml(w)}</span>`).join('')}</div>
549
+ <button data-session="${escapeHtml(s.session)}" class="loadDetail" style="margin-top:10px;background:#1f2937;color:#cbd5e1;border:1px solid #334155;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">load detail</button>
550
+ <div class="detail-slot" style="margin-top:10px;"></div>
551
+ </details>`).join('');
552
+
553
+ $$('button.loadDetail').forEach(btn => btn.addEventListener('click', async () => {
554
+ const slot = btn.parentElement.querySelector('.detail-slot');
555
+ slot.textContent = 'loading…';
556
+ try {
557
+ const d = await api('/api/sessions/' + encodeURIComponent(btn.dataset.session));
558
+ slot.innerHTML = d.workers.map(w => `<details><summary><code>${escapeHtml(w.slug)}</code> · task=${w.has_task} handoff=${w.has_handoff} status=${w.has_status}</summary>
559
+ ${w.task_excerpt ? `<h4 class="muted" style="margin:10px 0 4px;">task.md</h4><pre>${escapeHtml(w.task_excerpt.slice(0,800))}</pre>` : ''}
560
+ ${w.handoff_excerpt ? `<h4 class="muted" style="margin:10px 0 4px;">handoff.md</h4><pre>${escapeHtml(w.handoff_excerpt.slice(0,800))}</pre>` : ''}
561
+ ${w.status_excerpt ? `<h4 class="muted" style="margin:10px 0 4px;">status.md</h4><pre>${escapeHtml(w.status_excerpt.slice(0,800))}</pre>` : ''}
562
+ </details>`).join('');
563
+ } catch (e) {
564
+ slot.innerHTML = `<div class="empty">load failed: ${escapeHtml(e.message)}</div>`;
565
+ }
566
+ }));
567
+ setLastUpdated();
568
+ } catch (e) {
569
+ $('#sessionsList').innerHTML = `<div class="empty">failed: ${escapeHtml(e.message)}</div>`;
570
+ }
571
+ }
572
+
573
+ // ───── tab dispatcher ─────
574
+ function onTabShown(tab) {
575
+ if (tab === 'overview') loadOverview();
576
+ if (tab === 'memory') loadMemory();
577
+ if (tab === 'evolve') loadEvolve();
578
+ if (tab === 'catalog') loadCatalog();
579
+ if (tab === 'sessions') loadSessions();
580
+ }
581
+
582
+ // ───── real-time: SSE connection ─────
583
+ function updateConnBadge(online) {
584
+ const badge = $('#connBadge');
585
+ const dot = $('#liveDot');
586
+ if (online) {
587
+ badge.textContent = 'LIVE';
588
+ badge.className = 'pill ok';
589
+ dot.style.background = '#34d399';
590
+ dot.style.boxShadow = '0 0 12px #34d399';
591
+ } else {
592
+ badge.textContent = 'offline';
593
+ badge.className = 'pill rejected';
594
+ dot.style.background = '#ef4444';
595
+ dot.style.boxShadow = 'none';
596
+ }
597
+ }
598
+
599
+ let _es = null;
600
+ function connectSSE() {
601
+ if (_es) { try { _es.close(); } catch {} }
602
+ _es = new EventSource('/api/events');
603
+ _es.onopen = () => updateConnBadge(true);
604
+ _es.onmessage = evt => {
605
+ try {
606
+ const msg = JSON.parse(evt.data);
607
+ if (msg.type === 'connected') { updateConnBadge(true); return; }
608
+ // Only refresh on actual data changes. Heartbeat is a keepalive only —
609
+ // triggering a full reload on every heartbeat wastes 6 API calls/minute.
610
+ if (msg.type === 'data-changed') onTabShown(currentTab);
611
+ } catch { /* malformed event — ignore */ }
612
+ };
613
+ _es.onerror = () => {
614
+ updateConnBadge(false);
615
+ // EventSource auto-reconnects with back-off — no manual retry needed.
616
+ };
617
+ }
618
+
619
+ // ───── keyboard shortcuts ─────
620
+ // 1–5: switch to that tab | R: refresh active tab
621
+ document.addEventListener('keydown', e => {
622
+ if (['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.tagName)) return;
623
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
624
+ const TAB_KEYS = { '1': 'overview', '2': 'memory', '3': 'evolve', '4': 'catalog', '5': 'sessions' };
625
+ if (TAB_KEYS[e.key]) { switchTab(TAB_KEYS[e.key]); return; }
626
+ if (e.key === 'r' || e.key === 'R') onTabShown(currentTab);
627
+ });
628
+
629
+ // ───── boot ─────
630
+ $('#memSearchBtn').addEventListener('click', runMemorySearch);
631
+ $('#memQuery').addEventListener('keydown', e => { if (e.key === 'Enter') runMemorySearch(); });
632
+ $('#catRefreshBtn').addEventListener('click', loadCatalog);
633
+ let _catKindTimer = null;
634
+ $('#catKind').addEventListener('change', () => {
635
+ clearTimeout(_catKindTimer);
636
+ _catKindTimer = setTimeout(loadCatalog, 100);
637
+ });
638
+ $('#catQuery').addEventListener('keydown', e => { if (e.key === 'Enter') loadCatalog(); });
639
+ $('#refreshBtn').addEventListener('click', () => onTabShown(currentTab));
640
+
641
+ loadOverview();
642
+ connectSSE();
643
+ </script>
644
+ </body>
645
+ </html>