alvin-bot 4.13.1 → 4.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.13.2] — 2026-04-16
6
+
7
+ ### ✨ Slack: `/alvin` slash commands + rewritten setup guide
8
+
9
+ **Bug (carried over from v4.13.1):** Slash commands didn't work on Slack. When a user typed `/status` in a DM with the bot, Slack either hit its built-in `/status` (user status setter) or showed "Not a valid command" — nothing reached the bot. The Slack adapter only registered `message` + `app_mention` event handlers, no `command` handler; the manifest declared no slash commands.
10
+
11
+ **Why it was a gotcha**: Slack treats slash commands as a separate event type (`command`), not as message text. Apps must explicitly register each command in their manifest AND add a `app.command(...)` handler to receive the events. None of this had been set up.
12
+
13
+ **Fix**: v4.13.2 introduces a single namespaced command `/alvin` that takes a subcommand argument. Users type `/alvin status`, `/alvin new`, `/alvin effort high`, `/alvin help` — the Slack adapter parses the subcommand from `command.text` and forwards it as a `/status`/`/new`/etc. message through the existing `handlePlatformCommand` pipeline. Unknown subcommands fall through to normal LLM handling so `/alvin what's the weather` also works as a free-form query.
14
+
15
+ ### Technical details
16
+
17
+ **New parser** `src/platforms/slack-slash-parser.ts`: pure `parseSlackSlashCommand(text)` helper. Empty text → `/help`. Single word → `/<word>`. Word + args → `/<word> <args>`. Lowercases subcommand, preserves arg capitalization, strips defensive leading slash, collapses extra whitespace. 8 unit tests.
18
+
19
+ **Adapter change** `src/platforms/slack.ts`: new `app.command("/alvin", ...)` registration in `start()` (guarded with `typeof app.command === "function"` for test-mock compat). `ack()` fires immediately to meet Slack's 3-second requirement. New `handleSlashCommand(command)` method synthesizes an `IncomingMessage` with the translated `text` and the command's `channel_id`/`user_id` and forwards to the same `this.handler(...)` path as regular DMs. Response goes back via `chat.postMessage` (persistent, visible in channel history) rather than slash-command-native `respond()` (ephemeral) — matches DM behavior.
20
+
21
+ **Slack app manifest**: requires a new `features.slash_commands` entry declaring `/alvin` and a new `commands` OAuth scope. Both are in the manifest JSON the setup guide pastes in — no manual per-field config. Existing installations need a one-time re-install to pick up the new `commands` scope (Slack shows a yellow banner after manifest save).
22
+
23
+ **Setup guide rewrite** `src/web/setup-api.ts` Slack `setupSteps[]`: replaces the old 7-step "click-through every section" sequence with a 9-step manifest-paste flow that actually matches how the bot is currently set up (Messages Tab, Events, Socket Mode, slash commands — all covered in one JSON paste). Includes the full manifest JSON inline. New users get a working Slack app in ~2 minutes instead of hunting through the Slack API UI.
24
+
25
+ ### Testing
26
+
27
+ - **Baseline**: 475 tests (v4.13.1)
28
+ - **New**: `test/slack-slash-command.test.ts` — 8 tests (empty → /help, single word, args preservation, whitespace collapse, case insensitivity on subcommand, case preservation on args, defensive leading slash handling)
29
+ - **Total**: 483 tests, all green, TSC clean
30
+ - **Live smoke verification**: manifest pushed via Chrome browser automation, reinstall completed, Slack adapter re-registered with `app.command("/alvin")`. Live test of `/alvin status` pending user confirmation.
31
+
32
+ ### Files changed
33
+
34
+ - **NEW**: `src/platforms/slack-slash-parser.ts`, `test/slack-slash-command.test.ts`
35
+ - **Modified**: `src/platforms/slack.ts` (command registration + handler), `src/web/setup-api.ts` (slack setupSteps rewrite), `package.json` (4.13.1 → 4.13.2)
36
+
37
+ ### Known limitations
38
+
39
+ - **One command namespace only**: we register `/alvin` not individual `/status`/`/new` etc. because `/status` conflicts with Slack's built-in command. Side effect: slightly more typing for users (`/alvin status` vs `/status`). Alternative namespaces considered (`/alvin-status` as multiple commands each) would work too but require more manifest boilerplate; deferred unless users complain.
40
+ - **Channel responses are public**: when `/alvin status` is invoked in a channel, the bot's response is a normal `chat.postMessage` visible to the whole channel. If you want private responses there, use DM or switch the sendText call to use Slack's `response_url` (ephemeral). Deferred as enhancement — DM is the primary use case.
41
+
42
+ ---
43
+
5
44
  ## [4.13.1] — 2026-04-16
