@pinta-ai/pinta-codex 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.codex-plugin/plugin.json +40 -0
  2. package/CHANGELOG.md +50 -0
  3. package/LICENSE +136 -0
  4. package/README.md +214 -0
  5. package/dist/core/config.d.ts +10 -0
  6. package/dist/core/config.js +100 -0
  7. package/dist/core/config.js.map +1 -0
  8. package/dist/core/identity.d.ts +1 -0
  9. package/dist/core/identity.js +3 -0
  10. package/dist/core/identity.js.map +1 -0
  11. package/dist/core/otlp.d.ts +57 -0
  12. package/dist/core/otlp.js +182 -0
  13. package/dist/core/otlp.js.map +1 -0
  14. package/dist/core/redact.d.ts +57 -0
  15. package/dist/core/redact.js +149 -0
  16. package/dist/core/redact.js.map +1 -0
  17. package/dist/core/retry-queue.d.ts +26 -0
  18. package/dist/core/retry-queue.js +127 -0
  19. package/dist/core/retry-queue.js.map +1 -0
  20. package/dist/core/trace.d.ts +10 -0
  21. package/dist/core/trace.js +61 -0
  22. package/dist/core/trace.js.map +1 -0
  23. package/dist/core/transport.d.ts +10 -0
  24. package/dist/core/transport.js +77 -0
  25. package/dist/core/transport.js.map +1 -0
  26. package/dist/core/types.d.ts +48 -0
  27. package/dist/core/types.js +45 -0
  28. package/dist/core/types.js.map +1 -0
  29. package/dist/handlers/default.d.ts +6 -0
  30. package/dist/handlers/default.js +11 -0
  31. package/dist/handlers/default.js.map +1 -0
  32. package/dist/handlers/post-tool-use.d.ts +3 -0
  33. package/dist/handlers/post-tool-use.js +15 -0
  34. package/dist/handlers/post-tool-use.js.map +1 -0
  35. package/dist/handlers/pre-tool-use.d.ts +3 -0
  36. package/dist/handlers/pre-tool-use.js +15 -0
  37. package/dist/handlers/pre-tool-use.js.map +1 -0
  38. package/dist/handlers/session.d.ts +3 -0
  39. package/dist/handlers/session.js +15 -0
  40. package/dist/handlers/session.js.map +1 -0
  41. package/dist/handlers/stop.d.ts +3 -0
  42. package/dist/handlers/stop.js +15 -0
  43. package/dist/handlers/stop.js.map +1 -0
  44. package/dist/handlers/user-prompt.d.ts +3 -0
  45. package/dist/handlers/user-prompt.js +15 -0
  46. package/dist/handlers/user-prompt.js.map +1 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.js +53 -0
  49. package/dist/index.js.map +1 -0
  50. package/hooks.json +54 -0
  51. package/package.json +43 -0
  52. package/tools/.data/events.json +1585 -0
  53. package/tools/_lib.ts +274 -0
  54. package/tools/doctor.ts +250 -0
  55. package/tools/install-hooks.ts +71 -0
  56. package/tools/mock-server.ts +436 -0
  57. package/tools/setup.ts +288 -0
  58. package/tools/test-otlp.ts +58 -0
  59. package/tools/test-redact.ts +286 -0
  60. package/tools/tsconfig.json +13 -0
  61. package/tools/uninstall-hooks.ts +39 -0
