claude-inbox 0.2.0 → 0.3.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/README.md +160 -152
- data/exe/claude-inbox +23 -4
- data/lib/claude_inbox/agents_client.rb +57 -55
- data/lib/claude_inbox/app.rb +42 -26
- data/lib/claude_inbox/config.rb +24 -0
- data/lib/claude_inbox/dialog.rb +3 -1
- data/lib/claude_inbox/images.rb +37 -6
- data/lib/claude_inbox/job_state.rb +23 -8
- data/lib/claude_inbox/keymap.rb +1 -1
- data/lib/claude_inbox/new_session_form.rb +29 -39
- data/lib/claude_inbox/painter.rb +32 -0
- data/lib/claude_inbox/palette.rb +4 -7
- data/lib/claude_inbox/peek.rb +2 -2
- data/lib/claude_inbox/poller.rb +4 -3
- data/lib/claude_inbox/pull_requests.rb +5 -5
- data/lib/claude_inbox/records.rb +7 -2
- data/lib/claude_inbox/remote/http.rb +137 -0
- data/lib/claude_inbox/remote/icon.png +0 -0
- data/lib/claude_inbox/remote/icon.svg +8 -0
- data/lib/claude_inbox/remote/listener.rb +436 -0
- data/lib/claude_inbox/remote/page.html +477 -0
- data/lib/claude_inbox/remote/pairing.rb +111 -0
- data/lib/claude_inbox/remote/pairing_dialog.rb +93 -0
- data/lib/claude_inbox/remote/start.rb +202 -0
- data/lib/claude_inbox/remote.rb +10 -0
- data/lib/claude_inbox/renderer.rb +57 -57
- data/lib/claude_inbox/session.rb +7 -21
- data/lib/claude_inbox/session_request.rb +151 -0
- data/lib/claude_inbox/sessions.rb +0 -1
- data/lib/claude_inbox/settings.rb +19 -7
- data/lib/claude_inbox/store.rb +0 -2
- data/lib/claude_inbox/subprocess.rb +9 -1
- data/lib/claude_inbox/terminal.rb +9 -3
- data/lib/claude_inbox/text.rb +7 -1
- data/lib/claude_inbox/trust.rb +23 -0
- data/lib/claude_inbox/vt_screen.rb +1 -1
- data/lib/claude_inbox.rb +1 -1
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fc36460c912469438541ba91cd22f8c3635bb3fd80b60b695afe148f8230ee0
|
|
4
|
+
data.tar.gz: 105f161459cb30bd1d3751e2a3c926225e5087279b9056bafee46474542bca0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f31e385cff9fb60a06e7bcce8ba965398491ea65c2792ce9d609654a1d0fc5b417dcd147c0962145badf49da6bb0de734a4bcbae75c7055dbb57ec6090889087
|
|
7
|
+
data.tar.gz: 889b409820e77ec9d6b4a51de365f3524eac013b55131ac8de4d2b0eae9e573ab11c9a12c5240aea0ef40c74211893240adc22283a439f187668ce48949766ce
|
data/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# claude-inbox
|
|
2
2
|
|
|
3
|
-
Inbox-style triage
|
|
4
|
-
`claude agents`,
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Inbox-style triage for Claude Code's background sessions. It reads the same
|
|
4
|
+
daemon state as `claude agents`, through `claude agents --json`, and adds
|
|
5
|
+
snooze, auto-settle and each session's pull request. Use it next to
|
|
6
|
+
`claude agents`, not instead of it.
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|
|
|
@@ -18,51 +18,46 @@ Requires Ruby 3.2+ and a `claude` on PATH with the agents feature. Versions
|
|
|
18
18
|
are published to rubygems and listed under GitHub Releases.
|
|
19
19
|
`claude-inbox --version` prints the installed version.
|
|
20
20
|
|
|
21
|
+
Put arguments you want on every launch in `~/.config/claude-inbox/config`.
|
|
22
|
+
|
|
21
23
|
## Sections
|
|
22
24
|
|
|
23
|
-
1. **Pinned.** Parked at the top by hand,
|
|
24
|
-
2. **Needs you.** `blocked` or `failed`, until you attach to it
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
prints too: what it is waiting on while blocked (`↳ confirm: drop the top
|
|
54
|
-
line?`), what it produced once done, otherwise its status line. A terminal you
|
|
55
|
-
opened yourself has none and shows its directory instead.
|
|
56
|
-
|
|
57
|
-
A session you gave a color to with `/color` wears it on the label. Nothing
|
|
58
|
-
else is tinted, so a blocked session still looks blocked.
|
|
25
|
+
1. **Pinned.** Parked at the top by hand, whatever its state. `t` toggles it.
|
|
26
|
+
2. **Needs you.** `blocked` or `failed`, until you attach to it.
|
|
27
|
+
3. **Active.** `working`, finished but not yet settled, interactive, or a
|
|
28
|
+
needs-you session you attached to that hasn't resolved. An interactive
|
|
29
|
+
session only reports busy, idle or waiting, which show as working, done
|
|
30
|
+
and needs you. A terminal you're sitting in never settles and can't be
|
|
31
|
+
attached, peeked or stopped from here. Sub-agents and headless `claude -p`
|
|
32
|
+
runs don't show up at all.
|
|
33
|
+
4. **Snoozed.** Sorted by wake time, with "until I wake it" entries last.
|
|
34
|
+
Collapsed. Enter expands it.
|
|
35
|
+
5. **Settled.** Settled with `x`, or every pull request is merged or closed.
|
|
36
|
+
Collapsed. Enter expands it.
|
|
37
|
+
|
|
38
|
+
A row with a pull request shows it after the state, as `#885 open`,
|
|
39
|
+
`#885 draft`, `#885 merged` or `#885 closed`. `o` opens it in the browser.
|
|
40
|
+
|
|
41
|
+
The daemon's `working` covers two cases, and the row tells them apart. A
|
|
42
|
+
spinner and `working` mean the agent is thinking. A steady `◌` and
|
|
43
|
+
`idle · 1 shell` mean it stopped and is waiting on something it started, such
|
|
44
|
+
as a `--watch` shell or a sub-agent. That's why a session with nothing left to
|
|
45
|
+
do can sit there for an hour.
|
|
46
|
+
|
|
47
|
+
Under each row in Pinned, Needs you and Active is the same line
|
|
48
|
+
`claude agents` prints. While blocked it's what the session waits on
|
|
49
|
+
(`↳ confirm: drop the top line?`), once done it's what it produced, and
|
|
50
|
+
otherwise it's the status line. A terminal you opened yourself has none, so
|
|
51
|
+
it shows its directory.
|
|
52
|
+
|
|
53
|
+
A session you gave a color with `/color` wears it on the label. Nothing else
|
|
54
|
+
is tinted, so a blocked session still looks blocked.
|
|
59
55
|
|
|
60
56
|
## Keys
|
|
61
57
|
|
|
62
|
-
Vim bindings
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
scrolls its own history and clicks do nothing.
|
|
58
|
+
Vim bindings. Arrows work as well as `j`/`k`. In terminals that report the
|
|
59
|
+
mouse, a click selects and attaches and the wheel moves the selection. Apple
|
|
60
|
+
Terminal reports neither.
|
|
66
61
|
|
|
67
62
|
| Key | Action |
|
|
68
63
|
|---|---|
|
|
@@ -75,7 +70,8 @@ scrolls its own history and clicks do nothing.
|
|
|
75
70
|
| `za` `zo` `zc` | toggle / open / close the Snoozed or Settled fold under the cursor |
|
|
76
71
|
| `p` | toggle the read-only peek pane |
|
|
77
72
|
| `J` `K` (`Ctrl-e` `Ctrl-y`) | scroll the peek pane |
|
|
78
|
-
| `n` | new session
|
|
73
|
+
| `n` | new session |
|
|
74
|
+
| `N` | pair a phone: whether the inbox is listening, and the URL to pair with |
|
|
79
75
|
| `t` | pin / unpin |
|
|
80
76
|
| `s` | snooze: `1` 15m · `2` 1h · `3` tomorrow 9am · `4` until woken |
|
|
81
77
|
| `u` | wake a snoozed session now, or bring back one you settled |
|
|
@@ -90,129 +86,142 @@ scrolls its own history and clicks do nothing.
|
|
|
90
86
|
| `R` | poll now |
|
|
91
87
|
| `q` | quit |
|
|
92
88
|
|
|
93
|
-
`X` and `Ctrl-x` both ask first
|
|
94
|
-
|
|
95
|
-
`Enter` resumes it later. `Ctrl-x` runs `claude rm
|
|
96
|
-
transcript and its worktree
|
|
97
|
-
box before answering.
|
|
89
|
+
`X` and `Ctrl-x` both ask first and both take `y`, but they do different
|
|
90
|
+
things. `X` runs `claude stop`, which ends the process and keeps the
|
|
91
|
+
conversation, and `Enter` resumes it later. `Ctrl-x` runs `claude rm`, which
|
|
92
|
+
deletes the session, its transcript and its worktree. Nothing resumes after
|
|
93
|
+
that, so read the box before answering.
|
|
98
94
|
|
|
99
95
|
### New session
|
|
100
96
|
|
|
101
|
-
`n` opens a form
|
|
102
|
-
worktree
|
|
103
|
-
fields cycle with `h` `l` or the arrows
|
|
104
|
-
resolve to. The directory
|
|
105
|
-
it.
|
|
106
|
-
|
|
107
|
-
`Ctrl-S` starts the session and drops you back in the inbox
|
|
108
|
-
it and attaches.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
The prompt is multi-line
|
|
112
|
-
(or `Ctrl-V`)
|
|
113
|
-
|
|
114
|
-
[Image #1]". Pasted images are
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Remote Control set to yes
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
but still work.
|
|
97
|
+
`n` opens a form with prompt, name, directory, model, effort, permissions,
|
|
98
|
+
worktree and Remote Control. `Tab` and `Shift+Tab` move between fields and
|
|
99
|
+
`Esc` cancels. Choice fields cycle with `h` `l` or the arrows, and "default"
|
|
100
|
+
shows what your settings resolve to. The directory starts as the selected
|
|
101
|
+
row's, and `Tab` completes it.
|
|
102
|
+
|
|
103
|
+
`Ctrl-S` starts the session and drops you back in the inbox. `Ctrl-O` starts
|
|
104
|
+
it and attaches. It runs `claude --bg "<prompt>"` in that directory with only
|
|
105
|
+
the flags you changed, and the new row is selected once it shows up.
|
|
106
|
+
|
|
107
|
+
The prompt is multi-line, and `Enter` breaks a line. Paste an image with
|
|
108
|
+
`Cmd-V` (or `Ctrl-V`) or drop a file on the window, and an `[Image #1]` token
|
|
109
|
+
lands at the cursor for the prompt to point at: "make the button look like
|
|
110
|
+
[Image #1]". Pasted images are kept in `~/.config/claude-inbox/images/` for
|
|
111
|
+
14 days. A dropped file is referenced where it is. Pasting images works on
|
|
112
|
+
macOS only.
|
|
113
|
+
|
|
114
|
+
Remote Control set to yes adds `--remote-control`, so the session also shows
|
|
115
|
+
up at claude.ai/code and in the Claude mobile app. It defaults to yes when
|
|
116
|
+
"Enable Remote Control for all sessions" is on in `/config`.
|
|
117
|
+
|
|
118
|
+
Type `/` at the start of a word for a menu of your skills and commands.
|
|
119
|
+
Plugin skills appear as `plugin:name` and claude.ai's synced skills as
|
|
120
|
+
`anthropic-skills:name`. `↑` `↓` choose, `Tab` or `Enter` insert, `Esc`
|
|
121
|
+
closes the menu. Built-ins such as `/init` aren't listed but still work.
|
|
127
122
|
|
|
128
123
|
## How sessions move
|
|
129
124
|
|
|
130
|
-
**Wake.** A snoozed session
|
|
131
|
-
`u`, or when it
|
|
132
|
-
was already blocked when you snoozed it stays snoozed
|
|
133
|
-
snoozing. `u` also brings back a settled row
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
aren't pushed, and nothing here ever overrides that refusal. Every reap
|
|
161
|
-
appends a line to `~/.config/claude-inbox/reaped.log`, the last record a
|
|
162
|
-
session existed once its transcript is gone. `CLAUDE_INBOX_NO_REAP=1` turns
|
|
163
|
-
reaping off.
|
|
125
|
+
**Wake.** A snoozed session comes back when its timer runs out, when you
|
|
126
|
+
press `u`, or when it turns blocked or failed after you snoozed it. One that
|
|
127
|
+
was already blocked when you snoozed it stays snoozed, which is the point of
|
|
128
|
+
snoozing. `u` also brings back a settled row.
|
|
129
|
+
|
|
130
|
+
**Acknowledge.** Attaching to a needs-you session marks its state seen and
|
|
131
|
+
moves it to Active. It returns to Needs you when its state changes again. A
|
|
132
|
+
new prompt while still blocked doesn't count.
|
|
133
|
+
|
|
134
|
+
**Settle.** A session settles when you press `x` or when every pull request
|
|
135
|
+
it has is merged or closed. A hand-settled session stays put when it
|
|
136
|
+
finishes, and comes back on any other state change. `failed` never settles.
|
|
137
|
+
A session with no pull request never settles on its own, and one with an
|
|
138
|
+
open or draft PR stays Active.
|
|
139
|
+
|
|
140
|
+
**Reap.** On the next poll, the inbox deletes a background session that has
|
|
141
|
+
been quiet for 14 days with `claude rm`, transcript and worktree included.
|
|
142
|
+
It's the one thing here that deletes without asking. Reaping ignores Settled,
|
|
143
|
+
because an open draft keeps a row in Active for ever and those are exactly
|
|
144
|
+
the rows that need reaping. Idle time is the only clock.
|
|
145
|
+
|
|
146
|
+
The inbox never reaps a `working` session, one that still holds a process, a
|
|
147
|
+
pinned one or a snoozed one. It does reap `failed`, even though failed never
|
|
148
|
+
settles. After a fortnight of not looking at it, you won't. Pin or park a
|
|
149
|
+
session to keep it.
|
|
150
|
+
|
|
151
|
+
Unpushed work is safe, because `claude rm` refuses a worktree with unpushed
|
|
152
|
+
commits and the inbox never overrides that. Every reap adds a line to
|
|
153
|
+
`~/.config/claude-inbox/reaped.log`. `CLAUDE_INBOX_NO_REAP=1` turns reaping
|
|
154
|
+
off.
|
|
164
155
|
|
|
165
156
|
## Remote Control
|
|
166
157
|
|
|
167
|
-
|
|
168
|
-
it from a browser or the Claude mobile app
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
158
|
+
A session with Remote Control on has a page at claude.ai/code, where you can
|
|
159
|
+
follow and drive it from a browser or the Claude mobile app. `w` opens it,
|
|
160
|
+
and the row shows `⇅` after the state. Two kinds of session have it.
|
|
161
|
+
|
|
162
|
+
- A background session started with Remote Control on, from the `n` form or
|
|
163
|
+
as `claude --bg "…" --remote-control`. It behaves like any other row, and
|
|
164
|
+
keeps Remote Control across a stop and a resume.
|
|
165
|
+
- A worker a `claude remote-control` server spawned when you started a
|
|
166
|
+
session from your phone. The daemon can't attach to it, so `Enter` offers
|
|
167
|
+
to adopt it. `y` ends the worker, resumes the conversation as a background
|
|
168
|
+
session under the same id with Remote Control on, and attaches. The phone
|
|
169
|
+
session you were in ends and a new one with the whole conversation takes
|
|
170
|
+
its place. The server keeps running. A worker nobody has messaged yet has
|
|
171
|
+
nothing to adopt.
|
|
172
|
+
|
|
173
|
+
## Starting sessions from your phone
|
|
174
|
+
|
|
175
|
+
With `--listen-lan`, a phone on your Wi-Fi or VPN can start sessions the way
|
|
176
|
+
`n` does. It's off unless you ask for it.
|
|
177
|
+
|
|
178
|
+

|
|
179
|
+
|
|
180
|
+
1. Start the inbox with `claude-inbox --listen-lan`, or put `--listen-lan`
|
|
181
|
+
in `~/.config/claude-inbox/config` or `export CLAUDE_INBOX_LISTEN=lan` in
|
|
182
|
+
your shell rc.
|
|
183
|
+
2. Press `N`, then `c` to copy the pairing URL. Universal Clipboard takes it
|
|
184
|
+
to the phone.
|
|
185
|
+
3. Open it in Safari. To keep it, tap Share, then Add to Home Screen.
|
|
186
|
+
4. Open the new Inbox icon. It has its own storage, apart from Safari's, so
|
|
187
|
+
paste the pairing URL once more and tap Pair.
|
|
188
|
+
|
|
189
|
+
LAN mode is plain HTTP, so the token and every prompt cross the network
|
|
190
|
+
readable by anyone on it. Use it only on a network you control. Anyone with
|
|
191
|
+
the token can start sessions as you in your projects. If the URL leaks, press
|
|
192
|
+
`r` in the `N` dialog to replace it.
|
|
193
|
+
|
|
194
|
+
A remote start may use the `default`, `auto` and `plan` permission modes.
|
|
195
|
+
[docs/remote-start.md](docs/remote-start.md) covers the rest, including ssh,
|
|
196
|
+
the firewall prompt, the JSON API and an iOS Shortcut.
|
|
186
197
|
|
|
187
198
|
## Pull request state
|
|
188
199
|
|
|
189
200
|
The daemon scans each background session's transcript for PR links, so a
|
|
190
|
-
session that only reviews a PR gets it too.
|
|
191
|
-
|
|
201
|
+
session that only reviews a PR gets it too. It doesn't scan interactive
|
|
202
|
+
sessions. Use `P` to link one by hand, or to fix a bad scan.
|
|
192
203
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
all you get. The list goes up before gh is asked, so the inbox is up in under
|
|
196
|
-
a second.
|
|
204
|
+
The state comes from Claude Code's own cache, then from `gh pr view` at most
|
|
205
|
+
once a minute per PR. Without `gh`, you only get the cached state.
|
|
197
206
|
|
|
198
|
-
##
|
|
207
|
+
## Usage bars
|
|
199
208
|
|
|
200
|
-
The right end of the header shows how much of your
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
status lines draw:
|
|
209
|
+
The right end of the header shows how much of your 5-hour and 7-day rate
|
|
210
|
+
limit windows you've used. Each is a ten-cell bar that turns yellow at 70%
|
|
211
|
+
and red at 90%.
|
|
204
212
|
|
|
205
213
|
```
|
|
206
214
|
session ██░░░░░░░░ 24% · 3h left week ████░░░░░░ 41% · 2d left
|
|
207
215
|
```
|
|
208
216
|
|
|
209
|
-
`session` is the 5-hour window and `week` the 7-day one, as
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
`session` is the 5-hour window and `week` the 7-day one, as `/usage` names
|
|
218
|
+
them. `3h left` is the time until that window resets. The bars need a Pro or
|
|
219
|
+
Max account and a one-time setup.
|
|
212
220
|
|
|
213
|
-
Claude Code passes the numbers to your
|
|
214
|
-
script every turn,
|
|
215
|
-
|
|
221
|
+
Claude Code passes the numbers to your
|
|
222
|
+
[status line](https://code.claude.com/docs/en/statusline) script every turn,
|
|
223
|
+
and the inbox reads them from a file that script writes. Put these lines in
|
|
224
|
+
the script, just after it reads stdin:
|
|
216
225
|
|
|
217
226
|
```bash
|
|
218
227
|
input=$(cat)
|
|
@@ -220,13 +229,12 @@ limits=$(echo "$input" | jq -c '.rate_limits // empty')
|
|
|
220
229
|
[ -n "$limits" ] && echo "$limits" > ~/.claude/rate_limits.json.tmp && mv ~/.claude/rate_limits.json.tmp ~/.claude/rate_limits.json
|
|
221
230
|
```
|
|
222
231
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
blank the file.
|
|
232
|
+
With no status line script, run `/statusline` in any session to get one,
|
|
233
|
+
then add the lines. Keep the `-n` check. A session's first status line run
|
|
234
|
+
has no numbers yet, and writing anyway would blank the file.
|
|
227
235
|
|
|
228
|
-
The bars stay
|
|
229
|
-
|
|
236
|
+
The bars stay hidden until the file exists, and hide again once it's more
|
|
237
|
+
than fifteen minutes old.
|
|
230
238
|
|
|
231
239
|
## Contributing
|
|
232
240
|
|
data/exe/claude-inbox
CHANGED
|
@@ -9,24 +9,43 @@ if ARGV.include?("--version")
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
require "claude_inbox/app"
|
|
12
|
+
require "claude_inbox/config"
|
|
13
|
+
require "tmpdir"
|
|
14
|
+
|
|
15
|
+
fixture = ARGV.include?("--fixture")
|
|
16
|
+
# A fixture run takes the listen flags, for trying the API with curl, but
|
|
17
|
+
# not the environment or the config file, which are for the real inbox.
|
|
18
|
+
begin
|
|
19
|
+
argv = fixture ? ARGV : ClaudeInbox::Config.argv(ARGV)
|
|
20
|
+
listen = ClaudeInbox::Remote::Listener.options(argv, fixture ? {} : ENV)
|
|
21
|
+
rescue ArgumentError => e
|
|
22
|
+
abort "claude-inbox: #{e.message}"
|
|
23
|
+
end
|
|
12
24
|
|
|
13
25
|
# Crash traces go here instead of the alt screen when set.
|
|
14
26
|
$stderr.reopen(ENV["CLAUDE_INBOX_STDERR"], "a") if ENV["CLAUDE_INBOX_STDERR"]
|
|
15
27
|
|
|
16
28
|
opts = {}
|
|
17
|
-
if
|
|
18
|
-
agents =
|
|
29
|
+
if fixture
|
|
30
|
+
agents = argv[argv.index("--fixture") + 1]
|
|
19
31
|
fixtures = File.dirname(agents)
|
|
20
32
|
opts[:client] = ClaudeInbox::FixtureClient.new(agents, logs: File.binread(File.join(fixtures, "logs_raw.txt")))
|
|
21
33
|
opts[:store] = ClaudeInbox::Store.new(path: nil)
|
|
22
34
|
opts[:pull_requests] = ClaudeInbox::PullRequests.new(cache_path: File.join(fixtures, "gh-pr-status-cache.json"), resolved_path: nil, gh: nil)
|
|
35
|
+
# Its own token, lock and images, so a test inbox can neither rotate the
|
|
36
|
+
# token phones are paired with nor hold the real inbox's lock.
|
|
37
|
+
dir = File.join(Dir.tmpdir, "claude-inbox-fixture")
|
|
38
|
+
opts[:listen] = listen&.merge(fixture: true, pairing: ClaudeInbox::Remote::Pairing.new(path: File.join(dir, "listen.json")),
|
|
39
|
+
lock_path: File.join(dir, "listen.lock"), images_dir: File.join(dir, "images"))
|
|
23
40
|
else
|
|
24
41
|
# The only place the reaper is armed. Everything else — tests, --fixture,
|
|
25
|
-
# anything embedding App — gets one that deletes nothing.
|
|
42
|
+
# anything embedding App — gets one that deletes nothing. The listener,
|
|
43
|
+
# likewise, comes only from this file, and only when asked for.
|
|
26
44
|
opts[:client] = ClaudeInbox::AgentsClient.new
|
|
27
45
|
opts[:store] = ClaudeInbox::Store.new
|
|
28
46
|
opts[:reaper] = ClaudeInbox::Reaper.new(opts[:client], opts[:store])
|
|
47
|
+
opts[:listen] = listen
|
|
29
48
|
end
|
|
30
|
-
opts[:color] = false if
|
|
49
|
+
opts[:color] = false if argv.include?("--no-color")
|
|
31
50
|
|
|
32
51
|
ClaudeInbox::App.new(**opts).run
|
|
@@ -46,7 +46,7 @@ module ClaudeInbox
|
|
|
46
46
|
# and, the moment it becomes the agents view, terminate it. The user then
|
|
47
47
|
# lands back in the inbox instead of native agent view.
|
|
48
48
|
def attach(id)
|
|
49
|
-
pid = Process.spawn(@bin, "attach", id)
|
|
49
|
+
pid = Process.spawn(*Subprocess.command(@bin, "attach", id))
|
|
50
50
|
watchdog = Thread.new { kill_when_agents_view(pid) }
|
|
51
51
|
_, status = Process.wait2(pid)
|
|
52
52
|
status
|
|
@@ -64,10 +64,7 @@ module ClaudeInbox
|
|
|
64
64
|
|
|
65
65
|
# Start a background session. Returns its short id.
|
|
66
66
|
def spawn(prompt:, cwd:, **opts)
|
|
67
|
-
|
|
68
|
-
r = Subprocess.capture(*argv, chdir: cwd)
|
|
69
|
-
raise Error, "claude --bg failed: #{(r.err + r.out).strip}" unless r.success?
|
|
70
|
-
r.out[/\b[0-9a-f]{8}\b/] || r.out.strip
|
|
67
|
+
start_bg(self.class.spawn_args(@bin, prompt: prompt, **opts), cwd, "claude --bg")
|
|
71
68
|
end
|
|
72
69
|
|
|
73
70
|
# How a prompt attaches a file: the same @ mention the CLI's own prompt
|
|
@@ -75,18 +72,19 @@ module ClaudeInbox
|
|
|
75
72
|
# its path completion escapes them; a quoted path is not recognized.
|
|
76
73
|
def self.mention(path) = "@" + path.gsub(" ", "\\ ")
|
|
77
74
|
|
|
78
|
-
# Pure so it can be tested:
|
|
79
|
-
# --
|
|
80
|
-
#
|
|
75
|
+
# Pure so it can be tested: a setting left nil leaves its flag off. The
|
|
76
|
+
# prompt goes after "--", where a leading dash is text rather than an
|
|
77
|
+
# unknown option, and --remote-control sits just before it: its optional
|
|
78
|
+
# name would otherwise eat the prompt (both checked on 2.1.282).
|
|
81
79
|
def self.spawn_args(bin, prompt:, model: nil, effort: nil, permission_mode: nil, worktree: false, name: nil, remote: false)
|
|
82
|
-
argv = [bin, "--bg"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
argv = [bin, "--bg"]
|
|
81
|
+
{"--model" => model, "--effort" => effort, "--permission-mode" => permission_mode}.each do |flag, value|
|
|
82
|
+
argv += [flag, value] if value
|
|
83
|
+
end
|
|
86
84
|
argv += ["--name", name] if name && !name.strip.empty?
|
|
87
85
|
argv << "--worktree" if worktree
|
|
88
86
|
argv << "--remote-control" if remote
|
|
89
|
-
argv
|
|
87
|
+
argv + ["--", prompt]
|
|
90
88
|
end
|
|
91
89
|
|
|
92
90
|
# Pull a conversation a `claude remote-control` server is serving into
|
|
@@ -98,9 +96,7 @@ module ClaudeInbox
|
|
|
98
96
|
raise Error, "nothing to adopt yet — send it a message from your phone first" if transcript_empty?(cwd, session_id)
|
|
99
97
|
Process.kill("TERM", pid)
|
|
100
98
|
wait_gone(pid)
|
|
101
|
-
|
|
102
|
-
raise Error, "claude --bg --resume failed: #{(r.err + r.out).strip}" unless r.success?
|
|
103
|
-
r.out[/\b[0-9a-f]{8}\b/] || r.out.strip
|
|
99
|
+
start_bg(self.class.adopt_args(@bin, session_id), cwd, "claude --bg --resume")
|
|
104
100
|
end
|
|
105
101
|
|
|
106
102
|
def self.adopt_args(bin, session_id) = [bin, "--bg", "--resume", session_id, "--remote-control"]
|
|
@@ -111,6 +107,45 @@ module ClaudeInbox
|
|
|
111
107
|
File.join(home, ".claude", "projects", cwd.gsub(%r{[/.]}, "-"), "#{session_id}.jsonl")
|
|
112
108
|
end
|
|
113
109
|
|
|
110
|
+
# Flags that mark a claude process as one a program drives rather than one
|
|
111
|
+
# you type in: the headless print mode and the SDK's stream protocol.
|
|
112
|
+
HEADLESS_FLAGS = %w[-p --print --input-format --output-format].freeze
|
|
113
|
+
|
|
114
|
+
# pid => origin, given `ps` for the sessions and for their parents. Pure.
|
|
115
|
+
#
|
|
116
|
+
# :remote a Remote Control worker: --sdk-url, or a `claude rc` parent
|
|
117
|
+
# :headless `claude -p "..."` or an SDK stream run. Its parent is
|
|
118
|
+
# whatever shell spawned it, not the claude that asked for
|
|
119
|
+
# it, so only its own command line gives it away
|
|
120
|
+
# :subagent parented by another claude process
|
|
121
|
+
# :terminal everything else, which is a claude you are sitting in
|
|
122
|
+
def self.origins(rows, parent_cmd)
|
|
123
|
+
rows.to_h do |pid, ppid, cmd|
|
|
124
|
+
parent = parent_cmd[ppid].to_s
|
|
125
|
+
origin =
|
|
126
|
+
if cmd.include?("--sdk-url") || parent.match?(/\bclaude rc\b/) then :remote
|
|
127
|
+
elsif headless?(cmd) then :headless
|
|
128
|
+
elsif parent.match?(/(^|\/)claude\b/) then :subagent
|
|
129
|
+
else :terminal
|
|
130
|
+
end
|
|
131
|
+
[pid, origin]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# `ps` flattens quoting, so a prompt that mentions a flag reads the same
|
|
136
|
+
# as the flag itself. Nobody types `claude "what does -p do"` into a
|
|
137
|
+
# terminal often enough to matter; the false positive is accepted.
|
|
138
|
+
def self.headless?(cmd) = cmd.split.drop(1).any? { |arg| HEADLESS_FLAGS.include?(arg) }
|
|
139
|
+
|
|
140
|
+
# pid => bridge session id, for the workers a `claude remote-control`
|
|
141
|
+
# server spawned: each is started with `--session-id cse_…`, the same id
|
|
142
|
+
# a background session records as bridgeSessionId. Pure.
|
|
143
|
+
def self.bridge_ids(rows)
|
|
144
|
+
rows.filter_map { |pid, _, cmd| (m = cmd.match(/--session-id[= ](cse_\w+)/)) && [pid, m[1]] }.to_h
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
114
149
|
# A worker nobody has messaged yet has an empty file, and the daemon
|
|
115
150
|
# reports "source session not found" on resuming it.
|
|
116
151
|
def transcript_empty?(cwd, session_id)
|
|
@@ -133,10 +168,6 @@ module ClaudeInbox
|
|
|
133
168
|
JSON.parse(json).map { |h| Session.from_hash(h) }
|
|
134
169
|
end
|
|
135
170
|
|
|
136
|
-
# Flags that mark a claude process as one a program drives rather than one
|
|
137
|
-
# you type in: the headless print mode and the SDK's stream protocol.
|
|
138
|
-
HEADLESS_FLAGS = %w[-p --print --input-format --output-format].freeze
|
|
139
|
-
|
|
140
171
|
# The JSON reports Remote Control workers, local sub-agents and headless
|
|
141
172
|
# runs as `interactive`, same as a terminal you opened yourself, each named
|
|
142
173
|
# after its directory. The process tree tells them apart; see `origins`.
|
|
@@ -160,39 +191,6 @@ module ClaudeInbox
|
|
|
160
191
|
.reject(&:unattended?)
|
|
161
192
|
end
|
|
162
193
|
|
|
163
|
-
# pid => origin, given `ps` for the sessions and for their parents. Pure.
|
|
164
|
-
#
|
|
165
|
-
# :remote a Remote Control worker: --sdk-url, or a `claude rc` parent
|
|
166
|
-
# :headless `claude -p "..."` or an SDK stream run. Its parent is
|
|
167
|
-
# whatever shell spawned it, not the claude that asked for
|
|
168
|
-
# it, so only its own command line gives it away
|
|
169
|
-
# :subagent parented by another claude process
|
|
170
|
-
# :terminal everything else, which is a claude you are sitting in
|
|
171
|
-
def self.origins(rows, parent_cmd)
|
|
172
|
-
rows.to_h do |pid, ppid, cmd|
|
|
173
|
-
parent = parent_cmd[ppid].to_s
|
|
174
|
-
origin =
|
|
175
|
-
if cmd.include?("--sdk-url") || parent.match?(/\bclaude rc\b/) then :remote
|
|
176
|
-
elsif headless?(cmd) then :headless
|
|
177
|
-
elsif parent.match?(/(^|\/)claude\b/) then :subagent
|
|
178
|
-
else :terminal
|
|
179
|
-
end
|
|
180
|
-
[pid, origin]
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# `ps` flattens quoting, so a prompt that mentions a flag reads the same
|
|
185
|
-
# as the flag itself. Nobody types `claude "what does -p do"` into a
|
|
186
|
-
# terminal often enough to matter; the false positive is accepted.
|
|
187
|
-
def self.headless?(cmd) = cmd.split.drop(1).any? { |arg| HEADLESS_FLAGS.include?(arg) }
|
|
188
|
-
|
|
189
|
-
# pid => bridge session id, for the workers a `claude remote-control`
|
|
190
|
-
# server spawned: each is started with `--session-id cse_…`, the same id
|
|
191
|
-
# a background session records as bridgeSessionId. Pure.
|
|
192
|
-
def self.bridge_ids(rows)
|
|
193
|
-
rows.filter_map { |pid, _, cmd| (m = cmd.match(/--session-id[= ](cse_\w+)/)) && [pid, m[1]] }.to_h
|
|
194
|
-
end
|
|
195
|
-
|
|
196
194
|
def origins_from(rows)
|
|
197
195
|
return {} if rows.empty?
|
|
198
196
|
self.class.origins(rows, ps_commands(rows.map { |_, ppid, _| ppid }.uniq))
|
|
@@ -220,8 +218,6 @@ module ClaudeInbox
|
|
|
220
218
|
}
|
|
221
219
|
end
|
|
222
220
|
|
|
223
|
-
private
|
|
224
|
-
|
|
225
221
|
def kill_when_agents_view(pid)
|
|
226
222
|
loop do
|
|
227
223
|
sleep WATCH_INTERVAL
|
|
@@ -243,6 +239,12 @@ module ClaudeInbox
|
|
|
243
239
|
raise Error, "#{argv[1]} failed: #{r.err.strip}" unless r.success?
|
|
244
240
|
true
|
|
245
241
|
end
|
|
242
|
+
|
|
243
|
+
def start_bg(argv, cwd, what)
|
|
244
|
+
r = Subprocess.capture(*argv, chdir: cwd)
|
|
245
|
+
raise Error, "#{what} failed: #{(r.err + r.out).strip}" unless r.success?
|
|
246
|
+
r.out[/\b[0-9a-f]{8}\b/] || r.out.strip
|
|
247
|
+
end
|
|
246
248
|
end
|
|
247
249
|
|
|
248
250
|
# Reads a committed JSON fixture instead of the daemon.
|