@chrysb/alphaclaw 0.2.3 → 0.3.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 (63) hide show
  1. package/bin/alphaclaw.js +79 -0
  2. package/lib/public/css/shell.css +57 -2
  3. package/lib/public/css/theme.css +184 -0
  4. package/lib/public/js/app.js +330 -89
  5. package/lib/public/js/components/action-button.js +92 -0
  6. package/lib/public/js/components/channels.js +16 -7
  7. package/lib/public/js/components/confirm-dialog.js +25 -19
  8. package/lib/public/js/components/credentials-modal.js +32 -23
  9. package/lib/public/js/components/device-pairings.js +15 -2
  10. package/lib/public/js/components/envars.js +22 -65
  11. package/lib/public/js/components/features.js +1 -1
  12. package/lib/public/js/components/gateway.js +139 -32
  13. package/lib/public/js/components/global-restart-banner.js +31 -0
  14. package/lib/public/js/components/google.js +9 -9
  15. package/lib/public/js/components/icons.js +19 -0
  16. package/lib/public/js/components/info-tooltip.js +18 -0
  17. package/lib/public/js/components/loading-spinner.js +32 -0
  18. package/lib/public/js/components/modal-shell.js +42 -0
  19. package/lib/public/js/components/models.js +34 -29
  20. package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
  21. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  22. package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
  23. package/lib/public/js/components/page-header.js +13 -0
  24. package/lib/public/js/components/pairings.js +15 -2
  25. package/lib/public/js/components/providers.js +216 -142
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/secret-input.js +1 -0
  28. package/lib/public/js/components/telegram-workspace.js +37 -49
  29. package/lib/public/js/components/toast.js +34 -5
  30. package/lib/public/js/components/toggle-switch.js +25 -0
  31. package/lib/public/js/components/update-action-button.js +13 -53
  32. package/lib/public/js/components/watchdog-tab.js +312 -0
  33. package/lib/public/js/components/webhooks.js +981 -0
  34. package/lib/public/js/components/welcome.js +2 -1
  35. package/lib/public/js/lib/api.js +102 -1
  36. package/lib/public/js/lib/model-config.js +0 -5
  37. package/lib/server/alphaclaw-version.js +5 -3
  38. package/lib/server/constants.js +33 -0
  39. package/lib/server/discord-api.js +48 -0
  40. package/lib/server/gateway.js +64 -4
  41. package/lib/server/log-writer.js +102 -0
  42. package/lib/server/onboarding/github.js +21 -1
  43. package/lib/server/openclaw-version.js +2 -6
  44. package/lib/server/restart-required-state.js +86 -0
  45. package/lib/server/routes/auth.js +9 -4
  46. package/lib/server/routes/proxy.js +12 -14
  47. package/lib/server/routes/system.js +61 -15
  48. package/lib/server/routes/telegram.js +17 -48
  49. package/lib/server/routes/watchdog.js +68 -0
  50. package/lib/server/routes/webhooks.js +214 -0
  51. package/lib/server/telegram-api.js +11 -0
  52. package/lib/server/watchdog-db.js +148 -0
  53. package/lib/server/watchdog-notify.js +93 -0
  54. package/lib/server/watchdog.js +585 -0
  55. package/lib/server/webhook-middleware.js +195 -0
  56. package/lib/server/webhooks-db.js +265 -0
  57. package/lib/server/webhooks.js +238 -0
  58. package/lib/server.js +119 -4
  59. package/lib/setup/core-prompts/AGENTS.md +84 -0
  60. package/lib/setup/core-prompts/TOOLS.md +13 -0
  61. package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
  62. package/lib/setup/gitignore +2 -0
  63. package/package.json +2 -1
