@wibeco/bridge 0.2.6 → 0.2.8

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;
@@ -678,13 +735,13 @@ async function updatePresenceSession(source, sessionId, event, cwd = process.cwd
678
735
  );
679
736
  state.linesDeleted = isHeartbeat && state.linesDeleted > MAX_SESSION_LINE_COUNT ? workingDelta : Math.max(state.linesDeleted, workingDelta);
680
737
  }
681
- if (event.kind === "file.changed" && workingLinesAdded !== void 0 && state.taskBaselineLinesAdded !== void 0) {
738
+ if ((event.kind === "file.changed" || event.kind === "lifecycle.after") && workingLinesAdded !== void 0 && state.taskBaselineLinesAdded !== void 0) {
682
739
  state.taskLinesAdded = Math.max(
683
740
  state.taskLinesAdded,
684
741
  Math.max(0, workingLinesAdded - state.taskBaselineLinesAdded)
685
742
  );
686
743
  }
687
- if (event.kind === "file.changed" && workingLinesDeleted !== void 0 && state.taskBaselineLinesDeleted !== void 0) {
744
+ if ((event.kind === "file.changed" || event.kind === "lifecycle.after") && workingLinesDeleted !== void 0 && state.taskBaselineLinesDeleted !== void 0) {
688
745
  state.taskLinesDeleted = Math.max(
689
746
  state.taskLinesDeleted,
690
747
  Math.max(0, workingLinesDeleted - state.taskBaselineLinesDeleted)
@@ -700,6 +757,9 @@ async function updatePresenceSession(source, sessionId, event, cwd = process.cwd
700
757
  ...typeof event.metadata.path === "string" ? [event.metadata.path] : []
701
758
  ];
702
759
  state.paths = [.../* @__PURE__ */ new Set([...state.paths, ...paths])].slice(0, 2e3);
760
+ if (event.kind === "lifecycle.after" && state.taskPaths.length === 0) {
761
+ state.taskPaths = [...new Set(paths)].slice(0, 2e3);
762
+ }
703
763
  const category = toolCategory(event);
704
764
  if (category) {
705
765
  state.toolCounts[category] = (state.toolCounts[category] ?? 0) + 1;
@@ -708,8 +768,16 @@ async function updatePresenceSession(source, sessionId, event, cwd = process.cwd
708
768
  if (event.outcome === "success") state.testsPassed += 1;
709
769
  if (event.outcome === "failure") state.testsFailed += 1;
710
770
  }
771
+ const metrics = metricsFromState(state, measuredAt);
772
+ if (event.kind === "lifecycle.after") {
773
+ state.taskBaselineLinesAdded = workingLinesAdded;
774
+ state.taskBaselineLinesDeleted = workingLinesDeleted;
775
+ state.taskLinesAdded = 0;
776
+ state.taskLinesDeleted = 0;
777
+ state.taskPaths = [];
778
+ }
711
779
  await writePresenceState(statePath, state);
712
- return metricsFromState(state, measuredAt);
780
+ return metrics;
713
781
  }
714
782
  async function stopPresenceSession(source, sessionId, cwd = process.cwd()) {
715
783
  const statePath = presenceStatePath(source, sessionId, cwd);
@@ -889,16 +957,34 @@ function delay(milliseconds) {
889
957
  }
890
958
 
891
959
  // src/repo.ts
960
+ import { createHash as createHash2 } from "crypto";
892
961
  import { execFile } from "child_process";
962
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
963
+ import { homedir as homedir2 } from "os";
964
+ import { join as join2 } from "path";
893
965
  import { promisify } from "util";
894
966
  var execFileAsync = promisify(execFile);
967
+ function classifyHeadTransition(reflogAction, headParentCount) {
968
+ const reflog = reflogAction?.toLowerCase() ?? "";
969
+ if (reflog.startsWith("rebase")) return "git.rebase_completed";
970
+ if (reflog.startsWith("merge") && headParentCount && headParentCount > 1) {
971
+ return "git.merge_completed";
972
+ }
973
+ return reflog.startsWith("commit") ? "git.commit_created" : void 0;
974
+ }
975
+ function isObservedPush(previous, current, branchChanged = false) {
976
+ const reflog = current.reflogAction?.toLowerCase() ?? "";
977
+ return Boolean(
978
+ !branchChanged && !reflog.startsWith("pull") && !reflog.startsWith("fetch") && current.commit && current.upstreamCommit === current.commit && previous.upstreamCommit !== current.upstreamCommit
979
+ );
980
+ }
895
981
  async function git(cwd, args) {
896
982
  try {
897
983
  const { stdout } = await execFileAsync("git", args, {
898
984
  cwd,
899
985
  encoding: "utf8",
900
986
  timeout: 2e3,
901
- maxBuffer: 64 * 1024
987
+ maxBuffer: 4 * 1024 * 1024
902
988
  });
903
989
  return stdout.trim() || void 0;
904
990
  } catch {
@@ -929,10 +1015,24 @@ async function detectDefaultBranch(root) {
929
1015
  async function detectRepository(cwd = process.cwd()) {
930
1016
  const root = await git(cwd, ["rev-parse", "--show-toplevel"]);
931
1017
  if (!root) return void 0;
932
- const [remote, branch, commit] = await Promise.all([
1018
+ const [
1019
+ remote,
1020
+ branch,
1021
+ commit,
1022
+ upstreamCommit,
1023
+ stashCommit,
1024
+ parents,
1025
+ reflogAction,
1026
+ commitTitle
1027
+ ] = await Promise.all([
933
1028
  git(root, ["remote", "get-url", "origin"]),
934
1029
  git(root, ["branch", "--show-current"]),
935
- git(root, ["rev-parse", "HEAD"])
1030
+ git(root, ["rev-parse", "HEAD"]),
1031
+ git(root, ["rev-parse", "@{upstream}"]),
1032
+ git(root, ["rev-parse", "refs/stash"]),
1033
+ git(root, ["rev-list", "--parents", "-n", "1", "HEAD"]),
1034
+ git(root, ["reflog", "-1", "--format=%gs"]),
1035
+ git(root, ["log", "-1", "--format=%s"])
936
1036
  ]);
937
1037
  const defaultBranch = await detectDefaultBranch(root);
938
1038
  const baseCommit = branch && commit && defaultBranch ? branch === defaultBranch.branch ? commit : await git(root, [
@@ -953,7 +1053,144 @@ async function detectRepository(cwd = process.cwd()) {
953
1053
  ...commit ? { commit } : {},
954
1054
  ...defaultBranch ? { defaultBranch: defaultBranch.branch } : {},
955
1055
  ...baseCommit ? { baseCommit } : {},
956
- ...behindBy !== void 0 ? { behindBy } : {}
1056
+ ...behindBy !== void 0 ? { behindBy } : {},
1057
+ ...upstreamCommit ? { upstreamCommit } : {},
1058
+ ...stashCommit ? { stashCommit } : {},
1059
+ ...parents ? { headParentCount: Math.max(0, parents.split(/\s+/).length - 1) } : {},
1060
+ ...reflogAction ? { reflogAction } : {},
1061
+ ...commitTitle ? { commitTitle } : {}
1062
+ };
1063
+ }
1064
+ async function observeRepositoryTransitions(source, sessionId, current) {
1065
+ const directory = process.env.WIBE_GIT_STATE_DIR ?? join2(homedir2(), ".wibe", "git-state");
1066
+ const key = createHash2("sha256").update(`${source}\0${sessionId ?? ""}\0${current.root}`).digest("hex");
1067
+ const statePath = join2(directory, `${key}.json`);
1068
+ let previous;
1069
+ try {
1070
+ previous = JSON.parse(await readFile3(statePath, "utf8"));
1071
+ } catch {
1072
+ previous = void 0;
1073
+ }
1074
+ await mkdir3(directory, { recursive: true, mode: 448 });
1075
+ await writeFile3(statePath, `${JSON.stringify(current)}
1076
+ `, { mode: 384 });
1077
+ if (!previous) return [];
1078
+ const transitions = [];
1079
+ const branchChanged = Boolean(current.branch) && Boolean(previous.branch) && current.branch !== previous.branch;
1080
+ if (branchChanged) {
1081
+ transitions.push({
1082
+ kind: "git.branch_changed",
1083
+ metadata: {
1084
+ title: "Changed branch",
1085
+ previous_branch: previous.branch,
1086
+ branch: current.branch
1087
+ }
1088
+ });
1089
+ }
1090
+ if (current.stashCommit && current.stashCommit !== previous.stashCommit) {
1091
+ transitions.push({
1092
+ kind: "git.stash_created",
1093
+ metadata: {
1094
+ title: "Stashed changes",
1095
+ stash_sha: current.stashCommit
1096
+ }
1097
+ });
1098
+ }
1099
+ if (!branchChanged && current.commit && previous.commit && current.commit !== previous.commit) {
1100
+ const change = await detectRevisionChanges(
1101
+ current.root,
1102
+ previous.commit,
1103
+ current.commit
1104
+ );
1105
+ const kind = classifyHeadTransition(
1106
+ current.reflogAction,
1107
+ current.headParentCount
1108
+ );
1109
+ if (kind) {
1110
+ transitions.push({
1111
+ kind,
1112
+ metadata: {
1113
+ title: kind === "git.rebase_completed" ? "Rebased" : kind === "git.merge_completed" ? "Merged" : current.commitTitle || "Committed",
1114
+ previous_commit: previous.commit,
1115
+ commit_sha: current.commit,
1116
+ paths_known: change.pathsKnown,
1117
+ paths: change.paths,
1118
+ changes: change.changes,
1119
+ lines_added: change.linesAdded,
1120
+ lines_deleted: change.linesDeleted
1121
+ }
1122
+ });
1123
+ }
1124
+ }
1125
+ if (isObservedPush(previous, current, branchChanged) && current.commit) {
1126
+ const change = previous.upstreamCommit ? await detectRevisionChanges(
1127
+ current.root,
1128
+ previous.upstreamCommit,
1129
+ current.commit
1130
+ ) : {
1131
+ pathsKnown: false,
1132
+ paths: [],
1133
+ changes: [],
1134
+ linesAdded: 0,
1135
+ linesDeleted: 0
1136
+ };
1137
+ transitions.push({
1138
+ kind: "git.push_completed",
1139
+ metadata: {
1140
+ title: `Pushed to ${current.branch ?? "remote"}`,
1141
+ commit_sha: current.commit,
1142
+ paths_known: change.pathsKnown,
1143
+ paths: change.paths,
1144
+ changes: change.changes,
1145
+ lines_added: change.linesAdded,
1146
+ lines_deleted: change.linesDeleted
1147
+ }
1148
+ });
1149
+ }
1150
+ return transitions;
1151
+ }
1152
+ async function detectRevisionChanges(root, previous, current) {
1153
+ const [numstat, nameStatus] = await Promise.all([
1154
+ git(root, ["diff", "--numstat", previous, current, "--"]),
1155
+ git(root, ["diff", "--name-status", previous, current, "--"])
1156
+ ]);
1157
+ const pathsKnown = numstat !== void 0 && nameStatus !== void 0;
1158
+ const statusByPath = /* @__PURE__ */ new Map();
1159
+ for (const line of nameStatus?.split("\n") ?? []) {
1160
+ if (!line) continue;
1161
+ const [status, ...pathParts] = line.split(" ");
1162
+ const path = pathParts.at(-1);
1163
+ if (!path || isSensitivePath(path)) continue;
1164
+ statusByPath.set(
1165
+ path,
1166
+ status?.startsWith("A") ? "added" : status?.startsWith("D") ? "deleted" : "modified"
1167
+ );
1168
+ }
1169
+ let linesAdded = 0;
1170
+ let linesDeleted = 0;
1171
+ const changes = [];
1172
+ for (const line of numstat?.split("\n") ?? []) {
1173
+ if (!line) continue;
1174
+ const [added, deleted, ...pathParts] = line.split(" ");
1175
+ const path = pathParts.join(" ");
1176
+ if (!path || isSensitivePath(path)) continue;
1177
+ const additions = /^\d+$/.test(added) ? Number(added) : 0;
1178
+ const deletions = /^\d+$/.test(deleted) ? Number(deleted) : 0;
1179
+ linesAdded += additions;
1180
+ linesDeleted += deletions;
1181
+ changes.push({
1182
+ path,
1183
+ change_type: statusByPath.get(path) ?? "modified",
1184
+ additions,
1185
+ deletions
1186
+ });
1187
+ }
1188
+ return {
1189
+ pathsKnown,
1190
+ paths: changes.map((change) => change.path).slice(0, 2e3),
1191
+ changes: changes.slice(0, 2e3),
1192
+ linesAdded,
1193
+ linesDeleted
957
1194
  };
958
1195
  }
959
1196
  async function detectWorkingTreeMetrics(root) {
@@ -1181,7 +1418,10 @@ export {
1181
1418
  stopPresenceSession,
1182
1419
  runPresenceHeartbeat,
1183
1420
  presenceStatePath,
1421
+ classifyHeadTransition,
1422
+ isObservedPush,
1184
1423
  detectRepository,
1424
+ observeRepositoryTransitions,
1185
1425
  detectWorkingTreeMetrics,
1186
1426
  sanitizeRemote,
1187
1427
  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-44GDPIWM.js";
21
22
 
22
23
  // src/cli/commands.ts
23
24
  import { access, cp, mkdir, readFile, writeFile } from "fs/promises";
@@ -193,7 +194,7 @@ async function emitCommand(adapter, eventName, input) {
193
194
  const mapper = adapter === "cursor" ? mapCursorHook : adapter === "claude-code" ? mapClaudeCodeHook : mapCodexHook;
194
195
  let mappedEvent = mapper(eventName, input);
195
196
  const repo = await detectRepository(process.cwd());
196
- if (repo && (mappedEvent.kind === "session.started" || mappedEvent.kind === "session.ended" || mappedEvent.kind === "lifecycle.before" || mappedEvent.kind === "presence.heartbeat" || mappedEvent.kind === "file.changed")) {
197
+ if (repo && (mappedEvent.kind === "session.started" || mappedEvent.kind === "session.ended" || mappedEvent.kind === "lifecycle.before" || mappedEvent.kind === "lifecycle.after" || mappedEvent.kind === "presence.heartbeat" || mappedEvent.kind === "file.changed")) {
197
198
  const workingTree = await detectWorkingTreeMetrics(repo.root);
198
199
  if (workingTree) {
199
200
  mappedEvent = {
@@ -257,6 +258,17 @@ 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
+ paths: metrics.task_paths,
268
+ lines_added: metrics.task_lines_added,
269
+ lines_deleted: metrics.task_lines_deleted
270
+ }
271
+ };
260
272
  } else if (mappedEvent.kind === "presence.heartbeat" && metrics) {
261
273
  const publicMetadata = { ...mappedEvent.metadata };
262
274
  delete publicMetadata.working_tree_paths;
@@ -274,16 +286,32 @@ async function emitCommand(adapter, eventName, input) {
274
286
  mappedEvent = { ...mappedEvent, metadata: publicMetadata };
275
287
  }
276
288
  const event = repo ? { ...mappedEvent, repo } : mappedEvent;
289
+ const repositoryEvents = repo ? (await observeRepositoryTransitions(
290
+ adapter,
291
+ mappedEvent.sessionId,
292
+ repo
293
+ )).map(
294
+ (transition) => createHookEvent({
295
+ source: adapter,
296
+ kind: transition.kind,
297
+ ...mappedEvent.sessionId ? { sessionId: mappedEvent.sessionId } : {},
298
+ repo,
299
+ metadata: transition.metadata
300
+ })
301
+ ) : [];
302
+ const publishableEvents = [event, ...repositoryEvents].filter(
303
+ (candidate) => eventTypeForHook(candidate)
304
+ );
277
305
  const queue = new JsonFileOfflineQueue(queuePath());
278
306
  const credential = await loadCredential(process.cwd());
279
307
  if (!credential) {
280
- if (eventTypeForHook(event)) await queue.enqueue([event]);
308
+ if (publishableEvents.length) await queue.enqueue(publishableEvents);
281
309
  return {
282
310
  exitCode: 0,
283
- message: eventTypeForHook(event) ? `Queued ${event.kind}; this repository is not connected to Wibe.` : `Ignored operational ${event.kind} hook.`
311
+ message: publishableEvents.length ? `Queued ${publishableEvents.length} event(s); this repository is not connected to Wibe.` : `Ignored operational ${event.kind} hook.`
284
312
  };
285
313
  }
286
- const result = await new SignedBatchClient({
314
+ const client = new SignedBatchClient({
287
315
  endpoint: `${credential.appUrl}/api/events/batch`,
288
316
  accessToken: credential.accessToken,
289
317
  organizationId: credential.organizationId,
@@ -291,10 +319,19 @@ async function emitCommand(adapter, eventName, input) {
291
319
  repositoryId: credential.repositoryId,
292
320
  deviceId: credential.deviceId,
293
321
  queue
294
- }).capture(event);
322
+ });
323
+ let sent = 0;
324
+ let remaining = await queue.size();
325
+ let deliveryError;
326
+ for (const candidate of publishableEvents) {
327
+ const result = await client.capture(candidate);
328
+ sent += result.sent;
329
+ remaining = result.remaining;
330
+ deliveryError ??= result.error;
331
+ }
295
332
  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.`
333
+ exitCode: deliveryError ? 1 : 0,
334
+ message: deliveryError ? `Queued ${publishableEvents.length} event(s); delivery failed: ${deliveryError}` : publishableEvents.length ? `Delivered ${sent} event(s); ${remaining} queued.` : `Ignored operational ${event.kind} hook.`
298
335
  };
299
336
  }
300
337
  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-G66F67CK.js";
10
10
  import {
11
11
  runPresenceHeartbeat
12
- } from "./chunk-WVBSXZG3.js";
12
+ } from "./chunk-44GDPIWM.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-G66F67CK.js";
5
+ import "./chunk-44GDPIWM.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-44GDPIWM.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.8",
4
4
  "description": "Privacy-first live activity bridge for Cursor, Claude Code, and Codex.",
5
5
  "repository": {
6
6
  "type": "git",