@chrysb/alphaclaw 0.2.2 → 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.
- package/bin/alphaclaw.js +79 -0
- package/lib/public/css/shell.css +57 -2
- package/lib/public/css/theme.css +184 -0
- package/lib/public/js/app.js +330 -89
- package/lib/public/js/components/action-button.js +92 -0
- package/lib/public/js/components/channels.js +16 -7
- package/lib/public/js/components/confirm-dialog.js +25 -19
- package/lib/public/js/components/credentials-modal.js +32 -23
- package/lib/public/js/components/device-pairings.js +15 -2
- package/lib/public/js/components/envars.js +22 -65
- package/lib/public/js/components/features.js +1 -1
- package/lib/public/js/components/gateway.js +139 -32
- package/lib/public/js/components/global-restart-banner.js +31 -0
- package/lib/public/js/components/google.js +9 -9
- package/lib/public/js/components/icons.js +19 -0
- package/lib/public/js/components/info-tooltip.js +18 -0
- package/lib/public/js/components/loading-spinner.js +32 -0
- package/lib/public/js/components/modal-shell.js +42 -0
- package/lib/public/js/components/models.js +34 -29
- package/lib/public/js/components/onboarding/welcome-form-step.js +45 -32
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +7 -24
- package/lib/public/js/components/page-header.js +13 -0
- package/lib/public/js/components/pairings.js +15 -2
- package/lib/public/js/components/providers.js +216 -142
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/secret-input.js +1 -0
- package/lib/public/js/components/telegram-workspace.js +37 -49
- package/lib/public/js/components/toast.js +34 -5
- package/lib/public/js/components/toggle-switch.js +25 -0
- package/lib/public/js/components/update-action-button.js +13 -53
- package/lib/public/js/components/watchdog-tab.js +312 -0
- package/lib/public/js/components/webhooks.js +981 -0
- package/lib/public/js/components/welcome.js +2 -1
- package/lib/public/js/lib/api.js +102 -1
- package/lib/public/js/lib/model-config.js +0 -5
- package/lib/public/login.html +1 -0
- package/lib/public/setup.html +1 -0
- package/lib/server/alphaclaw-version.js +5 -3
- package/lib/server/constants.js +33 -0
- package/lib/server/discord-api.js +48 -0
- package/lib/server/gateway.js +64 -4
- package/lib/server/log-writer.js +102 -0
- package/lib/server/onboarding/github.js +21 -1
- package/lib/server/openclaw-version.js +2 -6
- package/lib/server/restart-required-state.js +86 -0
- package/lib/server/routes/auth.js +9 -4
- package/lib/server/routes/proxy.js +12 -14
- package/lib/server/routes/system.js +61 -15
- package/lib/server/routes/telegram.js +17 -48
- package/lib/server/routes/watchdog.js +68 -0
- package/lib/server/routes/webhooks.js +214 -0
- package/lib/server/telegram-api.js +11 -0
- package/lib/server/watchdog-db.js +148 -0
- package/lib/server/watchdog-notify.js +93 -0
- package/lib/server/watchdog.js +585 -0
- package/lib/server/webhook-middleware.js +195 -0
- package/lib/server/webhooks-db.js +265 -0
- package/lib/server/webhooks.js +238 -0
- package/lib/server.js +119 -4
- package/lib/setup/core-prompts/AGENTS.md +84 -0
- package/lib/setup/core-prompts/TOOLS.md +13 -0
- package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
- package/lib/setup/gitignore +2 -0
- 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,
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -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
|
|
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) =>
|
package/lib/public/login.html
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap"
|
|
9
9
|
rel="stylesheet"
|
|
10
10
|
/>
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="./img/logo.svg" />
|
|
11
12
|
<link rel="stylesheet" href="./css/theme.css" />
|
|
12
13
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
13
14
|
<script>
|
package/lib/public/setup.html
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>alphaclaw</title>
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="./img/logo.svg">
|
|
8
9
|
<link rel="stylesheet" href="./css/theme.css">
|
|
9
10
|
<link rel="stylesheet" href="./css/shell.css">
|
|
10
11
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
@@ -88,9 +88,11 @@ const createAlphaclawVersionService = () => {
|
|
|
88
88
|
latestVersion !== currentVersion
|
|
89
89
|
);
|
|
90
90
|
kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
package/lib/server/constants.js
CHANGED
|
@@ -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 };
|
package/lib/server/gateway.js
CHANGED
|
@@ -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) =>
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
83
|
-
`[alphaclaw] openclaw update status error: ${
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
133
|
+
if (req.originalUrl.startsWith("/api/")) {
|
|
129
134
|
return res.status(401).json({ error: "Unauthorized" });
|
|
130
135
|
}
|
|
131
136
|
return res.redirect("/login.html");
|