Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ nix run github:stablyai/agent-slack
- **Read**: fetch a message, browse channel history, list full threads
- **Search**: messages + files (with filters)
- **Artifacts**: auto-download snippets/images/files to local paths for agents
- **Write**: reply, edit/delete messages, add reactions (bullet lists auto-render as native Slack rich text)
- **Write**: send now or schedule delivery, edit/delete messages, add reactions (bullet lists auto-render as native Slack rich text)
- **Channels**: list conversations, create channels, and invite users by id/handle/email
- **Canvas**: fetch Slack canvases as Markdown

Expand Down Expand Up @@ -73,7 +73,10 @@ agent-slack
├── message
│ ├── get <target> # fetch 1 message (+ thread meta )
│ ├── list <target> # fetch thread or recent channel messages
│ ├── send <target> [text] # send / reply (supports --attach, --blocks, --reply-broadcast)
│ ├── send <target> [text] # send / reply / schedule (supports --attach, --blocks)
│ ├── scheduled
│ │ ├── list # list pending scheduled messages
│ │ └── cancel <id> # cancel a pending scheduled message
│ ├── draft <target> [text] # open Slack-like editor in browser
│ ├── edit <target> <text> # edit a message
│ ├── delete <target> # delete a message
Expand Down Expand Up @@ -219,6 +222,8 @@ After sending, the editor shows a "View in Slack" link to the posted message.
```bash
agent-slack message send "https://workspace.slack.com/archives/C123/p1700000000000000" "I can take this."
agent-slack message send "#alerts-staging" "here's the report" --attach ./report.md
agent-slack message send "#announcements" "Deploy starts at 6pm." --schedule "<future-iso-with-timezone>"
agent-slack message send "U05BRPTKL6A" "Heads up before standup" --schedule-in "monday 9am"
agent-slack message edit "https://workspace.slack.com/archives/C123/p1700000000000000" "I can take this today."
agent-slack message delete "https://workspace.slack.com/archives/C123/p1700000000000000"
agent-slack message react add "https://workspace.slack.com/archives/C123/p1700000000000000" "eyes"
Expand All @@ -239,6 +244,8 @@ Send options for `message send`:
- `--attach <path>` upload a local file (repeatable; `<text>` is optional when attaching files)
- `--blocks <path>` send raw [Block Kit](https://docs.slack.dev/block-kit/) blocks from a JSON file (or `-` for stdin). Bypasses the automatic markdown-to-rich-text conversion, unlocking header/divider/section/table blocks and other structured layouts. Cannot be combined with `--attach`.
- `--reply-broadcast` when replying in a thread, also post the reply to the parent channel (Slack's "Also send to #channel" checkbox). For channel targets, pair with `--thread-ts`; for URL targets, the thread context is derived from the message. Not supported for DM targets; cannot be combined with `--attach`.
- `--schedule <time>` schedule delivery at an ISO 8601 timestamp with explicit timezone (for example `YYYY-MM-DDTHH:mm:ss-07:00`) or a Unix timestamp. The timestamp must be in the future and within Slack's 120-day scheduled-send limit. Works with `--blocks`, `--thread-ts`, and `--reply-broadcast`; cannot be combined with `--attach`.
- `--schedule-in <duration>` schedule delivery after a duration or simple future phrase (`30m`, `3h`, `2d`, `tomorrow 9am`, `monday 9am`; phrases use your local timezone). Mutually exclusive with `--schedule`; cannot be combined with `--attach`.

Upload files through `message send`:

Expand All @@ -253,6 +260,29 @@ agent-slack message send "#general" "Decision: shipping v2 today" \
--thread-ts "1770160000.000001" --reply-broadcast
```

Scheduled sends use Slack's server-side scheduled message queue:

```bash
# Absolute time with explicit timezone; replace with a future value within 120 days
agent-slack message send "#general" "Reminder: deploy starts soon." \
--schedule "<future-iso-with-timezone>"

# Relative / natural future time
agent-slack message send "#general" "Monday launch checklist" --schedule-in "monday 9am"

# Scheduled thread reply with a Block Kit payload
agent-slack message send "#general" "fallback text" \
--thread-ts "1770160000.000001" --blocks /tmp/blocks.json --schedule-in "3h"
```

Manage pending scheduled messages:

```bash
agent-slack message scheduled list
agent-slack message scheduled list --channel "#general" --limit 25
agent-slack message scheduled cancel "Q1234ABCD" --channel "C12345678"
```

Example — post a message with a native Slack table block:

