@wibeco/bridge 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-FLTLCZ6E.js +1074 -0
- package/dist/{chunk-J2HWZMXA.js → chunk-YV3UV2XJ.js} +81 -7
- package/dist/cli.js +22 -3
- package/dist/codex-hook.js +5 -2
- package/dist/index.d.ts +33 -4
- package/dist/index.js +19 -3
- package/package.json +3 -2
- package/templates/claude-code/README.md +2 -0
- package/templates/codex/README.md +3 -0
- package/templates/codex/config.toml.example +1 -1
- package/templates/codex/hooks.json.example +10 -0
- package/templates/cursor/README.md +4 -0
- package/templates/cursor/hooks.json.example +22 -11
- package/dist/chunk-OTDZMJDK.js +0 -612
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
// src/events.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
var agentSourceSchema = z.enum(["cursor", "claude-code", "codex"]);
|
|
5
|
+
var hookEventKindSchema = z.enum([
|
|
6
|
+
"presence.heartbeat",
|
|
7
|
+
"session.started",
|
|
8
|
+
"session.ended",
|
|
9
|
+
"lifecycle.before",
|
|
10
|
+
"lifecycle.after",
|
|
11
|
+
"tool.started",
|
|
12
|
+
"tool.completed",
|
|
13
|
+
"file.changed",
|
|
14
|
+
"shell.started",
|
|
15
|
+
"shell.completed",
|
|
16
|
+
"mcp.started",
|
|
17
|
+
"mcp.completed",
|
|
18
|
+
"unknown"
|
|
19
|
+
]);
|
|
20
|
+
var safeScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
21
|
+
var safeValueSchema = z.lazy(
|
|
22
|
+
() => z.union([safeScalarSchema, z.array(safeValueSchema), z.record(z.string(), safeValueSchema)])
|
|
23
|
+
);
|
|
24
|
+
var canonicalHookEventSchema = z.object({
|
|
25
|
+
id: z.string().min(1),
|
|
26
|
+
version: z.literal(1),
|
|
27
|
+
source: agentSourceSchema,
|
|
28
|
+
kind: hookEventKindSchema,
|
|
29
|
+
occurredAt: z.string().datetime(),
|
|
30
|
+
sessionId: z.string().min(1).optional(),
|
|
31
|
+
repo: z.object({
|
|
32
|
+
root: z.string().min(1),
|
|
33
|
+
remote: z.string().min(1).optional(),
|
|
34
|
+
branch: z.string().min(1).optional(),
|
|
35
|
+
commit: z.string().min(1).optional()
|
|
36
|
+
}).optional(),
|
|
37
|
+
outcome: z.enum(["success", "failure", "cancelled", "unknown"]).optional(),
|
|
38
|
+
durationMs: z.number().nonnegative().optional(),
|
|
39
|
+
metadata: z.record(z.string(), safeValueSchema).default({})
|
|
40
|
+
});
|
|
41
|
+
function createHookEvent(input) {
|
|
42
|
+
return canonicalHookEventSchema.parse({
|
|
43
|
+
...input,
|
|
44
|
+
id: input.id ?? randomUUID(),
|
|
45
|
+
version: 1,
|
|
46
|
+
occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/redaction.ts
|
|
51
|
+
var SENSITIVE_KEY = /authorization|cookie|token|secret|password|passwd|api.?key|prompt|content|input|output|message|transcript/i;
|
|
52
|
+
var SECRET_VALUE = /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[oprsu]_[A-Za-z0-9_]{20,}|(?:bearer|basic)\s+[A-Za-z0-9._~+/-]+=*)\b/gi;
|
|
53
|
+
var PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
54
|
+
function redact(value, options = {}) {
|
|
55
|
+
const replacement = options.replacement ?? "[REDACTED]";
|
|
56
|
+
const visit = (current, key) => {
|
|
57
|
+
if (key && SENSITIVE_KEY.test(key) && !options.allowContent) {
|
|
58
|
+
return replacement;
|
|
59
|
+
}
|
|
60
|
+
if (current === null || typeof current === "boolean" || typeof current === "number") {
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
if (typeof current === "string") {
|
|
64
|
+
let result = current.replace(PRIVATE_KEY, replacement).replace(SECRET_VALUE, replacement);
|
|
65
|
+
if (options.homeDirectory) {
|
|
66
|
+
result = result.replaceAll(options.homeDirectory, "~");
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(current)) {
|
|
71
|
+
return current.map((item) => visit(item));
|
|
72
|
+
}
|
|
73
|
+
if (typeof current === "object") {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object.entries(current).map(([childKey, child]) => [
|
|
76
|
+
childKey,
|
|
77
|
+
visit(child, childKey)
|
|
78
|
+
])
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return String(current);
|
|
82
|
+
};
|
|
83
|
+
return visit(value);
|
|
84
|
+
}
|
|
85
|
+
function safeMetadata(input, allowedKeys) {
|
|
86
|
+
const selected = Object.fromEntries(
|
|
87
|
+
allowedKeys.filter((key) => isSafeMetadataScalar(input[key])).map((key) => [key, input[key]])
|
|
88
|
+
);
|
|
89
|
+
return redact(selected);
|
|
90
|
+
}
|
|
91
|
+
function isSafeMetadataScalar(value) {
|
|
92
|
+
return value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length <= 512;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/adapters/shared.ts
|
|
96
|
+
import { relative, resolve, sep } from "path";
|
|
97
|
+
var OUTCOME_KEYS = ["outcome", "status", "result"];
|
|
98
|
+
function mapHookPayload(source, rawEventName, payload, eventMap, metadataKeys) {
|
|
99
|
+
const outcomeValue = OUTCOME_KEYS.map((key) => payload[key]).find(
|
|
100
|
+
(value) => typeof value === "string"
|
|
101
|
+
);
|
|
102
|
+
const normalizedOutcome = outcomeValue === "success" || outcomeValue === "failure" || outcomeValue === "cancelled" ? outcomeValue : outcomeValue ? "unknown" : void 0;
|
|
103
|
+
const duration = payload.duration_ms ?? payload.durationMs;
|
|
104
|
+
return createHookEvent({
|
|
105
|
+
source,
|
|
106
|
+
kind: eventMap[normalizeName(rawEventName)] ?? "unknown",
|
|
107
|
+
...typeof payload.session_id === "string" ? { sessionId: payload.session_id } : {},
|
|
108
|
+
...normalizedOutcome ? { outcome: normalizedOutcome } : {},
|
|
109
|
+
...typeof duration === "number" && duration >= 0 ? { durationMs: duration } : {},
|
|
110
|
+
metadata: {
|
|
111
|
+
hookEvent: normalizeName(rawEventName),
|
|
112
|
+
...safeMetadata(payload, metadataKeys)
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function normalizeName(name) {
|
|
117
|
+
return name.trim().replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replaceAll(/[\s_.]+/g, "-");
|
|
118
|
+
}
|
|
119
|
+
function safeIdentifier(value) {
|
|
120
|
+
if (typeof value !== "string") return void 0;
|
|
121
|
+
const normalized = value.trim();
|
|
122
|
+
return normalized.length > 0 && normalized.length <= 160 && !/[\u0000-\u001f]/.test(normalized) ? normalized : void 0;
|
|
123
|
+
}
|
|
124
|
+
function safeModel(value) {
|
|
125
|
+
const normalized = safeIdentifier(value);
|
|
126
|
+
return normalized && /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(normalized) ? normalized : void 0;
|
|
127
|
+
}
|
|
128
|
+
function safeRelativePath(value, root = process.cwd()) {
|
|
129
|
+
if (typeof value !== "string" || value.includes("\0")) return void 0;
|
|
130
|
+
const candidate = value.trim();
|
|
131
|
+
if (!candidate) return void 0;
|
|
132
|
+
const resolvedRoot = resolve(root);
|
|
133
|
+
const resolvedPath = resolve(resolvedRoot, candidate);
|
|
134
|
+
const path = relative(resolvedRoot, resolvedPath);
|
|
135
|
+
if (path === ".." || path.startsWith(`..${sep}`)) return void 0;
|
|
136
|
+
return (path || ".").split(sep).join("/");
|
|
137
|
+
}
|
|
138
|
+
function safeDuration(value) {
|
|
139
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
140
|
+
}
|
|
141
|
+
function objectPayload(input) {
|
|
142
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/adapters/claude-code.ts
|
|
146
|
+
var events = {
|
|
147
|
+
"session-start": "session.started",
|
|
148
|
+
"session-end": "session.ended",
|
|
149
|
+
heartbeat: "presence.heartbeat",
|
|
150
|
+
"user-prompt-submit": "lifecycle.before",
|
|
151
|
+
stop: "lifecycle.after",
|
|
152
|
+
"pre-tool-use": "tool.started",
|
|
153
|
+
"post-tool-use": "tool.completed",
|
|
154
|
+
"post-tool-use-failure": "tool.completed",
|
|
155
|
+
notification: "lifecycle.after"
|
|
156
|
+
};
|
|
157
|
+
var safeKeys = [
|
|
158
|
+
"hook_event_name",
|
|
159
|
+
"tool_name",
|
|
160
|
+
"permission_mode",
|
|
161
|
+
"source",
|
|
162
|
+
"model",
|
|
163
|
+
"agent_id",
|
|
164
|
+
"path",
|
|
165
|
+
"lines_added",
|
|
166
|
+
"lines_deleted",
|
|
167
|
+
"session_started_at",
|
|
168
|
+
"heartbeat_at",
|
|
169
|
+
"heartbeat_interval_ms",
|
|
170
|
+
"session_elapsed_ms"
|
|
171
|
+
];
|
|
172
|
+
function mapClaudeCodeHook(eventName, input) {
|
|
173
|
+
const payload = objectPayload(input);
|
|
174
|
+
const toolInput = objectPayload(payload.tool_input);
|
|
175
|
+
const sessionId = safeIdentifier(payload.session_id);
|
|
176
|
+
const model = safeModel(payload.model);
|
|
177
|
+
const path = safeRelativePath(
|
|
178
|
+
toolInput.file_path ?? payload.file_path,
|
|
179
|
+
typeof payload.cwd === "string" ? payload.cwd : process.cwd()
|
|
180
|
+
);
|
|
181
|
+
const duration = safeDuration(payload.duration_ms ?? payload.durationMs);
|
|
182
|
+
const normalizedEvent = normalizeName(eventName);
|
|
183
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name.toLowerCase() : "";
|
|
184
|
+
const event = mapHookPayload(
|
|
185
|
+
"claude-code",
|
|
186
|
+
eventName,
|
|
187
|
+
{
|
|
188
|
+
...payload,
|
|
189
|
+
...sessionId ? { session_id: sessionId } : {},
|
|
190
|
+
...model ? { model } : {},
|
|
191
|
+
...path ? { path } : {},
|
|
192
|
+
...duration !== void 0 ? { duration_ms: duration } : {},
|
|
193
|
+
...normalizedEvent === "post-tool-use-failure" && !payload.outcome ? { outcome: "failure" } : {},
|
|
194
|
+
...typeof payload.lines_added === "number" ? { lines_added: payload.lines_added } : {},
|
|
195
|
+
...typeof payload.lines_deleted === "number" ? { lines_deleted: payload.lines_deleted } : {}
|
|
196
|
+
},
|
|
197
|
+
events,
|
|
198
|
+
safeKeys
|
|
199
|
+
);
|
|
200
|
+
return path && normalizedEvent === "post-tool-use" && (toolName === "write" || toolName === "edit") ? { ...event, kind: "file.changed" } : event;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/adapters/codex.ts
|
|
204
|
+
var events2 = {
|
|
205
|
+
"agent-turn-complete": "lifecycle.after",
|
|
206
|
+
"session-start": "session.started",
|
|
207
|
+
"session-end": "session.ended",
|
|
208
|
+
heartbeat: "presence.heartbeat",
|
|
209
|
+
"turn-start": "lifecycle.before",
|
|
210
|
+
"turn-complete": "lifecycle.after",
|
|
211
|
+
"tool-start": "tool.started",
|
|
212
|
+
"tool-complete": "tool.completed",
|
|
213
|
+
"command-start": "shell.started",
|
|
214
|
+
"command-complete": "shell.completed",
|
|
215
|
+
"file-change": "file.changed",
|
|
216
|
+
"user-prompt-submit": "lifecycle.before",
|
|
217
|
+
"pre-tool-use": "tool.started",
|
|
218
|
+
"post-tool-use": "tool.completed",
|
|
219
|
+
"subagent-start": "lifecycle.before",
|
|
220
|
+
"subagent-stop": "lifecycle.after",
|
|
221
|
+
stop: "lifecycle.after"
|
|
222
|
+
};
|
|
223
|
+
var safeKeys2 = [
|
|
224
|
+
"event",
|
|
225
|
+
"tool_name",
|
|
226
|
+
"command_type",
|
|
227
|
+
"model",
|
|
228
|
+
"reason",
|
|
229
|
+
"turn_id",
|
|
230
|
+
"path",
|
|
231
|
+
"lines_added",
|
|
232
|
+
"lines_deleted",
|
|
233
|
+
"session_started_at",
|
|
234
|
+
"heartbeat_at",
|
|
235
|
+
"heartbeat_interval_ms",
|
|
236
|
+
"session_elapsed_ms"
|
|
237
|
+
];
|
|
238
|
+
function mapCodexHook(eventName, input) {
|
|
239
|
+
const payload = objectPayload(input);
|
|
240
|
+
const toolInput = objectPayload(payload.tool_input);
|
|
241
|
+
const threadId = safeIdentifier(
|
|
242
|
+
payload["thread-id"] ?? payload.thread_id ?? payload.session_id
|
|
243
|
+
);
|
|
244
|
+
const turnId = safeIdentifier(payload["turn-id"] ?? payload.turn_id);
|
|
245
|
+
const model = safeModel(payload.model);
|
|
246
|
+
const path = safeRelativePath(
|
|
247
|
+
payload.file_path ?? payload.path ?? toolInput.file_path,
|
|
248
|
+
typeof payload.cwd === "string" ? payload.cwd : process.cwd()
|
|
249
|
+
);
|
|
250
|
+
return mapHookPayload(
|
|
251
|
+
"codex",
|
|
252
|
+
eventName,
|
|
253
|
+
{
|
|
254
|
+
...payload,
|
|
255
|
+
...threadId ? { session_id: threadId } : {},
|
|
256
|
+
...turnId ? { turn_id: turnId } : {},
|
|
257
|
+
...model ? { model } : {},
|
|
258
|
+
...path ? { path } : {},
|
|
259
|
+
...typeof payload.lines_added === "number" ? { lines_added: payload.lines_added } : {},
|
|
260
|
+
...typeof payload.lines_deleted === "number" ? { lines_deleted: payload.lines_deleted } : {}
|
|
261
|
+
},
|
|
262
|
+
events2,
|
|
263
|
+
safeKeys2
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/adapters/cursor.ts
|
|
268
|
+
var events3 = {
|
|
269
|
+
"session-start": "session.started",
|
|
270
|
+
"session-end": "session.ended",
|
|
271
|
+
heartbeat: "presence.heartbeat",
|
|
272
|
+
"before-submit-prompt": "lifecycle.before",
|
|
273
|
+
"after-agent-response": "lifecycle.after",
|
|
274
|
+
"before-tool": "tool.started",
|
|
275
|
+
"after-tool": "tool.completed",
|
|
276
|
+
"after-file-edit": "file.changed",
|
|
277
|
+
"before-shell-execution": "shell.started",
|
|
278
|
+
"after-shell-execution": "shell.completed",
|
|
279
|
+
"before-mcp-execution": "mcp.started",
|
|
280
|
+
"after-mcp-execution": "mcp.completed"
|
|
281
|
+
};
|
|
282
|
+
var safeKeys3 = [
|
|
283
|
+
"generation_id",
|
|
284
|
+
"tool_name",
|
|
285
|
+
"command_type",
|
|
286
|
+
"file_extension",
|
|
287
|
+
"language",
|
|
288
|
+
"model",
|
|
289
|
+
"path",
|
|
290
|
+
"lines_added",
|
|
291
|
+
"lines_deleted",
|
|
292
|
+
"session_started_at",
|
|
293
|
+
"heartbeat_at",
|
|
294
|
+
"heartbeat_interval_ms",
|
|
295
|
+
"session_elapsed_ms"
|
|
296
|
+
];
|
|
297
|
+
function mapCursorHook(eventName, input) {
|
|
298
|
+
const payload = objectPayload(input);
|
|
299
|
+
const sessionId = safeIdentifier(payload.conversation_id ?? payload.session_id);
|
|
300
|
+
const model = safeModel(payload.model);
|
|
301
|
+
const workspaceRoot = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.find(
|
|
302
|
+
(root) => typeof root === "string"
|
|
303
|
+
) : void 0;
|
|
304
|
+
const path = safeRelativePath(
|
|
305
|
+
payload.file_path,
|
|
306
|
+
workspaceRoot ?? process.cwd()
|
|
307
|
+
);
|
|
308
|
+
return mapHookPayload(
|
|
309
|
+
"cursor",
|
|
310
|
+
eventName,
|
|
311
|
+
{
|
|
312
|
+
...payload,
|
|
313
|
+
...sessionId ? { session_id: sessionId } : {},
|
|
314
|
+
...model ? { model } : {},
|
|
315
|
+
...path ? { path } : {},
|
|
316
|
+
...typeof payload.lines_added === "number" ? { lines_added: payload.lines_added } : {},
|
|
317
|
+
...typeof payload.lines_deleted === "number" ? { lines_deleted: payload.lines_deleted } : {}
|
|
318
|
+
},
|
|
319
|
+
events3,
|
|
320
|
+
safeKeys3
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/client.ts
|
|
325
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
326
|
+
function eventTypeForHook(event) {
|
|
327
|
+
if (event.kind === "session.started") return "presence.started";
|
|
328
|
+
if (event.kind === "presence.heartbeat") return "presence.heartbeat";
|
|
329
|
+
if (event.kind === "session.ended") return "presence.stopped";
|
|
330
|
+
if (event.kind === "file.changed") return "workspace.files_changed";
|
|
331
|
+
if (event.kind === "shell.completed" && event.outcome && isTestHookEvent(event)) {
|
|
332
|
+
return "workspace.test_completed";
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
function isTestHookEvent(event) {
|
|
337
|
+
const labels = [
|
|
338
|
+
event.metadata.command_type,
|
|
339
|
+
event.metadata.tool_name,
|
|
340
|
+
event.metadata.task_type
|
|
341
|
+
].filter((value) => typeof value === "string").join(" ").toLowerCase();
|
|
342
|
+
return /(^|\W)(test|tests|testing|vitest|jest|pytest|playwright)(\W|$)/.test(
|
|
343
|
+
labels
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
function toEnvelope(event, options) {
|
|
347
|
+
const type = eventTypeForHook(event);
|
|
348
|
+
if (!type) {
|
|
349
|
+
throw new Error(`Hook event ${event.kind} is not publishable`);
|
|
350
|
+
}
|
|
351
|
+
const paths = [
|
|
352
|
+
...Array.isArray(event.metadata.paths) ? event.metadata.paths.filter((path) => typeof path === "string") : [],
|
|
353
|
+
...typeof event.metadata.path === "string" ? [event.metadata.path] : []
|
|
354
|
+
];
|
|
355
|
+
const payload = event.kind === "file.changed" ? {
|
|
356
|
+
paths,
|
|
357
|
+
tool: event.source,
|
|
358
|
+
agent_name: event.source,
|
|
359
|
+
hook_kind: event.kind,
|
|
360
|
+
lines_added: typeof event.metadata.lines_added === "number" ? event.metadata.lines_added : void 0,
|
|
361
|
+
lines_deleted: typeof event.metadata.lines_deleted === "number" ? event.metadata.lines_deleted : void 0
|
|
362
|
+
} : event.kind === "presence.heartbeat" ? {
|
|
363
|
+
verification: event.metadata.verification,
|
|
364
|
+
sequence: event.metadata.sequence,
|
|
365
|
+
active_seconds: event.metadata.active_seconds,
|
|
366
|
+
lines_added: event.metadata.lines_added,
|
|
367
|
+
lines_deleted: event.metadata.lines_deleted,
|
|
368
|
+
changed_files: event.metadata.changed_files,
|
|
369
|
+
model: event.metadata.model,
|
|
370
|
+
model_active_seconds: event.metadata.model_active_seconds,
|
|
371
|
+
model_seconds: event.metadata.model_seconds,
|
|
372
|
+
tool_counts: event.metadata.tool_counts,
|
|
373
|
+
tests_passed: event.metadata.tests_passed,
|
|
374
|
+
tests_failed: event.metadata.tests_failed,
|
|
375
|
+
session_started_at: event.metadata.session_started_at,
|
|
376
|
+
heartbeat_at: event.metadata.heartbeat_at,
|
|
377
|
+
heartbeat_interval_ms: event.metadata.heartbeat_interval_ms,
|
|
378
|
+
session_elapsed_ms: event.metadata.session_elapsed_ms,
|
|
379
|
+
working_tree_lines_added: event.metadata.working_tree_lines_added,
|
|
380
|
+
working_tree_lines_deleted: event.metadata.working_tree_lines_deleted,
|
|
381
|
+
tool: event.source,
|
|
382
|
+
agent_name: event.source,
|
|
383
|
+
hook_kind: event.kind
|
|
384
|
+
} : {
|
|
385
|
+
paths,
|
|
386
|
+
tool: event.source,
|
|
387
|
+
agent_name: event.source,
|
|
388
|
+
outcome: event.outcome,
|
|
389
|
+
duration_ms: event.durationMs,
|
|
390
|
+
hook_kind: event.kind,
|
|
391
|
+
...event.metadata
|
|
392
|
+
};
|
|
393
|
+
return {
|
|
394
|
+
event_id: event.id,
|
|
395
|
+
schema_version: 1,
|
|
396
|
+
occurred_at: event.occurredAt,
|
|
397
|
+
organization_id: options.organizationId,
|
|
398
|
+
project_id: options.projectId,
|
|
399
|
+
repository_id: options.repositoryId,
|
|
400
|
+
device_id: options.deviceId,
|
|
401
|
+
source: "local_collector",
|
|
402
|
+
type,
|
|
403
|
+
visibility: "project",
|
|
404
|
+
idempotency_key: `${event.source}:${event.id}`,
|
|
405
|
+
correlation: {
|
|
406
|
+
session_id: event.sessionId,
|
|
407
|
+
branch: event.repo?.branch,
|
|
408
|
+
commit_sha: event.repo?.commit
|
|
409
|
+
},
|
|
410
|
+
payload
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
var SignedBatchClient = class {
|
|
414
|
+
constructor(options) {
|
|
415
|
+
this.options = options;
|
|
416
|
+
this.request = options.fetch ?? globalThis.fetch;
|
|
417
|
+
this.batchSize = options.batchSize ?? 50;
|
|
418
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
419
|
+
}
|
|
420
|
+
options;
|
|
421
|
+
request;
|
|
422
|
+
batchSize;
|
|
423
|
+
timeoutMs;
|
|
424
|
+
async capture(event) {
|
|
425
|
+
const parsed = canonicalHookEventSchema.parse(event);
|
|
426
|
+
if (!eventTypeForHook(parsed)) {
|
|
427
|
+
return { sent: 0, remaining: await this.options.queue.size() };
|
|
428
|
+
}
|
|
429
|
+
await this.options.queue.enqueue([
|
|
430
|
+
{
|
|
431
|
+
...parsed,
|
|
432
|
+
metadata: redact(parsed.metadata)
|
|
433
|
+
}
|
|
434
|
+
]);
|
|
435
|
+
return this.flush();
|
|
436
|
+
}
|
|
437
|
+
async flush() {
|
|
438
|
+
const queuedEvents = await this.options.queue.peek(this.batchSize);
|
|
439
|
+
if (queuedEvents.length === 0) return { sent: 0, remaining: 0 };
|
|
440
|
+
const events4 = queuedEvents.filter((event) => eventTypeForHook(event));
|
|
441
|
+
const ignoredIds = queuedEvents.filter((event) => !eventTypeForHook(event)).map((event) => event.id);
|
|
442
|
+
if (ignoredIds.length > 0) await this.options.queue.remove(ignoredIds);
|
|
443
|
+
if (events4.length === 0) {
|
|
444
|
+
return { sent: 0, remaining: await this.options.queue.size() };
|
|
445
|
+
}
|
|
446
|
+
const body = JSON.stringify({
|
|
447
|
+
batch_id: randomUUID2(),
|
|
448
|
+
events: events4.map((event) => toEnvelope(event, this.options))
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
const response = await this.request(this.options.endpoint, {
|
|
452
|
+
method: "POST",
|
|
453
|
+
body,
|
|
454
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
455
|
+
headers: {
|
|
456
|
+
"content-type": "application/json",
|
|
457
|
+
"user-agent": "wibe-bridge/1",
|
|
458
|
+
authorization: `Bearer ${this.options.accessToken}`
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
const requestId = response.headers.get("x-request-id");
|
|
463
|
+
let detail = "";
|
|
464
|
+
try {
|
|
465
|
+
const body2 = await response.json();
|
|
466
|
+
const code = typeof body2.code === "string" ? body2.code.slice(0, 80) : void 0;
|
|
467
|
+
const message = typeof body2.error === "string" ? body2.error.slice(0, 160) : void 0;
|
|
468
|
+
detail = [code, message].filter(Boolean).join(": ");
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
const suffix = [
|
|
472
|
+
detail ? `: ${detail}` : "",
|
|
473
|
+
requestId ? ` (request ${requestId.slice(0, 80)})` : ""
|
|
474
|
+
].join("");
|
|
475
|
+
throw new Error(`Collector returned HTTP ${response.status}${suffix}`);
|
|
476
|
+
}
|
|
477
|
+
await this.options.queue.remove(events4.map((event) => event.id));
|
|
478
|
+
return { sent: events4.length, remaining: await this.options.queue.size() };
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return {
|
|
481
|
+
sent: 0,
|
|
482
|
+
remaining: await this.options.queue.size(),
|
|
483
|
+
error: error instanceof Error ? error.message : String(error)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// src/queue.ts
|
|
490
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
491
|
+
import { dirname } from "path";
|
|
492
|
+
var MemoryOfflineQueue = class {
|
|
493
|
+
events = [];
|
|
494
|
+
async enqueue(events4) {
|
|
495
|
+
this.events.push(...events4);
|
|
496
|
+
}
|
|
497
|
+
async peek(limit) {
|
|
498
|
+
return this.events.slice(0, Math.max(0, limit));
|
|
499
|
+
}
|
|
500
|
+
async remove(ids) {
|
|
501
|
+
const removed = new Set(ids);
|
|
502
|
+
this.events = this.events.filter((event) => !removed.has(event.id));
|
|
503
|
+
}
|
|
504
|
+
async size() {
|
|
505
|
+
return this.events.length;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
var JsonFileOfflineQueue = class {
|
|
509
|
+
constructor(filePath) {
|
|
510
|
+
this.filePath = filePath;
|
|
511
|
+
}
|
|
512
|
+
filePath;
|
|
513
|
+
operation = Promise.resolve();
|
|
514
|
+
async enqueue(events4) {
|
|
515
|
+
await this.update((current) => [...current, ...events4]);
|
|
516
|
+
}
|
|
517
|
+
async peek(limit) {
|
|
518
|
+
await this.operation;
|
|
519
|
+
return (await this.read()).slice(0, Math.max(0, limit));
|
|
520
|
+
}
|
|
521
|
+
async remove(ids) {
|
|
522
|
+
const removed = new Set(ids);
|
|
523
|
+
await this.update((current) => current.filter((event) => !removed.has(event.id)));
|
|
524
|
+
}
|
|
525
|
+
async size() {
|
|
526
|
+
await this.operation;
|
|
527
|
+
return (await this.read()).length;
|
|
528
|
+
}
|
|
529
|
+
async read() {
|
|
530
|
+
try {
|
|
531
|
+
const data = JSON.parse(await readFile(this.filePath, "utf8"));
|
|
532
|
+
return canonicalHookEventSchema.array().parse(data);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (error.code === "ENOENT") return [];
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async update(transform) {
|
|
539
|
+
const next = this.operation.then(async () => {
|
|
540
|
+
const events4 = transform(await this.read());
|
|
541
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
|
|
542
|
+
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
543
|
+
await writeFile(temporary, JSON.stringify(events4), { encoding: "utf8", mode: 384 });
|
|
544
|
+
await rename(temporary, this.filePath);
|
|
545
|
+
});
|
|
546
|
+
this.operation = next.catch(() => void 0);
|
|
547
|
+
await next;
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/presence.ts
|
|
552
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
553
|
+
import { spawn } from "child_process";
|
|
554
|
+
import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
555
|
+
import { homedir } from "os";
|
|
556
|
+
import { join } from "path";
|
|
557
|
+
var PRESENCE_HEARTBEAT_INTERVAL_MS = 45e3;
|
|
558
|
+
var MAX_SESSION_DURATION_MS = 12 * 60 * 60 * 1e3;
|
|
559
|
+
async function startPresenceSession(source, sessionId, cwd = process.cwd()) {
|
|
560
|
+
const statePath = presenceStatePath(source, sessionId, cwd);
|
|
561
|
+
await rm(statePath, { force: true });
|
|
562
|
+
await mkdir2(presenceDirectory(), { recursive: true, mode: 448 });
|
|
563
|
+
const instanceId = randomUUID3();
|
|
564
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
565
|
+
const cliPath = process.argv[1];
|
|
566
|
+
const state = {
|
|
567
|
+
instanceId,
|
|
568
|
+
source,
|
|
569
|
+
...sessionId ? { sessionId } : {},
|
|
570
|
+
startedAt,
|
|
571
|
+
pid: 0,
|
|
572
|
+
sequence: 0,
|
|
573
|
+
linesAdded: 0,
|
|
574
|
+
linesDeleted: 0,
|
|
575
|
+
paths: [],
|
|
576
|
+
modelSeconds: {},
|
|
577
|
+
toolCounts: {},
|
|
578
|
+
testsPassed: 0,
|
|
579
|
+
testsFailed: 0,
|
|
580
|
+
measuredAt: startedAt
|
|
581
|
+
};
|
|
582
|
+
const metrics = metricsFromState(state, startedAt);
|
|
583
|
+
if (!cliPath) return metrics;
|
|
584
|
+
const child = spawn(
|
|
585
|
+
process.execPath,
|
|
586
|
+
[
|
|
587
|
+
...process.execArgv,
|
|
588
|
+
cliPath,
|
|
589
|
+
"_presence-heartbeat",
|
|
590
|
+
"--adapter",
|
|
591
|
+
source,
|
|
592
|
+
"--state",
|
|
593
|
+
statePath,
|
|
594
|
+
"--instance",
|
|
595
|
+
instanceId
|
|
596
|
+
],
|
|
597
|
+
{
|
|
598
|
+
cwd,
|
|
599
|
+
detached: true,
|
|
600
|
+
stdio: "ignore",
|
|
601
|
+
env: process.env
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
child.unref();
|
|
605
|
+
state.pid = child.pid ?? 0;
|
|
606
|
+
await writePresenceState(statePath, state);
|
|
607
|
+
return metrics;
|
|
608
|
+
}
|
|
609
|
+
async function updatePresenceSession(source, sessionId, event, cwd = process.cwd()) {
|
|
610
|
+
const statePath = presenceStatePath(source, sessionId, cwd);
|
|
611
|
+
const state = await readPresenceState(statePath);
|
|
612
|
+
if (!state) return void 0;
|
|
613
|
+
const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
614
|
+
accrueModelTime(state, measuredAt);
|
|
615
|
+
const model = typeof event.metadata.model === "string" ? event.metadata.model : void 0;
|
|
616
|
+
if (model) state.model = model;
|
|
617
|
+
state.linesAdded += safeCount(event.metadata.lines_added);
|
|
618
|
+
state.linesDeleted += safeCount(event.metadata.lines_deleted);
|
|
619
|
+
const workingLinesAdded = optionalCount(
|
|
620
|
+
event.metadata.working_tree_lines_added
|
|
621
|
+
);
|
|
622
|
+
const workingLinesDeleted = optionalCount(
|
|
623
|
+
event.metadata.working_tree_lines_deleted
|
|
624
|
+
);
|
|
625
|
+
if (workingLinesAdded !== void 0) {
|
|
626
|
+
state.baselineLinesAdded ??= workingLinesAdded;
|
|
627
|
+
state.linesAdded = Math.max(
|
|
628
|
+
state.linesAdded,
|
|
629
|
+
workingLinesAdded - state.baselineLinesAdded
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
if (workingLinesDeleted !== void 0) {
|
|
633
|
+
state.baselineLinesDeleted ??= workingLinesDeleted;
|
|
634
|
+
state.linesDeleted = Math.max(
|
|
635
|
+
state.linesDeleted,
|
|
636
|
+
workingLinesDeleted - state.baselineLinesDeleted
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const paths = [
|
|
640
|
+
...Array.isArray(event.metadata.paths) ? event.metadata.paths.filter(
|
|
641
|
+
(path) => typeof path === "string"
|
|
642
|
+
) : [],
|
|
643
|
+
...Array.isArray(event.metadata.working_tree_paths) ? event.metadata.working_tree_paths.filter(
|
|
644
|
+
(path) => typeof path === "string"
|
|
645
|
+
) : [],
|
|
646
|
+
...typeof event.metadata.path === "string" ? [event.metadata.path] : []
|
|
647
|
+
];
|
|
648
|
+
state.paths = [.../* @__PURE__ */ new Set([...state.paths, ...paths])].slice(0, 2e3);
|
|
649
|
+
const category = toolCategory(event);
|
|
650
|
+
if (category) {
|
|
651
|
+
state.toolCounts[category] = (state.toolCounts[category] ?? 0) + 1;
|
|
652
|
+
}
|
|
653
|
+
if (event.kind === "shell.completed" && isTestEvent(event)) {
|
|
654
|
+
if (event.outcome === "success") state.testsPassed += 1;
|
|
655
|
+
if (event.outcome === "failure") state.testsFailed += 1;
|
|
656
|
+
}
|
|
657
|
+
await writePresenceState(statePath, state);
|
|
658
|
+
return metricsFromState(state, measuredAt);
|
|
659
|
+
}
|
|
660
|
+
async function stopPresenceSession(source, sessionId, cwd = process.cwd()) {
|
|
661
|
+
const statePath = presenceStatePath(source, sessionId, cwd);
|
|
662
|
+
const state = await readPresenceState(statePath);
|
|
663
|
+
await rm(statePath, { force: true });
|
|
664
|
+
if (!state) return void 0;
|
|
665
|
+
const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
666
|
+
accrueModelTime(state, measuredAt);
|
|
667
|
+
state.sequence += 1;
|
|
668
|
+
return metricsFromState(state, measuredAt);
|
|
669
|
+
}
|
|
670
|
+
async function runPresenceHeartbeat(statePath, expectedInstanceId, emit, intervalMs = PRESENCE_HEARTBEAT_INTERVAL_MS) {
|
|
671
|
+
let nextHeartbeat = Date.now() + intervalMs;
|
|
672
|
+
for (; ; ) {
|
|
673
|
+
await delay(Math.min(1e3, Math.max(0, nextHeartbeat - Date.now())));
|
|
674
|
+
const state = await readPresenceState(statePath);
|
|
675
|
+
if (!state || state.instanceId !== expectedInstanceId) return;
|
|
676
|
+
if (Date.now() < nextHeartbeat) continue;
|
|
677
|
+
const heartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
678
|
+
if (Date.parse(heartbeatAt) - Date.parse(state.startedAt) > MAX_SESSION_DURATION_MS) {
|
|
679
|
+
await rm(statePath, { force: true });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
accrueModelTime(state, heartbeatAt);
|
|
683
|
+
state.sequence += 1;
|
|
684
|
+
await writePresenceState(statePath, state);
|
|
685
|
+
await emit(state.source, "heartbeat", {
|
|
686
|
+
...state.sessionId ? { session_id: state.sessionId } : {},
|
|
687
|
+
heartbeat_at: heartbeatAt,
|
|
688
|
+
heartbeat_interval_ms: intervalMs,
|
|
689
|
+
...metricsFromState(state, heartbeatAt)
|
|
690
|
+
});
|
|
691
|
+
nextHeartbeat += intervalMs;
|
|
692
|
+
if (nextHeartbeat <= Date.now()) nextHeartbeat = Date.now() + intervalMs;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function presenceStatePath(source, sessionId, cwd = process.cwd()) {
|
|
696
|
+
const key = createHash("sha256").update(`${source}\0${sessionId ?? ""}\0${cwd}`).digest("hex");
|
|
697
|
+
return join(presenceDirectory(), `${key}.json`);
|
|
698
|
+
}
|
|
699
|
+
function presenceDirectory() {
|
|
700
|
+
return process.env.WIBE_PRESENCE_DIR ?? join(homedir(), ".wibe", "presence");
|
|
701
|
+
}
|
|
702
|
+
async function readPresenceState(path) {
|
|
703
|
+
try {
|
|
704
|
+
const value = JSON.parse(await readFile2(path, "utf8"));
|
|
705
|
+
if (typeof value.instanceId !== "string" || value.source !== "cursor" && value.source !== "claude-code" && value.source !== "codex" || typeof value.startedAt !== "string" || !Number.isFinite(Date.parse(value.startedAt)) || typeof value.pid !== "number") {
|
|
706
|
+
return void 0;
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
instanceId: value.instanceId,
|
|
710
|
+
source: value.source,
|
|
711
|
+
sessionId: typeof value.sessionId === "string" ? value.sessionId : void 0,
|
|
712
|
+
startedAt: value.startedAt,
|
|
713
|
+
pid: value.pid,
|
|
714
|
+
sequence: safeCount(value.sequence),
|
|
715
|
+
linesAdded: safeCount(value.linesAdded),
|
|
716
|
+
linesDeleted: safeCount(value.linesDeleted),
|
|
717
|
+
baselineLinesAdded: optionalCount(value.baselineLinesAdded),
|
|
718
|
+
baselineLinesDeleted: optionalCount(value.baselineLinesDeleted),
|
|
719
|
+
paths: Array.isArray(value.paths) ? value.paths.filter((path2) => typeof path2 === "string") : [],
|
|
720
|
+
model: typeof value.model === "string" ? value.model : void 0,
|
|
721
|
+
modelSeconds: safeCountRecord(value.modelSeconds),
|
|
722
|
+
toolCounts: safeCountRecord(value.toolCounts),
|
|
723
|
+
testsPassed: safeCount(value.testsPassed),
|
|
724
|
+
testsFailed: safeCount(value.testsFailed),
|
|
725
|
+
measuredAt: typeof value.measuredAt === "string" && Number.isFinite(Date.parse(value.measuredAt)) ? value.measuredAt : value.startedAt
|
|
726
|
+
};
|
|
727
|
+
} catch {
|
|
728
|
+
return void 0;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function writePresenceState(path, state) {
|
|
732
|
+
await writeFile2(path, `${JSON.stringify(state)}
|
|
733
|
+
`, { mode: 384 });
|
|
734
|
+
}
|
|
735
|
+
function accrueModelTime(state, measuredAt) {
|
|
736
|
+
const elapsedSeconds = Math.max(
|
|
737
|
+
0,
|
|
738
|
+
Math.floor(
|
|
739
|
+
(Date.parse(measuredAt) - Date.parse(state.measuredAt || state.startedAt)) / 1e3
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
if (state.model && elapsedSeconds > 0) {
|
|
743
|
+
state.modelSeconds[state.model] = (state.modelSeconds[state.model] ?? 0) + elapsedSeconds;
|
|
744
|
+
}
|
|
745
|
+
state.measuredAt = measuredAt;
|
|
746
|
+
}
|
|
747
|
+
function metricsFromState(state, measuredAt) {
|
|
748
|
+
const sessionElapsedMs = Math.max(
|
|
749
|
+
0,
|
|
750
|
+
Date.parse(measuredAt) - Date.parse(state.startedAt)
|
|
751
|
+
);
|
|
752
|
+
return {
|
|
753
|
+
session_started_at: state.startedAt,
|
|
754
|
+
session_elapsed_ms: sessionElapsedMs,
|
|
755
|
+
sequence: state.sequence,
|
|
756
|
+
active_seconds: Math.floor(sessionElapsedMs / 1e3),
|
|
757
|
+
lines_added: state.linesAdded,
|
|
758
|
+
lines_deleted: state.linesDeleted,
|
|
759
|
+
changed_files: state.paths.length,
|
|
760
|
+
model_seconds: state.modelSeconds,
|
|
761
|
+
tool_counts: state.toolCounts,
|
|
762
|
+
tests_passed: state.testsPassed,
|
|
763
|
+
tests_failed: state.testsFailed
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function safeCount(value) {
|
|
767
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
|
|
768
|
+
}
|
|
769
|
+
function optionalCount(value) {
|
|
770
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
|
|
771
|
+
}
|
|
772
|
+
function safeCountRecord(value) {
|
|
773
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
774
|
+
return Object.fromEntries(
|
|
775
|
+
Object.entries(value).filter(
|
|
776
|
+
(entry) => typeof entry[1] === "number" && Number.isFinite(entry[1]) && entry[1] >= 0
|
|
777
|
+
).map(([key, count]) => [key.slice(0, 160), Math.floor(count)])
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
function toolCategory(event) {
|
|
781
|
+
if (event.kind === "file.changed") return "edit";
|
|
782
|
+
if (event.kind === "shell.started" || event.kind === "shell.completed") {
|
|
783
|
+
return "shell";
|
|
784
|
+
}
|
|
785
|
+
if (event.kind === "mcp.started" || event.kind === "mcp.completed") {
|
|
786
|
+
return "mcp";
|
|
787
|
+
}
|
|
788
|
+
if (event.kind === "tool.started" || event.kind === "tool.completed") {
|
|
789
|
+
return "tool";
|
|
790
|
+
}
|
|
791
|
+
return void 0;
|
|
792
|
+
}
|
|
793
|
+
function isTestEvent(event) {
|
|
794
|
+
const labels = [
|
|
795
|
+
event.metadata.command_type,
|
|
796
|
+
event.metadata.tool_name,
|
|
797
|
+
event.metadata.task_type
|
|
798
|
+
].filter((value) => typeof value === "string").join(" ").toLowerCase();
|
|
799
|
+
return /(^|\W)(test|tests|testing|vitest|jest|pytest|playwright)(\W|$)/.test(
|
|
800
|
+
labels
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
function delay(milliseconds) {
|
|
804
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/repo.ts
|
|
808
|
+
import { execFile } from "child_process";
|
|
809
|
+
import { promisify } from "util";
|
|
810
|
+
var execFileAsync = promisify(execFile);
|
|
811
|
+
async function git(cwd, args) {
|
|
812
|
+
try {
|
|
813
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
814
|
+
cwd,
|
|
815
|
+
encoding: "utf8",
|
|
816
|
+
timeout: 2e3,
|
|
817
|
+
maxBuffer: 64 * 1024
|
|
818
|
+
});
|
|
819
|
+
return stdout.trim() || void 0;
|
|
820
|
+
} catch {
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function detectRepository(cwd = process.cwd()) {
|
|
825
|
+
const root = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
826
|
+
if (!root) return void 0;
|
|
827
|
+
const [remote, branch, commit] = await Promise.all([
|
|
828
|
+
git(root, ["remote", "get-url", "origin"]),
|
|
829
|
+
git(root, ["branch", "--show-current"]),
|
|
830
|
+
git(root, ["rev-parse", "HEAD"])
|
|
831
|
+
]);
|
|
832
|
+
return {
|
|
833
|
+
root,
|
|
834
|
+
...remote ? { remote: sanitizeRemote(remote) } : {},
|
|
835
|
+
...branch ? { branch } : {},
|
|
836
|
+
...commit ? { commit } : {}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async function detectWorkingTreeMetrics(root) {
|
|
840
|
+
const output = await git(root, ["diff", "--numstat", "HEAD", "--"]);
|
|
841
|
+
if (output === void 0) return void 0;
|
|
842
|
+
let linesAdded = 0;
|
|
843
|
+
let linesDeleted = 0;
|
|
844
|
+
const paths = [];
|
|
845
|
+
for (const line of output.split("\n")) {
|
|
846
|
+
if (!line) continue;
|
|
847
|
+
const [added, deleted, ...pathParts] = line.split(" ");
|
|
848
|
+
const path = pathParts.join(" ");
|
|
849
|
+
if (!path || isSensitivePath(path)) continue;
|
|
850
|
+
if (/^\d+$/.test(added)) linesAdded += Number(added);
|
|
851
|
+
if (/^\d+$/.test(deleted)) linesDeleted += Number(deleted);
|
|
852
|
+
paths.push(path);
|
|
853
|
+
}
|
|
854
|
+
return { linesAdded, linesDeleted, paths: [...new Set(paths)].slice(0, 2e3) };
|
|
855
|
+
}
|
|
856
|
+
function sanitizeRemote(remote) {
|
|
857
|
+
try {
|
|
858
|
+
const url = new URL(remote);
|
|
859
|
+
url.username = "";
|
|
860
|
+
url.password = "";
|
|
861
|
+
return url.toString().replace(/\/$/, "");
|
|
862
|
+
} catch {
|
|
863
|
+
return remote.replace(
|
|
864
|
+
/^(?:[^@\s]+@)?([^:\s]+):(.+)$/,
|
|
865
|
+
(_match, host, path) => `${host}:${path}`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function normalizeGitHubRepository(value) {
|
|
870
|
+
const trimmed = value.trim();
|
|
871
|
+
if (!trimmed) return void 0;
|
|
872
|
+
let path = trimmed;
|
|
873
|
+
try {
|
|
874
|
+
const url = new URL(trimmed);
|
|
875
|
+
if (url.hostname.toLowerCase() !== "github.com") return void 0;
|
|
876
|
+
path = url.pathname;
|
|
877
|
+
} catch {
|
|
878
|
+
const scpLike = trimmed.match(
|
|
879
|
+
/^(?:(?:[^@\s]+)@)?github\.com:(.+)$/i
|
|
880
|
+
);
|
|
881
|
+
if (scpLike) {
|
|
882
|
+
path = scpLike[1];
|
|
883
|
+
} else if (/^github\.com\//i.test(trimmed)) {
|
|
884
|
+
path = trimmed.replace(/^github\.com\//i, "");
|
|
885
|
+
} else if (/^[^/\s]+\/[^/\s]+\/?$/.test(trimmed)) {
|
|
886
|
+
path = trimmed;
|
|
887
|
+
} else {
|
|
888
|
+
return void 0;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const normalized = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").toLowerCase();
|
|
892
|
+
return /^[^/\s]+\/[^/\s]+$/.test(normalized) ? normalized : void 0;
|
|
893
|
+
}
|
|
894
|
+
function matchesGitHubRepository(remote, expected) {
|
|
895
|
+
const normalizedRemote = remote ? normalizeGitHubRepository(remote) : void 0;
|
|
896
|
+
const normalizedExpected = normalizeGitHubRepository(expected);
|
|
897
|
+
return Boolean(
|
|
898
|
+
normalizedRemote && normalizedExpected && normalizedRemote === normalizedExpected
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
function isSensitivePath(path) {
|
|
902
|
+
return /(^|\/)\.env(?:\.|$)/i.test(path) || /(^|\/)(?:id_rsa|id_ed25519|credentials|secrets?)(?:\.|$)/i.test(path) || /\.(?:pem|key|p12|pfx)$/i.test(path);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/auth.ts
|
|
906
|
+
import { z as z2 } from "zod";
|
|
907
|
+
import { execFile as execFileCallback } from "child_process";
|
|
908
|
+
import { promisify as promisify2 } from "util";
|
|
909
|
+
var execFile2 = promisify2(execFileCallback);
|
|
910
|
+
var deviceAuthorizationSchema = z2.object({
|
|
911
|
+
device_code: z2.string().min(1),
|
|
912
|
+
user_code: z2.string().min(1),
|
|
913
|
+
verification_uri: z2.string().url(),
|
|
914
|
+
verification_uri_complete: z2.string().url().optional(),
|
|
915
|
+
expires_at: z2.string().datetime(),
|
|
916
|
+
interval: z2.number().int().positive().default(5)
|
|
917
|
+
});
|
|
918
|
+
var deviceTokenResponseSchema = z2.discriminatedUnion("status", [
|
|
919
|
+
z2.object({
|
|
920
|
+
status: z2.literal("approved"),
|
|
921
|
+
accessToken: z2.string().min(1),
|
|
922
|
+
projectId: z2.string().uuid(),
|
|
923
|
+
organizationId: z2.string().uuid(),
|
|
924
|
+
repositoryId: z2.string().uuid().optional(),
|
|
925
|
+
deviceId: z2.string().uuid()
|
|
926
|
+
}),
|
|
927
|
+
z2.object({
|
|
928
|
+
status: z2.enum(["pending", "denied"])
|
|
929
|
+
}),
|
|
930
|
+
z2.object({
|
|
931
|
+
status: z2.enum(["expired", "invalid"])
|
|
932
|
+
})
|
|
933
|
+
]);
|
|
934
|
+
var SystemCredentialStore = class {
|
|
935
|
+
async get(service, account) {
|
|
936
|
+
try {
|
|
937
|
+
if (process.platform === "darwin") {
|
|
938
|
+
const { stdout } = await execFile2("security", [
|
|
939
|
+
"find-generic-password",
|
|
940
|
+
"-s",
|
|
941
|
+
service,
|
|
942
|
+
"-a",
|
|
943
|
+
account,
|
|
944
|
+
"-w"
|
|
945
|
+
]);
|
|
946
|
+
return stdout.trim() || void 0;
|
|
947
|
+
}
|
|
948
|
+
if (process.platform === "linux") {
|
|
949
|
+
const { stdout } = await execFile2("secret-tool", [
|
|
950
|
+
"lookup",
|
|
951
|
+
"service",
|
|
952
|
+
service,
|
|
953
|
+
"account",
|
|
954
|
+
account
|
|
955
|
+
]);
|
|
956
|
+
return stdout.trim() || void 0;
|
|
957
|
+
}
|
|
958
|
+
throw new Error("Use WIBE_ACCESS_TOKEN on platforms without a supported keychain.");
|
|
959
|
+
} catch (error) {
|
|
960
|
+
if (error instanceof Error && error.message.startsWith("Use WIBE_")) throw error;
|
|
961
|
+
return void 0;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async set(service, account, value) {
|
|
965
|
+
if (process.platform === "darwin") {
|
|
966
|
+
await execFile2("security", [
|
|
967
|
+
"add-generic-password",
|
|
968
|
+
"-U",
|
|
969
|
+
"-s",
|
|
970
|
+
service,
|
|
971
|
+
"-a",
|
|
972
|
+
account,
|
|
973
|
+
"-w",
|
|
974
|
+
value
|
|
975
|
+
]);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (process.platform === "linux") {
|
|
979
|
+
await new Promise((resolve2, reject) => {
|
|
980
|
+
const child = execFileCallback(
|
|
981
|
+
"secret-tool",
|
|
982
|
+
["store", "--label=Wibe agent bridge", "service", service, "account", account],
|
|
983
|
+
(error) => error ? reject(error) : resolve2()
|
|
984
|
+
);
|
|
985
|
+
child.stdin?.end(value);
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
throw new Error("No supported OS keychain was found.");
|
|
990
|
+
}
|
|
991
|
+
async delete(service, account) {
|
|
992
|
+
if (process.platform === "darwin") {
|
|
993
|
+
await execFile2("security", [
|
|
994
|
+
"delete-generic-password",
|
|
995
|
+
"-s",
|
|
996
|
+
service,
|
|
997
|
+
"-a",
|
|
998
|
+
account
|
|
999
|
+
]).catch(() => void 0);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (process.platform === "linux") {
|
|
1003
|
+
await execFile2("secret-tool", [
|
|
1004
|
+
"clear",
|
|
1005
|
+
"service",
|
|
1006
|
+
service,
|
|
1007
|
+
"account",
|
|
1008
|
+
account
|
|
1009
|
+
]).catch(() => void 0);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
async function requestDeviceAuthorization(input) {
|
|
1015
|
+
const response = await fetch(
|
|
1016
|
+
`${input.appUrl.replace(/\/$/, "")}/api/devices/authorize`,
|
|
1017
|
+
{
|
|
1018
|
+
method: "POST",
|
|
1019
|
+
headers: { "content-type": "application/json" },
|
|
1020
|
+
body: JSON.stringify({
|
|
1021
|
+
projectId: input.projectId,
|
|
1022
|
+
deviceName: input.deviceName,
|
|
1023
|
+
agentName: input.agentName
|
|
1024
|
+
})
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
if (!response.ok) throw new Error(`Device authorization failed (${response.status}).`);
|
|
1028
|
+
return deviceAuthorizationSchema.parse(await response.json());
|
|
1029
|
+
}
|
|
1030
|
+
async function pollDeviceToken(input) {
|
|
1031
|
+
const response = await fetch(
|
|
1032
|
+
`${input.appUrl.replace(/\/$/, "")}/api/devices/authorize`,
|
|
1033
|
+
{
|
|
1034
|
+
method: "PUT",
|
|
1035
|
+
headers: { "content-type": "application/json" },
|
|
1036
|
+
body: JSON.stringify({ device_code: input.deviceCode })
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
if (!response.ok) throw new Error(`Device polling failed (${response.status}).`);
|
|
1040
|
+
return deviceTokenResponseSchema.parse(await response.json());
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export {
|
|
1044
|
+
agentSourceSchema,
|
|
1045
|
+
hookEventKindSchema,
|
|
1046
|
+
safeValueSchema,
|
|
1047
|
+
canonicalHookEventSchema,
|
|
1048
|
+
createHookEvent,
|
|
1049
|
+
redact,
|
|
1050
|
+
safeMetadata,
|
|
1051
|
+
mapClaudeCodeHook,
|
|
1052
|
+
mapCodexHook,
|
|
1053
|
+
mapCursorHook,
|
|
1054
|
+
eventTypeForHook,
|
|
1055
|
+
SignedBatchClient,
|
|
1056
|
+
MemoryOfflineQueue,
|
|
1057
|
+
JsonFileOfflineQueue,
|
|
1058
|
+
PRESENCE_HEARTBEAT_INTERVAL_MS,
|
|
1059
|
+
startPresenceSession,
|
|
1060
|
+
updatePresenceSession,
|
|
1061
|
+
stopPresenceSession,
|
|
1062
|
+
runPresenceHeartbeat,
|
|
1063
|
+
presenceStatePath,
|
|
1064
|
+
detectRepository,
|
|
1065
|
+
detectWorkingTreeMetrics,
|
|
1066
|
+
sanitizeRemote,
|
|
1067
|
+
normalizeGitHubRepository,
|
|
1068
|
+
matchesGitHubRepository,
|
|
1069
|
+
deviceAuthorizationSchema,
|
|
1070
|
+
deviceTokenResponseSchema,
|
|
1071
|
+
SystemCredentialStore,
|
|
1072
|
+
requestDeviceAuthorization,
|
|
1073
|
+
pollDeviceToken
|
|
1074
|
+
};
|