slk 0.6.0 → 0.8.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.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Models
5
+ # A status queued to turn on later, from users.customStatus.list.
6
+ #
7
+ # Slack derives `duration` server-side from the window, so it is not
8
+ # modelled here — the window itself is the source of truth.
9
+ ScheduledStatus = Data.define(:id, :text, :emoji, :date_scheduled, :date_expire, :dnd, :active) do
10
+ def self.from_api(data)
11
+ validate!(data)
12
+
13
+ new(
14
+ id: data['id'].to_s,
15
+ text: data['text'].to_s,
16
+ emoji: data['emoji'].to_s,
17
+ date_scheduled: data['date_scheduled'].to_i,
18
+ date_expire: data['date_expire'].to_i,
19
+ dnd: truthy?(data['is_dnd']),
20
+ active: truthy?(data['is_active'])
21
+ )
22
+ end
23
+
24
+ # Every field is coerced, so without this guard any payload at all —
25
+ # a bare string, nil, a hash of unexpected keys — becomes a valid-looking
26
+ # record with an empty id that prints as a blank line and never matches
27
+ # the id the user asked about. An unusable record is a protocol change,
28
+ # not a status.
29
+ def self.validate!(data)
30
+ return if data.is_a?(Hash) && !data['id'].to_s.empty?
31
+
32
+ raise ApiError.new("Slack returned a scheduled status with no id: #{data.inspect[0, 120]}",
33
+ code: :malformed_scheduled_status)
34
+ end
35
+ private_class_method :validate!
36
+
37
+ # These endpoints are string-typed on the way in — Api::CustomStatus sends
38
+ # is_dnd as 'true' — so a strict `== true` would quietly read a scheduled
39
+ # DND back as off. Accept the shapes Slack actually uses.
40
+ def self.truthy?(value) = [true, 'true', 1, '1'].include?(value)
41
+ private_class_method :truthy?
42
+
43
+ def starts_at = date_scheduled.positive? ? Time.at(date_scheduled) : nil
44
+ def ends_at = date_expire.positive? ? Time.at(date_expire) : nil
45
+
46
+ def to_s
47
+ span = window
48
+ [
49
+ emoji,
50
+ text,
51
+ span.empty? ? '' : "(#{span})",
52
+ ('[dnd]' if dnd),
53
+ # Slack marks the one that has already turned on. Worth showing: it
54
+ # is the difference between "will happen" and "is happening".
55
+ ('[active]' if active)
56
+ ].reject { |part| part.to_s.empty? }.join(' ')
57
+ end
58
+
59
+ # Human-readable window; the end date is only repeated when it differs
60
+ # from the start date, which makes multi-day windows obvious.
61
+ def window
62
+ dated = '%a %b %-d %-l:%M%P'
63
+ start_time = starts_at or return ''
64
+ formatted = start_time.strftime(dated)
65
+ finish = ends_at or return formatted
66
+
67
+ "#{formatted} -> #{finish.strftime(same_day?(start_time, finish) ? '%-l:%M%P' : dated)}"
68
+ end
69
+
70
+ private
71
+
72
+ def same_day?(start_time, finish)
73
+ start_time.to_date == finish.to_date
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Models
5
+ # Everything `slk status` knows about one workspace at one moment: the
6
+ # status itself, plus the two things that decide whether anyone can reach
7
+ # you (presence, DND) and whatever is queued to replace the status later.
8
+ #
9
+ # Every part but the status is optional, and nil means "not checked" —
10
+ # skipped by a flag, or a lookup that failed. That is deliberately distinct
11
+ # from checked-and-empty: `scheduled: []` says nothing is queued, while
12
+ # `scheduled: nil` says nobody looked.
13
+ StatusSnapshot = Data.define(:workspace, :status, :presence, :dnd, :scheduled) do
14
+ def initialize(workspace:, status:, presence: nil, dnd: nil, scheduled: nil)
15
+ super
16
+ end
17
+
18
+ def workspace_name = workspace.name
19
+
20
+ def away? = presence ? presence[:presence] == 'away' : false
21
+
22
+ # Short suffixes for the status line. Only the exceptional states appear:
23
+ # active presence and DND-off are the common case, and repeating them on
24
+ # every line would bury the workspace that differs.
25
+ def labels
26
+ [('[away]' if away?), dnd&.to_s].reject { |label| label.to_s.empty? }
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/slk/runner.rb CHANGED
@@ -88,6 +88,10 @@ module Slk
88
88
  Api::Saved.new(@api_client, workspace(workspace_name))
