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
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Guiding principle:

- **Token-efficient** — (compact JSON, minimal duplication, and empty/null fields pruned) so LLMs can consume results cheaply.
- **Zero-config auth** — Auth just works if you have Slack Desktop (with fallbacks available). No Python dependency.
- **Human-in-the-loop** — When appropriate (not in CI environments), loop humans in. Ex: `message draft`
- **Human-in-the-loop** — When appropriate (not in CI environments), loop humans in. Ex: `message compose`
<img width="1228" height="741" alt="image" src="https://github.com/user-attachments/assets/92ecbb71-18ca-4516-a874-c83c154b0709" />

## Getting started
Expand Down Expand Up @@ -35,6 +35,7 @@ nix run github:stablyai/agent-slack
- **Search**: messages + files (with filters)
- **Artifacts**: auto-download snippets/images/files to local paths for agents
- **Write**: send now or schedule delivery, edit/delete messages, add reactions (bullet lists auto-render as native Slack rich text)
- **Compose & drafts**: open a browser editor (`message compose`), or manage Slack-native drafts that show up in your Slack client (`message draft`)
- **Channels**: list conversations, create channels, and invite users by id/handle/email
- **Canvas**: create Slack canvases from Markdown and fetch them as Markdown

Expand Down Expand Up @@ -79,7 +80,12 @@ agent-slack
│ ├── scheduled
│ │ ├── list # list pending scheduled messages
│ │ └── cancel <id> # cancel a pending scheduled message
│ ├── draft <target> [text] # open Slack-like editor in browser
│ ├── compose <target> [text] # open Slack-like editor in browser
│ ├── draft # Slack-native drafts (appear in your Slack client)
│ │ ├── list # list Slack-native drafts
│ │ ├── create <target> <text> # create a Slack-native draft
│ │ ├── update <id> <text> # replace a draft's text
│ │ └── delete <id> # delete a draft
│ ├── edit <target> <text> # edit a message
│ ├── delete <target> # delete a message
│ └── react
Expand Down Expand Up @@ -203,24 +209,46 @@ Optional:
agent-slack message get "https://workspace.slack.com/archives/C123/p1700000000000000" --include-reactions
```

### Draft a message (browser editor)
### Compose a message (browser editor)

Opens a Slack-like WYSIWYG editor in your browser for composing messages with full formatting support (bold, italic, strikethrough, links, lists, quotes, code, code blocks).

```bash
# Open editor for a channel
agent-slack message draft "#general"
agent-slack message compose "#general"

# Open editor with initial text
agent-slack message draft "#general" "Here's my update"
agent-slack message compose "#general" "Here's my update"

# Reply in a thread
agent-slack message draft "https://workspace.slack.com/archives/C123/p1700000000000000"
agent-slack message compose "https://workspace.slack.com/archives/C123/p1700000000000000"
```

After sending, the editor shows a "View in Slack" link to the posted message.

`message draft` is send-capable. In CI, it skips the browser editor and immediately sends supplied text; do not use it for a compose-only request in a noninteractive environment.
`message compose` is send-capable. In CI, it skips the browser editor and immediately sends supplied text; do not use it for a compose-only request in a noninteractive environment.

### Slack-native drafts

Manage drafts through Slack's own drafts API, so they show up natively in the user's Slack client (mobile and desktop) ready to review and send. Requires browser-style auth (xoxc/xoxd).

> [!NOTE]
> These commands use Slack's **undocumented** internal `drafts.*` client endpoints (the same ones the Slack app uses), authenticated with your own browser session. They act only as you, on your own drafts — nothing is exposed that you can't already see, and `create` posts nothing. But because the endpoints are unsupported: their behavior may change without notice, and on **Enterprise Grid** this style of session-token API use can be flagged by Slack's security/anomaly detection. This is the same auth model the rest of agent-slack already uses (`later`, `unreads`, `search`); use it where that's acceptable.

```bash
# List unsent drafts
agent-slack message draft list

# Draft a message to a channel (shows up in Slack's Drafts section)
agent-slack message draft create "#general" "Here's my update"

# Draft a thread reply
agent-slack message draft create "https://workspace.slack.com/archives/C123/p1700000000000000" "Looking into it"

