@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
@@ -1,26 +1,24 @@
1
- const registerProxyRoutes = ({ app, proxy, SETUP_API_PREFIXES, requireAuth }) => {
1
+ const registerProxyRoutes = ({
2
+ app,
3
+ proxy,
4
+ SETUP_API_PREFIXES,
5
+ requireAuth,
6
+ webhookMiddleware,
7
+ }) => {
2
8
  app.all("/openclaw", requireAuth, (req, res) => {
3
9
  req.url = "/";
4
10
  proxy.web(req, res);
5
11
  });
6
- app.all("/openclaw/*path", requireAuth, (req, res) => {
12
+ app.all("/openclaw/*", requireAuth, (req, res) => {
7
13
  req.url = req.url.replace(/^\/openclaw/, "");
8
14
  proxy.web(req, res);
9
15
  });
10
- app.all("/assets/*path", requireAuth, (req, res) => proxy.web(req, res));
16
+ app.all("/assets/*", requireAuth, (req, res) => proxy.web(req, res));
11
17
 
12
- app.all("/webhook/*path", (req, res) => {
13
- if (!req.headers.authorization && req.query.token) {
14
- req.headers.authorization = `Bearer ${req.query.token}`;
15
- delete req.query.token;
16
- const url = new URL(req.url, `http://${req.headers.host}`);
17
- url.searchParams.delete("token");
18
- req.url = url.pathname + url.search;
19
- }
20
- proxy.web(req, res);
21
- });
18
+ app.all("/hooks/*", webhookMiddleware);
19
+ app.all("/webhook/*", webhookMiddleware);
22
20
 
23
- app.all("/api/*path", (req, res) => {
21
+ app.all("/api/*", (req, res) => {
24
22
  if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
25
23
  proxy.web(req, res);
26
24
  });
@@ -15,7 +15,9 @@ const registerSystemRoutes = ({
15
15
  alphaclawVersionService,
16
16
  clawCmd,
17
17
  restartGateway,
18
+ onExpectedGatewayRestart,
18
19
  OPENCLAW_DIR,
20
+ restartRequiredState,
19
21
  }) => {
20
22
  let envRestartPending = false;
21
23
  const kEnvVarsReservedForUserInput = new Set([
@@ -30,8 +32,7 @@ const registerSystemRoutes = ({
30
32
  new Set([...kSystemVars, ...kEnvVarsReservedForUserInput]),
31
33
  );
32
34
  const isReservedUserEnvVar = (key) =>
33
- kSystemVars.has(key)
34
- || kEnvVarsReservedForUserInput.has(key);
35
+ kSystemVars.has(key) || kEnvVarsReservedForUserInput.has(key);
35
36
  const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
36
37
  const kSystemCronConfigPath = `${OPENCLAW_DIR}/cron/system-sync.json`;
37
38
  const kSystemCronScriptPath = `${OPENCLAW_DIR}/hourly-git-sync.sh`;
@@ -69,11 +70,18 @@ const registerSystemRoutes = ({
69
70
  };
70
71
  const applySystemCronConfig = (nextConfig) => {
71
72
  fs.mkdirSync(`${OPENCLAW_DIR}/cron`, { recursive: true });
72
- fs.writeFileSync(kSystemCronConfigPath, JSON.stringify(nextConfig, null, 2));
73
+ fs.writeFileSync(
74
+ kSystemCronConfigPath,
75
+ JSON.stringify(nextConfig, null, 2),
76
+ );
73
77
  if (nextConfig.enabled) {
74
- fs.writeFileSync(kSystemCronPath, buildSystemCronContent(nextConfig.schedule), {
75
- mode: 0o644,
76
- });
78
+ fs.writeFileSync(
79
+ kSystemCronPath,
80
+ buildSystemCronContent(nextConfig.schedule),
81
+ {
82
+ mode: 0o644,
83
+ },
84
+ );
77
85
  } else {
78
86
  fs.rmSync(kSystemCronPath, { force: true });
79
87
  }
@@ -140,7 +148,9 @@ const registerSystemRoutes = ({
140
148
  }
141
149
 
142
150
  const filtered = vars.filter((v) => !isReservedUserEnvVar(v.key));
143
- const existingLockedVars = readEnvFile().filter((v) => isReservedUserEnvVar(v.key));
151
+ const existingLockedVars = readEnvFile().filter((v) =>
152
+ isReservedUserEnvVar(v.key),
153
+ );
144
154
  const nextEnvVars = [...filtered, ...existingLockedVars];
145
155
  syncChannelConfig(nextEnvVars, "remove");
146
156
  writeEnvFile(nextEnvVars);
@@ -149,7 +159,9 @@ const registerSystemRoutes = ({
149
159
  envRestartPending = true;
150
160
  }
151
161
  const restartRequired = envRestartPending && isOnboarded();
152
- console.log(`[alphaclaw] Env vars saved (${nextEnvVars.length} vars, changed=${changed})`);
162
+ console.log(
163
+ `[alphaclaw] Env vars saved (${nextEnvVars.length} vars, changed=${changed})`,
164
+ );
153
165
  syncChannelConfig(nextEnvVars, "add");
154
166
 
155
167
  res.json({ ok: true, changed, restartRequired });
@@ -161,7 +173,11 @@ const registerSystemRoutes = ({
161
173
  const repo = process.env.GITHUB_WORKSPACE_REPO || "";
162
174
  const openclawVersion = openclawVersionService.readOpenclawVersion();
163
175
  res.json({
164
- gateway: running ? "running" : configExists ? "starting" : "not_onboarded",
176
+ gateway: running
177
+ ? "running"
178
+ : configExists
179
+ ? "starting"
180
+ : "not_onboarded",
165
181
  configExists,
166
182
  channels: getChannelStatus(),
167
183
  repo,
@@ -178,10 +194,14 @@ const registerSystemRoutes = ({
178
194
  const current = readSystemCronConfig();
179
195
  const { enabled, schedule } = req.body || {};
180
196
  if (enabled !== undefined && typeof enabled !== "boolean") {
181
- return res.status(400).json({ ok: false, error: "enabled must be a boolean" });
197
+ return res
198
+ .status(400)
199
+ .json({ ok: false, error: "enabled must be a boolean" });
182
200
  }
183
201
  if (schedule !== undefined && !isValidCronSchedule(schedule)) {
184
- return res.status(400).json({ ok: false, error: "schedule must be a 5-field cron string" });
202
+ return res
203
+ .status(400)
204
+ .json({ ok: false, error: "schedule must be a 5-field cron string" });
185
205
  }
186
206
  const nextConfig = {
187
207
  enabled: typeof enabled === "boolean" ? enabled : current.enabled,
@@ -246,13 +266,39 @@ const registerSystemRoutes = ({
246
266
  res.json({ ok: true, url: "/openclaw" });
247
267
  });
248
268
 
249
- app.post("/api/gateway/restart", (req, res) => {
269
+ app.get("/api/restart-status", async (req, res) => {
270
+ try {
271
+ const snapshot = await restartRequiredState.getSnapshot();
272
+ res.json({
273
+ ok: true,
274
+ restartRequired: snapshot.restartRequired || envRestartPending,
275
+ restartInProgress: snapshot.restartInProgress,
276
+ gatewayRunning: snapshot.gatewayRunning,
277
+ });
278
+ } catch (err) {
279
+ res.status(500).json({ ok: false, error: err.message });
280
+ }
281
+ });
282
+
283
+ app.post("/api/gateway/restart", async (req, res) => {
250
284
  if (!isOnboarded()) {
251
285
  return res.status(400).json({ ok: false, error: "Not onboarded" });
252
286
  }
253
- restartGateway();
254
- envRestartPending = false;
255
- res.json({ ok: true });
287
+ restartRequiredState.markRestartInProgress();
288
+ try {
289
+ if (typeof onExpectedGatewayRestart === "function") {
290
+ onExpectedGatewayRestart();
291
+ }
292
+ restartGateway();
293
+ envRestartPending = false;
294
+ restartRequiredState.clearRequired();
295
+ restartRequiredState.markRestartComplete();
296
+ const snapshot = await restartRequiredState.getSnapshot();
297
+ res.json({ ok: true, restartRequired: snapshot.restartRequired });
298
+ } catch (err) {
299
+ restartRequiredState.markRestartComplete();
300
+ res.status(500).json({ ok: false, error: err.message });
301
+ }
256
302
  });
257
303
  };
258
304
 
@@ -4,25 +4,6 @@ const { isDebugEnabled } = require("../helpers");
4
4
  const topicRegistry = require("../topic-registry");
5
5
  const { syncConfigForTelegram } = require("../telegram-workspace");
6
6
 
7
- const getRequestBody = (req) => (req.body && typeof req.body === "object" ? req.body : {});
8
- const getRequestQuery = (req) => (req.query && typeof req.query === "object" ? req.query : {});
9
- const parseJsonString = (value) => {
10
- if (typeof value !== "string" || !value.trim()) return null;
11
- try {
12
- return JSON.parse(value);
13
- } catch {
14
- return null;
15
- }
16
- };
17
- const getRequestPayload = (req) => {
18
- const body = getRequestBody(req);
19
- const query = getRequestQuery(req);
20
- const payloadFromQuery = parseJsonString(query.payload);
21
- if (payloadFromQuery && typeof payloadFromQuery === "object" && !Array.isArray(payloadFromQuery)) {
22
- return { ...payloadFromQuery, ...body };
23
- }
24
- return body;
25
- };
26
7
  const parseBooleanValue = (value, fallbackValue = false) => {
27
8
  if (typeof value === "boolean") return value;
28
9
  if (typeof value === "number") return value !== 0;
@@ -34,9 +15,8 @@ const parseBooleanValue = (value, fallbackValue = false) => {
34
15
  return fallbackValue;
35
16
  };
36
17
  const resolveGroupId = (req) => {
37
- const body = getRequestPayload(req);
38
- const query = getRequestQuery(req);
39
- const rawGroupId = body.groupId ?? body.chatId ?? query.groupId ?? query.chatId;
18
+ const body = req.body || {};
19
+ const rawGroupId = body.groupId ?? body.chatId;
40
20
  return rawGroupId == null ? "" : String(rawGroupId).trim();
41
21
  };
42
22
  const resolveAllowUserId = async ({ telegramApi, groupId, preferredUserId }) => {
@@ -121,12 +101,11 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
121
101
  // Create a topic via Telegram API + add to registry
122
102
  app.post("/api/telegram/groups/:groupId/topics", async (req, res) => {
123
103
  const { groupId } = req.params;
124
- const payload = getRequestPayload(req);
125
- const query = getRequestQuery(req);
126
- const name = String(payload.name ?? query.name ?? "").trim();
127
- const rawIconColor = payload.iconColor ?? query.iconColor;
104
+ const body = req.body || {};
105
+ const name = String(body.name ?? "").trim();
106
+ const rawIconColor = body.iconColor;
128
107
  const systemInstructions = String(
129
- payload.systemInstructions ?? payload.systemPrompt ?? query.systemInstructions ?? query.systemPrompt ?? "",
108
+ body.systemInstructions ?? body.systemPrompt ?? "",
130
109
  ).trim();
131
110
  const iconColorValue = rawIconColor == null ? null : Number.parseInt(String(rawIconColor), 10);
132
111
  const iconColor = Number.isFinite(iconColorValue) ? iconColorValue : undefined;
@@ -160,14 +139,8 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
160
139
  // Bulk-create topics
161
140
  app.post("/api/telegram/groups/:groupId/topics/bulk", async (req, res) => {
162
141
  const { groupId } = req.params;
163
- const payload = getRequestPayload(req);
164
- const query = getRequestQuery(req);
165
- const queryTopics = parseJsonString(query.topics);
166
- const topics = Array.isArray(payload.topics)
167
- ? payload.topics
168
- : Array.isArray(queryTopics)
169
- ? queryTopics
170
- : [];
142
+ const body = req.body || {};
143
+ const topics = Array.isArray(body.topics) ? body.topics : [];
171
144
  if (!Array.isArray(topics) || topics.length === 0) {
172
145
  return res.status(400).json({ ok: false, error: "topics array is required" });
173
146
  }
@@ -247,15 +220,12 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
247
220
  // Rename a topic
248
221
  app.put("/api/telegram/groups/:groupId/topics/:topicId", async (req, res) => {
249
222
  const { groupId, topicId } = req.params;
250
- const payload = getRequestPayload(req);
251
- const query = getRequestQuery(req);
252
- const name = String(payload.name ?? query.name ?? "").trim();
253
- const hasSystemInstructions = Object.prototype.hasOwnProperty.call(payload, "systemInstructions")
254
- || Object.prototype.hasOwnProperty.call(payload, "systemPrompt")
255
- || Object.prototype.hasOwnProperty.call(query, "systemInstructions")
256
- || Object.prototype.hasOwnProperty.call(query, "systemPrompt");
223
+ const body = req.body || {};
224
+ const name = String(body.name ?? "").trim();
225
+ const hasSystemInstructions = Object.prototype.hasOwnProperty.call(body, "systemInstructions")
226
+ || Object.prototype.hasOwnProperty.call(body, "systemPrompt");
257
227
  const systemInstructions = String(
258
- payload.systemInstructions ?? payload.systemPrompt ?? query.systemInstructions ?? query.systemPrompt ?? "",
228
+ body.systemInstructions ?? body.systemPrompt ?? "",
259
229
  ).trim();
260
230
  if (!name) return res.status(400).json({ ok: false, error: "name is required" });
261
231
  try {
@@ -302,11 +272,10 @@ const registerTelegramRoutes = ({ app, telegramApi, syncPromptFiles }) => {
302
272
  // Configure openclaw.json for a group
303
273
  app.post("/api/telegram/groups/:groupId/configure", async (req, res) => {
304
274
  const { groupId } = req.params;
305
- const payload = getRequestPayload(req);
306
- const query = getRequestQuery(req);
307
- const userId = payload.userId ?? query.userId ?? "";
308
- const groupName = payload.groupName ?? query.groupName ?? "";
309
- const requireMention = parseBooleanValue(payload.requireMention ?? query.requireMention, false);
275
+ const body = req.body || {};
276
+ const userId = body.userId ?? "";
277
+ const groupName = body.groupName ?? "";
278
+ const requireMention = parseBooleanValue(body.requireMention, false);
310
279
  try {
311
280
  const resolvedUserId = await resolveAllowUserId({
312
281
  telegramApi,
@@ -0,0 +1,68 @@
1
+ const registerWatchdogRoutes = ({
2
+ app,
3
+ requireAuth,
4
+ watchdog,
5
+ getRecentEvents,
6
+ readLogTail,
7
+ }) => {
8
+ app.get("/api/watchdog/status", requireAuth, (req, res) => {
9
+ try {
10
+ const status = watchdog.getStatus();
11
+ res.json({ ok: true, status });
12
+ } catch (err) {
13
+ res.status(500).json({ ok: false, error: err.message });
14
+ }
15
+ });
16
+
17
+ app.get("/api/watchdog/events", requireAuth, (req, res) => {
18
+ try {
19
+ const limit = Number.parseInt(String(req.query.limit || "20"), 10) || 20;
20
+ const includeRoutine =
21
+ String(req.query.includeRoutine || "").trim() === "1" ||
22
+ String(req.query.includeRoutine || "").trim().toLowerCase() === "true";
23
+ const events = getRecentEvents({ limit, includeRoutine });
24
+ res.json({ ok: true, events });
25
+ } catch (err) {
26
+ res.status(500).json({ ok: false, error: err.message });
27
+ }
28
+ });
29
+
30
+ app.get("/api/watchdog/logs", requireAuth, (req, res) => {
31
+ try {
32
+ const tail = Number.parseInt(String(req.query.tail || "65536"), 10) || 65536;
33
+ const logs = readLogTail(tail);
34
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
35
+ res.status(200).send(logs);
36
+ } catch (err) {
37
+ res.status(500).json({ ok: false, error: err.message });
38
+ }
39
+ });
40
+
41
+ app.post("/api/watchdog/repair", requireAuth, async (req, res) => {
42
+ try {
43
+ const result = await watchdog.triggerRepair();
44
+ res.json({ ok: !!result?.ok, result });
45
+ } catch (err) {
46
+ res.status(500).json({ ok: false, error: err.message });
47
+ }
48
+ });
49
+
50
+ app.get("/api/watchdog/settings", requireAuth, (req, res) => {
51
+ try {
52
+ res.json({ ok: true, settings: watchdog.getSettings() });
53
+ } catch (err) {
54
+ res.status(500).json({ ok: false, error: err.message });
55
+ }
56
+ });
57
+
58
+ app.put("/api/watchdog/settings", requireAuth, (req, res) => {
59
+ try {
60
+ const settings = watchdog.updateSettings(req.body || {});
61
+ res.json({ ok: true, settings });
62
+ } catch (err) {
63
+ res.status(400).json({ ok: false, error: err.message });
64
+ }
65
+ });
66
+ };
67
+
68
+ module.exports = { registerWatchdogRoutes };
@@ -0,0 +1,214 @@
1
+ const {
2
+ listWebhooks,
3
+ getWebhookDetail,
4
+ createWebhook,
5
+ deleteWebhook,
6
+ validateWebhookName,
7
+ } = require("../webhooks");
8
+
9
+ const isFiniteInteger = (value) => Number.isFinite(value) && Number.isInteger(value);
10
+ const parseBooleanFlag = (value) => {
11
+ const normalized = String(value == null ? "" : value).trim().toLowerCase();
12
+ return ["1", "true", "yes", "on"].includes(normalized);
13
+ };
14
+
15
+ const buildHealth = ({ totalCount, errorCount }) => {
16
+ if (!totalCount || totalCount <= 0) return "green";
17
+ if (!errorCount || errorCount <= 0) return "green";
18
+ if (errorCount >= totalCount) return "red";
19
+ return "yellow";
20
+ };
21
+
22
+ const mapSummaryByHook = (summaries) => {
23
+ const byHook = new Map();
24
+ for (const summary of summaries || []) byHook.set(summary.hookName, summary);
25
+ return byHook;
26
+ };
27
+
28
+ const mergeWebhookAndSummary = ({ webhook, summary }) => {
29
+ const totalCount = Number(summary?.totalCount || 0);
30
+ const errorCount = Number(summary?.errorCount || 0);
31
+ const successCount = Number(summary?.successCount || 0);
32
+ return {
33
+ ...webhook,
34
+ lastReceived: summary?.lastReceived || null,
35
+ totalCount,
36
+ successCount,
37
+ errorCount,
38
+ health: buildHealth({ totalCount, errorCount }),
39
+ };
40
+ };
41
+
42
+ const normalizeStatusFilter = (rawStatus) => {
43
+ const status = String(rawStatus || "all").trim().toLowerCase();
44
+ if (["all", "success", "error"].includes(status)) return status;
45
+ return "all";
46
+ };
47
+
48
+ const buildWebhookUrls = ({ baseUrl, name }) => {
49
+ const fullUrl = `${baseUrl}/hooks/${name}`;
50
+ const token = String(process.env.WEBHOOK_TOKEN || "").trim();
51
+ const queryStringUrl = token
52
+ ? `${fullUrl}?token=${encodeURIComponent(token)}`
53
+ : `${fullUrl}?token=<WEBHOOK_TOKEN>`;
54
+ const authHeaderValue = token
55
+ ? `Authorization: Bearer ${token}`
56
+ : "Authorization: Bearer <WEBHOOK_TOKEN>";
57
+ return { fullUrl, queryStringUrl, authHeaderValue, hasRuntimeToken: !!token };
58
+ };
59
+
60
+ const registerWebhookRoutes = ({
61
+ app,
62
+ fs,
63
+ constants,
64
+ getBaseUrl,
65
+ webhooksDb,
66
+ shellCmd,
67
+ restartRequiredState,
68
+ }) => {
69
+ const fallbackRestartState = {
70
+ markRequired: () => {},
71
+ getSnapshot: async () => ({ restartRequired: false }),
72
+ };
73
+ const resolvedRestartState = restartRequiredState || fallbackRestartState;
74
+ const { markRequired: markRestartRequired, getSnapshot: getRestartSnapshot } = resolvedRestartState;
75
+ const runWebhookGitSync = async (action, name) => {
76
+ if (typeof shellCmd !== "function") return null;
77
+ const safeName = String(name || "").trim();
78
+ const message = `webhooks: ${action} ${safeName}`.replace(/"/g, "");
79
+ try {
80
+ await shellCmd(`alphaclaw git-sync -m "${message}"`, {
81
+ timeout: 30000,
82
+ });
83
+ return null;
84
+ } catch (err) {
85
+ return err?.message || "alphaclaw git-sync failed";
86
+ }
87
+ };
88
+
89
+ app.get("/api/webhooks", (req, res) => {
90
+ try {
91
+ const hooks = listWebhooks({ fs, constants });
92
+ const summaries = webhooksDb.getHookSummaries();
93
+ const summaryByHook = mapSummaryByHook(summaries);
94
+ const webhooks = hooks.map((webhook) =>
95
+ mergeWebhookAndSummary({
96
+ webhook,
97
+ summary: summaryByHook.get(webhook.name),
98
+ }));
99
+ res.json({ ok: true, webhooks });
100
+ } catch (err) {
101
+ res.status(500).json({ ok: false, error: err.message });
102
+ }
103
+ });
104
+
105
+ app.get("/api/webhooks/:name", (req, res) => {
106
+ try {
107
+ const name = validateWebhookName(req.params.name);
108
+ const detail = getWebhookDetail({ fs, constants, name });
109
+ if (!detail) return res.status(404).json({ ok: false, error: "Webhook not found" });
110
+ const summary = webhooksDb.getHookSummaries().find((item) => item.hookName === name);
111
+ const merged = mergeWebhookAndSummary({ webhook: detail, summary });
112
+ const baseUrl = getBaseUrl(req);
113
+ const urls = buildWebhookUrls({ baseUrl, name });
114
+ return res.json({
115
+ ok: true,
116
+ webhook: {
117
+ ...merged,
118
+ fullUrl: urls.fullUrl,
119
+ queryStringUrl: urls.queryStringUrl,
120
+ authHeaderValue: urls.authHeaderValue,
121
+ hasRuntimeToken: urls.hasRuntimeToken,
122
+ authNote:
123
+ "All hooks use WEBHOOK_TOKEN. Use Authorization: Bearer <token> or x-openclaw-token header.",
124
+ },
125
+ });
126
+ } catch (err) {
127
+ return res.status(400).json({ ok: false, error: err.message });
128
+ }
129
+ });
130
+
131
+ app.post("/api/webhooks", async (req, res) => {
132
+ try {
133
+ const { name: rawName } = req.body || {};
134
+ const name = validateWebhookName(rawName);
135
+ const webhook = createWebhook({ fs, constants, name });
136
+ const baseUrl = getBaseUrl(req);
137
+ const urls = buildWebhookUrls({ baseUrl, name });
138
+ const syncWarning = await runWebhookGitSync("create", name);
139
+ markRestartRequired("webhooks");
140
+ const snapshot = await getRestartSnapshot();
141
+ return res.status(201).json({
142
+ ok: true,
143
+ webhook: {
144
+ ...webhook,
145
+ fullUrl: urls.fullUrl,
146
+ queryStringUrl: urls.queryStringUrl,
147
+ authHeaderValue: urls.authHeaderValue,
148
+ hasRuntimeToken: urls.hasRuntimeToken,
149
+ },
150
+ restartRequired: snapshot.restartRequired,
151
+ syncWarning,
152
+ });
153
+ } catch (err) {
154
+ const status = String(err.message || "").includes("already exists") ? 409 : 400;
155
+ return res.status(status).json({ ok: false, error: err.message });
156
+ }
157
+ });
158
+
159
+ app.delete("/api/webhooks/:name", async (req, res) => {
160
+ try {
161
+ const name = validateWebhookName(req.params.name);
162
+ const deleteTransformDir = parseBooleanFlag(req?.body?.deleteTransformDir);
163
+ const deletion = deleteWebhook({ fs, constants, name, deleteTransformDir });
164
+ if (!deletion?.removed) return res.status(404).json({ ok: false, error: "Webhook not found" });
165
+ const deletedRequestCount = webhooksDb.deleteRequestsByHook(name);
166
+ const syncWarning = await runWebhookGitSync("delete", name);
167
+ markRestartRequired("webhooks");
168
+ const snapshot = await getRestartSnapshot();
169
+ return res.json({
170
+ ok: true,
171
+ restartRequired: snapshot.restartRequired,
172
+ syncWarning,
173
+ deletedRequestCount,
174
+ deletedTransformDir: !!deletion.deletedTransformDir,
175
+ });
176
+ } catch (err) {
177
+ return res.status(400).json({ ok: false, error: err.message });
178
+ }
179
+ });
180
+
181
+ app.get("/api/webhooks/:name/requests", (req, res) => {
182
+ try {
183
+ const name = validateWebhookName(req.params.name);
184
+ const limit = Number.parseInt(String(req.query.limit || 50), 10);
185
+ const offset = Number.parseInt(String(req.query.offset || 0), 10);
186
+ const status = normalizeStatusFilter(req.query.status);
187
+ const hasBadPaging = !isFiniteInteger(limit) || limit <= 0 || !isFiniteInteger(offset) || offset < 0;
188
+ if (hasBadPaging) {
189
+ return res.status(400).json({ ok: false, error: "Invalid limit/offset" });
190
+ }
191
+ const requests = webhooksDb.getRequests(name, { limit, offset, status });
192
+ return res.json({ ok: true, requests });
193
+ } catch (err) {
194
+ return res.status(400).json({ ok: false, error: err.message });
195
+ }
196
+ });
197
+
198
+ app.get("/api/webhooks/:name/requests/:id", (req, res) => {
199
+ try {
200
+ const name = validateWebhookName(req.params.name);
201
+ const requestId = Number.parseInt(String(req.params.id || 0), 10);
202
+ if (!isFiniteInteger(requestId) || requestId <= 0) {
203
+ return res.status(400).json({ ok: false, error: "Invalid request id" });
204
+ }
205
+ const request = webhooksDb.getRequestById(name, requestId);
206
+ if (!request) return res.status(404).json({ ok: false, error: "Request not found" });
207
+ return res.json({ ok: true, request });
208
+ } catch (err) {
209
+ return res.status(400).json({ ok: false, error: err.message });
210
+ }
211
+ });
212
+ };
213
+
214
+ module.exports = { registerWebhookRoutes };
@@ -51,6 +51,16 @@ const createTelegramApi = (getToken) => {
51
51
  ...(opts.iconCustomEmojiId && { icon_custom_emoji_id: opts.iconCustomEmojiId }),
52
52
  });
53
53
 
54
+ const sendMessage = (chatId, text, opts = {}) =>
55
+ call("sendMessage", {
56
+ chat_id: chatId,
57
+ text: String(text || ""),
58
+ ...(opts.parseMode && { parse_mode: opts.parseMode }),
59
+ ...(opts.disableWebPagePreview && {
60
+ disable_web_page_preview: !!opts.disableWebPagePreview,
61
+ }),
62
+ });
63
+
54
64
  return {
55
65
  getMe,
56
66
  getChat,
@@ -59,6 +69,7 @@ const createTelegramApi = (getToken) => {
59
69
  createForumTopic,
60
70
  deleteForumTopic,
61
71
  editForumTopic,
72
+ sendMessage,
62
73
  };
63
74
  };
64
75