@wibeco/bridge 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,11 @@ Dependency-light TypeScript foundations for normalizing agent hooks and sending
18
18
  Install and authorize Wibe from any GitHub repository:
19
19
 
20
20
  ```sh
21
+ npx --yes --package @wibeco/bridge@latest wibe onboard \
22
+ --adapter cursor \
23
+ --url https://trywibe.com \
24
+ --repository <owner/repository>
25
+
21
26
  npx --yes --package @wibeco/bridge@latest wibe setup \
22
27
  --adapter cursor \
23
28
  --project <uuid> \
@@ -27,7 +27,189 @@ import { homedir, hostname } from "os";
27
27
  import { basename, join, resolve } from "path";
28
28
  import { fileURLToPath } from "url";
29
29
  import { execFile } from "child_process";
30
+
31
+ // src/onboard.ts
32
+ import { z } from "zod";
33
+ var sessionCreatedSchema = z.object({
34
+ session_id: z.string().uuid(),
35
+ session_secret: z.string().min(1),
36
+ authorization_url: z.string().url(),
37
+ expires_at: z.string()
38
+ });
39
+ var snapshotSchema = z.object({
40
+ status: z.enum(["pending_auth", "authenticated", "completed", "expired"]),
41
+ next: z.enum([
42
+ "authenticate",
43
+ "create_organization",
44
+ "install_github",
45
+ "create_project",
46
+ "connect_device",
47
+ "activate",
48
+ "done"
49
+ ]),
50
+ user_id: z.string().uuid().nullable(),
51
+ organization_id: z.string().uuid().nullable(),
52
+ project_id: z.string().uuid().nullable(),
53
+ github_connected: z.boolean(),
54
+ project_url: z.string().url().nullable(),
55
+ expires_at: z.string(),
56
+ access_token: z.string().min(1).optional()
57
+ });
58
+ var organizationSchema = z.object({
59
+ organization_id: z.string().uuid()
60
+ });
61
+ var githubInstallSchema = z.object({
62
+ github_connected: z.boolean(),
63
+ install_url: z.string().url().nullable()
64
+ });
65
+ var repositoriesSchema = z.object({
66
+ repositories: z.array(
67
+ z.object({
68
+ id: z.number().int(),
69
+ installationId: z.number().int(),
70
+ fullName: z.string(),
71
+ name: z.string()
72
+ })
73
+ )
74
+ });
75
+ var projectSchema = z.object({
76
+ project_id: z.string().uuid(),
77
+ reused: z.boolean().optional(),
78
+ setup_status: z.string().optional()
79
+ });
80
+ var invitesSchema = z.object({
81
+ invites: z.array(
82
+ z.object({
83
+ email: z.string().nullable().optional(),
84
+ url: z.string().url()
85
+ })
86
+ )
87
+ });
88
+ var deviceSchema = z.object({
89
+ accessToken: z.string().min(1),
90
+ projectId: z.string().uuid(),
91
+ organizationId: z.string().uuid(),
92
+ repositoryId: z.string().uuid().optional(),
93
+ deviceId: z.string().uuid()
94
+ });
95
+ var activateSchema = z.object({
96
+ project_id: z.string().uuid(),
97
+ project_url: z.string().url()
98
+ });
99
+ function origin(appUrl) {
100
+ return appUrl.replace(/\/$/, "");
101
+ }
102
+ async function readJson(response) {
103
+ return await response.json().catch(() => ({}));
104
+ }
105
+ async function requestJson(url, schema, init) {
106
+ const response = await fetch(url, init);
107
+ const body = await readJson(response);
108
+ if (!response.ok) {
109
+ throw new Error(
110
+ typeof body.error === "string" ? body.error : `Onboarding request failed (${response.status}).`
111
+ );
112
+ }
113
+ return schema.parse(body);
114
+ }
115
+ function authorized(accessToken) {
116
+ return {
117
+ authorization: `Bearer ${accessToken}`,
118
+ "content-type": "application/json"
119
+ };
120
+ }
121
+ async function createOnboardingSession(appUrl) {
122
+ return requestJson(
123
+ `${origin(appUrl)}/api/onboarding/sessions`,
124
+ sessionCreatedSchema,
125
+ { method: "POST" }
126
+ );
127
+ }
128
+ async function pollOnboardingSession(input) {
129
+ return requestJson(
130
+ `${origin(input.appUrl)}/api/onboarding/sessions/${input.sessionId}`,
131
+ snapshotSchema,
132
+ { headers: { "x-wibe-onboarding-secret": input.sessionSecret } }
133
+ );
134
+ }
135
+ async function createOnboardingOrganization(input) {
136
+ return requestJson(
137
+ `${origin(input.appUrl)}/api/onboarding/organization`,
138
+ organizationSchema,
139
+ {
140
+ method: "POST",
141
+ headers: authorized(input.accessToken),
142
+ body: JSON.stringify({ name: input.name })
143
+ }
144
+ );
145
+ }
146
+ async function requestGitHubInstall(input) {
147
+ return requestJson(
148
+ `${origin(input.appUrl)}/api/onboarding/github/install`,
149
+ githubInstallSchema,
150
+ { method: "POST", headers: authorized(input.accessToken) }
151
+ );
152
+ }
153
+ async function listOnboardingRepositories(input) {
154
+ return requestJson(
155
+ `${origin(input.appUrl)}/api/onboarding/github/repositories`,
156
+ repositoriesSchema,
157
+ { headers: authorized(input.accessToken) }
158
+ );
159
+ }
160
+ async function createOnboardingProject(input) {
161
+ return requestJson(
162
+ `${origin(input.appUrl)}/api/onboarding/project`,
163
+ projectSchema,
164
+ {
165
+ method: "POST",
166
+ headers: authorized(input.accessToken),
167
+ body: JSON.stringify({
168
+ projectName: input.projectName,
169
+ githubInstallationId: input.githubInstallationId,
170
+ providerRepositoryId: input.providerRepositoryId,
171
+ repository: input.repository
172
+ })
173
+ }
174
+ );
175
+ }
176
+ async function createOnboardingInvites(input) {
177
+ return requestJson(
178
+ `${origin(input.appUrl)}/api/onboarding/invites`,
179
+ invitesSchema,
180
+ {
181
+ method: "POST",
182
+ headers: authorized(input.accessToken),
183
+ body: JSON.stringify({ emails: input.emails })
184
+ }
185
+ );
186
+ }
187
+ async function mintOnboardingDevice(input) {
188
+ return requestJson(
189
+ `${origin(input.appUrl)}/api/onboarding/device`,
190
+ deviceSchema,
191
+ {
192
+ method: "POST",
193
+ headers: authorized(input.accessToken),
194
+ body: JSON.stringify({
195
+ deviceName: input.deviceName,
196
+ agentName: input.agentName
197
+ })
198
+ }
199
+ );
200
+ }
201
+ async function activateOnboardingProject(input) {
202
+ return requestJson(
203
+ `${origin(input.appUrl)}/api/onboarding/activate`,
204
+ activateSchema,
205
+ { method: "POST", headers: authorized(input.accessToken) }
206
+ );
207
+ }
208
+
209
+ // src/cli/commands.ts
30
210
  var ADAPTERS = ["cursor", "claude-code", "codex"];
211
+ var WIBE_ACTIVITY_BEGIN = "<!-- BEGIN:wibe-activity -->";
212
+ var WIBE_ACTIVITY_END = "<!-- END:wibe-activity -->";
31
213
  var MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
32
214
  function screenshotMimeType(bytes) {
33
215
  if ([137, 80, 78, 71, 13, 10, 26, 10].every(
@@ -132,7 +314,7 @@ async function setupCommand(requestedAdapter, options = {}) {
132
314
  );
133
315
  return {
134
316
  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.`
