@chrysb/alphaclaw 0.1.25 → 0.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/bin/alphaclaw.js CHANGED
@@ -5,17 +5,59 @@ const fs = require("fs");
5
5
  const os = require("os");
6
6
  const path = require("path");
7
7
  const { execSync } = require("child_process");
8
+ const { buildSecretReplacements } = require("../lib/server/helpers");
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // Parse CLI flags
11
12
  // ---------------------------------------------------------------------------
12
13
 
13
14
  const args = process.argv.slice(2);
14
- const command = args.find((a) => !a.startsWith("-"));
15
15
 
16
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
16
+ const flagValue = (argv, ...flags) => {
17
+ for (const flag of flags) {
18
+ const idx = argv.indexOf(flag);
19
+ if (idx !== -1 && idx + 1 < argv.length) {
20
+ return argv[idx + 1];
21
+ }
22
+ }
23
+ return undefined;
24
+ };
25
+
26
+ const kGlobalValueFlags = new Set(["--root-dir", "--port"]);
27
+ const splitGlobalAndCommandArgs = (argv) => {
28
+ const globalArgs = [];
29
+ let index = 0;
30
+ while (index < argv.length) {
31
+ const token = argv[index];
32
+ if (!token.startsWith("-")) break;
33
+ globalArgs.push(token);
34
+ if (kGlobalValueFlags.has(token) && index + 1 < argv.length) {
35
+ globalArgs.push(argv[index + 1]);
36
+ index += 2;
37
+ continue;
38
+ }
39
+ index += 1;
40
+ }
41
+ return {
42
+ globalArgs,
43
+ commandArgs: argv.slice(index),
44
+ };
45
+ };
46
+
47
+ const { globalArgs, commandArgs } = splitGlobalAndCommandArgs(args);
48
+ const command = commandArgs[0];
49
+ const commandScope = commandArgs[1];
50
+ const commandAction = commandArgs[2];
17
51
 
