@chrysb/alphaclaw 0.9.15 → 0.9.17

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 (37) hide show
  1. package/lib/public/css/tailwind.generated.css +1 -1
  2. package/lib/public/dist/app.bundle.js +2168 -2103
  3. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +59 -7
  4. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +124 -0
  5. package/lib/public/js/components/envars.js +1 -1
  6. package/lib/public/js/components/file-tree.js +82 -6
  7. package/lib/public/js/components/row-accessory-select.js +52 -0
  8. package/lib/public/js/lib/api.js +11 -1
  9. package/lib/public/js/lib/model-catalog.js +6 -0
  10. package/lib/public/js/lib/model-config.js +12 -7
  11. package/lib/public/js/lib/thinking-levels.js +37 -0
  12. package/lib/server/agents/agents.js +33 -7
  13. package/lib/server/agents/channels.js +4 -2
  14. package/lib/server/chat-ws.js +4 -1
  15. package/lib/server/constants.js +25 -0
  16. package/lib/server/cost-utils.js +2 -0
  17. package/lib/server/db/auth/index.js +147 -0
  18. package/lib/server/db/auth/schema.js +17 -0
  19. package/lib/server/gateway.js +158 -19
  20. package/lib/server/helpers.js +1 -3
  21. package/lib/server/init/register-server-routes.js +37 -18
  22. package/lib/server/init/runtime-init.js +4 -0
  23. package/lib/server/init/server-lifecycle.js +1 -24
  24. package/lib/server/login-throttle.js +242 -60
  25. package/lib/server/model-catalog-bootstrap.json +5 -0
  26. package/lib/server/onboarding/index.js +2 -2
  27. package/lib/server/openclaw-thinking.js +103 -0
  28. package/lib/server/openclaw-version.js +1 -1
  29. package/lib/server/routes/agents.js +10 -3
  30. package/lib/server/routes/browse/constants.js +3 -1
  31. package/lib/server/routes/browse/index.js +39 -11
  32. package/lib/server/routes/models.js +35 -1
  33. package/lib/server/routes/onboarding.js +2 -2
  34. package/lib/server/routes/system.js +2 -2
  35. package/lib/server/usage-tracker-config.js +52 -1
  36. package/lib/server.js +26 -22
  37. package/package.json +2 -2
