@wibeco/bridge 0.1.0
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 +46 -0
- package/dist/chunk-5CBPZCBE.js +427 -0
- package/dist/chunk-BL74PDGC.js +591 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +71 -0
- package/dist/codex-hook.d.ts +1 -0
- package/dist/codex-hook.js +22 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +48 -0
- package/package.json +42 -0
- package/templates/claude-code/README.md +14 -0
- package/templates/claude-code/mcp.json.example +8 -0
- package/templates/claude-code/settings.json.example +77 -0
- package/templates/codex/README.md +13 -0
- package/templates/codex/config.toml.example +6 -0
- package/templates/codex/hooks.json.example +76 -0
- package/templates/cursor/README.md +13 -0
- package/templates/cursor/hooks.json.example +60 -0
- package/templates/cursor/mcp.json.example +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Wibe agent bridge
|
|
2
|
+
|
|
3
|
+
Dependency-light TypeScript foundations for normalizing agent hooks and sending signed event batches.
|
|
4
|
+
|
|
5
|
+
## Privacy and security
|
|
6
|
+
|
|
7
|
+
- Adapters copy only explicit allow-listed metadata. Prompt text, tool input/output, file content,
|
|
8
|
+
shell command text, messages, and transcripts are never captured by default.
|
|
9
|
+
- Redaction is defense in depth, not permission to send arbitrary payloads.
|
|
10
|
+
- Offline files are created with owner-only permissions. Set `WIBE_QUEUE_PATH` to relocate the queue.
|
|
11
|
+
- Device authorization opens Wibe in the browser and stores the resulting
|
|
12
|
+
revocable project-scoped bearer token in the operating-system keychain.
|
|
13
|
+
- Event batches are schema-validated, bounded, idempotent, and sent only to the
|
|
14
|
+
configured Wibe HTTPS endpoint.
|
|
15
|
+
|
|
16
|
+
## CLI
|
|
17
|
+
|
|
18
|
+
Install and authorize Wibe from any GitHub repository:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx --yes @wibeco/bridge@latest setup \
|
|
22
|
+
--adapter cursor \
|
|
23
|
+
--project <uuid> \
|
|
24
|
+
--url https://trywibe.com \
|
|
25
|
+
--repository <owner/repository>
|
|
26
|
+
|
|
27
|
+
npx --yes @wibeco/bridge@latest doctor --repository <owner/repository>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Use `--adapter claude-code` or `--adapter codex` for another supported agent.
|
|
31
|
+
The setup command opens a one-time browser authorization and stores the
|
|
32
|
+
project-scoped device credential in the operating-system keychain.
|
|
33
|
+
|
|
34
|
+
`setup` keeps reviewable templates under `.wibe/integrations/<adapter>` and
|
|
35
|
+
installs native project configuration only when the destination file does not
|
|
36
|
+
already exist. Existing Cursor, Claude Code, or Codex configuration is never
|
|
37
|
+
overwritten; merge the staged template when the project already has one.
|
|
38
|
+
|
|
39
|
+
`WIBE_APP_URL` overrides the default local Wibe URL during setup.
|
|
40
|
+
`WIBE_QUEUE_PATH` optionally relocates the owner-only offline queue. CI and
|
|
41
|
+
headless environments can inject `WIBE_ACCESS_TOKEN`, `WIBE_PROJECT_ID`,
|
|
42
|
+
`WIBE_ORGANIZATION_ID`, `WIBE_REPOSITORY_ID`, and `WIBE_DEVICE_ID`; interactive
|
|
43
|
+
developer machines should use the keychain-backed browser flow.
|
|
44
|
+
|
|
45
|
+
Without a device credential, `emit` keeps normalized events in the offline
|
|
46
|
+
queue and never sends them elsewhere.
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JsonFileOfflineQueue,
|
|
3
|
+
MemoryOfflineQueue,
|
|
4
|
+
SignedBatchClient,
|
|
5
|
+
SystemCredentialStore,
|
|
6
|
+
createHookEvent,
|
|
7
|
+
detectRepository,
|
|
8
|
+
mapClaudeCodeHook,
|
|
9
|
+
mapCodexHook,
|
|
10
|
+
mapCursorHook,
|
|
11
|
+
matchesGitHubRepository,
|
|
12
|
+
normalizeGitHubRepository,
|
|
13
|
+
pollDeviceToken,
|
|
14
|
+
requestDeviceAuthorization
|
|
15
|
+
} from "./chunk-BL74PDGC.js";
|
|
16
|
+
|
|
17
|
+
// src/cli/commands.ts
|
|
18
|
+
import { access, cp, mkdir, readFile, writeFile } from "fs/promises";
|
|
19
|
+
import { homedir, hostname } from "os";
|
|
20
|
+
import { join, resolve } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { execFile } from "child_process";
|
|
23
|
+
var ADAPTERS = ["cursor", "claude-code", "codex"];
|
|
24
|
+
async function setupCommand(requestedAdapter, options = {}) {
|
|
25
|
+
const cwd = options.cwd ?? process.cwd();
|
|
26
|
+
const expectedRepository = options.expectedRepository ? normalizeGitHubRepository(options.expectedRepository) : void 0;
|
|
27
|
+
if (options.expectedRepository && !expectedRepository) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Expected repository "${options.expectedRepository}" is not a valid GitHub owner/repository.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (expectedRepository) {
|
|
33
|
+
await assertExpectedRepository(cwd, expectedRepository);
|
|
34
|
+
}
|
|
35
|
+
const adapter = await detectAdapter(requestedAdapter, cwd);
|
|
36
|
+
const packageRoot = resolve(fileURLToPath(new URL("../../../", import.meta.url)));
|
|
37
|
+
const packagedTemplate = join(packageRoot, "templates", adapter);
|
|
38
|
+
const source = await exists(packagedTemplate) ? packagedTemplate : resolve(packageRoot, "..", "..", "integrations", adapter);
|
|
39
|
+
const destination = join(cwd, ".wibe", "integrations", adapter);
|
|
40
|
+
if (!await exists(destination)) {
|
|
41
|
+
await mkdir(join(cwd, ".wibe", "integrations"), { recursive: true });
|
|
42
|
+
await cp(source, destination, { recursive: true, errorOnExist: true, force: false });
|
|
43
|
+
}
|
|
44
|
+
const appUrl = (options.appUrl ?? process.env.WIBE_APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
|
45
|
+
if (!options.projectId) {
|
|
46
|
+
return {
|
|
47
|
+
exitCode: 0,
|
|
48
|
+
message: `Installed safe ${adapter} templates at ${destination}. Run setup again with --project <id> to authorize this device.`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const projectConfigPath = join(cwd, ".wibe", "project.json");
|
|
52
|
+
const existingProjectConfig = await readProjectConfig(projectConfigPath);
|
|
53
|
+
if (existingProjectConfig && (existingProjectConfig.projectId !== options.projectId || existingProjectConfig.adapter !== adapter || existingProjectConfig.appUrl.replace(/\/$/, "") !== appUrl || existingProjectConfig.repository && expectedRepository && existingProjectConfig.repository !== expectedRepository)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Existing ${projectConfigPath} targets a different project, URL, adapter, or repository. It was not overwritten; remove it intentionally or rerun setup with matching options.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const authorization = await requestDeviceAuthorization({
|
|
59
|
+
appUrl,
|
|
60
|
+
projectId: options.projectId,
|
|
61
|
+
deviceName: `${hostname()} (${adapter})`,
|
|
62
|
+
agentName: adapter
|
|
63
|
+
});
|
|
64
|
+
const verificationUrl = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
65
|
+
process.stdout.write(
|
|
66
|
+
`Open ${verificationUrl}
|
|
67
|
+
Confirm code ${authorization.user_code}
|
|
68
|
+
`
|
|
69
|
+
);
|
|
70
|
+
openBrowser(verificationUrl);
|
|
71
|
+
const expiresAt = new Date(authorization.expires_at).getTime();
|
|
72
|
+
let token;
|
|
73
|
+
while (Date.now() < expiresAt) {
|
|
74
|
+
await new Promise(
|
|
75
|
+
(resolveDelay) => setTimeout(resolveDelay, authorization.interval * 1e3)
|
|
76
|
+
);
|
|
77
|
+
const response = await pollDeviceToken({
|
|
78
|
+
appUrl,
|
|
79
|
+
deviceCode: authorization.device_code
|
|
80
|
+
});
|
|
81
|
+
if (response.status === "pending") continue;
|
|
82
|
+
if (response.status !== "approved") {
|
|
83
|
+
throw new Error(`Device authorization ${response.status}.`);
|
|
84
|
+
}
|
|
85
|
+
token = response;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (!token) throw new Error("Device authorization expired.");
|
|
89
|
+
const credential = {
|
|
90
|
+
appUrl,
|
|
91
|
+
accessToken: token.accessToken,
|
|
92
|
+
projectId: token.projectId,
|
|
93
|
+
organizationId: token.organizationId,
|
|
94
|
+
repositoryId: token.repositoryId,
|
|
95
|
+
deviceId: token.deviceId
|
|
96
|
+
};
|
|
97
|
+
await new SystemCredentialStore().set(
|
|
98
|
+
"dev.wibe.bridge",
|
|
99
|
+
token.projectId,
|
|
100
|
+
JSON.stringify(credential)
|
|
101
|
+
);
|
|
102
|
+
if (!existingProjectConfig) {
|
|
103
|
+
await mkdir(join(cwd, ".wibe"), { recursive: true });
|
|
104
|
+
await writeFile(
|
|
105
|
+
projectConfigPath,
|
|
106
|
+
`${JSON.stringify(
|
|
107
|
+
{
|
|
108
|
+
projectId: token.projectId,
|
|
109
|
+
appUrl,
|
|
110
|
+
adapter,
|
|
111
|
+
...expectedRepository ? { repository: expectedRepository } : {}
|
|
112
|
+
},
|
|
113
|
+
null,
|
|
114
|
+
2
|
|
115
|
+
)}
|
|
116
|
+
`,
|
|
117
|
+
{ mode: 384, flag: "wx" }
|
|
118
|
+
);
|
|
119
|
+
} else if (expectedRepository && !existingProjectConfig.repository) {
|
|
120
|
+
await writeFile(
|
|
121
|
+
projectConfigPath,
|
|
122
|
+
`${JSON.stringify(
|
|
123
|
+
{ ...existingProjectConfig, repository: expectedRepository },
|
|
124
|
+
null,
|
|
125
|
+
2
|
|
126
|
+
)}
|
|
127
|
+
`,
|
|
128
|
+
{ mode: 384 }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const installedNativeFiles = await installNativeConfigs(
|
|
132
|
+
adapter,
|
|
133
|
+
source,
|
|
134
|
+
cwd,
|
|
135
|
+
appUrl
|
|
136
|
+
);
|
|
137
|
+
const heartbeat = await sendVerificationHeartbeat(credential, adapter, "setup");
|
|
138
|
+
return {
|
|
139
|
+
exitCode: heartbeat.error ? 1 : 0,
|
|
140
|
+
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}.`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function statusCommand(cwd = process.cwd()) {
|
|
144
|
+
const repo = await detectRepository(cwd);
|
|
145
|
+
const credential = await loadCredential(cwd);
|
|
146
|
+
const queue = queuePath();
|
|
147
|
+
const installed = (await Promise.all(
|
|
148
|
+
ADAPTERS.map(async (adapter) => ({
|
|
149
|
+
adapter,
|
|
150
|
+
installed: await exists(join(cwd, ".wibe", "integrations", adapter))
|
|
151
|
+
}))
|
|
152
|
+
)).filter((item) => item.installed).map((item) => item.adapter);
|
|
153
|
+
return {
|
|
154
|
+
exitCode: 0,
|
|
155
|
+
message: JSON.stringify(
|
|
156
|
+
{
|
|
157
|
+
repository: repo ?? null,
|
|
158
|
+
installedAdapters: installed,
|
|
159
|
+
connected: Boolean(credential),
|
|
160
|
+
projectId: credential?.projectId ?? null,
|
|
161
|
+
eventEndpoint: credential ? `${credential.appUrl}/api/events/batch` : null,
|
|
162
|
+
offlineQueue: queue
|
|
163
|
+
},
|
|
164
|
+
null,
|
|
165
|
+
2
|
|
166
|
+
)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async function emitCommand(adapter, eventName, input) {
|
|
170
|
+
const mapper = adapter === "cursor" ? mapCursorHook : adapter === "claude-code" ? mapClaudeCodeHook : mapCodexHook;
|
|
171
|
+
const mappedEvent = mapper(eventName, input);
|
|
172
|
+
const repo = await detectRepository(process.cwd());
|
|
173
|
+
const event = repo ? { ...mappedEvent, repo } : mappedEvent;
|
|
174
|
+
const queue = new JsonFileOfflineQueue(queuePath());
|
|
175
|
+
const credential = await loadCredential(process.cwd());
|
|
176
|
+
if (!credential) {
|
|
177
|
+
await queue.enqueue([event]);
|
|
178
|
+
return {
|
|
179
|
+
exitCode: 0,
|
|
180
|
+
message: `Queued ${event.kind}; this repository is not connected to Wibe.`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const result = await new SignedBatchClient({
|
|
184
|
+
endpoint: `${credential.appUrl}/api/events/batch`,
|
|
185
|
+
accessToken: credential.accessToken,
|
|
186
|
+
organizationId: credential.organizationId,
|
|
187
|
+
projectId: credential.projectId,
|
|
188
|
+
repositoryId: credential.repositoryId,
|
|
189
|
+
deviceId: credential.deviceId,
|
|
190
|
+
queue
|
|
191
|
+
}).capture(event);
|
|
192
|
+
return {
|
|
193
|
+
exitCode: result.error ? 1 : 0,
|
|
194
|
+
message: result.error ? `Queued ${event.kind}; delivery failed: ${result.error}` : `Delivered ${result.sent} event(s); ${result.remaining} queued.`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function doctorCommand(cwd = process.cwd(), requestedRepository) {
|
|
198
|
+
const credential = await loadCredential(cwd);
|
|
199
|
+
const projectConfig = await readProjectConfig(join(cwd, ".wibe", "project.json"));
|
|
200
|
+
const expectedRepository = requestedRepository ? normalizeGitHubRepository(requestedRepository) : projectConfig?.repository;
|
|
201
|
+
if (requestedRepository && !expectedRepository) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Expected repository "${requestedRepository}" is not a valid GitHub owner/repository.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const repository = await detectRepository(cwd);
|
|
207
|
+
const repositoryMatches = expectedRepository ? matchesGitHubRepository(repository?.remote, expectedRepository) : true;
|
|
208
|
+
let adapter;
|
|
209
|
+
let adapterError;
|
|
210
|
+
try {
|
|
211
|
+
adapter = await detectAdapter(void 0, cwd);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
adapterError = error instanceof Error ? error.message : String(error);
|
|
214
|
+
}
|
|
215
|
+
const heartbeat = credential && adapter && repositoryMatches ? await sendVerificationHeartbeat(credential, adapter, "doctor") : void 0;
|
|
216
|
+
const nativeHooks = await Promise.all([
|
|
217
|
+
exists(join(cwd, ".cursor", "hooks.json")),
|
|
218
|
+
exists(join(cwd, ".claude", "settings.json")),
|
|
219
|
+
exists(join(cwd, ".codex", "hooks.json"))
|
|
220
|
+
]);
|
|
221
|
+
const nativeMcp = await Promise.all([
|
|
222
|
+
exists(join(cwd, ".cursor", "mcp.json")),
|
|
223
|
+
exists(join(cwd, ".mcp.json")),
|
|
224
|
+
exists(join(cwd, ".codex", "config.toml"))
|
|
225
|
+
]);
|
|
226
|
+
const checks = [
|
|
227
|
+
["node", Number(process.versions.node.split(".")[0]) >= 20],
|
|
228
|
+
["git repository", Boolean(repository)],
|
|
229
|
+
...expectedRepository ? [
|
|
230
|
+
[
|
|
231
|
+
`repository remote (${repository?.remote ?? "origin is missing"}; expected ${expectedRepository}; run from the expected repository or correct origin)`,
|
|
232
|
+
repositoryMatches
|
|
233
|
+
]
|
|
234
|
+
] : [],
|
|
235
|
+
["device authorization", Boolean(credential)],
|
|
236
|
+
["event endpoint", Boolean(credential?.appUrl)],
|
|
237
|
+
["project scope", Boolean(credential?.projectId)],
|
|
238
|
+
[
|
|
239
|
+
adapterError ? `adapter detection (${adapterError})` : "adapter detection",
|
|
240
|
+
Boolean(adapter)
|
|
241
|
+
],
|
|
242
|
+
["agent hooks", nativeHooks.some(Boolean)],
|
|
243
|
+
["MCP configuration", nativeMcp.some(Boolean)],
|
|
244
|
+
["verification heartbeat", Boolean(heartbeat && !heartbeat.error)]
|
|
245
|
+
];
|
|
246
|
+
const failures = checks.filter(([, okay]) => !okay).length;
|
|
247
|
+
return {
|
|
248
|
+
exitCode: failures ? 1 : 0,
|
|
249
|
+
message: checks.map(([name, okay]) => `${okay ? "ok" : "missing"} ${name}`).join("\n")
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function loadCredential(cwd) {
|
|
253
|
+
if (process.env.WIBE_ACCESS_TOKEN && process.env.WIBE_PROJECT_ID && process.env.WIBE_ORGANIZATION_ID && process.env.WIBE_DEVICE_ID) {
|
|
254
|
+
return {
|
|
255
|
+
appUrl: process.env.WIBE_APP_URL ?? "http://localhost:3000",
|
|
256
|
+
accessToken: process.env.WIBE_ACCESS_TOKEN,
|
|
257
|
+
projectId: process.env.WIBE_PROJECT_ID,
|
|
258
|
+
organizationId: process.env.WIBE_ORGANIZATION_ID,
|
|
259
|
+
repositoryId: process.env.WIBE_REPOSITORY_ID,
|
|
260
|
+
deviceId: process.env.WIBE_DEVICE_ID
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const project = JSON.parse(
|
|
265
|
+
await readFile(join(cwd, ".wibe", "project.json"), "utf8")
|
|
266
|
+
);
|
|
267
|
+
if (!project.projectId) return null;
|
|
268
|
+
const stored = await new SystemCredentialStore().get(
|
|
269
|
+
"dev.wibe.bridge",
|
|
270
|
+
project.projectId
|
|
271
|
+
);
|
|
272
|
+
return stored ? JSON.parse(stored) : null;
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function openBrowser(url) {
|
|
278
|
+
let executable = "xdg-open";
|
|
279
|
+
let args = [url];
|
|
280
|
+
if (process.platform === "darwin") executable = "open";
|
|
281
|
+
if (process.platform === "win32") {
|
|
282
|
+
executable = "cmd";
|
|
283
|
+
args = ["/c", "start", "", url];
|
|
284
|
+
}
|
|
285
|
+
execFile(executable, args, () => void 0);
|
|
286
|
+
}
|
|
287
|
+
async function installNativeConfigs(adapter, source, cwd, appUrl) {
|
|
288
|
+
const files = adapter === "cursor" ? [
|
|
289
|
+
["hooks.json.example", ".cursor/hooks.json"],
|
|
290
|
+
["mcp.json.example", ".cursor/mcp.json"]
|
|
291
|
+
] : adapter === "claude-code" ? [
|
|
292
|
+
["settings.json.example", ".claude/settings.json"],
|
|
293
|
+
["mcp.json.example", ".mcp.json"]
|
|
294
|
+
] : [
|
|
295
|
+
["hooks.json.example", ".codex/hooks.json"],
|
|
296
|
+
["config.toml.example", ".codex/config.toml"]
|
|
297
|
+
];
|
|
298
|
+
const installed = [];
|
|
299
|
+
for (const [sourceName, destinationName] of files) {
|
|
300
|
+
const destinationPath = join(cwd, destinationName);
|
|
301
|
+
if (await exists(destinationPath)) continue;
|
|
302
|
+
const template = await readFile(join(source, sourceName), "utf8");
|
|
303
|
+
await mkdir(resolve(destinationPath, ".."), { recursive: true });
|
|
304
|
+
await writeFile(
|
|
305
|
+
destinationPath,
|
|
306
|
+
template.replaceAll("${env:WIBE_APP_URL}", appUrl).replaceAll("${WIBE_APP_URL}", appUrl),
|
|
307
|
+
{ mode: 384 }
|
|
308
|
+
);
|
|
309
|
+
installed.push(destinationName);
|
|
310
|
+
}
|
|
311
|
+
return installed;
|
|
312
|
+
}
|
|
313
|
+
function queuePath() {
|
|
314
|
+
return process.env.WIBE_QUEUE_PATH ?? join(homedir(), ".wibe", "events.json");
|
|
315
|
+
}
|
|
316
|
+
async function exists(path) {
|
|
317
|
+
try {
|
|
318
|
+
await access(path);
|
|
319
|
+
return true;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function parseAdapter(value) {
|
|
325
|
+
if (value && ADAPTERS.includes(value)) return value;
|
|
326
|
+
throw new Error(`Adapter must be one of: ${ADAPTERS.join(", ")}`);
|
|
327
|
+
}
|
|
328
|
+
var ENVIRONMENT_SIGNALS = {
|
|
329
|
+
cursor: ["CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_PROJECT_DIR"],
|
|
330
|
+
"claude-code": ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_REMOTE"],
|
|
331
|
+
codex: ["CODEX_HOME", "CODEX_SANDBOX", "CODEX_THREAD_ID"]
|
|
332
|
+
};
|
|
333
|
+
var REPOSITORY_SIGNALS = {
|
|
334
|
+
cursor: [".wibe/integrations/cursor", ".cursor/hooks.json", ".cursor/mcp.json"],
|
|
335
|
+
"claude-code": [
|
|
336
|
+
".wibe/integrations/claude-code",
|
|
337
|
+
".claude/settings.json",
|
|
338
|
+
".mcp.json"
|
|
339
|
+
],
|
|
340
|
+
codex: [".wibe/integrations/codex", ".codex/hooks.json", ".codex/config.toml"]
|
|
341
|
+
};
|
|
342
|
+
async function detectAdapter(explicitAdapter, cwd = process.cwd(), environment = process.env) {
|
|
343
|
+
if (explicitAdapter) return explicitAdapter;
|
|
344
|
+
const configuredProject = await readProjectConfig(join(cwd, ".wibe", "project.json"));
|
|
345
|
+
if (configuredProject) return configuredProject.adapter;
|
|
346
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const adapter of ADAPTERS) {
|
|
348
|
+
const environmentMatches = ENVIRONMENT_SIGNALS[adapter].filter(
|
|
349
|
+
(name) => typeof environment[name] === "string" && environment[name] !== ""
|
|
350
|
+
);
|
|
351
|
+
const repositoryMatches = (await Promise.all(
|
|
352
|
+
REPOSITORY_SIGNALS[adapter].map(async (path) => ({
|
|
353
|
+
path,
|
|
354
|
+
found: await exists(join(cwd, path))
|
|
355
|
+
}))
|
|
356
|
+
)).filter(({ found }) => found).map(({ path }) => path);
|
|
357
|
+
const evidence = [
|
|
358
|
+
...environmentMatches.map((name) => `environment ${name}`),
|
|
359
|
+
...repositoryMatches.map((path) => `config ${path}`)
|
|
360
|
+
];
|
|
361
|
+
if (evidence.length > 0) candidates.set(adapter, evidence);
|
|
362
|
+
}
|
|
363
|
+
if (candidates.size === 1) return candidates.keys().next().value;
|
|
364
|
+
if (candidates.size === 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Could not detect an adapter from the environment or repository config. Pass --adapter <${ADAPTERS.join("|")}>.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const details = [...candidates].map(([adapter, evidence]) => `${adapter} (${evidence.join(", ")})`).join("; ");
|
|
370
|
+
throw new Error(
|
|
371
|
+
`Adapter detection is ambiguous: ${details}. Pass --adapter <${ADAPTERS.join("|")}> to choose explicitly.`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
async function readProjectConfig(path) {
|
|
375
|
+
if (!await exists(path)) return void 0;
|
|
376
|
+
let parsed;
|
|
377
|
+
try {
|
|
378
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
379
|
+
} catch {
|
|
380
|
+
throw new Error(`Existing ${path} is not valid JSON and was left untouched.`);
|
|
381
|
+
}
|
|
382
|
+
if (typeof parsed.projectId !== "string" || typeof parsed.appUrl !== "string" || !ADAPTERS.includes(parsed.adapter)) {
|
|
383
|
+
throw new Error(`Existing ${path} is incomplete or invalid and was left untouched.`);
|
|
384
|
+
}
|
|
385
|
+
if (parsed.repository !== void 0 && !normalizeGitHubRepository(parsed.repository)) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Existing ${path} has an invalid GitHub repository and was left untouched.`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (parsed.repository) {
|
|
391
|
+
parsed.repository = normalizeGitHubRepository(parsed.repository);
|
|
392
|
+
}
|
|
393
|
+
return parsed;
|
|
394
|
+
}
|
|
395
|
+
async function assertExpectedRepository(cwd, expected) {
|
|
396
|
+
const repository = await detectRepository(cwd);
|
|
397
|
+
if (matchesGitHubRepository(repository?.remote, expected)) return;
|
|
398
|
+
const actual = repository?.remote ? `"${repository.remote}"` : "no origin remote";
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Repository mismatch: expected GitHub repository "${expected}", but found ${actual}. Run this command from the expected repository root or correct the origin remote, then retry.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
async function sendVerificationHeartbeat(credential, adapter, reason) {
|
|
404
|
+
return new SignedBatchClient({
|
|
405
|
+
endpoint: `${credential.appUrl}/api/events/batch`,
|
|
406
|
+
accessToken: credential.accessToken,
|
|
407
|
+
organizationId: credential.organizationId,
|
|
408
|
+
projectId: credential.projectId,
|
|
409
|
+
repositoryId: credential.repositoryId,
|
|
410
|
+
deviceId: credential.deviceId,
|
|
411
|
+
queue: new MemoryOfflineQueue()
|
|
412
|
+
}).capture(
|
|
413
|
+
createHookEvent({
|
|
414
|
+
source: adapter,
|
|
415
|
+
kind: "unknown",
|
|
416
|
+
metadata: { verification: reason }
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export {
|
|
422
|
+
setupCommand,
|
|
423
|
+
statusCommand,
|
|
424
|
+
emitCommand,
|
|
425
|
+
doctorCommand,
|
|
426
|
+
parseAdapter
|
|
427
|
+
};
|