89
89
  end
90
90
 
91
+ def custom_status_api(workspace_name = nil)
92
+ Api::CustomStatus.new(@api_client, workspace(workspace_name))
93
+ end
94
+
91
95
  def team_api(workspace_name = nil)
92
96
  Api::Team.new(@api_client, workspace(workspace_name))
93
97
  end
@@ -104,8 +104,15 @@ module Slk
104
104
  'Please provide the correct public key for this private key.'
105
105
  end
106
106
 
107
+ # `-P ''` supplies the passphrase up front so ssh-keygen cannot ask for
108
+ # one. Without it, an encrypted key makes ssh-keygen prompt — and it
109
+ # prompts on the console directly, not on the stdin capture3 hands it, so
110
+ # closing stdin does not help. On Windows that read blocks forever; this
111
+ # hung the CI job until its timeout with no output. An empty passphrase
112
+ # fails an encrypted key with the same message it already reported, which
113
+ # is the right answer regardless: slk cannot use such a key anyway.
107
114
  def derive_public_key(private_key_path)
108
- output, error, status = Open3.capture3('ssh-keygen', '-y', '-f', private_key_path)
115
+ output, error, status = Open3.capture3('ssh-keygen', '-y', '-P', '', '-f', private_key_path)
109
116
  return output.strip if status.success?
110
117
 
