@chrysb/alphaclaw 0.3.1 → 0.3.2-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.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img width="771" height="339" alt="image" src="https://github.com/user-attachments/assets/99e194dd-902b-46db-ad0c-aaf723592271" />
2
+ <img width="771" height="339" alt="image" src="https://github.com/user-attachments/assets/b96b45ab-52f2-4010-bfbe-c640e66b0f36" />
3
3
  </p>
4
4
  <h1 align="center">AlphaClaw</h1>
5
5
  <p align="center">
@@ -47,9 +47,7 @@
47
47
 
48
48
  ## No Lock-in. Eject Anytime.
49
49
 
50
- AlphaClaw writes standard OpenClaw config (`openclaw.json`, `.env`). Remove AlphaClaw and your agent keeps running nothing proprietary, nothing to migrate.
51
-
52
- ---
50
+ AlphaClaw simply wraps OpenClaw, it's not a dependency. Remove AlphaClaw and your agent keeps running. Nothing proprietary, nothing to migrate.
53
51
 
54
52
  ## Quick Start
55
53
 
@@ -4,6 +4,7 @@ import htm from "https://esm.sh/htm";
4
4
  import {
5
5
  fetchWatchdogEvents,
6
6
  fetchWatchdogLogs,
7
+ fetchWatchdogResources,
7
8
  fetchWatchdogSettings,
8
9
  updateWatchdogSettings,
9
10
  triggerWatchdogRepair,
@@ -16,9 +17,96 @@ import { showToast } from "./toast.js";
16
17
 
17
18
  const html = htm.bind(h);
18
19
 
20
+ const formatBytes = (bytes) => {
21
+ if (bytes == null) return "—";
22
+ if (bytes < 1024) return `${bytes} B`;
23
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
24
+ if (bytes < 1024 * 1024 * 1024)
25
+ return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
26
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
27
+ };
28
+
29
+ const barColor = (percent) => {
30
+ if (percent == null) return "bg-gray-600";
31
+ if (percent >= 90) return "bg-red-500";
32
+ if (percent >= 70) return "bg-yellow-400";
33
+ return "bg-cyan-400";
34
+ };
35
+
36
+ const ResourceBar = ({
37
+ label,
38
+ percent,
39
+ detail,
40
+ segments = null,
41
+ expanded = false,
42
+ onToggle = null,
43
+ }) => html`
44
+ <div
45
+ class=${onToggle ? "cursor-pointer group" : ""}
46
+ onclick=${onToggle || undefined}
47
+ >
48
+ <span
49
+ class=${`text-xs text-gray-400 ${onToggle ? "group-hover:text-gray-200 transition-colors" : ""}`}
50
+ >${label}</span
51
+ >
52
+ <div
53
+ class=${`h-1.5 w-full bg-white/5 rounded-full overflow-hidden mt-1.5 flex ${onToggle ? "group-hover:bg-white/10 transition-colors" : ""}`}
54
+ >
55
+ ${expanded && segments
56
+ ? segments.map(
57
+ (seg) => html`
58
+ <div
59
+ class="h-full"
60
+ style=${{
61
+ width: `${Math.min(100, seg.percent ?? 0)}%`,
62
+ backgroundColor: seg.color,
63
+ transition:
64
+ "width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
65
+ }}
66
+ ></div>
67
+ `,
68
+ )
69
+ : html`
70
+ <div
71
+ class=${`h-full rounded-full ${barColor(percent)}`}
72
+ style=${{
73
+ width: `${Math.min(100, percent ?? 0)}%`,
74
+ transition:
75
+ "width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
76
+ }}
77
+ ></div>
78
+ `}
79
+ </div>
80
+ <div class="flex flex-wrap items-center gap-x-3 mt-2.5">
81
+ <span class="text-xs text-gray-500 font-mono flex-1">${detail}</span>
82
+ ${expanded &&
83
+ segments &&
84
+ segments
85
+ .filter((s) => s.label)
86
+ .map(
87
+ (seg) => html`
88
+ <span
89
+ class="inline-flex items-center gap-1 text-xs text-gray-500 font-mono"
90
+ >
91
+ <span
92
+ class="inline-block w-1.5 h-1.5 rounded-full"
93
+ style=${{ backgroundColor: seg.color }}
94
+ ></span>
95
+ ${seg.label}
96
+ </span>
97
+ `,
98
+ )}
99
+ </div>
100
+ </div>
101
+ `;
102
+
19
103
  const getIncidentStatusTone = (event) => {
20
- const eventType = String(event?.eventType || "").trim().toLowerCase();
21
- const status = String(event?.status || "").trim().toLowerCase();
104
+ const eventType = String(event?.eventType || "")
105
+ .trim()
106
+ .toLowerCase();
107
+ const status = String(event?.status || "")
108
+ .trim()
109
+ .toLowerCase();
22
110
  if (status === "failed") {
23
111
  return {
24
112
  dotClass: "bg-red-500/90",
@@ -53,6 +141,8 @@ export const WatchdogTab = ({
53
141
  restartSignal = 0,
54
142
  }) => {
55
143
  const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
144
+ const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
145
+ const [memoryExpanded, setMemoryExpanded] = useState(false);
56
146
  const [settings, setSettings] = useState({
57
147
  autoRepair: false,
58
148
  notificationsEnabled: true,
@@ -66,7 +156,8 @@ export const WatchdogTab = ({
66
156
 
67
157
  const currentWatchdogStatus = watchdogStatus || {};
68
158
  const events = eventsPoll.data?.events || [];
69
- const isRepairInProgress = repairing || !!currentWatchdogStatus?.operationInProgress;
159
+ const isRepairInProgress =
160
+ repairing || !!currentWatchdogStatus?.operationInProgress;
70
161
 
71
162
  useEffect(() => {
72
163
  let active = true;
@@ -172,7 +263,10 @@ export const WatchdogTab = ({
172
263
  },
173
264
  );
174
265
  onRefreshStatuses();
175
- showToast(`Notifications ${nextValue ? "enabled" : "disabled"}`, "success");
266
+ showToast(
267
+ `Notifications ${nextValue ? "enabled" : "disabled"}`,
268
+ "success",
269
+ );
176
270
  } catch (err) {
177
271
  showToast(err.message || "Could not update notifications", "error");
178
272
  } finally {
@@ -210,6 +304,80 @@ export const WatchdogTab = ({
210
304
  repairing=${isRepairInProgress}
211
305
  />
212
306
 
307
+ ${(() => {
308
+ const r = resourcesPoll.data?.resources;
309
+ if (!r) return null;
310
+ return html`
311
+ <div class="bg-surface border border-border rounded-xl p-4">
312
+ ${memoryExpanded
313
+ ? html`
314
+ <${ResourceBar}
315
+ label="Memory"
316
+ detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
317
+ percent=${r.memory?.percent}
318
+ expanded=${true}
319
+ onToggle=${() => setMemoryExpanded(false)}
320
+ segments=${(() => {
321
+ const p = r.processes;
322
+ const total = r.memory?.totalBytes;
323
+ const used = r.memory?.usedBytes;
324
+ if (!p || !total || !used) return null;
325
+ const segs = [];
326
+ let tracked = 0;
327
+ if (p.gateway?.rssBytes != null) {
328
+ tracked += p.gateway.rssBytes;
329
+ segs.push({
330
+ percent: (p.gateway.rssBytes / total) * 100,
331
+ color: "#22d3ee",
332
+ label: `Gateway ${formatBytes(p.gateway.rssBytes)}`,
333
+ });
334
+ }
335
+ if (p.alphaclaw?.rssBytes != null) {
336
+ tracked += p.alphaclaw.rssBytes;
337
+ segs.push({
338
+ percent: (p.alphaclaw.rssBytes / total) * 100,
339
+ color: "#a78bfa",
340
+ label: `AlphaClaw ${formatBytes(p.alphaclaw.rssBytes)}`,
341
+ });
342
+ }
343
+ const other = Math.max(0, used - tracked);
344
+ if (other > 0) {
345
+ segs.push({
346
+ percent: (other / total) * 100,
347
+ color: "#4b5563",
348
+ label: `Other ${formatBytes(other)}`,
349
+ });
350
+ }
351
+ return segs.length ? segs : null;
352
+ })()}
353
+ />
354
+ `
355
+ : html`
356
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
357
+ <${ResourceBar}
358
+ label="Memory"
359
+ percent=${r.memory?.percent}
360
+ detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
361
+ onToggle=${() => setMemoryExpanded(true)}
362
+ />
363
+ <${ResourceBar}
364
+ label="Disk"
365
+ percent=${r.disk?.percent}
366
+ detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
367
+ />
368
+ <${ResourceBar}
369
+ label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} core${r.cpu.cores > 1 ? "s" : ""})` : ""}`}
370
+ percent=${r.cpu?.percent}
371
+ detail=${r.cpu?.percent != null
372
+ ? `${r.cpu.percent}%`
373
+ : "—"}
374
+ />
375
+ </div>
376
+ `}
377
+ </div>
378
+ `;
379
+ })()}
380
+
213
381
  <div class="bg-surface border border-border rounded-xl p-4">
214
382
  <div class="flex items-center justify-between gap-3">
215
383
  <div class="inline-flex items-center gap-2 text-xs text-gray-400">
@@ -229,7 +397,7 @@ export const WatchdogTab = ({
229
397
  <div class="inline-flex items-center gap-2 text-xs text-gray-400">
230
398
  <span>Notifications</span>
231
399
  <${InfoTooltip}
232
- text="Sends Telegram notices for watchdog alerts and auto-repair outcomes."
400
+ text="Sends channel notices for watchdog alerts and auto-repair outcomes."
233
401
  />
234
402
  </div>
235
403
  <${ToggleSwitch}
@@ -256,7 +424,9 @@ export const WatchdogTab = ({
256
424
  <pre
257
425
  ref=${logsRef}
258
426
  class="bg-black/40 border border-border rounded-lg p-3 h-72 overflow-auto text-xs text-gray-300 whitespace-pre-wrap break-words"
259
- >${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre>
427
+ >
428
+ ${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre
429
+ >
260
430
  </div>
261
431
 
262
432
  <div class="bg-surface border border-border rounded-xl p-4">
@@ -272,39 +442,43 @@ export const WatchdogTab = ({
272
442
  <div class="ac-history-list">
273
443
  ${events.length === 0 &&
274
444
  html`<p class="text-xs text-gray-500">No incidents recorded.</p>`}
275
- ${events.map(
276
- (event) => {
277
- const tone = getIncidentStatusTone(event);
278
- return html`
279
- <details class="ac-history-item">
280
- <summary class="ac-history-summary">
281
- <div class="ac-history-summary-row">
282
- <span class="inline-flex items-center gap-2 min-w-0">
283
- <span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
284
- <span class="truncate">
285
- ${event.createdAt || ""} · ${event.eventType || "event"} · ${event.status ||
286
- "unknown"}
287
- </span>
288
- </span>
445
+ ${events.map((event) => {
446
+ const tone = getIncidentStatusTone(event);
447
+ return html`
448
+ <details class="ac-history-item">
449
+ <summary class="ac-history-summary">
450
+ <div class="ac-history-summary-row">
451
+ <span class="inline-flex items-center gap-2 min-w-0">
289
452
  <span
290
- class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
291
- title=${tone.label}
292
- aria-label=${tone.label}
293
- ></span>
294
- </div>
295
- </summary>
296
- <div class="ac-history-body text-xs text-gray-400">
297
- <div>Source: ${event.source || "unknown"}</div>
298
- <pre class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words"
299
- >${typeof event.details === "string"
300
- ? event.details
301
- : JSON.stringify(event.details || {}, null, 2)}</pre
302
- >
453
+ class="ac-history-toggle shrink-0"
454
+ aria-hidden="true"
455
+ >▸</span
456
+ >
457
+ <span class="truncate">
458
+ ${event.createdAt || ""} · ${event.eventType || "event"}
459
+ · ${event.status || "unknown"}
460
+ </span>
461
+ </span>
462
+ <span
463
+ class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
464
+ title=${tone.label}
465
+ aria-label=${tone.label}
466
+ ></span>
303
467
  </div>
304
- </details>
305
- `;
306
- },
307
- )}
468
+ </summary>
469
+ <div class="ac-history-body text-xs text-gray-400">
470
+ <div>Source: ${event.source || "unknown"}</div>
471
+ <pre
472
+ class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words"
473
+ >
474
+ ${typeof event.details === "string"
475
+ ? event.details
476
+ : JSON.stringify(event.details || {}, null, 2)}</pre
477
+ >
478
+ </div>
479
+ </details>
480
+ `;
481
+ })}
308
482
  </div>
309
483
  </div>
310
484
  </div>
@@ -93,6 +93,11 @@ export async function triggerWatchdogRepair() {
93
93
  return parseJsonOrThrow(res, 'Could not trigger watchdog repair');
94
94
  }
95
95
 
96
+ export async function fetchWatchdogResources() {
97
+ const res = await authFetch('/api/watchdog/resources');
98
+ return parseJsonOrThrow(res, 'Could not load system resources');
99
+ }
100
+
96
101
  export async function fetchWatchdogSettings() {
97
102
  const res = await authFetch('/api/watchdog/settings');
98
103
  return parseJsonOrThrow(res, 'Could not load watchdog settings');
@@ -1,3 +1,5 @@
1
+ const { getSystemResources } = require("../system-resources");
2
+
1
3
  const registerWatchdogRoutes = ({
2
4
  app,
3
5
  requireAuth,
@@ -55,6 +57,15 @@ const registerWatchdogRoutes = ({
55
57
  }
56
58
  });
57
59
 
60
+ app.get("/api/watchdog/resources", requireAuth, (req, res) => {
61
+ try {
62
+ const status = watchdog.getStatus();
63
+ res.json({ ok: true, resources: getSystemResources({ gatewayPid: status.gatewayPid }) });
64
+ } catch (err) {
65
+ res.status(500).json({ ok: false, error: err.message });
66
+ }
67
+ });
68
+
58
69
  app.put("/api/watchdog/settings", requireAuth, (req, res) => {
59
70
  try {
60
71
  const settings = watchdog.updateSettings(req.body || {});
@@ -0,0 +1,149 @@
1
+ const os = require("os");
2
+ const fs = require("fs");
3
+ const { execSync } = require("child_process");
4
+
5
+ const readCgroupFile = (filePath) => {
6
+ try {
7
+ return fs.readFileSync(filePath, "utf8").trim();
8
+ } catch {
9
+ return null;
10
+ }
11
+ };
12
+
13
+ const parseCgroupMemory = () => {
14
+ const current = readCgroupFile("/sys/fs/cgroup/memory.current");
15
+ const max = readCgroupFile("/sys/fs/cgroup/memory.max");
16
+ if (!current) return null;
17
+ const usedBytes = Number.parseInt(current, 10);
18
+ if (Number.isNaN(usedBytes)) return null;
19
+ const limitBytes =
20
+ max && max !== "max" ? Number.parseInt(max, 10) : null;
21
+ return {
22
+ usedBytes,
23
+ totalBytes: Number.isNaN(limitBytes) ? null : limitBytes,
24
+ };
25
+ };
26
+
27
+ const parseCgroupCpu = () => {
28
+ const stat = readCgroupFile("/sys/fs/cgroup/cpu.stat");
29
+ if (!stat) return null;
30
+ const lines = stat.split("\n");
31
+ const map = {};
32
+ for (const line of lines) {
33
+ const [key, val] = line.split(/\s+/);
34
+ if (key && val) map[key] = Number.parseInt(val, 10);
35
+ }
36
+ return {
37
+ usageUsec: map.usage_usec ?? null,
38
+ userUsec: map.user_usec ?? null,
39
+ systemUsec: map.system_usec ?? null,
40
+ };
41
+ };
42
+
43
+ const readProcStatus = (pid) => {
44
+ try {
45
+ const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
46
+ const vmRss = status.match(/VmRSS:\s+(\d+)\s+kB/);
47
+ return { rssBytes: vmRss ? Number.parseInt(vmRss[1], 10) * 1024 : null };
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
52
+
53
+ const readPsStats = (pid) => {
54
+ try {
55
+ const out = execSync(`ps -o rss=,pcpu= -p ${pid}`, {
56
+ encoding: "utf8",
57
+ timeout: 2000,
58
+ stdio: ["ignore", "pipe", "ignore"],
59
+ }).trim();
60
+ const [rss, pcpu] = out.split(/\s+/);
61
+ return {
62
+ rssBytes: rss ? Number.parseInt(rss, 10) * 1024 : null,
63
+ cpuPercent: pcpu ? Number.parseFloat(pcpu) : null,
64
+ };
65
+ } catch {
66
+ return null;
67
+ }
68
+ };
69
+
70
+ const getProcessUsage = (pid) => {
71
+ if (!pid) return null;
72
+ const proc = readProcStatus(pid);
73
+ if (proc) return { rssBytes: proc.rssBytes };
74
+ const ps = readPsStats(pid);
75
+ if (ps) return { rssBytes: ps.rssBytes };
76
+ return null;
77
+ };
78
+
79
+ let prevCpuSnapshot = null;
80
+ let prevCpuSnapshotAt = 0;
81
+
82
+ const getSystemResources = ({ gatewayPid = null } = {}) => {
83
+ const cgroupMem = parseCgroupMemory();
84
+ const mem = {
85
+ usedBytes: cgroupMem?.usedBytes ?? process.memoryUsage().rss,
86
+ totalBytes: cgroupMem?.totalBytes ?? os.totalmem(),
87
+ };
88
+
89
+ let diskUsedBytes = null;
90
+ let diskTotalBytes = null;
91
+ try {
92
+ const stat = fs.statfsSync("/");
93
+ diskTotalBytes = stat.bsize * stat.blocks;
94
+ diskUsedBytes = stat.bsize * (stat.blocks - stat.bfree);
95
+ } catch {
96
+ // statfsSync unavailable
97
+ }
98
+
99
+ const cgroupCpu = parseCgroupCpu();
100
+ let cpuPercent = null;
101
+ if (cgroupCpu?.usageUsec != null) {
102
+ const now = Date.now();
103
+ if (prevCpuSnapshot && prevCpuSnapshotAt) {
104
+ const elapsedMs = now - prevCpuSnapshotAt;
105
+ if (elapsedMs > 0) {
106
+ const usageDeltaUs = cgroupCpu.usageUsec - prevCpuSnapshot.usageUsec;
107
+ const elapsedUs = elapsedMs * 1000;
108
+ cpuPercent = Math.min(100, Math.max(0, (usageDeltaUs / elapsedUs) * 100));
109
+ }
110
+ }
111
+ prevCpuSnapshot = cgroupCpu;
112
+ prevCpuSnapshotAt = now;
113
+ } else {
114
+ const load = os.loadavg();
115
+ const cpus = os.cpus().length || 1;
116
+ cpuPercent = Math.min(100, Math.max(0, (load[0] / cpus) * 100));
117
+ }
118
+
119
+ const alphaclawRss = process.memoryUsage().rss;
120
+ const gatewayUsage = getProcessUsage(gatewayPid);
121
+ const gatewayRss = gatewayUsage?.rssBytes ?? null;
122
+
123
+ return {
124
+ memory: {
125
+ usedBytes: mem.usedBytes,
126
+ totalBytes: mem.totalBytes,
127
+ percent: mem.totalBytes
128
+ ? Math.round((mem.usedBytes / mem.totalBytes) * 1000) / 10
129
+ : null,
130
+ },
131
+ disk: {
132
+ usedBytes: diskUsedBytes,
133
+ totalBytes: diskTotalBytes,
134
+ percent: diskTotalBytes
135
+ ? Math.round((diskUsedBytes / diskTotalBytes) * 1000) / 10
136
+ : null,
137
+ },
138
+ cpu: {
139
+ percent: cpuPercent != null ? Math.round(cpuPercent * 10) / 10 : null,
140
+ cores: os.cpus().length,
141
+ },
142
+ processes: {
143
+ alphaclaw: { rssBytes: alphaclawRss },
144
+ gateway: { rssBytes: gatewayRss, pid: gatewayPid },
145
+ },
146
+ };
147
+ };
148
+
149
+ module.exports = { getSystemResources };
@@ -59,6 +59,7 @@ const createWatchdog = ({
59
59
  notificationsDisabled: isTruthy(process.env.WATCHDOG_NOTIFICATIONS_DISABLED),
60
60
  operationInProgress: false,
61
61
  gatewayStartedAt: null,
62
+ gatewayPid: null,
62
63
  crashRecoveryActive: false,
63
64
  expectedRestartInProgress: false,
64
65
  expectedRestartUntilMs: 0,
@@ -502,13 +503,14 @@ const createWatchdog = ({
502
503
  void restartAfterCrash(correlationId);
503
504
  };
504
505
 
505
- const onGatewayLaunch = ({ startedAt = Date.now() } = {}) => {
506
+ const onGatewayLaunch = ({ startedAt = Date.now(), pid = null } = {}) => {
506
507
  state.lifecycle = "running";
507
508
  state.health = "unknown";
508
509
  state.crashRecoveryActive = false;
509
510
  clearExpectedRestartWindow();
510
511
  state.uptimeStartedAt = startedAt;
511
512
  state.gatewayStartedAt = startedAt;
513
+ state.gatewayPid = pid;
512
514
  startBootstrapHealthChecks();
513
515
  };
514
516
 
@@ -566,6 +568,7 @@ const createWatchdog = ({
566
568
  crashLoopThreshold: kWatchdogCrashLoopThreshold,
567
569
  crashLoopWindowMs: kWatchdogCrashLoopWindowMs,
568
570
  operationInProgress: state.operationInProgress,
571
+ gatewayPid: state.gatewayPid,
569
572
  };
570
573
  };
571
574
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.3.1",
3
+ "version": "0.3.2-beta.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },