mirror of
https://github.com/paymog/slack-cli.git
synced 2026-09-18 23:43:49 +08:00
6a972a1f05
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.
162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
package edge
|
|
|
|
import (
|
|
"context"
|
|
"runtime/trace"
|
|
|
|
"github.com/paymog/slack-cli/pkg/limiter"
|
|
"github.com/paymog/slack-cli/pkg/provider/edge/fasttime"
|
|
"github.com/rusq/slack"
|
|
)
|
|
|
|
// client.* API
|
|
|
|
type clientCountsForm struct {
|
|
BaseRequest
|
|
ThreadCountsByChannel bool `json:"thread_counts_by_channel"`
|
|
OrgWideAware bool `json:"org_wide_aware"`
|
|
IncludeFileChannels bool `json:"include_file_channels"`
|
|
WebClientFields
|
|
}
|
|
|
|
type ClientCountsResponse struct {
|
|
baseResponse
|
|
Channels []ChannelSnapshot `json:"channels,omitempty"`
|
|
MPIMs []ChannelSnapshot `json:"mpims,omitempty"`
|
|
IMs []ChannelSnapshot `json:"ims,omitempty"`
|
|
}
|
|
|
|
type ChannelSnapshot struct {
|
|
ID string `json:"id"`
|
|
LastRead fasttime.Time `json:"last_read"`
|
|
Latest fasttime.Time `json:"latest"`
|
|
HistoryInvalid fasttime.Time `json:"history_invalid"`
|
|
MentionCount int `json:"mention_count"`
|
|
HasUnreads bool `json:"has_unreads"`
|
|
}
|
|
|
|
func (cl *Client) ClientCounts(ctx context.Context) (ClientCountsResponse, error) {
|
|
ctx, task := trace.NewTask(ctx, "ClientCounts")
|
|
defer task.End()
|
|
|
|
form := clientCountsForm{
|
|
BaseRequest: BaseRequest{Token: cl.token},
|
|
ThreadCountsByChannel: true,
|
|
OrgWideAware: true,
|
|
IncludeFileChannels: true,
|
|
WebClientFields: webclientReason("client-counts-api/fetchClientCounts"),
|
|
}
|
|
|
|
resp, err := cl.PostForm(ctx, "client.counts", values(form, true))
|
|
if err != nil {
|
|
return ClientCountsResponse{}, err
|
|
}
|
|
r := ClientCountsResponse{}
|
|
if err := cl.ParseResponse(&r, resp); err != nil {
|
|
return ClientCountsResponse{}, err
|
|
}
|
|
if err := r.validate("client.counts"); err != nil {
|
|
return ClientCountsResponse{}, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
type clientDMsForm struct {
|
|
BaseRequest
|
|
Count int `json:"count"`
|
|
IncludeClosed bool `json:"include_closed"`
|
|
IncludeChannel bool `json:"include_channel"`
|
|
ExcludeBots bool `json:"exclude_bots"`
|
|
Cursor string `json:"cursor,omitempty"`
|
|
WebClientFields
|
|
}
|
|
|
|
type clientDMsResponse struct {
|
|
baseResponse
|
|
IMs []ClientDM `json:"ims,omitempty"`
|
|
MPIMs []ClientDM `json:"mpims,omitempty"` //TODO
|
|
}
|
|
|
|
type ClientDM struct {
|
|
ID string `json:"id"`
|
|
// Message slack.Message `json:"message,omitempty"`
|
|
Channel IM `json:"channel,omitempty"`
|
|
Latest fasttime.Time `json:"latest,omitempty"` // i.e. "1710632873.037269"
|
|
}
|
|
|
|
type IM struct {
|
|
ID string `json:"id"`
|
|
Created slack.JSONTime `json:"created"`
|
|
IsFrozen bool `json:"is_frozen"`
|
|
IsArchived bool `json:"is_archived"`
|
|
IsIM bool `json:"is_im"`
|
|
IsOrgShared bool `json:"is_org_shared"`
|
|
ContextTeamID string `json:"context_team_id"`
|
|
Updated slack.JSONTime `json:"updated"`
|
|
IsShared bool `json:"is_shared"`
|
|
IsExtShared bool `json:"is_ext_shared"`
|
|
User string `json:"user"`
|
|
LastRead fasttime.Time `json:"last_read"`
|
|
Latest fasttime.Time `json:"latest"`
|
|
IsOpen bool `json:"is_open"`
|
|
SharedTeamIds []string `json:"shared_team_ids"`
|
|
ConnectedTeamIds []string `json:"connected_team_ids"`
|
|
}
|
|
|
|
func (c IM) SlackChannel() slack.Channel {
|
|
// Add Members array with just the User for IM channels
|
|
var members []string
|
|
if c.User != "" {
|
|
members = []string{c.User}
|
|
}
|
|
|
|
return slack.Channel{
|
|
GroupConversation: slack.GroupConversation{
|
|
Conversation: slack.Conversation{
|
|
ID: c.ID,
|
|
Created: c.Created,
|
|
IsIM: c.IsIM,
|
|
IsOrgShared: c.IsOrgShared,
|
|
User: c.User,
|
|
LastRead: c.LastRead.SlackString(),
|
|
},
|
|
IsArchived: c.IsArchived,
|
|
Members: members,
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
func (cl *Client) ClientDMs(ctx context.Context) ([]ClientDM, error) {
|
|
form := clientDMsForm{
|
|
BaseRequest: BaseRequest{Token: cl.token},
|
|
Count: 250,
|
|
IncludeClosed: true,
|
|
IncludeChannel: true,
|
|
ExcludeBots: false,
|
|
Cursor: "",
|
|
WebClientFields: webclientReason("dms-tab-populate"),
|
|
}
|
|
lim := limiter.Tier2boost.Limiter()
|
|
var IMs []ClientDM
|
|
for {
|
|
resp, err := cl.PostFormRaw(ctx, cl.webapiURL("client.dms"), values(form, true))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := clientDMsResponse{}
|
|
if err := cl.ParseResponse(&r, resp); err != nil {
|
|
return nil, err
|
|
}
|
|
IMs = append(IMs, r.IMs...)
|
|
if r.ResponseMetadata.NextCursor == "" {
|
|
break
|
|
}
|
|
form.Cursor = r.ResponseMetadata.NextCursor
|
|
if err := lim.Wait(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return IMs, nil
|
|
}
|