@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.
@@ -19,15 +19,206 @@ import {
19
19
  startPresenceSession,
20
20
  stopPresenceSession,
21
21
  updatePresenceSession
22
- } from "./chunk-K7TK7FHF.js";
22
+ } from "./chunk-4WIGVKDR.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";
39
+
40
+ // src/onboard.ts
41
+ import { z } from "zod";
42
+ var sessionCreatedSchema = z.object({
43
+ session_id: z.string().uuid(),
44
+ session_secret: z.string().min(1),
45
+ authorization_url: z.string().url(),
46
+ expires_at: z.string()
47
+ });
48
+ var snapshotSchema = z.object({
49
+ status: z.enum(["pending_auth", "authenticated", "completed", "expired"]),
50
+ next: z.enum([
51
+ "authenticate",
52
+ "create_organization",
53
+ "install_github",
54
+ "create_project",
55
+ "connect_device",
56
+ "activate",
57
+ "done"
58
+ ]),
59
+ user_id: z.string().uuid().nullable(),
60
+ organization_id: z.string().uuid().nullable(),
61
+ project_id: z.string().uuid().nullable(),
62
+ github_connected: z.boolean(),
63
+ project_url: z.string().url().nullable(),
64
+ expires_at: z.string(),
65
+ access_token: z.string().min(1).optional()
66
+ });
67
+ var organizationSchema = z.object({
68
+ organization_id: z.string().uuid()
69
+ });
70
+ var githubInstallSchema = z.object({
71
+ github_connected: z.boolean(),
72
+ install_url: z.string().url().nullable()
73
+ });
74
+ var repositoriesSchema = z.object({
75
+ repositories: z.array(
76
+ z.object({
77
+ id: z.number().int(),
78
+ installationId: z.number().int(),
79
+ fullName: z.string(),
80
+ name: z.string()
81
+ })
82
+ )
83
+ });
84
+ var projectSchema = z.object({
85
+ project_id: z.string().uuid(),
86
+ reused: z.boolean().optional(),
87
+ setup_status: z.string().optional()
88
+ });
89
+ var invitesSchema = z.object({
90
+ invites: z.array(
91
+ z.object({
92
+ email: z.string().nullable().optional(),
93
+ url: z.string().url()
94
+ })
95
+ )
96
+ });
97
+ var deviceSchema = z.object({
98
+ accessToken: z.string().min(1),
99
+ projectId: z.string().uuid(),
100
+ organizationId: z.string().uuid(),
101
+ repositoryId: z.string().uuid().optional(),
102
+ deviceId: z.string().uuid()
103
+ });
104
+ var activateSchema = z.object({
105
+ project_id: z.string().uuid(),
106
+ project_url: z.string().url()
107
+ });
108
+ function origin(appUrl) {
109
+ return appUrl.replace(/\/$/, "");
110
+ }
111
+ async function readJson(response) {
112
+ return await response.json().catch(() => ({}));
113
+ }
114
+ async function requestJson(url, schema, init) {
115
+ const response = await fetch(url, init);
116
+ const body = await readJson(response);
117
+ if (!response.ok) {
118
+ throw new Error(
119
+ typeof body.error === "string" ? body.error : `Onboarding request failed (${response.status}).`
120
+ );
121
+ }
122
+ return schema.parse(body);
123
+ }
124
+ function authorized(accessToken) {
125
+ return {
126
+ authorization: `Bearer ${accessToken}`,
127
+ "content-type": "application/json"
128
+ };
129
+ }
130
+ async function createOnboardingSession(appUrl) {
131
+ return requestJson(
132
+ `${origin(appUrl)}/api/onboarding/sessions`,
133
+ sessionCreatedSchema,
134
+ { method: "POST" }
135
+ );
136
+ }
137
+ async function pollOnboardingSession(input) {
138
+ return requestJson(
139
+ `${origin(input.appUrl)}/api/onboarding/sessions/${input.sessionId}`,
140
+ snapshotSchema,
141
+ { headers: { "x-wibe-onboarding-secret": input.sessionSecret } }
142
+ );
143
+ }
144
+ async function createOnboardingOrganization(input) {
145
+ return requestJson(
146
+ `${origin(input.appUrl)}/api/onboarding/organization`,
147
+ organizationSchema,
148
+ {
149
+ method: "POST",
150
+ headers: authorized(input.accessToken),
151
+ body: JSON.stringify({ name: input.name })
152
+ }
153
+ );
154
+ }
155
+ async function requestGitHubInstall(input) {
156
+ return requestJson(
157
+ `${origin(input.appUrl)}/api/onboarding/github/install`,
158
+ githubInstallSchema,
159
+ { method: "POST", headers: authorized(input.accessToken) }
160
+ );
161
+ }
162
+ async function listOnboardingRepositories(input) {
163
+ return requestJson(
164
+ `${origin(input.appUrl)}/api/onboarding/github/repositories`,
165
+ repositoriesSchema,
166
+ { headers: authorized(input.accessToken) }
167
+ );
168
+ }
169
+ async function createOnboardingProject(input) {
170
+ return requestJson(
171
+ `${origin(input.appUrl)}/api/onboarding/project`,
172
+ projectSchema,
173
+ {
174
+ method: "POST",
175
+ headers: authorized(input.accessToken),
176
+ body: JSON.stringify({
177
+ projectName: input.projectName,
178
+ githubInstallationId: input.githubInstallationId,
179
+ providerRepositoryId: input.providerRepositoryId,
180
+ repository: input.repository
181
+ })
182
+ }
183
+ );
184
+ }
185
+ async function createOnboardingInvites(input) {
186
+ return requestJson(
187
+ `${origin(input.appUrl)}/api/onboarding/invites`,
188
+ invitesSchema,
189
+ {
190
+ method: "POST",
191
+ headers: authorized(input.accessToken),
192
+ body: JSON.stringify({ emails: input.emails })
193
+ }
194
+ );
195
+ }
196
+ async function mintOnboardingDevice(input) {
197
+ return requestJson(
198
+ `${origin(input.appUrl)}/api/onboarding/device`,
199
+ deviceSchema,
200
+ {
201
+ method: "POST",
202
+ headers: authorized(input.accessToken),
203
+ body: JSON.stringify({
204
+ deviceName: input.deviceName,
205
+ agentName: input.agentName
206
+ })
207
+ }
208
+ );
209
+ }
210
+ async function activateOnboardingProject(input) {
211
+ return requestJson(
212
+ `${origin(input.appUrl)}/api/onboarding/activate`,
213
+ activateSchema,
214
+ { method: "POST", headers: authorized(input.accessToken) }
215
+ );
216
+ }
217
+
218
+ // src/cli/commands.ts
30
219
  var ADAPTERS = ["cursor", "claude-code", "codex"];
