132 Commits

Author SHA1 Message Date
Paymahn Moghadasian d781728c6c feat: slack-cli — a no-daemon CLI over the slack-mcp-server engine (#1)
* feat: add slack-cli, a no-daemon CLI over the slack-mcp-server engine

Turn the forked slack-mcp-server into a CLI so running many agents no
longer means one resident MCP process each. Every command is a
short-lived process that reads the shared on-disk cache.

- rename module to github.com/paymog/slack-cli (go install/homebrew/ldflags)
- internal/toolcall: invoke the upstream tool handlers in-process; the only
  mcp-go coupling lives here, so pkg/handler and pkg/provider are reused
  byte-for-byte (clean upstream merges, fork-and-extend)
- internal/{cli,cmds,config,credstore,runtime,output}: cobra command tree,
  keyring-backed credential profiles, provider bootstrap, result printing
- 21 tools as subcommands (channels, conversations, users, usergroups,
  saved, reactions, attachments, cache); write tools keep their env gating
- goreleaser + homebrew release workflow; ships a skills/slack-cli skill
- unit tests for config/credstore/toolcall; MCP server still builds

The MCP server (cmd/slack-mcp-server) is kept intact.

* chore(napkin): record real-workspace verification

* docs: explain how the CLI works (in-process handler invocation, shared cache)
2026-06-26 12:45:38 -05:00
Dmitrii Korotovskii c88ed62a61 fix: pass ctx to convertMessagesFromHistory calls in saved.go
PR #269 added a leading ctx parameter to convertMessagesFromHistory
but missed the three call sites in saved.go, breaking the build.
2026-05-14 23:09:33 +02:00
Dmitrii Korotovskii 342e3e3cd2 Merge pull request #269 from gkatz2/feature/targeted-user-cache-patch
Add targeted user cache patch on miss
2026-05-14 23:01:12 +02:00
Dmitrii Korotovskii 19e8a0c7b1 test: fix TestIntegrationChannelsListQueryFilter to assert on Name column
The integration test added in #252 was reading row[0], which is the channel
ID column in channels_list CSV output, not the channel name. The CI for
#252 never ran (status checks were empty at merge time), so this slipped
through. Now resolves to the Name column by header lookup, matching the
convention used elsewhere in this file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:32:53 +02:00
derodero24 c298e5cf9c feat: extract email metadata from file-type messages
When emails are forwarded to Slack channels, message content is stored in
files[] with filetype "email" rather than in text or blocks. This adds
FilesToText() to extract From, CC, and Subject metadata as a fallback
when msg.Text is empty, so these messages no longer appear as blank rows
in conversations_history output.

Closes #191
2026-05-14 22:27:26 +02:00
George Bashi ae3c7b4f13 perf: remove sort param from channels_me, always use fast path
The sort=popularity option forced fetching every channel the user belongs
to before sorting client-side — hundreds of API calls on large workspaces.
The Slack API doesn't support server-side sorting, so this was inherently
expensive. Remove it and always stop fetching once we have enough results,
using the Slack API's native cursor for pagination.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 22:20:31 +02:00
George Bashi 6f0114928a perf: stop fetching all API pages in channels_me when not needed
When sort is not "popularity", stop paginating the Slack API as soon as
we have enough results for the requested limit, and pass through the
API's native cursor. On large workspaces this avoids hundreds of API
calls when only a small number of channels are requested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 22:20:31 +02:00
George Bashi 55842fe2c8 feat: add channels_me tool
Add a new MCP tool to list channels the calling user is a member of,
using the users.conversations API. Follows the same pattern as
usergroups_me vs usergroups_list.

Unlike channels_list which returns all workspace channels, channels_me
returns only channels the user has joined — useful on large workspaces
where channels_list returns thousands of results.

Supports channel_types, sort (by popularity), limit, and cursor
parameters.
2026-05-14 22:20:31 +02:00
Dmitrii Korotovskii e49db6aa24 Merge pull request #252 from unsafe9/feature/channels-list-query-filter
feat: add query and query_targets parameters to channels_list tool
2026-05-14 22:11:40 +02:00
Dmitrii Korotovskii 69cd994d9d Merge pull request #240 from jonzarecki/feat/saved-items
feat: add saved_list, saved_update, and saved_clear_completed tools
2026-05-14 22:11:31 +02:00
Dmitrii Korotovskii 21775f640d Merge pull request #237 from vbichkovsky/feat/native-image-content
feat: return image attachments as native MCP image content
2026-05-14 22:10:26 +02:00
Greg Katz 2d19f8c752 Add targeted user cache patch on miss
When a user ID is not in the in-memory cache, message rendering
degrades to raw IDs and paramFormatUser fails the tool call
entirely. This is common on Enterprise Grid workspaces where the
user cache (50K+ users) can be hours stale.

On cache miss, fetch the single user via users.info and patch the
snapshot atomically. This costs one API call instead of rebuilding
the entire user cache. Disk persistence is skipped; the next full
refresh cycle handles it.

Fixes #268

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 12:03:17 -07:00
Dmitrii Korotovskii 29a73b191a Merge pull request #294 from arnstarn/feat/blocks-and-dep-bump
feat: add blocks parameter and bump slack-go-util to v0.4.0
2026-05-14 21:03:14 +02:00
Dmitrii Korotovskii 5cb7c2c12c Merge pull request #190 from derodero24/feat/extract-block-kit-text
feat: extract text content from message blocks in conversations_history
2026-05-14 21:02:19 +02:00
Dmitrii Korotovskii 9e9f84cfba Merge pull request #219 from georgebashi/feat/conversations-join-leave
feat: add conversations_join and conversations_leave tools
2026-05-14 20:47:36 +02:00
Dmitrii Korotovskii 262f0bc78e Merge pull request #279 from bkbaheti/fix/include-file-names-in-attachment-ids
Include file names in AttachmentIDs for message responses
2026-05-14 20:47:26 +02:00
Dmitrii Korotovskii 78005d34da Merge pull request #284 from clwluvw/slack-add-msg
fix: return plain confirmation from conversations_add_message
2026-05-14 20:47:21 +02:00
Dmitrii Korotovskii 8c4614ccda Merge pull request #262 from flacoste/fix/search-rate-limit-retry
fix: add rate limiting and retry to SearchContext
2026-05-14 20:46:32 +02:00
Arnold Mendez 97b24505c9 feat: add blocks parameter and bump slack-go-util to v0.4.0
Add optional `blocks` parameter to conversations_add_message for raw
Slack Block Kit JSON support (rich_text lists, code blocks, etc.).

When blocks is provided it takes precedence over text/content_type for
message rendering. The text parameter serves as notification fallback.

The blocks argument accepts both a JSON string and a raw JSON array to
accommodate different MCP client serialization behaviors.

Also bumps takara2314/slack-go-util from v0.3.0 to v0.4.0 which adds
nested list support to the existing text/markdown conversion path.
2026-05-07 10:10:53 -04:00
Seena Fallah 2cfccba8ca fix: return plain confirmation from conversations_add_message
The handler was re-fetching the just-posted message via conversations.history
and returning it as CSV. Slack's history endpoint has propagation lag, so when
the message wasn't yet indexed the response was a header-only CSV with no data
rows, which confused LLM clients into thinking the post had failed.

Drop the follow-up history call and return a short success string with channel
and ts (matching ReactionsAddHandler). This removes the race entirely and
saves a tier3 API call per post.

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
2026-04-26 22:01:25 +02:00
Braj Baheti cf81046419 Include file names alongside file IDs in AttachmentIDs field
Previously, the AttachmentIDs field only contained raw file IDs
(e.g. "F08ABC1234"), making it impossible to identify which file
an ID corresponds to without calling attachment_get_data first.

Now the field includes filenames: "F08ABC1234 (contract.pdf)".
This makes it practical to use AttachmentIDs to selectively
download relevant attachments.

Fixes #260
2026-04-15 20:46:05 +05:30
Tim Mastny 6c90a87c29 Add channel ID and permalink to search results
The Slack search API returns both Channel.ID and Permalink on every
SearchMessage, but the MCP server was only using Channel.Name (formatted
as '#channel-name') and discarding the rest. This made it impossible for
LLM agents to construct valid Slack permalink URLs.

- Add Permalink field to Message struct
- Include msg.Channel.ID in the Channel column (format: 'C0515UGHR0R (#channel-name)')
- Pass through msg.Permalink from the Slack API response

Addresses korotovsky/slack-mcp-server#100.
2026-03-29 20:03:15 -05:00
Francis J. Lacoste 68afbcc541 fix: add rate limiting and retry to SearchContext
SearchContext was the only Slack API call in conversations.go without
rate limiting or retry logic. Under concurrent load (e.g. parallel
searches across multiple workspaces), this caused immediate failures
when hitting Slack's Tier 2 rate limits.

Wrap the call with limiter.CallWithRetry using a Tier2 rate limiter,
matching the established pattern used by GetConversationHistoryContext
and GetConversationInfoContext in the same file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:59:56 -04:00
unsafe9 4d7d872310 test: verify non-matching channels are excluded from query results 2026-03-25 10:31:13 +09:00
unsafe9 ab1cb7d1a1 refactor: align query_targets parsing with channel_types pattern
Use map[string]bool instead of struct, parse inline in handler with
warn logging for invalid values, matching filterChannelsByTypes style.
2026-03-25 01:05:16 +09:00
unsafe9 06fa3b1931 feat: add query and query_targets parameters to channels_list tool
Closes #251. Adds keyword filtering to channels_list so users can find
channels without paginating through the entire list. Matches are
case-insensitive substrings; query_targets controls which fields to
search (name by default, optionally topic and purpose).
2026-03-25 00:51:57 +09:00
Valentin eb4d63b944 feat: return image attachments as native MCP image content
Previously, attachment_get_data returned image files as base64-encoded
strings inside a JSON text response. For typical images (100-200KB),
the base64 expansion produces 130-270KB of text that exceeds MCP client
token limits, forcing clients to save overflow to temp files and manually
decode base64 — defeating the purpose of the tool.

Use the MCP SDK's NewToolResultImage to return images as native image
content, which MCP clients can render directly. File metadata (file_id,
filename, mimetype, size) is returned as the text component. Non-image
binary files retain the existing base64-in-text behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:17:01 +01:00
Jonathan Zarecki 06b7e6bba3 fix: saved_list drops items when message fetch fails
When include_messages=true, items whose message content could not be
fetched (e.g. channel access denied) were silently dropped from the
output due to a `continue` that skipped the fetched counter.

Changes:
- Move fetched++ before the includeMessages block so all items count
- Let message fetch failures fall through instead of skipping the item
- Add a note to output when some items lack message content
- Fall back to metadata CSV when all message fetches fail

Fixes: 4 of 11 saved items silently dropped on enterprise Slack
Made-with: Cursor
2026-03-09 14:14:10 +02:00
Jonathan Zarecki 3617d5ee65 feat: add saved_list, saved_update, and saved_clear_completed tools
Add three new tools for managing Slack's "Save for Later" panel,
replacing the deprecated stars.* API (March 2023). Uses internal
saved.list, saved.update, and saved.clearCompleted endpoints.

- saved_list: List saved items with filter (saved/completed/archived),
  auto-pagination, and optional message content fetching
- saved_update: Mark items as completed and/or set due dates
- saved_clear_completed: Bulk-clear all completed items

All tools require browser session tokens (xoxc/xoxd) and are guarded
at registration time for bot/OAuth tokens.

Includes unit tests for CSV format, field extraction, timestamp
formatting, parameter validation, and response parsing.

Made-with: Cursor
2026-03-08 22:07:54 +02:00
George Bashi 110ae5a42f feat: add conversations_join and conversations_leave tools
Add two new MCP tools for channel membership management:

- conversations_join: Join public channels via conversations.join API.
  Idempotent — joining a channel you're already in is a no-op.
  Requires channels:join (xoxb) or channels:write (xoxp) scope.

- conversations_leave: Leave channels, group conversations, or DMs
  via conversations.leave API. Marked as destructive.
  Requires channels:manage (xoxb) or channels:write (xoxp) scope.
  On Enterprise Grid with session tokens (xoxc/xoxd), routes through
  the edge API to bypass enterprise_is_restricted errors.

Both tools accept channel IDs (Cxxxxxxxxxx) or names (#channel-name)
using the existing resolveChannelID helper.

Updated docs/01-authentication-setup.md with new OAuth scopes
(channels:join, channels:manage) and docs/03-configuration-and-usage.md
with the new tool names in the available tools list.
2026-03-03 15:20:18 -08:00
Manuel Kreutz f50473c33c perf: use users.conversations instead of conversations.list for xoxp scan (#7)
Switch the xoxp unread scan from conversations.list to users.conversations.
users.conversations returns only channels the calling user is a member of,
eliminating non-member public channels and closed DMs that cannot have unreads.

Empirically tested on a 2700-channel workspace:
- 37% fewer channels to scan (1724 vs 2737)
- 85% reduction for public channels (156 vs 1046)
- Zero real unreads missed (verified against client.counts ground truth)
- Both methods return identical channel sets for member channels
- The 10 'missed' channels are all archived with never-visited markers
2026-02-23 09:40:37 -06:00
Manuel Kreutz b540e8ee16 fix: add rate limiting and retry to includeMessages history calls (#5)
The includeMessages loop in getUnreadsViaConversationsInfo calls
GetConversationHistoryContext with no rate limiter and no retry — the
only conversations.history call in the xoxp path not wrapped in
CallWithRetry.

When include_messages=true and multiple unread channels are found,
these calls fire unthrottled, risking 429s that silently skip channels
and lose messages.

Fix: add a local Tier 3 rate limiter and wrap in CallWithRetry,
matching the pattern used everywhere else in the xoxp scan path.
2026-02-19 16:25:43 -06:00
Manuel Kreutz 7224ff65ca fix: add proactive rate limiting and retry for xoxp conversations.info/history calls (#4)
The xoxp fallback in conversations_unreads calls GetConversationInfoContext
per channel to check for unreads. slack-go's standard Client does NOT
auto-retry on HTTP 429 — it returns *slack.RateLimitedError to the caller.

The scan loop handled ALL errors with continue at Debug level, so rate
limit errors were silently swallowed, causing non-deterministic results
(niebloomj confirmed 279/400 channels hit rate limits on their workspace).

This commit adds:
- limiter.CallWithRetry[T] generic helper in pkg/limiter: proactive
  rate.Limiter.Wait + retry via caller-provided retryAfter callback
  (no slack-go dependency in the limiter package)
- slackRetryAfter helper in the handler to extract RetryAfter from
  *slack.RateLimitedError
- Applied to both GetConversationInfoContext and
  GetConversationHistoryContext in scanTypeGroupForUnreads
- rateLimited counter surfaced in scan metadata so the LLM response
  includes a WARNING when channels are skipped
- Rate limit errors promoted from Debug to Warn level
- 8 unit tests in pkg/limiter covering retry logic, exhaustion,
  context cancellation, and typed return values
2026-02-16 15:55:50 -06:00
Manuel Kreutz 43a2ec1145 fix: token-type routing and validation for conversations_unreads (#3)
* fix: pre-check token type to avoid faulty ClientCounts call

client.counts only works with browser session tokens (xoxc/xoxd). OAuth
tokens (xoxp) get 'not_allowed_token_type' and bot tokens (xoxb) have no
concept of user-level unreads.

Instead of calling ClientCounts and catching the error after the fact,
pre-check the token type using the existing IsOAuth()/IsBotToken()
infrastructure and route directly to the appropriate path:
- xoxc/xoxd: fast path via client.counts (unchanged)
- xoxp: conversations.info fallback (no wasted API call)
- xoxb: clear error message

Follows the same pattern used by SearchUsers and GetConversationsContext
which already branch on token type.

Addresses review comments on korotovsky/slack-mcp-server#171.

* fix: exclude conversations_unreads tool for bot tokens

Bot tokens (xoxb) don't support unread tracking — it's a user-level
concept. Don't register the tool at all for bot token users, matching
the same pattern used for conversations_search_messages.

This gives a clean UX: bot users simply don't see the tool, rather
than getting a runtime error.

* fix: cap xoxp fallback scan depth and document limitations

The xoxp path previously scanned ALL channels (~2000+ API calls on large
workspaces). Slack's API has no bulk unread endpoint for xoxp tokens —
every other open-source implementation (agent-kit, NextNotifier, wee-slack)
uses the same per-channel scanning approach.

Changes:
- Cap scan depth to budget*2 per type group (~300 API calls max with
  default max_channels=50, down from ~2000+)
- Return scan metadata (channels scanned, API calls) from each type group
- Prepend an xoxp limitation note to response text so the LLM knows
  results may be partial
- Update tool description to explain xoxc vs xoxp behavior
- Fix inaccurate comment about conversations.list sort order

* fix: validate GetMutedChannels response to surface xoxp failures

Add validate() call after ParseResponse in GetMutedChannels (prefs.go),
matching the pattern used in ClientCounts. Without this, xoxp tokens
receive {ok:false, error:missing_scope} but ParseResponse only checks
HTTP status — the error was silently swallowed, returning nil/nil.

This caused muted channels to leak into xoxp results (17/28 channels
were muted in testing). Now the error propagates to the handler's
existing warn-and-proceed logic.

Also surface the limitation in the xoxp response note so the LLM
knows muted filtering is unavailable.

* fix: add shouldAddTool gating for conversations_unreads and conversations_mark

Both tools were missing Tool* constants, ValidToolNames entries, and
shouldAddTool() wrapping — breaking the established pattern used by
every other tool in the server. This prevented selective enable/disable
via the enabledTools config.

Also fixes indentation on conversations_mark registration block.
2026-02-13 14:01:09 -06:00
Manuel Kreutz 3e0e4ea792 fix: unread count persistence + backfill + muted channel filtering (#2)
* fix: use index-based range loop so unread counts persist

The message-fetch loop uses 'for _, uc := range unreadChannels', so
writes to uc.UnreadCount go to a copy — the original slice element
stays at 0. Messages are fetched correctly (appended to a separate
slice), but the unread count column is silently wrong.

Switch to 'for i := range' and index into the slice directly.
Also aligns struct field formatting (gofmt).

* feat: backfill unread counts via conversations.history

client.counts returns HasUnreads (bool) and MentionCount per channel,
but MentionCount is only non-zero for @mentions. Regular channel
activity shows as 'has unreads' with count 0.

For channels where HasUnreads=true and MentionCount=0, call
conversations.history(oldest=lastRead, limit=20) to count actual
unread messages. DMs don't need this since every message counts
as a mention.

* feat: filter muted channels from unreads by default

Fetches muted channel set from users.prefs.get (all_notifications_prefs)
and excludes them from both ClientCounts and conversations.info fallback
paths. Adds include_muted opt-in parameter to show muted channels when
explicitly requested.

Muted status is only available in users.prefs.get as a nested JSON
string — it is not exposed by client.counts, conversations.info, or
conversations.list.
2026-02-11 19:43:05 -06:00
flare576 e238772b7e feat: add xoxp token fallback for conversations_unreads using conversations.info
Co-authored-by: niebloomj <5156403+niebloomj@users.noreply.github.com>
2026-02-11 17:11:17 -06:00
Saoud Rizwan 9cbbecb270 refactor: address PR review feedback
- Extract handler params to structs with parsing functions
  (unreadsParams, markParams) following existing patterns
- Add SLACK_MCP_MARK_TOOL env var guard for conversations_mark
  (disabled by default, requires explicit opt-in)
- Remove unused categorizeChannel and getChannelDisplayName functions
- Update README with conversations_mark safety note and env var docs
2026-02-11 17:10:48 -06:00
Saoud Rizwan f695d2c474 fix: use IsExtShared metadata instead of name prefix for partner channels
- Add IsExtShared field to Channel struct in cache
- Pass IsExtShared through mapChannel function
- Use cached.IsExtShared to identify external/partner channels
  instead of checking for ext-/shared- name prefixes

Note: Users may need to delete their channels cache file to repopulate
with the new IsExtShared field.
2026-02-11 17:10:04 -06:00
Saoud Rizwan cfde97c945 feat: add mentions_only filter and conversations_mark tool
- Add mentions_only parameter to conversations_unreads to filter
  channels to only those with @mentions (priority inbox)
- Add conversations_mark tool to mark channels/DMs as read
  - Supports channel IDs, #channel names, and @username
  - If no timestamp provided, marks all messages as read
2026-02-11 17:10:03 -06:00
Saoud Rizwan 456a8c0153 fix: handle # prefix in channel names and fix partner categorization
- Strip existing # prefix before adding to avoid ##name
- Use stripped name for ext-/shared- prefix checks for partner type
2026-02-11 17:10:03 -06:00
Saoud Rizwan b1c64345c4 fix: use ClientCounts API for reliable unread detection
- Switch from ClientUserBoot to ClientCounts API
- ClientCounts returns HasUnreads boolean for all channels
- Add ClientCounts to SlackAPI interface
- Process Channels, MPIMs, and IMs separately
2026-02-11 17:10:03 -06:00
Saoud Rizwan 7fc9c7f08c feat: add conversations_unreads tool for efficient unread message retrieval
- Uses ClientUserBoot to get all channels with LastRead/Latest in one API call
- Filters channels where Latest > LastRead to find unreads
- Prioritizes: DMs > group DMs > partner channels (ext-*) > internal
- Only fetches message history for channels with actual unreads
- Supports filtering by channel type and configurable limits

Addresses issue #114
2026-02-11 17:09:21 -06:00
sspiridonov d5ed2c4834 Merge branch 'master' into feat/usergroups-management 2026-02-08 23:30:14 +01:00
sspiridonov 9b29f7d907 Merge origin/master into feature/enabled-tools-flag
Resolve conflict in README.md by keeping updated env var descriptions
that document the SLACK_MCP_ENABLED_TOOLS interaction.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 22:23:46 +01:00
sspiridonov 829a70065c refactor: unify shouldAddTool function and clean up comments
- Merge shouldAddTool and shouldAddWriteTool into single function
- Remove redundant comments
- Add envVarName parameter for write tools requiring explicit enablement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 20:55:49 +01:00
sspiridonov 2ef39feae8 feat: don't register write tools unless explicitly enabled
Write tools (conversations_add_message, reactions_add, reactions_remove,
attachment_get_data) now require explicit enablement:
- If ENABLED_TOOLS explicitly includes the tool, register it
- If ENABLED_TOOLS is empty, only register if tool-specific env var is set
- If ENABLED_TOOLS excludes the tool, don't register

This resolves the conflict where ENABLED_TOOLS="" would register all tools
but SLACK_MCP_ADD_MESSAGE_TOOL="" would fail at runtime with an error.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 20:31:35 +01:00
sspiridonov 9afc063f24 feat: add user group management tools
Add five new tools for managing Slack user groups (subteams):

- usergroups_list: List all user groups in the workspace
- usergroups_me: Manage your own membership (list/join/leave)
- usergroups_create: Create a new user group
- usergroups_update: Update group metadata (name, handle, description)
- usergroups_users_update: Replace all members of a group

The usergroups_me tool provides a convenient way to join or leave
groups without needing to know the full member list. It handles
fetching current members and updating the list automatically.

Required OAuth scopes:
- usergroups:read (for list operations)
- usergroups:write (for create/update/join/leave operations)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 20:14:45 +01:00
sspiridonov d012daea6e fix: rename payload parameter to text in conversations_add_message
The parameter name was inconsistent with the error message which said
"text must be a string". Changed to use "text" as the primary parameter
name with backward compatibility for "payload".

Fixes #181

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:02:40 +01:00
derodero24 278ac36216 feat: extract text content from Block Kit structures in messages
Messages using Slack's Block Kit format (emails forwarded via Slack,
bot notifications from Grafana/Datadog, app messages) return empty
text when retrieved via conversations_history. This extracts text
from block structures so these messages are no longer empty.

Closes #186
2026-02-07 19:26:33 +09:00
sspiridonov eaf9719421 fix: correct channel allowlist/blocklist logic
The isChannelAllowedForConfig() function had inverted return logic.
When a channel was NOT found in the list, it returned the opposite
of what was intended:
- Allowlist mode incorrectly allowed unlisted channels
- Blocklist mode incorrectly blocked unlisted channels

Fixed by changing 'return !isNegated' to 'return isNegated'.
Added unit tests for isChannelAllowedForConfig().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 00:02:04 +01:00