@zeph-to/cli 2.12.0 → 2.13.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 +6 -3
- package/dist/agents.d.ts +7 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +12 -5
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +23 -3
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.d.ts.map +1 -0
- package/dist/tmux-install.js +65 -0
- package/dist/verify.d.ts +4 -4
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +7 -9
- package/dist/wrapper.d.ts +35 -0
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +61 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -200,8 +200,11 @@ block here.
|
|
|
200
200
|
### Setup
|
|
201
201
|
|
|
202
202
|
1. **Install tmux.** The listener uses `send-keys`; the wrapper spawns
|
|
203
|
-
named sessions. `
|
|
204
|
-
|
|
203
|
+
named sessions. `zeph install` checks for it and, on macOS with Homebrew,
|
|
204
|
+
offers to run `brew install tmux` for you. Elsewhere it prints the line to
|
|
205
|
+
run — `sudo apt install tmux` on Debian/Ubuntu, and so on — because
|
|
206
|
+
installing with `sudo` is not something an installer should do on your
|
|
207
|
+
behalf. tmux has no Windows build, so `zeph cc` needs WSL there.
|
|
205
208
|
|
|
206
209
|
2. **Add `wsUrl` to `~/.zeph/config.json`** (the WebSocket endpoint of
|
|
207
210
|
your Zeph backend — CDK output `WsApiUrl`):
|
|
@@ -527,7 +530,7 @@ zeph notify --title "Hello" --json
|
|
|
527
530
|
| `dismiss <id>` | Dismiss a push (or `--all`) |
|
|
528
531
|
| `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 |
|
|
529
532
|
| `test` | Verify connection and API key |
|
|
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 "..."`) |
|
|
533
|
+
| `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
534
|
| `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 |
|
|
532
535
|
| `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 |
|
|
533
536
|
|
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
|
package/dist/agents.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
12
|
-
return true;
|
|
17
|
+
return (0, child_process_1.execFileSync)('which', [cmd], { encoding: 'utf-8' }).trim() || null;
|
|
13
18
|
}
|
|
14
19
|
catch {
|
|
15
|
-
return
|
|
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') },
|
package/dist/installer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAeA,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,CA0I1F,CAAC"}
|
package/dist/installer.js
CHANGED
|
@@ -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 tmux_install_js_1 = require("./tmux-install.js");
|
|
13
14
|
const mcp_command_js_1 = require("./mcp-command.js");
|
|
14
15
|
const listener_service_js_1 = require("./listener-service.js");
|
|
15
16
|
const templates_js_1 = require("./templates.js");
|
|
@@ -464,7 +465,7 @@ const applyServiceChoice = async (choice) => {
|
|
|
464
465
|
try {
|
|
465
466
|
const { confirm } = await import('@inquirer/prompts');
|
|
466
467
|
wanted = await confirm({
|
|
467
|
-
message: 'Start the listener at every login
|
|
468
|
+
message: 'Start the listener at every login, so the phone sees this machine after a reboot? (enter to confirm)',
|
|
468
469
|
default: true,
|
|
469
470
|
});
|
|
470
471
|
}
|
|
@@ -551,6 +552,7 @@ const handleInstall = async (args) => {
|
|
|
551
552
|
if (selected.length === 0) {
|
|
552
553
|
console.log(' (no agents selected — only the config file will be saved)');
|
|
553
554
|
}
|
|
555
|
+
console.log(' - Check tmux (`zeph cc` runs agents inside it)');
|
|
554
556
|
if ((0, exports.serviceInstallChoice)(args, nonInteractive) !== 'no') {
|
|
555
557
|
console.log(' - Offer to start the listener at every login');
|
|
556
558
|
}
|
|
@@ -577,10 +579,28 @@ const handleInstall = async (args) => {
|
|
|
577
579
|
if (installer)
|
|
578
580
|
installer();
|
|
579
581
|
}
|
|
580
|
-
// 7.
|
|
582
|
+
// 7. tmux — `zeph cc` is tmux or nothing, so check before claiming success.
|
|
583
|
+
console.log('\n Checking tmux...');
|
|
584
|
+
await (0, tmux_install_js_1.ensureTmux)({
|
|
585
|
+
plan: (0, tmux_install_js_1.planTmuxInstall)({ resolve: agents_js_1.resolveCommand, platform: process.platform }),
|
|
586
|
+
nonInteractive,
|
|
587
|
+
askConfirm: async (message) => {
|
|
588
|
+
try {
|
|
589
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
590
|
+
return await confirm({ message, default: true });
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Ctrl-C or no TTY — an unanswered question is not a yes.
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
run: (argv) => { (0, child_process_1.execFileSync)(argv[0], argv.slice(1), { stdio: 'inherit' }); },
|
|
598
|
+
report: { ok, fail, note: skipNote },
|
|
599
|
+
});
|
|
600
|
+
// 8. Login-time service — the only thing that makes the phone see this
|
|
581
601
|
// machine after a reboot without a terminal being opened first.
|
|
582
602
|
await applyServiceChoice((0, exports.serviceInstallChoice)(args, nonInteractive));
|
|
583
|
-
//
|
|
603
|
+
// 9. Test connection
|
|
584
604
|
console.log('\n Testing connection...');
|
|
585
605
|
await testConnection(apiKey, baseUrl);
|
|
586
606
|
console.log('\n Done! Restart your agents.\n');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux is not optional for `zeph cc` — every remote-agent subcommand runs the
|
|
3
|
+
* agent inside a named tmux session so the listener (and the phone picker) can
|
|
4
|
+
* reach it. Nothing in the install flow used to mention that, so a machine
|
|
5
|
+
* without tmux looked fully set up until the first `zeph cc` failed with an
|
|
6
|
+
* error from a program the user was never told they needed.
|
|
7
|
+
*/
|
|
8
|
+
/** What this machine can do about a missing tmux. */
|
|
9
|
+
export type TmuxInstallPlan = {
|
|
10
|
+
readonly kind: 'present';
|
|
11
|
+
readonly path: string;
|
|
12
|
+
}
|
|
13
|
+
/** Safe to run unattended — no root, user-writable prefix. */
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: 'command';
|
|
16
|
+
readonly run: readonly string[];
|
|
17
|
+
readonly label: string;
|
|
18
|
+
}
|
|
19
|
+
/** Needs root, or there is no package manager we recognise. Tell, don't do. */
|
|
20
|
+
| {
|
|
21
|
+
readonly kind: 'manual';
|
|
22
|
+
readonly hint: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const planTmuxInstall: (deps: {
|
|
25
|
+
resolve: (cmd: string) => string | null;
|
|
26
|
+
platform: NodeJS.Platform;
|
|
27
|
+
}) => TmuxInstallPlan;
|
|
28
|
+
/**
|
|
29
|
+
* Act on the plan. Everything the outside world touches is injected, because
|
|
30
|
+
* the interesting branch — "offer, then run a package manager" — is otherwise
|
|
31
|
+
* only reachable through an interactive install that no test can drive.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ensureTmux: (deps: {
|
|
34
|
+
plan: TmuxInstallPlan;
|
|
35
|
+
nonInteractive: boolean;
|
|
36
|
+
askConfirm: (message: string) => Promise<boolean>;
|
|
37
|
+
run: (argv: readonly string[]) => void;
|
|
38
|
+
report: {
|
|
39
|
+
ok: (m: string) => void;
|
|
40
|
+
fail: (m: string) => void;
|
|
41
|
+
note: (m: string) => void;
|
|
42
|
+
};
|
|
43
|
+
}) => Promise<void>;
|
|
44
|
+
//# sourceMappingURL=tmux-install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux-install.d.ts","sourceRoot":"","sources":["../src/tmux-install.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qDAAqD;AACrD,MAAM,MAAM,eAAe,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACrD,8DAA8D;GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACvF,+EAA+E;GAC7E;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAWzD,eAAO,MAAM,eAAe,GAAI,MAAM;IAClC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC7B,KAAG,eAoBH,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAU,MAAM;IACnC,IAAI,EAAE,eAAe,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,GAAG,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE;QAAE,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAC7F,KAAG,OAAO,CAAC,IAAI,CAsBf,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* tmux is not optional for `zeph cc` — every remote-agent subcommand runs the
|
|
4
|
+
* agent inside a named tmux session so the listener (and the phone picker) can
|
|
5
|
+
* reach it. Nothing in the install flow used to mention that, so a machine
|
|
6
|
+
* without tmux looked fully set up until the first `zeph cc` failed with an
|
|
7
|
+
* error from a program the user was never told they needed.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ensureTmux = exports.planTmuxInstall = void 0;
|
|
11
|
+
/** Package managers that need root, and the line to hand the user for each. */
|
|
12
|
+
const ROOT_PACKAGE_MANAGERS = [
|
|
13
|
+
{ bin: 'apt-get', hint: 'sudo apt install tmux' },
|
|
14
|
+
{ bin: 'dnf', hint: 'sudo dnf install tmux' },
|
|
15
|
+
{ bin: 'pacman', hint: 'sudo pacman -S tmux' },
|
|
16
|
+
{ bin: 'zypper', hint: 'sudo zypper install tmux' },
|
|
17
|
+
{ bin: 'apk', hint: 'sudo apk add tmux' },
|
|
18
|
+
];
|
|
19
|
+
const planTmuxInstall = (deps) => {
|
|
20
|
+
const path = deps.resolve('tmux');
|
|
21
|
+
if (path)
|
|
22
|
+
return { kind: 'present', path };
|
|
23
|
+
// tmux has no native Windows build. `zeph cc` is POSIX-only by way of tmux,
|
|
24
|
+
// so point at WSL rather than at a package that cannot be installed.
|
|
25
|
+
if (deps.platform === 'win32') {
|
|
26
|
+
return { kind: 'manual', hint: '`zeph cc` needs tmux, which has no Windows build — run it under WSL' };
|
|
27
|
+
}
|
|
28
|
+
if (deps.platform === 'darwin') {
|
|
29
|
+
return deps.resolve('brew')
|
|
30
|
+
? { kind: 'command', run: ['brew', 'install', 'tmux'], label: 'brew install tmux' }
|
|
31
|
+
: { kind: 'manual', hint: 'install Homebrew (brew.sh), then: brew install tmux' };
|
|
32
|
+
}
|
|
33
|
+
const rootManager = ROOT_PACKAGE_MANAGERS.find((pm) => deps.resolve(pm.bin));
|
|
34
|
+
if (rootManager)
|
|
35
|
+
return { kind: 'manual', hint: rootManager.hint };
|
|
36
|
+
return { kind: 'manual', hint: 'install tmux with this system\'s package manager' };
|
|
37
|
+
};
|
|
38
|
+
exports.planTmuxInstall = planTmuxInstall;
|
|
39
|
+
/**
|
|
40
|
+
* Act on the plan. Everything the outside world touches is injected, because
|
|
41
|
+
* the interesting branch — "offer, then run a package manager" — is otherwise
|
|
42
|
+
* only reachable through an interactive install that no test can drive.
|
|
43
|
+
*/
|
|
44
|
+
const ensureTmux = async (deps) => {
|
|
45
|
+
const { plan, report } = deps;
|
|
46
|
+
if (plan.kind === 'present')
|
|
47
|
+
return report.ok(`tmux found (${plan.path})`);
|
|
48
|
+
if (plan.kind === 'manual')
|
|
49
|
+
return report.fail(`tmux not found — \`zeph cc\` needs it. ${plan.hint}`);
|
|
50
|
+
if (deps.nonInteractive) {
|
|
51
|
+
return report.note(`tmux not found — \`zeph cc\` needs it. Run: ${plan.label}`);
|
|
52
|
+
}
|
|
53
|
+
const wanted = await deps.askConfirm(`tmux is missing and \`zeph cc\` needs it — run \`${plan.label}\`? (enter to confirm)`);
|
|
54
|
+
if (!wanted) {
|
|
55
|
+
return report.note(`tmux not installed — \`zeph cc\` will not work until you run: ${plan.label}`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
deps.run(plan.run);
|
|
59
|
+
report.ok('tmux installed');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
report.fail(`\`${plan.label}\` failed — run it by hand`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
exports.ensureTmux = ensureTmux;
|
package/dist/verify.d.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
export declare const registeredMcpArgv: (filePath: string, key: string) => string[] | null;
|
|
9
9
|
/**
|
|
10
10
|
* Can this launch binary actually be run? Registered commands come out of a
|
|
11
|
-
* config file, so
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
15
|
*/
|
|
16
16
|
export declare const binaryResolves: (bin: string) => boolean;
|
|
17
17
|
/**
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"
|
|
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
|
@@ -4,7 +4,6 @@ exports.handleVerify = exports.classifyMcpRegistration = exports.binaryResolves
|
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
7
|
-
const child_process_1 = require("child_process");
|
|
8
7
|
const agents_js_1 = require("./agents.js");
|
|
9
8
|
const config_js_1 = require("./config.js");
|
|
10
9
|
const listener_service_js_1 = require("./listener-service.js");
|
|
@@ -85,17 +84,16 @@ const registeredMcpArgv = (filePath, key) => {
|
|
|
85
84
|
exports.registeredMcpArgv = registeredMcpArgv;
|
|
86
85
|
/**
|
|
87
86
|
* Can this launch binary actually be run? Registered commands come out of a
|
|
88
|
-
* config file, so
|
|
89
|
-
* `
|
|
90
|
-
*
|
|
91
|
-
*
|
|
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.
|
|
92
91
|
*/
|
|
93
92
|
const binaryResolves = (bin) => {
|
|
93
|
+
if (!bin.includes('/'))
|
|
94
|
+
return (0, agents_js_1.resolveCommand)(bin) !== null;
|
|
94
95
|
try {
|
|
95
|
-
|
|
96
|
-
(0, fs_1.accessSync)(bin, fs_1.constants.X_OK);
|
|
97
|
-
else
|
|
98
|
-
(0, child_process_1.execFileSync)('which', [bin], { stdio: 'pipe' });
|
|
96
|
+
(0, fs_1.accessSync)(bin, fs_1.constants.X_OK);
|
|
99
97
|
return true;
|
|
100
98
|
}
|
|
101
99
|
catch {
|
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
|
package/dist/wrapper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"
|
|
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
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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" (
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
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` +
|