317
+ 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
318
  };
137
319
  }
138
320
  const authorization = await requestDeviceAuthorization({
@@ -218,9 +400,225 @@ Confirm code ${authorization.user_code}
218
400
  const heartbeat = await sendVerificationHeartbeat(credential, adapter, "setup");
219
401
  return {
220
402
  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}.`
403
+ 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)}`
404
+ };
405
+ }
406
+ function adapterSetupNextSteps(adapter) {
407
+ 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." : "";
408
+ }
409
+ async function onboardCommand(requestedAdapter, options = {}) {
410
+ const cwd = options.cwd ?? process.cwd();
411
+ const expectedRepository = options.expectedRepository ? normalizeGitHubRepository(options.expectedRepository) : void 0;
412
+ if (options.expectedRepository && !expectedRepository) {
413
+ throw new Error(
414
+ `Expected repository "${options.expectedRepository}" is not a valid GitHub owner/repository.`
415
+ );
416
+ }
417
+ const detectedRepository = await detectRepository(cwd);
418
+ const repository = expectedRepository ?? (detectedRepository?.remote ? normalizeGitHubRepository(detectedRepository.remote) : void 0);
419
+ if (!repository) {
420
+ throw new Error(
421
+ "Could not detect a GitHub repository remote. Run this from a repository root or pass --repository owner/repo."
422
+ );
423
+ }
424
+ if (expectedRepository) {
425
+ await assertExpectedRepository(cwd, expectedRepository);
426
+ }
427
+ const adapter = await detectAdapter(requestedAdapter, cwd);
428
+ const source = await resolveTemplateSource(adapter);
429
+ const destination = join(cwd, ".wibe", "integrations", adapter);
430
+ if (!await exists(destination)) {
431
+ await mkdir(join(cwd, ".wibe", "integrations"), { recursive: true });
432
+ await cp(source, destination, { recursive: true, errorOnExist: true, force: false });
433
+ }
434
+ const appUrl = (options.appUrl ?? process.env.WIBE_APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
435
+ const created = options.sessionId && options.sessionSecret ? {
436
+ session_id: options.sessionId,
437
+ session_secret: options.sessionSecret,
438
+ authorization_url: `${appUrl}/onboard`,
439
+ expires_at: new Date(Date.now() + 30 * 60 * 1e3).toISOString()
440
+ } : await createOnboardingSession(appUrl);
441
+ let snapshot = await pollOnboardingSession({
442
+ appUrl,
443
+ sessionId: created.session_id,
444
+ sessionSecret: created.session_secret
445
+ });
446
+ if (snapshot.next === "authenticate") {
447
+ process.stdout.write(
448
+ `Open ${created.authorization_url}
449
+ Approve GitHub sign-in in your browser, then return here.
450
+ `
451
+ );
452
+ openBrowser(created.authorization_url);
453
+ snapshot = await waitForOnboarding(
454
+ appUrl,
455
+ created.session_id,
456
+ created.session_secret,
457
+ (current) => current.next !== "authenticate" && Boolean(current.access_token)
458
+ );
459
+ }
460
+ const accessToken = snapshot.access_token;
461
+ if (!accessToken) {
462
+ throw new Error("Wibe onboarding did not return an access token.");
463
+ }
464
+ if (snapshot.next === "create_organization" || !snapshot.organization_id) {
465
+ const orgName = options.orgName?.trim() || repository.split("/")[0] || "Organization";
466
+ await createOnboardingOrganization({
467
+ appUrl,
468
+ accessToken,
469
+ name: orgName
470
+ });
471
+ snapshot = await pollOnboardingSession({
472
+ appUrl,
473
+ sessionId: created.session_id,
474
+ sessionSecret: created.session_secret
475
+ });
476
+ }
477
+ if (snapshot.next === "install_github" || !snapshot.github_connected) {
478
+ const install = await requestGitHubInstall({ appUrl, accessToken });
479
+ if (!install.github_connected) {
480
+ if (!install.install_url) {
481
+ throw new Error("Wibe did not return a GitHub App install URL.");
482
+ }
483
+ process.stdout.write(
484
+ `Open ${install.install_url}
485
+ Install the Wibe GitHub App on ${repository}, then return here.
486
+ `
487
+ );
488
+ openBrowser(install.install_url);
489
+ snapshot = await waitForOnboarding(
490
+ appUrl,
491
+ created.session_id,
492
+ created.session_secret,
493
+ (current) => current.github_connected
494
+ );
495
+ }
496
+ }
497
+ const repositories = await listOnboardingRepositories({ appUrl, accessToken });
498
+ const matched = repositories.repositories.find(
499
+ (candidate) => normalizeGitHubRepository(candidate.fullName) === repository
500
+ );
501
+ if (!matched) {
502
+ throw new Error(
503
+ `GitHub App install does not include ${repository}. Install Wibe on that repository and retry.`
504
+ );
505
+ }
506
+ if (snapshot.next === "create_project" || !snapshot.project_id) {
507
+ await createOnboardingProject({
508
+ appUrl,
509
+ accessToken,
510
+ projectName: matched.name,
511
+ githubInstallationId: matched.installationId,
512
+ providerRepositoryId: matched.id,
513
+ repository: matched.fullName
514
+ });
515
+ snapshot = await pollOnboardingSession({
516
+ appUrl,
517
+ sessionId: created.session_id,
518
+ sessionSecret: created.session_secret
519
+ });
520
+ }
521
+ const inviteEmails = (options.invites ?? []).filter(Boolean);
522
+ const invites = await createOnboardingInvites({
523
+ appUrl,
524
+ accessToken,
525
+ emails: inviteEmails
526
+ });
527
+ if (!snapshot.project_id) {
528
+ throw new Error("Wibe onboarding did not create a project.");
529
+ }
530
+ const projectConfigPath = join(cwd, ".wibe", "project.json");
531
+ const existingProjectConfig = await readProjectConfig(projectConfigPath);
532
+ const existingCredential = existingProjectConfig ? await loadCredential(cwd) : null;
533
+ let credential = existingCredential;
534
+ if (!credential || credential.projectId !== snapshot.project_id || credential.appUrl.replace(/\/$/, "") !== appUrl) {
535
+ const minted = await mintOnboardingDevice({
536
+ appUrl,
537
+ accessToken,
538
+ deviceName: `${hostname()} (${adapter})`,
539
+ agentName: adapter
540
+ });
541
+ credential = {
542
+ appUrl,
543
+ accessToken: minted.accessToken,
544
+ projectId: minted.projectId,
545
+ organizationId: minted.organizationId,
546
+ repositoryId: minted.repositoryId,
547
+ deviceId: minted.deviceId
548
+ };
549
+ await new SystemCredentialStore().set(
550
+ "dev.wibe.bridge",
551
+ minted.projectId,
552
+ JSON.stringify(credential)
553
+ );
554
+ if (!existingProjectConfig) {
555
+ await mkdir(join(cwd, ".wibe"), { recursive: true });
556
+ await writeFile(
557
+ projectConfigPath,
558
+ `${JSON.stringify(
559
+ {
560
+ projectId: minted.projectId,
561
+ appUrl,
562
+ adapter,
563
+ repository
564
+ },
565
+ null,
566
+ 2
567
+ )}
568
+ `,
569
+ { mode: 384, flag: "wx" }
570
+ );
571
+ }
572
+ }
573
+ const installedNativeFiles = await installNativeConfigs(
574
+ adapter,
575
+ source,
576
+ cwd,
577
+ appUrl,
578
+ snapshot.project_id
579
+ );
580
+ const heartbeat = await sendVerificationHeartbeat(credential, adapter, "setup");
581
+ if (heartbeat.error) {
582
+ return {
583
+ exitCode: 1,
584
+ message: `Connected ${adapter} to Wibe, but the verification heartbeat could not be delivered (${heartbeat.error}). It remains safe to retry with wibe doctor.`
585
+ };
586
+ }
587
+ snapshot = await pollOnboardingSession({
588
+ appUrl,
589
+ sessionId: created.session_id,
590
+ sessionSecret: created.session_secret
591
+ });
592
+ let projectUrl = snapshot.project_url;
593
+ if (snapshot.next === "activate" || !projectUrl) {
594
+ const activated = await activateOnboardingProject({ appUrl, accessToken });
595
+ projectUrl = activated.project_url;
596
+ }
597
+ const inviteLines = invites.invites.map((invite) => invite.url).join("\n");
598
+ return {
599
+ exitCode: 0,
600
+ message: [
601
+ `Connected ${adapter} to Wibe and opened the project space.`,
602
+ projectUrl,
603
+ inviteLines ? `Invite links:
604
+ ${inviteLines}` : null,
605
+ `Installed ${installedNativeFiles.length} native config file(s); reviewable templates are at ${destination}.`
606
+ ].filter(Boolean).join("\n")
222
607
  };
223
608
  }
609
+ async function waitForOnboarding(appUrl, sessionId, sessionSecret, ready) {
610
+ const deadline = Date.now() + 10 * 60 * 1e3;
611
+ let snapshot = await pollOnboardingSession({ appUrl, sessionId, sessionSecret });
612
+ while (Date.now() < deadline) {
613
+ if (ready(snapshot)) return snapshot;
614
+ if (snapshot.status === "expired") {
615
+ throw new Error("Onboarding expired. Restart wibe onboard.");
616
+ }
617
+ await new Promise((resolve2) => setTimeout(resolve2, 3e3));
618
+ snapshot = await pollOnboardingSession({ appUrl, sessionId, sessionSecret });
619
+ }
620
+ throw new Error("Timed out waiting for the browser step to finish.");
621
+ }
224
622
  async function statusCommand(cwd = process.cwd()) {
225
623
  const repo = await detectRepository(cwd);
226
624
  const credential = await loadCredential(cwd);
@@ -488,7 +886,10 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
488
886
  adapterError = error instanceof Error ? error.message : String(error);
489
887
  }
490
888
  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 };
889
+ const nativeConfig = adapter ? await validateNativeConfigs(adapter, cwd) : { hooks: false, mcp: false, activityRule: false, projectTrust: void 0 };
890
+ const trustNotes = adapter === "codex" && nativeConfig.projectTrust === void 0 ? [
891
+ "note Codex project trust could not be verified because the user config was not found; trust this project in Codex and rerun doctor"
892
+ ] : [];
492
893
  const checks = [
493
894
  ["node", Number(process.versions.node.split(".")[0]) >= 20],
494
895
  ["git repository", Boolean(repository)],
@@ -508,6 +909,14 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
508
909
  ["agent hooks configuration", nativeConfig.hooks],
509
910
  ["MCP endpoint configuration", nativeConfig.mcp],
510
911
  ["agent activity instructions", nativeConfig.activityRule],
912
+ ...adapter === "codex" ? [
913
+ ...nativeConfig.projectTrust === void 0 ? [] : [
914
+ [
915
+ nativeConfig.projectTrust ? "Codex project trust" : "Codex project trust (trust this project in Codex, then review Wibe hooks in /hooks)",
916
+ nativeConfig.projectTrust
917
+ ]
918
+ ]
919
+ ] : [],
511
920
  [
512
921
  heartbeat?.error ? `verification heartbeat (${heartbeat.error})` : "verification heartbeat",
513
922
  Boolean(heartbeat && !heartbeat.error)
@@ -516,7 +925,10 @@ async function doctorCommand(cwd = process.cwd(), requestedRepository) {
516
925
  const failures = checks.filter(([, okay]) => !okay).length;
517
926
  return {
518
927
  exitCode: failures ? 1 : 0,
519
- message: checks.map(([name, okay]) => `${okay ? "ok" : "missing"} ${name}`).join("\n")
928
+ message: [
929
+ ...checks.map(([name, okay]) => `${okay ? "ok" : "missing"} ${name}`),
930
+ ...trustNotes
931
+ ].join("\n")
520
932
  };
521
933
  }
522
934
  async function loadCredential(cwd) {
@@ -582,13 +994,57 @@ async function installNativeConfigs(adapter, source, cwd, appUrl, projectId) {
582
994
  await mkdir(resolve(destinationPath, ".."), { recursive: true });
583
995
  await writeFile(
584
996
  destinationPath,
585
- template.replaceAll("${env:WIBE_APP_URL}", appUrl).replaceAll("${WIBE_APP_URL}", appUrl).replaceAll("${WIBE_PROJECT_ID}", projectId),
997
+ renderTemplate(template, appUrl, projectId),
586
998
  { mode: 384 }
587
999
  );
588
1000
  installed.push(destinationName);
589
1001
  }
1002
+ if (adapter !== "cursor") {
1003
+ const activityTemplate = renderTemplate(
1004
+ await readFile(join(source, "wibe-activity.md.example"), "utf8"),
1005
+ appUrl,
1006
+ projectId
1007
+ );
1008
+ await installManagedActivitySection(join(cwd, "AGENTS.md"), activityTemplate);
1009
+ installed.push("AGENTS.md");
1010
+ if (adapter === "claude-code") {
1011
+ const claudePath = join(cwd, "CLAUDE.md");
1012
+ if (await ensureClaudeImportsAgents(claudePath)) {
1013
+ installed.push("CLAUDE.md");
1014
+ }
1015
+ }
1016
+ }
590
1017
  return installed;
591
1018
  }
1019
+ function renderTemplate(template, appUrl, projectId) {
1020
+ return template.replaceAll("${env:WIBE_APP_URL}", appUrl).replaceAll("${WIBE_APP_URL}", appUrl).replaceAll("${WIBE_PROJECT_ID}", projectId);
1021
+ }
1022
+ async function installManagedActivitySection(path, template) {
1023
+ const managedSection = template.trim();
1024
+ const current = await exists(path) ? await readFile(path, "utf8") : "";
1025
+ const beginIndex = current.indexOf(WIBE_ACTIVITY_BEGIN);
1026
+ const endIndex = current.indexOf(WIBE_ACTIVITY_END);
1027
+ const hasBegin = beginIndex >= 0;
1028
+ const hasEnd = endIndex >= 0;
1029
+ if (hasBegin !== hasEnd || hasBegin && endIndex < beginIndex) {
1030
+ throw new Error(
1031
+ `${path} contains incomplete Wibe activity markers. Restore or remove the managed section markers, then rerun setup.`
1032
+ );
1033
+ }
1034
+ const next = hasBegin ? `${current.slice(0, beginIndex)}${managedSection}${current.slice(
1035
+ endIndex + WIBE_ACTIVITY_END.length
1036
+ )}` : `${current.trimEnd()}${current.trim() ? "\n\n" : ""}${managedSection}
1037
+ `;
1038
+ await writeFile(path, next, { mode: 420 });
1039
+ }
1040
+ async function ensureClaudeImportsAgents(path) {
1041
+ const current = await exists(path) ? await readFile(path, "utf8") : "";
1042
+ if (/^\s*@AGENTS\.md\s*$/m.test(current)) return false;
1043
+ const next = `${current.trimEnd()}${current.trim() ? "\n\n" : ""}@AGENTS.md
1044
+ `;
1045
+ await writeFile(path, next, { mode: 420 });
1046
+ return true;
1047
+ }
592
1048
  function queuePath() {
593
1049
  return process.env.WIBE_QUEUE_PATH ?? join(homedir(), ".wibe", "events.json");
594
1050
  }
@@ -613,7 +1069,12 @@ async function validateNativeConfigs(adapter, cwd) {
613
1069
  } catch {
614
1070
  mcp = false;
615
1071
  }
616
- return { hooks, mcp, activityRule: true };
1072
+ return {
1073
+ hooks,
1074
+ mcp,
1075
+ activityRule: await validWibeActivityInstructions(join(cwd, "AGENTS.md")),
1076
+ projectTrust: await codexProjectTrust(cwd)
1077
+ };
617
1078
  }
618
1079
  const hookPath = adapter === "cursor" ? join(cwd, ".cursor", "hooks.json") : join(cwd, ".claude", "settings.json");
619
1080
  const mcpPath = adapter === "cursor" ? join(cwd, ".cursor", "mcp.json") : join(cwd, ".mcp.json");
@@ -624,12 +1085,49 @@ async function validateNativeConfigs(adapter, cwd) {
624
1085
  const wibe = value.mcpServers.wibe;
625
1086
  return isRecord(wibe) && typeof wibe.url === "string" && wibe.url.replace(/\/$/, "").endsWith("/api/mcp");
626
1087
  }),
