@chrysb/alphaclaw 0.4.6-beta.5 → 0.4.6-beta.7

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.
@@ -1,3 +1,4 @@
1
+ const path = require("path");
1
2
  const { spawn, execSync } = require("child_process");
2
3
  const fs = require("fs");
3
4
  const net = require("net");
@@ -5,7 +6,9 @@ const {
5
6
  OPENCLAW_DIR,
6
7
  GATEWAY_HOST,
7
8
  GATEWAY_PORT,
9
+ kControlUiSkillPath,
8
10
  kChannelDefs,
11
+ kOnboardingMarkerPath,
9
12
  kRootDir,
10
13
  } = require("./constants");
11
14
 
@@ -45,7 +48,56 @@ const gatewayEnv = () => ({
45
48
  XDG_CONFIG_HOME: OPENCLAW_DIR,
46
49
  });
47
50
 
48
- const isOnboarded = () => fs.existsSync(`${OPENCLAW_DIR}/openclaw.json`);
51
+ const hasOnboardingModelConfig = () => {
52
+ const configPath = `${OPENCLAW_DIR}/openclaw.json`;
53
+ if (!fs.existsSync(configPath)) return false;
54
+ try {
55
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
56
+ const primaryModel = String(
57
+ config?.agents?.defaults?.model?.primary || "",
58
+ ).trim();
59
+ return primaryModel.includes("/");
60
+ } catch {
61
+ return false;
62
+ }
63
+ };
64
+
65
+ const hasLegacyOnboardingArtifacts = () => fs.existsSync(kControlUiSkillPath);
66
+
67
+ const writeOnboardingMarker = (reason) => {
68
+ try {
69
+ fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
70
+ fs.writeFileSync(
71
+ kOnboardingMarkerPath,
72
+ JSON.stringify(
73
+ {
74
+ onboarded: true,
75
+ reason: String(reason || "unknown"),
76
+ markedAt: new Date().toISOString(),
77
+ },
78
+ null,
79
+ 2,
80
+ ),
81
+ );
82
+ return true;
83
+ } catch (err) {
84
+ console.error(`[alphaclaw] Failed to write onboarding marker: ${err.message}`);
85
+ return false;
86
+ }
87
+ };
88
+
89
+ const isOnboarded = () => {
90
+ if (fs.existsSync(kOnboardingMarkerPath)) return true;
91
+ if (hasOnboardingModelConfig()) {
92
+ writeOnboardingMarker("config_primary_model");
93
+ return true;
94
+ }
95
+ if (hasLegacyOnboardingArtifacts()) {
96
+ writeOnboardingMarker("legacy_artifact_backfill");
97
+ return true;
98
+ }
99
+ return false;
100
+ };
49
101
 
50
102
  const isGatewayRunning = () =>
