@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.
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +5 -0
- package/README.md +363 -0
- package/bin/hook-handler +3 -0
- package/bin/mcp-server +3 -0
- package/bin/panopticon +3 -0
- package/bin/proxy +3 -0
- package/bin/server +3 -0
- package/dist/api/client.d.ts +67 -0
- package/dist/api/client.js +48 -0
- package/dist/api/client.js.map +1 -0
- package/dist/chunk-3BUJ7URA.js +387 -0
- package/dist/chunk-3BUJ7URA.js.map +1 -0
- package/dist/chunk-3TZAKV3M.js +158 -0
- package/dist/chunk-3TZAKV3M.js.map +1 -0
- package/dist/chunk-4SM2H22C.js +169 -0
- package/dist/chunk-4SM2H22C.js.map +1 -0
- package/dist/chunk-7Q3BJMLG.js +62 -0
- package/dist/chunk-7Q3BJMLG.js.map +1 -0
- package/dist/chunk-BVOE7A2Z.js +412 -0
- package/dist/chunk-BVOE7A2Z.js.map +1 -0
- package/dist/chunk-CF4GPWLI.js +170 -0
- package/dist/chunk-CF4GPWLI.js.map +1 -0
- package/dist/chunk-DZ5HJFB4.js +467 -0
- package/dist/chunk-DZ5HJFB4.js.map +1 -0
- package/dist/chunk-HQCY722C.js +428 -0
- package/dist/chunk-HQCY722C.js.map +1 -0
- package/dist/chunk-HRCEIYKU.js +134 -0
- package/dist/chunk-HRCEIYKU.js.map +1 -0
- package/dist/chunk-K7YUPLES.js +76 -0
- package/dist/chunk-K7YUPLES.js.map +1 -0
- package/dist/chunk-L7G27XWF.js +130 -0
- package/dist/chunk-L7G27XWF.js.map +1 -0
- package/dist/chunk-LWXF7YRG.js +626 -0
- package/dist/chunk-LWXF7YRG.js.map +1 -0
- package/dist/chunk-NXH7AONS.js +1120 -0
- package/dist/chunk-NXH7AONS.js.map +1 -0
- package/dist/chunk-QK5442ZP.js +55 -0
- package/dist/chunk-QK5442ZP.js.map +1 -0
- package/dist/chunk-QVK6VGCV.js +1703 -0
- package/dist/chunk-QVK6VGCV.js.map +1 -0
- package/dist/chunk-RX2RXHBH.js +1699 -0
- package/dist/chunk-RX2RXHBH.js.map +1 -0
- package/dist/chunk-SEXU2WYG.js +788 -0
- package/dist/chunk-SEXU2WYG.js.map +1 -0
- package/dist/chunk-SUGSQ4YI.js +264 -0
- package/dist/chunk-SUGSQ4YI.js.map +1 -0
- package/dist/chunk-TGXFVAID.js +138 -0
- package/dist/chunk-TGXFVAID.js.map +1 -0
- package/dist/chunk-WLBNFVIG.js +447 -0
- package/dist/chunk-WLBNFVIG.js.map +1 -0
- package/dist/chunk-XLTCUH5A.js +1072 -0
- package/dist/chunk-XLTCUH5A.js.map +1 -0
- package/dist/chunk-YVRWVDIA.js +146 -0
- package/dist/chunk-YVRWVDIA.js.map +1 -0
- package/dist/chunk-ZEC4LRKS.js +176 -0
- package/dist/chunk-ZEC4LRKS.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1084 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-NwoZC-GM.d.ts +20 -0
- package/dist/db.d.ts +46 -0
- package/dist/db.js +15 -0
- package/dist/db.js.map +1 -0
- package/dist/doctor.d.ts +37 -0
- package/dist/doctor.js +14 -0
- package/dist/doctor.js.map +1 -0
- package/dist/hooks/handler.d.ts +23 -0
- package/dist/hooks/handler.js +295 -0
- package/dist/hooks/handler.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +243 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/otlp/server.d.ts +7 -0
- package/dist/otlp/server.js +17 -0
- package/dist/otlp/server.js.map +1 -0
- package/dist/permissions.d.ts +33 -0
- package/dist/permissions.js +14 -0
- package/dist/permissions.js.map +1 -0
- package/dist/pricing.d.ts +29 -0
- package/dist/pricing.js +13 -0
- package/dist/pricing.js.map +1 -0
- package/dist/proxy/server.d.ts +10 -0
- package/dist/proxy/server.js +20 -0
- package/dist/proxy/server.js.map +1 -0
- package/dist/prune.d.ts +18 -0
- package/dist/prune.js +13 -0
- package/dist/prune.js.map +1 -0
- package/dist/query.d.ts +56 -0
- package/dist/query.js +27 -0
- package/dist/query.js.map +1 -0
- package/dist/reparse-636YZCE3.js +14 -0
- package/dist/reparse-636YZCE3.js.map +1 -0
- package/dist/repo.d.ts +17 -0
- package/dist/repo.js +9 -0
- package/dist/repo.js.map +1 -0
- package/dist/scanner.d.ts +73 -0
- package/dist/scanner.js +15 -0
- package/dist/scanner.js.map +1 -0
- package/dist/sdk.d.ts +82 -0
- package/dist/sdk.js +208 -0
- package/dist/sdk.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +35 -0
- package/dist/setup.js +19 -0
- package/dist/setup.js.map +1 -0
- package/dist/sync/index.d.ts +29 -0
- package/dist/sync/index.js +32 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/targets.d.ts +279 -0
- package/dist/targets.js +20 -0
- package/dist/targets.js.map +1 -0
- package/dist/types-D-MYCBol.d.ts +128 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/hooks/hooks.json +274 -0
- package/package.json +124 -0
- 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
|