@chrysb/alphaclaw 0.9.9 → 0.9.10

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 (34) hide show
  1. package/lib/public/dist/app.bundle.js +1424 -1398
  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/hooks/usePolling.js +46 -13
  13. package/lib/public/js/lib/model-config.js +4 -0
  14. package/lib/server/agents/channels.js +53 -9
  15. package/lib/server/commands.js +4 -1
  16. package/lib/server/constants.js +5 -0
  17. package/lib/server/cost-utils.js +9 -0
  18. package/lib/server/cron-service.js +12 -1
  19. package/lib/server/db/doctor/index.js +9 -0
  20. package/lib/server/db/usage/index.js +13 -0
  21. package/lib/server/db/watchdog/index.js +13 -1
  22. package/lib/server/db/webhooks/index.js +13 -1
  23. package/lib/server/init/register-server-routes.js +2 -0
  24. package/lib/server/internal-files-migration.js +11 -1
  25. package/lib/server/model-catalog-cache.js +85 -6
  26. package/lib/server/onboarding/github.js +79 -2
  27. package/lib/server/openclaw-version.js +2 -1
  28. package/lib/server/routes/models.js +26 -0
  29. package/lib/server/routes/pairings.js +106 -15
  30. package/lib/server/utils/command-output.js +11 -0
  31. package/lib/setup/gitignore +2 -0
  32. package/package.json +2 -2
  33. package/patches/openclaw+2026.4.21.patch +13 -0
  34. package/patches/openclaw+2026.4.15.patch +0 -13
@@ -20,8 +20,11 @@ const createCommands = ({ gatewayEnv }) => {
20
20
  );
