@zeph-to/cli 1.24.0 → 1.26.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
@@ -196,12 +202,22 @@ can never false-flag. Muted projects are never flagged.
196
202
 
197
203
  ### Diagnostics
198
204
 
199
- The auto-spawned listener writes to two files under `~/.zeph/`:
205
+ The auto-spawned listener writes to three files under `~/.zeph/`:
200
206
 
201
207
  - `listener.pid` — the running daemon's PID. `cat ~/.zeph/listener.pid`
202
208
  + `ps -p <pid>` to confirm it's alive.
209
+ - `listener.version` — the CLI version the daemon booted from. This is
210
+ what `zeph cc` compares against the installed package to spot a stale
211
+ daemon (see below).
203
212
  - `listener.log` — stdout + stderr from the daemon. `tail -f` to watch.
204
213
 
214
+ The daemon logs its version on the first line, which is the only
215
+ reliable way to tell which build a long-running process is on:
216
+
217
+ ```
218
+ [xx:xx:xx] zeph listener starting — v1.26.0 — wss://ws.zeph.to
219
+ ```
220
+
205
221
  A healthy listener log shows one line per cycle:
206
222
 
207
223
  ```
@@ -213,12 +229,31 @@ If you see `! server rejected listener.sessions: ...` instead, the
213
229
  message points at the failure (auth, missing device record, etc.) so
214
230
  you can fix the actual problem instead of guessing.
215
231
 
216
- To force a restart — e.g. after upgrading `@zeph-to/cli`:
232
+ To force a restart:
233
+
234
+ ```bash
235
+ zeph listener --restart
236
+ ```
237
+
238
+ **After upgrading `@zeph-to/cli` you normally don't have to.** `npm i -g`
239
+ replaces the package on disk but never the daemon already running from
240
+ the old build — that daemon keeps answering pushes (so agent chat looks
241
+ fine) while silently ignoring every message subtype added since it
242
+ booted. `zeph cc` compares `listener.version` against the installed
243
+ version and restarts the daemon for you when the installed one is newer
244
+ (a daemon *newer* than the `zeph cc` you ran is left alone, so several
245
+ installs on one machine don't fight over it):
246
+
247
+ ```
248
+ zeph: listener 1.25.0 is stale — restarting on 1.26.0
249
+ ```
250
+
251
+ If the PID file is missing (a different account started the daemon, or
252
+ it was removed by hand) the singleton guard can't see it. Find the real
253
+ process instead:
217
254
 
218
255
  ```bash
219
- kill $(cat ~/.zeph/listener.pid)
220
- rm ~/.zeph/listener.pid
221
- zeph cc # autospawns the new build
256
+ ps aux | grep '[c]li.js listener'
222
257
  ```
223
258
 
224
259
  To run it in the foreground (for development of the SDK itself):
@@ -363,7 +398,7 @@ zeph notify --title "Hello" --json
363
398
  | `--priority <p>` | Priority: `low`, `normal`, `high`, `urgent` |
364
399
  | `--device <id>` | Target device ID |
365
400
  | `--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 |
401
+ | `--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
402
  | `--marker <m>` | Push Signal marker for `--auto`: `skip`, `push`, `high` |
368
403
  | `--tools <n>`, `--nonreadonly <n>` | Turn tool counts feeding `--auto`'s heuristic (defaults assume real work) |
369
404
 
@@ -379,6 +414,8 @@ which project + branch finished without writing per-IDE wrappers. Pass
379
414
  | `--ws-url <url>` | WebSocket endpoint (or set `ZEPH_WS_URL` env, or `wsUrl` in `~/.zeph/config.json`) |
380
415
  | `--key <api-key>` | API key (or set `ZEPH_API_KEY` env) |
381
416
  | `--base-url <url>` | REST API base URL (or set `ZEPH_BASE_URL` env, or `baseUrl` in `~/.zeph/config.json`) |
417
+ | `--stop` | Stop the running daemon and clear its PID/version stamps |
418
+ | `--restart` | Stop it and relaunch detached (logs to `~/.zeph/listener.log`) |
382
419
 
383
420
  The listener reconnects with exponential backoff + jitter (1 s → 30 s
384
421
  cap). Heartbeat is ping every 25 s with a 10 s pong timeout. On an
@@ -401,23 +438,45 @@ code 3 instead of looping forever — fix the key and restart.
401
438
  | `--json` | Output JSON format |
402
439
  | `--version` | Print version |
403
440
 
404
- ### Mute
441
+ ### Mute & push mode
405
442
 
406
- Mute is project-scoped (uses project directory hash). Created by Claude
407
- Code `/zeph-mute` command.
443
+ Both live as state files under `${XDG_STATE_HOME:-~/.local/state}/zeph`,
444
+ keyed by a `cksum` hash of the project directory. Claude Code's
445
+ `/zeph-mute` / `/zeph-quiet` / `/zeph-loud` / `/zeph-normal` write them;
446
+ the CLI reads them (mute on every `notify`, push mode on `--auto`).
408
447
 
409
448
  Notifications are silently skipped when a mute file exists for the
410
449
  current project:
411
450
 
412
451
  ```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
