@zeph-to/cli 1.25.0 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/dist/cli.js +4 -0
- package/dist/listener-process.d.ts +63 -0
- package/dist/listener-process.d.ts.map +1 -0
- package/dist/listener-process.js +209 -0
- package/dist/listener.d.ts +8 -1
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +136 -38
- package/dist/wrapper.d.ts +25 -0
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +40 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,12 +202,22 @@ can never false-flag. Muted projects are never flagged.
|
|
|
202
202
|
|
|
203
203
|
### Diagnostics
|
|
204
204
|
|
|
205
|
-
The auto-spawned listener writes to
|
|
205
|
+
The auto-spawned listener writes to three files under `~/.zeph/`:
|
|
206
206
|
|
|
207
207
|
- `listener.pid` — the running daemon's PID. `cat ~/.zeph/listener.pid`
|
|
208
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).
|
|
209
212
|
- `listener.log` — stdout + stderr from the daemon. `tail -f` to watch.
|
|
210
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
|
+
|
|
211
221
|
A healthy listener log shows one line per cycle:
|
|
212
222
|
|
|
213
223
|
```
|
|
@@ -219,12 +229,31 @@ If you see `! server rejected listener.sessions: ...` instead, the
|
|
|
219
229
|
message points at the failure (auth, missing device record, etc.) so
|
|
220
230
|
you can fix the actual problem instead of guessing.
|
|
221
231
|
|
|
222
|
-
To force a restart
|
|
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:
|
|
223
254
|
|
|
224
255
|
```bash
|
|
225
|
-
|
|
226
|
-
rm ~/.zeph/listener.pid
|
|
227
|
-
zeph cc # autospawns the new build
|
|
256
|
+
ps aux | grep '[c]li.js listener'
|
|
228
257
|
```
|
|
229
258
|
|
|
230
259
|
To run it in the foreground (for development of the SDK itself):
|
|
@@ -385,6 +414,8 @@ which project + branch finished without writing per-IDE wrappers. Pass
|
|
|
385
414
|
| `--ws-url <url>` | WebSocket endpoint (or set `ZEPH_WS_URL` env, or `wsUrl` in `~/.zeph/config.json`) |
|
|
386
415
|
| `--key <api-key>` | API key (or set `ZEPH_API_KEY` env) |
|
|
387
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`) |
|
|
388
419
|
|
|
389
420
|
The listener reconnects with exponential backoff + jitter (1 s → 30 s
|
|
390
421
|
cap). Heartbeat is ping every 25 s with a 10 s pong timeout. On an
|
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
|
|
@@ -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;
|
package/dist/listener.d.ts
CHANGED
|
@@ -174,7 +174,14 @@ export interface StreamControl {
|
|
|
174
174
|
sessionName?: string;
|
|
175
175
|
/** Subscriber's device public key (Base64 SPKI) — presence turns on E2EE. */
|
|
176
176
|
subscriberPublicKey?: string;
|
|
177
|
+
/**
|
|
178
|
+
* Subscriber promises to send `agent.stream.renew` while it's watching.
|
|
179
|
+
* Only such a subscriber gets the short lease; clients that predate the
|
|
180
|
+
* renew protocol keep the 5-minute orphan guard (see STREAM_LEASE_MS).
|
|
181
|
+
*/
|
|
182
|
+
renew?: boolean;
|
|
177
183
|
}
|
|
184
|
+
export declare const STREAM_LEASE_MS = 15000;
|
|
178
185
|
export declare const MAX_CONCURRENT_STREAMS = 3;
|
|
179
186
|
export declare const stopStream: (sessionName: string) => void;
|
|
180
187
|
export declare const stopAllStreams: () => void;
|
|
@@ -198,7 +205,7 @@ export type StreamFramePayload = {
|
|
|
198
205
|
export type StreamErrorFrame = {
|
|
199
206
|
subtype: 'agent.stream.frame';
|
|
200
207
|
sessionName: string;
|
|
201
|
-
error: 'unknown_session' | 'e2ee_unavailable' | 'encrypt_failed' | 'stream_limit';
|
|
208
|
+
error: 'unknown_session' | 'e2ee_unavailable' | 'encrypt_failed' | 'stream_limit' | 'stream_gone';
|
|
202
209
|
};
|
|
203
210
|
/**
|
|
204
211
|
* Per-listener concurrency guard, as a pure predicate so the boundary is
|
package/dist/listener.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.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;
|
|
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.STREAM_LEASE_MS = 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");
|
|
@@ -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");
|
|
@@ -765,10 +766,27 @@ exports.scheduleInjectSnapshots = scheduleInjectSnapshots;
|
|
|
765
766
|
// still caps the actual payload, so very wide panes get top-truncated.
|
|
766
767
|
const STREAM_CAPTURE_LINES = 200;
|
|
767
768
|
const STREAM_INTERVAL_MS = 400;
|
|
768
|
-
// Orphan guard
|
|
769
|
-
//
|
|
770
|
-
//
|
|
769
|
+
// Orphan guard for subscribers that can't renew (clients older than the renew
|
|
770
|
+
// protocol): a phone that dies without sending agent.stream.stop must not leak
|
|
771
|
+
// an interval forever. Auto-stop after this long; the phone re-subscribes on
|
|
772
|
+
// reopen.
|
|
771
773
|
const STREAM_MAX_MS = 5 * 60_000;
|
|
774
|
+
// Lease for a renewal-capable subscriber (`start` carried `renew: true`).
|
|
775
|
+
// stream.stop is NOT a reliable slot-release signal: the native terminal runs
|
|
776
|
+
// in its own WebView that the OS destroys on swipe-back, killing the page mid-
|
|
777
|
+
// flight — no unmount, no stop, and the relay is pass-through so we never see
|
|
778
|
+
// the phone's socket drop either. Three such opens used to wedge
|
|
779
|
+
// MAX_CONCURRENT_STREAMS for a full STREAM_MAX_MS with every retry answering
|
|
780
|
+
// stream_limit. A lease inverts the burden: the viewer must keep proving it's
|
|
781
|
+
// there.
|
|
782
|
+
//
|
|
783
|
+
// This is also the worst-case wait before a vanished viewer's slot comes back,
|
|
784
|
+
// so it wants to be short — but a lease can only be as tight as the proof is
|
|
785
|
+
// frequent. At the web's 5s renew cadence this tolerates 3 consecutive dropped
|
|
786
|
+
// renews, the same margin as before, while cutting the wedge from 5 minutes to
|
|
787
|
+
// 15 seconds. A viewer that loses the race gets its stream reaped and re-
|
|
788
|
+
// subscribes on the next renew (see the stream_gone reply below).
|
|
789
|
+
exports.STREAM_LEASE_MS = 15_000;
|
|
772
790
|
// Per-listener concurrency guard: the API Gateway WS stage throttle (50 rps)
|
|
773
791
|
// is SHARED across every user, so a runaway machine (many sessions, or a
|
|
774
792
|
// client re-subscribe bug) streaming at 2.5 fps each can starve push delivery
|
|
@@ -784,6 +802,8 @@ const STREAM_MAX_ENCRYPT_FAILURES = 3;
|
|
|
784
802
|
// the log every STREAM_LOG_INTERVAL_MS and summarized on stop — the data that
|
|
785
803
|
// decides cloud-throttled (B-lite) vs direct/Tailscale (B-full).
|
|
786
804
|
const STREAM_LOG_INTERVAL_MS = 5_000;
|
|
805
|
+
/** How far a start (and each renew) pushes the deadline out. */
|
|
806
|
+
const leaseFor = (renewing) => (renewing ? exports.STREAM_LEASE_MS : STREAM_MAX_MS);
|
|
787
807
|
const activeStreams = new Map();
|
|
788
808
|
const maybeLogStreamStats = (sessionName, stats) => {
|
|
789
809
|
const elapsed = Date.now() - stats.lastLogAt;
|
|
@@ -822,6 +842,28 @@ exports.stopAllStreams = stopAllStreams;
|
|
|
822
842
|
*/
|
|
823
843
|
const isStreamCapReached = (activeCount) => activeCount >= exports.MAX_CONCURRENT_STREAMS;
|
|
824
844
|
exports.isStreamCapReached = isStreamCapReached;
|
|
845
|
+
/**
|
|
846
|
+
* At the cap, hand the slot to the new subscriber when the stalest holder
|
|
847
|
+
* can't prove it's still watching — an expired lease, or a client too old to
|
|
848
|
+
* renew at all (those hold a slot for STREAM_MAX_MS on nothing but hope, and
|
|
849
|
+
* a cached web build can keep producing them long after a deploy). Three
|
|
850
|
+
* actively-renewing viewers are all real, so that case still refuses.
|
|
851
|
+
* Returns the evicted session name, or null when every holder is live.
|
|
852
|
+
*/
|
|
853
|
+
const evictStalestStream = (now) => {
|
|
854
|
+
let victim = null;
|
|
855
|
+
for (const [name, entry] of activeStreams) {
|
|
856
|
+
if (entry.renewing && entry.expiresAt > now)
|
|
857
|
+
continue; // provably watched
|
|
858
|
+
if (!victim || entry.expiresAt < victim.expiresAt)
|
|
859
|
+
victim = { name, expiresAt: entry.expiresAt };
|
|
860
|
+
}
|
|
861
|
+
if (!victim)
|
|
862
|
+
return null;
|
|
863
|
+
log(`⧉ stream ${victim.name}: evicted — slot handed to a newer subscriber (no live lease)`);
|
|
864
|
+
(0, exports.stopStream)(victim.name);
|
|
865
|
+
return victim.name;
|
|
866
|
+
};
|
|
825
867
|
const streamErrorFrame = (sessionName, error) => ({
|
|
826
868
|
subtype: 'agent.stream.frame',
|
|
827
869
|
sessionName,
|
|
@@ -858,16 +900,40 @@ exports.buildStreamFrame = buildStreamFrame;
|
|
|
858
900
|
* path). start is idempotent — a repeat restarts the loop.
|
|
859
901
|
*/
|
|
860
902
|
const handleStreamControl = (req, send) => {
|
|
903
|
+
if (req.subtype !== 'agent.stream.start' && req.subtype !== 'agent.stream.stop' && req.subtype !== 'agent.stream.renew') {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
// Every stream-control message names one machine, and the relay fans them
|
|
907
|
+
// out to all of this user's connections — so decide addressing once, here.
|
|
908
|
+
// Two machines can run the same tmux session name, and stop/renew carry no
|
|
909
|
+
// other identity: without this gate a stop meant for one listener would
|
|
910
|
+
// reach into the other's stream of that name (and a renew would refresh it).
|
|
911
|
+
if (req.targetDeviceId !== (0, exports.computeListenerDeviceId)())
|
|
912
|
+
return false;
|
|
861
913
|
if (req.subtype === 'agent.stream.stop') {
|
|
862
914
|
if (req.sessionName)
|
|
863
915
|
(0, exports.stopStream)(req.sessionName);
|
|
864
916
|
return true;
|
|
865
917
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
918
|
+
// Lease renewal — the subscriber is still watching.
|
|
919
|
+
if (req.subtype === 'agent.stream.renew') {
|
|
920
|
+
if (!req.sessionName)
|
|
921
|
+
return true;
|
|
922
|
+
const entry = activeStreams.get(req.sessionName);
|
|
923
|
+
if (entry) {
|
|
924
|
+
entry.expiresAt = Date.now() + leaseFor(entry.renewing);
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
// Addressed to us but we hold no such stream: it was reaped (lost
|
|
928
|
+
// renews), evicted, or dropped when our socket last reconnected. The
|
|
929
|
+
// viewer has no other way to learn that — it just keeps painting a
|
|
930
|
+
// frozen pane under a LIVE badge — so tell it to re-subscribe. Renew
|
|
931
|
+
// carries no subscriber key, so restarting it here would silently
|
|
932
|
+
// downgrade an E2EE stream to plaintext; only the client can redo the
|
|
933
|
+
// handshake.
|
|
934
|
+
send(streamErrorFrame(req.sessionName, 'stream_gone'));
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
871
937
|
const sessionName = req.sessionName;
|
|
872
938
|
if (!sessionName)
|
|
873
939
|
return true;
|
|
@@ -879,7 +945,7 @@ const handleStreamControl = (req, send) => {
|
|
|
879
945
|
// Concurrency guard AFTER the restart-stop: re-subscribing to an already
|
|
880
946
|
// active session doesn't count against the cap, only a genuinely new one
|
|
881
947
|
// does. Refuse the new stream instead of adding to the shared-throttle load.
|
|
882
|
-
if ((0, exports.isStreamCapReached)(activeStreams.size)) {
|
|
948
|
+
if ((0, exports.isStreamCapReached)(activeStreams.size) && !evictStalestStream(Date.now())) {
|
|
883
949
|
log(`⧉ stream ${sessionName}: refused — ${activeStreams.size}/${exports.MAX_CONCURRENT_STREAMS} streams already active on this listener`);
|
|
884
950
|
send(streamErrorFrame(sessionName, 'stream_limit'));
|
|
885
951
|
return true;
|
|
@@ -914,6 +980,15 @@ const handleStreamControl = (req, send) => {
|
|
|
914
980
|
let wireSeq = 0;
|
|
915
981
|
let encryptFailures = 0;
|
|
916
982
|
const timer = setInterval(() => {
|
|
983
|
+
// Lease check rides the capture tick: one deadline field, no second
|
|
984
|
+
// timer to leak. (The previous per-start expiry setTimeout was never
|
|
985
|
+
// cleared on stop, so a restart left the old one armed to kill the new
|
|
986
|
+
// incarnation.) Reaping here is what frees the slot for everyone whose
|
|
987
|
+
// stop never arrived.
|
|
988
|
+
if (Date.now() >= (activeStreams.get(sessionName)?.expiresAt ?? 0)) {
|
|
989
|
+
(0, exports.stopStream)(sessionName);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
917
992
|
const captured = capturePane(sessionName, true, STREAM_CAPTURE_LINES);
|
|
918
993
|
if (!captured || captured.content === lastContent) {
|
|
919
994
|
stats.skipped++;
|
|
@@ -966,9 +1041,15 @@ const handleStreamControl = (req, send) => {
|
|
|
966
1041
|
});
|
|
967
1042
|
}, STREAM_INTERVAL_MS);
|
|
968
1043
|
timer.unref?.();
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1044
|
+
// A renewing subscriber gets the short lease; anything older keeps the
|
|
1045
|
+
// 5-minute orphan guard so a version-skewed client isn't cut off mid-view.
|
|
1046
|
+
const renewing = !!req.renew;
|
|
1047
|
+
activeStreams.set(sessionName, {
|
|
1048
|
+
timer,
|
|
1049
|
+
stats,
|
|
1050
|
+
expiresAt: Date.now() + leaseFor(renewing),
|
|
1051
|
+
renewing,
|
|
1052
|
+
});
|
|
972
1053
|
return true;
|
|
973
1054
|
};
|
|
974
1055
|
exports.handleStreamControl = handleStreamControl;
|
|
@@ -1644,8 +1725,6 @@ const resolveWsUrl = (args, config, baseUrl) => {
|
|
|
1644
1725
|
};
|
|
1645
1726
|
exports.resolveWsUrl = resolveWsUrl;
|
|
1646
1727
|
// ── 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
1728
|
/**
|
|
1650
1729
|
* Whether another `zeph listener` is already running on this machine.
|
|
1651
1730
|
* The wrapper's autostart and a user typing `zeph listener` by hand can
|
|
@@ -1656,38 +1735,54 @@ const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
|
|
|
1656
1735
|
* wrapper can recover from crashes without manual cleanup.
|
|
1657
1736
|
*/
|
|
1658
1737
|
const otherListenerAlive = () => {
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
}
|
|
1738
|
+
const pid = (0, listener_process_js_1.runningListenerPid)();
|
|
1739
|
+
return pid === null || pid === process.pid ? null : pid;
|
|
1669
1740
|
};
|
|
1670
1741
|
const writeListenerPid = () => {
|
|
1671
1742
|
try {
|
|
1672
|
-
|
|
1673
|
-
|
|
1743
|
+
// Stamp the version alongside the pid: `npm i -g` swaps the package
|
|
1744
|
+
// on disk without touching this already-running process, and the
|
|
1745
|
+
// stamp is how `zeph cc` notices the drift and restarts us.
|
|
1746
|
+
(0, listener_process_js_1.writeListenerRuntime)(config_js_1.VERSION);
|
|
1674
1747
|
}
|
|
1675
1748
|
catch (err) {
|
|
1676
|
-
log(`! could not write ${LISTENER_PID_FILE}: ${err.message}`);
|
|
1749
|
+
log(`! could not write ${listener_process_js_1.LISTENER_PID_FILE}: ${err.message}`);
|
|
1677
1750
|
}
|
|
1678
1751
|
};
|
|
1679
|
-
const removeListenerPid = () =>
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1752
|
+
const removeListenerPid = () => (0, listener_process_js_1.clearListenerRuntime)(process.pid);
|
|
1753
|
+
/**
|
|
1754
|
+
* `--stop` / `--restart`: the daemon is a background service, so the only
|
|
1755
|
+
* way to replace a stale one used to be hunting its pid by hand. `--restart`
|
|
1756
|
+
* relaunches detached (not in the caller's foreground) because restarting is
|
|
1757
|
+
* an ops action — the user wants their shell back, not a new daemon pinned
|
|
1758
|
+
* to it.
|
|
1759
|
+
*/
|
|
1760
|
+
const handleListenerLifecycle = async (restart) => {
|
|
1761
|
+
const pid = otherListenerAlive();
|
|
1762
|
+
if (pid) {
|
|
1763
|
+
const stopped = await (0, listener_process_js_1.stopListener)(pid);
|
|
1764
|
+
console.log(`zeph listener: stopped pid ${pid}${stopped ? '' : ' (forced)'}`);
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
console.log('zeph listener: no listener running');
|
|
1768
|
+
// A pid file left behind by a crashed daemon would otherwise keep
|
|
1769
|
+
// tripping the singleton guard on the next start.
|
|
1770
|
+
(0, listener_process_js_1.clearStaleListenerRuntime)();
|
|
1687
1771
|
}
|
|
1688
|
-
|
|
1772
|
+
if (!restart)
|
|
1773
|
+
return 0;
|
|
1774
|
+
if (!(0, listener_process_js_1.spawnListenerDetached)()) {
|
|
1775
|
+
console.error('zeph listener: could not resolve the cli entry to respawn — run `zeph listener` manually.');
|
|
1776
|
+
return 1;
|
|
1777
|
+
}
|
|
1778
|
+
console.log(`zeph listener: restarted in background (log: ${listener_process_js_1.LISTENER_LOG_FILE})`);
|
|
1779
|
+
return 0;
|
|
1689
1780
|
};
|
|
1690
1781
|
const handleListener = async (args) => {
|
|
1782
|
+
// Lifecycle flags run before verifyTmux — stopping a daemon shouldn't
|
|
1783
|
+
// require a healthy tmux.
|
|
1784
|
+
if (args.stop === true || args.restart === true)
|
|
1785
|
+
return handleListenerLifecycle(args.restart === true);
|
|
1691
1786
|
verifyTmux();
|
|
1692
1787
|
// Refuse to start when another listener is already running. The
|
|
1693
1788
|
// wrapper's autostart calls us blindly on every `zeph cc`; the user
|
|
@@ -1723,7 +1818,10 @@ const handleListener = async (args) => {
|
|
|
1723
1818
|
}
|
|
1724
1819
|
writeListenerPid();
|
|
1725
1820
|
process.on('exit', removeListenerPid);
|
|
1726
|
-
|
|
1821
|
+
// Version on the first line: the only way to tell which build a
|
|
1822
|
+
// long-lived daemon is actually running (an `npm i -g` days ago says
|
|
1823
|
+
// nothing about the process that's been up since before it).
|
|
1824
|
+
log(`zeph listener starting — v${config_js_1.VERSION} — ${wsUrl}`);
|
|
1727
1825
|
log(`device=${(0, exports.computeListenerDeviceId)()} host=${(0, os_1.hostname)()} pid=${process.pid}`);
|
|
1728
1826
|
log("Waiting for 'agent.command' pushes from the phone picker. Ctrl-C to stop.");
|
|
1729
1827
|
// 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
|
package/dist/wrapper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"
|
|
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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
|
137
|
-
|
|
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
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
193
|
-
console.error(
|
|
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
|
|
204
|
-
//
|
|
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();
|