111
118
  # Check if ssh-keygen is missing vs other failures
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'date'
5
+
6
+ module Slk
7
+ module Support
8
+ # Resolves a single clock time ("1:30p", "2026-08-12 8a") to a Unix
9
+ # timestamp. A bare time at or before now rolls to tomorrow; an explicit
10
+ # YYYY-MM-DD date is honoured as given.
11
+ #
12
+ # TimeRangeParser builds the two-sided form on the clock arithmetic here.
13
+ class TimeParser
14
+ TIME = /(\d{1,2})(?::(\d{2}))?\s*([ap]m?)?/i
15
+ PATTERN = /\A(?:(\d{4}-\d{2}-\d{2})\s+)?#{TIME}\z/i
16
+
17
+ EXAMPLE = '1:30p or 2026-08-12 8:00'
18
+
19
+ MINUTES_PER_DAY = 24 * 60
20
+
21
+ # A bare hour of 0 or 13-23 can only be 24-hour notation, so no am/pm was
22
+ # omitted. Anything in 1..12 is genuinely ambiguous without one.
23
+ CLOCK_HOURS = (1..12)
24
+
25
+ # @param now [Time] reference point for rolling bare times forward
26
+ # @return [Integer] Unix timestamp
27
+ def self.parse(input, now: Time.now) = new(now: now).parse(input)
28
+
29
+ def initialize(now: Time.now)
30
+ @now = now
31
+ end
32
+
33
+ def parse(input)
34
+ match = PATTERN.match(input.to_s.strip)
35
+ raise TimeFormatError, "Invalid time: #{input}. Use #{EXAMPLE}" unless match
36
+
37
+ parts = match.captures[1..3]
38
+ date = match[1] ? parse_date(match[1]) : roll_forward(parts)
39
+ at(date, *parts).to_i
40
+ end
41
+
42
+ # The rest of this class is clock arithmetic shared with TimeRangeParser,
43
+ # which needs to place the same parts on dates it works out itself.
44
+
45
+ # @param example [String] format hint, so a caller with its own syntax
46
+ # (TimeRangeParser) does not advertise this class's example
47
+ def parse_date(text, example: EXAMPLE)
48
+ Date.parse(text)
49
+ rescue Date::Error
50
+ raise TimeFormatError, "Invalid date: #{text}. Use #{example}"
51
+ end
52
+
53
+ def at(date, *parts)
54
+ hours, minutes = to_24_hour(*parts)
55
+ time = build(date, hours, minutes)
56
+ # Ruby silently shifts a local time that DST skips (2:30a on a
57
+ # spring-forward date becomes 3:30a), which would move the window
58
+ # rather than fail. Reject it instead.
59
+ return time if time.hour == hours && time.min == minutes
60
+
61
+ raise TimeFormatError,
62
+ format('%<clock>s does not exist on %<date>s (clocks skip forward for DST).',
63
+ clock: format('%<h>02d:%<m>02d', h: hours, m: minutes), date: date)
64
+ end
65
+
66
+ # Same placement without the existence check, for callers asking a
67
+ # question a nonexistent time still answers. `at` would raise, which for
68
+ # the roll-forward probe would reject "2:30a" outright on a spring-forward
69
+ # date instead of rolling it to the next day, where it does exist.
70
+ def place(date, *parts) = build(date, *to_24_hour(*parts))
71
+
72
+ # Minutes since midnight, for comparing two times before either has a date.
73
+ def minutes(parts)
74
+ hours, mins = to_24_hour(*parts)
75
+ (hours * 60) + mins
76
+ end
77
+
78
+ # True when these parts could have meant either am or pm — no meridiem
79
+ # given, and an hour small enough that both readings are plausible.
80
+ def ambiguous?(parts) = parts[2].nil? && CLOCK_HOURS.cover?(parts[0].to_i)
81
+
82
+ # The opposite: a bare hour too large to be a 12-hour clock reading, so
83
+ # the writer was plainly using 24-hour notation. TimeRangeParser treats
84
+ # one of these as settling how to read the *other* side of a range.
85
+ def twenty_four_hour?(parts) = parts[2].nil? && !CLOCK_HOURS.cover?(parts[0].to_i)
86
+
87
+ private
88
+
89
+ def build(date, hours, minutes) = Time.new(date.year, date.month, date.day, hours, minutes, 0)
90
+
91
+ def roll_forward(parts)
92
+ today = @now.to_date
93
+ place(today, *parts) <= @now ? today + 1 : today
94
+ end
95
+
96
+ def to_24_hour(hour, minute, meridiem)
97
+ hours = hour.to_i
98
+ minutes = minute.to_i
99
+ raise TimeFormatError, "Invalid minute: #{minute}" if minutes > 59
100
+ return [validate_24_hour(hours), minutes] unless meridiem
101
+
102
+ [apply_meridiem(hours, meridiem), minutes]
103
+ end
104
+
105
+ def validate_24_hour(hours)
106
+ raise TimeFormatError, "Invalid hour: #{hours}" if hours > 23
107
+
108
+ hours
109
+ end
110
+
111
+ def apply_meridiem(hours, meridiem)
112
+ raise TimeFormatError, "Invalid hour for 12-hour time: #{hours}" if hours.zero? || hours > 12
113
+
114
+ # 12am is hour 0 and 12pm is hour 12, so fold 12 down before shifting.
115
+ base = hours % 12
116
+ meridiem[0].casecmp?('p') ? base + 12 : base
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'date'
5
+
6
+ module Slk
7
+ module Support
8
+ # Parses a scheduling window ("1:30p-3:30p", "2026-08-04 13:30-15:30")
9
+ # into a [start, end] pair of Unix timestamps.
10
+ #
11
+ # Unlike DateParser, which resolves an input to a point in the past, this
12
+ # always resolves forward: a bare time at or before now rolls to tomorrow,
13
+ # and an end before the start rolls to the next day so overnight windows
14
+ # ("11p-1a") work.
15
+ #
16
+ # The start date is written once and the end can only reach the following
17
+ # day, so this cannot express a multi-day window — `slk status schedule
18
+ # --start/--end` is the general form for that.
19
+ class TimeRangeParser
20
+ RANGE_PATTERN = /\A(?:(\d{4}-\d{2}-\d{2})\s+)?#{TimeParser::TIME}\s*-\s*#{TimeParser::TIME}\z/i
21
+
22
+ EXAMPLE = '1:30p-3:30p or 2026-08-04 13:30-15:30'
23
+
24
+ # Longest overnight window accepted when a reading had to be guessed.
25
+ # Twelve hours is not a policy about window length — an explicit
26
+ # "8p-9a" is longer and fine — it is the point past which a guessed
27
+ # crossing can no longer be what the user meant.
28
+ MAX_GUESSED_OVERNIGHT_MINUTES = 12 * 60
29
+
30
+ # @param input [String] the range to parse
31
+ # @param now [Time] reference point for rolling bare times forward
32
+ # @return [Array(Integer, Integer)] start and end Unix timestamps
33
+ def self.parse(input, now: Time.now) = new(now: now).parse(input)
34
+
35
+ # True when input looks like a time range, used to pick it out of argv.
36
+ def self.match?(input) = RANGE_PATTERN.match?(input.to_s.strip)
37
+
38
+ def initialize(now: Time.now)
39
+ @now = now
40
+ @clock = TimeParser.new(now: now)
41
+ end
42
+
43
+ def parse(input)
44
+ match = RANGE_PATTERN.match(input.to_s.strip)
45
+ raise TimeFormatError, "Invalid time range: #{input}. Use #{EXAMPLE}" unless match
46
+
47
+ start_parts, end_parts = infer_meridiems(match)
48
+ validate_range(input, start_parts, end_parts)
49
+
50
+ date = start_date(match, start_parts)
51
+ start_at = @clock.at(date, *start_parts)
52
+
53
+ [start_at.to_i, end_time(date, start_at, end_parts).to_i]
54
+ end
55
+
56
+ private
57
+
58
+ # `match.captures` layout: 0 is the optional date, 1..3 the start time,
59
+ # 4..6 the end. Note the date is `match[1]` in the 1-indexed MatchData form.
60
+ #
61
+ # "1-3p" means 1pm, not 1am: when only one side names a meridiem the
62
+ # other borrows it. The borrow is skipped when it would invert the range
63
+ # ("9-5p" is 9am to 5pm, not 9pm to 5pm).
64
+ def infer_meridiems(match)
65
+ start_parts = match.captures[1..3]
66
+ end_parts = match.captures[4..6]
67
+
68
+ if borrowable?(start_parts, end_parts)
69
+ borrowed = with_meridiem(start_parts, end_parts[2])
70
+ return [borrowed, end_parts] if @clock.minutes(borrowed) < @clock.minutes(end_parts)
71
+ elsif borrowable?(end_parts, start_parts)
72
+ borrowed = with_meridiem(end_parts, start_parts[2])
73
+ return [start_parts, borrowed] if @clock.minutes(start_parts) < @clock.minutes(borrowed)
74
+ end
75
+
76
+ [start_parts, end_parts]
77
+ end
78
+
79
+ def borrowable?(parts, source) = !source[2].nil? && @clock.ambiguous?(parts)
80
+
81
+ def with_meridiem(parts, meridiem) = [parts[0], parts[1], meridiem]
82
+
83
+ # An explicit date is honoured as given; a bare time at or before now
84
+ # refers to tomorrow.
85
+ def start_date(match, start_parts)
86
+ return @clock.parse_date(match[1], example: EXAMPLE) if match[1]
87
+
88
+ today = @now.to_date
89
+ # `place`, not `at`: this only asks "is that time already past today?",
90
+ # and on a spring-forward date `at` would reject a skipped time outright
91
+ # instead of letting it roll to tomorrow, where it exists.
92
+ @clock.place(today, *start_parts) <= @now ? today + 1 : today
93
+ end
94
+
95
+ def end_time(date, start_at, end_parts)
96
+ end_at = @clock.at(date, *end_parts)
97
+ # An end before the start means the window crosses midnight.
98
+ end_at < start_at ? @clock.at(date + 1, *end_parts) : end_at
99
+ end
100
+
101
+ def validate_range(input, start_parts, end_parts)
102
+ if @clock.minutes(start_parts) == @clock.minutes(end_parts)
103
+ raise TimeFormatError, "Time range #{input} starts and ends at the same time."
104
+ end
105
+
106
+ validate_overnight(input, start_parts, end_parts)
107
+ end
108
+
109
+ # "9-5" almost always means 9am-5pm, but reads literally as 09:00 to
110
+ # 05:00 the next morning — a 20-hour window nobody asked for. So is
111
+ # "9a-5", where the am/pm is there but on the wrong side to help.
112
+ #
113
+ # Only guessed readings are checked, and only past 12 hours, which is
114
+ # what separates the mistake from the intent. An overnight shift written
115
+ # with a pm start ("9p-5", "10p-2") can only come out 12 hours or less;
116
+ # a dropped meridiem ("9-5", "9a-5", "10-5a") can only come out longer.
117
+ def validate_overnight(input, start_parts, end_parts)
118
+ return unless guessed?(start_parts, end_parts)
119
+
120
+ span = overnight_minutes(start_parts, end_parts)
121
+ return if span.nil? || span <= MAX_GUESSED_OVERNIGHT_MINUTES
122
+
123
+ raise TimeFormatError,
124
+ "Time range #{input} reads as crossing midnight and spans #{format_span(span)}. " \
125
+ 'Add am/pm to both sides (9a-5p), use 24-hour times (9:00-17:00), ' \
126
+ 'or --start/--end for a multi-day window.'
127
+ end
128
+
129
+ # True when a side's reading had to be guessed.
130
+ #
131
+ # A leftover ambiguous side is not enough on its own: in "20:00-6" the
132
+ # 24-hour start settles the whole range, so "6" is 6am and was not
133
+ # guessed. Without such an anchor, any side still bare after
134
+ # `infer_meridiems` was read as 24-hour for want of anything better —
135
+ # either because no meridiem was given at all ("9-5"), or because
136
+ # borrowing the one that was given would have inverted the range
137
+ # ("9a-5", where 5am precedes 9am).
138
+ def guessed?(start_parts, end_parts)
139
+ return false if @clock.twenty_four_hour?(start_parts) || @clock.twenty_four_hour?(end_parts)
140
+
141
+ @clock.ambiguous?(start_parts) || @clock.ambiguous?(end_parts)
142
+ end
143
+
144
+ # Wall-clock minutes from start to end across midnight, or nil when the
145
+ # window stays on one date.
146
+ #
147
+ # Deliberately not `end_at - start_at`: elapsed seconds stretch or shrink
148
+ # by an hour across a DST boundary, so the same text would report a
149
+ # different span depending on the date it happened to land on.
150
+ def overnight_minutes(start_parts, end_parts)
151
+ start_minutes = @clock.minutes(start_parts)
152
+ end_minutes = @clock.minutes(end_parts)
153
+ return nil if end_minutes > start_minutes
154
+
155
+ TimeParser::MINUTES_PER_DAY - start_minutes + end_minutes
156
+ end
157
+
158
+ # Whole hours read better, but rounding them would report a 12h01m window
159
+ # as "12 hours" against a 12-hour limit.
160
+ def format_span(minutes)
161
+ hours, mins = minutes.divmod(60)
162
+ mins.zero? ? "#{hours} hours" : format('%<h>dh%<m>02dm', h: hours, m: mins)
163
+ end
164
+ end
165
+ end
166
+ end
data/lib/slk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slk
4
- VERSION = '0.6.0'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/slk.rb CHANGED
@@ -12,6 +12,17 @@ require 'io/console'
12
12
  module Slk