18
- if (args.includes("--version") || args.includes("-v") || command === "version") {
52
+ const pkg = JSON.parse(
53
+ fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
54
+ );
55
+
56
+ if (
57
+ args.includes("--version") ||
58
+ args.includes("-v") ||
59
+ command === "version"
60
+ ) {
19
61
  console.log(pkg.version);
20
62
  process.exit(0);
21
63
  }
@@ -28,33 +70,55 @@ Usage: alphaclaw <command> [options]
28
70
 
29
71
  Commands:
30
72
  start Start the AlphaClaw server (Setup UI + gateway manager)
73
+ git-sync Commit and push /data/.openclaw safely using GITHUB_TOKEN
74
+ telegram topic add Add/update Telegram topic mapping by thread ID
31
75
  version Print version
32
76
 
33
- Options:
34
- --root-dir <path> Persistent data directory (default: ~/.alphaclaw)
35
- --port <number> Server port (default: 3000)
36
- --version, -v Print version
37
- --help Show this help message
77
+ Global options:
78
+ --version, -v Print version
79
+ --help Show this help message
80
+
81
+ start options:
82
+ --root-dir <path> Persistent data directory (default: ~/.alphaclaw)
83
+ --port <number> Server port (default: 3000)
84
+
85
+ git-sync options:
86
+ --message, -m <text> Commit message
87
+
88
+ telegram topic add options:
89
+ --thread <id> Telegram thread ID
90
+ --name <text> Topic name
91
+ --system <text> Optional system instructions
92
+ --group <id> Optional group ID override (auto-resolves when one group exists)
93
+
94
+ Examples:
95
+ alphaclaw git-sync --message "sync workspace"
96
+ alphaclaw telegram topic add --thread 12 --name "Testing"
97
+ alphaclaw telegram topic add --thread 12 --name "Testing" --system "Handle QA requests"
38
98
  `);
39
99
  process.exit(0);
40
100
  }
41
101
 
42
- const flagValue = (flag) => {
43
- const idx = args.indexOf(flag);
44
- return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
45
- };
102
+ const quoteArg = (value) => `'${String(value || "").replace(/'/g, "'\"'\"'")}'`;
103
+ const resolveGithubRepoPath = (value) =>
104
+ String(value || "")
105
+ .trim()
106
+ .replace(/^git@github\.com:/, "")
107
+ .replace(/^https:\/\/github\.com\//, "")
108
+ .replace(/\.git$/, "");
46
109
 
47
110
  // ---------------------------------------------------------------------------
48
111
  // 1. Resolve root directory (before requiring any lib/ modules)
49
112
  // ---------------------------------------------------------------------------
50
113
 
51
- const rootDir = flagValue("--root-dir")
52
- || process.env.ALPHACLAW_ROOT_DIR
53
- || path.join(os.homedir(), ".alphaclaw");
114
+ const rootDir =
115
+ flagValue(globalArgs, "--root-dir") ||
116
+ process.env.ALPHACLAW_ROOT_DIR ||
117
+ path.join(os.homedir(), ".alphaclaw");
54
118
 
55
119
  process.env.ALPHACLAW_ROOT_DIR = rootDir;
56
120
 
57
- const portFlag = flagValue("--port");
121
+ const portFlag = flagValue(globalArgs, "--port");
58
122
  if (portFlag) {
59
123
  process.env.PORT = portFlag;
60
124
  }
@@ -73,16 +137,24 @@ console.log(`[alphaclaw] Root directory: ${rootDir}`);
73
137
  // from the fresh container using the persistent volume marker.
74
138
  const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending");
75
139
  if (fs.existsSync(pendingUpdateMarker)) {
76
- console.log("[alphaclaw] Pending update detected, installing @chrysb/alphaclaw@latest...");
140
+ console.log(
141
+ "[alphaclaw] Pending update detected, installing @chrysb/alphaclaw@latest...",
142
+ );
77
143
  const alphaPkgRoot = path.resolve(__dirname, "..");
78
- const nmIndex = alphaPkgRoot.lastIndexOf(`${path.sep}node_modules${path.sep}`);
79
- const installDir = nmIndex >= 0 ? alphaPkgRoot.slice(0, nmIndex) : alphaPkgRoot;
144
+ const nmIndex = alphaPkgRoot.lastIndexOf(
145
+ `${path.sep}node_modules${path.sep}`,
146
+ );
147
+ const installDir =
148
+ nmIndex >= 0 ? alphaPkgRoot.slice(0, nmIndex) : alphaPkgRoot;
80
149
  try {
81
- execSync("npm install @chrysb/alphaclaw@latest --omit=dev --prefer-online", {
82
- cwd: installDir,
83
- stdio: "inherit",
84
- timeout: 180000,
85
- });
150
+ execSync(
151
+ "npm install @chrysb/alphaclaw@latest --omit=dev --prefer-online",
152
+ {
153
+ cwd: installDir,
154
+ stdio: "inherit",
155
+ timeout: 180000,
156
+ },
157
+ );
86
158
  fs.unlinkSync(pendingUpdateMarker);
87
159
  console.log("[alphaclaw] Update applied successfully");
88
160
  } catch (e) {
@@ -154,6 +226,227 @@ if (fs.existsSync(envFilePath)) {
154
226
  console.log("[alphaclaw] Loaded .env");
155
227
  }
156
228
 
229
+ const runGitSync = () => {
230
+ const githubToken = String(process.env.GITHUB_TOKEN || "").trim();
231
+ const githubRepo = resolveGithubRepoPath(
232
+ process.env.GITHUB_WORKSPACE_REPO || "",
233
+ );
234
+ const commitMessage = String(
235
+ flagValue(commandArgs, "--message", "-m") || "",
236
+ ).trim();
237
+ if (!commitMessage) {
238
+ console.error("[alphaclaw] Missing --message for git-sync");
239
+ return 1;
240
+ }
241
+ if (!githubToken) {
242
+ console.error("[alphaclaw] Missing GITHUB_TOKEN for git-sync");
243
+ return 1;
244
+ }
245
+ if (!githubRepo) {
246
+ console.error("[alphaclaw] Missing GITHUB_WORKSPACE_REPO for git-sync");
247
+ return 1;
248
+ }
249
+ if (!fs.existsSync(path.join(openclawDir, ".git"))) {
250
+ console.error("[alphaclaw] No git repository at /data/.openclaw");
251
+ return 1;
252
+ }
253
+
254
+ const originUrl = `https://github.com/${githubRepo}.git`;
255
+ let branch = "main";
256
+ try {
257
+ branch =
258
+ String(
259
+ execSync("git symbolic-ref --short HEAD", {
260
+ cwd: openclawDir,
261
+ encoding: "utf8",
262
+ stdio: ["ignore", "pipe", "ignore"],
263
+ }),
264
+ ).trim() || "main";
265
+ } catch {}
266
+ const askPassPath = path.join(
267
+ os.tmpdir(),
268
+ `alphaclaw-git-askpass-${process.pid}.sh`,
269
+ );
270
+ const runGit = (gitCommand, { withAuth = false } = {}) => {
271
+ const cmd = withAuth
272
+ ? `GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=${quoteArg(askPassPath)} git ${gitCommand}`
273
+ : `git ${gitCommand}`;
274
+ return execSync(cmd, {
275
+ cwd: openclawDir,
276
+ stdio: "pipe",
277
+ encoding: "utf8",
278
+ env: {
279
+ ...process.env,
280
+ GITHUB_TOKEN: githubToken,
281
+ },
282
+ });
283
+ };
284
+
285
+ try {
286
+ fs.writeFileSync(
287
+ askPassPath,
288
+ [
289
+ "#!/usr/bin/env sh",
290
+ 'case "$1" in',
291
+ ' *Username*) echo "x-access-token" ;;',
292
+ ' *Password*) echo "${GITHUB_TOKEN:-}" ;;',
293
+ ' *) echo "" ;;',
294
+ "esac",
295
+ "",
296
+ ].join("\n"),
297
+ { mode: 0o700 },
298
+ );
299
+
300
+ runGit(`remote set-url origin ${quoteArg(originUrl)}`);
301
+ runGit(`config user.name ${quoteArg("AlphaClaw Agent")}`);
302
+ runGit(`config user.email ${quoteArg("agent@alphaclaw.md")}`);
303
+ try {
304
+ runGit(`ls-remote --exit-code --heads origin ${quoteArg(branch)}`, {
305
+ withAuth: true,
306
+ });
307
+ runGit(`pull --rebase --autostash origin ${quoteArg(branch)}`, {
308
+ withAuth: true,
309
+ });
310
+ } catch {
311
+ console.log(
312
+ `[alphaclaw] Remote branch "${branch}" not found, skipping pull`,
313
+ );
314
+ }
315
+ runGit("add -A");
316
+ try {
317
+ runGit("diff --cached --quiet");
318
+ console.log("[alphaclaw] No changes to commit");
319
+ return 0;
320
+ } catch {}
321
+ runGit(`commit -m ${quoteArg(commitMessage)}`);
322
+ runGit(`push origin ${quoteArg(branch)}`, { withAuth: true });
323
+ const hash = String(runGit("rev-parse --short HEAD")).trim();
324
+ console.log(`[alphaclaw] Git sync complete (${hash})`);
325
+ console.log(
326
+ `[alphaclaw] Commit URL: https://github.com/${githubRepo}/commit/${hash}`,
327
+ );
328
+ return 0;
329
+ } catch (e) {
330
+ const details = String(e.stderr || e.stdout || e.message || "").trim();
331
+ console.error(`[alphaclaw] git-sync failed: ${details.slice(0, 400)}`);
332
+ return 1;
333
+ } finally {
334
+ try {
335
+ fs.rmSync(askPassPath, { force: true });
336
+ } catch {}
337
+ }
338
+ };
339
+
340
+ if (command === "git-sync") {
341
+ process.exit(runGitSync());
342
+ }
343
+
344
+ const runTelegramTopicAdd = () => {
345
+ const topicName = String(flagValue(commandArgs, "--name") || "").trim();
346
+ const threadId = String(flagValue(commandArgs, "--thread") || "").trim();
347
+ const systemInstructions = String(
348
+ flagValue(commandArgs, "--system") || "",
349
+ ).trim();
350
+ const requestedGroupId = String(
351
+ flagValue(commandArgs, "--group") || "",
352
+ ).trim();
353
+ if (!threadId) {
354
+ console.error("[alphaclaw] Missing --thread for telegram topic add");
355
+ return 1;
356
+ }
357
+ if (!topicName) {
358
+ console.error("[alphaclaw] Missing --name for telegram topic add");
359
+ return 1;
360
+ }
361
+
362
+ const configPath = path.join(openclawDir, "openclaw.json");
363
+ if (!fs.existsSync(configPath)) {
364
+ console.error("[alphaclaw] Missing openclaw.json. Run setup first.");
365
+ return 1;
366
+ }
367
+
368
+ try {
369
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
370
+ const configuredGroups = Object.keys(cfg.channels?.telegram?.groups || {});
371
+ let groupId = requestedGroupId;
372
+ if (!groupId) {
373
+ if (configuredGroups.length === 1) {
374
+ [groupId] = configuredGroups;
375
+ } else if (configuredGroups.length === 0) {
376
+ console.error(
377
+ "[alphaclaw] No Telegram group configured. Configure Telegram workspace first.",
378
+ );
379
+ return 1;
380
+ } else {
381
+ console.error(
382
+ "[alphaclaw] Multiple Telegram groups detected. Provide --group <groupId>.",
383
+ );
384
+ return 1;
385
+ }
386
+ }
387
+
388
+ const topicRegistry = require("../lib/server/topic-registry");
389
+ const {
390
+ syncConfigForTelegram,
391
+ } = require("../lib/server/telegram-workspace");
392
+ const {
393
+ syncBootstrapPromptFiles,
394
+ } = require("../lib/server/onboarding/workspace");
395
+ topicRegistry.updateTopic(groupId, threadId, {
396
+ name: topicName,
397
+ ...(systemInstructions ? { systemInstructions } : {}),
398
+ });
399
+
400
+ const requireMention =
401
+ !!cfg.channels?.telegram?.groups?.[groupId]?.requireMention;
402
+ const syncResult = syncConfigForTelegram({
403
+ fs,
404
+ openclawDir,
405
+ topicRegistry,
406
+ groupId,
407
+ requireMention,
408
+ resolvedUserId: "",
409
+ });
410
+ syncBootstrapPromptFiles({
411
+ fs,
412
+ workspaceDir: path.join(openclawDir, "workspace"),
413
+ });
414
+
415
+ console.log(
416
+ `[alphaclaw] Topic mapped: group=${groupId} thread=${threadId} name=${topicName}`,
417
+ );
418
+ console.log(
419
+ `[alphaclaw] Concurrency updated: agent=${syncResult.maxConcurrent} subagents=${syncResult.subagentMaxConcurrent} topics=${syncResult.totalTopics}`,
420
+ );
421
+ return 0;
422
+ } catch (e) {
423
+ console.error(`[alphaclaw] telegram topic add failed: ${e.message}`);
424
+ return 1;
425
+ }
426
+ };
427
+
428
+ if (
429
+ command === "telegram" &&
430
+ commandScope === "topic" &&
431
+ commandAction === "add"
432
+ ) {
433
+ process.exit(runTelegramTopicAdd());
434
+ }
435
+
436
+ const kSetupPassword = String(process.env.SETUP_PASSWORD || "").trim();
437
+ if (!kSetupPassword) {
438
+ console.error(
439
+ [
440
+ "[alphaclaw] Fatal config error: SETUP_PASSWORD is missing or empty.",
441
+ "[alphaclaw] Set SETUP_PASSWORD in your deployment environment variables and restart.",
442
+ "[alphaclaw] Examples:",
443
+ "[alphaclaw] - Render: Dashboard -> Environment -> Add SETUP_PASSWORD",
444
+ "[alphaclaw] - Railway: Project -> Variables -> Add SETUP_PASSWORD",
445
+ ].join("\n"),
446
+ );
447
+ process.exit(1);
448
+ }
449
+
157
450
  // ---------------------------------------------------------------------------
158
451
  // 7. Set OPENCLAW_HOME globally so all child processes inherit it
159
452
  // ---------------------------------------------------------------------------
@@ -184,7 +477,10 @@ if (!gogInstalled) {
184
477
  const arch = os.arch() === "arm64" ? "arm64" : "amd64";
185
478
  const tarball = `gogcli_${gogVersion}_${platform}_${arch}.tar.gz`;
186
479
  const url = `https://github.com/steipete/gogcli/releases/download/v${gogVersion}/${tarball}`;
187
- execSync(`curl -fsSL "${url}" -o /tmp/gog.tar.gz && tar -xzf /tmp/gog.tar.gz -C /tmp/ && mv /tmp/gog /usr/local/bin/gog && chmod +x /usr/local/bin/gog && rm -f /tmp/gog.tar.gz`, { stdio: "inherit" });
480
+ execSync(
481
+ `curl -fsSL "${url}" -o /tmp/gog.tar.gz && tar -xzf /tmp/gog.tar.gz -C /tmp/ && mv /tmp/gog /usr/local/bin/gog && chmod +x /usr/local/bin/gog && rm -f /tmp/gog.tar.gz`,
482
+ { stdio: "inherit" },
483
+ );
188
484
  console.log("[alphaclaw] gog CLI installed");
189
485
  } catch (e) {
190
486
  console.log(`[alphaclaw] gog install skipped: ${e.message}`);
@@ -195,7 +491,8 @@ if (!gogInstalled) {
195
491
  // 7. Configure gog keyring (file backend for headless environments)
196
492
  // ---------------------------------------------------------------------------
197
493
 
198
- process.env.GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
494
+ process.env.GOG_KEYRING_PASSWORD =
495
+ process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
199
496
  const gogConfigFile = path.join(openclawDir, "gogcli", "config.json");
200
497
 
201
498
  if (!fs.existsSync(gogConfigFile)) {
@@ -211,6 +508,31 @@ if (!fs.existsSync(gogConfigFile)) {
211
508
  // ---------------------------------------------------------------------------
212
509
 
213
510
  const hourlyGitSyncPath = path.join(openclawDir, "hourly-git-sync.sh");
511
+ const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
512
+
513
+ try {
514
+ if (fs.existsSync(packagedHourlyGitSyncPath)) {
515
+ const packagedSyncScript = fs.readFileSync(
516
+ packagedHourlyGitSyncPath,
517
+ "utf8",
518
+ );
519
+ const installedSyncScript = fs.existsSync(hourlyGitSyncPath)
520
+ ? fs.readFileSync(hourlyGitSyncPath, "utf8")
521
+ : "";
522
+ const shouldInstallSyncScript =
523
+ !installedSyncScript ||
524
+ !installedSyncScript.includes("GIT_ASKPASS") ||
525
+ !installedSyncScript.includes("GITHUB_TOKEN");
526
+ if (shouldInstallSyncScript && packagedSyncScript.trim()) {
527
+ fs.writeFileSync(hourlyGitSyncPath, packagedSyncScript, { mode: 0o755 });
528
+ console.log("[alphaclaw] Refreshed hourly git sync script");
529
+ }
530
+ }
531
+ } catch (e) {
532
+ console.log(
533
+ `[alphaclaw] Hourly git sync script refresh skipped: ${e.message}`,
534
+ );
535
+ }
214
536
 
215
537
  if (fs.existsSync(hourlyGitSyncPath)) {
216
538
  try {
@@ -238,7 +560,9 @@ if (fs.existsSync(hourlyGitSyncPath)) {
238
560
  fs.writeFileSync(cronFilePath, cronContent, { mode: 0o644 });
239
561
  console.log("[alphaclaw] System cron entry installed");
240
562
  } else {
241
- try { fs.unlinkSync(cronFilePath); } catch {}
563
+ try {
564
+ fs.unlinkSync(cronFilePath);
565
+ } catch {}
242
566
  console.log("[alphaclaw] System cron entry disabled");
243
567
  }
244
568
  } catch (e) {
@@ -271,13 +595,18 @@ if (process.env.GOG_CLIENT_CREDENTIALS_JSON && process.env.GOG_REFRESH_TOKEN) {
271
595
  fs.writeFileSync(tmpCreds, process.env.GOG_CLIENT_CREDENTIALS_JSON);
272
596
  execSync(`gog auth credentials set "${tmpCreds}"`, { stdio: "ignore" });
273
597
  fs.unlinkSync(tmpCreds);
274
- fs.writeFileSync(tmpToken, JSON.stringify({
275
- email: process.env.GOG_ACCOUNT || "",
276
- refresh_token: process.env.GOG_REFRESH_TOKEN,
277
- }));
598
+ fs.writeFileSync(
599
+ tmpToken,
600
+ JSON.stringify({
601
+ email: process.env.GOG_ACCOUNT || "",
602
+ refresh_token: process.env.GOG_REFRESH_TOKEN,
603
+ }),
604
+ );
278
605
  execSync(`gog auth tokens import "${tmpToken}"`, { stdio: "ignore" });
279
606
  fs.unlinkSync(tmpToken);
280
- console.log(`[alphaclaw] gog CLI configured for ${process.env.GOG_ACCOUNT || "account"}`);
607
+ console.log(
608
+ `[alphaclaw] gog CLI configured for ${process.env.GOG_ACCOUNT || "account"}`,
609
+ );
281
610
  } catch (e) {
282
611
  console.log(`[alphaclaw] gog credentials setup skipped: ${e.message}`);
283
612
  }
@@ -294,17 +623,42 @@ const configPath = path.join(openclawDir, "openclaw.json");
294
623
  if (fs.existsSync(configPath)) {
295
624
  console.log("[alphaclaw] Config exists, reconciling channels...");
296
625
 
297
- const githubToken = process.env.GITHUB_TOKEN;
298
626
  const githubRepo = process.env.GITHUB_WORKSPACE_REPO;
299
- if (githubToken && githubRepo && fs.existsSync(path.join(openclawDir, ".git"))) {
300
- const repoUrl = githubRepo
301
- .replace(/^git@github\.com:/, "")
302
- .replace(/^https:\/\/github\.com\//, "")
303
- .replace(/\.git$/, "");
304
- const remoteUrl = `https://${githubToken}@github.com/${repoUrl}.git`;
627
+ if (fs.existsSync(path.join(openclawDir, ".git"))) {
628
+ if (githubRepo) {
629
+ const repoUrl = githubRepo
630
+ .replace(/^git@github\.com:/, "")
631
+ .replace(/^https:\/\/github\.com\//, "")
632
+ .replace(/\.git$/, "");
633
+ const remoteUrl = `https://github.com/${repoUrl}.git`;
634
+ try {
635
+ execSync(`git remote set-url origin "${remoteUrl}"`, {
636
+ cwd: openclawDir,
637
+ stdio: "ignore",
638
+ });
639
+ console.log("[alphaclaw] Repo ready");
640
+ } catch {}
641
+ }
642
+
643
+ // Migration path: scrub persisted PATs from existing GitHub origin URLs.
305
644
  try {
306
- execSync(`git remote set-url origin "${remoteUrl}"`, { cwd: openclawDir, stdio: "ignore" });
307
- console.log("[alphaclaw] Repo ready");
645
+ const existingOrigin = execSync("git remote get-url origin", {
646
+ cwd: openclawDir,
647
+ stdio: ["ignore", "pipe", "ignore"],
648
+ encoding: "utf8",
649
+ }).trim();
650
+ const match = existingOrigin.match(
651
+ /^https:\/\/[^/@]+@github\.com\/(.+)$/i,
652
+ );
653
+ if (match?.[1]) {
654
+ const cleanedPath = String(match[1]).replace(/\.git$/i, "");
655
+ const cleanedOrigin = `https://github.com/${cleanedPath}.git`;
656
+ execSync(`git remote set-url origin "${cleanedOrigin}"`, {
657
+ cwd: openclawDir,
658
+ stdio: "ignore",
659
+ });
660
+ console.log("[alphaclaw] Scrubbed tokenized GitHub remote URL");
661
+ }
308
662
  } catch {}
309
663
  }
310
664
 
@@ -341,19 +695,9 @@ if (fs.existsSync(configPath)) {
341
695
 
342
696
  if (changed) {
343
697
  let content = JSON.stringify(cfg, null, 2);
344
- const replacements = [
345
- [process.env.OPENCLAW_GATEWAY_TOKEN, "${OPENCLAW_GATEWAY_TOKEN}"],
346
- [process.env.ANTHROPIC_API_KEY, "${ANTHROPIC_API_KEY}"],
347
- [process.env.ANTHROPIC_TOKEN, "${ANTHROPIC_TOKEN}"],
348
- [process.env.TELEGRAM_BOT_TOKEN, "${TELEGRAM_BOT_TOKEN}"],
349
- [process.env.DISCORD_BOT_TOKEN, "${DISCORD_BOT_TOKEN}"],
350
- [process.env.OPENAI_API_KEY, "${OPENAI_API_KEY}"],
351
- [process.env.GEMINI_API_KEY, "${GEMINI_API_KEY}"],
352
- [process.env.NOTION_API_KEY, "${NOTION_API_KEY}"],
353
- [process.env.BRAVE_API_KEY, "${BRAVE_API_KEY}"],
354
- ];
698
+ const replacements = buildSecretReplacements(process.env);
355
699
  for (const [secret, envRef] of replacements) {
356
- if (secret && secret.length > 8) {
700
+ if (secret) {
357
701
  content = content.split(secret).join(envRef);
358
702
  }
359
703
  }
@@ -364,7 +708,9 @@ if (fs.existsSync(configPath)) {
364
708
  console.error(`[alphaclaw] Channel reconciliation error: ${e.message}`);
365
709
  }
366
710
  } else {
367
- console.log("[alphaclaw] No config yet -- onboarding will run from the Setup UI");
711
+ console.log(
712
+ "[alphaclaw] No config yet -- onboarding will run from the Setup UI",
713
+ );
368
714
  }
369
715
 
370
716
  // ---------------------------------------------------------------------------
@@ -28,12 +28,14 @@ import { Providers } from "./components/providers.js";
28
28
  import { Welcome } from "./components/welcome.js";
29
29
  import { Envars } from "./components/envars.js";
30
30
  import { ToastContainer, showToast } from "./components/toast.js";
31
+ import { TelegramWorkspace } from "./components/telegram-workspace.js";
31
32
  import { ChevronDownIcon } from "./components/icons.js";
32
33
  const html = htm.bind(h);
33
34
  const kUiTabs = ["general", "providers", "envars"];
35
+ const kSubScreens = ["telegram"];
34
36
  const kDefaultUiTab = "general";
35
37
 
36
- const GeneralTab = ({ onSwitchTab, isActive }) => {
38
+ const GeneralTab = ({ onSwitchTab, onNavigate, isActive }) => {
37
39
  const [googleKey, setGoogleKey] = useState(0);
38
40
  const [dashboardLoading, setDashboardLoading] = useState(false);
39
41
 
@@ -156,7 +158,7 @@ const GeneralTab = ({ onSwitchTab, isActive }) => {
156
158
  return html`
157
159
  <div class="space-y-4">
158
160
  <${Gateway} status=${gatewayStatus} openclawVersion=${openclawVersion} />
159
- <${Channels} channels=${channels} onSwitchTab=${onSwitchTab} />
161
+ <${Channels} channels=${channels} onSwitchTab=${onSwitchTab} onNavigate=${onNavigate} />
160
162
  <${Pairings}
161
163
  pending=${pending}
162
164
  channels=${channels}
@@ -282,8 +284,13 @@ function App() {
282
284
  const [onboarded, setOnboarded] = useState(null);
283
285
  const [tab, setTab] = useState(() => {
284
286
  const hash = window.location.hash.replace("#", "");
287
+ if (kSubScreens.includes(hash)) return kDefaultUiTab;
285
288
  return kUiTabs.includes(hash) ? hash : kDefaultUiTab;
286
289
  });
290
+ const [subScreen, setSubScreen] = useState(() => {
291
+ const hash = window.location.hash.replace("#", "");
292
+ return kSubScreens.includes(hash) ? hash : null;
293
+ });
287
294
  const [acVersion, setAcVersion] = useState(null);
288
295
  const [acLatest, setAcLatest] = useState(null);
289
296
  const [acHasUpdate, setAcHasUpdate] = useState(false);
@@ -316,8 +323,8 @@ function App() {
316
323
  }, []);
317
324
 
318
325
  useEffect(() => {
319
- history.replaceState(null, "", `#${tab}`);
320
- }, [tab]);
326
+ history.replaceState(null, "", `#${subScreen || tab}`);
327
+ }, [tab, subScreen]);
321
328
 
322
329
  useEffect(() => {
323
330
  if (!onboarded) return;
@@ -401,6 +408,9 @@ function App() {
401
408
  `;
402
409
  }
403
410
 
411
+ const navigateToSubScreen = (screen) => setSubScreen(screen);
412
+ const exitSubScreen = () => setSubScreen(null);
413
+
404
414
  const kNavItems = [
405
415
  { id: "general", label: "General" },
406
416
  { id: "providers", label: "Providers" },
@@ -447,8 +457,8 @@ function App() {
447
457
  ${kNavItems.map(
448
458
  (item) => html`
449
459
  <a
450
- class=${tab === item.id ? "active" : ""}
451
- onclick=${() => setTab(item.id)}
460
+ class=${tab === item.id && !subScreen ? "active" : ""}
461
+ onclick=${() => { setSubScreen(null); setTab(item.id); }}
452
462
  >
453
463
  ${item.label}
454
464
  </a>
@@ -471,19 +481,28 @@ function App() {
471
481
  </div>
472
482
 
473
483
  <div class="app-content">
474
- <div class="max-w-2xl w-full mx-auto space-y-4">
475
- <div style=${{ display: tab === "general" ? "" : "none" }}>
476
- <${GeneralTab}
477
- onSwitchTab=${setTab}
478
- isActive=${tab === "general"}
479
- />
480
- </div>
481
- <div style=${{ display: tab === "providers" ? "" : "none" }}>
482
- <${Providers} />
483
- </div>
484
- <div style=${{ display: tab === "envars" ? "" : "none" }}>
485
- <${Envars} />
486
- </div>
484
+ <div class="max-w-2xl w-full mx-auto">
485
+ ${subScreen === "telegram"
486
+ ? html`
487
+ <div class="pt-4">
488
+ <${TelegramWorkspace} onBack=${exitSubScreen} />
489
+ </div>
490
+ `
491
+ : html`
492
+ <div class="pt-4" style=${{ display: tab === "general" ? "" : "none" }}>
493
+ <${GeneralTab}
494
+ onSwitchTab=${setTab}
495
+ onNavigate=${navigateToSubScreen}
496
+ isActive=${tab === "general" && !subScreen}
497
+ />
498
+ </div>
499
+ <div class="pt-4" style=${{ display: tab === "providers" ? "" : "none" }}>
500
+ <${Providers} />
501
+ </div>
502
+ <div class="pt-4" style=${{ display: tab === "envars" ? "" : "none" }}>
503
+ <${Envars} />
504
+ </div>
505
+ `}
487
506
  </div>
488
507
  </div>
489
508