@zeph-to/cli 1.21.0 → 1.23.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 +57 -46
- package/dist/cli.js +55 -1
- package/dist/installer.d.ts +6 -1
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +22 -24
- 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/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,64 +5,66 @@
|
|
|
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
|
|
|
36
|
-
|
|
37
|
-
# Sign in via browser — auto-fetches your API key + hook into
|
|
38
|
-
# ~/.zeph/config.json (no copy-paste)
|
|
39
|
-
npx @zeph-to/cli login
|
|
24
|
+
Install the CLI globally, then run setup:
|
|
40
25
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @zeph-to/cli
|
|
28
|
+
zeph install
|
|
44
29
|
```
|
|
45
30
|
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
`zeph install` opens a browser sign-in on a fresh machine — the web app
|
|
32
|
+
issues an API key + hook as a matched pair and the CLI writes them to
|
|
33
|
+
`~/.zeph/config.json` (no copy-paste). Then it installs rules + hooks +
|
|
34
|
+
MCP for every detected agent (Claude Code / Cursor / Windsurf / Gemini /
|
|
35
|
+
Codex / …). Safe to re-run: a saved login is reused untouched (no
|
|
36
|
+
prompts), so a re-run just refreshes the agent integrations. To switch
|
|
37
|
+
account, `zeph install --relogin` forces a fresh sign-in.
|
|
38
|
+
|
|
39
|
+
**Why global, not `npx`?** `zeph cc` (drive a session from your phone)
|
|
40
|
+
needs `zeph` on your `PATH`, and the agent hooks this installs are
|
|
41
|
+
`$(command -v zeph || npx …)` — a global binary skips an npx cold-start
|
|
42
|
+
on *every* notification. For notifications only, with no phone control,
|
|
43
|
+
`npx @zeph-to/cli install` is a lighter alternative that skips the global
|
|
44
|
+
binary.
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
`~/.zeph/config.json` is the single source of truth — the CLI, the MCP
|
|
47
|
+
server, the plugin hooks, and the listener all read it. You never need
|
|
48
|
+
`ZEPH_API_KEY`-style env vars for a normal setup; they exist as overrides
|
|
49
|
+
(second account, CI).
|
|
50
|
+
|
|
51
|
+
Variants:
|
|
54
52
|
|
|
55
53
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
# Headless box (no browser): paste credentials from another machine's
|
|
55
|
+
# ~/.zeph/config.json
|
|
56
|
+
zeph install --key ak_... --hook hook_...
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# Skip the interactive agent picker
|
|
59
|
+
zeph install --only claude,cursor
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
# Switch account — force a fresh browser sign-in over a saved login
|
|
62
|
+
zeph install --relogin
|
|
63
|
+
|
|
64
|
+
# Refresh credentials only (no agent re-install) — assumes you already
|
|
65
|
+
# ran `zeph install` once; on its own it does NOT wire MCP into agents
|
|
66
|
+
zeph login
|
|
67
|
+
```
|
|
66
68
|
|
|
67
69
|
To **send** notifications:
|
|
68
70
|
|
|
@@ -78,6 +80,10 @@ To **drive a Claude Code / Codex / Gemini session from your phone**, see
|
|
|
78
80
|
> Send messages from your phone *into* a live Claude Code / Codex /
|
|
79
81
|
> Gemini session — even after a `zeph_ask` polling window has expired.
|
|
80
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
|
+
|
|
81
87
|
The MCP tools `zeph_ask` / `zeph_prompt` / `zeph_input` wait on a fixed
|
|
82
88
|
timeout (120–600 s). Once that window closes the
|
|
83
89
|
session becomes unaddressable from the phone, even though it's still
|
|
@@ -309,6 +315,10 @@ zeph list --limit 10 --type note
|
|
|
309
315
|
zeph dismiss push_01JXY...
|
|
310
316
|
zeph dismiss --all
|
|
311
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
|
+
|
|
312
322
|
# Test connection
|
|
313
323
|
zeph test
|
|
314
324
|
|
|
@@ -337,6 +347,7 @@ zeph notify --title "Hello" --json
|
|
|
337
347
|
| `notify` | Send a push notification |
|
|
338
348
|
| `list` | List recent push notifications |
|
|
339
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 |
|
|
340
351
|
| `test` | Verify connection and API key |
|
|
341
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 "..."`) |
|
|
342
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
|
|
@@ -107,10 +109,13 @@ Login options:
|
|
|
107
109
|
|
|
108
110
|
Install options:
|
|
109
111
|
(no --key, no saved config → opens browser login automatically;
|
|
110
|
-
|
|
112
|
+
a saved login is reused untouched on re-run; headless falls back
|
|
113
|
+
to manual key entry)
|
|
111
114
|
--key <api-key> API key (non-interactive; skips browser login)
|
|
112
115
|
--hook <hook-id> Hook ID (non-interactive)
|
|
113
116
|
--base-url <url> Base URL (non-interactive)
|
|
117
|
+
--relogin Force a fresh browser sign-in even if a login is
|
|
118
|
+
already saved (switch account)
|
|
114
119
|
--web-url <url> Login web app URL [default: https://app.zeph.to]
|
|
115
120
|
--only <agents> Comma-separated agent ids to install for
|
|
116
121
|
(claude,cursor,windsurf,gemini,codex,copilot,cline,aider).
|
|
@@ -160,6 +165,53 @@ const createHook = (args) => {
|
|
|
160
165
|
...(baseUrl && { baseUrl }),
|
|
161
166
|
});
|
|
162
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
|
+
};
|
|
163
215
|
/** Parse a gate count flag; garbage input falls back to the default (never accidentally silences). */
|
|
164
216
|
const gateCount = (raw, fallback) => {
|
|
165
217
|
const n = typeof raw === 'string' ? Number(raw) : NaN;
|
|
@@ -405,6 +457,8 @@ const main = async () => {
|
|
|
405
457
|
return handleList(args);
|
|
406
458
|
case 'dismiss':
|
|
407
459
|
return handleDismiss(args);
|
|
460
|
+
case 'rename':
|
|
461
|
+
return handleRename(args);
|
|
408
462
|
case 'test':
|
|
409
463
|
return handleTest(args);
|
|
410
464
|
case 'listener':
|
package/dist/installer.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ import type { Agent } from './agents.js';
|
|
|
3
3
|
* True when install should auto-open browser login (ADR 0002): interactive
|
|
4
4
|
* context with no existing credential (--key/env/config all absent).
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Interactive install, past the `--key` path: open a browser login only when
|
|
8
|
+
* there's nothing saved yet, or the user explicitly asked to re-authenticate
|
|
9
|
+
* with `--relogin`. A plain re-run keeps the saved login untouched.
|
|
10
|
+
*/
|
|
11
|
+
export declare const shouldReauth: (currentKey: string | undefined, relogin: boolean) => boolean;
|
|
7
12
|
/** Exported for tests (installGemini shells out to the real `gemini` binary,
|
|
8
13
|
* so the merge semantics can't be covered through it deterministically). */
|
|
9
14
|
export declare const mergeJsonFile: (filePath: string, patch: Record<string, unknown>) => void;
|
package/dist/installer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA4BzC;;;GAGG;AACH,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA4BzC;;;GAGG;AACH;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,GAAG,SAAS,EAAE,SAAS,OAAO,KAAG,OACxD,CAAC;AAoBzB;6EAC6E;AAC7E,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAqBhF,CAAC;AAgQF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA8EF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAkG1F,CAAC"}
|
package/dist/installer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleInstall = exports.filterAgentsByIds = exports.mergeJsonFile = exports.
|
|
3
|
+
exports.handleInstall = exports.filterAgentsByIds = exports.mergeJsonFile = exports.shouldReauth = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const os_1 = require("os");
|
|
@@ -19,8 +19,13 @@ const fail = (msg) => console.log(` - ${msg}`);
|
|
|
19
19
|
* True when install should auto-open browser login (ADR 0002): interactive
|
|
20
20
|
* context with no existing credential (--key/env/config all absent).
|
|
21
21
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Interactive install, past the `--key` path: open a browser login only when
|
|
24
|
+
* there's nothing saved yet, or the user explicitly asked to re-authenticate
|
|
25
|
+
* with `--relogin`. A plain re-run keeps the saved login untouched.
|
|
26
|
+
*/
|
|
27
|
+
const shouldReauth = (currentKey, relogin) => !currentKey || relogin;
|
|
28
|
+
exports.shouldReauth = shouldReauth;
|
|
24
29
|
const promptInput = (question) => {
|
|
25
30
|
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
26
31
|
return new Promise((resolve) => {
|
|
@@ -348,26 +353,12 @@ const testConnection = async (apiKey, baseUrl) => {
|
|
|
348
353
|
return false;
|
|
349
354
|
}
|
|
350
355
|
};
|
|
351
|
-
/** Interactive path when a credential already exists: prompt to keep/replace. */
|
|
352
|
-
const promptExistingCredentials = async (currentKey, existing) => {
|
|
353
|
-
if (currentKey)
|
|
354
|
-
console.log(` Current API Key: ${currentKey.slice(0, 12)}...`);
|
|
355
|
-
const keyInput = await promptInput(currentKey ? ' New API Key (Enter to keep): ' : ' API Key (from app > Settings > API Keys): ');
|
|
356
|
-
const currentHook = (0, config_js_1.resolvedEnv)('ZEPH_HOOK_ID') || existing.hookId;
|
|
357
|
-
if (currentHook)
|
|
358
|
-
console.log(` Current Hook ID: ${currentHook}`);
|
|
359
|
-
const hookInput = await promptInput(currentHook ? ' New Hook ID (Enter to keep, "none" to remove): ' : ' Hook ID (optional, for prompt/input): ');
|
|
360
|
-
return {
|
|
361
|
-
apiKey: keyInput || currentKey,
|
|
362
|
-
hookId: hookInput === 'none' ? undefined : (hookInput || currentHook),
|
|
363
|
-
baseUrl: existing.baseUrl,
|
|
364
|
-
};
|
|
365
|
-
};
|
|
366
356
|
/**
|
|
367
|
-
* Resolve API key + hook for install. Priority: --key/env/config (non-interactive
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
357
|
+
* Resolve API key + hook for install. Priority: --key/env/config (non-interactive)
|
|
358
|
+
* → a saved login is reused untouched on re-run → otherwise a brand-new
|
|
359
|
+
* interactive install opens browser login (ADR 0002), falling back to manual
|
|
360
|
+
* paste when headless. wsUrl/deviceId from a login are persisted by
|
|
361
|
+
* runLoginFlow and re-read at config-save time.
|
|
371
362
|
*/
|
|
372
363
|
const collectCredentials = async (args, installArgs, nonInteractive, existing) => {
|
|
373
364
|
if (nonInteractive) {
|
|
@@ -379,8 +370,15 @@ const collectCredentials = async (args, installArgs, nonInteractive, existing) =
|
|
|
379
370
|
}
|
|
380
371
|
console.log('');
|
|
381
372
|
const currentKey = (0, config_js_1.resolvedEnv)('ZEPH_API_KEY') || existing.apiKey;
|
|
382
|
-
|
|
383
|
-
|
|
373
|
+
// Re-run with a saved login: reuse it silently and move on to agent wiring.
|
|
374
|
+
// Switching accounts is `zeph login` or `zeph install --relogin`.
|
|
375
|
+
if (!(0, exports.shouldReauth)(currentKey, args.relogin === true)) {
|
|
376
|
+
console.log(` Using saved login (${currentKey.slice(0, 12)}…). Run \`zeph install --relogin\` to switch account.`);
|
|
377
|
+
return {
|
|
378
|
+
apiKey: currentKey,
|
|
379
|
+
hookId: (0, config_js_1.resolvedEnv)('ZEPH_HOOK_ID') || existing.hookId,
|
|
380
|
+
baseUrl: existing.baseUrl,
|
|
381
|
+
};
|
|
384
382
|
}
|
|
385
383
|
const result = await (0, login_js_1.runLoginFlow)({
|
|
386
384
|
webUrl: (0, login_js_1.resolveWebUrl)(args['web-url']),
|
|
@@ -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/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);
|