13
13
  class Error < StandardError; end
14
14
 
15
+ # The invocation itself was wrong (a flag with no value, an unparseable
16
+ # time). The message is written for the user and is printed verbatim, with
17
+ # no "Error type:" label, because there is no internal fault to report.
18
+ class UsageError < Error; end
19
+
20
+ # A time or range the user typed that cannot be understood. Separate from
21
+ # UsageError so callers can rescue *only* malformed input: a blanket
22
+ # `rescue ArgumentError` around a parse call also swallows arity and range
23
+ # errors from the code itself and presents them as if the user mistyped.
24
+ class TimeFormatError < UsageError; end
25
+
15
26
  # Errors from any Slack-API-shaped failure: HTTP errors, network errors,
16
27
  # logical Slack errors (user_not_found, missing_scope, etc.), JSON parse
17
28
  # failures. The optional `code` symbol lets callers match specific cases
@@ -55,6 +66,9 @@ module Slk
55
66
  autoload :Duration, 'slk/models/duration'
56
67
  autoload :Workspace, 'slk/models/workspace'
57
68
  autoload :Status, 'slk/models/status'
69
+ autoload :ScheduledStatus, 'slk/models/scheduled_status'
70
+ autoload :DndState, 'slk/models/dnd_state'
71
+ autoload :StatusSnapshot, 'slk/models/status_snapshot'
58
72
  autoload :Message, 'slk/models/message'
