shapeup-cli 0.10.0 → 0.10.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7f49a40c881c3e40aa1749a419a41139d02a5c3066113e851efecf544cddc84
4
- data.tar.gz: baa1415c2d57ea6aeeb74c4562e67773041ddd72ad9d27667bf34756faca8e56
3
+ metadata.gz: 4000fd1f4308ab79838118f7d7b7dd14b2b58401d4afc5eba1627de201c7c563
4
+ data.tar.gz: 80940c7771815abf3c296eb42f1754f791cf1bb49c1392c3afedbf4af5cd0424
5
5
  SHA512:
6
- metadata.gz: 83fb9f6f9978710170a847e270469de2b7b0d57381adb4751a0748db279477e6780d2ca87176f1f787a06cd89c5594e7cd65533c773d6b7fb12b7d0f3ee3747d
7
- data.tar.gz: '098b476ae487d54975b7726227407462452da622fb5a7b188de2bd72d42a902dfc10f2fe474874eda44069b2014b5ea7d725a021e8cd988e90c7f8fa009faf11'
6
+ metadata.gz: 027de0d927844bfa5b56725b5cb64bfc0645f0c90f38d988df4f4e4bd9bc31ca909f70778f9f96ba165acb1e8a388986c312d92970eaae14cdc66eeb58119472
7
+ data.tar.gz: ef74608be6b19b2e6ce61c0288337144c1359ba55d6168032fd7fa4f1ae4e4bbc804cec1f3a4b9f4ccd54791f3ad86f332b3f3e75f9644dfde19829040d1e52a
@@ -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 SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT => e
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(announce: true)
40
+ poll
40
41
  until once
41
- sleep @interval
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(announce: false)
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
- emit(type: "connected", agent: agent, profile: @profile, organisation_id: org_id.to_s, trust: @trust, interval: @interval) if announce
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShapeupCli
4
- VERSION = "0.10.0"
4
+ VERSION = "0.10.2"
5
5
  end
@@ -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
@@ -109,7 +112,7 @@ again straight away, without telling the user. Every line is one JSON event:
109
112
  | `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
113
  | `request` | Handle it, below. |
111
114
  | `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. |
115
+ | `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
116
 
114
117
  Don't poll the inbox yourself. The connector does that.
115
118
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shapeup-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ShapeUp