@chrysb/alphaclaw 0.4.6-beta.4 → 0.4.6-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/public/js/app.js +158 -1073
- package/lib/public/js/components/envars.js +146 -29
- package/lib/public/js/components/features.js +1 -1
- package/lib/public/js/components/general/index.js +155 -0
- package/lib/public/js/components/icons.js +52 -0
- package/lib/public/js/components/info-tooltip.js +4 -7
- package/lib/public/js/components/models-tab/index.js +286 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
- package/lib/public/js/components/models-tab/use-models.js +262 -0
- package/lib/public/js/components/models.js +1 -1
- package/lib/public/js/components/providers.js +1 -1
- package/lib/public/js/components/routes/browse-route.js +35 -0
- package/lib/public/js/components/routes/doctor-route.js +21 -0
- package/lib/public/js/components/routes/envars-route.js +11 -0
- package/lib/public/js/components/routes/general-route.js +45 -0
- package/lib/public/js/components/routes/index.js +11 -0
- package/lib/public/js/components/routes/models-route.js +11 -0
- package/lib/public/js/components/routes/providers-route.js +11 -0
- package/lib/public/js/components/routes/route-redirect.js +10 -0
- package/lib/public/js/components/routes/telegram-route.js +11 -0
- package/lib/public/js/components/routes/usage-route.js +15 -0
- package/lib/public/js/components/routes/watchdog-route.js +32 -0
- package/lib/public/js/components/routes/webhooks-route.js +43 -0
- package/lib/public/js/components/sidebar.js +2 -3
- package/lib/public/js/components/tooltip.js +106 -0
- package/lib/public/js/components/usage-tab/constants.js +1 -1
- package/lib/public/js/components/usage-tab/overview-section.js +124 -50
- package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
- package/lib/public/js/components/welcome.js +1 -1
- package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
- package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
- package/lib/public/js/hooks/use-browse-navigation.js +193 -0
- package/lib/public/js/hooks/use-hash-location.js +32 -0
- package/lib/public/js/lib/api.js +35 -0
- package/lib/public/js/lib/app-navigation.js +39 -0
- package/lib/public/js/lib/browse-restart-policy.js +28 -0
- package/lib/public/js/lib/browse-route.js +57 -0
- package/lib/public/js/lib/format.js +12 -0
- package/lib/public/js/lib/model-config.js +1 -0
- package/lib/server/auth-profiles.js +291 -53
- package/lib/server/constants.js +24 -8
- package/lib/server/doctor/service.js +0 -3
- package/lib/server/gateway.js +50 -31
- package/lib/server/onboarding/index.js +2 -0
- package/lib/server/onboarding/validation.js +2 -2
- package/lib/server/routes/models.js +214 -2
- package/lib/server/routes/onboarding.js +2 -0
- package/lib/server/routes/system.js +42 -1
- package/lib/server/watchdog.js +14 -1
- package/lib/server.js +6 -0
- package/lib/setup/env.template +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
const { kFallbackOnboardingModels } = require("../constants");
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const runModelsGitSync = async (shellCmd) => {
|
|
4
|
+
if (typeof shellCmd !== "function") return null;
|
|
5
|
+
try {
|
|
6
|
+
await shellCmd('alphaclaw git-sync -m "models: update config" -f "openclaw.json"', {
|
|
7
|
+
timeout: 30000,
|
|
8
|
+
});
|
|
9
|
+
return null;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return err?.message || "alphaclaw git-sync failed";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const registerModelRoutes = ({
|
|
16
|
+
app,
|
|
17
|
+
shellCmd,
|
|
18
|
+
gatewayEnv,
|
|
19
|
+
parseJsonFromNoisyOutput,
|
|
20
|
+
normalizeOnboardingModels,
|
|
21
|
+
authProfiles,
|
|
22
|
+
readEnvFile,
|
|
23
|
+
writeEnvFile,
|
|
24
|
+
reloadEnv,
|
|
25
|
+
}) => {
|
|
26
|
+
const upsertEnvVar = (items, key, value) => {
|
|
27
|
+
const next = Array.isArray(items) ? [...items] : [];
|
|
28
|
+
const existing = next.find((entry) => entry.key === key);
|
|
29
|
+
if (existing) {
|
|
30
|
+
existing.value = value;
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
next.push({ key, value });
|
|
34
|
+
return next;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const syncEnvVarsForProfiles = (profiles) => {
|
|
38
|
+
if (
|
|
39
|
+
!Array.isArray(profiles) ||
|
|
40
|
+
typeof readEnvFile !== "function" ||
|
|
41
|
+
typeof writeEnvFile !== "function" ||
|
|
42
|
+
typeof reloadEnv !== "function"
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let nextEnvVars = readEnvFile();
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const profile of profiles) {
|
|
49
|
+
if (profile?.type !== "api_key") continue;
|
|
50
|
+
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(profile.provider);
|
|
51
|
+
const envValue = String(profile?.key || "").trim();
|
|
52
|
+
if (!envKey || !envValue) continue;
|
|
53
|
+
const prevValue = String(
|
|
54
|
+
nextEnvVars.find((entry) => entry.key === envKey)?.value || "",
|
|
55
|
+
);
|
|
56
|
+
if (prevValue === envValue) continue;
|
|
57
|
+
nextEnvVars = upsertEnvVar(nextEnvVars, envKey, envValue);
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
if (!changed) return;
|
|
61
|
+
writeEnvFile(nextEnvVars);
|
|
62
|
+
reloadEnv();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── Existing CLI-backed catalog/status routes ──
|
|
66
|
+
|
|
4
67
|
app.get("/api/models", async (req, res) => {
|
|
5
68
|
try {
|
|
6
69
|
const output = await shellCmd("openclaw models list --all --json", {
|
|
@@ -60,7 +123,156 @@ const registerModelRoutes = ({ app, shellCmd, gatewayEnv, parseJsonFromNoisyOutp
|
|
|
60
123
|
});
|
|
61
124
|
res.json({ ok: true });
|
|
62
125
|
} catch (err) {
|
|
63
|
-
res
|
|
126
|
+
res
|
|
127
|
+
.status(400)
|
|
128
|
+
.json({ ok: false, error: err.message || "Failed to set model" });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ── Model config (direct JSON) ──
|
|
133
|
+
|
|
134
|
+
app.get("/api/models/config", (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
const { primary, configuredModels } = authProfiles.getModelConfig();
|
|
137
|
+
const agentId = req.query.agentId || undefined;
|
|
138
|
+
const profiles = authProfiles.listProfiles(agentId);
|
|
139
|
+
const store = authProfiles.loadAuthStore(agentId);
|
|
140
|
+
res.json({
|
|
141
|
+
ok: true,
|
|
142
|
+
primary,
|
|
143
|
+
configuredModels,
|
|
144
|
+
authProfiles: profiles,
|
|
145
|
+
authOrder: store.order || {},
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
res
|
|
149
|
+
.status(500)
|
|
150
|
+
.json({ ok: false, error: err.message || "Failed to read config" });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
app.put("/api/models/config", async (req, res) => {
|
|
155
|
+
const { primary, configuredModels, profiles, authOrder } = req.body || {};
|
|
156
|
+
const agentId = req.query.agentId || undefined;
|
|
157
|
+
if (primary !== undefined && (typeof primary !== "string" || !primary.includes("/"))) {
|
|
158
|
+
return res
|
|
159
|
+
.status(400)
|
|
160
|
+
.json({ ok: false, error: "Invalid primary model key" });
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
configuredModels !== undefined &&
|
|
164
|
+
(typeof configuredModels !== "object" || configuredModels === null)
|
|
165
|
+
) {
|
|
166
|
+
return res
|
|
167
|
+
.status(400)
|
|
168
|
+
.json({ ok: false, error: "Invalid configuredModels" });
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
authProfiles.setModelConfig({ primary, configuredModels });
|
|
172
|
+
|
|
173
|
+
if (Array.isArray(profiles)) {
|
|
174
|
+
for (const { id: profileId, ...credential } of profiles) {
|
|
175
|
+
if (profileId && credential.type && credential.provider) {
|
|
176
|
+
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
syncEnvVarsForProfiles(profiles);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (authOrder && typeof authOrder === "object") {
|
|
183
|
+
for (const [provider, order] of Object.entries(authOrder)) {
|
|
184
|
+
if (Array.isArray(order)) {
|
|
185
|
+
authProfiles.setAuthOrder(provider, order, agentId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `auth-profiles.json` is the durable source of truth. Re-sync
|
|
191
|
+
// `openclaw.json.auth.profiles` on save so model re-adds restore refs.
|
|
192
|
+
authProfiles.syncConfigAuthReferencesForAgent(agentId);
|
|
193
|
+
|
|
194
|
+
const syncWarning = await runModelsGitSync(shellCmd);
|
|
195
|
+
res.json({
|
|
196
|
+
ok: true,
|
|
197
|
+
...(syncWarning ? { syncWarning } : {}),
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
res
|
|
201
|
+
.status(500)
|
|
202
|
+
.json({ ok: false, error: err.message || "Failed to save config" });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── Auth profiles (direct JSON) ──
|
|
207
|
+
|
|
208
|
+
app.get("/api/models/auth", (req, res) => {
|
|
209
|
+
try {
|
|
210
|
+
const agentId = req.query.agentId || undefined;
|
|
211
|
+
const profiles = authProfiles.listProfiles(agentId);
|
|
212
|
+
const store = authProfiles.loadAuthStore(agentId);
|
|
213
|
+
res.json({ ok: true, profiles, order: store.order || {} });
|
|
214
|
+
} catch (err) {
|
|
215
|
+
res
|
|
216
|
+
.status(500)
|
|
217
|
+
.json({
|
|
218
|
+
ok: false,
|
|
219
|
+
error: err.message || "Failed to read auth profiles",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
app.put("/api/models/auth/:profileId", (req, res) => {
|
|
225
|
+
const { profileId } = req.params;
|
|
226
|
+
const credential = req.body;
|
|
227
|
+
if (
|
|
228
|
+
!profileId ||
|
|
229
|
+
!credential?.type ||
|
|
230
|
+
!credential?.provider
|
|
231
|
+
) {
|
|
232
|
+
return res
|
|
233
|
+
.status(400)
|
|
234
|
+
.json({ ok: false, error: "Missing profileId, type, or provider" });
|
|
235
|
+
}
|
|
236
|
+
const validTypes = new Set(["api_key", "token", "oauth"]);
|
|
237
|
+
if (!validTypes.has(credential.type)) {
|
|
238
|
+
return res.status(400).json({
|
|
239
|
+
ok: false,
|
|
240
|
+
error: `Invalid credential type: ${credential.type}`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const agentId = req.query.agentId || undefined;
|
|
245
|
+
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
246
|
+
syncEnvVarsForProfiles([{ id: profileId, ...credential }]);
|
|
247
|
+
res.json({ ok: true });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
res
|
|
250
|
+
.status(500)
|
|
251
|
+
.json({
|
|
252
|
+
ok: false,
|
|
253
|
+
error: err.message || "Failed to save auth profile",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
app.delete("/api/models/auth/:profileId", (req, res) => {
|
|
259
|
+
const { profileId } = req.params;
|
|
260
|
+
if (!profileId) {
|
|
261
|
+
return res
|
|
262
|
+
.status(400)
|
|
263
|
+
.json({ ok: false, error: "Missing profileId" });
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const agentId = req.query.agentId || undefined;
|
|
267
|
+
const removed = authProfiles.removeProfile(profileId, agentId);
|
|
268
|
+
res.json({ ok: true, removed });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
res
|
|
271
|
+
.status(500)
|
|
272
|
+
.json({
|
|
273
|
+
ok: false,
|
|
274
|
+
error: err.message || "Failed to remove auth profile",
|
|
275
|
+
});
|
|
64
276
|
}
|
|
65
277
|
});
|
|
66
278
|
};
|
|
@@ -71,6 +71,7 @@ const registerOnboardingRoutes = ({
|
|
|
71
71
|
resolveGithubRepoUrl,
|
|
72
72
|
resolveModelProvider,
|
|
73
73
|
hasCodexOauthProfile,
|
|
74
|
+
authProfiles,
|
|
74
75
|
ensureGatewayProxyConfig,
|
|
75
76
|
getBaseUrl,
|
|
76
77
|
startGateway,
|
|
@@ -85,6 +86,7 @@ const registerOnboardingRoutes = ({
|
|
|
85
86
|
resolveGithubRepoUrl,
|
|
86
87
|
resolveModelProvider,
|
|
87
88
|
hasCodexOauthProfile,
|
|
89
|
+
authProfiles,
|
|
88
90
|
ensureGatewayProxyConfig,
|
|
89
91
|
getBaseUrl,
|
|
90
92
|
startGateway,
|
|
@@ -21,6 +21,7 @@ const registerSystemRoutes = ({
|
|
|
21
21
|
OPENCLAW_DIR,
|
|
22
22
|
restartRequiredState,
|
|
23
23
|
topicRegistry,
|
|
24
|
+
authProfiles,
|
|
24
25
|
}) => {
|
|
25
26
|
let envRestartPending = false;
|
|
26
27
|
const kEnvVarsReservedForUserInput = new Set([
|
|
@@ -93,6 +94,34 @@ const registerSystemRoutes = ({
|
|
|
93
94
|
}
|
|
94
95
|
return key || "Session";
|
|
95
96
|
};
|
|
97
|
+
const syncApiKeyAuthProfilesFromEnvVars = (nextEnvVars) => {
|
|
98
|
+
if (!authProfiles) return;
|
|
99
|
+
const envMap = new Map(
|
|
100
|
+
(nextEnvVars || []).map((entry) => [
|
|
101
|
+
String(entry?.key || "").trim(),
|
|
102
|
+
String(entry?.value || ""),
|
|
103
|
+
]),
|
|
104
|
+
);
|
|
105
|
+
const providers = [
|
|
106
|
+
"anthropic",
|
|
107
|
+
"openai",
|
|
108
|
+
"google",
|
|
109
|
+
"mistral",
|
|
110
|
+
"voyage",
|
|
111
|
+
"groq",
|
|
112
|
+
"deepgram",
|
|
113
|
+
];
|
|
114
|
+
for (const provider of providers) {
|
|
115
|
+
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
|
|
116
|
+
if (!envKey) continue;
|
|
117
|
+
const value = envMap.get(envKey) || "";
|
|
118
|
+
if (!value.trim()) {
|
|
119
|
+
authProfiles.removeApiKeyProfileForEnvVar?.(provider);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
authProfiles.upsertApiKeyProfileForEnvVar(provider, value);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
96
125
|
const listSendableAgentSessions = async () => {
|
|
97
126
|
const result = await clawCmd("sessions --json", { quiet: true });
|
|
98
127
|
if (!result.ok) {
|
|
@@ -168,6 +197,7 @@ const registerSystemRoutes = ({
|
|
|
168
197
|
}
|
|
169
198
|
return getSystemCronStatus();
|
|
170
199
|
};
|
|
200
|
+
const isVisibleInEnvars = (def) => def?.visibleInEnvars !== false;
|
|
171
201
|
|
|
172
202
|
app.get("/api/env", (req, res) => {
|
|
173
203
|
const fileVars = readEnvFile();
|
|
@@ -175,6 +205,7 @@ const registerSystemRoutes = ({
|
|
|
175
205
|
|
|
176
206
|
for (const def of kKnownVars) {
|
|
177
207
|
if (isReservedUserEnvVar(def.key)) continue;
|
|
208
|
+
if (!isVisibleInEnvars(def)) continue;
|
|
178
209
|
const fileEntry = fileVars.find((v) => v.key === def.key);
|
|
179
210
|
const value = fileEntry?.value || "";
|
|
180
211
|
merged.push({
|
|
@@ -183,6 +214,7 @@ const registerSystemRoutes = ({
|
|
|
183
214
|
label: def.label,
|
|
184
215
|
group: def.group,
|
|
185
216
|
hint: def.hint,
|
|
217
|
+
features: def.features,
|
|
186
218
|
source: fileEntry?.value ? "env_file" : "unset",
|
|
187
219
|
editable: true,
|
|
188
220
|
});
|
|
@@ -232,10 +264,19 @@ const registerSystemRoutes = ({
|
|
|
232
264
|
const existingLockedVars = readEnvFile().filter((v) =>
|
|
233
265
|
isReservedUserEnvVar(v.key),
|
|
234
266
|
);
|
|
235
|
-
const
|
|
267
|
+
const hiddenKnownVarKeys = new Set(
|
|
268
|
+
kKnownVars
|
|
269
|
+
.filter((def) => !isReservedUserEnvVar(def.key) && !isVisibleInEnvars(def))
|
|
270
|
+
.map((def) => def.key),
|
|
271
|
+
);
|
|
272
|
+
const existingHiddenKnownVars = readEnvFile().filter((v) =>
|
|
273
|
+
hiddenKnownVarKeys.has(v.key),
|
|
274
|
+
);
|
|
275
|
+
const nextEnvVars = [...filtered, ...existingHiddenKnownVars, ...existingLockedVars];
|
|
236
276
|
syncChannelConfig(nextEnvVars, "remove");
|
|
237
277
|
writeEnvFile(nextEnvVars);
|
|
238
278
|
const changed = reloadEnv();
|
|
279
|
+
syncApiKeyAuthProfilesFromEnvVars(nextEnvVars);
|
|
239
280
|
if (changed && isOnboarded()) {
|
|
240
281
|
envRestartPending = true;
|
|
241
282
|
}
|
package/lib/server/watchdog.js
CHANGED
|
@@ -364,14 +364,27 @@ const createWatchdog = ({
|
|
|
364
364
|
}
|
|
365
365
|
if (parsed.ok) {
|
|
366
366
|
const wasUnhealthy = state.health !== "healthy";
|
|
367
|
+
const recoveredFromCrashLoop = state.lifecycle === "crash_loop";
|
|
367
368
|
state.startupConsecutiveHealthFailures = 0;
|
|
368
369
|
clearDegradedHealthCheckTimer();
|
|
369
370
|
clearExpectedRestartWindow();
|
|
370
371
|
state.health = "healthy";
|
|
371
|
-
|
|
372
|
+
state.lifecycle = "running";
|
|
372
373
|
if (!state.uptimeStartedAt || wasUnhealthy) state.uptimeStartedAt = Date.now();
|
|
373
374
|
state.repairAttempts = 0;
|
|
374
375
|
state.crashRecoveryActive = false;
|
|
376
|
+
if (recoveredFromCrashLoop) {
|
|
377
|
+
logEvent(
|
|
378
|
+
"recovery",
|
|
379
|
+
source,
|
|
380
|
+
"ok",
|
|
381
|
+
{
|
|
382
|
+
previousLifecycle: "crash_loop",
|
|
383
|
+
health: "healthy",
|
|
384
|
+
},
|
|
385
|
+
correlationId,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
375
388
|
if (state.pendingRecoveryNoticeSource) {
|
|
376
389
|
const recoverySource = state.pendingRecoveryNoticeSource;
|
|
377
390
|
state.pendingRecoveryNoticeSource = "";
|
package/lib/server.js
CHANGED
|
@@ -203,6 +203,10 @@ registerModelRoutes({
|
|
|
203
203
|
gatewayEnv,
|
|
204
204
|
parseJsonFromNoisyOutput,
|
|
205
205
|
normalizeOnboardingModels,
|
|
206
|
+
authProfiles,
|
|
207
|
+
readEnvFile,
|
|
208
|
+
writeEnvFile,
|
|
209
|
+
reloadEnv,
|
|
206
210
|
});
|
|
207
211
|
registerOnboardingRoutes({
|
|
208
212
|
app,
|
|
@@ -216,6 +220,7 @@ registerOnboardingRoutes({
|
|
|
216
220
|
resolveGithubRepoUrl,
|
|
217
221
|
resolveModelProvider,
|
|
218
222
|
hasCodexOauthProfile: authProfiles.hasCodexOauthProfile,
|
|
223
|
+
authProfiles,
|
|
219
224
|
ensureGatewayProxyConfig,
|
|
220
225
|
getBaseUrl,
|
|
221
226
|
startGateway,
|
|
@@ -241,6 +246,7 @@ registerSystemRoutes({
|
|
|
241
246
|
OPENCLAW_DIR: constants.OPENCLAW_DIR,
|
|
242
247
|
restartRequiredState,
|
|
243
248
|
topicRegistry,
|
|
249
|
+
authProfiles,
|
|
244
250
|
});
|
|
245
251
|
registerBrowseRoutes({
|
|
246
252
|
app,
|
package/lib/setup/env.template
CHANGED