@chrysb/alphaclaw 0.1.25 → 0.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/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,218 @@ 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
+ const branch =
256
+ String(
257
+ execSync("git rev-parse --abbrev-ref HEAD", {
258
+ cwd: openclawDir,
259
+ encoding: "utf8",
260
+ }),
261
+ ).trim() || "main";
262
+ const askPassPath = path.join(
263
+ os.tmpdir(),
264
+ `alphaclaw-git-askpass-${process.pid}.sh`,
265
+ );
266
+ const runGit = (gitCommand, { withAuth = false } = {}) => {
267
+ const cmd = withAuth
268
+ ? `GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=${quoteArg(askPassPath)} git ${gitCommand}`
269
+ : `git ${gitCommand}`;
270
+ return execSync(cmd, {
271
+ cwd: openclawDir,
272
+ stdio: "pipe",
273
+ encoding: "utf8",
274
+ env: {
275
+ ...process.env,
276
+ GITHUB_TOKEN: githubToken,
277
+ },
278
+ });
279
+ };
280
+
281
+ try {
282
+ fs.writeFileSync(
283
+ askPassPath,
284
+ [
285
+ "#!/usr/bin/env sh",
286
+ 'case "$1" in',
287
+ ' *Username*) echo "x-access-token" ;;',
288
+ ' *Password*) echo "${GITHUB_TOKEN:-}" ;;',
289
+ ' *) echo "" ;;',
290
+ "esac",
291
+ "",
292
+ ].join("\n"),
293
+ { mode: 0o700 },
294
+ );
295
+
296
+ runGit(`remote set-url origin ${quoteArg(originUrl)}`);
297
+ try {
298
+ runGit(`ls-remote --exit-code --heads origin ${quoteArg(branch)}`, {
299
+ withAuth: true,
300
+ });
301
+ runGit(`pull --rebase --autostash origin ${quoteArg(branch)}`, {
302
+ withAuth: true,
303
+ });
304
+ } catch {
305
+ console.log(
306
+ `[alphaclaw] Remote branch "${branch}" not found, skipping pull`,
307
+ );
308
+ }
309
+ runGit("add -A");
310
+ try {
311
+ runGit("diff --cached --quiet");
312
+ console.log("[alphaclaw] No changes to commit");
313
+ return 0;
314
+ } catch {}
315
+ runGit(`commit -m ${quoteArg(commitMessage)}`);
316
+ runGit(`push origin ${quoteArg(branch)}`, { withAuth: true });
317
+ const hash = String(runGit("rev-parse --short HEAD")).trim();
318
+ console.log(`[alphaclaw] Git sync complete (${hash})`);
319
+ return 0;
320
+ } catch (e) {
321
+ const details = String(e.stderr || e.stdout || e.message || "").trim();
322
+ console.error(`[alphaclaw] git-sync failed: ${details.slice(0, 400)}`);
323
+ return 1;
324
+ } finally {
325
+ try {
326
+ fs.rmSync(askPassPath, { force: true });
327
+ } catch {}
328
+ }
329
+ };
330
+
331
+ if (command === "git-sync") {
332
+ process.exit(runGitSync());
333
+ }
334
+
335
+ const runTelegramTopicAdd = () => {
336
+ const topicName = String(flagValue(commandArgs, "--name") || "").trim();
337
+ const threadId = String(flagValue(commandArgs, "--thread") || "").trim();
338
+ const systemInstructions = String(
339
+ flagValue(commandArgs, "--system") || "",
340
+ ).trim();
341
+ const requestedGroupId = String(
342
+ flagValue(commandArgs, "--group") || "",
343
+ ).trim();
344
+ if (!threadId) {
345
+ console.error("[alphaclaw] Missing --thread for telegram topic add");
346
+ return 1;
347
+ }
348
+ if (!topicName) {
349
+ console.error("[alphaclaw] Missing --name for telegram topic add");
350
+ return 1;
351
+ }
352
+
353
+ const configPath = path.join(openclawDir, "openclaw.json");
354
+ if (!fs.existsSync(configPath)) {
355
+ console.error("[alphaclaw] Missing openclaw.json. Run setup first.");
356
+ return 1;
357
+ }
358
+
359
+ try {
360
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
361
+ const configuredGroups = Object.keys(cfg.channels?.telegram?.groups || {});
362
+ let groupId = requestedGroupId;
363
+ if (!groupId) {
364
+ if (configuredGroups.length === 1) {
365
+ [groupId] = configuredGroups;
366
+ } else if (configuredGroups.length === 0) {
367
+ console.error(
368
+ "[alphaclaw] No Telegram group configured. Configure Telegram workspace first.",
369
+ );
370
+ return 1;
371
+ } else {
372
+ console.error(
373
+ "[alphaclaw] Multiple Telegram groups detected. Provide --group <groupId>.",
374
+ );
375
+ return 1;
376
+ }
377
+ }
378
+
379
+ const topicRegistry = require("../lib/server/topic-registry");
380
+ const {
381
+ syncConfigForTelegram,
382
+ } = require("../lib/server/telegram-workspace");
383
+ const {
384
+ syncBootstrapPromptFiles,
385
+ } = require("../lib/server/onboarding/workspace");
386
+ topicRegistry.updateTopic(groupId, threadId, {
387
+ name: topicName,
388
+ ...(systemInstructions ? { systemInstructions } : {}),
389
+ });
390
+
391
+ const requireMention =
392
+ !!cfg.channels?.telegram?.groups?.[groupId]?.requireMention;
393
+ const syncResult = syncConfigForTelegram({
394
+ fs,
395
+ openclawDir,
396
+ topicRegistry,
397
+ groupId,
398
+ requireMention,
399
+ resolvedUserId: "",
400
+ });
401
+ syncBootstrapPromptFiles({
402
+ fs,
403
+ workspaceDir: path.join(openclawDir, "workspace"),
404
+ });
405
+
406
+ console.log(
407
+ `[alphaclaw] Topic mapped: group=${groupId} thread=${threadId} name=${topicName}`,
408
+ );
409
+ console.log(
410
+ `[alphaclaw] Concurrency updated: agent=${syncResult.maxConcurrent} subagents=${syncResult.subagentMaxConcurrent} topics=${syncResult.totalTopics}`,
411
+ );
412
+ return 0;
413
+ } catch (e) {
414
+ console.error(`[alphaclaw] telegram topic add failed: ${e.message}`);
415
+ return 1;
416
+ }
417
+ };
418
+
419
+ if (
420
+ command === "telegram" &&
421
+ commandScope === "topic" &&
422
+ commandAction === "add"
423
+ ) {
424
+ process.exit(runTelegramTopicAdd());
425
+ }
426
+
427
+ const kSetupPassword = String(process.env.SETUP_PASSWORD || "").trim();
428
+ if (!kSetupPassword) {
429
+ console.error(
430
+ [
431
+ "[alphaclaw] Fatal config error: SETUP_PASSWORD is missing or empty.",
432
+ "[alphaclaw] Set SETUP_PASSWORD in your deployment environment variables and restart.",
433
+ "[alphaclaw] Examples:",
434
+ "[alphaclaw] - Render: Dashboard -> Environment -> Add SETUP_PASSWORD",
435
+ "[alphaclaw] - Railway: Project -> Variables -> Add SETUP_PASSWORD",
436
+ ].join("\n"),
437
+ );
438
+ process.exit(1);
439
+ }
440
+
157
441
  // ---------------------------------------------------------------------------
158
442
  // 7. Set OPENCLAW_HOME globally so all child processes inherit it
159
443
  // ---------------------------------------------------------------------------
@@ -184,7 +468,10 @@ if (!gogInstalled) {
184
468
  const arch = os.arch() === "arm64" ? "arm64" : "amd64";
185
469
  const tarball = `gogcli_${gogVersion}_${platform}_${arch}.tar.gz`;
186
470
  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" });
471
+ execSync(
472
+ `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`,
473
+ { stdio: "inherit" },
474
+ );
188
475
  console.log("[alphaclaw] gog CLI installed");
189
476
  } catch (e) {
190
477
  console.log(`[alphaclaw] gog install skipped: ${e.message}`);
@@ -195,7 +482,8 @@ if (!gogInstalled) {
195
482
  // 7. Configure gog keyring (file backend for headless environments)
196
483
  // ---------------------------------------------------------------------------
197
484
 
198
- process.env.GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
485
+ process.env.GOG_KEYRING_PASSWORD =
486
+ process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
199
487
  const gogConfigFile = path.join(openclawDir, "gogcli", "config.json");
200
488
 
201
489
  if (!fs.existsSync(gogConfigFile)) {
@@ -211,6 +499,31 @@ if (!fs.existsSync(gogConfigFile)) {
211
499
  // ---------------------------------------------------------------------------
212
500
 
213
501
  const hourlyGitSyncPath = path.join(openclawDir, "hourly-git-sync.sh");
502
+ const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
503
+
504
+ try {
505
+ if (fs.existsSync(packagedHourlyGitSyncPath)) {
506
+ const packagedSyncScript = fs.readFileSync(
507
+ packagedHourlyGitSyncPath,
508
+ "utf8",
509
+ );
510
+ const installedSyncScript = fs.existsSync(hourlyGitSyncPath)
511
+ ? fs.readFileSync(hourlyGitSyncPath, "utf8")
512
+ : "";
513
+ const shouldInstallSyncScript =
514
+ !installedSyncScript ||
515
+ !installedSyncScript.includes("GIT_ASKPASS") ||
516
+ !installedSyncScript.includes("GITHUB_TOKEN");
517
+ if (shouldInstallSyncScript && packagedSyncScript.trim()) {
518
+ fs.writeFileSync(hourlyGitSyncPath, packagedSyncScript, { mode: 0o755 });
519
+ console.log("[alphaclaw] Refreshed hourly git sync script");
520
+ }
521
+ }
522
+ } catch (e) {
523
+ console.log(
524
+ `[alphaclaw] Hourly git sync script refresh skipped: ${e.message}`,
525
+ );
526
+ }
214
527
 
215
528
  if (fs.existsSync(hourlyGitSyncPath)) {
216
529
  try {
@@ -238,7 +551,9 @@ if (fs.existsSync(hourlyGitSyncPath)) {
238
551
  fs.writeFileSync(cronFilePath, cronContent, { mode: 0o644 });
239
552
  console.log("[alphaclaw] System cron entry installed");
240
553
  } else {
241
- try { fs.unlinkSync(cronFilePath); } catch {}
554
+ try {
555
+ fs.unlinkSync(cronFilePath);
556
+ } catch {}
242
557
  console.log("[alphaclaw] System cron entry disabled");
243
558
  }
244
559
  } catch (e) {
@@ -271,13 +586,18 @@ if (process.env.GOG_CLIENT_CREDENTIALS_JSON && process.env.GOG_REFRESH_TOKEN) {
271
586
  fs.writeFileSync(tmpCreds, process.env.GOG_CLIENT_CREDENTIALS_JSON);
272
587
  execSync(`gog auth credentials set "${tmpCreds}"`, { stdio: "ignore" });
273
588
  fs.unlinkSync(tmpCreds);
274
- fs.writeFileSync(tmpToken, JSON.stringify({
275
- email: process.env.GOG_ACCOUNT || "",
276
- refresh_token: process.env.GOG_REFRESH_TOKEN,
277
- }));
589
+ fs.writeFileSync(
590
+ tmpToken,
591
+ JSON.stringify({
592
+ email: process.env.GOG_ACCOUNT || "",
593
+ refresh_token: process.env.GOG_REFRESH_TOKEN,
594
+ }),
595
+ );
278
596
  execSync(`gog auth tokens import "${tmpToken}"`, { stdio: "ignore" });
279
597
  fs.unlinkSync(tmpToken);
280
- console.log(`[alphaclaw] gog CLI configured for ${process.env.GOG_ACCOUNT || "account"}`);
598
+ console.log(
599
+ `[alphaclaw] gog CLI configured for ${process.env.GOG_ACCOUNT || "account"}`,
600
+ );
281
601
  } catch (e) {
282
602
  console.log(`[alphaclaw] gog credentials setup skipped: ${e.message}`);
283
603
  }
@@ -294,17 +614,42 @@ const configPath = path.join(openclawDir, "openclaw.json");
294
614
  if (fs.existsSync(configPath)) {
295
615
  console.log("[alphaclaw] Config exists, reconciling channels...");
296
616
 
297
- const githubToken = process.env.GITHUB_TOKEN;
298
617
  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`;
618
+ if (fs.existsSync(path.join(openclawDir, ".git"))) {
619
+ if (githubRepo) {
620
+ const repoUrl = githubRepo
621
+ .replace(/^git@github\.com:/, "")
622
+ .replace(/^https:\/\/github\.com\//, "")
623
+ .replace(/\.git$/, "");
624
+ const remoteUrl = `https://github.com/${repoUrl}.git`;
625
+ try {
626
+ execSync(`git remote set-url origin "${remoteUrl}"`, {
627
+ cwd: openclawDir,
628
+ stdio: "ignore",
629
+ });
630
+ console.log("[alphaclaw] Repo ready");
631
+ } catch {}
632
+ }
633
+
634
+ // Migration path: scrub persisted PATs from existing GitHub origin URLs.
305
635
  try {
306
- execSync(`git remote set-url origin "${remoteUrl}"`, { cwd: openclawDir, stdio: "ignore" });
307
- console.log("[alphaclaw] Repo ready");
636
+ const existingOrigin = execSync("git remote get-url origin", {
637
+ cwd: openclawDir,
638
+ stdio: ["ignore", "pipe", "ignore"],
639
+ encoding: "utf8",
640
+ }).trim();
641
+ const match = existingOrigin.match(
642
+ /^https:\/\/[^/@]+@github\.com\/(.+)$/i,
643
+ );
644
+ if (match?.[1]) {
645
+ const cleanedPath = String(match[1]).replace(/\.git$/i, "");
646
+ const cleanedOrigin = `https://github.com/${cleanedPath}.git`;
647
+ execSync(`git remote set-url origin "${cleanedOrigin}"`, {
648
+ cwd: openclawDir,
649
+ stdio: "ignore",
650
+ });
651
+ console.log("[alphaclaw] Scrubbed tokenized GitHub remote URL");
652
+ }
308
653
  } catch {}
309
654
  }
310
655
 
@@ -341,19 +686,9 @@ if (fs.existsSync(configPath)) {
341
686
 
342
687
  if (changed) {
343
688
  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
- ];
689
+ const replacements = buildSecretReplacements(process.env);
355
690
  for (const [secret, envRef] of replacements) {
356
- if (secret && secret.length > 8) {
691
+ if (secret) {
357
692
  content = content.split(secret).join(envRef);
358
693
  }
359
694
  }
@@ -364,7 +699,9 @@ if (fs.existsSync(configPath)) {
364
699
  console.error(`[alphaclaw] Channel reconciliation error: ${e.message}`);
365
700
  }
366
701
  } else {
367
- console.log("[alphaclaw] No config yet -- onboarding will run from the Setup UI");
702
+ console.log(
703
+ "[alphaclaw] No config yet -- onboarding will run from the Setup UI",
704
+ );
368
705
  }
369
706
 
370
707
  // ---------------------------------------------------------------------------
@@ -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