@chrysb/alphaclaw 0.9.0-beta.7 → 0.9.1-beta.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.
Files changed (70) hide show
  1. package/bin/alphaclaw.js +26 -25
  2. package/lib/cli/git-runtime.js +97 -0
  3. package/lib/public/css/chat.css +0 -12
  4. package/lib/public/css/explorer.css +48 -0
  5. package/lib/public/css/shell.css +149 -0
  6. package/lib/public/css/tailwind.generated.css +1 -1
  7. package/lib/public/css/theme.css +265 -0
  8. package/lib/public/dist/app.bundle.js +2770 -2762
  9. package/lib/public/js/app.js +26 -14
  10. package/lib/public/js/components/agents-tab/create-channel-modal.js +259 -59
  11. package/lib/public/js/components/gateway.js +0 -286
  12. package/lib/public/js/components/general/index.js +0 -7
  13. package/lib/public/js/components/icons.js +26 -25
  14. package/lib/public/js/components/modal-shell.js +1 -1
  15. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  16. package/lib/public/js/components/models-tab/use-models.js +74 -9
  17. package/lib/public/js/components/models.js +52 -37
  18. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  19. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
  21. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  23. package/lib/public/js/components/providers.js +53 -42
  24. package/lib/public/js/components/routes/chat-route.js +2 -9
  25. package/lib/public/js/components/routes/general-route.js +0 -6
  26. package/lib/public/js/components/routes/index.js +0 -1
  27. package/lib/public/js/components/routes/watchdog-route.js +0 -6
  28. package/lib/public/js/components/sidebar.js +21 -7
  29. package/lib/public/js/components/theme-toggle.js +113 -0
  30. package/lib/public/js/components/update-modal.js +174 -51
  31. package/lib/public/js/components/watchdog-tab/index.js +0 -6
  32. package/lib/public/js/components/welcome/index.js +0 -2
  33. package/lib/public/js/components/welcome/use-welcome.js +107 -36
  34. package/lib/public/js/hooks/use-app-shell-controller.js +16 -33
  35. package/lib/public/js/lib/api.js +0 -28
  36. package/lib/public/js/lib/app-navigation.js +0 -2
  37. package/lib/public/js/lib/channel-provider-availability.js +1 -2
  38. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  39. package/lib/public/js/lib/model-catalog.js +31 -0
  40. package/lib/public/js/lib/storage-keys.js +1 -1
  41. package/lib/public/login.html +8 -4
  42. package/lib/public/setup.html +9 -0
  43. package/lib/scripts/git +110 -16
  44. package/lib/server/agents/channels.js +1 -4
  45. package/lib/server/alphaclaw-version.js +590 -132
  46. package/lib/server/constants.js +5 -0
  47. package/lib/server/db/webhooks/index.js +48 -8
  48. package/lib/server/exec-defaults-config.js +163 -0
  49. package/lib/server/gateway.js +1 -0
  50. package/lib/server/init/register-server-routes.js +0 -8
  51. package/lib/server/init/server-lifecycle.js +2 -0
  52. package/lib/server/model-catalog-cache.js +251 -0
  53. package/lib/server/onboarding/github.js +83 -2
  54. package/lib/server/onboarding/index.js +7 -0
  55. package/lib/server/routes/models.js +14 -23
  56. package/lib/server/routes/nodes.js +9 -23
  57. package/lib/server/routes/system.js +3 -16
  58. package/lib/server/routes/webhooks.js +12 -1
  59. package/lib/server/startup.js +8 -0
  60. package/lib/server/watchdog-notify.js +172 -55
  61. package/lib/server.js +17 -2
  62. package/lib/setup/core-prompts/AGENTS.md +12 -0
  63. package/lib/setup/core-prompts/TOOLS.md +12 -0
  64. package/package.json +2 -2
  65. package/patches/openclaw+2026.4.9.patch +13 -0
  66. package/lib/public/js/components/mcp-tab/index.js +0 -237
  67. package/lib/public/js/components/routes/mcp-route.js +0 -7
  68. package/lib/server/mcp-bridge.js +0 -158
  69. package/lib/server/routes/mcp.js +0 -292
  70. package/patches/openclaw+2026.3.28.patch +0 -13