6
45
 
7
46
  ### 🐛 Patch: Slack Test Connection + PM2 → launchd migration for Maintenance UI
@@ -0,0 +1,32 @@
1
+ /**
2
+ * v4.13.2 — Parse Slack `/alvin <subcommand> [args...]` slash command
3
+ * text into the platform-agnostic `/<subcommand> [args]` format that
4
+ * handlePlatformCommand already knows.
5
+ *
6
+ * Pure function — tested in isolation. Called from the Slack adapter's
7
+ * `app.command('/alvin')` handler.
8
+ *
9
+ * Rules:
10
+ * - Empty text → `/help` (useful default, shows the commands list)
11
+ * - Subcommand is lowercased for case-insensitive matching
12
+ * - Args are kept verbatim (preserve user capitalization)
13
+ * - A literal leading `/` on the subcommand is stripped defensively
14
+ * (handles `/alvin /status` which becomes just `/status`, not `//status`)
15
+ */
16
+ export function parseSlackSlashCommand(text) {
17
+ const trimmed = text.trim();
18
+ if (trimmed.length === 0)
19
+ return "/help";
20
+ // Split on first whitespace run — head is the subcommand, tail is args
21
+ const match = trimmed.match(/^(\S+)(?:\s+(.*))?$/);
22
+ if (!match)
23
+ return "/help";
24
+ let sub = (match[1] || "").toLowerCase();
25
+ // Strip a literal leading slash the user might have typed
26
+ if (sub.startsWith("/"))
27
+ sub = sub.slice(1);
28
+ if (sub.length === 0)
29
+ return "/help";
30
+ const args = (match[2] || "").trim();
31
+ return args ? `/${sub} ${args}` : `/${sub}`;
32
+ }
@@ -17,6 +17,7 @@
17
17
  * 7. Set env vars and restart bot
18
18
  */
19
19
  import fs from "fs";