@@ -0,0 +1,436 @@
1
+ import http from "http";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const PORT = 3000;
6
+ const DATA_DIR = path.join(__dirname, ".data");
7
+ const EVENTS_FILE = path.join(DATA_DIR, "events.json");
8
+
9
+ // --- Storage ---
10
+
11
+ function loadEvents(): unknown[] {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(EVENTS_FILE, "utf-8"));
14
+ } catch {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function saveEvent(event: unknown): void {
20
+ fs.mkdirSync(DATA_DIR, { recursive: true });
21
+ const events = loadEvents();
22
+ events.push(event);
23
+ fs.writeFileSync(EVENTS_FILE, JSON.stringify(events, null, 2));
24
+ }
25
+
26
+ function log(label: string, data: unknown) {
27
+ const timestamp = new Date().toISOString();
28
+ console.log(`\n[${timestamp}] ${label}`);
29
+ console.log(JSON.stringify(data, null, 2));
30
+ }
31
+
32
+ // --- HTML UI ---
33
+
34
+ const HTML = `<!DOCTYPE html>
35
+ <html lang="ko">
36
+ <head>
37
+ <meta charset="UTF-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
39
+ <title>Mock OTLP Collector</title>
40
+ <style>
41
+ * { margin: 0; padding: 0; box-sizing: border-box; }
42
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; }
43
+ .layout { display: flex; height: 100vh; }
44
+ .list-pane { flex: 1; overflow-y: auto; padding: 20px; border-right: 1px solid #21262d; min-width: 0; }
45
+ .detail-pane { width: 480px; flex-shrink: 0; overflow-y: auto; background: #161b22; display: none; }
46
+ .detail-pane.open { display: block; }
47
+ .detail-header { padding: 16px; border-bottom: 1px solid #21262d; display: flex; justify-content: space-between; align-items: center; }
48
+ .detail-header h3 { font-size: 14px; color: #58a6ff; }
49
+ .detail-close { background: none; border: none; color: #8b949e; cursor: pointer; font-size: 18px; }
50
+ .detail-close:hover { color: #c9d1d9; }
51
+ .detail-body { padding: 16px; }
52
+ .detail-section { margin-bottom: 16px; }
53
+ .detail-section h4 { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
54
+ .detail-row { display: flex; padding: 4px 0; font-size: 13px; }
55
+ .detail-label { color: #8b949e; width: 120px; flex-shrink: 0; }
56
+ .detail-value { color: #c9d1d9; word-break: break-all; }
57
+ .detail-value.type { font-weight: 600; }
58
+ .detail-value.PreToolUse { color: #d29922; }
59
+ .detail-value.PostToolUse { color: #3fb950; }
60
+ .detail-value.UserPromptSubmit { color: #bc8cff; }
61
+ .detail-value.SessionStart { color: #58a6ff; }
62
+ .detail-value.SessionEnd { color: #8b949e; }
63
+ .detail-json { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 12px; max-height: 300px; overflow-y: auto; }
64
+ .detail-json pre { font-size: 12px; color: #8b949e; white-space: pre-wrap; word-break: break-all; }
65
+ .detail-prompt { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 12px; font-size: 13px; color: #c9d1d9; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }
66
+ .detail-tabs { display: flex; border-bottom: 1px solid #21262d; padding: 0 16px; }
67
+ .detail-tab { padding: 8px 12px; font-size: 13px; color: #8b949e; cursor: pointer; border-bottom: 2px solid transparent; }
68
+ .detail-tab:hover { color: #c9d1d9; }
69
+ .detail-tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }
70
+ h1 { font-size: 20px; margin-bottom: 16px; color: #58a6ff; }
71
+ .toolbar { display: flex; gap: 12px; margin-bottom: 20px; align-items: center; }
72
+ .toolbar button { background: #21262d; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
73
+ .toolbar button:hover { background: #30363d; }
74
+ .toolbar select { background: #21262d; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 10px; border-radius: 6px; font-size: 13px; }
75
+ .stats { font-size: 13px; color: #8b949e; margin-left: auto; }
76
+ .session { margin-bottom: 24px; }
77
+ .session-header { font-size: 14px; color: #8b949e; padding: 8px 12px; background: #161b22; border-radius: 6px 6px 0 0; border: 1px solid #21262d; }
78
+ .trace { margin: 0; border-left: 3px solid #30363d; border-right: 1px solid #21262d; }
79
+ .trace-header { font-size: 13px; color: #58a6ff; padding: 8px 12px; background: #161b22; cursor: pointer; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid #21262d; }
80
+ .trace-header:hover { background: #1c2129; }
81
+ .trace-header .arrow { transition: transform 0.2s; font-size: 10px; }
82
+ .trace-header .arrow.open { transform: rotate(90deg); }
83
+ .trace-header .time { color: #8b949e; font-size: 12px; margin-left: auto; }
84
+ .event { padding: 10px 12px; border-bottom: 1px solid #21262d; font-size: 13px; display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
85
+ .event:hover { background: #1c2129; }
86
+ .event.selected { background: #1c2129; border-left: 2px solid #58a6ff; padding-left: 10px; }
87
+ .event-type { font-weight: 600; min-width: 160px; flex-shrink: 0; }
88
+ .event-type.PreToolUse { color: #d29922; }
89
+ .event-type.PostToolUse { color: #3fb950; }
90
+ .event-type.UserPromptSubmit { color: #bc8cff; }
91
+ .event-type.SessionStart { color: #58a6ff; }
92
+ .event-type.SessionEnd { color: #8b949e; }
93
+ .event-type.blocked { color: #f85149; }
94
+ .event-detail { color: #8b949e; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
95
+ .event-time { color: #484f58; font-size: 12px; flex-shrink: 0; }
96
+ .empty { text-align: center; padding: 60px; color: #484f58; }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <div class="layout">
101
+ <div class="list-pane">
102
+ <h1>Pinta Event Viewer</h1>
103
+ <div class="toolbar">
104
+ <button onclick="refresh()">Refresh</button>
105
+ <button onclick="clearEvents()">Clear</button>
106
+ <select id="filter" onchange="refresh()">
107
+ <option value="">All Events</option>
108
+ <option value="UserPromptSubmit">UserPromptSubmit</option>
109
+ <option value="PreToolUse">PreToolUse</option>
110
+ <option value="PostToolUse">PostToolUse</option>
111
+ <option value="SessionStart">SessionStart</option>
112
+ <option value="SessionEnd">SessionEnd</option>
113
+ </select>
114
+ <label style="font-size:13px"><input type="checkbox" id="autoRefresh" checked onchange="toggleAutoRefresh()"> Auto-refresh</label>
115
+ <span class="stats" id="stats"></span>
116
+ </div>
117
+ <div id="app"></div>
118
+ </div>
119
+ <div class="detail-pane" id="detailPane">
120
+ <div class="detail-header">
121
+ <h3 id="detailTitle">Event Detail</h3>
122
+ <button class="detail-close" onclick="closeDetail()">&times;</button>
123
+ </div>
124
+ <div class="detail-tabs">
125
+ <div class="detail-tab active" data-tab="overview" onclick="switchTab('overview')">Overview</div>
126
+ <div class="detail-tab" data-tab="payload" onclick="switchTab('payload')">Payload</div>
127
+ <div class="detail-tab" data-tab="raw" onclick="switchTab('raw')">Raw JSON</div>
128
+ </div>
129
+ <div class="detail-body" id="detailBody"></div>
130
+ </div>
131
+ </div>
132
+
133
+ <script>
134
+ let autoRefreshTimer = null;
135
+ let allEvents = [];
136
+ let selectedEventId = null;
137
+ let currentTab = 'overview';
138
+
139
+ async function fetchEvents() {
140
+ const res = await fetch('/api/events/list');
141
+ return res.json();
142
+ }
143
+
144
+ async function clearEvents() {
145
+ await fetch('/api/events/clear', { method: 'POST' });
146
+ closeDetail();
147
+ refresh();
148
+ }
149
+
150
+ function toggleAutoRefresh() {
151
+ if (document.getElementById('autoRefresh').checked) {
152
+ autoRefreshTimer = setInterval(refresh, 2000);
153
+ } else {
154
+ clearInterval(autoRefreshTimer);
155
+ }
156
+ }
157
+
158
+ function formatTime(ts) {
159
+ const d = new Date(ts);
160
+ return d.toLocaleTimeString('ko-KR', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0');
161
+ }
162
+
163
+ function formatFullTime(ts) {
164
+ const d = new Date(ts);
165
+ return d.toLocaleDateString('ko-KR') + ' ' + d.toLocaleTimeString('ko-KR', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0');
166
+ }
167
+
168
+ function escapeHtml(s) {
169
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
170
+ }
171
+
172
+ function summarize(ev) {
173
+ const p = ev.payload || {};
174
+ if (ev.eventType === 'UserPromptSubmit') return p.prompt ? p.prompt.slice(0, 120) : '';
175
+ if (ev.eventType === 'PreToolUse' || ev.eventType === 'PostToolUse') {
176
+ const tool = ev.toolName || p.tool_name || '';
177
+ const input = p.tool_input || {};
178
+ if (input.command) return tool + ': ' + input.command.slice(0, 100);
179
+ if (input.file_path) return tool + ': ' + input.file_path;
180
+ if (input.pattern) return tool + ': ' + input.pattern;
181
+ if (input.query) return tool + ': ' + input.query;
182
+ if (input.prompt) return tool + ': ' + input.prompt.slice(0, 80);
183
+ return tool;
184
+ }
185
+ return ev.eventType;
186
+ }
187
+
188
+ function closeDetail() {
189
+ document.getElementById('detailPane').classList.remove('open');
190
+ selectedEventId = null;
191
+ document.querySelectorAll('.event.selected').forEach(el => el.classList.remove('selected'));
192
+ }
193
+
194
+ function switchTab(tab) {
195
+ currentTab = tab;
196
+ document.querySelectorAll('.detail-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
197
+ if (selectedEventId !== null) showDetail(selectedEventId);
198
+ }
199
+
200
+ function showDetail(idx) {
201
+ const ev = allEvents[idx];
202
+ if (!ev) return;
203
+ selectedEventId = idx;
204
+ const pane = document.getElementById('detailPane');
205
+ pane.classList.add('open');
206
+
207
+ document.querySelectorAll('.event.selected').forEach(el => el.classList.remove('selected'));
208
+ const el = document.getElementById('ev-' + idx);
209
+ if (el) el.classList.add('selected');
210
+
211
+ document.getElementById('detailTitle').textContent = ev.eventType + (ev.toolName ? ' - ' + ev.toolName : '');
212
+
213
+ const body = document.getElementById('detailBody');
214
+ const p = ev.payload || {};
215
+
216
+ if (currentTab === 'overview') {
217
+ let html = '<div class="detail-section"><h4>Event Info</h4>';
218
+ html += row('Type', '<span class="detail-value type ' + ev.eventType + '">' + ev.eventType + '</span>');
219
+ html += row('Time', formatFullTime(ev.timestamp));
220
+ html += row('Event ID', ev.eventId);
221
+ html += row('Trace ID', ev.traceId || '-');
222
+ html += row('Session ID', ev.sessionId);
223
+ if (ev.toolName) html += row('Tool', ev.toolName);
224
+ if (p.tool_use_id) html += row('Tool Use ID', p.tool_use_id);
225
+ html += row('CWD', p.cwd || '-');
226
+ html += row('Permission', p.permission_mode || '-');
227
+ html += '</div>';
228
+
229
+ // Context-specific sections
230
+ if (ev.eventType === 'UserPromptSubmit' && p.prompt) {
231
+ html += '<div class="detail-section"><h4>User Prompt</h4>';
232
+ html += '<div class="detail-prompt">' + escapeHtml(p.prompt) + '</div></div>';
233
+ }
234
+
235
+ if ((ev.eventType === 'PreToolUse' || ev.eventType === 'PostToolUse') && p.tool_input) {
236
+ html += '<div class="detail-section"><h4>Tool Input</h4>';
237
+ html += '<div class="detail-json"><pre>' + escapeHtml(JSON.stringify(p.tool_input, null, 2)) + '</pre></div></div>';
238
+ }
239
+
240
+ if (ev.eventType === 'PostToolUse' && p.tool_response !== undefined) {
241
+ html += '<div class="detail-section"><h4>Tool Response</h4>';
242
+ const resp = typeof p.tool_response === 'string' ? p.tool_response : JSON.stringify(p.tool_response, null, 2);
243
+ html += '<div class="detail-json"><pre>' + escapeHtml(resp) + '</pre></div></div>';
244
+ }
245
+
246
+ body.innerHTML = html;
247
+ } else if (currentTab === 'payload') {
248
+ body.innerHTML = '<div class="detail-section"><h4>Payload</h4><div class="detail-json"><pre>' + escapeHtml(JSON.stringify(p, null, 2)) + '</pre></div></div>';
249
+ } else {
250
+ body.innerHTML = '<div class="detail-section"><h4>Full Event</h4><div class="detail-json"><pre>' + escapeHtml(JSON.stringify(ev, null, 2)) + '</pre></div></div>';
251
+ }
252
+ }
253
+
254
+ function row(label, value) {
255
+ return '<div class="detail-row"><span class="detail-label">' + label + '</span><span class="detail-value">' + value + '</span></div>';
256
+ }
257
+
258
+ function toggleTrace(id) {
259
+ const el = document.getElementById('trace-body-' + id);
260
+ const arrow = document.getElementById('arrow-' + id);
261
+ if (el.style.display === 'none') {
262
+ el.style.display = 'block';
263
+ arrow.classList.add('open');
264
+ } else {
265
+ el.style.display = 'none';
266
+ arrow.classList.remove('open');
267
+ }
268
+ }
269
+
270
+ async function refresh() {
271
+ const events = await fetchEvents();
272
+ allEvents = events;
273
+ const filter = document.getElementById('filter').value;
274
+ const filtered = filter ? events.filter(e => e.eventType === filter) : events;
275
+
276
+ document.getElementById('stats').textContent = filtered.length + ' events';
277
+
278
+ const sessions = new Map();
279
+ for (const ev of filtered) {
280
+ const sid = ev.sessionId || 'unknown';
281
+ if (!sessions.has(sid)) sessions.set(sid, new Map());
282
+ const traces = sessions.get(sid);
283
+ const tid = ev.traceId || 'no-trace';
284
+ if (!traces.has(tid)) traces.set(tid, []);
285
+ traces.get(tid).push({ ...ev, _idx: events.indexOf(ev) });
286
+ }
287
+
288
+ if (filtered.length === 0) {
289
+ document.getElementById('app').innerHTML = '<div class="empty">No events yet. Start using Claude Code with the Pinta plugin.</div>';
290
+ return;
291
+ }
292
+
293
+ let html = '';
294
+ let traceNum = 0;
295
+ const sessionEntries = [...sessions.entries()].reverse();
296
+ for (const [sid, traces] of sessionEntries) {
297
+ html += '<div class="session">';
298
+ html += '<div class="session-header">Session: ' + sid.slice(0, 12) + '...</div>';
299
+
300
+ const traceEntries = [...traces.entries()].reverse();
301
+ for (const [tid, tevents] of traceEntries) {
302
+ const traceIdx = traceNum++;
303
+ const firstTime = tevents[0]?.timestamp ? formatTime(tevents[0].timestamp) : '';
304
+ const evCount = tevents.length;
305
+ const hasPrompt = tevents.find(e => e.eventType === 'UserPromptSubmit');
306
+ const promptPreview = hasPrompt ? summarize(hasPrompt).slice(0, 60) : tid.slice(0, 12);
307
+
308
+ html += '<div class="trace">';
309
+ html += '<div class="trace-header" onclick="toggleTrace(' + traceIdx + ')">';
310
+ html += '<span class="arrow" id="arrow-' + traceIdx + '">&#9654;</span>';
311
+ html += '<strong>' + escapeHtml(promptPreview) + '</strong>';
312
+ html += '<span style="color:#484f58;font-size:12px">' + evCount + ' events</span>';
313
+ html += '<span class="time">' + firstTime + '</span>';
314
+ html += '</div>';
315
+ html += '<div id="trace-body-' + traceIdx + '" style="display:none">';
316
+
317
+ for (const ev of tevents) {
318
+ const globalIdx = ev._idx;
319
+ const sel = selectedEventId === globalIdx ? ' selected' : '';
320
+ html += '<div class="event' + sel + '" id="ev-' + globalIdx + '" onclick="showDetail(' + globalIdx + ')">';
321
+ html += '<span class="event-type ' + ev.eventType + '">' + ev.eventType + '</span>';
322
+ html += '<span class="event-detail">' + escapeHtml(summarize(ev)) + '</span>';
323
+ html += '<span class="event-time">' + (ev.timestamp ? formatTime(ev.timestamp) : '') + '</span>';
324
+ html += '</div>';
325
+ }
326
+
327
+ html += '</div></div>';
328
+ }
329
+ html += '</div>';
330
+ }
331
+
332
+ document.getElementById('app').innerHTML = html;
333
+
334
+ if (selectedEventId !== null) showDetail(selectedEventId);
335
+ }
336
+
337
+ refresh();
338
+ toggleAutoRefresh();
339
+ </script>
340
+ </body>
341
+ </html>`;
342
+
343
+ // --- OTLP helpers ---
344
+
345
+ function toViewerEvent(rs: any): Record<string, unknown> | null {
346
+ const span = rs?.scopeSpans?.[0]?.spans?.[0];
347
+ if (!span) return null;
348
+ const attrs: Record<string, unknown> = {};
349
+ for (const a of span.attributes ?? []) {
350
+ const v = a.value ?? {};
351
+ attrs[a.key] = v.stringValue ?? v.intValue ?? v.boolValue ?? v.doubleValue ?? null;
352
+ }
353
+ // Re-hydrate the original hook event shape under `payload` so the UI's summarize() works.
354
+ const payload: Record<string, unknown> = {};
355
+ for (const [k, v] of Object.entries(attrs)) {
356
+ if (!k.startsWith("codex.")) continue;
357
+ const key = k.slice(6);
358
+ // Try to JSON.parse object/array fields back so the viewer can drill in.
359
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
360
+ try { payload[key] = JSON.parse(v); continue; } catch { /* fall through */ }
361
+ }
362
+ payload[key] = v;
363
+ }
364
+ return {
365
+ eventId: span.spanId,
366
+ traceId: span.traceId,
367
+ timestamp: new Date(Number(BigInt(span.startTimeUnixNano) / 1_000_000n)).toISOString(),
368
+ sessionId: payload.session_id,
369
+ eventType: payload.hook ?? span.name,
370
+ toolName: payload.tool_name,
371
+ payload,
372
+ };
373
+ }
374
+
375
+ // --- Server ---
376
+
377
+ const server = http.createServer((req, res) => {
378
+ // UI - no auth required
379
+ if (req.method === "GET" && (req.url === "/" || req.url === "/ui")) {
380
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
381
+ res.end(HTML);
382
+ return;
383
+ }
384
+
385
+ // Events list API - no auth required (dev tool)
386
+ if (req.method === "GET" && req.url === "/api/events/list") {
387
+ res.writeHead(200, { "Content-Type": "application/json" });
388
+ res.end(JSON.stringify(loadEvents()));
389
+ return;
390
+ }
391
+
392
+ // Clear events - no auth required (dev tool)
393
+ if (req.method === "POST" && req.url === "/api/events/clear") {
394
+ fs.mkdirSync(DATA_DIR, { recursive: true });
395
+ fs.writeFileSync(EVENTS_FILE, "[]");
396
+ res.writeHead(200, { "Content-Type": "application/json" });
397
+ res.end(JSON.stringify({ cleared: true }));
398
+ log("EVENTS CLEARED", {});
399
+ return;
400
+ }
401
+
402
+ // POST /traces (OTLP)
403
+ if (req.method === "POST" && req.url === "/traces") {
404
+ let data = "";
405
+ req.on("data", (chunk) => (data += chunk));
406
+ req.on("end", () => {
407
+ try {
408
+ const body = JSON.parse(data);
409
+ const resourceSpans: unknown[] = Array.isArray(body?.resourceSpans) ? body.resourceSpans : [];
410
+ for (const rs of resourceSpans) {
411
+ // Persist a flattened, viewer-friendly record per span so the existing UI keeps working.
412
+ const flat = toViewerEvent(rs);
413
+ if (flat) saveEvent(flat);
414
+ }
415
+ log(`TRACES batch=${resourceSpans.length}`, { count: resourceSpans.length });
416
+ res.writeHead(200, { "Content-Type": "application/json" });
417
+ res.end(JSON.stringify({ partialSuccess: {} }));
418
+ } catch (err) {
419
+ res.writeHead(400, { "Content-Type": "application/json" });
420
+ res.end(JSON.stringify({ error: "Invalid JSON", detail: String(err) }));
421
+ }
422
+ });
423
+ return;
424
+ }
425
+
426
+ res.writeHead(404);
427
+ res.end();
428
+ });
429
+
430
+ server.listen(PORT, () => {
431
+ console.log(`\n📦 Mock OTLP Collector running on http://localhost:${PORT}`);
432
+ console.log(`\n UI: http://localhost:${PORT}/`);
433
+ console.log(` Traces: POST http://localhost:${PORT}/traces`);
434
+ console.log(` Events: http://localhost:${PORT}/api/events/list`);
435
+ console.log(`\n Waiting for events...\n`);
436
+ });