@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.
Files changed (52) hide show
  1. package/lib/public/js/app.js +158 -1073
  2. package/lib/public/js/components/envars.js +146 -29
  3. package/lib/public/js/components/features.js +1 -1
  4. package/lib/public/js/components/general/index.js +155 -0
  5. package/lib/public/js/components/icons.js +52 -0
  6. package/lib/public/js/components/info-tooltip.js +4 -7
  7. package/lib/public/js/components/models-tab/index.js +286 -0
  8. package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
  9. package/lib/public/js/components/models-tab/use-models.js +262 -0
  10. package/lib/public/js/components/models.js +1 -1
  11. package/lib/public/js/components/providers.js +1 -1
  12. package/lib/public/js/components/routes/browse-route.js +35 -0
  13. package/lib/public/js/components/routes/doctor-route.js +21 -0
  14. package/lib/public/js/components/routes/envars-route.js +11 -0
  15. package/lib/public/js/components/routes/general-route.js +45 -0
  16. package/lib/public/js/components/routes/index.js +11 -0
  17. package/lib/public/js/components/routes/models-route.js +11 -0
  18. package/lib/public/js/components/routes/providers-route.js +11 -0
  19. package/lib/public/js/components/routes/route-redirect.js +10 -0
  20. package/lib/public/js/components/routes/telegram-route.js +11 -0
  21. package/lib/public/js/components/routes/usage-route.js +15 -0
  22. package/lib/public/js/components/routes/watchdog-route.js +32 -0
  23. package/lib/public/js/components/routes/webhooks-route.js +43 -0
  24. package/lib/public/js/components/sidebar.js +2 -3
  25. package/lib/public/js/components/tooltip.js +106 -0
  26. package/lib/public/js/components/usage-tab/constants.js +1 -1
  27. package/lib/public/js/components/usage-tab/overview-section.js +124 -50
  28. package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
  29. package/lib/public/js/components/welcome.js +1 -1
  30. package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
  31. package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
  32. package/lib/public/js/hooks/use-browse-navigation.js +193 -0
  33. package/lib/public/js/hooks/use-hash-location.js +32 -0
  34. package/lib/public/js/lib/api.js +35 -0
  35. package/lib/public/js/lib/app-navigation.js +39 -0
  36. package/lib/public/js/lib/browse-restart-policy.js +28 -0
  37. package/lib/public/js/lib/browse-route.js +57 -0
  38. package/lib/public/js/lib/format.js +12 -0
  39. package/lib/public/js/lib/model-config.js +1 -0
  40. package/lib/server/auth-profiles.js +291 -53
  41. package/lib/server/constants.js +24 -8
  42. package/lib/server/doctor/service.js +0 -3
  43. package/lib/server/gateway.js +50 -31
  44. package/lib/server/onboarding/index.js +2 -0
  45. package/lib/server/onboarding/validation.js +2 -2
  46. package/lib/server/routes/models.js +214 -2
  47. package/lib/server/routes/onboarding.js +2 -0
  48. package/lib/server/routes/system.js +42 -1
  49. package/lib/server/watchdog.js +14 -1
  50. package/lib/server.js +6 -0
  51. package/lib/setup/env.template +1 -0
  52. package/package.json +1 -1
@@ -1,6 +1,69 @@
1
1
  const { kFallbackOnboardingModels } = require("../constants");
2
2
 
3
- const registerModelRoutes = ({ app, shellCmd, gatewayEnv, parseJsonFromNoisyOutput, normalizeOnboardingModels }) => {
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.status(400).json({ ok: false, error: err.message || "Failed to set model" });
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 nextEnvVars = [...filtered, ...existingLockedVars];
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
  }
@@ -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
- if (state.lifecycle !== "crash_loop") state.lifecycle = "running";
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,
@@ -6,6 +6,7 @@ ANTHROPIC_API_KEY=
6
6
  ANTHROPIC_TOKEN=
7
7
  OPENAI_API_KEY=
8
8
  GEMINI_API_KEY=
9
+ ELEVENLABS_API_KEY=
9
10
 
10
11
  # --- GitHub (required) ---
11
12
  GITHUB_TOKEN=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.4",
3
+ "version": "0.4.6-beta.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },