@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 +12 -7
- package/dist/{chunk-K7TK7FHF.js → chunk-HP5FH4VQ.js} +194 -75
- package/dist/{chunk-E2RWE6UW.js → chunk-UWS3QC3N.js} +213 -40
- package/dist/cli.js +2 -2
- package/dist/codex-hook.js +2 -2
- package/dist/index.d.ts +22 -3
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/templates/claude-code/README.md +4 -2
- package/templates/claude-code/mcp.json.example +1 -1
- package/templates/claude-code/wibe-activity.md.example +1 -1
- package/templates/codex/README.md +4 -2
- package/templates/codex/config.toml.example +1 -1
- package/templates/codex/wibe-activity.md.example +1 -1
- package/templates/cursor/README.md +4 -2
- package/templates/cursor/mcp.json.example +1 -1
- package/templates/cursor/wibe-activity.mdc.example +1 -1
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
|
|
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
|
|
45
|
-
overwritten
|
|
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
|
|
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
|
|
54
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
586
|
+
await this.withTransaction(async () => {
|
|
587
|
+
this.events.push(...events4);
|
|
588
|
+
});
|
|
575
589
|
}
|
|
576
590
|
async peek(limit) {
|
|
577
|
-
return this.
|
|
591
|
+
return this.withTransaction((queue) => queue.peek(limit));
|
|
578
592
|
}
|
|
579
593
|
async remove(ids) {
|
|
580
|
-
|
|
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.
|
|
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.
|
|
627
|
+
await this.withTransaction(async () => {
|
|
628
|
+
await this.updateUnlocked((current) => [...current, ...events4]);
|
|
629
|
+
});
|
|
595
630
|
}
|
|
596
631
|
async peek(limit) {
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
606
|
-
return (await this.read()).length;
|
|
638
|
+
return this.withTransaction((queue) => queue.size());
|
|
607
639
|
}
|
|
608
|
-
async
|
|
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
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
18
|
+
resolveLatestPresenceTask,
|
|
19
19
|
startPresenceSession,
|
|
20
20
|
stopPresenceSession,
|
|
21
21
|
updatePresenceSession
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-HP5FH4VQ.js";
|
|
23
23
|
|
|
24
24
|
// src/cli/commands.ts
|
|
25
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
...
|
|
849
|
-
paths:
|
|
850
|
-
lines_added:
|
|
851
|
-
lines_deleted:
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
947
|
-
|
|
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
|
-
|
|
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
|
|
1049
|
-
|
|
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]") &&
|
|
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(
|
|
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" &&
|
|
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(
|
|
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-
|
|
10
|
+
} from "./chunk-UWS3QC3N.js";
|
|
11
11
|
import {
|
|
12
12
|
runPresenceHeartbeat
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-HP5FH4VQ.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
16
|
var HELP = `wibe-bridge <command>
|
package/dist/codex-hook.js
CHANGED
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
|
-
|
|
253
|
-
private
|
|
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-
|
|
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
|
@@ -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
|
|
17
|
-
|
|
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.
|
|
@@ -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`,
|
|
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
|
|
13
|
-
|
|
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`,
|
|
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`.
|
|
21
|
-
|
|
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.
|
|
@@ -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`,
|
|
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.
|