@chrysb/alphaclaw 0.8.5 → 0.8.7-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 (45) hide show
  1. package/bin/alphaclaw.js +56 -20
  2. package/lib/public/css/explorer.css +48 -0
  3. package/lib/public/css/shell.css +149 -0
  4. package/lib/public/css/tailwind.generated.css +1 -1
  5. package/lib/public/css/theme.css +265 -0
  6. package/lib/public/dist/app.bundle.js +2441 -2352
  7. package/lib/public/js/app.js +7 -0
  8. package/lib/public/js/components/gateway.js +6 -3
  9. package/lib/public/js/components/general/index.js +2 -0
  10. package/lib/public/js/components/icons.js +38 -0
  11. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  12. package/lib/public/js/components/models-tab/use-models.js +74 -9
  13. package/lib/public/js/components/models.js +52 -37
  14. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  15. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  16. package/lib/public/js/components/onboarding/welcome-form-step.js +31 -11
  17. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  18. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  19. package/lib/public/js/components/providers.js +53 -42
  20. package/lib/public/js/components/routes/general-route.js +2 -0
  21. package/lib/public/js/components/routes/watchdog-route.js +2 -0
  22. package/lib/public/js/components/sidebar.js +29 -8
  23. package/lib/public/js/components/theme-toggle.js +113 -0
  24. package/lib/public/js/components/update-modal-helpers.js +12 -0
  25. package/lib/public/js/components/update-modal.js +2 -1
  26. package/lib/public/js/components/watchdog-tab/index.js +2 -0
  27. package/lib/public/js/components/welcome/index.js +1 -2
  28. package/lib/public/js/components/welcome/use-welcome.js +153 -38
  29. package/lib/public/js/hooks/use-app-shell-controller.js +33 -9
  30. package/lib/public/js/lib/api.js +35 -0
  31. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  32. package/lib/public/js/lib/model-catalog.js +20 -0
  33. package/lib/public/js/lib/storage-keys.js +1 -1
  34. package/lib/public/login.html +8 -4
  35. package/lib/public/setup.html +9 -0
  36. package/lib/server/alphaclaw-version.js +30 -127
  37. package/lib/server/db/webhooks/index.js +48 -8
  38. package/lib/server/model-catalog-cache.js +251 -0
  39. package/lib/server/openclaw-version.js +59 -130
  40. package/lib/server/pending-alphaclaw-update.js +71 -0
  41. package/lib/server/pending-openclaw-update.js +71 -0
  42. package/lib/server/routes/models.js +14 -23
  43. package/lib/server/routes/system.js +6 -1
  44. package/lib/server/routes/webhooks.js +12 -1
  45. package/package.json +1 -1
@@ -1,12 +1,10 @@
1
- const { exec, execSync } = require("child_process");
1
+ const { execSync } = require("child_process");
2
2
  const fs = require("fs");
3
- const os = require("os");
4
3
  const path = require("path");
5
4
  const {
6
5
  kVersionCacheTtlMs,
7
6
  kLatestVersionCacheTtlMs,
8
- kNpmPackageRoot,
9
- kOpenclawUpdateCopyTimeoutMs,
7
+ kRootDir,
10
8
  } = require("./constants");
11
9
  const { normalizeOpenclawVersion } = require("./helpers");
12
10
  const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
