@zeph-to/cli 1.19.1 → 1.19.2
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 -0
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +58 -12
- 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. */
|
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;
|
|
@@ -1080,22 +1111,37 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
1080
1111
|
sock.terminate();
|
|
1081
1112
|
}
|
|
1082
1113
|
}, WS_CONNECT_TIMEOUT_MS);
|
|
1114
|
+
// Report gate state — per connection on purpose: a fresh socket
|
|
1115
|
+
// must always send its first inventory (the server may have
|
|
1116
|
+
// nothing, or another listener's stale data, for this device).
|
|
1117
|
+
let lastSentFingerprint = null;
|
|
1118
|
+
let lastSentAtMs = 0;
|
|
1083
1119
|
const reportSessions = () => {
|
|
1084
1120
|
if (sock.readyState !== ws_1.default.OPEN)
|
|
1085
1121
|
return;
|
|
1086
1122
|
const { sessions, rejected } = (0, exports.collectSessionsVerbose)();
|
|
1087
|
-
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
// record and the next ack prunes it locally.
|
|
1123
|
+
// Watch hits are one-shot events — they go out on every poll,
|
|
1124
|
+
// independent of the sessions-report gate below.
|
|
1125
|
+
// (§S5 v2): probe watched panes and report hits; the server
|
|
1126
|
+
// consumes the record and the next ack prunes it locally.
|
|
1091
1127
|
for (const hit of (0, exports.collectWatchHits)(new Set(sessions.map((s) => s.name)))) {
|
|
1092
1128
|
sock.send(JSON.stringify({ type: 'listener.watch.hit', data: hit }));
|
|
1093
1129
|
log(`🔔 watch hit: ${hit.sessionName} ~ /${hit.pattern}/`);
|
|
1094
1130
|
}
|
|
1095
|
-
//
|
|
1131
|
+
// Unchanged inventory + heartbeat not due → skip the send.
|
|
1132
|
+
// The 5 s local poll above still ran, so a change is never
|
|
1133
|
+
// delayed; only the no-op reports (idle listener) are culled.
|
|
1134
|
+
const fingerprint = (0, exports.sessionsFingerprint)(sessions);
|
|
1135
|
+
if (!(0, exports.sessionsReportDue)(fingerprint, lastSentFingerprint, lastSentAtMs, Date.now())) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
sock.send(JSON.stringify({ type: 'listener.sessions', data: { sessions } }));
|
|
1139
|
+
lastSentFingerprint = fingerprint;
|
|
1140
|
+
lastSentAtMs = Date.now();
|
|
1141
|
+
// One line per send gives the user immediate feedback on
|
|
1096
1142
|
// what the phone picker will see — particularly important
|
|
1097
1143
|
// during setup, when an empty picker has no other observable
|
|
1098
|
-
// cause.
|
|
1144
|
+
// cause. (Skipped cycles stay quiet — same signal, less noise.)
|
|
1099
1145
|
const names = sessions
|
|
1100
1146
|
.map((s) => (s.state ? `${s.name}[${s.state}]` : s.name))
|
|
1101
1147
|
.join(', ') || '∅';
|