@wibeco/bridge 0.2.16 → 0.2.18

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 CHANGED
@@ -7,7 +7,8 @@ Dependency-light TypeScript foundations for normalizing agent hooks and sending
7
7
  - Adapters copy only explicit allow-listed metadata. Prompt text, tool input/output, file content,
8
8
  shell command text, messages, and transcripts are never captured by default.
9
9
  - Redaction is defense in depth, not permission to send arbitrary payloads.
10
- - Offline files are created with owner-only permissions. Set `WIBE_QUEUE_PATH` to relocate the queue.
10
+ - Offline files are created with owner-only permissions and separated by Wibe project. Set
11
+ `WIBE_QUEUE_PATH` to relocate the queue directory.
11
12
  - Device authorization opens Wibe in the browser and stores the resulting
12
13
  revocable project-scoped bearer token in the operating-system keychain.
13
14
  - Event batches are schema-validated, bounded, idempotent, and sent only to the
@@ -41,14 +42,18 @@ saved token was rejected or revoked.
41
42
 
42
43
  `setup` keeps reviewable templates under `.wibe/integrations/<adapter>` and
43
44
  installs native project configuration only when the destination file does not
44
- already exist. Existing Cursor, Claude Code, or Codex configuration is never
45
- overwritten; merge the staged template when the project already has one.
45
+ already exist. Existing hooks and unrelated agent configuration are never
46
+ overwritten. Wibe-managed activity instructions and an existing Wibe MCP URL
47
+ are refreshed to the repository's current project.
46
48
 
47
49
  `WIBE_APP_URL` overrides the default local Wibe URL during setup.
48
- `WIBE_QUEUE_PATH` optionally relocates the owner-only offline queue. CI and
50
+ `WIBE_QUEUE_PATH` optionally relocates the owner-only per-project queue directory. If a legacy
51
+ file path is supplied, project queues are stored beside it under a `.d` suffix. CI and
49
52
  headless environments can inject `WIBE_ACCESS_TOKEN`, `WIBE_PROJECT_ID`,
50
53
  `WIBE_ORGANIZATION_ID`, `WIBE_REPOSITORY_ID`, and `WIBE_DEVICE_ID`; interactive
51
- developer machines should use the keychain-backed browser flow.
54
+ developer machines should use the keychain-backed browser flow. Environment
55
+ credentials are accepted only when they match that repository's `.wibe/project.json`.
52
56
 
53
- Without a device credential, `emit` keeps normalized events in the offline
54
- queue and never sends them elsewhere.
57
+ Without a device credential, `emit` keeps normalized events in a repository-scoped pending queue.
58
+ Setup adopts only that repository's pending events. Legacy identity-less queues are quarantined
59
+ and reported by `wibe doctor`; they are never delivered into an arbitrary project.
@@ -514,74 +514,107 @@ var SignedBatchClient = class {
514
514
  return this.flush();
515
515
  }
516
516
  async flush() {
517
- const queuedEvents = await this.options.queue.peek(this.batchSize);
518
- if (queuedEvents.length === 0) return { sent: 0, remaining: 0 };
519
- const events4 = queuedEvents.filter((event) => eventTypeForHook(event));
520
- const ignoredIds = queuedEvents.filter((event) => !eventTypeForHook(event)).map((event) => event.id);
521
- if (ignoredIds.length > 0) await this.options.queue.remove(ignoredIds);
522
- if (events4.length === 0) {
523
- return { sent: 0, remaining: await this.options.queue.size() };
524
- }
525
- const body = JSON.stringify({
526
- batch_id: randomUUID2(),
527
- events: events4.map((event) => toEnvelope(event, this.options))
528
- });
529
- try {
530
- const response = await this.request(this.options.endpoint, {
531
- method: "POST",
532
- body,
533
- signal: AbortSignal.timeout(this.timeoutMs),
534
- headers: {
535
- "content-type": "application/json",
536
- "user-agent": "wibe-bridge/1",
537
- authorization: `Bearer ${this.options.accessToken}`
538
- }
517
+ return this.options.queue.withTransaction(async (queue) => {
518
+ const queuedEvents = await queue.peek(this.batchSize);
519
+ if (queuedEvents.length === 0) return { sent: 0, remaining: 0 };
520
+ const events4 = queuedEvents.filter((event) => eventTypeForHook(event));
521
+ const ignoredIds = queuedEvents.filter((event) => !eventTypeForHook(event)).map((event) => event.id);
522
+ if (ignoredIds.length > 0) await queue.remove(ignoredIds);
523
+ if (events4.length === 0) {
524
+ return { sent: 0, remaining: await queue.size() };
525
+ }
526
+ const body = JSON.stringify({
527
+ batch_id: randomUUID2(),
528
+ events: events4.map((event) => toEnvelope(event, this.options))
539
529
  });
540
- if (!response.ok) {
541
- const requestId = response.headers.get("x-request-id");
542
- let detail = "";
543
- try {
544
- const body2 = await response.json();
545
- const code = typeof body2.code === "string" ? body2.code.slice(0, 80) : void 0;
546
- const message = typeof body2.error === "string" ? body2.error.slice(0, 160) : void 0;
547
- detail = [code, message].filter(Boolean).join(": ");
548
- } catch {
530
+ try {
531
+ const response = await this.request(this.options.endpoint, {
532
+ method: "POST",
533
+ body,
534
+ signal: AbortSignal.timeout(this.timeoutMs),
535
+ headers: {
536
+ "content-type": "application/json",
537
+ "user-agent": "wibe-bridge/1",
538
+ authorization: `Bearer ${this.options.accessToken}`
539
+ }
540
+ });
541
+ if (!response.ok) {
542
+ const requestId = response.headers.get("x-request-id");
543
+ let detail = "";
544
+ try {
545
+ const responseBody = await response.json();
546
+ const code = typeof responseBody.code === "string" ? responseBody.code.slice(0, 80) : void 0;
547
+ const message = typeof responseBody.error === "string" ? responseBody.error.slice(0, 160) : void 0;
548
+ detail = [code, message].filter(Boolean).join(": ");
549
+ } catch {
550
+ }
551
+ const suffix = [
552
+ detail ? `: ${detail}` : "",
553
+ requestId ? ` (request ${requestId.slice(0, 80)})` : ""
554
+ ].join("");
555
+ throw new Error(`Collector returned HTTP ${response.status}${suffix}`);
549
556
  }
550
- const suffix = [
551
- detail ? `: ${detail}` : "",
552
- requestId ? ` (request ${requestId.slice(0, 80)})` : ""
553
- ].join("");
554
- throw new Error(`Collector returned HTTP ${response.status}${suffix}`);
557
+ await queue.remove(events4.map((event) => event.id));
558
+ return { sent: events4.length, remaining: await queue.size() };
559
+ } catch (error) {
560
+ return {
561
+ sent: 0,
562
+ remaining: await queue.size(),
563
+ error: error instanceof Error ? error.message : String(error)
564
+ };
555
565
  }
556
- await this.options.queue.remove(events4.map((event) => event.id));
557
- return { sent: events4.length, remaining: await this.options.queue.size() };
558
- } catch (error) {
559
- return {
560
- sent: 0,
561
- remaining: await this.options.queue.size(),
562
- error: error instanceof Error ? error.message : String(error)
563
- };
564
- }
566
+ });
565
567
  }
566
568
  };
567
569
 
568
570
  // src/queue.ts
569
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
571
+ import { randomUUID as randomUUID3 } from "crypto";
572
+ import {
573
+ mkdir,
574
+ open,
575
+ readFile,
576
+ rename,
577
+ stat,
578
+ unlink,
579
+ writeFile
580
+ } from "fs/promises";
570
581
  import { dirname } from "path";
571
582
  var MemoryOfflineQueue = class {
572
583
  events = [];
584
+ operation = Promise.resolve();
573
585
  async enqueue(events4) {
574
- this.events.push(...events4);
586
+ await this.withTransaction(async () => {
587
+ this.events.push(...events4);
588
+ });
575
589
  }
576
590
  async peek(limit) {
577
- return this.events.slice(0, Math.max(0, limit));
591
+ return this.withTransaction((queue) => queue.peek(limit));
578
592
  }
579
593
  async remove(ids) {
580
- const removed = new Set(ids);
581
- this.events = this.events.filter((event) => !removed.has(event.id));
594
+ await this.withTransaction((queue) => queue.remove(ids));
582
595
  }
583
596
  async size() {
584
- return this.events.length;
597
+ return this.withTransaction((queue) => queue.size());
598
+ }
599
+ async withTransaction(operation) {
600
+ return this.serialize(
601
+ () => operation({
602
+ peek: async (limit) => this.events.slice(0, Math.max(0, limit)),
603
+ remove: async (ids) => {
604
+ const removed = new Set(ids);
605
+ this.events = this.events.filter((event) => !removed.has(event.id));
606
+ },
607
+ size: async () => this.events.length
608
+ })
609
+ );
610
+ }
611
+ async serialize(operation) {
612
+ const next = this.operation.then(operation);
613
+ this.operation = next.then(
614
+ () => void 0,
615
+ () => void 0
616
+ );
617
+ return next;
585
618
  }
586
619
  };
587
620
  var JsonFileOfflineQueue = class {
@@ -591,21 +624,39 @@ var JsonFileOfflineQueue = class {
591
624
  filePath;
592
625
  operation = Promise.resolve();
593
626
  async enqueue(events4) {
594
- await this.update((current) => [...current, ...events4]);
627
+ await this.withTransaction(async () => {
628
+ await this.updateUnlocked((current) => [...current, ...events4]);
629
+ });
595
630
  }
596
631
  async peek(limit) {
597
- await this.operation;
598
- return (await this.read()).slice(0, Math.max(0, limit));
632
+ return this.withTransaction((queue) => queue.peek(limit));
599
633
  }
600
634
  async remove(ids) {
601
- const removed = new Set(ids);
602
- await this.update((current) => current.filter((event) => !removed.has(event.id)));
635
+ await this.withTransaction((queue) => queue.remove(ids));
603
636
  }
604
637
  async size() {
605
- await this.operation;
606
- return (await this.read()).length;
638
+ return this.withTransaction((queue) => queue.size());
607
639
  }
608
- async read() {
640
+ async withTransaction(operation) {
641
+ return this.serialize(async () => {
642
+ const release = await this.acquireLock();
643
+ try {
644
+ return await operation({
645
+ peek: async (limit) => (await this.readUnlocked()).slice(0, Math.max(0, limit)),
646
+ remove: async (ids) => {
647
+ const removed = new Set(ids);
648
+ await this.updateUnlocked(
649
+ (current) => current.filter((event) => !removed.has(event.id))
650
+ );
651
+ },
652
+ size: async () => (await this.readUnlocked()).length
653
+ });
654
+ } finally {
655
+ await release();
656
+ }
657
+ });
658
+ }
659
+ async readUnlocked() {
609
660
  try {
610
661
  const data = JSON.parse(await readFile(this.filePath, "utf8"));
611
662
  return canonicalHookEventSchema.array().parse(data);
@@ -614,21 +665,78 @@ var JsonFileOfflineQueue = class {
614
665
  throw error;
615
666
  }
616
667
  }
617
- async update(transform) {
618
- const next = this.operation.then(async () => {
619
- const events4 = transform(await this.read());
620
- await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
621
- const temporary = `${this.filePath}.${process.pid}.tmp`;
622
- await writeFile(temporary, JSON.stringify(events4), { encoding: "utf8", mode: 384 });
623
- await rename(temporary, this.filePath);
668
+ async updateUnlocked(transform) {
669
+ const events4 = transform(await this.readUnlocked());
670
+ await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
671
+ const temporary = `${this.filePath}.${process.pid}.${randomUUID3()}.tmp`;
672
+ await writeFile(temporary, JSON.stringify(events4), {
673
+ encoding: "utf8",
674
+ mode: 384
624
675
  });
625
- this.operation = next.catch(() => void 0);
626
- await next;
676
+ await rename(temporary, this.filePath);
677
+ }
678
+ async serialize(operation) {
679
+ const next = this.operation.then(operation);
680
+ this.operation = next.then(
681
+ () => void 0,
682
+ () => void 0
683
+ );
684
+ return next;
685
+ }
686
+ async acquireLock() {
687
+ const lockPath = `${this.filePath}.lock`;
688
+ const token = randomUUID3();
689
+ const startedAt = Date.now();
690
+ await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
691
+ while (true) {
692
+ try {
693
+ const handle = await open(lockPath, "wx", 384);
694
+ try {
695
+ await handle.writeFile(
696
+ JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }),
697
+ "utf8"
698
+ );
699
+ } catch (error) {
700
+ await handle.close().catch(() => void 0);
701
+ await unlink(lockPath).catch(() => void 0);
702
+ throw error;
703
+ }
704
+ await handle.close();
705
+ return async () => {
706
+ try {
707
+ const current = JSON.parse(
708
+ await readFile(lockPath, "utf8")
709
+ );
710
+ if (current.token === token) await unlink(lockPath);
711
+ } catch (error) {
712
+ if (error.code !== "ENOENT") throw error;
713
+ }
714
+ };
715
+ } catch (error) {
716
+ if (error.code !== "EEXIST") throw error;
717
+ }
718
+ try {
719
+ const details = await stat(lockPath);
720
+ if (Date.now() - details.mtimeMs > 3e4) {
721
+ const stalePath = `${lockPath}.${process.pid}.${token}.stale`;
722
+ await rename(lockPath, stalePath);
723
+ await unlink(stalePath).catch(() => void 0);
724
+ continue;
725
+ }
726
+ } catch (error) {
727
+ if (error.code === "ENOENT") continue;
728
+ throw error;
729
+ }
730
+ if (Date.now() - startedAt > 15e3) {
731
+ throw new Error(`Timed out waiting for offline queue lock: ${lockPath}`);
732
+ }
733
+ await new Promise((resolve2) => setTimeout(resolve2, 25));
734
+ }
627
735
  }
628
736
  };
629
737
 
630
738
  // src/presence.ts
631
- import { createHash, randomUUID as randomUUID3 } from "crypto";
739
+ import { createHash, randomUUID as randomUUID4 } from "crypto";
632
740
  import { spawn } from "child_process";
633
741
  import { mkdir as mkdir2, readFile as readFile2, readdir, rm, writeFile as writeFile2 } from "fs/promises";
634
742
  import { homedir } from "os";
@@ -642,7 +750,7 @@ async function startPresenceSession(source, sessionId, cwd = process.cwd()) {
642
750
  const statePath = presenceStatePath(source, sessionId, cwd);
643
751
  await rm(statePath, { force: true });
644
752
  await mkdir2(presenceDirectory(), { recursive: true, mode: 448 });
645
- const instanceId = randomUUID3();
753
+ const instanceId = randomUUID4();
646
754
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
647
755
  const cliPath = process.argv[1];
648
756
  const state = {
@@ -818,6 +926,19 @@ function presenceStatePath(source, sessionId, cwd = process.cwd()) {
818
926
  return join(presenceDirectory(), `${key}.json`);
819
927
  }
820
928
  async function resolveLatestPresenceSession(source, cwd = process.cwd(), maxAgeMs = 5 * 6e4) {
929
+ return (await resolveLatestPresenceState(source, cwd, maxAgeMs))?.sessionId;
930
+ }
931
+ async function resolveLatestPresenceTask(source, cwd = process.cwd(), maxAgeMs = 5 * 6e4) {
932
+ const state = await resolveLatestPresenceState(source, cwd, maxAgeMs);
933
+ if (!state?.sessionId) return void 0;
934
+ return {
935
+ sessionId: state.sessionId,
936
+ linesAdded: state.taskLinesAdded,
937
+ linesDeleted: state.taskLinesDeleted,
938
+ paths: state.taskPaths
939
+ };
940
+ }
941
+ async function resolveLatestPresenceState(source, cwd, maxAgeMs) {
821
942
  let names;
822
943
  try {
823
944
  names = await readdir(presenceDirectory());
@@ -838,13 +959,10 @@ async function resolveLatestPresenceSession(source, cwd = process.cwd(), maxAgeM
838
959
  continue;
839
960
  }
840
961
  if (!latest || state.lastActivityAt > latest.lastActivityAt) {
841
- latest = {
842
- sessionId: state.sessionId,
843
- lastActivityAt: state.lastActivityAt
844
- };
962
+ latest = state;
845
963
  }
846
964
  }
847
- return latest?.sessionId;
965
+ return latest;
848
966
  }
849
967
  function presenceDirectory() {
850
968
  return process.env.WIBE_PRESENCE_DIR ?? join(homedir(), ".wibe", "presence");
@@ -1451,6 +1569,7 @@ export {
1451
1569
  runPresenceHeartbeat,
1452
1570
  presenceStatePath,
1453
1571
  resolveLatestPresenceSession,
1572
+ resolveLatestPresenceTask,
1454
1573
  classifyHeadTransition,
1455
1574
  isObservedPush,
1456
1575
  detectRepository,
@@ -15,16 +15,25 @@ import {
15
15
  observeRepositoryTransitions,
16
16
  pollDeviceToken,
17
17
  requestDeviceAuthorization,
18
- resolveLatestPresenceSession,
18
+ resolveLatestPresenceTask,
19
19
  startPresenceSession,
20
20
  stopPresenceSession,
21
21
  updatePresenceSession
22
- } from "./chunk-K7TK7FHF.js";
22
+ } from "./chunk-HP5FH4VQ.js";
23
23
 
24
24
  // src/cli/commands.ts
25
- import { access, cp, mkdir, readFile, writeFile } from "fs/promises";
25
+ import { createHash } from "crypto";
26
+ import { realpathSync } from "fs";
27
+ import {
28
+ access,
29
+ cp,
30
+ mkdir,
31
+ readFile,
32
+ rename,
33
+ writeFile
34
+ } from "fs/promises";
26
35
  import { homedir, hostname } from "os";
27
- import { basename, join, resolve } from "path";
36
+ import { basename, extname, join, resolve } from "path";
28
37
  import { fileURLToPath } from "url";
29
38
  import { execFile } from "child_process";
30
39
 
@@ -307,10 +316,12 @@ async function setupCommand(requestedAdapter, options = {}) {
307
316
  appUrl,
308
317
  options.projectId
309
318
  );
319
+ await adoptPendingQueue(cwd, options.projectId);
310
320
  const heartbeat2 = await sendVerificationHeartbeat(
311
321
  existingCredential,
312
322
  adapter,
313
- "setup"
323
+ "setup",
324
+ cwd
314
325
  );
315
326
  return {
316
327
  exitCode: heartbeat2.error ? 1 : 0,
@@ -397,7 +408,13 @@ Confirm code ${authorization.user_code}
397
408
  appUrl,
398
409
  token.projectId
399
410
  );
400
- const heartbeat = await sendVerificationHeartbeat(credential, adapter, "setup");
411
+ await adoptPendingQueue(cwd, token.projectId);
412
+ const heartbeat = await sendVerificationHeartbeat(
413
+ credential,
414
+ adapter,
415
+ "setup",
416
+ cwd
417
+ );
401
418
  return {
402
419
  exitCode: heartbeat.error ? 1 : 0,
403
420
  message: `${heartbeat.error ? `Connected ${adapter} to Wibe, but the verification heartbeat could not be delivered (${heartbeat.error}). It remains safe to retry with wibe doctor. Installed or refreshed ${installedNativeFiles.length} native config file(s); unmanaged configuration was left untouched.` : `Connected ${adapter} to Wibe and verified the event connection. Installed or refreshed ${installedNativeFiles.length} native config file(s); unmanaged configuration was left untouched and reviewable templates are at ${destination}.`}${adapterSetupNextSteps(adapter)}`
@@ -577,7 +594,13 @@ Install the Wibe GitHub App on ${repository}, then return here.
577
594
  appUrl,
578
595
  snapshot.project_id
579
596
  );
580
- const heartbeat = await sendVerificationHeartbeat(credential, adapter, "setup");
597
+ await adoptPendingQueue(cwd, snapshot.project_id);
598
+ const heartbeat = await sendVerificationHeartbeat(
599
+ credential,
600
+ adapter,
601
+ "setup",
602
+ cwd
603
+ );
581
604
  if (heartbeat.error) {
582
605
  return {
583
606
  exitCode: 1,
@@ -620,9 +643,11 @@ async function waitForOnboarding(appUrl, sessionId, sessionSecret, ready) {
620
643
  throw new Error("Timed out waiting for the browser step to finish.");
621
644
  }
622
645
  async function statusCommand(cwd = process.cwd()) {
646
+ const legacyQueue = await quarantineLegacyQueue();
623
647
  const repo = await detectRepository(cwd);
624
648
  const credential = await loadCredential(cwd);
625
- const queue = queuePath();
649
+ const queue = credential ? projectQueuePath(credential.projectId) : pendingQueuePath(repo?.root ?? cwd);
650
+ const queueBacklog = await new JsonFileOfflineQueue(queue).size();
626
651
  const installed = (await Promise.all(
627
652
  ADAPTERS.map(async (adapter) => ({
628
653
  adapter,
@@ -638,7 +663,9 @@ async function statusCommand(cwd = process.cwd()) {
638
663
  connected: Boolean(credential),
639
664
  projectId: credential?.projectId ?? null,
640
665
  eventEndpoint: credential ? `${credential.appUrl}/api/events/batch` : null,
641
- offlineQueue: queue
666
+ offlineQueue: queue,
667
+ offlineQueueBacklog: queueBacklog,
668
+ legacyQueue: legacyQueue.pending ? legacyQueue.quarantined : null
642
669
  },
643
670
  null,
644
671
  2
@@ -757,8 +784,11 @@ async function emitCommand(adapter, eventName, input) {
757
784
  const publishableEvents = [event, ...repositoryEvents].filter(
758
785
  (candidate) => eventTypeForHook(candidate)
759
786
  );
760
- const queue = new JsonFileOfflineQueue(queuePath());
761
787
  const credential = await loadCredential(process.cwd());
788
+ await quarantineLegacyQueue();
789
+ const queue = new JsonFileOfflineQueue(
790
+ credential ? projectQueuePath(credential.projectId) : pendingQueuePath(repo?.root ?? process.cwd())
791
+ );
762
792
  if (!credential) {
763
793
  if (publishableEvents.length) await queue.enqueue(publishableEvents);
764
794
  return {
@@ -797,6 +827,7 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
797
827
  if (!projectConfig || !credential) {
798
828
  throw new Error("Wibe is not authorized in this repository. Run wibe setup first.");
799
829
  }
830
+ await quarantineLegacyQueue();
800
831
  const summary = options.summary?.trim() ?? "";
801
832
  const wordCount = summary.split(/\s+/).filter(Boolean).length;
802
833
  if (wordCount < 20 || wordCount > 55 || summary.length > 500) {
@@ -824,8 +855,7 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
824
855
  throw new Error("Progress confidence must be between 0 and 1.");
825
856
  }
826
857
  const repo = await detectRepository(cwd);
827
- const workingTree = repo ? await detectWorkingTreeMetrics(repo.root) : void 0;
828
- const sessionId = await resolveLatestPresenceSession(
858
+ const taskMetrics = await resolveLatestPresenceTask(
829
859
  projectConfig.adapter,
830
860
  cwd
831
861
  );
@@ -838,17 +868,17 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
838
868
  const event = createHookEvent({
839
869
  source: projectConfig.adapter,
840
870
  kind: "progress.shared",
841
- ...sessionId ? { sessionId } : {},
871
+ ...taskMetrics?.sessionId ? { sessionId: taskMetrics.sessionId } : {},
842
872
  metadata: {
843
873
  summary,
844
874
  ...title ? { title } : {},
845
875
  ...options.phase ? { phase: options.phase } : {},
846
876
  ...options.confidence !== void 0 ? { confidence: options.confidence } : {},
847
877
  ...screenshot.artifactId ? { artifact_id: screenshot.artifactId } : {},
848
- ...workingTree ? {
849
- paths: workingTree.paths,
850
- lines_added: workingTree.linesAdded,
851
- lines_deleted: workingTree.linesDeleted
878
+ ...taskMetrics && (taskMetrics.paths.length > 0 || taskMetrics.linesAdded > 0 || taskMetrics.linesDeleted > 0) ? {
879
+ paths: taskMetrics.paths,
880
+ lines_added: taskMetrics.linesAdded,
881
+ lines_deleted: taskMetrics.linesDeleted
852
882
  } : {}
853
883
  },
854
884
  ...repo ? { repo } : {}
@@ -860,7 +890,7 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
860
890
  projectId: credential.projectId,
861
891
  repositoryId: credential.repositoryId,
862
892
  deviceId: credential.deviceId,
863
- queue: new JsonFileOfflineQueue(queuePath())
893
+ queue: new JsonFileOfflineQueue(projectQueuePath(credential.projectId))
864
894
  }).capture(event);
865
895
  return {
866
896
  exitCode: result.error ? 1 : 0,
@@ -868,16 +898,29 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
868
898
  };
869
899
  }
870
900
  async function doctorCommand(cwd = process.cwd(), requestedRepository) {
901
+ const legacyQueue = await quarantineLegacyQueue();
871
902
  const credential = await loadCredential(cwd);
872
903
  const projectConfig = await readProjectConfig(join(cwd, ".wibe", "project.json"));
904
+ const repositoryForQueue = await detectRepository(cwd);
905
+ const queuePath = credential ? projectQueuePath(credential.projectId) : pendingQueuePath(repositoryForQueue?.root ?? cwd);
906
+ let queueBacklog;
907
+ let queueError;
908
+ try {
909
+ queueBacklog = await new JsonFileOfflineQueue(queuePath).size();
910
+ } catch (error) {
911
+ queueError = error instanceof Error ? error.message : String(error);
912
+ }
873
913
  const expectedRepository = requestedRepository ? normalizeGitHubRepository(requestedRepository) : projectConfig?.repository;
874
914
  if (requestedRepository && !expectedRepository) {
875
915
  throw new Error(
876
916
  `Expected repository "${requestedRepository}" is not a valid GitHub owner/repository.`
877
917
  );
878
918
  }
879
- const repository = await detectRepository(cwd);
919
+ const repository = repositoryForQueue;
880
920
  const repositoryMatches = expectedRepository ? matchesGitHubRepository(repository?.remote, expectedRepository) : true;
921
+ const credentialMatchesProjectConfig = Boolean(
922
+ credential && projectConfig && credential.projectId === projectConfig.projectId && credential.appUrl.replace(/\/$/, "") === projectConfig.appUrl.replace(/\/$/, "")
923
+ );
881
924
  let adapter;
882
925
  let adapterError;
883
926
  try {
@@ -885,11 +928,17 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
885
928
  } catch (error) {
886
929
  adapterError = error instanceof Error ? error.message : String(error);
887
930
  }
888
- const heartbeat = credential && adapter && repositoryMatches ? await sendVerificationHeartbeat(credential, adapter, "doctor") : void 0;
889
- const nativeConfig = adapter ? await validateNativeConfigs(adapter, cwd) : { hooks: false, mcp: false, activityRule: false, projectTrust: void 0 };
931
+ const heartbeat = credential && adapter && repositoryMatches && credentialMatchesProjectConfig ? await sendVerificationHeartbeat(credential, adapter, "doctor", cwd) : void 0;
932
+ const nativeConfig = adapter ? await validateNativeConfigs(adapter, cwd, projectConfig?.projectId) : { hooks: false, mcp: false, activityRule: false, projectTrust: void 0 };
890
933
  const trustNotes = adapter === "codex" && nativeConfig.projectTrust === void 0 ? [
891
934
  "note Codex project trust could not be verified because the user config was not found; trust this project in Codex and rerun doctor"
892
935
  ] : [];
936
+ const queueNotes = [
937
+ `note project queue ${queuePath} contains ${queueBacklog ?? "unknown"} event(s)`,
938
+ ...legacyQueue.pending ? [
939
+ `note legacy shared queue is quarantined at ${legacyQueue.quarantined} and will not be delivered automatically`
940
+ ] : []
941
+ ];
893
942
  const checks = [
894
943
  ["node", Number(process.versions.node.split(".")[0]) >= 20],
895
944
  ["git repository", Boolean(repository)],
@@ -902,6 +951,14 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
902
951
  ["device authorization", Boolean(credential)],
903
952
  ["event endpoint", Boolean(credential?.appUrl)],
904
953
  ["project scope", Boolean(credential?.projectId)],
954
+ [
955
+ "project config matches device credential",
956
+ credentialMatchesProjectConfig
957
+ ],
958
+ [
959
+ queueError ? `project queue (${queueError})` : "project queue",
960
+ queueError === void 0
961
+ ],
905
962
  [
906
963
  adapterError ? `adapter detection (${adapterError})` : "adapter detection",
907
964
  Boolean(adapter)
@@ -927,13 +984,14 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
927
984
  exitCode: failures ? 1 : 0,
928
985
  message: [
929
986
  ...checks.map(([name, okay]) => `${okay ? "ok" : "missing"} ${name}`),
930
- ...trustNotes
987
+ ...trustNotes,
988
+ ...queueNotes
931
989
  ].join("\n")
932
990
  };
933
991
  }
934
992
  async function loadCredential(cwd) {
935
993
  if (process.env.WIBE_ACCESS_TOKEN && process.env.WIBE_PROJECT_ID && process.env.WIBE_ORGANIZATION_ID && process.env.WIBE_DEVICE_ID) {
936
- return {
994
+ const credential = {
937
995
  appUrl: process.env.WIBE_APP_URL ?? "http://localhost:3000",
938
996
  accessToken: process.env.WIBE_ACCESS_TOKEN,
939
997
  projectId: process.env.WIBE_PROJECT_ID,
@@ -941,17 +999,22 @@ async function loadCredential(cwd) {
941
999
  repositoryId: process.env.WIBE_REPOSITORY_ID,
942
1000
  deviceId: process.env.WIBE_DEVICE_ID
943
1001
  };
1002
+ const project = await readProjectConfig(join(cwd, ".wibe", "project.json"));
1003
+ if (!project || project.projectId !== credential.projectId || project.appUrl.replace(/\/$/, "") !== credential.appUrl.replace(/\/$/, "")) {
1004
+ return null;
1005
+ }
1006
+ return credential;
944
1007
  }
945
1008
  try {
946
- const project = JSON.parse(
947
- await readFile(join(cwd, ".wibe", "project.json"), "utf8")
948
- );
949
- if (!project.projectId) return null;
1009
+ const project = await readProjectConfig(join(cwd, ".wibe", "project.json"));
1010
+ if (!project) return null;
950
1011
  const stored = await new SystemCredentialStore().get(
951
1012
  "dev.wibe.bridge",
952
1013
  project.projectId
953
1014
  );
954
- return stored ? JSON.parse(stored) : null;
1015
+ if (!stored) return null;
1016
+ const credential = JSON.parse(stored);
1017
+ return credential.projectId === project.projectId && credential.appUrl.replace(/\/$/, "") === project.appUrl.replace(/\/$/, "") ? credential : null;
955
1018
  } catch {
956
1019
  return null;
957
1020
  }
@@ -982,8 +1045,19 @@ async function installNativeConfigs(adapter, source, cwd, appUrl, projectId) {
982
1045
  for (const [sourceName, destinationName] of files) {
983
1046
  const destinationPath = join(cwd, destinationName);
984
1047
  const destinationExists = await exists(destinationPath);
1048
+ const isMcpConfig = destinationName === ".cursor/mcp.json" || destinationName === ".mcp.json" || destinationName === ".codex/config.toml";
985
1049
  const isCursorActivityRule = adapter === "cursor" && destinationName === ".cursor/rules/wibe-activity.mdc";
986
1050
  if (destinationExists) {
1051
+ if (isMcpConfig) {
1052
+ if (await refreshMcpConfig(
1053
+ adapter,
1054
+ destinationPath,
1055
+ `${appUrl}/api/mcp/projects/${projectId}`
1056
+ )) {
1057
+ installed.push(destinationName);
1058
+ }
1059
+ continue;
1060
+ }
987
1061
  if (!isCursorActivityRule) continue;
988
1062
  const existingRule = await readFile(destinationPath, "utf8");
989
1063
  if (!existingRule.includes("# Wibe activity") || !existingRule.includes("wibe_share_progress")) {
@@ -1016,6 +1090,37 @@ async function installNativeConfigs(adapter, source, cwd, appUrl, projectId) {
1016
1090
  }
1017
1091
  return installed;
1018
1092
  }
1093
+ async function refreshMcpConfig(adapter, path, resourceUrl) {
1094
+ const current = await readFile(path, "utf8");
1095
+ if (adapter === "codex") {
1096
+ const next = current.replace(
1097
+ /(\[mcp_servers\.wibe\][\s\S]*?^\s*url\s*=\s*)["'][^"']*\/api\/mcp(?:\/projects\/[0-9a-f-]+)?\/?["']/m,
1098
+ `$1${JSON.stringify(resourceUrl)}`
1099
+ );
1100
+ if (next === current) return false;
1101
+ await writeFile(path, next, { mode: 384 });
1102
+ return true;
1103
+ }
1104
+ let parsed;
1105
+ try {
1106
+ const value = JSON.parse(current);
1107
+ if (!isRecord(value)) return false;
1108
+ parsed = value;
1109
+ } catch {
1110
+ return false;
1111
+ }
1112
+ if (!isRecord(parsed.mcpServers) || !isRecord(parsed.mcpServers.wibe)) {
1113
+ return false;
1114
+ }
1115
+ const existingUrl = parsed.mcpServers.wibe.url;
1116
+ if (typeof existingUrl !== "string" || !existingUrl.includes("/api/mcp")) {
1117
+ return false;
1118
+ }
1119
+ parsed.mcpServers.wibe.url = resourceUrl;
1120
+ await writeFile(path, `${JSON.stringify(parsed, null, 2)}
1121
+ `, { mode: 384 });
1122
+ return true;
1123
+ }
1019
1124
  function renderTemplate(template, appUrl, projectId) {
1020
1125
  return template.replaceAll("${env:WIBE_APP_URL}", appUrl).replaceAll("${WIBE_APP_URL}", appUrl).replaceAll("${WIBE_PROJECT_ID}", projectId);
1021
1126
  }
@@ -1045,8 +1150,69 @@ async function ensureClaudeImportsAgents(path) {
1045
1150
  await writeFile(path, next, { mode: 420 });
1046
1151
  return true;
1047
1152
  }
1048
- function queuePath() {
1049
- return process.env.WIBE_QUEUE_PATH ?? join(homedir(), ".wibe", "events.json");
1153
+ function queueRoot() {
1154
+ const override = process.env.WIBE_QUEUE_PATH?.trim();
1155
+ if (!override) return join(homedir(), ".wibe", "queue");
1156
+ return extname(override) ? `${override}.d` : override;
1157
+ }
1158
+ function projectQueuePath(projectId) {
1159
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
1160
+ projectId
1161
+ )) {
1162
+ throw new Error("Wibe project ID must be a UUID.");
1163
+ }
1164
+ return join(queueRoot(), `${projectId}.json`);
1165
+ }
1166
+ function pendingQueuePath(cwd) {
1167
+ let canonicalRoot = resolve(cwd);
1168
+ try {
1169
+ canonicalRoot = realpathSync.native(canonicalRoot);
1170
+ } catch {
1171
+ }
1172
+ const repositoryHash = createHash("sha256").update(canonicalRoot).digest("hex").slice(0, 24);
1173
+ return join(queueRoot(), `pending-${repositoryHash}.json`);
1174
+ }
1175
+ function legacyQueuePaths() {
1176
+ const override = process.env.WIBE_QUEUE_PATH?.trim();
1177
+ if (override && extname(override)) {
1178
+ return {
1179
+ current: override,
1180
+ quarantined: `${override.slice(0, -extname(override).length)}.legacy${extname(override)}`
1181
+ };
1182
+ }
1183
+ const directory = override || join(homedir(), ".wibe");
1184
+ return {
1185
+ current: join(directory, "events.json"),
1186
+ quarantined: join(directory, "events.legacy.json")
1187
+ };
1188
+ }
1189
+ async function quarantineLegacyQueue() {
1190
+ const paths = legacyQueuePaths();
1191
+ if (await exists(paths.current)) {
1192
+ if (!await exists(paths.quarantined)) {
1193
+ await rename(paths.current, paths.quarantined).catch((error) => {
1194
+ if (error.code !== "ENOENT") throw error;
1195
+ });
1196
+ }
1197
+ }
1198
+ return {
1199
+ ...paths,
1200
+ pending: await exists(paths.current) || await exists(paths.quarantined)
1201
+ };
1202
+ }
1203
+ async function adoptPendingQueue(cwd, projectId) {
1204
+ await quarantineLegacyQueue();
1205
+ const repository = await detectRepository(cwd);
1206
+ const pending = new JsonFileOfflineQueue(
1207
+ pendingQueuePath(repository?.root ?? cwd)
1208
+ );
1209
+ const project = new JsonFileOfflineQueue(projectQueuePath(projectId));
1210
+ while (true) {
1211
+ const events = await pending.peek(100);
1212
+ if (!events.length) return;
1213
+ await project.enqueue(events);
1214
+ await pending.remove(events.map((event) => event.id));
1215
+ }
1050
1216
  }
1051
1217
  async function exists(path) {
1052
1218
  try {
@@ -1056,7 +1222,7 @@ async function exists(path) {
1056
1222
  return false;
1057
1223
  }
1058
1224
  }
1059
- async function validateNativeConfigs(adapter, cwd) {
1225
+ async function validateNativeConfigs(adapter, cwd, projectId) {
1060
1226
  if (adapter === "codex") {
1061
1227
  const hooks = await validJson(
1062
1228
  join(cwd, ".codex", "hooks.json"),
@@ -1065,14 +1231,19 @@ async function validateNativeConfigs(adapter, cwd) {
1065
1231
  let mcp = false;
1066
1232
  try {
1067
1233
  const config = await readFile(join(cwd, ".codex", "config.toml"), "utf8");
1068
- mcp = config.includes("[mcp_servers.wibe]") && config.includes("/api/mcp");
1234
+ mcp = config.includes("[mcp_servers.wibe]") && Boolean(
1235
+ projectId && config.includes(`/api/mcp/projects/${projectId}`)
1236
+ );
1069
1237
  } catch {
1070
1238
  mcp = false;
1071
1239
  }
1072
1240
  return {
1073
1241
  hooks,
1074
1242
  mcp,
1075
- activityRule: await validWibeActivityInstructions(join(cwd, "AGENTS.md")),
1243
+ activityRule: await validWibeActivityInstructions(
1244
+ join(cwd, "AGENTS.md"),
1245
+ projectId
1246
+ ),
1076
1247
  projectTrust: await codexProjectTrust(cwd)
1077
1248
  };
1078
1249
  }
@@ -1083,19 +1254,21 @@ async function validateNativeConfigs(adapter, cwd) {
1083
1254
  mcp: await validJson(mcpPath, (value) => {
1084
1255
  if (!isRecord(value.mcpServers)) return false;
1085
1256
  const wibe = value.mcpServers.wibe;
1086
- return isRecord(wibe) && typeof wibe.url === "string" && wibe.url.replace(/\/$/, "").endsWith("/api/mcp");
1257
+ return isRecord(wibe) && typeof wibe.url === "string" && Boolean(
1258
+ projectId && wibe.url.replace(/\/$/, "").endsWith(`/api/mcp/projects/${projectId}`)
1259
+ );
1087
1260
  }),
1088
1261
  activityRule: adapter === "cursor" ? await validText(
1089
1262
  join(cwd, ".cursor", "rules", "wibe-activity.mdc"),
1090
- (value) => value.includes("alwaysApply: true") && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
1091
- ) : await validWibeActivityInstructions(join(cwd, "AGENTS.md")),
1263
+ (value) => value.includes("alwaysApply: true") && Boolean(projectId && value.includes(projectId)) && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
1264
+ ) : await validWibeActivityInstructions(join(cwd, "AGENTS.md"), projectId),
1092
1265
  projectTrust: void 0
1093
1266
  };
1094
1267
  }
1095
- function validWibeActivityInstructions(path) {
1268
+ function validWibeActivityInstructions(path, projectId) {
1096
1269
  return validText(
1097
1270
  path,
1098
- (value) => value.includes(WIBE_ACTIVITY_BEGIN) && value.includes(WIBE_ACTIVITY_END) && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
1271
+ (value) => value.includes(WIBE_ACTIVITY_BEGIN) && value.includes(WIBE_ACTIVITY_END) && Boolean(projectId && value.includes(projectId)) && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
1099
1272
  );
1100
1273
  }
1101
1274
  async function codexProjectTrust(cwd) {
@@ -1255,8 +1428,8 @@ async function assertExpectedRepository(cwd, expected) {
1255
1428
  `Repository mismatch: expected GitHub repository "${expected}", but found ${actual}. Run this command from the expected repository root or correct the origin remote, then retry.`
1256
1429
  );
1257
1430
  }
1258
- async function sendVerificationHeartbeat(credential, adapter, reason) {
1259
- const repository = await detectRepository(process.cwd());
1431
+ async function sendVerificationHeartbeat(credential, adapter, reason, cwd) {
1432
+ const repository = await detectRepository(cwd);
1260
1433
  const workingTree = repository ? await detectWorkingTreeMetrics(repository.root) : void 0;
1261
1434
  return new SignedBatchClient({
1262
1435
  endpoint: `${credential.appUrl}/api/events/batch`,
package/dist/cli.js CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  setupCommand,
8
8
  shareProgressCommand,
9
9
  statusCommand
10
- } from "./chunk-E2RWE6UW.js";
10
+ } from "./chunk-UWS3QC3N.js";
11
11
  import {
12
12
  runPresenceHeartbeat
13
- } from "./chunk-K7TK7FHF.js";
13
+ } from "./chunk-HP5FH4VQ.js";
14
14
 
15
15
  // src/cli.ts
16
16
  var HELP = `wibe-bridge <command>
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  emitCommand
4
- } from "./chunk-E2RWE6UW.js";
5
- import "./chunk-K7TK7FHF.js";
4
+ } from "./chunk-UWS3QC3N.js";
5
+ import "./chunk-HP5FH4VQ.js";
6
6
 
7
7
  // src/codex-hook.ts
8
8
  async function main() {
package/dist/index.d.ts CHANGED
@@ -228,18 +228,27 @@ declare function pollDeviceToken(input: {
228
228
  }>;
229
229
  type AgentName = "cursor" | "claude-code" | "codex";
230
230
 
231
+ interface OfflineQueueTransaction {
232
+ peek(limit: number): Promise<CanonicalHookEvent[]>;
233
+ remove(ids: readonly string[]): Promise<void>;
234
+ size(): Promise<number>;
235
+ }
231
236
  interface OfflineQueue {
232
237
  enqueue(events: readonly CanonicalHookEvent[]): Promise<void>;
233
238
  peek(limit: number): Promise<CanonicalHookEvent[]>;
234
239
  remove(ids: readonly string[]): Promise<void>;
235
240
  size(): Promise<number>;
241
+ withTransaction<T>(operation: (queue: OfflineQueueTransaction) => Promise<T>): Promise<T>;
236
242
  }
237
243
  declare class MemoryOfflineQueue implements OfflineQueue {
238
244
  private events;
245
+ private operation;
239
246
  enqueue(events: readonly CanonicalHookEvent[]): Promise<void>;
240
247
  peek(limit: number): Promise<CanonicalHookEvent[]>;
241
248
  remove(ids: readonly string[]): Promise<void>;
242
249
  size(): Promise<number>;
250
+ withTransaction<T>(operation: (queue: OfflineQueueTransaction) => Promise<T>): Promise<T>;
251
+ private serialize;
243
252
  }
244
253
  declare class JsonFileOfflineQueue implements OfflineQueue {
245
254
  private readonly filePath;
@@ -249,8 +258,11 @@ declare class JsonFileOfflineQueue implements OfflineQueue {
249
258
  peek(limit: number): Promise<CanonicalHookEvent[]>;
250
259
  remove(ids: readonly string[]): Promise<void>;
251
260
  size(): Promise<number>;
252
- private read;
253
- private update;
261
+ withTransaction<T>(operation: (queue: OfflineQueueTransaction) => Promise<T>): Promise<T>;
262
+ private readUnlocked;
263
+ private updateUnlocked;
264
+ private serialize;
265
+ private acquireLock;
254
266
  }
255
267
 
256
268
  interface SignedBatchClientOptions {
@@ -299,12 +311,19 @@ interface PresenceMetrics {
299
311
  tests_failed: number;
300
312
  last_activity_at: string;
301
313
  }
314
+ interface PresenceTaskMetrics {
315
+ sessionId: string;
316
+ linesAdded: number;
317
+ linesDeleted: number;
318
+ paths: string[];
319
+ }
302
320
  declare function startPresenceSession(source: AgentSource, sessionId: string | undefined, cwd?: string): Promise<PresenceMetrics>;
303
321
  declare function updatePresenceSession(source: AgentSource, sessionId: string | undefined, event: CanonicalHookEvent, cwd?: string): Promise<PresenceMetrics | undefined>;
304
322
  declare function stopPresenceSession(source: AgentSource, sessionId: string | undefined, cwd?: string): Promise<PresenceMetrics | undefined>;
305
323
  declare function runPresenceHeartbeat(statePath: string, expectedInstanceId: string, emit: (source: AgentSource, eventName: string, payload: Record<string, unknown>) => Promise<unknown>, intervalMs?: number): Promise<void>;
306
324
  declare function presenceStatePath(source: AgentSource, sessionId: string | undefined, cwd?: string): string;
307
325
  declare function resolveLatestPresenceSession(source: AgentSource, cwd?: string, maxAgeMs?: number): Promise<string | undefined>;
326
+ declare function resolveLatestPresenceTask(source: AgentSource, cwd?: string, maxAgeMs?: number): Promise<PresenceTaskMetrics | undefined>;
308
327
 
309
328
  interface RedactionOptions {
310
329
  allowContent?: boolean;
@@ -346,4 +365,4 @@ declare function sanitizeRemote(remote: string): string;
346
365
  declare function normalizeGitHubRepository(value: string): string | undefined;
347
366
  declare function matchesGitHubRepository(remote: string | undefined, expected: string): boolean;
348
367
 
349
- 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, resolveLatestPresenceSession, runPresenceHeartbeat, safeMetadata, safeValueSchema, sanitizeRemote, startPresenceSession, stopPresenceSession, updatePresenceSession };
368
+ export { type AgentSource, type CanonicalHookEvent, type CredentialStore, type DeviceAuthorization, type DeviceTokenResponse, type FlushResult, type HookEventKind, JsonFileOfflineQueue, MemoryOfflineQueue, type OfflineQueue, type OfflineQueueTransaction, PRESENCE_HEARTBEAT_INTERVAL_MS, type PresenceMetrics, type PresenceTaskMetrics, 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, resolveLatestPresenceSession, resolveLatestPresenceTask, runPresenceHeartbeat, safeMetadata, safeValueSchema, sanitizeRemote, startPresenceSession, stopPresenceSession, updatePresenceSession };
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  redact,
27
27
  requestDeviceAuthorization,
28
28
  resolveLatestPresenceSession,
29
+ resolveLatestPresenceTask,
29
30
  runPresenceHeartbeat,
30
31
  safeMetadata,
31
32
  safeValueSchema,
@@ -33,7 +34,7 @@ import {
33
34
  startPresenceSession,
34
35
  stopPresenceSession,
35
36
  updatePresenceSession
36
- } from "./chunk-K7TK7FHF.js";
37
+ } from "./chunk-HP5FH4VQ.js";
37
38
  export {
38
39
  JsonFileOfflineQueue,
39
40
  MemoryOfflineQueue,
@@ -62,6 +63,7 @@ export {
62
63
  redact,
63
64
  requestDeviceAuthorization,
64
65
  resolveLatestPresenceSession,
66
+ resolveLatestPresenceTask,
65
67
  runPresenceHeartbeat,
66
68
  safeMetadata,
67
69
  safeValueSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wibeco/bridge",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Privacy-first live activity bridge for Cursor, Claude Code, and Codex.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,5 +13,7 @@ stored in the operating-system keychain; hooks do not require repository secrets
13
13
  allow-list excludes prompt, content, messages, command text, tool arguments/results, and transcripts.
14
14
 
15
15
  `mcp.json.example` connects Claude Code to Wibe's remote MCP endpoint at
16
- `${env:WIBE_APP_URL}/api/mcp`. The MCP client completes OAuth separately from the bridge device token.
17
- Set `WIBE_APP_URL` to the same Wibe origin used during setup.
16
+ `${env:WIBE_APP_URL}/api/mcp/projects/${WIBE_PROJECT_ID}`. The project-specific resource keeps this
17
+ repository's OAuth grant isolated from other Wibe projects on the same machine. The MCP client
18
+ completes OAuth separately from the bridge device token. Set `WIBE_APP_URL` to the same Wibe origin
19
+ used during setup.
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "wibe": {
4
4
  "type": "http",
5
- "url": "${WIBE_APP_URL}/api/mcp"
5
+ "url": "${WIBE_APP_URL}/api/mcp/projects/${WIBE_PROJECT_ID}"
6
6
  }
7
7
  }
8
8
  }
@@ -8,7 +8,7 @@
8
8
  - For text-only outcomes, use `wibe_share_progress` with phase `shipped` at completion. Publish earlier only with phase `blocked` when a material blocker leaves the task unfinished.
9
9
  - Write one self-contained paragraph of 20–55 words in past tense. Lead with what now works, then explain the user or team impact and meaningful validation. Mention important limitations when present.
10
10
  - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
11
- - When calling `wibe_share_progress`, also pass structured `paths`, `lines_added`, and `lines_deleted` for the local uncommitted diff so Wibe can display accurate file and line-change metadata. Prefer `git diff --numstat HEAD` totals rather than guessing.
11
+ - When calling `wibe_share_progress`, pass `paths`, `lines_added`, and `lines_deleted` only for the current user task. Use the agent's per-task edit results; never pass the repository's full uncommitted diff. If exact task metrics are unavailable, omit them so Wibe can use hook-derived metrics instead of guessed totals.
12
12
  - This instruction is standing user authorization to publish the single completion update described here; do not ask for separate per-task approval.
13
13
  - If the MCP tool is unavailable, run `npx --yes --package @wibeco/bridge@latest wibe share-progress --summary "<summary>" --phase <shipped|blocked>`.
14
14
  - Do not silently skip reporting. If both mechanisms fail, state `Wibe: update failed` and the reason in the final response. Report a Wibe update as sent only after the tool or CLI confirms success.
@@ -9,8 +9,10 @@ and transcripts are not retained.
9
9
  Run `wibe setup` to authorize a revocable, project-scoped device token stored in the operating-system
10
10
  keychain. Hooks do not require repository signing secrets.
11
11
 
12
- The MCP block connects Codex to Wibe's remote `/api/mcp` endpoint. Codex completes OAuth separately
13
- from the bridge device token. Set `WIBE_APP_URL` to the same Wibe origin used during setup.
12
+ The MCP block connects Codex to a project-specific remote
13
+ `/api/mcp/projects/${WIBE_PROJECT_ID}` endpoint, keeping this repository's OAuth grant isolated from
14
+ other Wibe projects on the same machine. Codex completes OAuth separately from the bridge device
15
+ token. Set `WIBE_APP_URL` to the same Wibe origin used during setup.
14
16
  Project-local Codex configuration loads only after the project is trusted, and command hooks must be
15
17
  reviewed in `/hooks`. Session hooks own a metadata-only 45-second heartbeat that ends cleanly with
16
18
  `SessionEnd`.
@@ -1,2 +1,2 @@
1
1
  [mcp_servers.wibe]
2
- url = "${WIBE_APP_URL}/api/mcp"
2
+ url = "${WIBE_APP_URL}/api/mcp/projects/${WIBE_PROJECT_ID}"
@@ -8,7 +8,7 @@
8
8
  - For text-only outcomes, use `wibe_share_progress` with phase `shipped` at completion. Publish earlier only with phase `blocked` when a material blocker leaves the task unfinished.
9
9
  - Write one self-contained paragraph of 20–55 words in past tense. Lead with what now works, then explain the user or team impact and meaningful validation. Mention important limitations when present.
10
10
  - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
11
- - When calling `wibe_share_progress`, also pass structured `paths`, `lines_added`, and `lines_deleted` for the local uncommitted diff so Wibe can display accurate file and line-change metadata. Prefer `git diff --numstat HEAD` totals rather than guessing.
11
+ - When calling `wibe_share_progress`, pass `paths`, `lines_added`, and `lines_deleted` only for the current user task. Use the agent's per-task edit results; never pass the repository's full uncommitted diff. If exact task metrics are unavailable, omit them so Wibe can use hook-derived metrics instead of guessed totals.
12
12
  - This instruction is standing user authorization to publish the single completion update described here; do not ask for separate per-task approval.
13
13
  - If the MCP tool is unavailable, run `npx --yes --package @wibeco/bridge@latest wibe share-progress --summary "<summary>" --phase <shipped|blocked>`.
14
14
  - Do not silently skip reporting. If both mechanisms fail, state `Wibe: update failed` and the reason in the final response. Report a Wibe update as sent only after the tool or CLI confirms success.
@@ -17,5 +17,7 @@ stored in the operating-system keychain; hooks do not require repository secrets
17
17
  prompt, content, file body, command text, tool arguments/results, and transcripts.
18
18
 
19
19
  `mcp.json.example` connects Cursor to Wibe's remote MCP endpoint at
20
- `${env:WIBE_APP_URL}/api/mcp`. Cursor completes the endpoint's OAuth flow separately from the bridge
21
- device token. Set `WIBE_APP_URL` to the same Wibe origin used during setup.
20
+ `${env:WIBE_APP_URL}/api/mcp/projects/${WIBE_PROJECT_ID}`. The project-specific resource keeps this
21
+ repository's OAuth grant isolated from other Wibe projects on the same machine. Cursor completes
22
+ OAuth separately from the bridge device token. Set `WIBE_APP_URL` to the same Wibe origin used
23
+ during setup.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "wibe": {
4
- "url": "${env:WIBE_APP_URL}/api/mcp"
4
+ "url": "${env:WIBE_APP_URL}/api/mcp/projects/${WIBE_PROJECT_ID}"
5
5
  }
6
6
  }
7
7
  }
@@ -12,7 +12,7 @@ alwaysApply: true
12
12
  - For text-only outcomes, use `wibe_share_progress` with phase `shipped` at completion. Publish earlier only with phase `blocked` when a material blocker leaves the task unfinished.
13
13
  - Write one self-contained paragraph of 20–55 words in past tense. Lead with what now works, then explain the user or team impact and meaningful validation. Mention important limitations when present.
14
14
  - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
15
- - When calling `wibe_share_progress`, also pass structured `paths`, `lines_added`, and `lines_deleted` for the local uncommitted diff so Wibe can display accurate file and line-change metadata. Prefer `git diff --numstat HEAD` totals rather than guessing.
15
+ - When calling `wibe_share_progress`, pass `paths`, `lines_added`, and `lines_deleted` only for the current user task. Use the agent's per-task edit results; never pass the repository's full uncommitted diff. If exact task metrics are unavailable, omit them so Wibe can use hook-derived metrics instead of guessed totals.
16
16
  - Bias frontend outcomes toward visual evidence without asking for per-task approval. For UI components, pages, styling, responsive behavior, interactions, and visual fixes, attach one screenshot when the app or preview is already runnable. Prefer a screenshot already made during visual QA; otherwise capture the clearest final state.
17
17
  - Frame screenshots around the feature, not the whole application. Use the browser snapshot to identify the smallest element that contains the changed component and the context needed to understand it, then call `browser_take_screenshot` with that element’s `ref` and a descriptive `element` name. Include the trigger with an open menu, popover, or dialog when practical. Use viewport or full-page screenshots only for page-wide work, and reject captures dominated by blank space.
18
18
  - Skip screenshots for backend, infrastructure, documentation, refactors, and non-visual frontend logic. Never launch or repair an app solely for Wibe, and omit the image when navigation needs manual authentication, the state contains sensitive data, visual validation is blocked, or the project has visual updates disabled.