@synap-core/cli 1.0.0 → 1.2.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 +158 -115
- package/dist/commands/finish.d.ts +15 -8
- package/dist/commands/finish.js +153 -43
- package/dist/commands/finish.js.map +1 -1
- package/dist/commands/openclaw.d.ts +16 -1
- package/dist/commands/openclaw.js +576 -192
- package/dist/commands/openclaw.js.map +1 -1
- package/dist/index.js +35 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* synap openclaw restart — restart the container
|
|
12
12
|
*/
|
|
13
13
|
import chalk from "chalk";
|
|
14
|
+
import ora from "ora";
|
|
14
15
|
import { execSync } from "child_process";
|
|
15
16
|
import prompts from "prompts";
|
|
17
|
+
import crypto from "node:crypto";
|
|
16
18
|
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
17
20
|
import { log, banner } from "../utils/logger.js";
|
|
18
21
|
import { detectOpenClaw } from "../lib/openclaw.js";
|
|
19
|
-
import { findSynapDeployDir } from "../lib/pod.js";
|
|
22
|
+
import { findSynapDeployDir, getLocalPodConfig } from "../lib/pod.js";
|
|
23
|
+
import { getStoredToken } from "../lib/auth.js";
|
|
20
24
|
// ─── Overview ────────────────────────────────────────────────────────────────
|
|
21
25
|
export async function openclawOverview() {
|
|
22
26
|
banner();
|
|
@@ -46,10 +50,18 @@ export async function openclawOverview() {
|
|
|
46
50
|
log.dim(`Version: ${oc.version}`);
|
|
47
51
|
// ── AI provider ─────────────────────────────────────────────────────────
|
|
48
52
|
log.heading("AI Provider");
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const aiConfig = readOpenClawAiConfig(oc);
|
|
54
|
+
const hasAnyKey = !!(aiConfig.anthropicKey || aiConfig.openaiKey || aiConfig.geminiKey);
|
|
55
|
+
const apiKeyStatus = { configured: hasAnyKey };
|
|
56
|
+
if (hasAnyKey) {
|
|
57
|
+
if (aiConfig.anthropicKey)
|
|
58
|
+
log.success(`Anthropic: ${maskKey(aiConfig.anthropicKey)}`);
|
|
59
|
+
if (aiConfig.openaiKey)
|
|
60
|
+
log.success(`OpenAI: ${maskKey(aiConfig.openaiKey)}`);
|
|
61
|
+
if (aiConfig.geminiKey)
|
|
62
|
+
log.success(`Google: ${maskKey(aiConfig.geminiKey)}`);
|
|
63
|
+
if (aiConfig.primaryModel)
|
|
64
|
+
log.dim(`Model: ${aiConfig.primaryModel}`);
|
|
53
65
|
}
|
|
54
66
|
else {
|
|
55
67
|
log.warn("No AI API key configured — OpenClaw cannot process requests");
|
|
@@ -78,16 +90,20 @@ export async function openclawOverview() {
|
|
|
78
90
|
}
|
|
79
91
|
// ── Dashboard ────────────────────────────────────────────────────────────
|
|
80
92
|
log.heading("Dashboard");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
log.
|
|
84
|
-
log.dim(
|
|
85
|
-
log.dim(
|
|
86
|
-
|
|
93
|
+
const publicUrl = getOpenClawPublicUrl();
|
|
94
|
+
if (publicUrl) {
|
|
95
|
+
log.success(`Public: ${chalk.cyan(publicUrl)}`);
|
|
96
|
+
log.dim(`Local: http://localhost:${oc.gatewayPort ?? 18789}`);
|
|
97
|
+
log.dim("Open: synap openclaw dashboard");
|
|
98
|
+
}
|
|
99
|
+
else if (oc.runtime === "docker") {
|
|
100
|
+
log.info(`Local: ${chalk.cyan(`http://localhost:${oc.gatewayPort ?? 18789}`)}`);
|
|
101
|
+
log.dim("Expose via domain: synap openclaw setup-domain");
|
|
102
|
+
log.dim("SSH tunnel (remote): synap openclaw dashboard");
|
|
87
103
|
}
|
|
88
104
|
else {
|
|
89
105
|
log.info(`Web UI: ${chalk.cyan(`http://localhost:${oc.gatewayPort ?? 18789}`)}`);
|
|
90
|
-
log.dim("Open: openclaw dashboard — or
|
|
106
|
+
log.dim("Open: openclaw dashboard — or: synap openclaw dashboard");
|
|
91
107
|
}
|
|
92
108
|
// ── How to connect ───────────────────────────────────────────────────────
|
|
93
109
|
log.heading("AI Client (MCP)");
|
|
@@ -126,37 +142,49 @@ export async function openclawConnect(opts) {
|
|
|
126
142
|
const gatewayPort = oc.gatewayPort ?? 18789;
|
|
127
143
|
const isDocker = oc.runtime === "docker";
|
|
128
144
|
// OpenClaw MCP is stdio-based — clients run `openclaw mcp serve` as a local process
|
|
129
|
-
// which connects to the gateway over WebSocket.
|
|
130
|
-
//
|
|
145
|
+
// which connects to the gateway over WebSocket. The gateway token authenticates
|
|
146
|
+
// the connection — we fetch it from the container so the config is ready to paste.
|
|
147
|
+
const token = readGatewayToken(oc) ?? undefined;
|
|
148
|
+
if (!token) {
|
|
149
|
+
log.warn("Could not read gateway token from OpenClaw.");
|
|
150
|
+
log.dim("The MCP configs below will require you to add --token manually.");
|
|
151
|
+
log.dim("Run: synap openclaw token");
|
|
152
|
+
log.blank();
|
|
153
|
+
}
|
|
131
154
|
const client = opts.client?.toLowerCase();
|
|
132
155
|
if (!client || client === "claude") {
|
|
133
|
-
printMcpConfig("Claude Desktop", gatewayPort, isDocker, "claude");
|
|
156
|
+
printMcpConfig("Claude Desktop", gatewayPort, isDocker, "claude", token);
|
|
134
157
|
}
|
|
135
158
|
if (!client || client === "cursor") {
|
|
136
|
-
printMcpConfig("Cursor", gatewayPort, isDocker, "cursor");
|
|
159
|
+
printMcpConfig("Cursor", gatewayPort, isDocker, "cursor", token);
|
|
137
160
|
}
|
|
138
161
|
if (!client || client === "windsurf") {
|
|
139
|
-
printMcpConfig("Windsurf", gatewayPort, isDocker, "windsurf");
|
|
162
|
+
printMcpConfig("Windsurf", gatewayPort, isDocker, "windsurf", token);
|
|
140
163
|
}
|
|
141
164
|
if (client && !["claude", "cursor", "windsurf"].includes(client)) {
|
|
142
165
|
log.warn(`Unknown client "${client}". Showing generic config.`);
|
|
143
|
-
printMcpConfig("MCP Client", gatewayPort, isDocker, "generic");
|
|
166
|
+
printMcpConfig("MCP Client", gatewayPort, isDocker, "generic", token);
|
|
144
167
|
}
|
|
145
168
|
if (isDocker) {
|
|
146
169
|
log.blank();
|
|
147
170
|
log.info("Remote server? Tunnel the gateway port first:");
|
|
148
|
-
log.dim(
|
|
149
|
-
log.dim(" Then use the configs above (they point to localhost
|
|
171
|
+
log.dim(` ssh -N -L ${gatewayPort}:localhost:${gatewayPort} user@your-server`);
|
|
172
|
+
log.dim(" Then use the configs above (they point to localhost)");
|
|
150
173
|
log.blank();
|
|
151
|
-
log.dim("openclaw must be installed locally
|
|
174
|
+
log.dim("openclaw must be installed locally on the client machine:");
|
|
175
|
+
log.dim(" npm i -g openclaw");
|
|
152
176
|
}
|
|
153
177
|
}
|
|
154
|
-
function printMcpConfig(label, gatewayPort, isRemote, client) {
|
|
178
|
+
function printMcpConfig(label, gatewayPort, isRemote, client, token) {
|
|
155
179
|
log.heading(label);
|
|
156
180
|
// MCP config: stdio command that connects to the local (or tunneled) gateway
|
|
157
|
-
const args =
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
const args = ["mcp", "serve"];
|
|
182
|
+
if (isRemote) {
|
|
183
|
+
args.push("--url", `ws://localhost:${gatewayPort}`);
|
|
184
|
+
}
|
|
185
|
+
if (token) {
|
|
186
|
+
args.push("--token", token);
|
|
187
|
+
}
|
|
160
188
|
const config = JSON.stringify({ mcpServers: { openclaw: { command: "openclaw", args } } }, null, 2);
|
|
161
189
|
const paths = {
|
|
162
190
|
claude: "macOS: ~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
@@ -181,95 +209,269 @@ function printMcpConfig(label, gatewayPort, isRemote, client) {
|
|
|
181
209
|
export function openclawDashboard() {
|
|
182
210
|
const oc = detectOpenClaw();
|
|
183
211
|
const port = oc.gatewayPort ?? 18789;
|
|
184
|
-
const
|
|
212
|
+
const localUrl = `http://localhost:${port}`;
|
|
213
|
+
// Check if a public domain is configured via Caddy
|
|
214
|
+
const publicUrl = getOpenClawPublicUrl();
|
|
215
|
+
if (publicUrl) {
|
|
216
|
+
log.info(`Dashboard: ${chalk.cyan(publicUrl)}`);
|
|
217
|
+
log.blank();
|
|
218
|
+
try {
|
|
219
|
+
const open = process.platform === "darwin" ? "open" : "xdg-open";
|
|
220
|
+
execSync(`${open} ${publicUrl}`, { stdio: "ignore" });
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
log.dim("Open the URL above in your browser.");
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
185
227
|
if (oc.runtime === "docker") {
|
|
186
|
-
// On a remote server the browser can't open — give SSH tunnel instructions
|
|
187
228
|
const isRemoteServer = !process.env.DISPLAY && process.platform === "linux";
|
|
188
229
|
if (isRemoteServer) {
|
|
189
|
-
log.info(`
|
|
230
|
+
log.info(`Dashboard: ${chalk.cyan(localUrl)}`);
|
|
231
|
+
log.blank();
|
|
232
|
+
log.info("You're on a remote server — tunnel the port to your laptop:");
|
|
190
233
|
log.blank();
|
|
191
|
-
log.
|
|
234
|
+
console.log(chalk.cyan(` ssh -N -L ${port}:localhost:${port} user@$(hostname -I | awk '{print $1}')`));
|
|
192
235
|
log.blank();
|
|
193
|
-
|
|
236
|
+
log.dim(`Then open ${chalk.cyan(localUrl)} in your browser.`);
|
|
194
237
|
log.blank();
|
|
195
|
-
log.
|
|
238
|
+
log.info("Or expose it permanently via your domain:");
|
|
239
|
+
log.dim(" synap openclaw setup-domain");
|
|
196
240
|
}
|
|
197
241
|
else {
|
|
198
|
-
|
|
199
|
-
log.info(`Opening ${chalk.cyan(url)} ...`);
|
|
242
|
+
log.info(`Opening ${chalk.cyan(localUrl)} ...`);
|
|
200
243
|
try {
|
|
201
244
|
const open = process.platform === "darwin" ? "open" : "xdg-open";
|
|
202
|
-
execSync(`${open} ${
|
|
245
|
+
execSync(`${open} ${localUrl}`, { stdio: "ignore" });
|
|
203
246
|
}
|
|
204
247
|
catch {
|
|
205
|
-
log.dim(`Open manually: ${
|
|
248
|
+
log.dim(`Open manually: ${localUrl}`);
|
|
206
249
|
}
|
|
207
250
|
}
|
|
208
251
|
}
|
|
209
252
|
else {
|
|
210
|
-
|
|
211
|
-
log.info(`Opening dashboard...`);
|
|
253
|
+
log.info("Opening dashboard...");
|
|
212
254
|
try {
|
|
213
255
|
execSync("openclaw dashboard", { stdio: "inherit", timeout: 5000 });
|
|
214
256
|
}
|
|
215
257
|
catch {
|
|
216
|
-
// Fallback: open URL directly
|
|
217
258
|
try {
|
|
218
259
|
const open = process.platform === "darwin" ? "open" : "xdg-open";
|
|
219
|
-
execSync(`${open} ${
|
|
260
|
+
execSync(`${open} ${localUrl}`, { stdio: "ignore" });
|
|
220
261
|
}
|
|
221
262
|
catch {
|
|
222
|
-
log.info(`Dashboard: ${chalk.cyan(
|
|
263
|
+
log.info(`Dashboard: ${chalk.cyan(localUrl)}`);
|
|
223
264
|
}
|
|
224
265
|
}
|
|
225
266
|
}
|
|
226
267
|
}
|
|
227
|
-
|
|
228
|
-
export async function openclawConfigure() {
|
|
268
|
+
export async function openclawSetupDomain() {
|
|
229
269
|
banner();
|
|
230
|
-
log.heading("
|
|
270
|
+
log.heading("Expose OpenClaw Dashboard");
|
|
271
|
+
log.blank();
|
|
231
272
|
const deployDir = findSynapDeployDir();
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
273
|
+
if (!deployDir) {
|
|
274
|
+
log.error("Couldn't find the Synap deploy directory.");
|
|
275
|
+
log.dim("Run this command on the server where your pod is deployed.");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const envDomain = readEnvVar(deployDir, "DOMAIN");
|
|
279
|
+
if (!envDomain) {
|
|
280
|
+
log.error("DOMAIN is not set in .env — can't determine pod domain.");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Detect pod type: managed (*.synap.live) vs self-hosted (custom domain)
|
|
284
|
+
const isManaged = envDomain.endsWith(".synap.live") || envDomain === "synap.live";
|
|
285
|
+
const localConfig = getLocalPodConfig();
|
|
286
|
+
const creds = getStoredToken();
|
|
287
|
+
log.info(`Pod domain: ${chalk.cyan(envDomain)}`);
|
|
288
|
+
log.dim(`Type: ${isManaged ? "managed (synap.live)" : "self-hosted"}`);
|
|
289
|
+
log.blank();
|
|
290
|
+
let publicDomain;
|
|
291
|
+
let authMode;
|
|
292
|
+
let basicAuthPassword;
|
|
293
|
+
if (isManaged) {
|
|
294
|
+
// ── Managed pod flow: CP creates DNS + CP OAuth ───────────────────────
|
|
295
|
+
if (!creds) {
|
|
296
|
+
log.error("Not logged in to Synap.");
|
|
297
|
+
log.dim("Run: synap login (or: synap login --token <token>)");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!localConfig?.podId) {
|
|
301
|
+
log.error("Pod ID not found in local config.");
|
|
302
|
+
log.dim("Run: synap init (to set up the pod connection)");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const { confirm } = await prompts({
|
|
306
|
+
type: "confirm",
|
|
307
|
+
name: "confirm",
|
|
308
|
+
message: `Create DNS record openclaw.${envDomain}? (CP will provision it)`,
|
|
309
|
+
initial: true,
|
|
310
|
+
});
|
|
311
|
+
if (!confirm)
|
|
312
|
+
return;
|
|
313
|
+
const spinner = ora("Asking Control Plane to create DNS record...").start();
|
|
314
|
+
try {
|
|
315
|
+
const result = await requestDashboardDomainFromCp(creds.token, localConfig.podId);
|
|
316
|
+
publicDomain = result.domain;
|
|
317
|
+
authMode = result.authMode;
|
|
318
|
+
spinner.succeed(`Domain created: ${chalk.cyan(publicDomain)}`);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
spinner.fail(err instanceof Error ? err.message : String(err));
|
|
322
|
+
log.dim("If this is a custom-domain or self-hosted pod, run again — it will fall back to manual mode.");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// ── Self-hosted flow: user sets DNS, we use basic auth ────────────────
|
|
328
|
+
log.info("Self-hosted pod — you'll need to add a DNS record yourself.");
|
|
237
329
|
log.blank();
|
|
330
|
+
const { subdomain } = await prompts({
|
|
331
|
+
type: "text",
|
|
332
|
+
name: "subdomain",
|
|
333
|
+
message: "Public subdomain for OpenClaw:",
|
|
334
|
+
initial: `openclaw.${envDomain}`,
|
|
335
|
+
});
|
|
336
|
+
if (!subdomain)
|
|
337
|
+
return;
|
|
338
|
+
publicDomain = subdomain;
|
|
339
|
+
authMode = "basic";
|
|
340
|
+
log.blank();
|
|
341
|
+
log.info("Add this DNS A record:");
|
|
342
|
+
log.dim(` Type: A`);
|
|
343
|
+
log.dim(` Name: ${publicDomain}`);
|
|
344
|
+
log.dim(` Value: <this server's public IP>`);
|
|
345
|
+
log.blank();
|
|
346
|
+
const { dnsReady } = await prompts({
|
|
347
|
+
type: "confirm",
|
|
348
|
+
name: "dnsReady",
|
|
349
|
+
message: "DNS record added?",
|
|
350
|
+
initial: true,
|
|
351
|
+
});
|
|
352
|
+
if (!dnsReady) {
|
|
353
|
+
log.dim("Run this command again once DNS is set up.");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Generate a strong random password for basic auth
|
|
357
|
+
basicAuthPassword = generatePassword(32);
|
|
238
358
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
359
|
+
// ── Write the Caddy auth snippet ──────────────────────────────────────────
|
|
360
|
+
const snippetPath = path.join(deployDir, "openclaw_auth.snippet");
|
|
361
|
+
if (authMode === "cp-oauth") {
|
|
362
|
+
fs.writeFileSync(snippetPath, generateCpOAuthSnippet(), { mode: 0o644 });
|
|
363
|
+
log.success("Wrote CP OAuth auth snippet");
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const hash = await bcryptHashViaCaddy(basicAuthPassword);
|
|
367
|
+
fs.writeFileSync(snippetPath, generateBasicAuthSnippet("openclaw", hash), { mode: 0o644 });
|
|
368
|
+
log.success("Wrote basic auth snippet");
|
|
369
|
+
}
|
|
370
|
+
// ── Update .env ───────────────────────────────────────────────────────────
|
|
371
|
+
const envFile = path.join(deployDir, ".env");
|
|
372
|
+
writeEnvVar(envFile, "OPENCLAW_DOMAIN", publicDomain);
|
|
373
|
+
log.success(`Set OPENCLAW_DOMAIN=${publicDomain} in .env`);
|
|
374
|
+
// ── Restart Caddy ────────────────────────────────────────────────────────
|
|
375
|
+
log.blank();
|
|
376
|
+
log.info("Restarting Caddy to apply changes...");
|
|
377
|
+
try {
|
|
378
|
+
execSync("docker compose restart caddy", {
|
|
379
|
+
cwd: deployDir,
|
|
380
|
+
stdio: "pipe",
|
|
381
|
+
timeout: 30000,
|
|
382
|
+
});
|
|
383
|
+
log.success("Caddy restarted");
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
log.warn("Caddy restart failed — run manually:");
|
|
387
|
+
log.dim(` cd ${deployDir} && docker compose restart caddy`);
|
|
388
|
+
}
|
|
389
|
+
// ── Summary ──────────────────────────────────────────────────────────────
|
|
390
|
+
log.blank();
|
|
391
|
+
console.log(chalk.green("═══════════════════════════════════════════"));
|
|
392
|
+
console.log(chalk.green.bold(" OpenClaw Dashboard Ready"));
|
|
393
|
+
console.log(chalk.green("═══════════════════════════════════════════"));
|
|
394
|
+
log.blank();
|
|
395
|
+
log.info(`URL: ${chalk.cyan(`https://${publicDomain}`)}`);
|
|
396
|
+
log.blank();
|
|
397
|
+
if (authMode === "cp-oauth") {
|
|
398
|
+
log.info("Auth: Synap session (you're already logged in to synap.live)");
|
|
399
|
+
log.dim("Open the URL — if you're not signed in, you'll be redirected to login.");
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
log.info("Auth: basic auth");
|
|
403
|
+
log.dim(` Username: openclaw`);
|
|
404
|
+
log.dim(` Password: ${chalk.cyan(basicAuthPassword)}`);
|
|
405
|
+
log.blank();
|
|
406
|
+
log.warn("Save this password — it won't be shown again.");
|
|
407
|
+
}
|
|
408
|
+
log.blank();
|
|
409
|
+
log.dim("TLS: Caddy will provision a Let's Encrypt certificate on first visit.");
|
|
410
|
+
log.dim("This takes ~30s the first time.");
|
|
411
|
+
log.blank();
|
|
412
|
+
}
|
|
413
|
+
export async function openclawConfigure(opts = {}) {
|
|
414
|
+
const oc = detectOpenClaw();
|
|
415
|
+
if (!oc.found) {
|
|
416
|
+
log.error("OpenClaw is not running.");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// ── Show current config ─────────────────────────────────────────────────
|
|
420
|
+
if (opts.show) {
|
|
421
|
+
log.heading("OpenClaw AI Config");
|
|
422
|
+
const current = readOpenClawAiConfig(oc);
|
|
423
|
+
if (current.anthropicKey)
|
|
424
|
+
log.success(`Anthropic: ${maskKey(current.anthropicKey)}`);
|
|
425
|
+
if (current.openaiKey)
|
|
426
|
+
log.success(`OpenAI: ${maskKey(current.openaiKey)}`);
|
|
427
|
+
if (current.geminiKey)
|
|
428
|
+
log.success(`Google: ${maskKey(current.geminiKey)}`);
|
|
429
|
+
if (current.primaryModel)
|
|
430
|
+
log.info(`Model: ${current.primaryModel}`);
|
|
431
|
+
if (!current.anthropicKey && !current.openaiKey && !current.geminiKey) {
|
|
432
|
+
log.warn("No AI provider key configured");
|
|
433
|
+
}
|
|
265
434
|
return;
|
|
266
|
-
|
|
435
|
+
}
|
|
436
|
+
// ── Interactive (delegate to OpenClaw's own wizard) ──────────────────────
|
|
437
|
+
if (opts.interactive && oc.runtime === "docker") {
|
|
438
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
439
|
+
log.heading("Handing off to OpenClaw");
|
|
440
|
+
log.dim(`Running: docker exec -it ${containerName} openclaw configure`);
|
|
267
441
|
log.blank();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
442
|
+
try {
|
|
443
|
+
execSync(`docker exec -it ${containerName} openclaw configure`, { stdio: "inherit" });
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
log.error(`openclaw configure failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
447
|
+
}
|
|
271
448
|
return;
|
|
272
449
|
}
|
|
450
|
+
banner();
|
|
451
|
+
log.heading("Configure AI Provider");
|
|
452
|
+
// ── Scripted path (--provider + --key) ───────────────────────────────────
|
|
453
|
+
let provider = opts.provider;
|
|
454
|
+
let apiKey = opts.key;
|
|
455
|
+
let model = opts.model;
|
|
456
|
+
if (!provider) {
|
|
457
|
+
const pick = await prompts({
|
|
458
|
+
type: "select",
|
|
459
|
+
name: "provider",
|
|
460
|
+
message: "Which AI provider?",
|
|
461
|
+
choices: [
|
|
462
|
+
{ title: "Anthropic (Claude)", description: "recommended", value: "anthropic" },
|
|
463
|
+
{ title: "OpenAI (GPT-4o)", value: "openai" },
|
|
464
|
+
{ title: "Google (Gemini)", value: "google" },
|
|
465
|
+
{ title: "Run OpenClaw's own wizard", description: "interactive", value: "wizard" },
|
|
466
|
+
],
|
|
467
|
+
});
|
|
468
|
+
if (!pick.provider)
|
|
469
|
+
return;
|
|
470
|
+
if (pick.provider === "wizard") {
|
|
471
|
+
return openclawConfigure({ interactive: true });
|
|
472
|
+
}
|
|
473
|
+
provider = pick.provider;
|
|
474
|
+
}
|
|
273
475
|
const envKey = provider === "anthropic"
|
|
274
476
|
? "ANTHROPIC_API_KEY"
|
|
275
477
|
: provider === "openai"
|
|
@@ -280,64 +482,47 @@ export async function openclawConfigure() {
|
|
|
280
482
|
: provider === "openai"
|
|
281
483
|
? "openai/gpt-4o"
|
|
282
484
|
: "google/gemini-2.0-flash";
|
|
283
|
-
|
|
284
|
-
type: "password",
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
});
|
|
296
|
-
if (deployDir) {
|
|
297
|
-
// Write to .env file
|
|
298
|
-
const envFile = `${deployDir}/.env`;
|
|
299
|
-
writeEnvVar(envFile, envKey, apiKey);
|
|
300
|
-
if (model && model !== modelDefault) {
|
|
301
|
-
writeEnvVar(envFile, "OPENCLAW_MODEL", model);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
writeEnvVar(envFile, "OPENCLAW_MODEL", modelDefault);
|
|
305
|
-
}
|
|
306
|
-
log.blank();
|
|
307
|
-
log.success(`${envKey} written to ${deployDir}/.env`);
|
|
308
|
-
// Restart container to pick up new env vars
|
|
309
|
-
const oc = detectOpenClaw();
|
|
310
|
-
const containerName = oc.containerName ?? "openclaw";
|
|
311
|
-
const { doRestart } = await prompts({
|
|
312
|
-
type: "confirm",
|
|
313
|
-
name: "doRestart",
|
|
314
|
-
message: `Restart ${containerName} to apply?`,
|
|
315
|
-
initial: true,
|
|
485
|
+
if (!apiKey) {
|
|
486
|
+
const res = await prompts({ type: "password", name: "apiKey", message: `${envKey}:` });
|
|
487
|
+
if (!res.apiKey)
|
|
488
|
+
return;
|
|
489
|
+
apiKey = res.apiKey;
|
|
490
|
+
}
|
|
491
|
+
if (!model) {
|
|
492
|
+
const res = await prompts({
|
|
493
|
+
type: "text",
|
|
494
|
+
name: "model",
|
|
495
|
+
message: "Model:",
|
|
496
|
+
initial: modelDefault,
|
|
316
497
|
});
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
execSync(`docker restart ${containerName}`, { stdio: "pipe", timeout: 30000 });
|
|
321
|
-
log.success("Restarted. Give it 30s to come back up.");
|
|
322
|
-
log.dim(`Check: synap openclaw`);
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
log.warn("Restart failed — restart manually:");
|
|
326
|
-
log.dim(` docker restart ${containerName}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
498
|
+
if (!res.model)
|
|
499
|
+
return;
|
|
500
|
+
model = res.model;
|
|
329
501
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
502
|
+
// ── Write via OpenClaw's own config system ──────────────────────────────
|
|
503
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
504
|
+
const spinner = ora("Writing OpenClaw config...").start();
|
|
505
|
+
try {
|
|
506
|
+
// Set the API key inside OpenClaw's env block
|
|
507
|
+
execSync(`docker exec ${containerName} openclaw config set env.${envKey} ${JSON.stringify(apiKey)}`, { stdio: "pipe", timeout: 15000 });
|
|
508
|
+
// Set the primary model
|
|
509
|
+
execSync(`docker exec ${containerName} openclaw config set agents.defaults.model.primary ${JSON.stringify(model)}`, { stdio: "pipe", timeout: 15000 });
|
|
510
|
+
spinner.succeed("Config written");
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
spinner.fail(`openclaw config set failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
514
|
+
log.dim("Make sure OpenClaw is running: synap openclaw");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// ── Restart to apply ─────────────────────────────────────────────────────
|
|
518
|
+
log.info("Restarting OpenClaw to apply...");
|
|
519
|
+
try {
|
|
520
|
+
execSync(`docker restart ${containerName}`, { stdio: "pipe", timeout: 30000 });
|
|
521
|
+
log.success("Restarted — give it ~30s to come back up");
|
|
522
|
+
log.dim("Check: synap openclaw");
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
log.warn("Restart failed — run manually: docker restart openclaw");
|
|
341
526
|
}
|
|
342
527
|
log.blank();
|
|
343
528
|
}
|
|
@@ -362,6 +547,138 @@ export function openclawLogs(opts) {
|
|
|
362
547
|
log.dim(`Try: docker logs ${containerName} --tail 50`);
|
|
363
548
|
}
|
|
364
549
|
}
|
|
550
|
+
// ─── Token: print the gateway token ──────────────────────────────────────────
|
|
551
|
+
export function openclawToken(opts) {
|
|
552
|
+
const oc = detectOpenClaw();
|
|
553
|
+
if (!oc.found) {
|
|
554
|
+
log.error("OpenClaw is not running.");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const token = readGatewayToken(oc);
|
|
558
|
+
if (!token) {
|
|
559
|
+
log.error("Could not read gateway token from OpenClaw.");
|
|
560
|
+
log.dim("Try: synap openclaw doctor");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (opts.for) {
|
|
564
|
+
// Print a pre-filled MCP client config with the token embedded
|
|
565
|
+
const client = opts.for.toLowerCase();
|
|
566
|
+
const gatewayPort = oc.gatewayPort ?? 18789;
|
|
567
|
+
const config = {
|
|
568
|
+
mcpServers: {
|
|
569
|
+
openclaw: {
|
|
570
|
+
command: "openclaw",
|
|
571
|
+
args: [
|
|
572
|
+
"mcp",
|
|
573
|
+
"serve",
|
|
574
|
+
"--url",
|
|
575
|
+
`ws://localhost:${gatewayPort}`,
|
|
576
|
+
"--token",
|
|
577
|
+
token,
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
const paths = {
|
|
583
|
+
claude: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
584
|
+
cursor: "~/.cursor/mcp.json",
|
|
585
|
+
windsurf: "~/.windsurf/mcp.json",
|
|
586
|
+
};
|
|
587
|
+
log.heading(client.charAt(0).toUpperCase() + client.slice(1));
|
|
588
|
+
if (paths[client])
|
|
589
|
+
log.dim(`Config file: ${paths[client]}`);
|
|
590
|
+
log.blank();
|
|
591
|
+
console.log(chalk.cyan(JSON.stringify(config, null, 2)));
|
|
592
|
+
log.blank();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (opts.copy) {
|
|
596
|
+
try {
|
|
597
|
+
const pbcopy = process.platform === "darwin"
|
|
598
|
+
? "pbcopy"
|
|
599
|
+
: process.platform === "linux"
|
|
600
|
+
? "xclip -selection clipboard"
|
|
601
|
+
: null;
|
|
602
|
+
if (pbcopy) {
|
|
603
|
+
execSync(`echo -n ${JSON.stringify(token)} | ${pbcopy}`, { stdio: "pipe" });
|
|
604
|
+
log.success("Token copied to clipboard");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
// fall through to print
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Plain print
|
|
613
|
+
console.log(token);
|
|
614
|
+
}
|
|
615
|
+
function readGatewayToken(oc) {
|
|
616
|
+
if (!oc.found)
|
|
617
|
+
return null;
|
|
618
|
+
if (oc.runtime === "docker") {
|
|
619
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
620
|
+
// Try OpenClaw's own config first — works even if token file path changes
|
|
621
|
+
try {
|
|
622
|
+
const raw = execSync(`docker exec ${containerName} openclaw config get gateway.token 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
623
|
+
if (raw && raw !== "undefined" && raw !== "null") {
|
|
624
|
+
return raw.replace(/^["']|["']$/g, "");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// fall through
|
|
629
|
+
}
|
|
630
|
+
// Fallback: read the token file directly
|
|
631
|
+
try {
|
|
632
|
+
const raw = execSync(`docker exec ${containerName} cat /root/.openclaw/gateway.token 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
633
|
+
return raw || null;
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Local install — read from host filesystem
|
|
640
|
+
try {
|
|
641
|
+
const tokenPath = `${process.env.HOME}/.openclaw/gateway.token`;
|
|
642
|
+
if (fs.existsSync(tokenPath)) {
|
|
643
|
+
return fs.readFileSync(tokenPath, "utf-8").trim();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// ignore
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
// ─── Doctor: run OpenClaw's own diagnostic ───────────────────────────────────
|
|
652
|
+
export function openclawDoctor(opts) {
|
|
653
|
+
const oc = detectOpenClaw();
|
|
654
|
+
if (!oc.found) {
|
|
655
|
+
log.error("OpenClaw is not running.");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const fixFlag = opts.fix ? " --fix" : "";
|
|
659
|
+
if (oc.runtime === "docker") {
|
|
660
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
661
|
+
log.dim(`Running: docker exec ${containerName} openclaw doctor${fixFlag}`);
|
|
662
|
+
log.blank();
|
|
663
|
+
try {
|
|
664
|
+
execSync(`docker exec ${containerName} openclaw doctor${fixFlag}`, {
|
|
665
|
+
stdio: "inherit",
|
|
666
|
+
timeout: 60000,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
log.warn("openclaw doctor reported issues or failed");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
try {
|
|
675
|
+
execSync(`openclaw doctor${fixFlag}`, { stdio: "inherit", timeout: 60000 });
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
log.warn("openclaw doctor reported issues or failed");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
365
682
|
// ─── Restart ─────────────────────────────────────────────────────────────────
|
|
366
683
|
export async function openclawRestart() {
|
|
367
684
|
const oc = detectOpenClaw();
|
|
@@ -388,53 +705,22 @@ export async function openclawRestart() {
|
|
|
388
705
|
log.dim(`Try: docker restart ${containerName}`);
|
|
389
706
|
}
|
|
390
707
|
}
|
|
391
|
-
|
|
392
|
-
|
|
708
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
709
|
+
function getOpenClawPublicUrl() {
|
|
393
710
|
const deployDir = findSynapDeployDir();
|
|
394
|
-
if (deployDir)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return { configured: true, provider: "OpenAI", model: vars.OPENCLAW_MODEL };
|
|
404
|
-
}
|
|
405
|
-
if (vars.GEMINI_API_KEY) {
|
|
406
|
-
return { configured: true, provider: "Google", model: vars.OPENCLAW_MODEL };
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
catch {
|
|
410
|
-
// unreadable
|
|
411
|
-
}
|
|
711
|
+
if (!deployDir)
|
|
712
|
+
return null;
|
|
713
|
+
try {
|
|
714
|
+
const content = fs.readFileSync(`${deployDir}/.env`, "utf-8");
|
|
715
|
+
const match = content.match(/^OPENCLAW_DOMAIN=(.+)$/m);
|
|
716
|
+
const domain = match?.[1]?.trim();
|
|
717
|
+
if (!domain || domain === "disabled.invalid" || domain === "")
|
|
718
|
+
return null;
|
|
719
|
+
return `https://${domain}`;
|
|
412
720
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (oc.runtime === "docker") {
|
|
416
|
-
try {
|
|
417
|
-
const containerName = oc.containerName ?? "openclaw";
|
|
418
|
-
const raw = execSync(`docker inspect --format '{{range .Config.Env}}{{.}}\\n{{end}}' ${containerName} 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
|
|
419
|
-
const envLines = raw.split("\\n").filter(Boolean);
|
|
420
|
-
const env = {};
|
|
421
|
-
for (const line of envLines) {
|
|
422
|
-
const idx = line.indexOf("=");
|
|
423
|
-
if (idx > 0)
|
|
424
|
-
env[line.slice(0, idx)] = line.slice(idx + 1);
|
|
425
|
-
}
|
|
426
|
-
if (env.ANTHROPIC_API_KEY)
|
|
427
|
-
return { configured: true, provider: "Anthropic", model: env.OPENCLAW_MODEL };
|
|
428
|
-
if (env.OPENAI_API_KEY)
|
|
429
|
-
return { configured: true, provider: "OpenAI", model: env.OPENCLAW_MODEL };
|
|
430
|
-
if (env.GEMINI_API_KEY)
|
|
431
|
-
return { configured: true, provider: "Google", model: env.OPENCLAW_MODEL };
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
// docker not available
|
|
435
|
-
}
|
|
721
|
+
catch {
|
|
722
|
+
return null;
|
|
436
723
|
}
|
|
437
|
-
return { configured: false };
|
|
438
724
|
}
|
|
439
725
|
function checkSkillInstalled(oc) {
|
|
440
726
|
if (!oc.found)
|
|
@@ -469,19 +755,117 @@ function writeEnvVar(envFile, key, value) {
|
|
|
469
755
|
: content + "\n" + line + "\n";
|
|
470
756
|
fs.writeFileSync(envFile, content, { mode: 0o600 });
|
|
471
757
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return
|
|
758
|
+
// ─── Domain setup helpers ────────────────────────────────────────────────────
|
|
759
|
+
function readEnvVar(deployDir, key) {
|
|
760
|
+
try {
|
|
761
|
+
const content = fs.readFileSync(path.join(deployDir, ".env"), "utf-8");
|
|
762
|
+
const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
763
|
+
return match?.[1]?.trim() ?? null;
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function generatePassword(length) {
|
|
770
|
+
// URL-safe random password
|
|
771
|
+
return crypto
|
|
772
|
+
.randomBytes(length)
|
|
773
|
+
.toString("base64url")
|
|
774
|
+
.slice(0, length);
|
|
775
|
+
}
|
|
776
|
+
async function bcryptHashViaCaddy(plaintext) {
|
|
777
|
+
// Caddy ships `caddy hash-password` which outputs a bcrypt hash.
|
|
778
|
+
// Run it via the running caddy container so we don't need a bcrypt dep in Node.
|
|
779
|
+
try {
|
|
780
|
+
const hash = execSync(`docker exec -i caddy caddy hash-password --plaintext ${JSON.stringify(plaintext)}`, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
781
|
+
return hash;
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
// Fallback: try without container (if caddy is in PATH)
|
|
785
|
+
try {
|
|
786
|
+
return execSync(`caddy hash-password --plaintext ${JSON.stringify(plaintext)}`, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
throw new Error(`Could not hash password via caddy: ${err instanceof Error ? err.message : String(err)}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function generateCpOAuthSnippet() {
|
|
794
|
+
return `# OpenClaw dashboard auth — CP OAuth (generated by synap openclaw setup-domain)
|
|
795
|
+
#
|
|
796
|
+
# Validates the Better Auth session cookie against the Synap Control Plane.
|
|
797
|
+
# Because the CP cookie is set with Domain=.synap.live (crossSubDomainCookies),
|
|
798
|
+
# the browser sends it automatically to this subdomain.
|
|
799
|
+
#
|
|
800
|
+
# 200 → authenticated, pass through to openclaw
|
|
801
|
+
# 401 → redirect to synap.live login
|
|
802
|
+
|
|
803
|
+
forward_auth https://api.synap.live {
|
|
804
|
+
uri /api/auth/me
|
|
805
|
+
copy_headers X-Authenticated-User
|
|
806
|
+
|
|
807
|
+
@unauthorized status 401
|
|
808
|
+
handle_response @unauthorized {
|
|
809
|
+
redir https://synap.live/login?redirect=https://{host}{uri} temporary
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
function generateBasicAuthSnippet(username, bcryptHash) {
|
|
815
|
+
return `# OpenClaw dashboard auth — basic auth (generated by synap openclaw setup-domain)
|
|
816
|
+
#
|
|
817
|
+
# Single credential protects the dashboard. The password hash below was
|
|
818
|
+
# generated with \`caddy hash-password\`. To rotate, run:
|
|
819
|
+
# synap openclaw setup-domain
|
|
820
|
+
|
|
821
|
+
basicauth {
|
|
822
|
+
${username} ${bcryptHash}
|
|
823
|
+
}
|
|
824
|
+
`;
|
|
825
|
+
}
|
|
826
|
+
function readOpenClawAiConfig(oc) {
|
|
827
|
+
if (!oc.found || oc.runtime !== "docker")
|
|
828
|
+
return {};
|
|
829
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
830
|
+
const read = (key) => {
|
|
831
|
+
try {
|
|
832
|
+
const out = execSync(`docker exec ${containerName} openclaw config get ${key} 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
833
|
+
if (!out || out === "undefined" || out === "null")
|
|
834
|
+
return undefined;
|
|
835
|
+
return out.replace(/^["']|["']$/g, "");
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
return {
|
|
842
|
+
anthropicKey: read("env.ANTHROPIC_API_KEY"),
|
|
843
|
+
openaiKey: read("env.OPENAI_API_KEY"),
|
|
844
|
+
geminiKey: read("env.GEMINI_API_KEY"),
|
|
845
|
+
primaryModel: read("agents.defaults.model.primary"),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function maskKey(key) {
|
|
849
|
+
if (key.length <= 8)
|
|
850
|
+
return "•".repeat(key.length);
|
|
851
|
+
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
852
|
+
}
|
|
853
|
+
async function requestDashboardDomainFromCp(cpToken, podId) {
|
|
854
|
+
const cpUrl = process.env.SYNAP_CP_URL ?? "https://api.synap.live";
|
|
855
|
+
const res = await fetch(`${cpUrl}/openclaw/expose-dashboard`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
headers: {
|
|
858
|
+
"Content-Type": "application/json",
|
|
859
|
+
Authorization: `Bearer ${cpToken}`,
|
|
860
|
+
},
|
|
861
|
+
body: JSON.stringify({ podId }),
|
|
862
|
+
signal: AbortSignal.timeout(20000),
|
|
863
|
+
});
|
|
864
|
+
if (!res.ok) {
|
|
865
|
+
const body = (await res.json().catch(() => null));
|
|
866
|
+
throw new Error(`CP request failed (HTTP ${res.status}): ${body?.error ?? "unknown error"}`);
|
|
867
|
+
}
|
|
868
|
+
const data = (await res.json());
|
|
869
|
+
return { domain: data.domain, authMode: data.authMode };
|
|
486
870
|
}
|
|
487
871
|
//# sourceMappingURL=openclaw.js.map
|