@fml-inc/panopticon 0.1.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 (124) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +5 -0
  3. package/README.md +363 -0
  4. package/bin/hook-handler +3 -0
  5. package/bin/mcp-server +3 -0
  6. package/bin/panopticon +3 -0
  7. package/bin/proxy +3 -0
  8. package/bin/server +3 -0
  9. package/dist/api/client.d.ts +67 -0
  10. package/dist/api/client.js +48 -0
  11. package/dist/api/client.js.map +1 -0
  12. package/dist/chunk-3BUJ7URA.js +387 -0
  13. package/dist/chunk-3BUJ7URA.js.map +1 -0
  14. package/dist/chunk-3TZAKV3M.js +158 -0
  15. package/dist/chunk-3TZAKV3M.js.map +1 -0
  16. package/dist/chunk-4SM2H22C.js +169 -0
  17. package/dist/chunk-4SM2H22C.js.map +1 -0
  18. package/dist/chunk-7Q3BJMLG.js +62 -0
  19. package/dist/chunk-7Q3BJMLG.js.map +1 -0
  20. package/dist/chunk-BVOE7A2Z.js +412 -0
  21. package/dist/chunk-BVOE7A2Z.js.map +1 -0
  22. package/dist/chunk-CF4GPWLI.js +170 -0
  23. package/dist/chunk-CF4GPWLI.js.map +1 -0
  24. package/dist/chunk-DZ5HJFB4.js +467 -0
  25. package/dist/chunk-DZ5HJFB4.js.map +1 -0
  26. package/dist/chunk-HQCY722C.js +428 -0
  27. package/dist/chunk-HQCY722C.js.map +1 -0
  28. package/dist/chunk-HRCEIYKU.js +134 -0
  29. package/dist/chunk-HRCEIYKU.js.map +1 -0
  30. package/dist/chunk-K7YUPLES.js +76 -0
  31. package/dist/chunk-K7YUPLES.js.map +1 -0
  32. package/dist/chunk-L7G27XWF.js +130 -0
  33. package/dist/chunk-L7G27XWF.js.map +1 -0
  34. package/dist/chunk-LWXF7YRG.js +626 -0
  35. package/dist/chunk-LWXF7YRG.js.map +1 -0
  36. package/dist/chunk-NXH7AONS.js +1120 -0
  37. package/dist/chunk-NXH7AONS.js.map +1 -0
  38. package/dist/chunk-QK5442ZP.js +55 -0
  39. package/dist/chunk-QK5442ZP.js.map +1 -0
  40. package/dist/chunk-QVK6VGCV.js +1703 -0
  41. package/dist/chunk-QVK6VGCV.js.map +1 -0
  42. package/dist/chunk-RX2RXHBH.js +1699 -0
  43. package/dist/chunk-RX2RXHBH.js.map +1 -0
  44. package/dist/chunk-SEXU2WYG.js +788 -0
  45. package/dist/chunk-SEXU2WYG.js.map +1 -0
  46. package/dist/chunk-SUGSQ4YI.js +264 -0
  47. package/dist/chunk-SUGSQ4YI.js.map +1 -0
  48. package/dist/chunk-TGXFVAID.js +138 -0
  49. package/dist/chunk-TGXFVAID.js.map +1 -0
  50. package/dist/chunk-WLBNFVIG.js +447 -0
  51. package/dist/chunk-WLBNFVIG.js.map +1 -0
  52. package/dist/chunk-XLTCUH5A.js +1072 -0
  53. package/dist/chunk-XLTCUH5A.js.map +1 -0
  54. package/dist/chunk-YVRWVDIA.js +146 -0
  55. package/dist/chunk-YVRWVDIA.js.map +1 -0
  56. package/dist/chunk-ZEC4LRKS.js +176 -0
  57. package/dist/chunk-ZEC4LRKS.js.map +1 -0
  58. package/dist/cli.d.ts +1 -0
  59. package/dist/cli.js +1084 -0
  60. package/dist/cli.js.map +1 -0
  61. package/dist/config-NwoZC-GM.d.ts +20 -0
  62. package/dist/db.d.ts +46 -0
  63. package/dist/db.js +15 -0
  64. package/dist/db.js.map +1 -0
  65. package/dist/doctor.d.ts +37 -0
  66. package/dist/doctor.js +14 -0
  67. package/dist/doctor.js.map +1 -0
  68. package/dist/hooks/handler.d.ts +23 -0
  69. package/dist/hooks/handler.js +295 -0
  70. package/dist/hooks/handler.js.map +1 -0
  71. package/dist/index.d.ts +57 -0
  72. package/dist/index.js +101 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/mcp/server.d.ts +1 -0
  75. package/dist/mcp/server.js +243 -0
  76. package/dist/mcp/server.js.map +1 -0
  77. package/dist/otlp/server.d.ts +7 -0
  78. package/dist/otlp/server.js +17 -0
  79. package/dist/otlp/server.js.map +1 -0
  80. package/dist/permissions.d.ts +33 -0
  81. package/dist/permissions.js +14 -0
  82. package/dist/permissions.js.map +1 -0
  83. package/dist/pricing.d.ts +29 -0
  84. package/dist/pricing.js +13 -0
  85. package/dist/pricing.js.map +1 -0
  86. package/dist/proxy/server.d.ts +10 -0
  87. package/dist/proxy/server.js +20 -0
  88. package/dist/proxy/server.js.map +1 -0
  89. package/dist/prune.d.ts +18 -0
  90. package/dist/prune.js +13 -0
  91. package/dist/prune.js.map +1 -0
  92. package/dist/query.d.ts +56 -0
  93. package/dist/query.js +27 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/reparse-636YZCE3.js +14 -0
  96. package/dist/reparse-636YZCE3.js.map +1 -0
  97. package/dist/repo.d.ts +17 -0
  98. package/dist/repo.js +9 -0
  99. package/dist/repo.js.map +1 -0
  100. package/dist/scanner.d.ts +73 -0
  101. package/dist/scanner.js +15 -0
  102. package/dist/scanner.js.map +1 -0
  103. package/dist/sdk.d.ts +82 -0
  104. package/dist/sdk.js +208 -0
  105. package/dist/sdk.js.map +1 -0
  106. package/dist/server.d.ts +5 -0
  107. package/dist/server.js +25 -0
  108. package/dist/server.js.map +1 -0
  109. package/dist/setup.d.ts +35 -0
  110. package/dist/setup.js +19 -0
  111. package/dist/setup.js.map +1 -0
  112. package/dist/sync/index.d.ts +29 -0
  113. package/dist/sync/index.js +32 -0
  114. package/dist/sync/index.js.map +1 -0
  115. package/dist/targets.d.ts +279 -0
  116. package/dist/targets.js +20 -0
  117. package/dist/targets.js.map +1 -0
  118. package/dist/types-D-MYCBol.d.ts +128 -0
  119. package/dist/types.d.ts +164 -0
  120. package/dist/types.js +1 -0
  121. package/dist/types.js.map +1 -0
  122. package/hooks/hooks.json +274 -0
  123. package/package.json +124 -0
  124. package/skills/panopticon-optimize/SKILL.md +222 -0
