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 +4 -4
- data/lib/shapeup_cli/commands/connect.rb +28 -4
- data/lib/shapeup_cli/commands/doctor.rb +54 -8
- data/lib/shapeup_cli/config.rb +4 -0
- data/lib/shapeup_cli/version.rb +1 -1
- data/skills/shapeup-connect/SKILL.md +14 -20
- 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: 6efeb5fc129ef621f013c263f9f34166099966883a85802eb13cac37106fff95
|
|
4
|
+
data.tar.gz: 8b0f1597a8e9788bb8aac040990ebb164e0494b6dbd5c4eaf1b5e1399b3e1239
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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(
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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! "
|
|
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)
|
data/lib/shapeup_cli/config.rb
CHANGED
data/lib/shapeup_cli/version.rb
CHANGED
|
@@ -93,39 +93,33 @@ themselves.
|
|
|
93
93
|
|
|
94
94
|
## Listen
|
|
95
95
|
|
|
96
|
-
Start the connector as a
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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` |
|
|
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
|
|