51
103
  new Promise((resolve) => {
@@ -133,6 +185,19 @@ const launchGatewayProcess = () => {
133
185
  return child;
134
186
  };
135
187
 
188
+ const markManagedGatewayExitExpected = () => {
189
+ if (
190
+ !gatewayChild ||
191
+ gatewayChild.exitCode !== null ||
192
+ gatewayChild.killed ||
193
+ !gatewayChild.pid
194
+ ) {
195
+ return false;
196
+ }
197
+ expectedExitPids.add(gatewayChild.pid);
198
+ return true;
199
+ };
200
+
136
201
  const startGateway = async () => {
137
202
  if (!isOnboarded()) {
138
203
  console.log("[alphaclaw] Not onboarded yet — skipping gateway start");
@@ -148,34 +213,8 @@ const startGateway = async () => {
148
213
 
149
214
  const restartGateway = (reloadEnv) => {
150
215
  reloadEnv();
151
- if (gatewayChild && gatewayChild.exitCode === null && !gatewayChild.killed) {
152
- console.log("[alphaclaw] Stopping managed gateway process...");
153
- try {
154
- expectedExitPids.add(gatewayChild.pid);
155
- gatewayChild.kill("SIGTERM");
156
- gatewayChild = null;
157
- } catch (e) {
158
- console.log(
159
- `[alphaclaw] Failed to stop managed gateway process: ${e.message}`,
160
- );
161
- runGatewayCmd("stop");
162
- }
163
- } else {
164
- runGatewayCmd("stop");
165
- }
166
- runGatewayCmd("start");
167
- const launchWhenReady = async () => {
168
- const waitUntil = Date.now() + 8000;
169
- while (Date.now() < waitUntil) {
170
- if (!(await isGatewayRunning())) break;
171
- await new Promise((resolve) => setTimeout(resolve, 250));
172
- }
173
- console.log(
174
- "[alphaclaw] Starting openclaw gateway with refreshed environment...",
175
- );
176
- launchGatewayProcess();
177
- };
178
- void launchWhenReady();
216
+ markManagedGatewayExitExpected();
217
+ runGatewayCmd("--force");
179
218
  };
180
219
 
181
220
  const attachGatewaySignalHandlers = () => {
@@ -70,19 +70,22 @@ const ensureTopicPathForClient = ({
70
70
  const normalizedClient = String(client || "default").trim() || "default";
71
71
  const push = getGmailPushConfig(state);
72
72
  const existingTopic = String(push.topics?.[normalizedClient] || "").trim();
73
- if (existingTopic) {
73
+ const requestedProjectId = String(projectIdOverride || "").trim();
74
+ const existingProjectId = parseProjectIdFromTopicPath(existingTopic);
75
+ if (existingTopic && (!requestedProjectId || requestedProjectId === existingProjectId)) {
74
76
  return { state, topicPath: existingTopic };
75
77
  }
76
78
  const credentials = readGoogleCredentials(normalizedClient);
77
79
  const projectId =
78
- String(projectIdOverride || "").trim() ||
80
+ requestedProjectId ||
79
81
  String(credentials?.projectId || "").trim();
80
82
  if (!projectId) {
81
83
  throw new Error(
82
84
  `Could not detect GCP project_id for client "${normalizedClient}". Save Google credentials first.`,
83
85
  );
84
86
  }
85
- const topicName = createTopicNameForClient(normalizedClient);
87
+ const topicName =
88
+ parseTopicName(existingTopic) || createTopicNameForClient(normalizedClient);
86
89
  const topicPath = `projects/${projectId}/topics/${topicName}`;
87
90
  const updated = setGmailPushConfig({
88
91
  state,
@@ -29,11 +29,12 @@ const createOnboardingService = ({
29
29
  resolveGithubRepoUrl,
30
30
  resolveModelProvider,
31
31
  hasCodexOauthProfile,
32
+ authProfiles,
32
33
  ensureGatewayProxyConfig,
33
34
  getBaseUrl,
34
35
  startGateway,
35
36
  }) => {
36
- const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
37
+ const { OPENCLAW_DIR, WORKSPACE_DIR, kOnboardingMarkerPath } = constants;
37
38
 
38
39
  const verifyGithubSetup = async ({
39
40
  githubRepoInput,
@@ -148,6 +149,7 @@ const createOnboardingService = ({
148
149
  } catch {}
149
150
 
150
151
  writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
152
+ authProfiles?.syncConfigAuthReferencesForAgent?.();
151
153
  ensureGatewayProxyConfig(getBaseUrl(req));
152
154
 
153
155
  installControlUiSkill({
@@ -158,6 +160,19 @@ const createOnboardingService = ({
158
160
 
159
161
  installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
160
162
  await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
163
+ fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
164
+ fs.writeFileSync(
165
+ kOnboardingMarkerPath,
166
+ JSON.stringify(
167
+ {
168
+ onboarded: true,
169
+ reason: "onboarding_complete",
170
+ markedAt: new Date().toISOString(),
171
+ },
172
+ null,
173
+ 2,
174
+ ),
175
+ );
161
176
 
162
177
  try {
163
178
  await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
@@ -46,7 +46,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
46
46
  const hasAiByProvider = {
47
47
  anthropic: !!(varMap.ANTHROPIC_API_KEY || varMap.ANTHROPIC_TOKEN),
48
48
  openai: !!varMap.OPENAI_API_KEY,
49
- "openai-codex": !!(hasCodexOauth || varMap.OPENAI_API_KEY),
49
+ "openai-codex": !!hasCodexOauth,
50
50
  google: !!varMap.GEMINI_API_KEY,
51
51
  };
52
52
  const hasAnyAi = !!(
@@ -68,7 +68,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
68
68
  return {
69
69
  ok: false,
70
70
  status: 400,
71
- error: "Connect OpenAI Codex OAuth or provide OPENAI_API_KEY before continuing",
71
+ error: "Connect OpenAI Codex OAuth before continuing",
72
72
  };
73
73
  }
74
74
  return {
@@ -19,7 +19,49 @@ const registerModelRoutes = ({
19
19
  parseJsonFromNoisyOutput,
20
20
  normalizeOnboardingModels,
21
21
  authProfiles,
22
+ readEnvFile,
23
+ writeEnvFile,
24
+ reloadEnv,
22
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
+
23
65
  // ── Existing CLI-backed catalog/status routes ──
24
66
 
25
67
  app.get("/api/models", async (req, res) => {
@@ -134,6 +176,7 @@ const registerModelRoutes = ({
134
176
  authProfiles.upsertProfile(profileId, credential, agentId);
135
177
  }
136
178
  }
179
+ syncEnvVarsForProfiles(profiles);
137
180
  }
138
181
 
139
182
  if (authOrder && typeof authOrder === "object") {
@@ -200,6 +243,7 @@ const registerModelRoutes = ({
200
243
  try {
201
244
  const agentId = req.query.agentId || undefined;
202
245
  authProfiles.upsertProfile(profileId, credential, agentId);
246
+ syncEnvVarsForProfiles([{ id: profileId, ...credential }]);
203
247
  res.json({ ok: true });
204
248
  } catch (err) {
205
249
  res
@@ -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,
@@ -17,10 +17,10 @@ const registerSystemRoutes = ({
17
17
  alphaclawVersionService,
18
18
  clawCmd,
19
19
  restartGateway,
20
- onExpectedGatewayRestart,
21
20
  OPENCLAW_DIR,
22
21
  restartRequiredState,
23
22
  topicRegistry,
23
+ authProfiles,
24
24
  }) => {
25
25
  let envRestartPending = false;
26
26
  const kEnvVarsReservedForUserInput = new Set([
@@ -58,7 +58,9 @@ const registerSystemRoutes = ({
58
58
  const parseJsonFromStdout = (stdout) => {
59
59
  const raw = String(stdout || "").trim();
60
60
  if (!raw) return null;
61
- const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter((idx) => idx >= 0);
61
+ const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter(
62
+ (idx) => idx >= 0,
63
+ );
62
64
  for (const start of candidateStarts) {
63
65
  const candidate = raw.slice(start);
64
66
  try {
@@ -74,7 +76,9 @@ const registerSystemRoutes = ({
74
76
  if (telegramMatch) {
75
77
  return `Telegram ${telegramMatch[1]}`;
76
78
  }
77
- const telegramTopicMatch = key.match(/:telegram:group:([^:]+):topic:([^:]+)$/);
79
+ const telegramTopicMatch = key.match(
80
+ /:telegram:group:([^:]+):topic:([^:]+)$/,
81
+ );
78
82
  if (telegramTopicMatch) {
79
83
  const [, groupId, topicId] = telegramTopicMatch;
80
84
  let groupEntry = null;
@@ -82,7 +86,9 @@ const registerSystemRoutes = ({
82
86
  groupEntry = topicRegistry?.getGroup?.(groupId) || null;
83
87
  } catch {}
84
88
  const groupName = String(groupEntry?.name || "").trim();
85
- const topicName = String(groupEntry?.topics?.[topicId]?.name || "").trim();
89
+ const topicName = String(
90
+ groupEntry?.topics?.[topicId]?.name || "",
91
+ ).trim();
86
92
  if (groupName && topicName) return `Telegram ${groupName} · ${topicName}`;
87
93
  if (topicName) return `Telegram Topic ${topicName}`;
88
94
  return `Telegram Topic ${topicId}`;
@@ -93,6 +99,34 @@ const registerSystemRoutes = ({
93
99
  }
94
100
  return key || "Session";
95
101
  };
102
+ const syncApiKeyAuthProfilesFromEnvVars = (nextEnvVars) => {
103
+ if (!authProfiles) return;
104
+ const envMap = new Map(
105
+ (nextEnvVars || []).map((entry) => [
106
+ String(entry?.key || "").trim(),
107
+ String(entry?.value || ""),
108
+ ]),
109
+ );
110
+ const providers = [
111
+ "anthropic",
112
+ "openai",
113
+ "google",
114
+ "mistral",
115
+ "voyage",
116
+ "groq",
117
+ "deepgram",
118
+ ];
119
+ for (const provider of providers) {
120
+ const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
121
+ if (!envKey) continue;
122
+ const value = envMap.get(envKey) || "";
123
+ if (!value.trim()) {
124
+ authProfiles.removeApiKeyProfileForEnvVar?.(provider);
125
+ continue;
126
+ }
127
+ authProfiles.upsertApiKeyProfileForEnvVar(provider, value);
128
+ }
129
+ };
96
130
  const listSendableAgentSessions = async () => {
97
131
  const result = await clawCmd("sessions --json", { quiet: true });
98
132
  if (!result.ok) {
@@ -168,6 +202,7 @@ const registerSystemRoutes = ({
168
202
  }
169
203
  return getSystemCronStatus();
170
204
  };
205
+ const isVisibleInEnvars = (def) => def?.visibleInEnvars !== false;
171
206
 
172
207
  app.get("/api/env", (req, res) => {
173
208
  const fileVars = readEnvFile();
@@ -175,6 +210,7 @@ const registerSystemRoutes = ({
175
210
 
176
211
  for (const def of kKnownVars) {
177
212
  if (isReservedUserEnvVar(def.key)) continue;
213
+ if (!isVisibleInEnvars(def)) continue;
178
214
  const fileEntry = fileVars.find((v) => v.key === def.key);
179
215
  const value = fileEntry?.value || "";
180
216
  merged.push({
@@ -183,6 +219,7 @@ const registerSystemRoutes = ({
183
219
  label: def.label,
184
220
  group: def.group,
185
221
  hint: def.hint,
222
+ features: def.features,
186
223
  source: fileEntry?.value ? "env_file" : "unset",
187
224
  editable: true,
188
225
  });
@@ -232,10 +269,25 @@ const registerSystemRoutes = ({
232
269
  const existingLockedVars = readEnvFile().filter((v) =>
233
270
  isReservedUserEnvVar(v.key),
234
271
  );
235
- const nextEnvVars = [...filtered, ...existingLockedVars];
272
+ const hiddenKnownVarKeys = new Set(
273
+ kKnownVars
274
+ .filter(
275
+ (def) => !isReservedUserEnvVar(def.key) && !isVisibleInEnvars(def),
276
+ )
277
+ .map((def) => def.key),
278
+ );
279
+ const existingHiddenKnownVars = readEnvFile().filter((v) =>
280
+ hiddenKnownVarKeys.has(v.key),
281
+ );
282
+ const nextEnvVars = [
283
+ ...filtered,
284
+ ...existingHiddenKnownVars,
285
+ ...existingLockedVars,
286
+ ];
236
287
  syncChannelConfig(nextEnvVars, "remove");
237
288
  writeEnvFile(nextEnvVars);
238
289
  const changed = reloadEnv();
290
+ syncApiKeyAuthProfilesFromEnvVars(nextEnvVars);
239
291
  if (changed && isOnboarded()) {
240
292
  envRestartPending = true;
241
293
  }
@@ -361,12 +413,15 @@ const registerSystemRoutes = ({
361
413
  let selectedSession = null;
362
414
  try {
363
415
  const sessions = await listSendableAgentSessions();
364
- selectedSession = sessions.find((sessionRow) => sessionRow.key === sessionKey) || null;
416
+ selectedSession =
417
+ sessions.find((sessionRow) => sessionRow.key === sessionKey) || null;
365
418
  } catch (err) {
366
419
  return res.status(502).json({ ok: false, error: err.message });
367
420
  }
368
421
  if (!selectedSession) {
369
- return res.status(400).json({ ok: false, error: "Selected session was not found" });
422
+ return res
423
+ .status(400)
424
+ .json({ ok: false, error: "Selected session was not found" });
370
425
  }
371
426
  if (selectedSession.replyChannel && selectedSession.replyTo) {
372
427
  command +=
@@ -380,7 +435,10 @@ const registerSystemRoutes = ({
380
435
  if (!result.ok) {
381
436
  return res
382
437
  .status(502)
383
- .json({ ok: false, error: result.stderr || "Could not send message to agent" });
438
+ .json({
439
+ ok: false,
440
+ error: result.stderr || "Could not send message to agent",
441
+ });
384
442
  }
385
443
  return res.json({ ok: true, stdout: result.stdout || "" });
386
444
  });
@@ -417,9 +475,6 @@ const registerSystemRoutes = ({
417
475
  }
418
476
  restartRequiredState.markRestartInProgress();
419
477
  try {
420
- if (typeof onExpectedGatewayRestart === "function") {
421
- onExpectedGatewayRestart();
422
- }
423
478
  restartGateway();
424
479
  envRestartPending = false;
425
480
  restartRequiredState.clearRequired();
@@ -9,7 +9,7 @@ const {
9
9
 
10
10
  const kHealthStartupGraceMs = 30 * 1000;
11
11
  const kBootstrapHealthCheckMs = 5 * 1000;
12
- const kExpectedRestartWindowMs = 45 * 1000;
12
+ const kExpectedRestartWindowMs = 15 * 1000;
13
13
 
14
14
  const isTruthy = (value) =>
15
15
  ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
@@ -506,7 +506,7 @@ const createWatchdog = ({
506
506
  const onGatewayExit = ({ code, signal, expectedExit = false, stderrTail = [] } = {}) => {
507
507
  const correlationId = createCorrelationId();
508
508
  clearDegradedHealthCheckTimer();
509
- if (expectedExit) {
509
+ if (expectedExit && (code == null || code === 0)) {
510
510
  state.lifecycle = "restarting";
511
511
  state.health = "unknown";
512
512
  state.crashRecoveryActive = false;
package/lib/server.js CHANGED
@@ -204,6 +204,9 @@ registerModelRoutes({
204
204
  parseJsonFromNoisyOutput,
205
205
  normalizeOnboardingModels,
206
206
  authProfiles,
207
+ readEnvFile,
208
+ writeEnvFile,
209
+ reloadEnv,
207
210
  });
208
211
  registerOnboardingRoutes({
209
212
  app,
@@ -217,6 +220,7 @@ registerOnboardingRoutes({
217
220
  resolveGithubRepoUrl,
218
221
  resolveModelProvider,
219
222
  hasCodexOauthProfile: authProfiles.hasCodexOauthProfile,
223
+ authProfiles,
220
224
  ensureGatewayProxyConfig,
221
225
  getBaseUrl,
222
226
  startGateway,
@@ -238,10 +242,10 @@ registerSystemRoutes({
238
242
  alphaclawVersionService,
239
243
  clawCmd,
240
244
  restartGateway,
241
- onExpectedGatewayRestart: () => watchdog.onExpectedRestart(),
242
245
  OPENCLAW_DIR: constants.OPENCLAW_DIR,
243
246
  restartRequiredState,
244
247
  topicRegistry,
248
+ authProfiles,
245
249
  });
246
250
  registerBrowseRoutes({
247
251
  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.5",
3
+ "version": "0.4.6-beta.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },