@zeph-to/cli 1.23.0 → 1.25.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
@@ -43,6 +43,12 @@ on *every* notification. For notifications only, with no phone control,
43
43
  `npx @zeph-to/cli install` is a lighter alternative that skips the global
44
44
  binary.
45
45
 
46
+ Once installed, the hooks fire in **every** session of each configured
47
+ agent — `zeph cc` is the phone-control bridge, not the notification
48
+ switch. Turn the volume down without uninstalling: `/zeph-quiet --global`
49
+ (blockers only, all projects), `/zeph-mute` (silence, current project),
50
+ `/zeph-status` (what's in effect). See [Mute & push mode](#mute--push-mode).
51
+
46
52
  `~/.zeph/config.json` is the single source of truth — the CLI, the MCP
47
53
  server, the plugin hooks, and the listener all read it. You never need
48
54
  `ZEPH_API_KEY`-style env vars for a normal setup; they exist as overrides
@@ -363,7 +369,7 @@ zeph notify --title "Hello" --json
363
369
  | `--priority <p>` | Priority: `low`, `normal`, `high`, `urgent` |
364
370
  | `--device <id>` | Target device ID |
365
371
  | `--session <id>` | AI session ID so the push threads into that session's chat (or `ZEPH_SESSION_ID` env) |
366
- | `--auto` | Apply the push gate before sending — honors the `/zeph-quiet` / `/zeph-loud` push-mode dial; gated-out exits silently with code 0 |
372
+ | `--auto` | Apply the push gate before sending — honors the `/zeph-quiet` / `/zeph-loud` push-mode dial, per project or machine-wide (`--global`); gated-out exits silently with code 0 |
367
373
  | `--marker <m>` | Push Signal marker for `--auto`: `skip`, `push`, `high` |
368
374
  | `--tools <n>`, `--nonreadonly <n>` | Turn tool counts feeding `--auto`'s heuristic (defaults assume real work) |
369
375
 
@@ -401,23 +407,45 @@ code 3 instead of looping forever — fix the key and restart.
401
407
  | `--json` | Output JSON format |
402
408
  | `--version` | Print version |
403
409
 
404
- ### Mute
410
+ ### Mute & push mode
405
411
 
406
- Mute is project-scoped (uses project directory hash). Created by Claude
407
- Code `/zeph-mute` command.
412
+ Both live as state files under `${XDG_STATE_HOME:-~/.local/state}/zeph`,
413
+ keyed by a `cksum` hash of the project directory. Claude Code's
414
+ `/zeph-mute` / `/zeph-quiet` / `/zeph-loud` / `/zeph-normal` write them;
415
+ the CLI reads them (mute on every `notify`, push mode on `--auto`).
408
416
 
409
417
  Notifications are silently skipped when a mute file exists for the
410
418
  current project:
411
419
 
412
420
  ```bash
413
- # Mute (created by /zeph-mute in Claude Code plugin)
414
- HASH=$(echo -n "$PROJECT_DIR" | cksum | cut -d' ' -f1)
415
- touch /tmp/zeph-muted-$HASH
421
+ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/zeph"
422
+ HASH=$(printf '%s' "$PROJECT_DIR" | cksum | cut -d' ' -f1)
423
+
424
+ # Mute (created by /zeph-mute in the Claude Code plugin)
425
+ mkdir -p "$STATE_DIR" && touch "$STATE_DIR/muted-$HASH"
416
426
 
417
427
  # Unmute
418
- rm /tmp/zeph-muted-$HASH
428
+ rm -f "$STATE_DIR/muted-$HASH"
419
429
  ```
420
430
 
431
+ Push mode is a one-word file (`quiet` / `loud` / `normal`) resolved in
432
+ this order — first hit wins:
433
+
434
+ | Order | File | Set by |
435
+ |-------|------|--------|
436
+ | 1 | `$STATE_DIR/pushmode-<hash>` | `/zeph-quiet` · `/zeph-loud` · `/zeph-normal` |
437
+ | 2 | `/tmp/zeph-pushmode-<hash>` | older versions (honored only when you own the file) |
438
+ | 3 | `$STATE_DIR/pushmode-default` | `/zeph-quiet --global` — the machine-wide default |
439
+ | 4 | *(none)* | `normal` |
440
+
441
+ So `/zeph-quiet --global` quiets every project that has no dial of its
442
+ own, and a per-project dial always overrides it. Mute has no `-default`
443
+ form on purpose: it is keyed on presence, not content, so a global mute
444
+ could never be lifted for a single project.
445
+
446
+ Legacy `/tmp/zeph-muted-<hash>` files are still honored when owned by the
447
+ current user (the state dir moved out of world-writable `/tmp`).
448
+
421
449
  The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`,
422
450
  `WINDSURF_PROJECT_DIR`, and falls back to `cwd`.
423
451
 
package/dist/crypto.d.ts CHANGED
@@ -27,6 +27,12 @@
27
27
  * key). That refactor is on the roadmap; until then, treat push bodies as
28
28
  * sensitive-but-not-secret.
29
29
  */
30
+ interface EncryptedPayload {
31
+ ciphertext: string;
32
+ iv: string;
33
+ encryptedKey: string;
34
+ keyIv: string;
35
+ }
30
36
  /**
31
37
  * Initialize crypto: sync keys with server, then fallback to local/generate.
32
38
  * Server is source of truth for per-user key pair.
@@ -80,4 +86,25 @@ export declare const encryptFileForSelf: (content: string) => Promise<{
80
86
  iv: string;
81
87
  encryptedKey: string;
82
88
  }>;
89
+ /**
90
+ * Flat ephemeral envelope — field-for-field what the web's decrypt()
91
+ * (libs/crypto) consumes, with the sender public key riding along so the
92
+ * receiver needs no out-of-band key lookup.
93
+ */
94
+ export interface EncryptedEphemeralPayload extends EncryptedPayload {
95
+ senderPublicKey: string;
96
+ }
97
+ /**
98
+ * Load-or-create the per-device keypair. Local-only — no server round-trip,
99
+ * no upload. Safe to call concurrently (dedupes to one init). Returns the
100
+ * exported public key (Base64 SPKI).
101
+ */
102
+ export declare const initDeviceCrypto: () => Promise<string>;
103
+ export declare const getDevicePublicKey: () => string | null;
104
+ /**
105
+ * Encrypt an ephemeral payload (e.g. a stream frame) for one recipient
106
+ * device. Requires initDeviceCrypto() to have completed.
107
+ */
108
+ export declare const encryptEphemeral: (plaintext: string, recipientPublicKeyRaw: string) => Promise<EncryptedEphemeralPayload>;
109
+ export {};
83
110
  //# sourceMappingURL=crypto.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+EH,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA6ED;;;;;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;AAWF;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,eAAe,EAAE,MAAM,CAAC;CACzB;AAsBD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,OAAO,CAAC,MAAM,CAoBjD,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAA+B,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,uBAAuB,MAAM,KAC5B,OAAO,CAAC,yBAAyB,CAKnC,CAAC"}
package/dist/crypto.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * sensitive-but-not-secret.
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
32
+ exports.encryptEphemeral = exports.getDevicePublicKey = exports.initDeviceCrypto = exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
33
33
  /// <reference lib="dom" />
34
34
  const fs_1 = require("fs");
35
35
  const os_1 = require("os");
@@ -301,3 +301,62 @@ const encryptFileForSelf = async (content) => {
301
301
  };
302
302
  };
303
303
  exports.encryptFileForSelf = encryptFileForSelf;
304
+ const DEVICE_KEYS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
305
+ const DEVICE_KEYS_PATH = (0, path_1.join)(DEVICE_KEYS_DIR, 'device-keys.json');
306
+ let deviceKeyPair = null;
307
+ let deviceExportedPublicKey = null;
308
+ let deviceInitPromise = null;
309
+ const loadStoredDeviceKeys = () => {
310
+ try {
311
+ return JSON.parse((0, fs_1.readFileSync)(DEVICE_KEYS_PATH, 'utf-8'));
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ };
317
+ const storeDeviceKeys = (exported) => {
318
+ (0, fs_1.mkdirSync)(DEVICE_KEYS_DIR, { recursive: true, mode: 0o700 });
319
+ (0, fs_1.writeFileSync)(DEVICE_KEYS_PATH, JSON.stringify(exported, null, 2), { mode: 0o600 });
320
+ };
321
+ /**
322
+ * Load-or-create the per-device keypair. Local-only — no server round-trip,
323
+ * no upload. Safe to call concurrently (dedupes to one init). Returns the
324
+ * exported public key (Base64 SPKI).
325
+ */
326
+ const initDeviceCrypto = () => {
327
+ if (deviceInitPromise)
328
+ return deviceInitPromise;
329
+ deviceInitPromise = (async () => {
330
+ const stored = loadStoredDeviceKeys();
331
+ if (stored) {
332
+ deviceKeyPair = await importKeyPair(stored);
333
+ deviceExportedPublicKey = stored.publicKey;
334
+ return stored.publicKey;
335
+ }
336
+ const keyPair = await generateKeyPair();
337
+ const exported = await exportKeyPair(keyPair);
338
+ storeDeviceKeys(exported);
339
+ deviceKeyPair = keyPair;
340
+ deviceExportedPublicKey = exported.publicKey;
341
+ return exported.publicKey;
342
+ })().catch((err) => {
343
+ deviceInitPromise = null;
344
+ throw err;
345
+ });
346
+ return deviceInitPromise;
347
+ };
348
+ exports.initDeviceCrypto = initDeviceCrypto;
349
+ const getDevicePublicKey = () => deviceExportedPublicKey;
350
+ exports.getDevicePublicKey = getDevicePublicKey;
351
+ /**
352
+ * Encrypt an ephemeral payload (e.g. a stream frame) for one recipient
353
+ * device. Requires initDeviceCrypto() to have completed.
354
+ */
355
+ const encryptEphemeral = async (plaintext, recipientPublicKeyRaw) => {
356
+ if (!deviceKeyPair || !deviceExportedPublicKey)
357
+ throw new Error('Device crypto not initialized');
358
+ const recipientKey = await importPublicKey(recipientPublicKeyRaw);
359
+ const payload = await encrypt(plaintext, deviceKeyPair.privateKey, recipientKey);
360
+ return { ...payload, senderPublicKey: deviceExportedPublicKey };
361
+ };
362
+ exports.encryptEphemeral = encryptEphemeral;
@@ -1 +1 @@
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"}
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;AA2BjF,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
@@ -78,13 +78,23 @@ const ownedByCurrentUser = (path) => {
78
78
  return false;
79
79
  }
80
80
  };
81
- /** Resolve a state file: current location first, then user-owned legacy /tmp. */
81
+ /**
82
+ * Resolve a state file: current location first, then user-owned legacy /tmp,
83
+ * then — for `pushmode` only — the machine-wide default written by
84
+ * `/zeph-quiet --global`. Mirrors plugin/hooks/gate.sh zeph_state_present,
85
+ * including the deliberate absence of a global mute (see the comment there).
86
+ */
82
87
  const findStateFile = (kind, hash) => {
83
88
  const current = (0, path_1.join)((0, exports.stateDir)(), `${kind}-${hash}`);
84
89
  if ((0, fs_1.existsSync)(current))
85
90
  return current;
86
91
  const legacy = `/tmp/zeph-${kind}-${hash}`;
87
- return ownedByCurrentUser(legacy) ? legacy : null;
92
+ if (ownedByCurrentUser(legacy))
93
+ return legacy;
94
+ if (kind !== 'pushmode')
95
+ return null;
96
+ const globalDefault = (0, path_1.join)((0, exports.stateDir)(), 'pushmode-default');
97
+ return (0, fs_1.existsSync)(globalDefault) ? globalDefault : null;
88
98
  };
89
99
  const projectHash = (dir) => {
90
100
  try {
@@ -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;AA4BzC;;;GAGG;AACH;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,GAAG,SAAS,EAAE,SAAS,OAAO,KAAG,OACxD,CAAC;AAoBzB;6EAC6E;AAC7E,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAqBhF,CAAC;AAgQF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA8EF,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;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,GAAG,SAAS,EAAE,SAAS,OAAO,KAAG,OACxD,CAAC;AAoBzB;6EAC6E;AAC7E,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAqBhF,CAAC;AAgQF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA8EF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA4G1F,CAAC"}
package/dist/installer.js CHANGED
@@ -485,6 +485,15 @@ const handleInstall = async (args) => {
485
485
  console.log('\n Testing connection...');
486
486
  await testConnection(apiKey, baseUrl);
487
487
  console.log('\n Done! Restart your agents.\n');
488
+ // The single most-reported surprise: pushes are not scoped to `zeph cc`.
489
+ // Hooks fire for every session of every configured agent, so say it here —
490
+ // at the moment the hooks get installed — together with the volume dials.
491
+ console.log(' Notifications now fire for EVERY session of each agent above');
492
+ console.log(' (not only sessions launched with `zeph cc`). Dial the volume');
493
+ console.log(' any time — in Claude Code:');
494
+ console.log(' /zeph-quiet --global only blockers + high-priority, all projects');
495
+ console.log(' /zeph-mute full silence, current project');
496
+ console.log(' /zeph-status show what is in effect\n');
488
497
  return 0;
489
498
  };
490
499
  exports.handleInstall = handleInstall;
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { type AgentKind, type RegisteredRemoteAgent } from './remote-agents.js';
24
24
  import { type AgentState } from './agent-state.js';
25
+ import { type EncryptedEphemeralPayload } from './crypto.js';
25
26
  export declare const SESSION_REPORT_HEARTBEAT_MS = 30000;
26
27
  interface AgentSession {
27
28
  name: string;
@@ -167,6 +168,59 @@ export declare const handleScreenRequest: (req: ScreenRequest) => ScreenSnapshot
167
168
  * captures so a burst of arrow presses yields frames after the LAST press.
168
169
  */
169
170
  export declare const scheduleInjectSnapshots: (sessionName: string, send: (data: Record<string, unknown>) => void, delays?: number[]) => void;
171
+ export interface StreamControl {
172
+ subtype?: string;
173
+ targetDeviceId?: string;
174
+ sessionName?: string;
175
+ /** Subscriber's device public key (Base64 SPKI) — presence turns on E2EE. */
176
+ subscriberPublicKey?: string;
177
+ }
178
+ export declare const MAX_CONCURRENT_STREAMS = 3;
179
+ export declare const stopStream: (sessionName: string) => void;
180
+ export declare const stopAllStreams: () => void;
181
+ /** Wire shape of one live-mirror data frame (before the relay's ephemeral wrap). */
182
+ export type StreamFramePayload = {
183
+ subtype: 'agent.stream.frame';
184
+ sessionName: string;
185
+ capturedAt: string;
186
+ truncated: boolean;
187
+ /** Capture-order stamp — attached at send time, not by buildStreamFrame. */
188
+ seq?: number;
189
+ /** Stream incarnation (stats.startedAt) — lets the receiver reset its
190
+ * seq high-water mark when the daemon restarts the stream. */
191
+ epoch?: number;
192
+ /** Plaintext pane content — only on unencrypted streams. */
193
+ content?: string;
194
+ /** E2EE envelope — replaces `content` on encrypted streams. */
195
+ encrypted?: EncryptedEphemeralPayload;
196
+ };
197
+ /** Wire shape of a live-mirror error frame — same subtype, no pane data. */
198
+ export type StreamErrorFrame = {
199
+ subtype: 'agent.stream.frame';
200
+ sessionName: string;
201
+ error: 'unknown_session' | 'e2ee_unavailable' | 'encrypt_failed' | 'stream_limit';
202
+ };
203
+ /**
204
+ * Per-listener concurrency guard, as a pure predicate so the boundary is
205
+ * unit-testable without touching the module-scope `activeStreams` map.
206
+ */
207
+ export declare const isStreamCapReached: (activeCount: number) => boolean;
208
+ /**
209
+ * Build the wire payload for one stream frame. With a subscriber public key
210
+ * the pane content rides ONLY inside the E2EE envelope; an encrypt failure
211
+ * (bad key, crypto not ready on the first tick) returns null so the caller
212
+ * drops the frame — an encrypted stream never leaks a plaintext frame.
213
+ */
214
+ export declare const buildStreamFrame: (captured: {
215
+ content: string;
216
+ truncated: boolean;
217
+ }, sessionName: string, subscriberPublicKey?: string) => Promise<StreamFramePayload | null>;
218
+ /**
219
+ * Handle agent.stream.start / agent.stream.stop. Returns true when the message
220
+ * was a stream-control message (so the caller skips the one-shot screen-peek
221
+ * path). start is idempotent — a repeat restarts the loop.
222
+ */
223
+ export declare const handleStreamControl: (req: StreamControl, send: (data: Record<string, unknown>) => void) => boolean;
170
224
  export interface CollectResult {
171
225
  sessions: AgentSession[];
172
226
  /** Diagnostic notes per rejected session — surfaced under `--verbose`. */
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAUH,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzG,OAAO,EAAiD,KAAK,UAAU,EAA4C,MAAM,kBAAkB,CAAC;AAE5I,OAAO,EAA0D,KAAK,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAmCrH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,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;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,YAAY,EAAE,KAAG,MAS7C,CAAC;AAEnB;6BAC6B;AAC7B,eAAO,MAAM,iBAAiB,GAC1B,aAAa,MAAM,EACnB,qBAAqB,MAAM,GAAG,IAAI,EAClC,cAAc,MAAM,EACpB,OAAO,MAAM,KACd,OAEoD,CAAC;AAYxD,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAenF,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;AAsCF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,KAAG,MAAM,EAAE,GAAG,IAQvD,CAAC;AA0CF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAG5C,CAAC;AA8NF;;;;;;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;AAEF,UAAU,QAAQ;IACd,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAiDD;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,QAAQ,KAAG,qBAAqB,GAAG,IAGhE,CAAC;AA0BZ,iBAAiB;AACjB,eAAO,MAAM,kBAAkB,QAAO,IAErC,CAAC;AAWF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC3B,MAAM,MAAM,EACZ,WAAW,SAAS,EACpB,UAAU,MAAM,GAAG,IAAI,EACvB,MAAK,MAAmB,KACzB,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,gBAAgB,GAAG,aAAa,CAmB/D,CAAC;AAWF,MAAM,WAAW,YAAY;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACnB;AASD,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,GAAI,KAAK,OAAO,KAAG,IAWhD,CAAC;AAEF,iBAAiB;AACjB,eAAO,MAAM,mBAAmB,QAAO,IAGtC,CAAC;AAEF,MAAM,WAAW,QAAQ;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GACzB,WAAW,WAAW,CAAC,MAAM,CAAC,EAC9B,UAAS,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAsB,KAC9D,QAAQ,EAgBV,CAAC;AAWF,MAAM,WAAW,aAAa;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,uBAAuB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,aAAa,KAAG,cAAc,GAAG,IAmBzE,CAAC;AAyCF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,GAChC,aAAa,MAAM,EACnB,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAC7C,SAAQ,MAAM,EAA8B,KAC7C,IAiBF,CAAC;AAcF,MAAM,WAAW,aAAa;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAChC;AAmBD,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAyCxC,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM,KAAG,IAYhD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAGjC,CAAC;AAEF,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;mEAC+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,cAAc,CAAC;CACrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,aAAa,MAAM,KAAG,OAChB,CAAC;AAQ1C;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GACzB,UAAU;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,EACjD,aAAa,MAAM,EACnB,sBAAsB,MAAM,KAC7B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAenC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC5B,KAAK,aAAa,EAClB,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,KAC9C,OA0GF,CAAC;AAEF,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,aAiEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,QAAO,YAAY,EAAuC,CAAC;AAIvF;;;;;GAKG;AACH,UAAU,kBAAkB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,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;IAC1B;sEACkE;IAClE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;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,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;IAC1D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzF,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC7C;sEACkE;IAClE,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAwBD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC1B,SAAS,MAAM,EACf,MAAM,MAAM,EACZ,MAAK,MAAM,MAAiB,KAC7B,OAUF,CAAC;AAwCF,eAAO,MAAM,oBAAoB,GAAI,KAAK;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAG,IAE/E,CAAC;AA2FF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACtB,MAAK,MAAmB,EACxB,MAAK,MAAwB,EAC7B,MAAK,MAA0B,KAChC,MAaF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,QAAQ,EACd,OAAM,cAAmB,KAC1B,OAAO,CAAC,OAAO,CAoCjB,CAAC;AAcF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAIhD,CAAC;AA4DF,eAAO,MAAM,uBAAuB,GAAI,OAAO,MAAM,KAAG,MA2BvD,CAAC;AAgPF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GACrB,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EACtC,QAAQ;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1B,SAAS,MAAM,KAChB,MAAM,GAAG,IAIX,CAAC;AA6CF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA+H3F,CAAC"}
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.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.scheduleInjectSnapshots = 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 = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
28
+ exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.scheduleInjectSnapshots = 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 = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = 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");
@@ -37,6 +37,7 @@ const gate_js_1 = require("./gate.js");
37
37
  const remote_agents_js_1 = require("./remote-agents.js");
38
38
  const agent_state_js_1 = require("./agent-state.js");
39
39
  const agent_rules_fetch_js_1 = require("./agent-rules-fetch.js");
40
+ const crypto_js_1 = require("./crypto.js");
40
41
  const PING_INTERVAL_MS = 25_000;
41
42
  const PONG_TIMEOUT_MS = 10_000;
42
43
  const RECONNECT_BASE_MS = 1_000;
@@ -164,6 +165,11 @@ const ALLOWED_KEYS = {
164
165
  left: 'Left',
165
166
  right: 'Right',
166
167
  enter: 'Enter',
168
+ tab: 'Tab',
169
+ backtab: 'BTab',
170
+ backspace: 'BSpace',
171
+ delete: 'DC',
172
+ space: 'Space',
167
173
  };
168
174
  /**
169
175
  * Translate a phone-supplied key list to tmux tokens. Returns null if ANY
@@ -692,8 +698,16 @@ const handleScreenRequest = (req) => {
692
698
  exports.handleScreenRequest = handleScreenRequest;
693
699
  /** Raw pane capture + size cap. Shared by the request path and the
694
700
  * post-injection auto-snapshot. */
695
- const capturePane = (sessionName) => {
696
- const r = (0, child_process_1.spawnSync)('tmux', tmuxArgs(['capture-pane', '-p', '-t', sessionName, '-S', `-${SCREEN_PEEK_LINES}`]), {
701
+ const capturePane = (sessionName, escapes = false, lines = SCREEN_PEEK_LINES) => {
702
+ // `-e` keeps ANSI/color escapes for the xterm.js live stream. The
703
+ // screen-peek path leaves it off so its <pre> renderer stays plain text.
704
+ // `lines` sets how far back the capture reaches — the live stream grabs
705
+ // more history so the mirror has room to scroll; SCREEN_PEEK_MAX_BYTES
706
+ // still caps the payload below.
707
+ const captureArgs = escapes
708
+ ? ['capture-pane', '-p', '-e', '-t', sessionName, '-S', `-${lines}`]
709
+ : ['capture-pane', '-p', '-t', sessionName, '-S', `-${lines}`];
710
+ const r = (0, child_process_1.spawnSync)('tmux', tmuxArgs(captureArgs), {
697
711
  encoding: 'utf-8',
698
712
  stdio: ['ignore', 'pipe', 'pipe'],
699
713
  });
@@ -743,6 +757,221 @@ const scheduleInjectSnapshots = (sessionName, send, delays = INJECT_SNAPSHOT_DEL
743
757
  pendingInjectSnapshots.set(sessionName, timers);
744
758
  };
745
759
  exports.scheduleInjectSnapshots = scheduleInjectSnapshots;
760
+ // ~2.5 fps ceiling. Cadence + diff-gating are the ONLY bound on API Gateway
761
+ // WS cost (the $default route has no per-message limit), so keep it modest
762
+ // and let unchanged frames drop.
763
+ // How far back the live-stream capture reaches — more than the screen-peek
764
+ // window so the mirror has scrollback to scroll through. SCREEN_PEEK_MAX_BYTES
765
+ // still caps the actual payload, so very wide panes get top-truncated.
766
+ const STREAM_CAPTURE_LINES = 200;
767
+ const STREAM_INTERVAL_MS = 400;
768
+ // Orphan guard: a phone that dies without sending agent.stream.stop must not
769
+ // leak an interval forever. Auto-stop after this long; the phone re-subscribes
770
+ // on reopen.
771
+ const STREAM_MAX_MS = 5 * 60_000;
772
+ // Per-listener concurrency guard: the API Gateway WS stage throttle (50 rps)
773
+ // is SHARED across every user, so a runaway machine (many sessions, or a
774
+ // client re-subscribe bug) streaming at 2.5 fps each can starve push delivery
775
+ // and presence for everyone. One phone watches one session at a time; 3 leaves
776
+ // headroom for multiple devices while capping the blast radius of one host.
777
+ exports.MAX_CONCURRENT_STREAMS = 3;
778
+ // Fail-closed guard: consecutive encrypt failures (malformed subscriber key)
779
+ // stop the stream with an error frame instead of retrying forever.
780
+ const STREAM_MAX_ENCRYPT_FAILURES = 3;
781
+ // R2 instrumentation: the daemon's outbound send() IS the API Gateway $default
782
+ // inbound message (1 Lambda + DDB ops + N PostToConnection each), so counting
783
+ // frames/bytes HERE measures the real cloud cost of a live stream. Rolled up to
784
+ // the log every STREAM_LOG_INTERVAL_MS and summarized on stop — the data that
785
+ // decides cloud-throttled (B-lite) vs direct/Tailscale (B-full).
786
+ const STREAM_LOG_INTERVAL_MS = 5_000;
787
+ const activeStreams = new Map();
788
+ const maybeLogStreamStats = (sessionName, stats) => {
789
+ const elapsed = Date.now() - stats.lastLogAt;
790
+ if (elapsed < STREAM_LOG_INTERVAL_MS)
791
+ return;
792
+ const secs = elapsed / 1000;
793
+ const fps = (stats.frames - stats.lastLogFrames) / secs;
794
+ const kbps = (stats.bytes - stats.lastLogBytes) / 1024 / secs;
795
+ log(`⧉ stream ${sessionName}: ${fps.toFixed(1)} fps, ${kbps.toFixed(1)} KB/s (${stats.frames} sent, ${stats.skipped} diff-skipped)`);
796
+ stats.lastLogAt = Date.now();
797
+ stats.lastLogFrames = stats.frames;
798
+ stats.lastLogBytes = stats.bytes;
799
+ };
800
+ const stopStream = (sessionName) => {
801
+ const entry = activeStreams.get(sessionName);
802
+ if (!entry)
803
+ return;
804
+ clearInterval(entry.timer);
805
+ activeStreams.delete(sessionName);
806
+ const { stats } = entry;
807
+ const secs = Math.max(0.001, (Date.now() - stats.startedAt) / 1000);
808
+ log(`⧉ stream ${sessionName} stopped: ${stats.frames} frames / ` +
809
+ `${(stats.bytes / 1024).toFixed(1)} KB over ${secs.toFixed(1)}s ` +
810
+ `(${(stats.frames / secs).toFixed(1)} fps avg, ${stats.skipped} diff-skipped)`);
811
+ };
812
+ exports.stopStream = stopStream;
813
+ const stopAllStreams = () => {
814
+ // Route through stopStream so each stream emits its summary line.
815
+ for (const sessionName of [...activeStreams.keys()])
816
+ (0, exports.stopStream)(sessionName);
817
+ };
818
+ exports.stopAllStreams = stopAllStreams;
819
+ /**
820
+ * Per-listener concurrency guard, as a pure predicate so the boundary is
821
+ * unit-testable without touching the module-scope `activeStreams` map.
822
+ */
823
+ const isStreamCapReached = (activeCount) => activeCount >= exports.MAX_CONCURRENT_STREAMS;
824
+ exports.isStreamCapReached = isStreamCapReached;
825
+ const streamErrorFrame = (sessionName, error) => ({
826
+ subtype: 'agent.stream.frame',
827
+ sessionName,
828
+ error,
829
+ });
830
+ /**
831
+ * Build the wire payload for one stream frame. With a subscriber public key
832
+ * the pane content rides ONLY inside the E2EE envelope; an encrypt failure
833
+ * (bad key, crypto not ready on the first tick) returns null so the caller
834
+ * drops the frame — an encrypted stream never leaks a plaintext frame.
835
+ */
836
+ const buildStreamFrame = async (captured, sessionName, subscriberPublicKey) => {
837
+ const base = {
838
+ subtype: 'agent.stream.frame',
839
+ sessionName,
840
+ capturedAt: new Date().toISOString(),
841
+ truncated: captured.truncated,
842
+ };
843
+ if (!subscriberPublicKey)
844
+ return { ...base, content: captured.content };
845
+ try {
846
+ const encrypted = await (0, crypto_js_1.encryptEphemeral)(captured.content, subscriberPublicKey);
847
+ return { ...base, encrypted };
848
+ }
849
+ catch (err) {
850
+ log(`⧉ stream ${sessionName}: frame encrypt failed (${err instanceof Error ? err.message : err}) — frame dropped`);
851
+ return null;
852
+ }
853
+ };
854
+ exports.buildStreamFrame = buildStreamFrame;
855
+ /**
856
+ * Handle agent.stream.start / agent.stream.stop. Returns true when the message
857
+ * was a stream-control message (so the caller skips the one-shot screen-peek
858
+ * path). start is idempotent — a repeat restarts the loop.
859
+ */
860
+ const handleStreamControl = (req, send) => {
861
+ if (req.subtype === 'agent.stream.stop') {
862
+ if (req.sessionName)
863
+ (0, exports.stopStream)(req.sessionName);
864
+ return true;
865
+ }
866
+ if (req.subtype !== 'agent.stream.start')
867
+ return false;
868
+ // Not addressed to this machine — let other listeners answer.
869
+ if (req.targetDeviceId !== (0, exports.computeListenerDeviceId)())
870
+ return false;
871
+ const sessionName = req.sessionName;
872
+ if (!sessionName)
873
+ return true;
874
+ if (!(0, exports.collectSessions)().some((s) => s.name === sessionName)) {
875
+ send(streamErrorFrame(sessionName, 'unknown_session'));
876
+ return true;
877
+ }
878
+ (0, exports.stopStream)(sessionName); // idempotent restart (frees this session's slot first)
879
+ // Concurrency guard AFTER the restart-stop: re-subscribing to an already
880
+ // active session doesn't count against the cap, only a genuinely new one
881
+ // does. Refuse the new stream instead of adding to the shared-throttle load.
882
+ if ((0, exports.isStreamCapReached)(activeStreams.size)) {
883
+ log(`⧉ stream ${sessionName}: refused — ${activeStreams.size}/${exports.MAX_CONCURRENT_STREAMS} streams already active on this listener`);
884
+ send(streamErrorFrame(sessionName, 'stream_limit'));
885
+ return true;
886
+ }
887
+ let lastContent = null;
888
+ const stats = {
889
+ startedAt: Date.now(),
890
+ frames: 0,
891
+ bytes: 0,
892
+ skipped: 0,
893
+ lastLogAt: Date.now(),
894
+ lastLogFrames: 0,
895
+ lastLogBytes: 0,
896
+ };
897
+ // E2EE handshake: load-or-create this device's keypair up front. The
898
+ // subscriber asked for encryption, so key failure is FAIL-CLOSED: refuse
899
+ // to stream rather than downgrade to plaintext — a silent downgrade is
900
+ // exactly the signal a key-stripping relay would produce. The phone sees
901
+ // the error frame and can choose to re-subscribe without a key.
902
+ const subscriberPublicKey = req.subscriberPublicKey;
903
+ if (subscriberPublicKey) {
904
+ (0, crypto_js_1.initDeviceCrypto)().catch((err) => {
905
+ // Stream stopped or replaced while init was in flight — the
906
+ // failure belongs to the old incarnation, not the current one.
907
+ if (activeStreams.get(sessionName)?.stats !== stats)
908
+ return;
909
+ log(`⧉ stream ${sessionName}: device crypto init failed (${err instanceof Error ? err.message : err}) — refusing to stream (fail-closed)`);
910
+ send(streamErrorFrame(sessionName, 'e2ee_unavailable'));
911
+ (0, exports.stopStream)(sessionName);
912
+ });
913
+ }
914
+ let wireSeq = 0;
915
+ let encryptFailures = 0;
916
+ const timer = setInterval(() => {
917
+ const captured = capturePane(sessionName, true, STREAM_CAPTURE_LINES);
918
+ if (!captured || captured.content === lastContent) {
919
+ stats.skipped++;
920
+ return; // diff-gate
921
+ }
922
+ lastContent = captured.content;
923
+ // Stamp the sequence in CAPTURE order, synchronously — frame assembly
924
+ // is async (encryption) and fire-and-forget, so resolve order is not
925
+ // guaranteed under load; the receiver drops any seq it has already
926
+ // painted past.
927
+ const seq = ++wireSeq;
928
+ void (0, exports.buildStreamFrame)(captured, sessionName, subscriberPublicKey).then((frame) => {
929
+ // Stream stopped or restarted while this frame was in flight —
930
+ // `stats` is unique per start, so it doubles as the identity
931
+ // token. Don't send into a dead/replaced stream or skew its stats.
932
+ if (activeStreams.get(sessionName)?.stats !== stats)
933
+ return;
934
+ if (!frame) {
935
+ // Encrypt failure — frame dropped, diff-gate un-marked so the
936
+ // next tick retries. A key that keeps failing (malformed
937
+ // subscriber key) never recovers: fail closed after a few
938
+ // strikes instead of retrying every 400ms for 5 minutes.
939
+ lastContent = null;
940
+ // Init still in flight (or failed — its own path fail-closes):
941
+ // a not-yet-ready key is not a malformed key, don't strike.
942
+ if ((0, crypto_js_1.getDevicePublicKey)() === null)
943
+ return;
944
+ encryptFailures++;
945
+ if (encryptFailures >= STREAM_MAX_ENCRYPT_FAILURES) {
946
+ send(streamErrorFrame(sessionName, 'encrypt_failed'));
947
+ (0, exports.stopStream)(sessionName);
948
+ }
949
+ return;
950
+ }
951
+ encryptFailures = 0;
952
+ stats.frames++;
953
+ // Measure the actual wire payload (the encrypted envelope is
954
+ // ~1.4× the plaintext; base fields now count too, so plaintext
955
+ // streams read slightly higher than the pre-E2EE content-only
956
+ // metric) — this feeds the R2 cost instrumentation.
957
+ stats.bytes += Buffer.byteLength(JSON.stringify(frame), 'utf-8');
958
+ // seq = capture order; epoch = this stream incarnation, so the
959
+ // receiver's ordering guard resets across daemon-side restarts.
960
+ send({ ...frame, seq, epoch: stats.startedAt });
961
+ maybeLogStreamStats(sessionName, stats);
962
+ }).catch((err) => {
963
+ // A long-lived daemon must never crash on an unhandled rejection
964
+ // from send()/logging — report and let the next tick carry on.
965
+ log(`⧉ stream ${sessionName}: frame send failed (${err instanceof Error ? err.message : err})`);
966
+ });
967
+ }, STREAM_INTERVAL_MS);
968
+ timer.unref?.();
969
+ activeStreams.set(sessionName, { timer, stats });
970
+ const expiry = setTimeout(() => (0, exports.stopStream)(sessionName), STREAM_MAX_MS);
971
+ expiry.unref?.();
972
+ return true;
973
+ };
974
+ exports.handleStreamControl = handleStreamControl;
746
975
  /**
747
976
  * Inventory pass that also records *why* each `zeph-*` session was
748
977
  * skipped. The verbose log uses the rejection notes to explain empty
@@ -1356,9 +1585,18 @@ const streamSession = (wsUrl, apiKey) => {
1356
1585
  // Screen peek (§S4): the phone asks for this machine's live
1357
1586
  // pane content over the ephemeral relay; the reply rides the
1358
1587
  // same channel. Nothing is persisted server-side.
1359
- const reply = (0, exports.handleScreenRequest)(m.data);
1360
- if (reply && sock.readyState === ws_1.default.OPEN) {
1361
- sock.send(JSON.stringify({ type: 'ephemeral', data: reply }));
1588
+ const sendEphemeral = (data) => {
1589
+ if (sock.readyState === ws_1.default.OPEN) {
1590
+ sock.send(JSON.stringify({ type: 'ephemeral', data }));
1591
+ }
1592
+ };
1593
+ // Live mirror (PoC): agent.stream.start/stop drives a
1594
+ // continuous, diff-gated frame loop; falls through to the
1595
+ // one-shot screen-peek when it isn't a stream-control message.
1596
+ if (!(0, exports.handleStreamControl)(m.data, sendEphemeral)) {
1597
+ const reply = (0, exports.handleScreenRequest)(m.data);
1598
+ if (reply)
1599
+ sendEphemeral(reply);
1362
1600
  }
1363
1601
  }
1364
1602
  // Surface server-side errors from listener.sessions reports.
@@ -1380,6 +1618,7 @@ const streamSession = (wsUrl, apiKey) => {
1380
1618
  log(`! ws error: ${err.message}`);
1381
1619
  });
1382
1620
  sock.on('close', (code, reasonBuf) => {
1621
+ (0, exports.stopAllStreams)();
1383
1622
  cleanup();
1384
1623
  resolve({ closeCode: code, reason: reasonBuf?.toString('utf-8') ?? '', connected: opened });
1385
1624
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "Zeph CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",