@chrysb/alphaclaw 0.9.18 → 0.9.20

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 CHANGED
@@ -157,6 +157,9 @@ The built-in watchdog monitors gateway health and recovers from failures automat
157
157
  | `WATCHDOG_NOTIFICATIONS_DISABLED` | Optional | Disable watchdog notifications (`true`/`false`) |
158
158
  | `PORT` | Optional | Server port (default `3000`) |
159
159
  | `ALPHACLAW_ROOT_DIR` | Optional | Data directory (default `/data`) |
160
+ | `ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL` | Optional | Skip writes to `/etc/cron.d` while keeping cron config (`true`/`false`); the managed hourly script still exits when sync is disabled |
161
+ | `ALPHACLAW_GIT_SHIM_PATH` | Optional | Install the managed git auth shim at this path and prepend its directory to runtime `PATH` (default `/usr/local/bin/git`) |
162
+ | `ALPHACLAW_GIT_ASKPASS_PATH` | Optional | Install the git askpass helper at this path (default `$TMPDIR/alphaclaw-git-askpass.sh`) |
160
163
  | `TRUST_PROXY_HOPS` | Optional | Trust proxy hop count for correct client IP |
161
164
  | `REMOTE_MCP_URL` | Optional | Upstream remote MCP server URL. When set together with `REMOTE_MCP_API_TOKEN`, AlphaClaw writes a managed `mcp.servers.<name>` entry to `openclaw.json` on every gateway start. |
162
165
  | `REMOTE_MCP_API_TOKEN` | Optional | Bearer token for the remote MCP server. Persisted in `openclaw.json` as the `${REMOTE_MCP_API_TOKEN}` reference, never as plaintext. |
package/bin/alphaclaw.js CHANGED
@@ -4,8 +4,12 @@
4
4
  const fs = require("fs");
5
5
  const os = require("os");
6
6
  const path = require("path");