```bash
Expand Down Expand Up @@ -282,7 +312,7 @@ agent-slack message send "#alerts-staging" --blocks /tmp/blocks.json

When `--blocks` is used, the positional `<text>` argument (if provided) is still sent as the message's `text` fallback (for notifications and unfurls).

`message send` returns `channel_id` plus the posted `ts` and a `permalink` (for non-attachment sends). `thread_ts` appears only when replying in a thread.
`message send` returns `channel_id` plus the posted `ts` and a `permalink` (for non-attachment sends). `thread_ts` appears only when replying in a thread. Scheduled sends return `scheduled_message_id` and `post_at` instead of `ts`/`permalink`.

### List, create, and invite channels

Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

- Read Slack messages, threads, and channel history from any URL or channel name
- Search Slack messages and files with filters for channel, user, date, and content type
- Send, edit, delete Slack messages, upload local files with `message send --attach`, and add/remove emoji reactions programmatically
- Send, schedule, edit, delete Slack messages, upload local files with `message send --attach`, and add/remove emoji reactions programmatically
- Auto-download Slack file attachments (snippets, images, files) to local paths for AI agent consumption
- Token-efficient compact JSON output so LLMs can consume Slack data cheaply
- Zero-config auth: auto-detects Slack Desktop credentials on macOS, Windows, and Linux — with Chrome, Brave, and Firefox fallbacks
Expand Down
26 changes: 23 additions & 3 deletions skills/agent-slack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: |
- Browsing recent channel messages / channel history
- Downloading Slack attachments (snippets, images, files) to local paths
- Searching Slack messages or files
- Sending messages, including local file uploads via `message send --attach`
- Sending messages, including scheduled delivery and local file uploads via `message send --attach`
- Editing or deleting a message; adding/removing reactions
- Listing channels/conversations; creating channels and inviting users
- Fetching a Slack canvas as markdown
Expand All @@ -16,7 +16,7 @@ description: |
- Discovering and running Slack workflows
- Managing saved-for-later messages (Later tab)
- Viewing all unread messages (inbox/unreads view)
Triggers: "slack message", "slack thread", "slack URL", "slack link", "read slack", "reply on slack", "search slack", "channel history", "recent messages", "channel messages", "latest messages", "mark as read", "mark read", "slack later", "saved for later", "save for later", "slack unreads", "slack inbox", "unread slack", "upload file", "slack file upload", "send file slack"
Triggers: "slack message", "slack thread", "slack URL", "slack link", "read slack", "reply on slack", "search slack", "channel history", "recent messages", "channel messages", "latest messages", "schedule slack", "scheduled slack", "scheduled message", "mark as read", "mark read", "slack later", "saved for later", "save for later", "slack unreads", "slack inbox", "unread slack", "upload file", "slack file upload", "send file slack"
---

# Slack automation with `agent-slack`
Expand Down Expand Up @@ -148,6 +148,8 @@ agent-slack message draft "https://workspace.slack.com/archives/C123/p1700000000
```bash
agent-slack message send "https://workspace.slack.com/archives/C123/p1700000000000000" "I can take this."
agent-slack message send "alerts-staging" "here's the report" --attach ./report.md
agent-slack message send "announcements" "Deploy starts at 6pm." --schedule "<future-iso-with-timezone>"
agent-slack message send "U05BRPTKL6A" "Heads up before standup" --schedule-in "monday 9am"
agent-slack message edit "https://workspace.slack.com/archives/C123/p1700000000000000" "I can take this today."
agent-slack message delete "https://workspace.slack.com/archives/C123/p1700000000000000"