@@ -3,34 +3,11 @@ const startServerLifecycle = ({
3
3
  PORT,
4
4
  isOnboarded,
5
5
  runOnboardedBootSequence,
6
- ensureManagedExecDefaults,
7
- ensureUsageTrackerPluginConfig,
8
- doSyncPromptFiles,
9
- reloadEnv,
10
- syncChannelConfig,
11
- readEnvFile,
12
- ensureGatewayProxyConfig,
13
- resolveSetupUrl,
14
- startGateway,
15
- watchdog,
16
- gmailWatchService,
17
6
  }) => {
18
7
  server.listen(PORT, "0.0.0.0", () => {
19
8
  console.log(`[alphaclaw] Express listening on :${PORT}`);
20
9
  if (isOnboarded()) {
21
- runOnboardedBootSequence({
22
- ensureManagedExecDefaults,
23
- ensureUsageTrackerPluginConfig,
24
- doSyncPromptFiles,
25
- reloadEnv,
26
- syncChannelConfig,
27
- readEnvFile,
28
- ensureGatewayProxyConfig,
29
- resolveSetupUrl,
30
- startGateway,
31
- watchdog,
32
- gmailWatchService,
33
- });
10
+ runOnboardedBootSequence();
34
11
  } else {
35
12
  console.log("[alphaclaw] Awaiting onboarding via Setup UI");
36
13
  }
@@ -1,77 +1,255 @@
1
- const { kLoginWindowMs, kLoginMaxAttempts, kLoginBaseLockMs, kLoginMaxLockMs, kLoginStateTtlMs } = require("./constants");
2
-
3
- const createLoginThrottle = () => {
4
- const kLoginAttemptStates = new Map();
5
-
6
- const getOrCreateLoginAttemptState = (clientKey, now) => {
7
- const existing = kLoginAttemptStates.get(clientKey);
8
- if (existing) {
9
- existing.lastSeenAt = now;
10
- return existing;
11
- }
12
- const next = {
13
- attempts: 0,
14
- windowStart: now,
15
- lockUntil: 0,
16
- failStreak: 0,
17
- lastSeenAt: now,
18
- };
19
- kLoginAttemptStates.set(clientKey, next);
20
- return next;
21
- };
1
+ const {
2
+ kLoginWindowMs,
3
+ kLoginMaxAttempts,
4
+ kLoginBaseLockMs,
5
+ kLoginMaxLockMs,
6
+ kLoginGlobalWindowMs,
7
+ kLoginGlobalMaxAttempts,
8
+ kLoginGlobalBaseLockMs,
9
+ kLoginGlobalMaxLockMs,
10
+ kLoginStateTtlMs,
11
+ } = require("./constants");
22
12
 
23
- const evaluateLoginThrottle = (state, now) => {
24
- if (!state) return { blocked: false, retryAfterSec: 0 };
25
- if (state.lockUntil > now) {
26
- return {
27
- blocked: true,
28
- retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
29
- };
30
- }
31
- if (now - state.windowStart >= kLoginWindowMs) {
32
- state.attempts = 0;
33
- state.windowStart = now;
34
- }
35
- return { blocked: false, retryAfterSec: 0 };
13
+ const kGlobalStateKey = "global:login";
14
+
15
+ const createLoginAttemptState = (now) => ({
16
+ attempts: 0,
17
+ windowStart: now,
18
+ lockUntil: 0,
19
+ failStreak: 0,
20
+ lastSeenAt: now,
21
+ });
22
+
23
+ const normalizeState = (state, now) => ({
24
+ attempts: Number.parseInt(String(state?.attempts ?? 0), 10) || 0,
25
+ windowStart: Number.parseInt(String(state?.windowStart ?? now), 10) || now,
26
+ lockUntil: Number.parseInt(String(state?.lockUntil ?? 0), 10) || 0,
27
+ failStreak: Number.parseInt(String(state?.failStreak ?? 0), 10) || 0,
28
+ lastSeenAt: Number.parseInt(String(state?.lastSeenAt ?? now), 10) || now,
29
+ });
30
+
31
+ const createMemoryLoginThrottleStore = () => {
32
+ const states = new Map();
33
+
34
+ return {
35
+ get: (stateKey) => states.get(stateKey) || null,
36
+ set: (stateKey, state) => {
37
+ states.set(stateKey, { ...state });
38
+ },
39
+ delete: (stateKey) => {
40
+ states.delete(stateKey);
41
+ },
42
+ entries: () =>
43
+ Array.from(states.entries()).map(([stateKey, state]) => [
44
+ stateKey,
45
+ { ...state },
46
+ ]),
47
+ runExclusive: (callback) => callback(),
36
48
  };
49
+ };
37
50
 
38
- const recordLoginFailure = (state, now) => {
39
- if (!state) return { lockMs: 0, locked: false };
40
- if (now - state.windowStart >= kLoginWindowMs) {
41
- state.attempts = 0;
42
- state.windowStart = now;
43
- }
44
- state.attempts += 1;
51
+ const getClientStateKey = (clientKey) =>
52
+ `client:${String(clientKey || "unknown")}`;
53
+
54
+ const getOrCreateState = (store, stateKey, now) => {
55
+ const existing = store.get(stateKey);
56
+ if (existing) {
57
+ const state = normalizeState(existing, now);
45
58
  state.lastSeenAt = now;
46
- if (state.attempts < kLoginMaxAttempts) {
47
- return { lockMs: 0, locked: false };
48
- }
49
- state.failStreak += 1;
59
+ store.set(stateKey, state);
60
+ return state;
61
+ }
62
+ const next = createLoginAttemptState(now);
63
+ store.set(stateKey, next);
64
+ return next;
65
+ };
66
+
67
+ const evaluateState = ({ store, stateKey, now, windowMs }) => {
68
+ const state = getOrCreateState(store, stateKey, now);
69
+ if (state.lockUntil > now) {
70
+ return {
71
+ state,
72
+ blocked: true,
73
+ retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
74
+ };
75
+ }
76
+ if (now - state.windowStart >= windowMs) {
77
+ state.attempts = 0;
78
+ state.windowStart = now;
79
+ store.set(stateKey, state);
80
+ }
81
+ return { state, blocked: false, retryAfterSec: 0 };
82
+ };
83
+
84
+ const recordStateFailure = ({
85
+ store,
86
+ stateKey,
87
+ now,
88
+ windowMs,
89
+ maxAttempts,
90
+ baseLockMs,
91
+ maxLockMs,
92
+ }) => {
93
+ const state = getOrCreateState(store, stateKey, now);
94
+ if (state.lockUntil > now) {
95
+ return {
96
+ state,
97
+ lockMs: state.lockUntil - now,
98
+ locked: true,
99
+ retryAfterSec: Math.max(1, Math.ceil((state.lockUntil - now) / 1000)),
100
+ };
101
+ }
102
+ if (now - state.windowStart >= windowMs) {
50
103
  state.attempts = 0;
51
104
  state.windowStart = now;
52
- const lockMultiplier = Math.max(1, 2 ** (state.failStreak - 1));
53
- const lockMs = Math.min(kLoginBaseLockMs * lockMultiplier, kLoginMaxLockMs);
54
- state.lockUntil = now + lockMs;
55
- return { lockMs, locked: true };
105
+ }
106
+ state.attempts += 1;
107
+ state.lastSeenAt = now;
108
+ if (state.attempts < maxAttempts) {
109
+ store.set(stateKey, state);
110
+ return { state, lockMs: 0, locked: false, retryAfterSec: 0 };
111
+ }
112
+ state.failStreak += 1;
113
+ state.attempts = 0;
114
+ state.windowStart = now;
115
+ const lockMultiplier = Math.max(1, 2 ** (state.failStreak - 1));
116
+ const lockMs = Math.min(baseLockMs * lockMultiplier, maxLockMs);
117
+ state.lockUntil = now + lockMs;
118
+ store.set(stateKey, state);
119
+ return {
120
+ state,
121
+ lockMs,
122
+ locked: true,
123
+ retryAfterSec: Math.max(1, Math.ceil(lockMs / 1000)),
56
124
  };
125
+ };
126
+
127
+ const chooseThrottleResult = (...results) => {
128
+ const blockedResults = results.filter((result) => result.blocked);
129
+ if (blockedResults.length === 0) {
130
+ return { blocked: false, retryAfterSec: 0 };
131
+ }
132
+ return {
133
+ blocked: true,
134
+ retryAfterSec: Math.max(
135
+ ...blockedResults.map((result) => result.retryAfterSec || 0),
136
+ ),
137
+ };
138
+ };
139
+
140
+ const chooseFailureResult = (...results) => {
141
+ const lockedResults = results.filter((result) => result.locked);
142
+ if (lockedResults.length === 0) {
143
+ return { lockMs: 0, locked: false, retryAfterSec: 0 };
144
+ }
145
+ return {
146
+ lockMs: Math.max(...lockedResults.map((result) => result.lockMs || 0)),
147
+ locked: true,
148
+ retryAfterSec: Math.max(
149
+ ...lockedResults.map((result) => result.retryAfterSec || 0),
150
+ ),
151
+ };
152
+ };
153
+
154
+ const createLoginThrottle = ({
155
+ store = createMemoryLoginThrottleStore(),
156
+ } = {}) => {
157
+ const runExclusive =
158
+ typeof store.runExclusive === "function"
159
+ ? (callback) => store.runExclusive(callback)
160
+ : (callback) => callback();
161
+
162
+ const getOrCreateLoginAttemptState = (clientKey, now) =>
163
+ runExclusive(() => {
164
+ const clientStateKey = getClientStateKey(clientKey);
165
+ return {
166
+ clientKey,
167
+ clientStateKey,
168
+ globalStateKey: kGlobalStateKey,
169
+ client: getOrCreateState(store, clientStateKey, now),
170
+ global: getOrCreateState(store, kGlobalStateKey, now),
171
+ };
172
+ });
173
+
174
+ const evaluateLoginThrottle = (stateBundle, now) =>
175
+ runExclusive(() => {
176
+ const clientStateKey =
177
+ stateBundle?.clientStateKey ||
178
+ getClientStateKey(stateBundle?.clientKey);
179
+ const globalStateKey = stateBundle?.globalStateKey || kGlobalStateKey;
180
+ const clientResult = evaluateState({
181
+ store,
182
+ stateKey: clientStateKey,
183
+ now,
184
+ windowMs: kLoginWindowMs,
185
+ });
186
+ const globalResult = evaluateState({
187
+ store,
188
+ stateKey: globalStateKey,
189
+ now,
190
+ windowMs: kLoginGlobalWindowMs,
191
+ });
192
+ if (stateBundle) {
193
+ stateBundle.client = clientResult.state;
194
+ stateBundle.global = globalResult.state;
195
+ }
196
+ return chooseThrottleResult(clientResult, globalResult);
197
+ });
198
+
199
+ const recordLoginFailure = (stateBundle, now) =>
200
+ runExclusive(() => {
201
+ const clientStateKey =
202
+ stateBundle?.clientStateKey ||
203
+ getClientStateKey(stateBundle?.clientKey);
204
+ const globalStateKey = stateBundle?.globalStateKey || kGlobalStateKey;
205
+ const clientResult = recordStateFailure({
206
+ store,
207
+ stateKey: clientStateKey,
208
+ now,
209
+ windowMs: kLoginWindowMs,
210
+ maxAttempts: kLoginMaxAttempts,
211
+ baseLockMs: kLoginBaseLockMs,
212
+ maxLockMs: kLoginMaxLockMs,
213
+ });
214
+ const globalResult = recordStateFailure({
215
+ store,
216
+ stateKey: globalStateKey,
217
+ now,
218
+ windowMs: kLoginGlobalWindowMs,
219
+ maxAttempts: kLoginGlobalMaxAttempts,
220
+ baseLockMs: kLoginGlobalBaseLockMs,
221
+ maxLockMs: kLoginGlobalMaxLockMs,
222
+ });
223
+ if (stateBundle) {
224
+ stateBundle.client = clientResult.state;
225
+ stateBundle.global = globalResult.state;
226
+ }
227
+ return chooseFailureResult(clientResult, globalResult);
228
+ });
57
229
 
58
230
  const recordLoginSuccess = (clientKey) => {
59
231
  if (!clientKey) return;
60
- kLoginAttemptStates.delete(clientKey);
232
+ runExclusive(() => {
233
+ store.delete(getClientStateKey(clientKey));
234
+ store.delete(kGlobalStateKey);
235
+ });
61
236
  };
62
237
 
63
238
  const cleanupLoginAttemptStates = () => {
64
239
  const now = Date.now();
65
- for (const [key, state] of kLoginAttemptStates.entries()) {
66
- if (!state) {
67
- kLoginAttemptStates.delete(key);
68
- continue;
240
+ runExclusive(() => {
241
+ for (const [stateKey, rawState] of store.entries()) {
242
+ if (!rawState) {
243
+ store.delete(stateKey);
244
+ continue;
245
+ }
246
+ const state = normalizeState(rawState, now);
247
+ if (state.lockUntil > now) continue;
248
+ if (now - state.lastSeenAt > kLoginStateTtlMs) {
249
+ store.delete(stateKey);
250
+ }
69
251
  }
70
- if (state.lockUntil > now) continue;
71
- if (now - state.lastSeenAt > kLoginStateTtlMs) {
72
- kLoginAttemptStates.delete(key);
73
- }
74
- }
252
+ });
75
253
  };
76
254
 
77
255
  return {
@@ -83,4 +261,8 @@ const createLoginThrottle = () => {
83
261
  };
84
262
  };
85
263
 
86
- module.exports = { createLoginThrottle };
264
+ module.exports = {
265
+ createLoginThrottle,
266
+ createMemoryLoginThrottleStore,
267
+ kGlobalStateKey,
268
+ };
@@ -554,6 +554,11 @@
554
554
  "provider": "anthropic",
555
555
  "label": "Claude Opus 4.6"
556
556
  },
557
+ {
558
+ "key": "anthropic/claude-opus-4-8",
559
+ "provider": "anthropic",
560
+ "label": "Claude Opus 4.8"
561
+ },
557
562
  {
558
563
  "key": "anthropic/claude-opus-4-7",
559
564
  "provider": "anthropic",
@@ -327,7 +327,7 @@ const createOnboardingService = ({
327
327
  authProfiles,
328
328
  ensureGatewayProxyConfig,
329
329
  getBaseUrl,
330
- startGateway,
330
+ runOnboardedBootSequence,
331
331
  }) => {
332
332
  const { OPENCLAW_DIR, WORKSPACE_DIR, kOnboardingMarkerPath } = constants;
333
333
 
@@ -567,7 +567,7 @@ const createOnboardingService = ({
567
567
  console.error("[onboard] Git push error:", e.message);
568
568
  }
569
569
 
570
- startGateway();
570
+ runOnboardedBootSequence();
571
571
  return { status: 200, body: { ok: true } };
572
572
  };
573
573
 
@@ -0,0 +1,103 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { pathToFileURL } = require("url");
4
+
5
+ const kThinkingModuleSentinel = "listThinkingLevelOptions";
6
+
7
+ let thinkingModulePromise = null;
8
+
9
+ const resolveOpenclawDistDir = () => path.dirname(require.resolve("openclaw"));
10
+
11
+ const resolveThinkingModulePath = (distDir = resolveOpenclawDistDir()) => {
12
+ for (const name of fs.readdirSync(distDir)) {
13
+ if (!/^thinking-.*\.js$/.test(name)) continue;
14
+ if (name.includes("api") || name.includes("policy")) continue;
15
+ const fullPath = path.join(distDir, name);
16
+ const source = fs.readFileSync(fullPath, "utf8");
17
+ if (source.includes(kThinkingModuleSentinel)) return fullPath;
18
+ }
19
+ throw new Error("OpenClaw thinking module not found");
20
+ };
21
+
22
+ const loadThinkingModule = async () => {
23
+ if (!thinkingModulePromise) {
24
+ const modulePath = resolveThinkingModulePath();
25
+ thinkingModulePromise = import(pathToFileURL(modulePath).href);
26
+ }
27
+ return thinkingModulePromise;
28
+ };
29
+
30
+ const splitModelKey = (modelKey = "") => {
31
+ const normalized = String(modelKey || "").trim();
32
+ const slashIndex = normalized.indexOf("/");
33
+ if (slashIndex <= 0) return { provider: "", model: normalized };
34
+ return {
35
+ provider: normalized.slice(0, slashIndex),
36
+ model: normalized.slice(slashIndex + 1),
37
+ };
38
+ };
39
+
40
+ const buildCatalogEntry = ({ provider, model, reasoning, compat } = {}) => {
41
+ const normalizedProvider = String(provider || "").trim();
42
+ const normalizedModel = String(model || "").trim();
43
+ if (!normalizedProvider || !normalizedModel) return null;
44
+ const entry = {
45
+ provider: normalizedProvider,
46
+ id: normalizedModel,
47
+ };
48
+ if (typeof reasoning === "boolean") entry.reasoning = reasoning;
49
+ if (compat && typeof compat === "object") entry.compat = compat;
50
+ return entry;
51
+ };
52
+
53
+ const resolveThinkingApi = async () => {
54
+ const mod = await loadThinkingModule();
55
+ return {
56
+ listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i,
57
+ resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s,
58
+ normalizeThinkLevel: mod.normalizeThinkLevel || mod.p,
59
+ };
60
+ };
61
+
62
+ const resolveThinkingOptionsForModel = async ({
63
+ modelKey = "",
64
+ catalog = [],
65
+ } = {}) => {
66
+ const { provider, model } = splitModelKey(modelKey);
67
+ if (!provider || !model) {
68
+ return {
69
+ levels: [],
70
+ modelDefault: "off",
71
+ };
72
+ }
73
+ const api = await resolveThinkingApi();
74
+ const levels = api.listThinkingLevelOptions(provider, model, catalog) || [];
75
+ const modelDefault =
76
+ api.resolveThinkingDefaultForModel({
77
+ provider,
78
+ model,
79
+ catalog,
80
+ }) || "off";
81
+ return {
82
+ levels: levels.map((entry) => ({
83
+ id: String(entry?.id || "").trim(),
84
+ label: String(entry?.label || entry?.id || "").trim(),
85
+ })),
86
+ modelDefault: String(modelDefault || "off").trim() || "off",
87
+ };
88
+ };
89
+
90
+ const normalizeThinkingDefaultValue = async (raw) => {
91
+ if (raw === null || raw === undefined || raw === "") return null;
92
+ const api = await resolveThinkingApi();
93
+ const normalized = api.normalizeThinkLevel(String(raw || "").trim());
94
+ return normalized || null;
95
+ };
96
+
97
+ module.exports = {
98
+ buildCatalogEntry,
99
+ loadThinkingModule,
100
+ normalizeThinkingDefaultValue,
101
+ resolveThinkingOptionsForModel,
102
+ splitModelKey,
103
+ };
@@ -237,7 +237,7 @@ const createOpenclawVersionService = ({
237
237
  });
238
238
  let restarted = false;
239
239
  if (isOnboarded()) {
240
- restartGateway();
240
+ await restartGateway();
241
241
  restarted = true;
242
242
  }
243
243
  return {
@@ -178,7 +178,11 @@ const registerAgentRoutes = ({
178
178
 
179
179
  app.get("/api/agents", (_req, res) => {
180
180
  try {
181
- res.json({ ok: true, agents: agentsService.listAgents() });
181
+ res.json({
182
+ ok: true,
183
+ agents: agentsService.listAgents(),
184
+ defaults: agentsService.getAgentDefaults?.() || {},
185
+ });
182
186
  } catch (error) {
183
187
  res.status(500).json({ ok: false, error: error.message });
184
188
  }
@@ -237,9 +241,12 @@ const registerAgentRoutes = ({
237
241
  }
238
242
  });
239
243
 
240
- app.put("/api/agents/:id", (req, res) => {
244
+ app.put("/api/agents/:id", async (req, res) => {
241
245
  try {
242
- const agent = agentsService.updateAgent(req.params.id, req.body || {});
246
+ const agent = await agentsService.updateAgent(
247
+ req.params.id,
248
+ req.body || {},
249
+ );
243
250
  return res.json({ ok: true, agent });
244
251
  } catch (error) {
245
252
  const status = String(error.message || "").includes("not found")
@@ -1,4 +1,5 @@
1
- const kDefaultTreeDepth = 10;
1
+ const kDefaultTreeDepth = 3;
2
+ const kMaxTreeDepth = 3;
2
3
  const kIgnoredDirectoryNames = new Set([
3
4
  ".git",
4
5
  ".alphaclaw",
@@ -42,6 +43,7 @@ const kSqliteTablePageSize = 50;
42
43
 
43
44
  module.exports = {
44
45
  kDefaultTreeDepth,
46
+ kMaxTreeDepth,
45
47
  kIgnoredDirectoryNames,
46
48
  kImageMimeTypeByExtension,
47
49
  kCommitHistoryLimit,
@@ -2,6 +2,7 @@ const path = require("path");
2
2
  const { kLockedBrowsePaths, kProtectedBrowsePaths } = require("../../constants");
3
3
  const {
4
4
  kDefaultTreeDepth,
5
+ kMaxTreeDepth,
5
6
  kIgnoredDirectoryNames,
6
7
  kCommitHistoryLimit,
7
8
  } = require("./constants");
@@ -34,6 +35,16 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
34
35
  fs.mkdirSync(kRootResolved, { recursive: true });
35
36
  }
36
37
 
38
+ const readVisibleDirectoryEntries = (absolutePath) =>
39
+ fs
40
+ .readdirSync(absolutePath, { withFileTypes: true })
41
+ .filter((entry) => {
42
+ if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) {
43
+ return false;
44
+ }
45
+ return entry.isDirectory() || entry.isFile();
46
+ });
47
+
37
48
  const buildTreeNode = (absolutePath, depthRemaining) => {
38
49
  const stats = fs.statSync(absolutePath);
39
50
  const nodeName = path.basename(absolutePath);
@@ -44,17 +55,17 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
44
55
  }
45
56
 
46
57
  if (depthRemaining <= 0) {
47
- return { type: "folder", name: nodeName, path: nodePath, children: [] };
58
+ const hasMoreChildren = readVisibleDirectoryEntries(absolutePath).length > 0;
59
+ return {
60
+ type: "folder",
61
+ name: nodeName,
62
+ path: nodePath,
63
+ children: [],
64
+ truncated: hasMoreChildren,
65
+ };
48
66
  }
49
67
 
50
- const children = fs
51
- .readdirSync(absolutePath, { withFileTypes: true })
52
- .filter((entry) => {
53
- if (entry.isDirectory() && kIgnoredDirectoryNames.has(entry.name)) {
54
- return false;
55
- }
56
- return entry.isDirectory() || entry.isFile();
57
- })
68
+ const children = readVisibleDirectoryEntries(absolutePath)
58
69
  .map((entry) =>
59
70
  buildTreeNode(path.join(absolutePath, entry.name), depthRemaining - 1),
60
71
  )
@@ -70,12 +81,29 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
70
81
 
71
82
  app.get("/api/browse/tree", (req, res) => {
72
83
  const depthValue = Number.parseInt(String(req.query.depth || ""), 10);
73
- const depth =
84
+ const requestedDepth =
74
85
  Number.isFinite(depthValue) && depthValue > 0
75
86
  ? depthValue
76
87
  : kDefaultTreeDepth;
88
+ const depth = Math.min(requestedDepth, kMaxTreeDepth);
89
+ const requestedPath = String(req.query.path || "").trim();
77
90
  try {
78
- const tree = buildTreeNode(kRootResolved, depth);
91
+ const resolvedPath = requestedPath
92
+ ? resolveSafePath(
93
+ requestedPath,
94
+ kRootResolved,
95
+ kRootWithSep,
96
+ kRootDisplayName,
97
+ )
98
+ : { ok: true, absolutePath: kRootResolved };
99
+ if (!resolvedPath.ok) {
100
+ return res.status(400).json({ ok: false, error: resolvedPath.error });
101
+ }
102
+ const stats = fs.statSync(resolvedPath.absolutePath);
103
+ if (!stats.isDirectory()) {
104
+ return res.status(400).json({ ok: false, error: "Path is not a folder" });
105
+ }
106
+ const tree = buildTreeNode(resolvedPath.absolutePath, depth);
79
107
  return res.json({ ok: true, root: tree });
80
108
  } catch (error) {
81
109
  return res.status(500).json({
@@ -1,5 +1,7 @@
1
- const { kFallbackOnboardingModels } = require("../constants");
1
+ const { kFallbackOnboardingModels, OPENCLAW_DIR } = require("../constants");
2
2
  const { createModelCatalogCache } = require("../model-catalog-cache");
3
+ const { resolveThinkingOptionsForModel } = require("../openclaw-thinking");
4
+ const { readOpenclawConfig } = require("../openclaw-config");
3
5
  const { getCommandOutputCandidates } = require("../utils/command-output");
4
6
 
5
7
  const runModelsGitSync = async (shellCmd) => {
@@ -182,6 +184,38 @@ const registerModelRoutes = ({
182
184
  return res.json(response);
183
185
  });
184
186
 
187
+ app.get("/api/models/thinking-options", async (req, res) => {
188
+ const modelKey = String(req.query?.modelKey || "").trim();
189
+ if (!modelKey.includes("/")) {
190
+ return res
191
+ .status(400)
192
+ .json({ ok: false, error: "modelKey is required (provider/model)" });
193
+ }
194
+ try {
195
+ const cfg = readOpenclawConfig({ openclawDir: OPENCLAW_DIR });
196
+ const globalThinkingDefault =
197
+ typeof cfg.agents?.defaults?.thinkingDefault === "string"
198
+ ? cfg.agents.defaults.thinkingDefault.trim()
199
+ : null;
200
+ const { levels, modelDefault } = await resolveThinkingOptionsForModel({
201
+ modelKey,
202
+ });
203
+ const inheritedDefault = globalThinkingDefault || modelDefault || "off";
204
+ return res.json({
205
+ ok: true,
206
+ modelKey,
207
+ levels,
208
+ modelDefault,
209
+ inheritedDefault,
210
+ });
211
+ } catch (err) {
212
+ return res.status(500).json({
213
+ ok: false,
214
+ error: err.message || "Failed to resolve thinking options",
215
+ });
216
+ }
217
+ });
218
+
185
219
  app.get("/api/models/status", async (req, res) => {
186
220
  try {
187
221
  const output = await shellCmd("openclaw models status --json", {