@zeph-to/cli 2.11.0 → 2.12.1

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
@@ -179,10 +179,15 @@ way. `zeph install` does not wire Hermes either — add the MCP server to
179
179
  ```yaml
180
180
  mcp_servers:
181
181
  zeph:
182
- command: "npx"
183
- args: ["-y", "@zeph-to/mcp-server"]
182
+ command: "zeph"
183
+ args: ["mcp"]
184
184
  ```
185
185
 
186
+ `zeph mcp` runs the MCP server inside the CLI process. The older
187
+ `command: "npx"` / `args: ["-y", "@zeph-to/mcp-server"]` form still works,
188
+ but leaves an `npm exec` launcher resident beside the server for as long as
189
+ the session lives — `zeph verify` flags a config still on it.
190
+
186
191
  Add the entry; do not replace the file. `mcp_servers` is shared with
187
192
  every other server you have configured, and nothing under `~/.hermes` is
188
193
  in version control. Then `/reload-mcp` inside Hermes.
@@ -522,7 +527,8 @@ zeph notify --title "Hello" --json
522
527
  | `dismiss <id>` | Dismiss a push (or `--all`) |
523
528
  | `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 |
524
529
  | `test` | Verify connection and API key |
525
- | `cc` · `codex` · `gemini` | Run the agent in a `zeph-<project>` tmux session — reattaches a detached session of that project (newest suffix first) when there is one, else auto-suffixes `-2`, `-3`, …. Auto-spawns the background listener on first invocation so the phone picker just works. Trailing args pass through to the agent (`zeph cc --resume "..."`) |
530
+ | `cc` · `codex` · `gemini` | Run the agent in a `zeph-<project>` tmux session — reattaches a detached session of that project (newest suffix first) when there is one, else auto-suffixes `-2`, `-3`, …. Auto-spawns the background listener on first invocation so the phone picker just works. Trailing args pass through to the agent (`zeph cc --resume "..."`). On node 22.15+ the wrapper hands its process to tmux rather than waiting on it, so a running session shows no `zeph cc` of its own in `ps` — older runtimes and Windows keep the waiting wrapper |
531
+ | `mcp` | Run the MCP server on stdio, in this process. What agent MCP configs launch — `zeph install` registers it and you never type it. Replaces the old `npx -y @zeph-to/mcp-server` registration, which left an `npm exec` launcher resident alongside the server for the life of the session. `@zeph-to/mcp-server` ships as a dependency now, so it updates with the CLI rather than being re-fetched by `npx` on every launch — `zeph check-update` reports the version you actually have |
526
532
  | `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 |
527
533
 
528
534
  ### Notify Options
package/dist/agents.d.ts CHANGED
@@ -3,6 +3,13 @@ export interface Agent {
3
3
  id: string;
4
4
  detected: boolean;
5
5
  }
6
+ /**
7
+ * Absolute path of a command on PATH, or null when it isn't there.
8
+ * `execFileSync`, not a shell: `process.execve` needs a real path (it does no
9
+ * PATH lookup of its own), and building `which ${cmd}` as a shell string would
10
+ * let a path containing a space split into two words.
11
+ */
12
+ export declare const resolveCommand: (cmd: string) => string | null;
6
13
  export declare const hasCommand: (cmd: string) => boolean;
7
14
  export declare const detectAgents: () => Agent[];
8
15
  //# sourceMappingURL=agents.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,KAAK;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;CACrB;AAID,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,OAOxC,CAAC;AAEF,eAAO,MAAM,YAAY,QAAO,KAAK,EAWpC,CAAC"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,KAAK;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;CACrB;AAID;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAMrD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,OAAuC,CAAC;AAEjF,eAAO,MAAM,YAAY,QAAO,KAAK,EAWpC,CAAC"}
package/dist/agents.js CHANGED
@@ -1,20 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectAgents = exports.hasCommand = void 0;
3
+ exports.detectAgents = exports.hasCommand = exports.resolveCommand = 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");
7
7
  const path_1 = require("path");
8
8
  const HOME = (0, os_1.homedir)();