7
- const { execSync } = require("child_process");
7
+ const { execFileSync, execSync } = require("child_process");
8
8
  const {
9
+ shouldSkipSystemCronInstall,
10
+ resolveGitAskPassPath,
11
+ resolveGitShimPath,
12
+ prependGitShimDirToPath,
9
13
  normalizeGitSyncFilePath,
10
14
  validateGitSyncFilePath,
11
15
  resolveRealGitPath,
@@ -16,6 +20,9 @@ const {
16
20
  restoreMissingOpenclawConfigFromRemote,
17
21
  } = require("../lib/cli/openclaw-config-restore");
18
22
  const { buildSecretReplacements } = require("../lib/server/helpers");
23
+ const {
24
+ migrateLegacyTelegramStreamingConfig,
25
+ } = require("../lib/server/openclaw-config-migrations");
19
26
  const {
20
27
  migrateManagedInternalFiles,
21
28
  } = require("../lib/server/internal-files-migration");
@@ -278,7 +285,7 @@ const runGitSync = () => {
278
285
  }
279
286
 
280
287
  const realGitPath = resolveRealGitPath({
281
- shimPath: "/usr/local/bin/git",
288
+ shimPath: resolveGitShimPath(),
282
289
  });
283
290
  if (!realGitPath) {
284
291
  console.error(
@@ -631,7 +638,11 @@ if (fs.existsSync(hourlyGitSyncPath)) {
631
638
  }
632
639
 
633
640
  const cronFilePath = "/etc/cron.d/openclaw-hourly-sync";
634
- if (cronEnabled) {
641
+ if (shouldSkipSystemCronInstall()) {
642
+ console.log(
643
+ "[alphaclaw] System cron setup skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
644
+ );
645
+ } else if (cronEnabled) {
635
646
  const cronContent = [
636
647
  "SHELL=/bin/bash",
637
648
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -722,6 +733,20 @@ if (fs.existsSync(path.join(openclawDir, ".git"))) {
722
733
  }
723
734
  }
724
735
 
736
+ if (fs.existsSync(configPath)) {
737
+ try {
738
+ execFileSync(process.execPath, [
739
+ path.join(__dirname, "..", "lib", "scripts", "migrate-openclaw-codex.js"),
740
+ ], {
741
+ env: process.env,
742
+ stdio: "inherit",
743
+ timeout: 60_000,
744
+ });
745
+ } catch (error) {
746
+ console.error(`[alphaclaw] Codex migration process failed: ${error.message}`);
747
+ }
748
+ }
749
+
725
750
  if (fs.existsSync(configPath)) {
726
751
  console.log("[alphaclaw] Config exists, reconciling channels...");
727
752
 
@@ -732,7 +757,10 @@ if (fs.existsSync(configPath)) {
732
757
  if (!cfg.plugins.load) cfg.plugins.load = {};
733
758
  if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
734
759
  if (!cfg.plugins.entries) cfg.plugins.entries = {};
735
- let changed = false;
760
+ let changed = migrateLegacyTelegramStreamingConfig(cfg);
761
+ if (changed) {
762
+ console.log("[alphaclaw] Migrated legacy Telegram streaming config");
763
+ }
736
764
 
737
765
  if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
738
766
  cfg.channels.telegram = {
@@ -819,11 +847,17 @@ try {
819
847
 
820
848
  try {
821
849
  const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
822
- const gitAskPassDest = "/tmp/alphaclaw-git-askpass.sh";
850
+ const gitAskPassDest = resolveGitAskPassPath({
851
+ tmpDir: os.tmpdir(),
852
+ });
823
853
  const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
824
- const gitShimDest = "/usr/local/bin/git";
854
+ const gitShimDest = resolveGitShimPath();
855
+ process.env.PATH = prependGitShimDirToPath({
856
+ shimPath: gitShimDest,
857
+ });
825
858
 
826
859
  if (fs.existsSync(gitAskPassSrc)) {
860
+ fs.mkdirSync(path.dirname(gitAskPassDest), { recursive: true });
827
861
  fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
828
862
  fs.chmodSync(gitAskPassDest, 0o755);
829
863
  }
@@ -837,7 +871,9 @@ try {
837
871
  const gitShimTemplate = fs.readFileSync(gitShimTemplatePath, "utf8");
838
872
  const gitShimContent = gitShimTemplate
839
873
  .replace("@@REAL_GIT@@", realGitPath)
840
- .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir);
874
+ .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir)
875
+ .replace("@@ASKPASS_PATH@@", gitAskPassDest);
876
+ fs.mkdirSync(path.dirname(gitShimDest), { recursive: true });
841
877
  fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
842
878
  console.log("[alphaclaw] git auth shim installed");
843
879
  }
@@ -2,6 +2,43 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
 
5
+ const isEnabledEnvFlag = (value) =>
6
+ ["1", "true", "yes", "on"].includes(
7
+ String(value || "").trim().toLowerCase(),
8
+ );
9
+
10
+ const shouldSkipSystemCronInstall = ({ env = process.env } = {}) =>
11
+ isEnabledEnvFlag(env.ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL);
12
+
13
+ const resolveGitAskPassPath = ({ env = process.env, tmpDir = "/tmp" } = {}) => {
14
+ const explicitPath = String(env.ALPHACLAW_GIT_ASKPASS_PATH || "").trim();
15
+ if (explicitPath) return explicitPath;
16
+
17
+ const runtimeTmpDir =
18
+ String(env.TMPDIR || "").trim() || String(tmpDir || "").trim() || "/tmp";
19
+ return path.join(runtimeTmpDir, "alphaclaw-git-askpass.sh");
20
+ };
21
+
22
+ const resolveGitShimPath = ({ env = process.env } = {}) => {
23
+ const explicitPath = String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim();
24
+ return explicitPath || "/usr/local/bin/git";
25
+ };
26
+
27
+ const prependGitShimDirToPath = ({
28
+ env = process.env,
29
+ shimPath = resolveGitShimPath({ env }),
30
+ } = {}) => {
31
+ if (!String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim()) {
32
+ return env.PATH || process.env.PATH || "";
33
+ }
34
+ const shimDir = path.dirname(String(shimPath || "").trim());
35
+ if (!shimDir || shimDir === ".") return env.PATH || process.env.PATH || "";
36
+ const pathEntries = String(env.PATH || process.env.PATH || "")
37
+ .split(path.delimiter)
38
+ .filter((entry) => entry && entry !== shimDir);
39
+ return [shimDir, ...pathEntries].join(path.delimiter);
40
+ };
41
+
5
42
  const normalizeGitSyncFilePath = (requestedFilePath) => {
6
43
  const rawPath = String(requestedFilePath || "").trim();
7
44
  if (!rawPath) return "";
@@ -90,6 +127,10 @@ const shouldRefreshHourlyGitSyncScript = ({
90
127
  };
91
128
 
92
129
  module.exports = {
130
+ shouldSkipSystemCronInstall,
131
+ resolveGitAskPassPath,
132
+ resolveGitShimPath,
133
+ prependGitShimDirToPath,
93
134
  normalizeGitSyncFilePath,
94
135
  validateGitSyncFilePath,
95
136
  resolveRealGitPath,
@@ -27,7 +27,7 @@ const resolveCurrentBranch = ({ execSyncImpl, openclawDir }) => {
27
27
 
28
28
  const createGitEnv = ({ fsModule, osModule, env, processId }) => {
29
29
  const githubToken = String(env.GITHUB_TOKEN || "").trim();
30
- const gitEnv = { ...env };
30
+ const gitEnv = { ...env, PATH: env.PATH || process.env.PATH };
31
31
  if (!githubToken) {
32
32
  return { gitEnv, askPassPath: "" };
33
33
  }
package/lib/scripts/git CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  REAL_GIT_HINT="@@REAL_GIT@@"
6
6
  OPENCLAW_REPO_ROOT="@@OPENCLAW_REPO_ROOT@@"
7
- ASKPASS_PATH="/tmp/alphaclaw-git-askpass.sh"
7
+ ASKPASS_PATH="@@ASKPASS_PATH@@"
8
8
 
9
9
  same_path() {
10
10
  local left_path right_path
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { migrateLegacyCodexState } = require("../server/openclaw-codex-migration");
4
+
5
+ migrateLegacyCodexState()
6
+ .then((result) => {
7
+ if (result.changed) {
8
+ console.log("[alphaclaw] Migrated legacy OpenAI Codex model and auth state");
9
+ }
10
+ for (const warning of result.warnings) {
11
+ console.warn(`[alphaclaw] Codex migration warning: ${warning}`);
12
+ }
13
+ })
14
+ .catch((error) => {
15
+ console.error(`[alphaclaw] Codex migration failed: ${error.message}`);
16
+ process.exitCode = 1;
17
+ });
@@ -505,12 +505,16 @@ const readPairedCountsByAccount = ({
505
505
  } catch {}
506
506
 
507
507
  for (const accountId of counts.keys()) {
508
- if (String(channelId || "").trim() === "whatsapp") continue;
509
508
  const accountConfig =
510
509
  accountId === "default" &&
511
510
  !(config.accounts && typeof config.accounts === "object")
512
511
  ? config
513
512
  : config.accounts?.[accountId] || {};
513
+ if (String(channelId || "").trim() === "whatsapp") {
514
+ if (!hasSavedWhatsAppCredentials({ fsImpl, OPENCLAW_DIR, accountId })) {
515
+ continue;
516
+ }
517
+ }
514
518
  const inlineAllowFrom = accountConfig?.allowFrom;
515
519
  if (!Array.isArray(inlineAllowFrom)) continue;
516
520
  counts.set(
@@ -616,7 +620,15 @@ const listConfiguredChannelAccounts = ({ fsImpl, OPENCLAW_DIR, cfg }) => {
616
620
  status:
617
621
  Number(pairedCounts.get(accountId) || 0) > 0
618
622
  ? "paired"
619
- : "configured",
623
+ : hasImplicitWhatsAppSelfPairing({
624
+ fsImpl,
625
+ OPENCLAW_DIR,
626
+ channelId,
627
+ accountId,
628
+ accountConfig,
629
+ })
630
+ ? "paired"
631
+ : "configured",
620
632
  };
621
633
  }),
622
634
  };
@@ -764,6 +776,10 @@ module.exports = {
764
776
  resolveCredentialsDirPath,
765
777
  resolveWhatsAppCredentialCandidatePaths,
766
778
  hasSavedWhatsAppCredentials,
779
+ normalizeChannelAccountId,
780
+ resolveCredentialPairingAccountId,
781
+ hasImplicitWhatsAppSelfPairing,
782
+ readPairedCountsByAccount,
767
783
  resolveAgentWorkspacePath,
768
784
  loadConfig,
769
785
  saveConfig,
@@ -281,7 +281,12 @@ const createAuthProfiles = () => {
281
281
 
282
282
  // ── Legacy Codex-specific wrappers ──
283
283
 
284
- const listCodexProfiles = () => listProfilesByProvider("openai-codex");
284
+ const listCodexProfiles = () =>
285
+ listProfiles().filter(
286
+ (profile) =>
287
+ profile.type === "oauth" &&
288
+ (profile.provider === "openai" || profile.provider === "openai-codex"),
289
+ );
285
290
 
286
291
  const getCodexProfile = () => {
287
292
  const profiles = listCodexProfiles();
@@ -299,7 +304,7 @@ const createAuthProfiles = () => {
299
304
  const upsertCodexProfile = ({ access, refresh, expires, accountId }) => {
300
305
  upsertProfile(CODEX_PROFILE_ID, {
301
306
  type: "oauth",
302
- provider: "openai-codex",
307
+ provider: "openai",
303
308
  access,
304
309
  refresh,
305
310
  expires,
@@ -311,7 +316,10 @@ const createAuthProfiles = () => {
311
316
  const store = loadAuthStore();
312
317
  let changed = false;
313
318
  for (const [id, cred] of Object.entries(store.profiles || {})) {
314
- if (cred?.provider === "openai-codex") {
319
+ if (
320
+ cred?.type === "oauth" &&
321
+ (cred.provider === "openai" || cred.provider === "openai-codex")
322
+ ) {
315
323
  delete store.profiles[id];
316
324
  changed = true;
317
325
  }
@@ -321,7 +329,10 @@ const createAuthProfiles = () => {
321
329
  if (!canSyncOpenclawAuthReferences()) return changed;
322
330
  let cfg = loadOpenclawConfig();
323
331
  for (const [id, cred] of Object.entries(cfg.auth?.profiles || {})) {
324
- if (cred?.provider === "openai-codex") {
332
+ if (
333
+ cred?.mode === "oauth" &&
334
+ (cred.provider === "openai" || cred.provider === "openai-codex")
335
+ ) {
325
336
  cfg = removeConfigAuthReference(cfg, id);
326
337
  }
327
338
  }
@@ -28,7 +28,7 @@ const AUTH_PROFILES_PATH = path.join(
28
28
  "agent",
29
29
  "auth-profiles.json",
30
30
  );
31
- const CODEX_PROFILE_ID = "openai-codex:codex-cli";
31
+ const CODEX_PROFILE_ID = "openai:codex-cli";
32
32
  const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
33
33
  const CODEX_OAUTH_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
34
34
  const CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
package/lib/server/env.js CHANGED
@@ -1,21 +1,48 @@
1
1
  const fs = require("fs");
2
2
  const { ENV_FILE_PATH, kKnownVars } = require("./constants");
3
3
 
4
+ const kSensitiveEnvKeyPattern = /token|key|password/i;
5
+ const kEnvWatchDebounceMs = 250;
6
+ let envWatchDebounceTimer = null;
7
+ let lastLoadedEnvSignature = null;
8
+ let pendingSelfWriteSignature = null;
9
+
10
+ const normalizeEnvVars = (vars) => {
11
+ const byKey = new Map();
12
+ for (const entry of vars || []) {
13
+ const key = String(entry?.key || "").trim();
14
+ if (!key) continue;
15
+ if (byKey.has(key)) byKey.delete(key);
16
+ byKey.set(key, {
17
+ key,
18
+ value: String(entry?.value || ""),
19
+ });
20
+ }
21
+ return Array.from(byKey.values());
22
+ };
23
+
24
+ const buildEnvSignature = (vars) =>
25
+ JSON.stringify(normalizeEnvVars(vars).map(({ key, value }) => [key, value]));
26
+
27
+ const readRawEnvFile = () => {
28
+ const content = fs.readFileSync(ENV_FILE_PATH, "utf8");
29
+ const vars = [];
30
+ for (const line of content.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith("#")) continue;
33
+ const eqIdx = trimmed.indexOf("=");
34
+ if (eqIdx === -1) continue;
35
+ vars.push({
36
+ key: trimmed.slice(0, eqIdx).trim(),
37
+ value: trimmed.slice(eqIdx + 1),
38
+ });
39
+ }
40
+ return vars;
41
+ };
42
+
4
43
  const readEnvFile = () => {
5
44
  try {
6
- const content = fs.readFileSync(ENV_FILE_PATH, "utf8");
7
- const vars = [];
8
- for (const line of content.split("\n")) {
9
- const trimmed = line.trim();
10
- if (!trimmed || trimmed.startsWith("#")) continue;
11
- const eqIdx = trimmed.indexOf("=");
12
- if (eqIdx === -1) continue;
13
- vars.push({
14
- key: trimmed.slice(0, eqIdx),
15
- value: trimmed.slice(eqIdx + 1),
16
- });
17
- }
18
- return vars;
45
+ return normalizeEnvVars(readRawEnvFile());
19
46
  } catch {
20
47
  return [];
21
48
  }
@@ -23,22 +50,24 @@ const readEnvFile = () => {
23
50
 
24
51
  const writeEnvFile = (vars) => {
25
52
  const lines = [];
26
- for (const { key, value } of vars || []) {
27
- if (!key) continue;
53
+ const normalizedVars = normalizeEnvVars(vars);
54
+ for (const { key, value } of normalizedVars) {
28
55
  lines.push(`${key}=${String(value || "")}`);
29
56
  }
30
57
  fs.writeFileSync(ENV_FILE_PATH, lines.join("\n"));
58
+ pendingSelfWriteSignature = buildEnvSignature(normalizedVars);
31
59
  };
32
60
 
33
61
  const reloadEnv = () => {
34
62
  const vars = readEnvFile();
63
+ const signature = buildEnvSignature(vars);
35
64
  const fileKeys = new Set(vars.map((v) => v.key));
36
65
  let changed = false;
37
66
 
38
67
  for (const { key, value } of vars) {
39
68
  if (value && value !== process.env[key]) {
40
69
  console.log(
41
- `[alphaclaw] Env updated: ${key}=${key.toLowerCase().includes("token") || key.toLowerCase().includes("key") || key.toLowerCase().includes("password") ? "***" : value}`,
70
+ `[alphaclaw] Env updated: ${key}=${kSensitiveEnvKeyPattern.test(key) ? "***" : value}`,
42
71
  );
43
72
  process.env[key] = value;
44
73
  changed = true;
@@ -58,21 +87,43 @@ const reloadEnv = () => {
58
87
  }
59
88
  }
60
89
 
90
+ lastLoadedEnvSignature = signature;
61
91
  return changed;
62
92
  };
63
93
 
94
+ const readEnvFileSignature = () => {
95
+ try {
96
+ return buildEnvSignature(readRawEnvFile());
97
+ } catch {
98
+ return null;
99
+ }
100
+ };
101
+
64
102
  const startEnvWatcher = () => {
65
103
  try {
66
104
  fs.watchFile(ENV_FILE_PATH, { interval: 2000 }, () => {
67
- console.log(
68
- `[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
69
- );
70
- reloadEnv();
105
+ if (envWatchDebounceTimer) clearTimeout(envWatchDebounceTimer);
106
+ envWatchDebounceTimer = setTimeout(() => {
107
+ envWatchDebounceTimer = null;
108
+ const signature = readEnvFileSignature();
109
+ if (signature && signature === pendingSelfWriteSignature) {
110
+ pendingSelfWriteSignature = null;
111
+ lastLoadedEnvSignature = signature;
112
+ return;
113
+ }
114
+ pendingSelfWriteSignature = null;
115
+ if (signature && signature === lastLoadedEnvSignature) return;
116
+ console.log(
117
+ `[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
118
+ );
119
+ reloadEnv();
120
+ }, kEnvWatchDebounceMs);
71
121
  });
72
122
  } catch {}
73
123
  };
74
124
 
75
125
  module.exports = {
126
+ normalizeEnvVars,
76
127
  readEnvFile,
77
128
  writeEnvFile,
78
129
  reloadEnv,
@@ -11,6 +11,10 @@ const {
11
11
  kOnboardingMarkerPath,
12
12
  kRootDir,
13
13
  } = require("./constants");
14
+ const {
15
+ normalizeChannelAccountId,
16
+ readPairedCountsByAccount,
17
+ } = require("./agents/shared");
14
18
  const { withOpenclawStartupEnv } = require("./openclaw-runtime-env");
15
19
  const { isOpenAiCompatApiEnabled } = require("./alphaclaw-config");
16
20
 
@@ -203,18 +207,6 @@ const getGatewayPort = () => {
203
207
 
204
208
  const getGatewayUrl = () => `http://${GATEWAY_HOST}:${getGatewayPort()}`;
205
209
 
206
- const normalizeChannelAccountId = (value) => String(value || "").trim() || "default";
207
-
208
- const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
209
- const prefix = `${String(channel || "").trim()}-`;
210
- const suffix = "-allowFrom.json";
211
- if (!String(fileName || "").startsWith(prefix) || !String(fileName || "").endsWith(suffix)) {
212
- return "";
213
- }
214
- const rawAccountId = String(fileName || "").slice(prefix.length, -suffix.length);
215
- return normalizeChannelAccountId(rawAccountId);
216
- };
217
-
218
210
  const isGatewayRunning = () =>
219
211
  new Promise((resolve) => {
220
212
  const sock = net.createConnection(getGatewayPort(), GATEWAY_HOST);
@@ -781,32 +773,6 @@ const getChannelStatus = () => {
781
773
  );
782
774
  const credDir = `${OPENCLAW_DIR}/credentials`;
783
775
  const channels = {};
784
- const hasImplicitWhatsAppSelfPairing = ({ accountId, accountConfig }) => {
785
- if (!accountConfig || typeof accountConfig !== "object") return false;
786
- if (accountConfig.selfChatMode === false) return false;
787
- if (String(accountConfig.dmPolicy || "").trim().toLowerCase() === "disabled") {
788
- return false;
789
- }
790
- const candidatePaths = [
791
- `${credDir}/whatsapp/${accountId}/creds.json`,
792
- ...(accountId === "default" ? [`${credDir}/creds.json`] : []),
793
- ];
794
- const matches = candidatePaths.map((targetPath) => {
795
- try {
796
- return {
797
- path: targetPath,
798
- exists: !!String(fs.readFileSync(targetPath, "utf8") || "").trim(),
799
- };
800
- } catch (error) {
801
- return {
802
- path: targetPath,
803
- exists: false,
804
- error: String(error?.message || error || "read failed"),
805
- };
806
- }
807
- });
808
- return matches.some((entry) => entry.exists);
809
- };
810
776
 
811
777
  for (const ch of Object.keys(kChannelDefs)) {
812
778
  const channelConfig =
@@ -836,55 +802,14 @@ const getChannelStatus = () => {
836
802
  });
837
803
  if (!hasConfiguredToken) continue;
838
804
 
839
- const pairedByAccount = new Map(
840
- Array.from(configuredAccountIds).map((accountId) => [accountId, 0]),
841
- );
842
- try {
843
- if (ch !== "whatsapp") {
844
- const files = fs
845
- .readdirSync(credDir)
846
- .filter(
847
- (f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
848
- );
849
- for (const file of files) {
850
- const accountId = resolveCredentialPairingAccountId({
851
- channel: ch,
852
- fileName: file,
853
- });
854
- if (!accountId || !configuredAccountIds.has(accountId)) continue;
855
- const data = JSON.parse(
856
- fs.readFileSync(`${credDir}/${file}`, "utf8"),
857
- );
858
- const nextCount =
859
- Number(pairedByAccount.get(accountId) || 0)
860
- + (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
861
- pairedByAccount.set(accountId, nextCount);
862
- }
863
- }
864
- } catch {}
865
- for (const [accountId, accountConfig] of accountEntries) {
866
- if (ch === "whatsapp") continue;
867
- const inlineAllowFrom = accountConfig?.allowFrom;
868
- if (!Array.isArray(inlineAllowFrom)) continue;
869
- const normalizedAccountId = normalizeChannelAccountId(accountId);
870
- const nextCount =
871
- Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
872
- pairedByAccount.set(normalizedAccountId, nextCount);
873
- }
874
- if (ch === "whatsapp") {
875
- for (const [accountId, accountConfig] of accountEntries) {
876
- const normalizedAccountId = normalizeChannelAccountId(accountId);
877
- if (Number(pairedByAccount.get(normalizedAccountId) || 0) > 0) continue;
878
- if (
879
- hasImplicitWhatsAppSelfPairing({
880
- accountId: normalizedAccountId,
881
- accountConfig,
882
- })
883
- ) {
884
- pairedByAccount.set(normalizedAccountId, 1);
885
- }
886
- }
887
- }
805
+ const pairedByAccount = readPairedCountsByAccount({
806
+ fsImpl: fs,
807
+ OPENCLAW_DIR,
808
+ channelId: ch,
809
+ accountIds: Array.from(configuredAccountIds),
810
+ config: channelConfig,
811
+ });
812
+
888
813
  const accounts = Object.fromEntries(
889
814
  Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
890
815
  accountId,
@@ -1,6 +1,7 @@
1
1
  const path = require("path");
2
2
  const { kSetupDir } = require("../constants");
3
3
  const { buildManagedPaths } = require("../internal-files-migration");
4
+ const { shouldSkipSystemCronInstall } = require("../../cli/git-runtime");
4
5
 
5
6
  const kHourlyGitSyncTemplatePath = path.join(kSetupDir, "hourly-git-sync.sh");
6
7
  const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
@@ -37,6 +38,13 @@ const installHourlyGitSyncCron = async ({ fs, openclawDir }) => {
37
38
  fs.mkdirSync(configDir, { recursive: true });
38
39
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
39
40
 
41
+ if (shouldSkipSystemCronInstall()) {
42
+ console.log(
43
+ "[onboard] System cron install skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
44
+ );
45
+ return false;
46
+ }
47
+
40
48
  const cronContent = buildSystemCronFile({
41
49
  schedule: config.schedule,
42
50
  scriptPath: hourlyGitSyncPath,
@@ -0,0 +1,78 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { pathToFileURL } = require("url");
4
+
5
+ const findOpenclawDistModule = (prefix) => {
6
+ const entryPath = require.resolve("openclaw");
7
+ const distDir = path.dirname(entryPath);
8
+ const filename = fs
9
+ .readdirSync(distDir)
10
+ .find((name) => name.startsWith(`${prefix}-`) && name.endsWith(".js"));
11
+ if (!filename) throw new Error(`OpenClaw migration module not found: ${prefix}`);
12
+ return path.join(distDir, filename);
13
+ };
14
+
15
+ const writeConfig = (configPath, cfg) => {
16
+ fs.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
17
+ };
18
+
19
+ const migrateLegacyCodexState = async ({
20
+ configPath = process.env.OPENCLAW_CONFIG_PATH,
21
+ env = process.env,
22
+ } = {}) => {
23
+ if (!configPath || !fs.existsSync(configPath)) {
24
+ return { changed: false, changes: [], warnings: [] };
25
+ }
26
+
27
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
28
+ const routeModule = await import(
29
+ pathToFileURL(findOpenclawDistModule("codex-route-warnings")).href
30
+ );
31
+ const authModule = await import(
32
+ pathToFileURL(findOpenclawDistModule("doctor-auth-flat-profiles")).href
33
+ );
34
+
35
+ const changes = [];
36
+ const warnings = [];
37
+ const routeRepair = routeModule.r({ cfg, env, shouldRepair: true });
38
+ let nextCfg = routeRepair.cfg;
39
+ changes.push(...routeRepair.changes);
40
+ warnings.push(...routeRepair.warnings);
41
+
42
+ const profileIdMap = authModule.t({ cfg: nextCfg, env });
43
+ const configAuthRepair = authModule.a(nextCfg, { profileIdMap });
44
+ nextCfg = configAuthRepair.config;
45
+ changes.push(...configAuthRepair.changes);
46
+ warnings.push(...configAuthRepair.warnings);
47
+
48
+ if (changes.length > 0) writeConfig(configPath, nextCfg);
49
+
50
+ const storeRepair = await authModule.o({ cfg: nextCfg, env });
51
+ changes.push(...storeRepair.changes);
52
+ warnings.push(...storeRepair.warnings);
53
+
54
+ const sqliteMigration = await authModule.n({
55
+ cfg: nextCfg,
56
+ env,
57
+ prompter: { confirmAutoFix: async () => true },
58
+ });
59
+ changes.push(...sqliteMigration.changes);
60
+ warnings.push(...sqliteMigration.warnings);
61
+ if (sqliteMigration.configChanged) writeConfig(configPath, nextCfg);
62
+
63
+ const sessionRepair = await routeModule.i({
64
+ cfg: nextCfg,
65
+ env,
66
+ shouldRepair: true,
67
+ });
68
+ changes.push(...sessionRepair.changes);
69
+ warnings.push(...sessionRepair.warnings);
70
+
71
+ return {
72
+ changed: changes.length > 0,
73
+ changes,
74
+ warnings,
75
+ };
76
+ };
77
+
78
+ module.exports = { migrateLegacyCodexState };
@@ -0,0 +1,77 @@
1
+ const isRecord = (value) =>
2
+ !!value && typeof value === "object" && !Array.isArray(value);
3
+
4
+ const parseStreamingMode = (value) => {
5
+ if (typeof value === "boolean") return value ? "partial" : "off";
6
+ const normalized = String(value || "").trim().toLowerCase();
7
+ return ["off", "partial", "block", "progress"].includes(normalized)
8
+ ? normalized
9
+ : null;
10
+ };
11
+
12
+ const migrateLegacyTelegramStreamingEntry = (entry) => {
13
+ if (!isRecord(entry)) return false;
14
+
15
+ const legacyStreaming = entry.streaming;
16
+ const hasLegacyStreaming =
17
+ typeof legacyStreaming === "boolean" || typeof legacyStreaming === "string";
18
+ const hasLegacyFields =
19
+ entry.streamMode !== undefined ||
20
+ entry.chunkMode !== undefined ||
21
+ entry.blockStreaming !== undefined ||
22
+ entry.blockStreamingCoalesce !== undefined ||
23
+ entry.draftChunk !== undefined;
24
+ if (!hasLegacyStreaming && !hasLegacyFields) return false;
25
+
26
+ const streaming = isRecord(legacyStreaming) ? { ...legacyStreaming } : {};
27
+ const resolvedMode =
28
+ parseStreamingMode(isRecord(legacyStreaming) ? legacyStreaming.mode : legacyStreaming) ||
29
+ parseStreamingMode(entry.streamMode) ||
30
+ "partial";
31
+ if (streaming.mode === undefined) streaming.mode = resolvedMode;
32
+
33
+ if (entry.chunkMode !== undefined && streaming.chunkMode === undefined) {
34
+ streaming.chunkMode = entry.chunkMode;
35
+ }
36
+ if (entry.draftChunk !== undefined) {
37
+ const preview = isRecord(streaming.preview) ? { ...streaming.preview } : {};
38
+ if (preview.chunk === undefined) preview.chunk = entry.draftChunk;
39
+ streaming.preview = preview;
40
+ }
41
+ if (entry.blockStreaming !== undefined || entry.blockStreamingCoalesce !== undefined) {
42
+ const block = isRecord(streaming.block) ? { ...streaming.block } : {};
43
+ if (entry.blockStreaming !== undefined && block.enabled === undefined) {
44
+ block.enabled = entry.blockStreaming;
45
+ }
46
+ if (entry.blockStreamingCoalesce !== undefined && block.coalesce === undefined) {
47
+ block.coalesce = entry.blockStreamingCoalesce;
48
+ }
49
+ streaming.block = block;
50
+ }
51
+
52
+ entry.streaming = streaming;
53
+ delete entry.streamMode;
54
+ delete entry.chunkMode;
55
+ delete entry.blockStreaming;
56
+ delete entry.blockStreamingCoalesce;
57
+ delete entry.draftChunk;
58
+ return true;
59
+ };
60
+
61
+ const migrateLegacyTelegramStreamingConfig = (cfg = {}) => {
62
+ const telegram = cfg.channels?.telegram;
63
+ if (!isRecord(telegram)) return false;
64
+
65
+ let changed = migrateLegacyTelegramStreamingEntry(telegram);
66
+ if (isRecord(telegram.accounts)) {
67
+ for (const account of Object.values(telegram.accounts)) {
68
+ if (migrateLegacyTelegramStreamingEntry(account)) changed = true;
69
+ }
70
+ }
71
+ return changed;
72
+ };
73
+
74
+ module.exports = {
75
+ migrateLegacyTelegramStreamingConfig,
76
+ migrateLegacyTelegramStreamingEntry,
77
+ };
@@ -27,6 +27,31 @@ const loadThinkingModule = async () => {
27
27
  return thinkingModulePromise;
28
28
  };
29
29
 
30
+ const isNormalizeThinkLevel = (candidate) => {
31
+ if (typeof candidate !== "function") return false;
32
+ try {
33
+ return (
34
+ candidate("off") === "off" &&
35
+ candidate("low") === "low" &&
36
+ candidate("high") === "high"
37
+ );
38
+ } catch {
39
+ return false;
40
+ }
41
+ };
42
+
43
+ const resolveNormalizeThinkLevel = (mod) => {
44
+ const candidates = [
45
+ mod.normalizeThinkLevel,
46
+ mod.f,
47
+ mod.p,
48
+ ...Object.values(mod),
49
+ ];
50
+ const match = candidates.find(isNormalizeThinkLevel);
51
+ if (!match) throw new Error("OpenClaw normalizeThinkLevel export not found");
52
+ return match;
53
+ };
54
+
30
55
  const splitModelKey = (modelKey = "") => {
31
56
  const normalized = String(modelKey || "").trim();
32
57
  const slashIndex = normalized.indexOf("/");
@@ -55,7 +80,7 @@ const resolveThinkingApi = async () => {
55
80
  return {
56
81
  listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i,
57
82
  resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s,
58
- normalizeThinkLevel: mod.normalizeThinkLevel || mod.p,
83
+ normalizeThinkLevel: resolveNormalizeThinkLevel(mod),
59
84
  };
60
85
  };
61
86
 
@@ -1,5 +1,6 @@
1
1
  const { buildManagedPaths } = require("../internal-files-migration");
2
2
  const { readOpenclawConfig } = require("../openclaw-config");
3
+ const { shouldSkipSystemCronInstall } = require("../../cli/git-runtime");
3
4
  const {
4
5
  readAlphaclawConfig,
5
6
  updateOpenAiCompatApiFeature,
@@ -388,6 +389,9 @@ const registerSystemRoutes = ({
388
389
  kSystemCronConfigPath,
389
390
  JSON.stringify(nextConfig, null, 2),
390
391
  );
392
+ if (shouldSkipSystemCronInstall()) {
393
+ return getSystemCronStatus();
394
+ }
391
395
  if (nextConfig.enabled) {
392
396
  fs.writeFileSync(
393
397
  kSystemCronPath,
@@ -1,5 +1,8 @@
1
1
  const path = require("path");
2
2
  const { readOpenclawConfig, writeOpenclawConfig } = require("./openclaw-config");
3
+ const {
4
+ migrateLegacyTelegramStreamingConfig,
5
+ } = require("./openclaw-config-migrations");
3
6
 
4
7
  const kUsageTrackerPluginPath = path.resolve(
5
8
  __dirname,
@@ -123,7 +126,8 @@ const ensureUsageTrackerPluginConfig = ({ fsModule, openclawDir }) => {
123
126
  openclawDir,
124
127
  fallback: {},
125
128
  });
126
- const changed = reconcileManagedPluginConfig(cfg);
129
+ const migrated = migrateLegacyTelegramStreamingConfig(cfg);
130
+ const changed = reconcileManagedPluginConfig(cfg) || migrated;
127
131
  if (!changed) return false;
128
132
  writeOpenclawConfig({
129
133
  fsModule,
@@ -12,6 +12,23 @@ if [[ -f "$REPO/.env" ]]; then
12
12
  set +a
13
13
  fi
14
14
 
15
+ if [[ -f "$REPO/cron/system-sync.json" ]]; then
16
+ if node - "$REPO/cron/system-sync.json" <<'NODE'
17
+ const fs = require('fs');
18
+ const file = process.argv[2];
19
+ try {
20
+ const config = JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ process.exit(config && config.enabled === false ? 0 : 1);
22
+ } catch {
23
+ process.exit(1);
24
+ }
25
+ NODE
26
+ then
27
+ echo "hourly-git-sync: disabled by cron/system-sync.json"
28
+ exit 0
29
+ fi
30
+ fi
31
+
15
32
  # Drop cron scheduler runtime-only churn when it is metadata/timestamp-only.
16
33
  maybe_restore_if_runtime_only() {
17
34
  local file="$1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.18",
3
+ "version": "0.9.20",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "express": "^4.21.0",
35
35
  "http-proxy": "^1.18.1",
36
- "openclaw": "2026.5.28",
36
+ "openclaw": "2026.6.11",
37
37
  "ws": "^8.19.0"
38
38
  },
39
39
  "devDependencies": {
@@ -51,6 +51,6 @@
51
51
  "wouter-preact": "^3.7.1"
52
52
  },
53
53
  "engines": {
54
- "node": ">=22.14.0"
54
+ "node": ">=22.19.0"
55
55
  }
56
56
  }