@zeph-to/cli 1.19.1 → 1.19.3
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 +6 -3
- package/dist/listener.d.ts +12 -7
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +75 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,9 +102,12 @@ tmux session via `tmux send-keys`.
|
|
|
102
102
|
[tmux session "zeph-myapp" running claude / codex / gemini]
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
The listener
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
The listener polls its tmux session inventory every 5 seconds and
|
|
106
|
+
reports it to the server whenever something changed (new session, agent
|
|
107
|
+
state transition, activity) — an unchanged inventory is re-sent only as
|
|
108
|
+
a 30-second idle heartbeat. The phone picker stays in sync with no
|
|
109
|
+
manual configuration, and an idle listener costs the backend a fraction
|
|
110
|
+
of what a fixed 5-second report cycle would.
|
|
108
111
|
|
|
109
112
|
### Remote-origin detection (sticky REMOTE mode)
|
|
110
113
|
|
package/dist/listener.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { type AgentKind, type RegisteredRemoteAgent } from './remote-agents.js';
|
|
24
24
|
import { type AgentState } from './agent-state.js';
|
|
25
|
+
export declare const SESSION_REPORT_HEARTBEAT_MS = 30000;
|
|
25
26
|
interface AgentSession {
|
|
26
27
|
name: string;
|
|
27
28
|
attached: boolean;
|
|
@@ -41,6 +42,17 @@ interface AgentSession {
|
|
|
41
42
|
stateChangedAt?: string;
|
|
42
43
|
stateRuleId?: string;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Change-fingerprint for the report gate. Mirrors the server's
|
|
47
|
+
* REPORTED_SESSION_FIELDS comparison (zeph ws.ts sameReportedSessions):
|
|
48
|
+
* same fields, order-independent, undefined/null collapsed — so the
|
|
49
|
+
* client-side skip can never suppress a report the server would have
|
|
50
|
+
* treated as a change. Keep the field list in sync with the server.
|
|
51
|
+
*/
|
|
52
|
+
export declare const sessionsFingerprint: (sessions: AgentSession[]) => string;
|
|
53
|
+
/** True when this cycle's inventory must be sent: it changed, or the
|
|
54
|
+
* idle heartbeat is due. */
|
|
55
|
+
export declare const sessionsReportDue: (fingerprint: string, lastSentFingerprint: string | null, lastSentAtMs: number, nowMs: number) => boolean;
|
|
44
56
|
export declare const AUTH_FAILURE_CODES: ReadonlySet<number>;
|
|
45
57
|
export declare const checkRateLimit: (session: string, now?: number) => boolean;
|
|
46
58
|
/** Read the foreground command in the named tmux session's active pane. */
|
|
@@ -240,13 +252,6 @@ export declare const gcAttachments: (now?: number, dir?: string, ttl?: number) =
|
|
|
240
252
|
*/
|
|
241
253
|
export declare const handlePush: (push: PushItem, deps?: HandlePushDeps) => Promise<boolean>;
|
|
242
254
|
export declare const computeBackoff: (attempt: number) => number;
|
|
243
|
-
/**
|
|
244
|
-
* Stable per-host device id for the listener. We hash the OS hostname so
|
|
245
|
-
* the same machine reuses the same DeviceRecord across listener restarts
|
|
246
|
-
* (otherwise the phone's session inventory grows a new ghost device every
|
|
247
|
-
* time `zeph listener` rebinds). `dev_listener_<sha8(hostname)>` keeps it
|
|
248
|
-
* human-recognisable in dev logs without leaking the raw hostname.
|
|
249
|
-
*/
|
|
250
255
|
export declare const computeListenerDeviceId: (host?: string) => string;
|
|
251
256
|
export declare const handleListener: (args: Record<string, string | boolean>) => Promise<number>;
|
|
252
257
|
export {};
|
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;AAUH,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzG,OAAO,EAAiD,KAAK,UAAU,EAA4C,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = void 0;
|
|
28
|
+
exports.handleListener = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = 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");
|
|
@@ -52,12 +52,43 @@ const WS_CONNECT_TIMEOUT_MS = 20_000;
|
|
|
52
52
|
// peer never replies. macOS App Nap / Wi-Fi handoff / VPN flap all
|
|
53
53
|
// surface this way. Force-terminate so the reconnect loop runs.
|
|
54
54
|
const WS_STALL_TIMEOUT_MS = 90_000;
|
|
55
|
-
// How often the listener
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// to reflect new `zeph cc` sessions within a few seconds, not half a
|
|
59
|
-
// minute.
|
|
55
|
+
// How often the listener POLLS its local tmux session inventory (and
|
|
56
|
+
// immediately on $connect). Cheap — tmux runs locally, so a change is
|
|
57
|
+
// still detected (and sent) within a few seconds.
|
|
60
58
|
const SESSION_REPORT_INTERVAL_MS = 5_000;
|
|
59
|
+
// How often an UNCHANGED inventory is re-sent. Every send costs the
|
|
60
|
+
// backend a Lambda invocation + several DynamoDB reads + a WS round
|
|
61
|
+
// trip, and an idle listener's reports are ~95% no-ops — the dominant
|
|
62
|
+
// per-user variable cost. A changed inventory still goes out on the
|
|
63
|
+
// next 5 s poll (no latency loss); this only throttles the no-ops.
|
|
64
|
+
// Kept above the server's 20 s write-through window
|
|
65
|
+
// (AGENT_SESSIONS_REFRESH_MS) so each heartbeat also refreshes
|
|
66
|
+
// agentSessionsUpdatedAt. Side effect: the ack (which carries pattern
|
|
67
|
+
// watches) arrives at this cadence while idle, so a newly-created
|
|
68
|
+
// watch can take up to 30 s to reach an idle listener.
|
|
69
|
+
exports.SESSION_REPORT_HEARTBEAT_MS = 30_000;
|
|
70
|
+
/**
|
|
71
|
+
* Change-fingerprint for the report gate. Mirrors the server's
|
|
72
|
+
* REPORTED_SESSION_FIELDS comparison (zeph ws.ts sameReportedSessions):
|
|
73
|
+
* same fields, order-independent, undefined/null collapsed — so the
|
|
74
|
+
* client-side skip can never suppress a report the server would have
|
|
75
|
+
* treated as a change. Keep the field list in sync with the server.
|
|
76
|
+
*/
|
|
77
|
+
const sessionsFingerprint = (sessions) => sessions
|
|
78
|
+
.map((s) => JSON.stringify([
|
|
79
|
+
s.name, s.attached, s.agentKind, s.agentSessionId ?? null,
|
|
80
|
+
s.project, s.label ?? null, s.createdAt ?? null,
|
|
81
|
+
s.lastActivityAt ?? null, s.state ?? null,
|
|
82
|
+
s.stateChangedAt ?? null, s.stateRuleId ?? null,
|
|
83
|
+
]))
|
|
84
|
+
.sort()
|
|
85
|
+
.join('|');
|
|
86
|
+
exports.sessionsFingerprint = sessionsFingerprint;
|
|
87
|
+
/** True when this cycle's inventory must be sent: it changed, or the
|
|
88
|
+
* idle heartbeat is due. */
|
|
89
|
+
const sessionsReportDue = (fingerprint, lastSentFingerprint, lastSentAtMs, nowMs) => fingerprint !== lastSentFingerprint ||
|
|
90
|
+
nowMs - lastSentAtMs >= exports.SESSION_REPORT_HEARTBEAT_MS;
|
|
91
|
+
exports.sessionsReportDue = sessionsReportDue;
|
|
61
92
|
// Per-session token bucket — caps a runaway/compromised sender. 30/min
|
|
62
93
|
// is generous for human-driven phone use, tight enough to block flooding.
|
|
63
94
|
const RATE_LIMIT_TOKENS = 30;
|
|
@@ -1005,10 +1036,24 @@ exports.computeBackoff = computeBackoff;
|
|
|
1005
1036
|
* (otherwise the phone's session inventory grows a new ghost device every
|
|
1006
1037
|
* time `zeph listener` rebinds). `dev_listener_<sha8(hostname)>` keeps it
|
|
1007
1038
|
* human-recognisable in dev logs without leaking the raw hostname.
|
|
1039
|
+
*
|
|
1040
|
+
* The no-arg result is pinned for the process lifetime: macOS renames the
|
|
1041
|
+
* hostname with network changes, and a mid-run drift made the screen-peek
|
|
1042
|
+
* check (fresh recompute) reject requests addressed to the id we CONNECTED
|
|
1043
|
+
* with — the phone targets the id the sessions were reported under, so
|
|
1044
|
+
* every caller in one process must see the same value.
|
|
1008
1045
|
*/
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1046
|
+
let processListenerDeviceId;
|
|
1047
|
+
const computeListenerDeviceId = (host) => {
|
|
1048
|
+
if (host !== undefined) {
|
|
1049
|
+
const h = (0, crypto_1.createHash)('sha256').update(host).digest('hex').slice(0, 8);
|
|
1050
|
+
return `dev_listener_${h}`;
|
|
1051
|
+
}
|
|
1052
|
+
if (!processListenerDeviceId) {
|
|
1053
|
+
const h = (0, crypto_1.createHash)('sha256').update((0, os_1.hostname)()).digest('hex').slice(0, 8);
|
|
1054
|
+
processListenerDeviceId = `dev_listener_${h}`;
|
|
1055
|
+
}
|
|
1056
|
+
return processListenerDeviceId;
|
|
1012
1057
|
};
|
|
1013
1058
|
exports.computeListenerDeviceId = computeListenerDeviceId;
|
|
1014
1059
|
/**
|
|
@@ -1080,22 +1125,37 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
1080
1125
|
sock.terminate();
|
|
1081
1126
|
}
|
|
1082
1127
|
}, WS_CONNECT_TIMEOUT_MS);
|
|
1128
|
+
// Report gate state — per connection on purpose: a fresh socket
|
|
1129
|
+
// must always send its first inventory (the server may have
|
|
1130
|
+
// nothing, or another listener's stale data, for this device).
|
|
1131
|
+
let lastSentFingerprint = null;
|
|
1132
|
+
let lastSentAtMs = 0;
|
|
1083
1133
|
const reportSessions = () => {
|
|
1084
1134
|
if (sock.readyState !== ws_1.default.OPEN)
|
|
1085
1135
|
return;
|
|
1086
1136
|
const { sessions, rejected } = (0, exports.collectSessionsVerbose)();
|
|
1087
|
-
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
// record and the next ack prunes it locally.
|
|
1137
|
+
// Watch hits are one-shot events — they go out on every poll,
|
|
1138
|
+
// independent of the sessions-report gate below.
|
|
1139
|
+
// (§S5 v2): probe watched panes and report hits; the server
|
|
1140
|
+
// consumes the record and the next ack prunes it locally.
|
|
1091
1141
|
for (const hit of (0, exports.collectWatchHits)(new Set(sessions.map((s) => s.name)))) {
|
|
1092
1142
|
sock.send(JSON.stringify({ type: 'listener.watch.hit', data: hit }));
|
|
1093
1143
|
log(`🔔 watch hit: ${hit.sessionName} ~ /${hit.pattern}/`);
|
|
1094
1144
|
}
|
|
1095
|
-
//
|
|
1145
|
+
// Unchanged inventory + heartbeat not due → skip the send.
|
|
1146
|
+
// The 5 s local poll above still ran, so a change is never
|
|
1147
|
+
// delayed; only the no-op reports (idle listener) are culled.
|
|
1148
|
+
const fingerprint = (0, exports.sessionsFingerprint)(sessions);
|
|
1149
|
+
if (!(0, exports.sessionsReportDue)(fingerprint, lastSentFingerprint, lastSentAtMs, Date.now())) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
sock.send(JSON.stringify({ type: 'listener.sessions', data: { sessions } }));
|
|
1153
|
+
lastSentFingerprint = fingerprint;
|
|
1154
|
+
lastSentAtMs = Date.now();
|
|
1155
|
+
// One line per send gives the user immediate feedback on
|
|
1096
1156
|
// what the phone picker will see — particularly important
|
|
1097
1157
|
// during setup, when an empty picker has no other observable
|
|
1098
|
-
// cause.
|
|
1158
|
+
// cause. (Skipped cycles stay quiet — same signal, less noise.)
|
|
1099
1159
|
const names = sessions
|
|
1100
1160
|
.map((s) => (s.state ? `${s.name}[${s.state}]` : s.name))
|
|
1101
1161
|
.join(', ') || '∅';
|