9
- const hasCommand = (cmd) => {
9
+ /**
10
+ * Absolute path of a command on PATH, or null when it isn't there.
11
+ * `execFileSync`, not a shell: `process.execve` needs a real path (it does no
12
+ * PATH lookup of its own), and building `which ${cmd}` as a shell string would
13
+ * let a path containing a space split into two words.
14
+ */
15
+ const resolveCommand = (cmd) => {
10
16
  try {
11
- (0, child_process_1.execSync)(`which ${cmd}`, { stdio: 'pipe' });
12
- return true;
17
+ return (0, child_process_1.execFileSync)('which', [cmd], { encoding: 'utf-8' }).trim() || null;
13
18
  }
14
19
  catch {
15
- return false;
20
+ return null;
16
21
  }
17
22
  };
23
+ exports.resolveCommand = resolveCommand;
24
+ const hasCommand = (cmd) => (0, exports.resolveCommand)(cmd) !== null;
18
25
  exports.hasCommand = hasCommand;
19
26
  const detectAgents = () => [
20
27
  { name: 'Claude Code', id: 'claude', detected: (0, exports.hasCommand)('claude') },
@@ -1,4 +1,11 @@
1
1
  /** Semver-ish compare: returns true when `latest` is strictly newer than `current`. */
2
2
  export declare const isNewer: (latest: string, current: string) => boolean;
3
+ /**
4
+ * The mcp-server version this CLI will actually run. It ships as a dependency
5
+ * (see mcp.ts), so it is pinned at install time — no longer "whatever npx
6
+ * fetched last". Returns null on a broken install rather than throwing; the
7
+ * report degrades to "latest is vX" the way it always did for this package.
8
+ */
9
+ export declare const installedMcpServerVersion: () => string | null;
3
10
  export declare const handleCheckUpdate: (args: Record<string, string | boolean>) => Promise<number>;
4
11
  //# sourceMappingURL=check-update.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"check-update.d.ts","sourceRoot":"","sources":["../src/check-update.ts"],"names":[],"mappings":"AAsBA,uFAAuF;AACvF,eAAO,MAAM,OAAO,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,OAQzD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA4C9F,CAAC"}
1
+ {"version":3,"file":"check-update.d.ts","sourceRoot":"","sources":["../src/check-update.ts"],"names":[],"mappings":"AAuBA,uFAAuF;AACvF,eAAO,MAAM,OAAO,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,OAQzD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAAO,MAAM,GAAG,IASrD,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA4C9F,CAAC"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleCheckUpdate = exports.isNewer = void 0;
3
+ exports.handleCheckUpdate = exports.installedMcpServerVersion = exports.isNewer = void 0;
4
+ const fs_1 = require("fs");
4
5
  const config_js_1 = require("./config.js");
5
6
  // Compares installed versions against the npm registry. Pure read-only —
6
7
  // never installs anything; just tells the user if a newer release exists.
@@ -34,19 +35,35 @@ const isNewer = (latest, current) => {
34
35
  return false;
35
36
  };
36
37
  exports.isNewer = isNewer;
38
+ /**
39
+ * The mcp-server version this CLI will actually run. It ships as a dependency
40
+ * (see mcp.ts), so it is pinned at install time — no longer "whatever npx
41
+ * fetched last". Returns null on a broken install rather than throwing; the
42
+ * report degrades to "latest is vX" the way it always did for this package.
43
+ */
44
+ const installedMcpServerVersion = () => {
45
+ try {
46
+ const pkg = JSON.parse((0, fs_1.readFileSync)(require.resolve('@zeph-to/mcp-server/package.json'), 'utf-8'));
47
+ return pkg.version ?? null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ };
53
+ exports.installedMcpServerVersion = installedMcpServerVersion;
37
54
  const handleCheckUpdate = async (args) => {
38
55
  const isJson = args.json === true;
39
- // The CLI's own installed version is known from package.json.
40
- // mcp-server's installed version isn't reliably knowable from here
41
- // (it's a separate package, often run via npx), so we only report its
42
- // latestthe user compares against whatever they have.
43
- const results = [];
44
- for (const pkg of PACKAGES) {
45
- const latest = await fetchLatest(pkg);
46
- const current = pkg === '@zeph-to/cli' ? config_js_1.VERSION : null;
47
- const outdated = !!(latest && current && (0, exports.isNewer)(latest, current));
48
- results.push({ pkg, current, latest, outdated });
49
- }
56
+ // Both installed versions are knowable: the CLI's from its own
57
+ // package.json, and mcp-server's because `zeph mcp` runs it in-process,
58
+ // which makes it a dependency rather than an npx download of unknown age.
59
+ // Two unrelated registry round-trips awaiting them one after the other
60
+ // doubled the command's wall time for nothing.
61
+ const latestVersions = await Promise.all(PACKAGES.map(fetchLatest));
62
+ const results = PACKAGES.map((pkg, i) => {
63
+ const latest = latestVersions[i];
64
+ const current = pkg === '@zeph-to/cli' ? config_js_1.VERSION : (0, exports.installedMcpServerVersion)();
65
+ return { pkg, current, latest, outdated: !!(latest && current && (0, exports.isNewer)(latest, current)) };
66
+ });
50
67
  if (isJson) {
51
68
  console.log(JSON.stringify({ results }, null, 2));
52
69
  return 0;
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ const verify_js_1 = require("./verify.js");
11
11
  const check_update_js_1 = require("./check-update.js");
12
12
  const ask_js_1 = require("./ask.js");
13
13
  const wrapper_js_1 = require("./wrapper.js");
14
+ const mcp_js_1 = require("./mcp.js");
14
15
  const listener_js_1 = require("./listener.js");
15
16
  const config_js_1 = require("./config.js");
16
17
  const gate_js_1 = require("./gate.js");
@@ -84,6 +85,9 @@ ${usageAgentLines()}
84
85
  -2/-3/… only when every existing one has a client
85
86
  attached. Any args after the subcommand are forwarded
86
87
  verbatim, e.g. 'zeph cc --resume')
88
+ mcp Run the MCP server on stdio. This is what agent MCP
89
+ configs launch — 'zeph install' registers it, you
90
+ never type it
87
91
  listener Resident daemon — receives 'agent.command' pushes from
88
92
  the phone picker and injects them into the matching
89
93
  tmux session.
@@ -491,6 +495,8 @@ const main = async () => {
491
495
  return handleRename(args);
492
496
  case 'test':
493
497
  return handleTest(args);
498
+ case 'mcp':
499
+ return (0, mcp_js_1.handleMcp)();
494
500
  case 'listener':
495
501
  return (0, listener_js_1.handleListener)(args);
496
502
  case 'remote-hook':
@@ -12,18 +12,6 @@ export declare const shouldReauth: (currentKey: string | undefined, relogin: boo
12
12
  /** Exported for tests (installGemini shells out to the real `gemini` binary,
13
13
  * so the merge semantics can't be covered through it deterministically). */
14
14
  export declare const mergeJsonFile: (filePath: string, patch: Record<string, unknown>) => void;
15
- /**
16
- * opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
17
- * `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
18
- * opencode is a terminal app that inherits shell env, and the MCP server
19
- * falls back to ~/.zeph/config.json anyway (an unexpanded "${ZEPH_API_KEY}"
20
- * literal would shadow that fallback with junk). Exported for tests (shape pin).
21
- */
22
- export declare const OPENCODE_MCP_ENTRY: {
23
- type: string;
24
- command: string[];
25
- enabled: boolean;
26
- };
27
15
  /** Add the zeph entry under `key` in an MCP registry JSON file, preserving
28
16
  * everything else in it. Exported for tests (merge semantics). */
29
17
  export declare const injectMcpEntry: (filePath: string, key: string, entry: Record<string, unknown>) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA+BzC;;;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;AAgDF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;CAI9B,CAAC;AAEF;mEACmE;AACnE,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,EAAE,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAQ9F,CAAC;AAqPF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA8EF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAC/B,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EACtC,gBAAgB,OAAO,KACtB,KAAK,GAAG,IAAI,GAAG,KAKjB,CAAC;AAgCF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAuH1F,CAAC"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AA+BzC;;;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;AAsCF;mEACmE;AACnE,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,EAAE,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAQ9F,CAAC;AAmPF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,KAAK,EAAE,EAAE,MAAM,MAAM,KAAG,KAAK,EAKxE,CAAC;AA8EF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAC/B,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EACtC,gBAAgB,OAAO,KACtB,KAAK,GAAG,IAAI,GAAG,KAKjB,CAAC;AAgCF,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAuH1F,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.serviceInstallChoice = exports.filterAgentsByIds = exports.injectMcpEntry = exports.OPENCODE_MCP_ENTRY = exports.mergeJsonFile = exports.shouldReauth = void 0;
3
+ exports.handleInstall = exports.serviceInstallChoice = exports.filterAgentsByIds = exports.injectMcpEntry = 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");
@@ -10,6 +10,7 @@ const zeph_hook_js_1 = require("./zeph-hook.js");
10
10
  const config_js_1 = require("./config.js");
11
11
  const login_js_1 = require("./login.js");
12
12
  const agents_js_1 = require("./agents.js");
13
+ const mcp_command_js_1 = require("./mcp-command.js");
13
14
  const listener_service_js_1 = require("./listener-service.js");
14
15
  const templates_js_1 = require("./templates.js");
15
16
  const HOME = (0, os_1.homedir)();
@@ -104,27 +105,6 @@ const addAiderReadDirective = (confPath, conventionsPath) => {
104
105
  (0, fs_1.writeFileSync)(confPath, (base ? `${base}\n\n` : '') + line);
105
106
  };
106
107
  // ── Per-Agent Installers ─────────────────────────────────────────
107
- // Pass through env explicitly so the MCP server doesn't have to rely on
108
- // process-env inheritance (which behaves differently per IDE — Cursor and
109
- // Windsurf spawn the MCP from a graphical context that may not inherit
110
- // shell env). Mirrors plugin/.mcp.json.
111
- const MCP_SERVERS_ENTRY = {
112
- command: 'npx',
113
- args: ['-y', '@zeph-to/mcp-server'],
114
- env: { ZEPH_API_KEY: '${ZEPH_API_KEY}' },
115
- };
116
- /**
117
- * opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
118
- * `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
119
- * opencode is a terminal app that inherits shell env, and the MCP server
120
- * falls back to ~/.zeph/config.json anyway (an unexpanded "${ZEPH_API_KEY}"
121
- * literal would shadow that fallback with junk). Exported for tests (shape pin).
122
- */
123
- exports.OPENCODE_MCP_ENTRY = {
124
- type: 'local',
125
- command: ['npx', '-y', '@zeph-to/mcp-server'],
126
- enabled: true,
127
- };
128
108
  /** Add the zeph entry under `key` in an MCP registry JSON file, preserving
129
109
  * everything else in it. Exported for tests (merge semantics). */
130
110
  const injectMcpEntry = (filePath, key, entry) => {
@@ -153,7 +133,7 @@ const installClaude = () => {
153
133
  };
154
134
  const installCursor = () => {
155
135
  try {
156
- (0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.cursor', 'mcp.json'), 'mcpServers', MCP_SERVERS_ENTRY);
136
+ (0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.cursor', 'mcp.json'), 'mcpServers', mcp_command_js_1.MCP_SERVERS_ENTRY);
157
137
  ok('MCP server added');
158
138
  }
159
139
  catch {
@@ -176,7 +156,7 @@ const installCursor = () => {
176
156
  };
177
157
  const installWindsurf = () => {
178
158
  try {
179
- (0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), 'mcpServers', MCP_SERVERS_ENTRY);
159
+ (0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), 'mcpServers', mcp_command_js_1.MCP_SERVERS_ENTRY);
180
160
  ok('MCP server added');
181
161
  }
182
162
  catch {
@@ -201,20 +181,18 @@ const installWindsurf = () => {
201
181
  };
202
182
  const installGemini = () => {
203
183
  try {
204
- // gemini ≥0.26 syntax: `mcp add [-s scope] <name> <command> [args…]`
205
- // the default scope is "project" (cwd-local .gemini/), so pin user
206
- // scope for a machine-wide install. Older gemini CLIs only accept the
207
- // legacy `mcp add zeph -- npx …` form; fall back for those.
208
- (0, child_process_1.execSync)('gemini mcp add -s user zeph npx -- -y @zeph-to/mcp-server', { stdio: 'pipe' });
184
+ // Both forms are built in mcp-command.ts; older gemini CLIs only accept
185
+ // the legacy one, so try the modern form first and fall back.
186
+ (0, child_process_1.execSync)(mcp_command_js_1.GEMINI_MCP_ADD, { stdio: 'pipe' });
209
187
  ok('MCP server added');
210
188
  }
211
189
  catch {
212
190
  try {
213
- (0, child_process_1.execSync)('gemini mcp add zeph -- npx -y @zeph-to/mcp-server', { stdio: 'pipe' });
191
+ (0, child_process_1.execSync)(mcp_command_js_1.GEMINI_MCP_ADD_LEGACY, { stdio: 'pipe' });
214
192
  ok('MCP server added (legacy gemini CLI)');
215
193
  }
216
194
  catch {
217
- fail('MCP add failed. Manual: gemini mcp add -s user zeph npx -- -y @zeph-to/mcp-server');
195
+ fail(`MCP add failed. Manual: ${mcp_command_js_1.GEMINI_MCP_ADD}`);
218
196
  }
219
197
  }
220
198
  try {
@@ -320,7 +298,7 @@ const installPi = () => {
320
298
  };
321
299
  const installOpencode = () => {
322
300
  try {
323
- (0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.config', 'opencode', 'opencode.json'), 'mcp', exports.OPENCODE_MCP_ENTRY);
301
+ (0, exports.injectMcpEntry)((0, path_1.join)(HOME, '.config', 'opencode', 'opencode.json'), 'mcp', mcp_command_js_1.OPENCODE_MCP_ENTRY);
324
302
  ok('MCP server added');
325
303
  }
326
304
  catch {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * How every agent launches the Zeph MCP server — one source, three schemas.
3
+ *
4
+ * Registrations used to say `npx -y @zeph-to/mcp-server`, which left an
5
+ * `npm exec` launcher resident beside the server for the life of the session.
6
+ * `zeph mcp` (see mcp.ts) runs the server in the CLI's own process instead.
7
+ *
8
+ * Cursor/Windsurf, Gemini and OpenCode each want that launch in a different
9
+ * shape, and the shapes drifted apart when each site spelled the command out
10
+ * itself. Everything below is derived from MCP_LAUNCH_ARGV.
11
+ */
12
+ /** What the agents run. `zeph` resolves out of the same global bin dir as `npx`. */
13
+ export declare const MCP_LAUNCH_ARGV: readonly ["zeph", "mcp"];
14
+ /**
15
+ * `mcpServers.zeph` — what the installer writes for Cursor and Windsurf.
16
+ * The env block is deliberate: both spawn the MCP server from a graphical
17
+ * context that may not inherit shell env, so the key is passed through
18
+ * explicitly rather than left to inheritance.
19
+ *
20
+ * `zeph-to/plugin`'s `.mcp.json` holds the same shape but is a HAND-KEPT COPY —
21
+ * it lives in another repo and cannot import this. Change one, change both.
22
+ */
23
+ export declare const MCP_SERVERS_ENTRY: {
24
+ command: "zeph";
25
+ args: "mcp"[];
26
+ env: {
27
+ ZEPH_API_KEY: string;
28
+ };
29
+ };
30
+ /**
31
+ * opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
32
+ * `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
33
+ * opencode is a terminal app that inherits shell env, and the MCP server
34
+ * falls back to ~/.zeph/config.json anyway (an unexpanded "${ZEPH_API_KEY}"
35
+ * literal would shadow that fallback with junk).
36
+ */
37
+ export declare const OPENCODE_MCP_ENTRY: {
38
+ type: string;
39
+ command: ("zeph" | "mcp")[];
40
+ enabled: boolean;
41
+ };
42
+ export declare const GEMINI_MCP_ADD: string;
43
+ /** Older gemini CLIs only accept `mcp add <name> -- <command…>`, with no scope flag. */
44
+ export declare const GEMINI_MCP_ADD_LEGACY: string;
45
+ //# sourceMappingURL=mcp-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-command.d.ts","sourceRoot":"","sources":["../src/mcp-command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oFAAoF;AACpF,eAAO,MAAM,eAAe,0BAA2B,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;;;CAI7B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;CAI9B,CAAC;AAMF,eAAO,MAAM,cAAc,QAA0E,CAAC;AAEtG,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,QAAwD,CAAC"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * How every agent launches the Zeph MCP server — one source, three schemas.
4
+ *
5
+ * Registrations used to say `npx -y @zeph-to/mcp-server`, which left an
6
+ * `npm exec` launcher resident beside the server for the life of the session.
7
+ * `zeph mcp` (see mcp.ts) runs the server in the CLI's own process instead.
8
+ *
9
+ * Cursor/Windsurf, Gemini and OpenCode each want that launch in a different
10
+ * shape, and the shapes drifted apart when each site spelled the command out
11
+ * itself. Everything below is derived from MCP_LAUNCH_ARGV.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.GEMINI_MCP_ADD_LEGACY = exports.GEMINI_MCP_ADD = exports.OPENCODE_MCP_ENTRY = exports.MCP_SERVERS_ENTRY = exports.MCP_LAUNCH_ARGV = void 0;
15
+ /** What the agents run. `zeph` resolves out of the same global bin dir as `npx`. */
16
+ exports.MCP_LAUNCH_ARGV = ['zeph', 'mcp'];
17
+ const [MCP_BIN, ...MCP_SUBCOMMAND] = exports.MCP_LAUNCH_ARGV;
18
+ /**
19
+ * `mcpServers.zeph` — what the installer writes for Cursor and Windsurf.
20
+ * The env block is deliberate: both spawn the MCP server from a graphical
21
+ * context that may not inherit shell env, so the key is passed through
22
+ * explicitly rather than left to inheritance.
23
+ *
24
+ * `zeph-to/plugin`'s `.mcp.json` holds the same shape but is a HAND-KEPT COPY —
25
+ * it lives in another repo and cannot import this. Change one, change both.
26
+ */
27
+ exports.MCP_SERVERS_ENTRY = {
28
+ command: MCP_BIN,
29
+ args: [...MCP_SUBCOMMAND],
30
+ env: { ZEPH_API_KEY: '${ZEPH_API_KEY}' },
31
+ };
32
+ /**
33
+ * opencode.json's own MCP schema — top-level `mcp` key (not `mcpServers`),
34
+ * `command` as ARRAY (binary + args), `type: "local"`. No `environment` —
35
+ * opencode is a terminal app that inherits shell env, and the MCP server
36
+ * falls back to ~/.zeph/config.json anyway (an unexpanded "${ZEPH_API_KEY}"
37
+ * literal would shadow that fallback with junk).
38
+ */
39
+ exports.OPENCODE_MCP_ENTRY = {
40
+ type: 'local',
41
+ command: [...exports.MCP_LAUNCH_ARGV],
42
+ enabled: true,
43
+ };
44
+ // gemini ≥0.26 syntax: `mcp add [-s scope] <name> <command> [args…]`. The
45
+ // server NAME and the BINARY are both "zeph" here, so the doubled word below
46
+ // is correct, not a copy-paste slip. Default scope is "project" (cwd-local
47
+ // .gemini/), so user scope is pinned for a machine-wide install.
48
+ exports.GEMINI_MCP_ADD = `gemini mcp add -s user zeph ${MCP_BIN} -- ${MCP_SUBCOMMAND.join(' ')}`;
49
+ /** Older gemini CLIs only accept `mcp add <name> -- <command…>`, with no scope flag. */
50
+ exports.GEMINI_MCP_ADD_LEGACY = `gemini mcp add zeph -- ${exports.MCP_LAUNCH_ARGV.join(' ')}`;
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `zeph mcp` — run the Zeph MCP server inside THIS process.
3
+ *
4
+ * Every registration used to say `npx -y @zeph-to/mcp-server`, which left an
5
+ * `npm exec` launcher resident for the whole session on top of the server it
6
+ * spawned — two node processes where one does the work. `@zeph-to/mcp-server`
7
+ * is a dependency of this package now, so agents register `zeph mcp` and get
8
+ * exactly one process.
9
+ */
10
+ /**
11
+ * Importing `@zeph-to/mcp-server` IS starting it: the package is a bin entry
12
+ * whose module body wires an McpServer to a StdioServerTransport and exports
13
+ * nothing. Verified against 2.3.0 (`dist/index.js` ends in `main().catch(…)`).
14
+ * Injectable so tests don't need the real package resolvable.
15
+ */
16
+ type McpServerLoader = () => Promise<unknown>;
17
+ export declare const handleMcp: (load?: McpServerLoader) => Promise<number>;
18
+ export {};
19
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,KAAK,eAAe,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;AAI9C,eAAO,MAAM,SAAS,GAAU,OAAM,eAAiC,KAAG,OAAO,CAAC,MAAM,CAcvF,CAAC"}
package/dist/mcp.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * `zeph mcp` — run the Zeph MCP server inside THIS process.
4
+ *
5
+ * Every registration used to say `npx -y @zeph-to/mcp-server`, which left an
6
+ * `npm exec` launcher resident for the whole session on top of the server it
7
+ * spawned — two node processes where one does the work. `@zeph-to/mcp-server`
8
+ * is a dependency of this package now, so agents register `zeph mcp` and get
9
+ * exactly one process.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.handleMcp = void 0;
13
+ const importMcpServer = () => import('@zeph-to/mcp-server');
14
+ const handleMcp = async (load = importMcpServer) => {
15
+ try {
16
+ await load();
17
+ }
18
+ catch (err) {
19
+ const reason = err instanceof Error ? err.message : String(err);
20
+ console.error(`zeph: could not start the MCP server (@zeph-to/mcp-server): ${reason}`);
21
+ console.error('zeph: reinstall the CLI with `npm i -g @zeph-to/cli`.');
22
+ return 1;
23
+ }
24
+ // The server owns this process from here: it holds stdio open and decides
25
+ // when to exit. Resolving would hand cli.ts a code for `process.exit()`,
26
+ // killing the server the line above just started.
27
+ return new Promise(() => { });
28
+ };
29
+ exports.handleMcp = handleMcp;
package/dist/verify.d.ts CHANGED
@@ -1,2 +1,36 @@
1
+ /**
2
+ * The launch argv a registry file records for zeph, verbatim — null when the
3
+ * file is absent, unparseable, or has no zeph entry. Two schemas in the wild:
4
+ * `command` + `args` (Cursor/Windsurf/Gemini) and `command` as a whole array
5
+ * (opencode). Returned as-is on purpose: a stale entry is only detectable if
6
+ * the reader does not normalize it into the shape it expects.
7
+ */
8
+ export declare const registeredMcpArgv: (filePath: string, key: string) => string[] | null;
9
+ /**
10
+ * Can this launch binary actually be run? Registered commands come out of a
11
+ * config file, so unlike everywhere else in the CLI they can be an absolute
12
+ * path — `which` is the wrong question for one of those, hence the split.
13
+ * The bare-name half is `resolveCommand`; neither half touches a shell, so a
14
+ * path with a space in it stays one word and a `;` in the file stays inert.
15
+ */
16
+ export declare const binaryResolves: (bin: string) => boolean;
17
+ /**
18
+ * What a recorded registration is worth right now. `unresolvable` outranks
19
+ * `stale`: a stale entry still starts a server (through the npm launcher this
20
+ * release exists to delete), an unresolvable one starts nothing at all and
21
+ * takes the agent's whole zeph toolset down with it, silently — the config is
22
+ * read once at install time and never checked again.
23
+ */
24
+ export type McpRegistration = {
25
+ readonly state: 'current';
26
+ } | {
27
+ readonly state: 'stale';
28
+ readonly argv: string[];
29
+ } | {
30
+ readonly state: 'unresolvable';
31
+ readonly argv: string[];
32
+ };
33
+ /** null = this agent has no zeph registration at all, so there is nothing to report. */
34
+ export declare const classifyMcpRegistration: (argv: string[] | null, isOnPath: (bin: string) => boolean) => McpRegistration | null;
1
35
  export declare const handleVerify: (args: Record<string, string | boolean>) => Promise<number>;
2
36
  //# sourceMappingURL=verify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AA4CA,eAAO,MAAM,YAAY,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAiFzF,CAAC"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AA4DA;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,EAAE,GAAG,IAa5E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,OAQ5C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GACrB;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAElE,wFAAwF;AACxF,eAAO,MAAM,uBAAuB,GAChC,MAAM,MAAM,EAAE,GAAG,IAAI,EACrB,UAAU,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KACnC,eAAe,GAAG,IAMpB,CAAC;AAYF,eAAO,MAAM,YAAY,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CA+GzF,CAAC"}
package/dist/verify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleVerify = void 0;
3
+ exports.handleVerify = exports.classifyMcpRegistration = exports.binaryResolves = exports.registeredMcpArgv = void 0;
4
4
  const fs_1 = require("fs");
5
5
  const os_1 = require("os");
6
6
  const path_1 = require("path");
@@ -8,6 +8,7 @@ const agents_js_1 = require("./agents.js");
8
8
  const config_js_1 = require("./config.js");
9
9
  const listener_service_js_1 = require("./listener-service.js");
10
10
  const zeph_hook_js_1 = require("./zeph-hook.js");
11
+ const mcp_command_js_1 = require("./mcp-command.js");
11
12
  const HOME = (0, os_1.homedir)();
12
13
  const pass = (msg) => console.log(` ✓ ${msg}`);
13
14
  const warn = (msg) => console.log(` ! ${msg}`);
@@ -41,6 +42,82 @@ const AGENT_RULE_PRESENT = {
41
42
  pi: () => hasManagedBlock((0, path_1.join)(HOME, '.pi', 'agent', 'AGENTS.md')),
42
43
  opencode: () => hasManagedBlock((0, path_1.join)(HOME, '.config', 'opencode', 'AGENTS.md')),
43
44
  };
45
+ /**
46
+ * Where each agent records the MCP launch, and under which container key.
47
+ * `agent` matches the label `detectAgents` uses so one report never names the
48
+ * same agent two ways. Cursor/Windsurf/OpenCode are files the installer writes
49
+ * itself; the Gemini row is READ-ONLY — `zeph install` shells out to
50
+ * `gemini mcp add`, so this path is the gemini CLI's own storage layout,
51
+ * confirmed by reading the file it produced, not something Zeph chose.
52
+ */
53
+ const MCP_REGISTRIES = [
54
+ { agent: 'Cursor', path: (0, path_1.join)(HOME, '.cursor', 'mcp.json'), key: 'mcpServers' },
55
+ { agent: 'Windsurf', path: (0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers' },
56
+ { agent: 'Gemini CLI', path: (0, path_1.join)(HOME, '.gemini', 'settings.json'), key: 'mcpServers' },
57
+ { agent: 'OpenCode', path: (0, path_1.join)(HOME, '.config', 'opencode', 'opencode.json'), key: 'mcp' },
58
+ ];
59
+ /**
60
+ * The launch argv a registry file records for zeph, verbatim — null when the
61
+ * file is absent, unparseable, or has no zeph entry. Two schemas in the wild:
62
+ * `command` + `args` (Cursor/Windsurf/Gemini) and `command` as a whole array
63
+ * (opencode). Returned as-is on purpose: a stale entry is only detectable if
64
+ * the reader does not normalize it into the shape it expects.
65
+ */
66
+ const registeredMcpArgv = (filePath, key) => {
67
+ let data;
68
+ try {
69
+ data = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ const container = data[key];
75
+ const entry = container?.zeph;
76
+ if (!entry)
77
+ return null;
78
+ if (Array.isArray(entry.command))
79
+ return entry.command.map(String);
80
+ if (typeof entry.command !== 'string')
81
+ return null;
82
+ return [entry.command, ...(Array.isArray(entry.args) ? entry.args.map(String) : [])];
83
+ };
84
+ exports.registeredMcpArgv = registeredMcpArgv;
85
+ /**
86
+ * Can this launch binary actually be run? Registered commands come out of a
87
+ * config file, so unlike everywhere else in the CLI they can be an absolute
88
+ * path — `which` is the wrong question for one of those, hence the split.
89
+ * The bare-name half is `resolveCommand`; neither half touches a shell, so a
90
+ * path with a space in it stays one word and a `;` in the file stays inert.
91
+ */
92
+ const binaryResolves = (bin) => {
93
+ if (!bin.includes('/'))
94
+ return (0, agents_js_1.resolveCommand)(bin) !== null;
95
+ try {
96
+ (0, fs_1.accessSync)(bin, fs_1.constants.X_OK);
97
+ return true;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ };
103
+ exports.binaryResolves = binaryResolves;
104
+ /** null = this agent has no zeph registration at all, so there is nothing to report. */
105
+ const classifyMcpRegistration = (argv, isOnPath) => {
106
+ if (!argv || argv.length === 0)
107
+ return null;
108
+ if (!isOnPath(argv[0]))
109
+ return { state: 'unresolvable', argv };
110
+ const isCanonical = argv.length === mcp_command_js_1.MCP_LAUNCH_ARGV.length
111
+ && argv.every((token, i) => token === mcp_command_js_1.MCP_LAUNCH_ARGV[i]);
112
+ return isCanonical ? { state: 'current' } : { state: 'stale', argv };
113
+ };
114
+ exports.classifyMcpRegistration = classifyMcpRegistration;
115
+ /** The registries that actually hold a zeph entry, classified. Agents that were
116
+ * never installed drop out here so the report has no row for them. */
117
+ const activeMcpRegistrations = (isOnPath) => MCP_REGISTRIES.flatMap((registry) => {
118
+ const result = (0, exports.classifyMcpRegistration)((0, exports.registeredMcpArgv)(registry.path, registry.key), isOnPath);
119
+ return result ? [{ agent: registry.agent, result }] : [];
120
+ });
44
121
  const handleVerify = async (args) => {
45
122
  const doPing = args.ping === true;
46
123
  const checks = [];
@@ -64,12 +141,28 @@ const handleVerify = async (args) => {
64
141
  ? 'ZEPH_HOOK_ID is set (two-way zeph_ask/prompt/input enabled)'
65
142
  : 'ZEPH_HOOK_ID not set (notify-only — set it for remote control)', hookId ? 'pass' : 'warn');
66
143
  // ── Runtime ──────────────────────────────────────────────────
144
+ // One resolution per binary: `zeph` alone was being looked up five times
145
+ // (twice here, once per registry below), and each lookup is a spawn.
146
+ const resolved = new Map();
147
+ const resolves = (bin) => {
148
+ const cached = resolved.get(bin);
149
+ if (cached !== undefined)
150
+ return cached;
151
+ const found = (0, exports.binaryResolves)(bin);
152
+ resolved.set(bin, found);
153
+ return found;
154
+ };
67
155
  console.log('\n Runtime:');
68
- record((0, agents_js_1.hasCommand)('node') ? 'node available' : 'node not found', (0, agents_js_1.hasCommand)('node') ? 'pass' : 'fail');
69
- record((0, agents_js_1.hasCommand)('npx') ? 'npx available (MCP server runs via npx)' : 'npx not found', (0, agents_js_1.hasCommand)('npx') ? 'pass' : 'fail');
70
- record((0, agents_js_1.hasCommand)('zeph')
71
- ? 'zeph CLI on PATH'
72
- : 'zeph CLI not on PATH (hooks fall back to npx slower first call)', (0, agents_js_1.hasCommand)('zeph') ? 'pass' : 'warn');
156
+ const hasNode = resolves('node');
157
+ const hasNpx = resolves('npx');
158
+ const hasZeph = resolves('zeph');
159
+ record(hasNode ? 'node available' : 'node not found', hasNode ? 'pass' : 'fail');
160
+ record(hasNpx ? 'npx available (hook fallback when zeph is off PATH)' : 'npx not found', hasNpx ? 'pass' : 'warn');
161
+ // Not a warning any more: `zeph mcp` IS the MCP server, so a zeph that
162
+ // isn't on PATH means no MCP tools at all, not just a slower first call.
163
+ record(hasZeph
164
+ ? 'zeph CLI on PATH (runs the MCP server and the hooks)'
165
+ : 'zeph CLI not on PATH — MCP tools will not start. Reinstall: npm i -g @zeph-to/cli', hasZeph ? 'pass' : 'fail');
73
166
  // ── Login-time service ───────────────────────────────────────
74
167
  // Optional, so a missing one is a warning. A broken one is not: every way
75
168
  // it breaks still looks installed from the outside.
@@ -79,6 +172,21 @@ const handleVerify = async (args) => {
79
172
  for (const row of serviceRows)
80
173
  record(row.label, row.state);
81
174
  }
175
+ // ── MCP registrations ────────────────────────────────────────
176
+ const registrations = activeMcpRegistrations(resolves);
177
+ if (registrations.length > 0) {
178
+ console.log('\n MCP registrations:');
179
+ for (const { agent, result } of registrations) {
180
+ if (result.state === 'current')
181
+ record(`${agent}: launches \`${mcp_command_js_1.MCP_LAUNCH_ARGV.join(' ')}\``, 'pass');
182
+ else if (result.state === 'stale') {
183
+ record(`${agent}: still launches \`${result.argv.join(' ')}\` — one extra resident process per session. Re-run: zeph install`, 'warn');
184
+ }
185
+ else {
186
+ record(`${agent}: launches \`${result.argv[0]}\`, which is not on PATH — zeph tools will not load. Re-run: zeph install`, 'fail');
187
+ }
188
+ }
189
+ }
82
190
  // ── Per-agent config ─────────────────────────────────────────
83
191
  console.log('\n Agents:');
84
192
  const detected = (0, agents_js_1.detectAgents)().filter((a) => a.detected);
package/dist/wrapper.d.ts CHANGED
@@ -27,6 +27,19 @@ export declare const tmuxSessionName: (project: string) => string;
27
27
  * this" from "the user closed the terminal and will come back".
28
28
  */
29
29
  export declare const findAvailableSession: (base: string) => string;
30
+ interface SpawnTarget {
31
+ /**
32
+ * `tmux-new` opens (or reattaches) a session and needs a terminal to
33
+ * attach to; `direct` runs the agent in the pane we are already in and
34
+ * does not. Several decisions downstream turn on which one this is, so
35
+ * `targetForAgent` states it rather than letting them re-derive it from
36
+ * `cmd === 'tmux'`.
37
+ */
38
+ kind: 'direct' | 'tmux-new';
39
+ cmd: string;
40
+ args: string[];
41
+ }
42
+ export declare const targetForAgent: (agent: string, extra: string[]) => SpawnTarget;
30
43
  /**
31
44
  * Whether the daemon on record is running a different build than the one
32
45
  * we're launching from. `npm i -g @zeph-to/cli` swaps dist/ but leaves the
@@ -58,5 +71,27 @@ export declare const ensureListenerRunning: () => Promise<void>;
58
71
  * `zeph cc --resume foo` runs `claude --resume foo` inside the session.
59
72
  * Returns when the agent exits.
60
73
  */
74
+ /** `process.execve` with the optionality resolved — see handOffExec. */
75
+ export type ProcessHandOff = NonNullable<typeof process.execve>;
76
+ /**
77
+ * The exec that replaces this process with the child, or null when the wrapper
78
+ * has to stay and wait instead. Returning the function rather than a boolean
79
+ * keeps one check where two would otherwise drift: the platform gate and the
80
+ * policy are the same answer.
81
+ *
82
+ * `process.execve` is declared optional by @types/node, and is absent on node
83
+ * <22.15 (added there) and on Windows (its doc: "not available on Windows or
84
+ * IBM i"), so its presence covers both without a version check. `engines.node`
85
+ * is `>=18`, so the spawn path below is not a transitional fallback — it stays.
86
+ *
87
+ * `isTTY` is `undefined` rather than `false` on a pipe, hence every `=== true`.
88
+ */
89
+ export declare const handOffExec: (deps: {
90
+ execve: ProcessHandOff | undefined;
91
+ kind: SpawnTarget["kind"];
92
+ stdinTTY: boolean | undefined;
93
+ stdoutTTY: boolean | undefined;
94
+ }) => ProcessHandOff | null;
61
95
  export declare const handleAgentSession: (agent: RemoteAgent, extra?: string[]) => Promise<number>;
96
+ export {};
62
97
  //# sourceMappingURL=wrapper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAkC9E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAOnD,CAAC;AAmCF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,GAAG,IAAI,EAAE,WAAW,MAAM,KAAG,OAChC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CAgC1D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAU,OAAO,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCjG,CAAC"}
1
+ {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAOtD,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAkC9E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAOnD,CAAC;AAEF,UAAU,WAAW;IACjB;;;;;;OAMG;IACH,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;CAClB;AAOD,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,EAAE,OAAO,MAAM,EAAE,KAAG,WAmB/D,CAAC;AAIF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,GAAG,IAAI,EAAE,WAAW,MAAM,KAAG,OAChC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CAgC1D,CAAC;AAEF;;;;;GAKG;AACH,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM;IAC9B,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC,KAAG,cAAc,GAAG,IASpB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,OAAO,WAAW,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CA+DjG,CAAC"}
package/dist/wrapper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleAgentSession = exports.ensureListenerRunning = exports.listenerVersionDrifted = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
3
+ exports.handleAgentSession = exports.handOffExec = exports.ensureListenerRunning = exports.listenerVersionDrifted = exports.targetForAgent = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
4
4
  /**
5
5
  * `zeph cc` / `zeph codex` / `zeph gemini` — spawn an agent inside a named
6
6
  * tmux session so the resident listener (`zeph listener`) can address it
@@ -14,6 +14,7 @@ exports.handleAgentSession = exports.ensureListenerRunning = exports.listenerVer
14
14
  */
15
15
  const child_process_1 = require("child_process");
16
16
  const path_1 = require("path");
17
+ const agents_js_1 = require("./agents.js");
17
18
  const check_update_js_1 = require("./check-update.js");
18
19
  const config_js_1 = require("./config.js");
19
20
  const listener_process_js_1 = require("./listener-process.js");
@@ -114,7 +115,7 @@ const targetForAgent = (agent, extra) => {
114
115
  // current pane. Nested tmux prefix collisions are confusing and the
115
116
  // listener can't reach a session it didn't name anyway.
116
117
  if (process.env.TMUX) {
117
- return { cmd: agent, args: extra };
118
+ return { kind: 'direct', cmd: agent, args: extra };
118
119
  }
119
120
  const base = (0, exports.tmuxSessionName)((0, exports.detectProjectName)());
120
121
  // Reattach a detached session of this project when there is one,
@@ -127,8 +128,9 @@ const targetForAgent = (agent, extra) => {
127
128
  // `--resume` would be eaten by tmux's own parser. Build one quoted
128
129
  // shell string instead, which tmux passes through verbatim.
129
130
  const shellCmd = [agent, ...extra].map(shellQuote).join(' ');
130
- return { cmd: 'tmux', args: ['new', '-A', '-s', session, shellCmd] };
131
+ return { kind: 'tmux-new', cmd: 'tmux', args: ['new', '-A', '-s', session, shellCmd] };
131
132
  };
133
+ exports.targetForAgent = targetForAgent;
132
134
  // ── Background listener auto-start ────────────────────────────────────
133
135
  /**
134
136
  * Whether the daemon on record is running a different build than the one
@@ -193,27 +195,74 @@ const ensureListenerRunning = async () => {
193
195
  };
194
196
  exports.ensureListenerRunning = ensureListenerRunning;
195
197
  /**
196
- * Launch the agent in a named tmux session (or directly if nested) and
197
- * forward its exit code. `extra` is appended to the agent invocation, so
198
- * `zeph cc --resume foo` runs `claude --resume foo` inside the session.
199
- * Returns when the agent exits.
198
+ * The exec that replaces this process with the child, or null when the wrapper
199
+ * has to stay and wait instead. Returning the function rather than a boolean
200
+ * keeps one check where two would otherwise drift: the platform gate and the
201
+ * policy are the same answer.
202
+ *
203
+ * `process.execve` is declared optional by @types/node, and is absent on node
204
+ * <22.15 (added there) and on Windows (its doc: "not available on Windows or
205
+ * IBM i"), so its presence covers both without a version check. `engines.node`
206
+ * is `>=18`, so the spawn path below is not a transitional fallback — it stays.
207
+ *
208
+ * `isTTY` is `undefined` rather than `false` on a pipe, hence every `=== true`.
200
209
  */
210
+ const handOffExec = (deps) => {
211
+ if (!deps.execve)
212
+ return null;
213
+ // execve runs no cleanup handler, and node only writes stdout synchronously
214
+ // to a TTY on POSIX — anything buffered to a pipe would vanish with us.
215
+ if (deps.stdoutTTY !== true)
216
+ return null;
217
+ // `tmux new` additionally needs a terminal of its own to attach to. That is
218
+ // the failure the spawn path below exists to explain, so send it there.
219
+ if (deps.kind === 'tmux-new' && deps.stdinTTY !== true)
220
+ return null;
221
+ return deps.execve;
222
+ };
223
+ exports.handOffExec = handOffExec;
201
224
  const handleAgentSession = async (agent, extra = []) => {
202
225
  // Best-effort: make sure the phone-bridge daemon is running, and running
203
226
  // the build we were launched from. The user shouldn't need to remember a
204
227
  // second command for the picker on their phone to work.
205
228
  await (0, exports.ensureListenerRunning)();
229
+ const { kind, cmd, args } = (0, exports.targetForAgent)(agent.binary, extra);
230
+ // Hand the terminal over and stop existing. Waiting on the child is all
231
+ // this process does for the rest of the session, and it holds a whole node
232
+ // heap to do it — one resident process per `zeph cc`, purely to forward an
233
+ // exit code that execve gives us for free by being the same process.
234
+ const handOff = (0, exports.handOffExec)({
235
+ execve: process.execve,
236
+ kind,
237
+ stdinTTY: process.stdin.isTTY,
238
+ stdoutTTY: process.stdout.isTTY,
239
+ });
240
+ if (handOff) {
241
+ const file = (0, agents_js_1.resolveCommand)(cmd);
242
+ // execve does no PATH lookup, so resolving is mandatory — and the
243
+ // resolve failing is the same fact the spawn path learns from ENOENT.
244
+ if (!file) {
245
+ console.error(`zeph: '${cmd}' not found on PATH`);
246
+ return 127;
247
+ }
248
+ // Nothing buffered survives: execve runs no cleanup handler and fires
249
+ // no exit event. Safe here only because stdout is a TTY, which node
250
+ // writes synchronously on POSIX — every `zeph:` line above is already out.
251
+ // The env argument is left off: it defaults to process.env.
252
+ handOff(file, [cmd, ...args]);
253
+ }
206
254
  return new Promise((resolve) => {
207
- const { cmd, args } = targetForAgent(agent.binary, extra);
208
255
  const start = Date.now();
209
256
  const child = (0, child_process_1.spawn)(cmd, args, { stdio: 'inherit' });
210
257
  child.on('exit', (code) => {
211
258
  const dur = Date.now() - start;
212
259
  // Short-lived non-zero exits are the symptom of "ran from a
213
- // pane that isn't a real TTY" (iTerm tmux integration pane,
214
- // some IDE terminals). The user otherwise just sees their
215
- // shell return with `[exited]` and no clue what went wrong.
216
- if (code && code !== 0 && dur < 2000) {
260
+ // pane that isn't a real TTY" (some IDE terminals). The user
261
+ // otherwise just sees their shell return with `[exited]` and no
262
+ // clue what went wrong. Only `tmux new` fails that way telling
263
+ // someone whose agent died inside a perfectly good pane to "run
264
+ // from a plain shell pane" would send them after the wrong thing.
265
+ if (kind === 'tmux-new' && code && code !== 0 && dur < 2000) {
217
266
  console.error(`zeph: ${cmd} ${args.join(' ')} exited ${code} after ${dur}ms.\n` +
218
267
  ` If this terminal is itself inside tmux (or an iTerm/Warp\n` +
219
268
  ` tmux-integration pane), run \`zeph cc\` from a plain shell\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "Zeph CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -72,6 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@inquirer/prompts": "^8.4.3",
75
+ "@zeph-to/mcp-server": "^2.3.0",
75
76
  "ws": "^8.21.0"
76
77
  }
77
78
  }