shapeup-cli 0.10.4 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c09048590d2fb0edc18e5479d5d9a761c2d7360b165db1eb1de88887de2a1b6
4
- data.tar.gz: 4802cc019c1372cc91c8b1ea089f0a3b0af559e390a9cd54fadd85f1a293fde4
3
+ metadata.gz: 6efeb5fc129ef621f013c263f9f34166099966883a85802eb13cac37106fff95
4
+ data.tar.gz: 8b0f1597a8e9788bb8aac040990ebb164e0494b6dbd5c4eaf1b5e1399b3e1239
5
5
  SHA512:
6
- metadata.gz: 5bb78243d2cf2cc5bb30bf303a6eded89088921f13901e8d128f54096701709aadd6c75b80bdf35aa55fc5c2e884720a527316f922eb16cf2b17b4a8959cf47c
7
- data.tar.gz: 632ab3fc5ecbcc4a753b79b92735509698ca9e04c8d823d6119616a12f30328d85142107594256f015db633631b590845ec80995ba0efaba6d5a213d16295bb0
6
+ metadata.gz: 57f00b45c5e0d1c35439a490dac6ee9ff7c7a2f985b86e64c986d2b364f694c9db0aa48f577ae5bb7d0ad430fddcf018706d97d9aa2c403076086572d4985d31
7
+ data.tar.gz: 7cbc8fcc785b949da9748df97051ac74618ab73fe4d3bb6db7f85b438fd3f5b5f6e682a3449c5b80bc297037bd3234be2a30ec880bbd8d16226375eb86f853c0
@@ -16,12 +16,15 @@ module ShapeupCli
16
16
  { name: "as", type: "string", usage: "Which of your agents, by handle (default: your only one)" },
17
17
  { name: "interval", type: "integer", usage: "Seconds between checks (default: #{DEFAULT_INTERVAL})" },
18
18
  { name: "trust", type: "string", usage: "Whose requests to pass on: owner (default) or members" },
19
- { name: "once", type: "bool", usage: "Check once and exit" }
19
+ { name: "once", type: "bool", usage: "Check once and exit" },
20
+ { name: "until-request", type: "bool", usage: "Exit after the first check that passes on a request, remembering what was handed out" },
21
+ { name: "fresh", type: "bool", usage: "With --until-request, forget what earlier runs handed out" }
20
22
  ],
21
23
  examples: [
22
24
  "shapeup connect",
23
25
  "shapeup connect --as bond --trust members",
24
- "SHAPEUP_PROFILE=bond shapeup connect --once"
26
+ "SHAPEUP_PROFILE=bond shapeup connect --once",
27
+ "shapeup connect --until-request --fresh"
25
28
  ]
26
29
  }
27
30
  end
@@ -32,13 +35,14 @@ module ShapeupCli
32
35
  @trust = extract_option("--trust") || "owner"
33
36
  abort "--trust must be one of: #{TRUST_LEVELS.join(", ")}" unless TRUST_LEVELS.include?(@trust)
34
37
  once = consume_flag("--once")
38
+ @until_request = consume_flag("--until-request")
39
+ @fresh = consume_flag("--fresh")
35
40
 
36
41
  $stdout.sync = true
37
- @handed_out = Set.new
38
42
  @profile, @agent_client = keep_trying(forever: !once) { sign_in_as_agent(handle) }
39
43
 
40
44
  poll
41
- until once
45
+ until once || @requested
42
46
  pause @interval
43
47
  poll
44
48
  end
@@ -97,20 +101,40 @@ module ShapeupCli
97
101
  agent = inbox["agent"]
98
102
 
99
103
  announce(agent) unless @connected
104
+ @handed_out ||= Set.new(@until_request && !@fresh ? read_handed_out(agent) : [])
100
105
 
101
106
  inbox["items"].reverse_each do |item|
102
107
  next unless @handed_out.add?(item["id"])
103
108
 
104
109
  if trusted?(item["requester"], agent)
105
110
  emit(type: "request", **item.transform_keys(&:to_sym))
111
+ @requested = @until_request
106
112
  else
107
113
  emit(type: "ignored", id: item["id"], requester: item["requester"], why: "#{item["requester"]["name"]} is not trusted (--trust #{@trust})")
108
114
  end
109
115
  end
116
+
117
+ write_handed_out(agent, inbox["items"].map { |item| item["id"] }) if @until_request
110
118
  rescue Client::NetworkError, Client::RateLimitError => error
111
119
  emit(type: "error", message: error.message, retryable: true)
112
120
  end
113
121
 
