@chrysb/alphaclaw 0.9.9 → 0.9.11

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.
Files changed (43) hide show
  1. package/lib/public/dist/app.bundle.js +1773 -1747
  2. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +4 -5
  3. package/lib/public/js/components/agents-tab/agent-pairing-section.js +11 -5
  4. package/lib/public/js/components/cron-tab/cron-helpers.js +13 -1
  5. package/lib/public/js/components/cron-tab/use-cron-tab.js +4 -3
  6. package/lib/public/js/components/general/index.js +6 -1
  7. package/lib/public/js/components/general/use-general-tab.js +17 -20
  8. package/lib/public/js/components/models-tab/index.js +5 -1
  9. package/lib/public/js/components/models-tab/model-picker.js +52 -0
  10. package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -1
  11. package/lib/public/js/components/pairings.js +75 -4
  12. package/lib/public/js/components/welcome/use-welcome.js +37 -8
  13. package/lib/public/js/hooks/usePolling.js +46 -13
  14. package/lib/public/js/lib/model-config.js +6 -2
  15. package/lib/server/agents/channels.js +53 -9
  16. package/lib/server/commands.js +4 -1
  17. package/lib/server/constants.js +14 -3
  18. package/lib/server/cost-utils.js +9 -0
  19. package/lib/server/cron-service.js +12 -1
  20. package/lib/server/db/doctor/index.js +9 -0
  21. package/lib/server/db/usage/index.js +13 -0
  22. package/lib/server/db/watchdog/index.js +13 -1
  23. package/lib/server/db/webhooks/index.js +13 -1
  24. package/lib/server/gateway.js +119 -8
  25. package/lib/server/init/register-server-routes.js +3 -0
  26. package/lib/server/init/runtime-init.js +2 -0
  27. package/lib/server/internal-files-migration.js +11 -1
  28. package/lib/server/model-catalog-bootstrap.json +3193 -0
  29. package/lib/server/model-catalog-cache.js +124 -32
  30. package/lib/server/onboarding/github.js +79 -2
  31. package/lib/server/onboarding/index.js +2 -9
  32. package/lib/server/onboarding/openclaw.js +18 -4
  33. package/lib/server/openclaw-runtime-env.js +55 -0
  34. package/lib/server/openclaw-version.js +2 -1
  35. package/lib/server/routes/models.js +28 -0
  36. package/lib/server/routes/pairings.js +106 -15
  37. package/lib/server/usage-tracker-config.js +28 -3
  38. package/lib/server/utils/command-output.js +11 -0
  39. package/lib/server.js +4 -0
  40. package/lib/setup/gitignore +2 -0
  41. package/package.json +2 -2
  42. package/patches/openclaw+2026.4.23.patch +63 -0
  43. package/patches/openclaw+2026.4.15.patch +0 -13
@@ -1,9 +1,12 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { ALPHACLAW_DIR, kFallbackOnboardingModels } = require("./constants");
4
+ const { getCommandOutputCandidates } = require("./utils/command-output");
4
5
 
5
6
  const kModelCatalogCacheVersion = 1;
6
7
  const kModelCatalogRefreshBackoffMs = 30 * 1000;
8
+ const kModelCatalogLoadTimeoutMs = 120 * 1000;
9
+ const kModelCatalogBootstrapSource = "bootstrap";
7
10
  const kDefaultCachePath = path.join(ALPHACLAW_DIR, "cache", "model-catalog.json");
8
11
 
