@wibeco/bridge 0.2.6 → 0.2.7

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.
@@ -15,7 +15,14 @@ var hookEventKindSchema = z.enum([
15
15
  "shell.completed",
16
16
  "mcp.started",
17
17
  "mcp.completed",
18
+ "response.completed",
18
19
  "progress.shared",
20
+ "git.branch_changed",
21
+ "git.commit_created",
22
+ "git.push_completed",
23
+ "git.merge_completed",
24
+ "git.rebase_completed",
25
+ "git.stash_created",
19
26
  "unknown"
20
27
  ]);
21
28
  var safeScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
@@ -156,7 +163,7 @@ var events = {
156
163
  "pre-tool-use": "tool.started",
157
164
  "post-tool-use": "tool.completed",
158
165
  "post-tool-use-failure": "tool.completed",
159
- notification: "lifecycle.after"
166
+ notification: "unknown"
160
167
  };
161
168
  var safeKeys = [
162
169
  "hook_event_name",
@@ -171,12 +178,16 @@ var safeKeys = [
171
178
  "session_started_at",
172
179
  "heartbeat_at",
173
180
  "heartbeat_interval_ms",
174
- "session_elapsed_ms"
181
+ "session_elapsed_ms",
182
+ "title"
175
183
  ];
176
184
  function mapClaudeCodeHook(eventName, input) {
177
185
  const payload = objectPayload(input);
178
186
  const toolInput = objectPayload(payload.tool_input);
179
187
  const sessionId = safeIdentifier(payload.session_id);
188
+ const title = safeIdentifier(
189
+ payload.session_name ?? payload.title ?? payload.name
190
+ );
180
191
  const model = safeModel(payload.model);
181
192
  const path = safeRelativePath(
182
193
  toolInput.file_path ?? payload.file_path,
@@ -191,6 +202,7 @@ function mapClaudeCodeHook(eventName, input) {
191
202
  {
192
203
  ...payload,
193
204
  ...sessionId ? { session_id: sessionId } : {},
205
+ ...title ? { title } : {},
194
206
  ...model ? { model } : {},
195
207
  ...path ? { path } : {},
196
208
  ...duration !== void 0 ? { duration_ms: duration } : {},
@@ -220,8 +232,8 @@ var events2 = {
220
232
  "user-prompt-submit": "lifecycle.before",
221
233
  "pre-tool-use": "tool.started",
222
234
  "post-tool-use": "tool.completed",
223
- "subagent-start": "lifecycle.before",
224
- "subagent-stop": "lifecycle.after",
235
+ "subagent-start": "unknown",
236
+ "subagent-stop": "unknown",
225
237
  stop: "lifecycle.after"
226
238
  };
227
239
  var safeKeys2 = [
@@ -237,7 +249,8 @@ var safeKeys2 = [
237
249
  "session_started_at",
238
250
  "heartbeat_at",
239
251
  "heartbeat_interval_ms",
240
- "session_elapsed_ms"
252
+ "session_elapsed_ms",
253
+ "title"
241
254
  ];
242
255
  function mapCodexHook(eventName, input) {
243
256
  const payload = objectPayload(input);
@@ -245,6 +258,9 @@ function mapCodexHook(eventName, input) {
245
258
  const threadId = safeIdentifier(
246
259
  payload["thread-id"] ?? payload.thread_id ?? payload.session_id
247
260
  );
261
+ const title = safeIdentifier(
262
+ payload["thread-name"] ?? payload.thread_name ?? payload.title
263
+ );
248
264
  const turnId = safeIdentifier(payload["turn-id"] ?? payload.turn_id);
249
265
  const model = safeModel(payload.model);
250
266
  const path = safeRelativePath(
@@ -257,6 +273,7 @@ function mapCodexHook(eventName, input) {
257
273
  {
258
274
  ...payload,
259
275
  ...threadId ? { session_id: threadId } : {},
276
+ ...title ? { title } : {},
260
277
  ...turnId ? { turn_id: turnId } : {},
261
278
  ...model ? { model } : {},
262
279
  ...path ? { path } : {},
@@ -296,11 +313,15 @@ var safeKeys3 = [
296
313
  "session_started_at",
297
314
  "heartbeat_at",
298
315
  "heartbeat_interval_ms",
299
- "session_elapsed_ms"
316
+ "session_elapsed_ms",
317
+ "title"
300
318
  ];
301
319
  function mapCursorHook(eventName, input) {
302
320
  const payload = objectPayload(input);
303
321
  const sessionId = safeIdentifier(payload.conversation_id ?? payload.session_id);
322
+ const title = safeIdentifier(
323
+ payload.conversation_name ?? payload.title ?? payload.name
324
+ );
304
325
  const model = safeModel(payload.model);
305
326
  const workspaceRoot = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.find(
306
327
  (root) => typeof root === "string"
@@ -315,6 +336,7 @@ function mapCursorHook(eventName, input) {
315
336
  {
316
337
  ...payload,
317
338
  ...sessionId ? { session_id: sessionId } : {},
339
+ ...title ? { title } : {},
318
340
  ...model ? { model } : {},
319
341
  ...path ? { path } : {},
320
342
  ...typeof payload.lines_added === "number" ? { lines_added: payload.lines_added } : {},
@@ -332,7 +354,11 @@ function eventTypeForHook(event) {
332
354
  if (event.kind === "presence.heartbeat") return "presence.heartbeat";
333
355
  if (event.kind === "session.ended") return "presence.stopped";
334
356
  if (event.kind === "file.changed") return "workspace.files_changed";
357
+ if (event.kind === "response.completed") return "agent.response_completed";
335
358
  if (event.kind === "progress.shared") return "agent.progress_shared";
359
+ if (event.kind === "git.branch_changed" || event.kind === "git.commit_created" || event.kind === "git.push_completed" || event.kind === "git.merge_completed" || event.kind === "git.rebase_completed" || event.kind === "git.stash_created") {
360
+ return event.kind;
361
+ }
336
362
  if (event.kind === "shell.completed" && event.outcome && isTestHookEvent(event)) {
337
363
  return "workspace.test_completed";
338
364
  }
@@ -373,6 +399,14 @@ function toEnvelope(event, options) {
373
399
  task_lines_added: typeof event.metadata.task_lines_added === "number" ? event.metadata.task_lines_added : void 0,
374
400
  task_lines_deleted: typeof event.metadata.task_lines_deleted === "number" ? event.metadata.task_lines_deleted : void 0,
375
401
  task_paths: Array.isArray(event.metadata.task_paths) ? event.metadata.task_paths : void 0
402
+ } : event.kind === "response.completed" ? {
403
+ title: event.metadata.title,
404
+ paths,
405
+ lines_added: event.metadata.lines_added,
406
+ lines_deleted: event.metadata.lines_deleted,
407
+ tool: event.source,
408
+ agent_name: event.source,
409
+ hook_kind: event.kind
376
410
  } : event.kind === "presence.heartbeat" ? {
377
411
  verification: event.metadata.verification,
378
412
  sequence: event.metadata.sequence,
@@ -396,6 +430,21 @@ function toEnvelope(event, options) {
396
430
  tool: event.source,
397
431
  agent_name: event.source,
398
432
  hook_kind: event.kind
433
+ } : event.kind.startsWith("git.") ? {
434
+ title: event.metadata.title,
435
+ previous_branch: event.metadata.previous_branch,
436
+ branch: event.repo?.branch,
437
+ previous_commit: event.metadata.previous_commit,
438
+ commit_sha: event.metadata.commit_sha ?? event.repo?.commit,
439
+ stash_sha: event.metadata.stash_sha,
440
+ paths_known: event.metadata.paths_known,
441
+ paths,
442
+ changes: event.metadata.changes,
443
+ lines_added: event.metadata.lines_added,
444
+ lines_deleted: event.metadata.lines_deleted,
445
+ tool: event.source,
446
+ agent_name: event.source,
447
+ hook_kind: event.kind
399
448
  } : {
400
449
  paths,
401
450
  tool: event.source,
@@ -416,7 +465,7 @@ function toEnvelope(event, options) {
416
465
  source: "local_collector",
417
466
  type,
418
467
  visibility: "project",
419
- idempotency_key: `${event.source}:${event.id}`,
468
+ idempotency_key: gitIdempotencyKey(event, options) ?? `${event.source}:${event.id}`,
420
469
  correlation: {
421
470
  session_id: event.sessionId,
422
471
  branch: event.repo?.branch,
@@ -428,6 +477,14 @@ function toEnvelope(event, options) {
428
477
  payload
429
478
  };
430
479
  }
480
+ function gitIdempotencyKey(event, options) {
481
+ if (!event.kind.startsWith("git.")) return void 0;
482
+ const identity = event.kind === "git.stash_created" ? event.metadata.stash_sha : event.metadata.commit_sha ?? event.repo?.commit;
483
+ if (typeof identity !== "string" || !identity) return void 0;
484
+ const repository = options.repositoryId ?? "repository";
485
+ const branch = event.repo?.branch ?? "detached";
486
+ return `git:${event.kind.slice(4)}:${repository}:${branch}:${identity}`;
487
+ }
431
488
  var SignedBatchClient = class {
432
489
  constructor(options) {
433
490
  this.options = options;
@@ -708,8 +765,16 @@ async function updatePresenceSession(source, sessionId, event, cwd = process.cwd
708
765
  if (event.outcome === "success") state.testsPassed += 1;
709
766
  if (event.outcome === "failure") state.testsFailed += 1;
710
767
  }
768
+ const metrics = metricsFromState(state, measuredAt);
769
+ if (event.kind === "lifecycle.after") {
770
+ state.taskBaselineLinesAdded = workingLinesAdded;
771
+ state.taskBaselineLinesDeleted = workingLinesDeleted;
772
+ state.taskLinesAdded = 0;
773
+ state.taskLinesDeleted = 0;
774
+ state.taskPaths = [];
775
+ }
711
776
  await writePresenceState(statePath, state);
712
- return metricsFromState(state, measuredAt);
777
+ return metrics;
713
778
  }
714
779
  async function stopPresenceSession(source, sessionId, cwd = process.cwd()) {
715
780
  const statePath = presenceStatePath(source, sessionId, cwd);
@@ -889,16 +954,34 @@ function delay(milliseconds) {
889
954
  }
890
955
 
891
956
  // src/repo.ts
957
+ import { createHash as createHash2 } from "crypto";
892
958
  import { execFile } from "child_process";
959
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
960
+ import { homedir as homedir2 } from "os";
961
+ import { join as join2 } from "path";
893
962
  import { promisify } from "util";
894
963
  var execFileAsync = promisify(execFile);
964
+ function classifyHeadTransition(reflogAction, headParentCount) {
965
+ const reflog = reflogAction?.toLowerCase() ?? "";
966
+ if (reflog.startsWith("rebase")) return "git.rebase_completed";
967
+ if (reflog.startsWith("merge") && headParentCount && headParentCount > 1) {
968
+ return "git.merge_completed";
969
+ }
970
+ return reflog.startsWith("commit") ? "git.commit_created" : void 0;
971
+ }
972
+ function isObservedPush(previous, current, branchChanged = false) {
973
+ const reflog = current.reflogAction?.toLowerCase() ?? "";
974
+ return Boolean(
975
+ !branchChanged && !reflog.startsWith("pull") && !reflog.startsWith("fetch") && current.commit && current.upstreamCommit === current.commit && previous.upstreamCommit !== current.upstreamCommit
976
+ );
977
+ }
895
978
  async function git(cwd, args) {
896
979
  try {
897
980
  const { stdout } = await execFileAsync("git", args, {
898
981
  cwd,
899
982
  encoding: "utf8",
900
983
  timeout: 2e3,
901
- maxBuffer: 64 * 1024
984
+ maxBuffer: 4 * 1024 * 1024
902
985
  });
903
986
  return stdout.trim() || void 0;
904
987
  } catch {
@@ -929,10 +1012,24 @@ async function detectDefaultBranch(root) {
929
1012
  async function detectRepository(cwd = process.cwd()) {
930
1013
  const root = await git(cwd, ["rev-parse", "--show-toplevel"]);
931
1014
  if (!root) return void 0;
932
- const [remote, branch, commit] = await Promise.all([
1015
+ const [
1016
+ remote,
1017
+ branch,
1018
+ commit,
1019
+ upstreamCommit,
1020
+ stashCommit,
1021
+ parents,
1022
+ reflogAction,
1023
+ commitTitle
1024
+ ] = await Promise.all([
933
1025
  git(root, ["remote", "get-url", "origin"]),
934
1026
  git(root, ["branch", "--show-current"]),
935
- git(root, ["rev-parse", "HEAD"])
1027
+ git(root, ["rev-parse", "HEAD"]),
1028
+ git(root, ["rev-parse", "@{upstream}"]),
1029
+ git(root, ["rev-parse", "refs/stash"]),
1030
+ git(root, ["rev-list", "--parents", "-n", "1", "HEAD"]),
1031
+ git(root, ["reflog", "-1", "--format=%gs"]),
1032
+ git(root, ["log", "-1", "--format=%s"])
936
1033
  ]);
937
1034
  const defaultBranch = await detectDefaultBranch(root);
938
1035
  const baseCommit = branch && commit && defaultBranch ? branch === defaultBranch.branch ? commit : await git(root, [
@@ -953,7 +1050,144 @@ async function detectRepository(cwd = process.cwd()) {
953
1050
  ...commit ? { commit } : {},
954
1051
  ...defaultBranch ? { defaultBranch: defaultBranch.branch } : {},
955
1052
  ...baseCommit ? { baseCommit } : {},
956
- ...behindBy !== void 0 ? { behindBy } : {}
1053
+ ...behindBy !== void 0 ? { behindBy } : {},
1054
+ ...upstreamCommit ? { upstreamCommit } : {},
1055
+ ...stashCommit ? { stashCommit } : {},
1056
+ ...parents ? { headParentCount: Math.max(0, parents.split(/\s+/).length - 1) } : {},
1057
+ ...reflogAction ? { reflogAction } : {},
1058
+ ...commitTitle ? { commitTitle } : {}
1059
+ };
1060
+ }
1061
+ async function observeRepositoryTransitions(source, sessionId, current) {
1062
+ const directory = process.env.WIBE_GIT_STATE_DIR ?? join2(homedir2(), ".wibe", "git-state");
1063
+ const key = createHash2("sha256").update(`${source}\0${sessionId ?? ""}\0${current.root}`).digest("hex");
1064
+ const statePath = join2(directory, `${key}.json`);
1065
+ let previous;
1066
+ try {
1067
+ previous = JSON.parse(await readFile3(statePath, "utf8"));
1068
+ } catch {
1069
+ previous = void 0;
1070
+ }
1071
+ await mkdir3(directory, { recursive: true, mode: 448 });
1072
+ await writeFile3(statePath, `${JSON.stringify(current)}
1073
+ `, { mode: 384 });
1074
+ if (!previous) return [];
1075
+ const transitions = [];
1076
+ const branchChanged = Boolean(current.branch) && Boolean(previous.branch) && current.branch !== previous.branch;
1077
+ if (branchChanged) {
1078
+ transitions.push({
1079
+ kind: "git.branch_changed",
1080
+ metadata: {
1081
+ title: "Changed branch",
1082
+ previous_branch: previous.branch,
1083
+ branch: current.branch
1084
+ }
1085
+ });
1086
+ }
1087
+ if (current.stashCommit && current.stashCommit !== previous.stashCommit) {
1088
+ transitions.push({
1089
+ kind: "git.stash_created",
1090
+ metadata: {
1091
+ title: "Stashed changes",
1092
+ stash_sha: current.stashCommit
1093
+ }
1094
+ });
1095
+ }
1096
+ if (!branchChanged && current.commit && previous.commit && current.commit !== previous.commit) {
1097
+ const change = await detectRevisionChanges(
1098
+ current.root,
1099
+ previous.commit,
1100
+ current.commit
1101
+ );
1102
+ const kind = classifyHeadTransition(
1103
+ current.reflogAction,
1104
+ current.headParentCount
1105
+ );
1106
+ if (kind) {
1107
+ transitions.push({
1108
+ kind,
1109
+ metadata: {
1110
+ title: kind === "git.rebase_completed" ? "Rebased" : kind === "git.merge_completed" ? "Merged" : current.commitTitle || "Committed",
1111
+ previous_commit: previous.commit,
1112
+ commit_sha: current.commit,
1113
+ paths_known: change.pathsKnown,
1114
+ paths: change.paths,
1115
+ changes: change.changes,
1116
+ lines_added: change.linesAdded,
1117
+ lines_deleted: change.linesDeleted
1118
+ }
1119
+ });
1120
+ }
1121
+ }
1122
+ if (isObservedPush(previous, current, branchChanged) && current.commit) {
1123
+ const change = previous.upstreamCommit ? await detectRevisionChanges(
1124
+ current.root,
1125
+ previous.upstreamCommit,
1126
+ current.commit
1127
+ ) : {
1128
+ pathsKnown: false,
1129
+ paths: [],
1130
+ changes: [],
1131
+ linesAdded: 0,
1132
+ linesDeleted: 0
1133
+ };
1134
+ transitions.push({
1135
+ kind: "git.push_completed",
1136
+ metadata: {
1137
+ title: `Pushed to ${current.branch ?? "remote"}`,
1138
+ commit_sha: current.commit,
1139
+ paths_known: change.pathsKnown,
1140
+ paths: change.paths,
1141
+ changes: change.changes,
1142
+ lines_added: change.linesAdded,
1143
+ lines_deleted: change.linesDeleted
1144
+ }
1145
+ });
1146
+ }
1147
+ return transitions;
1148
+ }
1149
+ async function detectRevisionChanges(root, previous, current) {
1150
+ const [numstat, nameStatus] = await Promise.all([
1151
+ git(root, ["diff", "--numstat", previous, current, "--"]),
1152
+ git(root, ["diff", "--name-status", previous, current, "--"])
1153
+ ]);
1154
+ const pathsKnown = numstat !== void 0 && nameStatus !== void 0;
1155
+ const statusByPath = /* @__PURE__ */ new Map();
1156
+ for (const line of nameStatus?.split("\n") ?? []) {
1157
+ if (!line) continue;
1158
+ const [status, ...pathParts] = line.split(" ");
1159
+ const path = pathParts.at(-1);
1160
+ if (!path || isSensitivePath(path)) continue;
1161
+ statusByPath.set(
1162
+ path,
1163
+ status?.startsWith("A") ? "added" : status?.startsWith("D") ? "deleted" : "modified"
1164
+ );
1165
+ }
1166
+ let linesAdded = 0;
1167
+ let linesDeleted = 0;
1168
+ const changes = [];
1169
+ for (const line of numstat?.split("\n") ?? []) {
1170
+ if (!line) continue;
1171
+ const [added, deleted, ...pathParts] = line.split(" ");
1172
+ const path = pathParts.join(" ");
1173
+ if (!path || isSensitivePath(path)) continue;
1174
+ const additions = /^\d+$/.test(added) ? Number(added) : 0;
1175
+ const deletions = /^\d+$/.test(deleted) ? Number(deleted) : 0;
1176
+ linesAdded += additions;
1177
+ linesDeleted += deletions;
1178
+ changes.push({
1179
+ path,
1180
+ change_type: statusByPath.get(path) ?? "modified",
1181
+ additions,
1182
+ deletions
1183
+ });
1184
+ }
1185
+ return {
1186
+ pathsKnown,
1187
+ paths: changes.map((change) => change.path).slice(0, 2e3),
1188
+ changes: changes.slice(0, 2e3),
1189
+ linesAdded,
1190
+ linesDeleted
957
1191
  };
958
1192
  }
959
1193
  async function detectWorkingTreeMetrics(root) {
@@ -1181,7 +1415,10 @@ export {
1181
1415
  stopPresenceSession,
1182
1416
  runPresenceHeartbeat,
1183
1417
  presenceStatePath,
1418
+ classifyHeadTransition,
1419
+ isObservedPush,
1184
1420
  detectRepository,
1421
+ observeRepositoryTransitions,
1185
1422
  detectWorkingTreeMetrics,
1186
1423
  sanitizeRemote,
1187
1424
  normalizeGitHubRepository,
@@ -12,12 +12,13 @@ import {
12
12
  mapCursorHook,
13
13
  matchesGitHubRepository,
14
14
  normalizeGitHubRepository,
15
+ observeRepositoryTransitions,
15
16
  pollDeviceToken,
16
17
  requestDeviceAuthorization,
17
18
  startPresenceSession,
18
19
  stopPresenceSession,
19
20
  updatePresenceSession
20
- } from "./chunk-WVBSXZG3.js";
21
+ } from "./chunk-O4MHMJZZ.js";
21
22
 
22
23
  // src/cli/commands.ts
23
24
  import { access, cp, mkdir, readFile, writeFile } from "fs/promises";
@@ -257,6 +258,18 @@ async function emitCommand(adapter, eventName, input) {
257
258
  task_paths: metrics.task_paths
258
259
  }
259
260
  };
261
+ } else if (mappedEvent.kind === "lifecycle.after" && metrics && metrics.task_paths.length > 0) {
262
+ mappedEvent = {
263
+ ...mappedEvent,
264
+ kind: "response.completed",
265
+ metadata: {
266
+ ...mappedEvent.metadata,
267
+ title: "Agent update",
268
+ paths: metrics.task_paths,
269
+ lines_added: metrics.task_lines_added,
270
+ lines_deleted: metrics.task_lines_deleted
271
+ }
272
+ };
260
273
  } else if (mappedEvent.kind === "presence.heartbeat" && metrics) {
261
274
  const publicMetadata = { ...mappedEvent.metadata };
262
275
  delete publicMetadata.working_tree_paths;
@@ -274,16 +287,32 @@ async function emitCommand(adapter, eventName, input) {
274
287
  mappedEvent = { ...mappedEvent, metadata: publicMetadata };
275
288
  }
276
289
  const event = repo ? { ...mappedEvent, repo } : mappedEvent;
290
+ const repositoryEvents = repo ? (await observeRepositoryTransitions(
291
+ adapter,
292
+ mappedEvent.sessionId,
293
+ repo
294
+ )).map(
295
+ (transition) => createHookEvent({
296
+ source: adapter,
297
+ kind: transition.kind,
298
+ ...mappedEvent.sessionId ? { sessionId: mappedEvent.sessionId } : {},
299
+ repo,
300
+ metadata: transition.metadata
301
+ })
302
+ ) : [];
303
+ const publishableEvents = [event, ...repositoryEvents].filter(
304
+ (candidate) => eventTypeForHook(candidate)
305
+ );
277
306
  const queue = new JsonFileOfflineQueue(queuePath());
278
307
  const credential = await loadCredential(process.cwd());
279
308
  if (!credential) {
280
- if (eventTypeForHook(event)) await queue.enqueue([event]);
309
+ if (publishableEvents.length) await queue.enqueue(publishableEvents);
281
310
  return {
282
311
  exitCode: 0,
283
- message: eventTypeForHook(event) ? `Queued ${event.kind}; this repository is not connected to Wibe.` : `Ignored operational ${event.kind} hook.`
312
+ message: publishableEvents.length ? `Queued ${publishableEvents.length} event(s); this repository is not connected to Wibe.` : `Ignored operational ${event.kind} hook.`
284
313
  };
285
314
  }
286
- const result = await new SignedBatchClient({
315
+ const client = new SignedBatchClient({
287
316
  endpoint: `${credential.appUrl}/api/events/batch`,
288
317
  accessToken: credential.accessToken,
289
318
  organizationId: credential.organizationId,
@@ -291,10 +320,19 @@ async function emitCommand(adapter, eventName, input) {
291
320
  repositoryId: credential.repositoryId,
292
321
  deviceId: credential.deviceId,
293
322
  queue
294
- }).capture(event);
323
+ });
324
+ let sent = 0;
325
+ let remaining = await queue.size();
326
+ let deliveryError;
327
+ for (const candidate of publishableEvents) {
328
+ const result = await client.capture(candidate);
329
+ sent += result.sent;
330
+ remaining = result.remaining;
331
+ deliveryError ??= result.error;
332
+ }
295
333
  return {
296
- exitCode: result.error ? 1 : 0,
297
- message: result.error ? `Queued ${event.kind}; delivery failed: ${result.error}` : `Delivered ${result.sent} event(s); ${result.remaining} queued.`
334
+ exitCode: deliveryError ? 1 : 0,
335
+ message: deliveryError ? `Queued ${publishableEvents.length} event(s); delivery failed: ${deliveryError}` : publishableEvents.length ? `Delivered ${sent} event(s); ${remaining} queued.` : `Ignored operational ${event.kind} hook.`
298
336
  };
299
337
  }
300
338
  async function shareProgressCommand(options, cwd = process.cwd()) {
package/dist/cli.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  setupCommand,
7
7
  shareProgressCommand,
8
8
  statusCommand
9
- } from "./chunk-7UJHKOF6.js";
9
+ } from "./chunk-VBQSVC34.js";
10
10
  import {
11
11
  runPresenceHeartbeat
12
- } from "./chunk-WVBSXZG3.js";
12
+ } from "./chunk-O4MHMJZZ.js";
13
13
 
14
14
  // src/cli.ts
15
15
  var HELP = `wibe-bridge <command>
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  emitCommand
4
- } from "./chunk-7UJHKOF6.js";
5
- import "./chunk-WVBSXZG3.js";
4
+ } from "./chunk-VBQSVC34.js";
5
+ import "./chunk-O4MHMJZZ.js";
6
6
 
7
7
  // src/codex-hook.ts
8
8
  async function main() {
package/dist/index.d.ts CHANGED
@@ -19,7 +19,14 @@ declare const hookEventKindSchema: z.ZodEnum<{
19
19
  "shell.completed": "shell.completed";
20
20
  "mcp.started": "mcp.started";
21
21
  "mcp.completed": "mcp.completed";
22
+ "response.completed": "response.completed";
22
23
  "progress.shared": "progress.shared";
24
+ "git.branch_changed": "git.branch_changed";
25
+ "git.commit_created": "git.commit_created";
26
+ "git.push_completed": "git.push_completed";
27
+ "git.merge_completed": "git.merge_completed";
28
+ "git.rebase_completed": "git.rebase_completed";
29
+ "git.stash_created": "git.stash_created";
23
30
  unknown: "unknown";
24
31
  }>;
25
32
  type HookEventKind = z.infer<typeof hookEventKindSchema>;
@@ -49,7 +56,14 @@ declare const canonicalHookEventSchema: z.ZodObject<{
49
56
  "shell.completed": "shell.completed";
50
57
  "mcp.started": "mcp.started";
51
58
  "mcp.completed": "mcp.completed";
59
+ "response.completed": "response.completed";
52
60
  "progress.shared": "progress.shared";
61
+ "git.branch_changed": "git.branch_changed";
62
+ "git.commit_created": "git.commit_created";
63
+ "git.push_completed": "git.push_completed";
64
+ "git.merge_completed": "git.merge_completed";
65
+ "git.rebase_completed": "git.rebase_completed";
66
+ "git.stash_created": "git.stash_created";
53
67
  unknown: "unknown";
54
68
  }>;
55
69
  occurredAt: z.ZodString;
@@ -79,7 +93,7 @@ declare function mapClaudeCodeHook(eventName: string, input: unknown): {
79
93
  id: string;
80
94
  version: 1;
81
95
  source: "cursor" | "claude-code" | "codex";
82
- kind: "presence.heartbeat" | "session.started" | "session.ended" | "lifecycle.before" | "lifecycle.after" | "tool.started" | "tool.completed" | "file.changed" | "shell.started" | "shell.completed" | "mcp.started" | "mcp.completed" | "progress.shared" | "unknown";
96
+ kind: "presence.heartbeat" | "session.started" | "session.ended" | "lifecycle.before" | "lifecycle.after" | "tool.started" | "tool.completed" | "file.changed" | "shell.started" | "shell.completed" | "mcp.started" | "mcp.completed" | "response.completed" | "progress.shared" | "git.branch_changed" | "git.commit_created" | "git.push_completed" | "git.merge_completed" | "git.rebase_completed" | "git.stash_created" | "unknown";
83
97
  occurredAt: string;
84
98
  metadata: Record<string, SafeValue>;
85
99
  sessionId?: string | undefined;
@@ -100,7 +114,7 @@ declare function mapCodexHook(eventName: string, input: unknown): {
100
114
  id: string;
101
115
  version: 1;
102
116
  source: "cursor" | "claude-code" | "codex";
103
- kind: "presence.heartbeat" | "session.started" | "session.ended" | "lifecycle.before" | "lifecycle.after" | "tool.started" | "tool.completed" | "file.changed" | "shell.started" | "shell.completed" | "mcp.started" | "mcp.completed" | "progress.shared" | "unknown";
117
+ kind: "presence.heartbeat" | "session.started" | "session.ended" | "lifecycle.before" | "lifecycle.after" | "tool.started" | "tool.completed" | "file.changed" | "shell.started" | "shell.completed" | "mcp.started" | "mcp.completed" | "response.completed" | "progress.shared" | "git.branch_changed" | "git.commit_created" | "git.push_completed" | "git.merge_completed" | "git.rebase_completed" | "git.stash_created" | "unknown";
104
118
  occurredAt: string;
105
119
  metadata: Record<string, SafeValue>;
106
120
  sessionId?: string | undefined;
@@ -121,7 +135,7 @@ declare function mapCursorHook(eventName: string, input: unknown): {
121
135
  id: string;
122
136
  version: 1;
123
137
  source: "cursor" | "claude-code" | "codex";
124
- kind: "presence.heartbeat" | "session.started" | "session.ended" | "lifecycle.before" | "lifecycle.after" | "tool.started" | "tool.completed" | "file.changed" | "shell.started" | "shell.completed" | "mcp.started" | "mcp.completed" | "progress.shared" | "unknown";
138
+ kind: "presence.heartbeat" | "session.started" | "session.ended" | "lifecycle.before" | "lifecycle.after" | "tool.started" | "tool.completed" | "file.changed" | "shell.started" | "shell.completed" | "mcp.started" | "mcp.completed" | "response.completed" | "progress.shared" | "git.branch_changed" | "git.commit_created" | "git.push_completed" | "git.merge_completed" | "git.rebase_completed" | "git.stash_created" | "unknown";
125
139
  occurredAt: string;
126
140
  metadata: Record<string, SafeValue>;
127
141
  sessionId?: string | undefined;
@@ -251,7 +265,7 @@ interface SignedBatchClientOptions {
251
265
  batchSize?: number;
252
266
  timeoutMs?: number;
253
267
  }
254
- declare function eventTypeForHook(event: CanonicalHookEvent): "presence.heartbeat" | "presence.started" | "presence.stopped" | "workspace.files_changed" | "agent.progress_shared" | "workspace.test_completed" | null;
268
+ declare function eventTypeForHook(event: CanonicalHookEvent): "presence.heartbeat" | "git.branch_changed" | "git.commit_created" | "git.push_completed" | "git.merge_completed" | "git.rebase_completed" | "git.stash_created" | "presence.started" | "presence.stopped" | "workspace.files_changed" | "agent.response_completed" | "agent.progress_shared" | "workspace.test_completed" | null;
255
269
  interface FlushResult {
256
270
  sent: number;
257
271
  remaining: number;
@@ -307,16 +321,28 @@ interface RepositoryInfo {
307
321
  defaultBranch?: string;
308
322
  baseCommit?: string;
309
323
  behindBy?: number;
324
+ upstreamCommit?: string;
325
+ stashCommit?: string;
326
+ headParentCount?: number;
327
+ reflogAction?: string;
328
+ commitTitle?: string;
310
329
  }
311
330
  interface WorkingTreeMetrics {
312
331
  linesAdded: number;
313
332
  linesDeleted: number;
314
333
  paths: string[];
315
334
  }
335
+ interface RepositoryTransition {
336
+ kind: Extract<HookEventKind, "git.branch_changed" | "git.commit_created" | "git.push_completed" | "git.merge_completed" | "git.rebase_completed" | "git.stash_created">;
337
+ metadata: Record<string, SafeValue>;
338
+ }
339
+ declare function classifyHeadTransition(reflogAction: string | undefined, headParentCount: number | undefined): RepositoryTransition["kind"] | undefined;
340
+ declare function isObservedPush(previous: RepositoryInfo, current: RepositoryInfo, branchChanged?: boolean): boolean;
316
341
  declare function detectRepository(cwd?: string): Promise<RepositoryInfo | undefined>;
342
+ declare function observeRepositoryTransitions(source: AgentSource, sessionId: string | undefined, current: RepositoryInfo): Promise<RepositoryTransition[]>;
317
343
  declare function detectWorkingTreeMetrics(root: string): Promise<WorkingTreeMetrics | undefined>;
318
344
  declare function sanitizeRemote(remote: string): string;
319
345
  declare function normalizeGitHubRepository(value: string): string | undefined;
320
346
  declare function matchesGitHubRepository(remote: string | undefined, expected: string): boolean;
321
347
 
322
- export { type AgentSource, type CanonicalHookEvent, type CredentialStore, type DeviceAuthorization, type DeviceTokenResponse, type FlushResult, type HookEventKind, JsonFileOfflineQueue, MemoryOfflineQueue, type OfflineQueue, PRESENCE_HEARTBEAT_INTERVAL_MS, type PresenceMetrics, type RedactionOptions, type RepositoryInfo, type SafeValue, SignedBatchClient, type SignedBatchClientOptions, type StoredCredential, SystemCredentialStore, type WorkingTreeMetrics, agentSourceSchema, canonicalHookEventSchema, createHookEvent, detectRepository, detectWorkingTreeMetrics, deviceAuthorizationSchema, deviceTokenResponseSchema, eventTypeForHook, hookEventKindSchema, mapClaudeCodeHook, mapCodexHook, mapCursorHook, matchesGitHubRepository, normalizeGitHubRepository, pollDeviceToken, presenceStatePath, redact, requestDeviceAuthorization, runPresenceHeartbeat, safeMetadata, safeValueSchema, sanitizeRemote, startPresenceSession, stopPresenceSession, updatePresenceSession };
348
+ export { type AgentSource, type CanonicalHookEvent, type CredentialStore, type DeviceAuthorization, type DeviceTokenResponse, type FlushResult, type HookEventKind, JsonFileOfflineQueue, MemoryOfflineQueue, type OfflineQueue, PRESENCE_HEARTBEAT_INTERVAL_MS, type PresenceMetrics, type RedactionOptions, type RepositoryInfo, type RepositoryTransition, type SafeValue, SignedBatchClient, type SignedBatchClientOptions, type StoredCredential, SystemCredentialStore, type WorkingTreeMetrics, agentSourceSchema, canonicalHookEventSchema, classifyHeadTransition, createHookEvent, detectRepository, detectWorkingTreeMetrics, deviceAuthorizationSchema, deviceTokenResponseSchema, eventTypeForHook, hookEventKindSchema, isObservedPush, mapClaudeCodeHook, mapCodexHook, mapCursorHook, matchesGitHubRepository, normalizeGitHubRepository, observeRepositoryTransitions, pollDeviceToken, presenceStatePath, redact, requestDeviceAuthorization, runPresenceHeartbeat, safeMetadata, safeValueSchema, sanitizeRemote, startPresenceSession, stopPresenceSession, updatePresenceSession };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  SystemCredentialStore,
7
7
  agentSourceSchema,
8
8
  canonicalHookEventSchema,
9
+ classifyHeadTransition,
9
10
  createHookEvent,
10
11
  detectRepository,
11
12
  detectWorkingTreeMetrics,
@@ -13,11 +14,13 @@ import {
13
14
  deviceTokenResponseSchema,
14
15
  eventTypeForHook,
15
16
  hookEventKindSchema,
17
+ isObservedPush,
16
18
  mapClaudeCodeHook,
17
19
  mapCodexHook,
18
20
  mapCursorHook,
19
21
  matchesGitHubRepository,
20
22
  normalizeGitHubRepository,
23
+ observeRepositoryTransitions,
21
24
  pollDeviceToken,
22
25
  presenceStatePath,
23
26
  redact,
@@ -29,7 +32,7 @@ import {
29
32
  startPresenceSession,
30
33
  stopPresenceSession,
31
34
  updatePresenceSession
32
- } from "./chunk-WVBSXZG3.js";
35
+ } from "./chunk-O4MHMJZZ.js";
33
36
  export {
34
37
  JsonFileOfflineQueue,
35
38
  MemoryOfflineQueue,
@@ -38,6 +41,7 @@ export {
38
41
  SystemCredentialStore,
39
42
  agentSourceSchema,
40
43
  canonicalHookEventSchema,
44
+ classifyHeadTransition,
41
45
  createHookEvent,
42
46
  detectRepository,
43
47
  detectWorkingTreeMetrics,
@@ -45,11 +49,13 @@ export {
45
49
  deviceTokenResponseSchema,
46
50
  eventTypeForHook,
47
51
  hookEventKindSchema,
52
+ isObservedPush,
48
53
  mapClaudeCodeHook,
49
54
  mapCodexHook,
50
55
  mapCursorHook,
51
56
  matchesGitHubRepository,
52
57
  normalizeGitHubRepository,
58
+ observeRepositoryTransitions,
53
59
  pollDeviceToken,
54
60
  presenceStatePath,
55
61
  redact,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wibeco/bridge",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Privacy-first live activity bridge for Cursor, Claude Code, and Codex.",
5
5
  "repository": {
6
6
  "type": "git",