@zeph-to/hook-sdk 1.9.0 → 1.10.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.
- package/README.md +282 -14
- package/dist/cli.js +30 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/listener.d.ts +116 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +878 -0
- package/dist/wrapper.d.ts +26 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +210 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# @zeph-to/hook-sdk
|
|
2
2
|
|
|
3
|
-
Push notification SDK + CLI for [Zeph](https://zeph.to)
|
|
3
|
+
Push notification SDK + CLI for [Zeph](https://zeph.to), with an optional
|
|
4
|
+
resident listener that **drives Claude Code / Codex / Gemini sessions
|
|
5
|
+
from your phone** by injecting messages into named tmux sessions.
|
|
6
|
+
|
|
7
|
+
- `ZephHook` SDK — native `fetch`, no runtime deps. Send/list/dismiss pushes.
|
|
8
|
+
- `zeph` CLI — install Zeph plugins, send pushes, run agents under tmux,
|
|
9
|
+
and listen for inbound messages from your phone.
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
8
|
-
npm install @zeph-to/hook-sdk
|
|
9
|
-
# or
|
|
14
|
+
npm install -g @zeph-to/hook-sdk
|
|
15
|
+
# or for one-off use
|
|
10
16
|
npx @zeph-to/hook-sdk notify --title "Hello"
|
|
11
17
|
```
|
|
12
18
|
|
|
@@ -20,7 +26,214 @@ npx @zeph-to/hook-sdk install
|
|
|
20
26
|
npx @zeph-to/hook-sdk install --key ak_... --hook hook_...
|
|
21
27
|
```
|
|
22
28
|
|
|
23
|
-
Saves to `~/.zeph/config.json`. All Zeph tools (CLI, MCP server, plugin
|
|
29
|
+
Saves to `~/.zeph/config.json`. All Zeph tools (CLI, MCP server, plugin
|
|
30
|
+
hooks, listener) read this file.
|
|
31
|
+
|
|
32
|
+
To **send** notifications:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
zeph notify --title "Deploy done" --body "v2.1.0 shipped"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To **drive a Claude Code / Codex / Gemini session from your phone**, see
|
|
39
|
+
[Remote Control](#remote-control) below.
|
|
40
|
+
|
|
41
|
+
## Remote Control
|
|
42
|
+
|
|
43
|
+
> Send messages from your phone *into* a live Claude Code / Codex /
|
|
44
|
+
> Gemini session — even after a `zeph_ask` polling window has expired.
|
|
45
|
+
|
|
46
|
+
The MCP tools `zeph_ask` / `zeph_prompt` / `zeph_input` open a polling
|
|
47
|
+
loop on a fixed timeout (120–600 s). Once that window closes the
|
|
48
|
+
session becomes unaddressable from the phone, even though it's still
|
|
49
|
+
running. The `zeph listener` daemon fixes this by keeping a persistent
|
|
50
|
+
WebSocket open to Zeph and injecting matching messages into a *named*
|
|
51
|
+
tmux session via `tmux send-keys`.
|
|
52
|
+
|
|
53
|
+
### Architecture
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
[phone — "Active Agents" picker on Zeph app]
|
|
57
|
+
│ selects session, types message
|
|
58
|
+
▼ POST /pushes/send { type: 'agent.command',
|
|
59
|
+
│ agentSessionName: 'zeph-myapp',
|
|
60
|
+
│ body: '리팩토링 마무리해줘' }
|
|
61
|
+
[Zeph backend]
|
|
62
|
+
│ WebSocket fan-out (push.new)
|
|
63
|
+
▼
|
|
64
|
+
[zeph listener — resident daemon, started by `zeph cc` automatically]
|
|
65
|
+
│ tmux send-keys -l -t zeph-myapp "리팩토링 마무리해줘" + Enter
|
|
66
|
+
▼
|
|
67
|
+
[tmux session "zeph-myapp" running claude / codex / gemini]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The listener also reports its tmux session inventory back to the server
|
|
71
|
+
every 5 seconds, so the phone picker stays in sync — no manual
|
|
72
|
+
configuration needed once a session is running.
|
|
73
|
+
|
|
74
|
+
### Setup
|
|
75
|
+
|
|
76
|
+
1. **Install tmux.** The listener uses `send-keys`; the wrapper spawns
|
|
77
|
+
named sessions. `brew install tmux` on macOS, `apt install tmux` on
|
|
78
|
+
Debian/Ubuntu.
|
|
79
|
+
|
|
80
|
+
2. **Add `wsUrl` to `~/.zeph/config.json`** (the WebSocket endpoint of
|
|
81
|
+
your Zeph backend — CDK output `WsApiUrl`):
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"apiKey": "ak_...",
|
|
86
|
+
"hookId": "hook_...",
|
|
87
|
+
"wsUrl": "wss://<api-id>.execute-api.<region>.amazonaws.com/<stage>"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Alternatively set `ZEPH_WS_URL` in your shell env.
|
|
92
|
+
|
|
93
|
+
3. **Run agents through the wrapper.** That's it.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
zeph cc # claude → tmux session "zeph-<project>"
|
|
97
|
+
zeph codex # codex → tmux session "zeph-<project>"
|
|
98
|
+
zeph gemini # gemini → tmux session "zeph-<project>"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The first `zeph cc` on a machine **auto-spawns a background
|
|
102
|
+
listener** (singleton, PID file at `~/.zeph/listener.pid`,
|
|
103
|
+
stdout/stderr at `~/.zeph/listener.log`). You never run
|
|
104
|
+
`zeph listener` by hand — every `zeph cc` checks the PID file and
|
|
105
|
+
skips the spawn when one is already alive, so opening a dozen
|
|
106
|
+
terminals doesn't create a dozen daemons. The daemon survives
|
|
107
|
+
between `zeph cc` invocations.
|
|
108
|
+
|
|
109
|
+
Project name resolves from `CLAUDE_PROJECT_DIR` /
|
|
110
|
+
`CURSOR_PROJECT_DIR` / `WINDSURF_PROJECT_DIR` if set, else the git
|
|
111
|
+
repo root, else the cwd basename. Any extra args after the command
|
|
112
|
+
pass through to the agent verbatim:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
zeph cc --resume "abc123"
|
|
116
|
+
zeph cc --dangerously-skip-permissions
|
|
117
|
+
zeph codex --model gpt-5-high "fix the failing test"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Multiple sessions in one project.** Open another terminal in the
|
|
121
|
+
same folder, run `zeph cc` again, and the wrapper auto-suffixes:
|
|
122
|
+
first session is `zeph-encl`, the next attached one becomes
|
|
123
|
+
`zeph-encl-2`, then `zeph-encl-3`, etc. The phone picker shows them
|
|
124
|
+
as `encl · Claude`, `encl · Claude #2`, `encl · Claude #3`. If
|
|
125
|
+
`zeph-encl` already exists but is **detached** (no one attached),
|
|
126
|
+
the wrapper reattaches to it instead of spawning a new one — close
|
|
127
|
+
the terminal, come back later, pick up where you left off.
|
|
128
|
+
|
|
129
|
+
If you're already inside a tmux session (`$TMUX` set) the wrapper
|
|
130
|
+
skips the outer tmux and runs the agent in the current pane — the
|
|
131
|
+
listener can't target an unnamed session that way, but you keep your
|
|
132
|
+
existing multiplexer setup.
|
|
133
|
+
|
|
134
|
+
### Diagnostics
|
|
135
|
+
|
|
136
|
+
The auto-spawned listener writes to two files under `~/.zeph/`:
|
|
137
|
+
|
|
138
|
+
- `listener.pid` — the running daemon's PID. `cat ~/.zeph/listener.pid`
|
|
139
|
+
+ `ps -p <pid>` to confirm it's alive.
|
|
140
|
+
- `listener.log` — stdout + stderr from the daemon. `tail -f` to watch.
|
|
141
|
+
|
|
142
|
+
A healthy listener log shows one line per cycle:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
[xx:xx:xx] reported 2 session(s): zeph-myapp, zeph-otherapp
|
|
146
|
+
[xx:xx:xx] ✓ server persisted 2 session(s)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If you see `! server rejected listener.sessions: ...` instead, the
|
|
150
|
+
message points at the failure (auth, missing device record, etc.) so
|
|
151
|
+
you can fix the actual problem instead of guessing.
|
|
152
|
+
|
|
153
|
+
To force a restart — e.g. after upgrading `@zeph-to/hook-sdk`:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
kill $(cat ~/.zeph/listener.pid)
|
|
157
|
+
rm ~/.zeph/listener.pid
|
|
158
|
+
zeph cc # autospawns the new build
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
To run it in the foreground (for development of the SDK itself):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
zeph listener
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
You'll get the same logs you'd otherwise tail from `listener.log`.
|
|
168
|
+
|
|
169
|
+
### Custom tmux sockets
|
|
170
|
+
|
|
171
|
+
The listener auto-discovers the tmux socket — it probes the default
|
|
172
|
+
location, walks per-user `$TMPDIR` paths (macOS `/var/folders/.../T/`),
|
|
173
|
+
falls back to `/tmp/tmux-<uid>/`, and finally finds running tmux servers
|
|
174
|
+
via `lsof` so stale socket files don't trip discovery. If your tmux
|
|
175
|
+
uses `tmux -L <name>` or a non-standard `-S <path>`, set the override
|
|
176
|
+
explicitly:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
export ZEPH_TMUX_SOCKET=/path/to/socket
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
(The wrapper passes the env to the auto-spawned listener, so setting
|
|
183
|
+
it in your shell rc is enough.)
|
|
184
|
+
|
|
185
|
+
### Wire format
|
|
186
|
+
|
|
187
|
+
The listener only acts on pushes with `type='agent.command'` carrying
|
|
188
|
+
the tmux session name in `agentSessionName` and the message in `body`.
|
|
189
|
+
Other pushes (Stop-hook auto-pushes, `zeph_ask` responses, channel
|
|
190
|
+
broadcasts, plain notes) are ignored. End-to-end:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
tmux send-keys -l -t <agentSessionName> "<body>"
|
|
194
|
+
tmux send-keys -t <agentSessionName> Enter
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
If you need to send one from the command line (debugging, scripting),
|
|
198
|
+
build the structured push directly:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
curl -X POST "$ZEPH_BASE_URL/pushes/send" \
|
|
202
|
+
-H "X-API-Key: $ZEPH_API_KEY" \
|
|
203
|
+
-H 'Content-Type: application/json' \
|
|
204
|
+
-d '{
|
|
205
|
+
"type": "agent.command",
|
|
206
|
+
"targetDeviceId": "dev_listener_<sha8(hostname)>",
|
|
207
|
+
"agentSessionName": "zeph-myapp",
|
|
208
|
+
"body": "테스트 통과시키고 PR 올려줘"
|
|
209
|
+
}'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Defense
|
|
213
|
+
|
|
214
|
+
The listener is a remote-code-execution surface by design (it types
|
|
215
|
+
into a shell-adjacent pane). The defense is layered:
|
|
216
|
+
|
|
217
|
+
1. **Pane guard** — before injecting, the listener checks
|
|
218
|
+
`tmux display-message -p '#{pane_current_command}'`. If the pane is
|
|
219
|
+
at an interactive shell (`bash`/`zsh`/`fish`/`sh`/`dash`/`ksh`/
|
|
220
|
+
`tcsh`/`csh`/`pwsh`), the inject is refused. CC/Codex/Gemini exited
|
|
221
|
+
≠ phone gets free shell access.
|
|
222
|
+
2. **Literal injection** — `tmux send-keys -l` takes the payload as
|
|
223
|
+
data; tmux escape sequences inside a message can't drive other tmux
|
|
224
|
+
commands.
|
|
225
|
+
3. **Session-name allowlist** — only `[A-Za-z0-9._-]+` is accepted as
|
|
226
|
+
a session target, so shell metacharacters never reach the tmux argv.
|
|
227
|
+
4. **Per-session rate limit** — 30 injections/minute/session token
|
|
228
|
+
bucket caps a runaway/compromised sender.
|
|
229
|
+
5. **Agent permission gate stays on** — your CC/Codex/Gemini permission
|
|
230
|
+
prompt is still in front of every destructive tool call. The phone
|
|
231
|
+
can *talk* but can't approve `rm -rf` for you.
|
|
232
|
+
|
|
233
|
+
The transport (WS) is currently authenticated by API key + `push:read`
|
|
234
|
+
scope and is **not** end-to-end encrypted in v1 — your Zeph backend
|
|
235
|
+
sees the message plaintext. If you self-host or trust your backend,
|
|
236
|
+
that's fine. If you don't, hold off until per-device E2E ships.
|
|
24
237
|
|
|
25
238
|
## CLI Usage
|
|
26
239
|
|
|
@@ -42,6 +255,15 @@ zeph dismiss --all
|
|
|
42
255
|
# Test connection
|
|
43
256
|
zeph test
|
|
44
257
|
|
|
258
|
+
# Run an agent in a named tmux session (so the listener can reach it)
|
|
259
|
+
zeph cc # claude
|
|
260
|
+
zeph codex # codex
|
|
261
|
+
zeph gemini # gemini
|
|
262
|
+
|
|
263
|
+
# Run the resident listener (foreground; background it as you like)
|
|
264
|
+
zeph listener
|
|
265
|
+
zeph listener --ws-url wss://... # override config
|
|
266
|
+
|
|
45
267
|
# JSON output
|
|
46
268
|
zeph notify --title "Hello" --json
|
|
47
269
|
```
|
|
@@ -58,6 +280,8 @@ zeph notify --title "Hello" --json
|
|
|
58
280
|
| `list` | List recent push notifications |
|
|
59
281
|
| `dismiss <id>` | Dismiss a push (or `--all`) |
|
|
60
282
|
| `test` | Verify connection and API key |
|
|
283
|
+
| `cc` · `codex` · `gemini` | Run the agent in a `zeph-<project>` tmux session (auto-suffixed `-2`, `-3`, … on attached collisions). Auto-spawns the background listener on first invocation so the phone picker just works. Trailing args pass through to the agent (`zeph cc --resume "..."`) |
|
|
284
|
+
| `listener` | (Usually unnecessary — `zeph cc` autospawns it.) Resident daemon: subscribes via WebSocket, reports tmux session inventory every 5 s, injects `agent.command` pushes into the matching session. Run in the foreground for SDK development; otherwise let `zeph cc` manage it |
|
|
61
285
|
|
|
62
286
|
### Notify Options
|
|
63
287
|
|
|
@@ -70,7 +294,22 @@ zeph notify --title "Hello" --json
|
|
|
70
294
|
| `--priority <p>` | Priority: `low`, `normal`, `high`, `urgent` |
|
|
71
295
|
| `--device <id>` | Target device ID |
|
|
72
296
|
|
|
73
|
-
The defaults are tuned for hook-driven invocations (e.g. Stop hooks
|
|
297
|
+
The defaults are tuned for hook-driven invocations (e.g. Stop hooks
|
|
298
|
+
calling `zeph notify --title "Task done"` without a body) — you'll see
|
|
299
|
+
which project + branch finished without writing per-IDE wrappers. Pass
|
|
300
|
+
`--body ""` explicitly to suppress.
|
|
301
|
+
|
|
302
|
+
### Listener Options
|
|
303
|
+
|
|
304
|
+
| Flag | Description |
|
|
305
|
+
|------|-------------|
|
|
306
|
+
| `--ws-url <url>` | WebSocket endpoint (or set `ZEPH_WS_URL` env, or `wsUrl` in `~/.zeph/config.json`) |
|
|
307
|
+
| `--key <api-key>` | API key (or set `ZEPH_API_KEY` env) |
|
|
308
|
+
|
|
309
|
+
The listener reconnects with exponential backoff + jitter (1 s → 30 s
|
|
310
|
+
cap). Heartbeat is ping every 25 s with a 10 s pong timeout. On an
|
|
311
|
+
authentication failure close (4001/4002/4003) the listener exits with
|
|
312
|
+
code 3 instead of looping forever — fix the key and restart.
|
|
74
313
|
|
|
75
314
|
### List Options
|
|
76
315
|
|
|
@@ -90,9 +329,11 @@ The defaults are tuned for hook-driven invocations (e.g. Stop hooks calling `zep
|
|
|
90
329
|
|
|
91
330
|
### Mute
|
|
92
331
|
|
|
93
|
-
Mute is project-scoped (uses project directory hash). Created by Claude
|
|
332
|
+
Mute is project-scoped (uses project directory hash). Created by Claude
|
|
333
|
+
Code `/zeph-mute` command.
|
|
94
334
|
|
|
95
|
-
Notifications are silently skipped when a mute file exists for the
|
|
335
|
+
Notifications are silently skipped when a mute file exists for the
|
|
336
|
+
current project:
|
|
96
337
|
|
|
97
338
|
```bash
|
|
98
339
|
# Mute (created by /zeph-mute in Claude Code plugin)
|
|
@@ -103,7 +344,8 @@ touch /tmp/zeph-muted-$HASH
|
|
|
103
344
|
rm /tmp/zeph-muted-$HASH
|
|
104
345
|
```
|
|
105
346
|
|
|
106
|
-
The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`,
|
|
347
|
+
The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`,
|
|
348
|
+
`WINDSURF_PROJECT_DIR`, and falls back to `cwd`.
|
|
107
349
|
|
|
108
350
|
### Exit Codes
|
|
109
351
|
|
|
@@ -112,7 +354,8 @@ The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`, `WINDSURF_PROJECT_DIR
|
|
|
112
354
|
| 0 | Success |
|
|
113
355
|
| 1 | General error |
|
|
114
356
|
| 2 | Quota exceeded |
|
|
115
|
-
| 3 | Authentication failed |
|
|
357
|
+
| 3 | Authentication failed (also: listener auth close 4001/4002/4003) |
|
|
358
|
+
| 127 | A required external binary (e.g. `tmux`, `claude`) was not found on PATH |
|
|
116
359
|
|
|
117
360
|
### Environment Variables
|
|
118
361
|
|
|
@@ -120,6 +363,9 @@ The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`, `WINDSURF_PROJECT_DIR
|
|
|
120
363
|
|----------|-------------|
|
|
121
364
|
| `ZEPH_API_KEY` | API key (fallback when `--key` not provided) |
|
|
122
365
|
| `ZEPH_BASE_URL` | API base URL (default: `https://api.zeph.to/v1`) |
|
|
366
|
+
| `ZEPH_WS_URL` | WebSocket endpoint for `zeph listener` (no default — required) |
|
|
367
|
+
| `ZEPH_TMUX_SOCKET` | Explicit tmux socket path for the listener (skips auto-discovery — use when your tmux runs with `-L <name>` or a custom `-S <path>`) |
|
|
368
|
+
| `ZEPH_SESSION_ID` | AI session ID (fallback when `--session` not provided) |
|
|
123
369
|
|
|
124
370
|
## SDK Usage
|
|
125
371
|
|
|
@@ -193,17 +439,39 @@ try {
|
|
|
193
439
|
| Copilot CLI | Session end hook |
|
|
194
440
|
| Cline | Rules file |
|
|
195
441
|
|
|
196
|
-
|
|
442
|
+
For remote-control via `zeph listener` the per-agent setup is the same
|
|
443
|
+
across CC/Codex/Gemini — the wrapper just spawns them in a named tmux
|
|
444
|
+
session.
|
|
197
445
|
|
|
198
|
-
|
|
446
|
+
## Encryption
|
|
199
447
|
|
|
200
|
-
|
|
448
|
+
Push bodies are encrypted with AES-256-GCM. The wrapping key is derived
|
|
449
|
+
via ECDH P-256 and synced across your own devices on first run so every
|
|
450
|
+
device can read the same push. Toggle encryption in the Zeph app
|
|
451
|
+
(Settings → Encryption); when disabled, the CLI sends plaintext. No
|
|
452
|
+
configuration needed.
|
|
453
|
+
|
|
454
|
+
**Threat model honesty:** keys are persisted on the Zeph backend to
|
|
455
|
+
enable cross-device sync, so this is *device-shared* encryption — not
|
|
456
|
+
true end-to-end. It protects push contents from passive network
|
|
457
|
+
observers and from a leaked database snapshot taken without the key
|
|
458
|
+
store, but it does **not** protect against the Zeph backend itself (it
|
|
459
|
+
has the keys it serves to your devices). A true E2E mode (per-device
|
|
460
|
+
keypairs, server stores only public keys, no key escrow) is on the
|
|
461
|
+
roadmap.
|
|
462
|
+
|
|
463
|
+
The `zeph listener` ignores `isEncrypted` pushes for now — it has no
|
|
464
|
+
per-device key to decrypt them. Stop-hook auto-pushes and `zeph_ask`
|
|
465
|
+
responses are not part of the `@<session>` injection path, so this
|
|
466
|
+
doesn't affect normal use.
|
|
201
467
|
|
|
202
468
|
## Requirements
|
|
203
469
|
|
|
204
|
-
- Node.js >= 18 (uses native `fetch`)
|
|
470
|
+
- **Node.js >= 18** (uses native `fetch`).
|
|
471
|
+
- **tmux** — required for `zeph cc` / `codex` / `gemini` and `zeph listener`.
|
|
205
472
|
- The `ZephHook` SDK has no runtime dependencies. The CLI depends on
|
|
206
|
-
`@inquirer/prompts` for the interactive `zeph install` picker
|
|
473
|
+
`@inquirer/prompts` for the interactive `zeph install` picker and on
|
|
474
|
+
`ws` for the listener's WebSocket subscription.
|
|
207
475
|
|
|
208
476
|
## License
|
|
209
477
|
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ const installer_js_1 = require("./installer.js");
|
|
|
9
9
|
const uninstall_js_1 = require("./uninstall.js");
|
|
10
10
|
const verify_js_1 = require("./verify.js");
|
|
11
11
|
const check_update_js_1 = require("./check-update.js");
|
|
12
|
+
const wrapper_js_1 = require("./wrapper.js");
|
|
13
|
+
const listener_js_1 = require("./listener.js");
|
|
12
14
|
const config_js_1 = require("./config.js");
|
|
13
15
|
const PROJECT_DIR_VARS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
|
|
14
16
|
const detectProjectDir = () => PROJECT_DIR_VARS.reduce((found, key) => found || process.env[key], undefined) ?? process.cwd();
|
|
@@ -76,6 +78,16 @@ Commands:
|
|
|
76
78
|
list List recent push notifications
|
|
77
79
|
dismiss <id> Dismiss a push notification (or --all)
|
|
78
80
|
test Send a test notification to verify setup
|
|
81
|
+
cc [args…] Run 'claude' in a named tmux session ('zeph-<project>')
|
|
82
|
+
codex [args…] Run 'codex' in a named tmux session
|
|
83
|
+
gemini [args…] Run 'gemini' in a named tmux session
|
|
84
|
+
(auto-suffixed -2/-3/… when another zeph cc is already
|
|
85
|
+
attached to the default name; any args after the
|
|
86
|
+
subcommand are forwarded verbatim, e.g.
|
|
87
|
+
'zeph cc --resume')
|
|
88
|
+
listener Resident daemon — receives 'agent.command' pushes from
|
|
89
|
+
the phone picker and injects them into the matching
|
|
90
|
+
tmux session.
|
|
79
91
|
|
|
80
92
|
Notify options:
|
|
81
93
|
--title <text> Push title
|
|
@@ -294,6 +306,16 @@ const handleError = (err, isJson) => {
|
|
|
294
306
|
printError(err instanceof Error ? err.message : 'Unknown error', isJson);
|
|
295
307
|
return 1;
|
|
296
308
|
};
|
|
309
|
+
// ── Passthrough ─────────────────────────────────────────────────
|
|
310
|
+
/**
|
|
311
|
+
* Collect raw argv after the given subcommand token so flags like
|
|
312
|
+
* `--resume` reach the wrapped agent verbatim instead of being swallowed
|
|
313
|
+
* by `parseArgs`. Returns [] when the command isn't found.
|
|
314
|
+
*/
|
|
315
|
+
const collectPassthrough = (argv, cmd) => {
|
|
316
|
+
const idx = argv.indexOf(cmd, 2);
|
|
317
|
+
return idx >= 0 ? argv.slice(idx + 1) : [];
|
|
318
|
+
};
|
|
297
319
|
// ── Main ────────────────────────────────────────────────────────
|
|
298
320
|
const main = async () => {
|
|
299
321
|
const args = parseArgs(process.argv);
|
|
@@ -324,6 +346,14 @@ const main = async () => {
|
|
|
324
346
|
return handleDismiss(args);
|
|
325
347
|
case 'test':
|
|
326
348
|
return handleTest(args);
|
|
349
|
+
case 'cc':
|
|
350
|
+
return (0, wrapper_js_1.handleAgentSession)('claude', collectPassthrough(process.argv, 'cc'));
|
|
351
|
+
case 'codex':
|
|
352
|
+
return (0, wrapper_js_1.handleAgentSession)('codex', collectPassthrough(process.argv, 'codex'));
|
|
353
|
+
case 'gemini':
|
|
354
|
+
return (0, wrapper_js_1.handleAgentSession)('gemini', collectPassthrough(process.argv, 'gemini'));
|
|
355
|
+
case 'listener':
|
|
356
|
+
return (0, listener_js_1.handleListener)(args);
|
|
327
357
|
default:
|
|
328
358
|
printError(`Unknown command: ${command}`, args.json === true);
|
|
329
359
|
printUsage();
|
package/dist/config.d.ts
CHANGED
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,QAA2B,CAAC;AACnD,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAG/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,QAA2B,CAAC;AACnD,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAG/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zeph listener` — resident daemon that watches the user's Zeph feed
|
|
3
|
+
* over a persistent WebSocket and injects matching messages into a
|
|
4
|
+
* named tmux session via `tmux send-keys`.
|
|
5
|
+
*
|
|
6
|
+
* Solves the MCP polling-window problem: an `zeph_ask` polling cycle
|
|
7
|
+
* times out (120–600 s) and the CC/Codex session becomes unaddressable
|
|
8
|
+
* from the phone. The listener stays subscribed indefinitely and can
|
|
9
|
+
* deliver to any named tmux session at any time.
|
|
10
|
+
*
|
|
11
|
+
* Wire format: pushes with `type='agent.command'` carry the tmux
|
|
12
|
+
* session name in `agentSessionName` and the message in `body`. The
|
|
13
|
+
* "AI Agent에게 명령" sheet on the phone builds these structured
|
|
14
|
+
* pushes from the listener-reported session inventory. Other push
|
|
15
|
+
* types (Stop-hook auto-pushes, zeph_ask responses, channel
|
|
16
|
+
* broadcasts) are ignored.
|
|
17
|
+
*
|
|
18
|
+
* Transport: WebSocket against the Zeph $connect endpoint with
|
|
19
|
+
* `?apiKey=<key>`. The server fan-out pushes `{ type: 'push.new', data }`
|
|
20
|
+
* messages as new pushes are created. Reconnects with exponential
|
|
21
|
+
* backoff on transient failures; gives up on auth failures (4001/4002/4003).
|
|
22
|
+
*/
|
|
23
|
+
type AgentKind = 'claude' | 'codex' | 'gemini';
|
|
24
|
+
interface AgentSession {
|
|
25
|
+
name: string;
|
|
26
|
+
attached: boolean;
|
|
27
|
+
agentKind: AgentKind;
|
|
28
|
+
agentSessionId?: string | null;
|
|
29
|
+
project: string;
|
|
30
|
+
label?: string | null;
|
|
31
|
+
createdAt?: string;
|
|
32
|
+
lastActivityAt?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const checkRateLimit: (session: string, now?: number) => boolean;
|
|
35
|
+
/** Read the foreground command in the named tmux session's active pane. */
|
|
36
|
+
export declare const paneCurrentCommand: (session: string) => string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Parse a `zeph-*` tmux session name into `{project, label}`. For
|
|
39
|
+
* Phase 1 the wrapper only emits `zeph-<project>` (no labels), so the
|
|
40
|
+
* whole tail becomes the project. When labels land in Phase 2 the
|
|
41
|
+
* wrapper will sidecar `{project, label}` so the listener doesn't need
|
|
42
|
+
* to guess from a name that allows dashes in project names.
|
|
43
|
+
*/
|
|
44
|
+
export declare const parseSessionName: (name: string) => {
|
|
45
|
+
project: string;
|
|
46
|
+
label: string | null;
|
|
47
|
+
} | null;
|
|
48
|
+
/**
|
|
49
|
+
* Locate the most recent Claude Code session UUID for the working
|
|
50
|
+
* directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
|
|
51
|
+
* detectClaudeSessionId: CC writes per-session jsonl files at
|
|
52
|
+
* `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
|
|
53
|
+
* the cwd with `/` replaced by `-`.
|
|
54
|
+
*/
|
|
55
|
+
export declare const detectClaudeSessionId: (cwd: string) => string | null;
|
|
56
|
+
export interface CollectResult {
|
|
57
|
+
sessions: AgentSession[];
|
|
58
|
+
/** Diagnostic notes per rejected session — surfaced under `--verbose`. */
|
|
59
|
+
rejected: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Inventory pass that also records *why* each `zeph-*` session was
|
|
66
|
+
* skipped. The verbose log uses the rejection notes to explain empty
|
|
67
|
+
* pickers (most common cause: tmux pane lost its start_command after a
|
|
68
|
+
* re-attach, and the current command is `node` rather than `claude`).
|
|
69
|
+
*/
|
|
70
|
+
export declare const collectSessionsVerbose: () => CollectResult;
|
|
71
|
+
/**
|
|
72
|
+
* Snapshot the live `zeph-*` tmux sessions on this machine, enriched
|
|
73
|
+
* with the running agent kind, CC session UUID (claude only), project,
|
|
74
|
+
* and tmux activity timestamps. Returns [] when tmux is unreachable
|
|
75
|
+
* or no agent sessions exist. Sessions whose pane is at a shell or
|
|
76
|
+
* running something other than claude/codex/gemini are filtered out
|
|
77
|
+
* — the phone can't usefully address them.
|
|
78
|
+
*/
|
|
79
|
+
export declare const collectSessions: () => AgentSession[];
|
|
80
|
+
interface PushItem {
|
|
81
|
+
pushId: string;
|
|
82
|
+
type?: string;
|
|
83
|
+
body?: string;
|
|
84
|
+
title?: string;
|
|
85
|
+
createdAt?: string;
|
|
86
|
+
isEncrypted?: boolean;
|
|
87
|
+
/** Set when type='agent.command' — tmux session name to inject into. */
|
|
88
|
+
agentSessionName?: string;
|
|
89
|
+
}
|
|
90
|
+
interface HandlePushDeps {
|
|
91
|
+
paneCommand?: (session: string) => string | null;
|
|
92
|
+
inject?: (session: string, text: string) => boolean;
|
|
93
|
+
rateLimit?: (session: string) => boolean;
|
|
94
|
+
now?: () => number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Process one push. Returns true when an injection actually fired.
|
|
98
|
+
* Exported for unit testing with mocked deps.
|
|
99
|
+
*
|
|
100
|
+
* Only acts on `type='agent.command'` pushes carrying both an
|
|
101
|
+
* `agentSessionName` (tmux session to inject into) and a non-empty
|
|
102
|
+
* `body`. Everything else (Stop-hook auto-pushes, zeph_ask responses,
|
|
103
|
+
* encrypted pushes, normal text/link/file notifications) is ignored.
|
|
104
|
+
*/
|
|
105
|
+
export declare const handlePush: (push: PushItem, deps?: HandlePushDeps) => boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Stable per-host device id for the listener. We hash the OS hostname so
|
|
108
|
+
* the same machine reuses the same DeviceRecord across listener restarts
|
|
109
|
+
* (otherwise the phone's session inventory grows a new ghost device every
|
|
110
|
+
* time `zeph listener` rebinds). `dev_listener_<sha8(hostname)>` keeps it
|
|
111
|
+
* human-recognisable in dev logs without leaking the raw hostname.
|
|
112
|
+
*/
|
|
113
|
+
export declare const computeListenerDeviceId: (host?: string) => string;
|
|
114
|
+
export declare const handleListener: (args: Record<string, string | boolean>) => Promise<number>;
|
|
115
|
+
export {};
|
|
116
|
+
//# sourceMappingURL=listener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuBH,KAAK,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAG/C,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,MAAK,MAAmB,KAAG,OAgB1E,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAO7D,CAAC;AA6QF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAK3F,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAkB5D,CAAC;AAoEF,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAO,aA0DzC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,QAAO,YAAY,EAAuC,CAAC;AAIvF,UAAU,QAAQ;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAgCD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,QAAQ,EACd,OAAM,cAAmB,KAC1B,OAQF,CAAC;AA2BF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAAI,OAAM,MAAmB,KAAG,MAGnE,CAAC;AAyLF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA6E3F,CAAC"}
|