@@ -176,6 +176,11 @@ const kSystemVars = new Set([
176
176
  "OPENCLAW_GATEWAY_TOKEN",
177
177
  "SETUP_PASSWORD",
178
178
  "PORT",
179
+ "ALPHACLAW_DEPLOYMENT_PROVIDER",
180
+ "ALPHACLAW_MANAGED_UPDATE_URL",
181
+ "ALPHACLAW_MANAGED_UPDATE_TOKEN",
182
+ "ALPHACLAW_TEMPLATE_REPO_URL",
183
+ "ALPHACLAW_TEMPLATE_BRANCH",
179
184
  "WATCHDOG_AUTO_REPAIR",
180
185
  "WATCHDOG_NOTIFICATIONS_DISABLED",
181
186
  ]);
@@ -10,6 +10,7 @@ let pruneTimer = null;
10
10
  const kDefaultRequestLimit = 50;
11
11
  const kMaxRequestLimit = 200;
12
12
  const kPruneIntervalMs = 12 * 60 * 60 * 1000;
13
+ const kHealthSummaryWindow = 25;
13
14
 
14
15
  const ensureDb = () => {
15
16
  if (!db) throw new Error("Webhooks DB not initialized");
@@ -202,22 +203,61 @@ const getHookSummaries = () => {
202
203
  const database = ensureDb();
203
204
  const rows = database
204
205
  .prepare(`
206
+ WITH ranked_requests AS (
207
+ SELECT
208
+ hook_name,
209
+ created_at,
210
+ gateway_status,
211
+ ROW_NUMBER() OVER (
212
+ PARTITION BY hook_name
213
+ ORDER BY created_at DESC, id DESC
214
+ ) AS row_num
215
+ FROM webhook_requests
216
+ ),
217
+ overall_counts AS (
218
+ SELECT
219
+ hook_name,
220
+ MAX(created_at) AS last_received,
221
+ COUNT(*) AS total_count,
222
+ SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS success_count,
223
+ SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS error_count
224
+ FROM webhook_requests
225
+ GROUP BY hook_name
226
+ ),
227
+ recent_counts AS (
228
+ SELECT
229
+ hook_name,
230
+ COUNT(*) AS recent_total_count,
231
+ SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS recent_success_count,
232
+ SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS recent_error_count
233
+ FROM ranked_requests
234
+ WHERE row_num <= $health_window
235
+ GROUP BY hook_name
236
+ )
205
237
  SELECT
206
- hook_name,
207
- MAX(created_at) AS last_received,
208
- COUNT(*) AS total_count,
209
- SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS success_count,
210
- SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS error_count
211
- FROM webhook_requests
212
- GROUP BY hook_name
238
+ overall_counts.hook_name,
239
+ overall_counts.last_received,
240
+ overall_counts.total_count,
241
+ overall_counts.success_count,
242
+ overall_counts.error_count,
243
+ COALESCE(recent_counts.recent_total_count, 0) AS recent_total_count,
244
+ COALESCE(recent_counts.recent_success_count, 0) AS recent_success_count,
245
+ COALESCE(recent_counts.recent_error_count, 0) AS recent_error_count
246
+ FROM overall_counts
247
+ LEFT JOIN recent_counts
248
+ ON recent_counts.hook_name = overall_counts.hook_name
213
249
  `)
214
- .all();
250
+ .all({ $health_window: kHealthSummaryWindow });
215
251
  return rows.map((row) => ({
216
252
  hookName: row.hook_name,
217
253
  lastReceived: row.last_received || null,
218
254
  totalCount: Number(row.total_count || 0),
219
255
  successCount: Number(row.success_count || 0),
220
256
  errorCount: Number(row.error_count || 0),
257
+ recentTotalCount: Number(row.recent_total_count || 0),
258
+ recentSuccessCount: Number(row.recent_success_count || 0),
259
+ recentErrorCount: Number(row.recent_error_count || 0),
260
+ healthWindowSize: kHealthSummaryWindow,
221
261
  }));
222
262
  };
223
263
 
@@ -0,0 +1,163 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ readOpenclawConfig,
5
+ resolveOpenclawConfigPath,
6
+ writeOpenclawConfig,
7
+ } = require("./openclaw-config");
8
+
9
+ const kManagedExecApprovalsDefaults = Object.freeze({
10
+ security: "full",
11
+ ask: "off",
12
+ askFallback: "full",
13
+ });
14
+
15
+ const kManagedOpenclawExecDefaults = Object.freeze({
16
+ security: "full",
17
+ strictInlineEval: false,
18
+ });
19
+
20
+ const resolveExecApprovalsConfigPath = ({ openclawDir }) =>
21
+ path.join(openclawDir, "exec-approvals.json");
22
+
23
+ const readExecApprovalsConfig = ({
24
+ fsModule = fs,
25
+ openclawDir,
26
+ fallback = { version: 1 },
27
+ } = {}) => {
28
+ const filePath = resolveExecApprovalsConfigPath({ openclawDir });
29
+ try {
30
+ const parsed = JSON.parse(fsModule.readFileSync(filePath, "utf8"));
31
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
32
+ ? parsed
33
+ : fallback;
34
+ } catch {
35
+ return fallback;
36
+ }
37
+ };
38
+
39
+ const writeExecApprovalsConfig = ({
40
+ fsModule = fs,
41
+ openclawDir,
42
+ file = {},
43
+ spacing = 2,
44
+ } = {}) => {
45
+ const filePath = resolveExecApprovalsConfigPath({ openclawDir });
46
+ fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
47
+ fsModule.writeFileSync(filePath, JSON.stringify(file, null, spacing) + "\n", "utf8");
48
+ return filePath;
49
+ };
50
+
51
+ const hasOwn = (obj, key) =>
52
+ !!obj && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key);
53
+
54
+ const ensureManagedExecApprovalsDefaults = (rawFile = {}) => {
55
+ const file =
56
+ rawFile && typeof rawFile === "object" && !Array.isArray(rawFile) ? rawFile : {};
57
+ const before = JSON.stringify(file);
58
+ const defaults =
59
+ file.defaults && typeof file.defaults === "object" && !Array.isArray(file.defaults)
60
+ ? file.defaults
61
+ : null;
62
+ const hasNonEmptyDefaults = !!defaults && Object.keys(defaults).length > 0;
63
+ if (!hasNonEmptyDefaults) {
64
+ if (!Number.isInteger(file.version)) file.version = 1;
65
+ file.defaults = {
66
+ security: kManagedExecApprovalsDefaults.security,
67
+ ask: kManagedExecApprovalsDefaults.ask,
68
+ askFallback: kManagedExecApprovalsDefaults.askFallback,
69
+ };
70
+ if (!file.agents || typeof file.agents !== "object" || Array.isArray(file.agents)) {
71
+ file.agents = {};
72
+ }
73
+ }
74
+ return {
75
+ file,
76
+ changed: JSON.stringify(file) !== before,
77
+ };
78
+ };
79
+
80
+ const ensureManagedOpenclawExecDefaults = (rawConfig = {}) => {
81
+ const config =
82
+ rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) ? rawConfig : {};
83
+ const before = JSON.stringify(config);
84
+ if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
85
+ config.tools = {};
86
+ }
87
+ if (!hasOwn(config.tools, "exec")) {
88
+ config.tools.exec = {
89
+ security: kManagedOpenclawExecDefaults.security,
90
+ strictInlineEval: kManagedOpenclawExecDefaults.strictInlineEval,
91
+ };
92
+ }
93
+ return {
94
+ config,
95
+ changed: JSON.stringify(config) !== before,
96
+ };
97
+ };
98
+
99
+ const ensureManagedExecDefaults = ({ fsModule = fs, openclawDir } = {}) => {
100
+ let openclawChanged = false;
101
+ let approvalsChanged = false;
102
+
103
+ const openclawConfigPath = resolveOpenclawConfigPath({ openclawDir });
104
+ const openclawExists =
105
+ typeof fsModule.existsSync === "function" ? fsModule.existsSync(openclawConfigPath) : null;
106
+ if (openclawExists !== false) {
107
+ const cfg = readOpenclawConfig({
108
+ fsModule,
109
+ openclawDir,
110
+ fallback: openclawExists === true ? null : {},
111
+ });
112
+ if (cfg && typeof cfg === "object" && !Array.isArray(cfg)) {
113
+ const ensuredConfig = ensureManagedOpenclawExecDefaults(cfg);
114
+ if (ensuredConfig.changed) {
115
+ writeOpenclawConfig({
116
+ fsModule,
117
+ openclawDir,
118
+ config: ensuredConfig.config,
119
+ spacing: 2,
120
+ });
121
+ openclawChanged = true;
122
+ }
123
+ }
124
+ }
125
+
126
+ const approvalsPath = resolveExecApprovalsConfigPath({ openclawDir });
127
+ const approvalsExists =
128
+ typeof fsModule.existsSync === "function" ? fsModule.existsSync(approvalsPath) : null;
129
+ const approvals = readExecApprovalsConfig({
130
+ fsModule,
131
+ openclawDir,
132
+ fallback: approvalsExists === true ? null : { version: 1 },
133
+ });
134
+ if (approvals && typeof approvals === "object" && !Array.isArray(approvals)) {
135
+ const ensuredApprovals = ensureManagedExecApprovalsDefaults(approvals);
136
+ if (ensuredApprovals.changed || approvalsExists === false) {
137
+ writeExecApprovalsConfig({
138
+ fsModule,
139
+ openclawDir,
140
+ file: ensuredApprovals.file,
141
+ spacing: 2,
142
+ });
143
+ approvalsChanged = true;
144
+ }
145
+ }
146
+
147
+ return {
148
+ changed: openclawChanged || approvalsChanged,
149
+ openclawChanged,
150
+ approvalsChanged,
151
+ };
152
+ };
153
+
154
+ module.exports = {
155
+ kManagedExecApprovalsDefaults,
156
+ kManagedOpenclawExecDefaults,
157
+ resolveExecApprovalsConfigPath,
158
+ readExecApprovalsConfig,
159
+ writeExecApprovalsConfig,
160
+ ensureManagedExecApprovalsDefaults,
161
+ ensureManagedOpenclawExecDefaults,
162
+ ensureManagedExecDefaults,
163
+ };
@@ -45,6 +45,7 @@ const gatewayEnv = () => ({
45
45
  ...process.env,
46
46
  OPENCLAW_HOME: kRootDir,
47
47
  OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
48
+ OPENCLAW_STATE_DIR: OPENCLAW_DIR,
48
49
  XDG_CONFIG_HOME: OPENCLAW_DIR,
49
50
  });