@@ -0,0 +1,447 @@
1
+ import {
2
+ handleOtlpRequest
3
+ } from "./chunk-XLTCUH5A.js";
4
+ import {
5
+ handleProxyRequest,
6
+ processHookEvent,
7
+ tunnelWebSocket
8
+ } from "./chunk-RX2RXHBH.js";
9
+ import {
10
+ addTarget,
11
+ createSyncLoop,
12
+ listTargets,
13
+ removeTarget
14
+ } from "./chunk-HQCY722C.js";
15
+ import {
16
+ createScannerLoop
17
+ } from "./chunk-NXH7AONS.js";
18
+ import {
19
+ autoPrune,
20
+ pruneEstimate,
21
+ pruneExecute
22
+ } from "./chunk-HRCEIYKU.js";
23
+ import {
24
+ addBreadcrumb,
25
+ captureException,
26
+ flushSentry,
27
+ initSentry,
28
+ setTag
29
+ } from "./chunk-CF4GPWLI.js";
30
+ import {
31
+ log
32
+ } from "./chunk-7Q3BJMLG.js";
33
+ import {
34
+ refreshPricing
35
+ } from "./chunk-3TZAKV3M.js";
36
+ import {
37
+ TABLE_SYNC_REGISTRY,
38
+ readWatermark,
39
+ resetWatermarks,
40
+ watermarkKey,
41
+ writeWatermark
42
+ } from "./chunk-SEXU2WYG.js";
43
+ import {
44
+ loadUnifiedConfig
45
+ } from "./chunk-QK5442ZP.js";
46
+ import {
47
+ activitySummary,
48
+ costBreakdown,
49
+ dbStats,
50
+ listPlans,
51
+ listSessions,
52
+ print,
53
+ rawQuery,
54
+ search,
55
+ sessionTimeline
56
+ } from "./chunk-LWXF7YRG.js";
57
+ import {
58
+ getDb
59
+ } from "./chunk-DZ5HJFB4.js";
60
+ import {
61
+ config
62
+ } from "./chunk-K7YUPLES.js";
63
+
64
+ // src/server.ts
65
+ import fs from "fs";
66
+ import http from "http";
67
+
68
+ // src/api/routes.ts
69
+ var TOOLS = {
70
+ sessions: (p) => listSessions(p),
71
+ timeline: (p) => sessionTimeline(p),
72
+ costs: (p) => costBreakdown(p),
73
+ summary: (p) => activitySummary(p),
74
+ plans: (p) => listPlans(p),
75
+ search: (p) => search(p),
76
+ get: (p) => print(p),
77
+ query: (p) => rawQuery(p.sql),
78
+ status: () => dbStats()
79
+ };
80
+ var EXEC = {
81
+ prune: (p) => {
82
+ const cutoffMs = p.cutoffMs;
83
+ if (typeof cutoffMs !== "number") {
84
+ throw new Error("cutoffMs is required and must be a number");
85
+ }
86
+ if (p.dryRun) {
87
+ return pruneEstimate(cutoffMs);
88
+ }
89
+ const result = pruneExecute(cutoffMs);
90
+ if (p.vacuum) {
91
+ const db = getDb();
92
+ db.pragma("wal_checkpoint(TRUNCATE)");
93
+ db.exec("VACUUM");
94
+ }
95
+ return result;
96
+ },
97
+ "refresh-pricing": () => refreshPricing(),
98
+ "sync-reset": (p) => {
99
+ const target = p.target;
100
+ resetWatermarks(target);
101
+ return { ok: true, target: target ?? "all" };
102
+ },
103
+ "sync-watermark-get": (p) => {
104
+ const target = p.target;
105
+ if (!target) throw new Error("target is required");
106
+ const table = p.table;
107
+ if (table) {
108
+ return {
109
+ key: watermarkKey(table, target),
110
+ value: readWatermark(watermarkKey(table, target))
111
+ };
112
+ }
113
+ const watermarks = {};
114
+ for (const desc of TABLE_SYNC_REGISTRY) {
115
+ const key = watermarkKey(desc.table, target);
116
+ watermarks[desc.table] = readWatermark(key);
117
+ }
118
+ return { target, watermarks };
119
+ },
120
+ "sync-watermark-set": (p) => {
121
+ const target = p.target;
122
+ const table = p.table;
123
+ const value = p.value;
124
+ if (!target) throw new Error("target is required");
125
+ if (!table) throw new Error("table is required");
126
+ if (typeof value !== "number") throw new Error("value must be a number");
127
+ const key = watermarkKey(table, target);
128
+ writeWatermark(key, value);
129
+ return { key, value };
130
+ },
131
+ "sync-pending": (p) => {
132
+ const target = p.target;
133
+ if (!target) throw new Error("target is required");
134
+ const db = getDb();
135
+ const pending = {};
136
+ for (const desc of TABLE_SYNC_REGISTRY) {
137
+ const key = watermarkKey(desc.table, target);
138
+ const wm = readWatermark(key);
139
+ const maxId = db.prepare(
140
+ `SELECT MAX(${desc.table === "sessions" ? "sync_seq" : "id"}) as m FROM ${desc.table}`
141
+ ).get()?.m ?? 0;
142
+ const count = Math.max(0, maxId - wm);
143
+ if (count > 0) {
144
+ pending[desc.table] = { maxId, watermark: wm, pending: count };
145
+ }
146
+ }
147
+ const totalPending = Object.values(pending).reduce(
148
+ (s, v) => s + v.pending,
149
+ 0
150
+ );
151
+ return { target, totalPending, tables: pending };
152
+ },
153
+ "sync-target-list": () => {
154
+ return { targets: listTargets() };
155
+ },
156
+ "sync-target-add": (p) => {
157
+ const target = p;
158
+ if (!target.name) throw new Error("name is required");
159
+ if (!target.url) throw new Error("url is required");
160
+ addTarget(target);
161
+ return { ok: true, name: target.name, url: target.url };
162
+ },
163
+ "sync-target-remove": (p) => {
164
+ const name = p.name;
165
+ if (!name) throw new Error("name is required");
166
+ const removed = removeTarget(name);
167
+ return { ok: removed, name };
168
+ }
169
+ };
170
+ function collectBody(req) {
171
+ return new Promise((resolve, reject) => {
172
+ const chunks = [];
173
+ req.on("data", (chunk) => chunks.push(chunk));
174
+ req.on("end", () => resolve(Buffer.concat(chunks)));
175
+ req.on("error", reject);
176
+ });
177
+ }
178
+ function jsonResponse(res, status, data) {
179
+ res.writeHead(status, { "Content-Type": "application/json" });
180
+ res.end(JSON.stringify(data));
181
+ }
182
+ async function handleApiRequest(req, res) {
183
+ const url = req.url ?? "";
184
+ let body;
185
+ try {
186
+ const raw = await collectBody(req);
187
+ body = raw.length > 0 ? JSON.parse(raw.toString("utf-8")) : {};
188
+ } catch {
189
+ jsonResponse(res, 400, { error: "Invalid JSON body" });
190
+ return;
191
+ }
192
+ if (url === "/api/tool") {
193
+ const name = body.name;
194
+ if (!name || !(name in TOOLS)) {
195
+ jsonResponse(res, 404, {
196
+ error: `Unknown tool: ${name}`,
197
+ available: Object.keys(TOOLS)
198
+ });
199
+ return;
200
+ }
201
+ try {
202
+ const params = body.params ?? {};
203
+ const result = TOOLS[name](params);
204
+ jsonResponse(res, 200, result);
205
+ } catch (err) {
206
+ log.server.error(`API tool "${name}" error:`, err);
207
+ jsonResponse(res, 500, {
208
+ error: err instanceof Error ? err.message : String(err)
209
+ });
210
+ }
211
+ return;
212
+ }
213
+ if (url === "/api/exec") {
214
+ const command = body.command;
215
+ if (!command || !(command in EXEC)) {
216
+ jsonResponse(res, 404, {
217
+ error: `Unknown command: ${command}`,
218
+ available: Object.keys(EXEC)
219
+ });
220
+ return;
221
+ }
222
+ try {
223
+ const params = body.params ?? {};
224
+ const result = await EXEC[command](params);
225
+ jsonResponse(res, 200, result ?? { ok: true });
226
+ } catch (err) {
227
+ log.server.error(`API exec "${command}" error:`, err);
228
+ jsonResponse(res, 500, {
229
+ error: err instanceof Error ? err.message : String(err)
230
+ });
231
+ }
232
+ return;
233
+ }
234
+ jsonResponse(res, 404, { error: "Unknown API endpoint", url });
235
+ }
236
+
237
+ // src/db/sync-prune.ts
238
+ function minWatermarkForTable(table, targets) {
239
+ if (targets.length === 0) return 0;
240
+ let min = Infinity;
241
+ for (const t of targets) {
242
+ const wm = readWatermark(watermarkKey(table, t.name));
243
+ if (wm === 0) return 0;
244
+ if (wm < min) min = wm;
245
+ }
246
+ return min === Infinity ? 0 : min;
247
+ }
248
+ function syncAwarePrune(targets, retention) {
249
+ const result = {
250
+ hook_events: 0,
251
+ otel_logs: 0,
252
+ otel_metrics: 0
253
+ };
254
+ if (!retention.syncedMaxAgeDays || targets.length === 0) {
255
+ return result;
256
+ }
257
+ const cutoffMs = Date.now() - retention.syncedMaxAgeDays * 24 * 60 * 60 * 1e3;
258
+ const cutoffNs = cutoffMs * 1e6;
259
+ const db = getDb();
260
+ const tx = db.transaction(() => {
261
+ const hookMinWm = minWatermarkForTable("hook_events", targets);
262
+ if (hookMinWm > 0) {
263
+ db.prepare(
264
+ "DELETE FROM hook_events_fts WHERE rowid IN (SELECT id FROM hook_events WHERE id <= ? AND timestamp_ms < ?)"
265
+ ).run(hookMinWm, cutoffMs);
266
+ result.hook_events = db.prepare("DELETE FROM hook_events WHERE id <= ? AND timestamp_ms < ?").run(hookMinWm, cutoffMs).changes;
267
+ }
268
+ const logsMinWm = minWatermarkForTable("otel_logs", targets);
269
+ if (logsMinWm > 0) {
270
+ result.otel_logs = db.prepare("DELETE FROM otel_logs WHERE id <= ? AND timestamp_ns < ?").run(logsMinWm, cutoffNs).changes;
271
+ }
272
+ const metricsMinWm = minWatermarkForTable("otel_metrics", targets);
273
+ if (metricsMinWm > 0) {
274
+ result.otel_metrics = db.prepare("DELETE FROM otel_metrics WHERE id <= ? AND timestamp_ns < ?").run(metricsMinWm, cutoffNs).changes;
275
+ }
276
+ });
277
+ tx();
278
+ return result;
279
+ }
280
+
281
+ // src/server.ts
282
+ function collectBody2(req) {
283
+ return new Promise((resolve, reject) => {
284
+ const chunks = [];
285
+ req.on("data", (chunk) => chunks.push(chunk));
286
+ req.on("end", () => resolve(Buffer.concat(chunks)));
287
+ req.on("error", reject);
288
+ });
289
+ }
290
+ function createUnifiedServer() {
291
+ const server = http.createServer(async (req, res) => {
292
+ const url = req.url ?? "";
293
+ const method = req.method ?? "";
294
+ if (url === "/health" && method === "GET") {
295
+ res.writeHead(200, { "Content-Type": "application/json" });
296
+ res.end(JSON.stringify({ status: "ok", port: config.port }));
297
+ return;
298
+ }
299
+ if (url === "/hooks" && method === "POST") {
300
+ try {
301
+ const body = await collectBody2(req);
302
+ const data = JSON.parse(body.toString("utf-8"));
303
+ addBreadcrumb("hooks", `${data.hook_event_name ?? "unknown"} event`, {
304
+ session_id: data.session_id,
305
+ tool_name: data.tool_name,
306
+ target: data.target ?? data.source
307
+ });
308
+ const result = processHookEvent(data);
309
+ res.writeHead(200, { "Content-Type": "application/json" });
310
+ res.end(JSON.stringify(result));
311
+ } catch (err) {
312
+ log.hooks.error("Hook ingest error:", err);
313
+ captureException(err, { component: "hooks" });
314
+ if (!res.headersSent) {
315
+ res.writeHead(500, { "Content-Type": "application/json" });
316
+ res.end(JSON.stringify({ error: "hook ingest failed" }));
317
+ }
318
+ }
319
+ return;
320
+ }
321
+ if (method === "POST" && (url.startsWith("/v1/") || url === "/" || url === "")) {
322
+ await handleOtlpRequest(req, res);
323
+ return;
324
+ }
325
+ if (url.startsWith("/proxy/")) {
326
+ if (method !== "POST") {
327
+ res.writeHead(405);
328
+ res.end();
329
+ return;
330
+ }
331
+ addBreadcrumb("proxy", `Proxy ${url}`);
332
+ req.url = url.slice(6);
333
+ await handleProxyRequest(req, res);
334
+ return;
335
+ }
336
+ if (url.startsWith("/api/") && method === "POST") {
337
+ await handleApiRequest(req, res);
338
+ return;
339
+ }
340
+ res.writeHead(404, { "Content-Type": "application/json" });
341
+ res.end(JSON.stringify({ error: "not_found" }));
342
+ });
343
+ server.on("upgrade", (req, socket, head) => {
344
+ const url = req.url ?? "";
345
+ if (url.startsWith("/proxy/")) {
346
+ req.url = url.slice(6);
347
+ tunnelWebSocket(req, socket, head);
348
+ } else {
349
+ socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
350
+ }
351
+ });
352
+ return server;
353
+ }
354
+ var entryScript = process.argv[1]?.replaceAll("\\", "/") ?? "";
355
+ if (entryScript.endsWith("/server.js") || entryScript.endsWith("/server.ts")) {
356
+ let runPrune = function() {
357
+ try {
358
+ const cfg = loadUnifiedConfig();
359
+ addBreadcrumb("prune", "Running scheduled prune");
360
+ autoPrune(cfg.retention.maxAgeDays, cfg.retention.maxSizeMb);
361
+ if (cfg.sync.targets.length > 0 && cfg.retention.syncedMaxAgeDays) {
362
+ syncAwarePrune(cfg.sync.targets, cfg.retention);
363
+ }
364
+ } catch (err) {
365
+ log.server.error("Prune error:", err);
366
+ captureException(err, { component: "prune" });
367
+ }
368
+ };
369
+ runPrune2 = runPrune;
370
+ const PRUNE_INTERVAL_MS = 60 * 60 * 1e3;
371
+ const sentryActive = initSentry();
372
+ if (sentryActive) log.server.info("Sentry: enabled");
373
+ const server = createUnifiedServer();
374
+ let syncHandle = null;
375
+ let scannerHandle = null;
376
+ let pruneTimer = null;
377
+ let takeoverAttempted = false;
378
+ server.on("error", (err) => {
379
+ if (err.code === "EADDRINUSE") {
380
+ if (takeoverAttempted) {
381
+ log.server.warn(`Already running on ${config.host}:${config.port}`);
382
+ process.exit(0);
383
+ }
384
+ takeoverAttempted = true;
385
+ log.server.warn(`Port ${config.port} in use, attempting takeover...`);
386
+ try {
387
+ const pidFile = config.serverPidFile;
388
+ const pidStr = fs.readFileSync(pidFile, "utf-8").trim();
389
+ const oldPid = parseInt(pidStr, 10);
390
+ if (oldPid && oldPid !== process.pid) {
391
+ try {
392
+ process.kill(oldPid, "SIGTERM");
393
+ } catch {
394
+ }
395
+ }
396
+ setTimeout(() => server.listen(config.port, config.host), 1500);
397
+ } catch {
398
+ log.server.warn(`Already running on ${config.host}:${config.port}`);
399
+ process.exit(0);
400
+ }
401
+ return;
402
+ }
403
+ captureException(err, { component: "server" });
404
+ throw err;
405
+ });
406
+ server.listen(config.port, config.host, () => {
407
+ log.server.info(`Listening on ${config.host}:${config.port}`);
408
+ const cfg = loadUnifiedConfig();
409
+ scannerHandle = createScannerLoop({
410
+ onReady: () => {
411
+ if (cfg.sync.targets.length > 0) {
412
+ log.sync.info(
413
+ `Targets: ${cfg.sync.targets.map((t) => t.name).join(", ")}`
414
+ );
415
+ setTag("sync_targets", cfg.sync.targets.length);
416
+ syncHandle = createSyncLoop({
417
+ targets: cfg.sync.targets,
418
+ filter: cfg.sync.filter
419
+ });
420
+ syncHandle.start();
421
+ }
422
+ }
423
+ });
424
+ scannerHandle.start();
425
+ runPrune();
426
+ pruneTimer = setInterval(runPrune, PRUNE_INTERVAL_MS);
427
+ pruneTimer.unref();
428
+ });
429
+ const shutdown = async () => {
430
+ if (pruneTimer) clearInterval(pruneTimer);
431
+ scannerHandle?.stop();
432
+ syncHandle?.stop();
433
+ await flushSentry();
434
+ server.close();
435
+ process.exit(0);
436
+ };
437
+ process.on("SIGTERM", shutdown);
438
+ process.on("SIGINT", shutdown);
439
+ process.on("SIGHUP", shutdown);
440
+ }
441
+ var runPrune2;
442
+
443
+ export {
444
+ syncAwarePrune,
445
+ createUnifiedServer
446
+ };
447
+ //# sourceMappingURL=chunk-WLBNFVIG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/api/routes.ts","../src/db/sync-prune.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport http from \"node:http\";\nimport { handleApiRequest } from \"./api/routes.js\";\nimport { config } from \"./config.js\";\nimport { autoPrune } from \"./db/prune.js\";\nimport { syncAwarePrune } from \"./db/sync-prune.js\";\nimport { type HookInput, processHookEvent } from \"./hooks/ingest.js\";\nimport { log } from \"./log.js\";\nimport { handleOtlpRequest } from \"./otlp/server.js\";\nimport { handleProxyRequest, tunnelWebSocket } from \"./proxy/server.js\";\nimport { createScannerLoop } from \"./scanner/index.js\";\nimport type { ScannerHandle } from \"./scanner/types.js\";\nimport {\n addBreadcrumb,\n captureException,\n flushSentry,\n initSentry,\n setTag,\n} from \"./sentry.js\";\nimport { createSyncLoop } from \"./sync/loop.js\";\nimport type { SyncHandle } from \"./sync/types.js\";\nimport { loadUnifiedConfig } from \"./unified-config.js\";\n\nfunction collectBody(req: http.IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nexport function createUnifiedServer(): http.Server {\n const server = http.createServer(async (req, res) => {\n const url = req.url ?? \"\";\n const method = req.method ?? \"\";\n\n // Health check\n if (url === \"/health\" && method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", port: config.port }));\n return;\n }\n\n // Hook event ingest\n if (url === \"/hooks\" && method === \"POST\") {\n try {\n const body = await collectBody(req);\n const data: HookInput = JSON.parse(body.toString(\"utf-8\"));\n addBreadcrumb(\"hooks\", `${data.hook_event_name ?? \"unknown\"} event`, {\n session_id: data.session_id,\n tool_name: data.tool_name,\n target: data.target ?? data.source,\n });\n const result = processHookEvent(data);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n } catch (err) {\n log.hooks.error(\"Hook ingest error:\", err);\n captureException(err, { component: \"hooks\" });\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"hook ingest failed\" }));\n }\n }\n return;\n }\n\n // OTLP ingest — /v1/logs, /v1/metrics, /v1/traces, or bare \"/\" (Gemini)\n if (\n method === \"POST\" &&\n (url.startsWith(\"/v1/\") || url === \"/\" || url === \"\")\n ) {\n await handleOtlpRequest(req, res);\n return;\n }\n\n // Proxy routes — /proxy/anthropic/*, /proxy/openai/*, /proxy/codex/*, /proxy/google/*\n if (url.startsWith(\"/proxy/\")) {\n if (method !== \"POST\") {\n res.writeHead(405);\n res.end();\n return;\n }\n addBreadcrumb(\"proxy\", `Proxy ${url}`);\n // Strip /proxy prefix so the proxy handler sees /anthropic/*, /openai/*, etc.\n req.url = url.slice(6);\n await handleProxyRequest(req, res);\n return;\n }\n\n // API routes — /api/tool, /api/exec\n if (url.startsWith(\"/api/\") && method === \"POST\") {\n await handleApiRequest(req, res);\n return;\n }\n\n // 404\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"not_found\" }));\n });\n\n // WebSocket upgrades for proxy routes\n server.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n if (url.startsWith(\"/proxy/\")) {\n req.url = url.slice(6);\n tunnelWebSocket(req, socket, head);\n } else {\n socket.end(\"HTTP/1.1 404 Not Found\\r\\n\\r\\n\");\n }\n });\n\n return server;\n}\n\n// When run directly, start the unified server\nconst entryScript = process.argv[1]?.replaceAll(\"\\\\\", \"/\") ?? \"\";\nif (entryScript.endsWith(\"/server.js\") || entryScript.endsWith(\"/server.ts\")) {\n const PRUNE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\n\n function runPrune(): void {\n try {\n const cfg = loadUnifiedConfig();\n addBreadcrumb(\"prune\", \"Running scheduled prune\");\n autoPrune(cfg.retention.maxAgeDays, cfg.retention.maxSizeMb);\n if (cfg.sync.targets.length > 0 && cfg.retention.syncedMaxAgeDays) {\n syncAwarePrune(cfg.sync.targets, cfg.retention);\n }\n } catch (err) {\n log.server.error(\"Prune error:\", err);\n captureException(err, { component: \"prune\" });\n }\n }\n\n const sentryActive = initSentry();\n if (sentryActive) log.server.info(\"Sentry: enabled\");\n\n const server = createUnifiedServer();\n let syncHandle: SyncHandle | null = null;\n let scannerHandle: ScannerHandle | null = null;\n let pruneTimer: ReturnType<typeof setInterval> | null = null;\n\n let takeoverAttempted = false;\n server.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n if (takeoverAttempted) {\n log.server.warn(`Already running on ${config.host}:${config.port}`);\n process.exit(0);\n }\n takeoverAttempted = true;\n log.server.warn(`Port ${config.port} in use, attempting takeover...`);\n try {\n // Only kill the old panopticon server via PID file, not all\n // processes on the port (which could include Claude Code CLI)\n const pidFile = config.serverPidFile;\n const pidStr = fs.readFileSync(pidFile, \"utf-8\").trim();\n const oldPid = parseInt(pidStr, 10);\n if (oldPid && oldPid !== process.pid) {\n try {\n process.kill(oldPid, \"SIGTERM\");\n } catch {}\n }\n setTimeout(() => server.listen(config.port, config.host), 1500);\n } catch {\n log.server.warn(`Already running on ${config.host}:${config.port}`);\n process.exit(0);\n }\n return;\n }\n captureException(err, { component: \"server\" });\n throw err;\n });\n server.listen(config.port, config.host, () => {\n log.server.info(`Listening on ${config.host}:${config.port}`);\n\n const cfg = loadUnifiedConfig();\n\n // Start session file scanner first — sync is deferred until scanner\n // finishes any initial resync so we don't sync stale/partial data.\n scannerHandle = createScannerLoop({\n onReady: () => {\n if (cfg.sync.targets.length > 0) {\n log.sync.info(\n `Targets: ${cfg.sync.targets.map((t) => t.name).join(\", \")}`,\n );\n setTag(\"sync_targets\", cfg.sync.targets.length);\n syncHandle = createSyncLoop({\n targets: cfg.sync.targets,\n filter: cfg.sync.filter,\n });\n syncHandle.start();\n }\n },\n });\n scannerHandle.start();\n\n // Run prune on startup, then hourly\n runPrune();\n pruneTimer = setInterval(runPrune, PRUNE_INTERVAL_MS);\n pruneTimer.unref();\n });\n\n const shutdown = async () => {\n if (pruneTimer) clearInterval(pruneTimer);\n scannerHandle?.stop();\n syncHandle?.stop();\n await flushSentry();\n server.close();\n process.exit(0);\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGHUP\", shutdown);\n}\n","/**\n * Server-side API route handler.\n *\n * Two endpoints:\n * POST /api/tool — read-only query dispatch (CLI + MCP)\n * POST /api/exec — write command dispatch (CLI only)\n */\nimport type http from \"node:http\";\nimport { refreshPricing } from \"../db/pricing.js\";\nimport { pruneEstimate, pruneExecute } from \"../db/prune.js\";\nimport {\n activitySummary,\n costBreakdown,\n dbStats,\n listPlans,\n listSessions,\n print,\n rawQuery,\n search,\n sessionTimeline,\n} from \"../db/query.js\";\nimport { getDb } from \"../db/schema.js\";\nimport { log } from \"../log.js\";\nimport { addTarget, listTargets, removeTarget } from \"../sync/config.js\";\nimport { TABLE_SYNC_REGISTRY } from \"../sync/registry.js\";\nimport type { SyncTarget } from \"../sync/types.js\";\nimport {\n readWatermark,\n resetWatermarks,\n watermarkKey,\n writeWatermark,\n} from \"../sync/watermark.js\";\n\n// ── Tool dispatch ────────────────────────────────────────────────────────────\n\ntype ToolFn = (params: Record<string, unknown>) => unknown;\n\nconst TOOLS: Record<string, ToolFn> = {\n sessions: (p) => listSessions(p as Parameters<typeof listSessions>[0]),\n timeline: (p) => sessionTimeline(p as Parameters<typeof sessionTimeline>[0]),\n costs: (p) => costBreakdown(p as Parameters<typeof costBreakdown>[0]),\n summary: (p) => activitySummary(p as Parameters<typeof activitySummary>[0]),\n plans: (p) => listPlans(p as Parameters<typeof listPlans>[0]),\n search: (p) => search(p as Parameters<typeof search>[0]),\n get: (p) => print(p as Parameters<typeof print>[0]),\n query: (p) => rawQuery((p as { sql: string }).sql),\n status: () => dbStats(),\n};\n\n// ── Exec dispatch ────────────────────────────────────────────────────────────\n\ntype ExecFn = (params: Record<string, unknown>) => unknown;\n\nconst EXEC: Record<string, ExecFn> = {\n prune: (p) => {\n const cutoffMs = p.cutoffMs as number;\n if (typeof cutoffMs !== \"number\") {\n throw new Error(\"cutoffMs is required and must be a number\");\n }\n if (p.dryRun) {\n return pruneEstimate(cutoffMs);\n }\n const result = pruneExecute(cutoffMs);\n if (p.vacuum) {\n const db = getDb();\n db.pragma(\"wal_checkpoint(TRUNCATE)\");\n db.exec(\"VACUUM\");\n }\n return result;\n },\n \"refresh-pricing\": () => refreshPricing(),\n \"sync-reset\": (p) => {\n const target = p.target as string | undefined;\n resetWatermarks(target);\n return { ok: true, target: target ?? \"all\" };\n },\n \"sync-watermark-get\": (p) => {\n const target = p.target as string;\n if (!target) throw new Error(\"target is required\");\n const table = p.table as string | undefined;\n if (table) {\n return {\n key: watermarkKey(table, target),\n value: readWatermark(watermarkKey(table, target)),\n };\n }\n // Return all watermarks for this target\n const watermarks: Record<string, number> = {};\n for (const desc of TABLE_SYNC_REGISTRY) {\n const key = watermarkKey(desc.table, target);\n watermarks[desc.table] = readWatermark(key);\n }\n return { target, watermarks };\n },\n \"sync-watermark-set\": (p) => {\n const target = p.target as string;\n const table = p.table as string;\n const value = p.value as number;\n if (!target) throw new Error(\"target is required\");\n if (!table) throw new Error(\"table is required\");\n if (typeof value !== \"number\") throw new Error(\"value must be a number\");\n const key = watermarkKey(table, target);\n writeWatermark(key, value);\n return { key, value };\n },\n \"sync-pending\": (p) => {\n const target = p.target as string;\n if (!target) throw new Error(\"target is required\");\n const db = getDb();\n const pending: Record<\n string,\n { maxId: number; watermark: number; pending: number }\n > = {};\n for (const desc of TABLE_SYNC_REGISTRY) {\n const key = watermarkKey(desc.table, target);\n const wm = readWatermark(key);\n const maxId =\n (\n db\n .prepare(\n `SELECT MAX(${desc.table === \"sessions\" ? \"sync_seq\" : \"id\"}) as m FROM ${desc.table}`,\n )\n .get() as {\n m: number | null;\n }\n )?.m ?? 0;\n const count = Math.max(0, maxId - wm);\n if (count > 0) {\n pending[desc.table] = { maxId, watermark: wm, pending: count };\n }\n }\n const totalPending = Object.values(pending).reduce(\n (s, v) => s + v.pending,\n 0,\n );\n return { target, totalPending, tables: pending };\n },\n \"sync-target-list\": () => {\n return { targets: listTargets() };\n },\n \"sync-target-add\": (p) => {\n const target = p as unknown as SyncTarget;\n if (!target.name) throw new Error(\"name is required\");\n if (!target.url) throw new Error(\"url is required\");\n addTarget(target);\n return { ok: true, name: target.name, url: target.url };\n },\n \"sync-target-remove\": (p) => {\n const name = p.name as string;\n if (!name) throw new Error(\"name is required\");\n const removed = removeTarget(name);\n return { ok: removed, name };\n },\n};\n\n// ── Request handler ──────────────────────────────────────────────────────────\n\nfunction collectBody(req: http.IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nfunction jsonResponse(\n res: http.ServerResponse,\n status: number,\n data: unknown,\n): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(data));\n}\n\nexport async function handleApiRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n): Promise<void> {\n const url = req.url ?? \"\";\n\n let body: Record<string, unknown>;\n try {\n const raw = await collectBody(req);\n body = raw.length > 0 ? JSON.parse(raw.toString(\"utf-8\")) : {};\n } catch {\n jsonResponse(res, 400, { error: \"Invalid JSON body\" });\n return;\n }\n\n if (url === \"/api/tool\") {\n const name = body.name as string | undefined;\n if (!name || !(name in TOOLS)) {\n jsonResponse(res, 404, {\n error: `Unknown tool: ${name}`,\n available: Object.keys(TOOLS),\n });\n return;\n }\n try {\n const params = (body.params as Record<string, unknown>) ?? {};\n const result = TOOLS[name](params);\n jsonResponse(res, 200, result);\n } catch (err) {\n log.server.error(`API tool \"${name}\" error:`, err);\n jsonResponse(res, 500, {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n\n if (url === \"/api/exec\") {\n const command = body.command as string | undefined;\n if (!command || !(command in EXEC)) {\n jsonResponse(res, 404, {\n error: `Unknown command: ${command}`,\n available: Object.keys(EXEC),\n });\n return;\n }\n try {\n const params = (body.params as Record<string, unknown>) ?? {};\n const result = await EXEC[command](params);\n jsonResponse(res, 200, result ?? { ok: true });\n } catch (err) {\n log.server.error(`API exec \"${command}\" error:`, err);\n jsonResponse(res, 500, {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n return;\n }\n\n jsonResponse(res, 404, { error: \"Unknown API endpoint\", url });\n}\n","import type { SyncTarget } from \"../sync/types.js\";\nimport { readWatermark, watermarkKey } from \"../sync/watermark.js\";\nimport type { RetentionConfig } from \"../unified-config.js\";\nimport { getDb } from \"./schema.js\";\n\nexport interface SyncPruneResult {\n hook_events: number;\n otel_logs: number;\n otel_metrics: number;\n}\n\n/**\n * Compute the minimum watermark for a table across all sync targets.\n * Returns 0 if no targets exist or any target has watermark 0 (hasn't synced yet).\n */\nexport function minWatermarkForTable(\n table: string,\n targets: SyncTarget[],\n): number {\n if (targets.length === 0) return 0;\n\n let min = Infinity;\n for (const t of targets) {\n const wm = readWatermark(watermarkKey(table, t.name));\n if (wm === 0) return 0;\n if (wm < min) min = wm;\n }\n return min === Infinity ? 0 : min;\n}\n\n/**\n * Aggressively prune rows that have been confirmed synced to ALL targets\n * and are older than `syncedMaxAgeDays`.\n *\n * No-op when:\n * - No sync targets configured\n * - `syncedMaxAgeDays` is not set\n * - Any target has watermark 0 for a given table (hasn't completed first sync)\n */\nexport function syncAwarePrune(\n targets: SyncTarget[],\n retention: RetentionConfig,\n): SyncPruneResult {\n const result: SyncPruneResult = {\n hook_events: 0,\n otel_logs: 0,\n otel_metrics: 0,\n };\n\n if (!retention.syncedMaxAgeDays || targets.length === 0) {\n return result;\n }\n\n const cutoffMs =\n Date.now() - retention.syncedMaxAgeDays * 24 * 60 * 60 * 1000;\n const cutoffNs = cutoffMs * 1_000_000;\n const db = getDb();\n\n const tx = db.transaction(() => {\n // -- hook_events --\n const hookMinWm = minWatermarkForTable(\"hook_events\", targets);\n if (hookMinWm > 0) {\n db.prepare(\n \"DELETE FROM hook_events_fts WHERE rowid IN (SELECT id FROM hook_events WHERE id <= ? AND timestamp_ms < ?)\",\n ).run(hookMinWm, cutoffMs);\n\n result.hook_events = db\n .prepare(\"DELETE FROM hook_events WHERE id <= ? AND timestamp_ms < ?\")\n .run(hookMinWm, cutoffMs).changes;\n }\n\n // -- otel_logs --\n const logsMinWm = minWatermarkForTable(\"otel_logs\", targets);\n if (logsMinWm > 0) {\n result.otel_logs = db\n .prepare(\"DELETE FROM otel_logs WHERE id <= ? AND timestamp_ns < ?\")\n .run(logsMinWm, cutoffNs).changes;\n }\n\n // -- otel_metrics --\n const metricsMinWm = minWatermarkForTable(\"otel_metrics\", targets);\n if (metricsMinWm > 0) {\n result.otel_metrics = db\n .prepare(\"DELETE FROM otel_metrics WHERE id <= ? AND timestamp_ns < ?\")\n .run(metricsMinWm, cutoffNs).changes;\n }\n\n // Session metadata (session_repositories, session_cwds) is local-only —\n // not synced to remote targets. Left to regular time/size-based pruneExecute.\n });\n\n tx();\n return result;\n}\n"],"mappings":"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","names":["collectBody","runPrune"]}