@chrysb/alphaclaw 0.7.0 → 0.7.1

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 (31) hide show
  1. package/lib/public/css/cron.css +26 -17
  2. package/lib/public/css/theme.css +14 -0
  3. package/lib/public/js/components/cron-tab/cron-calendar.js +17 -12
  4. package/lib/public/js/components/cron-tab/cron-job-list.js +11 -1
  5. package/lib/public/js/components/cron-tab/index.js +16 -2
  6. package/lib/public/js/components/icons.js +11 -0
  7. package/lib/public/js/components/routes/watchdog-route.js +1 -1
  8. package/lib/public/js/components/watchdog-tab/console/index.js +115 -0
  9. package/lib/public/js/components/watchdog-tab/console/use-console.js +137 -0
  10. package/lib/public/js/components/watchdog-tab/helpers.js +106 -0
  11. package/lib/public/js/components/watchdog-tab/incidents/index.js +56 -0
  12. package/lib/public/js/components/watchdog-tab/incidents/use-incidents.js +33 -0
  13. package/lib/public/js/components/watchdog-tab/index.js +84 -0
  14. package/lib/public/js/components/watchdog-tab/resource-bar.js +76 -0
  15. package/lib/public/js/components/watchdog-tab/resources/index.js +85 -0
  16. package/lib/public/js/components/watchdog-tab/resources/use-resources.js +13 -0
  17. package/lib/public/js/components/watchdog-tab/settings/index.js +44 -0
  18. package/lib/public/js/components/watchdog-tab/settings/use-settings.js +117 -0
  19. package/lib/public/js/components/watchdog-tab/terminal/index.js +20 -0
  20. package/lib/public/js/components/watchdog-tab/terminal/use-terminal.js +263 -0
  21. package/lib/public/js/components/watchdog-tab/use-watchdog-tab.js +55 -0
  22. package/lib/public/js/lib/api.js +39 -0
  23. package/lib/server/init/register-server-routes.js +239 -0
  24. package/lib/server/init/runtime-init.js +44 -0
  25. package/lib/server/init/server-lifecycle.js +55 -0
  26. package/lib/server/routes/watchdog.js +62 -0
  27. package/lib/server/watchdog-terminal-ws.js +114 -0
  28. package/lib/server/watchdog-terminal.js +262 -0
  29. package/lib/server.js +89 -215
  30. package/package.json +3 -2
  31. package/lib/public/js/components/watchdog-tab.js +0 -535
