@wibeco/bridge 0.2.15 → 0.2.17

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
@@ -18,6 +19,11 @@ Dependency-light TypeScript foundations for normalizing agent hooks and sending
18
19
  Install and authorize Wibe from any GitHub repository:
19
20
 
20
21
  ```sh
22
+ npx --yes --package @wibeco/bridge@latest wibe onboard \
23
+ --adapter cursor \
24
+ --url https://trywibe.com \
25
+ --repository <owner/repository>
26
+
21
27
  npx --yes --package @wibeco/bridge@latest wibe setup \
22
28
  --adapter cursor \
23
29
  --project <uuid> \
@@ -36,14 +42,18 @@ saved token was rejected or revoked.
36
42
 
37
43
  `setup` keeps reviewable templates under `.wibe/integrations/<adapter>` and
38
44
  installs native project configuration only when the destination file does not
39
- already exist. Existing Cursor, Claude Code, or Codex configuration is never
40
- 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.
41
48
 
42
49
  `WIBE_APP_URL` overrides the default local Wibe URL during setup.
43
- `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
44
52
  headless environments can inject `WIBE_ACCESS_TOKEN`, `WIBE_PROJECT_ID`,
45
53
  `WIBE_ORGANIZATION_ID`, `WIBE_REPOSITORY_ID`, and `WIBE_DEVICE_ID`; interactive
46
- 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`.
47
56
 
48
- Without a device credential, `emit` keeps normalized events in the offline
49
- 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());
639
+ }
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
+ });
607
658
  }
608
- async read() {
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 = {