@@ -24,6 +22,9 @@ const createOpenclawVersionService = ({
24
22
  };
25
23
  let kOpenclawUpdateInProgress = false;
26
24
 
25
+ const buildOpenclawInstallSpec = (version = "latest") =>
26
+ `openclaw@${String(version || "").trim() || "latest"}`;
27
+
27
28
  const readOpenclawVersion = () => {
28
29
  const now = Date.now();
29
30
  if (
@@ -87,118 +88,6 @@ const createOpenclawVersionService = ({
87
88
  }
88
89
  };
89
90
 
90
- const findInstallDir = () => {
91
- // Resolve the consumer app root (for example /app in Docker), not this package directory.
92
- let dir = kNpmPackageRoot;
93
- while (dir !== path.dirname(dir)) {
94
- const parent = path.dirname(dir);
95
- if (
96
- path.basename(parent) === "node_modules" ||
97
- parent.includes(`${path.sep}node_modules${path.sep}`)
98
- ) {
99
- dir = parent;
100
- continue;
101
- }
102
- const pkgPath = path.join(parent, "package.json");
103
- if (fs.existsSync(pkgPath)) {
104
- try {
105
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
106
- if (
107
- pkg.dependencies?.["@chrysb/alphaclaw"] ||
108
- pkg.devDependencies?.["@chrysb/alphaclaw"] ||
109
- pkg.optionalDependencies?.["@chrysb/alphaclaw"]
110
- ) {
111
- return parent;
112
- }
113
- } catch {}
114
- }
115
- dir = parent;
116
- }
117
- return kNpmPackageRoot;
118
- };
119
-
120
- // Install to a temp directory, then copy into the real node_modules.
121
- // Running `npm install` directly in the app dir causes EBUSY on Docker
122
- // because npm tries to rename directories that the running process holds open.
123
- // Copying individual files (cp -af) avoids the rename syscall entirely.
124
- const installLatestOpenclaw = () =>
125
- new Promise((resolve, reject) => {
126
- const installDir = findInstallDir();
127
- const tmpDir = fs.mkdtempSync(
128
- path.join(os.tmpdir(), "openclaw-update-"),
129
- );
130
- const cleanup = () => {
131
- try {
132
- fs.rmSync(tmpDir, { recursive: true, force: true });
133
- } catch {}
134
- };
135
-
136
- fs.writeFileSync(
137
- path.join(tmpDir, "package.json"),
138
- JSON.stringify({
139
- private: true,
140
- dependencies: { openclaw: "latest" },
141
- }),
142
- );
143
-
144
- const npmEnv = {
145
- ...process.env,
146
- npm_config_update_notifier: "false",
147
- npm_config_fund: "false",
148
- npm_config_audit: "false",
149
- };
150
-
151
- console.log(
152
- `[alphaclaw] Running: npm install openclaw@latest in temp dir (target: ${installDir})`,
153
- );
154
- exec(
155
- "npm install --omit=dev --prefer-online --package-lock=false",
156
- { cwd: tmpDir, env: npmEnv, timeout: 180000 },
157
- (installErr, stdout, stderr) => {
158
- if (installErr) {
159
- const message = String(stderr || installErr.message || "").trim();
160
- console.log(
161
- `[alphaclaw] openclaw install error: ${message.slice(0, 200)}`,
162
- );
163
- cleanup();
164
- return reject(
165
- new Error(message || "Failed to install openclaw@latest"),
166
- );
167
- }
168
- if (stdout?.trim()) {
169
- console.log(
170
- `[alphaclaw] openclaw install stdout: ${stdout.trim().slice(0, 300)}`,
171
- );
172
- }
173
-
174
- const src = path.join(tmpDir, "node_modules");
175
- const dest = path.join(installDir, "node_modules");
176
- exec(
177
- `cp -af "${src}/." "${dest}/"`,
178
- { timeout: kOpenclawUpdateCopyTimeoutMs },
179
- (cpErr) => {
180
- cleanup();
181
- if (cpErr) {
182
- console.log(
183
- `[alphaclaw] openclaw copy error: ${(cpErr.message || "").slice(0, 200)}`,
184
- );
185
- return reject(
186
- new Error(
187
- `Failed to copy updated openclaw files: ${cpErr.message}`,
188
- ),
189
- );
190
- }
191
- console.log("[alphaclaw] openclaw install completed");
192
- resolve({
193
- stdout: stdout?.trim() || "",
194
- stderr: stderr?.trim() || "",
195
- });
196
- },
197
- );
198
- },
199
- );
200
- });
201
-
202
91
  const getVersionStatus = async (refresh) => {
203
92
  const currentVersion = readOpenclawVersion();
204
93
  try {
@@ -228,27 +117,67 @@ const createOpenclawVersionService = ({
228
117
  kOpenclawUpdateInProgress = true;
229
118
  const previousVersion = readOpenclawVersion();
230
119
  try {
231
- await installLatestOpenclaw();
232
- kOpenclawVersionCache = { value: null, fetchedAt: 0 };
233
- const currentVersion = readOpenclawVersion();
234
- const { latestVersion, hasUpdate } = readOpenclawUpdateStatus({
235
- refresh: true,
236
- });
237
- let restarted = false;
238
- if (isOnboarded()) {
239
- restartGateway();
240
- restarted = true;
120
+ let latestVersion = null;
121
+ let hasUpdate = false;
122
+ try {
123
+ const updateStatus = readOpenclawUpdateStatus({ refresh: true });
124
+ latestVersion = updateStatus.latestVersion || null;
125
+ hasUpdate = !!updateStatus.hasUpdate;
126
+ } catch (error) {
127
+ console.log(
128
+ `[alphaclaw] Could not resolve exact OpenClaw version before restart: ${error.message || "unknown error"}`,
129
+ );
241
130
  }
131
+
132
+ if (!hasUpdate && latestVersion && latestVersion === previousVersion) {
133
+ return {
134
+ status: 200,
135
+ body: {
136
+ ok: true,
137
+ previousVersion,
138
+ currentVersion: previousVersion,
139
+ latestVersion,
140
+ hasUpdate: false,
141
+ restarted: false,
142
+ restarting: false,
143
+ updated: false,
144
+ },
145
+ };
146
+ }
147
+
148
+ const targetVersion = latestVersion || "latest";
149
+ const spec = buildOpenclawInstallSpec(targetVersion);
150
+ const markerPath = path.join(kRootDir, ".openclaw-update-pending");
151
+ fs.writeFileSync(
152
+ markerPath,
153
+ JSON.stringify({
154
+ from: previousVersion,
155
+ to: targetVersion,
156
+ spec,
157
+ ts: Date.now(),
158
+ }),
159
+ );
160
+ console.log(
161
+ `[alphaclaw] OpenClaw update marker written to ${markerPath} for ${spec}`,
162
+ );
163
+ kOpenclawVersionCache = { value: previousVersion, fetchedAt: 0 };
164
+ kOpenclawUpdateStatusCache = {
165
+ latestVersion,
166
+ hasUpdate,
167
+ fetchedAt: 0,
168
+ };
242
169
  return {
243
170
  status: 200,
244
171
  body: {
245
172
  ok: true,
246
173
  previousVersion,
247
- currentVersion,
174
+ currentVersion: previousVersion,
175
+ targetVersion: targetVersion === "latest" ? null : targetVersion,
248
176
  latestVersion,
249
- hasUpdate,
250
- restarted,
251
- updated: previousVersion !== currentVersion,
177
+ hasUpdate: true,
178
+ restarted: false,
179
+ restarting: true,
180
+ updated: previousVersion !== targetVersion,
252
181
  },
253
182
  };
254
183
  } catch (err) {
@@ -0,0 +1,71 @@
1
+ const buildPendingAlphaclawInstallSpec = (marker = {}) => {
2
+ const explicitSpec = String(marker?.spec || "").trim();
3
+ if (explicitSpec) {
4
+ return explicitSpec;
5
+ }
6
+ const targetVersion = String(marker?.to || "").trim() || "latest";
7
+ return `@chrysb/alphaclaw@${targetVersion}`;
8
+ };
9
+
10
+ const shellQuote = (value) =>
11
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
12
+
13
+ const applyPendingAlphaclawUpdate = ({
14
+ execSyncImpl,
15
+ fsModule,
16
+ installDir,
17
+ logger = console,
18
+ markerPath,
19
+ }) => {
20
+ if (!fsModule.existsSync(markerPath)) {
21
+ return {
22
+ attempted: false,
23
+ installed: false,
24
+ spec: "",
25
+ };
26
+ }
27
+
28
+ let marker = {};
29
+ try {
30
+ marker = JSON.parse(fsModule.readFileSync(markerPath, "utf8"));
31
+ } catch {
32
+ marker = {};
33
+ }
34
+
35
+ const spec = buildPendingAlphaclawInstallSpec(marker);
36
+ logger.log(`[alphaclaw] Pending update detected, installing ${spec}...`);
37
+
38
+ try {
39
+ execSyncImpl(
40
+ `npm install ${shellQuote(spec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
41
+ {
42
+ cwd: installDir,
43
+ stdio: "inherit",
44
+ timeout: 180000,
45
+ },
46
+ );
47
+ fsModule.unlinkSync(markerPath);
48
+ logger.log("[alphaclaw] Update applied successfully");
49
+ return {
50
+ attempted: true,
51
+ installed: true,
52
+ spec,
53
+ };
54
+ } catch (error) {
55
+ logger.log(`[alphaclaw] Update install failed: ${error.message}`);
56
+ try {
57
+ fsModule.unlinkSync(markerPath);
58
+ } catch {}
59
+ return {
60
+ attempted: true,
61
+ installed: false,
62
+ spec,
63
+ error,
64
+ };
65
+ }
66
+ };
67
+
68
+ module.exports = {
69
+ applyPendingAlphaclawUpdate,
70
+ buildPendingAlphaclawInstallSpec,
71
+ };
@@ -0,0 +1,71 @@
1
+ const buildPendingOpenclawInstallSpec = (marker = {}) => {
2
+ const explicitSpec = String(marker?.spec || "").trim();
3
+ if (explicitSpec) {
4
+ return explicitSpec;
5
+ }
6
+ const targetVersion = String(marker?.to || "").trim() || "latest";
7
+ return `openclaw@${targetVersion}`;
8
+ };
9
+
10
+ const shellQuote = (value) =>
11
+ `'${String(value || "").replace(/'/g, `'\"'\"'`)}'`;
12
+
13
+ const applyPendingOpenclawUpdate = ({
14
+ execSyncImpl,
15
+ fsModule,
16
+ installDir,
17
+ logger = console,
18
+ markerPath,
19
+ }) => {
20
+ if (!fsModule.existsSync(markerPath)) {
21
+ return {
22
+ attempted: false,
23
+ installed: false,
24
+ spec: "",
25
+ };
26
+ }
27
+
28
+ let marker = {};
29
+ try {
30
+ marker = JSON.parse(fsModule.readFileSync(markerPath, "utf8"));
31
+ } catch {
32
+ marker = {};
33
+ }
34
+
35
+ const spec = buildPendingOpenclawInstallSpec(marker);
36
+ logger.log(`[alphaclaw] Pending OpenClaw update detected, installing ${spec}...`);
37
+
38
+ try {
39
+ execSyncImpl(
40
+ `npm install ${shellQuote(spec)} --omit=dev --no-save --save=false --package-lock=false --prefer-online`,
41
+ {
42
+ cwd: installDir,
43
+ stdio: "inherit",
44
+ timeout: 180000,
45
+ },
46
+ );
47
+ fsModule.unlinkSync(markerPath);
48
+ logger.log("[alphaclaw] OpenClaw update applied successfully");
49
+ return {
50
+ attempted: true,
51
+ installed: true,
52
+ spec,
53
+ };
54
+ } catch (error) {
55
+ logger.log(`[alphaclaw] OpenClaw update install failed: ${error.message}`);
56
+ try {
57
+ fsModule.unlinkSync(markerPath);
58
+ } catch {}
59
+ return {
60
+ attempted: true,
61
+ installed: false,
62
+ spec,
63
+ error,
64
+ };
65
+ }
66
+ };
67
+
68
+ module.exports = {
69
+ applyPendingOpenclawUpdate,
70
+ buildPendingOpenclawInstallSpec,
71
+ };
@@ -1,4 +1,5 @@
1
1
  const { kFallbackOnboardingModels } = require("../constants");
2
+ const { createModelCatalogCache } = require("../model-catalog-cache");
2
3
 
3
4
  const runModelsGitSync = async (shellCmd) => {
4
5
  if (typeof shellCmd !== "function") return null;
@@ -22,6 +23,13 @@ const registerModelRoutes = ({
22
23
  readEnvFile,
23
24
  writeEnvFile,
24
25
  reloadEnv,
26
+ modelCatalogCache = createModelCatalogCache({
27
+ shellCmd,
28
+ gatewayEnv,
29
+ parseJsonFromNoisyOutput,
30
+ normalizeOnboardingModels,
31
+ fallbackModels: kFallbackOnboardingModels,
32
+ }),
25
33
  }) => {
26
34
  const upsertEnvVar = (items, key, value) => {
27
35
  const next = Array.isArray(items) ? [...items] : [];
@@ -154,29 +162,8 @@ const registerModelRoutes = ({
154
162
  // ── Existing CLI-backed catalog/status routes ──
155
163
 
156
164
  app.get("/api/models", async (req, res) => {
157
- try {
158
- const output = await shellCmd("openclaw models list --all --json", {
159
- env: gatewayEnv(),
160
- timeout: 20000,
161
- });
162
- const parsed = parseJsonFromNoisyOutput(output);
163
- const models = normalizeOnboardingModels(parsed?.models || []);
164
- if (models.length > 0) {
165
- return res.json({ ok: true, source: "openclaw", models });
166
- }
167
- return res.json({
168
- ok: true,
169
- source: "fallback",
170
- models: kFallbackOnboardingModels,
171
- });
172
- } catch (err) {
173
- console.error("[models] Failed to load dynamic models:", err.message);
174
- return res.json({
175
- ok: true,
176
- source: "fallback",
177
- models: kFallbackOnboardingModels,
178
- });
179
- }
165
+ const response = await modelCatalogCache.getCatalogResponse();
166
+ return res.json(response);
180
167
  });
181
168
 
182
169
  app.get("/api/models/status", async (req, res) => {
@@ -210,6 +197,7 @@ const registerModelRoutes = ({
210
197
  env: gatewayEnv(),
211
198
  timeout: 30000,
212
199
  });
200
+ modelCatalogCache.markStale();
213
201
  res.json({ ok: true });
214
202
  } catch (err) {
215
203
  res
@@ -286,6 +274,7 @@ const registerModelRoutes = ({
286
274
  authProfiles.syncConfigAuthReferencesForAgent(agentId);
287
275
 
288
276
  const syncWarning = await runModelsGitSync(shellCmd);
277
+ modelCatalogCache.markStale();
289
278
  res.json({
290
279
  ok: true,
291
280
  ...(syncWarning ? { syncWarning } : {}),
@@ -338,6 +327,7 @@ const registerModelRoutes = ({
338
327
  const agentId = req.query.agentId || undefined;
339
328
  authProfiles.upsertProfile(profileId, credential, agentId);
340
329
  syncEnvVarsForProfiles([{ id: profileId, ...credential }]);
330
+ modelCatalogCache.markStale();
341
331
  res.json({ ok: true });
342
332
  } catch (err) {
343
333
  res
@@ -359,6 +349,7 @@ const registerModelRoutes = ({
359
349
  try {
360
350
  const agentId = req.query.agentId || undefined;
361
351
  const removed = authProfiles.removeProfile(profileId, agentId);
352
+ modelCatalogCache.markStale();
362
353
  res.json({ ok: true, removed });
363
354
  } catch (err) {
364
355
  res
@@ -588,7 +588,12 @@ const registerSystemRoutes = ({
588
588
  console.log(
589
589
  `[alphaclaw] /api/openclaw/update result: status=${result.status} ok=${result.body?.ok === true}`,
590
590
  );
591
- res.status(result.status).json(result.body);
591
+ if (result.status === 200 && result.body?.ok && result.body?.restarting) {
592
+ res.json(result.body);
593
+ setTimeout(() => alphaclawVersionService.restartProcess(), 1000);
594
+ } else {
595
+ res.status(result.status).json(result.body);
596
+ }
592
597
  });
593
598
 
594
599
  app.get("/api/alphaclaw/version", async (req, res) => {
@@ -29,13 +29,24 @@ const mergeWebhookAndSummary = ({ webhook, summary }) => {
29
29
  const totalCount = Number(summary?.totalCount || 0);
30
30
  const errorCount = Number(summary?.errorCount || 0);
31
31
  const successCount = Number(summary?.successCount || 0);
32
+ const recentTotalCount = Number(summary?.recentTotalCount || 0);
33
+ const recentErrorCount = Number(summary?.recentErrorCount || 0);
34
+ const recentSuccessCount = Number(summary?.recentSuccessCount || 0);
35
+ const healthWindowSize = Number(summary?.healthWindowSize || 0);
32
36
  return {
33
37
  ...webhook,
34
38
  lastReceived: summary?.lastReceived || null,
35
39
  totalCount,
36
40
  successCount,
37
41
  errorCount,
38
- health: buildHealth({ totalCount, errorCount }),
42
+ recentTotalCount,
43
+ recentSuccessCount,
44
+ recentErrorCount,
45
+ healthWindowSize,
46
+ health: buildHealth({
47
+ totalCount: recentTotalCount || totalCount,
48
+ errorCount: recentTotalCount > 0 ? recentErrorCount : errorCount,
49
+ }),
39
50
  };
40
51
  };
41
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.5",
3
+ "version": "0.8.7-beta.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },