@wibeco/bridge 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/dist/{chunk-K7TK7FHF.js → chunk-4WIGVKDR.js} +178 -70
- package/dist/{chunk-7LWUGOKM.js → chunk-YLC4TF4F.js} +706 -33
- package/dist/cli.js +30 -3
- package/dist/codex-hook.js +2 -2
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/claude-code/README.md +9 -6
- package/templates/claude-code/mcp.json.example +1 -1
- package/templates/claude-code/wibe-activity.md.example +15 -0
- package/templates/codex/README.md +12 -11
- package/templates/codex/config.toml.example +1 -5
- package/templates/codex/wibe-activity.md.example +15 -0
- package/templates/cursor/README.md +4 -2
- package/templates/cursor/mcp.json.example +1 -1
- package/templates/cursor/wibe-activity.mdc.example +3 -0
|
@@ -19,15 +19,206 @@ import {
|
|
|
19
19
|
startPresenceSession,
|
|
20
20
|
stopPresenceSession,
|
|
21
21
|
updatePresenceSession
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-4WIGVKDR.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";
|
|
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}
|
|
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
|
-
|
|
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);
|
|
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 =
|
|
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(
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
535
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
593
|
-
|
|
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]") &&
|
|
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 {
|
|
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" &&
|
|
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
|
|
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(
|
|
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,
|