@@ -355,6 +355,7 @@ export const Welcome = ({ onComplete }) => {
355
355
 
356
356
  const goBack = () => {
357
357
  if (isSetupStep) return;
358
+ setError(null);
358
359
  setStep((prev) => Math.max(0, prev - 1));
359
360
  };
360
361
  const goBackFromSetupError = () => {
@@ -364,9 +365,9 @@ export const Welcome = ({ onComplete }) => {
364
365
 
365
366
  const goNext = async () => {
366
367
  if (!activeGroup || !currentGroupValid) return;
368
+ setError(null);
367
369
  if (activeGroup.id === "github") {
368
370
  setGithubStepLoading(true);
369
- setError(null);
370
371
  try {
371
372
  const result = await verifyGithubOnboardingRepo(
372
373
  vals.GITHUB_WORKSPACE_REPO,
@@ -64,7 +64,47 @@ export async function disconnectGoogle() {
64
64
 
65
65
  export async function restartGateway() {
66
66
  const res = await authFetch('/api/gateway/restart', { method: 'POST' });
67
- return res.json();
67
+ return parseJsonOrThrow(res, 'Could not restart gateway');
68
+ }
69
+
70
+ export async function fetchRestartStatus() {
71
+ const res = await authFetch('/api/restart-status');
72
+ return parseJsonOrThrow(res, 'Could not load restart status');
73
+ }
74
+
75
+ export async function fetchWatchdogStatus() {
76
+ const res = await authFetch('/api/watchdog/status');
77
+ return parseJsonOrThrow(res, 'Could not load watchdog status');
78
+ }
79
+
80
+ export async function fetchWatchdogEvents(limit = 20) {
81
+ const res = await authFetch(`/api/watchdog/events?limit=${encodeURIComponent(String(limit))}`);
82
+ return parseJsonOrThrow(res, 'Could not load watchdog events');
83
+ }
84
+
85
+ export async function fetchWatchdogLogs(tail = 65536) {
86
+ const res = await authFetch(`/api/watchdog/logs?tail=${encodeURIComponent(String(tail))}`);
87
+ if (!res.ok) throw new Error('Could not load watchdog logs');
88
+ return res.text();
89
+ }
90
+
91
+ export async function triggerWatchdogRepair() {
92
+ const res = await authFetch('/api/watchdog/repair', { method: 'POST' });
93
+ return parseJsonOrThrow(res, 'Could not trigger watchdog repair');
94
+ }
95
+
96
+ export async function fetchWatchdogSettings() {
97
+ const res = await authFetch('/api/watchdog/settings');
98
+ return parseJsonOrThrow(res, 'Could not load watchdog settings');
99
+ }
100
+
101
+ export async function updateWatchdogSettings(settings) {
102
+ const res = await authFetch('/api/watchdog/settings', {
103
+ method: 'PUT',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(settings || {}),
106
+ });
107
+ return parseJsonOrThrow(res, 'Could not update watchdog settings');
68
108
  }
69
109
 
70
110
  export async function fetchDashboardUrl() {
@@ -237,3 +277,64 @@ export async function saveEnvVars(vars) {
237
277
  }
238
278
  return data;
239
279
  }
280
+
281
+ const parseJsonOrThrow = async (res, fallbackError) => {
282
+ const text = await res.text();
283
+ let data;
284
+ try {
285
+ data = text ? JSON.parse(text) : {};
286
+ } catch {
287
+ throw new Error(text || fallbackError);
288
+ }
289
+ if (!res.ok || data?.ok === false) {
290
+ throw new Error(data.error || text || `HTTP ${res.status}`);
291
+ }
292
+ return data;
293
+ };
294
+
295
+ export async function fetchWebhooks() {
296
+ const res = await authFetch('/api/webhooks');
297
+ return parseJsonOrThrow(res, 'Could not load webhooks');
298
+ }
299
+
300
+ export async function fetchWebhookDetail(name) {
301
+ const res = await authFetch(`/api/webhooks/${encodeURIComponent(name)}`);
302
+ return parseJsonOrThrow(res, 'Could not load webhook detail');
303
+ }
304
+
305
+ export async function createWebhook(name) {
306
+ const res = await authFetch('/api/webhooks', {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify({ name }),
310
+ });
311
+ return parseJsonOrThrow(res, 'Could not create webhook');
312
+ }
313
+
314
+ export async function deleteWebhook(name, { deleteTransformDir = false } = {}) {
315
+ const res = await authFetch(`/api/webhooks/${encodeURIComponent(name)}`, {
316
+ method: 'DELETE',
317
+ headers: { 'Content-Type': 'application/json' },
318
+ body: JSON.stringify({ deleteTransformDir: !!deleteTransformDir }),
319
+ });
320
+ return parseJsonOrThrow(res, 'Could not delete webhook');
321
+ }
322
+
323
+ export async function fetchWebhookRequests(name, { limit = 50, offset = 0, status = 'all' } = {}) {
324
+ const params = new URLSearchParams({
325
+ limit: String(limit),
326
+ offset: String(offset),
327
+ status: String(status || 'all'),
328
+ });
329
+ const res = await authFetch(
330
+ `/api/webhooks/${encodeURIComponent(name)}/requests?${params.toString()}`,
331
+ );
332
+ return parseJsonOrThrow(res, 'Could not load webhook requests');
333
+ }
334
+
335
+ export async function fetchWebhookRequest(name, id) {
336
+ const res = await authFetch(
337
+ `/api/webhooks/${encodeURIComponent(name)}/requests/${encodeURIComponent(String(id))}`,
338
+ );
339
+ return parseJsonOrThrow(res, 'Could not load webhook request');
340
+ }
@@ -1,8 +1,3 @@
1
- import { h } from "https://esm.sh/preact";
2
- import htm from "https://esm.sh/htm";
3
-
4
- const html = htm.bind(h);
5
-
6
1
  export const getModelProvider = (modelKey) => String(modelKey || "").split("/")[0] || "";
7
2
 
8
3
  export const getAuthProviderFromModelProvider = (provider) =>
@@ -88,9 +88,11 @@ const createAlphaclawVersionService = () => {
88
88
  latestVersion !== currentVersion
89
89
  );
90
90
  kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
91
- console.log(
92
- `[alphaclaw] alphaclaw update status: hasUpdate=${hasUpdate} current=${currentVersion} latest=${latestVersion || "unknown"}`,
93
- );
91
+ if (hasUpdate) {
92
+ console.log(
93
+ `[alphaclaw] alphaclaw update available: current=${currentVersion} latest=${latestVersion || "unknown"}`,
94
+ );
95
+ }
94
96
  return { latestVersion, hasUpdate };
95
97
  };
96
98
 
@@ -115,6 +115,28 @@ const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
115
115
  const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
116
116
  const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
117
117
  const kAppDir = kNpmPackageRoot;
118
+ const kMaxPayloadBytes = parsePositiveIntEnv(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024);
119
+ const kWebhookPruneDays = parsePositiveIntEnv(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30);
120
+ const kWatchdogCheckIntervalMs =
121
+ parsePositiveIntEnv(process.env.WATCHDOG_CHECK_INTERVAL, 120) * 1000;
122
+ const kWatchdogMaxRepairAttempts = parsePositiveIntEnv(
123
+ process.env.WATCHDOG_MAX_REPAIR_ATTEMPTS,
124
+ 2,
125
+ );
126
+ const kWatchdogCrashLoopWindowMs =
127
+ parsePositiveIntEnv(process.env.WATCHDOG_CRASH_LOOP_WINDOW, 300) * 1000;
128
+ const kWatchdogCrashLoopThreshold = parsePositiveIntEnv(
129
+ process.env.WATCHDOG_CRASH_LOOP_THRESHOLD,
130
+ 3,
131
+ );
132
+ const kWatchdogLogRetentionDays = parsePositiveIntEnv(
133
+ process.env.WATCHDOG_LOG_RETENTION_DAYS,
134
+ 30,
135
+ );
136
+ const kLogMaxBytes = parsePositiveIntEnv(
137
+ process.env.LOG_MAX_BYTES,
138
+ 2 * 1024 * 1024,
139
+ );
118
140
 
119
141
  const kSystemVars = new Set([
120
142
  "WEBHOOK_TOKEN",
@@ -258,6 +280,7 @@ const SETUP_API_PREFIXES = [
258
280
  "/api/codex",
259
281
  "/api/models",
260
282
  "/api/gateway",
283
+ "/api/restart-status",
261
284
  "/api/onboard",
262
285
  "/api/env",
263
286
  "/api/auth",
@@ -265,6 +288,8 @@ const SETUP_API_PREFIXES = [
265
288
  "/api/devices",
266
289
  "/api/sync-cron",
267
290
  "/api/telegram",
291
+ "/api/webhooks",
292
+ "/api/watchdog",
268
293
  ];
269
294
 
270
295
  module.exports = {
@@ -303,6 +328,14 @@ module.exports = {
303
328
  kOpenclawRegistryUrl,
304
329
  kAlphaclawRegistryUrl,
305
330
  kAppDir,
331
+ kMaxPayloadBytes,
332
+ kWebhookPruneDays,
333
+ kWatchdogCheckIntervalMs,
334
+ kWatchdogMaxRepairAttempts,
335
+ kWatchdogCrashLoopWindowMs,
336
+ kWatchdogCrashLoopThreshold,
337
+ kWatchdogLogRetentionDays,
338
+ kLogMaxBytes,
306
339
  kSystemVars,
307
340
  kKnownVars,
308
341
  kKnownKeys,
@@ -0,0 +1,48 @@
1
+ const kDiscordApiBase = "https://discord.com/api/v10";
2
+
3
+ const createDiscordApi = (getToken) => {
4
+ const call = async (path, { method = "GET", body } = {}) => {
5
+ const token = typeof getToken === "function" ? getToken() : getToken;
6
+ if (!token) throw new Error("DISCORD_BOT_TOKEN is not set");
7
+ const res = await fetch(`${kDiscordApiBase}${path}`, {
8
+ method,
9
+ headers: {
10
+ Authorization: `Bot ${token}`,
11
+ "Content-Type": "application/json",
12
+ },
13
+ ...(body != null ? { body: JSON.stringify(body) } : {}),
14
+ });
15
+ const data = await res.json().catch(() => ({}));
16
+ if (!res.ok) {
17
+ const err = new Error(data?.message || `Discord API error: ${method} ${path}`);
18
+ err.discordStatusCode = res.status;
19
+ throw err;
20
+ }
21
+ return data;
22
+ };
23
+
24
+ const createDmChannel = (userId) =>
25
+ call("/users/@me/channels", {
26
+ method: "POST",
27
+ body: { recipient_id: String(userId || "") },
28
+ });
29
+
30
+ const sendMessage = (channelId, content) =>
31
+ call(`/channels/${channelId}/messages`, {
32
+ method: "POST",
33
+ body: { content: String(content || "") },
34
+ });
35
+
36
+ const sendDirectMessage = async (userId, content) => {
37
+ const channel = await createDmChannel(userId);
38
+ return sendMessage(channel?.id, content);
39
+ };
40
+
41
+ return {
42
+ createDmChannel,
43
+ sendMessage,
44
+ sendDirectMessage,
45
+ };
46
+ };
47
+
48
+ module.exports = { createDiscordApi };
@@ -4,6 +4,31 @@ const net = require("net");
4
4
  const { OPENCLAW_DIR, GATEWAY_HOST, GATEWAY_PORT, kChannelDefs, kRootDir } = require("./constants");
5
5
 
6
6
  let gatewayChild = null;
7
+ let gatewayExitHandler = null;
8
+ let gatewayLaunchHandler = null;
9
+ const kGatewayStderrTailLines = 50;
10
+ let gatewayStderrTail = [];
11
+ const expectedExitPids = new Set();
12
+
13
+ const appendStderrTail = (chunk) => {
14
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? "");
15
+ for (const line of text.split("\n")) {
16
+ const trimmed = line.trimEnd();
17
+ if (!trimmed) continue;
18
+ gatewayStderrTail.push(trimmed);
19
+ }
20
+ if (gatewayStderrTail.length > kGatewayStderrTailLines) {
21
+ gatewayStderrTail = gatewayStderrTail.slice(-kGatewayStderrTailLines);
22
+ }
23
+ };
24
+
25
+ const setGatewayExitHandler = (handler) => {
26
+ gatewayExitHandler = typeof handler === "function" ? handler : null;
27
+ };
28
+
29
+ const setGatewayLaunchHandler = (handler) => {
30
+ gatewayLaunchHandler = typeof handler === "function" ? handler : null;
31
+ };
7
32
 
8
33
  const gatewayEnv = () => ({
9
34
  ...process.env,
@@ -52,19 +77,50 @@ const runGatewayCmd = (cmd) => {
52
77
  const launchGatewayProcess = () => {
53
78
  if (gatewayChild && gatewayChild.exitCode === null && !gatewayChild.killed) {
54
79
  console.log("[alphaclaw] Managed gateway process already running — skipping launch");
55
- return;
80
+ return gatewayChild;
56
81
  }
82
+ gatewayStderrTail = [];
57
83
  const child = spawn("openclaw", ["gateway", "run"], {
58
84
  env: gatewayEnv(),
59
85
  stdio: ["pipe", "pipe", "pipe"],
60
86
  });
61
87
  gatewayChild = child;
88
+ if (gatewayLaunchHandler) {
89
+ try {
90
+ gatewayLaunchHandler({
91
+ pid: child.pid,
92
+ startedAt: Date.now(),
93
+ });
94
+ } catch (err) {
95
+ console.error(`[alphaclaw] Gateway launch handler error: ${err.message}`);
96
+ }
97
+ }
62
98
  child.stdout.on("data", (d) => process.stdout.write(`[gateway] ${d}`));
63
- child.stderr.on("data", (d) => process.stderr.write(`[gateway] ${d}`));
64
- child.on("exit", (code) => {
65
- console.log(`[alphaclaw] Gateway launcher exited with code ${code}`);
99
+ child.stderr.on("data", (d) => {
100
+ appendStderrTail(d);
101
+ process.stderr.write(`[gateway] ${d}`);
102
+ });
103
+ child.on("exit", (code, signal) => {
104
+ const expectedExit = expectedExitPids.has(child.pid);
105
+ expectedExitPids.delete(child.pid);
106
+ console.log(
107
+ `[alphaclaw] Gateway launcher exited with code ${code}${signal ? ` signal ${signal}` : ""}`,
108
+ );
109
+ if (gatewayExitHandler) {
110
+ try {
111
+ gatewayExitHandler({
112
+ code,
113
+ signal,
114
+ expectedExit,
115
+ stderrTail: gatewayStderrTail.slice(-kGatewayStderrTailLines),
116
+ });
117
+ } catch (err) {
118
+ console.error(`[alphaclaw] Gateway exit handler error: ${err.message}`);
119
+ }
120
+ }
66
121
  if (gatewayChild === child) gatewayChild = null;
67
122
  });
123
+ return child;
68
124
  };
69
125
 
70
126
  const startGateway = async () => {
@@ -85,6 +141,7 @@ const restartGateway = (reloadEnv) => {
85
141
  if (gatewayChild && gatewayChild.exitCode === null && !gatewayChild.killed) {
86
142
  console.log("[alphaclaw] Stopping managed gateway process...");
87
143
  try {
144
+ expectedExitPids.add(gatewayChild.pid);
88
145
  gatewayChild.kill("SIGTERM");
89
146
  gatewayChild = null;
90
147
  } catch (e) {
@@ -252,6 +309,9 @@ module.exports = {
252
309
  gatewayEnv,
253
310
  isOnboarded,
254
311
  isGatewayRunning,
312
+ launchGatewayProcess,
313
+ setGatewayExitHandler,
314
+ setGatewayLaunchHandler,
255
315
  runGatewayCmd,
256
316
  startGateway,
257
317
  restartGateway,
@@ -0,0 +1,102 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ let logPath = "";
5
+ let linesSinceSizeCheck = 0;
6
+ let lastSizeCheckAtMs = 0;
7
+
8
+ const kTruncateCheckEveryLines = 25;
9
+ const kTruncateCheckMinIntervalMs = 2000;
10
+
11
+ const shouldCheckTruncate = () => {
12
+ linesSinceSizeCheck += 1;
13
+ const now = Date.now();
14
+ if (
15
+ linesSinceSizeCheck >= kTruncateCheckEveryLines ||
16
+ now - lastSizeCheckAtMs >= kTruncateCheckMinIntervalMs
17
+ ) {
18
+ linesSinceSizeCheck = 0;
19
+ lastSizeCheckAtMs = now;
20
+ return true;
21
+ }
22
+ return false;
23
+ };
24
+
25
+ const appendLine = (line, maxBytes) => {
26
+ if (!logPath) return;
27
+ const prefixed = /^\d{4}-\d{2}-\d{2}T/.test(line)
28
+ ? line
29
+ : `${new Date().toISOString()} ${line}`;
30
+ fs.appendFileSync(logPath, prefixed.endsWith("\n") ? prefixed : `${prefixed}\n`);
31
+ if (shouldCheckTruncate()) truncateIfNeeded(maxBytes);
32
+ };
33
+
34
+ const truncateIfNeeded = (maxBytes) => {
35
+ try {
36
+ const stat = fs.statSync(logPath);
37
+ if (stat.size <= maxBytes) return;
38
+ const keepBytes = Math.floor(maxBytes / 2);
39
+ const fd = fs.openSync(logPath, "r");
40
+ const buffer = Buffer.alloc(keepBytes);
41
+ const startPos = Math.max(0, stat.size - keepBytes);
42
+ const bytesRead = fs.readSync(fd, buffer, 0, keepBytes, startPos);
43
+ fs.closeSync(fd);
44
+ const chunk = buffer.subarray(0, bytesRead).toString("utf8");
45
+ const firstNewLine = chunk.indexOf("\n");
46
+ const safeChunk = firstNewLine === -1 ? chunk : chunk.slice(firstNewLine + 1);
47
+ fs.writeFileSync(logPath, safeChunk, "utf8");
48
+ } catch (err) {
49
+ console.error(`[alphaclaw] log truncate error: ${err.message}`);
50
+ }
51
+ };
52
+
53
+ const initLogWriter = ({ rootDir, maxBytes }) => {
54
+ const logsDir = path.join(rootDir, "logs");
55
+ fs.mkdirSync(logsDir, { recursive: true });
56
+ logPath = path.join(logsDir, "process.log");
57
+ if (!fs.existsSync(logPath)) fs.writeFileSync(logPath, "", "utf8");
58
+ linesSinceSizeCheck = 0;
59
+ lastSizeCheckAtMs = Date.now();
60
+
61
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
62
+ const stderrWrite = process.stderr.write.bind(process.stderr);
63
+
64
+ process.stdout.write = (chunk, encoding, cb) => {
65
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? "");
66
+ for (const line of text.split("\n")) {
67
+ if (!line) continue;
68
+ appendLine(line, maxBytes);
69
+ }
70
+ return stdoutWrite(chunk, encoding, cb);
71
+ };
72
+
73
+ process.stderr.write = (chunk, encoding, cb) => {
74
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? "");
75
+ for (const line of text.split("\n")) {
76
+ if (!line) continue;
77
+ appendLine(line, maxBytes);
78
+ }
79
+ return stderrWrite(chunk, encoding, cb);
80
+ };
81
+ };
82
+
83
+ const getLogPath = () => logPath;
84
+
85
+ const readLogTail = (tailBytes = 65536) => {
86
+ if (!logPath || !fs.existsSync(logPath)) return "";
87
+ const stat = fs.statSync(logPath);
88
+ const readBytes = Math.max(1024, Number.parseInt(String(tailBytes || 65536), 10) || 65536);
89
+ const startPos = Math.max(0, stat.size - readBytes);
90
+ const len = stat.size - startPos;
91
+ const fd = fs.openSync(logPath, "r");
92
+ const buffer = Buffer.alloc(len);
93
+ fs.readSync(fd, buffer, 0, len, startPos);
94
+ fs.closeSync(fd);
95
+ return buffer.toString("utf8");
96
+ };
97
+
98
+ module.exports = {
99
+ initLogWriter,
100
+ getLogPath,
101
+ readLogTail,
102
+ };
@@ -30,6 +30,22 @@ const verifyGithubRepoForOnboarding = async ({ repoUrl, githubToken }) => {
30
30
  error: `Cannot verify GitHub token: ${details}`,
31
31
  };
32
32
  }
33
+ const oauthScopes = (userRes.headers?.get?.("x-oauth-scopes") || "")
34
+ .toLowerCase()
35
+ .split(",")
36
+ .map((s) => s.trim())
37
+ .filter(Boolean);
38
+ if (
39
+ oauthScopes.length > 0 &&
40
+ !oauthScopes.includes("repo") &&
41
+ !oauthScopes.includes("public_repo")
42
+ ) {
43
+ return {
44
+ ok: false,
45
+ status: 400,
46
+ error: `Your token needs the "repo" scope to create repositories. Current scopes: ${oauthScopes.join(", ")}`,
47
+ };
48
+ }
33
49
  const authedUser = await userRes.json().catch(() => ({}));
34
50
  const authedLogin = String(authedUser?.login || "").trim();
35
51
  if (
@@ -98,10 +114,14 @@ const ensureGithubRepoAccessible = async ({
98
114
  });
99
115
  if (!createRes.ok) {
100
116
  const details = await parseGithubErrorMessage(createRes);
117
+ const hint =
118
+ createRes.status === 404 || createRes.status === 403
119
+ ? ' Ensure your token is a classic PAT with the "repo" scope.'
120
+ : "";
101
121
  return {
102
122
  ok: false,
103
123
  status: 400,
104
- error: `Failed to create repo: ${details}`,
124
+ error: `Failed to create repo: ${details}.${hint}`,
105
125
  };
106
126
  }
107
127
  console.log(`[onboard] Repo ${repoUrl} created`);
@@ -57,7 +57,6 @@ const createOpenclawVersionService = ({
57
57
  };
58
58
  }
59
59
  try {
60
- console.log("[alphaclaw] Running: openclaw update status --json");
61
60
  const raw = execSync("openclaw update status --json", {
62
61
  env: gatewayEnv(),
63
62
  timeout: 8000,
@@ -74,13 +73,10 @@ const createOpenclawVersionService = ({
74
73
  hasUpdate,
75
74
  fetchedAt: now,
76
75
  };
77
- console.log(
78
- `[alphaclaw] openclaw update status: hasUpdate=${hasUpdate} latest=${latestVersion || "unknown"}`,
79
- );
80
76
  return { latestVersion, hasUpdate };
81
77
  } catch (err) {
82
- console.log(
83
- `[alphaclaw] openclaw update status error: ${(err.message || "unknown").slice(0, 200)}`,
78
+ console.error(
79
+ `[alphaclaw] openclaw update status error: ${err.message || "unknown error"}`,
84
80
  );
85
81
  throw new Error(err.message || "Failed to read OpenClaw update status");
86
82
  }
@@ -0,0 +1,86 @@
1
+ const createRestartRequiredState = ({ isGatewayRunning }) => {
2
+ const state = {
3
+ restartRequired: false,
4
+ restartInProgress: false,
5
+ sawGatewayDownSincePending: false,
6
+ updatedAt: Date.now(),
7
+ reason: "",
8
+ };
9
+
10
+ const touch = () => {
11
+ state.updatedAt = Date.now();
12
+ };
13
+
14
+ const markRequired = (reason = "config_changed") => {
15
+ state.restartRequired = true;
16
+ state.reason = reason;
17
+ state.sawGatewayDownSincePending = false;
18
+ touch();
19
+ };
20
+
21
+ const markRestartInProgress = () => {
22
+ state.restartInProgress = true;
23
+ touch();
24
+ };
25
+
26
+ const markRestartComplete = () => {
27
+ state.restartInProgress = false;
28
+ touch();
29
+ };
30
+
31
+ const clearRequired = () => {
32
+ state.restartRequired = false;
33
+ state.reason = "";
34
+ state.sawGatewayDownSincePending = false;
35
+ touch();
36
+ };
37
+
38
+ const checkAndClearIfRecovered = async () => {
39
+ const gatewayRunning = await isGatewayRunning();
40
+ if (state.restartRequired && !state.restartInProgress) {
41
+ if (!gatewayRunning) {
42
+ state.sawGatewayDownSincePending = true;
43
+ touch();
44
+ } else if (state.sawGatewayDownSincePending) {
45
+ clearRequired();
46
+ }
47
+ }
48
+ return gatewayRunning;
49
+ };
50
+
51
+ const getSnapshot = async () => {
52
+ const gatewayRunning = await checkAndClearIfRecovered();
53
+ return {
54
+ restartRequired: state.restartRequired,
55
+ restartInProgress: state.restartInProgress,
56
+ gatewayRunning,
57
+ updatedAt: state.updatedAt,
58
+ };
59
+ };
60
+
61
+ return {
62
+ markRequired,
63
+ markRestartInProgress,
64
+ markRestartComplete,
65
+ clearRequired,
66
+ getSnapshot,
67
+ };
68
+ };
69
+
70
+ const waitForGatewayRunning = async ({
71
+ isGatewayRunning,
72
+ timeoutMs = 25000,
73
+ intervalMs = 400,
74
+ }) => {
75
+ const deadline = Date.now() + timeoutMs;
76
+ while (Date.now() < deadline) {
77
+ if (await isGatewayRunning()) return true;
78
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
79
+ }
80
+ return isGatewayRunning();
81
+ };
82
+
83
+ module.exports = {
84
+ createRestartRequiredState,
85
+ waitForGatewayRunning,
86
+ };
@@ -7,7 +7,10 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
7
7
  const kSessionTtlMs = 7 * 24 * 60 * 60 * 1000;
8
8
 
9
9
  const signSessionPayload = (payload) =>
10
- crypto.createHmac("sha256", SETUP_PASSWORD).update(payload).digest("base64url");
10
+ crypto
11
+ .createHmac("sha256", SETUP_PASSWORD)
12
+ .update(payload)
13
+ .digest("base64url");
11
14
 
12
15
  const createSessionToken = () => {
13
16
  const now = Date.now();
@@ -34,7 +37,9 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
34
37
  if (expectedBuffer.length !== signatureBuffer.length) return false;
35
38
  if (!crypto.timingSafeEqual(expectedBuffer, signatureBuffer)) return false;
36
39
  try {
37
- const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
40
+ const parsed = JSON.parse(
41
+ Buffer.from(payload, "base64url").toString("utf8"),
42
+ );
38
43
  return Number.isFinite(parsed?.exp) && parsed.exp > Date.now();
39
44
  } catch {
40
45
  return false;
@@ -110,7 +115,7 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
110
115
 
111
116
  const requireAuth = (req, res, next) => {
112
117
  if (kAuthMisconfigured) {
113
- if (req.path.startsWith("/api/")) {
118
+ if (req.originalUrl.startsWith("/api/")) {
114
119
  return res.status(503).json({
115
120
  error:
116
121
  "Server misconfigured: SETUP_PASSWORD is missing. Set it in your deployment environment variables and restart.",
@@ -125,7 +130,7 @@ const registerAuthRoutes = ({ app, loginThrottle }) => {
125
130
  if (req.path.startsWith("/auth/google/callback")) return next();
126
131
  if (req.path.startsWith("/auth/codex/callback")) return next();
127
132
  if (isAuthorizedRequest(req)) return next();
128
- if (req.path.startsWith("/api/")) {
133
+ if (req.originalUrl.startsWith("/api/")) {
129
134
  return res.status(401).json({ error: "Unauthorized" });
130
135
  }
131
136
  return res.redirect("/login.html");