@synap-core/cli 1.1.0 → 1.2.1
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 +169 -41
- package/dist/commands/finish.js.map +1 -1
- package/dist/commands/openclaw.d.ts +15 -1
- package/dist/commands/openclaw.js +367 -176
- package/dist/commands/openclaw.js.map +1 -1
- package/dist/index.js +28 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -50,10 +50,18 @@ export async function openclawOverview() {
|
|
|
50
50
|
log.dim(`Version: ${oc.version}`);
|
|
51
51
|
// ── AI provider ─────────────────────────────────────────────────────────
|
|
52
52
|
log.heading("AI Provider");
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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}`);
|
|
57
65
|
}
|
|
58
66
|
else {
|
|
59
67
|
log.warn("No AI API key configured — OpenClaw cannot process requests");
|
|
@@ -134,37 +142,49 @@ export async function openclawConnect(opts) {
|
|
|
134
142
|
const gatewayPort = oc.gatewayPort ?? 18789;
|
|
135
143
|
const isDocker = oc.runtime === "docker";
|
|
136
144
|
// OpenClaw MCP is stdio-based — clients run `openclaw mcp serve` as a local process
|
|
137
|
-
// which connects to the gateway over WebSocket.
|
|
138
|
-
//
|
|
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
|
+
}
|
|
139
154
|
const client = opts.client?.toLowerCase();
|
|
140
155
|
if (!client || client === "claude") {
|
|
141
|
-
printMcpConfig("Claude Desktop", gatewayPort, isDocker, "claude");
|
|
156
|
+
printMcpConfig("Claude Desktop", gatewayPort, isDocker, "claude", token);
|
|
142
157
|
}
|
|
143
158
|
if (!client || client === "cursor") {
|
|
144
|
-
printMcpConfig("Cursor", gatewayPort, isDocker, "cursor");
|
|
159
|
+
printMcpConfig("Cursor", gatewayPort, isDocker, "cursor", token);
|
|
145
160
|
}
|
|
146
161
|
if (!client || client === "windsurf") {
|
|
147
|
-
printMcpConfig("Windsurf", gatewayPort, isDocker, "windsurf");
|
|
162
|
+
printMcpConfig("Windsurf", gatewayPort, isDocker, "windsurf", token);
|
|
148
163
|
}
|
|
149
164
|
if (client && !["claude", "cursor", "windsurf"].includes(client)) {
|
|
150
165
|
log.warn(`Unknown client "${client}". Showing generic config.`);
|
|
151
|
-
printMcpConfig("MCP Client", gatewayPort, isDocker, "generic");
|
|
166
|
+
printMcpConfig("MCP Client", gatewayPort, isDocker, "generic", token);
|
|
152
167
|
}
|
|
153
168
|
if (isDocker) {
|
|
154
169
|
log.blank();
|
|
155
170
|
log.info("Remote server? Tunnel the gateway port first:");
|
|
156
|
-
log.dim(
|
|
157
|
-
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)");
|
|
158
173
|
log.blank();
|
|
159
|
-
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");
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
|
-
function printMcpConfig(label, gatewayPort, isRemote, client) {
|
|
178
|
+
function printMcpConfig(label, gatewayPort, isRemote, client, token) {
|
|
163
179
|
log.heading(label);
|
|
164
180
|
// MCP config: stdio command that connects to the local (or tunneled) gateway
|
|
165
|
-
const args =
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
}
|
|
168
188
|
const config = JSON.stringify({ mcpServers: { openclaw: { command: "openclaw", args } } }, null, 2);
|
|
169
189
|
const paths = {
|
|
170
190
|
claude: "macOS: ~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
@@ -390,52 +410,68 @@ export async function openclawSetupDomain() {
|
|
|
390
410
|
log.dim("This takes ~30s the first time.");
|
|
391
411
|
log.blank();
|
|
392
412
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// Show current status
|
|
399
|
-
const current = getAiKeyStatus();
|
|
400
|
-
if (current.configured) {
|
|
401
|
-
log.success(`Currently using: ${current.provider}`);
|
|
402
|
-
log.dim(`Model: ${current.model ?? "default"}`);
|
|
403
|
-
log.blank();
|
|
413
|
+
export async function openclawConfigure(opts = {}) {
|
|
414
|
+
const oc = detectOpenClaw();
|
|
415
|
+
if (!oc.found) {
|
|
416
|
+
log.error("OpenClaw is not running.");
|
|
417
|
+
return;
|
|
404
418
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
{
|
|
411
|
-
title: "Anthropic (Claude)",
|
|
412
|
-
description: "claude-sonnet-4-6 — best quality, recommended",
|
|
413
|
-
value: "anthropic",
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
title: "OpenAI (GPT-4o)",
|
|
417
|
-
value: "openai",
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
title: "Google (Gemini)",
|
|
421
|
-
value: "google",
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
title: "Synap IS (via pod)",
|
|
425
|
-
description: "Uses your pod AI — no external key needed",
|
|
426
|
-
value: "synap",
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
});
|
|
430
|
-
if (!provider)
|
|
419
|
+
// Verify openclaw binary is actually callable inside the container before
|
|
420
|
+
// we start writing config. Protects against "container up but openclaw not ready".
|
|
421
|
+
if (oc.runtime === "docker" && !verifyOpenclawCliReady(oc.containerName ?? "openclaw")) {
|
|
422
|
+
log.error("OpenClaw container is running, but the openclaw CLI isn't responding.");
|
|
423
|
+
log.dim("It may still be initializing. Wait 30s and try again, or run: synap openclaw doctor");
|
|
431
424
|
return;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
log.
|
|
436
|
-
|
|
425
|
+
}
|
|
426
|
+
// ── Show current config ─────────────────────────────────────────────────
|
|
427
|
+
if (opts.show) {
|
|
428
|
+
log.heading("OpenClaw AI Config");
|
|
429
|
+
const current = readOpenClawAiConfig(oc);
|
|
430
|
+
if (current.anthropicKey)
|
|
431
|
+
log.success(`Anthropic: ${maskKey(current.anthropicKey)}`);
|
|
432
|
+
if (current.openaiKey)
|
|
433
|
+
log.success(`OpenAI: ${maskKey(current.openaiKey)}`);
|
|
434
|
+
if (current.geminiKey)
|
|
435
|
+
log.success(`Google: ${maskKey(current.geminiKey)}`);
|
|
436
|
+
if (current.primaryModel)
|
|
437
|
+
log.info(`Model: ${current.primaryModel}`);
|
|
438
|
+
if (!current.anthropicKey && !current.openaiKey && !current.geminiKey) {
|
|
439
|
+
log.warn("No AI provider key configured");
|
|
440
|
+
}
|
|
437
441
|
return;
|
|
438
442
|
}
|
|
443
|
+
// ── Interactive (delegate to OpenClaw's own wizard) ──────────────────────
|
|
444
|
+
if (opts.interactive) {
|
|
445
|
+
handoffToOpenClawWizard(oc);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
banner();
|
|
449
|
+
log.heading("Configure AI Provider");
|
|
450
|
+
// ── Scripted path (--provider + --key) ───────────────────────────────────
|
|
451
|
+
let provider = opts.provider;
|
|
452
|
+
let apiKey = opts.key;
|
|
453
|
+
let model = opts.model;
|
|
454
|
+
if (!provider) {
|
|
455
|
+
const pick = await prompts({
|
|
456
|
+
type: "select",
|
|
457
|
+
name: "provider",
|
|
458
|
+
message: "Which AI provider?",
|
|
459
|
+
choices: [
|
|
460
|
+
{ title: "Anthropic (Claude)", description: "recommended", value: "anthropic" },
|
|
461
|
+
{ title: "OpenAI (GPT-4o)", value: "openai" },
|
|
462
|
+
{ title: "Google (Gemini)", value: "google" },
|
|
463
|
+
{ title: "Run OpenClaw's own wizard", description: "interactive", value: "wizard" },
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
if (!pick.provider)
|
|
467
|
+
return;
|
|
468
|
+
// Inline handoff — don't recurse, we already have `oc` in scope
|
|
469
|
+
if (pick.provider === "wizard") {
|
|
470
|
+
handoffToOpenClawWizard(oc);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
provider = pick.provider;
|
|
474
|
+
}
|
|
439
475
|
const envKey = provider === "anthropic"
|
|
440
476
|
? "ANTHROPIC_API_KEY"
|
|
441
477
|
: provider === "openai"
|
|
@@ -446,66 +482,125 @@ export async function openclawConfigure() {
|
|
|
446
482
|
: provider === "openai"
|
|
447
483
|
? "openai/gpt-4o"
|
|
448
484
|
: "google/gemini-2.0-flash";
|
|
449
|
-
|
|
450
|
-
type: "password",
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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,
|
|
497
|
+
});
|
|
498
|
+
if (!res.model)
|
|
499
|
+
return;
|
|
500
|
+
model = res.model;
|
|
501
|
+
}
|
|
502
|
+
// Final narrowing — both are guaranteed strings by this point
|
|
503
|
+
const finalKey = apiKey;
|
|
504
|
+
const finalModel = model;
|
|
505
|
+
// ── Write via OpenClaw's own config system ──────────────────────────────
|
|
506
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
507
|
+
// Step 1: API key — MUST succeed
|
|
508
|
+
const keySpinner = ora(`Setting env.${envKey}...`).start();
|
|
509
|
+
const keyResult = runOpenClawConfigSet(containerName, `env.${envKey}`, finalKey);
|
|
510
|
+
if (!keyResult.ok) {
|
|
511
|
+
keySpinner.fail(`Failed to set env.${envKey}`);
|
|
512
|
+
log.dim(keyResult.stderr || keyResult.error || "(no output)");
|
|
513
|
+
log.dim("Diagnose: synap openclaw doctor");
|
|
455
514
|
return;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
515
|
+
}
|
|
516
|
+
keySpinner.succeed(`Set env.${envKey}`);
|
|
517
|
+
// Step 2: Model — non-fatal (schema path may differ across OpenClaw versions)
|
|
518
|
+
const modelSpinner = ora(`Setting default model to ${finalModel}...`).start();
|
|
519
|
+
const modelPaths = [
|
|
520
|
+
"agents.defaults.model.primary",
|
|
521
|
+
"models.default",
|
|
522
|
+
"agent.model",
|
|
523
|
+
];
|
|
524
|
+
let modelSet = false;
|
|
525
|
+
let modelError = "";
|
|
526
|
+
for (const modelPath of modelPaths) {
|
|
527
|
+
const r = runOpenClawConfigSet(containerName, modelPath, finalModel);
|
|
528
|
+
if (r.ok) {
|
|
529
|
+
modelSpinner.succeed(`Set ${modelPath} = ${finalModel}`);
|
|
530
|
+
modelSet = true;
|
|
531
|
+
break;
|
|
471
532
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
533
|
+
modelError = r.stderr || r.error || modelError;
|
|
534
|
+
}
|
|
535
|
+
if (!modelSet) {
|
|
536
|
+
modelSpinner.warn(`Couldn't set model automatically — set it manually`);
|
|
537
|
+
log.dim(`Tried: ${modelPaths.join(", ")}`);
|
|
538
|
+
if (modelError)
|
|
539
|
+
log.dim(`Last error: ${modelError.split("\n")[0]}`);
|
|
540
|
+
log.dim(`Run: docker exec -it ${containerName} openclaw configure`);
|
|
541
|
+
}
|
|
542
|
+
// ── Restart to apply ─────────────────────────────────────────────────────
|
|
543
|
+
log.info("Restarting OpenClaw to apply...");
|
|
544
|
+
try {
|
|
545
|
+
execSync(`docker restart ${containerName}`, { stdio: "pipe", timeout: 30000 });
|
|
546
|
+
log.success("Restarted — give it ~30s to come back up");
|
|
547
|
+
log.dim("Check: synap openclaw");
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
log.warn("Restart failed — run manually: docker restart openclaw");
|
|
551
|
+
}
|
|
552
|
+
log.blank();
|
|
553
|
+
}
|
|
554
|
+
// ─── Configure helpers ───────────────────────────────────────────────────────
|
|
555
|
+
function verifyOpenclawCliReady(containerName) {
|
|
556
|
+
try {
|
|
557
|
+
execSync(`docker exec ${containerName} openclaw --version`, {
|
|
558
|
+
stdio: "pipe",
|
|
559
|
+
timeout: 8000,
|
|
482
560
|
});
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
log.info(`Restarting ${containerName}...`);
|
|
486
|
-
execSync(`docker restart ${containerName}`, { stdio: "pipe", timeout: 30000 });
|
|
487
|
-
log.success("Restarted. Give it 30s to come back up.");
|
|
488
|
-
log.dim(`Check: synap openclaw`);
|
|
489
|
-
}
|
|
490
|
-
catch {
|
|
491
|
-
log.warn("Restart failed — restart manually:");
|
|
492
|
-
log.dim(` docker restart ${containerName}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
561
|
+
return true;
|
|
495
562
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
log.blank();
|
|
499
|
-
log.warn("Could not find deploy directory. Set the key manually:");
|
|
500
|
-
log.blank();
|
|
501
|
-
console.log(chalk.cyan(` export ${envKey}="${apiKey}"`));
|
|
502
|
-
if (model) {
|
|
503
|
-
console.log(chalk.cyan(` export OPENCLAW_MODEL="${model}"`));
|
|
504
|
-
}
|
|
505
|
-
log.blank();
|
|
506
|
-
log.dim("Or add to your pod .env file, then: docker restart openclaw");
|
|
563
|
+
catch {
|
|
564
|
+
return false;
|
|
507
565
|
}
|
|
566
|
+
}
|
|
567
|
+
function handoffToOpenClawWizard(oc) {
|
|
568
|
+
if (oc.runtime !== "docker") {
|
|
569
|
+
log.error("Interactive wizard only works for Docker runtime.");
|
|
570
|
+
log.dim("For local install, run: openclaw configure");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
574
|
+
log.heading("Handing off to OpenClaw");
|
|
575
|
+
log.dim(`Running: docker exec -it ${containerName} openclaw configure`);
|
|
508
576
|
log.blank();
|
|
577
|
+
try {
|
|
578
|
+
// stdio: inherit passes our TTY through to docker exec.
|
|
579
|
+
// If we're not on a TTY (e.g. piped input), docker exec -it will fail — catch it.
|
|
580
|
+
execSync(`docker exec -it ${containerName} openclaw configure`, { stdio: "inherit" });
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
584
|
+
log.error(`openclaw configure failed: ${msg}`);
|
|
585
|
+
log.dim(`Run directly: docker exec -it ${containerName} openclaw configure`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function runOpenClawConfigSet(containerName, key, value) {
|
|
589
|
+
try {
|
|
590
|
+
// Use shell quoting via JSON.stringify — handles quotes + special chars safely.
|
|
591
|
+
// stdio: pipe so we can capture stderr on failure.
|
|
592
|
+
execSync(`docker exec ${containerName} openclaw config set ${key} ${JSON.stringify(value)}`, { stdio: "pipe", timeout: 15000, encoding: "utf-8" });
|
|
593
|
+
return { ok: true };
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
const e = err;
|
|
597
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString("utf-8");
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
stderr: stderr?.trim(),
|
|
601
|
+
error: e.message,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
509
604
|
}
|
|
510
605
|
// ─── Logs: tail container output ─────────────────────────────────────────────
|
|
511
606
|
export function openclawLogs(opts) {
|
|
@@ -528,6 +623,138 @@ export function openclawLogs(opts) {
|
|
|
528
623
|
log.dim(`Try: docker logs ${containerName} --tail 50`);
|
|
529
624
|
}
|
|
530
625
|
}
|
|
626
|
+
// ─── Token: print the gateway token ──────────────────────────────────────────
|
|
627
|
+
export function openclawToken(opts) {
|
|
628
|
+
const oc = detectOpenClaw();
|
|
629
|
+
if (!oc.found) {
|
|
630
|
+
log.error("OpenClaw is not running.");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const token = readGatewayToken(oc);
|
|
634
|
+
if (!token) {
|
|
635
|
+
log.error("Could not read gateway token from OpenClaw.");
|
|
636
|
+
log.dim("Try: synap openclaw doctor");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (opts.for) {
|
|
640
|
+
// Print a pre-filled MCP client config with the token embedded
|
|
641
|
+
const client = opts.for.toLowerCase();
|
|
642
|
+
const gatewayPort = oc.gatewayPort ?? 18789;
|
|
643
|
+
const config = {
|
|
644
|
+
mcpServers: {
|
|
645
|
+
openclaw: {
|
|
646
|
+
command: "openclaw",
|
|
647
|
+
args: [
|
|
648
|
+
"mcp",
|
|
649
|
+
"serve",
|
|
650
|
+
"--url",
|
|
651
|
+
`ws://localhost:${gatewayPort}`,
|
|
652
|
+
"--token",
|
|
653
|
+
token,
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
const paths = {
|
|
659
|
+
claude: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
660
|
+
cursor: "~/.cursor/mcp.json",
|
|
661
|
+
windsurf: "~/.windsurf/mcp.json",
|
|
662
|
+
};
|
|
663
|
+
log.heading(client.charAt(0).toUpperCase() + client.slice(1));
|
|
664
|
+
if (paths[client])
|
|
665
|
+
log.dim(`Config file: ${paths[client]}`);
|
|
666
|
+
log.blank();
|
|
667
|
+
console.log(chalk.cyan(JSON.stringify(config, null, 2)));
|
|
668
|
+
log.blank();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (opts.copy) {
|
|
672
|
+
try {
|
|
673
|
+
const pbcopy = process.platform === "darwin"
|
|
674
|
+
? "pbcopy"
|
|
675
|
+
: process.platform === "linux"
|
|
676
|
+
? "xclip -selection clipboard"
|
|
677
|
+
: null;
|
|
678
|
+
if (pbcopy) {
|
|
679
|
+
execSync(`echo -n ${JSON.stringify(token)} | ${pbcopy}`, { stdio: "pipe" });
|
|
680
|
+
log.success("Token copied to clipboard");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// fall through to print
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Plain print
|
|
689
|
+
console.log(token);
|
|
690
|
+
}
|
|
691
|
+
function readGatewayToken(oc) {
|
|
692
|
+
if (!oc.found)
|
|
693
|
+
return null;
|
|
694
|
+
if (oc.runtime === "docker") {
|
|
695
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
696
|
+
// Try OpenClaw's own config first — works even if token file path changes
|
|
697
|
+
try {
|
|
698
|
+
const raw = execSync(`docker exec ${containerName} openclaw config get gateway.token 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
699
|
+
if (raw && raw !== "undefined" && raw !== "null") {
|
|
700
|
+
return raw.replace(/^["']|["']$/g, "");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
// fall through
|
|
705
|
+
}
|
|
706
|
+
// Fallback: read the token file directly
|
|
707
|
+
try {
|
|
708
|
+
const raw = execSync(`docker exec ${containerName} cat /root/.openclaw/gateway.token 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
709
|
+
return raw || null;
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Local install — read from host filesystem
|
|
716
|
+
try {
|
|
717
|
+
const tokenPath = `${process.env.HOME}/.openclaw/gateway.token`;
|
|
718
|
+
if (fs.existsSync(tokenPath)) {
|
|
719
|
+
return fs.readFileSync(tokenPath, "utf-8").trim();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// ignore
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
// ─── Doctor: run OpenClaw's own diagnostic ───────────────────────────────────
|
|
728
|
+
export function openclawDoctor(opts) {
|
|
729
|
+
const oc = detectOpenClaw();
|
|
730
|
+
if (!oc.found) {
|
|
731
|
+
log.error("OpenClaw is not running.");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const fixFlag = opts.fix ? " --fix" : "";
|
|
735
|
+
if (oc.runtime === "docker") {
|
|
736
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
737
|
+
log.dim(`Running: docker exec ${containerName} openclaw doctor${fixFlag}`);
|
|
738
|
+
log.blank();
|
|
739
|
+
try {
|
|
740
|
+
execSync(`docker exec ${containerName} openclaw doctor${fixFlag}`, {
|
|
741
|
+
stdio: "inherit",
|
|
742
|
+
timeout: 60000,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
log.warn("openclaw doctor reported issues or failed");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
try {
|
|
751
|
+
execSync(`openclaw doctor${fixFlag}`, { stdio: "inherit", timeout: 60000 });
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
log.warn("openclaw doctor reported issues or failed");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
531
758
|
// ─── Restart ─────────────────────────────────────────────────────────────────
|
|
532
759
|
export async function openclawRestart() {
|
|
533
760
|
const oc = detectOpenClaw();
|
|
@@ -571,54 +798,6 @@ function getOpenClawPublicUrl() {
|
|
|
571
798
|
return null;
|
|
572
799
|
}
|
|
573
800
|
}
|
|
574
|
-
function getAiKeyStatus() {
|
|
575
|
-
// Check deploy dir .env first (most accurate for Docker deployments)
|
|
576
|
-
const deployDir = findSynapDeployDir();
|
|
577
|
-
if (deployDir) {
|
|
578
|
-
const envFile = `${deployDir}/.env`;
|
|
579
|
-
try {
|
|
580
|
-
const envContent = fs.readFileSync(envFile, "utf-8");
|
|
581
|
-
const vars = parseEnvFile(envContent);
|
|
582
|
-
if (vars.ANTHROPIC_API_KEY) {
|
|
583
|
-
return { configured: true, provider: "Anthropic", model: vars.OPENCLAW_MODEL };
|
|
584
|
-
}
|
|
585
|
-
if (vars.OPENAI_API_KEY) {
|
|
586
|
-
return { configured: true, provider: "OpenAI", model: vars.OPENCLAW_MODEL };
|
|
587
|
-
}
|
|
588
|
-
if (vars.GEMINI_API_KEY) {
|
|
589
|
-
return { configured: true, provider: "Google", model: vars.OPENCLAW_MODEL };
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
catch {
|
|
593
|
-
// unreadable
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// Fallback: check live container env (via docker inspect)
|
|
597
|
-
const oc = detectOpenClaw();
|
|
598
|
-
if (oc.runtime === "docker") {
|
|
599
|
-
try {
|
|
600
|
-
const containerName = oc.containerName ?? "openclaw";
|
|
601
|
-
const raw = execSync(`docker inspect --format '{{range .Config.Env}}{{.}}\\n{{end}}' ${containerName} 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
|
|
602
|
-
const envLines = raw.split("\\n").filter(Boolean);
|
|
603
|
-
const env = {};
|
|
604
|
-
for (const line of envLines) {
|
|
605
|
-
const idx = line.indexOf("=");
|
|
606
|
-
if (idx > 0)
|
|
607
|
-
env[line.slice(0, idx)] = line.slice(idx + 1);
|
|
608
|
-
}
|
|
609
|
-
if (env.ANTHROPIC_API_KEY)
|
|
610
|
-
return { configured: true, provider: "Anthropic", model: env.OPENCLAW_MODEL };
|
|
611
|
-
if (env.OPENAI_API_KEY)
|
|
612
|
-
return { configured: true, provider: "OpenAI", model: env.OPENCLAW_MODEL };
|
|
613
|
-
if (env.GEMINI_API_KEY)
|
|
614
|
-
return { configured: true, provider: "Google", model: env.OPENCLAW_MODEL };
|
|
615
|
-
}
|
|
616
|
-
catch {
|
|
617
|
-
// docker not available
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return { configured: false };
|
|
621
|
-
}
|
|
622
801
|
function checkSkillInstalled(oc) {
|
|
623
802
|
if (!oc.found)
|
|
624
803
|
return false;
|
|
@@ -652,21 +831,6 @@ function writeEnvVar(envFile, key, value) {
|
|
|
652
831
|
: content + "\n" + line + "\n";
|
|
653
832
|
fs.writeFileSync(envFile, content, { mode: 0o600 });
|
|
654
833
|
}
|
|
655
|
-
function parseEnvFile(content) {
|
|
656
|
-
const result = {};
|
|
657
|
-
for (const line of content.split("\n")) {
|
|
658
|
-
const trimmed = line.trim();
|
|
659
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
660
|
-
continue;
|
|
661
|
-
const idx = trimmed.indexOf("=");
|
|
662
|
-
if (idx > 0) {
|
|
663
|
-
const key = trimmed.slice(0, idx);
|
|
664
|
-
const val = trimmed.slice(idx + 1).replace(/^["']|["']$/g, "");
|
|
665
|
-
result[key] = val;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
return result;
|
|
669
|
-
}
|
|
670
834
|
// ─── Domain setup helpers ────────────────────────────────────────────────────
|
|
671
835
|
function readEnvVar(deployDir, key) {
|
|
672
836
|
try {
|
|
@@ -735,6 +899,33 @@ basicauth {
|
|
|
735
899
|
}
|
|
736
900
|
`;
|
|
737
901
|
}
|
|
902
|
+
function readOpenClawAiConfig(oc) {
|
|
903
|
+
if (!oc.found || oc.runtime !== "docker")
|
|
904
|
+
return {};
|
|
905
|
+
const containerName = oc.containerName ?? "openclaw";
|
|
906
|
+
const read = (key) => {
|
|
907
|
+
try {
|
|
908
|
+
const out = execSync(`docker exec ${containerName} openclaw config get ${key} 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
909
|
+
if (!out || out === "undefined" || out === "null")
|
|
910
|
+
return undefined;
|
|
911
|
+
return out.replace(/^["']|["']$/g, "");
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
return {
|
|
918
|
+
anthropicKey: read("env.ANTHROPIC_API_KEY"),
|
|
919
|
+
openaiKey: read("env.OPENAI_API_KEY"),
|
|
920
|
+
geminiKey: read("env.GEMINI_API_KEY"),
|
|
921
|
+
primaryModel: read("agents.defaults.model.primary"),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function maskKey(key) {
|
|
925
|
+
if (key.length <= 8)
|
|
926
|
+
return "•".repeat(key.length);
|
|
927
|
+
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
|
928
|
+
}
|
|
738
929
|
async function requestDashboardDomainFromCp(cpToken, podId) {
|
|
739
930
|
const cpUrl = process.env.SYNAP_CP_URL ?? "https://api.synap.live";
|
|
740
931
|
const res = await fetch(`${cpUrl}/openclaw/expose-dashboard`, {
|