9
12
  const createResponse = ({
@@ -21,6 +24,12 @@ const createResponse = ({
21
24
  models,
22
25
  });
23
26
 
27
+ const normalizeOpenclawVersion = (value) => {
28
+ if (typeof value !== "string") return null;
29
+ const normalized = value.trim();
30
+ return normalized || null;
31
+ };
32
+
24
33
  const normalizeCachedModels = ({
25
34
  models,
26
35
  normalizeOnboardingModels = (items) => items,
@@ -48,10 +57,20 @@ const normalizeCacheEntry = ({
48
57
  return {
49
58
  version: kModelCatalogCacheVersion,
50
59
  fetchedAt,
60
+ openclawVersion: normalizeOpenclawVersion(raw.openclawVersion),
51
61
  models,
52
62
  };
53
63
  };
54
64
 
65
+ const parseCatalogModelsFromOutput = ({
66
+ rawOutput,
67
+ parseJsonFromNoisyOutput = () => ({}),
68
+ normalizeOnboardingModels = (items) => items,
69
+ } = {}) => {
70
+ const parsed = parseJsonFromNoisyOutput(rawOutput);
71
+ return normalizeOnboardingModels(parsed?.models || []);
72
+ };
73
+
55
74
  const createModelCatalogCache = ({
56
75
  fsModule = fs,
57
76
  pathModule = path,
@@ -59,6 +78,8 @@ const createModelCatalogCache = ({
59
78
  gatewayEnv = () => ({}),
60
79
  parseJsonFromNoisyOutput = () => ({}),
61
80
  normalizeOnboardingModels = (items) => items,
81
+ readOpenclawVersion = () => null,
82
+ shouldStartDynamicRefresh = () => true,
62
83
  fallbackModels = kFallbackOnboardingModels,
63
84
  cachePath = kDefaultCachePath,
64
85
  refreshBackoffMs = kModelCatalogRefreshBackoffMs,
@@ -74,6 +95,23 @@ const createModelCatalogCache = ({
74
95
  let retryTimer = null;
75
96
  let backoffUntilMs = 0;
76
97
 
98
+ const readCurrentOpenclawVersion = ({ refresh = false } = {}) => {
99
+ try {
100
+ return normalizeOpenclawVersion(readOpenclawVersion({ refresh }));
101
+ } catch {
102
+ return null;
103
+ }
104
+ };
105
+
106
+ const isCompatibleWithCurrentOpenclaw = ({
107
+ entry,
108
+ currentOpenclawVersion,
109
+ } = {}) => {
110
+ if (!entry) return false;
111
+ if (!currentOpenclawVersion) return true;
112
+ return entry.openclawVersion === currentOpenclawVersion;
113
+ };
114
+
77
115
  const clearRetryTimer = () => {
78
116
  if (!retryTimer) return;
79
117
  clearTimeoutFn(retryTimer);
@@ -82,6 +120,14 @@ const createModelCatalogCache = ({
82
120
 
83
121
  const isRefreshPending = () => !!refreshPromise || !!retryTimer;
84
122
 
123
+ const canStartDynamicRefresh = () => {
124
+ try {
125
+ return shouldStartDynamicRefresh() !== false;
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
130
+
85
131
  const setCacheEntry = (entry, { fresh = false } = {}) => {
86
132
  memoryCache = entry;
87
133
  cacheLoaded = true;
@@ -121,41 +167,76 @@ const createModelCatalogCache = ({
121
167
  };
122
168
 
123
169
  const loadFreshCatalog = async () => {
124
- const output = await shellCmd("openclaw models list --all --json", {
125
- env: gatewayEnv(),
126
- timeout: 30000,
127
- });
128
- const parsed = parseJsonFromNoisyOutput(output);
129
- const models = normalizeOnboardingModels(parsed?.models || []);
170
+ const openclawVersion = readCurrentOpenclawVersion({ refresh: true });
171
+ let models = [];
172
+ let recoveredFromCommandError = false;
173
+ try {
174
+ const output = await shellCmd("openclaw models list --all --json", {
175
+ env: gatewayEnv(),
176
+ timeout: kModelCatalogLoadTimeoutMs,
177
+ });
178
+ models = parseCatalogModelsFromOutput({
179
+ rawOutput: output,
180
+ parseJsonFromNoisyOutput,
181
+ normalizeOnboardingModels,
182
+ });
183
+ } catch (err) {
184
+ for (const rawOutput of getCommandOutputCandidates(err)) {
185
+ models = parseCatalogModelsFromOutput({
186
+ rawOutput,
187
+ parseJsonFromNoisyOutput,
188
+ normalizeOnboardingModels,
189
+ });
190
+ if (models.length > 0) {
191
+ recoveredFromCommandError = true;
192
+ logger.warn?.(
193
+ `[models] Recovered model catalog from failed command output: ${err.message || String(err)}`,
194
+ );
195
+ break;
196
+ }
197
+ }
198
+ if (models.length === 0) throw err;
199
+ }
130
200
  if (models.length === 0) {
131
201
  throw new Error("No models found");
132
202
  }
133
203
  const entry = {
134
204
  version: kModelCatalogCacheVersion,
135
205
  fetchedAt: now(),
206
+ openclawVersion,
136
207
  models,
137
208
  };
138
209
  writeDiskCache(entry);
139
210
  setCacheEntry(entry, { fresh: true });
211
+ if (recoveredFromCommandError) {
212
+ backoffUntilMs = 0;
213
+ clearRetryTimer();
214
+ }
140
215
  return entry;
141
216
  };
142
217
 
143
218
  const scheduleRetry = () => {
144
- if (!memoryCache || retryTimer) return;
219
+ if (!canStartDynamicRefresh()) {
220
+ clearRetryTimer();
221
+ return;
222
+ }
223
+ if (retryTimer) return;
145
224
  const delayMs = Math.max(backoffUntilMs - now(), 0);
146
225
  retryTimer = setTimeoutFn(() => {
147
226
  retryTimer = null;
148
- if (!memoryCache || !cacheIsStale || refreshPromise) return;
227
+ if (!canStartDynamicRefresh()) return;
228
+ if (refreshPromise) return;
229
+ if (memoryCache && !cacheIsStale) return;
149
230
  void startBackgroundRefresh();
150
231
  }, delayMs);
151
232
  if (typeof retryTimer?.unref === "function") retryTimer.unref();
152
233
  };
153
234
 
154
235
  const handleRefreshFailure = (err) => {
236
+ backoffUntilMs = now() + refreshBackoffMs;
237
+ scheduleRetry();
155
238
  if (memoryCache) {
156
239
  cacheIsStale = true;
157
- backoffUntilMs = now() + refreshBackoffMs;
158
- scheduleRetry();
159
240
  logger.error?.(
160
241
  `[models] Failed to refresh cached models: ${err.message || String(err)}`,
161
242
  );
@@ -167,8 +248,11 @@ const createModelCatalogCache = ({
167
248
  };
168
249
 
169
250
  const startBackgroundRefresh = () => {
251
+ if (!canStartDynamicRefresh()) {
252
+ clearRetryTimer();
253
+ return null;
254
+ }
170
255
  readDiskCache();
171
- if (!memoryCache) return null;
172
256
  if (refreshPromise) return refreshPromise;
173
257
  if (retryTimer) return null;
174
258
  if (backoffUntilMs > now()) {
@@ -190,6 +274,21 @@ const createModelCatalogCache = ({
190
274
  return {
191
275
  async getCatalogResponse() {
192
276
  readDiskCache();
277
+ if (memoryCache && !cacheIsStale) {
278
+ const currentOpenclawVersion = readCurrentOpenclawVersion({
279
+ refresh: true,
280
+ });
281
+ if (
282
+ !isCompatibleWithCurrentOpenclaw({
283
+ entry: memoryCache,
284
+ currentOpenclawVersion,
285
+ })
286
+ ) {
287
+ cacheIsStale = true;
288
+ backoffUntilMs = 0;
289
+ clearRetryTimer();
290
+ }
291
+ }
193
292
  if (memoryCache && !cacheIsStale) {
194
293
  return createResponse({
195
294
  source: "openclaw",
@@ -200,34 +299,25 @@ const createModelCatalogCache = ({
200
299
  });
201
300
  }
202
301
  if (memoryCache) {
203
- startBackgroundRefresh();
302
+ const didStartRefresh = !!startBackgroundRefresh();
204
303
  return createResponse({
205
304
  source: "cache",
206
305
  fetchedAt: memoryCache.fetchedAt,
207
306
  stale: true,
208
- refreshing: isRefreshPending(),
307
+ refreshing:
308
+ canStartDynamicRefresh() && (didStartRefresh || isRefreshPending()),
209
309
  models: memoryCache.models,
210
310
  });
211
311
  }
212
- try {
213
- const freshEntry = await loadFreshCatalog();
214
- return createResponse({
215
- source: "openclaw",
216
- fetchedAt: freshEntry.fetchedAt,
217
- stale: false,
218
- refreshing: false,
219
- models: freshEntry.models,
220
- });
221
- } catch (err) {
222
- handleRefreshFailure(err);
223
- return createResponse({
224
- source: "fallback",
225
- fetchedAt: null,
226
- stale: false,
227
- refreshing: false,
228
- models: fallbackModels,
229
- });
230
- }
312
+ const didStartRefresh = !!startBackgroundRefresh();
313
+ return createResponse({
314
+ source: kModelCatalogBootstrapSource,
315
+ fetchedAt: null,
316
+ stale: true,
317
+ refreshing:
318
+ canStartDynamicRefresh() && (didStartRefresh || isRefreshPending()),
319
+ models: fallbackModels,
320
+ });
231
321
  },
232
322
 
233
323
  markStale() {
@@ -247,5 +337,7 @@ module.exports = {
247
337
  normalizeCacheEntry,
248
338
  kModelCatalogCacheVersion,
249
339
  kModelCatalogRefreshBackoffMs,
340
+ kModelCatalogLoadTimeoutMs,
341
+ kModelCatalogBootstrapSource,
250
342
  kDefaultCachePath,
251
343
  };
@@ -113,6 +113,42 @@ const findOwnedRepoByName = async ({
113
113
  return null;
114
114
  };
115
115
 
116
+ const findAccessibleOrgByLogin = async ({ repoOwner, ghHeaders }) => {
117
+ const normalizedRepoOwner = String(repoOwner || "").trim().toLowerCase();
118
+ if (!normalizedRepoOwner) return null;
119
+
120
+ let nextUrl = "https://api.github.com/user/orgs?per_page=100&page=1";
121
+ while (nextUrl) {
122
+ const res = await fetch(nextUrl, { headers: ghHeaders });
123
+ if (!res.ok) {
124
+ const details = await parseGithubErrorMessage(res);
125
+ return {
126
+ error:
127
+ `Cannot verify organization "${repoOwner}" access: ${details}. ` +
128
+ "Check the owner name or use a token that can access that organization.",
129
+ };
130
+ }
131
+
132
+ const orgs = await res.json();
133
+ if (!Array.isArray(orgs)) {
134
+ return {
135
+ error: `Cannot verify organization "${repoOwner}" access from GitHub response.`,
136
+ };
137
+ }
138
+
139
+ const org = orgs.find(
140
+ (item) =>
141
+ String(item?.login || "").trim().toLowerCase() ===
142
+ normalizedRepoOwner,
143
+ );
144
+ if (org) return { org };
145
+
146
+ nextUrl = getNextGithubPageUrl(res.headers?.get?.("link"));
147
+ }
148
+
149
+ return null;
150
+ };
151
+
116
152
  const isClassicPat = (token) => String(token || "").startsWith("ghp_");
117
153
  const isFineGrainedPat = (token) =>
118
154
  String(token || "").startsWith("github_pat_");
@@ -187,7 +223,43 @@ const verifyGithubRepoForOnboarding = async ({
187
223
  error: `Repository "${repoUrl}" not found. Check the repo name and token permissions.`,
188
224
  };
189
225
  }
190
- return { ok: true, repoExists: false, repoIsEmpty: false };
226
+ if (!viewerLogin) {
227
+ return {
228
+ ok: false,
229
+ status: 400,
230
+ error: "Cannot verify GitHub account owner for this token.",
231
+ };
232
+ }
233
+ if (repoOwner.toLowerCase() !== viewerLogin.toLowerCase()) {
234
+ const orgLookup = await findAccessibleOrgByLogin({
235
+ repoOwner,
236
+ ghHeaders,
237
+ });
238
+ if (!orgLookup?.org) {
239
+ return {
240
+ ok: false,
241
+ status: 400,
242
+ error:
243
+ orgLookup?.error ||
244
+ `Repository owner "${repoOwner}" does not match the authenticated GitHub user "${viewerLogin}" ` +
245
+ "and was not found in the token's accessible organizations. Check the owner name or use a token that can create repositories for that organization.",
246
+ };
247
+ }
248
+ return {
249
+ ok: true,
250
+ repoExists: false,
251
+ repoIsEmpty: false,
252
+ createOwnerType: "org",
253
+ viewerLogin,
254
+ };
255
+ }
256
+ return {
257
+ ok: true,
258
+ repoExists: false,
259
+ repoIsEmpty: false,
260
+ createOwnerType: "user",
261
+ viewerLogin,
262
+ };
191
263
  }
192
264
  if (checkRes.ok) {
193
265
  const commitsRes = await fetch(
@@ -250,6 +322,7 @@ const ensureGithubRepoAccessible = async ({
250
322
  githubToken,
251
323
  }) => {
252
324
  const ghHeaders = buildGithubHeaders(githubToken);
325
+ const [repoOwner = ""] = String(repoUrl || "").split("/");
253
326
  const verification = await verifyGithubRepoForOnboarding({
254
327
  repoUrl,
255
328
  githubToken,
@@ -262,7 +335,11 @@ const ensureGithubRepoAccessible = async ({
262
335
 
263
336
  try {
264
337
  console.log(`[onboard] Creating repo ${repoUrl}...`);
265
- const createRes = await fetch("https://api.github.com/user/repos", {
338
+ const createUrl =
339
+ verification.createOwnerType === "org"
340
+ ? `https://api.github.com/orgs/${repoOwner}/repos`
341
+ : "https://api.github.com/user/repos";
342
+ const createRes = await fetch(createUrl, {
266
343
  method: "POST",
267
344
  headers: { ...ghHeaders, "Content-Type": "application/json" },
268
345
  body: JSON.stringify({
@@ -1,5 +1,5 @@
1
1
  const path = require("path");
2
- const { kSetupDir, kRootDir } = require("../constants");
2
+ const { kSetupDir } = require("../constants");
3
3
  const {
4
4
  resolveConfigIncludes,
5
5
  resolveImportedConfigPaths,
@@ -491,14 +491,7 @@ const createOnboardingService = ({
491
491
  await shellCmd(
492
492
  `openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
493
493
  {
494
- env: {
495
- ...process.env,
496
- HOME: kRootDir,
497
- OPENCLAW_HOME: kRootDir,
498
- OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
499
- OPENCLAW_STATE_DIR: OPENCLAW_DIR,
500
- XDG_CONFIG_HOME: OPENCLAW_DIR,
501
- },
494
+ env: gatewayEnv(),
502
495
  timeout: 120000,
503
496
  },
504
497
  );
@@ -162,7 +162,10 @@ const getSafeImportedDmPolicy = (channelConfig = {}) => {
162
162
  return channelConfig?.dmPolicy || "pairing";
163
163
  };
164
164
 
165
- const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
165
+ const applyFreshOnboardingChannels = ({
166
+ cfg,
167
+ varMap,
168
+ }) => {
166
169
  if (varMap.TELEGRAM_BOT_TOKEN) {
167
170
  cfg.channels.telegram = {
168
171
  enabled: true,
@@ -214,11 +217,18 @@ const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
214
217
  ensureUsageTrackerPluginEntry(cfg);
215
218
  };
216
219
 
217
- const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
220
+ const writeSanitizedOpenclawConfig = ({
221
+ fs,
222
+ openclawDir,
223
+ varMap,
224
+ }) => {
218
225
  const configPath = `${openclawDir}/openclaw.json`;
219
226
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
220
227
  ensureManagedConfigShell(cfg);
221
- applyFreshOnboardingChannels({ cfg, varMap });
228
+ applyFreshOnboardingChannels({
229
+ cfg,
230
+ varMap,
231
+ });
222
232
 
223
233
  let content = JSON.stringify(cfg, null, 2);
224
234
  const replacements = buildSecretReplacements(varMap, process.env);
@@ -239,7 +249,11 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
239
249
  console.log("[onboard] Config sanitized");
240
250
  };
241
251
 
242
- const writeManagedImportOpenclawConfig = ({ fs, openclawDir, varMap }) => {
252
+ const writeManagedImportOpenclawConfig = ({
253
+ fs,
254
+ openclawDir,
255
+ varMap,
256
+ }) => {
243
257
  const configPath = `${openclawDir}/openclaw.json`;
244
258
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
245
259
  ensureManagedConfigShell(cfg);
@@ -0,0 +1,55 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { kRootDir } = require("./constants");
4
+
5
+ const kDefaultOpenclawCompileCacheDir = path.join(
6
+ kRootDir,
7
+ "cache",
8
+ "openclaw-compile-cache",
9
+ );
10
+
11
+ const normalizeEnvValue = (value) => String(value || "").trim();
12
+
13
+ const resolveOpenclawCompileCacheDir = (env = process.env) =>
14
+ normalizeEnvValue(env.NODE_COMPILE_CACHE) || kDefaultOpenclawCompileCacheDir;
15
+
16
+ const resolveOpenclawNoRespawn = (env = process.env) =>
17
+ normalizeEnvValue(env.OPENCLAW_NO_RESPAWN) || "1";
18
+
19
+ const withOpenclawStartupEnv = (env = process.env) => ({
20
+ ...env,
21
+ NODE_COMPILE_CACHE: resolveOpenclawCompileCacheDir(env),
22
+ OPENCLAW_NO_RESPAWN: resolveOpenclawNoRespawn(env),
23
+ });
24
+
25
+ const ensureOpenclawStartupEnv = ({
26
+ fsModule = fs,
27
+ env = process.env,
28
+ logger = console,
29
+ } = {}) => {
30
+ const nextEnv = withOpenclawStartupEnv(env);
31
+ try {
32
+ fsModule.mkdirSync(nextEnv.NODE_COMPILE_CACHE, { recursive: true });
33
+ } catch (err) {
34
+ logger?.warn?.(
35
+ `[alphaclaw] OpenClaw compile cache directory unavailable: ${err.message}`,
36
+ );
37
+ }
38
+
39
+ if (!normalizeEnvValue(env.NODE_COMPILE_CACHE)) {
40
+ env.NODE_COMPILE_CACHE = nextEnv.NODE_COMPILE_CACHE;
41
+ }
42
+ if (!normalizeEnvValue(env.OPENCLAW_NO_RESPAWN)) {
43
+ env.OPENCLAW_NO_RESPAWN = nextEnv.OPENCLAW_NO_RESPAWN;
44
+ }
45
+
46
+ return nextEnv;
47
+ };
48
+
49
+ module.exports = {
50
+ kDefaultOpenclawCompileCacheDir,
51
+ ensureOpenclawStartupEnv,
52
+ resolveOpenclawCompileCacheDir,
53
+ resolveOpenclawNoRespawn,
54
+ withOpenclawStartupEnv,
55
+ };
@@ -24,9 +24,10 @@ const createOpenclawVersionService = ({
24
24
  };
25
25
  let kOpenclawUpdateInProgress = false;
26
26
 
27
- const readOpenclawVersion = () => {
27
+ const readOpenclawVersion = ({ refresh = false } = {}) => {
28
28
  const now = Date.now();
29
29
  if (
30
+ !refresh &&
30
31
  kOpenclawVersionCache.value &&
31
32
  now - kOpenclawVersionCache.fetchedAt < kVersionCacheTtlMs
32
33
  ) {
@@ -1,5 +1,6 @@
1
1
  const { kFallbackOnboardingModels } = require("../constants");
2
2
  const { createModelCatalogCache } = require("../model-catalog-cache");
3
+ const { getCommandOutputCandidates } = require("../utils/command-output");
3
4
 
4
5
  const runModelsGitSync = async (shellCmd) => {
5
6
  if (typeof shellCmd !== "function") return null;
@@ -13,12 +14,25 @@ const runModelsGitSync = async (shellCmd) => {
13
14
  }
14
15
  };
15
16
 
17
+ const parseJsonFromShellError = ({
18
+ error,
19
+ parseJsonFromNoisyOutput = () => null,
20
+ } = {}) => {
21
+ for (const rawOutput of getCommandOutputCandidates(error)) {
22
+ const parsed = parseJsonFromNoisyOutput(rawOutput);
23
+ if (parsed) return parsed;
24
+ }
25
+ return null;
26
+ };
27
+
16
28
  const registerModelRoutes = ({
17
29
  app,
18
30
  shellCmd,
19
31
  gatewayEnv,
20
32
  parseJsonFromNoisyOutput,
21
33
  normalizeOnboardingModels,
34
+ readOpenclawVersion,
35
+ isOnboarded = () => true,
22
36
  authProfiles,
23
37
  readEnvFile,
24
38
  writeEnvFile,
@@ -28,6 +42,8 @@ const registerModelRoutes = ({
28
42
  gatewayEnv,
29
43
  parseJsonFromNoisyOutput,
30
44
  normalizeOnboardingModels,
45
+ readOpenclawVersion,
46
+ shouldStartDynamicRefresh: isOnboarded,
31
47
  fallbackModels: kFallbackOnboardingModels,
32
48
  }),
33
49
  }) => {
@@ -180,6 +196,18 @@ const registerModelRoutes = ({
180
196
  imageModel: parsed.imageModel || null,
181
197
  });
182
198
  } catch (err) {
199
+ const parsed = parseJsonFromShellError({
200
+ error: err,
201
+ parseJsonFromNoisyOutput,
202
+ });
203
+ if (parsed) {
204
+ return res.json({
205
+ ok: true,
206
+ modelKey: parsed.resolvedDefault || parsed.defaultModel || null,
207
+ fallbacks: parsed.fallbacks || [],
208
+ imageModel: parsed.imageModel || null,
209
+ });
210
+ }
183
211
  res.json({
184
212
  ok: false,
185
213
  error: err.message || "Failed to read model status",