59
73
  autoload :Reaction, 'slk/models/reaction'
60
74
  autoload :User, 'slk/models/user'
@@ -105,6 +119,7 @@ module Slk
105
119
  autoload :MessageFormatter, 'slk/formatters/message_formatter'
106
120
  autoload :ReactionFormatter, 'slk/formatters/reaction_formatter'
107
121
  autoload :JsonMessageFormatter, 'slk/formatters/json_message_formatter'
122
+ autoload :JsonStatusFormatter, 'slk/formatters/json_status_formatter'
108
123
  autoload :ActivityFormatter, 'slk/formatters/activity_formatter'
109
124
  autoload :AttachmentFormatter, 'slk/formatters/attachment_formatter'
110
125
  autoload :BlockFormatter, 'slk/formatters/block_formatter'
@@ -153,6 +168,7 @@ module Slk
153
168
  autoload :Activity, 'slk/api/activity'
154
169
  autoload :Search, 'slk/api/search'
155
170
  autoload :Saved, 'slk/api/saved'
171
+ autoload :CustomStatus, 'slk/api/custom_status'
156
172
  autoload :Team, 'slk/api/team'
157
173
  end
158
174
 
@@ -167,5 +183,7 @@ module Slk
167
183
  autoload :TextWrapper, 'slk/support/text_wrapper'
