@chrysb/alphaclaw 0.6.2-beta.4 → 0.6.2-beta.6

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 (40) hide show
  1. package/lib/public/assets/icons/slack.svg +17 -0
  2. package/lib/public/css/cron.css +91 -39
  3. package/lib/public/js/components/add-channel-menu.js +59 -0
  4. package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +14 -38
  5. package/lib/public/js/components/agents-tab/agent-bindings-section/use-channel-items.js +0 -6
  6. package/lib/public/js/components/agents-tab/create-channel-modal.js +185 -47
  7. package/lib/public/js/components/channels.js +15 -44
  8. package/lib/public/js/components/cron-tab/cron-calendar.js +287 -164
  9. package/lib/public/js/components/cron-tab/cron-insights-panel.js +325 -0
  10. package/lib/public/js/components/cron-tab/cron-job-detail.js +38 -363
  11. package/lib/public/js/components/cron-tab/cron-job-settings-card.js +233 -0
  12. package/lib/public/js/components/cron-tab/cron-overview.js +40 -19
  13. package/lib/public/js/components/cron-tab/cron-prompt-editor.js +173 -0
  14. package/lib/public/js/components/cron-tab/cron-run-history-panel.js +69 -56
  15. package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +20 -2
  16. package/lib/public/js/components/cron-tab/index.js +170 -78
  17. package/lib/public/js/components/envars.js +4 -3
  18. package/lib/public/js/components/file-viewer/editor-surface.js +5 -1
  19. package/lib/public/js/components/file-viewer/use-editor-line-number-sync.js +36 -0
  20. package/lib/public/js/components/file-viewer/use-file-viewer.js +7 -23
  21. package/lib/public/js/components/file-viewer/utils.js +1 -5
  22. package/lib/public/js/components/onboarding/pairing-utils.js +1 -0
  23. package/lib/public/js/components/onboarding/welcome-config.js +31 -1
  24. package/lib/public/js/components/onboarding/welcome-form-step.js +145 -67
  25. package/lib/public/js/components/onboarding/welcome-pairing-step.js +89 -50
  26. package/lib/public/js/components/pairings.js +1 -1
  27. package/lib/public/js/components/welcome/index.js +1 -0
  28. package/lib/public/js/lib/channel-provider-availability.js +23 -0
  29. package/lib/server/agents/channels.js +110 -6
  30. package/lib/server/agents/shared.js +70 -1
  31. package/lib/server/constants.js +13 -0
  32. package/lib/server/gateway.js +28 -11
  33. package/lib/server/onboarding/openclaw.js +30 -0
  34. package/lib/server/onboarding/validation.js +1 -1
  35. package/lib/server/routes/pairings.js +2 -2
  36. package/lib/server/routes/system.js +9 -2
  37. package/lib/server/slack-api.js +38 -0
  38. package/lib/server/watchdog-notify.js +20 -3
  39. package/lib/server.js +3 -1
  40. package/package.json +1 -1
