@chrysb/alphaclaw 0.8.3-beta.5 → 0.8.4-beta.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.
@@ -1,7 +1,10 @@
1
- const path = require("path");
2
1
  const crypto = require("crypto");
3
2
  const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
4
3
  const { quoteShellArg } = require("../utils/shell");
4
+ const {
5
+ readExecApprovalsConfig,
6
+ writeExecApprovalsConfig,
7
+ } = require("../exec-defaults-config");
5
8
 
6
9
  const kAllowedExecHosts = new Set(["gateway", "node"]);
7
10
  const kAllowedExecSecurity = new Set(["deny", "allowlist", "full"]);
@@ -81,23 +84,6 @@ const parseNodeBrowserStatus = (stdout) => {
81
84
  return decodedResult && typeof decodedResult === "object" ? decodedResult : null;
82
85
  };
83
86
 
84
- const readExecApprovalsFile = ({ fsModule, openclawDir }) => {
85
- const filePath = path.join(openclawDir, "exec-approvals.json");
86
- try {
87
- const raw = fsModule.readFileSync(filePath, "utf8");
88
- const parsed = JSON.parse(raw);
89
- return parsed && typeof parsed === "object" ? parsed : { version: 1 };
90
- } catch {
91
- return { version: 1 };
92
- }
93
- };
94
-
95
- const writeExecApprovalsFile = ({ fsModule, openclawDir, file }) => {
96
- const filePath = path.join(openclawDir, "exec-approvals.json");
97
- fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
98
- fsModule.writeFileSync(filePath, JSON.stringify(file, null, 2) + "\n", "utf8");
99
- };
100
-
101
87
  const ensureWildcardAgent = (file) => {
102
88
  const agents = file.agents && typeof file.agents === "object" ? file.agents : {};
103
89
  const wildcard =
@@ -372,7 +358,7 @@ const registerNodeRoutes = ({
372
358
 
373
359
  app.get("/api/nodes/exec-approvals", (_req, res) => {
374
360
  const approvals = ensureWildcardAgent(
375
- readExecApprovalsFile({ fsModule, openclawDir }),
361
+ readExecApprovalsConfig({ fsModule, openclawDir }),
376
362
  );
377
363
  const allowlist = approvals?.agents?.["*"]?.allowlist || [];
378
364
  return res.json({
@@ -388,7 +374,7 @@ const registerNodeRoutes = ({
388
374
  return res.status(400).json({ ok: false, error: "pattern is required" });
389
375
  }
390
376
  const approvals = ensureWildcardAgent(
391
- readExecApprovalsFile({ fsModule, openclawDir }),
377
+ readExecApprovalsConfig({ fsModule, openclawDir }),
392
378
  );
393
379
  const allowlist = approvals.agents["*"].allowlist;
394
380
  const existing = allowlist.find(
@@ -403,7 +389,7 @@ const registerNodeRoutes = ({
403
389
  lastUsedAt: Date.now(),
404
390
  };
405
391
  approvals.agents["*"].allowlist = [...allowlist, entry];
406
- writeExecApprovalsFile({ fsModule, openclawDir, file: approvals });
392
+ writeExecApprovalsConfig({ fsModule, openclawDir, file: approvals });
407
393
  return res.json({ ok: true, entry });
408
394
  });
409
395
 
@@ -413,7 +399,7 @@ const registerNodeRoutes = ({
413
399
  return res.status(400).json({ ok: false, error: "id is required" });
414
400
  }
415
401
  const approvals = ensureWildcardAgent(
416
- readExecApprovalsFile({ fsModule, openclawDir }),
402
+ readExecApprovalsConfig({ fsModule, openclawDir }),
417
403
  );
418
404
  const allowlist = approvals.agents["*"].allowlist;
419
405
  const nextAllowlist = allowlist.filter((entry) => String(entry?.id || "") !== id);
@@ -421,7 +407,7 @@ const registerNodeRoutes = ({
421
407
  return res.status(404).json({ ok: false, error: "Allowlist entry not found" });
422
408
  }
423
409
  approvals.agents["*"].allowlist = nextAllowlist;
424
- writeExecApprovalsFile({ fsModule, openclawDir, file: approvals });
410
+ writeExecApprovalsConfig({ fsModule, openclawDir, file: approvals });
425
411
  return res.json({ ok: true });
426
412
  });
427
413
  };
@@ -233,6 +233,72 @@ const createSlackApi = (getToken) => {
233
233
  });
234
234
  };
235
235
 
236
+ /**
237
+ * Update an existing message
238
+ * @param {string} channel - Channel ID
239
+ * @param {string} timestamp - Message timestamp
240
+ * @param {string} text - New message text
241
+ * @returns {Promise<object>} Update response
242
+ * @requires chat:write OAuth scope
243
+ */
244
+ const updateMessage = (channel, timestamp, text) => {
245
+ return call("chat.update", {
246
+ channel,
247
+ ts: timestamp,
248
+ text: String(text || ""),
249
+ });
250
+ };
251
+
252
+ /**
253
+ * Delete a message
254
+ * @param {string} channel - Channel ID
255
+ * @param {string} timestamp - Message timestamp
256
+ * @requires chat:write OAuth scope
257
+ */
258
+ const deleteMessage = (channel, timestamp) => {
259
+ return call("chat.delete", { channel, ts: timestamp });
260
+ };
261
+
262
+ /**
263
+ * Pin a message to a channel
264
+ * @param {string} channel - Channel ID
265
+ * @param {string} timestamp - Message timestamp
266
+ * @requires pins:write OAuth scope
267
+ */
268
+ const pinMessage = (channel, timestamp) => {
269
+ return call("pins.add", { channel, timestamp });
270
+ };
271
+
272
+ /**
273
+ * Unpin a message from a channel
274
+ * @param {string} channel - Channel ID
275
+ * @param {string} timestamp - Message timestamp
276
+ * @requires pins:write OAuth scope
277
+ */
278
+ const unpinMessage = (channel, timestamp) => {
279
+ return call("pins.remove", { channel, timestamp });
280
+ };
281
+
282
+ /**
283
+ * Get user information
284
+ * @param {string} userId - User ID
285
+ * @returns {Promise<object>} User info (name, real_name, email, etc.)
286
+ * @requires users:read OAuth scope
287
+ */
288
+ const getUserInfo = (userId) => {
289
+ return call("users.info", { user: userId });
290
+ };
291
+
292
+ /**
293
+ * Get channel information
294
+ * @param {string} channelId - Channel ID
295
+ * @returns {Promise<object>} Channel info (name, topic, purpose, etc.)
296
+ * @requires channels:read or groups:read OAuth scope (depending on channel type)
297
+ */
298
+ const getChannelInfo = (channelId) => {
299
+ return call("conversations.info", { channel: channelId });
300
+ };
301
+
236
302
  return {
237
303
  authTest,
238
304
  postMessage,
@@ -241,6 +307,12 @@ const createSlackApi = (getToken) => {
241
307
  removeReaction,
242
308
  uploadFile,
243
309
  uploadTextSnippet,
310
+ updateMessage,
311
+ deleteMessage,
312
+ pinMessage,
313
+ unpinMessage,
314
+ getUserInfo,
315
+ getChannelInfo,
244
316
  };
245
317
  };
246
318
 
@@ -1,4 +1,5 @@
1
1
  const runOnboardedBootSequence = ({
2
+ ensureManagedExecDefaults,
2
3
  ensureUsageTrackerPluginConfig,
3
4
  doSyncPromptFiles,
4
5
  reloadEnv,
@@ -10,6 +11,13 @@ const runOnboardedBootSequence = ({
10
11
  watchdog,
11
12
  gmailWatchService,
12
13
  }) => {
14
+ try {
15
+ ensureManagedExecDefaults();
16
+ } catch (error) {
17
+ console.error(
18
+ `[alphaclaw] Failed to ensure managed exec defaults on boot: ${error.message}`,
19
+ );
20
+ }
13
21
  try {
14
22
  ensureUsageTrackerPluginConfig();
15
23
  } catch (error) {
@@ -1,35 +1,95 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { OPENCLAW_DIR } = require("./constants");
4
+ const { createSlackApi } = require("./slack-api");
4
5
 
5
- const getPairedIds = (channel) => {
6
+ const kSlackBotEnvKey = "SLACK_BOT_TOKEN";
7
+
8
+ const normalizeAccountId = (value) =>
9
+ String(value || "").trim().toLowerCase() || "default";
10
+
11
+ const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
12
+ const prefix = `${String(channel || "").trim().toLowerCase()}-`;
13
+ const suffix = "-allowFrom.json";
14
+ const rawFileName = String(fileName || "").trim();
15
+ if (!rawFileName.startsWith(prefix) || !rawFileName.endsWith(suffix)) {
16
+ return "";
17
+ }
18
+ return normalizeAccountId(rawFileName.slice(prefix.length, -suffix.length));
19
+ };
20
+
21
+ const deriveSlackBotEnvKey = (accountId = "default") => {
22
+ const normalizedAccountId = normalizeAccountId(accountId);
23
+ if (normalizedAccountId === "default") return kSlackBotEnvKey;
24
+ return `${kSlackBotEnvKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
25
+ };
26
+
27
+ const getPairedTargetsByAccount = ({
28
+ channel,
29
+ fsImpl = fs,
30
+ openclawDir = OPENCLAW_DIR,
31
+ }) => {
6
32
  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();
33
+ if (!safeChannel) return new Map();
34
+ const credentialsDir = path.join(openclawDir, "credentials");
35
+ if (!fsImpl.existsSync(credentialsDir)) return new Map();
36
+ const idsByAccount = new Map();
11
37
  try {
12
- const files = fs
38
+ const files = fsImpl
13
39
  .readdirSync(credentialsDir)
14
40
  .filter(
15
41
  (fileName) =>
16
42
  fileName.startsWith(`${safeChannel}-`) && fileName.endsWith("-allowFrom.json"),
17
43
  );
18
44
  for (const fileName of files) {
45
+ const accountId = resolveCredentialPairingAccountId({
46
+ channel: safeChannel,
47
+ fileName,
48
+ });
49
+ if (!accountId) continue;
19
50
  const filePath = path.join(credentialsDir, fileName);
20
- const raw = fs.readFileSync(filePath, "utf8");
51
+ const raw = fsImpl.readFileSync(filePath, "utf8");
21
52
  const parsed = JSON.parse(raw);
22
53
  const allowFrom = Array.isArray(parsed?.allowFrom) ? parsed.allowFrom : [];
54
+ const ids =
55
+ idsByAccount.get(accountId) instanceof Set
56
+ ? idsByAccount.get(accountId)
57
+ : new Set();
23
58
  for (const id of allowFrom) {
24
59
  if (id == null) continue;
25
60
  const value = String(id).trim();
26
61
  if (!value) continue;
27
62
  ids.add(value);
28
63
  }
64
+ idsByAccount.set(accountId, ids);
29
65
  }
30
66
  } catch (err) {
31
67
  console.error(`[watchdog] could not resolve ${safeChannel} allowFrom IDs: ${err.message}`);
32
68
  }
69
+ return new Map(
70
+ Array.from(idsByAccount.entries()).map(([accountId, ids]) => [
71
+ accountId,
72
+ Array.from(ids),
73
+ ]),
74
+ );
75
+ };
76
+
77
+ const getPairedIds = ({
78
+ channel,
79
+ fsImpl = fs,
80
+ openclawDir = OPENCLAW_DIR,
81
+ }) => {
82
+ const ids = new Set();
83
+ const idsByAccount = getPairedTargetsByAccount({
84
+ channel,
85
+ fsImpl,
86
+ openclawDir,
87
+ });
88
+ for (const accountIds of idsByAccount.values()) {
89
+ for (const id of accountIds) {
90
+ ids.add(id);
91
+ }
92
+ }
33
93
  return Array.from(ids);
34
94
  };
35
95
 
@@ -38,18 +98,30 @@ const formatDiscordMessage = (message) =>
38
98
 
39
99
  /**
40
100
  * Track thread state for Slack notifications
41
- * Key: userId, Value: { threadTs, lastEvent }
101
+ * Key: accountId:userId, Value: { threadTs, lastEvent }
42
102
  */
43
103
  const slackThreads = new Map();
44
104
 
45
- const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
105
+ const createWatchdogNotifier = ({
106
+ telegramApi,
107
+ discordApi,
108
+ slackApi,
109
+ readEnvFile = () => [],
110
+ createSlackApi: createSlackApiFactory = createSlackApi,
111
+ fsImpl = fs,
112
+ openclawDir = OPENCLAW_DIR,
113
+ }) => {
46
114
  const notify = async (message, opts = {}) => {
47
115
  const summary = {
48
116
  telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
49
117
  discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
50
118
  slack: { sent: 0, failed: 0, skipped: false, targets: 0 },
51
119
  };
52
- const telegramTargets = getPairedIds("telegram");
120
+ const telegramTargets = getPairedIds({
121
+ channel: "telegram",
122
+ fsImpl,
123
+ openclawDir,
124
+ });
53
125
  summary.telegram.targets = telegramTargets.length;
54
126
  if (!telegramApi?.sendMessage || !process.env.TELEGRAM_BOT_TOKEN || telegramTargets.length === 0) {
55
127
  summary.telegram.skipped = true;
@@ -67,7 +139,11 @@ const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
67
139
  }
68
140
  }
69
141
 
70
- const discordTargets = getPairedIds("discord");
142
+ const discordTargets = getPairedIds({
143
+ channel: "discord",
144
+ fsImpl,
145
+ openclawDir,
146
+ });
71
147
  summary.discord.targets = discordTargets.length;
72
148
  if (!discordApi?.sendDirectMessage || !process.env.DISCORD_BOT_TOKEN || discordTargets.length === 0) {
73
149
  summary.discord.skipped = true;
@@ -85,61 +161,102 @@ const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
85
161
  }
86
162
 
87
163
  // Enhanced Slack notifications with threading and reactions
88
- const slackTargets = getPairedIds("slack");
89
- summary.slack.targets = slackTargets.length;
90
- if (!slackApi?.postMessage || !process.env.SLACK_BOT_TOKEN || slackTargets.length === 0) {
164
+ const slackTargetsByAccount = getPairedTargetsByAccount({
165
+ channel: "slack",
166
+ fsImpl,
167
+ openclawDir,
168
+ });
169
+ summary.slack.targets = Array.from(slackTargetsByAccount.values()).reduce(
170
+ (total, targets) => total + targets.length,
171
+ 0,
172
+ );
173
+ if (summary.slack.targets === 0) {
91
174
  summary.slack.skipped = true;
92
175
  } else {
93
176
  const eventType = opts.eventType || "info"; // crash, recovery, health, info
94
-
95
- for (const userId of slackTargets) {
96
- try {
97
- let threadTs = null;
98
- let shouldCreateNewThread = true;
99
-
100
- // Check if we have an active thread for this user
101
- const existingThread = slackThreads.get(userId);
102
- if (existingThread && existingThread.lastEvent === "crash" && eventType === "recovery") {
103
- // Recovery message goes in the crash thread
104
- threadTs = existingThread.threadTs;
105
- shouldCreateNewThread = false;
177
+ const envVars = typeof readEnvFile === "function" ? readEnvFile() : [];
178
+
179
+ const envMap = new Map(
180
+ (Array.isArray(envVars) ? envVars : [])
181
+ .map((entry) => [
182
+ String(entry?.key || "").trim(),
183
+ String(entry?.value || "").trim(),
184
+ ])
185
+ .filter(([key]) => key),
186
+ );
187
+
188
+ for (const [accountId, slackTargets] of slackTargetsByAccount.entries()) {
189
+ if (!slackTargets.length) continue;
190
+ const envKey = deriveSlackBotEnvKey(accountId);
191
+ const botToken = String(envMap.get(envKey) || process.env[envKey] || "").trim();
192
+ if (!botToken) {
193
+ summary.slack.failed += slackTargets.length;
194
+ for (const userId of slackTargets) {
195
+ console.error(
196
+ `[watchdog] slack notification failed for ${accountId}/${userId}: missing ${envKey}`,
197
+ );
106
198
  }
199
+ continue;
200
+ }
107
201
 
108
- // Send message (in thread if continuing conversation)
109
- const result = await slackApi.postMessage(userId, String(message || ""), {
110
- thread_ts: threadTs,
111
- mrkdwn: true,
112
- });
202
+ const accountSlackApi =
203
+ accountId === "default" &&
204
+ slackApi?.postMessage &&
205
+ botToken === String(process.env.SLACK_BOT_TOKEN || "").trim()
206
+ ? slackApi
207
+ : createSlackApiFactory(() => botToken);
208
+
209
+ for (const userId of slackTargets) {
210
+ try {
211
+ let threadTs = null;
212
+ let shouldCreateNewThread = true;
213
+ const threadKey = `${accountId}:${userId}`;
214
+
215
+ const existingThread = slackThreads.get(threadKey);
216
+ if (existingThread && existingThread.lastEvent === "crash" && eventType === "recovery") {
217
+ threadTs = existingThread.threadTs;
218
+ shouldCreateNewThread = false;
219
+ }
113
220
 
114
- // Store thread for future related messages
115
- if (shouldCreateNewThread && result.ts) {
116
- slackThreads.set(userId, {
117
- threadTs: result.ts,
118
- lastEvent: eventType,
221
+ const result = await accountSlackApi.postMessage(userId, String(message || ""), {
222
+ thread_ts: threadTs,
223
+ mrkdwn: true,
119
224
  });
120
- }
121
225
 
122
- // Add reactions based on event type
123
- // Use result.channel (the actual conversation/DM ID) instead of userId
124
- if (result.ts && result.channel && slackApi.addReaction) {
125
- try {
126
- if (eventType === "crash") {
127
- await slackApi.addReaction(result.channel, result.ts, "x");
128
- } else if (eventType === "recovery") {
129
- await slackApi.addReaction(result.channel, result.ts, "white_check_mark");
130
- } else if (eventType === "health") {
131
- await slackApi.addReaction(result.channel, result.ts, "heart");
226
+ if (shouldCreateNewThread && result.ts) {
227
+ slackThreads.set(threadKey, {
228
+ threadTs: result.ts,
229
+ lastEvent: eventType,
230
+ });
231
+ }
232
+
233
+ if (result.ts && result.channel && accountSlackApi.addReaction) {
234
+ try {
235
+ if (eventType === "crash") {
236
+ await accountSlackApi.addReaction(result.channel, result.ts, "x");
237
+ } else if (eventType === "recovery") {
238
+ await accountSlackApi.addReaction(
239
+ result.channel,
240
+ result.ts,
241
+ "white_check_mark",
242
+ );
243
+ } else if (eventType === "health") {
244
+ await accountSlackApi.addReaction(result.channel, result.ts, "heart");
245
+ }
246
+ } catch (reactionErr) {
247
+ console.error(
248
+ `[watchdog] slack reaction failed for ${accountId}/${userId}: ${reactionErr.message}`,
249
+ );
132
250
  }
133
- } catch (reactionErr) {
134
- // Reactions are nice-to-have, don't fail the whole notification
135
- console.error(`[watchdog] slack reaction failed for ${userId}: ${reactionErr.message}`);
136
251
  }
137
- }
138
252
 
139
- summary.slack.sent += 1;
140
- } catch (err) {
141
- summary.slack.failed += 1;
142
- console.error(`[watchdog] slack notification failed for ${userId}: ${err.message}`);
253
+ summary.slack.sent += 1;
254
+ } catch (err) {
255
+ summary.slack.failed += 1;
256
+ console.error(
257
+ `[watchdog] slack notification failed for ${accountId}/${userId}: ${err.message}`,
258
+ );
259
+ }
143
260
  }
144
261
  }
145
262
  }
package/lib/server.js CHANGED
@@ -138,6 +138,9 @@ const {
138
138
  const {
139
139
  ensureUsageTrackerPluginConfig,
140
140
  } = require("./server/usage-tracker-config");
141
+ const {
142
+ ensureManagedExecDefaults,
143
+ } = require("./server/exec-defaults-config");
141
144
 
142
145
  const { PORT, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
143
146
 
@@ -223,7 +226,12 @@ const webhookMiddleware = createWebhookMiddleware({
223
226
  const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
224
227
  const discordApi = createDiscordApi(() => process.env.DISCORD_BOT_TOKEN);
225
228
  const slackApi = createSlackApi(() => process.env.SLACK_BOT_TOKEN);
226
- const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi, slackApi });
229
+ const watchdogNotifier = createWatchdogNotifier({
230
+ telegramApi,
231
+ discordApi,
232
+ slackApi,
233
+ readEnvFile,
234
+ });
227
235
  const watchdog = createWatchdog({
228
236
  clawCmd,
229
237
  launchGatewayProcess,
@@ -379,6 +387,11 @@ startServerLifecycle({
379
387
  PORT,
380
388
  isOnboarded,
381
389
  runOnboardedBootSequence,
390
+ ensureManagedExecDefaults: () =>
391
+ ensureManagedExecDefaults({
392
+ fsModule: fs,
393
+ openclawDir: constants.OPENCLAW_DIR,
394
+ }),
382
395
  ensureUsageTrackerPluginConfig: () =>
383
396
  ensureUsageTrackerPluginConfig({
384
397
  fsModule: fs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.3-beta.5",
3
+ "version": "0.8.4-beta.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "express": "^4.21.0",
38
38
  "http-proxy": "^1.18.1",
39
- "openclaw": "^2026.3.28",
39
+ "openclaw": "2026.4.1",
40
40
  "patch-package": "^8.0.1",
41
41
  "ws": "^8.19.0"
42
42
  },
@@ -0,0 +1,13 @@
1
+ diff --git a/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js b/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js
2
+ index 4c742cee..bb87239b 100644
3
+ --- a/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js
4
+ +++ b/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js
5
+ @@ -26669,7 +26669,7 @@ function attachGatewayWsMessageHandler(params) {
6
+ close(1008, truncateCloseReason(authMessage));
7
+ };
8
+ const clearUnboundScopes = () => {
9
+ - if (scopes.length > 0) {
10
+ + if (scopes.length > 0 && !sharedAuthOk) {
11
+ scopes = [];
12
+ connectParams.scopes = scopes;
13
+ }
@@ -1,50 +0,0 @@
1
- ### Prioritized Implementation Order
2
-
3
- 1. **[P0 | S] Unify toast type mapping in one place so callers consistently use semantic levels.**
4
- - [x] Update `lib/public/js/components/toast.js` (`showToast`, `ToastContainer`) to normalize legacy values.
5
- - [x] Normalize highest-impact callers first: `lib/public/js/components/providers.js`, `lib/public/js/components/models.js`, `lib/public/js/components/google.js`, `lib/public/js/components/gateway.js`, `lib/public/js/components/envars.js`, `lib/public/js/components/webhooks.js`.
6
- - Current inconsistency snapshot:
7
- - Legacy color callers: `lib/public/js/components/providers.js`, `lib/public/js/components/models.js`, `lib/public/js/components/google.js`.
8
- - Semantic callers: `lib/public/js/components/envars.js`, `lib/public/js/components/webhooks.js`, `lib/public/js/components/gateway.js`, `lib/public/js/components/telegram-workspace.js`.
9
-
10
- 2. **[P0 | M] Add a shared restart banner component (and/or restart hook) for restart-required flows.**
11
- - [x] Extract shared banner + handler state from: `lib/public/js/components/envars.js`, `lib/public/js/components/providers.js`, `lib/public/js/components/webhooks.js`.
12
- - [x] Keep `restartGateway` integration aligned with existing gateway action UI in `lib/public/js/components/gateway.js`.
13
- - Current inconsistency snapshot:
14
- - Duplicate banner + state + handler logic in the same 3 files above.
15
-
16
- 3. **[P1 | S] Add a small shared loading spinner component to replace repeated inline SVG.**
17
- - [x] Replace inline spinners in: `lib/public/js/app.js`, `lib/public/js/components/providers.js`, `lib/public/js/components/models.js`, `lib/public/js/components/onboarding/welcome-setup-step.js`.
18
- - [x] Evaluate whether `lib/public/js/components/update-action-button.js` should consume the shared spinner as well.
19
- - Current inconsistency snapshot:
20
- - Spinner markup shape and sizing differ slightly by file.
21
-
22
- 4. **[P1 | M] Standardize button styles around `ac-btn-*` + common disabled treatment.**
23
- - [x] Convert ad-hoc utility button patterns in: `lib/public/js/components/pairings.js` (Reject), `lib/public/js/components/device-pairings.js` (Reject), `lib/public/js/components/webhooks.js` (Create/Delete), `lib/public/js/components/google.js` (disconnect action), `lib/public/js/components/providers.js` (Codex reconnect/disconnect/restart).
24
- - [x] Align onboarding variants in: `lib/public/js/components/onboarding/welcome-setup-step.js`, `lib/public/js/components/onboarding/welcome-form-step.js`, `lib/public/js/components/onboarding/welcome-pairing-step.js`.
25
- - Current inconsistency snapshot:
26
- - Mixed custom classes vs ad-hoc border utility buttons.
27
-
28
- 5. **[P1 | S] Normalize disabled opacity usage to one convention.**
29
- - [x] Standardize `opacity-50` vs `opacity-60` in:
30
- - `lib/public/js/components/envars.js`
31
- - `lib/public/js/components/providers.js`
32
- - `lib/public/js/components/webhooks.js`
33
- - `lib/public/js/components/telegram-workspace.js`
34
- - `lib/public/js/components/onboarding/welcome-setup-step.js`
35
- - `lib/public/js/components/onboarding/welcome-pairing-step.js`
36
- - Current inconsistency snapshot:
37
- - `opacity-50` and `opacity-60` both used for disabled states.
38
-
39
- 6. **[P2 | M] Consider a shared action button wrapper for common save/create/delete states.**
40
- - [x] Evaluate extraction points in: `lib/public/js/components/envars.js`, `lib/public/js/components/providers.js`, `lib/public/js/components/webhooks.js`, `lib/public/js/components/models.js`, `lib/public/js/components/pairings.js`, `lib/public/js/components/device-pairings.js`.
41
- - Current inconsistency snapshot:
42
- - Repeated action-label + loading-label + disabled-guard patterns.
43
-
44
- 7. **[P2 | M] Extract a shared modal shell wrapper for non-confirm custom modals.**
45
- - [x] Factor shared overlay/panel shell from:
46
- - `lib/public/js/components/webhooks.js` (`CreateWebhookModal`)
47
- - `lib/public/js/components/credentials-modal.js`
48
- - [x] Keep parity with `lib/public/js/components/confirm-dialog.js` behavior (overlay click + Escape semantics).
49
- - Current inconsistency snapshot:
50
- - Same modal shell classes/structure repeated with minor differences.
@@ -1,13 +0,0 @@
1
- diff --git a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
2
- index ca48b932..c12478c4 100644
3
- --- a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
4
- +++ b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
5
- @@ -25935,7 +25935,7 @@ function attachGatewayWsMessageHandler(params) {
6
- close(1008, truncateCloseReason(authMessage));
7
- };
8
- const clearUnboundScopes = () => {
9
- - if (scopes.length > 0) {
10
- + if (scopes.length > 0 && !sharedAuthOk) {
11
- scopes = [];
12
- connectParams.scopes = scopes;
13
- }