50
51
 
@@ -17,7 +17,6 @@ const { registerDoctorRoutes } = require("../routes/doctor");
17
17
  const { registerAgentRoutes } = require("../routes/agents");
18
18
  const { registerCronRoutes } = require("../routes/cron");
19
19
  const { registerNodeRoutes } = require("../routes/nodes");
20
- const { registerMcpRoutes } = require("../routes/mcp");
21
20
  const {
22
21
  createOauthCallbackMiddleware,
23
22
  } = require("../oauth-callback-middleware");
@@ -250,13 +249,6 @@ const registerServerRoutes = ({
250
249
  gatewayToken: constants.GATEWAY_TOKEN,
251
250
  fsModule: fs,
252
251
  });
253
- registerMcpRoutes({
254
- app,
255
- requireAuth,
256
- constants,
257
- gatewayEnv,
258
- openclawDir: constants.OPENCLAW_DIR,
259
- });
260
252
  registerProxyRoutes({
261
253
  app,
262
254
  proxy,
@@ -3,6 +3,7 @@ const startServerLifecycle = ({
3
3
  PORT,
4
4
  isOnboarded,
5
5
  runOnboardedBootSequence,
6
+ ensureManagedExecDefaults,
6
7
  ensureUsageTrackerPluginConfig,
7
8
  doSyncPromptFiles,
8
9
  reloadEnv,
@@ -18,6 +19,7 @@ const startServerLifecycle = ({
18
19
  console.log(`[alphaclaw] Express listening on :${PORT}`);
19
20
  if (isOnboarded()) {
20
21
  runOnboardedBootSequence({
22
+ ensureManagedExecDefaults,
21
23
  ensureUsageTrackerPluginConfig,
22
24
  doSyncPromptFiles,
23
25
  reloadEnv,
@@ -0,0 +1,251 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { ALPHACLAW_DIR, kFallbackOnboardingModels } = require("./constants");
4
+
5
+ const kModelCatalogCacheVersion = 1;
6
+ const kModelCatalogRefreshBackoffMs = 30 * 1000;
7
+ const kDefaultCachePath = path.join(ALPHACLAW_DIR, "cache", "model-catalog.json");
8
+
9
+ const createResponse = ({
10
+ source = "fallback",
11
+ fetchedAt = null,
12
+ stale = false,
13
+ refreshing = false,
14
+ models = [],
15
+ } = {}) => ({
16
+ ok: true,
17
+ source,
18
+ fetchedAt,
19
+ stale,
20
+ refreshing,
21
+ models,
22
+ });
23
+
24
+ const normalizeCachedModels = ({
25
+ models,
26
+ normalizeOnboardingModels = (items) => items,
27
+ } = {}) =>
28
+ normalizeOnboardingModels(
29
+ (Array.isArray(models) ? models : []).map((model) => ({
30
+ key: model?.key,
31
+ name: model?.label || model?.name || model?.key,
32
+ })),
33
+ );
34
+
35
+ const normalizeCacheEntry = ({
36
+ raw,
37
+ normalizeOnboardingModels = (items) => items,
38
+ } = {}) => {
39
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
40
+ const fetchedAt = Number(raw.fetchedAt || 0);
41
+ const models = normalizeCachedModels({
42
+ models: raw.models,
43
+ normalizeOnboardingModels,
44
+ });
45
+ if (!Number.isFinite(fetchedAt) || fetchedAt <= 0 || models.length === 0) {
46
+ return null;
47
+ }
48
+ return {
49
+ version: kModelCatalogCacheVersion,
50
+ fetchedAt,
51
+ models,
52
+ };
53
+ };
54
+
55
+ const createModelCatalogCache = ({
56
+ fsModule = fs,
57
+ pathModule = path,
58
+ shellCmd,
59
+ gatewayEnv = () => ({}),
60
+ parseJsonFromNoisyOutput = () => ({}),
61
+ normalizeOnboardingModels = (items) => items,
62
+ fallbackModels = kFallbackOnboardingModels,
63
+ cachePath = kDefaultCachePath,
64
+ refreshBackoffMs = kModelCatalogRefreshBackoffMs,
65
+ now = () => Date.now(),
66
+ setTimeoutFn = setTimeout,
67
+ clearTimeoutFn = clearTimeout,
68
+ logger = console,
69
+ } = {}) => {
70
+ let cacheLoaded = false;
71
+ let memoryCache = null;
72
+ let cacheIsStale = false;
73
+ let refreshPromise = null;
74
+ let retryTimer = null;
75
+ let backoffUntilMs = 0;
76
+
77
+ const clearRetryTimer = () => {
78
+ if (!retryTimer) return;
79
+ clearTimeoutFn(retryTimer);
80
+ retryTimer = null;
81
+ };
82
+
83
+ const isRefreshPending = () => !!refreshPromise || !!retryTimer;
84
+
85
+ const setCacheEntry = (entry, { fresh = false } = {}) => {
86
+ memoryCache = entry;
87
+ cacheLoaded = true;
88
+ cacheIsStale = !fresh;
89
+ backoffUntilMs = 0;
90
+ clearRetryTimer();
91
+ return memoryCache;
92
+ };
93
+
94
+ const readDiskCache = () => {
95
+ if (cacheLoaded) return memoryCache;
96
+ cacheLoaded = true;
97
+ try {
98
+ const raw = JSON.parse(fsModule.readFileSync(cachePath, "utf8"));
99
+ const entry = normalizeCacheEntry({
100
+ raw,
101
+ normalizeOnboardingModels,
102
+ });
103
+ if (!entry) return null;
104
+ memoryCache = entry;
105
+ cacheIsStale = true;
106
+ return memoryCache;
107
+ } catch {
108
+ memoryCache = null;
109
+ cacheIsStale = false;
110
+ return null;
111
+ }
112
+ };
113
+
114
+ const writeDiskCache = (entry) => {
115
+ fsModule.mkdirSync(pathModule.dirname(cachePath), { recursive: true });
116
+ fsModule.writeFileSync(
117
+ cachePath,
118
+ `${JSON.stringify(entry, null, 2)}\n`,
119
+ "utf8",
120
+ );
121
+ };
122
+
123
+ 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 || []);
130
+ if (models.length === 0) {
131
+ throw new Error("No models found");
132
+ }
133
+ const entry = {
134
+ version: kModelCatalogCacheVersion,
135
+ fetchedAt: now(),
136
+ models,
137
+ };
138
+ writeDiskCache(entry);
139
+ setCacheEntry(entry, { fresh: true });
140
+ return entry;
141
+ };
142
+
143
+ const scheduleRetry = () => {
144
+ if (!memoryCache || retryTimer) return;
145
+ const delayMs = Math.max(backoffUntilMs - now(), 0);
146
+ retryTimer = setTimeoutFn(() => {
147
+ retryTimer = null;
148
+ if (!memoryCache || !cacheIsStale || refreshPromise) return;
149
+ void startBackgroundRefresh();
150
+ }, delayMs);
151
+ if (typeof retryTimer?.unref === "function") retryTimer.unref();
152
+ };
153
+
154
+ const handleRefreshFailure = (err) => {
155
+ if (memoryCache) {
156
+ cacheIsStale = true;
157
+ backoffUntilMs = now() + refreshBackoffMs;
158
+ scheduleRetry();
159
+ logger.error?.(
160
+ `[models] Failed to refresh cached models: ${err.message || String(err)}`,
161
+ );
162
+ return;
163
+ }
164
+ logger.error?.(
165
+ `[models] Failed to load dynamic models: ${err.message || String(err)}`,
166
+ );
167
+ };
168
+
169
+ const startBackgroundRefresh = () => {
170
+ readDiskCache();
171
+ if (!memoryCache) return null;
172
+ if (refreshPromise) return refreshPromise;
173
+ if (retryTimer) return null;
174
+ if (backoffUntilMs > now()) {
175
+ scheduleRetry();
176
+ return null;
177
+ }
178
+ refreshPromise = Promise.resolve()
179
+ .then(() => loadFreshCatalog())
180
+ .catch((err) => {
181
+ handleRefreshFailure(err);
182
+ return null;
183
+ })
184
+ .finally(() => {
185
+ refreshPromise = null;
186
+ });
187
+ return refreshPromise;
188
+ };
189
+
190
+ return {
191
+ async getCatalogResponse() {
192
+ readDiskCache();
193
+ if (memoryCache && !cacheIsStale) {
194
+ return createResponse({
195
+ source: "openclaw",
196
+ fetchedAt: memoryCache.fetchedAt,
197
+ stale: false,
198
+ refreshing: false,
199
+ models: memoryCache.models,
200
+ });
201
+ }
202
+ if (memoryCache) {
203
+ startBackgroundRefresh();
204
+ return createResponse({
205
+ source: "cache",
206
+ fetchedAt: memoryCache.fetchedAt,
207
+ stale: true,
208
+ refreshing: isRefreshPending(),
209
+ models: memoryCache.models,
210
+ });
211
+ }
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
+ }
231
+ },
232
+
233
+ markStale() {
234
+ readDiskCache();
235
+ if (!memoryCache) return;
236
+ cacheIsStale = true;
237
+ backoffUntilMs = 0;
238
+ clearRetryTimer();
239
+ },
240
+ };
241
+ };
242
+
243
+ module.exports = {
244
+ createModelCatalogCache,
245
+ createResponse,
246
+ normalizeCachedModels,
247
+ normalizeCacheEntry,
248
+ kModelCatalogCacheVersion,
249
+ kModelCatalogRefreshBackoffMs,
250
+ kDefaultCachePath,
251
+ };
@@ -63,6 +63,56 @@ const repoContainsOnlyBoilerplate = async (repoUrl, ghHeaders) => {
63
63
  }
64
64
  };
65
65
 
66
+ const getNextGithubPageUrl = (linkHeader = "") => {
67
+ const nextLink = String(linkHeader || "")
68
+ .split(",")
69
+ .map((entry) => entry.trim())
70
+ .find((entry) => entry.endsWith('rel="next"'));
71
+ const match = nextLink?.match(/<([^>]+)>/);
72
+ return match?.[1] || "";
73
+ };
74
+
75
+ const findOwnedRepoByName = async ({
76
+ repoUrl,
77
+ repoOwner,
78
+ repoName,
79
+ viewerLogin,
80
+ ghHeaders,
81
+ }) => {
82
+ if (
83
+ !repoOwner ||
84
+ !repoName ||
85
+ !viewerLogin ||
86
+ repoOwner.toLowerCase() !== viewerLogin.toLowerCase()
87
+ ) {
88
+ return null;
89
+ }
90
+
91
+ let nextUrl =
92
+ "https://api.github.com/user/repos?affiliation=owner&per_page=100&page=1";
93
+ const normalizedRepoUrl = String(repoUrl || "").trim().toLowerCase();
94
+ const normalizedRepoName = String(repoName || "").trim().toLowerCase();
95
+
96
+ while (nextUrl) {
97
+ const res = await fetch(nextUrl, { headers: ghHeaders });
98
+ if (!res.ok) return null;
99
+
100
+ const repos = await res.json();
101
+ if (!Array.isArray(repos)) return null;
102
+
103
+ const existingRepo = repos.find((repo) => {
104
+ const fullName = String(repo?.full_name || "").trim().toLowerCase();
105
+ const name = String(repo?.name || "").trim().toLowerCase();
106
+ return fullName === normalizedRepoUrl || name === normalizedRepoName;
107
+ });
108
+ if (existingRepo) return existingRepo;
109
+
110
+ nextUrl = getNextGithubPageUrl(res.headers?.get?.("link"));
111
+ }
112
+
113
+ return null;
114
+ };
115
+
66
116
  const isClassicPat = (token) => String(token || "").startsWith("ghp_");
67
117
  const isFineGrainedPat = (token) =>
68
118
  String(token || "").startsWith("github_pat_");
@@ -73,8 +123,9 @@ const verifyGithubRepoForOnboarding = async ({
73
123
  mode = "new",
74
124
  }) => {
75
125
  const ghHeaders = buildGithubHeaders(githubToken);
76
- const [repoOwner] = String(repoUrl || "").split("/", 1);
126
+ const [repoOwner = "", repoName = ""] = String(repoUrl || "").split("/");
77
127
  const isExisting = mode === "existing";
128
+ let viewerLogin = "";
78
129
 
79
130
  try {
80
131
  const userRes = await fetch("https://api.github.com/user", {
@@ -106,12 +157,29 @@ const verifyGithubRepoForOnboarding = async ({
106
157
  };
107
158
  }
108
159
  }
109
- await userRes.json().catch(() => ({}));
160
+ const userPayload = await userRes.json().catch(() => ({}));
161
+ viewerLogin = String(userPayload?.login || "").trim();
110
162
 
111
163
  const checkRes = await fetch(`https://api.github.com/repos/${repoUrl}`, {
112
164
  headers: ghHeaders,
113
165
  });
114
166
  if (checkRes.status === 404) {
167
+ const hiddenOwnedRepo = await findOwnedRepoByName({
168
+ repoUrl,
169
+ repoOwner,
170
+ repoName,
171
+ viewerLogin,
172
+ ghHeaders,
173
+ });
174
+ if (hiddenOwnedRepo) {
175
+ return {
176
+ ok: false,
177
+ status: 400,
178
+ error:
179
+ `Repository "${repoUrl}" already exists, but this token cannot inspect it. ` +
180
+ "Choose a different repo name or use a token that can access that repo.",
181
+ };
182
+ }
115
183
  if (isExisting) {
116
184
  return {
117
185
  ok: false,
@@ -205,6 +273,19 @@ const ensureGithubRepoAccessible = async ({
205
273
  });
206
274
  if (!createRes.ok) {
207
275
  const details = await parseGithubErrorMessage(createRes);
276
+ if (
277
+ String(details || "")
278
+ .toLowerCase()
279
+ .includes("name already exists on this account")
280
+ ) {
281
+ return {
282
+ ok: false,
283
+ status: 400,
284
+ error:
285
+ `Repository "${repoUrl}" already exists. ` +
286
+ "Choose a different repo name or use a token that can access that repo.",
287
+ };
288
+ }
208
289
  const hint =
209
290
  createRes.status === 404 || createRes.status === 403
210
291
  ? ' Ensure your token is a classic PAT with the "repo" scope.'
@@ -25,6 +25,7 @@ const {
25
25
  } = require("./cron");
26
26
  const { migrateManagedInternalFiles } = require("../internal-files-migration");
27
27
  const { installGogCliSkill } = require("../gog-skill");
28
+ const { ensureManagedExecDefaults } = require("../exec-defaults-config");
28
29
 
29
30
  const kPlaceholderEnvValue = "placeholder";
30
31
  const kEnvRefPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
@@ -494,6 +495,8 @@ const createOnboardingService = ({
494
495
  ...process.env,
495
496
  OPENCLAW_HOME: kRootDir,
496
497
  OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
498
+ OPENCLAW_STATE_DIR: OPENCLAW_DIR,
499
+ XDG_CONFIG_HOME: OPENCLAW_DIR,
497
500
  },
498
501
  timeout: 120000,
499
502
  },
@@ -529,6 +532,10 @@ const createOnboardingService = ({
529
532
  });
530
533
  }
531
534
  authProfiles?.syncConfigAuthReferencesForAgent?.();
535
+ ensureManagedExecDefaults({
536
+ fsModule: fs,
537
+ openclawDir: OPENCLAW_DIR,
538
+ });
532
539
 
533
540
  installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });
534
541