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,324 @@
1
+ // scripts/dashboard/server.js
2
+ //
3
+ // Local observability dashboard server (v1.7.0).
4
+ //
5
+ // HTTP server using ONLY Node.js built-ins. Serves:
6
+ // GET / → static index.html
7
+ // GET /api/overview → counts + storage paths
8
+ // GET /api/memory → stats + recent captures
9
+ // GET /api/memory/search?q=… → BM25 search
10
+ // GET /api/evolve → reuse + miss + proposals
11
+ // GET /api/catalog?kind=…&q=… → agents | skills | commands | rules | bundles
12
+ // GET /api/sessions → orchestration session list
13
+ // GET /api/sessions/:name → single session detail
14
+ // GET /api/token-budget → per-session token usage
15
+ // GET /api/health → tiny liveness probe
16
+ //
17
+ // Default bind: 127.0.0.1 (localhost ONLY). Refuses to bind 0.0.0.0 unless
18
+ // the caller explicitly opts in. Zero telemetry. No external deps.
19
+ 'use strict';
20
+
21
+ const http = require('http');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const os = require('os');
25
+ const { execFileSync } = require('child_process');
26
+
27
+ const data = require('./data.js');
28
+
29
+ // ── SSE (Server-Sent Events) — real-time push to connected browsers ───────────
30
+
31
+ const sseClients = new Set();
32
+ const MAX_SSE_CLIENTS = 10;
33
+
34
+ // Key files that signal data changes when their mtime moves.
35
+ const WATCH_PATHS = [
36
+ path.join(os.homedir(), '.kodelyth', 'memory', 'memories.jsonl'),
37
+ path.join(os.homedir(), '.kodelyth', 'evolve', 'reuse.json'),
38
+ path.join(os.homedir(), '.kodelyth', 'evolve', 'proposals.jsonl'),
39
+ ];
40
+ const lastMtimes = new Map();
41
+ const FILE_CHECK_MS = 10000; // check every 10 s
42
+
43
+ function broadcastSSE(obj) {
44
+ const msg = `data: ${JSON.stringify(obj)}\n\n`;
45
+ for (const client of sseClients) {
46
+ try { client.write(msg); } catch { sseClients.delete(client); }
47
+ }
48
+ }
49
+
50
+ let _fileWatchTimer = null;
51
+
52
+ function startFileWatcher() {
53
+ if (_fileWatchTimer) return; // already running
54
+ // Prime mtimes so the first tick doesn't fire 'data-changed' spuriously.
55
+ for (const p of WATCH_PATHS) {
56
+ try { lastMtimes.set(p, fs.statSync(p).mtimeMs); } catch { /* file not yet present */ }
57
+ }
58
+ _fileWatchTimer = setInterval(() => {
59
+ if (sseClients.size === 0) return; // no connected browsers — skip stat I/O
60
+ let changed = false;
61
+ for (const p of WATCH_PATHS) {
62
+ try {
63
+ const m = fs.statSync(p).mtimeMs;
64
+ if (lastMtimes.get(p) !== m) { changed = true; lastMtimes.set(p, m); }
65
+ } catch { /* file doesn't exist yet — that's fine */ }
66
+ }
67
+ broadcastSSE({ type: changed ? 'data-changed' : 'heartbeat', time: new Date().toISOString() });
68
+ }, FILE_CHECK_MS);
69
+ }
70
+
71
+ function stopFileWatcher() {
72
+ if (_fileWatchTimer) { clearInterval(_fileWatchTimer); _fileWatchTimer = null; }
73
+ }
74
+ process.on('exit', stopFileWatcher);
75
+
76
+ // ─────────────────────────────────────────────────────────────────────────────
77
+
78
+ const STATIC_DIR = path.join(__dirname, 'static');
79
+
80
+ const MIME = {
81
+ '.html': 'text/html; charset=utf-8',
82
+ '.js': 'application/javascript; charset=utf-8',
83
+ '.css': 'text/css; charset=utf-8',
84
+ '.svg': 'image/svg+xml',
85
+ '.json': 'application/json; charset=utf-8',
86
+ '.ico': 'image/x-icon',
87
+ };
88
+
89
+ function jsonResponse(res, statusCode, payload) {
90
+ const body = JSON.stringify(payload, null, 2);
91
+ res.writeHead(statusCode, {
92
+ 'Content-Type': 'application/json; charset=utf-8',
93
+ 'Content-Length': Buffer.byteLength(body),
94
+ // Reinforce localhost-only posture: refuse to be embedded in foreign frames.
95
+ 'Content-Security-Policy': "default-src 'none'",
96
+ 'X-Frame-Options': 'DENY',
97
+ 'X-Content-Type-Options': 'nosniff',
98
+ 'Referrer-Policy': 'no-referrer',
99
+ // Dashboard is read-only; no CORS allowed.
100
+ 'Cache-Control': 'no-store',
101
+ });
102
+ res.end(body);
103
+ }
104
+
105
+ function notFound(res) {
106
+ jsonResponse(res, 404, { ok: false, error: 'not found' });
107
+ }
108
+
109
+ function badRequest(res, msg) {
110
+ jsonResponse(res, 400, { ok: false, error: msg });
111
+ }
112
+
113
+ function serveStaticFile(res, filePath) {
114
+ fs.readFile(filePath, (err, body) => {
115
+ if (err) return notFound(res);
116
+ const ext = path.extname(filePath).toLowerCase();
117
+ res.writeHead(200, {
118
+ 'Content-Type': MIME[ext] || 'application/octet-stream',
119
+ 'Content-Length': body.length,
120
+ 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self';",
121
+ 'X-Frame-Options': 'DENY',
122
+ 'X-Content-Type-Options': 'nosniff',
123
+ 'Referrer-Policy': 'no-referrer',
124
+ 'Cache-Control': 'no-store',
125
+ });
126
+ res.end(body);
127
+ });
128
+ }
129
+
130
+ // Defensive — block path traversal, only allow files under STATIC_DIR.
131
+ function resolveStatic(reqPath) {
132
+ const decoded = decodeURIComponent(reqPath.replace(/^\/+/, ''));
133
+ if (decoded.includes('..')) return null;
134
+ if (decoded === '' || decoded === '/') return path.join(STATIC_DIR, 'index.html');
135
+ const abs = path.resolve(STATIC_DIR, decoded);
136
+ if (!abs.startsWith(STATIC_DIR + path.sep) && abs !== path.join(STATIC_DIR, 'index.html')) return null;
137
+ return abs;
138
+ }
139
+
140
+ // ── route handlers ───────────────────────────────────────────────────────────
141
+
142
+ function handleRequest(req, res) {
143
+ // DNS-rebinding defence: only respond to requests targeting localhost.
144
+ const reqHost = (req.headers.host || '').split(':')[0];
145
+ if (reqHost !== '' && reqHost !== '127.0.0.1' && reqHost !== 'localhost') {
146
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
147
+ return res.end(JSON.stringify({ ok: false, error: 'bad request' }));
148
+ }
149
+
150
+ // Only GET is supported. The dashboard is purely read-only.
151
+ if (req.method !== 'GET') {
152
+ const body = JSON.stringify({ ok: false, error: 'method not allowed' });
153
+ res.writeHead(405, {
154
+ 'Content-Type': 'application/json; charset=utf-8',
155
+ 'Content-Length': Buffer.byteLength(body),
156
+ 'Allow': 'GET',
157
+ 'Content-Security-Policy':"default-src 'none'",
158
+ 'X-Frame-Options': 'DENY',
159
+ 'X-Content-Type-Options': 'nosniff',
160
+ 'Cache-Control': 'no-store',
161
+ });
162
+ return res.end(body);
163
+ }
164
+
165
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
166
+ const p = url.pathname;
167
+ const q = url.searchParams;
168
+
169
+ try {
170
+ // ── API routes ────────────────────────────────────────────────────────
171
+
172
+ // SSE keep-alive: must be handled before the standard json routes because
173
+ // we never call res.end() here — the connection stays open until the client
174
+ // disconnects or the process exits.
175
+ if (p === '/api/events') {
176
+ if (sseClients.size >= MAX_SSE_CLIENTS) {
177
+ return jsonResponse(res, 503, { ok: false, error: 'too many SSE connections' });
178
+ }
179
+ res.writeHead(200, {
180
+ 'Content-Type': 'text/event-stream',
181
+ 'Cache-Control': 'no-cache',
182
+ 'Connection': 'keep-alive',
183
+ 'X-Accel-Buffering': 'no', // prevent nginx from buffering SSE
184
+ 'X-Content-Type-Options':'nosniff',
185
+ 'X-Frame-Options': 'DENY',
186
+ 'Referrer-Policy': 'no-referrer',
187
+ });
188
+ res.flushHeaders();
189
+ res.write(`data: ${JSON.stringify({ type: 'connected', time: new Date().toISOString() })}\n\n`);
190
+ sseClients.add(res);
191
+ req.on('close', () => sseClients.delete(res));
192
+ return; // intentionally no res.end() — keep connection alive
193
+ }
194
+
195
+ if (p === '/api/health') {
196
+ return jsonResponse(res, 200, { ok: true, time: new Date().toISOString() });
197
+ }
198
+
199
+ if (p === '/api/overview') {
200
+ return jsonResponse(res, 200, data.overview());
201
+ }
202
+
203
+ if (p === '/api/memory') {
204
+ return jsonResponse(res, 200, {
205
+ stats: data.memoryStats(),
206
+ recent: data.recentMemories({ limit: Number(q.get('limit')) || 20 }),
207
+ });
208
+ }
209
+
210
+ if (p === '/api/memory/search') {
211
+ const query = q.get('q') || '';
212
+ if (!query) return badRequest(res, 'query parameter `q` is required');
213
+ return jsonResponse(res, 200, {
214
+ query,
215
+ results: data.memorySearch({ query, limit: Number(q.get('limit')) || 10 }),
216
+ });
217
+ }
218
+
219
+ if (p === '/api/evolve') {
220
+ return jsonResponse(res, 200, data.evolveSnapshot({
221
+ proposalLimit: Math.max(1, Math.min(200, Number(q.get('limit')) || 50)),
222
+ }));
223
+ }
224
+
225
+ if (p === '/api/catalog') {
226
+ const kind = q.get('kind');
227
+ if (!kind) return badRequest(res, '`kind` parameter is required: agents | skills | commands | rules | bundles');
228
+ if (!['agents', 'skills', 'commands', 'rules', 'bundles'].includes(kind)) {
229
+ return badRequest(res, `kind must be one of agents | skills | commands | rules | bundles`);
230
+ }
231
+ return jsonResponse(res, 200, {
232
+ kind,
233
+ counts: data.catalogCounts(),
234
+ items: data.catalogList({ kind, query: q.get('q') || '', limit: Number(q.get('limit')) || 50 }),
235
+ });
236
+ }
237
+
238
+ if (p === '/api/sessions') {
239
+ return jsonResponse(res, 200, {
240
+ sessions: data.sessionsList({ limit: Number(q.get('limit')) || 30 }),
241
+ });
242
+ }
243
+
244
+ const sessMatch = p.match(/^\/api\/sessions\/([A-Za-z0-9._-]+)$/);
245
+ if (sessMatch) {
246
+ const sessionName = sessMatch[1];
247
+ if (sessionName === '..' || sessionName === '.') return notFound(res);
248
+ const detail = data.sessionDetail({ session: sessionName });
249
+ if (!detail) return notFound(res);
250
+ return jsonResponse(res, 200, detail);
251
+ }
252
+
253
+ if (p === '/api/token-budget') {
254
+ return jsonResponse(res, 200, data.tokenBudgetSnapshot());
255
+ }
256
+
257
+ if (p.startsWith('/api/')) return notFound(res);
258
+
259
+ // ── static fallback ───────────────────────────────────────────────────
260
+ const filePath = resolveStatic(p);
261
+ if (!filePath) return notFound(res);
262
+ return serveStaticFile(res, filePath);
263
+ } catch (err) {
264
+ process.stderr.write(`[dashboard] route ${p}: ${err.message}\n`);
265
+ return jsonResponse(res, 500, { ok: false, error: 'internal server error' });
266
+ }
267
+ }
268
+
269
+ // ── public entry: createServer / start ───────────────────────────────────────
270
+
271
+ function createServer() {
272
+ return http.createServer(handleRequest);
273
+ }
274
+
275
+ function chooseOpenCommand() {
276
+ switch (process.platform) {
277
+ case 'darwin': return 'open';
278
+ case 'win32': return 'start ""';
279
+ default: return 'xdg-open';
280
+ }
281
+ }
282
+
283
+ function tryOpenBrowser(url) {
284
+ try {
285
+ // Use execFileSync (no shell) to prevent shell injection via crafted --host values.
286
+ if (process.platform === 'win32') {
287
+ execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
288
+ } else {
289
+ execFileSync(chooseOpenCommand(), [url], { stdio: 'ignore' });
290
+ }
291
+ return true;
292
+ } catch { return false; }
293
+ }
294
+
295
+ function start({ port = 5747, host = '127.0.0.1', openBrowser = true, log = console.log } = {}) {
296
+ return new Promise((resolve, reject) => {
297
+ if (host !== '127.0.0.1' && host !== 'localhost' && !process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE) {
298
+ return reject(new Error(
299
+ `[dashboard] refusing to bind host=${host}. Dashboard is localhost-only by default.\n` +
300
+ `To override (UNSAFE — exposes your memory + evolve data), set KODELYTH_DASHBOARD_ALLOW_REMOTE=1.`
301
+ ));
302
+ }
303
+ const server = createServer();
304
+ startFileWatcher();
305
+ server.once('close', stopFileWatcher);
306
+ server.once('error', reject);
307
+ server.listen(port, host, () => {
308
+ const addr = server.address();
309
+ const url = `http://${host}:${addr.port}/`;
310
+ log(`✓ Kodelyth ECC dashboard\n ${url}`);
311
+ log(` Press Ctrl+C to stop. Localhost only — zero telemetry.`);
312
+ if (openBrowser) tryOpenBrowser(url);
313
+ resolve({ server, url, port: addr.port });
314
+ });
315
+ });
316
+ }
317
+
318
+ module.exports = {
319
+ start,
320
+ createServer,
321
+ handleRequest,
322
+ // exposed for tests
323
+ _internals: { resolveStatic, chooseOpenCommand, startFileWatcher, stopFileWatcher },
324
+ };