220
+ var WIBE_ACTIVITY_BEGIN = "<!-- BEGIN:wibe-activity -->";
221
+ var WIBE_ACTIVITY_END = "<!-- END:wibe-activity -->";
31
222
  var MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
32
223
  function screenshotMimeType(bytes) {
33
224
  if ([137, 80, 78, 71, 13, 10, 26, 10].every(
@@ -125,14 +316,16 @@ async function setupCommand(requestedAdapter, options = {}) {
125
316
  appUrl,
126
317
  options.projectId
127
318
  );
319
+ await adoptPendingQueue(cwd, options.projectId);
128
320
  const heartbeat2 = await sendVerificationHeartbeat(
129
321
  existingCredential,
130
322
  adapter,
131
- "setup"
323
+ "setup",
324
+ cwd
132
325
  );
133
326
  return {
134
327
  exitCode: heartbeat2.error ? 1 : 0,
135
- message: heartbeat2.error ? `Wibe is already authorized, but verification failed (${heartbeat2.error}). No new device was created. Retry with wibe doctor; if the token is rejected, rerun setup with --reauthorize.` : `Wibe was already authorized and the event connection was verified. Installed ${installedNativeFiles2.length} missing native config file(s); existing configs were left untouched.`
328
+ message: `${heartbeat2.error ? `Wibe is already authorized, but verification failed (${heartbeat2.error}). No new device was created. Retry with wibe doctor; if the token is rejected, rerun setup with --reauthorize.` : `Wibe was already authorized and the event connection was verified. Installed or refreshed ${installedNativeFiles2.length} native config file(s); unmanaged configuration was left untouched.`}${adapterSetupNextSteps(adapter)}`
136
329
  };
137
330
  }
138
331
  const authorization = await requestDeviceAuthorization({
@@ -215,16 +408,246 @@ Confirm code ${authorization.user_code}
215
408
  appUrl,
216
409
  token.projectId
217
410
  );
218
- 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
+ );
219
418
  return {
220
419
  exitCode: heartbeat.error ? 1 : 0,
221
- 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 ${installedNativeFiles.length} native config file(s); existing configs were left untouched.` : `Connected ${adapter} to Wibe and verified the event connection. Installed ${installedNativeFiles.length} native config file(s); existing configs were left untouched and reviewable templates are at ${destination}.`
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)}`
222
421
  };
223
422
  }
423
+ function adapterSetupNextSteps(adapter) {
424
+ return adapter === "codex" ? "\nCodex next steps: trust this project when prompted, review and trust the Wibe command hooks in /hooks, then restart Codex so the project MCP server and activity instructions load." : "";
425
+ }
426
+ async function onboardCommand(requestedAdapter, options = {}) {
427
+ const cwd = options.cwd ?? process.cwd();
428
+ const expectedRepository = options.expectedRepository ? normalizeGitHubRepository(options.expectedRepository) : void 0;
429
+ if (options.expectedRepository && !expectedRepository) {
430
+ throw new Error(
431
+ `Expected repository "${options.expectedRepository}" is not a valid GitHub owner/repository.`
432
+ );
433
+ }
434
+ const detectedRepository = await detectRepository(cwd);
435
+ const repository = expectedRepository ?? (detectedRepository?.remote ? normalizeGitHubRepository(detectedRepository.remote) : void 0);
436
+ if (!repository) {
437
+ throw new Error(
438
+ "Could not detect a GitHub repository remote. Run this from a repository root or pass --repository owner/repo."
439
+ );
440
+ }
441
+ if (expectedRepository) {
442
+ await assertExpectedRepository(cwd, expectedRepository);
443
+ }
444
+ const adapter = await detectAdapter(requestedAdapter, cwd);
445
+ const source = await resolveTemplateSource(adapter);
446
+ const destination = join(cwd, ".wibe", "integrations", adapter);
447
+ if (!await exists(destination)) {
448
+ await mkdir(join(cwd, ".wibe", "integrations"), { recursive: true });
449
+ await cp(source, destination, { recursive: true, errorOnExist: true, force: false });
450
+ }
451
+ const appUrl = (options.appUrl ?? process.env.WIBE_APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
452
+ const created = options.sessionId && options.sessionSecret ? {
453
+ session_id: options.sessionId,
454
+ session_secret: options.sessionSecret,
455
+ authorization_url: `${appUrl}/onboard`,
456
+ expires_at: new Date(Date.now() + 30 * 60 * 1e3).toISOString()
457
+ } : await createOnboardingSession(appUrl);
458
+ let snapshot = await pollOnboardingSession({
459
+ appUrl,
460
+ sessionId: created.session_id,
461
+ sessionSecret: created.session_secret
462
+ });
463
+ if (snapshot.next === "authenticate") {
464
+ process.stdout.write(
465
+ `Open ${created.authorization_url}
466
+ Approve GitHub sign-in in your browser, then return here.
467
+ `
468
+ );
469
+ openBrowser(created.authorization_url);
470
+ snapshot = await waitForOnboarding(
471
+ appUrl,
472
+ created.session_id,
473
+ created.session_secret,
474
+ (current) => current.next !== "authenticate" && Boolean(current.access_token)
475
+ );
476
+ }
477
+ const accessToken = snapshot.access_token;
478
+ if (!accessToken) {
479
+ throw new Error("Wibe onboarding did not return an access token.");
480
+ }
481
+ if (snapshot.next === "create_organization" || !snapshot.organization_id) {
482
+ const orgName = options.orgName?.trim() || repository.split("/")[0] || "Organization";
483
+ await createOnboardingOrganization({
484
+ appUrl,
485
+ accessToken,
486
+ name: orgName
487
+ });
488
+ snapshot = await pollOnboardingSession({
489
+ appUrl,
490
+ sessionId: created.session_id,
491
+ sessionSecret: created.session_secret
492
+ });
493
+ }
494
+ if (snapshot.next === "install_github" || !snapshot.github_connected) {
495
+ const install = await requestGitHubInstall({ appUrl, accessToken });
496
+ if (!install.github_connected) {
497
+ if (!install.install_url) {
498
+ throw new Error("Wibe did not return a GitHub App install URL.");
499
+ }
500
+ process.stdout.write(
501
+ `Open ${install.install_url}
502
+ Install the Wibe GitHub App on ${repository}, then return here.
503
+ `
504
+ );
505
+ openBrowser(install.install_url);
506
+ snapshot = await waitForOnboarding(
507
+ appUrl,
508
+ created.session_id,
509
+ created.session_secret,
510
+ (current) => current.github_connected
511
+ );
512
+ }
513
+ }
514
+ const repositories = await listOnboardingRepositories({ appUrl, accessToken });
515
+ const matched = repositories.repositories.find(
516
+ (candidate) => normalizeGitHubRepository(candidate.fullName) === repository
517
+ );
518
+ if (!matched) {
519
+ throw new Error(
520
+ `GitHub App install does not include ${repository}. Install Wibe on that repository and retry.`
521
+ );
522
+ }
523
+ if (snapshot.next === "create_project" || !snapshot.project_id) {
524
+ await createOnboardingProject({
525
+ appUrl,
526
+ accessToken,
527
+ projectName: matched.name,
528
+ githubInstallationId: matched.installationId,
529
+ providerRepositoryId: matched.id,
530
+ repository: matched.fullName
531
+ });
532
+ snapshot = await pollOnboardingSession({
533
+ appUrl,
534
+ sessionId: created.session_id,
535
+ sessionSecret: created.session_secret
536
+ });
537
+ }
538
+ const inviteEmails = (options.invites ?? []).filter(Boolean);
539
+ const invites = await createOnboardingInvites({
540
+ appUrl,
541
+ accessToken,
542
+ emails: inviteEmails
543
+ });
544
+ if (!snapshot.project_id) {
545
+ throw new Error("Wibe onboarding did not create a project.");
546
+ }
547
+ const projectConfigPath = join(cwd, ".wibe", "project.json");
548
+ const existingProjectConfig = await readProjectConfig(projectConfigPath);
549
+ const existingCredential = existingProjectConfig ? await loadCredential(cwd) : null;
550
+ let credential = existingCredential;
551
+ if (!credential || credential.projectId !== snapshot.project_id || credential.appUrl.replace(/\/$/, "") !== appUrl) {
552
+ const minted = await mintOnboardingDevice({
553
+ appUrl,
554
+ accessToken,
555
+ deviceName: `${hostname()} (${adapter})`,
556
+ agentName: adapter
557
+ });
558
+ credential = {
559
+ appUrl,
560
+ accessToken: minted.accessToken,
561
+ projectId: minted.projectId,
562
+ organizationId: minted.organizationId,
563
+ repositoryId: minted.repositoryId,
564
+ deviceId: minted.deviceId
565
+ };
566
+ await new SystemCredentialStore().set(
567
+ "dev.wibe.bridge",
568
+ minted.projectId,
569
+ JSON.stringify(credential)
570
+ );
571
+ if (!existingProjectConfig) {
572
+ await mkdir(join(cwd, ".wibe"), { recursive: true });
573
+ await writeFile(
574
+ projectConfigPath,
575
+ `${JSON.stringify(
576
+ {
577
+ projectId: minted.projectId,
578
+ appUrl,
579
+ adapter,
580
+ repository
581
+ },
582
+ null,
583
+ 2
584
+ )}
585
+ `,
586
+ { mode: 384, flag: "wx" }
587
+ );
588
+ }
589
+ }
590
+ const installedNativeFiles = await installNativeConfigs(
591
+ adapter,
592
+ source,
593
+ cwd,
594
+ appUrl,
595
+ snapshot.project_id
596
+ );
597
+ await adoptPendingQueue(cwd, snapshot.project_id);
598
+ const heartbeat = await sendVerificationHeartbeat(
599
+ credential,
600
+ adapter,
601
+ "setup",
602
+ cwd
603
+ );
604
+ if (heartbeat.error) {
605
+ return {
606
+ exitCode: 1,
607
+ message: `Connected ${adapter} to Wibe, but the verification heartbeat could not be delivered (${heartbeat.error}). It remains safe to retry with wibe doctor.`
608
+ };
609
+ }
610
+ snapshot = await pollOnboardingSession({
611
+ appUrl,
612
+ sessionId: created.session_id,
613
+ sessionSecret: created.session_secret
614
+ });
615
+ let projectUrl = snapshot.project_url;
616
+ if (snapshot.next === "activate" || !projectUrl) {
617
+ const activated = await activateOnboardingProject({ appUrl, accessToken });
618
+ projectUrl = activated.project_url;
619
+ }
620
+ const inviteLines = invites.invites.map((invite) => invite.url).join("\n");
621
+ return {
622
+ exitCode: 0,
623
+ message: [
624
+ `Connected ${adapter} to Wibe and opened the project space.`,
625
+ projectUrl,
626
+ inviteLines ? `Invite links:
627
+ ${inviteLines}` : null,
628
+ `Installed ${installedNativeFiles.length} native config file(s); reviewable templates are at ${destination}.`
629
+ ].filter(Boolean).join("\n")
630
+ };
631
+ }
632
+ async function waitForOnboarding(appUrl, sessionId, sessionSecret, ready) {
633
+ const deadline = Date.now() + 10 * 60 * 1e3;
634
+ let snapshot = await pollOnboardingSession({ appUrl, sessionId, sessionSecret });
635
+ while (Date.now() < deadline) {
636
+ if (ready(snapshot)) return snapshot;
637
+ if (snapshot.status === "expired") {
638
+ throw new Error("Onboarding expired. Restart wibe onboard.");
639
+ }
640
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
641
+ snapshot = await pollOnboardingSession({ appUrl, sessionId, sessionSecret });
642
+ }
643
+ throw new Error("Timed out waiting for the browser step to finish.");
644
+ }
224
645
  async function statusCommand(cwd = process.cwd()) {
646
+ const legacyQueue = await quarantineLegacyQueue();
225
647
  const repo = await detectRepository(cwd);
226
648
  const credential = await loadCredential(cwd);
227
- const queue = queuePath();
649
+ const queue = credential ? projectQueuePath(credential.projectId) : pendingQueuePath(repo?.root ?? cwd);
650
+ const queueBacklog = await new JsonFileOfflineQueue(queue).size();
228
651
  const installed = (await Promise.all(
229
652
  ADAPTERS.map(async (adapter) => ({
230
653
  adapter,
@@ -240,7 +663,9 @@ async function statusCommand(cwd = process.cwd()) {
240
663
  connected: Boolean(credential),
241
664
  projectId: credential?.projectId ?? null,
242
665
  eventEndpoint: credential ? `${credential.appUrl}/api/events/batch` : null,
243
- offlineQueue: queue
666
+ offlineQueue: queue,
667
+ offlineQueueBacklog: queueBacklog,
668
+ legacyQueue: legacyQueue.pending ? legacyQueue.quarantined : null
244
669
  },
245
670
  null,
246
671
  2
@@ -359,8 +784,11 @@ async function emitCommand(adapter, eventName, input) {
359
784
  const publishableEvents = [event, ...repositoryEvents].filter(
360
785
  (candidate) => eventTypeForHook(candidate)
361
786
  );
362
- const queue = new JsonFileOfflineQueue(queuePath());
363
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
+ );
364
792
  if (!credential) {
365
793
  if (publishableEvents.length) await queue.enqueue(publishableEvents);
366
794
  return {
@@ -399,6 +827,7 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
399
827
  if (!projectConfig || !credential) {
400
828
  throw new Error("Wibe is not authorized in this repository. Run wibe setup first.");
401
829
  }
830
+ await quarantineLegacyQueue();
402
831
  const summary = options.summary?.trim() ?? "";
403
832
  const wordCount = summary.split(/\s+/).filter(Boolean).length;
404
833
  if (wordCount < 20 || wordCount > 55 || summary.length > 500) {
@@ -462,7 +891,7 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
462
891
  projectId: credential.projectId,
463
892
  repositoryId: credential.repositoryId,
464
893
  deviceId: credential.deviceId,
465
- queue: new JsonFileOfflineQueue(queuePath())
894
+ queue: new JsonFileOfflineQueue(projectQueuePath(credential.projectId))
466
895
  }).capture(event);
467
896
  return {
468
897
  exitCode: result.error ? 1 : 0,
@@ -470,16 +899,29 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
470
899
  };
471
900
  }
472
901
  async function doctorCommand(cwd = process.cwd(), requestedRepository) {
902
+ const legacyQueue = await quarantineLegacyQueue();
473
903
  const credential = await loadCredential(cwd);
474
904
  const projectConfig = await readProjectConfig(join(cwd, ".wibe", "project.json"));
905
+ const repositoryForQueue = await detectRepository(cwd);
906
+ const queuePath = credential ? projectQueuePath(credential.projectId) : pendingQueuePath(repositoryForQueue?.root ?? cwd);
907
+ let queueBacklog;
908
+ let queueError;
909
+ try {
910
+ queueBacklog = await new JsonFileOfflineQueue(queuePath).size();
911
+ } catch (error) {
912
+ queueError = error instanceof Error ? error.message : String(error);
913
+ }
475
914
  const expectedRepository = requestedRepository ? normalizeGitHubRepository(requestedRepository) : projectConfig?.repository;
476
915
  if (requestedRepository && !expectedRepository) {
477
916
  throw new Error(
478
917
  `Expected repository "${requestedRepository}" is not a valid GitHub owner/repository.`
479
918
  );
480
919
  }
481
- const repository = await detectRepository(cwd);
920
+ const repository = repositoryForQueue;
482
921
  const repositoryMatches = expectedRepository ? matchesGitHubRepository(repository?.remote, expectedRepository) : true;
922
+ const credentialMatchesProjectConfig = Boolean(
923
+ credential && projectConfig && credential.projectId === projectConfig.projectId && credential.appUrl.replace(/\/$/, "") === projectConfig.appUrl.replace(/\/$/, "")
924
+ );
483
925
  let adapter;
484
926
  let adapterError;
485
927
  try {
@@ -487,8 +929,17 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
487
929
  } catch (error) {
488
930
  adapterError = error instanceof Error ? error.message : String(error);
489
931
  }
490
- const heartbeat = credential && adapter && repositoryMatches ? await sendVerificationHeartbeat(credential, adapter, "doctor") : void 0;
491
- const nativeConfig = adapter ? await validateNativeConfigs(adapter, cwd) : { hooks: false, mcp: false, activityRule: false };
932
+ const heartbeat = credential && adapter && repositoryMatches && credentialMatchesProjectConfig ? await sendVerificationHeartbeat(credential, adapter, "doctor", cwd) : void 0;
933
+ const nativeConfig = adapter ? await validateNativeConfigs(adapter, cwd, projectConfig?.projectId) : { hooks: false, mcp: false, activityRule: false, projectTrust: void 0 };
934
+ const trustNotes = adapter === "codex" && nativeConfig.projectTrust === void 0 ? [
935
+ "note Codex project trust could not be verified because the user config was not found; trust this project in Codex and rerun doctor"
936
+ ] : [];
937
+ const queueNotes = [
938
+ `note project queue ${queuePath} contains ${queueBacklog ?? "unknown"} event(s)`,
939
+ ...legacyQueue.pending ? [
940
+ `note legacy shared queue is quarantined at ${legacyQueue.quarantined} and will not be delivered automatically`
941
+ ] : []
942
+ ];
492
943
  const checks = [
493
944
  ["node", Number(process.versions.node.split(".")[0]) >= 20],
494
945
  ["git repository", Boolean(repository)],
@@ -501,6 +952,14 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
501
952
  ["device authorization", Boolean(credential)],
502
953
  ["event endpoint", Boolean(credential?.appUrl)],
503
954
  ["project scope", Boolean(credential?.projectId)],
955
+ [
956
+ "project config matches device credential",
957
+ credentialMatchesProjectConfig
958
+ ],
959
+ [
960
+ queueError ? `project queue (${queueError})` : "project queue",
961
+ queueError === void 0
962
+ ],
504
963
  [
505
964
  adapterError ? `adapter detection (${adapterError})` : "adapter detection",
506
965
  Boolean(adapter)
@@ -508,6 +967,14 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
508
967
  ["agent hooks configuration", nativeConfig.hooks],
509
968
  ["MCP endpoint configuration", nativeConfig.mcp],
510
969
  ["agent activity instructions", nativeConfig.activityRule],
970
+ ...adapter === "codex" ? [
971
+ ...nativeConfig.projectTrust === void 0 ? [] : [
972
+ [
973
+ nativeConfig.projectTrust ? "Codex project trust" : "Codex project trust (trust this project in Codex, then review Wibe hooks in /hooks)",
974
+ nativeConfig.projectTrust
975
+ ]
976
+ ]
977
+ ] : [],
511
978
  [
512
979
  heartbeat?.error ? `verification heartbeat (${heartbeat.error})` : "verification heartbeat",
513
980
  Boolean(heartbeat && !heartbeat.error)
@@ -516,12 +983,16 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
516
983
  const failures = checks.filter(([, okay]) => !okay).length;
517
984
  return {
518
985
  exitCode: failures ? 1 : 0,
519
- message: checks.map(([name, okay]) => `${okay ? "ok" : "missing"} ${name}`).join("\n")
986
+ message: [
987
+ ...checks.map(([name, okay]) => `${okay ? "ok" : "missing"} ${name}`),
988
+ ...trustNotes,
989
+ ...queueNotes
990
+ ].join("\n")
520
991
  };
521
992
  }
522
993
  async function loadCredential(cwd) {
523
994
  if (process.env.WIBE_ACCESS_TOKEN && process.env.WIBE_PROJECT_ID && process.env.WIBE_ORGANIZATION_ID && process.env.WIBE_DEVICE_ID) {
524
- return {
995
+ const credential = {
525
996
  appUrl: process.env.WIBE_APP_URL ?? "http://localhost:3000",
526
997
  accessToken: process.env.WIBE_ACCESS_TOKEN,
527
998
  projectId: process.env.WIBE_PROJECT_ID,
@@ -529,17 +1000,22 @@ async function loadCredential(cwd) {
529
1000
  repositoryId: process.env.WIBE_REPOSITORY_ID,
530
1001
  deviceId: process.env.WIBE_DEVICE_ID
531
1002
  };
1003
+ const project = await readProjectConfig(join(cwd, ".wibe", "project.json"));
1004
+ if (!project || project.projectId !== credential.projectId || project.appUrl.replace(/\/$/, "") !== credential.appUrl.replace(/\/$/, "")) {
1005
+ return null;
1006
+ }
1007
+ return credential;
532
1008
  }
533
1009
  try {
534
- const project = JSON.parse(
535
- await readFile(join(cwd, ".wibe", "project.json"), "utf8")
536
- );
537
- if (!project.projectId) return null;
1010
+ const project = await readProjectConfig(join(cwd, ".wibe", "project.json"));
1011
+ if (!project) return null;
538
1012
  const stored = await new SystemCredentialStore().get(
539
1013
  "dev.wibe.bridge",
540
1014
  project.projectId
541
1015
  );
542
- return stored ? JSON.parse(stored) : null;
1016
+ if (!stored) return null;
1017
+ const credential = JSON.parse(stored);
1018
+ return credential.projectId === project.projectId && credential.appUrl.replace(/\/$/, "") === project.appUrl.replace(/\/$/, "") ? credential : null;
543
1019
  } catch {
544
1020
  return null;
545
1021
  }
@@ -570,8 +1046,19 @@ async function installNativeConfigs(adapter, source, cwd, appUrl, projectId) {
570
1046
  for (const [sourceName, destinationName] of files) {
571
1047
  const destinationPath = join(cwd, destinationName);
572
1048
  const destinationExists = await exists(destinationPath);
1049
+ const isMcpConfig = destinationName === ".cursor/mcp.json" || destinationName === ".mcp.json" || destinationName === ".codex/config.toml";
573
1050
  const isCursorActivityRule = adapter === "cursor" && destinationName === ".cursor/rules/wibe-activity.mdc";
574
1051
  if (destinationExists) {
1052
+ if (isMcpConfig) {
1053
+ if (await refreshMcpConfig(
1054
+ adapter,
1055
+ destinationPath,
1056
+ `${appUrl}/api/mcp/projects/${projectId}`
1057
+ )) {
1058
+ installed.push(destinationName);
1059
+ }
1060
+ continue;
1061
+ }
575
1062
  if (!isCursorActivityRule) continue;
576
1063
  const existingRule = await readFile(destinationPath, "utf8");
577
1064
  if (!existingRule.includes("# Wibe activity") || !existingRule.includes("wibe_share_progress")) {
@@ -582,15 +1069,151 @@ async function installNativeConfigs(adapter, source, cwd, appUrl, projectId) {
582
1069
  await mkdir(resolve(destinationPath, ".."), { recursive: true });
583
1070
  await writeFile(
584
1071
  destinationPath,
585
- template.replaceAll("${env:WIBE_APP_URL}", appUrl).replaceAll("${WIBE_APP_URL}", appUrl).replaceAll("${WIBE_PROJECT_ID}", projectId),
1072
+ renderTemplate(template, appUrl, projectId),
586
1073
  { mode: 384 }
587
1074
  );
588
1075
  installed.push(destinationName);
589
1076
  }
1077
+ if (adapter !== "cursor") {
1078
+ const activityTemplate = renderTemplate(
1079
+ await readFile(join(source, "wibe-activity.md.example"), "utf8"),
1080
+ appUrl,
1081
+ projectId
1082
+ );
1083
+ await installManagedActivitySection(join(cwd, "AGENTS.md"), activityTemplate);
1084
+ installed.push("AGENTS.md");
1085
+ if (adapter === "claude-code") {
1086
+ const claudePath = join(cwd, "CLAUDE.md");
1087
+ if (await ensureClaudeImportsAgents(claudePath)) {
1088
+ installed.push("CLAUDE.md");
1089
+ }
1090
+ }
1091
+ }
590
1092
  return installed;
591
1093
  }
592
- function queuePath() {
593
- return process.env.WIBE_QUEUE_PATH ?? join(homedir(), ".wibe", "events.json");
1094
+ async function refreshMcpConfig(adapter, path, resourceUrl) {
1095
+ const current = await readFile(path, "utf8");
1096
+ if (adapter === "codex") {
1097
+ const next = current.replace(
1098
+ /(\[mcp_servers\.wibe\][\s\S]*?^\s*url\s*=\s*)["'][^"']*\/api\/mcp(?:\/projects\/[0-9a-f-]+)?\/?["']/m,
1099
+ `$1${JSON.stringify(resourceUrl)}`
1100
+ );
1101
+ if (next === current) return false;
1102
+ await writeFile(path, next, { mode: 384 });
1103
+ return true;
1104
+ }
1105
+ let parsed;
1106
+ try {
1107
+ const value = JSON.parse(current);
1108
+ if (!isRecord(value)) return false;
1109
+ parsed = value;
1110
+ } catch {
1111
+ return false;
1112
+ }
1113
+ if (!isRecord(parsed.mcpServers) || !isRecord(parsed.mcpServers.wibe)) {
1114
+ return false;
1115
+ }
1116
+ const existingUrl = parsed.mcpServers.wibe.url;
1117
+ if (typeof existingUrl !== "string" || !existingUrl.includes("/api/mcp")) {
1118
+ return false;
1119
+ }
1120
+ parsed.mcpServers.wibe.url = resourceUrl;
1121
+ await writeFile(path, `${JSON.stringify(parsed, null, 2)}
1122
+ `, { mode: 384 });
1123
+ return true;
1124
+ }
1125
+ function renderTemplate(template, appUrl, projectId) {
1126
+ return template.replaceAll("${env:WIBE_APP_URL}", appUrl).replaceAll("${WIBE_APP_URL}", appUrl).replaceAll("${WIBE_PROJECT_ID}", projectId);
1127
+ }
1128
+ async function installManagedActivitySection(path, template) {
1129
+ const managedSection = template.trim();
1130
+ const current = await exists(path) ? await readFile(path, "utf8") : "";
1131
+ const beginIndex = current.indexOf(WIBE_ACTIVITY_BEGIN);
1132
+ const endIndex = current.indexOf(WIBE_ACTIVITY_END);
1133
+ const hasBegin = beginIndex >= 0;
1134
+ const hasEnd = endIndex >= 0;
1135
+ if (hasBegin !== hasEnd || hasBegin && endIndex < beginIndex) {
1136
+ throw new Error(
1137
+ `${path} contains incomplete Wibe activity markers. Restore or remove the managed section markers, then rerun setup.`
1138
+ );
1139
+ }
1140
+ const next = hasBegin ? `${current.slice(0, beginIndex)}${managedSection}${current.slice(
1141
+ endIndex + WIBE_ACTIVITY_END.length
1142
+ )}` : `${current.trimEnd()}${current.trim() ? "\n\n" : ""}${managedSection}
1143
+ `;
1144
+ await writeFile(path, next, { mode: 420 });
1145
+ }
1146
+ async function ensureClaudeImportsAgents(path) {
1147
+ const current = await exists(path) ? await readFile(path, "utf8") : "";
1148
+ if (/^\s*@AGENTS\.md\s*$/m.test(current)) return false;
1149
+ const next = `${current.trimEnd()}${current.trim() ? "\n\n" : ""}@AGENTS.md
1150
+ `;
1151
+ await writeFile(path, next, { mode: 420 });
1152
+ return true;
1153
+ }
1154
+ function queueRoot() {
1155
+ const override = process.env.WIBE_QUEUE_PATH?.trim();
1156
+ if (!override) return join(homedir(), ".wibe", "queue");
1157
+ return extname(override) ? `${override}.d` : override;
1158
+ }
1159
+ function projectQueuePath(projectId) {
1160
+ 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(
1161
+ projectId
1162
+ )) {
1163
+ throw new Error("Wibe project ID must be a UUID.");
1164
+ }
1165
+ return join(queueRoot(), `${projectId}.json`);
1166
+ }
1167
+ function pendingQueuePath(cwd) {
1168
+ let canonicalRoot = resolve(cwd);
1169
+ try {
1170
+ canonicalRoot = realpathSync.native(canonicalRoot);
1171
+ } catch {
1172
+ }
1173
+ const repositoryHash = createHash("sha256").update(canonicalRoot).digest("hex").slice(0, 24);
1174
+ return join(queueRoot(), `pending-${repositoryHash}.json`);
1175
+ }
1176
+ function legacyQueuePaths() {
1177
+ const override = process.env.WIBE_QUEUE_PATH?.trim();
1178
+ if (override && extname(override)) {
1179
+ return {
1180
+ current: override,
1181
+ quarantined: `${override.slice(0, -extname(override).length)}.legacy${extname(override)}`
1182
+ };
1183
+ }
1184
+ const directory = override || join(homedir(), ".wibe");
1185
+ return {
1186
+ current: join(directory, "events.json"),
1187
+ quarantined: join(directory, "events.legacy.json")
1188
+ };
1189
+ }
1190
+ async function quarantineLegacyQueue() {
1191
+ const paths = legacyQueuePaths();
1192
+ if (await exists(paths.current)) {
1193
+ if (!await exists(paths.quarantined)) {
1194
+ await rename(paths.current, paths.quarantined).catch((error) => {
1195
+ if (error.code !== "ENOENT") throw error;
1196
+ });
1197
+ }
1198
+ }
1199
+ return {
1200
+ ...paths,
1201
+ pending: await exists(paths.current) || await exists(paths.quarantined)
1202
+ };
1203
+ }
1204
+ async function adoptPendingQueue(cwd, projectId) {
1205
+ await quarantineLegacyQueue();
1206
+ const repository = await detectRepository(cwd);
1207
+ const pending = new JsonFileOfflineQueue(
1208
+ pendingQueuePath(repository?.root ?? cwd)
1209
+ );
1210
+ const project = new JsonFileOfflineQueue(projectQueuePath(projectId));
1211
+ while (true) {
1212
+ const events = await pending.peek(100);
1213
+ if (!events.length) return;
1214
+ await project.enqueue(events);
1215
+ await pending.remove(events.map((event) => event.id));
1216
+ }
594
1217
  }
595
1218
  async function exists(path) {
596
1219
  try {
@@ -600,7 +1223,7 @@ async function exists(path) {
600
1223
  return false;
601
1224
  }
602
1225
  }
603
- async function validateNativeConfigs(adapter, cwd) {
1226
+ async function validateNativeConfigs(adapter, cwd, projectId) {
604
1227
  if (adapter === "codex") {
605
1228
  const hooks = await validJson(
606
1229
  join(cwd, ".codex", "hooks.json"),
@@ -609,11 +1232,21 @@ async function validateNativeConfigs(adapter, cwd) {
609
1232
  let mcp = false;
610
1233
  try {
611
1234
  const config = await readFile(join(cwd, ".codex", "config.toml"), "utf8");
612
- mcp = config.includes("[mcp_servers.wibe]") && config.includes("/api/mcp");
1235
+ mcp = config.includes("[mcp_servers.wibe]") && Boolean(
1236
+ projectId && config.includes(`/api/mcp/projects/${projectId}`)
1237
+ );
613
1238
  } catch {
614
1239
  mcp = false;
615
1240
  }
616
- return { hooks, mcp, activityRule: true };
1241
+ return {
1242
+ hooks,
1243
+ mcp,
1244
+ activityRule: await validWibeActivityInstructions(
1245
+ join(cwd, "AGENTS.md"),
1246
+ projectId
1247
+ ),
1248
+ projectTrust: await codexProjectTrust(cwd)
1249
+ };
617
1250
  }
618
1251
  const hookPath = adapter === "cursor" ? join(cwd, ".cursor", "hooks.json") : join(cwd, ".claude", "settings.json");
619
1252
  const mcpPath = adapter === "cursor" ? join(cwd, ".cursor", "mcp.json") : join(cwd, ".mcp.json");
@@ -622,14 +1255,53 @@ async function validateNativeConfigs(adapter, cwd) {
622
1255
  mcp: await validJson(mcpPath, (value) => {
623
1256
  if (!isRecord(value.mcpServers)) return false;
624
1257
  const wibe = value.mcpServers.wibe;
625
- return isRecord(wibe) && typeof wibe.url === "string" && wibe.url.replace(/\/$/, "").endsWith("/api/mcp");
1258
+ return isRecord(wibe) && typeof wibe.url === "string" && Boolean(
1259
+ projectId && wibe.url.replace(/\/$/, "").endsWith(`/api/mcp/projects/${projectId}`)
1260
+ );
626
1261
  }),
627
- activityRule: adapter !== "cursor" || await validText(
1262
+ activityRule: adapter === "cursor" ? await validText(
628
1263
  join(cwd, ".cursor", "rules", "wibe-activity.mdc"),
629
- (value) => value.includes("alwaysApply: true") && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
630
- )
1264
+ (value) => value.includes("alwaysApply: true") && Boolean(projectId && value.includes(projectId)) && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
1265
+ ) : await validWibeActivityInstructions(join(cwd, "AGENTS.md"), projectId),
1266
+ projectTrust: void 0
631
1267
  };
632
1268
  }
1269
+ function validWibeActivityInstructions(path, projectId) {
1270
+ return validText(
1271
+ path,
1272
+ (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")
1273
+ );
1274
+ }
1275
+ async function codexProjectTrust(cwd) {
1276
+ const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
1277
+ let config;
1278
+ try {
1279
+ config = await readFile(join(codexHome, "config.toml"), "utf8");
1280
+ } catch {
1281
+ return void 0;
1282
+ }
1283
+ let currentProject;
1284
+ for (const rawLine of config.split(/\r?\n/)) {
1285
+ const line = rawLine.trim();
1286
+ const section = line.match(
1287
+ /^\[projects\.(?:"((?:\\.|[^"])*)"|'([^']*)')\]$/
1288
+ );
1289
+ if (section) {
1290
+ try {
1291
+ currentProject = resolve(
1292
+ section[1] !== void 0 ? JSON.parse(`"${section[1]}"`) : section[2]
1293
+ );
1294
+ } catch {
1295
+ currentProject = void 0;
1296
+ }
1297
+ continue;
1298
+ }
1299
+ if (currentProject === resolve(cwd) && /^trust_level\s*=\s*["']trusted["'](?:\s*#.*)?$/.test(line)) {
1300
+ return true;
1301
+ }
1302
+ }
1303
+ return false;
1304
+ }
633
1305
  async function validText(path, predicate) {
634
1306
  try {
635
1307
  return predicate(await readFile(path, "utf8"));
@@ -757,8 +1429,8 @@ async function assertExpectedRepository(cwd, expected) {
757
1429
  `Repository mismatch: expected GitHub repository "${expected}", but found ${actual}. Run this command from the expected repository root or correct the origin remote, then retry.`
758
1430
  );
759
1431
  }
760
- async function sendVerificationHeartbeat(credential, adapter, reason) {
761
- const repository = await detectRepository(process.cwd());
1432
+ async function sendVerificationHeartbeat(credential, adapter, reason, cwd) {
1433
+ const repository = await detectRepository(cwd);
762
1434
  const workingTree = repository ? await detectWorkingTreeMetrics(repository.root) : void 0;
763
1435
  return new SignedBatchClient({
764
1436
  endpoint: `${credential.appUrl}/api/events/batch`,
@@ -787,6 +1459,7 @@ async function sendVerificationHeartbeat(credential, adapter, reason) {
787
1459
 
788
1460
  export {
789
1461
  setupCommand,
1462
+ onboardCommand,
790
1463
  statusCommand,
791
1464
  emitCommand,
792
1465
  shareProgressCommand,