@wibeco/bridge 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/README.md +46 -0
- package/dist/chunk-5CBPZCBE.js +427 -0
- package/dist/chunk-BL74PDGC.js +591 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +71 -0
- package/dist/codex-hook.d.ts +1 -0
- package/dist/codex-hook.js +22 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +48 -0
- package/package.json +42 -0
- package/templates/claude-code/README.md +14 -0
- package/templates/claude-code/mcp.json.example +8 -0
- package/templates/claude-code/settings.json.example +77 -0
- package/templates/codex/README.md +13 -0
- package/templates/codex/config.toml.example +6 -0
- package/templates/codex/hooks.json.example +76 -0
- package/templates/cursor/README.md +13 -0
- package/templates/cursor/hooks.json.example +60 -0
- package/templates/cursor/mcp.json.example +7 -0
|
@@ -0,0 +1,591 @@
|
|
|
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
|
+
"session.started",
|
|
7
|
+
"session.ended",
|
|
8
|
+
"lifecycle.before",
|
|
9
|
+
"lifecycle.after",
|
|
10
|
+
"tool.started",
|
|
11
|
+
"tool.completed",
|
|
12
|
+
"file.changed",
|
|
13
|
+
"shell.started",
|
|
14
|
+
"shell.completed",
|
|
15
|
+
"mcp.started",
|
|
16
|
+
"mcp.completed",
|
|
17
|
+
"unknown"
|
|
18
|
+
]);
|
|
19
|
+
var safeScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
20
|
+
var safeValueSchema = z.lazy(
|
|
21
|
+
() => z.union([safeScalarSchema, z.array(safeValueSchema), z.record(z.string(), safeValueSchema)])
|
|
22
|
+
);
|
|
23
|
+
var canonicalHookEventSchema = z.object({
|
|
24
|
+
id: z.string().min(1),
|
|
25
|
+
version: z.literal(1),
|
|
26
|
+
source: agentSourceSchema,
|
|
27
|
+
kind: hookEventKindSchema,
|
|
28
|
+
occurredAt: z.string().datetime(),
|
|
29
|
+
sessionId: z.string().min(1).optional(),
|
|
30
|
+
repo: z.object({
|
|
31
|
+
root: z.string().min(1),
|
|
32
|
+
remote: z.string().min(1).optional(),
|
|
33
|
+
branch: z.string().min(1).optional(),
|
|
34
|
+
commit: z.string().min(1).optional()
|
|
35
|
+
}).optional(),
|
|
36
|
+
outcome: z.enum(["success", "failure", "cancelled", "unknown"]).optional(),
|
|
37
|
+
durationMs: z.number().nonnegative().optional(),
|
|
38
|
+
metadata: z.record(z.string(), safeValueSchema).default({})
|
|
39
|
+
});
|
|
40
|
+
function createHookEvent(input) {
|
|
41
|
+
return canonicalHookEventSchema.parse({
|
|
42
|
+
...input,
|
|
43
|
+
id: input.id ?? randomUUID(),
|
|
44
|
+
version: 1,
|
|
45
|
+
occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/redaction.ts
|
|
50
|
+
var SENSITIVE_KEY = /authorization|cookie|token|secret|password|passwd|api.?key|prompt|content|input|output|message|transcript/i;
|
|
51
|
+
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;
|
|
52
|
+
var PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
53
|
+
function redact(value, options = {}) {
|
|
54
|
+
const replacement = options.replacement ?? "[REDACTED]";
|
|
55
|
+
const visit = (current, key) => {
|
|
56
|
+
if (key && SENSITIVE_KEY.test(key) && !options.allowContent) {
|
|
57
|
+
return replacement;
|
|
58
|
+
}
|
|
59
|
+
if (current === null || typeof current === "boolean" || typeof current === "number") {
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
if (typeof current === "string") {
|
|
63
|
+
let result = current.replace(PRIVATE_KEY, replacement).replace(SECRET_VALUE, replacement);
|
|
64
|
+
if (options.homeDirectory) {
|
|
65
|
+
result = result.replaceAll(options.homeDirectory, "~");
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(current)) {
|
|
70
|
+
return current.map((item) => visit(item));
|
|
71
|
+
}
|
|
72
|
+
if (typeof current === "object") {
|
|
73
|
+
return Object.fromEntries(
|
|
74
|
+
Object.entries(current).map(([childKey, child]) => [
|
|
75
|
+
childKey,
|
|
76
|
+
visit(child, childKey)
|
|
77
|
+
])
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return String(current);
|
|
81
|
+
};
|
|
82
|
+
return visit(value);
|
|
83
|
+
}
|
|
84
|
+
function safeMetadata(input, allowedKeys) {
|
|
85
|
+
const selected = Object.fromEntries(
|
|
86
|
+
allowedKeys.filter((key) => input[key] !== void 0).map((key) => [key, input[key]])
|
|
87
|
+
);
|
|
88
|
+
return redact(selected);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/adapters/shared.ts
|
|
92
|
+
var OUTCOME_KEYS = ["outcome", "status", "result"];
|
|
93
|
+
function mapHookPayload(source, rawEventName, payload, eventMap, metadataKeys) {
|
|
94
|
+
const outcomeValue = OUTCOME_KEYS.map((key) => payload[key]).find(
|
|
95
|
+
(value) => typeof value === "string"
|
|
96
|
+
);
|
|
97
|
+
const normalizedOutcome = outcomeValue === "success" || outcomeValue === "failure" || outcomeValue === "cancelled" ? outcomeValue : outcomeValue ? "unknown" : void 0;
|
|
98
|
+
const duration = payload.duration_ms ?? payload.durationMs;
|
|
99
|
+
return createHookEvent({
|
|
100
|
+
source,
|
|
101
|
+
kind: eventMap[normalizeName(rawEventName)] ?? "unknown",
|
|
102
|
+
...typeof payload.session_id === "string" ? { sessionId: payload.session_id } : {},
|
|
103
|
+
...normalizedOutcome ? { outcome: normalizedOutcome } : {},
|
|
104
|
+
...typeof duration === "number" && duration >= 0 ? { durationMs: duration } : {},
|
|
105
|
+
metadata: {
|
|
106
|
+
hookEvent: normalizeName(rawEventName),
|
|
107
|
+
...safeMetadata(payload, metadataKeys)
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function normalizeName(name) {
|
|
112
|
+
return name.trim().replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replaceAll(/[\s_.]+/g, "-");
|
|
113
|
+
}
|
|
114
|
+
function objectPayload(input) {
|
|
115
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/adapters/claude-code.ts
|
|
119
|
+
var events = {
|
|
120
|
+
"session-start": "session.started",
|
|
121
|
+
"session-end": "session.ended",
|
|
122
|
+
"user-prompt-submit": "lifecycle.before",
|
|
123
|
+
stop: "lifecycle.after",
|
|
124
|
+
"pre-tool-use": "tool.started",
|
|
125
|
+
"post-tool-use": "tool.completed",
|
|
126
|
+
"post-tool-use-failure": "tool.completed",
|
|
127
|
+
notification: "lifecycle.after"
|
|
128
|
+
};
|
|
129
|
+
var safeKeys = [
|
|
130
|
+
"hook_event_name",
|
|
131
|
+
"tool_name",
|
|
132
|
+
"permission_mode",
|
|
133
|
+
"source",
|
|
134
|
+
"model",
|
|
135
|
+
"agent_id"
|
|
136
|
+
];
|
|
137
|
+
function mapClaudeCodeHook(eventName, input) {
|
|
138
|
+
return mapHookPayload("claude-code", eventName, objectPayload(input), events, safeKeys);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/adapters/codex.ts
|
|
142
|
+
var events2 = {
|
|
143
|
+
"agent-turn-complete": "lifecycle.after",
|
|
144
|
+
"session-start": "session.started",
|
|
145
|
+
"session-end": "session.ended",
|
|
146
|
+
"turn-start": "lifecycle.before",
|
|
147
|
+
"turn-complete": "lifecycle.after",
|
|
148
|
+
"tool-start": "tool.started",
|
|
149
|
+
"tool-complete": "tool.completed",
|
|
150
|
+
"command-start": "shell.started",
|
|
151
|
+
"command-complete": "shell.completed",
|
|
152
|
+
"file-change": "file.changed"
|
|
153
|
+
};
|
|
154
|
+
var safeKeys2 = ["event", "tool_name", "command_type", "model", "reason", "turn_id"];
|
|
155
|
+
function mapCodexHook(eventName, input) {
|
|
156
|
+
return mapHookPayload("codex", eventName, objectPayload(input), events2, safeKeys2);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/adapters/cursor.ts
|
|
160
|
+
var events3 = {
|
|
161
|
+
"session-start": "session.started",
|
|
162
|
+
"session-end": "session.ended",
|
|
163
|
+
"before-submit-prompt": "lifecycle.before",
|
|
164
|
+
"after-agent-response": "lifecycle.after",
|
|
165
|
+
"before-tool": "tool.started",
|
|
166
|
+
"after-tool": "tool.completed",
|
|
167
|
+
"after-file-edit": "file.changed",
|
|
168
|
+
"before-shell-execution": "shell.started",
|
|
169
|
+
"after-shell-execution": "shell.completed",
|
|
170
|
+
"before-mcp-execution": "mcp.started",
|
|
171
|
+
"after-mcp-execution": "mcp.completed"
|
|
172
|
+
};
|
|
173
|
+
var safeKeys3 = [
|
|
174
|
+
"conversation_id",
|
|
175
|
+
"generation_id",
|
|
176
|
+
"tool_name",
|
|
177
|
+
"command_type",
|
|
178
|
+
"file_extension",
|
|
179
|
+
"language",
|
|
180
|
+
"workspace_roots"
|
|
181
|
+
];
|
|
182
|
+
function mapCursorHook(eventName, input) {
|
|
183
|
+
return mapHookPayload("cursor", eventName, objectPayload(input), events3, safeKeys3);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/client.ts
|
|
187
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
188
|
+
function eventType(event) {
|
|
189
|
+
if (event.kind === "session.started") return "agent.session_started";
|
|
190
|
+
if (event.kind === "session.ended") return "agent.session_ended";
|
|
191
|
+
if (event.kind === "file.changed") return "workspace.files_changed";
|
|
192
|
+
if (event.kind === "shell.completed" && event.outcome) {
|
|
193
|
+
return "workspace.test_completed";
|
|
194
|
+
}
|
|
195
|
+
return "presence.heartbeat";
|
|
196
|
+
}
|
|
197
|
+
function toEnvelope(event, options) {
|
|
198
|
+
const paths = [
|
|
199
|
+
...Array.isArray(event.metadata.paths) ? event.metadata.paths.filter((path) => typeof path === "string") : [],
|
|
200
|
+
...typeof event.metadata.path === "string" ? [event.metadata.path] : []
|
|
201
|
+
];
|
|
202
|
+
const payload = event.kind === "file.changed" ? {
|
|
203
|
+
paths,
|
|
204
|
+
tool: event.source,
|
|
205
|
+
agent_name: event.source,
|
|
206
|
+
hook_kind: event.kind,
|
|
207
|
+
lines_added: typeof event.metadata.lines_added === "number" ? event.metadata.lines_added : void 0,
|
|
208
|
+
lines_deleted: typeof event.metadata.lines_deleted === "number" ? event.metadata.lines_deleted : void 0
|
|
209
|
+
} : {
|
|
210
|
+
paths,
|
|
211
|
+
tool: event.source,
|
|
212
|
+
agent_name: event.source,
|
|
213
|
+
outcome: event.outcome,
|
|
214
|
+
duration_ms: event.durationMs,
|
|
215
|
+
hook_kind: event.kind,
|
|
216
|
+
...event.metadata
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
event_id: event.id,
|
|
220
|
+
schema_version: 1,
|
|
221
|
+
occurred_at: event.occurredAt,
|
|
222
|
+
organization_id: options.organizationId,
|
|
223
|
+
project_id: options.projectId,
|
|
224
|
+
repository_id: options.repositoryId,
|
|
225
|
+
device_id: options.deviceId,
|
|
226
|
+
source: "local_collector",
|
|
227
|
+
type: eventType(event),
|
|
228
|
+
visibility: "project",
|
|
229
|
+
idempotency_key: `${event.source}:${event.id}`,
|
|
230
|
+
correlation: {
|
|
231
|
+
session_id: event.sessionId,
|
|
232
|
+
branch: event.repo?.branch,
|
|
233
|
+
commit_sha: event.repo?.commit
|
|
234
|
+
},
|
|
235
|
+
payload
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var SignedBatchClient = class {
|
|
239
|
+
constructor(options) {
|
|
240
|
+
this.options = options;
|
|
241
|
+
this.request = options.fetch ?? globalThis.fetch;
|
|
242
|
+
this.batchSize = options.batchSize ?? 50;
|
|
243
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
244
|
+
}
|
|
245
|
+
options;
|
|
246
|
+
request;
|
|
247
|
+
batchSize;
|
|
248
|
+
timeoutMs;
|
|
249
|
+
async capture(event) {
|
|
250
|
+
const parsed = canonicalHookEventSchema.parse(event);
|
|
251
|
+
await this.options.queue.enqueue([
|
|
252
|
+
{
|
|
253
|
+
...parsed,
|
|
254
|
+
metadata: redact(parsed.metadata)
|
|
255
|
+
}
|
|
256
|
+
]);
|
|
257
|
+
return this.flush();
|
|
258
|
+
}
|
|
259
|
+
async flush() {
|
|
260
|
+
const events4 = await this.options.queue.peek(this.batchSize);
|
|
261
|
+
if (events4.length === 0) return { sent: 0, remaining: 0 };
|
|
262
|
+
const body = JSON.stringify({
|
|
263
|
+
batch_id: randomUUID2(),
|
|
264
|
+
events: events4.map((event) => toEnvelope(event, this.options))
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
const response = await this.request(this.options.endpoint, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
body,
|
|
270
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
271
|
+
headers: {
|
|
272
|
+
"content-type": "application/json",
|
|
273
|
+
"user-agent": "wibe-bridge/1",
|
|
274
|
+
authorization: `Bearer ${this.options.accessToken}`
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok) throw new Error(`Collector returned HTTP ${response.status}`);
|
|
278
|
+
await this.options.queue.remove(events4.map((event) => event.id));
|
|
279
|
+
return { sent: events4.length, remaining: await this.options.queue.size() };
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
sent: 0,
|
|
283
|
+
remaining: await this.options.queue.size(),
|
|
284
|
+
error: error instanceof Error ? error.message : String(error)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/queue.ts
|
|
291
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
292
|
+
import { dirname } from "path";
|
|
293
|
+
var MemoryOfflineQueue = class {
|
|
294
|
+
events = [];
|
|
295
|
+
async enqueue(events4) {
|
|
296
|
+
this.events.push(...events4);
|
|
297
|
+
}
|
|
298
|
+
async peek(limit) {
|
|
299
|
+
return this.events.slice(0, Math.max(0, limit));
|
|
300
|
+
}
|
|
301
|
+
async remove(ids) {
|
|
302
|
+
const removed = new Set(ids);
|
|
303
|
+
this.events = this.events.filter((event) => !removed.has(event.id));
|
|
304
|
+
}
|
|
305
|
+
async size() {
|
|
306
|
+
return this.events.length;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
var JsonFileOfflineQueue = class {
|
|
310
|
+
constructor(filePath) {
|
|
311
|
+
this.filePath = filePath;
|
|
312
|
+
}
|
|
313
|
+
filePath;
|
|
314
|
+
operation = Promise.resolve();
|
|
315
|
+
async enqueue(events4) {
|
|
316
|
+
await this.update((current) => [...current, ...events4]);
|
|
317
|
+
}
|
|
318
|
+
async peek(limit) {
|
|
319
|
+
await this.operation;
|
|
320
|
+
return (await this.read()).slice(0, Math.max(0, limit));
|
|
321
|
+
}
|
|
322
|
+
async remove(ids) {
|
|
323
|
+
const removed = new Set(ids);
|
|
324
|
+
await this.update((current) => current.filter((event) => !removed.has(event.id)));
|
|
325
|
+
}
|
|
326
|
+
async size() {
|
|
327
|
+
await this.operation;
|
|
328
|
+
return (await this.read()).length;
|
|
329
|
+
}
|
|
330
|
+
async read() {
|
|
331
|
+
try {
|
|
332
|
+
const data = JSON.parse(await readFile(this.filePath, "utf8"));
|
|
333
|
+
return canonicalHookEventSchema.array().parse(data);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (error.code === "ENOENT") return [];
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async update(transform) {
|
|
340
|
+
const next = this.operation.then(async () => {
|
|
341
|
+
const events4 = transform(await this.read());
|
|
342
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
|
|
343
|
+
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
344
|
+
await writeFile(temporary, JSON.stringify(events4), { encoding: "utf8", mode: 384 });
|
|
345
|
+
await rename(temporary, this.filePath);
|
|
346
|
+
});
|
|
347
|
+
this.operation = next.catch(() => void 0);
|
|
348
|
+
await next;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/repo.ts
|
|
353
|
+
import { execFile } from "child_process";
|
|
354
|
+
import { promisify } from "util";
|
|
355
|
+
var execFileAsync = promisify(execFile);
|
|
356
|
+
async function git(cwd, args) {
|
|
357
|
+
try {
|
|
358
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
359
|
+
cwd,
|
|
360
|
+
encoding: "utf8",
|
|
361
|
+
timeout: 2e3,
|
|
362
|
+
maxBuffer: 64 * 1024
|
|
363
|
+
});
|
|
364
|
+
return stdout.trim() || void 0;
|
|
365
|
+
} catch {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function detectRepository(cwd = process.cwd()) {
|
|
370
|
+
const root = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
371
|
+
if (!root) return void 0;
|
|
372
|
+
const [remote, branch, commit] = await Promise.all([
|
|
373
|
+
git(root, ["remote", "get-url", "origin"]),
|
|
374
|
+
git(root, ["branch", "--show-current"]),
|
|
375
|
+
git(root, ["rev-parse", "HEAD"])
|
|
376
|
+
]);
|
|
377
|
+
return {
|
|
378
|
+
root,
|
|
379
|
+
...remote ? { remote: sanitizeRemote(remote) } : {},
|
|
380
|
+
...branch ? { branch } : {},
|
|
381
|
+
...commit ? { commit } : {}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function sanitizeRemote(remote) {
|
|
385
|
+
try {
|
|
386
|
+
const url = new URL(remote);
|
|
387
|
+
url.username = "";
|
|
388
|
+
url.password = "";
|
|
389
|
+
return url.toString().replace(/\/$/, "");
|
|
390
|
+
} catch {
|
|
391
|
+
return remote.replace(
|
|
392
|
+
/^(?:[^@\s]+@)?([^:\s]+):(.+)$/,
|
|
393
|
+
(_match, host, path) => `${host}:${path}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function normalizeGitHubRepository(value) {
|
|
398
|
+
const trimmed = value.trim();
|
|
399
|
+
if (!trimmed) return void 0;
|
|
400
|
+
let path = trimmed;
|
|
401
|
+
try {
|
|
402
|
+
const url = new URL(trimmed);
|
|
403
|
+
if (url.hostname.toLowerCase() !== "github.com") return void 0;
|
|
404
|
+
path = url.pathname;
|
|
405
|
+
} catch {
|
|
406
|
+
const scpLike = trimmed.match(
|
|
407
|
+
/^(?:(?:[^@\s]+)@)?github\.com:(.+)$/i
|
|
408
|
+
);
|
|
409
|
+
if (scpLike) {
|
|
410
|
+
path = scpLike[1];
|
|
411
|
+
} else if (/^github\.com\//i.test(trimmed)) {
|
|
412
|
+
path = trimmed.replace(/^github\.com\//i, "");
|
|
413
|
+
} else if (/^[^/\s]+\/[^/\s]+\/?$/.test(trimmed)) {
|
|
414
|
+
path = trimmed;
|
|
415
|
+
} else {
|
|
416
|
+
return void 0;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const normalized = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").toLowerCase();
|
|
420
|
+
return /^[^/\s]+\/[^/\s]+$/.test(normalized) ? normalized : void 0;
|
|
421
|
+
}
|
|
422
|
+
function matchesGitHubRepository(remote, expected) {
|
|
423
|
+
const normalizedRemote = remote ? normalizeGitHubRepository(remote) : void 0;
|
|
424
|
+
const normalizedExpected = normalizeGitHubRepository(expected);
|
|
425
|
+
return Boolean(
|
|
426
|
+
normalizedRemote && normalizedExpected && normalizedRemote === normalizedExpected
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/auth.ts
|
|
431
|
+
import { z as z2 } from "zod";
|
|
432
|
+
import { execFile as execFileCallback } from "child_process";
|
|
433
|
+
import { promisify as promisify2 } from "util";
|
|
434
|
+
var execFile2 = promisify2(execFileCallback);
|
|
435
|
+
var deviceAuthorizationSchema = z2.object({
|
|
436
|
+
device_code: z2.string().min(1),
|
|
437
|
+
user_code: z2.string().min(1),
|
|
438
|
+
verification_uri: z2.string().url(),
|
|
439
|
+
verification_uri_complete: z2.string().url().optional(),
|
|
440
|
+
expires_at: z2.string().datetime(),
|
|
441
|
+
interval: z2.number().int().positive().default(5)
|
|
442
|
+
});
|
|
443
|
+
var deviceTokenResponseSchema = z2.discriminatedUnion("status", [
|
|
444
|
+
z2.object({
|
|
445
|
+
status: z2.literal("approved"),
|
|
446
|
+
accessToken: z2.string().min(1),
|
|
447
|
+
projectId: z2.string().uuid(),
|
|
448
|
+
organizationId: z2.string().uuid(),
|
|
449
|
+
repositoryId: z2.string().uuid().optional(),
|
|
450
|
+
deviceId: z2.string().uuid()
|
|
451
|
+
}),
|
|
452
|
+
z2.object({
|
|
453
|
+
status: z2.enum(["pending", "denied"])
|
|
454
|
+
}),
|
|
455
|
+
z2.object({
|
|
456
|
+
status: z2.enum(["expired", "invalid"])
|
|
457
|
+
})
|
|
458
|
+
]);
|
|
459
|
+
var SystemCredentialStore = class {
|
|
460
|
+
async get(service, account) {
|
|
461
|
+
try {
|
|
462
|
+
if (process.platform === "darwin") {
|
|
463
|
+
const { stdout } = await execFile2("security", [
|
|
464
|
+
"find-generic-password",
|
|
465
|
+
"-s",
|
|
466
|
+
service,
|
|
467
|
+
"-a",
|
|
468
|
+
account,
|
|
469
|
+
"-w"
|
|
470
|
+
]);
|
|
471
|
+
return stdout.trim() || void 0;
|
|
472
|
+
}
|
|
473
|
+
if (process.platform === "linux") {
|
|
474
|
+
const { stdout } = await execFile2("secret-tool", [
|
|
475
|
+
"lookup",
|
|
476
|
+
"service",
|
|
477
|
+
service,
|
|
478
|
+
"account",
|
|
479
|
+
account
|
|
480
|
+
]);
|
|
481
|
+
return stdout.trim() || void 0;
|
|
482
|
+
}
|
|
483
|
+
throw new Error("Use WIBE_ACCESS_TOKEN on platforms without a supported keychain.");
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error instanceof Error && error.message.startsWith("Use WIBE_")) throw error;
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async set(service, account, value) {
|
|
490
|
+
if (process.platform === "darwin") {
|
|
491
|
+
await execFile2("security", [
|
|
492
|
+
"add-generic-password",
|
|
493
|
+
"-U",
|
|
494
|
+
"-s",
|
|
495
|
+
service,
|
|
496
|
+
"-a",
|
|
497
|
+
account,
|
|
498
|
+
"-w",
|
|
499
|
+
value
|
|
500
|
+
]);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (process.platform === "linux") {
|
|
504
|
+
await new Promise((resolve, reject) => {
|
|
505
|
+
const child = execFileCallback(
|
|
506
|
+
"secret-tool",
|
|
507
|
+
["store", "--label=Wibe agent bridge", "service", service, "account", account],
|
|
508
|
+
(error) => error ? reject(error) : resolve()
|
|
509
|
+
);
|
|
510
|
+
child.stdin?.end(value);
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
throw new Error("No supported OS keychain was found.");
|
|
515
|
+
}
|
|
516
|
+
async delete(service, account) {
|
|
517
|
+
if (process.platform === "darwin") {
|
|
518
|
+
await execFile2("security", [
|
|
519
|
+
"delete-generic-password",
|
|
520
|
+
"-s",
|
|
521
|
+
service,
|
|
522
|
+
"-a",
|
|
523
|
+
account
|
|
524
|
+
]).catch(() => void 0);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (process.platform === "linux") {
|
|
528
|
+
await execFile2("secret-tool", [
|
|
529
|
+
"clear",
|
|
530
|
+
"service",
|
|
531
|
+
service,
|
|
532
|
+
"account",
|
|
533
|
+
account
|
|
534
|
+
]).catch(() => void 0);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
async function requestDeviceAuthorization(input) {
|
|
540
|
+
const response = await fetch(
|
|
541
|
+
`${input.appUrl.replace(/\/$/, "")}/api/devices/authorize`,
|
|
542
|
+
{
|
|
543
|
+
method: "POST",
|
|
544
|
+
headers: { "content-type": "application/json" },
|
|
545
|
+
body: JSON.stringify({
|
|
546
|
+
projectId: input.projectId,
|
|
547
|
+
deviceName: input.deviceName,
|
|
548
|
+
agentName: input.agentName
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
if (!response.ok) throw new Error(`Device authorization failed (${response.status}).`);
|
|
553
|
+
return deviceAuthorizationSchema.parse(await response.json());
|
|
554
|
+
}
|
|
555
|
+
async function pollDeviceToken(input) {
|
|
556
|
+
const response = await fetch(
|
|
557
|
+
`${input.appUrl.replace(/\/$/, "")}/api/devices/authorize`,
|
|
558
|
+
{
|
|
559
|
+
method: "PUT",
|
|
560
|
+
headers: { "content-type": "application/json" },
|
|
561
|
+
body: JSON.stringify({ device_code: input.deviceCode })
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
if (!response.ok) throw new Error(`Device polling failed (${response.status}).`);
|
|
565
|
+
return deviceTokenResponseSchema.parse(await response.json());
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export {
|
|
569
|
+
agentSourceSchema,
|
|
570
|
+
hookEventKindSchema,
|
|
571
|
+
safeValueSchema,
|
|
572
|
+
canonicalHookEventSchema,
|
|
573
|
+
createHookEvent,
|
|
574
|
+
redact,
|
|
575
|
+
safeMetadata,
|
|
576
|
+
mapClaudeCodeHook,
|
|
577
|
+
mapCodexHook,
|
|
578
|
+
mapCursorHook,
|
|
579
|
+
SignedBatchClient,
|
|
580
|
+
MemoryOfflineQueue,
|
|
581
|
+
JsonFileOfflineQueue,
|
|
582
|
+
detectRepository,
|
|
583
|
+
sanitizeRemote,
|
|
584
|
+
normalizeGitHubRepository,
|
|
585
|
+
matchesGitHubRepository,
|
|
586
|
+
deviceAuthorizationSchema,
|
|
587
|
+
deviceTokenResponseSchema,
|
|
588
|
+
SystemCredentialStore,
|
|
589
|
+
requestDeviceAuthorization,
|
|
590
|
+
pollDeviceToken
|
|
591
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
doctorCommand,
|
|
4
|
+
emitCommand,
|
|
5
|
+
parseAdapter,
|
|
6
|
+
setupCommand,
|
|
7
|
+
statusCommand
|
|
8
|
+
} from "./chunk-5CBPZCBE.js";
|
|
9
|
+
import "./chunk-BL74PDGC.js";
|
|
10
|
+
|
|
11
|
+
// src/cli.ts
|
|
12
|
+
var HELP = `wibe-bridge <command>
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
setup --project <uuid> [--adapter <cursor|claude-code|codex>] [--url <wibe-url>] [--repository <owner/repo>]
|
|
16
|
+
Auto-detects one adapter from the environment or repository config.
|
|
17
|
+
Use --adapter when multiple agent configs are present.
|
|
18
|
+
status
|
|
19
|
+
emit --adapter <name> --event <hook-name> (JSON payload on stdin)
|
|
20
|
+
doctor [--repository <owner/repo>]`;
|
|
21
|
+
async function main() {
|
|
22
|
+
const [command, ...args] = process.argv.slice(2);
|
|
23
|
+
let result;
|
|
24
|
+
if (command === "setup") {
|
|
25
|
+
const adapterOption = option(args, "--adapter");
|
|
26
|
+
result = await setupCommand(adapterOption ? parseAdapter(adapterOption) : void 0, {
|
|
27
|
+
projectId: option(args, "--project"),
|
|
28
|
+
appUrl: option(args, "--url"),
|
|
29
|
+
expectedRepository: option(args, "--repository")
|
|
30
|
+
});
|
|
31
|
+
} else if (command === "status") {
|
|
32
|
+
result = await statusCommand();
|
|
33
|
+
} else if (command === "emit") {
|
|
34
|
+
const adapter = parseAdapter(option(args, "--adapter"));
|
|
35
|
+
const eventName = option(args, "--event");
|
|
36
|
+
if (!eventName) throw new Error("--event is required");
|
|
37
|
+
result = await emitCommand(adapter, eventName, await readJsonStdin());
|
|
38
|
+
} else if (command === "doctor") {
|
|
39
|
+
result = await doctorCommand(process.cwd(), option(args, "--repository"));
|
|
40
|
+
} else {
|
|
41
|
+
process.stdout.write(`${HELP}
|
|
42
|
+
`);
|
|
43
|
+
process.exitCode = command ? 1 : 0;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
process.stdout.write(`${result.message}
|
|
47
|
+
`);
|
|
48
|
+
process.exitCode = result.exitCode;
|
|
49
|
+
}
|
|
50
|
+
function option(args, name) {
|
|
51
|
+
const index = args.indexOf(name);
|
|
52
|
+
return index >= 0 ? args[index + 1] : void 0;
|
|
53
|
+
}
|
|
54
|
+
async function readJsonStdin() {
|
|
55
|
+
if (process.stdin.isTTY) return {};
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let size = 0;
|
|
58
|
+
for await (const chunk of process.stdin) {
|
|
59
|
+
const buffer = Buffer.from(chunk);
|
|
60
|
+
size += buffer.length;
|
|
61
|
+
if (size > 1024 * 1024) throw new Error("Hook payload exceeds 1 MiB limit");
|
|
62
|
+
chunks.push(buffer);
|
|
63
|
+
}
|
|
64
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
65
|
+
return text ? JSON.parse(text) : {};
|
|
66
|
+
}
|
|
67
|
+
main().catch((error) => {
|
|
68
|
+
process.stderr.write(`wibe-bridge: ${error instanceof Error ? error.message : String(error)}
|
|
69
|
+
`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
emitCommand
|
|
4
|
+
} from "./chunk-5CBPZCBE.js";
|
|
5
|
+
import "./chunk-BL74PDGC.js";
|
|
6
|
+
|
|
7
|
+
// src/codex-hook.ts
|
|
8
|
+
async function main() {
|
|
9
|
+
const text = process.argv[2];
|
|
10
|
+
const payload = text ? JSON.parse(text) : {};
|
|
11
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
12
|
+
const eventName = typeof record.type === "string" ? record.type : "turn-complete";
|
|
13
|
+
const result = await emitCommand("codex", eventName, record);
|
|
14
|
+
process.stderr.write(`${result.message}
|
|
15
|
+
`);
|
|
16
|
+
process.exitCode = result.exitCode;
|
|
17
|
+
}
|
|
18
|
+
main().catch((error) => {
|
|
19
|
+
process.stderr.write(`wibe-codex-hook: ${error instanceof Error ? error.message : String(error)}
|
|
20
|
+
`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
});
|