Expand All @@ -173,6 +175,8 @@ Send options for `message send`:
- `--attach <path>` upload a local file (repeatable; message text is optional when attaching files)
- `--blocks <path>` send raw [Block Kit](https://docs.slack.dev/block-kit/) blocks from a JSON file (or `-` for stdin). Enables headers, dividers, table blocks, and other structured layouts. Incompatible with `--attach`.
- `--reply-broadcast` when replying in a thread, also post the reply to the parent channel (Slack's "Also send to #channel" checkbox). For channel targets, pair with `--thread-ts`; for URL targets, the thread context is derived from the message. Not supported for DM targets; incompatible with `--attach`.
- `--schedule <time>` schedule delivery at an ISO 8601 timestamp with explicit timezone (for example `YYYY-MM-DDTHH:mm:ss-07:00`) or a Unix timestamp. The timestamp must be in the future and within Slack's 120-day scheduled-send limit. Works with `--blocks`, `--thread-ts`, and `--reply-broadcast`; incompatible with `--attach`.
- `--schedule-in <duration>` schedule delivery after a duration or simple future phrase (`30m`, `3h`, `2d`, `tomorrow 9am`, `monday 9am`; phrases use your local timezone). Mutually exclusive with `--schedule`; incompatible with `--attach`.

File upload example:

Expand All @@ -187,7 +191,23 @@ agent-slack message send "general" "Decision: shipping v2 today" \
--thread-ts "1770160000.000001" --reply-broadcast
```

`message send` returns `channel_id` plus the posted `ts` and a `permalink` (for non-attachment sends). `thread_ts` appears only when replying in a thread.
Schedule a message:

```bash
agent-slack message send "general" "Reminder: deploy starts soon." --schedule "<future-iso-with-timezone>"
agent-slack message send "general" "Monday launch checklist" --schedule-in "monday 9am"
agent-slack message send "general" "fallback text" --thread-ts "1770160000.000001" --blocks /tmp/blocks.json --schedule-in "3h"
```

Manage pending scheduled messages:

```bash
agent-slack message scheduled list
agent-slack message scheduled list --channel "general" --limit 25
agent-slack message scheduled cancel "Q1234ABCD" --channel "C12345678"
```

`message send` returns `channel_id` plus the posted `ts` and a `permalink` (for non-attachment sends). `thread_ts` appears only when replying in a thread. Scheduled sends return `scheduled_message_id` and `post_at` instead of `ts`/`permalink`.

Mentions: just write `@U05BRPTKL6A`, `@here`, `@channel`, or `@everyone` — the CLI converts them to real Slack mention tokens and escapes literal `&`/`<`/`>` in your text. You don't need to wrap IDs yourself.

Expand Down
19 changes: 19 additions & 0 deletions skills/agent-slack/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,31 @@ Run `agent-slack --help` (or `agent-slack <command> --help`) for the full option
- `[text]` is optional when uploading files with `--attach`; when present, it becomes the initial comment on the first uploaded file.
- Bullet lists (`- `, `* `, `• `, `1. `, etc.) are automatically converted to Slack’s native rich text format, so recipients see real editable bullets instead of plain-text dashes.
- Example: `agent-slack message send "general" "Coverage report" --attach ./report.md`
- Example: `agent-slack message send "general" "Monday launch checklist" --schedule-in "monday 9am"`
- Options:
- `--workspace <url-or-unique-substring>` (needed for channel _names_ across multiple workspaces)
- `--thread-ts <seconds>.<micros>` (optional, channel mode only)
- `--attach <path>` (repeatable; upload local files as attachments)
- `--blocks <path>` raw Block Kit blocks from a JSON file (or `-` for stdin). Bypasses markdown-to-rich-text conversion; enables header/divider/section/table blocks. Cannot be combined with `--attach`.
- `--reply-broadcast` when replying in a thread, also post the reply to the parent channel. For channel targets, pair with `--thread-ts`; for URL targets, the thread context is derived from the message. Not supported for DM targets; cannot be combined with `--attach`.
- `--schedule <time>` schedule delivery at an ISO 8601 timestamp with explicit timezone (or Unix timestamp). Must be in the future and within Slack's 120-day scheduled-send limit. Compatible with `--blocks`, `--thread-ts`, and `--reply-broadcast`; cannot be combined with `--attach`.
- `--schedule-in <duration>` schedule delivery after a duration or simple future phrase (`30m`, `3h`, `2d`, `tomorrow 9am`, `monday 9am`; phrases use your local timezone). Mutually exclusive with `--schedule`; cannot be combined with `--attach`.

- `agent-slack message scheduled list`
- Lists pending scheduled messages from Slack's server-side scheduled message queue.
- Options:
- `--workspace <url-or-unique-substring>` (defaults to configured workspace)
- `--channel <channel>` filter to a channel/DM id or channel name
- `--oldest <unix-ts>` only messages scheduled after this time
- `--latest <unix-ts>` only messages scheduled before this time
- `--cursor <cursor>` fetch the next page
- `--limit <n>` max messages to return

- `agent-slack message scheduled cancel <scheduled_message_id>`
- Cancels a pending scheduled message before it is sent.
- Options:
- `--channel <channel>` required channel/DM id or channel name for the scheduled message
- `--workspace <url-or-unique-substring>` (needed for channel _names_ across multiple workspaces)

- `agent-slack message edit <target> <text>`
- URL target edits that exact message.
Expand Down
11 changes: 11 additions & 0 deletions skills/agent-slack/references/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ All commands print JSON to stdout.
- `ts?: "<seconds>.<micros>"` — the posted message's ts; absent on file-attachment sends
- `thread_ts?: "<seconds>.<micros>"` — present only when the send was into an existing thread
- `permalink?: "https://.../archives/..."` — present when `ts` is known and a workspace URL was resolvable
- `scheduled_message_id?: "Q..."` — present for `--schedule` / `--schedule-in` sends
- `post_at?: <unix-seconds>` — present for scheduled sends

- `message scheduled list` returns:
- `scheduled_messages: [{ id, channel_id, post_at, date_created?, text? }]`
- `next_cursor?: "<cursor>"`

- `message scheduled cancel` returns:
- `ok: true`
- `channel_id: "C..." | "D..."`
- `scheduled_message_id: "Q..."`

Message payload fields keep canonical user IDs (for example `author.user_id`, reaction `users[]`, and `@U...` mentions in rendered content).
`referenced_users` provides display metadata for those IDs. The cache is per-workspace with a 24-hour per-entry TTL.
Expand Down
50 changes: 49 additions & 1 deletion src/cli/message-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { formatOutboundSlackText } from "../slack/format-outbound.ts";
import type { SlackApiClient } from "../slack/client.ts";
import { uploadLocalFileToSlack } from "../slack/upload.ts";
import { buildSlackMessageUrl } from "../slack/url.ts";
import { resolveSchedulePostAt } from "../slack/scheduled-messages.ts";

function loadBlocksFromPath(path: string): unknown[] {
const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
Expand Down Expand Up @@ -111,16 +112,27 @@ export async function sendMessage(input: {
attach?: string[];
blocks?: string;
replyBroadcast?: boolean;
schedule?: string;
scheduleIn?: string;
};
}): Promise<Record<string, unknown>> {
const target = parseMsgTarget(String(input.targetInput));
const attachPaths = normalizeAttachPaths(input.options.attach);
const postAt = resolveSchedulePostAt({
schedule: input.options.schedule,
scheduleIn: input.options.scheduleIn,
});
if (postAt !== undefined && attachPaths.length > 0) {
throw new Error(
"--schedule/--schedule-in cannot be combined with --attach (Slack scheduled messages do not support file uploads).",
);
}
const formattedText = formatOutboundSlackText(input.text);
const blocks = input.options.blocks
? loadBlocksFromPath(input.options.blocks)
: input.text
? textToRichTextBlocks(input.text)
: null;
const attachPaths = normalizeAttachPaths(input.options.attach);

if (target.kind === "url") {
const { ref } = target;
Expand All @@ -140,6 +152,7 @@ export async function sendMessage(input: {
threadTs,
replyBroadcast: input.options.replyBroadcast,
attachPaths,
postAt,
});
},
});
Expand All @@ -162,6 +175,7 @@ export async function sendMessage(input: {
text: formattedText,
blocks,
attachPaths,
postAt,
});
},
});
Expand Down Expand Up @@ -189,6 +203,7 @@ export async function sendMessage(input: {
threadTs: input.options.threadTs ? String(input.options.threadTs) : undefined,
replyBroadcast: input.options.replyBroadcast,
attachPaths,
postAt,
});
},
});
Expand Down Expand Up @@ -216,7 +231,29 @@ async function sendMessageToChannel(input: {
threadTs?: string;
replyBroadcast?: boolean;
attachPaths: string[];
postAt?: number;
}): Promise<Record<string, unknown>> {
if (input.postAt !== undefined) {
const resp = await input.client.api("chat.scheduleMessage", {
channel: input.channelId,
text: input.text,
post_at: input.postAt,
thread_ts: input.threadTs,
...(input.blocks ? { blocks: input.blocks } : {}),
...(input.replyBroadcast && input.threadTs ? { reply_broadcast: true } : {}),
});
const channelId = typeof resp.channel === "string" ? resp.channel : input.channelId;
const scheduledMessageId =
typeof resp.scheduled_message_id === "string" ? resp.scheduled_message_id : undefined;
return {
ok: true,
channel_id: channelId,
scheduled_message_id: scheduledMessageId,
post_at: getResponseNumber(resp.post_at) ?? input.postAt,
thread_ts: input.threadTs,
};
}

if (input.attachPaths.length === 0) {
const resp = await input.client.api("chat.postMessage", {
channel: input.channelId,
Expand Down Expand Up @@ -270,6 +307,17 @@ async function sendMessageToChannel(input: {
};
}

function getResponseNumber(value: unknown): number | undefined {
if (typeof value === "number") {
return value;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}

export async function editMessage(input: {
ctx: CliContext;
targetInput: string;
Expand Down
Loading
Loading