@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.
Files changed (65) 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/public/login.html +1 -0
  38. package/lib/public/setup.html +1 -0
  39. package/lib/server/alphaclaw-version.js +5 -3
  40. package/lib/server/constants.js +33 -0
  41. package/lib/server/discord-api.js +48 -0
  42. package/lib/server/gateway.js +64 -4
  43. package/lib/server/log-writer.js +102 -0
  44. package/lib/server/onboarding/github.js +21 -1
  45. package/lib/server/openclaw-version.js +2 -6
  46. package/lib/server/restart-required-state.js +86 -0
  47. package/lib/server/routes/auth.js +9 -4
  48. package/lib/server/routes/proxy.js +12 -14
  49. package/lib/server/routes/system.js +61 -15
  50. package/lib/server/routes/telegram.js +17 -48
  51. package/lib/server/routes/watchdog.js +68 -0
  52. package/lib/server/routes/webhooks.js +214 -0
  53. package/lib/server/telegram-api.js +11 -0
  54. package/lib/server/watchdog-db.js +148 -0
  55. package/lib/server/watchdog-notify.js +93 -0
  56. package/lib/server/watchdog.js +585 -0
  57. package/lib/server/webhook-middleware.js +195 -0
  58. package/lib/server/webhooks-db.js +265 -0
  59. package/lib/server/webhooks.js +238 -0
  60. package/lib/server.js +119 -4
  61. package/lib/setup/core-prompts/AGENTS.md +84 -0
  62. package/lib/setup/core-prompts/TOOLS.md +13 -0
  63. package/lib/setup/core-prompts/UI-DRY-OPPORTUNITIES.md +50 -0
  64. package/lib/setup/gitignore +2 -0
  65. package/package.json +2 -1
