@zeph-to/hook-sdk 1.9.0 → 1.10.1

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,23 @@
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
+ [![npm](https://img.shields.io/npm/v/@zeph-to/hook-sdk.svg)](https://www.npmjs.com/package/@zeph-to/hook-sdk)
4
+ [![downloads](https://img.shields.io/npm/dm/@zeph-to/hook-sdk.svg)](https://www.npmjs.com/package/@zeph-to/hook-sdk)
5
+ [![node](https://img.shields.io/node/v/@zeph-to/hook-sdk.svg)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/@zeph-to/hook-sdk.svg)](./LICENSE)
7
+
8
+ Push notification SDK + CLI for [Zeph](https://zeph.to), with an optional
9
+ resident listener that **drives Claude Code / Codex / Gemini sessions
10
+ from your phone** by injecting messages into named tmux sessions.
11
+
12
+ - `ZephHook` SDK — native `fetch`, no runtime deps. Send/list/dismiss pushes.
13
+ - `zeph` CLI — install Zeph plugins, send pushes, run agents under tmux,
14
+ and listen for inbound messages from your phone.
4
15
 
5
16
  ## Installation
6
17
 
7
18
  ```bash
8
- npm install @zeph-to/hook-sdk
9
- # or
19
+ npm install -g @zeph-to/hook-sdk
20
+ # or for one-off use
10
21
  npx @zeph-to/hook-sdk notify --title "Hello"
11
22
  ```
12
23
 
@@ -20,7 +31,214 @@ npx @zeph-to/hook-sdk install
20
31
  npx @zeph-to/hook-sdk install --key ak_... --hook hook_...
21
32
  ```
22
33
 
23
- Saves to `~/.zeph/config.json`. All Zeph tools (CLI, MCP server, plugin hooks) read this file.
34
+ Saves to `~/.zeph/config.json`. All Zeph tools (CLI, MCP server, plugin
35
+ hooks, listener) read this file.
36
+
37
+ To **send** notifications:
38
+
39
+ ```bash
40
+ zeph notify --title "Deploy done" --body "v2.1.0 shipped"
41
+ ```
42
+
43
+ To **drive a Claude Code / Codex / Gemini session from your phone**, see
44
+ [Remote Control](#remote-control) below.
45
+
46
+ ## Remote Control
47
+
48
+ > Send messages from your phone *into* a live Claude Code / Codex /
49
+ > Gemini session — even after a `zeph_ask` polling window has expired.
50
+
51
+ The MCP tools `zeph_ask` / `zeph_prompt` / `zeph_input` open a polling
52
+ loop on a fixed timeout (120–600 s). Once that window closes the
53
+ session becomes unaddressable from the phone, even though it's still
54
+ running. The `zeph listener` daemon fixes this by keeping a persistent
55
+ WebSocket open to Zeph and injecting matching messages into a *named*
56
+ tmux session via `tmux send-keys`.
57
+
58
+ ### Architecture
59
+
60
+ ```
61
+ [phone — "Active Agents" picker on Zeph app]
62
+ │ selects session, types message
63
+ ▼ POST /pushes/send { type: 'agent.command',
64
+ │ agentSessionName: 'zeph-myapp',
65
+ │ body: '리팩토링 마무리해줘' }
66
+ [Zeph backend]
67
+ │ WebSocket fan-out (push.new)
68
+
69
+ [zeph listener — resident daemon, started by `zeph cc` automatically]
70
+ │ tmux send-keys -l -t zeph-myapp "리팩토링 마무리해줘" + Enter
71
+
72
+ [tmux session "zeph-myapp" running claude / codex / gemini]
73
+ ```
74
+
75
+ The listener also reports its tmux session inventory back to the server
76
+ every 5 seconds, so the phone picker stays in sync — no manual
77
+ configuration needed once a session is running.
78
+
79
+ ### Setup
80
+
81
+ 1. **Install tmux.** The listener uses `send-keys`; the wrapper spawns
82
+ named sessions. `brew install tmux` on macOS, `apt install tmux` on
83
+ Debian/Ubuntu.
84
+
85
+ 2. **Add `wsUrl` to `~/.zeph/config.json`** (the WebSocket endpoint of
86
+ your Zeph backend — CDK output `WsApiUrl`):
87
+
88
+ ```json
89
+ {
90
+ "apiKey": "ak_...",
91
+ "hookId": "hook_...",
92
+ "wsUrl": "wss://<api-id>.execute-api.<region>.amazonaws.com/<stage>"
93
+ }
94
+ ```
95
+
96
+ Alternatively set `ZEPH_WS_URL` in your shell env.
97
+
98
+ 3. **Run agents through the wrapper.** That's it.
99
+
100
+ ```bash
101
+ zeph cc # claude → tmux session "zeph-<project>"
102
+ zeph codex # codex → tmux session "zeph-<project>"
103
+ zeph gemini # gemini → tmux session "zeph-<project>"
104
+ ```
105
+
106
+ The first `zeph cc` on a machine **auto-spawns a background
107
+ listener** (singleton, PID file at `~/.zeph/listener.pid`,
108
+ stdout/stderr at `~/.zeph/listener.log`). You never run
109
+ `zeph listener` by hand — every `zeph cc` checks the PID file and
110
+ skips the spawn when one is already alive, so opening a dozen
111
+ terminals doesn't create a dozen daemons. The daemon survives
112
+ between `zeph cc` invocations.
113
+
114
+ Project name resolves from `CLAUDE_PROJECT_DIR` /
115
+ `CURSOR_PROJECT_DIR` / `WINDSURF_PROJECT_DIR` if set, else the git
116
+ repo root, else the cwd basename. Any extra args after the command
117
+ pass through to the agent verbatim:
118
+
119
+ ```bash
120
+ zeph cc --resume "abc123"
121
+ zeph cc --dangerously-skip-permissions
122
+ zeph codex --model gpt-5-high "fix the failing test"
123
+ ```
124
+
125
+ **Multiple sessions in one project.** Open another terminal in the
126
+ same folder, run `zeph cc` again, and the wrapper auto-suffixes:
127
+ first session is `zeph-encl`, the next attached one becomes
128
+ `zeph-encl-2`, then `zeph-encl-3`, etc. The phone picker shows them
129
+ as `encl · Claude`, `encl · Claude #2`, `encl · Claude #3`. If
130
+ `zeph-encl` already exists but is **detached** (no one attached),
131
+ the wrapper reattaches to it instead of spawning a new one — close
132
+ the terminal, come back later, pick up where you left off.
133
+
134
+ If you're already inside a tmux session (`$TMUX` set) the wrapper
135
+ skips the outer tmux and runs the agent in the current pane — the
136
+ listener can't target an unnamed session that way, but you keep your
137
+ existing multiplexer setup.
138
+
139
+ ### Diagnostics
140
+
141
+ The auto-spawned listener writes to two files under `~/.zeph/`:
142
+
143
+ - `listener.pid` — the running daemon's PID. `cat ~/.zeph/listener.pid`
144
+ + `ps -p <pid>` to confirm it's alive.
145
+ - `listener.log` — stdout + stderr from the daemon. `tail -f` to watch.
146
+
147
+ A healthy listener log shows one line per cycle:
148
+
149
+ ```
150
+ [xx:xx:xx] reported 2 session(s): zeph-myapp, zeph-otherapp
151
+ [xx:xx:xx] ✓ server persisted 2 session(s)
152
+ ```
153
+
154
+ If you see `! server rejected listener.sessions: ...` instead, the
155
+ message points at the failure (auth, missing device record, etc.) so
156
+ you can fix the actual problem instead of guessing.
157
+
158
+ To force a restart — e.g. after upgrading `@zeph-to/hook-sdk`:
159
+
160
+ ```bash
161
+ kill $(cat ~/.zeph/listener.pid)
162
+ rm ~/.zeph/listener.pid
163
+ zeph cc # autospawns the new build
164
+ ```
165
+
166
+ To run it in the foreground (for development of the SDK itself):
167
+
168
+ ```bash
169
+ zeph listener
170
+ ```
171
+
172
+ You'll get the same logs you'd otherwise tail from `listener.log`.
173
+
174
+ ### Custom tmux sockets
175
+
176
+ The listener auto-discovers the tmux socket — it probes the default
177
+ location, walks per-user `$TMPDIR` paths (macOS `/var/folders/.../T/`),
178
+ falls back to `/tmp/tmux-<uid>/`, and finally finds running tmux servers
179
+ via `lsof` so stale socket files don't trip discovery. If your tmux
180
+ uses `tmux -L <name>` or a non-standard `-S <path>`, set the override
181
+ explicitly:
182
+
183
+ ```bash
184
+ export ZEPH_TMUX_SOCKET=/path/to/socket
185
+ ```
186
+
187
+ (The wrapper passes the env to the auto-spawned listener, so setting
188
+ it in your shell rc is enough.)
189
+
190
+ ### Wire format
191
+
192
+ The listener only acts on pushes with `type='agent.command'` carrying
193
+ the tmux session name in `agentSessionName` and the message in `body`.
194
+ Other pushes (Stop-hook auto-pushes, `zeph_ask` responses, channel
195
+ broadcasts, plain notes) are ignored. End-to-end:
196
+
197
+ ```
198
+ tmux send-keys -l -t <agentSessionName> "<body>"
199
+ tmux send-keys -t <agentSessionName> Enter
200
+ ```
201
+
202
+ If you need to send one from the command line (debugging, scripting),
203
+ build the structured push directly:
204
+
205
+ ```bash
206
+ curl -X POST "$ZEPH_BASE_URL/pushes/send" \
207
+ -H "X-API-Key: $ZEPH_API_KEY" \
208
+ -H 'Content-Type: application/json' \
209
+ -d '{
210
+ "type": "agent.command",
211
+ "targetDeviceId": "dev_listener_<sha8(hostname)>",
212
+ "agentSessionName": "zeph-myapp",
213
+ "body": "테스트 통과시키고 PR 올려줘"
214
+ }'
215
+ ```
216
+
217
+ ### Defense
218
+
219
+ The listener is a remote-code-execution surface by design (it types
220
+ into a shell-adjacent pane). The defense is layered:
221
+
222
+ 1. **Pane guard** — before injecting, the listener checks
223
+ `tmux display-message -p '#{pane_current_command}'`. If the pane is
224
+ at an interactive shell (`bash`/`zsh`/`fish`/`sh`/`dash`/`ksh`/
225
+ `tcsh`/`csh`/`pwsh`), the inject is refused. CC/Codex/Gemini exited
226
+ ≠ phone gets free shell access.
227
+ 2. **Literal injection** — `tmux send-keys -l` takes the payload as
228
+ data; tmux escape sequences inside a message can't drive other tmux
229
+ commands.
230
+ 3. **Session-name allowlist** — only `[A-Za-z0-9._-]+` is accepted as
231
+ a session target, so shell metacharacters never reach the tmux argv.
232
+ 4. **Per-session rate limit** — 30 injections/minute/session token
233
+ bucket caps a runaway/compromised sender.
234
+ 5. **Agent permission gate stays on** — your CC/Codex/Gemini permission
235
+ prompt is still in front of every destructive tool call. The phone
236
+ can *talk* but can't approve `rm -rf` for you.
237
+
238
+ The transport (WS) is currently authenticated by API key + `push:read`
239
+ scope and is **not** end-to-end encrypted in v1 — your Zeph backend
240
+ sees the message plaintext. If you self-host or trust your backend,
241
+ that's fine. If you don't, hold off until per-device E2E ships.
24
242
 
25
243
  ## CLI Usage
26
244
 
@@ -42,6 +260,15 @@ zeph dismiss --all
42
260
  # Test connection
43
261
  zeph test
44
262
 
263
+ # Run an agent in a named tmux session (so the listener can reach it)
264
+ zeph cc # claude
265
+ zeph codex # codex
266
+ zeph gemini # gemini
267
+
268
+ # Run the resident listener (foreground; background it as you like)
269
+ zeph listener
270
+ zeph listener --ws-url wss://... # override config
271
+
45
272
  # JSON output
46
273
  zeph notify --title "Hello" --json
47
274
  ```
@@ -58,6 +285,8 @@ zeph notify --title "Hello" --json
58
285
  | `list` | List recent push notifications |
59
286
  | `dismiss <id>` | Dismiss a push (or `--all`) |
60
287
  | `test` | Verify connection and API key |
288
+ | `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 "..."`) |
289
+ | `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
290
 
62
291
  ### Notify Options
63
292
 
@@ -70,7 +299,22 @@ zeph notify --title "Hello" --json
70
299
  | `--priority <p>` | Priority: `low`, `normal`, `high`, `urgent` |
71
300
  | `--device <id>` | Target device ID |
72
301
 
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.
302
+ The defaults are tuned for hook-driven invocations (e.g. Stop hooks
303
+ calling `zeph notify --title "Task done"` without a body) — you'll see
304
+ which project + branch finished without writing per-IDE wrappers. Pass
305
+ `--body ""` explicitly to suppress.
306
+
307
+ ### Listener Options
308
+
309
+ | Flag | Description |
310
+ |------|-------------|
311
+ | `--ws-url <url>` | WebSocket endpoint (or set `ZEPH_WS_URL` env, or `wsUrl` in `~/.zeph/config.json`) |
312
+ | `--key <api-key>` | API key (or set `ZEPH_API_KEY` env) |
313
+
314
+ The listener reconnects with exponential backoff + jitter (1 s → 30 s
315
+ cap). Heartbeat is ping every 25 s with a 10 s pong timeout. On an
316
+ authentication failure close (4001/4002/4003) the listener exits with
317
+ code 3 instead of looping forever — fix the key and restart.
74
318
 
75
319
  ### List Options
76
320
 
@@ -90,9 +334,11 @@ The defaults are tuned for hook-driven invocations (e.g. Stop hooks calling `zep
90
334
 
91
335
  ### Mute
92
336
 
93
- Mute is project-scoped (uses project directory hash). Created by Claude Code `/zeph-mute` command.
337
+ Mute is project-scoped (uses project directory hash). Created by Claude
338
+ Code `/zeph-mute` command.
94
339
 
95
- Notifications are silently skipped when a mute file exists for the current project:
340
+ Notifications are silently skipped when a mute file exists for the
341
+ current project:
96
342
 
97
343
  ```bash
98
344
  # Mute (created by /zeph-mute in Claude Code plugin)
@@ -103,7 +349,8 @@ touch /tmp/zeph-muted-$HASH
103
349
  rm /tmp/zeph-muted-$HASH
104
350
  ```
105
351
 
106
- The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`, `WINDSURF_PROJECT_DIR`, and falls back to `cwd`.
352
+ The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`,
353
+ `WINDSURF_PROJECT_DIR`, and falls back to `cwd`.
107
354
 
108
355
  ### Exit Codes
109
356
 
@@ -112,7 +359,8 @@ The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`, `WINDSURF_PROJECT_DIR
112
359
  | 0 | Success |
113
360
  | 1 | General error |
114
361
  | 2 | Quota exceeded |
115
- | 3 | Authentication failed |
362
+ | 3 | Authentication failed (also: listener auth close 4001/4002/4003) |
363
+ | 127 | A required external binary (e.g. `tmux`, `claude`) was not found on PATH |
116
364
 
117
365
  ### Environment Variables
118
366
 
@@ -120,6 +368,9 @@ The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`, `WINDSURF_PROJECT_DIR
120
368
  |----------|-------------|
121
369
  | `ZEPH_API_KEY` | API key (fallback when `--key` not provided) |
122
370
  | `ZEPH_BASE_URL` | API base URL (default: `https://api.zeph.to/v1`) |
371
+ | `ZEPH_WS_URL` | WebSocket endpoint for `zeph listener` (no default — required) |
372
+ | `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>`) |
373
+ | `ZEPH_SESSION_ID` | AI session ID (fallback when `--session` not provided) |
123
374
 
124
375
  ## SDK Usage
125
376
 
@@ -193,17 +444,39 @@ try {
193
444
  | Copilot CLI | Session end hook |
194
445
  | Cline | Rules file |
195
446
 
196
- ## Encryption
447
+ For remote-control via `zeph listener` the per-agent setup is the same
448
+ across CC/Codex/Gemini — the wrapper just spawns them in a named tmux
449
+ session.
197
450
 
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.
451
+ ## Encryption
199
452
 
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.
453
+ Push bodies are encrypted with AES-256-GCM. The wrapping key is derived
454
+ via ECDH P-256 and synced across your own devices on first run so every
455
+ device can read the same push. Toggle encryption in the Zeph app
456
+ (Settings → Encryption); when disabled, the CLI sends plaintext. No
457
+ configuration needed.
458
+
459
+ **Threat model honesty:** keys are persisted on the Zeph backend to
460
+ enable cross-device sync, so this is *device-shared* encryption — not
461
+ true end-to-end. It protects push contents from passive network
462
+ observers and from a leaked database snapshot taken without the key
463
+ store, but it does **not** protect against the Zeph backend itself (it
464
+ has the keys it serves to your devices). A true E2E mode (per-device
465
+ keypairs, server stores only public keys, no key escrow) is on the
466
+ roadmap.
467
+
468
+ The `zeph listener` ignores `isEncrypted` pushes for now — it has no
469
+ per-device key to decrypt them. Stop-hook auto-pushes and `zeph_ask`
470
+ responses are not part of the `@<session>` injection path, so this
471
+ doesn't affect normal use.
201
472
 
202
473
  ## Requirements
203
474
 
204
- - Node.js >= 18 (uses native `fetch`)
475
+ - **Node.js >= 18** (uses native `fetch`).
476
+ - **tmux** — required for `zeph cc` / `codex` / `gemini` and `zeph listener`.
205
477
  - The `ZephHook` SDK has no runtime dependencies. The CLI depends on
206
- `@inquirer/prompts` for the interactive `zeph install` picker.
478
+ `@inquirer/prompts` for the interactive `zeph install` picker and on
479
+ `ws` for the listener's WebSocket subscription.
207
480
 
208
481
  ## License
209
482
 
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,117 @@
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 `-`. Cached for 60s — see
54
+ * claudeSessionCache.
55
+ */
56
+ export declare const detectClaudeSessionId: (cwd: string) => string | null;
57
+ export interface CollectResult {
58
+ sessions: AgentSession[];
59
+ /** Diagnostic notes per rejected session — surfaced under `--verbose`. */
60
+ rejected: Array<{
61
+ name: string;
62
+ reason: string;
63
+ }>;
64
+ }
65
+ /**
66
+ * Inventory pass that also records *why* each `zeph-*` session was
67
+ * skipped. The verbose log uses the rejection notes to explain empty
68
+ * pickers (most common cause: tmux pane lost its start_command after a
69
+ * re-attach, and the current command is `node` rather than `claude`).
70
+ */
71
+ export declare const collectSessionsVerbose: () => CollectResult;
72
+ /**
73
+ * Snapshot the live `zeph-*` tmux sessions on this machine, enriched
74
+ * with the running agent kind, CC session UUID (claude only), project,
75
+ * and tmux activity timestamps. Returns [] when tmux is unreachable
76
+ * or no agent sessions exist. Sessions whose pane is at a shell or
77
+ * running something other than claude/codex/gemini are filtered out
78
+ * — the phone can't usefully address them.
79
+ */
80
+ export declare const collectSessions: () => AgentSession[];
81
+ interface PushItem {
82
+ pushId: string;
83
+ type?: string;
84
+ body?: string;
85
+ title?: string;
86
+ createdAt?: string;
87
+ isEncrypted?: boolean;
88
+ /** Set when type='agent.command' — tmux session name to inject into. */
89
+ agentSessionName?: string;
90
+ }
91
+ interface HandlePushDeps {
92
+ paneCommand?: (session: string) => string | null;
93
+ inject?: (session: string, text: string) => boolean;
94
+ rateLimit?: (session: string) => boolean;
95
+ now?: () => number;
96
+ }
97
+ /**
98
+ * Process one push. Returns true when an injection actually fired.
99
+ * Exported for unit testing with mocked deps.
100
+ *
101
+ * Only acts on `type='agent.command'` pushes carrying both an
102
+ * `agentSessionName` (tmux session to inject into) and a non-empty
103
+ * `body`. Everything else (Stop-hook auto-pushes, zeph_ask responses,
104
+ * encrypted pushes, normal text/link/file notifications) is ignored.
105
+ */
106
+ export declare const handlePush: (push: PushItem, deps?: HandlePushDeps) => boolean;
107
+ /**
108
+ * Stable per-host device id for the listener. We hash the OS hostname so
109
+ * the same machine reuses the same DeviceRecord across listener restarts
110
+ * (otherwise the phone's session inventory grows a new ghost device every
111
+ * time `zeph listener` rebinds). `dev_listener_<sha8(hostname)>` keeps it
112
+ * human-recognisable in dev logs without leaking the raw hostname.
113
+ */
114
+ export declare const computeListenerDeviceId: (host?: string) => string;
115
+ export declare const handleListener: (args: Record<string, string | boolean>) => Promise<number>;
116
+ export {};
117
+ //# 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;AAuCF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,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,CAyF3F,CAAC"}