slk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/LICENSE +21 -0
  4. data/README.md +190 -0
  5. data/bin/slk +7 -0
  6. data/lib/slack_cli/api/activity.rb +28 -0
  7. data/lib/slack_cli/api/bots.rb +32 -0
  8. data/lib/slack_cli/api/client.rb +49 -0
  9. data/lib/slack_cli/api/conversations.rb +52 -0
  10. data/lib/slack_cli/api/dnd.rb +40 -0
  11. data/lib/slack_cli/api/emoji.rb +21 -0
  12. data/lib/slack_cli/api/threads.rb +44 -0
  13. data/lib/slack_cli/api/usergroups.rb +25 -0
  14. data/lib/slack_cli/api/users.rb +101 -0
  15. data/lib/slack_cli/cli.rb +118 -0
  16. data/lib/slack_cli/commands/activity.rb +292 -0
  17. data/lib/slack_cli/commands/base.rb +175 -0
  18. data/lib/slack_cli/commands/cache.rb +116 -0
  19. data/lib/slack_cli/commands/catchup.rb +484 -0
  20. data/lib/slack_cli/commands/config.rb +159 -0
  21. data/lib/slack_cli/commands/dnd.rb +143 -0
  22. data/lib/slack_cli/commands/emoji.rb +412 -0
  23. data/lib/slack_cli/commands/help.rb +76 -0
  24. data/lib/slack_cli/commands/messages.rb +317 -0
  25. data/lib/slack_cli/commands/presence.rb +107 -0
  26. data/lib/slack_cli/commands/preset.rb +239 -0
  27. data/lib/slack_cli/commands/status.rb +194 -0
  28. data/lib/slack_cli/commands/thread.rb +62 -0
  29. data/lib/slack_cli/commands/unread.rb +312 -0
  30. data/lib/slack_cli/commands/workspaces.rb +151 -0
  31. data/lib/slack_cli/formatters/duration_formatter.rb +28 -0
  32. data/lib/slack_cli/formatters/emoji_replacer.rb +143 -0
  33. data/lib/slack_cli/formatters/mention_replacer.rb +154 -0
  34. data/lib/slack_cli/formatters/message_formatter.rb +429 -0
  35. data/lib/slack_cli/formatters/output.rb +89 -0
  36. data/lib/slack_cli/models/channel.rb +52 -0
  37. data/lib/slack_cli/models/duration.rb +85 -0
  38. data/lib/slack_cli/models/message.rb +217 -0
  39. data/lib/slack_cli/models/preset.rb +73 -0
  40. data/lib/slack_cli/models/reaction.rb +54 -0
  41. data/lib/slack_cli/models/status.rb +57 -0
  42. data/lib/slack_cli/models/user.rb +56 -0
  43. data/lib/slack_cli/models/workspace.rb +52 -0
  44. data/lib/slack_cli/runner.rb +123 -0
  45. data/lib/slack_cli/services/api_client.rb +149 -0
  46. data/lib/slack_cli/services/cache_store.rb +198 -0
  47. data/lib/slack_cli/services/configuration.rb +74 -0
  48. data/lib/slack_cli/services/encryption.rb +51 -0
  49. data/lib/slack_cli/services/preset_store.rb +112 -0
  50. data/lib/slack_cli/services/reaction_enricher.rb +87 -0
  51. data/lib/slack_cli/services/token_store.rb +117 -0
  52. data/lib/slack_cli/support/error_logger.rb +28 -0
  53. data/lib/slack_cli/support/help_formatter.rb +139 -0
  54. data/lib/slack_cli/support/inline_images.rb +62 -0
  55. data/lib/slack_cli/support/slack_url_parser.rb +78 -0
  56. data/lib/slack_cli/support/user_resolver.rb +114 -0
  57. data/lib/slack_cli/support/xdg_paths.rb +37 -0
  58. data/lib/slack_cli/version.rb +5 -0
  59. data/lib/slack_cli.rb +91 -0
  60. metadata +103 -0
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Services
5
+ class TokenStore
6
+ attr_accessor :on_warning
7
+
8
+ def initialize(config: nil, encryption: nil, paths: nil)
9
+ @config = config || Configuration.new
10
+ @encryption = encryption || Encryption.new
11
+ @paths = paths || Support::XdgPaths.new
12
+ @on_warning = nil
13
+ end
14
+
15
+ def workspace(name)
16
+ tokens = load_tokens
17
+ data = tokens[name]
18
+ raise WorkspaceNotFoundError, "Workspace '#{name}' not found" unless data
19
+
20
+ Models::Workspace.new(
21
+ name: name,
22
+ token: data["token"],
23
+ cookie: data["cookie"]
24
+ )
25
+ end
26
+
27
+ def all_workspaces
28
+ load_tokens.map do |name, data|
29
+ Models::Workspace.new(
30
+ name: name,
31
+ token: data["token"],
32
+ cookie: data["cookie"]
33
+ )
34
+ end
35
+ end
36
+
37
+ def workspace_names
38
+ load_tokens.keys
39
+ end
40
+
41
+ def exists?(name)
42
+ load_tokens.key?(name)
43
+ end
44
+
45
+ def add(name, token, cookie = nil)
46
+ # Validate by constructing a Workspace (will raise ArgumentError if invalid)
47
+ Models::Workspace.new(name: name, token: token, cookie: cookie)
48
+
49
+ tokens = load_tokens
50
+ tokens[name] = { "token" => token, "cookie" => cookie }.compact
51
+ save_tokens(tokens)
52
+ end
53
+
54
+ def remove(name)
55
+ tokens = load_tokens
56
+ removed = tokens.delete(name)
57
+ save_tokens(tokens) if removed
58
+ !removed.nil?
59
+ end
60
+
61
+ def empty?
62
+ load_tokens.empty?
63
+ end
64
+
65
+ private
66
+
67
+ def load_tokens
68
+ if encrypted_file_exists?
69
+ decrypt_tokens
70
+ elsif plain_file_exists?
71
+ JSON.parse(File.read(plain_tokens_file))
72
+ else
73
+ {}
74
+ end
75
+ rescue JSON::ParserError => e
76
+ raise TokenStoreError, "Tokens file #{plain_tokens_file} is corrupted: #{e.message}"
77
+ end
78
+
79
+ def save_tokens(tokens)
80
+ @paths.ensure_config_dir
81
+
82
+ if @config.ssh_key
83
+ # When encryption is configured, always use it - don't silently fall back
84
+ @encryption.encrypt(JSON.generate(tokens), @config.ssh_key, encrypted_tokens_file)
85
+ File.delete(plain_tokens_file) if File.exist?(plain_tokens_file)
86
+ else
87
+ # Plain text storage (no encryption configured)
88
+ File.write(plain_tokens_file, JSON.pretty_generate(tokens))
89
+ File.chmod(0o600, plain_tokens_file)
90
+ end
91
+ end
92
+
93
+ def decrypt_tokens
94
+ content = @encryption.decrypt(encrypted_tokens_file, @config.ssh_key)
95
+ content ? JSON.parse(content) : {}
96
+ rescue JSON::ParserError => e
97
+ raise TokenStoreError, "Encrypted tokens file is corrupted: #{e.message}"
98
+ end
99
+
100
+ def encrypted_file_exists?
101
+ File.exist?(encrypted_tokens_file)
102
+ end
103
+
104
+ def plain_file_exists?
105
+ File.exist?(plain_tokens_file)
106
+ end
107
+
108
+ def encrypted_tokens_file
109
+ @paths.config_file("tokens.age")
110
+ end
111
+
112
+ def plain_tokens_file
113
+ @paths.config_file("tokens.json")
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Support
5
+ module ErrorLogger
6
+ # Log an error to the error log file
7
+ # @param error [Exception] The error to log
8
+ # @param paths [XdgPaths] Path helper (for testing)
9
+ # @return [String, nil] Path to the log file, or nil if logging failed
10
+ def self.log(error, paths: XdgPaths.new)
11
+ paths.ensure_cache_dir
12
+
13
+ log_file = paths.cache_file('error.log')
14
+ File.open(log_file, 'a') do |f|
15
+ f.puts "#{Time.now.iso8601} - #{error.class}: #{error.message}"
16
+ f.puts error.backtrace.first(10).map { |line| " #{line}" }.join("\n") if error.backtrace
17
+ f.puts
18
+ end
19
+
20
+ log_file
21
+ rescue SystemCallError, IOError
22
+ # If we can't write to the log, fail silently rather than crashing
23
+ # The user will still see the error message in the console
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Support
5
+ # Formats help text with auto-aligned columns
6
+ #
7
+ # Example usage:
8
+ # help = HelpFormatter.new("slk status [text] [emoji] [options]")
9
+ # help.description("Get or set your Slack status.")
10
+ # help.note("GET shows all workspaces by default. SET applies to primary only.")
11
+ #
12
+ # help.section("EXAMPLES") do |s|
13
+ # s.example("slk status", "Show status (all workspaces)")
14
+ # s.example("slk status clear", "Clear status")
15
+ # end
16
+ #
17
+ # help.section("OPTIONS") do |s|
18
+ # s.option("-n, --limit N", "Messages per channel (default: 10)")
19
+ # s.option("--muted", "Include muted channels")
20
+ # end
21
+ #
22
+ # puts help.render
23
+ #
24
+ class HelpFormatter
25
+ def initialize(usage)
26
+ @usage = usage
27
+ @description = nil
28
+ @notes = []
29
+ @sections = []
30
+ end
31
+
32
+ def description(text)
33
+ @description = text
34
+ self
35
+ end
36
+
37
+ def note(text)
38
+ @notes << text
39
+ self
40
+ end
41
+
42
+ def section(title, &block)
43
+ section = Section.new(title)
44
+ block.call(section)
45
+ @sections << section
46
+ self
47
+ end
48
+
49
+ def render
50
+ lines = []
51
+ lines << "USAGE: #{@usage}"
52
+ lines << ""
53
+
54
+ if @description
55
+ lines << @description
56
+ end
57
+
58
+ @notes.each do |note|
59
+ lines << note
60
+ end
61
+
62
+ lines << "" if @description || @notes.any?
63
+
64
+ @sections.each do |section|
65
+ lines << "#{section.title}:"
66
+ lines.concat(section.render)
67
+ lines << ""
68
+ end
69
+
70
+ # Remove trailing blank line
71
+ lines.pop if lines.last == ""
72
+
73
+ lines.join("\n")
74
+ end
75
+
76
+ class Section
77
+ attr_reader :title
78
+
79
+ def initialize(title)
80
+ @title = title
81
+ @items = []
82
+ end
83
+
84
+ def option(flags, description)
85
+ @items << [:option, flags, description]
86
+ self
87
+ end
88
+
89
+ def action(name, description)
90
+ @items << [:action, name, description]
91
+ self
92
+ end
93
+
94
+ def example(command, description = nil)
95
+ @items << [:example, command, description]
96
+ self
97
+ end
98
+
99
+ def item(left, right)
100
+ @items << [:item, left, right]
101
+ self
102
+ end
103
+
104
+ def text(content)
105
+ @items << [:text, content, nil]
106
+ self
107
+ end
108
+
109
+ def render
110
+ return [] if @items.empty?
111
+
112
+ # Calculate max width of left column
113
+ max_left = @items
114
+ .reject { |type, _, _| type == :text }
115
+ .map { |_, left, _| left.length }
116
+ .max || 0
117
+
118
+ # Add padding (2 spaces between columns)
119
+ padding = 2
120
+
121
+ @items.map do |type, left, right|
122
+ case type
123
+ when :text
124
+ " #{left}"
125
+ when :example
126
+ if right
127
+ " #{left.ljust(max_left + padding)}#{right}"
128
+ else
129
+ " #{left}"
130
+ end
131
+ else
132
+ " #{left.ljust(max_left + padding)}#{right}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Support
5
+ # Shared module for inline image display in iTerm2/WezTerm/Mintty terminals
6
+ # Includes special handling for tmux passthrough sequences
7
+ module InlineImages
8
+ # Check if terminal supports iTerm2 inline image protocol
9
+ def inline_images_supported?
10
+ # iTerm2, WezTerm, Mintty support inline images
11
+ # LC_TERMINAL persists through tmux/ssh
12
+ ENV["TERM_PROGRAM"] == "iTerm.app" ||
13
+ ENV["TERM_PROGRAM"] == "WezTerm" ||
14
+ ENV["LC_TERMINAL"] == "iTerm2" ||
15
+ ENV["LC_TERMINAL"] == "WezTerm" ||
16
+ ENV["TERM"] == "mintty"
17
+ end
18
+
19
+ # Check if running inside tmux
20
+ def in_tmux?
21
+ # tmux sets TERM to screen-* or tmux-*
22
+ ENV["TERM"]&.include?("screen") || ENV["TERM"]&.start_with?("tmux")
23
+ end
24
+
25
+ # Print an inline image using iTerm2 protocol
26
+ # In tmux, uses passthrough sequence and cursor positioning
27
+ def print_inline_image(path, height: 1)
28
+ return unless File.exist?(path)
29
+
30
+ data = File.binread(path)
31
+ encoded = [data].pack("m0") # Base64 encode
32
+
33
+ if in_tmux?
34
+ # tmux passthrough: \n + space required for image to render
35
+ printf "\ePtmux;\e\e]1337;File=inline=1;preserveAspectRatio=0;size=%d;height=%d:%s\a\e\\\n ",
36
+ encoded.length, height, encoded
37
+ else
38
+ # Standard iTerm2 format
39
+ printf "\e]1337;File=inline=1;height=%d:%s\a", height, encoded
40
+ end
41
+ end
42
+
43
+ # Print inline image with name on same line
44
+ # Handles tmux cursor positioning to keep image and text on same line
45
+ def print_inline_image_with_text(path, text, height: 1)
46
+ return false unless inline_images_supported? && File.exist?(path)
47
+
48
+ print_inline_image(path, height: height)
49
+
50
+ if in_tmux?
51
+ # tmux: image ends with \n + space, cursor on next line
52
+ # Move up 1 line, right 3 cols (past image), then print text
53
+ print "\e[1A\e[3C#{text}\n"
54
+ else
55
+ puts " #{text}"
56
+ end
57
+
58
+ true
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Support
5
+ class SlackUrlParser
6
+ # Patterns for Slack URLs
7
+ # Channel IDs: C=channel, G=group DM, D=direct message
8
+ URL_PATTERNS = [
9
+ # https://workspace.slack.com/archives/C123ABC/p1234567890123456
10
+ %r{https?://([^.]+)\.slack\.com/archives/([CDG][A-Z0-9]+)/p(\d+)(?:\?thread_ts=(\d+\.\d+))?},
11
+ # https://workspace.slack.com/archives/C123ABC (no message)
12
+ %r{https?://([^.]+)\.slack\.com/archives/([CDG][A-Z0-9]+)/?$}
13
+ ].freeze
14
+
15
+ Result = Data.define(:workspace, :channel_id, :msg_ts, :thread_ts) do
16
+ def message?
17
+ !msg_ts.nil?
18
+ end
19
+
20
+ def thread?
21
+ !thread_ts.nil?
22
+ end
23
+
24
+ # Returns the thread parent timestamp if this URL points to a threaded message.
25
+ # Use this when fetching thread replies - pass this as the thread_ts parameter.
26
+ # Returns nil if the URL does not contain a thread_ts query parameter.
27
+ def ts
28
+ thread_ts
29
+ end
30
+ end
31
+
32
+ def parse(input)
33
+ return nil unless input.to_s.include?("slack.com")
34
+
35
+ URL_PATTERNS.each do |pattern|
36
+ match = input.match(pattern)
37
+ next unless match
38
+
39
+ workspace = match[1]
40
+ channel_id = match[2]
41
+ msg_ts = match[3] ? format_ts(match[3]) : nil
42
+ thread_ts = match[4]
43
+
44
+ return Result.new(
45
+ workspace: workspace,
46
+ channel_id: channel_id,
47
+ msg_ts: msg_ts,
48
+ thread_ts: thread_ts
49
+ )
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def slack_url?(input)
56
+ input.to_s.include?("slack.com/archives")
57
+ end
58
+
59
+ private
60
+
61
+ # Convert Slack URL timestamp format to API format
62
+ # URL: p1234567890123456 -> API: 1234567890.123456
63
+ def format_ts(url_ts)
64
+ return nil unless url_ts
65
+
66
+ # Remove 'p' prefix if present
67
+ ts = url_ts.sub(/^p/, "")
68
+
69
+ # Insert decimal point
70
+ if ts.length > 6
71
+ "#{ts[0..-7]}.#{ts[-6..]}"
72
+ else
73
+ ts
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Support
5
+ # Shared logic for resolving user and channel names from Slack data.
6
+ # Include this module in commands that need to look up user/channel names.
7
+ #
8
+ # == Required Interface
9
+ # Including classes must provide these methods:
10
+ # - runner: Returns the Runner instance for API access
11
+ # - cache_store: Returns the CacheStore for name lookups
12
+ # - debug(message): Logs debug messages (can be a no-op)
13
+ module UserResolver
14
+ # Resolves a DM channel ID to the user's display name
15
+ # @param workspace [Models::Workspace] The workspace to look up in
16
+ # @param channel_id [String] The DM channel ID (starts with D)
17
+ # @param conversations [Api::Conversations] API client for conversation info
18
+ # @return [String] User name or channel ID if not found
19
+ def resolve_dm_user_name(workspace, channel_id, conversations)
20
+ info = conversations.info(channel: channel_id)
21
+ return channel_id unless info["ok"] && info["channel"]
22
+
23
+ user_id = info["channel"]["user"]
24
+ return channel_id unless user_id
25
+
26
+ # Try cache first
27
+ cached = cache_store.get_user(workspace.name, user_id)
28
+ return cached if cached
29
+
30
+ # Try users API lookup
31
+ begin
32
+ users_api = runner.users_api(workspace.name)
33
+ user_info = users_api.info(user_id)
34
+ if user_info["ok"] && user_info["user"]
35
+ profile = user_info["user"]["profile"] || {}
36
+ name = profile["display_name"]
37
+ name = profile["real_name"] if name.to_s.empty?
38
+ name = user_info["user"]["name"] if name.to_s.empty?
39
+ if name && !name.empty?
40
+ cache_store.set_user(workspace.name, user_id, name, persist: true)
41
+ return name
42
+ end
43
+ end
44
+ rescue ApiError => e
45
+ debug("User lookup failed for #{user_id}: #{e.message}")
46
+ end
47
+
48
+ user_id
49
+ rescue ApiError => e
50
+ debug("DM info lookup failed for #{channel_id}: #{e.message}")
51
+ channel_id
52
+ end
53
+
54
+ # Resolves a channel ID to a formatted label (@username or #channel)
55
+ # @param workspace [Models::Workspace] The workspace to look up in
56
+ # @param channel_id [String] The channel ID
57
+ # @return [String] Formatted label like "@username" or "#channel"
58
+ def resolve_conversation_label(workspace, channel_id)
59
+ # DM channels start with D
60
+ if channel_id.start_with?("D")
61
+ conversations = runner.conversations_api(workspace.name)
62
+ user_name = resolve_dm_user_name(workspace, channel_id, conversations)
63
+ return "@#{user_name}"
64
+ end
65
+
66
+ # Try cache first
67
+ cached_name = cache_store.get_channel_name(workspace.name, channel_id)
68
+ return "##{cached_name}" if cached_name
69
+
70
+ # Try API lookup
71
+ begin
72
+ conversations = runner.conversations_api(workspace.name)
73
+ response = conversations.info(channel: channel_id)
74
+ if response["ok"] && response["channel"]
75
+ name = response["channel"]["name"]
76
+ if name
77
+ cache_store.set_channel(workspace.name, name, channel_id)
78
+ return "##{name}"
79
+ end
80
+ end
81
+ rescue ApiError => e
82
+ debug("Channel info lookup failed for #{channel_id}: #{e.message}")
83
+ end
84
+
85
+ "##{channel_id}"
86
+ end
87
+
88
+ # Extracts the user name from a message hash
89
+ # @param msg [Hash] The message data from API
90
+ # @param workspace [Models::Workspace] The workspace context
91
+ # @return [String] User name, user_id as fallback, or "unknown" if neither found
92
+ def extract_user_from_message(msg, workspace)
93
+ # Try user_profile embedded in message
94
+ if msg["user_profile"]
95
+ name = msg["user_profile"]["display_name"]
96
+ name = msg["user_profile"]["real_name"] if name.to_s.empty?
97
+ return name unless name.to_s.empty?
98
+ end
99
+
100
+ # Try username field
101
+ return msg["username"] unless msg["username"].to_s.empty?
102
+
103
+ # Try cache
104
+ user_id = msg["user"] || msg["bot_id"]
105
+ if user_id
106
+ cached = cache_store.get_user(workspace.name, user_id)
107
+ return cached if cached
108
+ end
109
+
110
+ user_id || "unknown"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Support
5
+ class XdgPaths
6
+ def config_dir
7
+ @config_dir ||= File.join(
8
+ ENV.fetch("XDG_CONFIG_HOME", File.join(Dir.home, ".config")),
9
+ "slack-cli"
10
+ )
11
+ end
12
+
13
+ def cache_dir
14
+ @cache_dir ||= File.join(
15
+ ENV.fetch("XDG_CACHE_HOME", File.join(Dir.home, ".cache")),
16
+ "slack-cli"
17
+ )
18
+ end
19
+
20
+ def config_file(filename)
21
+ File.join(config_dir, filename)
22
+ end
23
+
24
+ def cache_file(filename)
25
+ File.join(cache_dir, filename)
26
+ end
27
+
28
+ def ensure_config_dir
29
+ FileUtils.mkdir_p(config_dir)
30
+ end
31
+
32
+ def ensure_cache_dir
33
+ FileUtils.mkdir_p(cache_dir)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ VERSION = "0.1.0"
5
+ end
data/lib/slack_cli.rb ADDED
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "fileutils"
7
+ require "optparse"
8
+ require "time"
9
+ require "io/console"
10
+
11
+ module SlackCli
12
+ class Error < StandardError; end
13
+ class ApiError < Error; end
14
+ class ConfigError < Error; end
15
+ class EncryptionError < Error; end
16
+ class TokenStoreError < Error; end
17
+ class WorkspaceNotFoundError < ConfigError; end
18
+ class PresetNotFoundError < ConfigError; end
19
+
20
+ autoload :VERSION, "slack_cli/version"
21
+ autoload :CLI, "slack_cli/cli"
22
+ autoload :Runner, "slack_cli/runner"
23
+
24
+ module Models
25
+ autoload :Duration, "slack_cli/models/duration"
26
+ autoload :Workspace, "slack_cli/models/workspace"
27
+ autoload :Status, "slack_cli/models/status"
28
+ autoload :Message, "slack_cli/models/message"
29
+ autoload :Reaction, "slack_cli/models/reaction"
30
+ autoload :User, "slack_cli/models/user"
31
+ autoload :Channel, "slack_cli/models/channel"
32
+ autoload :Preset, "slack_cli/models/preset"
33
+ end
34
+
35
+ module Services
36
+ autoload :ApiClient, "slack_cli/services/api_client"
37
+ autoload :Configuration, "slack_cli/services/configuration"
38
+ autoload :TokenStore, "slack_cli/services/token_store"
39
+ autoload :CacheStore, "slack_cli/services/cache_store"
40
+ autoload :PresetStore, "slack_cli/services/preset_store"
41
+ autoload :Encryption, "slack_cli/services/encryption"
42
+ autoload :ReactionEnricher, "slack_cli/services/reaction_enricher"
43
+ end
44
+
45
+ module Formatters
46
+ autoload :Output, "slack_cli/formatters/output"
47
+ autoload :DurationFormatter, "slack_cli/formatters/duration_formatter"
48
+ autoload :MentionReplacer, "slack_cli/formatters/mention_replacer"
49
+ autoload :EmojiReplacer, "slack_cli/formatters/emoji_replacer"
50
+ autoload :MessageFormatter, "slack_cli/formatters/message_formatter"
51
+ end
52
+
53
+ module Commands
54
+ autoload :Base, "slack_cli/commands/base"
55
+ autoload :Status, "slack_cli/commands/status"
56
+ autoload :Presence, "slack_cli/commands/presence"
57
+ autoload :Dnd, "slack_cli/commands/dnd"
58
+ autoload :Messages, "slack_cli/commands/messages"
59
+ autoload :Thread, "slack_cli/commands/thread"
60
+ autoload :Unread, "slack_cli/commands/unread"
61
+ autoload :Catchup, "slack_cli/commands/catchup"
62
+ autoload :Activity, "slack_cli/commands/activity"
63
+ autoload :Preset, "slack_cli/commands/preset"
64
+ autoload :Workspaces, "slack_cli/commands/workspaces"
65
+ autoload :Cache, "slack_cli/commands/cache"
66
+ autoload :Emoji, "slack_cli/commands/emoji"
67
+ autoload :Config, "slack_cli/commands/config"
68
+ autoload :Help, "slack_cli/commands/help"
69
+ end
70
+
71
+ module Api
72
+ autoload :Users, "slack_cli/api/users"
73
+ autoload :Conversations, "slack_cli/api/conversations"
74
+ autoload :Dnd, "slack_cli/api/dnd"
75
+ autoload :Emoji, "slack_cli/api/emoji"
76
+ autoload :Client, "slack_cli/api/client"
77
+ autoload :Bots, "slack_cli/api/bots"
78
+ autoload :Threads, "slack_cli/api/threads"
79
+ autoload :Usergroups, "slack_cli/api/usergroups"
80
+ autoload :Activity, "slack_cli/api/activity"
81
+ end
82
+
83
+ module Support
84
+ autoload :XdgPaths, "slack_cli/support/xdg_paths"
85
+ autoload :SlackUrlParser, "slack_cli/support/slack_url_parser"
86
+ autoload :InlineImages, "slack_cli/support/inline_images"
87
+ autoload :HelpFormatter, "slack_cli/support/help_formatter"
88
+ autoload :ErrorLogger, "slack_cli/support/error_logger"
89
+ autoload :UserResolver, "slack_cli/support/user_resolver"
90
+ end
91
+ end