@zeph-to/hook-sdk 1.8.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 +283 -14
- package/dist/cli.js +33 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/installer.d.ts +7 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +84 -27
- 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 +8 -3
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,16 +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`)
|
|
205
|
-
-
|
|
470
|
+
- **Node.js >= 18** (uses native `fetch`).
|
|
471
|
+
- **tmux** — required for `zeph cc` / `codex` / `gemini` and `zeph listener`.
|
|
472
|
+
- The `ZephHook` SDK has no runtime dependencies. The CLI depends on
|
|
473
|
+
`@inquirer/prompts` for the interactive `zeph install` picker and on
|
|
474
|
+
`ws` for the listener's WebSocket subscription.
|
|
206
475
|
|
|
207
476
|
## License
|
|
208
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
|
|
@@ -97,6 +109,9 @@ Install options:
|
|
|
97
109
|
--key <api-key> API key (non-interactive)
|
|
98
110
|
--hook <hook-id> Hook ID (non-interactive)
|
|
99
111
|
--base-url <url> Base URL (non-interactive)
|
|
112
|
+
--only <agents> Comma-separated agent ids to install for
|
|
113
|
+
(claude,cursor,windsurf,gemini,codex,copilot,cline,aider).
|
|
114
|
+
Skips the interactive picker.
|
|
100
115
|
|
|
101
116
|
Uninstall options:
|
|
102
117
|
--dry-run Preview what would be removed, change nothing
|
|
@@ -291,6 +306,16 @@ const handleError = (err, isJson) => {
|
|
|
291
306
|
printError(err instanceof Error ? err.message : 'Unknown error', isJson);
|
|
292
307
|
return 1;
|
|
293
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
|
+
};
|
|
294
319
|
// ── Main ────────────────────────────────────────────────────────
|
|
295
320
|
const main = async () => {
|
|
296
321
|
const args = parseArgs(process.argv);
|
|
@@ -321,6 +346,14 @@ const main = async () => {
|
|
|
321
346
|
return handleDismiss(args);
|
|
322
347
|
case 'test':
|
|
323
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);
|
|
324
357
|
default:
|
|
325
358
|
printError(`Unknown command: ${command}`, args.json === true);
|
|
326
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"}
|
package/dist/installer.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import type { Agent } from './agents.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve agents from a non-interactive `--only cursor,gemini` flag.
|
|
4
|
+
* Matches on agent id; unknown ids are silently dropped. Exported for
|
|
5
|
+
* unit testing.
|
|
6
|
+
*/
|
|
7
|
+
export declare const filterAgentsByIds: (detected: Agent[], only: string) => Agent[];
|
|
1
8
|
export declare const handleInstall: (args: Record<string, string | boolean>) => Promise<number>;
|
|
2
9
|
//# sourceMappingURL=installer.d.ts.map
|
package/dist/installer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAwSzC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AAqBF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA4H1F,CAAC"}
|
package/dist/installer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleInstall = void 0;
|
|
3
|
+
exports.handleInstall = exports.filterAgentsByIds = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const os_1 = require("os");
|
|
@@ -251,6 +251,52 @@ const AGENT_INSTALLERS = {
|
|
|
251
251
|
cline: installCline,
|
|
252
252
|
aider: installAider,
|
|
253
253
|
};
|
|
254
|
+
// One-line summary of what each agent's installer does — shown in the
|
|
255
|
+
// interactive plan before anything is written.
|
|
256
|
+
const AGENT_PLAN_LABELS = {
|
|
257
|
+
claude: 'Claude Code — install plugin',
|
|
258
|
+
cursor: 'Cursor — MCP + hooks + rules',
|
|
259
|
+
windsurf: 'Windsurf — MCP + hooks + rules',
|
|
260
|
+
gemini: 'Gemini CLI — MCP + hooks + rules',
|
|
261
|
+
codex: 'Codex CLI — hooks + rules',
|
|
262
|
+
copilot: 'Copilot CLI — hooks + rules',
|
|
263
|
+
cline: 'Cline — rules',
|
|
264
|
+
aider: 'Aider — conventions',
|
|
265
|
+
};
|
|
266
|
+
// ── Agent selection ──────────────────────────────────────────────
|
|
267
|
+
/**
|
|
268
|
+
* Interactive agent picker — an @inquirer/prompts checkbox (arrow keys
|
|
269
|
+
* to move, space to toggle, enter to confirm). Every agent starts
|
|
270
|
+
* checked, so a bare Enter installs for all. Returns the chosen Agent[].
|
|
271
|
+
*
|
|
272
|
+
* Dynamic import keeps @inquirer/prompts (ESM) loadable from this
|
|
273
|
+
* CommonJS build, and means the dependency is only touched on the
|
|
274
|
+
* interactive path — `notify` / `list` / scripted `install --only`
|
|
275
|
+
* never load it.
|
|
276
|
+
*/
|
|
277
|
+
const pickAgentsInteractive = async (detected) => {
|
|
278
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
279
|
+
const picked = await checkbox({
|
|
280
|
+
message: 'Install Zeph for which agents? (space to toggle, enter to confirm)',
|
|
281
|
+
choices: detected.map((agent) => ({
|
|
282
|
+
name: AGENT_PLAN_LABELS[agent.id] ?? agent.name,
|
|
283
|
+
value: agent.id,
|
|
284
|
+
checked: true,
|
|
285
|
+
})),
|
|
286
|
+
loop: false,
|
|
287
|
+
});
|
|
288
|
+
return detected.filter((a) => picked.includes(a.id));
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Resolve agents from a non-interactive `--only cursor,gemini` flag.
|
|
292
|
+
* Matches on agent id; unknown ids are silently dropped. Exported for
|
|
293
|
+
* unit testing.
|
|
294
|
+
*/
|
|
295
|
+
const filterAgentsByIds = (detected, only) => {
|
|
296
|
+
const ids = new Set(only.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
|
|
297
|
+
return detected.filter((a) => ids.has(a.id));
|
|
298
|
+
};
|
|
299
|
+
exports.filterAgentsByIds = filterAgentsByIds;
|
|
254
300
|
// ── Test Connection ──────────────────────────────────────────────
|
|
255
301
|
const testConnection = async (apiKey, baseUrl) => {
|
|
256
302
|
try {
|
|
@@ -291,7 +337,32 @@ const handleInstall = async (args) => {
|
|
|
291
337
|
if (detected.length === 0) {
|
|
292
338
|
console.log('\n No supported agents found. Config will still be saved.\n');
|
|
293
339
|
}
|
|
294
|
-
// 2.
|
|
340
|
+
// 2. Choose which agents to install for — asked up front so the user
|
|
341
|
+
// sees the choice before being walked through credential prompts.
|
|
342
|
+
let selected = detected;
|
|
343
|
+
const onlyArg = args.only?.trim();
|
|
344
|
+
if (detected.length > 0) {
|
|
345
|
+
if (onlyArg) {
|
|
346
|
+
// Non-interactive or scripted: --only cursor,gemini
|
|
347
|
+
selected = (0, exports.filterAgentsByIds)(detected, onlyArg);
|
|
348
|
+
console.log(`\n --only ${onlyArg} → ${selected.map((a) => a.name).join(', ') || '(no match)'}`);
|
|
349
|
+
}
|
|
350
|
+
else if (nonInteractive) {
|
|
351
|
+
// Scripted run with no --only: keep the all-detected default
|
|
352
|
+
selected = detected;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
try {
|
|
356
|
+
selected = await pickAgentsInteractive(detected);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Ctrl-C in the picker (or no TTY) — treat as a clean cancel.
|
|
360
|
+
console.log('\n Cancelled.\n');
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// 3. Collect credentials
|
|
295
366
|
const existing = (0, config_js_1.loadConfig)();
|
|
296
367
|
let apiKey;
|
|
297
368
|
let hookId;
|
|
@@ -321,33 +392,19 @@ const handleInstall = async (args) => {
|
|
|
321
392
|
console.error('\n Error: API key is required.\n');
|
|
322
393
|
return 1;
|
|
323
394
|
}
|
|
324
|
-
//
|
|
395
|
+
// 4. Show the resolved plan before touching anything (interactive only).
|
|
325
396
|
if (!nonInteractive) {
|
|
326
397
|
console.log('\n Will do:');
|
|
327
|
-
console.log(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const labels = {
|
|
331
|
-
claude: 'Install Claude Code plugin',
|
|
332
|
-
cursor: 'Setup Cursor (MCP + hooks + rules)',
|
|
333
|
-
windsurf: 'Setup Windsurf (MCP + hooks + rules)',
|
|
334
|
-
gemini: 'Setup Gemini CLI (MCP + hooks + rules)',
|
|
335
|
-
codex: 'Setup Codex CLI (hooks + rules)',
|
|
336
|
-
copilot: 'Setup Copilot CLI (hooks + rules)',
|
|
337
|
-
cline: 'Setup Cline (rules)',
|
|
338
|
-
aider: 'Setup Aider (conventions)',
|
|
339
|
-
};
|
|
340
|
-
console.log(` ${step}. ${labels[agent.id] ?? `Install for ${agent.name}`}`);
|
|
341
|
-
step++;
|
|
398
|
+
console.log(` - Save config to ${config_js_1.CONFIG_FILE}`);
|
|
399
|
+
for (const agent of selected) {
|
|
400
|
+
console.log(` - ${AGENT_PLAN_LABELS[agent.id] ?? `Install for ${agent.name}`}`);
|
|
342
401
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (confirm.toLowerCase() === 'n') {
|
|
346
|
-
console.log('\n Cancelled.\n');
|
|
347
|
-
return 0;
|
|
402
|
+
if (selected.length === 0) {
|
|
403
|
+
console.log(' (no agents selected — only the config file will be saved)');
|
|
348
404
|
}
|
|
405
|
+
console.log(' - Test connection');
|
|
349
406
|
}
|
|
350
|
-
//
|
|
407
|
+
// 5. Save config
|
|
351
408
|
console.log('');
|
|
352
409
|
const config = {
|
|
353
410
|
apiKey,
|
|
@@ -356,14 +413,14 @@ const handleInstall = async (args) => {
|
|
|
356
413
|
};
|
|
357
414
|
(0, config_js_1.saveConfig)(config);
|
|
358
415
|
ok(`Config saved to ${config_js_1.CONFIG_FILE}`);
|
|
359
|
-
//
|
|
360
|
-
for (const agent of
|
|
416
|
+
// 6. Install for the selected agents only
|
|
417
|
+
for (const agent of selected) {
|
|
361
418
|
console.log(`\n Installing for ${agent.name}...`);
|
|
362
419
|
const installer = AGENT_INSTALLERS[agent.id];
|
|
363
420
|
if (installer)
|
|
364
421
|
installer();
|
|
365
422
|
}
|
|
366
|
-
//
|
|
423
|
+
// 7. Test connection
|
|
367
424
|
console.log('\n Testing connection...');
|
|
368
425
|
await testConnection(apiKey, baseUrl);
|
|
369
426
|
console.log('\n Done! Restart your agents.\n');
|