@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,1699 @@
1
+ import {
2
+ incrementEventTypeCount,
3
+ incrementToolCount,
4
+ insertHookEvent,
5
+ insertOtelLogs,
6
+ insertOtelMetrics,
7
+ insertRepoConfigSnapshot,
8
+ insertUserConfigSnapshot,
9
+ upsertSession,
10
+ upsertSessionCwd,
11
+ upsertSessionRepository
12
+ } from "./chunk-BVOE7A2Z.js";
13
+ import {
14
+ resolveRepoFromCwd
15
+ } from "./chunk-YVRWVDIA.js";
16
+ import {
17
+ isGitignored,
18
+ readConfig,
19
+ resolveGitRoot
20
+ } from "./chunk-3BUJ7URA.js";
21
+ import {
22
+ addBreadcrumb,
23
+ captureException
24
+ } from "./chunk-CF4GPWLI.js";
25
+ import {
26
+ log
27
+ } from "./chunk-7Q3BJMLG.js";
28
+ import {
29
+ ALL_EVENTS
30
+ } from "./chunk-ZEC4LRKS.js";
31
+ import {
32
+ allTargets,
33
+ getTarget
34
+ } from "./chunk-QVK6VGCV.js";
35
+ import {
36
+ config
37
+ } from "./chunk-K7YUPLES.js";
38
+
39
+ // src/proxy/server.ts
40
+ import http from "http";
41
+ import https from "https";
42
+
43
+ // src/hooks/ingest.ts
44
+ import { execFileSync } from "child_process";
45
+ import fs2 from "fs";
46
+ import os from "os";
47
+ import path2 from "path";
48
+
49
+ // src/eventConfig.ts
50
+ import fs from "fs";
51
+ import path from "path";
52
+ var EVENT_CONFIG_PATH = path.join(config.dataDir, "event-config.json");
53
+ var cachedEventConfig = null;
54
+ function defaultEventConfig() {
55
+ const cfg = {};
56
+ for (const e of ALL_EVENTS) cfg[e] = true;
57
+ return cfg;
58
+ }
59
+ function loadEventConfig() {
60
+ if (cachedEventConfig) return cachedEventConfig;
61
+ const defaults = defaultEventConfig();
62
+ try {
63
+ const raw = JSON.parse(fs.readFileSync(EVENT_CONFIG_PATH, "utf-8"));
64
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
65
+ for (const key of Object.keys(raw)) {
66
+ if (key in defaults && typeof raw[key] === "boolean") {
67
+ defaults[key] = raw[key];
68
+ }
69
+ }
70
+ }
71
+ } catch {
72
+ }
73
+ cachedEventConfig = defaults;
74
+ return cachedEventConfig;
75
+ }
76
+ function isEventEnabled(eventType) {
77
+ const cfg = loadEventConfig();
78
+ if (!(eventType in cfg)) return true;
79
+ return cfg[eventType];
80
+ }
81
+
82
+ // src/hooks/permissions.ts
83
+ function splitChainComponents(cmd) {
84
+ return cmd.split(/\s*(?:&&|\|\||;|\|)\s*/).map((s) => s.trim()).filter(Boolean);
85
+ }
86
+ function extractBaseCommands(component) {
87
+ let cmd = component.replace(/^(?:[A-Z_][A-Z0-9_]*=[^\s]*\s+)+/, "");
88
+ cmd = cmd.replace(/\s*\d*>[>&]?\s*\S+/g, " ").trim();
89
+ cmd = cmd.replace(/\s*\d*<\s*\S+/g, " ").trim();
90
+ const tokens = cmd.split(/\s+/).filter(Boolean);
91
+ if (tokens.length === 0) return [];
92
+ if ((tokens[0] === "bash" || tokens[0] === "sh") && tokens.includes("-c")) {
93
+ return [tokens[0]];
94
+ }
95
+ const COMPOUND_TOOLS = {
96
+ git: /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace"]),
97
+ gh: /* @__PURE__ */ new Set(["-R", "--repo"]),
98
+ npx: /* @__PURE__ */ new Set(["-p", "--package"]),
99
+ pnpm: /* @__PURE__ */ new Set(["--filter", "-C", "--dir"]),
100
+ xargs: /* @__PURE__ */ new Set([
101
+ "-I",
102
+ "-L",
103
+ "-n",
104
+ "-P",
105
+ "-s",
106
+ "--max-args",
107
+ "--max-procs",
108
+ "--replace"
109
+ ]),
110
+ env: /* @__PURE__ */ new Set([]),
111
+ nice: /* @__PURE__ */ new Set(["-n", "--adjustment"]),
112
+ timeout: /* @__PURE__ */ new Set(["-k", "--kill-after", "-s", "--signal"]),
113
+ watch: /* @__PURE__ */ new Set(["-n", "-d", "--interval"])
114
+ };
115
+ const flagsWithArg = COMPOUND_TOOLS[tokens[0]];
116
+ if (flagsWithArg && tokens.length > 1) {
117
+ for (let i = 1; i < tokens.length; i++) {
118
+ const t = tokens[i];
119
+ if (t.startsWith("-")) {
120
+ if (flagsWithArg.has(t) && !t.includes("=")) i++;
121
+ continue;
122
+ }
123
+ if (t.includes("=") && tokens[0] === "env") continue;
124
+ if (tokens[0] === "timeout" && /^\d/.test(t)) continue;
125
+ return [`${tokens[0]} ${t}`];
126
+ }
127
+ return [tokens[0]];
128
+ }
129
+ const baseCmd = tokens[0];
130
+ const results = [baseCmd];
131
+ if (baseCmd === "find") {
132
+ for (let i = 1; i < tokens.length; i++) {
133
+ if (tokens[i] === "-exec" || tokens[i] === "-execdir") {
134
+ const delegated = tokens[i + 1];
135
+ if (delegated && delegated !== "{}" && delegated !== ";") {
136
+ const binName = delegated.split("/").pop();
137
+ results.push(binName);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return results;
143
+ }
144
+ function checkBashPermission(command, allowedCommands) {
145
+ if (!allowedCommands.length) return null;
146
+ const components = splitChainComponents(command);
147
+ if (components.length === 0) return null;
148
+ const bases = components.flatMap(extractBaseCommands);
149
+ const unapproved = bases.filter((b) => !allowedCommands.includes(b));
150
+ if (unapproved.length === 0) {
151
+ return {
152
+ allow: true,
153
+ reason: `All ${bases.length} component(s) approved: ${bases.join(", ")}`
154
+ };
155
+ }
156
+ return null;
157
+ }
158
+
159
+ // src/hooks/ingest.ts
160
+ var gitIdentityCache = /* @__PURE__ */ new Map();
161
+ function resolveGitIdentity(cwd) {
162
+ const cached = gitIdentityCache.get(cwd);
163
+ if (cached) return cached;
164
+ const result = { name: null, email: null };
165
+ try {
166
+ result.name = execFileSync("git", ["-C", cwd, "config", "user.name"], {
167
+ encoding: "utf-8",
168
+ timeout: 3e3,
169
+ stdio: ["ignore", "pipe", "ignore"]
170
+ }).trim() || null;
171
+ } catch {
172
+ }
173
+ try {
174
+ result.email = execFileSync("git", ["-C", cwd, "config", "user.email"], {
175
+ encoding: "utf-8",
176
+ timeout: 3e3,
177
+ stdio: ["ignore", "pipe", "ignore"]
178
+ }).trim() || null;
179
+ } catch {
180
+ }
181
+ gitIdentityCache.set(cwd, result);
182
+ return result;
183
+ }
184
+ var lastSessionRepo = /* @__PURE__ */ new Map();
185
+ var userConfigCaptured = /* @__PURE__ */ new Set();
186
+ var seenSessionRepos = /* @__PURE__ */ new Set();
187
+ var ALLOWED_PATH = path2.join(config.dataDir, "permissions", "allowed.json");
188
+ function loadAllowed() {
189
+ try {
190
+ return JSON.parse(fs2.readFileSync(ALLOWED_PATH, "utf-8"));
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ function isPanopticonMcpTool(toolName) {
196
+ return toolName.startsWith("mcp__plugin_panopticon_panopticon__") || toolName.startsWith("mcp__panopticon__");
197
+ }
198
+ function extractShellPwd(data) {
199
+ if (typeof data.shell_pwd === "string") return data.shell_pwd;
200
+ if (typeof data.tool_input?.shell_pwd === "string")
201
+ return data.tool_input.shell_pwd;
202
+ return null;
203
+ }
204
+ function extractEventPaths(data) {
205
+ const paths = [];
206
+ const seen = /* @__PURE__ */ new Set();
207
+ const add = (dir, source) => {
208
+ if (!seen.has(dir)) {
209
+ seen.add(dir);
210
+ paths.push({ dir, source });
211
+ }
212
+ };
213
+ const shellPwd = extractShellPwd(data);
214
+ if (shellPwd) add(shellPwd, "shell_pwd");
215
+ const toolInput = data.tool_input;
216
+ if (toolInput && typeof toolInput === "object") {
217
+ const fp = toolInput.file_path;
218
+ if (typeof fp === "string" && path2.isAbsolute(fp)) {
219
+ add(path2.dirname(fp), "tool_input.file_path");
220
+ }
221
+ const p = toolInput.path;
222
+ if (typeof p === "string" && path2.isAbsolute(p)) {
223
+ add(path2.dirname(p), "tool_input.path");
224
+ }
225
+ }
226
+ if (typeof data.cwd === "string") add(data.cwd, "cwd");
227
+ return paths;
228
+ }
229
+ function normalizeResolveFn(resolveFn) {
230
+ return (dir) => {
231
+ const result = resolveFn(dir);
232
+ if (!result) return null;
233
+ if (typeof result === "string") return { repo: result };
234
+ return result;
235
+ };
236
+ }
237
+ function resolveEventRepo(data, resolveFn = resolveRepoFromCwd) {
238
+ const sessionId = data.session_id ?? "unknown";
239
+ let repo = data.repository ?? null;
240
+ if (!repo) {
241
+ const resolve = normalizeResolveFn(resolveFn);
242
+ for (const { dir } of extractEventPaths(data)) {
243
+ const info = resolve(dir);
244
+ if (info) {
245
+ repo = info.repo;
246
+ break;
247
+ }
248
+ }
249
+ }
250
+ if (!repo) {
251
+ repo = lastSessionRepo.get(sessionId) ?? null;
252
+ }
253
+ if (repo) {
254
+ lastSessionRepo.set(sessionId, repo);
255
+ }
256
+ return repo;
257
+ }
258
+ function resolveAllEventRepos(data, resolveFn = resolveRepoFromCwd) {
259
+ const results = [];
260
+ const seen = /* @__PURE__ */ new Set();
261
+ const resolve = normalizeResolveFn(resolveFn);
262
+ if (data.repository) {
263
+ seen.add(data.repository);
264
+ const shellPwd = extractShellPwd(data);
265
+ results.push({
266
+ repo: data.repository,
267
+ dir: shellPwd ?? data.cwd ?? "."
268
+ });
269
+ }
270
+ for (const { dir } of extractEventPaths(data)) {
271
+ const info = resolve(dir);
272
+ if (info && !seen.has(info.repo)) {
273
+ seen.add(info.repo);
274
+ results.push({ repo: info.repo, dir, branch: info.branch });
275
+ }
276
+ }
277
+ return results;
278
+ }
279
+ var sessionTargetCache = /* @__PURE__ */ new Map();
280
+ function resolveTarget(data) {
281
+ const targets = allTargets();
282
+ const sessionId = data.session_id;
283
+ const source = data.source ?? data.target;
284
+ if (source) {
285
+ for (const v of targets) {
286
+ if (v.id === source) {
287
+ if (sessionId) sessionTargetCache.set(sessionId, v.id);
288
+ return v;
289
+ }
290
+ }
291
+ }
292
+ if (sessionId) {
293
+ const cached = sessionTargetCache.get(sessionId);
294
+ if (cached) {
295
+ return targets.find((v) => v.id === cached);
296
+ }
297
+ }
298
+ const rawEvent = data.hook_event_name;
299
+ if (rawEvent) {
300
+ let matched;
301
+ for (const v of targets) {
302
+ if (rawEvent in v.events.eventMap) {
303
+ if (matched) {
304
+ log.hooks.warn(
305
+ `Event "${rawEvent}" claimed by both "${matched.id}" and "${v.id}" \u2014 using "${matched.id}"`
306
+ );
307
+ break;
308
+ }
309
+ matched = v;
310
+ }
311
+ }
312
+ if (matched) {
313
+ if (sessionId) sessionTargetCache.set(sessionId, matched.id);
314
+ return matched;
315
+ }
316
+ }
317
+ const model = typeof data.model === "string" ? data.model : null;
318
+ if (model) {
319
+ let matched;
320
+ for (const v of targets) {
321
+ if (v.ident?.modelPatterns?.some((re) => re.test(model))) {
322
+ matched = v;
323
+ break;
324
+ }
325
+ }
326
+ if (matched) {
327
+ log.hooks.warn(
328
+ `Target resolved via model-name heuristic: model="${model}" \u2192 "${matched.id}". Set an explicit source/target field to avoid ambiguous detection.`
329
+ );
330
+ if (sessionId) sessionTargetCache.set(sessionId, matched.id);
331
+ return matched;
332
+ }
333
+ }
334
+ return void 0;
335
+ }
336
+ function processHookEvent(data) {
337
+ const sessionId = data.session_id ?? "unknown";
338
+ const rawEventType = data.hook_event_name ?? "Unknown";
339
+ let eventType = rawEventType;
340
+ const toolName = data.tool_name ?? null;
341
+ const timestampMs = Date.now();
342
+ const target = resolveTarget(data);
343
+ if (target) {
344
+ const mapped = target.events.eventMap[eventType];
345
+ if (mapped) eventType = mapped;
346
+ if (target.events.normalizePayload) {
347
+ data = target.events.normalizePayload(data);
348
+ }
349
+ }
350
+ const repo = resolveEventRepo(data);
351
+ const targetId = target?.id ?? "unknown";
352
+ if (!isEventEnabled(eventType)) {
353
+ if (eventType === "PreToolUse" && toolName) {
354
+ return buildPermissionResponse(toolName, data, target);
355
+ }
356
+ return {};
357
+ }
358
+ insertHookEvent({
359
+ session_id: sessionId,
360
+ event_type: eventType,
361
+ timestamp_ms: timestampMs,
362
+ cwd: data.cwd,
363
+ repository: repo ?? void 0,
364
+ tool_name: toolName ?? void 0,
365
+ target: targetId,
366
+ payload: data
367
+ });
368
+ const sessionFields = {
369
+ session_id: sessionId,
370
+ target: targetId,
371
+ has_hooks: 1
372
+ };
373
+ if (eventType === "SessionStart") {
374
+ sessionFields.started_at_ms = timestampMs;
375
+ sessionFields.created_at = timestampMs;
376
+ sessionFields.permission_mode = typeof data.permission_mode === "string" ? data.permission_mode : void 0;
377
+ sessionFields.agent_version = typeof data.agent_version === "string" ? data.agent_version : void 0;
378
+ const cwd = data.cwd;
379
+ if (cwd) {
380
+ const repoInfo = resolveRepoFromCwd(cwd);
381
+ sessionFields.project = repoInfo?.repo ?? path2.basename(cwd);
382
+ }
383
+ }
384
+ if (eventType === "UserPromptSubmit") {
385
+ const prompt = typeof data.prompt === "string" ? data.prompt : typeof data.user_prompt === "string" ? data.user_prompt : void 0;
386
+ if (prompt) sessionFields.first_prompt = prompt;
387
+ }
388
+ if (eventType === "Stop" || eventType === "SessionEnd") {
389
+ sessionFields.ended_at_ms = timestampMs;
390
+ }
391
+ upsertSession(sessionFields);
392
+ if (eventType === "SubagentStart" || eventType === "SubagentStop") {
393
+ const agentId = data.agent_id;
394
+ if (agentId) {
395
+ const subagentSessionId = `agent-${agentId}`;
396
+ const subagentFields = {
397
+ session_id: subagentSessionId,
398
+ target: targetId,
399
+ parent_session_id: sessionId,
400
+ relationship_type: "subagent",
401
+ is_automated: 1
402
+ };
403
+ if (eventType === "SubagentStart") {
404
+ subagentFields.started_at_ms = timestampMs;
405
+ subagentFields.created_at = timestampMs;
406
+ } else {
407
+ subagentFields.ended_at_ms = timestampMs;
408
+ }
409
+ upsertSession(subagentFields);
410
+ }
411
+ }
412
+ incrementEventTypeCount(sessionId, eventType);
413
+ if (eventType === "PreToolUse" && toolName) {
414
+ incrementToolCount(sessionId, toolName);
415
+ }
416
+ const allRepos = resolveAllEventRepos(data);
417
+ for (const { repo: r, dir, branch } of allRepos) {
418
+ const gitId = resolveGitIdentity(dir);
419
+ upsertSessionRepository(sessionId, r, timestampMs, gitId, branch);
420
+ const repoKey = `${sessionId}:${r}`;
421
+ if (!seenSessionRepos.has(repoKey)) {
422
+ seenSessionRepos.add(repoKey);
423
+ try {
424
+ const cfg = readConfig(dir);
425
+ const gitRoot = resolveGitRoot(dir);
426
+ const localSettingsPath = path2.join(
427
+ gitRoot ?? dir,
428
+ ".claude",
429
+ "settings.local.json"
430
+ );
431
+ insertRepoConfigSnapshot({
432
+ repository: r,
433
+ cwd: dir,
434
+ sessionId,
435
+ hooks: cfg.project?.hooks ?? [],
436
+ mcpServers: cfg.project?.mcpServers ?? [],
437
+ commands: cfg.project?.commands ?? [],
438
+ agents: cfg.project?.agents ?? [],
439
+ rules: cfg.project?.rules ?? [],
440
+ localHooks: cfg.projectLocal?.hooks ?? [],
441
+ localMcpServers: cfg.projectLocal?.mcpServers ?? [],
442
+ localPermissions: cfg.projectLocal?.permissions ?? {
443
+ allow: [],
444
+ ask: [],
445
+ deny: []
446
+ },
447
+ localIsGitignored: isGitignored(localSettingsPath, gitRoot ?? dir),
448
+ instructions: cfg.instructions
449
+ });
450
+ } catch {
451
+ }
452
+ }
453
+ }
454
+ if (data.cwd) {
455
+ upsertSessionCwd(sessionId, data.cwd, timestampMs);
456
+ }
457
+ if (eventType === "SessionStart" && !userConfigCaptured.has(sessionId)) {
458
+ userConfigCaptured.add(sessionId);
459
+ try {
460
+ const config2 = readConfig(data.cwd);
461
+ insertUserConfigSnapshot({
462
+ deviceName: os.hostname(),
463
+ permissions: config2.user.permissions,
464
+ enabledPlugins: config2.enabledPlugins,
465
+ hooks: config2.user.hooks,
466
+ commands: config2.user.commands,
467
+ rules: config2.user.rules,
468
+ skills: config2.user.skills
469
+ });
470
+ } catch {
471
+ }
472
+ }
473
+ if (eventType === "PreToolUse" && toolName) {
474
+ return buildPermissionResponse(toolName, data, target);
475
+ }
476
+ return {};
477
+ }
478
+ function buildPermissionResponse(toolName, data, target) {
479
+ let decision = null;
480
+ if (isPanopticonMcpTool(toolName)) {
481
+ decision = { allow: true, reason: "Panopticon tool (always allowed)" };
482
+ } else {
483
+ const allowed = loadAllowed();
484
+ if (allowed) {
485
+ if (toolName === "Bash") {
486
+ const command = data.tool_input?.command;
487
+ if (typeof command === "string" && allowed.bash_commands?.length) {
488
+ decision = checkBashPermission(command, allowed.bash_commands);
489
+ }
490
+ } else if (allowed.tools?.includes(toolName)) {
491
+ decision = { allow: true, reason: `Tool "${toolName}" is allowed` };
492
+ }
493
+ }
494
+ }
495
+ if (decision) {
496
+ if (target) {
497
+ return target.events.formatPermissionResponse(decision);
498
+ }
499
+ return {
500
+ hookSpecificOutput: {
501
+ hookEventName: "PreToolUse",
502
+ permissionDecision: "allow",
503
+ permissionDecisionReason: decision.reason
504
+ }
505
+ };
506
+ }
507
+ return {};
508
+ }
509
+
510
+ // src/proxy/emit.ts
511
+ function emitHookEventAsync(event) {
512
+ try {
513
+ processHookEvent(event);
514
+ } catch (err) {
515
+ if (process.env.PANOPTICON_DEBUG) {
516
+ log.proxy.error("hook emit error:", err);
517
+ }
518
+ }
519
+ }
520
+ function emitOtelMetrics(metrics) {
521
+ if (metrics.length === 0) return;
522
+ const now = Date.now() * 1e6;
523
+ try {
524
+ const rows = metrics.map((m) => ({
525
+ timestamp_ns: now,
526
+ name: m.name,
527
+ value: m.value,
528
+ metric_type: "gauge",
529
+ unit: m.unit,
530
+ attributes: m.attributes,
531
+ resource_attributes: m.sessionId ? { "session.id": m.sessionId } : void 0,
532
+ session_id: m.sessionId
533
+ }));
534
+ insertOtelMetrics(rows);
535
+ } catch (err) {
536
+ if (process.env.PANOPTICON_DEBUG) {
537
+ log.proxy.error("OTel metric emit error:", err);
538
+ }
539
+ }
540
+ }
541
+ function emitOtelLogs(logs) {
542
+ if (logs.length === 0) return;
543
+ const now = Date.now() * 1e6;
544
+ try {
545
+ const rows = logs.map((l) => ({
546
+ timestamp_ns: now,
547
+ severity_text: l.severityText ?? "INFO",
548
+ body: l.body,
549
+ attributes: l.attributes,
550
+ resource_attributes: l.sessionId ? { "session.id": l.sessionId } : void 0,
551
+ session_id: l.sessionId
552
+ }));
553
+ insertOtelLogs(rows);
554
+ } catch (err) {
555
+ if (process.env.PANOPTICON_DEBUG) {
556
+ log.proxy.error("OTel log emit error:", err);
557
+ }
558
+ }
559
+ }
560
+
561
+ // src/proxy/formats/anthropic.ts
562
+ var anthropicParser = {
563
+ matches(path3) {
564
+ return path3.includes("/v1/messages");
565
+ },
566
+ extractEvents(capture) {
567
+ const events = [];
568
+ const { request, response, sessionId } = capture;
569
+ const reqBody = request.body;
570
+ const resBody = response.body;
571
+ if (!reqBody) return events;
572
+ const messages = reqBody.messages;
573
+ if (messages) {
574
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
575
+ if (lastUser) {
576
+ const prompt = extractTextContent(lastUser.content);
577
+ if (prompt) {
578
+ events.push({
579
+ session_id: sessionId,
580
+ hook_event_name: "UserPromptSubmit",
581
+ prompt
582
+ });
583
+ }
584
+ }
585
+ for (const msg of messages) {
586
+ if (msg.role === "tool" || msg.role === "tool_result") {
587
+ const toolMsg = msg;
588
+ events.push({
589
+ session_id: sessionId,
590
+ hook_event_name: "PostToolUse",
591
+ tool_name: toolMsg.tool_use_id ?? "unknown",
592
+ tool_input: {
593
+ tool_use_id: toolMsg.tool_use_id,
594
+ content: extractTextContent(toolMsg.content)
595
+ }
596
+ });
597
+ }
598
+ }
599
+ }
600
+ if (resBody && Array.isArray(resBody.content)) {
601
+ for (const block of resBody.content) {
602
+ if (block.type === "tool_use") {
603
+ events.push({
604
+ session_id: sessionId,
605
+ hook_event_name: "PreToolUse",
606
+ tool_name: block.name ?? "unknown",
607
+ tool_input: block.input ?? {}
608
+ });
609
+ }
610
+ }
611
+ }
612
+ return events;
613
+ },
614
+ extractMetrics(capture) {
615
+ const metrics = [];
616
+ const resBody = capture.response.body;
617
+ if (!resBody) return metrics;
618
+ const usage = resBody.usage;
619
+ const model = resBody.model ?? "unknown";
620
+ if (usage) {
621
+ if (usage.input_tokens) {
622
+ metrics.push({
623
+ name: "token.usage",
624
+ value: usage.input_tokens,
625
+ attributes: {
626
+ model,
627
+ token_type: "input",
628
+ target: capture.target
629
+ },
630
+ sessionId: capture.sessionId
631
+ });
632
+ }
633
+ if (usage.output_tokens) {
634
+ metrics.push({
635
+ name: "token.usage",
636
+ value: usage.output_tokens,
637
+ attributes: {
638
+ model,
639
+ token_type: "output",
640
+ target: capture.target
641
+ },
642
+ sessionId: capture.sessionId
643
+ });
644
+ }
645
+ if (usage.cache_read_input_tokens) {
646
+ metrics.push({
647
+ name: "token.usage",
648
+ value: usage.cache_read_input_tokens,
649
+ attributes: {
650
+ model,
651
+ token_type: "cacheRead",
652
+ target: capture.target
653
+ },
654
+ sessionId: capture.sessionId
655
+ });
656
+ }
657
+ if (usage.cache_creation_input_tokens) {
658
+ metrics.push({
659
+ name: "token.usage",
660
+ value: usage.cache_creation_input_tokens,
661
+ attributes: {
662
+ model,
663
+ token_type: "cacheWrite",
664
+ target: capture.target
665
+ },
666
+ sessionId: capture.sessionId
667
+ });
668
+ }
669
+ }
670
+ return metrics;
671
+ },
672
+ extractLogs(capture) {
673
+ const resBody = capture.response.body;
674
+ const reqBody = capture.request.body;
675
+ const model = resBody?.model ?? reqBody?.model ?? "unknown";
676
+ const usage = resBody?.usage;
677
+ return [
678
+ {
679
+ body: "api_request",
680
+ sessionId: capture.sessionId,
681
+ attributes: {
682
+ model,
683
+ target: capture.target,
684
+ duration_ms: capture.duration_ms,
685
+ status: capture.response.status,
686
+ stop_reason: resBody?.stop_reason,
687
+ input_tokens: usage?.input_tokens,
688
+ output_tokens: usage?.output_tokens
689
+ }
690
+ }
691
+ ];
692
+ }
693
+ };
694
+ function extractTextContent(content) {
695
+ if (typeof content === "string") return content;
696
+ if (Array.isArray(content)) {
697
+ return content.filter((c) => c.type === "text").map((c) => c.text).join("\n") || void 0;
698
+ }
699
+ return void 0;
700
+ }
701
+
702
+ // src/proxy/formats/openai.ts
703
+ var openaiParser = {
704
+ matches(path3) {
705
+ return path3.includes("/v1/chat/completions");
706
+ },
707
+ extractEvents(capture) {
708
+ const events = [];
709
+ const { request, response, sessionId } = capture;
710
+ const reqBody = request.body;
711
+ const resBody = response.body;
712
+ if (!reqBody) return events;
713
+ const messages = reqBody.messages;
714
+ if (messages) {
715
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
716
+ if (lastUser) {
717
+ const prompt = extractTextContent2(lastUser.content);
718
+ if (prompt) {
719
+ events.push({
720
+ session_id: sessionId,
721
+ hook_event_name: "UserPromptSubmit",
722
+ prompt
723
+ });
724
+ }
725
+ }
726
+ for (const msg of messages) {
727
+ if (msg.role === "tool") {
728
+ events.push({
729
+ session_id: sessionId,
730
+ hook_event_name: "PostToolUse",
731
+ tool_name: msg.tool_call_id ?? "unknown",
732
+ tool_input: {
733
+ tool_call_id: msg.tool_call_id,
734
+ content: extractTextContent2(msg.content)
735
+ }
736
+ });
737
+ }
738
+ }
739
+ }
740
+ if (resBody) {
741
+ const choices = resBody.choices;
742
+ if (choices) {
743
+ for (const choice of choices) {
744
+ const toolCalls = choice.message?.tool_calls;
745
+ if (toolCalls) {
746
+ for (const tc of toolCalls) {
747
+ const fn = tc.function;
748
+ let parsedArgs = {};
749
+ if (fn?.arguments) {
750
+ try {
751
+ parsedArgs = JSON.parse(fn.arguments);
752
+ } catch {
753
+ parsedArgs = { raw: fn.arguments };
754
+ }
755
+ }
756
+ events.push({
757
+ session_id: sessionId,
758
+ hook_event_name: "PreToolUse",
759
+ tool_name: fn?.name ?? "unknown",
760
+ tool_input: parsedArgs
761
+ });
762
+ }
763
+ }
764
+ }
765
+ }
766
+ }
767
+ return events;
768
+ },
769
+ extractMetrics(capture) {
770
+ const metrics = [];
771
+ const resBody = capture.response.body;
772
+ const reqBody = capture.request.body;
773
+ if (!resBody) return metrics;
774
+ const usage = resBody.usage;
775
+ const model = resBody.model ?? reqBody?.model ?? "unknown";
776
+ if (usage) {
777
+ if (usage.prompt_tokens) {
778
+ metrics.push({
779
+ name: "token.usage",
780
+ value: usage.prompt_tokens,
781
+ attributes: {
782
+ model,
783
+ token_type: "input",
784
+ target: capture.target
785
+ },
786
+ sessionId: capture.sessionId
787
+ });
788
+ }
789
+ if (usage.completion_tokens) {
790
+ metrics.push({
791
+ name: "token.usage",
792
+ value: usage.completion_tokens,
793
+ attributes: {
794
+ model,
795
+ token_type: "output",
796
+ target: capture.target
797
+ },
798
+ sessionId: capture.sessionId
799
+ });
800
+ }
801
+ }
802
+ return metrics;
803
+ },
804
+ extractLogs(capture) {
805
+ const resBody = capture.response.body;
806
+ const reqBody = capture.request.body;
807
+ const model = resBody?.model ?? reqBody?.model ?? "unknown";
808
+ const usage = resBody?.usage;
809
+ const choices = resBody?.choices;
810
+ return [
811
+ {
812
+ body: "api_request",
813
+ sessionId: capture.sessionId,
814
+ attributes: {
815
+ model,
816
+ target: capture.target,
817
+ duration_ms: capture.duration_ms,
818
+ status: capture.response.status,
819
+ stop_reason: choices?.[0]?.finish_reason,
820
+ input_tokens: usage?.prompt_tokens,
821
+ output_tokens: usage?.completion_tokens
822
+ }
823
+ }
824
+ ];
825
+ }
826
+ };
827
+ function extractTextContent2(content) {
828
+ if (typeof content === "string") return content;
829
+ if (Array.isArray(content)) {
830
+ return content.filter((c) => c.type === "text").map((c) => c.text).join("\n") || void 0;
831
+ }
832
+ return void 0;
833
+ }
834
+
835
+ // src/proxy/formats/openai-responses.ts
836
+ var openaiResponsesParser = {
837
+ matches(path3) {
838
+ return path3.endsWith("/responses") || path3.includes("/responses?");
839
+ },
840
+ extractEvents(capture) {
841
+ const events = [];
842
+ const { request, response, sessionId } = capture;
843
+ const reqBody = request.body;
844
+ const resBody = response.body;
845
+ if (!reqBody) return events;
846
+ const input = reqBody.input;
847
+ const prompt = extractInputText(input);
848
+ if (prompt) {
849
+ events.push({
850
+ session_id: sessionId,
851
+ hook_event_name: "UserPromptSubmit",
852
+ prompt
853
+ });
854
+ }
855
+ if (Array.isArray(input)) {
856
+ for (const item of input) {
857
+ const it = item;
858
+ if (it.type === "function_call_output") {
859
+ events.push({
860
+ session_id: sessionId,
861
+ hook_event_name: "PostToolUse",
862
+ tool_name: it.call_id ?? "unknown",
863
+ tool_input: {
864
+ call_id: it.call_id,
865
+ content: it.output
866
+ }
867
+ });
868
+ }
869
+ }
870
+ }
871
+ if (resBody) {
872
+ const output = resBody.output;
873
+ if (output) {
874
+ for (const item of output) {
875
+ if (item.type === "function_call") {
876
+ let parsedArgs = {};
877
+ if (typeof item.arguments === "string") {
878
+ try {
879
+ parsedArgs = JSON.parse(item.arguments);
880
+ } catch {
881
+ parsedArgs = { raw: item.arguments };
882
+ }
883
+ }
884
+ events.push({
885
+ session_id: sessionId,
886
+ hook_event_name: "PreToolUse",
887
+ tool_name: item.name ?? "unknown",
888
+ tool_input: parsedArgs
889
+ });
890
+ }
891
+ }
892
+ }
893
+ }
894
+ return events;
895
+ },
896
+ extractMetrics(capture) {
897
+ const metrics = [];
898
+ const resBody = capture.response.body;
899
+ const reqBody = capture.request.body;
900
+ if (!resBody) return metrics;
901
+ const usage = resBody.usage;
902
+ const model = resBody.model ?? reqBody?.model ?? "unknown";
903
+ if (usage) {
904
+ if (usage.input_tokens) {
905
+ metrics.push({
906
+ name: "token.usage",
907
+ value: usage.input_tokens,
908
+ attributes: {
909
+ model,
910
+ token_type: "input",
911
+ target: capture.target
912
+ },
913
+ sessionId: capture.sessionId
914
+ });
915
+ }
916
+ if (usage.output_tokens) {
917
+ metrics.push({
918
+ name: "token.usage",
919
+ value: usage.output_tokens,
920
+ attributes: {
921
+ model,
922
+ token_type: "output",
923
+ target: capture.target
924
+ },
925
+ sessionId: capture.sessionId
926
+ });
927
+ }
928
+ }
929
+ return metrics;
930
+ },
931
+ extractLogs(capture) {
932
+ const resBody = capture.response.body;
933
+ const reqBody = capture.request.body;
934
+ const model = resBody?.model ?? reqBody?.model ?? "unknown";
935
+ const usage = resBody?.usage;
936
+ return [
937
+ {
938
+ body: "api_request",
939
+ sessionId: capture.sessionId,
940
+ attributes: {
941
+ model,
942
+ target: capture.target,
943
+ duration_ms: capture.duration_ms,
944
+ status: capture.response.status,
945
+ stop_reason: resBody?.status,
946
+ input_tokens: usage?.input_tokens,
947
+ output_tokens: usage?.output_tokens
948
+ }
949
+ }
950
+ ];
951
+ }
952
+ };
953
+ function extractInputText(input) {
954
+ if (typeof input === "string") return input;
955
+ if (!Array.isArray(input)) return void 0;
956
+ const texts = [];
957
+ for (let i = input.length - 1; i >= 0; i--) {
958
+ const item = input[i];
959
+ if (item.role === "user") {
960
+ const text = extractContentField(item.content);
961
+ if (text) return text;
962
+ }
963
+ if (item.type === "message" && item.role === "user") {
964
+ const text = extractContentField(item.content);
965
+ if (text) return text;
966
+ }
967
+ if (item.type === "input_text" && typeof item.text === "string") {
968
+ texts.unshift(item.text);
969
+ }
970
+ }
971
+ return texts.length > 0 ? texts.join("\n") : void 0;
972
+ }
973
+ function extractContentField(content) {
974
+ if (typeof content === "string") return content;
975
+ if (Array.isArray(content)) {
976
+ return content.filter(
977
+ (c) => c.type === "input_text" || c.type === "text"
978
+ ).map((c) => c.text).join("\n") || void 0;
979
+ }
980
+ return void 0;
981
+ }
982
+
983
+ // src/proxy/sessions.ts
984
+ var SessionTracker = class {
985
+ sessions = /* @__PURE__ */ new Map();
986
+ getOrCreateSession(target, requestBody) {
987
+ const now = Date.now();
988
+ const existing = this.sessions.get(target);
989
+ const messageCount = countMessages(requestBody);
990
+ if (existing) {
991
+ const isReset = this.isConversationReset(existing, messageCount, now);
992
+ if (!isReset) {
993
+ existing.lastRequestMs = now;
994
+ if (messageCount > 0) existing.lastMessageCount = messageCount;
995
+ return { sessionId: existing.id, isNew: false };
996
+ }
997
+ }
998
+ const seq = existing ? existing.seq + 1 : 1;
999
+ const date = new Date(now).toISOString().slice(0, 10).replace(/-/g, "");
1000
+ const sessionId = `${target}-${date}-${String(seq).padStart(3, "0")}`;
1001
+ this.sessions.set(target, {
1002
+ id: sessionId,
1003
+ lastRequestMs: now,
1004
+ lastMessageCount: messageCount,
1005
+ seq
1006
+ });
1007
+ return { sessionId, isNew: true };
1008
+ }
1009
+ isConversationReset(state, messageCount, now) {
1010
+ if (messageCount > 0 && state.lastMessageCount > 3 && messageCount <= 2) {
1011
+ return true;
1012
+ }
1013
+ if (messageCount > 0 && state.lastMessageCount > 0 && messageCount < state.lastMessageCount / 2 && state.lastMessageCount - messageCount >= 3) {
1014
+ return true;
1015
+ }
1016
+ if (now - state.lastRequestMs >= config.proxyIdleSessionMs) {
1017
+ return true;
1018
+ }
1019
+ return false;
1020
+ }
1021
+ };
1022
+ function countMessages(body) {
1023
+ if (typeof body !== "object" || body === null) return 0;
1024
+ const messages = body.messages;
1025
+ if (!Array.isArray(messages)) return 0;
1026
+ return messages.filter((m) => m.role !== "system").length;
1027
+ }
1028
+
1029
+ // src/proxy/streaming.ts
1030
+ function createAnthropicAccumulator() {
1031
+ let message = {};
1032
+ let usage = {};
1033
+ const contentBlocks = [];
1034
+ let currentBlockIndex = -1;
1035
+ const textParts = /* @__PURE__ */ new Map();
1036
+ return {
1037
+ push(chunk) {
1038
+ const text = chunk.toString("utf-8");
1039
+ for (const line of text.split("\n")) {
1040
+ if (!line.startsWith("data: ")) continue;
1041
+ const data = line.slice(6).trim();
1042
+ if (data === "[DONE]") continue;
1043
+ try {
1044
+ const event = JSON.parse(data);
1045
+ switch (event.type) {
1046
+ case "message_start":
1047
+ message = event.message ?? {};
1048
+ usage = event.message?.usage ?? {};
1049
+ break;
1050
+ case "content_block_start":
1051
+ currentBlockIndex = event.index ?? contentBlocks.length;
1052
+ contentBlocks[currentBlockIndex] = event.content_block ?? {};
1053
+ break;
1054
+ case "content_block_delta":
1055
+ if (event.delta?.type === "text_delta" && event.delta.text) {
1056
+ const idx = event.index ?? currentBlockIndex;
1057
+ textParts.set(
1058
+ idx,
1059
+ (textParts.get(idx) ?? "") + event.delta.text
1060
+ );
1061
+ } else if (event.delta?.type === "input_json_delta" && event.delta.partial_json) {
1062
+ const idx = event.index ?? currentBlockIndex;
1063
+ textParts.set(
1064
+ idx,
1065
+ (textParts.get(idx) ?? "") + event.delta.partial_json
1066
+ );
1067
+ }
1068
+ break;
1069
+ case "message_delta":
1070
+ if (event.delta) {
1071
+ Object.assign(message, event.delta);
1072
+ }
1073
+ if (event.usage) {
1074
+ Object.assign(usage, event.usage);
1075
+ }
1076
+ break;
1077
+ }
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ return chunk;
1082
+ },
1083
+ finish() {
1084
+ const content = contentBlocks.map((block, i) => {
1085
+ if (block.type === "text") {
1086
+ return { ...block, text: textParts.get(i) ?? block.text ?? "" };
1087
+ }
1088
+ if (block.type === "tool_use") {
1089
+ const raw = textParts.get(i);
1090
+ let input = block.input;
1091
+ if (raw) {
1092
+ try {
1093
+ input = JSON.parse(raw);
1094
+ } catch {
1095
+ input = { raw };
1096
+ }
1097
+ }
1098
+ return { ...block, input };
1099
+ }
1100
+ return block;
1101
+ });
1102
+ return {
1103
+ ...message,
1104
+ content,
1105
+ usage
1106
+ };
1107
+ }
1108
+ };
1109
+ }
1110
+ function createOpenaiAccumulator() {
1111
+ let model = "";
1112
+ let finishReason = null;
1113
+ let role = "";
1114
+ let contentParts = "";
1115
+ const toolCalls = /* @__PURE__ */ new Map();
1116
+ let usage = {};
1117
+ return {
1118
+ push(chunk) {
1119
+ const text = chunk.toString("utf-8");
1120
+ for (const line of text.split("\n")) {
1121
+ if (!line.startsWith("data: ")) continue;
1122
+ const data = line.slice(6).trim();
1123
+ if (data === "[DONE]") continue;
1124
+ try {
1125
+ const event = JSON.parse(data);
1126
+ if (event.model) model = event.model;
1127
+ if (event.usage) usage = event.usage;
1128
+ const choice = event.choices?.[0];
1129
+ if (!choice) continue;
1130
+ if (choice.finish_reason) finishReason = choice.finish_reason;
1131
+ const delta = choice.delta;
1132
+ if (!delta) continue;
1133
+ if (delta.role) role = delta.role;
1134
+ if (delta.content) contentParts += delta.content;
1135
+ if (delta.tool_calls) {
1136
+ for (const tc of delta.tool_calls) {
1137
+ const idx = tc.index ?? 0;
1138
+ const existing = toolCalls.get(idx);
1139
+ if (existing) {
1140
+ if (tc.function?.arguments) {
1141
+ existing.function.arguments += tc.function.arguments;
1142
+ }
1143
+ } else {
1144
+ toolCalls.set(idx, {
1145
+ id: tc.id ?? "",
1146
+ type: tc.type ?? "function",
1147
+ function: {
1148
+ name: tc.function?.name ?? "",
1149
+ arguments: tc.function?.arguments ?? ""
1150
+ }
1151
+ });
1152
+ }
1153
+ }
1154
+ }
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ return chunk;
1159
+ },
1160
+ finish() {
1161
+ const message = {
1162
+ role: role || "assistant",
1163
+ content: contentParts || null
1164
+ };
1165
+ if (toolCalls.size > 0) {
1166
+ message.tool_calls = [...toolCalls.entries()].sort(([a], [b]) => a - b).map(([, tc]) => tc);
1167
+ }
1168
+ return {
1169
+ model,
1170
+ choices: [
1171
+ {
1172
+ index: 0,
1173
+ message,
1174
+ finish_reason: finishReason
1175
+ }
1176
+ ],
1177
+ usage
1178
+ };
1179
+ }
1180
+ };
1181
+ }
1182
+ function isStreamingRequest(body) {
1183
+ if (typeof body === "object" && body !== null) {
1184
+ return body.stream === true;
1185
+ }
1186
+ return false;
1187
+ }
1188
+
1189
+ // src/proxy/ws-capture.ts
1190
+ var WebSocketMessageExtractor = class {
1191
+ buffer = Buffer.alloc(0);
1192
+ fragments = [];
1193
+ currentOpcode = 0;
1194
+ onMessage;
1195
+ push(data) {
1196
+ this.buffer = Buffer.concat([this.buffer, data]);
1197
+ this.drain();
1198
+ }
1199
+ drain() {
1200
+ while (this.buffer.length >= 2) {
1201
+ const byte0 = this.buffer[0];
1202
+ const byte1 = this.buffer[1];
1203
+ const fin = (byte0 & 128) !== 0;
1204
+ const opcode = byte0 & 15;
1205
+ const masked = (byte1 & 128) !== 0;
1206
+ let payloadLen = byte1 & 127;
1207
+ let headerLen = 2;
1208
+ if (payloadLen === 126) {
1209
+ if (this.buffer.length < 4) return;
1210
+ payloadLen = this.buffer.readUInt16BE(2);
1211
+ headerLen = 4;
1212
+ } else if (payloadLen === 127) {
1213
+ if (this.buffer.length < 10) return;
1214
+ payloadLen = Number(this.buffer.readBigUInt64BE(2));
1215
+ headerLen = 10;
1216
+ }
1217
+ if (masked) headerLen += 4;
1218
+ const totalLen = headerLen + payloadLen;
1219
+ if (this.buffer.length < totalLen) return;
1220
+ let payload = this.buffer.subarray(headerLen, totalLen);
1221
+ if (masked) {
1222
+ const maskKey = this.buffer.subarray(headerLen - 4, headerLen);
1223
+ payload = Buffer.from(payload);
1224
+ for (let i = 0; i < payload.length; i++) {
1225
+ payload[i] ^= maskKey[i % 4];
1226
+ }
1227
+ }
1228
+ if (opcode === 1 || opcode === 2) {
1229
+ this.currentOpcode = opcode;
1230
+ this.fragments = [payload];
1231
+ } else if (opcode === 0) {
1232
+ this.fragments.push(payload);
1233
+ }
1234
+ if (fin && this.fragments.length > 0 && opcode <= 2) {
1235
+ if (this.currentOpcode === 1) {
1236
+ try {
1237
+ const msg = Buffer.concat(this.fragments).toString("utf-8");
1238
+ this.onMessage?.(msg);
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ this.fragments = [];
1243
+ }
1244
+ this.buffer = this.buffer.subarray(totalLen);
1245
+ }
1246
+ }
1247
+ };
1248
+
1249
+ // src/proxy/server.ts
1250
+ function buildUpstreamRoutes() {
1251
+ const routes = {};
1252
+ for (const v of allTargets()) {
1253
+ if (v.proxy && typeof v.proxy.upstreamHost === "string") {
1254
+ routes[v.id] = v.proxy.upstreamHost;
1255
+ }
1256
+ }
1257
+ if (!routes.openai) routes.openai = "api.openai.com";
1258
+ if (!routes.google) routes.google = "generativelanguage.googleapis.com";
1259
+ if (!routes.anthropic) routes.anthropic = "api.anthropic.com";
1260
+ return routes;
1261
+ }
1262
+ var UPSTREAM_ROUTES = buildUpstreamRoutes();
1263
+ var KNOWN_ROUTES_MSG = [
1264
+ ...Object.keys(UPSTREAM_ROUTES),
1265
+ ...allTargets().filter((v) => v.proxy && typeof v.proxy.upstreamHost === "function").map((v) => v.id)
1266
+ ].filter((v, i, a) => a.indexOf(v) === i).map((v) => `/${v}/*`).join(", ");
1267
+ var FORMAT_PARSERS = [
1268
+ anthropicParser,
1269
+ openaiParser,
1270
+ openaiResponsesParser
1271
+ ];
1272
+ var sessions = new SessionTracker();
1273
+ function parseRoute(url, headers) {
1274
+ const match = url.match(/^\/([^/]+)(\/.*)?$/);
1275
+ if (!match) return null;
1276
+ const targetId = match[1];
1277
+ const requestPath = match[2] ?? "/";
1278
+ const targetAdapter = getTarget(targetId);
1279
+ if (targetAdapter?.proxy) {
1280
+ const { proxy } = targetAdapter;
1281
+ const flatHeaders = flattenHeaders(headers ?? {});
1282
+ const upstream2 = typeof proxy.upstreamHost === "function" ? proxy.upstreamHost(flatHeaders) : proxy.upstreamHost;
1283
+ const finalPath = proxy.rewritePath ? proxy.rewritePath(requestPath, flatHeaders) : requestPath;
1284
+ return { target: targetId, upstream: upstream2, path: finalPath };
1285
+ }
1286
+ const upstream = UPSTREAM_ROUTES[targetId];
1287
+ if (!upstream) return null;
1288
+ return { target: targetId, upstream, path: requestPath };
1289
+ }
1290
+ function collectBody(req) {
1291
+ return new Promise((resolve, reject) => {
1292
+ const chunks = [];
1293
+ req.on("data", (chunk) => chunks.push(chunk));
1294
+ req.on("end", () => resolve(Buffer.concat(chunks)));
1295
+ req.on("error", reject);
1296
+ });
1297
+ }
1298
+ function processCapture(capture) {
1299
+ for (const parser of FORMAT_PARSERS) {
1300
+ if (!parser.matches(capture.request.path)) continue;
1301
+ const hookEvents = parser.extractEvents(capture);
1302
+ for (const event of hookEvents) {
1303
+ event.source = "proxy";
1304
+ event.target = capture.target;
1305
+ emitHookEventAsync(event);
1306
+ }
1307
+ const metrics = parser.extractMetrics(capture);
1308
+ for (const metric of metrics) {
1309
+ metric.attributes = { ...metric.attributes, source: "proxy" };
1310
+ }
1311
+ if (metrics.length > 0) {
1312
+ emitOtelMetrics(metrics);
1313
+ }
1314
+ const logs = parser.extractLogs(capture);
1315
+ for (const log2 of logs) {
1316
+ log2.attributes = { ...log2.attributes, source: "proxy" };
1317
+ }
1318
+ if (logs.length > 0) {
1319
+ emitOtelLogs(logs);
1320
+ }
1321
+ return;
1322
+ }
1323
+ }
1324
+ function forwardNonStreaming(route, clientReq, clientRes, requestBody, parsedReqBody) {
1325
+ const startMs = Date.now();
1326
+ const { sessionId, isNew } = sessions.getOrCreateSession(
1327
+ route.target,
1328
+ parsedReqBody
1329
+ );
1330
+ if (isNew) {
1331
+ emitHookEventAsync({
1332
+ session_id: sessionId,
1333
+ hook_event_name: "SessionStart",
1334
+ source: "proxy",
1335
+ target: route.target
1336
+ });
1337
+ }
1338
+ const headers = {};
1339
+ for (const [key, value] of Object.entries(clientReq.headers)) {
1340
+ if (value !== void 0 && key !== "host") {
1341
+ headers[key] = value;
1342
+ }
1343
+ }
1344
+ const upstreamReq = https.request(
1345
+ {
1346
+ hostname: route.upstream,
1347
+ port: 443,
1348
+ path: route.path,
1349
+ method: clientReq.method,
1350
+ headers
1351
+ },
1352
+ (upstreamRes) => {
1353
+ const chunks = [];
1354
+ upstreamRes.on("data", (chunk) => chunks.push(chunk));
1355
+ upstreamRes.on("end", () => {
1356
+ const responseBody = Buffer.concat(chunks);
1357
+ const duration_ms = Date.now() - startMs;
1358
+ clientRes.writeHead(upstreamRes.statusCode ?? 200, upstreamRes.headers);
1359
+ clientRes.end(responseBody);
1360
+ let parsedResBody;
1361
+ try {
1362
+ parsedResBody = JSON.parse(responseBody.toString("utf-8"));
1363
+ } catch {
1364
+ parsedResBody = {};
1365
+ }
1366
+ const capture = {
1367
+ target: route.target,
1368
+ sessionId,
1369
+ timestamp_ms: startMs,
1370
+ request: {
1371
+ path: route.path,
1372
+ headers: flattenHeaders(clientReq.headers),
1373
+ body: parsedReqBody
1374
+ },
1375
+ response: {
1376
+ status: upstreamRes.statusCode ?? 0,
1377
+ body: parsedResBody
1378
+ },
1379
+ duration_ms
1380
+ };
1381
+ processCapture(capture);
1382
+ });
1383
+ }
1384
+ );
1385
+ upstreamReq.on("error", (err) => {
1386
+ log.proxy.error(`Upstream error (${route.target}):`, err.message);
1387
+ addBreadcrumb(
1388
+ "proxy",
1389
+ `Upstream error: ${route.target}`,
1390
+ { target: route.target, upstream: route.upstream, error: err.message },
1391
+ "error"
1392
+ );
1393
+ if (!clientRes.headersSent) {
1394
+ clientRes.writeHead(502);
1395
+ clientRes.end(
1396
+ JSON.stringify({ error: "upstream_error", message: err.message })
1397
+ );
1398
+ }
1399
+ });
1400
+ upstreamReq.write(requestBody);
1401
+ upstreamReq.end();
1402
+ }
1403
+ function forwardStreaming(route, clientReq, clientRes, requestBody, parsedReqBody) {
1404
+ const startMs = Date.now();
1405
+ const { sessionId, isNew } = sessions.getOrCreateSession(
1406
+ route.target,
1407
+ parsedReqBody
1408
+ );
1409
+ if (isNew) {
1410
+ emitHookEventAsync({
1411
+ session_id: sessionId,
1412
+ hook_event_name: "SessionStart",
1413
+ source: "proxy",
1414
+ target: route.target
1415
+ });
1416
+ }
1417
+ const targetSpec = getTarget(route.target);
1418
+ const accumulator = targetSpec?.proxy?.accumulatorType === "anthropic" ? createAnthropicAccumulator() : createOpenaiAccumulator();
1419
+ const headers = {};
1420
+ for (const [key, value] of Object.entries(clientReq.headers)) {
1421
+ if (value !== void 0 && key !== "host") {
1422
+ headers[key] = value;
1423
+ }
1424
+ }
1425
+ const upstreamReq = https.request(
1426
+ {
1427
+ hostname: route.upstream,
1428
+ port: 443,
1429
+ path: route.path,
1430
+ method: clientReq.method,
1431
+ headers
1432
+ },
1433
+ (upstreamRes) => {
1434
+ clientRes.writeHead(upstreamRes.statusCode ?? 200, upstreamRes.headers);
1435
+ upstreamRes.on("data", (chunk) => {
1436
+ accumulator.push(chunk);
1437
+ clientRes.write(chunk);
1438
+ });
1439
+ upstreamRes.on("end", () => {
1440
+ clientRes.end();
1441
+ const duration_ms = Date.now() - startMs;
1442
+ const reconstructed = accumulator.finish();
1443
+ const capture = {
1444
+ target: route.target,
1445
+ sessionId,
1446
+ timestamp_ms: startMs,
1447
+ request: {
1448
+ path: route.path,
1449
+ headers: flattenHeaders(clientReq.headers),
1450
+ body: parsedReqBody
1451
+ },
1452
+ response: {
1453
+ status: upstreamRes.statusCode ?? 0,
1454
+ body: reconstructed
1455
+ },
1456
+ duration_ms
1457
+ };
1458
+ processCapture(capture);
1459
+ });
1460
+ }
1461
+ );
1462
+ upstreamReq.on("error", (err) => {
1463
+ log.proxy.error(`Upstream error (${route.target}):`, err.message);
1464
+ addBreadcrumb(
1465
+ "proxy",
1466
+ `Upstream error: ${route.target}`,
1467
+ { target: route.target, upstream: route.upstream, error: err.message },
1468
+ "error"
1469
+ );
1470
+ if (!clientRes.headersSent) {
1471
+ clientRes.writeHead(502);
1472
+ clientRes.end(
1473
+ JSON.stringify({ error: "upstream_error", message: err.message })
1474
+ );
1475
+ }
1476
+ });
1477
+ upstreamReq.write(requestBody);
1478
+ upstreamReq.end();
1479
+ }
1480
+ function flattenHeaders(headers) {
1481
+ const flat = {};
1482
+ for (const [key, value] of Object.entries(headers)) {
1483
+ if (value !== void 0) {
1484
+ flat[key] = Array.isArray(value) ? value.join(", ") : value;
1485
+ }
1486
+ }
1487
+ return flat;
1488
+ }
1489
+ function tunnelWebSocket(req, clientSocket, head) {
1490
+ const url = req.url ?? "";
1491
+ const route = parseRoute(url, req.headers);
1492
+ if (!route) {
1493
+ clientSocket.end("HTTP/1.1 404 Not Found\r\n\r\n");
1494
+ return;
1495
+ }
1496
+ const { sessionId, isNew } = sessions.getOrCreateSession(route.target, {});
1497
+ if (isNew) {
1498
+ emitHookEventAsync({
1499
+ session_id: sessionId,
1500
+ hook_event_name: "SessionStart",
1501
+ source: "proxy",
1502
+ target: route.target
1503
+ });
1504
+ }
1505
+ clientSocket.on("error", (err) => {
1506
+ log.proxy.error(`WebSocket client error (${route.target}):`, err.message);
1507
+ });
1508
+ const proxyHeaders = {};
1509
+ for (const [key, value] of Object.entries(req.headers)) {
1510
+ if (key !== "host" && key !== "sec-websocket-extensions" && value !== void 0) {
1511
+ proxyHeaders[key] = value;
1512
+ }
1513
+ }
1514
+ proxyHeaders.host = route.upstream;
1515
+ const proxyReq = https.request({
1516
+ hostname: route.upstream,
1517
+ port: 443,
1518
+ path: route.path,
1519
+ method: req.method,
1520
+ headers: proxyHeaders
1521
+ });
1522
+ proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
1523
+ let response = `HTTP/${proxyRes.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage}\r
1524
+ `;
1525
+ for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
1526
+ response += `${proxyRes.rawHeaders[i]}: ${proxyRes.rawHeaders[i + 1]}\r
1527
+ `;
1528
+ }
1529
+ response += "\r\n";
1530
+ clientSocket.write(response);
1531
+ if (proxyHead.length > 0) clientSocket.write(proxyHead);
1532
+ if (head.length > 0) proxySocket.write(head);
1533
+ const clientExtractor = new WebSocketMessageExtractor();
1534
+ const serverExtractor = new WebSocketMessageExtractor();
1535
+ let pendingRequest;
1536
+ let requestTimestamp = Date.now();
1537
+ clientExtractor.onMessage = (msg) => {
1538
+ try {
1539
+ pendingRequest = JSON.parse(msg);
1540
+ requestTimestamp = Date.now();
1541
+ } catch (err) {
1542
+ log.proxy.error("Failed to parse client WebSocket message:", err);
1543
+ captureException(err, { component: "proxy", phase: "ws-client-parse" });
1544
+ }
1545
+ };
1546
+ serverExtractor.onMessage = (msg) => {
1547
+ try {
1548
+ const event = JSON.parse(msg);
1549
+ if (event.type === "response.completed" && pendingRequest) {
1550
+ const capture = {
1551
+ target: route.target,
1552
+ sessionId,
1553
+ timestamp_ms: requestTimestamp,
1554
+ request: {
1555
+ path: route.path,
1556
+ headers: flattenHeaders(req.headers),
1557
+ body: pendingRequest
1558
+ },
1559
+ response: {
1560
+ status: 200,
1561
+ body: event.response
1562
+ },
1563
+ duration_ms: Date.now() - requestTimestamp
1564
+ };
1565
+ processCapture(capture);
1566
+ pendingRequest = void 0;
1567
+ }
1568
+ } catch (err) {
1569
+ log.proxy.error("Failed to parse server WebSocket message:", err);
1570
+ captureException(err, { component: "proxy", phase: "ws-server-parse" });
1571
+ }
1572
+ };
1573
+ proxySocket.on("data", (chunk) => {
1574
+ serverExtractor.push(chunk);
1575
+ clientSocket.write(chunk);
1576
+ });
1577
+ clientSocket.on("data", (chunk) => {
1578
+ clientExtractor.push(chunk);
1579
+ proxySocket.write(chunk);
1580
+ });
1581
+ proxySocket.on("error", () => clientSocket.destroy());
1582
+ proxySocket.on("close", () => clientSocket.destroy());
1583
+ clientSocket.on("close", () => proxySocket.destroy());
1584
+ });
1585
+ proxyReq.on("error", (err) => {
1586
+ log.proxy.error(`WebSocket upstream error (${route.target}):`, err.message);
1587
+ clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
1588
+ });
1589
+ proxyReq.on("response", (res) => {
1590
+ let response = `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r
1591
+ `;
1592
+ for (let i = 0; i < res.rawHeaders.length; i += 2) {
1593
+ response += `${res.rawHeaders[i]}: ${res.rawHeaders[i + 1]}\r
1594
+ `;
1595
+ }
1596
+ response += "\r\n";
1597
+ clientSocket.write(response);
1598
+ res.pipe(clientSocket);
1599
+ });
1600
+ proxyReq.end();
1601
+ }
1602
+ async function handleProxyRequest(req, res) {
1603
+ const url = req.url ?? "";
1604
+ const route = parseRoute(url, req.headers);
1605
+ if (!route) {
1606
+ res.writeHead(404, { "Content-Type": "application/json" });
1607
+ res.end(
1608
+ JSON.stringify({
1609
+ error: "unknown_route",
1610
+ message: `Unknown target prefix. Known: ${KNOWN_ROUTES_MSG}`
1611
+ })
1612
+ );
1613
+ return;
1614
+ }
1615
+ try {
1616
+ const requestBody = await collectBody(req);
1617
+ let parsedReqBody;
1618
+ let streaming = false;
1619
+ try {
1620
+ parsedReqBody = JSON.parse(requestBody.toString("utf-8"));
1621
+ streaming = isStreamingRequest(parsedReqBody);
1622
+ } catch {
1623
+ parsedReqBody = {};
1624
+ }
1625
+ if (streaming) {
1626
+ forwardStreaming(route, req, res, requestBody, parsedReqBody);
1627
+ } else {
1628
+ forwardNonStreaming(route, req, res, requestBody, parsedReqBody);
1629
+ }
1630
+ } catch (err) {
1631
+ log.proxy.error("Proxy error:", err);
1632
+ captureException(err, { component: "proxy", path: url });
1633
+ if (!res.headersSent) {
1634
+ res.writeHead(500);
1635
+ res.end();
1636
+ }
1637
+ }
1638
+ }
1639
+ function createProxyServer() {
1640
+ const server = http.createServer(async (req, res) => {
1641
+ const url = req.url ?? "";
1642
+ const method = req.method ?? "";
1643
+ if (url === "/health" && method === "GET") {
1644
+ res.writeHead(200, { "Content-Type": "application/json" });
1645
+ res.end(JSON.stringify({ status: "ok", port: config.proxyPort }));
1646
+ return;
1647
+ }
1648
+ if (method !== "POST") {
1649
+ res.writeHead(405);
1650
+ res.end();
1651
+ return;
1652
+ }
1653
+ await handleProxyRequest(req, res);
1654
+ });
1655
+ server.on("upgrade", (req, socket, head) => {
1656
+ tunnelWebSocket(req, socket, head);
1657
+ });
1658
+ return server;
1659
+ }
1660
+ var entryScript = process.argv[1]?.replaceAll("\\", "/") ?? "";
1661
+ if (entryScript.endsWith("/proxy/server.js") || entryScript.endsWith("/proxy/server.ts")) {
1662
+ const server = createProxyServer();
1663
+ server.on("error", (err) => {
1664
+ if (err.code === "EADDRINUSE") {
1665
+ log.proxy.warn(
1666
+ `Already running on ${config.proxyHost}:${config.proxyPort}`
1667
+ );
1668
+ process.exit(0);
1669
+ }
1670
+ throw err;
1671
+ });
1672
+ server.listen(config.proxyPort, config.proxyHost, () => {
1673
+ log.proxy.info(`Listening on ${config.proxyHost}:${config.proxyPort}`);
1674
+ log.proxy.info("Routes:");
1675
+ for (const [prefix, host] of Object.entries(UPSTREAM_ROUTES)) {
1676
+ log.proxy.info(` /${prefix}/* \u2192 https://${host}/*`);
1677
+ }
1678
+ for (const v of allTargets()) {
1679
+ if (v.proxy && typeof v.proxy.upstreamHost === "function" && !UPSTREAM_ROUTES[v.id]) {
1680
+ log.proxy.info(` /${v.id}/* \u2192 (dynamic)`);
1681
+ }
1682
+ }
1683
+ });
1684
+ const shutdown = () => {
1685
+ server.close();
1686
+ process.exit(0);
1687
+ };
1688
+ process.on("SIGTERM", shutdown);
1689
+ process.on("SIGINT", shutdown);
1690
+ process.on("SIGHUP", shutdown);
1691
+ }
1692
+
1693
+ export {
1694
+ processHookEvent,
1695
+ tunnelWebSocket,
1696
+ handleProxyRequest,
1697
+ createProxyServer
1698
+ };
1699
+ //# sourceMappingURL=chunk-RX2RXHBH.js.map