@zeph-to/cli 1.22.0 → 1.24.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 +21 -24
- package/dist/cli.js +51 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +60 -1
- package/dist/listener-device-id.d.ts +10 -0
- package/dist/listener-device-id.d.ts.map +1 -0
- package/dist/listener-device-id.js +62 -0
- package/dist/listener.d.ts +54 -0
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +245 -6
- package/dist/zeph-hook.d.ts +8 -0
- package/dist/zeph-hook.d.ts.map +1 -1
- package/dist/zeph-hook.js +15 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,31 +5,19 @@
|
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
side of the round trip: your agent works, hits a decision, and asks your
|
|
10
|
-
phone; you tap a button (or type a reply), and the answer lands back in
|
|
11
|
-
the live session so the agent keeps going.
|
|
12
|
-
|
|
13
|
-
- `ZephHook` SDK — native `fetch`, no runtime deps. Send/list/dismiss pushes.
|
|
14
|
-
- `zeph` CLI — one-command setup for 8 AI agents, push sending, and an
|
|
15
|
-
optional resident listener that **drives Claude Code / Codex / Gemini
|
|
16
|
-
sessions from your phone** by injecting messages into named tmux
|
|
17
|
-
sessions.
|
|
18
|
-
|
|
19
|
-
Part of the Zeph toolchain:
|
|
20
|
-
[`@zeph-to/mcp-server`](https://github.com/zeph-to/mcp-server) (the MCP
|
|
21
|
-
tools your agent calls, e.g. `zeph_ask`) ·
|
|
22
|
-
[`zeph-to/plugin`](https://github.com/zeph-to/plugin) (Claude Code plugin
|
|
23
|
-
bundling hooks + MCP + rules) · the [Zeph app](https://zeph.to) on your
|
|
24
|
-
phone.
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
8
|
+
**Your agent works, hits a decision, and asks your phone. You tap a button (or type a reply), and the answer lands back in the live session — so the agent keeps going.**
|
|
27
9
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
10
|
+
`@zeph-to/cli` is the terminal side of that round trip: a zero-dependency push SDK, a `zeph` CLI that wires up 8 AI agents in one command, and a resident listener that lets your phone **drive Claude Code / Codex / Gemini sessions** by typing straight into named tmux sessions.
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://zeph.to/readme/demo.gif" alt="Agent asks 'Deploy to prod?' on your phone; you tap Deploy; the session ships" width="560"><br>
|
|
14
|
+
<sub><em>Your agent asks on your phone → you tap <b>Deploy</b> → the session ships. No terminal.</em></sub>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
- **`ZephHook` SDK** — native `fetch`, no runtime deps. Send / list / dismiss pushes.
|
|
18
|
+
- **`zeph` CLI** — one-command setup for 8 agents, push sending, and the resident listener for phone-driven remote control.
|
|
19
|
+
|
|
20
|
+
Part of the Zeph toolchain: [`@zeph-to/mcp-server`](https://github.com/zeph-to/mcp-server) (the MCP tools your agent calls, e.g. `zeph_ask`) · [`zeph-to/plugin`](https://github.com/zeph-to/plugin) (Claude Code plugin bundling hooks + MCP + rules) · the [Zeph app](https://zeph.to) on your phone.
|
|
33
21
|
|
|
34
22
|
## Quick Start
|
|
35
23
|
|
|
@@ -92,6 +80,10 @@ To **drive a Claude Code / Codex / Gemini session from your phone**, see
|
|
|
92
80
|
> Send messages from your phone *into* a live Claude Code / Codex /
|
|
93
81
|
> Gemini session — even after a `zeph_ask` polling window has expired.
|
|
94
82
|
|
|
83
|
+
<p align="center">
|
|
84
|
+
<img src="https://zeph.to/readme/ask-phone.png" alt="A Zeph hook on the phone: a question with tappable answer buttons and a text field" width="300">
|
|
85
|
+
</p>
|
|
86
|
+
|
|
95
87
|
The MCP tools `zeph_ask` / `zeph_prompt` / `zeph_input` wait on a fixed
|
|
96
88
|
timeout (120–600 s). Once that window closes the
|
|
97
89
|
session becomes unaddressable from the phone, even though it's still
|
|
@@ -323,6 +315,10 @@ zeph list --limit 10 --type note
|
|
|
323
315
|
zeph dismiss push_01JXY...
|
|
324
316
|
zeph dismiss --all
|
|
325
317
|
|
|
318
|
+
# Rename the current agent session (shows in the app's Agents list)
|
|
319
|
+
zeph rename "Prod deploy"
|
|
320
|
+
zeph rename --clear # reset to the default name
|
|
321
|
+
|
|
326
322
|
# Test connection
|
|
327
323
|
zeph test
|
|
328
324
|
|
|
@@ -351,6 +347,7 @@ zeph notify --title "Hello" --json
|
|
|
351
347
|
| `notify` | Send a push notification |
|
|
352
348
|
| `list` | List recent push notifications |
|
|
353
349
|
| `dismiss <id>` | Dismiss a push (or `--all`) |
|
|
350
|
+
| `rename <name>` | Set the current agent session's display name in the app — run inside a `zeph cc` session (`--clear` resets). Auto-detects the tmux session + this machine's listener device id, so the alias lands on the right device |
|
|
354
351
|
| `test` | Verify connection and API key |
|
|
355
352
|
| `cc` · `codex` · `gemini` | Run the agent in a `zeph-<project>` tmux session (auto-suffixed `-2`, `-3`, … on attached collisions). Auto-spawns the background listener on first invocation so the phone picker just works. Trailing args pass through to the agent (`zeph cc --resume "..."`) |
|
|
356
353
|
| `listener` | (Usually unnecessary — `zeph cc` autospawns it.) Resident daemon: subscribes via WebSocket, reports tmux session inventory every 5 s, injects `agent.command` pushes into the matching session. Run in the foreground for SDK development; otherwise let `zeph cc` manage it |
|
package/dist/cli.js
CHANGED
|
@@ -70,6 +70,8 @@ Commands:
|
|
|
70
70
|
notify Send a push notification
|
|
71
71
|
list List recent push notifications
|
|
72
72
|
dismiss <id> Dismiss a push notification (or --all)
|
|
73
|
+
rename <name> Set this agent session's display name in the app
|
|
74
|
+
(run inside a zeph cc session; --clear resets it)
|
|
73
75
|
test Send a test notification to verify setup
|
|
74
76
|
${usageAgentLines()}
|
|
75
77
|
(auto-suffixed -2/-3/… when another zeph cc is already
|
|
@@ -163,6 +165,53 @@ const createHook = (args) => {
|
|
|
163
165
|
...(baseUrl && { baseUrl }),
|
|
164
166
|
});
|
|
165
167
|
};
|
|
168
|
+
/** Current tmux session name (the rename key), or null when not inside tmux. */
|
|
169
|
+
const detectCurrentTmuxSession = () => {
|
|
170
|
+
if (!process.env.TMUX)
|
|
171
|
+
return null;
|
|
172
|
+
try {
|
|
173
|
+
const name = (0, child_process_1.execFileSync)('tmux', ['display-message', '-p', '#S'], { encoding: 'utf-8' }).trim();
|
|
174
|
+
return name || null;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* `zeph rename "<name>"` — set THIS agent session's display name in the app.
|
|
182
|
+
* Auto-detects the current tmux session + this machine's listener device id
|
|
183
|
+
* (`computeListenerDeviceId`, the same id the listener registers, so the
|
|
184
|
+
* rename lands on the right device). `--clear` resets to the computed label.
|
|
185
|
+
*/
|
|
186
|
+
const handleRename = async (args) => {
|
|
187
|
+
const isJson = args.json === true;
|
|
188
|
+
const clearing = args.clear === true;
|
|
189
|
+
const alias = args._arg1 || '';
|
|
190
|
+
if (!alias && !clearing) {
|
|
191
|
+
printError('Usage: zeph rename "New name" (or --clear to reset to default)', isJson);
|
|
192
|
+
return 2;
|
|
193
|
+
}
|
|
194
|
+
const sessionName = args.session || detectCurrentTmuxSession();
|
|
195
|
+
if (!sessionName) {
|
|
196
|
+
printError('No tmux session detected. Run inside a `zeph cc` session, or pass --session <name>.', isJson);
|
|
197
|
+
return 2;
|
|
198
|
+
}
|
|
199
|
+
const deviceId = args.device || (0, listener_js_1.computeListenerDeviceId)();
|
|
200
|
+
const hook = createHook(args);
|
|
201
|
+
if (!hook)
|
|
202
|
+
return 3;
|
|
203
|
+
try {
|
|
204
|
+
await hook.renameAgentSession(deviceId, sessionName, clearing ? '' : alias);
|
|
205
|
+
if (isJson)
|
|
206
|
+
printJson({ session: sessionName, alias: clearing ? null : alias, status: 'ok' });
|
|
207
|
+
else
|
|
208
|
+
console.log(clearing ? `Reset name for ${sessionName}` : `Renamed ${sessionName} → ${alias}`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
return handleError(err, isJson);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
166
215
|
/** Parse a gate count flag; garbage input falls back to the default (never accidentally silences). */
|
|
167
216
|
const gateCount = (raw, fallback) => {
|
|
168
217
|
const n = typeof raw === 'string' ? Number(raw) : NaN;
|
|
@@ -408,6 +457,8 @@ const main = async () => {
|
|
|
408
457
|
return handleList(args);
|
|
409
458
|
case 'dismiss':
|
|
410
459
|
return handleDismiss(args);
|
|
460
|
+
case 'rename':
|
|
461
|
+
return handleRename(args);
|
|
411
462
|
case 'test':
|
|
412
463
|
return handleTest(args);
|
|
413
464
|
case 'listener':
|
package/dist/crypto.d.ts
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
* key). That refactor is on the roadmap; until then, treat push bodies as
|
|
28
28
|
* sensitive-but-not-secret.
|
|
29
29
|
*/
|
|
30
|
+
interface EncryptedPayload {
|
|
31
|
+
ciphertext: string;
|
|
32
|
+
iv: string;
|
|
33
|
+
encryptedKey: string;
|
|
34
|
+
keyIv: string;
|
|
35
|
+
}
|
|
30
36
|
/**
|
|
31
37
|
* Initialize crypto: sync keys with server, then fallback to local/generate.
|
|
32
38
|
* Server is source of truth for per-user key pair.
|
|
@@ -80,4 +86,25 @@ export declare const encryptFileForSelf: (content: string) => Promise<{
|
|
|
80
86
|
iv: string;
|
|
81
87
|
encryptedKey: string;
|
|
82
88
|
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Flat ephemeral envelope — field-for-field what the web's decrypt()
|
|
91
|
+
* (libs/crypto) consumes, with the sender public key riding along so the
|
|
92
|
+
* receiver needs no out-of-band key lookup.
|
|
93
|
+
*/
|
|
94
|
+
export interface EncryptedEphemeralPayload extends EncryptedPayload {
|
|
95
|
+
senderPublicKey: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Load-or-create the per-device keypair. Local-only — no server round-trip,
|
|
99
|
+
* no upload. Safe to call concurrently (dedupes to one init). Returns the
|
|
100
|
+
* exported public key (Base64 SPKI).
|
|
101
|
+
*/
|
|
102
|
+
export declare const initDeviceCrypto: () => Promise<string>;
|
|
103
|
+
export declare const getDevicePublicKey: () => string | null;
|
|
104
|
+
/**
|
|
105
|
+
* Encrypt an ephemeral payload (e.g. a stream frame) for one recipient
|
|
106
|
+
* device. Requires initDeviceCrypto() to have completed.
|
|
107
|
+
*/
|
|
108
|
+
export declare const encryptEphemeral: (plaintext: string, recipientPublicKeyRaw: string) => Promise<EncryptedEphemeralPayload>;
|
|
109
|
+
export {};
|
|
83
110
|
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+EH,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA6ED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AA2CF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC;AAWF;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,eAAe,EAAE,MAAM,CAAC;CACzB;AAsBD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,OAAO,CAAC,MAAM,CAoBjD,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAA+B,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,uBAAuB,MAAM,KAC5B,OAAO,CAAC,yBAAyB,CAKnC,CAAC"}
|
package/dist/crypto.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* sensitive-but-not-secret.
|
|
30
30
|
*/
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
|
|
32
|
+
exports.encryptEphemeral = exports.getDevicePublicKey = exports.initDeviceCrypto = exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
|
|
33
33
|
/// <reference lib="dom" />
|
|
34
34
|
const fs_1 = require("fs");
|
|
35
35
|
const os_1 = require("os");
|
|
@@ -301,3 +301,62 @@ const encryptFileForSelf = async (content) => {
|
|
|
301
301
|
};
|
|
302
302
|
};
|
|
303
303
|
exports.encryptFileForSelf = encryptFileForSelf;
|
|
304
|
+
const DEVICE_KEYS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
|
|
305
|
+
const DEVICE_KEYS_PATH = (0, path_1.join)(DEVICE_KEYS_DIR, 'device-keys.json');
|
|
306
|
+
let deviceKeyPair = null;
|
|
307
|
+
let deviceExportedPublicKey = null;
|
|
308
|
+
let deviceInitPromise = null;
|
|
309
|
+
const loadStoredDeviceKeys = () => {
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse((0, fs_1.readFileSync)(DEVICE_KEYS_PATH, 'utf-8'));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const storeDeviceKeys = (exported) => {
|
|
318
|
+
(0, fs_1.mkdirSync)(DEVICE_KEYS_DIR, { recursive: true, mode: 0o700 });
|
|
319
|
+
(0, fs_1.writeFileSync)(DEVICE_KEYS_PATH, JSON.stringify(exported, null, 2), { mode: 0o600 });
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Load-or-create the per-device keypair. Local-only — no server round-trip,
|
|
323
|
+
* no upload. Safe to call concurrently (dedupes to one init). Returns the
|
|
324
|
+
* exported public key (Base64 SPKI).
|
|
325
|
+
*/
|
|
326
|
+
const initDeviceCrypto = () => {
|
|
327
|
+
if (deviceInitPromise)
|
|
328
|
+
return deviceInitPromise;
|
|
329
|
+
deviceInitPromise = (async () => {
|
|
330
|
+
const stored = loadStoredDeviceKeys();
|
|
331
|
+
if (stored) {
|
|
332
|
+
deviceKeyPair = await importKeyPair(stored);
|
|
333
|
+
deviceExportedPublicKey = stored.publicKey;
|
|
334
|
+
return stored.publicKey;
|
|
335
|
+
}
|
|
336
|
+
const keyPair = await generateKeyPair();
|
|
337
|
+
const exported = await exportKeyPair(keyPair);
|
|
338
|
+
storeDeviceKeys(exported);
|
|
339
|
+
deviceKeyPair = keyPair;
|
|
340
|
+
deviceExportedPublicKey = exported.publicKey;
|
|
341
|
+
return exported.publicKey;
|
|
342
|
+
})().catch((err) => {
|
|
343
|
+
deviceInitPromise = null;
|
|
344
|
+
throw err;
|
|
345
|
+
});
|
|
346
|
+
return deviceInitPromise;
|
|
347
|
+
};
|
|
348
|
+
exports.initDeviceCrypto = initDeviceCrypto;
|
|
349
|
+
const getDevicePublicKey = () => deviceExportedPublicKey;
|
|
350
|
+
exports.getDevicePublicKey = getDevicePublicKey;
|
|
351
|
+
/**
|
|
352
|
+
* Encrypt an ephemeral payload (e.g. a stream frame) for one recipient
|
|
353
|
+
* device. Requires initDeviceCrypto() to have completed.
|
|
354
|
+
*/
|
|
355
|
+
const encryptEphemeral = async (plaintext, recipientPublicKeyRaw) => {
|
|
356
|
+
if (!deviceKeyPair || !deviceExportedPublicKey)
|
|
357
|
+
throw new Error('Device crypto not initialized');
|
|
358
|
+
const recipientKey = await importPublicKey(recipientPublicKeyRaw);
|
|
359
|
+
const payload = await encrypt(plaintext, deviceKeyPair.privateKey, recipientKey);
|
|
360
|
+
return { ...payload, senderPublicKey: deviceExportedPublicKey };
|
|
361
|
+
};
|
|
362
|
+
exports.encryptEphemeral = encryptEphemeral;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listener device id, resolved READ-ONLY — MUST equal what `listener.ts`
|
|
3
|
+
* `computeListenerDeviceId()` registers, so pushes tagged with this id land
|
|
4
|
+
* under the listener's device (otherwise a hook/notify reaches the feed but
|
|
5
|
+
* never threads into the agent chat). Hash the platform machine id first, then
|
|
6
|
+
* the sticky file the listener persisted, then a hostname hash — the same order
|
|
7
|
+
* as the writer, but never writing the file here.
|
|
8
|
+
*/
|
|
9
|
+
export declare const listenerDeviceId: () => string;
|
|
10
|
+
//# sourceMappingURL=listener-device-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener-device-id.d.ts","sourceRoot":"","sources":["../src/listener-device-id.ts"],"names":[],"mappings":"AAuCA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,QAAO,MAQnC,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listenerDeviceId = 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
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
// The listener daemon writes this file (see listener.ts computeListenerDeviceId);
|
|
10
|
+
// non-listener processes in the CLI (zeph-hook's agent-session context) only
|
|
11
|
+
// READ it, so both resolve the same id without racing the write.
|
|
12
|
+
const LISTENER_ID_FILE = (0, node_path_1.join)((0, node_os_1.homedir)(), '.zeph', 'listener-device-id');
|
|
13
|
+
const hashListenerId = (seed) => `dev_listener_${(0, node_crypto_1.createHash)('sha256').update(seed).digest('hex').slice(0, 8)}`;
|
|
14
|
+
/** Platform machine id (macOS IOPlatformUUID / Linux machine-id), or null. */
|
|
15
|
+
const readMachineId = () => {
|
|
16
|
+
try {
|
|
17
|
+
if (process.platform === 'darwin') {
|
|
18
|
+
// stdio: swallow ioreg's stderr so a one-off `zeph rename` / hook
|
|
19
|
+
// never leaks ioreg warnings to the terminal (matches listener.ts).
|
|
20
|
+
const out = (0, node_child_process_1.execFileSync)('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
});
|
|
24
|
+
const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
25
|
+
if (m)
|
|
26
|
+
return m[1];
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === 'linux') {
|
|
29
|
+
for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
|
|
30
|
+
try {
|
|
31
|
+
const v = (0, node_fs_1.readFileSync)(p, 'utf-8').trim();
|
|
32
|
+
if (v)
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
catch { /* try next path */ }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { /* no machine id readable — fall through */ }
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Listener device id, resolved READ-ONLY — MUST equal what `listener.ts`
|
|
44
|
+
* `computeListenerDeviceId()` registers, so pushes tagged with this id land
|
|
45
|
+
* under the listener's device (otherwise a hook/notify reaches the feed but
|
|
46
|
+
* never threads into the agent chat). Hash the platform machine id first, then
|
|
47
|
+
* the sticky file the listener persisted, then a hostname hash — the same order
|
|
48
|
+
* as the writer, but never writing the file here.
|
|
49
|
+
*/
|
|
50
|
+
const listenerDeviceId = () => {
|
|
51
|
+
const machineId = readMachineId();
|
|
52
|
+
if (machineId)
|
|
53
|
+
return hashListenerId(machineId);
|
|
54
|
+
try {
|
|
55
|
+
const saved = (0, node_fs_1.readFileSync)(LISTENER_ID_FILE, 'utf-8').trim();
|
|
56
|
+
if (/^dev_listener_[0-9a-f]{8}$/.test(saved))
|
|
57
|
+
return saved;
|
|
58
|
+
}
|
|
59
|
+
catch { /* no sticky file — fall back to hostname */ }
|
|
60
|
+
return hashListenerId((0, node_os_1.hostname)());
|
|
61
|
+
};
|
|
62
|
+
exports.listenerDeviceId = listenerDeviceId;
|
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
|
+
import { type EncryptedEphemeralPayload } from './crypto.js';
|
|
25
26
|
export declare const SESSION_REPORT_HEARTBEAT_MS = 30000;
|
|
26
27
|
interface AgentSession {
|
|
27
28
|
name: string;
|
|
@@ -167,6 +168,59 @@ export declare const handleScreenRequest: (req: ScreenRequest) => ScreenSnapshot
|
|
|
167
168
|
* captures so a burst of arrow presses yields frames after the LAST press.
|
|
168
169
|
*/
|
|
169
170
|
export declare const scheduleInjectSnapshots: (sessionName: string, send: (data: Record<string, unknown>) => void, delays?: number[]) => void;
|
|
171
|
+
export interface StreamControl {
|
|
172
|
+
subtype?: string;
|
|
173
|
+
targetDeviceId?: string;
|
|
174
|
+
sessionName?: string;
|
|
175
|
+
/** Subscriber's device public key (Base64 SPKI) — presence turns on E2EE. */
|
|
176
|
+
subscriberPublicKey?: string;
|
|
177
|
+
}
|
|
178
|
+
export declare const MAX_CONCURRENT_STREAMS = 3;
|
|
179
|
+
export declare const stopStream: (sessionName: string) => void;
|
|
180
|
+
export declare const stopAllStreams: () => void;
|
|
181
|
+
/** Wire shape of one live-mirror data frame (before the relay's ephemeral wrap). */
|
|
182
|
+
export type StreamFramePayload = {
|
|
183
|
+
subtype: 'agent.stream.frame';
|
|
184
|
+
sessionName: string;
|
|
185
|
+
capturedAt: string;
|
|
186
|
+
truncated: boolean;
|
|
187
|
+
/** Capture-order stamp — attached at send time, not by buildStreamFrame. */
|
|
188
|
+
seq?: number;
|
|
189
|
+
/** Stream incarnation (stats.startedAt) — lets the receiver reset its
|
|
190
|
+
* seq high-water mark when the daemon restarts the stream. */
|
|
191
|
+
epoch?: number;
|
|
192
|
+
/** Plaintext pane content — only on unencrypted streams. */
|
|
193
|
+
content?: string;
|
|
194
|
+
/** E2EE envelope — replaces `content` on encrypted streams. */
|
|
195
|
+
encrypted?: EncryptedEphemeralPayload;
|
|
196
|
+
};
|
|
197
|
+
/** Wire shape of a live-mirror error frame — same subtype, no pane data. */
|
|
198
|
+
export type StreamErrorFrame = {
|
|
199
|
+
subtype: 'agent.stream.frame';
|
|
200
|
+
sessionName: string;
|
|
201
|
+
error: 'unknown_session' | 'e2ee_unavailable' | 'encrypt_failed' | 'stream_limit';
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Per-listener concurrency guard, as a pure predicate so the boundary is
|
|
205
|
+
* unit-testable without touching the module-scope `activeStreams` map.
|
|
206
|
+
*/
|
|
207
|
+
export declare const isStreamCapReached: (activeCount: number) => boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Build the wire payload for one stream frame. With a subscriber public key
|
|
210
|
+
* the pane content rides ONLY inside the E2EE envelope; an encrypt failure
|
|
211
|
+
* (bad key, crypto not ready on the first tick) returns null so the caller
|
|
212
|
+
* drops the frame — an encrypted stream never leaks a plaintext frame.
|
|
213
|
+
*/
|
|
214
|
+
export declare const buildStreamFrame: (captured: {
|
|
215
|
+
content: string;
|
|
216
|
+
truncated: boolean;
|
|
217
|
+
}, sessionName: string, subscriberPublicKey?: string) => Promise<StreamFramePayload | null>;
|
|
218
|
+
/**
|
|
219
|
+
* Handle agent.stream.start / agent.stream.stop. Returns true when the message
|
|
220
|
+
* was a stream-control message (so the caller skips the one-shot screen-peek
|
|
221
|
+
* path). start is idempotent — a repeat restarts the loop.
|
|
222
|
+
*/
|
|
223
|
+
export declare const handleStreamControl: (req: StreamControl, send: (data: Record<string, unknown>) => void) => boolean;
|
|
170
224
|
export interface CollectResult {
|
|
171
225
|
sessions: AgentSession[];
|
|
172
226
|
/** Diagnostic notes per rejected session — surfaced under `--verbose`. */
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAUH,OAAO,EAA2B,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzG,OAAO,EAAiD,KAAK,UAAU,EAA4C,MAAM,kBAAkB,CAAC;AAE5I,OAAO,EAA0D,KAAK,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAmCrH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,YAAY,EAAE,KAAG,MAS7C,CAAC;AAEnB;6BAC6B;AAC7B,eAAO,MAAM,iBAAiB,GAC1B,aAAa,MAAM,EACnB,qBAAqB,MAAM,GAAG,IAAI,EAClC,cAAc,MAAM,EACpB,OAAO,MAAM,KACd,OAEoD,CAAC;AAYxD,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAenF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,MAAK,MAAmB,KAAG,OAgB1E,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAO7D,CAAC;AAsCF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,KAAG,MAAM,EAAE,GAAG,IAQvD,CAAC;AA0CF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAG5C,CAAC;AA8NF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAK3F,CAAC;AAEF,UAAU,QAAQ;IACd,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAiDD;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,QAAQ,KAAG,qBAAqB,GAAG,IAGhE,CAAC;AA0BZ,iBAAiB;AACjB,eAAO,MAAM,kBAAkB,QAAO,IAErC,CAAC;AAWF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC3B,MAAM,MAAM,EACZ,WAAW,SAAS,EACpB,UAAU,MAAM,GAAG,IAAI,EACvB,MAAK,MAAmB,KACzB,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,gBAAgB,GAAG,aAAa,CAmB/D,CAAC;AAWF,MAAM,WAAW,YAAY;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACnB;AASD,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,GAAI,KAAK,OAAO,KAAG,IAWhD,CAAC;AAEF,iBAAiB;AACjB,eAAO,MAAM,mBAAmB,QAAO,IAGtC,CAAC;AAEF,MAAM,WAAW,QAAQ;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GACzB,WAAW,WAAW,CAAC,MAAM,CAAC,EAC9B,UAAS,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAsB,KAC9D,QAAQ,EAgBV,CAAC;AAWF,MAAM,WAAW,aAAa;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,uBAAuB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,aAAa,KAAG,cAAc,GAAG,IAmBzE,CAAC;AAyCF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,GAChC,aAAa,MAAM,EACnB,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAC7C,SAAQ,MAAM,EAA8B,KAC7C,IAiBF,CAAC;AAcF,MAAM,WAAW,aAAa;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAChC;AAmBD,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAyCxC,eAAO,MAAM,UAAU,GAAI,aAAa,MAAM,KAAG,IAYhD,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAGjC,CAAC;AAEF,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;mEAC+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,yBAAyB,CAAC;CACzC,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,cAAc,CAAC;CACrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,aAAa,MAAM,KAAG,OAChB,CAAC;AAQ1C;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GACzB,UAAU;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,EACjD,aAAa,MAAM,EACnB,sBAAsB,MAAM,KAC7B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAenC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC5B,KAAK,aAAa,EAClB,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,KAC9C,OA0GF,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAO,aAiEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,QAAO,YAAY,EAAuC,CAAC;AAIvF;;;;;GAKG;AACH,UAAU,kBAAkB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,QAAQ;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;sEACkE;IAClE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;IAC1D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzF,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC7C;sEACkE;IAClE,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAwBD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC1B,SAAS,MAAM,EACf,MAAM,MAAM,EACZ,MAAK,MAAM,MAAiB,KAC7B,OAUF,CAAC;AAwCF,eAAO,MAAM,oBAAoB,GAAI,KAAK;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAG,IAE/E,CAAC;AA2FF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACtB,MAAK,MAAmB,EACxB,MAAK,MAAwB,EAC7B,MAAK,MAA0B,KAChC,MAaF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,QAAQ,EACd,OAAM,cAAmB,KAC1B,OAAO,CAAC,OAAO,CAoCjB,CAAC;AAcF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAIhD,CAAC;AA4DF,eAAO,MAAM,uBAAuB,GAAI,OAAO,MAAM,KAAG,MA2BvD,CAAC;AAgPF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GACrB,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EACtC,QAAQ;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1B,SAAS,MAAM,KAChB,MAAM,GAAG,IAIX,CAAC;AA6CF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA+H3F,CAAC"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.scheduleInjectSnapshots = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
|
|
28
|
+
exports.handleListener = exports.resolveWsUrl = exports.computeListenerDeviceId = exports.computeBackoff = exports.handlePush = exports.gcAttachments = exports.setAttachmentContext = exports.writeRemoteMarker = exports.collectSessions = exports.collectSessionsVerbose = exports.handleStreamControl = exports.buildStreamFrame = exports.isStreamCapReached = exports.stopAllStreams = exports.stopStream = exports.MAX_CONCURRENT_STREAMS = exports.scheduleInjectSnapshots = exports.handleScreenRequest = exports.collectWatchHits = exports.resetPatternWatches = exports.setPatternWatches = exports.deriveSessionState = exports.resetSessionStates = exports.detectRemoteAgent = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.resolveKeys = exports.paneCurrentCommand = exports.checkRateLimit = exports.AUTH_FAILURE_CODES = exports.sessionsReportDue = exports.sessionsFingerprint = exports.SESSION_REPORT_HEARTBEAT_MS = void 0;
|
|
29
29
|
const child_process_1 = require("child_process");
|
|
30
30
|
const crypto_1 = require("crypto");
|
|
31
31
|
const fs_1 = require("fs");
|
|
@@ -37,6 +37,7 @@ const gate_js_1 = require("./gate.js");
|
|
|
37
37
|
const remote_agents_js_1 = require("./remote-agents.js");
|
|
38
38
|
const agent_state_js_1 = require("./agent-state.js");
|
|
39
39
|
const agent_rules_fetch_js_1 = require("./agent-rules-fetch.js");
|
|
40
|
+
const crypto_js_1 = require("./crypto.js");
|
|
40
41
|
const PING_INTERVAL_MS = 25_000;
|
|
41
42
|
const PONG_TIMEOUT_MS = 10_000;
|
|
42
43
|
const RECONNECT_BASE_MS = 1_000;
|
|
@@ -164,6 +165,11 @@ const ALLOWED_KEYS = {
|
|
|
164
165
|
left: 'Left',
|
|
165
166
|
right: 'Right',
|
|
166
167
|
enter: 'Enter',
|
|
168
|
+
tab: 'Tab',
|
|
169
|
+
backtab: 'BTab',
|
|
170
|
+
backspace: 'BSpace',
|
|
171
|
+
delete: 'DC',
|
|
172
|
+
space: 'Space',
|
|
167
173
|
};
|
|
168
174
|
/**
|
|
169
175
|
* Translate a phone-supplied key list to tmux tokens. Returns null if ANY
|
|
@@ -692,8 +698,16 @@ const handleScreenRequest = (req) => {
|
|
|
692
698
|
exports.handleScreenRequest = handleScreenRequest;
|
|
693
699
|
/** Raw pane capture + size cap. Shared by the request path and the
|
|
694
700
|
* post-injection auto-snapshot. */
|
|
695
|
-
const capturePane = (sessionName) => {
|
|
696
|
-
|
|
701
|
+
const capturePane = (sessionName, escapes = false, lines = SCREEN_PEEK_LINES) => {
|
|
702
|
+
// `-e` keeps ANSI/color escapes for the xterm.js live stream. The
|
|
703
|
+
// screen-peek path leaves it off so its <pre> renderer stays plain text.
|
|
704
|
+
// `lines` sets how far back the capture reaches — the live stream grabs
|
|
705
|
+
// more history so the mirror has room to scroll; SCREEN_PEEK_MAX_BYTES
|
|
706
|
+
// still caps the payload below.
|
|
707
|
+
const captureArgs = escapes
|
|
708
|
+
? ['capture-pane', '-p', '-e', '-t', sessionName, '-S', `-${lines}`]
|
|
709
|
+
: ['capture-pane', '-p', '-t', sessionName, '-S', `-${lines}`];
|
|
710
|
+
const r = (0, child_process_1.spawnSync)('tmux', tmuxArgs(captureArgs), {
|
|
697
711
|
encoding: 'utf-8',
|
|
698
712
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
699
713
|
});
|
|
@@ -743,6 +757,221 @@ const scheduleInjectSnapshots = (sessionName, send, delays = INJECT_SNAPSHOT_DEL
|
|
|
743
757
|
pendingInjectSnapshots.set(sessionName, timers);
|
|
744
758
|
};
|
|
745
759
|
exports.scheduleInjectSnapshots = scheduleInjectSnapshots;
|
|
760
|
+
// ~2.5 fps ceiling. Cadence + diff-gating are the ONLY bound on API Gateway
|
|
761
|
+
// WS cost (the $default route has no per-message limit), so keep it modest
|
|
762
|
+
// and let unchanged frames drop.
|
|
763
|
+
// How far back the live-stream capture reaches — more than the screen-peek
|
|
764
|
+
// window so the mirror has scrollback to scroll through. SCREEN_PEEK_MAX_BYTES
|
|
765
|
+
// still caps the actual payload, so very wide panes get top-truncated.
|
|
766
|
+
const STREAM_CAPTURE_LINES = 200;
|
|
767
|
+
const STREAM_INTERVAL_MS = 400;
|
|
768
|
+
// Orphan guard: a phone that dies without sending agent.stream.stop must not
|
|
769
|
+
// leak an interval forever. Auto-stop after this long; the phone re-subscribes
|
|
770
|
+
// on reopen.
|
|
771
|
+
const STREAM_MAX_MS = 5 * 60_000;
|
|
772
|
+
// Per-listener concurrency guard: the API Gateway WS stage throttle (50 rps)
|
|
773
|
+
// is SHARED across every user, so a runaway machine (many sessions, or a
|
|
774
|
+
// client re-subscribe bug) streaming at 2.5 fps each can starve push delivery
|
|
775
|
+
// and presence for everyone. One phone watches one session at a time; 3 leaves
|
|
776
|
+
// headroom for multiple devices while capping the blast radius of one host.
|
|
777
|
+
exports.MAX_CONCURRENT_STREAMS = 3;
|
|
778
|
+
// Fail-closed guard: consecutive encrypt failures (malformed subscriber key)
|
|
779
|
+
// stop the stream with an error frame instead of retrying forever.
|
|
780
|
+
const STREAM_MAX_ENCRYPT_FAILURES = 3;
|
|
781
|
+
// R2 instrumentation: the daemon's outbound send() IS the API Gateway $default
|
|
782
|
+
// inbound message (1 Lambda + DDB ops + N PostToConnection each), so counting
|
|
783
|
+
// frames/bytes HERE measures the real cloud cost of a live stream. Rolled up to
|
|
784
|
+
// the log every STREAM_LOG_INTERVAL_MS and summarized on stop — the data that
|
|
785
|
+
// decides cloud-throttled (B-lite) vs direct/Tailscale (B-full).
|
|
786
|
+
const STREAM_LOG_INTERVAL_MS = 5_000;
|
|
787
|
+
const activeStreams = new Map();
|
|
788
|
+
const maybeLogStreamStats = (sessionName, stats) => {
|
|
789
|
+
const elapsed = Date.now() - stats.lastLogAt;
|
|
790
|
+
if (elapsed < STREAM_LOG_INTERVAL_MS)
|
|
791
|
+
return;
|
|
792
|
+
const secs = elapsed / 1000;
|
|
793
|
+
const fps = (stats.frames - stats.lastLogFrames) / secs;
|
|
794
|
+
const kbps = (stats.bytes - stats.lastLogBytes) / 1024 / secs;
|
|
795
|
+
log(`⧉ stream ${sessionName}: ${fps.toFixed(1)} fps, ${kbps.toFixed(1)} KB/s (${stats.frames} sent, ${stats.skipped} diff-skipped)`);
|
|
796
|
+
stats.lastLogAt = Date.now();
|
|
797
|
+
stats.lastLogFrames = stats.frames;
|
|
798
|
+
stats.lastLogBytes = stats.bytes;
|
|
799
|
+
};
|
|
800
|
+
const stopStream = (sessionName) => {
|
|
801
|
+
const entry = activeStreams.get(sessionName);
|
|
802
|
+
if (!entry)
|
|
803
|
+
return;
|
|
804
|
+
clearInterval(entry.timer);
|
|
805
|
+
activeStreams.delete(sessionName);
|
|
806
|
+
const { stats } = entry;
|
|
807
|
+
const secs = Math.max(0.001, (Date.now() - stats.startedAt) / 1000);
|
|
808
|
+
log(`⧉ stream ${sessionName} stopped: ${stats.frames} frames / ` +
|
|
809
|
+
`${(stats.bytes / 1024).toFixed(1)} KB over ${secs.toFixed(1)}s ` +
|
|
810
|
+
`(${(stats.frames / secs).toFixed(1)} fps avg, ${stats.skipped} diff-skipped)`);
|
|
811
|
+
};
|
|
812
|
+
exports.stopStream = stopStream;
|
|
813
|
+
const stopAllStreams = () => {
|
|
814
|
+
// Route through stopStream so each stream emits its summary line.
|
|
815
|
+
for (const sessionName of [...activeStreams.keys()])
|
|
816
|
+
(0, exports.stopStream)(sessionName);
|
|
817
|
+
};
|
|
818
|
+
exports.stopAllStreams = stopAllStreams;
|
|
819
|
+
/**
|
|
820
|
+
* Per-listener concurrency guard, as a pure predicate so the boundary is
|
|
821
|
+
* unit-testable without touching the module-scope `activeStreams` map.
|
|
822
|
+
*/
|
|
823
|
+
const isStreamCapReached = (activeCount) => activeCount >= exports.MAX_CONCURRENT_STREAMS;
|
|
824
|
+
exports.isStreamCapReached = isStreamCapReached;
|
|
825
|
+
const streamErrorFrame = (sessionName, error) => ({
|
|
826
|
+
subtype: 'agent.stream.frame',
|
|
827
|
+
sessionName,
|
|
828
|
+
error,
|
|
829
|
+
});
|
|
830
|
+
/**
|
|
831
|
+
* Build the wire payload for one stream frame. With a subscriber public key
|
|
832
|
+
* the pane content rides ONLY inside the E2EE envelope; an encrypt failure
|
|
833
|
+
* (bad key, crypto not ready on the first tick) returns null so the caller
|
|
834
|
+
* drops the frame — an encrypted stream never leaks a plaintext frame.
|
|
835
|
+
*/
|
|
836
|
+
const buildStreamFrame = async (captured, sessionName, subscriberPublicKey) => {
|
|
837
|
+
const base = {
|
|
838
|
+
subtype: 'agent.stream.frame',
|
|
839
|
+
sessionName,
|
|
840
|
+
capturedAt: new Date().toISOString(),
|
|
841
|
+
truncated: captured.truncated,
|
|
842
|
+
};
|
|
843
|
+
if (!subscriberPublicKey)
|
|
844
|
+
return { ...base, content: captured.content };
|
|
845
|
+
try {
|
|
846
|
+
const encrypted = await (0, crypto_js_1.encryptEphemeral)(captured.content, subscriberPublicKey);
|
|
847
|
+
return { ...base, encrypted };
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
log(`⧉ stream ${sessionName}: frame encrypt failed (${err instanceof Error ? err.message : err}) — frame dropped`);
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
exports.buildStreamFrame = buildStreamFrame;
|
|
855
|
+
/**
|
|
856
|
+
* Handle agent.stream.start / agent.stream.stop. Returns true when the message
|
|
857
|
+
* was a stream-control message (so the caller skips the one-shot screen-peek
|
|
858
|
+
* path). start is idempotent — a repeat restarts the loop.
|
|
859
|
+
*/
|
|
860
|
+
const handleStreamControl = (req, send) => {
|
|
861
|
+
if (req.subtype === 'agent.stream.stop') {
|
|
862
|
+
if (req.sessionName)
|
|
863
|
+
(0, exports.stopStream)(req.sessionName);
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
if (req.subtype !== 'agent.stream.start')
|
|
867
|
+
return false;
|
|
868
|
+
// Not addressed to this machine — let other listeners answer.
|
|
869
|
+
if (req.targetDeviceId !== (0, exports.computeListenerDeviceId)())
|
|
870
|
+
return false;
|
|
871
|
+
const sessionName = req.sessionName;
|
|
872
|
+
if (!sessionName)
|
|
873
|
+
return true;
|
|
874
|
+
if (!(0, exports.collectSessions)().some((s) => s.name === sessionName)) {
|
|
875
|
+
send(streamErrorFrame(sessionName, 'unknown_session'));
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
(0, exports.stopStream)(sessionName); // idempotent restart (frees this session's slot first)
|
|
879
|
+
// Concurrency guard AFTER the restart-stop: re-subscribing to an already
|
|
880
|
+
// active session doesn't count against the cap, only a genuinely new one
|
|
881
|
+
// does. Refuse the new stream instead of adding to the shared-throttle load.
|
|
882
|
+
if ((0, exports.isStreamCapReached)(activeStreams.size)) {
|
|
883
|
+
log(`⧉ stream ${sessionName}: refused — ${activeStreams.size}/${exports.MAX_CONCURRENT_STREAMS} streams already active on this listener`);
|
|
884
|
+
send(streamErrorFrame(sessionName, 'stream_limit'));
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
let lastContent = null;
|
|
888
|
+
const stats = {
|
|
889
|
+
startedAt: Date.now(),
|
|
890
|
+
frames: 0,
|
|
891
|
+
bytes: 0,
|
|
892
|
+
skipped: 0,
|
|
893
|
+
lastLogAt: Date.now(),
|
|
894
|
+
lastLogFrames: 0,
|
|
895
|
+
lastLogBytes: 0,
|
|
896
|
+
};
|
|
897
|
+
// E2EE handshake: load-or-create this device's keypair up front. The
|
|
898
|
+
// subscriber asked for encryption, so key failure is FAIL-CLOSED: refuse
|
|
899
|
+
// to stream rather than downgrade to plaintext — a silent downgrade is
|
|
900
|
+
// exactly the signal a key-stripping relay would produce. The phone sees
|
|
901
|
+
// the error frame and can choose to re-subscribe without a key.
|
|
902
|
+
const subscriberPublicKey = req.subscriberPublicKey;
|
|
903
|
+
if (subscriberPublicKey) {
|
|
904
|
+
(0, crypto_js_1.initDeviceCrypto)().catch((err) => {
|
|
905
|
+
// Stream stopped or replaced while init was in flight — the
|
|
906
|
+
// failure belongs to the old incarnation, not the current one.
|
|
907
|
+
if (activeStreams.get(sessionName)?.stats !== stats)
|
|
908
|
+
return;
|
|
909
|
+
log(`⧉ stream ${sessionName}: device crypto init failed (${err instanceof Error ? err.message : err}) — refusing to stream (fail-closed)`);
|
|
910
|
+
send(streamErrorFrame(sessionName, 'e2ee_unavailable'));
|
|
911
|
+
(0, exports.stopStream)(sessionName);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
let wireSeq = 0;
|
|
915
|
+
let encryptFailures = 0;
|
|
916
|
+
const timer = setInterval(() => {
|
|
917
|
+
const captured = capturePane(sessionName, true, STREAM_CAPTURE_LINES);
|
|
918
|
+
if (!captured || captured.content === lastContent) {
|
|
919
|
+
stats.skipped++;
|
|
920
|
+
return; // diff-gate
|
|
921
|
+
}
|
|
922
|
+
lastContent = captured.content;
|
|
923
|
+
// Stamp the sequence in CAPTURE order, synchronously — frame assembly
|
|
924
|
+
// is async (encryption) and fire-and-forget, so resolve order is not
|
|
925
|
+
// guaranteed under load; the receiver drops any seq it has already
|
|
926
|
+
// painted past.
|
|
927
|
+
const seq = ++wireSeq;
|
|
928
|
+
void (0, exports.buildStreamFrame)(captured, sessionName, subscriberPublicKey).then((frame) => {
|
|
929
|
+
// Stream stopped or restarted while this frame was in flight —
|
|
930
|
+
// `stats` is unique per start, so it doubles as the identity
|
|
931
|
+
// token. Don't send into a dead/replaced stream or skew its stats.
|
|
932
|
+
if (activeStreams.get(sessionName)?.stats !== stats)
|
|
933
|
+
return;
|
|
934
|
+
if (!frame) {
|
|
935
|
+
// Encrypt failure — frame dropped, diff-gate un-marked so the
|
|
936
|
+
// next tick retries. A key that keeps failing (malformed
|
|
937
|
+
// subscriber key) never recovers: fail closed after a few
|
|
938
|
+
// strikes instead of retrying every 400ms for 5 minutes.
|
|
939
|
+
lastContent = null;
|
|
940
|
+
// Init still in flight (or failed — its own path fail-closes):
|
|
941
|
+
// a not-yet-ready key is not a malformed key, don't strike.
|
|
942
|
+
if ((0, crypto_js_1.getDevicePublicKey)() === null)
|
|
943
|
+
return;
|
|
944
|
+
encryptFailures++;
|
|
945
|
+
if (encryptFailures >= STREAM_MAX_ENCRYPT_FAILURES) {
|
|
946
|
+
send(streamErrorFrame(sessionName, 'encrypt_failed'));
|
|
947
|
+
(0, exports.stopStream)(sessionName);
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
encryptFailures = 0;
|
|
952
|
+
stats.frames++;
|
|
953
|
+
// Measure the actual wire payload (the encrypted envelope is
|
|
954
|
+
// ~1.4× the plaintext; base fields now count too, so plaintext
|
|
955
|
+
// streams read slightly higher than the pre-E2EE content-only
|
|
956
|
+
// metric) — this feeds the R2 cost instrumentation.
|
|
957
|
+
stats.bytes += Buffer.byteLength(JSON.stringify(frame), 'utf-8');
|
|
958
|
+
// seq = capture order; epoch = this stream incarnation, so the
|
|
959
|
+
// receiver's ordering guard resets across daemon-side restarts.
|
|
960
|
+
send({ ...frame, seq, epoch: stats.startedAt });
|
|
961
|
+
maybeLogStreamStats(sessionName, stats);
|
|
962
|
+
}).catch((err) => {
|
|
963
|
+
// A long-lived daemon must never crash on an unhandled rejection
|
|
964
|
+
// from send()/logging — report and let the next tick carry on.
|
|
965
|
+
log(`⧉ stream ${sessionName}: frame send failed (${err instanceof Error ? err.message : err})`);
|
|
966
|
+
});
|
|
967
|
+
}, STREAM_INTERVAL_MS);
|
|
968
|
+
timer.unref?.();
|
|
969
|
+
activeStreams.set(sessionName, { timer, stats });
|
|
970
|
+
const expiry = setTimeout(() => (0, exports.stopStream)(sessionName), STREAM_MAX_MS);
|
|
971
|
+
expiry.unref?.();
|
|
972
|
+
return true;
|
|
973
|
+
};
|
|
974
|
+
exports.handleStreamControl = handleStreamControl;
|
|
746
975
|
/**
|
|
747
976
|
* Inventory pass that also records *why* each `zeph-*` session was
|
|
748
977
|
* skipped. The verbose log uses the rejection notes to explain empty
|
|
@@ -1356,9 +1585,18 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
1356
1585
|
// Screen peek (§S4): the phone asks for this machine's live
|
|
1357
1586
|
// pane content over the ephemeral relay; the reply rides the
|
|
1358
1587
|
// same channel. Nothing is persisted server-side.
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1588
|
+
const sendEphemeral = (data) => {
|
|
1589
|
+
if (sock.readyState === ws_1.default.OPEN) {
|
|
1590
|
+
sock.send(JSON.stringify({ type: 'ephemeral', data }));
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
// Live mirror (PoC): agent.stream.start/stop drives a
|
|
1594
|
+
// continuous, diff-gated frame loop; falls through to the
|
|
1595
|
+
// one-shot screen-peek when it isn't a stream-control message.
|
|
1596
|
+
if (!(0, exports.handleStreamControl)(m.data, sendEphemeral)) {
|
|
1597
|
+
const reply = (0, exports.handleScreenRequest)(m.data);
|
|
1598
|
+
if (reply)
|
|
1599
|
+
sendEphemeral(reply);
|
|
1362
1600
|
}
|
|
1363
1601
|
}
|
|
1364
1602
|
// Surface server-side errors from listener.sessions reports.
|
|
@@ -1380,6 +1618,7 @@ const streamSession = (wsUrl, apiKey) => {
|
|
|
1380
1618
|
log(`! ws error: ${err.message}`);
|
|
1381
1619
|
});
|
|
1382
1620
|
sock.on('close', (code, reasonBuf) => {
|
|
1621
|
+
(0, exports.stopAllStreams)();
|
|
1383
1622
|
cleanup();
|
|
1384
1623
|
resolve({ closeCode: code, reason: reasonBuf?.toString('utf-8') ?? '', connected: opened });
|
|
1385
1624
|
});
|
package/dist/zeph-hook.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export declare class ZephHook {
|
|
|
17
17
|
list(params?: ListParams): Promise<ListResult>;
|
|
18
18
|
dismiss(pushId: string): Promise<DismissOneResult>;
|
|
19
19
|
dismissAll(): Promise<DismissAllResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Set (or clear, with an empty alias) the display name for an agent session,
|
|
22
|
+
* keyed by its tmux `name` on `deviceId`. The alias overrides the app's
|
|
23
|
+
* computed session label; it survives listener re-reports (server-side).
|
|
24
|
+
*/
|
|
25
|
+
renameAgentSession(deviceId: string, name: string, alias: string): Promise<{
|
|
26
|
+
deviceId: string;
|
|
27
|
+
}>;
|
|
20
28
|
private request;
|
|
21
29
|
private parseError;
|
|
22
30
|
}
|
package/dist/zeph-hook.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoCxL,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,OAAO,EAAE,WAAW;YASlB,YAAY;IAYpB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YAmC7C,cAAc;IAsDtB,aAAa,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAK7G,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrF,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB9C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKlD,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAK7C;;;;OAIG;IACG,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;YASxF,OAAO;IAiCrB,OAAO,CAAC,UAAU;CASnB"}
|
package/dist/zeph-hook.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ZephHook = void 0;
|
|
4
|
-
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
const node_os_1 = require("node:os");
|
|
6
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const listener_device_id_js_1 = require("./listener-device-id.js");
|
|
7
6
|
const errors_js_1 = require("./errors.js");
|
|
8
7
|
const crypto_js_1 = require("./crypto.js");
|
|
9
8
|
const DEFAULT_BASE_URL = 'https://api.zeph.to/v1';
|
|
@@ -14,7 +13,10 @@ const PREVIEW_LENGTH = 200;
|
|
|
14
13
|
* Stable agent-session grouping when running inside a tmux agent session, so a
|
|
15
14
|
* hook/notify (e.g. the Stop-hook recap) files under the same session key as
|
|
16
15
|
* the listener's pushes — surviving Claude session-UUID rotation. The device id
|
|
17
|
-
* MUST
|
|
16
|
+
* MUST equal the listener's `computeListenerDeviceId`, so it's resolved the same
|
|
17
|
+
* way (machine-id hash → sticky file → hostname) via the shared read-only
|
|
18
|
+
* helper — NOT a bare hostname hash, which drifts from the listener's id when a
|
|
19
|
+
* machine id is readable and files the push under a non-matching session key.
|
|
18
20
|
*/
|
|
19
21
|
const agentSessionContext = () => {
|
|
20
22
|
if (!process.env.TMUX)
|
|
@@ -28,8 +30,7 @@ const agentSessionContext = () => {
|
|
|
28
30
|
}
|
|
29
31
|
if (!name)
|
|
30
32
|
return null;
|
|
31
|
-
|
|
32
|
-
return { agentDeviceId: `dev_listener_${h}`, agentSessionName: name };
|
|
33
|
+
return { agentDeviceId: (0, listener_device_id_js_1.listenerDeviceId)(), agentSessionName: name };
|
|
33
34
|
};
|
|
34
35
|
const inferMimeType = (fileName) => {
|
|
35
36
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
@@ -184,6 +185,15 @@ class ZephHook {
|
|
|
184
185
|
const json = await this.request('POST', '/pushes/dismiss-all');
|
|
185
186
|
return { dismissed: json.data?.dismissed ?? 0 };
|
|
186
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Set (or clear, with an empty alias) the display name for an agent session,
|
|
190
|
+
* keyed by its tmux `name` on `deviceId`. The alias overrides the app's
|
|
191
|
+
* computed session label; it survives listener re-reports (server-side).
|
|
192
|
+
*/
|
|
193
|
+
async renameAgentSession(deviceId, name, alias) {
|
|
194
|
+
const res = await this.request('PATCH', `/devices/${encodeURIComponent(deviceId)}/agent-sessions/${encodeURIComponent(name)}`, { alias });
|
|
195
|
+
return { deviceId: res.data.deviceId };
|
|
196
|
+
}
|
|
187
197
|
async request(method, path, body) {
|
|
188
198
|
const controller = new AbortController();
|
|
189
199
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|