@chrysb/alphaclaw 0.1.1 → 0.1.3

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/bin/alphaclaw.js CHANGED
@@ -13,16 +13,27 @@ const { execSync } = require("child_process");
13
13
  const args = process.argv.slice(2);
14
14
  const command = args.find((a) => !a.startsWith("-"));
15
15
 
16
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
17
+
18
+ if (args.includes("--version") || args.includes("-v") || command === "version") {
19
+ console.log(pkg.version);
20
+ process.exit(0);
21
+ }
22
+
16
23
  if (!command || command === "help" || args.includes("--help")) {
17
24
  console.log(`
25
+ alphaclaw v${pkg.version}
26
+
18
27
  Usage: alphaclaw <command> [options]
19
28
 
20
29
  Commands:
21
30
  start Start the AlphaClaw server (Setup UI + gateway manager)
31
+ version Print version
22
32
 
23
33
  Options:
24
34
  --root-dir <path> Persistent data directory (default: ~/.alphaclaw)
25
35
  --port <number> Server port (default: 3000)
36
+ --version, -v Print version
26
37
  --help Show this help message
27
38
  `);
28
39
  process.exit(0);
@@ -3,102 +3,126 @@ import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import {
5
5
  fetchOpenclawVersion,
6
+ fetchAlphaclawVersion,
6
7
  restartGateway,
7
8
  updateOpenclaw,
9
+ updateAlphaclaw,
8
10
  } from "../lib/api.js";
9
11
  import { showToast } from "./toast.js";
10
12
  const html = htm.bind(h);
11
13
 
12
- export function Gateway({ status, openclawVersion }) {
13
- const [restarting, setRestarting] = useState(false);
14
- const [checkingUpdate, setCheckingUpdate] = useState(false);
15
- const [currentVersion, setCurrentVersion] = useState(openclawVersion || null);
14
+ function VersionRow({ label, currentVersion, fetchVersion, applyUpdate, tagsUrl }) {
15
+ const [checking, setChecking] = useState(false);
16
+ const [version, setVersion] = useState(currentVersion || null);
16
17
  const [latestVersion, setLatestVersion] = useState(null);
17
18
  const [hasUpdate, setHasUpdate] = useState(false);
18
- const [updateError, setUpdateError] = useState("");
19
- const isRunning = status === "running" && !restarting;
20
- const dotClass = isRunning
21
- ? "w-2 h-2 rounded-full bg-green-500"
22
- : "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
19
+ const [error, setError] = useState("");
23
20
 
24
21
  useEffect(() => {
25
- setCurrentVersion(openclawVersion || null);
26
- }, [openclawVersion]);
22
+ setVersion(currentVersion || null);
23
+ }, [currentVersion]);
27
24
 
28
25
  useEffect(() => {
29
26
  let active = true;
30
- const loadLatest = async () => {
27
+ const load = async () => {
31
28
  try {
32
- const data = await fetchOpenclawVersion(false);
29
+ const data = await fetchVersion(false);
33
30
  if (!active) return;
34
- setCurrentVersion(data.currentVersion || openclawVersion || null);
31
+ setVersion(data.currentVersion || currentVersion || null);
35
32
  setLatestVersion(data.latestVersion || null);
36
33
  setHasUpdate(!!data.hasUpdate);
37
- setUpdateError(data.ok ? "" : data.error || "");
34
+ setError(data.ok ? "" : data.error || "");
38
35
  } catch (err) {
39
36
  if (!active) return;
40
- setUpdateError(err.message || "Could not check updates");
37
+ setError(err.message || "Could not check updates");
41
38
  }
42
39
  };
43
- loadLatest();
44
- return () => {
45
- active = false;
46
- };
40
+ load();
41
+ return () => { active = false; };
47
42
  }, []);
48
43
 
49
- const handleRestart = async () => {
50
- if (restarting) return;
51
- setRestarting(true);
52
- try {
53
- await restartGateway();
54
- showToast("Gateway restarted", "success");
55
- } catch (err) {
56
- showToast("Restart failed: " + err.message, "error");
57
- }
58
- setRestarting(false);
59
- };
60
-
61
- const handleUpdate = async () => {
62
- if (checkingUpdate) return;
63
- setCheckingUpdate(true);
64
- setUpdateError("");
44
+ const handleAction = async () => {
45
+ if (checking) return;
46
+ setChecking(true);
47
+ setError("");
65
48
  try {
66
49
  const data = hasUpdate
67
- ? await updateOpenclaw()
68
- : await fetchOpenclawVersion(true);
69
- setCurrentVersion(data.currentVersion || currentVersion);
50
+ ? await applyUpdate()
51
+ : await fetchVersion(true);
52
+ setVersion(data.currentVersion || version);
70
53
  setLatestVersion(data.latestVersion || null);
71
54
  setHasUpdate(!!data.hasUpdate);
72
- setUpdateError(data.ok ? "" : data.error || "");
55
+ setError(data.ok ? "" : data.error || "");
73
56
  if (hasUpdate) {
74
57
  if (!data.ok) {
75
- showToast(data.error || "OpenClaw update failed", "error");
76
- } else if (data.updated) {
58
+ showToast(data.error || `${label} update failed`, "error");
59
+ } else if (data.updated || data.restarting) {
77
60
  showToast(
78
- data.restarted
79
- ? `Updated to ${data.currentVersion} and restarted gateway`
80
- : `Updated to ${data.currentVersion}`,
61
+ data.restarting
62
+ ? `${label} updated restarting...`
63
+ : `Updated ${label} to ${data.currentVersion}`,
81
64
  "success",
82
65
  );
83
66
  } else {
84
- showToast("Already at latest OpenClaw version", "success");
67
+ showToast(`Already at latest ${label} version`, "success");
85
68
  }
86
69
  } else if (data.hasUpdate && data.latestVersion) {
87
- showToast(`Update available: ${data.latestVersion}`, "warning");
70
+ showToast(`${label} update available: ${data.latestVersion}`, "warning");
88
71
  } else {
89
- showToast("OpenClaw is up to date", "success");
72
+ showToast(`${label} is up to date`, "success");
90
73
  }
91
74
  } catch (err) {
92
- setUpdateError(
93
- err.message ||
94
- (hasUpdate ? "Could not update OpenClaw" : "Could not check updates"),
95
- );
96
- showToast(
97
- hasUpdate ? "Could not update OpenClaw" : "Could not check updates",
98
- "error",
99
- );
75
+ setError(err.message || (hasUpdate ? `Could not update ${label}` : "Could not check updates"));
76
+ showToast(hasUpdate ? `Could not update ${label}` : "Could not check updates", "error");
77
+ }
78
+ setChecking(false);
79
+ };
80
+
81
+ return html`
82
+ <div class="flex items-center justify-between gap-3">
83
+ <div class="min-w-0">
84
+ <p class="text-sm text-gray-300 truncate">
85
+ <span class="text-gray-500">${label}</span>${" "}v${version || "unknown"}
86
+ </p>
87
+ ${error && html`<p class="text-xs text-yellow-500 mt-1">${error}</p>`}
88
+ </div>
89
+ <div class="flex items-center gap-2 shrink-0">
90
+ ${hasUpdate && latestVersion && tagsUrl && html`
91
+ <a href=${tagsUrl} target="_blank"
92
+ class="text-xs text-yellow-500 hover:text-yellow-300 transition-colors"
93
+ >${latestVersion} available</a>
94
+ `}
95
+ <button
96
+ onclick=${handleAction}
97
+ disabled=${checking}
98
+ class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${checking ? "opacity-50 cursor-not-allowed" : ""}"
99
+ >
100
+ ${checking
101
+ ? hasUpdate ? "Updating..." : "Checking..."
102
+ : hasUpdate ? "Update" : "Check updates"}
103
+ </button>
104
+ </div>
105
+ </div>
106
+ `;
107
+ }
108
+
109
+ export function Gateway({ status, openclawVersion }) {
110
+ const [restarting, setRestarting] = useState(false);
111
+ const isRunning = status === "running" && !restarting;
112
+ const dotClass = isRunning
113
+ ? "w-2 h-2 rounded-full bg-green-500"
114
+ : "w-2 h-2 rounded-full bg-yellow-500 animate-pulse";
115
+
116
+ const handleRestart = async () => {
117
+ if (restarting) return;
118
+ setRestarting(true);
119
+ try {
120
+ await restartGateway();
121
+ showToast("Gateway restarted", "success");
122
+ } catch (err) {
123
+ showToast("Restart failed: " + err.message, "error");
100
124
  }
101
- setCheckingUpdate(false);
125
+ setRestarting(false);
102
126
  };
103
127
 
104
128
  return html` <div class="bg-surface border border-border rounded-xl p-4">
@@ -123,41 +147,21 @@ export function Gateway({ status, openclawVersion }) {
123
147
  Restart
124
148
  </button>
125
149
  </div>
126
- <div class="mt-3 pt-3 border-t border-border">
127
- <div class="flex items-center justify-between gap-3">
128
- <div class="min-w-0">
129
- <p class="text-sm text-gray-300 truncate">
130
- v${currentVersion || openclawVersion || "unknown"}
131
- </p>
132
- ${updateError &&
133
- html`<p class="text-xs text-yellow-500 mt-1">${updateError}</p>`}
134
- </div>
135
- <div class="flex items-center gap-2 shrink-0">
136
- ${hasUpdate &&
137
- latestVersion &&
138
- html`<a
139
- href="https://github.com/openclaw/openclaw/tags"
140
- target="_blank"
141
- class="text-xs text-yellow-500 hover:text-yellow-300 transition-colors"
142
- >${latestVersion} available</a
143
- >`}
144
- <button
145
- onclick=${handleUpdate}
146
- disabled=${checkingUpdate}
147
- class="text-xs px-2.5 py-1 rounded-lg border border-border text-gray-500 hover:text-gray-300 hover:border-gray-500 transition-colors ${checkingUpdate
148
- ? "opacity-50 cursor-not-allowed"
149
- : ""}"
150
- >
151
- ${checkingUpdate
152
- ? hasUpdate
153
- ? "Updating..."
154
- : "Checking..."
155
- : hasUpdate
156
- ? "Update"
157
- : "Check updates"}
158
- </button>
159
- </div>
160
- </div>
150
+ <div class="mt-3 pt-3 border-t border-border space-y-3">
151
+ <${VersionRow}
152
+ label="OpenClaw"
153
+ currentVersion=${openclawVersion}
154
+ fetchVersion=${fetchOpenclawVersion}
155
+ applyUpdate=${updateOpenclaw}
156
+ tagsUrl="https://github.com/openclaw/openclaw/tags"
157
+ />
158
+ <${VersionRow}
159
+ label="AlphaClaw"
160
+ currentVersion=${null}
161
+ fetchVersion=${fetchAlphaclawVersion}
162
+ applyUpdate=${updateAlphaclaw}
163
+ tagsUrl="https://www.npmjs.com/package/@chrysb/alphaclaw"
164
+ />
161
165
  </div>
162
166
  </div>`;
163
167
  }
@@ -80,6 +80,17 @@ export async function updateOpenclaw() {
80
80
  return res.json();
81
81
  }
82
82
 
83
+ export async function fetchAlphaclawVersion(refresh = false) {
84
+ const query = refresh ? '?refresh=1' : '';
85
+ const res = await authFetch(`/api/alphaclaw/version${query}`);
86
+ return res.json();
87
+ }
88
+
89
+ export async function updateAlphaclaw() {
90
+ const res = await authFetch('/api/alphaclaw/update', { method: 'POST' });
91
+ return res.json();
92
+ }
93
+
83
94
  export async function fetchSyncCron() {
84
95
  const res = await authFetch('/api/sync-cron');
85
96
  const text = await res.text();
@@ -0,0 +1,190 @@
1
+ const { exec } = require("child_process");
2
+ const { spawn } = require("child_process");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const https = require("https");
6
+ const http = require("http");
7
+ const {
8
+ kLatestVersionCacheTtlMs,
9
+ kAlphaclawRegistryUrl,
10
+ kPackageRoot,
11
+ } = require("./constants");
12
+
13
+ // kPackageRoot is lib/ — the actual npm package root (with package.json) is one level up
14
+ const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
15
+
16
+ const createAlphaclawVersionService = () => {
17
+ let kUpdateStatusCache = { latestVersion: null, hasUpdate: false, fetchedAt: 0 };
18
+ let kUpdateInProgress = false;
19
+
20
+ const readAlphaclawVersion = () => {
21
+ try {
22
+ const pkg = JSON.parse(
23
+ fs.readFileSync(path.join(kNpmPackageRoot, "package.json"), "utf8"),
24
+ );
25
+ return pkg.version || null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ };
30
+
31
+ const fetchLatestVersionFromRegistry = () =>
32
+ new Promise((resolve, reject) => {
33
+ const get = kAlphaclawRegistryUrl.startsWith("https") ? https.get : http.get;
34
+ get(kAlphaclawRegistryUrl, { headers: { Accept: "application/json" } }, (res) => {
35
+ let data = "";
36
+ res.on("data", (chunk) => { data += chunk; });
37
+ res.on("end", () => {
38
+ try {
39
+ const parsed = JSON.parse(data);
40
+ resolve(parsed["dist-tags"]?.latest || null);
41
+ } catch (e) {
42
+ reject(new Error("Failed to parse registry response"));
43
+ }
44
+ });
45
+ }).on("error", reject);
46
+ });
47
+
48
+ const readAlphaclawUpdateStatus = async ({ refresh = false } = {}) => {
49
+ const now = Date.now();
50
+ if (
51
+ !refresh &&
52
+ kUpdateStatusCache.fetchedAt &&
53
+ now - kUpdateStatusCache.fetchedAt < kLatestVersionCacheTtlMs
54
+ ) {
55
+ return {
56
+ latestVersion: kUpdateStatusCache.latestVersion,
57
+ hasUpdate: kUpdateStatusCache.hasUpdate,
58
+ };
59
+ }
60
+ const currentVersion = readAlphaclawVersion();
61
+ const latestVersion = await fetchLatestVersionFromRegistry();
62
+ const hasUpdate = !!(currentVersion && latestVersion && latestVersion !== currentVersion);
63
+ kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
64
+ console.log(
65
+ `[alphaclaw] alphaclaw update status: hasUpdate=${hasUpdate} current=${currentVersion} latest=${latestVersion || "unknown"}`,
66
+ );
67
+ return { latestVersion, hasUpdate };
68
+ };
69
+
70
+ const findInstallDir = () => {
71
+ // Walk up from kNpmPackageRoot to find the consuming project's directory
72
+ // (the one with node_modules/@chrysb/alphaclaw). In Docker this is /app.
73
+ let dir = kNpmPackageRoot;
74
+ while (dir !== path.dirname(dir)) {
75
+ const parent = path.dirname(dir);
76
+ if (path.basename(parent) === "node_modules" || parent.includes("node_modules")) {
77
+ dir = parent;
78
+ continue;
79
+ }
80
+ const pkgPath = path.join(parent, "package.json");
81
+ if (fs.existsSync(pkgPath)) {
82
+ try {
83
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
84
+ if (pkg.dependencies?.["@chrysb/alphaclaw"]) {
85
+ return parent;
86
+ }
87
+ } catch {}
88
+ }
89
+ dir = parent;
90
+ }
91
+ // Fallback: if running directly (not from node_modules), use kNpmPackageRoot
92
+ return kNpmPackageRoot;
93
+ };
94
+
95
+ const installLatestAlphaclaw = () =>
96
+ new Promise((resolve, reject) => {
97
+ const installDir = findInstallDir();
98
+ console.log(`[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (cwd: ${installDir})`);
99
+ exec(
100
+ "npm install @chrysb/alphaclaw@latest --omit=dev --no-save --package-lock=false",
101
+ {
102
+ cwd: installDir,
103
+ env: {
104
+ ...process.env,
105
+ npm_config_update_notifier: "false",
106
+ npm_config_fund: "false",
107
+ npm_config_audit: "false",
108
+ },
109
+ timeout: 180000,
110
+ },
111
+ (err, stdout, stderr) => {
112
+ if (err) {
113
+ const message = String(stderr || err.message || "").trim();
114
+ console.log(`[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`);
115
+ return reject(new Error(message || "Failed to install @chrysb/alphaclaw@latest"));
116
+ }
117
+ if (stdout?.trim()) {
118
+ console.log(`[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`);
119
+ }
120
+ console.log("[alphaclaw] alphaclaw install completed");
121
+ resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
122
+ },
123
+ );
124
+ });
125
+
126
+ const restartProcess = () => {
127
+ console.log("[alphaclaw] Restarting process...");
128
+ const child = spawn(process.argv[0], process.argv.slice(1), {
129
+ detached: true,
130
+ stdio: "inherit",
131
+ });
132
+ child.unref();
133
+ process.exit(0);
134
+ };
135
+
136
+ const getVersionStatus = async (refresh) => {
137
+ const currentVersion = readAlphaclawVersion();
138
+ try {
139
+ const { latestVersion, hasUpdate } = await readAlphaclawUpdateStatus({ refresh });
140
+ return { ok: true, currentVersion, latestVersion, hasUpdate };
141
+ } catch (err) {
142
+ return {
143
+ ok: false,
144
+ currentVersion,
145
+ latestVersion: kUpdateStatusCache.latestVersion,
146
+ hasUpdate: kUpdateStatusCache.hasUpdate,
147
+ error: err.message || "Failed to fetch latest AlphaClaw version",
148
+ };
149
+ }
150
+ };
151
+
152
+ const updateAlphaclaw = async () => {
153
+ if (kUpdateInProgress) {
154
+ return {
155
+ status: 409,
156
+ body: { ok: false, error: "AlphaClaw update already in progress" },
157
+ };
158
+ }
159
+
160
+ kUpdateInProgress = true;
161
+ const previousVersion = readAlphaclawVersion();
162
+ try {
163
+ await installLatestAlphaclaw();
164
+ kUpdateStatusCache = { latestVersion: null, hasUpdate: false, fetchedAt: 0 };
165
+ return {
166
+ status: 200,
167
+ body: {
168
+ ok: true,
169
+ previousVersion,
170
+ restarting: true,
171
+ },
172
+ };
173
+ } catch (err) {
174
+ kUpdateInProgress = false;
175
+ return {
176
+ status: 500,
177
+ body: { ok: false, error: err.message || "Failed to update AlphaClaw" },
178
+ };
179
+ }
180
+ };
181
+
182
+ return {
183
+ readAlphaclawVersion,
184
+ getVersionStatus,
185
+ updateAlphaclaw,
186
+ restartProcess,
187
+ };
188
+ };
189
+
190
+ module.exports = { createAlphaclawVersionService };
@@ -106,6 +106,7 @@ const kFallbackOnboardingModels = [
106
106
  const kVersionCacheTtlMs = 60 * 1000;
107
107
  const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
108
108
  const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
109
+ const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
109
110
  const kAppDir = kPackageRoot;
110
111
 
111
112
  const kSystemVars = new Set([
@@ -265,6 +266,7 @@ module.exports = {
265
266
  kVersionCacheTtlMs,
266
267
  kLatestVersionCacheTtlMs,
267
268
  kOpenclawRegistryUrl,
269
+ kAlphaclawRegistryUrl,
268
270
  kAppDir,
269
271
  kSystemVars,
270
272
  kKnownVars,
@@ -12,6 +12,7 @@ const registerSystemRoutes = ({
12
12
  isOnboarded,
13
13
  getChannelStatus,
14
14
  openclawVersionService,
15
+ alphaclawVersionService,
15
16
  clawCmd,
16
17
  restartGateway,
17
18
  OPENCLAW_DIR,
@@ -183,6 +184,26 @@ const registerSystemRoutes = ({
183
184
  res.status(result.status).json(result.body);
184
185
  });
185
186
 
187
+ app.get("/api/alphaclaw/version", async (req, res) => {
188
+ const refresh = String(req.query.refresh || "") === "1";
189
+ const status = await alphaclawVersionService.getVersionStatus(refresh);
190
+ res.json(status);
191
+ });
192
+
193
+ app.post("/api/alphaclaw/update", async (req, res) => {
194
+ console.log("[alphaclaw] /api/alphaclaw/update requested");
195
+ const result = await alphaclawVersionService.updateAlphaclaw();
196
+ console.log(
197
+ `[alphaclaw] /api/alphaclaw/update result: status=${result.status} ok=${result.body?.ok === true}`,
198
+ );
199
+ if (result.status === 200 && result.body?.ok) {
200
+ res.json(result.body);
201
+ setTimeout(() => alphaclawVersionService.restartProcess(), 1000);
202
+ } else {
203
+ res.status(result.status).json(result.body);
204
+ }
205
+ });
206
+
186
207
  app.get("/api/gateway-status", async (req, res) => {
187
208
  const result = await clawCmd("status");
188
209
  res.json(result);
package/lib/server.js CHANGED
@@ -34,6 +34,7 @@ const { createCommands } = require("./server/commands");
34
34
  const { createAuthProfiles } = require("./server/auth-profiles");
35
35
  const { createLoginThrottle } = require("./server/login-throttle");
36
36
  const { createOpenclawVersionService } = require("./server/openclaw-version");
37
+ const { createAlphaclawVersionService } = require("./server/alphaclaw-version");
37
38
  const { syncBootstrapPromptFiles } = require("./server/onboarding/workspace");
38
39
 
39
40
  const { registerAuthRoutes } = require("./server/routes/auth");
@@ -76,6 +77,7 @@ const openclawVersionService = createOpenclawVersionService({
76
77
  restartGateway,
77
78
  isOnboarded,
78
79
  });
80
+ const alphaclawVersionService = createAlphaclawVersionService();
79
81
 
80
82
  const { requireAuth } = registerAuthRoutes({ app, loginThrottle });
81
83
  app.use(express.static(path.join(__dirname, "public")));
@@ -118,6 +120,7 @@ registerSystemRoutes({
118
120
  isOnboarded,
119
121
  getChannelStatus,
120
122
  openclawVersionService,
123
+ alphaclawVersionService,
121
124
  clawCmd,
122
125
  restartGateway,
123
126
  OPENCLAW_DIR: constants.OPENCLAW_DIR,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },