@zeph-to/cli 1.22.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 CHANGED
@@ -5,31 +5,19 @@
5
5
  [![node](https://img.shields.io/node/v/@zeph-to/cli.svg)](https://nodejs.org)
6
6
  [![license](https://img.shields.io/npm/l/@zeph-to/cli.svg)](./LICENSE)
7
7
 
8
- Push notification SDK + CLI for [Zeph](https://zeph.to) — the terminal
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
- ```bash
29
- npm install -g @zeph-to/cli
30
- # or for one-off use
31
- npx @zeph-to/cli notify --title "Hello"
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':
@@ -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;
@@ -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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAkCxL,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;YAK/B,OAAO;IAiCrB,OAAO,CAAC,UAAU;CASnB"}
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 match cli `computeListenerDeviceId` (`dev_listener_<sha8(hostname)>`).
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
- const h = (0, node_crypto_1.createHash)('sha256').update((0, node_os_1.hostname)()).digest('hex').slice(0, 8);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Zeph CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",