21
21
  exec(cmd, { timeout: timeoutMs, ...execOpts }, (err, stdout, stderr) => {
22
22
  if (err) {
23
+ err.stdout = String(stdout || "").trim();
24
+ err.stderr = String(stderr || "").trim();
25
+ err.cmd = cmd;
23
26
  console.error(
24
- `[onboard] Error: ${(stderr || err.message).slice(0, 300)}`,
27
+ `[onboard] Error: ${String(stderr || err.message || "").slice(0, 300)}`,
25
28
  );
26
29
  return reject(err);
27
30
  }
@@ -91,6 +91,11 @@ const kOnboardingModelProviders = new Set([
91
91
  "vllm",
92
92
  ]);
93
93
  const kFallbackOnboardingModels = [
94
+ {
95
+ key: "anthropic/claude-opus-4-7",
96
+ provider: "anthropic",
97
+ label: "Claude Opus 4.7",
98
+ },
94
99
  {
95
100
  key: "anthropic/claude-opus-4-6",
96
101
  provider: "anthropic",
@@ -5,7 +5,16 @@ const kTokensPerMillion = 1_000_000;
5
5
  const kLongContextThresholdTokens = 200_000;
6
6
  const kNodeModulesPricingCacheTtlMs = 60_000;
7
7
 
8
+ const kClaudeOpus47Pricing = {
9
+ input: 5.0,
10
+ output: 25.0,
11
+ cacheRead: 0.5,
12
+ cacheWrite: 6.25,
13
+ };
14
+
8
15
  const kGlobalModelPricing = {
16
+ "claude-opus-4-7": kClaudeOpus47Pricing,
17
+ "claude-opus-4.7": kClaudeOpus47Pricing,
9
18
  "claude-opus-4-6": {
10
19
  input: (tokens) => (tokens > kLongContextThresholdTokens ? 10.0 : 5.0),
11
20
  output: (tokens) => (tokens > kLongContextThresholdTokens ? 37.5 : 25.0),
@@ -485,6 +485,16 @@ const parseCommandJson = (rawOutput) => {
485
485
  return null;
486
486
  };
487
487
 
488
+ const resolvePromptEditFlag = ({ cronDir, jobId }) => {
489
+ const store = readCronStore({ cronDir });
490
+ const job = store.jobs.find((entry) => String(entry?.id || "") === jobId);
491
+ if (!job) throw new Error(`unknown cron job id: ${jobId}`);
492
+ const payloadKind = String(job?.payload?.kind || "").trim();
493
+ if (payloadKind === "systemEvent") return "--system-event";
494
+ if (payloadKind === "agentTurn") return "--message";
495
+ throw new Error(`unsupported cron payload kind: ${payloadKind || "unknown"}`);
496
+ };
497
+
488
498
  const createCronService = ({
489
499
  clawCmd,
490
500
  OPENCLAW_DIR,
@@ -548,7 +558,8 @@ const createCronService = ({
548
558
 
549
559
  const updateJobPrompt = async ({ jobId, message }) => {
550
560
  const safeJobId = sanitizeCronJobId(jobId);
551
- const command = `cron edit ${shellEscapeArg(safeJobId)} --message ${shellEscapeArg(message || "")}`;
561
+ const promptFlag = resolvePromptEditFlag({ cronDir, jobId: safeJobId });
562
+ const command = `cron edit ${shellEscapeArg(safeJobId)} ${promptFlag} ${shellEscapeArg(message || "")}`;
552
563
  return runCommand(command, { timeoutMs: 60000 });
553
564
  };
554
565
 
@@ -18,6 +18,13 @@ const ensureDb = () => {
18
18
  return db;
19
19
  };
20
20
 
21
+ const closeDoctorDb = () => {
22
+ if (!db) return;
23
+ const database = db;
24
+ db = null;
25
+ database.close();
26
+ };
27
+
21
28
  const parseJsonText = (value, fallbackValue) => {
22
29
  if (typeof value !== "string" || !value) return fallbackValue;
23
30
  try {
@@ -171,6 +178,7 @@ const toRunModel = (row) => {
171
178
  };
172
179
 
173
180
  const initDoctorDb = ({ rootDir }) => {
181
+ closeDoctorDb();
174
182
  const dbDir = path.join(rootDir, "db");
175
183
  fs.mkdirSync(dbDir, { recursive: true });
176
184
  const dbPath = path.join(dbDir, "doctor.db");
@@ -511,6 +519,7 @@ const updateDoctorCardStatus = ({ id, status }) => {
511
519
 
512
520
  module.exports = {
513
521
  initDoctorDb,
522
+ closeDoctorDb,
514
523
  markIncompleteRunsFailed,
515
524
  getDoctorMeta,
516
525
  setDoctorMeta,
@@ -15,7 +15,19 @@ const ensureDb = () => {
15
15
  return db;
16
16
  };
17
17
 
18
+ const closeUsageDb = () => {
19
+ if (!db) {
20
+ usageDbPath = "";
21
+ return;
22
+ }
23
+ const database = db;
24
+ db = null;
25
+ usageDbPath = "";
26
+ database.close();
27
+ };
28
+
18
29
  const initUsageDb = ({ rootDir }) => {
30
+ closeUsageDb();
19
31
  const dbDir = path.join(rootDir, "db");
20
32
  fs.mkdirSync(dbDir, { recursive: true });
21
33
  usageDbPath = path.join(dbDir, "usage.db");
@@ -155,6 +167,7 @@ const getSessionUsageByKeyPattern = ({ keyPattern = "", sinceMs = 0 } = {}) => {
155
167
 
156
168
  module.exports = {
157
169
  initUsageDb,
170
+ closeUsageDb,
158
171
  getDailySummary: (options = {}) => getDailySummary({ database: ensureDb(), ...options }),
159
172
  getSessionsList: (options = {}) => getSessionsList({ database: ensureDb(), ...options }),
160
173
  getSessionDetail: (options = {}) => getSessionDetail({ database: ensureDb(), ...options }),
@@ -15,14 +15,25 @@ const ensureDb = () => {
15
15
  return db;
16
16
  };
17
17
 
18
+ const closeWatchdogDb = () => {
19
+ if (pruneTimer) {
20
+ clearInterval(pruneTimer);
21
+ pruneTimer = null;
22
+ }
23
+ if (!db) return;
24
+ const database = db;
25
+ db = null;
26
+ database.close();
27
+ };
28
+
18
29
  const initWatchdogDb = ({ rootDir, pruneDays = 30 }) => {
30
+ closeWatchdogDb();
19
31
  const dbDir = path.join(rootDir, "db");
20
32
  fs.mkdirSync(dbDir, { recursive: true });
21
33
  const dbPath = path.join(dbDir, "watchdog.db");
22
34
  db = new DatabaseSync(dbPath);
23
35
  createSchema(db);
24
36
  pruneWatchdogEvents(pruneDays);
25
- if (pruneTimer) clearInterval(pruneTimer);
26
37
  pruneTimer = setInterval(() => {
27
38
  try {
28
39
  pruneWatchdogEvents(pruneDays);
@@ -125,6 +136,7 @@ const pruneWatchdogEvents = (days = 30) => {
125
136
 
126
137
  module.exports = {
127
138
  initWatchdogDb,
139
+ closeWatchdogDb,
128
140
  insertWatchdogEvent,
129
141
  getRecentEvents,
130
142
  pruneWatchdogEvents,
@@ -17,14 +17,25 @@ const ensureDb = () => {
17
17
  return db;
18
18
  };
19
19
 
20
+ const closeWebhooksDb = () => {
21
+ if (pruneTimer) {
22
+ clearInterval(pruneTimer);
23
+ pruneTimer = null;
24
+ }
25
+ if (!db) return;
26
+ const database = db;
27
+ db = null;
28
+ database.close();
29
+ };
30
+
20
31
  const initWebhooksDb = ({ rootDir, pruneDays = 30 }) => {
32
+ closeWebhooksDb();
21
33
  const dbDir = path.join(rootDir, "db");
22
34
  fs.mkdirSync(dbDir, { recursive: true });
23
35
  const dbPath = path.join(dbDir, "webhooks.db");
24
36
  db = new DatabaseSync(dbPath);
25
37
  createSchema(db);
26
38
  pruneOldEntries(pruneDays);
27
- if (pruneTimer) clearInterval(pruneTimer);
28
39
  pruneTimer = setInterval(() => {
29
40
  try {
30
41
  pruneOldEntries(pruneDays);
@@ -413,6 +424,7 @@ const pruneOldEntries = (days = 30) => {
413
424
 
414
425
  module.exports = {
415
426
  initWebhooksDb,
427
+ closeWebhooksDb,
416
428
  insertRequest,
417
429
  getRequests,
418
430
  getRequestById,
@@ -97,6 +97,8 @@ const registerServerRoutes = ({
97
97
  gatewayEnv,
98
98
  parseJsonFromNoisyOutput,
99
99
  normalizeOnboardingModels,
100
+ readOpenclawVersion: (options) =>
101
+ openclawVersionService?.readOpenclawVersion(options),
100
102
  authProfiles,
101
103
  readEnvFile,
102
104
  writeEnvFile,
@@ -9,6 +9,16 @@ const kOpenclawGitignoreHookEntries = [
9
9
  "!hooks/transforms/**",
10
10
  ];
11
11
 
12
+ const kOpenclawGitignoreCronRuntimeEntries = [
13
+ "# OpenClaw cron runtime state (local only; job definitions stay in cron/jobs.json)",
14
+ "cron/jobs-state.json",
15
+ ];
16
+
17
+ const kOpenclawGitignoreAppendEntries = [
18
+ ...kOpenclawGitignoreHookEntries,
19
+ ...kOpenclawGitignoreCronRuntimeEntries,
20
+ ];
21
+
12
22
  const buildManagedPaths = ({ openclawDir, pathModule = path }) => {
13
23
  const internalDir = pathModule.join(openclawDir, kInternalDirName);
14
24
  return {
@@ -89,7 +99,7 @@ const migrateManagedInternalFiles = ({
89
99
  const raw = String(fs.readFileSync(gitignorePath, "utf8") || "");
90
100
  const existingLines = raw.split(/\r?\n/);
91
101
  const existingSet = new Set(existingLines.map((line) => line.trim()));
92
- const missing = kOpenclawGitignoreHookEntries.filter(
102
+ const missing = kOpenclawGitignoreAppendEntries.filter(
93
103
  (line) => !existingSet.has(line),
94
104
  );
95
105
  if (missing.length) {
@@ -1,6 +1,7 @@
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;
@@ -21,6 +22,12 @@ const createResponse = ({
21
22
  models,
22
23
  });
23
24
 
25
+ const normalizeOpenclawVersion = (value) => {
26
+ if (typeof value !== "string") return null;
27
+ const normalized = value.trim();
28
+ return normalized || null;
29
+ };
30
+
24
31
  const normalizeCachedModels = ({
25
32
  models,
26
33
  normalizeOnboardingModels = (items) => items,
@@ -48,10 +55,20 @@ const normalizeCacheEntry = ({
48
55
  return {
49
56
  version: kModelCatalogCacheVersion,
50
57
  fetchedAt,
58
+ openclawVersion: normalizeOpenclawVersion(raw.openclawVersion),
51
59
  models,
52
60
  };
53
61
  };
54
62
 
63
+ const parseCatalogModelsFromOutput = ({
64
+ rawOutput,
65
+ parseJsonFromNoisyOutput = () => ({}),
66
+ normalizeOnboardingModels = (items) => items,
67
+ } = {}) => {
68
+ const parsed = parseJsonFromNoisyOutput(rawOutput);
69
+ return normalizeOnboardingModels(parsed?.models || []);
70
+ };
71
+
55
72
  const createModelCatalogCache = ({
56
73
  fsModule = fs,
57
74
  pathModule = path,
@@ -59,6 +76,7 @@ const createModelCatalogCache = ({
59
76
  gatewayEnv = () => ({}),
60
77
  parseJsonFromNoisyOutput = () => ({}),
61
78
  normalizeOnboardingModels = (items) => items,
79
+ readOpenclawVersion = () => null,
62
80
  fallbackModels = kFallbackOnboardingModels,
63
81
  cachePath = kDefaultCachePath,
64
82
  refreshBackoffMs = kModelCatalogRefreshBackoffMs,
@@ -74,6 +92,23 @@ const createModelCatalogCache = ({
74
92
  let retryTimer = null;
75
93
  let backoffUntilMs = 0;
76
94
 
95
+ const readCurrentOpenclawVersion = ({ refresh = false } = {}) => {
96
+ try {
97
+ return normalizeOpenclawVersion(readOpenclawVersion({ refresh }));
98
+ } catch {
99
+ return null;
100
+ }
101
+ };
102
+
103
+ const isCompatibleWithCurrentOpenclaw = ({
104
+ entry,
105
+ currentOpenclawVersion,
106
+ } = {}) => {
107
+ if (!entry) return false;
108
+ if (!currentOpenclawVersion) return true;
109
+ return entry.openclawVersion === currentOpenclawVersion;
110
+ };
111
+
77
112
  const clearRetryTimer = () => {
78
113
  if (!retryTimer) return;
79
114
  clearTimeoutFn(retryTimer);
@@ -121,22 +156,51 @@ const createModelCatalogCache = ({
121
156
  };
122
157
 
123
158
  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 || []);
159
+ const openclawVersion = readCurrentOpenclawVersion({ refresh: true });
160
+ let models = [];
161
+ let recoveredFromCommandError = false;
162
+ try {
163
+ const output = await shellCmd("openclaw models list --all --json", {
164
+ env: gatewayEnv(),
165
+ timeout: 30000,
166
+ });
167
+ models = parseCatalogModelsFromOutput({
168
+ rawOutput: output,
169
+ parseJsonFromNoisyOutput,
170
+ normalizeOnboardingModels,
171
+ });
172
+ } catch (err) {
173
+ for (const rawOutput of getCommandOutputCandidates(err)) {
174
+ models = parseCatalogModelsFromOutput({
175
+ rawOutput,
176
+ parseJsonFromNoisyOutput,
177
+ normalizeOnboardingModels,
178
+ });
179
+ if (models.length > 0) {
180
+ recoveredFromCommandError = true;
181
+ logger.warn?.(
182
+ `[models] Recovered model catalog from failed command output: ${err.message || String(err)}`,
183
+ );
184
+ break;
185
+ }
186
+ }
187
+ if (models.length === 0) throw err;
188
+ }
130
189
  if (models.length === 0) {
131
190
  throw new Error("No models found");
132
191
  }
133
192
  const entry = {
134
193
  version: kModelCatalogCacheVersion,
135
194
  fetchedAt: now(),
195
+ openclawVersion,
136
196
  models,
137
197
  };
138
198
  writeDiskCache(entry);
139
199
  setCacheEntry(entry, { fresh: true });
200
+ if (recoveredFromCommandError) {
201
+ backoffUntilMs = 0;
202
+ clearRetryTimer();
203
+ }
140
204
  return entry;
141
205
  };
142
206
 
@@ -190,6 +254,21 @@ const createModelCatalogCache = ({
190
254
  return {
191
255
  async getCatalogResponse() {
192
256
  readDiskCache();
257
+ if (memoryCache && !cacheIsStale) {
258
+ const currentOpenclawVersion = readCurrentOpenclawVersion({
259
+ refresh: true,
260
+ });
261
+ if (
262
+ !isCompatibleWithCurrentOpenclaw({
263
+ entry: memoryCache,
264
+ currentOpenclawVersion,
265
+ })
266
+ ) {
267
+ cacheIsStale = true;
268
+ backoffUntilMs = 0;
269
+ clearRetryTimer();
270
+ }
271
+ }
193
272
  if (memoryCache && !cacheIsStale) {
194
273
  return createResponse({
195
274
  source: "openclaw",
@@ -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({
@@ -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,24 @@ 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,
22
35
  authProfiles,
23
36
  readEnvFile,
24
37
  writeEnvFile,
@@ -28,6 +41,7 @@ const registerModelRoutes = ({
28
41
  gatewayEnv,
29
42
  parseJsonFromNoisyOutput,
30
43
  normalizeOnboardingModels,
44
+ readOpenclawVersion,
31
45
  fallbackModels: kFallbackOnboardingModels,
32
46
  }),
33
47
  }) => {
@@ -180,6 +194,18 @@ const registerModelRoutes = ({
180
194
  imageModel: parsed.imageModel || null,
181
195
  });
182
196
  } catch (err) {
197
+ const parsed = parseJsonFromShellError({
198
+ error: err,
199
+ parseJsonFromNoisyOutput,
200
+ });
201
+ if (parsed) {
202
+ return res.json({
203
+ ok: true,
204
+ modelKey: parsed.resolvedDefault || parsed.defaultModel || null,
205
+ fallbacks: parsed.fallbacks || [],
206
+ imageModel: parsed.imageModel || null,
207
+ });
208
+ }
183
209
  res.json({
184
210
  ok: false,
185
211
  error: err.message || "Failed to read model status",