168
184
  autoload :InteractivePrompt, 'slk/support/interactive_prompt'
169
185
  autoload :DateParser, 'slk/support/date_parser'
186
+ autoload :TimeParser, 'slk/support/time_parser'
187
+ autoload :TimeRangeParser, 'slk/support/time_range_parser'
170
188
  end
171
189
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Boehs
@@ -30,6 +30,7 @@ files:
30
30
  - lib/slk/api/bots.rb
31
31
  - lib/slk/api/client.rb
32
32
  - lib/slk/api/conversations.rb
33
+ - lib/slk/api/custom_status.rb
33
34
  - lib/slk/api/dnd.rb
34
35
  - lib/slk/api/emoji.rb
35
36
  - lib/slk/api/saved.rb
@@ -66,6 +67,7 @@ files:
66
67
  - lib/slk/formatters/duration_formatter.rb
67
68
  - lib/slk/formatters/emoji_replacer.rb
68
69
  - lib/slk/formatters/json_message_formatter.rb
70
+ - lib/slk/formatters/json_status_formatter.rb
69
71
  - lib/slk/formatters/markdown_output.rb
70
72
  - lib/slk/formatters/mention_replacer.rb
71
73
  - lib/slk/formatters/message_formatter.rb
@@ -78,6 +80,7 @@ files:
78
80
  - lib/slk/formatters/search_formatter.rb
79
81
  - lib/slk/formatters/text_processor.rb
80
82
  - lib/slk/models/channel.rb
83
+ - lib/slk/models/dnd_state.rb
81
84
  - lib/slk/models/duration.rb
82
85
  - lib/slk/models/message.rb
83
86
  - lib/slk/models/preset.rb
@@ -85,8 +88,10 @@ files:
85
88
  - lib/slk/models/profile_field.rb
86
89
  - lib/slk/models/reaction.rb
87
90
  - lib/slk/models/saved_item.rb
91
+ - lib/slk/models/scheduled_status.rb
88
92
  - lib/slk/models/search_result.rb
89
93
  - lib/slk/models/status.rb
94
+ - lib/slk/models/status_snapshot.rb
90
95
  - lib/slk/models/user.rb
91
96
  - lib/slk/models/workspace.rb
92
97
  - lib/slk/runner.rb
@@ -123,6 +128,8 @@ files:
123
128
  - lib/slk/support/platform.rb
124
129
  - lib/slk/support/slack_url_parser.rb
125
130
  - lib/slk/support/text_wrapper.rb
131
+ - lib/slk/support/time_parser.rb
132
+ - lib/slk/support/time_range_parser.rb
126
133
  - lib/slk/support/user_resolver.rb
127
134
  - lib/slk/support/xdg_paths.rb
128
135
  - lib/slk/version.rb
@@ -148,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
155
  - !ruby/object:Gem::Version
149
156
  version: '0'
150
157
  requirements: []
151
- rubygems_version: 4.0.10
158
+ rubygems_version: 4.0.16
152
159
  specification_version: 4
153
160
  summary: A command-line interface for Slack
154
161
  test_files: []