# Replace a draft's text, or delete it
agent-slack message draft update "DR_ID" "Here's my revised update"
agent-slack message draft delete "DR_ID"
```

### Reply, edit, delete, and react

Expand Down
3 changes: 2 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
- 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
- Multi-workspace Slack management with automatic workspace resolution
- Human-in-the-loop message drafting via browser-based WYSIWYG rich text editor
- Human-in-the-loop message composing via browser-based WYSIWYG rich text editor (`message compose`)
- Slack-native drafts that appear in the user's Slack client via `message draft` (list/create/update/delete)
- Slack canvas creation from Markdown files or inline content, plus export to Markdown
- Slack channel creation, user invites, and Slack Connect external invites

Expand Down
6 changes: 4 additions & 2 deletions skills/agent-slack/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: agent-slack
description: "Slack CLI for agents: read URLs/threads/history/unreads/later/canvases/workflows, create canvases from Markdown, search messages/files, download attachments, lookup users, list/create/invite channels, open DMs, draft messages, schedule sends, and explicit sends/edits/deletes/reactions/mark-read/uploads."
description: "Slack CLI for agents: read URLs/threads/history/unreads/later/canvases/workflows, create canvases from Markdown, search messages/files, download attachments, lookup users, list/create/invite channels, open DMs, compose messages, manage Slack-native drafts, schedule sends, and explicit sends/edits/deletes/reactions/mark-read/uploads."
---

# agent-slack
Expand All @@ -20,7 +20,7 @@ If a capability named here is absent from installed help, report version skew in

- Read and search freely.
- Perform write actions only when explicitly requested: sends, edits, deletes, reactions, invitations, channel or canvas creation, mark-read operations, scheduling or canceling delivery, uploads, Later state/reminder changes, DM/group-DM creation, and `workflow run`. Workflow runs can execute downstream actions.
- For compose- or review-only requests, return proposed text without invoking Slack. `message draft` is send-capable; use it only when the user explicitly asks to open the interactive editor. In CI or another noninteractive environment, do not invoke it without separate authorization to send immediately: CI skips the editor and sends supplied text.
- For compose- or review-only requests, return proposed text without invoking Slack, or use `message draft create` to add a Slack-native draft the user can review and send (nothing is posted). `message compose` is send-capable; use it only when the user explicitly asks to open the interactive editor. In CI or another noninteractive environment, do not invoke it without separate authorization to send immediately: CI skips the editor and sends supplied text.

## Workflow

Expand All @@ -36,6 +36,8 @@ Named `later remind --in` values such as `tomorrow` or `monday` also use the exe

Ordinary `message send` and `message edit` calls auto-convert lists. `message send --blocks` uses supplied blocks, while `message send --attach` sends its initial comment without automatic list conversion. Inside auto-converted lists, use Slack's `<URL|label>` syntax because CommonMark `[label](URL)` links are not converted into labeled link elements.

Slack-native drafts (`message draft list|create|update|delete`) manage drafts that appear in the user's Slack client; `create` posts nothing. They use undocumented session endpoints and require browser-style auth (xoxc/xoxd).

## Conditional references

- Read [references/targets.md](references/targets.md) only when choosing between a message URL, channel, or user target, or when resolving multiple workspaces.
Expand Down
4 changes: 2 additions & 2 deletions skills/agent-slack/references/targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A URL supplies the workspace, channel, and message timestamp. With a URL target:

- `message get`, `edit`, `delete`, and `react` operate on that message.
- `message list` returns the thread root and all replies.
- `message send` replies in that message's thread. `message draft` does the same unless `--thread-ts` explicitly overrides the URL-derived thread.
- `message send` replies in that message's thread. `message compose` and `message draft create` do the same unless `--thread-ts` explicitly overrides the URL-derived thread.
- `channel mark` marks through that message timestamp; `--ts` explicitly overrides the timestamp. It rejects `--workspace` because the URL supplies it.

Use a channel name or a `C...`, `G...`, or `D...` channel ID only when no URL is available:
Expand All @@ -19,7 +19,7 @@ Use a channel name or a `C...`, `G...`, or `D...` channel ID only when no URL is
- `message list` reads channel history unless `--thread-ts` or `--ts` selects a thread.
- `channel mark` requires `--ts`.

Among `message` subcommands, only `message send` accepts a `U...` or `W...` user ID as a target; it opens or reuses that user's direct-message channel. Treat `U`- and `W`-prefixed user IDs equivalently.
Among `message` subcommands, `message send` and `message draft create` accept a `U...` or `W...` user ID as a target; it opens or reuses that user's direct-message channel. `message draft update --channel` can likewise re-address a draft to a DM. Treat `U`- and `W`-prefixed user IDs equivalently.

Use `user dm-open <users...>` with one to eight other user IDs or handles to get a DM or group-DM channel ID, then use that channel ID for message operations. The authenticated caller is implicit.

Expand Down
4 changes: 2 additions & 2 deletions src/cli/draft-actions.ts → src/cli/compose-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { resolveChannelId, resolveChannelName, normalizeChannelInput } from "../
import { warnOnTruncatedSlackUrl } from "./message-url-warning.ts";
import { openDraftEditor } from "./draft-server.ts";

export async function draftMessage(input: {
export async function composeMessage(input: {
ctx: CliContext;
targetInput: string;
initialText?: string;
Expand All @@ -14,7 +14,7 @@ export async function draftMessage(input: {
const target = parseMsgTarget(String(input.targetInput));
if (target.kind === "user") {
throw new Error(
"message draft does not support user ID targets. Use a channel name, channel ID, or message URL.",
"message compose does not support user ID targets. Use a channel name, channel ID, or message URL.",
);
}

Expand Down
12 changes: 7 additions & 5 deletions src/cli/message-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
sendMessage,
type MessageCommandOptions,
} from "./message-actions.ts";
import { draftMessage } from "./draft-actions.ts";
import { composeMessage } from "./compose-actions.ts";
import { registerScheduledMessageCommand } from "./message-scheduled-command.ts";
import { registerMessageDraftCommand } from "./message-draft-command.ts";

function collectOptionValue(value: string, previous: string[] = []): string[] {
return [...previous, value];
Expand Down Expand Up @@ -253,14 +254,15 @@ export function registerMessageCommand(input: { program: Command; ctx: CliContex
});

registerScheduledMessageCommand({ messageCmd, ctx: input.ctx });
registerMessageDraftCommand({ messageCmd, ctx: input.ctx });

messageCmd
.command("draft")
.command("compose")
.description(
"Open a send-capable rich editor; CI skips the editor and immediately sends supplied text",
"Open a send-capable rich editor in the browser; CI skips the editor and immediately sends supplied text",
)
.argument("<target>", "Slack message URL, #name/name, or channel id")
.argument("[text]", "Initial draft text (sent immediately when CI skips the editor)")
.argument("[text]", "Initial message text (mrkdwn; sent immediately when CI skips the editor)")
.option(
"--workspace <url>",
"Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)",
Expand All @@ -276,7 +278,7 @@ export function registerMessageCommand(input: { program: Command; ctx: CliContex
{ workspace?: string; threadTs?: string },
];
try {
const payload = await draftMessage({
const payload = await composeMessage({
ctx: input.ctx,
targetInput,
initialText: text,
Expand Down
Loading
Loading