627
- activityRule: adapter !== "cursor" || await validText(
1088
+ activityRule: adapter === "cursor" ? await validText(
628
1089
  join(cwd, ".cursor", "rules", "wibe-activity.mdc"),
629
1090
  (value) => value.includes("alwaysApply: true") && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
630
- )
1091
+ ) : await validWibeActivityInstructions(join(cwd, "AGENTS.md")),
1092
+ projectTrust: void 0
631
1093
  };
632
1094
  }
1095
+ function validWibeActivityInstructions(path) {
1096
+ return validText(
1097
+ path,
1098
+ (value) => value.includes(WIBE_ACTIVITY_BEGIN) && value.includes(WIBE_ACTIVITY_END) && value.includes("wibe_share_progress") && value.includes("wibe share-progress")
1099
+ );
1100
+ }
1101
+ async function codexProjectTrust(cwd) {
1102
+ const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
1103
+ let config;
1104
+ try {
1105
+ config = await readFile(join(codexHome, "config.toml"), "utf8");
1106
+ } catch {
1107
+ return void 0;
1108
+ }
1109
+ let currentProject;
1110
+ for (const rawLine of config.split(/\r?\n/)) {
1111
+ const line = rawLine.trim();
1112
+ const section = line.match(
1113
+ /^\[projects\.(?:"((?:\\.|[^"])*)"|'([^']*)')\]$/
1114
+ );
1115
+ if (section) {
1116
+ try {
1117
+ currentProject = resolve(
1118
+ section[1] !== void 0 ? JSON.parse(`"${section[1]}"`) : section[2]
1119
+ );
1120
+ } catch {
1121
+ currentProject = void 0;
1122
+ }
1123
+ continue;
1124
+ }
1125
+ if (currentProject === resolve(cwd) && /^trust_level\s*=\s*["']trusted["'](?:\s*#.*)?$/.test(line)) {
1126
+ return true;
1127
+ }
1128
+ }
1129
+ return false;
1130
+ }
633
1131
  async function validText(path, predicate) {
634
1132
  try {
635
1133
  return predicate(await readFile(path, "utf8"));
@@ -787,6 +1285,7 @@ async function sendVerificationHeartbeat(credential, adapter, reason) {
787
1285
 
788
1286
  export {
789
1287
  setupCommand,
1288
+ onboardCommand,
790
1289
  statusCommand,
791
1290
  emitCommand,
792
1291
  shareProgressCommand,
package/dist/cli.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import {
3
3
  doctorCommand,
4
4
  emitCommand,
5
+ onboardCommand,
5
6
  parseAdapter,
6
7
  setupCommand,
7
8
  shareProgressCommand,
8
9
  statusCommand
9
- } from "./chunk-7LWUGOKM.js";
10
+ } from "./chunk-E2RWE6UW.js";
10
11
  import {
11
12
  runPresenceHeartbeat
12
13
  } from "./chunk-K7TK7FHF.js";
@@ -15,6 +16,9 @@ import {
15
16
  var HELP = `wibe-bridge <command>
16
17
 
17
18
  Commands:
19
+ onboard [--adapter <cursor|claude-code|codex>] [--url <wibe-url>] [--repository <owner/repo>] [--org-name <name>] [--invite <email>]
20
+ Creates a Wibe account session, organization, project, and local connection.
21
+ Prints browser URLs for GitHub sign-in and GitHub App install.
18
22
  setup --project <uuid> [--adapter <cursor|claude-code|codex>] [--url <wibe-url>] [--repository <owner/repo>] [--reauthorize]
19
23
  Auto-detects one adapter from the environment or repository config.
20
24
  Use --adapter when multiple agent configs are present.
@@ -26,7 +30,20 @@ Commands:
26
30
  async function main() {
27
31
  const [command, ...args] = process.argv.slice(2);
28
32
  let result;
29
- if (command === "setup") {
33
+ if (command === "onboard") {
34
+ const adapterOption = option(args, "--adapter");
35
+ result = await onboardCommand(
36
+ adapterOption ? parseAdapter(adapterOption) : void 0,
37
+ {
38
+ appUrl: option(args, "--url"),
39
+ expectedRepository: option(args, "--repository"),
40
+ orgName: option(args, "--org-name"),
41
+ invites: options(args, "--invite"),
42
+ sessionId: option(args, "--session"),
43
+ sessionSecret: option(args, "--session-secret")
44
+ }
45
+ );
46
+ } else if (command === "setup") {
30
47
  const adapterOption = option(args, "--adapter");
31
48
  result = await setupCommand(adapterOption ? parseAdapter(adapterOption) : void 0, {
32
49
  projectId: option(args, "--project"),
@@ -79,6 +96,16 @@ function option(args, name) {
79
96
  const index = args.indexOf(name);
80
97
  return index >= 0 ? args[index + 1] : void 0;
81
98
  }
99
+ function options(args, name) {
100
+ const values = [];
101
+ for (let index = 0; index < args.length; index += 1) {
102
+ if (args[index] === name && args[index + 1]) {
103
+ values.push(args[index + 1]);
104
+ index += 1;
105
+ }
106
+ }
107
+ return values;
108
+ }
82
109
  async function readJsonStdin() {
83
110
  if (process.stdin.isTTY) return {};
84
111
  const chunks = [];
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  emitCommand
4
- } from "./chunk-7LWUGOKM.js";
4
+ } from "./chunk-E2RWE6UW.js";
5
5
  import "./chunk-K7TK7FHF.js";
6
6
 
7
7
  // src/codex-hook.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wibeco/bridge",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "Privacy-first live activity bridge for Cursor, Claude Code, and Codex.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,9 +1,10 @@
1
1
  # Claude Code template
2
2
 
3
- These files are inert examples. Review and merge `settings.json.example` into the appropriate
4
- Claude Code settings file. It uses command hooks because the local bridge must normalize and redact
5
- the payload before network delivery. Do not point an HTTP hook directly at a collector: Claude hook
6
- payloads can include prompts, tool inputs/results, and other raw content.
3
+ These files are reviewable setup templates. `settings.json.example` configures command hooks,
4
+ `mcp.json.example` configures MCP, and `wibe-activity.md.example` is installed as a managed section
5
+ in the repository's `AGENTS.md`. Setup also ensures `CLAUDE.md` imports `AGENTS.md`. Do not point an
6
+ HTTP hook directly at a collector: Claude hook payloads can include prompts, tool inputs/results,
7
+ and other raw content.
7
8
  `SessionStart` owns a metadata-only heartbeat every 45 seconds until `SessionEnd`; `Stop` and tool
8
9
  hooks remain operational signals and are not published as presence.
9
10
 
@@ -0,0 +1,15 @@
1
+ <!-- BEGIN:wibe-activity -->
2
+ ## Wibe activity
3
+
4
+ - This repository is connected to Wibe project `${WIBE_PROJECT_ID}`.
5
+ - At the start of a task that will edit files, call `wibe_get_active_work` for this project. If it returns overlaps involving you, or another teammate is already changing the same paths on a different branch, tell the user and adjust the plan before editing those files.
6
+ - If the set of files you plan to change shifts, call `wibe_get_active_work` or `wibe_check_overlap` again. Do not poll on every edit.
7
+ - For most tasks, publish one update only after the requested outcome is complete. Do not publish plans, routine progress, or separate updates for editing, testing, committing, or pushing.
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
+ - 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
+ - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
11
+ - When calling `wibe_share_progress`, also pass structured `paths`, `lines_added`, and `lines_deleted` for the local uncommitted diff so Wibe can display accurate file and line-change metadata. Prefer `git diff --numstat HEAD` totals rather than guessing.
12
+ - This instruction is standing user authorization to publish the single completion update described here; do not ask for separate per-task approval.
13
+ - If the MCP tool is unavailable, run `npx --yes --package @wibeco/bridge@latest wibe share-progress --summary "<summary>" --phase <shipped|blocked>`.
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.
15
+ <!-- END:wibe-activity -->
@@ -1,17 +1,16 @@
1
1
  # Codex template
2
2
 
3
- These are reviewable setup templates. `config.toml.example` configures Codex notifications and MCP;
4
- `hooks.json.example` provides lifecycle hooks where supported. Wibe installs native files only when
5
- the destination does not already exist. The bridge maps allow-listed fields in-process and queues or
6
- sends only canonical events. Prompt text, messages, tool data, command text, file content, and
7
- transcripts are not retained.
3
+ These are reviewable setup templates. `config.toml.example` configures MCP,
4
+ `hooks.json.example` provides lifecycle hooks, and `wibe-activity.md.example` is installed as a
5
+ managed section in the repository's `AGENTS.md`. The bridge maps allow-listed fields in-process and
6
+ queues or sends only canonical events. Prompt text, messages, tool data, command text, file content,
7
+ and transcripts are not retained.
8
8
 
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
12
  The MCP block connects Codex to Wibe's remote `/api/mcp` endpoint. Codex completes OAuth separately
13
- from the bridge device token. Set `WIBE_APP_URL` to the same Wibe origin used during setup. Codex
14
- notification coverage can be narrower than Cursor or Claude Code lifecycle hooks.
15
- Codex appends one JSON notification argument to `wibe-codex-hook`; the bridge extracts only opaque
16
- `thread-id`/`turn-id` and approved model metadata. Where session hooks are available, they own a
17
- metadata-only 45-second heartbeat that ends cleanly with `SessionEnd`.
13
+ from the bridge device token. Set `WIBE_APP_URL` to the same Wibe origin used during setup.
14
+ Project-local Codex configuration loads only after the project is trusted, and command hooks must be
15
+ reviewed in `/hooks`. Session hooks own a metadata-only 45-second heartbeat that ends cleanly with
16
+ `SessionEnd`.
@@ -1,6 +1,2 @@
1
- # Codex passes one JSON notification argument to this command. The adapter only keeps allow-listed
2
- # operational metadata and never forwards the full argument.
3
- notify = ["npx", "-y", "--package", "@wibeco/bridge@latest", "wibe-codex-hook"]
4
-
5
1
  [mcp_servers.wibe]
6
2
  url = "${WIBE_APP_URL}/api/mcp"
@@ -0,0 +1,15 @@
1
+ <!-- BEGIN:wibe-activity -->
2
+ ## Wibe activity
3
+
4
+ - This repository is connected to Wibe project `${WIBE_PROJECT_ID}`.
5
+ - At the start of a task that will edit files, call `wibe_get_active_work` for this project. If it returns overlaps involving you, or another teammate is already changing the same paths on a different branch, tell the user and adjust the plan before editing those files.
6
+ - If the set of files you plan to change shifts, call `wibe_get_active_work` or `wibe_check_overlap` again. Do not poll on every edit.
7
+ - For most tasks, publish one update only after the requested outcome is complete. Do not publish plans, routine progress, or separate updates for editing, testing, committing, or pushing.
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
+ - 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
+ - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
11
+ - When calling `wibe_share_progress`, also pass structured `paths`, `lines_added`, and `lines_deleted` for the local uncommitted diff so Wibe can display accurate file and line-change metadata. Prefer `git diff --numstat HEAD` totals rather than guessing.
12
+ - This instruction is standing user authorization to publish the single completion update described here; do not ask for separate per-task approval.
13
+ - If the MCP tool is unavailable, run `npx --yes --package @wibeco/bridge@latest wibe share-progress --summary "<summary>" --phase <shipped|blocked>`.
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.
15
+ <!-- END:wibe-activity -->
@@ -6,10 +6,13 @@ alwaysApply: true
6
6
  # Wibe activity
7
7
 
8
8
  - This repository is connected to Wibe project `${WIBE_PROJECT_ID}`.
9
+ - At the start of a task that will edit files, call `wibe_get_active_work` for this project. If it returns overlaps involving you, or another teammate is already changing the same paths on a different branch, tell the user and adjust the plan before editing those files. Use teammate summaries in the response to explain the conflict.
10
+ - If the set of files you plan to change shifts, call `wibe_get_active_work` or `wibe_check_overlap` again. Do not poll on every edit. Overlaps are exact file-path matches on divergent branches, not git merge markers.
9
11
  - For most tasks, publish one update only after the requested outcome is complete. Do not publish plans, routine progress, or separate updates for editing, testing, committing, or pushing.
10
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.
11
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.
12
14
  - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
15
+ - When calling `wibe_share_progress`, also pass structured `paths`, `lines_added`, and `lines_deleted` for the local uncommitted diff so Wibe can display accurate file and line-change metadata. Prefer `git diff --numstat HEAD` totals rather than guessing.
13
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.
14
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.
15
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.