shapeup-cli 0.10.0 → 0.10.3
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 +4 -4
- data/lib/shapeup_cli/client.rb +10 -1
- data/lib/shapeup_cli/commands/base.rb +19 -0
- data/lib/shapeup_cli/commands/connect.rb +30 -5
- data/lib/shapeup_cli/version.rb +1 -1
- data/skills/shapeup-connect/SKILL.md +26 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c375b2778939c522cdc558c09d8065c5ef8a05e3fe66ffba8ff294c4bd216d30
|
|
4
|
+
data.tar.gz: 19e00ac320970b20aa0690fc971c9bbfafcc33edcb18cd6601f529173cf9da6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 053af8fe5fc51632541677b89db6473376aadf1a93120cfbbaf16d2510f85576e06d2177d7104b8e55bb1e39a90520083a6ba5dd7ec3d04d5263f08bd8aaf9d3
|
|
7
|
+
data.tar.gz: 721a2cf000f7e3f87769b4715efe8153d977a8912d5e69bff2117416c2f4edb54f43b874534ed17c1588481b7cd4448c7f2347e1f304c6e2da6ca6752b965c5c
|
data/lib/shapeup_cli/client.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
3
5
|
module ShapeupCli
|
|
4
6
|
class Client
|
|
5
7
|
class ApiError < StandardError; end
|
|
@@ -11,6 +13,12 @@ module ShapeupCli
|
|
|
11
13
|
|
|
12
14
|
MCP_PROTOCOL_VERSION = "2025-06-18"
|
|
13
15
|
|
|
16
|
+
DROPPED_CONNECTION_ERRORS = [
|
|
17
|
+
SocketError, IOError, OpenSSL::SSL::SSLError,
|
|
18
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE,
|
|
19
|
+
Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ENETDOWN, Errno::ETIMEDOUT
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
14
22
|
def initialize(host: Config.host, token: Auth.token)
|
|
15
23
|
raise AuthError, "Not authenticated" unless token
|
|
16
24
|
@host = host
|
|
@@ -68,7 +76,7 @@ module ShapeupCli
|
|
|
68
76
|
yield
|
|
69
77
|
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
70
78
|
raise NetworkError, "The server took too long to respond."
|
|
71
|
-
rescue
|
|
79
|
+
rescue *DROPPED_CONNECTION_ERRORS => e
|
|
72
80
|
raise NetworkError, "Couldn't reach #{@host} (#{e.class})."
|
|
73
81
|
end
|
|
74
82
|
|
|
@@ -77,6 +85,7 @@ module ShapeupCli
|
|
|
77
85
|
when 401 then raise AuthError, "Authentication failed — run 'shapeup login'"
|
|
78
86
|
when 403 then raise PermissionError, "Check your subscription or permissions"
|
|
79
87
|
when 429 then raise RateLimitError, "Too many requests — slow down and retry"
|
|
88
|
+
when 502..504 then raise NetworkError, "Couldn't reach #{@host} (HTTP #{response.code})."
|
|
80
89
|
when 200..299 then nil
|
|
81
90
|
else raise ApiError, "The server returned an error (HTTP #{response.code})."
|
|
82
91
|
end
|
|
@@ -7,11 +7,30 @@ module ShapeupCli
|
|
|
7
7
|
instance = new(args)
|
|
8
8
|
if instance.agent_help?
|
|
9
9
|
puts JSON.pretty_generate(instance.class.metadata)
|
|
10
|
+
elsif args.intersect?(%w[ --help -h ])
|
|
11
|
+
print_help
|
|
10
12
|
else
|
|
11
13
|
instance.execute
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
def self.print_help
|
|
18
|
+
about = metadata
|
|
19
|
+
puts "#{about[:path]}: #{about[:short]}"
|
|
20
|
+
about[:subcommands]&.then do |subcommands|
|
|
21
|
+
puts "\nSubcommands:"
|
|
22
|
+
subcommands.each { |sub| puts " #{sub[:path].to_s.ljust(48)} #{sub[:short]}" }
|
|
23
|
+
end
|
|
24
|
+
about[:flags]&.then do |flags|
|
|
25
|
+
puts "\nFlags:" if flags.any?
|
|
26
|
+
flags.each { |flag| puts " --#{flag[:name].to_s.ljust(46)} #{flag[:usage]}" }
|
|
27
|
+
end
|
|
28
|
+
about[:examples]&.then do |examples|
|
|
29
|
+
puts "\nExamples:"
|
|
30
|
+
examples.each { |example| puts " #{example}" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
15
34
|
# Override in subclasses to define structured command metadata
|
|
16
35
|
def self.metadata
|
|
17
36
|
{ command: name.split("::").last.downcase, description: "No metadata defined" }
|
|
@@ -5,6 +5,7 @@ module ShapeupCli
|
|
|
5
5
|
class Connect < Base
|
|
6
6
|
DEFAULT_INTERVAL = 30
|
|
7
7
|
TRUST_LEVELS = %w[ owner members ].freeze
|
|
8
|
+
RETRY_DELAYS = [ 5, 10, 20, 40, 80, 120 ].freeze
|
|
8
9
|
|
|
9
10
|
def self.metadata
|
|
10
11
|
{
|
|
@@ -34,16 +35,35 @@ module ShapeupCli
|
|
|
34
35
|
|
|
35
36
|
$stdout.sync = true
|
|
36
37
|
@handed_out = Set.new
|
|
37
|
-
@profile, @agent_client = sign_in_as_agent(handle)
|
|
38
|
+
@profile, @agent_client = keep_trying(forever: !once) { sign_in_as_agent(handle) }
|
|
38
39
|
|
|
39
|
-
poll
|
|
40
|
+
poll
|
|
40
41
|
until once
|
|
41
|
-
|
|
42
|
+
pause @interval
|
|
42
43
|
poll
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
private
|
|
48
|
+
def keep_trying(forever:)
|
|
49
|
+
attempt = 0
|
|
50
|
+
begin
|
|
51
|
+
yield
|
|
52
|
+
rescue Client::NetworkError, Client::RateLimitError => error
|
|
53
|
+
raise unless forever
|
|
54
|
+
|
|
55
|
+
delay = RETRY_DELAYS[attempt] || RETRY_DELAYS.last
|
|
56
|
+
emit(type: "error", message: error.message, retryable: true, retrying_in: delay)
|
|
57
|
+
pause delay
|
|
58
|
+
attempt += 1
|
|
59
|
+
retry
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def pause(seconds)
|
|
64
|
+
sleep seconds
|
|
65
|
+
end
|
|
66
|
+
|
|
47
67
|
def sign_in_as_agent(handle)
|
|
48
68
|
mine = Output.extract_data(call_tool("list_my_agents"))
|
|
49
69
|
return [ ENV["SHAPEUP_PROFILE"], client ] if mine["signed_in_as_agent"]
|
|
@@ -72,11 +92,11 @@ module ShapeupCli
|
|
|
72
92
|
exit EXIT_USAGE
|
|
73
93
|
end
|
|
74
94
|
|
|
75
|
-
def poll
|
|
95
|
+
def poll
|
|
76
96
|
inbox = Output.extract_data(@agent_client.call_tool("list_inbox", organisation: org_id.to_s))
|
|
77
97
|
agent = inbox["agent"]
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
announce(agent) unless @connected
|
|
80
100
|
|
|
81
101
|
inbox["items"].reverse_each do |item|
|
|
82
102
|
next unless @handed_out.add?(item["id"])
|
|
@@ -91,6 +111,11 @@ module ShapeupCli
|
|
|
91
111
|
emit(type: "error", message: error.message, retryable: true)
|
|
92
112
|
end
|
|
93
113
|
|
|
114
|
+
def announce(agent)
|
|
115
|
+
emit(type: "connected", agent: agent, profile: @profile, organisation_id: org_id.to_s, trust: @trust, interval: @interval)
|
|
116
|
+
@connected = true
|
|
117
|
+
end
|
|
118
|
+
|
|
94
119
|
def trusted?(requester, agent)
|
|
95
120
|
@trust == "members" || requester["id"] == agent.dig("owner", "id")
|
|
96
121
|
end
|
data/lib/shapeup_cli/version.rb
CHANGED
|
@@ -70,8 +70,8 @@ themselves.
|
|
|
70
70
|
|
|
71
71
|
| You find | Add |
|
|
72
72
|
|----------|-----|
|
|
73
|
-
| `bin/rails` | `"Bash(bin/rails test *)"` |
|
|
74
|
-
| `Rakefile` | `"Bash(bundle exec rake *)"` |
|
|
73
|
+
| `bin/rails` | `"Bash(bin/rails test *)"`, and nothing broader: never `rake *` or `bin/rails *`, which would also approve tasks that wipe databases |
|
|
74
|
+
| `Rakefile` and no `bin/rails` | `"Bash(bundle exec rake test *)"` |
|
|
75
75
|
| a `test/` folder of Ruby files and no Rakefile | `"Bash(ruby test/*)"` |
|
|
76
76
|
| `package.json` with a `test` script | `"Bash(npm test *)"` (or the yarn/pnpm equivalent the lockfile shows) |
|
|
77
77
|
| `Makefile` with a test or check target | `"Bash(make test *)"` / `"Bash(make check *)"` |
|
|
@@ -79,7 +79,10 @@ themselves.
|
|
|
79
79
|
| `go.mod` | `"Bash(go test *)"` |
|
|
80
80
|
| `Cargo.toml` | `"Bash(cargo test *)"` |
|
|
81
81
|
|
|
82
|
-
If you can't tell, ask the user how they run the tests rather than leaving it out.
|
|
82
|
+
If you can't tell, ask the user how they run the tests rather than leaving it out. Add the
|
|
83
|
+
project's linter the same way when it has one, such as `"Bash(bin/rubocop *)"`,
|
|
84
|
+
`"Bash(npm run lint *)"` or `"Bash(make check *)"`. Keep every rule to the one command it
|
|
85
|
+
names.
|
|
83
86
|
|
|
84
87
|
Merge with what is there, never replace it, and make sure `.claude/settings.local.json` is in
|
|
85
88
|
`.gitignore`. If they say no, carry on: they will approve each command instead. If prompts keep
|
|
@@ -100,16 +103,32 @@ Signed in as the user, it gives the agent a fresh token, saves it as a CLI profi
|
|
|
100
103
|
the agent, and listens as the agent. Add `--trust members` only if the user asked for anyone in
|
|
101
104
|
the organisation to be able to use the agent. The default trusts only the agent's owner.
|
|
102
105
|
|
|
103
|
-
Watch its output
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
Watch its output with the Monitor tool when you have it, using the longest timeout it allows and
|
|
107
|
+
exactly this command, with `OUTPUT` set to the connector's output file:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
f=OUTPUT; seen="$f.seen"; n=$(cat "$seen" 2>/dev/null || echo 0)
|
|
111
|
+
while true; do
|
|
112
|
+
m=$(wc -l < "$f")
|
|
113
|
+
if [ "$m" -gt "$n" ]; then
|
|
114
|
+
sed -n "$((n + 1)),${m}p" "$f" | grep -v '"retryable":true'
|
|
115
|
+
n=$m; echo "$n" > "$seen"
|
|
116
|
+
fi
|
|
117
|
+
sleep 3
|
|
118
|
+
done
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Don't use `tail -f` or `tail -F` on the output file: they can miss lines. The command keeps its place
|
|
122
|
+
in `OUTPUT.seen`, so when the watch ends but the connector is still running, start it again
|
|
123
|
+
straight away, without telling the user, and nothing is missed or repeated. If the connector
|
|
124
|
+
exits, you are told separately. Every line is one JSON event:
|
|
106
125
|
|
|
107
126
|
| `type` | What to do |
|
|
108
127
|
|--------|------------|
|
|
109
128
|
| `connected` | Tell the user the agent is listening. Note `profile`: from now on, run every ShapeUp command for this agent as `shapeup --profile <profile> ...` (leave out `--profile` when it is null). |
|
|
110
129
|
| `request` | Handle it, below. |
|
|
111
130
|
| `ignored` | Tell the user in one line who asked and that they are not trusted. Do nothing else. |
|
|
112
|
-
| `error` | Nothing while `retryable` is true. Otherwise tell the user the `message`. If the connector exits, restart it once. |
|
|
131
|
+
| `error` | Nothing while `retryable` is true: the connector keeps retrying by itself, pausing longer each time, up to two minutes. Otherwise tell the user the `message`. If the connector exits after a network error (`code` is `network`, or the message mentions a connection), wait a minute and restart it; give up and tell the user after five restarts in a row that never reach `connected`. For any other exit, restart it once. |
|
|
113
132
|
|
|
114
133
|
Don't poll the inbox yourself. The connector does that.
|
|
115
134
|
|