@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 CHANGED
@@ -1,12 +1,18 @@
1
1
  # @zeph-to/hook-sdk
2
2
 
3
- Push notification SDK + CLI for [Zeph](https://zeph.to). The `ZephHook` SDK uses native `fetch` with no runtime dependencies; the `zeph` CLI adds `@inquirer/prompts` for its interactive installer.
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 hooks) read this file.
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 calling `zeph notify --title "Task done"` without a body) — you'll see which project + branch finished without writing per-IDE wrappers. Pass `--body ""` explicitly to suppress.
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 Code `/zeph-mute` command.
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 current project:
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`, `WINDSURF_PROJECT_DIR`, and falls back to `cwd`.
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
- ## Encryption
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
- Push bodies are encrypted with AES-256-GCM. The wrapping key is derived via ECDH P-256 and synced across your own devices on first run so every device can read the same push. Toggle encryption in the Zeph app (Settings → Encryption); when disabled, the CLI sends plaintext. No configuration needed.
446
+ ## Encryption
199
447
 
200
- **Threat model honesty:** keys are persisted on the Zeph backend to enable cross-device sync, so this is *device-shared* encryption — not true end-to-end. It protects push contents from passive network observers and from a leaked database snapshot taken without the key store, but it does **not** protect against the Zeph backend itself (it has the keys it serves to your devices). A true E2E mode (per-device keypairs, server stores only public keys, no key escrow) is on the roadmap.
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
@@ -4,6 +4,7 @@ export interface ZephConfig {
4
4
  apiKey?: string;
5
5
  hookId?: string;
6
6
  baseUrl?: string;
7
+ wsUrl?: string;
7
8
  deviceId?: string;
8
9
  }
9
10
  export declare const resolvedEnv: (key: string) => string | undefined;
@@ -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"}