- Concurrently fetch conversations.info and conversations.history instead of sequentially
- Utilize client.counts' unread_count and unread_count_display to prevent history fetches for --counts-only
- Change filtering to use a system subtype denylist instead of a broad allowlist to prevent missing custom messages or file shares
- Fix mangled ellipsis character in truncated messages
Uses Slack's internal client.counts API (available with xoxc browser
tokens) to efficiently fetch unread state across all conversations in a
single call, then hydrates each channel with conversations.info for
names and conversations.history for actual unread messages.
Features:
- Shows unread messages across channels, DMs, group DMs, and threads
- Sorts by mention count then unread count (most important first)
- Resolves DM display names via users.info
- Filters system messages (joins/leaves/topic changes) by default
- --counts-only mode for lightweight overview
- --max-messages and --max-body-chars for token budget control
- --include-system to show filtered system messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add later command for managing saved-for-later messages
Adds `agent-slack later` with subcommands for managing Slack's Later tab:
list, complete, archive, reopen, save, remove, and remind.
Uses Slack's internal saved.update API (with xoxc browser tokens) to
manage saved items. Key implementation detail: saved.update requires
multipart/form-data encoding — the standard form-urlencoded format
silently ignores state-change parameters like `mark`. Adds apiMultipart()
to SlackApiClient for this.
The `mark` parameter controls state transitions:
- completed/uncompleted: toggle completion
- archived/unarchived: toggle archive
- date_due (unix ts): set/clear reminder on item
Includes 23 unit tests covering API calls, state filtering, message
hydration, DM name resolution, content truncation, and duration parsing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback for later command
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* feat: add workflow discovery and execution
Add `workflow` command with subcommands:
- `list` — discover workflows bookmarked in a channel
- `preview` — get trigger metadata
- `get` — get workflow definition including form fields
- `run` — trip a workflow trigger
* fix: use trigger_id instead of bookmark_id in trip context
The context payload passed to workflows.triggers.trip was labeling the
trigger ID as bookmark_id, which is misleading. Renamed to trigger_id
to accurately reflect what the value represents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Introduce SlackDownloadError and tryDownloadSlackFile wrapper that
distinguishes network/HTTP errors (caught) from disk/write errors
(propagated). Failed downloads now produce DownloadResult objects
that flow through toCompactMessage, preserving file metadata so
content-type filtering works correctly even when downloads fail.
A 401/404 on a single file attachment (deleted file, no read
permission) would throw through downloadSlackFile and kill the
entire search operation. The message-read path already handled
this gracefully via downloadMessageFiles' per-file try/catch,
but search-messages and search-files called downloadSlackFile
directly with no error handling.
Wrap each downloadSlackFile call in try/catch { continue } so
failed downloads are skipped and remaining results still return.
* Add `channel mark` command to mark conversations as read
Wraps Slack's `conversations.mark` API. Accepts message URLs,
channel names/IDs, or DM channel IDs. Works with channels, DMs,
and group DMs.
* Reject --workspace when target is a URL in channel mark
URL targets are authoritative for workspace — allowing --workspace
to override creates a footgun where a permalink from workspace A
could be executed against workspace B. This aligns channel mark
with the existing message command behavior.
Adds tests for URL workspace rejection and --ts validation.
* Reject --ts when target is a URL in channel mark
A URL is a fully-specified target (workspace + channel + ts), so
allowing --ts to override the timestamp is inconsistent with the
existing --workspace rejection. If you want a different ts, use
the #channel --ts form instead.
Adds test for URL --ts rejection and updates commands.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Allow --ts override with URL targets in channel mark
A URL provides channel + workspace + ts, but --ts override is useful
(e.g. marking to just before a message to make it appear unread).
This differs from --workspace which remains rejected since overriding
the workspace is a genuine footgun.
Adds test for --ts override with URL targets and updates commands.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Refactor decryptChromiumCookieValue to use options object (max-params)
- Remove unused imports in message-actions.ts and context.ts
- Extract getSafeStoragePasswords and decryptCookieWindows to desktop-crypto.ts (max-lines)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, getSlackPaths() returned only the first Slack data directory
found. On macOS, if both the Electron (~/Library/Application Support/Slack)
and App Store (~/Library/Containers/com.tinyspeck.slackmacgap/...) directories
existed, the Electron path was always picked — even if its cookie could not
be decrypted (e.g. stale data from a previous installation).
Now getAllSlackPaths() returns all valid candidate paths, and
extractFromSlackDesktop() tries each one until both LevelDB team extraction
and cookie decryption succeed. The error message now reports all attempted
paths with their individual failures.
For Slack Connect users (external org), the `users.info` API response
does not include `real_name` at the top level — it only appears inside
`profile.real_name`. This causes `user get` to return only the handle
(e.g. "bob") instead of the full name (e.g. "Bob Smith").
Fall back to `profile.real_name` when the top-level field is missing.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat[users]: add dm-channel command for discovering DM and group DM channels
* feat[users]: add dm-open command for discovering DM and group DM channels
- Replace execSync with execFileSync for PowerShell invocation to bypass
cmd.exe shell interpolation, consistent with every other exec call in
the file. Eliminates double-quote injection surface.
- Add existence check for decrypted key file after PowerShell DPAPI call
to provide a meaningful error instead of cryptic ENOENT.
- Update JSDoc to document v10/v11 prefix support (both are handled).
- Remove unused execSync import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: implement file attachment functionality for messages
- feat: implement file attachment functionality for messages
- initial implementation of attaching files to a message
- feat(message): auto-detect bullet lists and send as rich_text blocks (#45)
- fix(lint): add curly braces to single-line if statements in users.ts
* feat(send): make message text optional when attaching files
No longer need to pass a throwaway "Files" text when the intent is
just to upload attachments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove require("node:crypto") in ESM module; use existing top-level
createDecipheriv import instead (fixes ReferenceError in Node ESM)
- Escape single quotes in PowerShell temp file paths to prevent breakage
and command injection for profiles like C:\Users\O'Connor
- Update README auth section to reflect Windows platform support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#44
When `message send` text contains bullet or numbered list patterns
(- , * , • , 1. , etc.), automatically convert to Slack rich_text
blocks so recipients see native editable bullets instead of plain
dashes. Plain text without lists is sent unchanged (zero behavior
change).
Also fixes object/array serialization in browserApi() where String(v)
would produce "[object Object]" — now uses JSON.stringify for objects.
Co-authored-by: Claude <noreply@anthropic.com>
Add support for extracting Slack credentials from the Windows desktop
app (both regular installer and Microsoft Store version).
- Add Windows Slack data paths (%APPDATA%\Slack and Microsoft Store package)
- Implement DPAPI + AES-256-GCM cookie decryption via PowerShell
- Copy Cookies DB to temp file before reading (Windows file locking)
- URL-decode the decrypted cookie value
- Allow refreshFromDesktopIfPossible on win32
The eslint `curly` rule requires braces on all control flow statements.
Seven violations in src/slack/users.ts cause CI to fail on every PR.
Co-Authored-By: Claude <noreply@anthropic.com>
Support importing xoxc/xoxd from Firefox profiles on macOS/Linux so browser auth can be bootstrapped without Chrome or Desktop extraction.
Refs: #42
Signed-off-by: Major Hayden <major@mhtx.net>
Fetches IM conversations in parallel with users.list and enriches each
user object with their DM channel ID (dm_id) when one exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests for call ordering, user-not-found error path, upper bound
clamping, listAllConversations forwarding, and normalizeConversationsPage
edge cases. Remove arbitrary min-10 floor on --limit to support
token-efficient small fetches (e.g. --limit 3).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a Slack user ID (U[A-Z0-9]{8,}) is passed as the target to
`message send`, the CLI now recognizes it as a user target, opens a DM
channel via `conversations.open`, and posts the message there.
Previously, user IDs fell through to the channel-name resolution path,
which called `search.messages` with `in:#U12345...` — a query that
returns no results and falls back to paginating `conversations.list`
across the entire workspace, hanging indefinitely on large workspaces.
Other subcommands (get, list, edit, delete, react, draft) that don't
support DMs now throw a clear error instead of hanging.
Fixes#35
- Check execPath basename to distinguish bun runtime from compiled binary
- Use basename match for node detection instead of substring
- Capture pkg manager stdout to preserve JSON output contract
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detect install method (npm, bun, or standalone binary) and use the
appropriate update strategy. npm/bun installs now run the package
manager directly instead of downloading a GitHub release binary.
Background update notices also show the correct command.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 'By Stably.ai' subtitle under Agent Slack header (links to stably.ai)
- Stably logo in top-right corner (inlined as base64 data URI)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Keep server open on /send failure so user can retry (was silently cancelling)
- Validate /send body has non-empty text string before calling onSend
- Escape < as \u003c in injected config to prevent </script> XSS
- Add 30-minute idle timeout to prevent CLI hanging forever
- Preserve leading indentation in code block serialization (trimEnd vs trim)
- Return ts from sendFn in CI mode for downstream consumers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add resolveChannelName() using conversations.info + users.info for DMs
- URL targets now show human-readable names (e.g. 'Brennan Benson' not 'D05CB2ZQRDF')
- Channel ID targets also resolve to names via resolveChannelName
- Extract and display workspace name from URL in editor header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Match Slack compositor styling (Aa toolbar toggle, square send button,
border colors, bottom bar layout)
- Return message ts from onSend to build permalink after sending
- Show View in Slack link in post-send overlay
- Update README, SKILL.md, and commands.md with draft command docs
- Add editor screenshot for PR
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds `message draft` CLI command that opens a WYSIWYG editor in the
browser for composing Slack messages with full rich text support (bold,
italic, strikethrough, code, code blocks, lists, blockquotes, links).
Features:
- Self-contained HTML editor served via ephemeral HTTP server
- Slack dark theme with Agent Slack branding
- Toolbar + keyboard shortcuts matching Slack (Cmd+B/I/E/K/etc.)
- Code block escape via double-Enter
- Inline link popover with Text + URL fields (Cmd+K)
- mrkdwn source mode toggle
- Thread context with Slack hyperlink
- Sends message directly via Slack API on submit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>