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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +124 -0
- data/exe/sf +6 -0
- data/lib/shakaflow_cli/version.rb +5 -0
- data/lib/shakaflow_cli.rb +478 -0
- metadata +53 -0
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,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: []
|