452
+ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/zeph"
453
+ HASH=$(printf '%s' "$PROJECT_DIR" | cksum | cut -d' ' -f1)
454
+
455
+ # Mute (created by /zeph-mute in the Claude Code plugin)
456
+ mkdir -p "$STATE_DIR" && touch "$STATE_DIR/muted-$HASH"
416
457
 
417
458
  # Unmute
418
- rm /tmp/zeph-muted-$HASH
459
+ rm -f "$STATE_DIR/muted-$HASH"
419
460
  ```
420
461
 
462
+ Push mode is a one-word file (`quiet` / `loud` / `normal`) resolved in
463
+ this order — first hit wins:
464
+
465
+ | Order | File | Set by |
466
+ |-------|------|--------|
467
+ | 1 | `$STATE_DIR/pushmode-<hash>` | `/zeph-quiet` · `/zeph-loud` · `/zeph-normal` |
468
+ | 2 | `/tmp/zeph-pushmode-<hash>` | older versions (honored only when you own the file) |
469
+ | 3 | `$STATE_DIR/pushmode-default` | `/zeph-quiet --global` — the machine-wide default |
470
+ | 4 | *(none)* | `normal` |
471
+
472
+ So `/zeph-quiet --global` quiets every project that has no dial of its
473
+ own, and a per-project dial always overrides it. Mute has no `-default`
474
+ form on purpose: it is keyed on presence, not content, so a global mute
475
+ could never be lifted for a single project.
476
+
477
+ Legacy `/tmp/zeph-muted-<hash>` files are still honored when owned by the
478
+ current user (the state dir moved out of world-writable `/tmp`).
479
+
421
480
  The CLI checks `CLAUDE_PROJECT_DIR`, `CURSOR_PROJECT_DIR`,
422
481
  `WINDSURF_PROJECT_DIR`, and falls back to `cwd`.
423
482
 
package/dist/cli.js CHANGED
@@ -81,6 +81,10 @@ ${usageAgentLines()}
81
81
  listener Resident daemon — receives 'agent.command' pushes from
82
82
  the phone picker and injects them into the matching
83
83
  tmux session.
84
+ --stop stop the running daemon
85
+ --restart stop it and relaunch in the background
86
+ (needed after an upgrade: 'npm i -g' swaps
87
+ the package but never the live process)
84
88
 
85
89
  Notify options:
86
90
  --title <text> Push title
@@ -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;
@@ -0,0 +1,63 @@
1
+ export declare const LISTENER_PID_FILE: string;
2
+ /** CLI version the running daemon booted from. Absent ⇒ pre-stamp build. */
3
+ export declare const LISTENER_VERSION_FILE: string;
4
+ export declare const LISTENER_LOG_FILE: string;
5
+ /**
6
+ * PID of the listener on record, or null when the file is missing or stale.
7
+ * Stale PID files (process gone) read as "no listener" so a crashed daemon
8
+ * never blocks the next autostart.
9
+ */
10
+ export declare const runningListenerPid: () => number | null;
11
+ /**
12
+ * CLI version the running daemon started from, or null when unknown.
13
+ * null is meaningful: every build before the stamp existed predates the
14
+ * live-terminal protocol, so callers treat "unknown" as "stale".
15
+ */
16
+ export declare const runningListenerVersion: () => string | null;
17
+ /** Claim the singleton slot for THIS process and stamp the version with it. */
18
+ export declare const writeListenerRuntime: (version: string) => void;
19
+ /**
20
+ * Drop the PID/version stamps, but only when they still name `pid` — a
21
+ * successor that already claimed the slot must not be trampled by a
22
+ * predecessor's exit handler.
23
+ */
24
+ export declare const clearListenerRuntime: (pid: number) => void;
25
+ /**
26
+ * Drop stamps left behind by a daemon that died without running its exit
27
+ * handler (crash, SIGKILL, reboot-era leftovers). No-op while one is alive.
28
+ */
29
+ export declare const clearStaleListenerRuntime: () => void;
30
+ /**
31
+ * Stop the daemon and wait for the process to actually go away. SIGTERM
32
+ * first so its exit handler clears the stamps, SIGKILL once the grace window
33
+ * expires. Resolves true when the process is confirmed gone.
34
+ *
35
+ * Waiting matters: the caller's next move is spawning a replacement, and two
36
+ * live daemons on one machine both answer the same pushes.
37
+ */
38
+ export declare const stopListener: (pid: number, timeoutMs?: number) => Promise<boolean>;
39
+ /**
40
+ * Path to the running cli.js entry. wrapper.js/listener.js sit next to cli.js
41
+ * in dist/, so __dirname resolves it directly — independent of how the user
42
+ * invoked us.
43
+ *
44
+ * `process.argv[1]` is unreliable here: when `zeph` runs via the npm-installed
45
+ * bin shim (`/usr/local/bin/zeph` → cli.js via a wrapper script), argv[1] is
46
+ * the shim path (`.../bin/zeph`), NOT `cli.js`. That made the original
47
+ * `/cli\.(js|ts|mjs|cjs)$/` check silently reject the entry and the autospawn
48
+ * never fired — the bug only surfaced once the user switched to the global
49
+ * npm install.
50
+ *
51
+ * Fall back to argv[1] only when the __dirname-relative file doesn't exist
52
+ * (some packaging where dist layout differs).
53
+ */
54
+ export declare const resolveCliPath: () => string | null;
55
+ export declare const rotateListenerLogIfLarge: () => void;
56
+ /**
57
+ * Launch `zeph listener` detached, with output appended to the listener log
58
+ * so nothing is lost once we let go of it. Returns false when the cli entry
59
+ * couldn't be resolved or the spawn failed — the caller decides how loud that
60
+ * is (autostart is best-effort, an explicit `--restart` is not).
61
+ */
62
+ export declare const spawnListenerDetached: () => boolean;
63
+ //# sourceMappingURL=listener-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener-process.d.ts","sourceRoot":"","sources":["../src/listener-process.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,iBAAiB,QAAiC,CAAC;AAChE,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,QAAqC,CAAC;AACxE,eAAO,MAAM,iBAAiB,QAAiC,CAAC;AAIhE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAS9C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAAM,GAAG,IAMlD,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,IAItD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,KAAG,IASlD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAQ5C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAU,KAAK,MAAM,EAAE,kBAAiB,KAAG,OAAO,CAAC,OAAO,CAqBlF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,QAAO,MAAM,GAAG,IAM1C,CAAC;AASF,eAAO,MAAM,wBAAwB,QAAO,IAM3C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,OAiBxC,CAAC"}
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spawnListenerDetached = exports.rotateListenerLogIfLarge = exports.resolveCliPath = exports.stopListener = exports.clearStaleListenerRuntime = exports.clearListenerRuntime = exports.writeListenerRuntime = exports.runningListenerVersion = exports.runningListenerPid = exports.LISTENER_LOG_FILE = exports.LISTENER_VERSION_FILE = exports.LISTENER_PID_FILE = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ /**
9
+ * Process-level facts about the resident `zeph listener` daemon: where its
10
+ * PID/version stamps live, whether one is alive, how to stop it and how to
11
+ * respawn it detached.
12
+ *
13
+ * This lives OUTSIDE listener.ts on purpose. `wrapper.ts` (the `zeph cc` hot
14
+ * path) needs all of it, and importing listener.ts there would drag `ws` +
15
+ * the crypto stack into every agent launch. Same split as
16
+ * listener-device-id.ts — one small module both sides can read.
17
+ *
18
+ * Why a version stamp at all: `npm i -g @zeph-to/cli` replaces the package on
19
+ * disk but NOT the daemon already running from the old dist. That daemon
20
+ * keeps answering pushes (so chat looks fine) while silently ignoring every
21
+ * subtype added since it started — which is exactly how a pre-1.24 listener
22
+ * left the phone's live terminal spinning until the machine was rebooted.
23
+ * Stamping the version next to the PID makes that drift detectable.
24
+ */
25
+ const ZEPH_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.zeph');
26
+ exports.LISTENER_PID_FILE = (0, node_path_1.join)(ZEPH_DIR, 'listener.pid');
27
+ /** CLI version the running daemon booted from. Absent ⇒ pre-stamp build. */
28
+ exports.LISTENER_VERSION_FILE = (0, node_path_1.join)(ZEPH_DIR, 'listener.version');
29
+ exports.LISTENER_LOG_FILE = (0, node_path_1.join)(ZEPH_DIR, 'listener.log');
30
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31
+ /**
32
+ * PID of the listener on record, or null when the file is missing or stale.
33
+ * Stale PID files (process gone) read as "no listener" so a crashed daemon
34
+ * never blocks the next autostart.
35
+ */
36
+ const runningListenerPid = () => {
37
+ try {
38
+ const pid = Number((0, node_fs_1.readFileSync)(exports.LISTENER_PID_FILE, 'utf-8').trim());
39
+ if (!Number.isFinite(pid) || pid <= 0)
40
+ return null;
41
+ process.kill(pid, 0); // signal 0 = existence check, throws when gone
42
+ return pid;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ };
48
+ exports.runningListenerPid = runningListenerPid;
49
+ /**
50
+ * CLI version the running daemon started from, or null when unknown.
51
+ * null is meaningful: every build before the stamp existed predates the
52
+ * live-terminal protocol, so callers treat "unknown" as "stale".
53
+ */
54
+ const runningListenerVersion = () => {
55
+ try {
56
+ return (0, node_fs_1.readFileSync)(exports.LISTENER_VERSION_FILE, 'utf-8').trim() || null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ };
62
+ exports.runningListenerVersion = runningListenerVersion;
63
+ /** Claim the singleton slot for THIS process and stamp the version with it. */
64
+ const writeListenerRuntime = (version) => {
65
+ (0, node_fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
66
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_PID_FILE, String(process.pid));
67
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_VERSION_FILE, version);
68
+ };
69
+ exports.writeListenerRuntime = writeListenerRuntime;
70
+ /**
71
+ * Drop the PID/version stamps, but only when they still name `pid` — a
72
+ * successor that already claimed the slot must not be trampled by a
73
+ * predecessor's exit handler.
74
+ */
75
+ const clearListenerRuntime = (pid) => {
76
+ try {
77
+ if (!(0, node_fs_1.existsSync)(exports.LISTENER_PID_FILE))
78
+ return;
79
+ if (Number((0, node_fs_1.readFileSync)(exports.LISTENER_PID_FILE, 'utf-8').trim()) !== pid)
80
+ return;
81
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_PID_FILE);
82
+ try {
83
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_VERSION_FILE);
84
+ }
85
+ catch { /* best-effort — a missing stamp is already the desired state */ }
86
+ }
87
+ catch { /* best-effort */ }
88
+ };
89
+ exports.clearListenerRuntime = clearListenerRuntime;
90
+ /**
91
+ * Drop stamps left behind by a daemon that died without running its exit
92
+ * handler (crash, SIGKILL, reboot-era leftovers). No-op while one is alive.
93
+ */
94
+ const clearStaleListenerRuntime = () => {
95
+ if ((0, exports.runningListenerPid)() !== null)
96
+ return;
97
+ try {
98
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_PID_FILE);
99
+ }
100
+ catch { /* nothing to clear */ }
101
+ try {
102
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_VERSION_FILE);
103
+ }
104
+ catch { /* nothing to clear */ }
105
+ };
106
+ exports.clearStaleListenerRuntime = clearStaleListenerRuntime;
107
+ /**
108
+ * Stop the daemon and wait for the process to actually go away. SIGTERM
109
+ * first so its exit handler clears the stamps, SIGKILL once the grace window
110
+ * expires. Resolves true when the process is confirmed gone.
111
+ *
112
+ * Waiting matters: the caller's next move is spawning a replacement, and two
113
+ * live daemons on one machine both answer the same pushes.
114
+ */
115
+ const stopListener = async (pid, timeoutMs = 3_000) => {
116
+ try {
117
+ process.kill(pid, 'SIGTERM');
118
+ }
119
+ catch {
120
+ return true; // already gone
121
+ }
122
+ const deadline = Date.now() + timeoutMs;
123
+ while (Date.now() < deadline) {
124
+ await sleep(50);
125
+ try {
126
+ process.kill(pid, 0);
127
+ }
128
+ catch {
129
+ (0, exports.clearListenerRuntime)(pid);
130
+ return true;
131
+ }
132
+ }
133
+ try {
134
+ process.kill(pid, 'SIGKILL');
135
+ }
136
+ catch { /* raced us to exit */ }
137
+ (0, exports.clearListenerRuntime)(pid);
138
+ return false;
139
+ };
140
+ exports.stopListener = stopListener;
141
+ /**
142
+ * Path to the running cli.js entry. wrapper.js/listener.js sit next to cli.js
143
+ * in dist/, so __dirname resolves it directly — independent of how the user
144
+ * invoked us.
145
+ *
146
+ * `process.argv[1]` is unreliable here: when `zeph` runs via the npm-installed
147
+ * bin shim (`/usr/local/bin/zeph` → cli.js via a wrapper script), argv[1] is
148
+ * the shim path (`.../bin/zeph`), NOT `cli.js`. That made the original
149
+ * `/cli\.(js|ts|mjs|cjs)$/` check silently reject the entry and the autospawn
150
+ * never fired — the bug only surfaced once the user switched to the global
151
+ * npm install.
152
+ *
153
+ * Fall back to argv[1] only when the __dirname-relative file doesn't exist
154
+ * (some packaging where dist layout differs).
155
+ */
156
+ const resolveCliPath = () => {
157
+ const local = (0, node_path_1.join)(__dirname, 'cli.js');
158
+ if ((0, node_fs_1.existsSync)(local))
159
+ return local;
160
+ const entry = process.argv[1];
161
+ if (entry && /cli\.(js|ts|mjs|cjs)$/.test(entry))
162
+ return entry;
163
+ return null;
164
+ };
165
+ exports.resolveCliPath = resolveCliPath;
166
+ /**
167
+ * Rotate the listener log once it grows past 5 MB. The daemon runs for days
168
+ * and writes 2-3 lines per 5-s cycle, so without rotation the file climbs
169
+ * into the tens of megabytes. Keep the previous run's tail under `.old`.
170
+ */
171
+ const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
172
+ const rotateListenerLogIfLarge = () => {
173
+ try {
174
+ if (!(0, node_fs_1.existsSync)(exports.LISTENER_LOG_FILE))
175
+ return;
176
+ if ((0, node_fs_1.statSync)(exports.LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
177
+ return;
178
+ (0, node_fs_1.renameSync)(exports.LISTENER_LOG_FILE, exports.LISTENER_LOG_FILE + '.old');
179
+ }
180
+ catch { /* best-effort */ }
181
+ };
182
+ exports.rotateListenerLogIfLarge = rotateListenerLogIfLarge;
183
+ /**
184
+ * Launch `zeph listener` detached, with output appended to the listener log
185
+ * so nothing is lost once we let go of it. Returns false when the cli entry
186
+ * couldn't be resolved or the spawn failed — the caller decides how loud that
187
+ * is (autostart is best-effort, an explicit `--restart` is not).
188
+ */
189
+ const spawnListenerDetached = () => {
190
+ const cliPath = (0, exports.resolveCliPath)();
191
+ if (!cliPath)
192
+ return false;
193
+ try {
194
+ (0, node_fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
195
+ (0, exports.rotateListenerLogIfLarge)();
196
+ const out = (0, node_fs_1.openSync)(exports.LISTENER_LOG_FILE, 'a');
197
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
198
+ detached: true,
199
+ stdio: ['ignore', out, out],
200
+ env: { ...process.env, ZEPH_LISTENER_AUTOSTART: '1' },
201
+ });
202
+ child.unref();
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ };
209
+ exports.spawnListenerDetached = spawnListenerDetached;
@@ -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;AAoBH,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;AA0DF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAsI3F,CAAC"}
package/dist/listener.js CHANGED
@@ -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 listener_process_js_1 = require("./listener-process.js");
36
37
  const gate_js_1 = require("./gate.js");
37
38
  const remote_agents_js_1 = require("./remote-agents.js");
38
39
  const agent_state_js_1 = require("./agent-state.js");
@@ -1644,8 +1645,6 @@ const resolveWsUrl = (args, config, baseUrl) => {
1644
1645
  };
1645
1646
  exports.resolveWsUrl = resolveWsUrl;
1646
1647
  // ── Singleton guard (PID file) ──────────────────────────────────────
1647
- const ZEPH_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
1648
- const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
1649
1648
  /**
1650
1649
  * Whether another `zeph listener` is already running on this machine.
1651
1650
  * The wrapper's autostart and a user typing `zeph listener` by hand can
@@ -1656,38 +1655,54 @@ const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
1656
1655
  * wrapper can recover from crashes without manual cleanup.
1657
1656
  */
1658
1657
  const otherListenerAlive = () => {
1659
- try {
1660
- const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
1661
- if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
1662
- return null;
1663
- process.kill(pid, 0); // existence check, throws if dead
1664
- return pid;
1665
- }
1666
- catch {
1667
- return null;
1668
- }
1658
+ const pid = (0, listener_process_js_1.runningListenerPid)();
1659
+ return pid === null || pid === process.pid ? null : pid;
1669
1660
  };
1670
1661
  const writeListenerPid = () => {
1671
1662
  try {
1672
- (0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
1673
- (0, fs_1.writeFileSync)(LISTENER_PID_FILE, String(process.pid));
1663
+ // Stamp the version alongside the pid: `npm i -g` swaps the package
1664
+ // on disk without touching this already-running process, and the
1665
+ // stamp is how `zeph cc` notices the drift and restarts us.
1666
+ (0, listener_process_js_1.writeListenerRuntime)(config_js_1.VERSION);
1674
1667
  }
1675
1668
  catch (err) {
1676
- log(`! could not write ${LISTENER_PID_FILE}: ${err.message}`);
1669
+ log(`! could not write ${listener_process_js_1.LISTENER_PID_FILE}: ${err.message}`);
1677
1670
  }
1678
1671
  };
1679
- const removeListenerPid = () => {
1680
- try {
1681
- if (!(0, fs_1.existsSync)(LISTENER_PID_FILE))
1682
- return;
1683
- // Only remove our own pid file — don't trample a successor's.
1684
- const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
1685
- if (pid === process.pid)
1686
- (0, fs_1.unlinkSync)(LISTENER_PID_FILE);
1672
+ const removeListenerPid = () => (0, listener_process_js_1.clearListenerRuntime)(process.pid);
1673
+ /**
1674
+ * `--stop` / `--restart`: the daemon is a background service, so the only
1675
+ * way to replace a stale one used to be hunting its pid by hand. `--restart`
1676
+ * relaunches detached (not in the caller's foreground) because restarting is
1677
+ * an ops action — the user wants their shell back, not a new daemon pinned
1678
+ * to it.
1679
+ */
1680
+ const handleListenerLifecycle = async (restart) => {
1681
+ const pid = otherListenerAlive();
1682
+ if (pid) {
1683
+ const stopped = await (0, listener_process_js_1.stopListener)(pid);
1684
+ console.log(`zeph listener: stopped pid ${pid}${stopped ? '' : ' (forced)'}`);
1687
1685
  }
1688
- catch { /* best-effort */ }
1686
+ else {
1687
+ console.log('zeph listener: no listener running');
1688
+ // A pid file left behind by a crashed daemon would otherwise keep
1689
+ // tripping the singleton guard on the next start.
1690
+ (0, listener_process_js_1.clearStaleListenerRuntime)();
1691
+ }
1692
+ if (!restart)
1693
+ return 0;
1694
+ if (!(0, listener_process_js_1.spawnListenerDetached)()) {
1695
+ console.error('zeph listener: could not resolve the cli entry to respawn — run `zeph listener` manually.');
1696
+ return 1;
1697
+ }
1698
+ console.log(`zeph listener: restarted in background (log: ${listener_process_js_1.LISTENER_LOG_FILE})`);
1699
+ return 0;
1689
1700
  };
1690
1701
  const handleListener = async (args) => {
1702
+ // Lifecycle flags run before verifyTmux — stopping a daemon shouldn't
1703
+ // require a healthy tmux.
1704
+ if (args.stop === true || args.restart === true)
1705
+ return handleListenerLifecycle(args.restart === true);
1691
1706
  verifyTmux();
1692
1707
  // Refuse to start when another listener is already running. The
1693
1708
  // wrapper's autostart calls us blindly on every `zeph cc`; the user
@@ -1723,7 +1738,10 @@ const handleListener = async (args) => {
1723
1738
  }
1724
1739
  writeListenerPid();
1725
1740
  process.on('exit', removeListenerPid);
1726
- log(`zeph listener starting ${wsUrl}`);
1741
+ // Version on the first line: the only way to tell which build a
1742
+ // long-lived daemon is actually running (an `npm i -g` days ago says
1743
+ // nothing about the process that's been up since before it).
1744
+ log(`zeph listener starting — v${config_js_1.VERSION} — ${wsUrl}`);
1727
1745
  log(`device=${(0, exports.computeListenerDeviceId)()} host=${(0, os_1.hostname)()} pid=${process.pid}`);
1728
1746
  log("Waiting for 'agent.command' pushes from the phone picker. Ctrl-C to stop.");
1729
1747
  // Heartbeat memory log — once an hour. Lets the user (and us) spot
package/dist/wrapper.d.ts CHANGED
@@ -17,6 +17,31 @@ export declare const tmuxSessionName: (project: string) => string;
17
17
  * dependency-free against the user's running tmux server.
18
18
  */
19
19
  export declare const findAvailableSession: (base: string) => string;
20
+ /**
21
+ * Whether the daemon on record is running a different build than the one
22
+ * we're launching from. `npm i -g @zeph-to/cli` swaps dist/ but leaves the
23
+ * live process untouched, so a daemon can stay days behind the installed
24
+ * package — answering pushes (chat looks fine) while silently ignoring every
25
+ * message subtype added since it booted. That is exactly how a pre-1.24
26
+ * listener left the phone's live terminal spinning until a reboot.
27
+ *
28
+ * A missing stamp means the daemon predates version stamping, which puts it
29
+ * behind by definition — treat unknown as drifted.
30
+ *
31
+ * Strictly "installed is newer", never a plain inequality: one account can
32
+ * have several installs at different versions (nvm node versions, a repo
33
+ * build alongside the global one), and `!==` would make each `zeph cc` kill
34
+ * and respawn the other's daemon forever. An older `zeph cc` leaves a newer
35
+ * daemon alone — downgrading it would reintroduce the very gap this closes.
36
+ */
37
+ export declare const listenerVersionDrifted: (running: string | null, installed: string) => boolean;
38
+ /**
39
+ * Make sure the phone-bridge daemon is running AND is the build we just
40
+ * launched from. `zeph cc` is the right moment to replace a drifted one: the
41
+ * user is starting fresh work rather than mid-task, so the ~1s restart window
42
+ * (in which a push would be dropped — WS fan-out has no queue) costs nothing.
43
+ */
44
+ export declare const ensureListenerRunning: () => Promise<void>;
20
45
  /**
21
46
  * Launch the agent in a named tmux session (or directly if nested) and
22
47
  * forward its exit code. `extra` is appended to the agent invocation, so
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAenD,CAAC;AA6HF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmC3F,CAAC"}
1
+ {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAenD,CAAC;AAmCF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,GAAG,IAAI,EAAE,WAAW,MAAM,KAAG,OAChC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CAa1D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAU,OAAO,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCjG,CAAC"}
package/dist/wrapper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleAgentSession = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
3
+ exports.handleAgentSession = exports.ensureListenerRunning = exports.listenerVersionDrifted = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
4
4
  /**
5
5
  * `zeph cc` / `zeph codex` / `zeph gemini` — spawn an agent inside a named
6
6
  * tmux session so the resident listener (`zeph listener`) can address it
@@ -13,10 +13,10 @@ exports.handleAgentSession = exports.findAvailableSession = exports.tmuxSessionN
13
13
  * agent directly — letting power users keep their own multiplexer setup.
14
14
  */
15
15
  const child_process_1 = require("child_process");
16
- const fs_1 = require("fs");
17
- const os_1 = require("os");
18
16
  const path_1 = require("path");
17
+ const check_update_js_1 = require("./check-update.js");
19
18
  const config_js_1 = require("./config.js");
19
+ const listener_process_js_1 = require("./listener-process.js");
20
20
  const FALLBACK_NAME = 'project';
21
21
  /** basename(), with a stable fallback for edge paths like `/`. */
22
22
  const safeBasename = (path) => (0, path_1.basename)(path) || FALLBACK_NAME;
@@ -99,111 +99,60 @@ const targetForAgent = (agent, extra) => {
99
99
  return { cmd: 'tmux', args: ['new', '-A', '-s', session, shellCmd] };
100
100
  };
101
101
  // ── Background listener auto-start ────────────────────────────────────
102
- const ZEPH_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
103
- const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
104
- const LISTENER_LOG_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.log');
105
- /** True when the PID file points at a still-alive process. */
106
- const listenerAlive = () => {
107
- try {
108
- const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
109
- if (!Number.isFinite(pid) || pid <= 0)
110
- return false;
111
- // Signal 0 = existence check; throws when the process is gone.
112
- process.kill(pid, 0);
113
- return true;
114
- }
115
- catch {
116
- return false;
117
- }
118
- };
119
102
  /**
120
- * Path to the running cli.js entry. wrapper.js sits next to cli.js in
121
- * dist/, so __dirname resolves it directly independent of how the
122
- * user invoked us.
103
+ * Whether the daemon on record is running a different build than the one
104
+ * we're launching from. `npm i -g @zeph-to/cli` swaps dist/ but leaves the
105
+ * live process untouched, so a daemon can stay days behind the installed
106
+ * package — answering pushes (chat looks fine) while silently ignoring every
107
+ * message subtype added since it booted. That is exactly how a pre-1.24
108
+ * listener left the phone's live terminal spinning until a reboot.
123
109
  *
124
- * `process.argv[1]` is unreliable here: when `zeph` runs via the
125
- * npm-installed bin shim (`/usr/local/bin/zeph` cli.js via a wrapper
126
- * script), argv[1] is the shim path (`.../bin/zeph`), NOT `cli.js`.
127
- * That made the original `/cli\\.(js|ts|mjs|cjs)$/` check silently
128
- * reject the entry and the autospawn never fired — exactly the bug
129
- * the user hit ('원래 싱글톤으로 됐었잖아' — yes, on the local alias
130
- * path where argv[1] IS cli.js; the bug only surfaced once the user
131
- * switched to the global npm install).
110
+ * A missing stamp means the daemon predates version stamping, which puts it
111
+ * behind by definition treat unknown as drifted.
132
112
  *
133
- * Fall back to argv[1] only when the __dirname-relative file doesn't
134
- * exist (some packaging where dist layout differs).
113
+ * Strictly "installed is newer", never a plain inequality: one account can
114
+ * have several installs at different versions (nvm node versions, a repo
115
+ * build alongside the global one), and `!==` would make each `zeph cc` kill
116
+ * and respawn the other's daemon forever. An older `zeph cc` leaves a newer
117
+ * daemon alone — downgrading it would reintroduce the very gap this closes.
135
118
  */
136
- const resolveCliPath = () => {
137
- const local = (0, path_1.join)(__dirname, 'cli.js');
138
- if ((0, fs_1.existsSync)(local))
139
- return local;
140
- const entry = process.argv[1];
141
- if (entry && /cli\.(js|ts|mjs|cjs)$/.test(entry))
142
- return entry;
143
- return null;
144
- };
119
+ const listenerVersionDrifted = (running, installed) => running === null || (0, check_update_js_1.isNewer)(installed, running);
120
+ exports.listenerVersionDrifted = listenerVersionDrifted;
145
121
  /**
146
- * Spawn `zeph listener` in the background if it isn't already running on
147
- * this machine. The intent is that the user only ever has to know about
148
- * `zeph cc` the phone-to-tmux bridge tags along automatically. Output
149
- * goes to `~/.zeph/listener.log` so it isn't lost on detach; the listener
150
- * itself writes its own PID to `~/.zeph/listener.pid` on startup and
151
- * removes it on graceful exit, so subsequent `zeph cc` invocations skip
152
- * the spawn when a listener is already up.
153
- *
154
- * Failure here is non-fatal — `zeph cc` still launches the agent. The
155
- * user just loses the phone-bridge feature until they restart.
122
+ * Make sure the phone-bridge daemon is running AND is the build we just
123
+ * launched from. `zeph cc` is the right moment to replace a drifted one: the
124
+ * user is starting fresh work rather than mid-task, so the ~1s restart window
125
+ * (in which a push would be dropped WS fan-out has no queue) costs nothing.
156
126
  */
157
- /**
158
- * Rotate the listener log once it grows past 5 MB. The daemon runs for
159
- * days and writes 2-3 lines per 5-s cycle, so without rotation the file
160
- * climbs into the tens of megabytes range pretty quickly. We keep the
161
- * previous run's tail under `.old` for post-mortem and start fresh.
162
- */
163
- const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
164
- const rotateListenerLogIfLarge = () => {
165
- try {
166
- if (!(0, fs_1.existsSync)(LISTENER_LOG_FILE))
167
- return;
168
- if ((0, fs_1.statSync)(LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
127
+ const ensureListenerRunning = async () => {
128
+ const pid = (0, listener_process_js_1.runningListenerPid)();
129
+ if (pid !== null) {
130
+ const running = (0, listener_process_js_1.runningListenerVersion)();
131
+ if (!(0, exports.listenerVersionDrifted)(running, config_js_1.VERSION))
169
132
  return;
170
- (0, fs_1.renameSync)(LISTENER_LOG_FILE, LISTENER_LOG_FILE + '.old');
133
+ console.log(`zeph: listener ${running ?? '(pre-1.26)'} is stale — restarting on ${config_js_1.VERSION}`);
134
+ await (0, listener_process_js_1.stopListener)(pid);
171
135
  }
172
- catch { /* best-effort */ }
173
- };
174
- const ensureListenerRunning = () => {
175
- if (listenerAlive())
176
- return;
177
- const cliPath = resolveCliPath();
178
- if (!cliPath || !(0, fs_1.existsSync)(cliPath))
179
- return;
180
- try {
181
- (0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
182
- rotateListenerLogIfLarge();
183
- const out = (0, fs_1.openSync)(LISTENER_LOG_FILE, 'a');
184
- const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
185
- detached: true,
186
- stdio: ['ignore', out, out],
187
- env: { ...process.env, ZEPH_LISTENER_AUTOSTART: '1' },
188
- });
189
- child.unref();
190
- console.log(`zeph: listener autostarted in background (log: ${LISTENER_LOG_FILE})`);
136
+ if ((0, listener_process_js_1.spawnListenerDetached)()) {
137
+ if (pid === null)
138
+ console.log(`zeph: listener autostarted in background (log: ${listener_process_js_1.LISTENER_LOG_FILE})`);
191
139
  }
192
- catch (err) {
193
- console.error(`zeph: listener autostart failed: ${err.message}`);
140
+ else {
141
+ console.error('zeph: listener autostart failed — run `zeph listener` manually.');
194
142
  }
195
143
  };
144
+ exports.ensureListenerRunning = ensureListenerRunning;
196
145
  /**
197
146
  * Launch the agent in a named tmux session (or directly if nested) and
198
147
  * forward its exit code. `extra` is appended to the agent invocation, so
199
148
  * `zeph cc --resume foo` runs `claude --resume foo` inside the session.
200
149
  * Returns when the agent exits.
201
150
  */
202
- const handleAgentSession = (agent, extra = []) => {
203
- // Best-effort: make sure the phone-bridge daemon is running before we
204
- // launch the agent. The user shouldn't need to remember a second
205
- // command for the picker on their phone to work.
206
- ensureListenerRunning();
151
+ const handleAgentSession = async (agent, extra = []) => {
152
+ // Best-effort: make sure the phone-bridge daemon is running, and running
153
+ // the build we were launched from. The user shouldn't need to remember a
154
+ // second command for the picker on their phone to work.
155
+ await (0, exports.ensureListenerRunning)();
207
156
  return new Promise((resolve) => {
208
157
  const { cmd, args } = targetForAgent(agent.binary, extra);
209
158
  const start = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "1.24.0",
3
+ "version": "1.26.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",