shakaflow-cli 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5941531ee5176e9d53e3df254edb01918e8c6d3d5b28e8790b452c9728ed3e21
4
+ data.tar.gz: 7b3e5e54753ed9996fb3d9d924b7e51691e05d1962b77ecc30b2ac5ae577b737
5
+ SHA512:
6
+ metadata.gz: 4aa231099e5fed28dc52f1cb7494164aad62242f2a7b12455fdf521238389187b79e8878c9f1a1d2b1a51e10f5c33378c750257ee8337a12341342a0f0ddfbd3
7
+ data.tar.gz: 119e02b309760de3f9f813658d6e35e49cacdd7b47dad6205f3073e41f1a85bc565a13070c33fcc56c0927e69ef9874bfcaa9bc008d816d38034756d1cfb7dad
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 ShakaCode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # ShakaFlow CLI
2
+
3
+ Command-line interface for interacting with the ShakaFlow development API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install shakaflow-cli
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Before using the CLI, configure your API credentials:
14
+
15
+ ```bash
16
+ # Interactive configuration
17
+ sf config
18
+
19
+ # Or with flags
20
+ sf config --url https://shakaflow.com --token YOUR_API_TOKEN
21
+
22
+ # Configure a different environment
23
+ sf config --env staging --url https://staging.shakaflow.com --token YOUR_STAGING_TOKEN
24
+ ```
25
+
26
+ Configuration is stored in `~/.sf.yml`.
27
+
28
+ ### Environment Variables
29
+
30
+ You can also use environment variables:
31
+
32
+ - `SF_API_URL` - API URL (overrides config file)
33
+ - `SF_API_TOKEN` - API token (overrides config file)
34
+ - `SF_ENV` - Environment to use (default: production)
35
+
36
+ ## Commands
37
+
38
+ ### List LLM Providers
39
+
40
+ ```bash
41
+ sf providers
42
+ ```
43
+
44
+ ### List Channel Members
45
+
46
+ ```bash
47
+ sf members --workspace "My Workspace" --channel engineering
48
+ ```
49
+
50
+ ### Get Member Activity Context
51
+
52
+ Get raw activity data for a member (no LLM processing):
53
+
54
+ ```bash
55
+ sf member-activity-context \
56
+ --workspace "My Workspace" \
57
+ --channel engineering \
58
+ --member justin \
59
+ --days 7
60
+ ```
61
+
62
+ ### Generate Member Activity Summary
63
+
64
+ Generate an AI-powered activity summary:
65
+
66
+ ```bash
67
+ sf member-activity-summary \
68
+ --workspace "My Workspace" \
69
+ --channel engineering \
70
+ --member justin \
71
+ --days 7
72
+ ```
73
+
74
+ With custom LLM provider:
75
+
76
+ ```bash
77
+ sf member-activity-summary \
78
+ --workspace "My Workspace" \
79
+ --channel engineering \
80
+ --member justin \
81
+ --days 7 \
82
+ --provider anthropic
83
+ ```
84
+
85
+ With custom prompt:
86
+
87
+ ```bash
88
+ sf member-activity-summary \
89
+ --workspace "My Workspace" \
90
+ --channel engineering \
91
+ --member justin \
92
+ --days 7 \
93
+ --prompt "Summarize in 3 bullet points"
94
+ ```
95
+
96
+ ### Test Raw LLM Prompt
97
+
98
+ ```bash
99
+ sf prompt --message "Hello, world!"
100
+ sf prompt --message "Explain Ruby blocks" --provider openai --model gpt-4o
101
+ ```
102
+
103
+ ## Global Options
104
+
105
+ - `--env ENV` - Environment to use (production, staging). Default: production
106
+ - `--format FORMAT` - Output format (json, table). Default: table
107
+ - `--json` - Shortcut for `--format json`
108
+
109
+ ## Output Formats
110
+
111
+ By default, the CLI outputs human-readable tables. Use `--json` or `--format json` for machine-readable output:
112
+
113
+ ```bash
114
+ sf providers --json
115
+ sf members --workspace "My Workspace" --channel engineering --format json
116
+ ```
117
+
118
+ ## Getting Your API Token
119
+
120
+ API tokens are created through Slack using the `/sc api-token` command, or by your ShakaFlow administrator.
121
+
122
+ ## License
123
+
124
+ MIT License - see [LICENSE.txt](LICENSE.txt)
data/exe/sf ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "shakaflow_cli"
5
+
6
+ ShakaflowCli::CLI.new(ARGV).run
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShakaflowCli
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,478 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "yaml"
6
+ require "optparse"
7
+ require "uri"
8
+ require "date"
9
+ require "fileutils"
10
+
11
+ require_relative "shakaflow_cli/version"
12
+
13
+ module ShakaflowCli
14
+ # ShakaFlow CLI - Development API client
15
+ # Usage: sf <command> [options]
16
+ class CLI
17
+ CONFIG_FILE = ".sf.yml"
18
+ HOME_CONFIG = File.join(Dir.home, CONFIG_FILE)
19
+
20
+ def initialize(args)
21
+ @args = args
22
+ @command = args.shift
23
+ @options = {}
24
+ end
25
+
26
+ def run
27
+ case @command
28
+ when "config"
29
+ handle_config
30
+ when "providers"
31
+ handle_providers
32
+ when "prompt"
33
+ handle_prompt
34
+ when "member-activity-summary"
35
+ handle_member_activity_summary
36
+ when "member-activity-context"
37
+ handle_member_activity_context
38
+ when "members"
39
+ handle_members
40
+ when "version", "-v", "--version"
41
+ puts "shakaflow-cli #{VERSION}"
42
+ when "help", nil, "-h", "--help"
43
+ print_help
44
+ else
45
+ puts "Unknown command: #{@command}"
46
+ print_help
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def print_help
54
+ puts <<~HELP
55
+ ShakaFlow CLI - Development API client (v#{VERSION})
56
+
57
+ Usage: sf <command> [options]
58
+
59
+ Commands:
60
+ config Configure API URL and token
61
+ providers List available LLM providers
62
+ prompt Test a raw LLM prompt
63
+ member-activity-summary Generate activity summary for a member
64
+ member-activity-context Get raw activity context data (no LLM)
65
+ members List members of a channel
66
+ version Show version
67
+
68
+ Global Options:
69
+ --env ENV Environment to use (production, staging). Default: production
70
+ --format FORMAT Output format (json, table). Default: table
71
+ --help Show help for a command
72
+
73
+ Examples:
74
+ sf config --url https://shakaflow.com --token abc123
75
+ sf providers
76
+ sf members --workspace "My Workspace" --channel engineering
77
+ sf member-activity-context --workspace "My Workspace" --channel engineering --member justin --days 7
78
+ sf member-activity-summary --workspace "My Workspace" --channel engineering --member justin --days 7
79
+ sf member-activity-summary --workspace "My Workspace" --channel engineering --member justin --provider anthropic
80
+ sf member-activity-summary --workspace "My Workspace" --channel engineering --member justin --prompt-file prompt.txt
81
+ sf prompt --message "Hello, world!"
82
+ HELP
83
+ end
84
+
85
+ def handle_config
86
+ parser = OptionParser.new do |opts|
87
+ opts.banner = "Usage: sf config [options]"
88
+ opts.on("--url URL", "API URL") { |v| @options[:url] = v }
89
+ opts.on("--token TOKEN", "API token") { |v| @options[:token] = v }
90
+ opts.on("--env ENV", "Environment (production, staging)") { |v| @options[:env] = v }
91
+ end
92
+ parser.parse!(@args)
93
+
94
+ env = @options[:env] || "production"
95
+
96
+ config = load_config
97
+ config[env] ||= {}
98
+
99
+ if @options[:url]
100
+ config[env]["url"] = @options[:url]
101
+ else
102
+ print "API URL [#{config.dig(env, 'url')}]: "
103
+ input = $stdin.gets.chomp
104
+ config[env]["url"] = input unless input.empty?
105
+ end
106
+
107
+ if @options[:token]
108
+ config[env]["token"] = @options[:token]
109
+ else
110
+ print "API Token [#{config.dig(env, 'token') ? '****' : '(not set)'}]: "
111
+ input = $stdin.gets.chomp
112
+ config[env]["token"] = input unless input.empty?
113
+ end
114
+
115
+ save_config(config)
116
+ puts "Configuration saved to #{HOME_CONFIG}"
117
+ end
118
+
119
+ def handle_providers
120
+ parse_global_options
121
+ response = api_get("/api/v1/llm/providers")
122
+ output_response(response, :providers)
123
+ end
124
+
125
+ def handle_prompt
126
+ parser = OptionParser.new do |opts|
127
+ opts.banner = "Usage: sf prompt [options]"
128
+ opts.on("--message MESSAGE", "Prompt message") { |v| @options[:message] = v }
129
+ opts.on("--provider PROVIDER", "LLM provider") { |v| @options[:provider] = v }
130
+ opts.on("--model MODEL", "LLM model") { |v| @options[:model] = v }
131
+ opts.on("--temperature TEMP", Float, "Temperature") { |v| @options[:temperature] = v }
132
+ opts.on("--max-tokens TOKENS", Integer, "Max tokens") { |v| @options[:max_tokens] = v }
133
+ end
134
+ parser.parse!(@args)
135
+ parse_global_options
136
+
137
+ message = @options[:message]
138
+ if message.nil? || message.empty?
139
+ puts "Error: --message is required"
140
+ exit 1
141
+ end
142
+
143
+ body = {
144
+ messages: [{ role: "user", content: message }],
145
+ provider: @options[:provider],
146
+ model: @options[:model],
147
+ temperature: @options[:temperature],
148
+ max_tokens: @options[:max_tokens]
149
+ }.compact
150
+
151
+ response = api_post("/api/v1/llm/test_prompt", body)
152
+ output_response(response, :prompt)
153
+ end
154
+
155
+ def handle_member_activity_summary
156
+ parser = OptionParser.new do |opts|
157
+ opts.banner = "Usage: sf member-activity-summary [options]"
158
+ opts.on("--workspace WORKSPACE", "Slack workspace name") { |v| @options[:workspace] = v }
159
+ opts.on("--channel CHANNEL", "Slack channel name") { |v| @options[:channel] = v }
160
+ opts.on("--member MEMBER", "Member handle") { |v| @options[:member] = v }
161
+ opts.on("--days DAYS", Integer, "Number of days") { |v| @options[:days] = v }
162
+ opts.on("--start-date DATE", "Start date (YYYY-MM-DD)") { |v| @options[:start_date] = v }
163
+ opts.on("--end-date DATE", "End date (YYYY-MM-DD)") { |v| @options[:end_date] = v }
164
+ opts.on("--period-type TYPE", "Period type (daily, weekly, monthly)") { |v| @options[:period_type] = v }
165
+ opts.on("--provider PROVIDER", "LLM provider") { |v| @options[:provider] = v }
166
+ opts.on("--model MODEL", "LLM model") { |v| @options[:model] = v }
167
+ opts.on("--prompt PROMPT", "Custom prompt (use - for stdin)") { |v| @options[:prompt] = v }
168
+ opts.on("--prompt-file FILE", "Read prompt from file") { |v| @options[:prompt_file] = v }
169
+ opts.on("--include-context", "Include activity context in response") { @options[:include_context] = true }
170
+ end
171
+ parser.parse!(@args)
172
+ parse_global_options
173
+
174
+ validate_required(:workspace, :channel, :member)
175
+
176
+ custom_prompt = read_prompt
177
+
178
+ body = {
179
+ workspace: @options[:workspace],
180
+ channel: @options[:channel],
181
+ member: @options[:member],
182
+ days: @options[:days],
183
+ start_date: @options[:start_date],
184
+ end_date: @options[:end_date],
185
+ period_type: @options[:period_type],
186
+ provider: @options[:provider],
187
+ model: @options[:model],
188
+ custom_prompt:,
189
+ include_context: @options[:include_context] ? "true" : nil
190
+ }.compact
191
+
192
+ response = api_post("/api/v1/activity_reports/member_activity_summary", body)
193
+ output_response(response, :summary)
194
+ end
195
+
196
+ def handle_member_activity_context
197
+ parser = OptionParser.new do |opts|
198
+ opts.banner = "Usage: sf member-activity-context [options]"
199
+ opts.on("--workspace WORKSPACE", "Slack workspace name") { |v| @options[:workspace] = v }
200
+ opts.on("--channel CHANNEL", "Slack channel name") { |v| @options[:channel] = v }
201
+ opts.on("--member MEMBER", "Member handle") { |v| @options[:member] = v }
202
+ opts.on("--days DAYS", Integer, "Number of days") { |v| @options[:days] = v }
203
+ opts.on("--start-date DATE", "Start date (YYYY-MM-DD)") { |v| @options[:start_date] = v }
204
+ opts.on("--end-date DATE", "End date (YYYY-MM-DD)") { |v| @options[:end_date] = v }
205
+ opts.on("--period-type TYPE", "Period type (daily, weekly, monthly)") { |v| @options[:period_type] = v }
206
+ end
207
+ parser.parse!(@args)
208
+ parse_global_options
209
+
210
+ validate_required(:workspace, :channel, :member)
211
+
212
+ query = {
213
+ workspace: @options[:workspace],
214
+ channel: @options[:channel],
215
+ member: @options[:member],
216
+ days: @options[:days],
217
+ start_date: @options[:start_date],
218
+ end_date: @options[:end_date],
219
+ period_type: @options[:period_type]
220
+ }.compact
221
+
222
+ response = api_get("/api/v1/activity_reports/member_activity_context", query)
223
+ output_response(response, :context)
224
+ end
225
+
226
+ def handle_members
227
+ parser = OptionParser.new do |opts|
228
+ opts.banner = "Usage: sf members [options]"
229
+ opts.on("--workspace WORKSPACE", "Slack workspace name") { |v| @options[:workspace] = v }
230
+ opts.on("--channel CHANNEL", "Slack channel name") { |v| @options[:channel] = v }
231
+ end
232
+ parser.parse!(@args)
233
+ parse_global_options
234
+
235
+ validate_required(:workspace, :channel)
236
+
237
+ response = api_get("/api/v1/activity_reports/members", {
238
+ workspace: @options[:workspace],
239
+ channel: @options[:channel]
240
+ })
241
+ output_response(response, :members)
242
+ end
243
+
244
+ def parse_global_options
245
+ @options[:env] ||= ENV["SF_ENV"] || "production"
246
+ @options[:format] ||= "table"
247
+
248
+ # Check remaining args for global options
249
+ remaining = []
250
+ while (arg = @args.shift)
251
+ case arg
252
+ when "--env"
253
+ @options[:env] = @args.shift
254
+ when "--format"
255
+ @options[:format] = @args.shift
256
+ when "--json"
257
+ @options[:format] = "json"
258
+ else
259
+ remaining << arg
260
+ end
261
+ end
262
+ @args = remaining
263
+ end
264
+
265
+ def validate_required(*keys)
266
+ missing = keys.select { |k| @options[k].nil? || @options[k].to_s.empty? }
267
+ return if missing.empty?
268
+
269
+ puts "Error: Missing required options: #{missing.map { |k| "--#{k.to_s.tr('_', '-')}" }.join(', ')}"
270
+ exit 1
271
+ end
272
+
273
+ def read_prompt
274
+ if @options[:prompt_file]
275
+ File.read(@options[:prompt_file])
276
+ elsif @options[:prompt] == "-"
277
+ $stdin.read
278
+ elsif @options[:prompt]
279
+ @options[:prompt]
280
+ end
281
+ end
282
+
283
+ def load_config
284
+ if File.exist?(HOME_CONFIG)
285
+ YAML.load_file(HOME_CONFIG) || {}
286
+ elsif File.exist?(CONFIG_FILE)
287
+ YAML.load_file(CONFIG_FILE) || {}
288
+ else
289
+ {}
290
+ end
291
+ end
292
+
293
+ def save_config(config)
294
+ File.write(HOME_CONFIG, YAML.dump(config))
295
+ File.chmod(0o600, HOME_CONFIG)
296
+ end
297
+
298
+ def api_url
299
+ env = @options[:env] || "production"
300
+ url = ENV["SF_API_URL"] || load_config.dig(env, "url")
301
+ unless url
302
+ puts "Error: API URL not configured. Run 'sf config' first."
303
+ exit 1
304
+ end
305
+ url
306
+ end
307
+
308
+ def api_token
309
+ env = @options[:env] || "production"
310
+ token = ENV["SF_API_TOKEN"] || load_config.dig(env, "token")
311
+ unless token
312
+ puts "Error: API token not configured. Run 'sf config' first."
313
+ exit 1
314
+ end
315
+ token
316
+ end
317
+
318
+ def api_get(path, query = {})
319
+ uri = URI.join(api_url, path)
320
+ uri.query = URI.encode_www_form(query) unless query.empty?
321
+
322
+ request = Net::HTTP::Get.new(uri)
323
+ request["Authorization"] = "Bearer #{api_token}"
324
+ request["Content-Type"] = "application/json"
325
+
326
+ execute_request(uri, request)
327
+ end
328
+
329
+ def api_post(path, body)
330
+ uri = URI.join(api_url, path)
331
+
332
+ request = Net::HTTP::Post.new(uri)
333
+ request["Authorization"] = "Bearer #{api_token}"
334
+ request["Content-Type"] = "application/json"
335
+ request.body = body.to_json
336
+
337
+ execute_request(uri, request)
338
+ end
339
+
340
+ def execute_request(uri, request)
341
+ http = Net::HTTP.new(uri.host, uri.port)
342
+ http.use_ssl = uri.scheme == "https"
343
+ http.open_timeout = 10
344
+ http.read_timeout = 120
345
+
346
+ response = http.request(request)
347
+
348
+ case response
349
+ when Net::HTTPSuccess
350
+ JSON.parse(response.body)
351
+ else
352
+ { error: "HTTP #{response.code}: #{response.message}", body: response.body }
353
+ end
354
+ rescue StandardError => e
355
+ { error: e.message }
356
+ end
357
+
358
+ def output_response(response, type)
359
+ if @options[:format] == "json"
360
+ puts JSON.pretty_generate(response)
361
+ else
362
+ output_table(response, type)
363
+ end
364
+ end
365
+
366
+ def output_table(response, type)
367
+ if response[:error] || response["error"]
368
+ puts "Error: #{response[:error] || response['error']}"
369
+ puts response[:body] || response["body"] if response[:body] || response["body"]
370
+ exit 1
371
+ end
372
+
373
+ case type
374
+ when :providers
375
+ output_providers_table(response)
376
+ when :prompt
377
+ output_prompt_result(response)
378
+ when :summary
379
+ output_summary_result(response)
380
+ when :context
381
+ output_context_result(response)
382
+ when :members
383
+ output_members_table(response)
384
+ else
385
+ puts JSON.pretty_generate(response)
386
+ end
387
+ end
388
+
389
+ def output_providers_table(response)
390
+ puts "LLM Providers:"
391
+ puts "-" * 60
392
+ response["providers"]&.each do |provider|
393
+ default = provider["is_default"] ? " (default)" : ""
394
+ available = provider["available"] ? "+" : "-"
395
+ puts " #{available} #{provider['name']}#{default}"
396
+ puts " Model: #{provider['default_model']}"
397
+ end
398
+ end
399
+
400
+ def output_prompt_result(response)
401
+ puts "Response:"
402
+ puts response["response"]
403
+ puts
404
+ puts "Model: #{response['model']} (#{response['provider']})"
405
+ end
406
+
407
+ def output_summary_result(response)
408
+ puts "Summary:"
409
+ puts response["summary"]
410
+ puts
411
+ puts "Model: #{response['model']} (#{response['provider']})"
412
+ puts
413
+ puts "Prompt used:"
414
+ puts "-" * 60
415
+ puts response["prompt_used"]
416
+
417
+ if response["member_content"]
418
+ puts
419
+ puts "Activity Context:"
420
+ puts "-" * 60
421
+ puts JSON.pretty_generate(response["member_content"])
422
+ end
423
+ end
424
+
425
+ def output_context_result(response)
426
+ puts "Activity Context for #{response['member_name']}:"
427
+ puts "Period: #{response['start_date']} to #{response['end_date']} (#{response['period_type']})"
428
+ puts
429
+
430
+ tasks = response["tasks"] || []
431
+ if tasks.any?
432
+ puts "Tasks (#{tasks.count}):"
433
+ tasks.each do |task|
434
+ puts " [#{task['state']}] #{task['name']}"
435
+ puts " URL: #{task['url']}"
436
+ end
437
+ puts
438
+ end
439
+
440
+ prs = response["prs_without_task"] || []
441
+ if prs.any?
442
+ puts "PRs without task (#{prs.count}):"
443
+ prs.each do |pr|
444
+ puts " #{pr['title']}"
445
+ puts " URL: #{pr['url']}"
446
+ end
447
+ puts
448
+ end
449
+
450
+ reviews = response["reviews_done"] || []
451
+ if reviews.any?
452
+ puts "Reviews done (#{reviews.count}):"
453
+ reviews.each do |review|
454
+ puts " #{review['pr_title']} (#{review['state']})"
455
+ end
456
+ puts
457
+ end
458
+
459
+ stalled = response["stalled_prs"] || []
460
+ return unless stalled.any?
461
+
462
+ puts "Stalled PRs (#{stalled.count}):"
463
+ stalled.each do |pr|
464
+ puts " #{pr['title']} (#{pr['days_unresponsive']} days)"
465
+ end
466
+ end
467
+
468
+ def output_members_table(response)
469
+ puts "Channel Members:"
470
+ puts "-" * 60
471
+ response["members"]&.each do |member|
472
+ github = member["github_login"] ? " (@#{member['github_login']})" : ""
473
+ puts " #{member['handle']} - #{member['name']}#{github}"
474
+ puts " Email: #{member['email']}"
475
+ end
476
+ end
477
+ end
478
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shakaflow-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ShakaCode
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-01-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Command-line interface for interacting with ShakaFlow development API
14
+ email:
15
+ - support@shakacode.com
16
+ executables:
17
+ - sf
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE.txt
22
+ - README.md
23
+ - exe/sf
24
+ - lib/shakaflow_cli.rb
25
+ - lib/shakaflow_cli/version.rb
26
+ homepage: https://github.com/shakacode/shakaflow
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ homepage_uri: https://github.com/shakacode/shakaflow
31
+ source_code_uri: https://github.com/shakacode/shakaflow
32
+ changelog_uri: https://github.com/shakacode/shakaflow/blob/main/cli/CHANGELOG.md
33
+ rubygems_mfa_required: 'true'
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '3.1'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.5.16
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: CLI client for ShakaFlow API
53
+ test_files: []