122
+ def handed_out_path(agent)
123
+ File.join(Config::CONFIG_DIR, "connect", "#{agent["name"]}.json")
124
+ end
125
+
126
+ def read_handed_out(agent)
127
+ JSON.parse(File.read(handed_out_path(agent)))
128
+ rescue Errno::ENOENT, JSON::ParserError
129
+ []
130
+ end
131
+
132
+ def write_handed_out(agent, waiting)
133
+ @handed_out &= waiting
134
+ FileUtils.mkdir_p(File.dirname(handed_out_path(agent)))
135
+ File.write(handed_out_path(agent), JSON.generate(@handed_out.to_a))
136
+ end
137
+
114
138
  def announce(agent)
115
139
  emit(type: "connected", agent: agent, profile: @profile, organisation_id: org_id.to_s, trust: @trust, interval: @interval)
116
140
  @connected = true
@@ -33,7 +33,9 @@ module ShapeupCli
33
33
  check_api_reachable,
34
34
  check_auth,
35
35
  check_org_access,
36
- check_claude_skill
36
+ check_claude_skills,
37
+ check_agent,
38
+ check_connector
37
39
  ]
38
40
 
39
41
  report(checks)
@@ -127,7 +129,7 @@ module ShapeupCli
127
129
  def check_auth
128
130
  return skip("auth", "no token to test") unless Config.token
129
131
 
130
- result = Output.extract_data(Client.new.call_tool("list_organisations"))
132
+ result = Output.extract_data(client.call_tool("list_organisations"))
131
133
  @organisations = result.is_a?(Hash) ? (result["organisations"] || []) : Array(result)
132
134
  pass "auth", "signed in, #{@organisations.length} organisation(s)"
133
135
  rescue Client::AuthError
@@ -149,15 +151,59 @@ module ShapeupCli
149
151
  end
150
152
  end
151
153
 
152
- def check_claude_skill
153
- installed = File.join(Dir.home, ".claude", "skills", "shapeup", "SKILL.md")
154
- return warn!("claude skill", "not installed", hint: "shapeup setup claude") unless File.exist?(installed)
154
+ def check_claude_skills
155
+ states = Setup::SKILL_NAMES.to_h do |name|
156
+ installed = File.join(Dir.home, ".claude", "skills", name, "SKILL.md")
157
+ current = File.exist?(installed) && File.read(installed) == File.read(File.join(Setup::SKILLS_DIR, name, "SKILL.md"))
158
+ [ name, File.exist?(installed) ? (current ? :current : :stale) : :missing ]
159
+ end
160
+
161
+ if states.values.all?(:current)
162
+ pass "claude skills", "#{states.keys.join(", ")} installed and current"
163
+ else
164
+ behind = states.reject { |_, state| state == :current }.map { |name, state| "#{name} #{state == :missing ? "not installed" : "stale"}" }
165
+ warn! "claude skills", behind.join(", "), hint: "shapeup setup claude"
166
+ end
167
+ end
168
+
169
+ def check_agent
170
+ return skip("agent", "auth not verified") unless @organisations && Config.organisation_id
171
+
172
+ mine = Output.extract_data(client.call_tool("list_my_agents", organisation: Config.organisation_id.to_s))
173
+ return pass("agent", "signed in as an agent") if mine["signed_in_as_agent"]
174
+
175
+ @agent = mine["agents"].first
176
+ return skip("agent", mine["can_add"] ? "you have no agent here" : "agents need a Pro organisation where you're a member") unless @agent
155
177
 
156
- if File.read(installed) == File.read(Setup::SKILL_SOURCE)
157
- pass "claude skill", "installed and current"
178
+ handle = @agent["name"]
179
+ profile = Config.profile(handle)
180
+ return warn!("agent", "@#{handle} isn't connected on this computer yet", hint: "/shapeup-connect in Claude Code") unless profile&.dig("token")
181
+
182
+ Client.new(host: profile["host"] || Config.host, token: profile["token"])
183
+ .call_tool("list_inbox", organisation: (profile["organisation_id"] || Config.organisation_id).to_s)
184
+ @connected_agent = handle
185
+ pass "agent", "@#{handle} signs in with profile '#{handle}'"
186
+ rescue Client::AuthError
187
+ fail! "agent", "@#{handle}'s saved token was rejected", hint: "/shapeup-connect in Claude Code"
188
+ rescue Client::ApiError => e
189
+ fail! "agent", e.message
190
+ end
191
+
192
+ def check_connector
193
+ return skip("connector", "no connected agent to listen for") unless @connected_agent
194
+
195
+ if connector_running_for?(@connected_agent)
196
+ pass "connector", "listening as @#{@connected_agent} on this computer"
158
197
  else
