@zeph-to/cli 1.17.0 → 1.19.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
@@ -5,13 +5,23 @@
5
5
  [![node](https://img.shields.io/node/v/@zeph-to/cli.svg)](https://nodejs.org)
6
6
  [![license](https://img.shields.io/npm/l/@zeph-to/cli.svg)](./LICENSE)
7
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.
8
+ Push notification SDK + CLI for [Zeph](https://zeph.to) the terminal
9
+ side of the round trip: your agent works, hits a decision, and asks your
10
+ phone; you tap a button (or type a reply), and the answer lands back in
11
+ the live session so the agent keeps going.
11
12
 
12
13
  - `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.
14
+ - `zeph` CLI — one-command setup for 8 AI agents, push sending, and an
15
+ optional resident listener that **drives Claude Code / Codex / Gemini
16
+ sessions from your phone** by injecting messages into named tmux
17
+ sessions.
18
+
19
+ Part of the Zeph toolchain:
20
+ [`@zeph-to/mcp-server`](https://github.com/zeph-to/mcp-server) (the MCP
21
+ tools your agent calls, e.g. `zeph_ask`) ·
22
+ [`zeph-to/plugin`](https://github.com/zeph-to/plugin) (Claude Code plugin
23
+ bundling hooks + MCP + rules) · the [Zeph app](https://zeph.to) on your
24
+ phone.
15
25
 
16
26
  ## Installation
17
27
 
@@ -49,6 +59,11 @@ npx @zeph-to/cli install --key ak_... --hook hook_...
49
59
  Either way it saves to `~/.zeph/config.json`. All Zeph tools (CLI, MCP
50
60
  server, plugin hooks, listener) read this file.
51
61
 
62
+ On a fresh machine `install` alone is enough: with no `--key` and no
63
+ saved config it opens the same browser login automatically (headless
64
+ boxes fall back to manual key entry). Pass `--only claude,cursor,…` to
65
+ skip the interactive agent picker.
66
+
52
67
  To **send** notifications:
53
68
 
54
69
  ```bash
@@ -63,8 +78,8 @@ To **drive a Claude Code / Codex / Gemini session from your phone**, see
63
78
  > Send messages from your phone *into* a live Claude Code / Codex /
64
79
  > Gemini session — even after a `zeph_ask` polling window has expired.
65
80
 
66
- The MCP tools `zeph_ask` / `zeph_prompt` / `zeph_input` open a polling
67
- loop on a fixed timeout (120–600 s). Once that window closes the
81
+ The MCP tools `zeph_ask` / `zeph_prompt` / `zeph_input` wait on a fixed
82
+ timeout (120–600 s). Once that window closes the
68
83
  session becomes unaddressable from the phone, even though it's still
69
84
  running. The `zeph listener` daemon fixes this by keeping a persistent
70
85
  WebSocket open to Zeph and injecting matching messages into a *named*
@@ -91,6 +106,25 @@ The listener also reports its tmux session inventory back to the server
91
106
  every 5 seconds, so the phone picker stays in sync — no manual
92
107
  configuration needed once a session is running.
93
108
 
109
+ ### Remote-origin detection (sticky REMOTE mode)
110
+
111
+ A message injected via `send-keys` is indistinguishable from typing — so
112
+ the listener also records each injection as a one-shot marker (epoch +
113
+ sha256 of the text, keyed by the pane's project dir). A prompt-submit
114
+ hook on the agent side matches the submitted prompt against that marker
115
+ and, on an exact match, tells the model the user is driving the session
116
+ from their phone — entering sticky REMOTE mode (every response ends with
117
+ an answerable `zeph_ask`).
118
+
119
+ | Agent | Hook | Installed by |
120
+ |-------|------|--------------|
121
+ | Claude Code | `UserPromptSubmit` → plugin's `zeph-remote.sh` | Zeph plugin |
122
+ | Gemini CLI | `BeforeAgent` → `zeph remote-hook gemini` | `zeph setup` |
123
+ | Codex CLI | `UserPromptSubmit` → `zeph remote-hook codex` | `zeph setup` |
124
+
125
+ Detection is exact-match: a terminal keystroke racing a phone message
126
+ can never false-flag. Muted projects are never flagged.
127
+
94
128
  ### Setup
95
129
 
96
130
  1. **Install tmux.** The listener uses `send-keys`; the wrapper spawns
@@ -293,7 +327,7 @@ zeph notify --title "Hello" --json
293
327
  | Command | Description |
294
328
  |---------|-------------|
295
329
  | `login` | Browser sign-in: auto-fetch API key + hook into `~/.zeph/config.json` over a localhost loopback (`--web-url`, `--timeout`). No copy-paste |
296
- | `install` | One-command setup: detect agents, save config, install rules + hooks + MCP |
330
+ | `install` (alias: `setup`) | One-command setup: detect agents, save config, install rules + hooks + MCP. No saved config → opens browser login automatically. `--only claude,cursor,…` skips the picker |
297
331
  | `uninstall` | Remove Zeph from all detected agents (`--dry-run`, `--purge`) |
298
332
  | `verify` | Check installation health across detected agents (`--ping` for a live API call) |
299
333
  | `check-update` | Check whether a newer Zeph version is on npm |
@@ -314,6 +348,10 @@ zeph notify --title "Hello" --json
314
348
  | `--type <type>` | Push type: `note`, `link`, `file`, `hook` |
315
349
  | `--priority <p>` | Priority: `low`, `normal`, `high`, `urgent` |
316
350
  | `--device <id>` | Target device ID |
351
+ | `--session <id>` | AI session ID so the push threads into that session's chat (or `ZEPH_SESSION_ID` env) |
352
+ | `--auto` | Apply the push gate before sending — honors the `/zeph-quiet` / `/zeph-loud` push-mode dial; gated-out exits silently with code 0 |
353
+ | `--marker <m>` | Push Signal marker for `--auto`: `skip`, `push`, `high` |
354
+ | `--tools <n>`, `--nonreadonly <n>` | Turn tool counts feeding `--auto`'s heuristic (defaults assume real work) |
317
355
 
318
356
  The defaults are tuned for hook-driven invocations (e.g. Stop hooks
319
357
  calling `zeph notify --title "Task done"` without a body) — you'll see
@@ -326,6 +364,7 @@ which project + branch finished without writing per-IDE wrappers. Pass
326
364
  |------|-------------|
327
365
  | `--ws-url <url>` | WebSocket endpoint (or set `ZEPH_WS_URL` env, or `wsUrl` in `~/.zeph/config.json`) |
328
366
  | `--key <api-key>` | API key (or set `ZEPH_API_KEY` env) |
367
+ | `--base-url <url>` | REST API base URL (or set `ZEPH_BASE_URL` env, or `baseUrl` in `~/.zeph/config.json`) |
329
368
 
330
369
  The listener reconnects with exponential backoff + jitter (1 s → 30 s
331
370
  cap). Heartbeat is ping every 25 s with a 10 s pong timeout. On an
@@ -454,11 +493,12 @@ try {
454
493
  |-------|-------------------|
455
494
  | Claude Code | Plugin (hooks + MCP server) |
456
495
  | Cursor | MCP server + stop hook + rules |
457
- | Windsurf | MCP server + response hook |
496
+ | Windsurf | MCP server + response hook + rules |
458
497
  | Gemini CLI | MCP server + AfterAgent hook |
459
- | Codex CLI | Stop hook |
460
- | Copilot CLI | Session end hook |
461
- | Cline | Rules file |
498
+ | Codex CLI | Stop hook + rules |
499
+ | Copilot CLI | Session end hook + rules |
500
+ | Cline | Rules file (`~/.cline/rules/zeph.md`) |
501
+ | Aider | Conventions file + `read:` directive in `~/.aider.conf.yml` |
462
502
 
463
503
  For remote-control via `zeph listener` the per-agent setup is the same
464
504
  across CC/Codex/Gemini — the wrapper just spawns them in a named tmux
@@ -49,7 +49,7 @@ const handleCheckUpdate = async (args) => {
49
49
  }
50
50
  if (isJson) {
51
51
  console.log(JSON.stringify({ results }, null, 2));
52
- return results.some((r) => r.outdated) ? 0 : 0;
52
+ return 0;
53
53
  }
54
54
  console.log('\n Zeph — update check\n');
55
55
  let anyOutdated = false;
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ const listener_js_1 = require("./listener.js");
14
14
  const config_js_1 = require("./config.js");
15
15
  const gate_js_1 = require("./gate.js");
16
16
  const remote_agents_js_1 = require("./remote-agents.js");
17
+ const remote_hook_js_1 = require("./remote-hook.js");
17
18
  const detectBranchAndProject = () => {
18
19
  const dir = (0, config_js_1.detectProjectDir)();
19
20
  const project = dir.split('/').filter(Boolean).pop() ?? 'project';
@@ -164,6 +165,30 @@ const gateCount = (raw, fallback) => {
164
165
  const n = typeof raw === 'string' ? Number(raw) : NaN;
165
166
  return Number.isFinite(n) && n >= 0 ? n : fallback;
166
167
  };
168
+ /**
169
+ * Internal — invoked by the Gemini/Codex prompt-submit hooks that
170
+ * `zeph setup` registers, never typed by users (hence absent from help).
171
+ * Reads the hook JSON on stdin; prints additionalContext JSON on a match
172
+ * (remote-origin detection, ADR-0002). Always exits 0: the hook only adds
173
+ * context and must never block a prompt.
174
+ */
175
+ const handleRemoteHook = async (args) => {
176
+ const agent = args._arg1;
177
+ if (!(0, remote_hook_js_1.isRemoteHookAgent)(agent))
178
+ return 0;
179
+ try {
180
+ let stdin = '';
181
+ for await (const chunk of process.stdin)
182
+ stdin += chunk;
183
+ const out = (0, remote_hook_js_1.runRemoteHook)(agent, stdin);
184
+ if (out)
185
+ console.log(out);
186
+ }
187
+ catch {
188
+ /* never block a prompt */
189
+ }
190
+ return 0;
191
+ };
167
192
  const handleNotify = async (args) => {
168
193
  const isJson = args.json === true;
169
194
  const projectDir = (0, config_js_1.detectProjectDir)();
@@ -229,7 +254,10 @@ const handleList = async (args) => {
229
254
  if (!hook)
230
255
  return 3;
231
256
  try {
232
- const limit = args.limit ? Number(args.limit) : undefined;
257
+ // Clamp to the documented 1-20 range; a non-numeric value falls back to
258
+ // the server default instead of sending limit=NaN in the query string.
259
+ const rawLimit = args.limit ? Number(args.limit) : NaN;
260
+ const limit = Number.isFinite(rawLimit) ? Math.min(20, Math.max(1, Math.floor(rawLimit))) : undefined;
233
261
  const result = await hook.list({
234
262
  limit,
235
263
  type: args.type,
@@ -381,10 +409,15 @@ const main = async () => {
381
409
  return handleTest(args);
382
410
  case 'listener':
383
411
  return (0, listener_js_1.handleListener)(args);
412
+ case 'remote-hook':
413
+ return handleRemoteHook(args);
384
414
  default:
385
415
  printError(`Unknown command: ${command}`, args.json === true);
386
416
  printUsage();
387
417
  return 1;
388
418
  }
389
419
  };
390
- main().then((code) => process.exit(code));
420
+ main().then((code) => process.exit(code), (err) => {
421
+ console.error(err instanceof Error ? err.message : String(err));
422
+ process.exit(1);
423
+ });
@@ -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,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAMF,eAAO,MAAM,oBAAoB,+EAAgF,CAAC;AAElH,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,QAAO,MAMnC,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;AAMF,eAAO,MAAM,oBAAoB,+EAAgF,CAAC;AAElH,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,QAAO,MAMnC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAM/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
package/dist/config.js CHANGED
@@ -36,8 +36,11 @@ const loadConfig = () => {
36
36
  };
37
37
  exports.loadConfig = loadConfig;
38
38
  const saveConfig = (config) => {
39
- (0, fs_1.mkdirSync)(exports.CONFIG_DIR, { recursive: true });
40
- (0, fs_1.writeFileSync)(exports.CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
39
+ (0, fs_1.mkdirSync)(exports.CONFIG_DIR, { recursive: true, mode: 0o700 });
40
+ (0, fs_1.writeFileSync)(exports.CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
41
+ // `mode` only applies on creation — tighten pre-existing installs too,
42
+ // since this file holds the API key.
43
+ (0, fs_1.chmodSync)(exports.CONFIG_FILE, 0o600);
41
44
  };
42
45
  exports.saveConfig = saveConfig;
43
46
  exports.VERSION = (() => {
package/dist/crypto.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Device-shared encryption for Hook SDK — self-contained ECDH P-256 +
3
3
  * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
4
- * dependency). Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
4
+ * dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
5
+ * (the `crypto` global only exists unflagged from Node 19, so we import it).
5
6
  *
6
7
  * Threat model honesty (do not call this "E2E" without a footnote):
7
8
  *
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA2JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AA2CF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAiKH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AA2CF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
package/dist/crypto.js CHANGED
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * Device-shared encryption for Hook SDK — self-contained ECDH P-256 +
4
4
  * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
5
- * dependency). Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
5
+ * dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
6
+ * (the `crypto` global only exists unflagged from Node 19, so we import it).
6
7
  *
7
8
  * Threat model honesty (do not call this "E2E" without a footnote):
8
9
  *
@@ -33,6 +34,11 @@ exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPu
33
34
  const fs_1 = require("fs");
34
35
  const os_1 = require("os");
35
36
  const path_1 = require("path");
37
+ const node_crypto_1 = require("node:crypto");
38
+ // Node 18 has no `crypto` global (unflagged only from 19.0.0) — resolve the
39
+ // Web Crypto implementation explicitly so encryption works on the declared
40
+ // minimum runtime instead of silently falling back to plaintext.
41
+ const crypto = node_crypto_1.webcrypto;
36
42
  // ─── Base64 helpers ───
37
43
  const toBase64 = (buffer) => {
38
44
  const bytes = new Uint8Array(buffer);
package/dist/gate.d.ts CHANGED
@@ -28,7 +28,17 @@ export declare const GATE_DEFAULTS: {
28
28
  export declare const normalizeMarker: (raw: string | undefined) => GateMarker;
29
29
  export declare const normalizePushMode: (raw: string | undefined) => GatePushMode;
30
30
  export declare const decidePush: (input: GateInput) => GateVerdict;
31
+ export declare const stateDir: () => string;
31
32
  export declare const projectHash: (dir: string) => string | null;
33
+ /** Marker path for a project hash: `<stateDir>/remote-<cksum(projectDir)>`. */
34
+ export declare const remoteMarkerPath: (hash: string) => string;
35
+ /**
36
+ * sha256 over the ASCII-only-trimmed text — recorded at inject time,
37
+ * recomputed at prompt submit. Trim strips ONLY ' \t\r\n\f\v', NOT
38
+ * String.trim(): trim() also eats Unicode whitespace (U+00A0 etc.) that
39
+ * the bash reader keeps, and both sides must hash identical bytes.
40
+ */
41
+ export declare const remoteDigest: (text: string) => string;
32
42
  /** True when the user ran /zeph-mute for this project. */
33
43
  export declare const isMuted: (dir: string) => boolean;
34
44
  /** The user's session push-mode dial (/zeph-quiet | /zeph-loud), default normal. */
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,UACS,CAAC;AAEpE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,YACR,CAAC;AAErD,eAAO,MAAM,UAAU,GAAI,OAAO,SAAS,KAAG,WAW7C,CAAC;AAQF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,OAGrC,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,KAAG,YAQ1C,CAAC"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,UACS,CAAC;AAEpE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,YACR,CAAC;AAErD,eAAO,MAAM,UAAU,GAAI,OAAO,SAAS,KAAG,WAW7C,CAAC;AAeF,eAAO,MAAM,QAAQ,QAAO,MACoD,CAAC;AAmBjF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAWF,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAA4C,CAAC;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAG1B,CAAC;AAEnB,0DAA0D;AAC1D,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,OAGrC,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,KAAG,YAU1C,CAAC"}
package/dist/gate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readPushMode = exports.isMuted = exports.projectHash = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
3
+ exports.readPushMode = exports.isMuted = exports.remoteDigest = exports.remoteMarkerPath = exports.projectHash = exports.stateDir = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
4
4
  /**
5
5
  * Push-gate decision — the portable half of the Zeph Stop-hook logic.
6
6
  *
@@ -21,7 +21,10 @@ exports.readPushMode = exports.isMuted = exports.projectHash = exports.decidePus
21
21
  * (the B1 read-only floor).
22
22
  */
23
23
  const child_process_1 = require("child_process");
24
+ const crypto_1 = require("crypto");
24
25
  const fs_1 = require("fs");
26
+ const os_1 = require("os");
27
+ const path_1 = require("path");
25
28
  /**
26
29
  * "Assume real work" defaults for hooks that can't supply turn facts
27
30
  * (most non-Claude agents pass no counts): in normal mode the push still
@@ -54,9 +57,35 @@ const decidePush = (input) => {
54
57
  exports.decidePush = decidePush;
55
58
  // ── Per-project gate state (mute + push-mode dial) ───────────────
56
59
  //
57
- // The plugin's bash hooks key these tmp files off `cksum` of the project
58
- // dir; shelling out to the same `cksum` here (instead of a pure-TS CRC)
60
+ // State lives under ${XDG_STATE_HOME:-~/.local/state}/zeph a per-user
61
+ // directory. It used to live at predictable names in world-writable /tmp,
62
+ // where any local user could pre-create a victim's mute file (and sticky
63
+ // /tmp makes that file un-deletable by the victim). Legacy /tmp files are
64
+ // still honored during the migration window, but only when owned by the
65
+ // current user, which neutralizes planted files.
66
+ //
67
+ // The plugin's bash hooks key these files off `cksum` of the project dir;
68
+ // shelling out to the same `cksum` here (instead of a pure-TS CRC)
59
69
  // guarantees hash parity with every already-written file.
70
+ const stateDir = () => (0, path_1.join)(process.env.XDG_STATE_HOME || (0, path_1.join)((0, os_1.homedir)(), '.local', 'state'), 'zeph');
71
+ exports.stateDir = stateDir;
72
+ const ownedByCurrentUser = (path) => {
73
+ try {
74
+ const st = (0, fs_1.statSync)(path);
75
+ return typeof process.getuid !== 'function' || st.uid === process.getuid();
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ };
81
+ /** Resolve a state file: current location first, then user-owned legacy /tmp. */
82
+ const findStateFile = (kind, hash) => {
83
+ const current = (0, path_1.join)((0, exports.stateDir)(), `${kind}-${hash}`);
84
+ if ((0, fs_1.existsSync)(current))
85
+ return current;
86
+ const legacy = `/tmp/zeph-${kind}-${hash}`;
87
+ return ownedByCurrentUser(legacy) ? legacy : null;
88
+ };
60
89
  const projectHash = (dir) => {
61
90
  try {
62
91
  const raw = (0, child_process_1.execFileSync)('cksum', { input: dir, encoding: 'utf-8' });
@@ -67,10 +96,31 @@ const projectHash = (dir) => {
67
96
  }
68
97
  };
69
98
  exports.projectHash = projectHash;
99
+ // ── Remote-origin marker (ADR-0002) ──────────────────────────────
100
+ //
101
+ // The listener records every phone→pane text injection as a one-shot
102
+ // marker file; a prompt-submit hook (Claude Code plugin's zeph-remote.sh,
103
+ // or `zeph remote-hook` for Gemini/Codex) consumes it on an exact-text
104
+ // match and flags the prompt as remote-originated. Writer and TS reader
105
+ // share these two helpers so their bytes can never diverge; the bash
106
+ // reader is held to the same semantics by the plugin's test suite.
107
+ /** Marker path for a project hash: `<stateDir>/remote-<cksum(projectDir)>`. */
108
+ const remoteMarkerPath = (hash) => (0, path_1.join)((0, exports.stateDir)(), `remote-${hash}`);
109
+ exports.remoteMarkerPath = remoteMarkerPath;
110
+ /**
111
+ * sha256 over the ASCII-only-trimmed text — recorded at inject time,
112
+ * recomputed at prompt submit. Trim strips ONLY ' \t\r\n\f\v', NOT
113
+ * String.trim(): trim() also eats Unicode whitespace (U+00A0 etc.) that
114
+ * the bash reader keeps, and both sides must hash identical bytes.
115
+ */
116
+ const remoteDigest = (text) => (0, crypto_1.createHash)('sha256')
117
+ .update(text.replace(/^[ \t\r\n\f\v]+|[ \t\r\n\f\v]+$/g, ''))
118
+ .digest('hex');
119
+ exports.remoteDigest = remoteDigest;
70
120
  /** True when the user ran /zeph-mute for this project. */
71
121
  const isMuted = (dir) => {
72
122
  const hash = (0, exports.projectHash)(dir);
73
- return hash !== null && (0, fs_1.existsSync)(`/tmp/zeph-muted-${hash}`);
123
+ return hash !== null && findStateFile('muted', hash) !== null;
74
124
  };
75
125
  exports.isMuted = isMuted;
76
126
  /** The user's session push-mode dial (/zeph-quiet | /zeph-loud), default normal. */
@@ -78,8 +128,11 @@ const readPushMode = (dir) => {
78
128
  const hash = (0, exports.projectHash)(dir);
79
129
  if (!hash)
80
130
  return 'normal';
131
+ const file = findStateFile('pushmode', hash);
132
+ if (!file)
133
+ return 'normal';
81
134
  try {
82
- return (0, exports.normalizePushMode)((0, fs_1.readFileSync)(`/tmp/zeph-pushmode-${hash}`, 'utf-8').replace(/\s+/g, ''));
135
+ return (0, exports.normalizePushMode)((0, fs_1.readFileSync)(file, 'utf-8').replace(/\s+/g, ''));
83
136
  }
84
137
  catch {
85
138
  return 'normal';
@@ -4,6 +4,9 @@ import type { Agent } from './agents.js';
4
4
  * context with no existing credential (--key/env/config all absent).
5
5
  */
6
6
  export declare const shouldTriggerLogin: (nonInteractive: boolean, currentKey: string | undefined) => boolean;
7
+ /** Exported for tests (installGemini shells out to the real `gemini` binary,
8
+ * so the merge semantics can't be covered through it deterministically). */
9
+ export declare const mergeJsonFile: (filePath: string, patch: Record<string, unknown>) => void;
7
10
  /**
8
11
  * Resolve agents from a non-interactive `--only cursor,gemini` flag.
9
12
  * Matches on agent id; unknown ids are silently dropped. Exported for
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA2BzC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,gBAAgB,OAAO,EAAE,YAAY,MAAM,GAAG,SAAS,KAAG,OAC7D,CAAC;AA+QjC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA4FF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAkG1F,CAAC"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA4BzC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,gBAAgB,OAAO,EAAE,YAAY,MAAM,GAAG,SAAS,KAAG,OAC7D,CAAC;AAoBjC;6EAC6E;AAC7E,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAqBhF,CAAC;AAuPF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA4FF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAkG1F,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 = exports.filterAgentsByIds = exports.shouldTriggerLogin = void 0;
3
+ exports.handleInstall = exports.filterAgentsByIds = exports.mergeJsonFile = exports.shouldTriggerLogin = 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");
@@ -34,6 +34,9 @@ const writeFile = (filePath, content) => {
34
34
  (0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
35
35
  (0, fs_1.writeFileSync)(filePath, content + '\n');
36
36
  };
37
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
38
+ /** Exported for tests (installGemini shells out to the real `gemini` binary,
39
+ * so the merge semantics can't be covered through it deterministically). */
37
40
  const mergeJsonFile = (filePath, patch) => {
38
41
  let data = {};
39
42
  try {
@@ -41,8 +44,23 @@ const mergeJsonFile = (filePath, patch) => {
41
44
  }
42
45
  catch { /* new file */ }
43
46
  const merged = { ...data, ...patch };
47
+ // `hooks` maps event names to matcher-group arrays, and the user may own
48
+ // entries at BOTH levels (their own events, and their own groups inside
49
+ // an event zeph also uses) — so instead of clobbering the whole object,
50
+ // merge events, and inside each event zeph writes keep the user's groups
51
+ // and replace only zeph's own (re-runs stay idempotent).
52
+ if (isPlainObject(data.hooks) && isPlainObject(patch.hooks)) {
53
+ const hooks = { ...data.hooks };
54
+ for (const [event, patchGroups] of Object.entries(patch.hooks)) {
55
+ const prev = hooks[event];
56
+ const userGroups = Array.isArray(prev) ? prev.filter((g) => !(0, templates_js_1.isZephHookGroup)(g)) : [];
57
+ hooks[event] = [...userGroups, ...(Array.isArray(patchGroups) ? patchGroups : [])];
58
+ }
59
+ merged.hooks = hooks;
60
+ }
44
61
  writeFile(filePath, JSON.stringify(merged, null, 2));
45
62
  };
63
+ exports.mergeJsonFile = mergeJsonFile;
46
64
  /**
47
65
  * Write a Zeph rule into a SHARED agent rule file (Windsurf global_rules.md,
48
66
  * Gemini GEMINI.md, Codex AGENTS.md) without clobbering the user's own
@@ -168,8 +186,8 @@ const installGemini = () => {
168
186
  fail('MCP add failed. Manual: gemini mcp add zeph -- npx -y @zeph-to/mcp-server');
169
187
  }
170
188
  try {
171
- mergeJsonFile((0, path_1.join)(HOME, '.gemini', 'settings.json'), templates_js_1.GEMINI_HOOKS);
172
- ok('AfterAgent hook added');
189
+ (0, exports.mergeJsonFile)((0, path_1.join)(HOME, '.gemini', 'settings.json'), templates_js_1.GEMINI_HOOKS);
190
+ ok('BeforeAgent + AfterAgent hooks added');
173
191
  }
174
192
  catch {
175
193
  fail('Hook install failed');
@@ -185,8 +203,8 @@ const installGemini = () => {
185
203
  };
186
204
  const installCodex = () => {
187
205
  try {
188
- writeFile((0, path_1.join)(HOME, '.codex', 'hooks.json'), templates_js_1.CODEX_HOOKS);
189
- ok('Stop hook added');
206
+ (0, exports.mergeJsonFile)((0, path_1.join)(HOME, '.codex', 'hooks.json'), templates_js_1.CODEX_HOOKS);
207
+ ok('UserPromptSubmit + Stop hooks added');
190
208
  }
191
209
  catch {
192
210
  fail('Hook install failed. Manual: add zeph to ~/.codex/hooks.json');
@@ -207,7 +207,18 @@ interface HandlePushDeps {
207
207
  now?: () => number;
208
208
  /** Injectable for tests; defaults to the REST-backed downloader. */
209
209
  downloadAttachments?: (pushId: string, files: PushFileAttachment[]) => Promise<string[]>;
210
+ /** Pane cwd lookup for the remote-origin marker (ADR-0002). */
211
+ paneCwd?: (session: string) => string | null;
210
212
  }
213
+ /**
214
+ * Record a successful phone→pane text injection so a prompt-submit hook
215
+ * (Claude Code plugin's zeph-remote.sh, or `zeph remote-hook` for
216
+ * Gemini/Codex) can flag the matching prompt as remote-originated and
217
+ * enter sticky REMOTE mode (ADR-0002). One file per project dir,
218
+ * overwritten on every inject; the hook consumes it on an exact-text match.
219
+ * Best-effort: a write failure must never fail the injection itself.
220
+ */
221
+ export declare const writeRemoteMarker: (paneCwd: string, text: string, now?: () => number) => boolean;
211
222
  export declare const setAttachmentContext: (ctx: {
212
223
  apiKey: string;
213
224
  baseUrl: string;
@@ -1 +1 @@
1
- {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
package/dist/listener.js CHANGED
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
25
25
  return (mod && mod.__esModule) ? mod : { "default": mod };
26
26
  };
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
- exports.handleListener = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.collectSessions = exports.collectSessionsVerbose = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = void 0;
28
+ exports.handleListener = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = void 0;
29
29
  const child_process_1 = require("child_process");
30
30
  const crypto_1 = require("crypto");
31
31
  const fs_1 = require("fs");
@@ -33,6 +33,7 @@ const os_1 = require("os");
33
33
  const path_1 = require("path");
34
34
  const ws_1 = __importDefault(require("ws"));
35
35
  const config_js_1 = require("./config.js");
36
+ const gate_js_1 = require("./gate.js");
36
37
  const remote_agents_js_1 = require("./remote-agents.js");
37
38
  const agent_state_js_1 = require("./agent-state.js");
38
39
  const agent_rules_fetch_js_1 = require("./agent-rules-fetch.js");
@@ -772,6 +773,29 @@ const passesInjectGuards = (session, deps) => {
772
773
  }
773
774
  return true;
774
775
  };
776
+ /**
777
+ * Record a successful phone→pane text injection so a prompt-submit hook
778
+ * (Claude Code plugin's zeph-remote.sh, or `zeph remote-hook` for
779
+ * Gemini/Codex) can flag the matching prompt as remote-originated and
780
+ * enter sticky REMOTE mode (ADR-0002). One file per project dir,
781
+ * overwritten on every inject; the hook consumes it on an exact-text match.
782
+ * Best-effort: a write failure must never fail the injection itself.
783
+ */
784
+ const writeRemoteMarker = (paneCwd, text, now = Date.now) => {
785
+ const hash = (0, gate_js_1.projectHash)(paneCwd);
786
+ if (!hash)
787
+ return false;
788
+ try {
789
+ (0, fs_1.mkdirSync)((0, gate_js_1.stateDir)(), { recursive: true, mode: 0o700 });
790
+ (0, fs_1.writeFileSync)((0, gate_js_1.remoteMarkerPath)(hash), `${Math.floor(now() / 1000)} ${(0, gate_js_1.remoteDigest)(text)}\n`);
791
+ return true;
792
+ }
793
+ catch {
794
+ return false;
795
+ }
796
+ };
797
+ exports.writeRemoteMarker = writeRemoteMarker;
798
+ const defaultPaneCwd = (session) => readPaneInfo(session).currentPath;
775
799
  const tryInject = (session, text, deps) => {
776
800
  if (!text) {
777
801
  log(`! ${session}: empty text — drop`);
@@ -782,6 +806,11 @@ const tryInject = (session, text, deps) => {
782
806
  const ok = (deps.inject ?? injectKeys)(session, text);
783
807
  const preview = text.length > 60 ? text.slice(0, 60) + '…' : text;
784
808
  log(`${ok ? '→' : '✗'} ${session}: ${preview}`);
809
+ if (ok) {
810
+ const cwd = (deps.paneCwd ?? defaultPaneCwd)(session);
811
+ if (cwd)
812
+ (0, exports.writeRemoteMarker)(cwd, text);
813
+ }
785
814
  return ok;
786
815
  };
787
816
  /** Key-event sibling of tryInject: same pane/shell/rate guards, but sends
@@ -990,11 +1019,16 @@ exports.computeListenerDeviceId = computeListenerDeviceId;
990
1019
  */
991
1020
  const streamSession = (wsUrl, apiKey) => {
992
1021
  let ws = null;
1022
+ let opened = false;
993
1023
  const done = new Promise((resolve) => {
994
1024
  // deviceId + listenerNickname let the backend attach the connection
995
1025
  // to a DeviceRecord (auto-created on first connect for apiKey auth).
996
1026
  // Without these the `listener.sessions` reports are silently dropped
997
1027
  // server-side and the phone's picker stays empty.
1028
+ // KNOWN TRADEOFF: apiKey rides the query string, so it can land in
1029
+ // API Gateway access logs. The $connect route can't read custom
1030
+ // headers from every WS client; moving to first-message auth needs a
1031
+ // server-side change (tracked upstream).
998
1032
  const deviceId = (0, exports.computeListenerDeviceId)();
999
1033
  const nickname = (0, os_1.hostname)() || 'listener';
1000
1034
  const params = new URLSearchParams({
@@ -1099,6 +1133,7 @@ const streamSession = (wsUrl, apiKey) => {
1099
1133
  clearTimeout(connectTimer);
1100
1134
  connectTimer = null;
1101
1135
  }
1136
+ opened = true;
1102
1137
  lastRoundTripAt = Date.now();
1103
1138
  log('connected');
1104
1139
  // Initial inventory so the phone's picker has something to
@@ -1183,7 +1218,7 @@ const streamSession = (wsUrl, apiKey) => {
1183
1218
  });
1184
1219
  sock.on('close', (code, reasonBuf) => {
1185
1220
  cleanup();
1186
- resolve({ closeCode: code, reason: reasonBuf?.toString('utf-8') ?? '' });
1221
+ resolve({ closeCode: code, reason: reasonBuf?.toString('utf-8') ?? '', connected: opened });
1187
1222
  });
1188
1223
  });
1189
1224
  return {
@@ -1312,6 +1347,11 @@ const handleListener = async (args) => {
1312
1347
  rulesTimer.unref();
1313
1348
  let shuttingDown = false;
1314
1349
  let activeHandle = null;
1350
+ // Resolved by stop() so the reconnect backoff sleep below can be
1351
+ // interrupted — otherwise a SIGINT during the (up to 30s) backoff waits
1352
+ // out the full delay before the loop notices shuttingDown.
1353
+ let notifyStop = () => undefined;
1354
+ const stopped = new Promise((resolve) => { notifyStop = resolve; });
1315
1355
  const stop = (sig) => {
1316
1356
  if (shuttingDown)
1317
1357
  return;
@@ -1320,6 +1360,7 @@ const handleListener = async (args) => {
1320
1360
  // Force-close any open WS so the streamSession promise resolves
1321
1361
  // immediately instead of waiting for the server to drop us.
1322
1362
  activeHandle?.terminate();
1363
+ notifyStop();
1323
1364
  };
1324
1365
  process.on('SIGINT', () => stop('SIGINT'));
1325
1366
  process.on('SIGTERM', () => stop('SIGTERM'));
@@ -1335,9 +1376,14 @@ const handleListener = async (args) => {
1335
1376
  }
1336
1377
  if (shuttingDown)
1337
1378
  break;
1379
+ // A session that actually connected resets the backoff — otherwise a
1380
+ // long-lived daemon on a flaky link ratchets up to the 30s ceiling
1381
+ // permanently, even when every reconnect succeeds instantly.
1382
+ if (result.connected)
1383
+ attempt = 0;
1338
1384
  const delay = (0, exports.computeBackoff)(attempt);
1339
1385
  log(`disconnected (code=${result.closeCode}) — reconnect in ${Math.round(delay / 1000)}s`);
1340
- await sleep(delay);
1386
+ await Promise.race([sleep(delay), stopped]);
1341
1387
  attempt = Math.min(attempt + 1, 10);
1342
1388
  }
1343
1389
  removeListenerPid();