@@ -6,6 +6,7 @@ const registerWatchdogRoutes = ({
6
6
  watchdog,
7
7
  getRecentEvents,
8
8
  readLogTail,
9
+ watchdogTerminal,
9
10
  }) => {
10
11
  app.get("/api/watchdog/status", requireAuth, (req, res) => {
11
12
  try {
@@ -74,6 +75,67 @@ const registerWatchdogRoutes = ({
74
75
  res.status(400).json({ ok: false, error: err.message });
75
76
  }
76
77
  });
78
+
79
+ app.post("/api/watchdog/terminal/session", requireAuth, (req, res) => {
80
+ try {
81
+ const terminalSession = watchdogTerminal.createOrReuseSession();
82
+ res.json({ ok: true, session: terminalSession });
83
+ } catch (err) {
84
+ res.status(500).json({ ok: false, error: err.message });
85
+ }
86
+ });
87
+
88
+ app.get("/api/watchdog/terminal/output", requireAuth, (req, res) => {
89
+ try {
90
+ const sessionId = String(req.query.sessionId || "");
91
+ if (!sessionId) {
92
+ res.status(400).json({ ok: false, error: "Missing sessionId" });
93
+ return;
94
+ }
95
+ const cursor = Number.parseInt(String(req.query.cursor || "0"), 10) || 0;
96
+ const output = watchdogTerminal.readOutput({ sessionId, cursor });
97
+ if (!output.found) {
98
+ res.status(404).json({ ok: false, error: "Terminal session not found" });
99
+ return;
100
+ }
101
+ res.json({ ok: true, ...output });
102
+ } catch (err) {
103
+ res.status(500).json({ ok: false, error: err.message });
104
+ }
105
+ });
106
+
107
+ app.post("/api/watchdog/terminal/input", requireAuth, (req, res) => {
108
+ try {
109
+ const sessionId = String(req.body?.sessionId || "");
110
+ const input = String(req.body?.input || "");
111
+ if (!sessionId) {
112
+ res.status(400).json({ ok: false, error: "Missing sessionId" });
113
+ return;
114
+ }
115
+ const result = watchdogTerminal.writeInput({ sessionId, input });
116
+ if (!result.ok) {
117
+ res.status(400).json({ ok: false, error: result.error || "Write failed" });
118
+ return;
119
+ }
120
+ res.json({ ok: true });
121
+ } catch (err) {
122
+ res.status(500).json({ ok: false, error: err.message });
123
+ }
124
+ });
125
+
126
+ app.post("/api/watchdog/terminal/close", requireAuth, (req, res) => {
127
+ try {
128
+ const sessionId = String(req.body?.sessionId || "");
129
+ if (!sessionId) {
130
+ res.status(400).json({ ok: false, error: "Missing sessionId" });
131
+ return;
132
+ }
133
+ watchdogTerminal.closeSession({ sessionId });
134
+ res.json({ ok: true });
135
+ } catch (err) {
136
+ res.status(500).json({ ok: false, error: err.message });
137
+ }
138
+ });
77
139
  };
78
140
 
79
141
  module.exports = { registerWatchdogRoutes };
@@ -0,0 +1,114 @@
1
+ const { WebSocketServer } = require("ws");
2
+
3
+ const kWatchdogTerminalWsPath = "/api/watchdog/terminal/ws";
4
+
5
+ const createWatchdogTerminalWsBridge = ({
6
+ server,
7
+ proxy,
8
+ getGatewayUrl,
9
+ isAuthorizedRequest,
10
+ watchdogTerminal,
11
+ }) => {
12
+ const watchdogTerminalWss = new WebSocketServer({ noServer: true });
13
+
14
+ watchdogTerminalWss.on("connection", (socket) => {
15
+ let closed = false;
16
+ const terminalSession = watchdogTerminal.createOrReuseSession();
17
+ const sessionId = String(terminalSession?.id || "");
18
+ if (!sessionId) {
19
+ socket.close(1011, "No terminal session");
20
+ return;
21
+ }
22
+
23
+ const send = (payload = {}) => {
24
+ if (closed || socket.readyState !== 1) return;
25
+ socket.send(JSON.stringify(payload));
26
+ };
27
+
28
+ send({
29
+ type: "session",
30
+ session: terminalSession,
31
+ });
32
+
33
+ const subscription = watchdogTerminal.subscribe({
34
+ sessionId,
35
+ replayBuffer: false,
36
+ tailLines: 1,
37
+ onEvent: (event) => {
38
+ if (event?.type === "output") {
39
+ send({ type: "output", data: String(event.data || "") });
40
+ return;
41
+ }
42
+ if (event?.type === "exit") {
43
+ send({
44
+ type: "exit",
45
+ code: event.code ?? null,
46
+ signal: event.signal ?? null,
47
+ });
48
+ }
49
+ },
50
+ });
51
+ if (!subscription.ok) {
52
+ socket.close(1011, "Terminal subscribe failed");
53
+ return;
54
+ }
55
+
56
+ socket.on("message", (rawData) => {
57
+ let payload = null;
58
+ try {
59
+ payload = JSON.parse(String(rawData || ""));
60
+ } catch {
61
+ return;
62
+ }
63
+ const messageType = String(payload?.type || "");
64
+ if (messageType !== "input") return;
65
+ const data = String(payload?.data || "");
66
+ if (!data) return;
67
+ watchdogTerminal.writeInput({ sessionId, input: data });
68
+ });
69
+
70
+ socket.on("close", () => {
71
+ closed = true;
72
+ subscription.unsubscribe();
73
+ });
74
+ socket.on("error", () => {
75
+ closed = true;
76
+ subscription.unsubscribe();
77
+ });
78
+ });
79
+
80
+ server.on("upgrade", (req, socket, head) => {
81
+ const requestUrl = new URL(
82
+ req.url || "/",
83
+ `http://${req.headers.host || "localhost"}`,
84
+ );
85
+ if (
86
+ requestUrl.pathname.startsWith("/openclaw") ||
87
+ requestUrl.pathname === kWatchdogTerminalWsPath
88
+ ) {
89
+ const upgradeReq = {
90
+ headers: req.headers,
91
+ path: requestUrl.pathname,
92
+ query: Object.fromEntries(requestUrl.searchParams.entries()),
93
+ };
94
+ if (!isAuthorizedRequest(upgradeReq)) {
95
+ socket.write(
96
+ "HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nUnauthorized",
97
+ );
98
+ socket.destroy();
99
+ return;
100
+ }
101
+ }
102
+ if (requestUrl.pathname === kWatchdogTerminalWsPath) {
103
+ watchdogTerminalWss.handleUpgrade(req, socket, head, (ws) => {
104
+ watchdogTerminalWss.emit("connection", ws, req);
105
+ });
106
+ return;
107
+ }
108
+ proxy.ws(req, socket, head, { target: getGatewayUrl() });
109
+ });
110
+ };
111
+
112
+ module.exports = {
113
+ createWatchdogTerminalWsBridge,
114
+ };
@@ -0,0 +1,262 @@
1
+ const crypto = require("crypto");
2
+ const { spawn, spawnSync } = require("child_process");
3
+
4
+ const kSessionIdleTtlMs = 15 * 60 * 1000;
5
+ const kCleanupIntervalMs = 30 * 1000;
6
+ const kMaxBufferedOutputChars = 200000;
7
+
8
+ const hasScriptCommand = () => {
9
+ try {
10
+ const result = spawnSync("sh", ["-lc", "command -v script >/dev/null 2>&1"], {
11
+ stdio: "ignore",
12
+ });
13
+ return result.status === 0;
14
+ } catch {
15
+ return false;
16
+ }
17
+ };
18
+
19
+ const createShellProcess = ({
20
+ shell = "/bin/bash",
21
+ cwd = process.cwd(),
22
+ env = {},
23
+ preferPty = false,
24
+ } = {}) => {
25
+ if (preferPty && process.platform === "darwin") {
26
+ return spawn("script", ["-q", "/dev/null", shell, "-i"], {
27
+ cwd,
28
+ env: { ...env, TERM: env.TERM || "xterm-256color" },
29
+ stdio: "pipe",
30
+ });
31
+ }
32
+ if (preferPty) {
33
+ return spawn("script", ["-q", "-f", "-c", `${shell} -i`, "/dev/null"], {
34
+ cwd,
35
+ env: { ...env, TERM: env.TERM || "xterm-256color" },
36
+ stdio: "pipe",
37
+ });
38
+ }
39
+ return spawn(shell, ["-i"], {
40
+ cwd,
41
+ env: { ...env, TERM: env.TERM || "xterm-256color" },
42
+ stdio: "pipe",
43
+ });
44
+ };
45
+
46
+ const createWatchdogTerminalService = ({
47
+ cwd = process.cwd(),
48
+ shell = process.env.SHELL || "/bin/bash",
49
+ env = process.env,
50
+ } = {}) => {
51
+ let session = null;
52
+ const preferPty = hasScriptCommand();
53
+
54
+ const notifySubscribers = (event) => {
55
+ if (!session?.subscribers?.size) return;
56
+ session.subscribers.forEach((subscriber) => {
57
+ try {
58
+ subscriber(event);
59
+ } catch {}
60
+ });
61
+ };
62
+
63
+ const appendOutput = (chunk = "") => {
64
+ if (!session || !chunk) return;
65
+ const chunkText = String(chunk);
66
+ session.output += chunkText;
67
+ session.endCursor += chunkText.length;
68
+ if (session.output.length > kMaxBufferedOutputChars) {
69
+ const trimCount = session.output.length - kMaxBufferedOutputChars;
70
+ session.output = session.output.slice(trimCount);
71
+ session.startCursor += trimCount;
72
+ }
73
+ notifySubscribers({ type: "output", data: chunkText });
74
+ };
75
+
76
+ const markActive = () => {
77
+ if (!session) return;
78
+ session.lastActiveAtMs = Date.now();
79
+ };
80
+
81
+ const createOrReuseSession = () => {
82
+ if (session && !session.ended) {
83
+ markActive();
84
+ return {
85
+ id: session.id,
86
+ shell,
87
+ cwd,
88
+ ended: false,
89
+ };
90
+ }
91
+ if (session && session.ended) session = null;
92
+
93
+ const proc = createShellProcess({ shell, cwd, env, preferPty });
94
+ const sessionId = crypto.randomUUID();
95
+ session = {
96
+ id: sessionId,
97
+ proc,
98
+ output: "",
99
+ startCursor: 0,
100
+ endCursor: 0,
101
+ ended: false,
102
+ exitCode: null,
103
+ signal: null,
104
+ lastActiveAtMs: Date.now(),
105
+ subscribers: new Set(),
106
+ };
107
+
108
+ proc.stdout.setEncoding("utf8");
109
+ proc.stderr.setEncoding("utf8");
110
+ proc.stdout.on("data", (chunk) => appendOutput(chunk));
111
+ proc.stderr.on("data", (chunk) => appendOutput(chunk));
112
+ proc.on("close", (code, signal) => {
113
+ if (!session || session.id !== sessionId) return;
114
+ session.ended = true;
115
+ session.exitCode = code;
116
+ session.signal = signal;
117
+ const endLine = `\r\n[terminal exited${code != null ? ` with code ${code}` : ""}${signal ? ` (${signal})` : ""}]\r\n`;
118
+ appendOutput(endLine);
119
+ notifySubscribers({
120
+ type: "exit",
121
+ code,
122
+ signal,
123
+ });
124
+ });
125
+
126
+ return {
127
+ id: session.id,
128
+ shell,
129
+ cwd,
130
+ ended: false,
131
+ };
132
+ };
133
+
134
+ const subscribe = ({
135
+ sessionId = "",
136
+ onEvent = () => {},
137
+ replayBuffer = true,
138
+ tailLines = 0,
139
+ } = {}) => {
140
+ if (!session || String(session.id) !== String(sessionId || "")) {
141
+ return {
142
+ ok: false,
143
+ error: "Terminal session not found",
144
+ unsubscribe: () => {},
145
+ };
146
+ }
147
+ markActive();
148
+ const subscriber = (event) => onEvent(event);
149
+ session.subscribers.add(subscriber);
150
+ if (replayBuffer && session.output) {
151
+ onEvent({ type: "output", data: session.output });
152
+ } else if (!replayBuffer && Number(tailLines || 0) > 0 && !session.ended) {
153
+ const lines = String(session.output || "").split("\n");
154
+ const count = Math.max(1, Math.floor(Number(tailLines || 0)));
155
+ const tail = lines.slice(-count).join("\n");
156
+ if (tail.trim()) onEvent({ type: "output", data: tail });
157
+ }
158
+ if (session.ended) {
159
+ onEvent({
160
+ type: "exit",
161
+ code: session.exitCode,
162
+ signal: session.signal,
163
+ });
164
+ }
165
+ return {
166
+ ok: true,
167
+ unsubscribe: () => {
168
+ if (!session) return;
169
+ session.subscribers.delete(subscriber);
170
+ },
171
+ };
172
+ };
173
+
174
+ const readOutput = ({ sessionId = "", cursor = 0 } = {}) => {
175
+ if (!session || String(session.id) !== String(sessionId || "")) {
176
+ return {
177
+ found: false,
178
+ output: "",
179
+ cursor: 0,
180
+ startCursor: 0,
181
+ endCursor: 0,
182
+ ended: true,
183
+ };
184
+ }
185
+ markActive();
186
+ const requestedCursor = Number(cursor);
187
+ const safeCursor = Number.isFinite(requestedCursor)
188
+ ? Math.max(0, Math.floor(requestedCursor))
189
+ : 0;
190
+ const effectiveCursor =
191
+ safeCursor < session.startCursor || safeCursor > session.endCursor
192
+ ? session.startCursor
193
+ : safeCursor;
194
+ const sliceIndex = Math.max(0, effectiveCursor - session.startCursor);
195
+ return {
196
+ found: true,
197
+ output: session.output.slice(sliceIndex),
198
+ cursor: session.endCursor,
199
+ startCursor: session.startCursor,
200
+ endCursor: session.endCursor,
201
+ ended: !!session.ended,
202
+ exitCode: session.exitCode,
203
+ signal: session.signal,
204
+ };
205
+ };
206
+
207
+ const writeInput = ({ sessionId = "", input = "" } = {}) => {
208
+ if (!session || String(session.id) !== String(sessionId || "")) {
209
+ return { ok: false, error: "Terminal session not found" };
210
+ }
211
+ if (session.ended || !session.proc.stdin.writable) {
212
+ return { ok: false, error: "Terminal session has ended" };
213
+ }
214
+ markActive();
215
+ session.proc.stdin.write(String(input || ""));
216
+ return { ok: true };
217
+ };
218
+
219
+ const closeSession = ({ sessionId = "" } = {}) => {
220
+ if (!session || String(session.id) !== String(sessionId || "")) {
221
+ return { ok: true };
222
+ }
223
+ const targetProc = session.proc;
224
+ session = null;
225
+ try {
226
+ targetProc.kill("SIGTERM");
227
+ } catch {}
228
+ return { ok: true };
229
+ };
230
+
231
+ const disposeSession = () => {
232
+ if (!session) return;
233
+ const targetProc = session.proc;
234
+ session = null;
235
+ try {
236
+ targetProc.kill("SIGTERM");
237
+ } catch {}
238
+ };
239
+
240
+ const cleanupTimer = setInterval(() => {
241
+ if (!session || session.ended) return;
242
+ const idleForMs = Date.now() - Number(session.lastActiveAtMs || 0);
243
+ if (idleForMs < kSessionIdleTtlMs) return;
244
+ try {
245
+ session.proc.kill("SIGTERM");
246
+ } catch {}
247
+ }, kCleanupIntervalMs);
248
+ cleanupTimer.unref?.();
249
+
250
+ return {
251
+ createOrReuseSession,
252
+ subscribe,
253
+ readOutput,
254
+ writeInput,
255
+ closeSession,
256
+ disposeSession,
257
+ };
258
+ };
259
+
260
+ module.exports = {
261
+ createWatchdogTerminalService,
262
+ };