@kodax-ai/kodax 0.7.95 → 0.7.96-alpha.2

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 (163) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/LICENSE +158 -158
  3. package/README.md +149 -24
  4. package/README_CN.md +117 -24
  5. package/config-templates/config.example.jsonc +2 -1
  6. package/config-templates/integrations/a2a.example.jsonc +91 -91
  7. package/config-templates/integrations/extensions.example.jsonc +7 -7
  8. package/config-templates/integrations/mcp.example.jsonc +16 -16
  9. package/dist/builtin/code-review/SKILL.md +82 -82
  10. package/dist/builtin/git-workflow/SKILL.md +84 -84
  11. package/dist/builtin/skill-creator/SKILL.md +127 -127
  12. package/dist/builtin/skill-creator/agents/analyzer.md +12 -12
  13. package/dist/builtin/skill-creator/agents/comparator.md +13 -13
  14. package/dist/builtin/skill-creator/agents/grader.md +13 -13
  15. package/dist/builtin/skill-creator/references/schemas.md +227 -227
  16. package/dist/builtin/skill-creator/scripts/aggregate-benchmark.d.ts +46 -46
  17. package/dist/builtin/skill-creator/scripts/aggregate-benchmark.js +208 -208
  18. package/dist/builtin/skill-creator/scripts/analyze-benchmark.d.ts +46 -46
  19. package/dist/builtin/skill-creator/scripts/analyze-benchmark.js +286 -286
  20. package/dist/builtin/skill-creator/scripts/compare-runs.d.ts +62 -62
  21. package/dist/builtin/skill-creator/scripts/compare-runs.js +330 -330
  22. package/dist/builtin/skill-creator/scripts/generate-review.d.ts +33 -33
  23. package/dist/builtin/skill-creator/scripts/generate-review.js +414 -414
  24. package/dist/builtin/skill-creator/scripts/grade-evals.d.ts +73 -73
  25. package/dist/builtin/skill-creator/scripts/grade-evals.js +402 -402
  26. package/dist/builtin/skill-creator/scripts/improve-description.d.ts +23 -23
  27. package/dist/builtin/skill-creator/scripts/improve-description.js +160 -160
  28. package/dist/builtin/skill-creator/scripts/init-skill.d.ts +14 -14
  29. package/dist/builtin/skill-creator/scripts/init-skill.js +155 -155
  30. package/dist/builtin/skill-creator/scripts/install-skill.d.ts +29 -29
  31. package/dist/builtin/skill-creator/scripts/install-skill.js +173 -173
  32. package/dist/builtin/skill-creator/scripts/package-skill.d.ts +38 -38
  33. package/dist/builtin/skill-creator/scripts/package-skill.js +121 -121
  34. package/dist/builtin/skill-creator/scripts/quick-validate.d.ts +8 -8
  35. package/dist/builtin/skill-creator/scripts/quick-validate.js +163 -163
  36. package/dist/builtin/skill-creator/scripts/run-eval.d.ts +66 -66
  37. package/dist/builtin/skill-creator/scripts/run-eval.js +353 -353
  38. package/dist/builtin/skill-creator/scripts/run-loop.d.ts +49 -49
  39. package/dist/builtin/skill-creator/scripts/run-loop.js +242 -242
  40. package/dist/builtin/skill-creator/scripts/run-trigger-eval.d.ts +58 -58
  41. package/dist/builtin/skill-creator/scripts/run-trigger-eval.js +224 -224
  42. package/dist/builtin/tdd/SKILL.md +56 -56
  43. package/dist/chunks/agent-4FABF6WK.js +2 -0
  44. package/dist/chunks/argument-completer-72VHB2VL.js +2 -0
  45. package/dist/chunks/{chunk-7JXA3533.js → chunk-4OIQZWA5.js} +223 -226
  46. package/dist/chunks/{chunk-BNVSKIAB.js → chunk-6BVF3HZ7.js} +136 -136
  47. package/dist/chunks/chunk-7JZZIFZH.js +501 -0
  48. package/dist/chunks/chunk-AC7Z2OIY.js +2 -0
  49. package/dist/chunks/{chunk-JS452J2F.js → chunk-C4KYZAFU.js} +2 -2
  50. package/dist/chunks/chunk-CHUPIWIF.js +251 -0
  51. package/dist/chunks/{chunk-C4DTUKTR.js → chunk-GYUU7XLH.js} +5 -4
  52. package/dist/chunks/{chunk-OK4AXDC5.js → chunk-HAX55GOQ.js} +1 -1
  53. package/dist/chunks/chunk-JKTJCGO2.js +418 -0
  54. package/dist/chunks/{chunk-O743AJ5V.js → chunk-LQYETRFK.js} +171 -175
  55. package/dist/chunks/{chunk-2PEDBGKN.js → chunk-LTUZTQ6W.js} +1 -1
  56. package/dist/chunks/chunk-LYWDUSKO.js +2 -0
  57. package/dist/chunks/chunk-NGH6T4FD.js +92 -0
  58. package/dist/chunks/{chunk-SRIJX5ZP.js → chunk-NVRWCSA5.js} +1 -1
  59. package/dist/chunks/chunk-O4DS7RIQ.js +384 -0
  60. package/dist/chunks/chunk-ORXU6OWZ.js +2 -0
  61. package/dist/chunks/chunk-POTW3O65.js +124 -0
  62. package/dist/chunks/{chunk-2TM3W3GK.js → chunk-RIQSS56Y.js} +1 -1
  63. package/dist/chunks/chunk-U27XKEN5.js +407 -0
  64. package/dist/chunks/{chunk-6ABQJUK6.js → chunk-XUX6OUCA.js} +2 -2
  65. package/dist/chunks/{client-JDOS3YJR.js → client-4C456K4O.js} +1 -1
  66. package/dist/chunks/compaction-config-3VQRDFF4.js +2 -0
  67. package/dist/chunks/{construction-bootstrap-YCWHHUYO.js → construction-bootstrap-AHN7EDGG.js} +1 -1
  68. package/dist/chunks/dist-BCBQXAGI.js +2 -0
  69. package/dist/chunks/dist-QK2YDWU6.js +2 -0
  70. package/dist/chunks/host-VIHV7ADG.js +2 -0
  71. package/dist/chunks/run-manager-NBVMU3KV.js +2 -0
  72. package/dist/chunks/utils-MEJZ2IGI.js +2 -0
  73. package/dist/index.d.ts +22 -21
  74. package/dist/index.js +7 -7
  75. package/dist/kodax_bootstrap.js +26 -26
  76. package/dist/kodax_cli.js +1905 -1882
  77. package/dist/native/darwin-arm64/LICENSE-APACHE.txt +13 -0
  78. package/dist/native/darwin-arm64/kodax-text-transaction.node +0 -0
  79. package/dist/native/darwin-arm64/manifest.json +16 -0
  80. package/dist/native/darwin-x64/LICENSE-APACHE.txt +13 -0
  81. package/dist/native/darwin-x64/kodax-text-transaction.node +0 -0
  82. package/dist/native/darwin-x64/manifest.json +16 -0
  83. package/dist/native/linux-arm64/LICENSE-APACHE.txt +13 -0
  84. package/dist/native/linux-arm64/kodax-text-transaction.node +0 -0
  85. package/dist/native/linux-arm64/manifest.json +16 -0
  86. package/dist/native/linux-x64/LICENSE-APACHE.txt +13 -0
  87. package/dist/native/linux-x64/kodax-text-transaction.node +0 -0
  88. package/dist/native/linux-x64/manifest.json +16 -0
  89. package/dist/native/win32-x64/LICENSE-APACHE.txt +13 -0
  90. package/dist/native/win32-x64/NOTICE-windows-sandbox.txt +8 -0
  91. package/dist/native/win32-x64/kodax-windows-sandbox.exe +0 -0
  92. package/dist/native/win32-x64/kodax-windows-text-transaction.node +0 -0
  93. package/dist/native/win32-x64/manifest.json +30 -0
  94. package/dist/provider-capabilities.json +108 -2
  95. package/dist/runtime-worker.js +1745 -1727
  96. package/dist/sandbox-network-broker.js +1715 -0
  97. package/dist/sdk-a2a.d.ts +13 -12
  98. package/dist/sdk-a2a.js +1 -1
  99. package/dist/sdk-agent.d.ts +27 -18
  100. package/dist/sdk-agent.js +1 -1
  101. package/dist/sdk-coding.d.ts +107 -58
  102. package/dist/sdk-coding.js +1 -1
  103. package/dist/sdk-experimental-memory.d.ts +4 -4
  104. package/dist/sdk-experimental-memory.js +1 -1
  105. package/dist/sdk-llm.d.ts +9 -10
  106. package/dist/sdk-llm.js +1 -1
  107. package/dist/sdk-mcp.js +1 -1
  108. package/dist/sdk-media.d.ts +1 -1
  109. package/dist/sdk-media.js +1 -1
  110. package/dist/sdk-repl.d.ts +25 -17
  111. package/dist/sdk-repl.js +1 -1
  112. package/dist/sdk-runtime.d.ts +41 -17
  113. package/dist/sdk-runtime.js +1 -1
  114. package/dist/sdk-sandbox.d.ts +17 -3
  115. package/dist/sdk-sandbox.js +1 -1
  116. package/dist/sdk-session.d.ts +8 -7
  117. package/dist/sdk-session.js +1 -1
  118. package/dist/sdk-skills.js +1 -1
  119. package/dist/semantic-worker.js +61 -61
  120. package/dist/types-chunks/{base.d-vopO1rml.d.ts → base.d-COo8E2dk.d.ts} +18 -27
  121. package/dist/types-chunks/{bash-prefix-extractor.d-B2rxNI4L.d.ts → bash-prefix-extractor.d-DQOlA8cy.d.ts} +72 -52
  122. package/dist/types-chunks/{capability-learning.d-CE1tm29C.d.ts → capability-learning.d-lRPgA3Or.d.ts} +1 -1
  123. package/dist/types-chunks/{capsule.d-Y7QRS4J2.d.ts → capsule.d-0zxsgXg_.d.ts} +3 -3
  124. package/dist/types-chunks/{controller.d-BR-UVeUR.d.ts → controller.d-BTtyIIC1.d.ts} +1 -1
  125. package/dist/types-chunks/{controller.d-C5noq5-w.d.ts → controller.d-C7tXUcXo.d.ts} +2 -2
  126. package/dist/types-chunks/{guardrail.d-DqHD-61O.d.ts → guardrail.d-DKTyEHJa.d.ts} +5 -5
  127. package/dist/types-chunks/{history-retrieval.d-CKU8zmt_.d.ts → history-retrieval.d-CHnL99S7.d.ts} +2 -2
  128. package/dist/types-chunks/{public-api.d-D7fXaWW8.d.ts → public-api.d-Dx3L3W4Z.d.ts} +7 -4
  129. package/dist/types-chunks/{repl.d-BYDb14s6.d.ts → repl.d-mO01olIa.d.ts} +5 -5
  130. package/dist/types-chunks/{resolver.d-CwHUwbdE.d.ts → resolver.d-DlXPjeyx.d.ts} +38 -6
  131. package/dist/types-chunks/{review-inbox.d-BSgFewyj.d.ts → review-inbox.d-WDSe1nze.d.ts} +1 -1
  132. package/dist/types-chunks/{run-manager.d-BVJSntpS.d.ts → run-manager.d-DENbbzgR.d.ts} +1 -1
  133. package/dist/types-chunks/{sdk-session-CqEG84oP.d.ts → sdk-session-DYHGXXH4.d.ts} +3 -3
  134. package/dist/types-chunks/{shell-command-sets.d-DqaaBwhU.d.ts → shell-command-sets.d-CjFqS4dp.d.ts} +1 -1
  135. package/dist/types-chunks/{side-query.d-iH0P9ASy.d.ts → side-query.d-j8A3sFv_.d.ts} +2 -2
  136. package/dist/types-chunks/{types.d-rUOjXych.d.ts → types.d-BbZjR0Wr.d.ts} +1 -1
  137. package/dist/types-chunks/{types.d-CaTXAW_Q.d.ts → types.d-BkTIKdH6.d.ts} +2 -2
  138. package/dist/types-chunks/{types.d-C5xQEFcj.d.ts → types.d-DncLrpu_.d.ts} +4 -4
  139. package/dist/types-chunks/{types.d-CK9A9rpP.d.ts → types.d-Xy0f3x4i.d.ts} +9 -0
  140. package/dist/types-chunks/{utils.d-CWUyLiXo.d.ts → utils.d-CnxefMos.d.ts} +6 -6
  141. package/package.json +10 -3
  142. package/public_docs/README.md +130 -0
  143. package/public_docs/configuration/sandbox.md +289 -0
  144. package/public_docs/sdk/embedder-guide.md +6674 -0
  145. package/scripts/kodax-bin.cjs +28 -28
  146. package/scripts/production-env.cjs +25 -25
  147. package/dist/chunks/agent-XXZMXKKA.js +0 -2
  148. package/dist/chunks/argument-completer-KCYDGTQW.js +0 -2
  149. package/dist/chunks/chunk-77PNP27P.js +0 -92
  150. package/dist/chunks/chunk-7QBWHCIZ.js +0 -2
  151. package/dist/chunks/chunk-BHE66SP3.js +0 -123
  152. package/dist/chunks/chunk-BR2OSB2I.js +0 -722
  153. package/dist/chunks/chunk-DSMONSVB.js +0 -407
  154. package/dist/chunks/chunk-Q5VRWSKG.js +0 -418
  155. package/dist/chunks/chunk-XWRPOGJN.js +0 -4
  156. package/dist/chunks/chunk-Y5VIQBT6.js +0 -383
  157. package/dist/chunks/compaction-config-EGS4577X.js +0 -2
  158. package/dist/chunks/dist-ITTXQHRJ.js +0 -2
  159. package/dist/chunks/dist-KFYN3OKD.js +0 -2
  160. package/dist/chunks/host-VAYADIPW.js +0 -2
  161. package/dist/chunks/run-manager-52IJZMZJ.js +0 -2
  162. package/dist/chunks/utils-P7PBSL3R.js +0 -2
  163. package/dist/sandbox-workspace-session.js +0 -1705
@@ -1,416 +1,416 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
4
- import { createServer } from 'node:http';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- const TEXT_EXTENSIONS = new Set([
9
- '.txt', '.md', '.json', '.csv', '.ts', '.tsx', '.js', '.jsx',
10
- '.yaml', '.yml', '.html', '.css', '.sql', '.toml', '.xml',
11
- ]);
12
- const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp']);
13
- const METADATA_FILES = new Set(['transcript.md', 'metrics.json']);
14
-
15
- async function readJson(filePath, fallback = null) {
16
- try {
17
- return JSON.parse(await readFile(filePath, 'utf8'));
18
- } catch {
19
- return fallback;
20
- }
21
- }
22
-
23
- async function pathExists(filePath) {
24
- try {
25
- await stat(filePath);
26
- return true;
27
- } catch {
28
- return false;
29
- }
30
- }
31
-
32
- async function collectOutputFiles(outputsDir) {
33
- const entries = await readdir(outputsDir, { withFileTypes: true }).catch(() => []);
34
- const files = [];
35
-
36
- for (const entry of entries) {
37
- if (!entry.isFile() || METADATA_FILES.has(entry.name)) {
38
- continue;
39
- }
40
-
41
- const filePath = path.join(outputsDir, entry.name);
42
- const extension = path.extname(entry.name).toLowerCase();
43
-
44
- if (TEXT_EXTENSIONS.has(extension)) {
45
- files.push({
46
- name: entry.name,
47
- kind: 'text',
48
- content: await readFile(filePath, 'utf8').catch(() => '(Error reading file)'),
49
- });
50
- continue;
51
- }
52
-
53
- const raw = await readFile(filePath).catch(() => null);
54
- if (!raw) {
55
- files.push({ name: entry.name, kind: 'error', content: '(Error reading file)' });
56
- continue;
57
- }
58
-
59
- const base64 = raw.toString('base64');
60
- if (IMAGE_EXTENSIONS.has(extension)) {
61
- const mime = extension === '.svg' ? 'image/svg+xml' : `image/${extension.slice(1)}`;
62
- files.push({
63
- name: entry.name,
64
- kind: 'image',
65
- dataUri: `data:${mime};base64,${base64}`,
66
- });
67
- } else if (extension === '.pdf') {
68
- files.push({
69
- name: entry.name,
70
- kind: 'pdf',
71
- dataUri: `data:application/pdf;base64,${base64}`,
72
- });
73
- } else {
74
- files.push({
75
- name: entry.name,
76
- kind: 'binary',
77
- dataUri: `data:application/octet-stream;base64,${base64}`,
78
- });
79
- }
80
- }
81
-
82
- return files;
83
- }
84
-
85
- async function buildRun(workspaceRoot, runDir) {
86
- const outputsDir = path.join(runDir, 'outputs');
87
- if (!(await pathExists(outputsDir))) {
88
- return null;
89
- }
90
-
91
- const evalMetadata = await readJson(path.join(runDir, 'eval_metadata.json'))
92
- ?? await readJson(path.join(path.dirname(runDir), 'eval_metadata.json'))
93
- ?? await readJson(path.join(path.dirname(path.dirname(runDir)), 'eval_metadata.json'), {});
94
-
95
- let prompt = evalMetadata.prompt ?? '';
96
- if (!prompt) {
97
- const transcript = await readFile(path.join(runDir, 'transcript.md'), 'utf8').catch(() => '');
98
- const match = transcript.match(/## Eval Prompt\s+([\s\S]*?)(?:\n##|$)/);
99
- prompt = match?.[1]?.trim() ?? '(No prompt found)';
100
- }
101
-
102
- return {
103
- id: path.relative(workspaceRoot, runDir).replace(/\\/g, '/'),
104
- evalId: evalMetadata.eval_id ?? null,
105
- prompt,
106
- grading: await readJson(path.join(runDir, 'grading.json'), null),
107
- outputs: await collectOutputFiles(outputsDir),
108
- };
109
- }
110
-
111
- export async function findRuns(workspaceRoot, currentDir = workspaceRoot, runs = []) {
112
- const outputsDir = path.join(currentDir, 'outputs');
113
- if (await pathExists(outputsDir)) {
114
- const run = await buildRun(workspaceRoot, currentDir);
115
- if (run) {
116
- runs.push(run);
117
- }
118
- return runs;
119
- }
120
-
121
- const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => []);
122
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
123
- if (!entry.isDirectory()) {
124
- continue;
125
- }
126
- if (['node_modules', '.git', '__pycache__', 'inputs'].includes(entry.name)) {
127
- continue;
128
- }
129
- await findRuns(workspaceRoot, path.join(currentDir, entry.name), runs);
130
- }
131
-
132
- return runs;
133
- }
134
-
135
- function escapeHtml(value) {
136
- return value
137
- .replace(/&/g, '&')
138
- .replace(/</g, '&lt;')
139
- .replace(/>/g, '&gt;')
140
- .replace(/"/g, '&quot;');
141
- }
142
-
143
- export function renderHtml(payload, staticMode) {
144
- const serialized = JSON.stringify(payload).replace(/</g, '\\u003c');
145
-
146
- return `<!doctype html>
147
- <html lang="en">
148
- <head>
149
- <meta charset="utf-8">
150
- <meta name="viewport" content="width=device-width, initial-scale=1">
151
- <title>${escapeHtml(payload.skillName)} Review</title>
152
- <style>
153
- :root { color-scheme: light; font-family: "Segoe UI", "IBM Plex Sans", sans-serif; }
154
- body { margin: 0; background: #f5f1e8; color: #1f2328; }
155
- header { padding: 24px 28px; background: linear-gradient(135deg, #113946, #bca37f); color: #fffdf7; }
156
- main { display: grid; grid-template-columns: 280px 1fr; min-height: calc(100vh - 100px); }
157
- nav { border-right: 1px solid #d7d1c7; background: #fffaf1; padding: 20px; overflow: auto; }
158
- .panel { padding: 24px 28px; overflow: auto; }
159
- .run-button { display: block; width: 100%; text-align: left; margin: 0 0 10px; padding: 12px; border: 1px solid #d7d1c7; border-radius: 12px; background: #fff; cursor: pointer; }
160
- .run-button.active { border-color: #113946; box-shadow: 0 0 0 2px rgba(17,57,70,.12); }
161
- .tabs { display: flex; gap: 12px; margin-bottom: 18px; }
162
- .tab { border: 0; border-radius: 999px; padding: 10px 16px; background: #e6dccd; cursor: pointer; }
163
- .tab.active { background: #113946; color: white; }
164
- .card { background: white; border: 1px solid #d7d1c7; border-radius: 18px; padding: 18px; margin-bottom: 18px; }
165
- pre { white-space: pre-wrap; word-break: break-word; background: #f7f7f8; padding: 14px; border-radius: 12px; }
166
- textarea { width: 100%; min-height: 180px; border-radius: 12px; border: 1px solid #c8c1b6; padding: 12px; font: inherit; }
167
- table { width: 100%; border-collapse: collapse; background: white; border-radius: 18px; overflow: hidden; }
168
- th, td { padding: 12px; border-bottom: 1px solid #ece7df; text-align: left; }
169
- .hidden { display: none; }
170
- .muted { color: #57606a; }
171
- img, iframe { max-width: 100%; border-radius: 12px; }
172
- @media (max-width: 920px) {
173
- main { grid-template-columns: 1fr; }
174
- nav { border-right: 0; border-bottom: 1px solid #d7d1c7; }
175
- }
176
- </style>
177
- </head>
178
- <body>
179
- <header>
180
- <div class="muted">KodaX Skill Review</div>
181
- <h1>${escapeHtml(payload.skillName)}</h1>
182
- <div>${escapeHtml(payload.workspace)}</div>
183
- </header>
184
- <main>
185
- <nav>
186
- <div class="tabs">
187
- <button class="tab active" data-tab="outputs">Outputs</button>
188
- <button class="tab" data-tab="benchmark">Benchmark</button>
189
- </div>
190
- <div id="run-list"></div>
191
- </nav>
192
- <section class="panel">
193
- <div id="outputs-panel"></div>
194
- <div id="benchmark-panel" class="hidden"></div>
195
- </section>
196
- </main>
197
- <script>
198
- const payload = ${serialized};
199
- const feedback = { ...(payload.feedback || {}) };
200
- let currentRunId = payload.runs[0]?.id || null;
201
-
202
- const runList = document.getElementById('run-list');
203
- const outputsPanel = document.getElementById('outputs-panel');
204
- const benchmarkPanel = document.getElementById('benchmark-panel');
205
- const tabs = Array.from(document.querySelectorAll('.tab'));
206
-
207
- function escapeHtml(value) {
208
- return String(value)
209
- .replace(/&/g, '&amp;')
210
- .replace(/</g, '&lt;')
211
- .replace(/>/g, '&gt;')
212
- .replace(/"/g, '&quot;');
213
- }
214
-
215
- function persistFeedback() {
216
- if (${staticMode ? 'true' : 'false'}) {
217
- localStorage.setItem('kodax-skill-review-feedback', JSON.stringify(feedback));
218
- return Promise.resolve();
219
- }
220
- return fetch('/feedback', {
221
- method: 'POST',
222
- headers: { 'Content-Type': 'application/json' },
223
- body: JSON.stringify(feedback),
224
- });
225
- }
226
-
227
- function renderRunList() {
228
- runList.innerHTML = '';
229
- for (const run of payload.runs) {
230
- const button = document.createElement('button');
231
- button.className = 'run-button' + (run.id === currentRunId ? ' active' : '');
232
- button.innerHTML = '<strong>' + run.id + '</strong><div class="muted">' + (run.prompt || '') + '</div>';
233
- button.onclick = () => {
234
- currentRunId = run.id;
235
- renderRunList();
236
- renderOutputs();
237
- };
238
- runList.appendChild(button);
239
- }
240
- }
241
-
242
- function renderOutputs() {
243
- const run = payload.runs.find((item) => item.id === currentRunId);
244
- if (!run) {
245
- outputsPanel.innerHTML = '<div class="card">No run selected.</div>';
246
- return;
247
- }
248
-
249
- const cards = [];
250
- cards.push('<div class="card"><h2>Prompt</h2><pre>' + escapeHtml(run.prompt || '(No prompt found)') + '</pre></div>');
251
- if (run.grading) {
252
- cards.push('<div class="card"><h2>Grading</h2><pre>' + escapeHtml(JSON.stringify(run.grading, null, 2)) + '</pre></div>');
253
- }
254
-
255
- for (const output of run.outputs) {
256
- if (output.kind === 'text') {
257
- cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><pre>' + escapeHtml(output.content) + '</pre></div>');
258
- } else if (output.kind === 'image') {
259
- cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><img alt="" src="' + output.dataUri + '"></div>');
260
- } else if (output.kind === 'pdf') {
261
- cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><iframe title="" src="' + output.dataUri + '" style="width:100%;min-height:500px;"></iframe></div>');
262
- } else {
263
- cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><a download="' + escapeHtml(output.name) + '" href="' + output.dataUri + '">Download output</a></div>');
264
- }
265
- }
266
-
267
- cards.push('<div class="card"><h2>Feedback</h2><textarea id="feedback-box">' + escapeHtml(feedback[run.id] || '') + '</textarea><div class="muted">Feedback is ' + (${staticMode ? '"saved in localStorage for this browser."' : '"written to feedback.json in the workspace."'}) + '</div></div>');
268
- outputsPanel.innerHTML = cards.join('');
269
-
270
- const textarea = document.getElementById('feedback-box');
271
- textarea.addEventListener('input', () => {
272
- feedback[run.id] = textarea.value;
273
- persistFeedback();
274
- });
275
- }
276
-
277
- function renderBenchmark() {
278
- const benchmark = payload.benchmark;
279
- if (!benchmark) {
280
- benchmarkPanel.innerHTML = '<div class="card">No benchmark.json found.</div>';
281
- return;
282
- }
283
-
284
- const rows = Object.entries(benchmark.configs || {}).map(([name, stats]) => (
285
- '<tr><td>' + escapeHtml(name) + '</td><td>' + stats.pass_rate.mean + ' ± ' + stats.pass_rate.stddev + '</td><td>' + stats.time_seconds.mean + ' ± ' + stats.time_seconds.stddev + '</td><td>' + stats.tokens.mean + ' ± ' + stats.tokens.stddev + '</td></tr>'
286
- )).join('');
287
-
288
- benchmarkPanel.innerHTML = '<div class="card"><h2>Summary</h2><table><thead><tr><th>Config</th><th>Pass rate</th><th>Time (s)</th><th>Tokens</th></tr></thead><tbody>' + rows + '</tbody></table><p class="muted">Delta: pass rate ' + (benchmark.delta?.pass_rate || 'n/a') + ', time ' + (benchmark.delta?.time_seconds || 'n/a') + ', tokens ' + (benchmark.delta?.tokens || 'n/a') + '</p></div>';
289
- }
290
-
291
- function setTab(tabName) {
292
- tabs.forEach((tab) => tab.classList.toggle('active', tab.dataset.tab === tabName));
293
- outputsPanel.classList.toggle('hidden', tabName !== 'outputs');
294
- benchmarkPanel.classList.toggle('hidden', tabName !== 'benchmark');
295
- }
296
-
297
- tabs.forEach((tab) => {
298
- tab.addEventListener('click', () => setTab(tab.dataset.tab));
299
- });
300
-
301
- if (${staticMode ? 'true' : 'false'}) {
302
- try {
303
- Object.assign(feedback, JSON.parse(localStorage.getItem('kodax-skill-review-feedback') || '{}'));
304
- } catch {}
305
- }
306
-
307
- renderRunList();
308
- renderOutputs();
309
- renderBenchmark();
310
- </script>
311
- </body>
312
- </html>`;
313
- }
314
-
315
- function parseArgs(argv) {
316
- const args = {
317
- workspace: argv[2],
318
- skillName: 'unknown-skill',
319
- benchmark: null,
320
- staticOutput: null,
321
- port: 4173,
322
- };
323
-
324
- for (let index = 3; index < argv.length; index += 1) {
325
- const token = argv[index];
326
- if (token === '--skill-name' && argv[index + 1]) {
327
- args.skillName = argv[index + 1];
328
- index += 1;
329
- } else if (token === '--benchmark' && argv[index + 1]) {
330
- args.benchmark = argv[index + 1];
331
- index += 1;
332
- } else if (token === '--static' && argv[index + 1]) {
333
- args.staticOutput = argv[index + 1];
334
- index += 1;
335
- } else if (token === '--port' && argv[index + 1]) {
336
- args.port = Number(argv[index + 1]);
337
- index += 1;
338
- }
339
- }
340
-
341
- return args;
342
- }
343
-
344
- export async function buildPayload(workspace, args) {
345
- const benchmarkPath = args.benchmark ?? path.join(workspace, 'benchmark.json');
346
-
347
- return {
348
- skillName: args.skillName,
349
- workspace: path.resolve(workspace),
350
- benchmark: await readJson(benchmarkPath, null),
351
- feedback: await readJson(path.join(workspace, 'feedback.json'), {}),
352
- runs: await findRuns(path.resolve(workspace)),
353
- };
354
- }
355
-
356
- async function main() {
357
- const args = parseArgs(process.argv);
358
- if (!args.workspace) {
359
- console.error('Usage: node scripts/generate-review.js <workspace> [--skill-name name] [--benchmark file] [--static output.html] [--port 4173]');
360
- process.exit(1);
361
- }
362
-
363
- const payload = await buildPayload(args.workspace, args);
364
-
365
- if (args.staticOutput) {
366
- await writeFile(args.staticOutput, renderHtml(payload, true));
367
- console.log(`Wrote ${args.staticOutput}`);
368
- return;
369
- }
370
-
371
- const server = createServer(async (request, response) => {
372
- if (!request.url || request.method === 'GET' && request.url === '/') {
373
- response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
374
- response.end(renderHtml(payload, false));
375
- return;
376
- }
377
-
378
- if (request.method === 'POST' && request.url === '/feedback') {
379
- let body = '';
380
- request.on('data', (chunk) => {
381
- body += chunk;
382
- });
383
- request.on('end', async () => {
384
- try {
385
- const parsed = JSON.parse(body);
386
- payload.feedback = parsed;
387
- await writeFile(path.join(args.workspace, 'feedback.json'), JSON.stringify(parsed, null, 2));
388
- response.writeHead(204);
389
- response.end();
390
- } catch {
391
- response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
392
- response.end('Invalid feedback payload');
393
- }
394
- });
395
- return;
396
- }
397
-
398
- response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
399
- response.end('Not found');
400
- });
401
-
402
- server.listen(args.port, () => {
403
- console.log(`Review server running at http://127.0.0.1:${args.port}`);
404
- });
405
- }
406
-
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const TEXT_EXTENSIONS = new Set([
9
+ '.txt', '.md', '.json', '.csv', '.ts', '.tsx', '.js', '.jsx',
10
+ '.yaml', '.yml', '.html', '.css', '.sql', '.toml', '.xml',
11
+ ]);
12
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp']);
13
+ const METADATA_FILES = new Set(['transcript.md', 'metrics.json']);
14
+
15
+ async function readJson(filePath, fallback = null) {
16
+ try {
17
+ return JSON.parse(await readFile(filePath, 'utf8'));
18
+ } catch {
19
+ return fallback;
20
+ }
21
+ }
22
+
23
+ async function pathExists(filePath) {
24
+ try {
25
+ await stat(filePath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function collectOutputFiles(outputsDir) {
33
+ const entries = await readdir(outputsDir, { withFileTypes: true }).catch(() => []);
34
+ const files = [];
35
+
36
+ for (const entry of entries) {
37
+ if (!entry.isFile() || METADATA_FILES.has(entry.name)) {
38
+ continue;
39
+ }
40
+
41
+ const filePath = path.join(outputsDir, entry.name);
42
+ const extension = path.extname(entry.name).toLowerCase();
43
+
44
+ if (TEXT_EXTENSIONS.has(extension)) {
45
+ files.push({
46
+ name: entry.name,
47
+ kind: 'text',
48
+ content: await readFile(filePath, 'utf8').catch(() => '(Error reading file)'),
49
+ });
50
+ continue;
51
+ }
52
+
53
+ const raw = await readFile(filePath).catch(() => null);
54
+ if (!raw) {
55
+ files.push({ name: entry.name, kind: 'error', content: '(Error reading file)' });
56
+ continue;
57
+ }
58
+
59
+ const base64 = raw.toString('base64');
60
+ if (IMAGE_EXTENSIONS.has(extension)) {
61
+ const mime = extension === '.svg' ? 'image/svg+xml' : `image/${extension.slice(1)}`;
62
+ files.push({
63
+ name: entry.name,
64
+ kind: 'image',
65
+ dataUri: `data:${mime};base64,${base64}`,
66
+ });
67
+ } else if (extension === '.pdf') {
68
+ files.push({
69
+ name: entry.name,
70
+ kind: 'pdf',
71
+ dataUri: `data:application/pdf;base64,${base64}`,
72
+ });
73
+ } else {
74
+ files.push({
75
+ name: entry.name,
76
+ kind: 'binary',
77
+ dataUri: `data:application/octet-stream;base64,${base64}`,
78
+ });
79
+ }
80
+ }
81
+
82
+ return files;
83
+ }
84
+
85
+ async function buildRun(workspaceRoot, runDir) {
86
+ const outputsDir = path.join(runDir, 'outputs');
87
+ if (!(await pathExists(outputsDir))) {
88
+ return null;
89
+ }
90
+
91
+ const evalMetadata = await readJson(path.join(runDir, 'eval_metadata.json'))
92
+ ?? await readJson(path.join(path.dirname(runDir), 'eval_metadata.json'))
93
+ ?? await readJson(path.join(path.dirname(path.dirname(runDir)), 'eval_metadata.json'), {});
94
+
95
+ let prompt = evalMetadata.prompt ?? '';
96
+ if (!prompt) {
97
+ const transcript = await readFile(path.join(runDir, 'transcript.md'), 'utf8').catch(() => '');
98
+ const match = transcript.match(/## Eval Prompt\s+([\s\S]*?)(?:\n##|$)/);
99
+ prompt = match?.[1]?.trim() ?? '(No prompt found)';
100
+ }
101
+
102
+ return {
103
+ id: path.relative(workspaceRoot, runDir).replace(/\\/g, '/'),
104
+ evalId: evalMetadata.eval_id ?? null,
105
+ prompt,
106
+ grading: await readJson(path.join(runDir, 'grading.json'), null),
107
+ outputs: await collectOutputFiles(outputsDir),
108
+ };
109
+ }
110
+
111
+ export async function findRuns(workspaceRoot, currentDir = workspaceRoot, runs = []) {
112
+ const outputsDir = path.join(currentDir, 'outputs');
113
+ if (await pathExists(outputsDir)) {
114
+ const run = await buildRun(workspaceRoot, currentDir);
115
+ if (run) {
116
+ runs.push(run);
117
+ }
118
+ return runs;
119
+ }
120
+
121
+ const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => []);
122
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
123
+ if (!entry.isDirectory()) {
124
+ continue;
125
+ }
126
+ if (['node_modules', '.git', '__pycache__', 'inputs'].includes(entry.name)) {
127
+ continue;
128
+ }
129
+ await findRuns(workspaceRoot, path.join(currentDir, entry.name), runs);
130
+ }
131
+
132
+ return runs;
133
+ }
134
+
135
+ function escapeHtml(value) {
136
+ return value
137
+ .replace(/&/g, '&amp;')
138
+ .replace(/</g, '&lt;')
139
+ .replace(/>/g, '&gt;')
140
+ .replace(/"/g, '&quot;');
141
+ }
142
+
143
+ export function renderHtml(payload, staticMode) {
144
+ const serialized = JSON.stringify(payload).replace(/</g, '\\u003c');
145
+
146
+ return `<!doctype html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="utf-8">
150
+ <meta name="viewport" content="width=device-width, initial-scale=1">
151
+ <title>${escapeHtml(payload.skillName)} Review</title>
152
+ <style>
153
+ :root { color-scheme: light; font-family: "Segoe UI", "IBM Plex Sans", sans-serif; }
154
+ body { margin: 0; background: #f5f1e8; color: #1f2328; }
155
+ header { padding: 24px 28px; background: linear-gradient(135deg, #113946, #bca37f); color: #fffdf7; }
156
+ main { display: grid; grid-template-columns: 280px 1fr; min-height: calc(100vh - 100px); }
157
+ nav { border-right: 1px solid #d7d1c7; background: #fffaf1; padding: 20px; overflow: auto; }
158
+ .panel { padding: 24px 28px; overflow: auto; }
159
+ .run-button { display: block; width: 100%; text-align: left; margin: 0 0 10px; padding: 12px; border: 1px solid #d7d1c7; border-radius: 12px; background: #fff; cursor: pointer; }
160
+ .run-button.active { border-color: #113946; box-shadow: 0 0 0 2px rgba(17,57,70,.12); }
161
+ .tabs { display: flex; gap: 12px; margin-bottom: 18px; }
162
+ .tab { border: 0; border-radius: 999px; padding: 10px 16px; background: #e6dccd; cursor: pointer; }
163
+ .tab.active { background: #113946; color: white; }
164
+ .card { background: white; border: 1px solid #d7d1c7; border-radius: 18px; padding: 18px; margin-bottom: 18px; }
165
+ pre { white-space: pre-wrap; word-break: break-word; background: #f7f7f8; padding: 14px; border-radius: 12px; }
166
+ textarea { width: 100%; min-height: 180px; border-radius: 12px; border: 1px solid #c8c1b6; padding: 12px; font: inherit; }
167
+ table { width: 100%; border-collapse: collapse; background: white; border-radius: 18px; overflow: hidden; }
168
+ th, td { padding: 12px; border-bottom: 1px solid #ece7df; text-align: left; }
169
+ .hidden { display: none; }
170
+ .muted { color: #57606a; }
171
+ img, iframe { max-width: 100%; border-radius: 12px; }
172
+ @media (max-width: 920px) {
173
+ main { grid-template-columns: 1fr; }
174
+ nav { border-right: 0; border-bottom: 1px solid #d7d1c7; }
175
+ }
176
+ </style>
177
+ </head>
178
+ <body>
179
+ <header>
180
+ <div class="muted">KodaX Skill Review</div>
181
+ <h1>${escapeHtml(payload.skillName)}</h1>
182
+ <div>${escapeHtml(payload.workspace)}</div>
183
+ </header>
184
+ <main>
185
+ <nav>
186
+ <div class="tabs">
187
+ <button class="tab active" data-tab="outputs">Outputs</button>
188
+ <button class="tab" data-tab="benchmark">Benchmark</button>
189
+ </div>
190
+ <div id="run-list"></div>
191
+ </nav>
192
+ <section class="panel">
193
+ <div id="outputs-panel"></div>
194
+ <div id="benchmark-panel" class="hidden"></div>
195
+ </section>
196
+ </main>
197
+ <script>
198
+ const payload = ${serialized};
199
+ const feedback = { ...(payload.feedback || {}) };
200
+ let currentRunId = payload.runs[0]?.id || null;
201
+
202
+ const runList = document.getElementById('run-list');
203
+ const outputsPanel = document.getElementById('outputs-panel');
204
+ const benchmarkPanel = document.getElementById('benchmark-panel');
205
+ const tabs = Array.from(document.querySelectorAll('.tab'));
206
+
207
+ function escapeHtml(value) {
208
+ return String(value)
209
+ .replace(/&/g, '&amp;')
210
+ .replace(/</g, '&lt;')
211
+ .replace(/>/g, '&gt;')
212
+ .replace(/"/g, '&quot;');
213
+ }
214
+
215
+ function persistFeedback() {
216
+ if (${staticMode ? 'true' : 'false'}) {
217
+ localStorage.setItem('kodax-skill-review-feedback', JSON.stringify(feedback));
218
+ return Promise.resolve();
219
+ }
220
+ return fetch('/feedback', {
221
+ method: 'POST',
222
+ headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify(feedback),
224
+ });
225
+ }
226
+
227
+ function renderRunList() {
228
+ runList.innerHTML = '';
229
+ for (const run of payload.runs) {
230
+ const button = document.createElement('button');
231
+ button.className = 'run-button' + (run.id === currentRunId ? ' active' : '');
232
+ button.innerHTML = '<strong>' + run.id + '</strong><div class="muted">' + (run.prompt || '') + '</div>';
233
+ button.onclick = () => {
234
+ currentRunId = run.id;
235
+ renderRunList();
236
+ renderOutputs();
237
+ };
238
+ runList.appendChild(button);
239
+ }
240
+ }
241
+
242
+ function renderOutputs() {
243
+ const run = payload.runs.find((item) => item.id === currentRunId);
244
+ if (!run) {
245
+ outputsPanel.innerHTML = '<div class="card">No run selected.</div>';
246
+ return;
247
+ }
248
+
249
+ const cards = [];
250
+ cards.push('<div class="card"><h2>Prompt</h2><pre>' + escapeHtml(run.prompt || '(No prompt found)') + '</pre></div>');
251
+ if (run.grading) {
252
+ cards.push('<div class="card"><h2>Grading</h2><pre>' + escapeHtml(JSON.stringify(run.grading, null, 2)) + '</pre></div>');
253
+ }
254
+
255
+ for (const output of run.outputs) {
256
+ if (output.kind === 'text') {
257
+ cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><pre>' + escapeHtml(output.content) + '</pre></div>');
258
+ } else if (output.kind === 'image') {
259
+ cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><img alt="" src="' + output.dataUri + '"></div>');
260
+ } else if (output.kind === 'pdf') {
261
+ cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><iframe title="" src="' + output.dataUri + '" style="width:100%;min-height:500px;"></iframe></div>');
262
+ } else {
263
+ cards.push('<div class="card"><h2>' + escapeHtml(output.name) + '</h2><a download="' + escapeHtml(output.name) + '" href="' + output.dataUri + '">Download output</a></div>');
264
+ }
265
+ }
266
+
267
+ cards.push('<div class="card"><h2>Feedback</h2><textarea id="feedback-box">' + escapeHtml(feedback[run.id] || '') + '</textarea><div class="muted">Feedback is ' + (${staticMode ? '"saved in localStorage for this browser."' : '"written to feedback.json in the workspace."'}) + '</div></div>');
268
+ outputsPanel.innerHTML = cards.join('');
269
+
270
+ const textarea = document.getElementById('feedback-box');
271
+ textarea.addEventListener('input', () => {
272
+ feedback[run.id] = textarea.value;
273
+ persistFeedback();
274
+ });
275
+ }
276
+
277
+ function renderBenchmark() {
278
+ const benchmark = payload.benchmark;
279
+ if (!benchmark) {
280
+ benchmarkPanel.innerHTML = '<div class="card">No benchmark.json found.</div>';
281
+ return;
282
+ }
283
+
284
+ const rows = Object.entries(benchmark.configs || {}).map(([name, stats]) => (
285
+ '<tr><td>' + escapeHtml(name) + '</td><td>' + stats.pass_rate.mean + ' ± ' + stats.pass_rate.stddev + '</td><td>' + stats.time_seconds.mean + ' ± ' + stats.time_seconds.stddev + '</td><td>' + stats.tokens.mean + ' ± ' + stats.tokens.stddev + '</td></tr>'
286
+ )).join('');
287
+
288
+ benchmarkPanel.innerHTML = '<div class="card"><h2>Summary</h2><table><thead><tr><th>Config</th><th>Pass rate</th><th>Time (s)</th><th>Tokens</th></tr></thead><tbody>' + rows + '</tbody></table><p class="muted">Delta: pass rate ' + (benchmark.delta?.pass_rate || 'n/a') + ', time ' + (benchmark.delta?.time_seconds || 'n/a') + ', tokens ' + (benchmark.delta?.tokens || 'n/a') + '</p></div>';
289
+ }
290
+
291
+ function setTab(tabName) {
292
+ tabs.forEach((tab) => tab.classList.toggle('active', tab.dataset.tab === tabName));
293
+ outputsPanel.classList.toggle('hidden', tabName !== 'outputs');
294
+ benchmarkPanel.classList.toggle('hidden', tabName !== 'benchmark');
295
+ }
296
+
297
+ tabs.forEach((tab) => {
298
+ tab.addEventListener('click', () => setTab(tab.dataset.tab));
299
+ });
300
+
301
+ if (${staticMode ? 'true' : 'false'}) {
302
+ try {
303
+ Object.assign(feedback, JSON.parse(localStorage.getItem('kodax-skill-review-feedback') || '{}'));
304
+ } catch {}
305
+ }
306
+
307
+ renderRunList();
308
+ renderOutputs();
309
+ renderBenchmark();
310
+ </script>
311
+ </body>
312
+ </html>`;
313
+ }
314
+
315
+ function parseArgs(argv) {
316
+ const args = {
317
+ workspace: argv[2],
318
+ skillName: 'unknown-skill',
319
+ benchmark: null,
320
+ staticOutput: null,
321
+ port: 4173,
322
+ };
323
+
324
+ for (let index = 3; index < argv.length; index += 1) {
325
+ const token = argv[index];
326
+ if (token === '--skill-name' && argv[index + 1]) {
327
+ args.skillName = argv[index + 1];
328
+ index += 1;
329
+ } else if (token === '--benchmark' && argv[index + 1]) {
330
+ args.benchmark = argv[index + 1];
331
+ index += 1;
332
+ } else if (token === '--static' && argv[index + 1]) {
333
+ args.staticOutput = argv[index + 1];
334
+ index += 1;
335
+ } else if (token === '--port' && argv[index + 1]) {
336
+ args.port = Number(argv[index + 1]);
337
+ index += 1;
338
+ }
339
+ }
340
+
341
+ return args;
342
+ }
343
+
344
+ export async function buildPayload(workspace, args) {
345
+ const benchmarkPath = args.benchmark ?? path.join(workspace, 'benchmark.json');
346
+
347
+ return {
348
+ skillName: args.skillName,
349
+ workspace: path.resolve(workspace),
350
+ benchmark: await readJson(benchmarkPath, null),
351
+ feedback: await readJson(path.join(workspace, 'feedback.json'), {}),
352
+ runs: await findRuns(path.resolve(workspace)),
353
+ };
354
+ }
355
+
356
+ async function main() {
357
+ const args = parseArgs(process.argv);
358
+ if (!args.workspace) {
359
+ console.error('Usage: node scripts/generate-review.js <workspace> [--skill-name name] [--benchmark file] [--static output.html] [--port 4173]');
360
+ process.exit(1);
361
+ }
362
+
363
+ const payload = await buildPayload(args.workspace, args);
364
+
365
+ if (args.staticOutput) {
366
+ await writeFile(args.staticOutput, renderHtml(payload, true));
367
+ console.log(`Wrote ${args.staticOutput}`);
368
+ return;
369
+ }
370
+
371
+ const server = createServer(async (request, response) => {
372
+ if (!request.url || request.method === 'GET' && request.url === '/') {
373
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
374
+ response.end(renderHtml(payload, false));
375
+ return;
376
+ }
377
+
378
+ if (request.method === 'POST' && request.url === '/feedback') {
379
+ let body = '';
380
+ request.on('data', (chunk) => {
381
+ body += chunk;
382
+ });
383
+ request.on('end', async () => {
384
+ try {
385
+ const parsed = JSON.parse(body);
386
+ payload.feedback = parsed;
387
+ await writeFile(path.join(args.workspace, 'feedback.json'), JSON.stringify(parsed, null, 2));
388
+ response.writeHead(204);
389
+ response.end();
390
+ } catch {
391
+ response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
392
+ response.end('Invalid feedback payload');
393
+ }
394
+ });
395
+ return;
396
+ }
397
+
398
+ response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
399
+ response.end('Not found');
400
+ });
401
+
402
+ server.listen(args.port, () => {
403
+ console.log(`Review server running at http://127.0.0.1:${args.port}`);
404
+ });
405
+ }
406
+
407
407
  const isDirectRun = process.env.KODAX_MODULE_BUNDLE !== 'true'
408
408
  && process.argv[1]
409
- && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
410
-
411
- if (isDirectRun) {
412
- main().catch((error) => {
413
- console.error(error instanceof Error ? error.message : String(error));
414
- process.exit(1);
415
- });
416
- }
409
+ && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
410
+
411
+ if (isDirectRun) {
412
+ main().catch((error) => {
413
+ console.error(error instanceof Error ? error.message : String(error));
414
+ process.exit(1);
415
+ });
416
+ }