@@ -0,0 +1,148 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { DatabaseSync } = require("node:sqlite");
4
+
5
+ let db = null;
6
+ let pruneTimer = null;
7
+
8
+ const kDefaultLimit = 20;
9
+ const kMaxLimit = 200;
10
+ const kPruneIntervalMs = 12 * 60 * 60 * 1000;
11
+
12
+ const ensureDb = () => {
13
+ if (!db) throw new Error("Watchdog DB not initialized");
14
+ return db;
15
+ };
16
+
17
+ const createSchema = (database) => {
18
+ database.exec(`
19
+ CREATE TABLE IF NOT EXISTS watchdog_events (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ event_type TEXT NOT NULL,
22
+ source TEXT NOT NULL,
23
+ status TEXT NOT NULL,
24
+ details TEXT,
25
+ correlation_id TEXT,
26
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
27
+ );
28
+ `);
29
+ database.exec(`
30
+ CREATE INDEX IF NOT EXISTS idx_watchdog_events_ts
31
+ ON watchdog_events(created_at DESC);
32
+ `);
33
+ };
34
+
35
+ const initWatchdogDb = ({ rootDir, pruneDays = 30 }) => {
36
+ const dbDir = path.join(rootDir, "db");
37
+ fs.mkdirSync(dbDir, { recursive: true });
38
+ const dbPath = path.join(dbDir, "watchdog.db");
39
+ db = new DatabaseSync(dbPath);
40
+ createSchema(db);
41
+ pruneWatchdogEvents(pruneDays);
42
+ if (pruneTimer) clearInterval(pruneTimer);
43
+ pruneTimer = setInterval(() => {
44
+ try {
45
+ pruneWatchdogEvents(pruneDays);
46
+ } catch (err) {
47
+ console.error(`[watchdog-db] prune error: ${err.message}`);
48
+ }
49
+ }, kPruneIntervalMs);
50
+ if (typeof pruneTimer.unref === "function") pruneTimer.unref();
51
+ return { path: dbPath };
52
+ };
53
+
54
+ const insertWatchdogEvent = ({
55
+ eventType,
56
+ source,
57
+ status,
58
+ details = null,
59
+ correlationId = "",
60
+ }) => {
61
+ const database = ensureDb();
62
+ const stmt = database.prepare(`
63
+ INSERT INTO watchdog_events (
64
+ event_type,
65
+ source,
66
+ status,
67
+ details,
68
+ correlation_id
69
+ ) VALUES (
70
+ $event_type,
71
+ $source,
72
+ $status,
73
+ $details,
74
+ $correlation_id
75
+ )
76
+ `);
77
+ const result = stmt.run({
78
+ $event_type: String(eventType || ""),
79
+ $source: String(source || ""),
80
+ $status: String(status || "failed"),
81
+ $details:
82
+ details == null
83
+ ? null
84
+ : typeof details === "string"
85
+ ? details
86
+ : JSON.stringify(details),
87
+ $correlation_id: String(correlationId || ""),
88
+ });
89
+ return Number(result.lastInsertRowid || 0);
90
+ };
91
+
92
+ const getRecentEvents = ({ limit = kDefaultLimit, includeRoutine = false } = {}) => {
93
+ const database = ensureDb();
94
+ const safeLimit = Math.max(
95
+ 1,
96
+ Math.min(Number.parseInt(String(limit || kDefaultLimit), 10) || kDefaultLimit, kMaxLimit),
97
+ );
98
+ const whereClause = includeRoutine
99
+ ? ""
100
+ : "WHERE NOT (event_type = 'health_check' AND status = 'ok')";
101
+ const rows = database
102
+ .prepare(`
103
+ SELECT id, event_type, source, status, details, correlation_id, created_at
104
+ FROM watchdog_events
105
+ ${whereClause}
106
+ ORDER BY created_at DESC
107
+ LIMIT $limit
108
+ `)
109
+ .all({ $limit: safeLimit });
110
+ const mapped = rows.map((row) => {
111
+ let parsedDetails = row.details;
112
+ if (typeof row.details === "string" && row.details) {
113
+ try {
114
+ parsedDetails = JSON.parse(row.details);
115
+ } catch {}
116
+ }
117
+ return {
118
+ id: row.id,
119
+ eventType: row.event_type,
120
+ source: row.source,
121
+ status: row.status,
122
+ details: parsedDetails,
123
+ correlationId: row.correlation_id || "",
124
+ createdAt: row.created_at,
125
+ };
126
+ });
127
+ return mapped;
128
+ };
129
+
130
+ const pruneWatchdogEvents = (days = 30) => {
131
+ const database = ensureDb();
132
+ const safeDays = Math.max(1, Number.parseInt(String(days || 30), 10) || 30);
133
+ const modifier = `-${safeDays} days`;
134
+ const result = database
135
+ .prepare(`
136
+ DELETE FROM watchdog_events
137
+ WHERE created_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', $modifier)
138
+ `)
139
+ .run({ $modifier: modifier });
140
+ return Number(result.changes || 0);
141
+ };
142
+
143
+ module.exports = {
144
+ initWatchdogDb,
145
+ insertWatchdogEvent,
146
+ getRecentEvents,
147
+ pruneWatchdogEvents,
148
+ };
@@ -0,0 +1,93 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { OPENCLAW_DIR } = require("./constants");
4
+
5
+ const getPairedIds = (channel) => {
6
+ const safeChannel = String(channel || "").trim().toLowerCase();
7
+ if (!safeChannel) return [];
8
+ const credentialsDir = path.join(OPENCLAW_DIR, "credentials");
9
+ if (!fs.existsSync(credentialsDir)) return [];
10
+ const ids = new Set();
11
+ try {
12
+ const files = fs
13
+ .readdirSync(credentialsDir)
14
+ .filter(
15
+ (fileName) =>
16
+ fileName.startsWith(`${safeChannel}-`) && fileName.endsWith("-allowFrom.json"),
17
+ );
18
+ for (const fileName of files) {
19
+ const filePath = path.join(credentialsDir, fileName);
20
+ const raw = fs.readFileSync(filePath, "utf8");
21
+ const parsed = JSON.parse(raw);
22
+ const allowFrom = Array.isArray(parsed?.allowFrom) ? parsed.allowFrom : [];
23
+ for (const id of allowFrom) {
24
+ if (id == null) continue;
25
+ const value = String(id).trim();
26
+ if (!value) continue;
27
+ ids.add(value);
28
+ }
29
+ }
30
+ } catch (err) {
31
+ console.error(`[watchdog] could not resolve ${safeChannel} allowFrom IDs: ${err.message}`);
32
+ }
33
+ return Array.from(ids);
34
+ };
35
+
36
+ const formatDiscordMessage = (message) => String(message || "");
37
+
38
+ const createWatchdogNotifier = ({ telegramApi, discordApi }) => {
39
+ const notify = async (message) => {
40
+ const summary = {
41
+ telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
42
+ discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
43
+ };
44
+ const telegramTargets = getPairedIds("telegram");
45
+ summary.telegram.targets = telegramTargets.length;
46
+ if (!telegramApi?.sendMessage || !process.env.TELEGRAM_BOT_TOKEN || telegramTargets.length === 0) {
47
+ summary.telegram.skipped = true;
48
+ } else {
49
+ for (const chatId of telegramTargets) {
50
+ try {
51
+ await telegramApi.sendMessage(chatId, String(message || ""), {
52
+ parseMode: "Markdown",
53
+ });
54
+ summary.telegram.sent += 1;
55
+ } catch (err) {
56
+ summary.telegram.failed += 1;
57
+ console.error(`[watchdog] telegram notification failed for ${chatId}: ${err.message}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ const discordTargets = getPairedIds("discord");
63
+ summary.discord.targets = discordTargets.length;
64
+ if (!discordApi?.sendDirectMessage || !process.env.DISCORD_BOT_TOKEN || discordTargets.length === 0) {
65
+ summary.discord.skipped = true;
66
+ } else {
67
+ const discordMessage = formatDiscordMessage(message);
68
+ for (const userId of discordTargets) {
69
+ try {
70
+ await discordApi.sendDirectMessage(userId, discordMessage);
71
+ summary.discord.sent += 1;
72
+ } catch (err) {
73
+ summary.discord.failed += 1;
74
+ console.error(`[watchdog] discord notification failed for ${userId}: ${err.message}`);
75
+ }
76
+ }
77
+ }
78
+
79
+ const sent = summary.telegram.sent + summary.discord.sent;
80
+ const failed = summary.telegram.failed + summary.discord.failed;
81
+ return {
82
+ ok: sent > 0,
83
+ sent,
84
+ failed,
85
+ channels: summary,
86
+ ...(sent === 0 ? { reason: "no_channels_delivered" } : {}),
87
+ };
88
+ };
89
+
90
+ return { notify };
91
+ };
92
+
93
+ module.exports = { createWatchdogNotifier };