20
+ import { parseSlackSlashCommand } from "./slack-slash-parser.js";
20
21
  let _slackState = {
21
22
  status: "disconnected",
22
23
  botName: null,
@@ -80,6 +81,31 @@ export class SlackAdapter {
80
81
  this.app.event("app_mention", async ({ event, say, client }) => {
81
82
  await this.handleMention(event, say, client);
82
83
  });
84
+ // v4.13.2 — Handle the /alvin slash command.
85
+ //
86
+ // Slack sends slash commands as their own "command" event type
87
+ // (not as regular messages), so without this handler users who
88
+ // type /status see "Not a valid command" from Slack's built-in
89
+ // /status (which sets their user status). We register /alvin as
90
+ // a namespaced parent and parse the subcommand from command.text.
91
+ //
92
+ // CRITICAL: Slack requires ack() within 3 seconds or the user
93
+ // sees "/alvin didn't respond". We ack FIRST, then do the work
94
+ // asynchronously via the normal handler pipeline.
95
+ //
96
+ // Defensive: older/mocked Bolt versions might not expose .command().
97
+ // Skip registration silently rather than crashing start().
98
+ if (typeof this.app.command === "function") {
99
+ this.app.command("/alvin", async ({ command, ack }) => {
100
+ await ack();
101
+ try {
102
+ await this.handleSlashCommand(command);
103
+ }
104
+ catch (err) {
105
+ console.error("[slack] /alvin command failed:", err);
106
+ }
107
+ });
108
+ }
83
109
  await this.app.start();
84
110
  _slackState.status = "connected";
85
111
  _slackState.connectedAt = Date.now();
@@ -160,6 +186,43 @@ export class SlackAdapter {
160
186
  };
161
187
  await this.handler(incoming);
162
188
  }
189
+ /**
190
+ * v4.13.2 — Handle /alvin slash command.
191
+ *
192
+ * Slack delivers these with command.text containing the part after
193
+ * "/alvin " (so "/alvin status" arrives with text="status"). We
194
+ * translate into a platform-agnostic "/<sub>[ args]" string and
195
+ * forward through the normal message handler — handlePlatformCommand
196
+ * picks it up since it starts with "/".
197
+ *
198
+ * The response goes back via the same sendText path as regular
199
+ * messages (chat.postMessage in command.channel_id). Slack allows
200
+ * this in addition to the slash-command-native respond() mechanism,
201
+ * and it keeps the codepath identical to message.im responses.
202
+ */
203
+ async handleSlashCommand(command) {
204
+ if (!this.handler)
205
+ return;
206
+ const translated = parseSlackSlashCommand(command.text || "");
207
+ const channelId = command.channel_id || "";
208
+ const userId = command.user_id || "";
209
+ const userName = command.user_name || userId;
210
+ const incoming = {
211
+ platform: "slack",
212
+ messageId: `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
213
+ chatId: channelId,
214
+ userId,
215
+ userName,
216
+ text: translated,
217
+ // Slack slash commands are always issued 1:1 in the sense of
218
+ // "one user invoking". isGroup reflects the CHANNEL context
219
+ // (channel_name=directmessage is a DM, otherwise channel/group).
220
+ isGroup: command.channel_name && command.channel_name !== "directmessage",
221
+ isMention: false,
222
+ isReplyToBot: false,
223
+ };
224
+ await this.handler(incoming);
225
+ }
163
226
  async handleMention(event, _say, client) {
164
227
  if (!this.handler)
165
228
  return;
@@ -118,7 +118,7 @@ const PLATFORMS = [
118
118
  id: "slack",
119
119
  name: "Slack",
120
120
  icon: "💼",
121
- description: "Slack workspace integration via Socket Mode (no public URL needed). DMs and @mentions in channels.",
121
+ description: "Slack workspace integration via Socket Mode (no public URL needed). DMs, @mentions in channels, and the `/alvin` slash command for /status, /new, /effort, /help (v4.13.2+).",
122
122
  envVars: [
123
123
  { key: "SLACK_BOT_TOKEN", label: "Bot Token (xoxb-...)", placeholder: "xoxb-...", secret: true },
124
124
  { key: "SLACK_APP_TOKEN", label: "App Token (xapp-...)", placeholder: "xapp-...", secret: true },
@@ -126,13 +126,15 @@ const PLATFORMS = [
126
126
  npmPackages: ["@slack/bolt"],
127
127
  setupUrl: "https://api.slack.com/apps",
128
128
  setupSteps: [
129
- "Create a new App at api.slack.com/apps (From scratch)",
130
- "Enable Socket Mode (Settings Socket Mode Enable)",
131
- "Generate App-Level Token with 'connections:write' scope copy as SLACK_APP_TOKEN",
132
- "Go to OAuth & Permissionsadd Bot Token Scopes: chat:write, channels:history, groups:history, im:history, mpim:history, app_mentions:read, files:write, reactions:write",
133
- "Install App to Workspace → copy Bot User OAuth Token as SLACK_BOT_TOKEN",
134
- "Subscribe to Events: message.im, message.groups, message.channels, app_mention",
135
- "Invite the bot to channels with /invite @botname",
129
+ "Go to https://api.slack.com/apps and click 'Create New App' → 'From an app manifest'. Choose your workspace.",
130
+ "Paste the full manifest JSON below into the JSON tab (replaces the template). This sets scopes, events, Messages Tab, Socket Mode, and the /alvin slash command in one go:\n\n{\n \"display_information\": { \"name\": \"Alvin\" },\n \"features\": {\n \"app_home\": {\n \"home_tab_enabled\": false,\n \"messages_tab_enabled\": true,\n \"messages_tab_read_only_enabled\": false\n },\n \"bot_user\": { \"display_name\": \"Alvin\", \"always_online\": false },\n \"slash_commands\": [\n {\n \"command\": \"/alvin\",\n \"description\": \"Alvin bot commands\",\n \"usage_hint\": \"new | status | effort low|medium|high|max | help\",\n \"should_escape\": false\n }\n ]\n },\n \"oauth_config\": {\n \"scopes\": {\n \"bot\": [\n \"app_mentions:read\", \"mpim:read\", \"chat:write\",\n \"channels:history\", \"groups:history\", \"im:history\", \"mpim:history\",\n \"files:write\", \"reactions:write\", \"files:read\",\n \"commands\"\n ]\n },\n \"pkce_enabled\": true\n },\n \"settings\": {\n \"event_subscriptions\": {\n \"bot_events\": [\n \"app_mention\", \"message.channels\", \"message.groups\",\n \"message.im\", \"message.mpim\"\n ]\n },\n \"interactivity\": { \"is_enabled\": true },\n \"org_deploy_enabled\": false,\n \"socket_mode_enabled\": true,\n \"token_rotation_enabled\": false\n }\n}",
131
+ "Click Create. Slack creates the App with all the correct config pre-wired.",
132
+ "Go to Settings Basic Information App-Level Tokens → 'Generate Token and Scopes'. Name it 'socket', pick scope 'connections:write', click Generate. Copy the xapp-... token into SLACK_APP_TOKEN below.",
133
+ "Go to Settings → Install App → Install to Workspace → Allow. Copy the 'Bot User OAuth Token' (xoxb-...) into SLACK_BOT_TOKEN below.",
134
+ "Save both tokens here (click 'Save') and then click 'Test Connection' — you should see '@alvin on <Workspace>'.",
135
+ "Click 'Restart bot' in Maintenance the Slack adapter connects automatically (look for '💬 Slack connected' in the logs).",
136
+ "In Slack: open the app in the sidebar, click 'Messages' tab, send a DM. Or use the slash command: /alvin status, /alvin new, /alvin effort high, /alvin help.",
137
+ "To use in channels: invite the bot with /invite @alvin and then @mention it (e.g. '@alvin what's the weather in Berlin?').",
136
138
  ],
137
139
  },
138
140
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "4.13.1",
3
+ "version": "4.13.2",
4
4
  "description": "Alvin Bot \u2014 Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,61 @@
1
+ /**
2
+ * v4.13.2 — Slack slash command parser tests.
3
+ *
4
+ * Users on Slack type `/alvin <subcommand> [args...]` which Bolt
5
+ * delivers via app.command('/alvin') with `command.text` containing
6
+ * the part after `/alvin `. We parse it into a platform-agnostic
7
+ * "/subcommand [args]" text that handlePlatformCommand already knows
8
+ * how to route (/new, /status, /effort, /help).
9
+ *
10
+ * Empty text → `/help` (most helpful default).
11
+ * Pass-through for everything else — unknown subcommand falls through
12
+ * to normal LLM prompt handling.
13
+ */
14
+ import { describe, it, expect } from "vitest";
15
+ import { parseSlackSlashCommand } from "../src/platforms/slack-slash-parser.js";
16
+
17
+ describe("parseSlackSlashCommand (v4.13.2)", () => {
18
+ it("empty text maps to /help", () => {
19
+ expect(parseSlackSlashCommand("")).toBe("/help");
20
+ expect(parseSlackSlashCommand(" ")).toBe("/help");
21
+ });
22
+
23
+ it("single-word subcommand becomes /<subcommand>", () => {
24
+ expect(parseSlackSlashCommand("status")).toBe("/status");
25
+ expect(parseSlackSlashCommand("new")).toBe("/new");
26
+ expect(parseSlackSlashCommand("help")).toBe("/help");
27
+ });
28
+
29
+ it("subcommand with args preserves the args", () => {
30
+ expect(parseSlackSlashCommand("effort high")).toBe("/effort high");
31
+ expect(parseSlackSlashCommand("effort low")).toBe("/effort low");
32
+ });
33
+
34
+ it("multi-word args are preserved verbatim", () => {
35
+ expect(parseSlackSlashCommand("ask what is the weather in berlin")).toBe(
36
+ "/ask what is the weather in berlin",
37
+ );
38
+ });
39
+
40
+ it("collapses extra whitespace around subcommand", () => {
41
+ expect(parseSlackSlashCommand(" status ")).toBe("/status");
42
+ expect(parseSlackSlashCommand(" effort max ")).toBe("/effort max");
43
+ });
44
+
45
+ it("lowercases the subcommand for case-insensitive matching", () => {
46
+ expect(parseSlackSlashCommand("Status")).toBe("/status");
47
+ expect(parseSlackSlashCommand("HELP")).toBe("/help");
48
+ });
49
+
50
+ it("does NOT lowercase the args (preserve user intent)", () => {
51
+ expect(parseSlackSlashCommand("ask What is THIS")).toBe(
52
+ "/ask What is THIS",
53
+ );
54
+ });
55
+
56
+ it("handles leading slash defensively — strips duplicate", () => {
57
+ // If a user literally types `/alvin /status`, Slack delivers text="/status"
58
+ expect(parseSlackSlashCommand("/status")).toBe("/status");
59
+ expect(parseSlackSlashCommand("/effort max")).toBe("/effort max");
60
+ });
61
+ });