@zeph-to/cli 2.3.1 → 2.4.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
@@ -8,7 +8,7 @@
8
8
 
9
9
  **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.**
10
10
 
11
- `@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 / Cursor / Gemini sessions** by typing straight into named tmux sessions.
11
+ `@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 any agent session it can start** by typing straight into named tmux sessions.
12
12
 
13
13
  <p align="center">
14
14
  <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>
@@ -83,13 +83,13 @@ To **send** notifications:
83
83
  zeph notify --title "Deploy done" --body "v2.1.0 shipped"
84
84
  ```
85
85
 
86
- To **drive a Claude Code / Codex / Cursor / Gemini session from your phone**, see
86
+ To **drive an agent session from your phone**, see
87
87
  [Remote Control](#remote-control) below.
88
88
 
89
89
  ## Remote Control
90
90
 
91
- > Send messages from your phone *into* a live Claude Code / Codex /
92
- > Gemini session — even after a `zeph_ask` polling window has expired.
91
+ > Send messages from your phone *into* a live agent session even after
92
+ > a `zeph_ask` polling window has expired.
93
93
 
94
94
  <p align="center">
95
95
  <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">
@@ -116,7 +116,7 @@ tmux session via `tmux send-keys`.
116
116
  [zeph listener — resident daemon, started by `zeph cc` automatically]
117
117
  │ tmux send-keys -l -t zeph-myapp "리팩토링 마무리해줘" + Enter
118
118
 
119
- [tmux session "zeph-myapp" running claude / codex / cursor-agent / gemini]
119
+ [tmux session "zeph-myapp" running claude / codex / cursor-agent / gemini / hermes]
120
120
  ```
121
121
 
122
122
  The listener polls its tmux session inventory every 5 seconds and
@@ -164,6 +164,26 @@ through. Same reason the `stop`-hook auto-push `zeph setup` installs
164
164
  covers the Cursor **IDE** but not `zeph cursor` panes; ask for a
165
165
  `zeph_notify` when you want one.
166
166
 
167
+ `zeph hermes` is in the same position, and asking for it works the same
168
+ way. `zeph install` does not wire Hermes either — add the MCP server to
169
+ `~/.hermes/config.yaml` by hand, as one entry under `mcp_servers`:
170
+
171
+ ```yaml
172
+ mcp_servers:
173
+ zeph:
174
+ command: "npx"
175
+ args: ["-y", "@zeph-to/mcp-server"]
176
+ ```
177
+
178
+ Add the entry; do not replace the file. `mcp_servers` is shared with
179
+ every other server you have configured, and nothing under `~/.hermes` is
180
+ in version control. Then `/reload-mcp` inside Hermes.
181
+
182
+ `zeph_ask` needs a hook id, which the MCP server reads from
183
+ `ZEPH_HOOK_ID` or from `hookId` in `~/.zeph/config.json` — `zeph install`
184
+ writes the latter, so a machine that has run it once needs no `env`
185
+ block here.
186
+
167
187
  ### Setup
168
188
 
169
189
  1. **Install tmux.** The listener uses `send-keys`; the wrapper spawns
@@ -190,12 +210,27 @@ covers the Cursor **IDE** but not `zeph cursor` panes; ask for a
190
210
  zeph codex # codex → tmux session "zeph-<project>"
191
211
  zeph cursor # cursor-agent → tmux session "zeph-<project>"
192
212
  zeph gemini # gemini → tmux session "zeph-<project>"
213
+ zeph hermes # hermes → tmux session "zeph-<project>"
193
214
  ```
194
215
 
195
216
  `zeph cursor` runs **`cursor-agent`**, Cursor's terminal agent — a
196
217
  separate install from the Cursor IDE (the bare `cursor` on your PATH
197
218
  is the editor launcher, which exits immediately and can't be driven).
198
219
 
220
+ `zeph hermes` runs Hermes in its classic REPL, which is the default.
221
+ Launching it in TUI mode instead (`zeph hermes --tui`, or `HERMES_TUI=1`
222
+ in the environment) puts the pane on the terminal's alternate screen,
223
+ and the alternate screen has no scrollback — the live mirror on your
224
+ phone still draws, but scrolling up in it comes back empty.
225
+
226
+ If a Hermes session disappears from the phone's picker, the cause is
227
+ tmux having dropped the pane's `start_command`: all the listener sees
228
+ then is the interpreter Hermes execs (`python3.11` at time of writing),
229
+ which names no agent. `zeph listener` run in the foreground prints the
230
+ reason (`no agent in pane (start=…, current=python3.11)`). Kill the
231
+ session and start it again with `zeph hermes` to recover. `claude` sits
232
+ on the same edge for the same reason — it execs node.
233
+
199
234
  The first `zeph cc` on a machine **auto-spawns a background
200
235
  listener** (singleton, PID file at `~/.zeph/listener.pid`,
201
236
  stdout/stderr at `~/.zeph/listener.log`). You never run
@@ -407,6 +442,7 @@ zeph cc # claude
407
442
  zeph codex # codex
408
443
  zeph cursor # cursor-agent (Cursor CLI, not the IDE)
409
444
  zeph gemini # gemini
445
+ zeph hermes # hermes
410
446
 
411
447
  # Run the resident listener (foreground; background it as you like)
412
448
  zeph listener
@@ -23,8 +23,9 @@ export interface RemoteAgent {
23
23
  /**
24
24
  * Resolve the agent's own session id from the pane's cwd (+ pane pid
25
25
  * when the caller knows it — enables exact process-tree matching).
26
- * EXTENSION POINT: omitted for codex/gemini until their session-file
27
- * formats are confirmed — the listener then reports agentSessionId: null.
26
+ * EXTENSION POINT: carried only by Claude Code. Every other row omits
27
+ * it until that agent's session-file format is confirmed — the listener
28
+ * then reports agentSessionId: null.
28
29
  */
29
30
  resolveSessionId?: (paneCwd: string, panePid?: number) => string | null;
30
31
  }
@@ -76,8 +77,13 @@ declare const REMOTE_AGENT_TABLE: readonly [{
76
77
  readonly displayName: "Gemini CLI";
77
78
  readonly binary: "gemini";
78
79
  readonly subcommands: readonly ["gemini"];
80
+ }, {
81
+ readonly kind: "hermes";
82
+ readonly displayName: "Hermes";
83
+ readonly binary: "hermes";
84
+ readonly subcommands: readonly ["hermes"];
79
85
  }];
80
- /** Closed union of remote-controllable agent kinds ('claude' | 'codex' | 'cursor' | 'gemini'). */
86
+ /** Closed union of remote-controllable agent kinds, derived from the table above. */
81
87
  export type AgentKind = (typeof REMOTE_AGENT_TABLE)[number]['kind'];
82
88
  /** A registry row: the uniform RemoteAgent shape with `kind` narrowed to the closed union. */
83
89
  export type RegisteredRemoteAgent = RemoteAgent & {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IACxB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC3E;AAyCD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,CAAC;AAaF,UAAU,gBAAgB;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACf;AA4ED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GACnC,SAAS,MAAM,EACf,SAAS,MAAM,GAAG,IAAI,EACtB,OAAM,cAAmB,KAC1B,MAAM,GAAG,IAOX,CAAC;AAIF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;EAmCmB,CAAC;AAE5C,kGAAkG;AAClG,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,8FAA8F;AAC9F,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEtE,eAAO,MAAM,aAAa,EAAE,SAAS,qBAAqB,EAAuB,CAAC;AAElF,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,qBAAqB,GAAG,SAClB,CAAC;AAE3D,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,qBAAqB,GAAG,SAG9E,CAAC"}
1
+ {"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IACxB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC3E;AAyCD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,CAAC;AAaF,UAAU,gBAAgB;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACf;AA4ED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GACnC,SAAS,MAAM,EACf,SAAS,MAAM,GAAG,IAAI,EACtB,OAAM,cAAmB,KAC1B,MAAM,GAAG,IAOX,CAAC;AAIF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+CmB,CAAC;AAE5C,qFAAqF;AACrF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,8FAA8F;AAC9F,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEtE,eAAO,MAAM,aAAa,EAAE,SAAS,qBAAqB,EAAuB,CAAC;AAElF,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,qBAAqB,GAAG,SAClB,CAAC;AAE3D,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,qBAAqB,GAAG,SAG9E,CAAC"}
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.detectClaudeSessionIdByPid = exports.detectClaudeSessionId = void 0;
4
4
  /**
5
- * Remote-control agent registry — the single table behind `zeph cc` /
6
- * `zeph codex` / `zeph gemini`, the listener's pane matching, and the
5
+ * Remote-control agent registry — the single table behind every
6
+ * `zeph <agent>` subcommand, the listener's pane matching, and the
7
7
  * per-agent session-id enrichment. Adding a remote-controllable agent is
8
8
  * one row here (plus, for a genuinely new kind, backend/phone support:
9
9
  * `kind` is a wire contract — AgentSession.agentKind flows to the server
@@ -217,6 +217,18 @@ const REMOTE_AGENT_TABLE = [
217
217
  binary: 'gemini',
218
218
  subcommands: ['gemini'],
219
219
  },
220
+ {
221
+ // `hermes` on PATH is a bash script that execs a venv Python, so the
222
+ // pane's *current* command reads the interpreter (`python3.11` as
223
+ // measured). Only `pane_start_command` identifies it, which is the
224
+ // same bargain `claude` (node) already makes. Deliberately no
225
+ // paneMatchAliases: matching interpreter names would adopt every
226
+ // Python REPL on the machine as a Hermes session.
227
+ kind: 'hermes',
228
+ displayName: 'Hermes',
229
+ binary: 'hermes',
230
+ subcommands: ['hermes'],
231
+ },
220
232
  ];
221
233
  exports.REMOTE_AGENTS = REMOTE_AGENT_TABLE;
222
234
  const findAgentBySubcommand = (cmd) => exports.REMOTE_AGENTS.find((a) => a.subcommands.includes(cmd));
@@ -126,8 +126,17 @@ const remoteOrigin = (prompt, cwd, now) => {
126
126
  return 'unclear';
127
127
  // Marker format: "<epochSec> <sha256hex>\n" (listener.ts writeRemoteMarker).
128
128
  const record = content.match(/^(\d+) ([0-9a-f]{64})\n?$/);
129
- if (!record)
129
+ if (!record) {
130
+ // Junk can only ever be re-read and re-rejected — same housekeeping a
131
+ // stale marker gets. Twin: zeph-remote.sh's `rm -f` on the parse branches.
132
+ try {
133
+ (0, fs_1.unlinkSync)(marker);
134
+ }
135
+ catch {
136
+ /* best-effort housekeeping */
137
+ }
130
138
  return 'keyboard';
139
+ }
131
140
  if (Math.floor(now() / 1000) - Number(record[1]) > FRESH_WINDOW_SEC) {
132
141
  // Stale markers are dead weight (can never flag) — delete on sight.
133
142
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "2.3.1",
3
+ "version": "2.4.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",