159
- warn! "claude skill", "installed but stale", hint: "shapeup setup claude"
198
+ warn! "connector", "not listening as @#{@connected_agent} on this computer", hint: "/shapeup-connect in Claude Code"
160
199
  end
200
+ rescue Errno::ENOENT
201
+ skip "connector", "can't check for running processes here"
202
+ end
203
+
204
+ def connector_running_for?(handle)
205
+ commands = IO.popen([ "pgrep", "-lf", "shapeup connect" ], err: File::NULL, &:readlines)
206
+ commands.any? { |command| command.match?(/--as @?#{Regexp.escape(handle)}\b/) || !command.include?("--as") }
161
207
  end
162
208
 
163
209
  def pass(name, message) = Check.new(name: name, status: :pass, message: message)
@@ -66,6 +66,10 @@ module ShapeupCli
66
66
  end
67
67
  end
68
68
 
69
+ def self.profile(name)
70
+ load_profiles_raw.dig("profiles", name)
71
+ end
72
+
69
73
  def self.current_profile
70
74
  data = load_profiles_raw
71
75
  name = ENV["SHAPEUP_PROFILE"] || data["default"]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShapeupCli
4
- VERSION = "0.10.4"
4
+ VERSION = "0.11.0"
5
5
  end
@@ -93,39 +93,33 @@ themselves.
93
93
 
94
94
  ## Listen
95
95
 
96
- Start the connector as a background command and keep it running:
96
+ Start the connector in the background, as a command you are told about when it finishes (in
97
+ Claude Code: Bash with `run_in_background`):
97
98
 
98
99
  ```bash
99
- shapeup connect [--as <handle>]
100
+ shapeup connect --until-request --fresh [--as <handle>]
100
101
  ```
101
102
 
102
103
  Signed in as the user, it gives the agent a fresh token, saves it as a CLI profile named after
103
104
  the agent, and listens as the agent. Add `--trust members` only if the user asked for anyone in
104
105
  the organisation to be able to use the agent. The default trusts only the agent's owner.
105
106
 
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:
107
+ It checks the inbox every 30 seconds and finishes as soon as it has passed on a request. When it
108
+ finishes, read its whole output and act on every line, then start it again straight away, in the
109
+ background, as the agent and without `--fresh` (keep `--trust members` if you used it):
108
110
 
109
111
  ```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
112
+ shapeup --profile <profile> connect --until-request
119
113
  ```
120
114
 
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:
115
+ `--fresh` is only for the first start in a session: it hands out again anything an earlier
116
+ session left unfinished. Without it, the connector never hands out the same request twice.
117
+ Don't watch its output with Monitor or `tail`: the connector finishing is what wakes you, even
118
+ when your last turn ended in an error. Every line is one JSON event:
125
119
 
126
120
  | `type` | What to do |
127
121
  |--------|------------|
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). |
122
+ | `connected` | The first time, tell the user the agent is listening; after a restart, say nothing. Note `profile`: from now on, run every ShapeUp command for this agent as `shapeup --profile <profile> ...` (leave out `--profile` when it is null). |
129
123
  | `request` | Handle it, below. |
130
124
  | `ignored` | Tell the user in one line who asked and that they are not trusted. Do nothing else. |
131
125
  | `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. |
@@ -134,7 +128,7 @@ Don't poll the inbox yourself. The connector does that.
134
128
 
135
129
  ## Handle a request, within seconds
136
130
 
137
- A `request` line carries `id`, `reason` (mentioned, assigned or watching), `issue`, `request`
131
+ A `request` line carries `id`, `reason` (mentioned, replied, assigned or watching), `issue`, `request`
138
132
  (the words that asked), `requester` and `reply_to`.
139
133
 
140
134
  1. **Acknowledge it.** When `request.comment_id` is set, react to it:
@@ -148,7 +142,7 @@ A `request` line carries `id`, `reason` (mentioned, assigned or watching), `issu
148
142
  `shapeup --profile <profile> issues move <issue_id> --column "<name>"`. If there is none, skip this.
149
143
  3. **Hand it off.** Start one background subagent per request, with the request line and the
150
144
  worker brief below. Tell the user in one line, for example "@bond picked up #42 from @miles".
151
- 4. **Keep listening.**
145
+ 4. **Keep listening.** Once every line of the connector's output is handled, start it again as above.
152
146
 
153
147
  ## Worker brief
154
148
 
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.4
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ShapeUp