@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.
@@ -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 apiKeyStatus = getAiKeyStatus();
54
- if (apiKeyStatus.configured) {
55
- log.success(`${apiKeyStatus.provider} key configured`);
56
- log.dim(`Model: ${apiKeyStatus.model ?? "default"}`);
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
- // For Docker/remote setups the gateway is at ws://localhost:<port> after SSH tunnel.
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(" ssh -N -L 18789:localhost:18789 user@your-server");
157
- log.dim(" Then use the configs above (they point to localhost:18789)");
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: npm i -g openclaw");
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 = isRemote
166
- ? ["mcp", "serve", "--url", `ws://localhost:${gatewayPort}`]
167
- : ["mcp", "serve"];
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
- // ─── Configure: set AI provider key ──────────────────────────────────────────
394
- export async function openclawConfigure() {
395
- banner();
396
- log.heading("Configure AI Provider");
397
- const deployDir = findSynapDeployDir();
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
- const { provider } = await prompts({
406
- type: "select",
407
- name: "provider",
408
- message: "Which AI provider?",
409
- choices: [
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
- if (provider === "synap") {
433
- log.blank();
434
- log.info("Synap IS uses your pod's AI subscription.");
435
- log.dim("Make sure IS is provisioned: synap finish");
436
- log.dim("Then OpenClaw will use your pod URL as the AI endpoint.");
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
- const { apiKey } = await prompts({
450
- type: "password",
451
- name: "apiKey",
452
- message: `${envKey}:`,
453
- });
454
- if (!apiKey)
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
- const { model } = await prompts({
457
- type: "text",
458
- name: "model",
459
- message: "Model (leave blank for default):",
460
- initial: modelDefault,
461
- });
462
- if (deployDir) {
463
- // Write to .env file
464
- const envFile = `${deployDir}/.env`;
465
- writeEnvVar(envFile, envKey, apiKey);
466
- if (model && model !== modelDefault) {
467
- writeEnvVar(envFile, "OPENCLAW_MODEL", model);
468
- }
469
- else {
470
- writeEnvVar(envFile, "OPENCLAW_MODEL", modelDefault);
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
- log.blank();
473
- log.success(`${envKey} written to ${deployDir}/.env`);
474
- // Restart container to pick up new env vars
475
- const oc = detectOpenClaw();
476
- const containerName = oc.containerName ?? "openclaw";
477
- const { doRestart } = await prompts({
478
- type: "confirm",
479
- name: "doRestart",
480
- message: `Restart ${containerName} to apply?`,
481
- initial: true,
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
- if (doRestart) {
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
- else {
497
- // No deploy dir — show env export instructions
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`, {