@@ -15,6 +15,7 @@ const {
15
15
  isValidChannelAccountId,
16
16
  normalizeChannelProvider,
17
17
  deriveChannelEnvKey,
18
+ deriveChannelExtraEnvKeys,
18
19
  getConfiguredChannelEnvKeys,
19
20
  assertActiveChannelTokenEnvVars,
20
21
  normalizeChannelConfig,
@@ -63,15 +64,28 @@ const createChannelsDomain = ({
63
64
  throw new Error(`Channel account "${provider}/${accountId}" not found`);
64
65
  }
65
66
  const envKey = deriveChannelEnvKey({ provider, accountId });
67
+ const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
66
68
  const envVars = readEnvFile();
67
69
  const envEntry = (Array.isArray(envVars) ? envVars : []).find(
68
70
  (entry) => String(entry?.key || "").trim() === envKey,
69
71
  );
72
+ const appEnvKey = extraEnvKeys[0] || "";
73
+ const appEnvEntry = appEnvKey
74
+ ? (Array.isArray(envVars) ? envVars : []).find(
75
+ (entry) => String(entry?.key || "").trim() === appEnvKey,
76
+ )
77
+ : null;
70
78
  return {
71
79
  provider,
72
80
  accountId,
73
81
  envKey,
74
82
  token: String(envEntry?.value || ""),
83
+ ...(provider === "slack"
84
+ ? {
85
+ appEnvKey,
86
+ appToken: String(appEnvEntry?.value || ""),
87
+ }
88
+ : {}),
75
89
  };
76
90
  };
77
91
 
@@ -129,11 +143,21 @@ const createChannelsDomain = ({
129
143
  `Channel account "${provider}/${accountId}" already exists`,
130
144
  );
131
145
  }
132
- if (provider === "discord" && Object.keys(existingAccounts).length > 0) {
133
- throw new Error("Discord supports a single channel account");
146
+ if (
147
+ (provider === "discord" || provider === "slack") &&
148
+ Object.keys(existingAccounts).length > 0
149
+ ) {
150
+ throw new Error(
151
+ `${kChannelLabels[provider] || "This provider"} supports a single channel account`,
152
+ );
134
153
  }
135
154
 
136
155
  const envKey = deriveChannelEnvKey({ provider, accountId });
156
+ const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
157
+ const appToken = String(input.appToken || "").trim();
158
+ if (provider === "slack" && !appToken) {
159
+ throw new Error("Slack App Token is required");
160
+ }
137
161
  const tokenField = kChannelTokenFields[provider];
138
162
  const currentEnvVars = readEnvFile();
139
163
  const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
@@ -156,11 +180,41 @@ const createChannelsDomain = ({
156
180
  `[alphaclaw] Overwriting orphaned channel env var ${dupKey} (no matching config entry)`,
157
181
  );
158
182
  }
183
+ let orphanedExtraEnvKey = null;
184
+ if (provider === "slack") {
185
+ const appEnvKey = extraEnvKeys[0];
186
+ const duplicateAppTokenEntry = previousEnvVars.find((entry) => {
187
+ const existingKey = String(entry?.key || "").trim();
188
+ const existingValue = String(entry?.value || "").trim();
189
+ if (!existingKey || !existingValue) return false;
190
+ if (existingKey === envKey || existingKey === appEnvKey) return false;
191
+ return existingValue === appToken;
192
+ });
193
+ if (duplicateAppTokenEntry) {
194
+ const dupKey = String(duplicateAppTokenEntry.key || "").trim();
195
+ const configuredKeys = getConfiguredChannelEnvKeys(cfg);
196
+ if (configuredKeys.has(dupKey)) {
197
+ throw new Error(`Channel token already exists in ${dupKey}`);
198
+ }
199
+ orphanedExtraEnvKey = dupKey;
200
+ console.log(
201
+ `[alphaclaw] Overwriting orphaned channel env var ${dupKey} (no matching config entry)`,
202
+ );
203
+ }
204
+ }
159
205
  const nextEnvVars = previousEnvVars.filter((entry) => {
160
206
  const key = String(entry?.key || "").trim();
161
- return key !== envKey && key !== orphanedEnvKey;
207
+ return (
208
+ key !== envKey &&
209
+ key !== orphanedEnvKey &&
210
+ !extraEnvKeys.includes(key) &&
211
+ key !== orphanedExtraEnvKey
212
+ );
162
213
  });
163
214
  nextEnvVars.push({ key: envKey, value: token });
215
+ if (provider === "slack" && extraEnvKeys[0]) {
216
+ nextEnvVars.push({ key: extraEnvKeys[0], value: appToken });
217
+ }
164
218
 
165
219
  const previousConfig = cloneJson(cfg);
166
220
  try {
@@ -188,7 +242,12 @@ const createChannelsDomain = ({
188
242
  ? `--account ${shellEscapeArg(accountId)}`
189
243
  : "",
190
244
  name ? `--name ${shellEscapeArg(name)}` : "",
191
- `--token ${shellEscapeArg(token)}`,
245
+ provider === "slack"
246
+ ? `--bot-token ${shellEscapeArg(token)}`
247
+ : `--token ${shellEscapeArg(token)}`,
248
+ provider === "slack" && appToken
249
+ ? `--app-token ${shellEscapeArg(appToken)}`
250
+ : "",
192
251
  ].filter(Boolean);
193
252
  const addResult = await clawCmd(addArgs.join(" "), {
194
253
  quiet: true,
@@ -225,6 +284,9 @@ const createChannelsDomain = ({
225
284
  : {}),
226
285
  ...(name ? { name } : {}),
227
286
  [tokenField]: `\${${envKey}}`,
287
+ ...(provider === "slack" && extraEnvKeys[0]
288
+ ? { appToken: `\${${extraEnvKeys[0]}}` }
289
+ : {}),
228
290
  dmPolicy: "pairing",
229
291
  };
230
292
  nextProviderConfig.accounts = nextAccounts;
@@ -308,6 +370,7 @@ const createChannelsDomain = ({
308
370
  const nextName = String(input.name || "").trim();
309
371
  const nextAgentId = String(input.agentId || "").trim();
310
372
  const nextToken = String(input.token || "").trim();
373
+ const nextAppToken = String(input.appToken || "").trim();
311
374
  if (!nextName) throw new Error("Channel name is required");
312
375
  if (!nextAgentId) throw new Error("Agent is required");
313
376
 
@@ -337,6 +400,41 @@ const createChannelsDomain = ({
337
400
  }
338
401
 
339
402
  let tokenUpdated = false;
403
+ if (provider === "slack" && nextAppToken) {
404
+ const appEnvKey = deriveChannelExtraEnvKeys({ provider, accountId })[0];
405
+ const currentEnvVars = readEnvFile();
406
+ const previousEnvVars = Array.isArray(currentEnvVars)
407
+ ? currentEnvVars
408
+ : [];
409
+ const existingAppToken = String(
410
+ previousEnvVars.find(
411
+ (entry) => String(entry?.key || "").trim() === appEnvKey,
412
+ )?.value || "",
413
+ );
414
+ const duplicateEnvEntry = previousEnvVars.find((entry) => {
415
+ const existingKey = String(entry?.key || "").trim();
416
+ const existingValue = String(entry?.value || "").trim();
417
+ if (!existingKey || !existingValue) return false;
418
+ if (existingKey === appEnvKey) return false;
419
+ return existingValue === nextAppToken;
420
+ });
421
+ if (duplicateEnvEntry) {
422
+ const dupKey = String(duplicateEnvEntry.key || "").trim();
423
+ const configuredKeys = getConfiguredChannelEnvKeys(cfg);
424
+ if (configuredKeys.has(dupKey)) {
425
+ throw new Error(`Channel token already exists in ${dupKey}`);
426
+ }
427
+ }
428
+ if (existingAppToken !== nextAppToken) {
429
+ const nextEnvVars = previousEnvVars.filter(
430
+ (entry) => String(entry?.key || "").trim() !== appEnvKey,
431
+ );
432
+ nextEnvVars.push({ key: appEnvKey, value: nextAppToken });
433
+ writeEnvFile(nextEnvVars);
434
+ reloadEnv();
435
+ tokenUpdated = true;
436
+ }
437
+ }
340
438
  if (nextToken) {
341
439
  const envKey = deriveChannelEnvKey({ provider, accountId });
342
440
  const currentEnvVars = readEnvFile();
@@ -542,12 +640,15 @@ const createChannelsDomain = ({
542
640
  saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
543
641
 
544
642
  const envKey = deriveChannelEnvKey({ provider, accountId });
643
+ const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
545
644
  const currentEnvVars = readEnvFile();
546
645
  const previousEnvVars = Array.isArray(currentEnvVars)
547
646
  ? currentEnvVars
548
647
  : [];
549
648
  const nextEnvVars = previousEnvVars.filter(
550
- (entry) => String(entry?.key || "").trim() !== envKey,
649
+ (entry) =>
650
+ String(entry?.key || "").trim() !== envKey &&
651
+ !extraEnvKeys.includes(String(entry?.key || "").trim()),
551
652
  );
552
653
  if (nextEnvVars.length !== previousEnvVars.length) {
553
654
  writeEnvFile(nextEnvVars);
@@ -577,10 +678,13 @@ const createChannelsDomain = ({
577
678
  }
578
679
 
579
680
  const envKey = deriveChannelEnvKey({ provider, accountId });
681
+ const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
580
682
  const currentEnvVars = readEnvFile();
581
683
  const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
582
684
  const nextEnvVars = previousEnvVars.filter(
583
- (entry) => String(entry?.key || "").trim() !== envKey,
685
+ (entry) =>
686
+ String(entry?.key || "").trim() !== envKey &&
687
+ !extraEnvKeys.includes(String(entry?.key || "").trim()),
584
688
  );
585
689
  if (nextEnvVars.length !== previousEnvVars.length) {
586
690
  writeEnvFile(nextEnvVars);
@@ -13,14 +13,23 @@ const kDefaultAgentFiles = ["SOUL.md", "AGENTS.md", "USER.md", "IDENTITY.md"];
13
13
  const kChannelEnvKeys = {
14
14
  telegram: "TELEGRAM_BOT_TOKEN",
15
15
  discord: "DISCORD_BOT_TOKEN",
16
+ slack: "SLACK_BOT_TOKEN",
17
+ };
18
+ const kChannelExtraEnvKeys = {
19
+ slack: ["SLACK_APP_TOKEN"],
16
20
  };
17
21
  const kChannelTokenFields = {
18
22
  telegram: "botToken",
19
23
  discord: "token",
24
+ slack: "botToken",
25
+ };
26
+ const kChannelExtraTokenFields = {
27
+ slack: ["appToken"],
20
28
  };
21
29
  const kChannelLabels = {
22
30
  telegram: "Telegram",
23
31
  discord: "Discord",
32
+ slack: "Slack",
24
33
  };
25
34
  const kMaskedChannelToken = "********";
26
35
 
@@ -152,6 +161,19 @@ const deriveChannelEnvKey = ({ provider, accountId }) => {
152
161
  return `${envKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
153
162
  };
154
163
 
164
+ const deriveChannelExtraEnvKeys = ({ provider, accountId }) => {
165
+ const normalizedProvider = normalizeChannelProvider(provider);
166
+ const baseEnvKeys = Array.isArray(kChannelExtraEnvKeys[normalizedProvider])
167
+ ? kChannelExtraEnvKeys[normalizedProvider]
168
+ : [];
169
+ const normalizedAccountId = String(accountId || "").trim();
170
+ if (!normalizedAccountId || normalizedAccountId === "default") {
171
+ return [...baseEnvKeys];
172
+ }
173
+ const suffix = normalizedAccountId.replace(/-/g, "_").toUpperCase();
174
+ return baseEnvKeys.map((baseKey) => `${baseKey}_${suffix}`);
175
+ };
176
+
155
177
  const getConfiguredChannelEnvKeys = (cfg) => {
156
178
  const keys = new Set();
157
179
  const channels =
@@ -164,9 +186,19 @@ const getConfiguredChannelEnvKeys = (cfg) => {
164
186
  : {};
165
187
  for (const accountId of Object.keys(accounts)) {
166
188
  keys.add(deriveChannelEnvKey({ provider, accountId }));
189
+ const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
190
+ for (const extraEnvKey of extraEnvKeys) {
191
+ keys.add(extraEnvKey);
192
+ }
167
193
  }
168
194
  if (Object.keys(accounts).length === 0 && providerConfig?.enabled) {
169
195
  keys.add(kChannelEnvKeys[provider]);
196
+ for (const extraEnvKey of deriveChannelExtraEnvKeys({
197
+ provider,
198
+ accountId: "default",
199
+ })) {
200
+ keys.add(extraEnvKey);
201
+ }
170
202
  }
171
203
  }
172
204
  return keys;
@@ -208,6 +240,15 @@ const assertActiveChannelTokenEnvVars = ({ cfg, envVars }) => {
208
240
  `Missing required channel token env var ${envKey} for active channel ${provider}/${accountId}`,
209
241
  );
210
242
  }
243
+ const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
244
+ for (const extraEnvKey of extraEnvKeys) {
245
+ const extraEnvValue = String(envMap.get(extraEnvKey) || "").trim();
246
+ if (!extraEnvValue) {
247
+ throw new Error(
248
+ `Missing required channel token env var ${extraEnvKey} for active channel ${provider}/${accountId}`,
249
+ );
250
+ }
251
+ }
211
252
  }
212
253
  }
213
254
  };
@@ -223,8 +264,13 @@ const normalizeChannelConfig = ({ provider, channelConfig }) => {
223
264
  ? { ...nextConfig.accounts }
224
265
  : {};
225
266
  const tokenField = kChannelTokenFields[normalizedProvider];
267
+ const extraTokenFields = Array.isArray(
268
+ kChannelExtraTokenFields[normalizedProvider],
269
+ )
270
+ ? kChannelExtraTokenFields[normalizedProvider]
271
+ : [];
226
272
  if (Object.keys(existingAccounts).length > 0) {
227
- if (tokenField) {
273
+ if (tokenField || extraTokenFields.length > 0) {
228
274
  for (const [accountId, accountConfig] of Object.entries(
229
275
  existingAccounts,
230
276
  )) {
@@ -239,6 +285,17 @@ const normalizeChannelConfig = ({ provider, channelConfig }) => {
239
285
  accountId,
240
286
  })}}`;
241
287
  }
288
+ const extraEnvKeys = deriveChannelExtraEnvKeys({
289
+ provider: normalizedProvider,
290
+ accountId,
291
+ });
292
+ extraTokenFields.forEach((fieldName, index) => {
293
+ const rawValue = String(nextAccountConfig[fieldName] || "").trim();
294
+ if (!rawValue) return;
295
+ if (isEnvRef(rawValue)) return;
296
+ if (!extraEnvKeys[index]) return;
297
+ nextAccountConfig[fieldName] = `\${${extraEnvKeys[index]}}`;
298
+ });
242
299
  existingAccounts[accountId] = nextAccountConfig;
243
300
  }
244
301
  }
@@ -266,6 +323,17 @@ const normalizeChannelConfig = ({ provider, channelConfig }) => {
266
323
  defaultAccountConfig[tokenField] = defaultTokenEnvRef;
267
324
  }
268
325
  }
326
+ const defaultExtraEnvKeys = deriveChannelExtraEnvKeys({
327
+ provider: normalizedProvider,
328
+ accountId: "default",
329
+ });
330
+ extraTokenFields.forEach((fieldName, index) => {
331
+ const rawValue = String(defaultAccountConfig[fieldName] || "").trim();
332
+ if (!rawValue) return;
333
+ if (isEnvRef(rawValue)) return;
334
+ if (!defaultExtraEnvKeys[index]) return;
335
+ defaultAccountConfig[fieldName] = `\${${defaultExtraEnvKeys[index]}}`;
336
+ });
269
337
  if (
270
338
  Object.keys(defaultAccountConfig).length > 0 ||
271
339
  defaultAccountConfig[tokenField]
@@ -615,6 +683,7 @@ module.exports = {
615
683
  isValidChannelAccountId,
616
684
  normalizeChannelProvider,
617
685
  deriveChannelEnvKey,
686
+ deriveChannelExtraEnvKeys,
618
687
  getConfiguredChannelEnvKeys,
619
688
  assertActiveChannelTokenEnvVars,
620
689
  normalizeChannelConfig,
@@ -235,6 +235,18 @@ const kKnownVars = [
235
235
  group: "channels",
236
236
  hint: "From Discord Developer Portal",
237
237
  },
238
+ {
239
+ key: "SLACK_BOT_TOKEN",
240
+ label: "Slack Bot Token",
241
+ group: "channels",
242
+ hint: "From your Slack app's OAuth & Permissions page (xoxb-...)",
243
+ },
244
+ {
245
+ key: "SLACK_APP_TOKEN",
246
+ label: "Slack App Token",
247
+ group: "channels",
248
+ hint: "From Basic Information → App-Level Tokens (xapp-...)",
249
+ },
238
250
  {
239
251
  key: "MISTRAL_API_KEY",
240
252
  label: "Mistral API Key",
@@ -336,6 +348,7 @@ const API_TEST_COMMANDS = {
336
348
  const kChannelDefs = {
337
349
  telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
338
350
  discord: { envKey: "DISCORD_BOT_TOKEN" },
351
+ slack: { envKey: "SLACK_BOT_TOKEN", extraEnvKeys: ["SLACK_APP_TOKEN"] },
339
352
  };
340
353
  const kProtectedBrowsePaths = new Set(
341
354
  Array.isArray(kBrowseFilePolicies?.protectedPaths)
@@ -302,17 +302,34 @@ const syncChannelConfig = (savedVars, mode = "all") => {
302
302
  if (token && !isConfigured && (mode === "add" || mode === "all")) {
303
303
  console.log(`[alphaclaw] Adding channel: ${ch}`);
304
304
  try {
305
- execSync(`openclaw channels add --channel ${ch} --token "${token}"`, {
306
- env,
307
- timeout: 15000,
308
- encoding: "utf8",
309
- });
310
- const raw = fs.readFileSync(configPath, "utf8");
311
- if (raw.includes(token)) {
312
- fs.writeFileSync(
313
- configPath,
314
- raw.split(token).join("${" + def.envKey + "}"),
305
+ if (ch === "slack") {
306
+ const appToken = savedMap[def.extraEnvKeys?.[0]];
307
+ if (!appToken) continue;
308
+ execSync(
309
+ `openclaw channels add --channel slack --bot-token "${token}" --app-token "${appToken}"`,
310
+ { env, timeout: 15000, encoding: "utf8" },
315
311
  );
312
+ let raw = fs.readFileSync(configPath, "utf8");
313
+ if (raw.includes(token)) {
314
+ raw = raw.split(token).join("${" + def.envKey + "}");
315
+ }
316
+ if (raw.includes(appToken)) {
317
+ raw = raw.split(appToken).join("${" + def.extraEnvKeys[0] + "}");
318
+ }
319
+ fs.writeFileSync(configPath, raw);
320
+ } else {
321
+ execSync(`openclaw channels add --channel ${ch} --token "${token}"`, {
322
+ env,
323
+ timeout: 15000,
324
+ encoding: "utf8",
325
+ });
326
+ const raw = fs.readFileSync(configPath, "utf8");
327
+ if (raw.includes(token)) {
328
+ fs.writeFileSync(
329
+ configPath,
330
+ raw.split(token).join("${" + def.envKey + "}"),
331
+ );
332
+ }
316
333
  }
317
334
  console.log(`[alphaclaw] Channel ${ch} added`);
318
335
  } catch (e) {
@@ -353,7 +370,7 @@ const getChannelStatus = () => {
353
370
  const credDir = `${OPENCLAW_DIR}/credentials`;
354
371
  const channels = {};
355
372
 
356
- for (const ch of ["telegram", "discord"]) {
373
+ for (const ch of Object.keys(kChannelDefs)) {
357
374
  const channelConfig =
358
375
  config.channels?.[ch] && typeof config.channels[ch] === "object"
359
376
  ? config.channels[ch]
@@ -198,6 +198,19 @@ const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
198
198
  ensurePluginAllowed(cfg, "discord");
199
199
  console.log("[onboard] Discord configured");
200
200
  }
201
+ if (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN) {
202
+ cfg.channels.slack = {
203
+ enabled: true,
204
+ botToken: varMap.SLACK_BOT_TOKEN,
205
+ appToken: varMap.SLACK_APP_TOKEN,
206
+ mode: "socket",
207
+ dmPolicy: "pairing",
208
+ groupPolicy: "open",
209
+ };
210
+ cfg.plugins.entries.slack = { enabled: true };
211
+ ensurePluginAllowed(cfg, "slack");
212
+ console.log("[onboard] Slack configured");
213
+ }
201
214
  if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
202
215
  cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
203
216
  }
@@ -274,6 +287,23 @@ const writeManagedImportOpenclawConfig = ({ fs, openclawDir, varMap }) => {
274
287
  ensurePluginAllowed(cfg, "discord");
275
288
  }
276
289
 
290
+ if (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN) {
291
+ cfg.channels.slack = {
292
+ ...(cfg.channels.slack || {}),
293
+ enabled: true,
294
+ botToken: "${SLACK_BOT_TOKEN}",
295
+ appToken: "${SLACK_APP_TOKEN}",
296
+ mode: cfg.channels.slack?.mode || "socket",
297
+ dmPolicy: getSafeImportedDmPolicy(cfg.channels.slack),
298
+ groupPolicy: cfg.channels.slack?.groupPolicy || "open",
299
+ };
300
+ cfg.plugins.entries.slack = {
301
+ ...(cfg.plugins.entries.slack || {}),
302
+ enabled: true,
303
+ };
304
+ ensurePluginAllowed(cfg, "slack");
305
+ }
306
+
277
307
  fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
278
308
  };
279
309
 
@@ -89,7 +89,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
89
89
  ? hasAiByProvider[selectedProvider]
90
90
  : hasAnyAi;
91
91
  const hasGithub = !!(githubToken && githubRepoInput);
92
- const hasChannel = !!(varMap.TELEGRAM_BOT_TOKEN || varMap.DISCORD_BOT_TOKEN);
92
+ const hasChannel = !!(varMap.TELEGRAM_BOT_TOKEN || varMap.DISCORD_BOT_TOKEN || (varMap.SLACK_BOT_TOKEN && varMap.SLACK_APP_TOKEN));
93
93
 
94
94
  if (!hasAi) {
95
95
  if (selectedProvider === "openai-codex") {
@@ -5,7 +5,7 @@ const { buildManagedPaths } = require("../internal-files-migration");
5
5
  const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
6
6
  const { quoteShellArg } = require("../utils/shell");
7
7
 
8
- const kAllowedPairingChannels = new Set(["telegram", "discord"]);
8
+ const kAllowedPairingChannels = new Set(["telegram", "discord", "slack"]);
9
9
  const kSafePairingArgPattern = /^[\w\-:.]+$/;
10
10
  const quoteCliArg = (value) => quoteShellArg(value, { strategy: "single" });
11
11
 
@@ -111,7 +111,7 @@ const registerPairingRoutes = ({ app, clawCmd, isOnboarded, fsModule = fs, openc
111
111
  }
112
112
 
113
113
  const pending = [];
114
- const channels = ["telegram", "discord"];
114
+ const channels = ["telegram", "discord", "slack"];
115
115
 
116
116
  for (const ch of channels) {
117
117
  try {
@@ -25,7 +25,8 @@ const registerSystemRoutes = ({
25
25
  authProfiles,
26
26
  }) => {
27
27
  let envRestartPending = false;
28
- const kManagedChannelTokenPattern = /^(TELEGRAM|DISCORD)_BOT_TOKEN(?:_[A-Z0-9_]+)?$/;
28
+ const kManagedChannelTokenPattern =
29
+ /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN)(?:_[A-Z0-9_]+)?$/;
29
30
  const kEnvVarsReservedForUserInput = new Set([
30
31
  "GITHUB_WORKSPACE_REPO",
31
32
  "GOG_KEYRING_PASSWORD",
@@ -311,7 +312,13 @@ const registerSystemRoutes = ({
311
312
  }
312
313
 
313
314
  for (const v of fileVars) {
314
- if (kKnownKeys.has(v.key) || isReservedUserEnvVar(v.key)) continue;
315
+ if (
316
+ kKnownKeys.has(v.key) ||
317
+ isReservedUserEnvVar(v.key) ||
318
+ isManagedChannelTokenKey(v.key)
319
+ ) {
320
+ continue;
321
+ }
315
322
  merged.push({
316
323
  key: v.key,
317
324
  value: v.value,
@@ -0,0 +1,38 @@
1
+ const kSlackApiBase = "https://slack.com/api";
2
+
3
+ const createSlackApi = (getToken) => {
4
+ const call = async (method, body = {}) => {
5
+ const token = typeof getToken === "function" ? getToken() : getToken;
6
+ if (!token) throw new Error("SLACK_BOT_TOKEN is not set");
7
+ const res = await fetch(`${kSlackApiBase}/${method}`, {
8
+ method: "POST",
9
+ headers: {
10
+ Authorization: `Bearer ${token}`,
11
+ "Content-Type": "application/json",
12
+ },
13
+ body: JSON.stringify(body),
14
+ });
15
+ if (!res.ok) {
16
+ throw new Error(`Slack API ${method}: HTTP ${res.status}`);
17
+ }
18
+ const data = await res.json();
19
+ if (!data.ok) {
20
+ const err = new Error(data.error || `Slack API error: ${method}`);
21
+ err.slackError = data.error;
22
+ throw err;
23
+ }
24
+ return data;
25
+ };
26
+
27
+ const authTest = () => call("auth.test");
28
+
29
+ const postMessage = (channel, text) =>
30
+ call("chat.postMessage", { channel, text: String(text || "") });
31
+
32
+ return {
33
+ authTest,
34
+ postMessage,
35
+ };
36
+ };
37
+
38
+ module.exports = { createSlackApi };
@@ -36,11 +36,12 @@ const getPairedIds = (channel) => {
36
36
  const formatDiscordMessage = (message) =>
37
37
  String(message || "").replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "**$1**");
38
38
 
39
- const createWatchdogNotifier = ({ telegramApi, discordApi }) => {
39
+ const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
40
40
  const notify = async (message) => {
41
41
  const summary = {
42
42
  telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
43
43
  discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
44
+ slack: { sent: 0, failed: 0, skipped: false, targets: 0 },
44
45
  };
45
46
  const telegramTargets = getPairedIds("telegram");
46
47
  summary.telegram.targets = telegramTargets.length;
@@ -77,8 +78,24 @@ const createWatchdogNotifier = ({ telegramApi, discordApi }) => {
77
78
  }
78
79
  }
79
80
 
80
- const sent = summary.telegram.sent + summary.discord.sent;
81
- const failed = summary.telegram.failed + summary.discord.failed;
81
+ const slackTargets = getPairedIds("slack");
82
+ summary.slack.targets = slackTargets.length;
83
+ if (!slackApi?.postMessage || !process.env.SLACK_BOT_TOKEN || slackTargets.length === 0) {
84
+ summary.slack.skipped = true;
85
+ } else {
86
+ for (const userId of slackTargets) {
87
+ try {
88
+ await slackApi.postMessage(userId, String(message || ""));
89
+ summary.slack.sent += 1;
90
+ } catch (err) {
91
+ summary.slack.failed += 1;
92
+ console.error(`[watchdog] slack notification failed for ${userId}: ${err.message}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ const sent = summary.telegram.sent + summary.discord.sent + summary.slack.sent;
98
+ const failed = summary.telegram.failed + summary.discord.failed + summary.slack.failed;
82
99
  return {
83
100
  ok: sent > 0,
84
101
  sent,
package/lib/server.js CHANGED
@@ -104,6 +104,7 @@ const {
104
104
  const { installGogCliSkill } = require("./server/gog-skill");
105
105
  const { createTelegramApi } = require("./server/telegram-api");
106
106
  const { createDiscordApi } = require("./server/discord-api");
107
+ const { createSlackApi } = require("./server/slack-api");
107
108
  const { createWatchdogNotifier } = require("./server/watchdog-notify");
108
109
  const { createWatchdog } = require("./server/watchdog");
109
110
  const { createDoctorService } = require("./server/doctor/service");
@@ -307,7 +308,8 @@ const gmailWatchService = registerGmailRoutes({
307
308
  });
308
309
  const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
309
310
  const discordApi = createDiscordApi(() => process.env.DISCORD_BOT_TOKEN);
310
- const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi });
311
+ const slackApi = createSlackApi(() => process.env.SLACK_BOT_TOKEN);
312
+ const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi, slackApi });
311
313
  const watchdog = createWatchdog({
312
314
  clawCmd,
313
315
  launchGatewayProcess,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.6.2-beta.4",
3
+ "version": "0.6.2-beta.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },