@zeph-to/hook-sdk 1.8.0 → 1.10.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.
@@ -0,0 +1,26 @@
1
+ /** Resolve a project name for the tmux session: env > git root > cwd basename. */
2
+ export declare const detectProjectName: () => string;
3
+ /** `zeph-<project>` — the canonical tmux session base name. */
4
+ export declare const tmuxSessionName: (project: string) => string;
5
+ /**
6
+ * Pick a tmux session name that won't steal focus from another live
7
+ * `zeph cc`. Strategy:
8
+ * - If `<base>` doesn't exist → use it (create new).
9
+ * - If `<base>` exists but is detached → use it (reattach).
10
+ * - If `<base>` exists *and* has a client attached → try `<base>-2`,
11
+ * `<base>-3`, … so the new `zeph cc` gets an independent session
12
+ * instead of joining the existing one.
13
+ * Falls back to `<base>` after 20 attempts (shouldn't realistically hit).
14
+ *
15
+ * Detection uses `tmux has-session` and `tmux list-clients`; both are
16
+ * dependency-free against the user's running tmux server.
17
+ */
18
+ export declare const findAvailableSession: (base: string) => string;
19
+ /**
20
+ * Launch the agent in a named tmux session (or directly if nested) and
21
+ * forward its exit code. `extra` is appended to the agent invocation, so
22
+ * `zeph cc --resume foo` runs `claude --resume foo` inside the session.
23
+ * Returns when the agent exits.
24
+ */
25
+ export declare const handleAgentSession: (agent: string, extra?: string[]) => Promise<number>;
26
+ //# sourceMappingURL=wrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../src/wrapper.ts"],"names":[],"mappings":"AAwBA,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,QAAO,MAapC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,MAA2B,CAAC;AAI9E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAenD,CAAC;AAkGF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCtF,CAAC"}
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleAgentSession = exports.findAvailableSession = exports.tmuxSessionName = exports.detectProjectName = void 0;
4
+ /**
5
+ * `zeph cc` / `zeph codex` / `zeph gemini` — spawn an agent inside a named
6
+ * tmux session so the resident listener (`zeph listener`) can address it
7
+ * by session name to inject messages later.
8
+ *
9
+ * The tmux session name follows `zeph-<project>` where <project> resolves
10
+ * from CLAUDE/CURSOR/WINDSURF_PROJECT_DIR → git repo root → cwd basename.
11
+ * When the wrapper is invoked from inside an existing tmux session
12
+ * ($TMUX set) it skips the outer tmux to avoid nesting and execs the
13
+ * agent directly — letting power users keep their own multiplexer setup.
14
+ */
15
+ const child_process_1 = require("child_process");
16
+ const fs_1 = require("fs");
17
+ const os_1 = require("os");
18
+ const path_1 = require("path");
19
+ /** First non-empty value among the supported per-agent project dir env vars. */
20
+ const PROJECT_DIR_ENVS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
21
+ const FALLBACK_NAME = 'project';
22
+ /** basename(), with a stable fallback for edge paths like `/`. */
23
+ const safeBasename = (path) => (0, path_1.basename)(path) || FALLBACK_NAME;
24
+ /** Resolve a project name for the tmux session: env > git root > cwd basename. */
25
+ const detectProjectName = () => {
26
+ for (const key of PROJECT_DIR_ENVS) {
27
+ const v = process.env[key];
28
+ if (v)
29
+ return safeBasename(v.replace(/\/+$/, ''));
30
+ }
31
+ try {
32
+ const root = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
33
+ encoding: 'utf-8',
34
+ stdio: ['ignore', 'pipe', 'ignore'],
35
+ }).trim();
36
+ if (root)
37
+ return safeBasename(root);
38
+ }
39
+ catch { /* not a git repo — fall through */ }
40
+ return safeBasename(process.cwd());
41
+ };
42
+ exports.detectProjectName = detectProjectName;
43
+ /** `zeph-<project>` — the canonical tmux session base name. */
44
+ const tmuxSessionName = (project) => `zeph-${project}`;
45
+ exports.tmuxSessionName = tmuxSessionName;
46
+ const MAX_SUFFIX_ATTEMPTS = 20;
47
+ /**
48
+ * Pick a tmux session name that won't steal focus from another live
49
+ * `zeph cc`. Strategy:
50
+ * - If `<base>` doesn't exist → use it (create new).
51
+ * - If `<base>` exists but is detached → use it (reattach).
52
+ * - If `<base>` exists *and* has a client attached → try `<base>-2`,
53
+ * `<base>-3`, … so the new `zeph cc` gets an independent session
54
+ * instead of joining the existing one.
55
+ * Falls back to `<base>` after 20 attempts (shouldn't realistically hit).
56
+ *
57
+ * Detection uses `tmux has-session` and `tmux list-clients`; both are
58
+ * dependency-free against the user's running tmux server.
59
+ */
60
+ const findAvailableSession = (base) => {
61
+ for (let i = 0; i < MAX_SUFFIX_ATTEMPTS; i++) {
62
+ const name = i === 0 ? base : `${base}-${i + 1}`;
63
+ const has = (0, child_process_1.spawnSync)('tmux', ['has-session', '-t', name], {
64
+ stdio: ['ignore', 'ignore', 'ignore'],
65
+ });
66
+ if (has.status !== 0)
67
+ return name; // doesn't exist — fresh session
68
+ const clients = (0, child_process_1.spawnSync)('tmux', ['list-clients', '-t', name, '-F', '#{client_tty}'], {
69
+ encoding: 'utf-8',
70
+ stdio: ['ignore', 'pipe', 'ignore'],
71
+ });
72
+ const attached = (clients.stdout ?? '').trim().length > 0;
73
+ if (!attached)
74
+ return name; // exists but detached — reattach
75
+ }
76
+ return base;
77
+ };
78
+ exports.findAvailableSession = findAvailableSession;
79
+ /** POSIX shell-quote so passthrough args survive being joined into a tmux shell-command string. */
80
+ const SHELL_SAFE = /^[\w\-./=:@%+,]+$/;
81
+ const shellQuote = (s) => s.length > 0 && SHELL_SAFE.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
82
+ const targetForAgent = (agent, extra) => {
83
+ // Already inside tmux → no nested session, just run the agent in the
84
+ // current pane. Nested tmux prefix collisions are confusing and the
85
+ // listener can't reach a session it didn't name anyway.
86
+ if (process.env.TMUX) {
87
+ return { cmd: agent, args: extra };
88
+ }
89
+ const base = (0, exports.tmuxSessionName)((0, exports.detectProjectName)());
90
+ // Auto-suffix when the default name is taken by another attached
91
+ // session — lets the user keep `zeph cc` workflow simple and still
92
+ // get independent sessions when opening multiple terminals in the
93
+ // same project.
94
+ const session = (0, exports.findAvailableSession)(base);
95
+ // `tmux new -A`: attach if the named session exists, else create it.
96
+ // tmux joins trailing argv into a single shell-command, so flags like
97
+ // `--resume` would be eaten by tmux's own parser. Build one quoted
98
+ // shell string instead, which tmux passes through verbatim.
99
+ const shellCmd = [agent, ...extra].map(shellQuote).join(' ');
100
+ return { cmd: 'tmux', args: ['new', '-A', '-s', session, shellCmd] };
101
+ };
102
+ // ── Background listener auto-start ────────────────────────────────────
103
+ const ZEPH_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
104
+ const LISTENER_PID_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.pid');
105
+ const LISTENER_LOG_FILE = (0, path_1.join)(ZEPH_DIR, 'listener.log');
106
+ /** True when the PID file points at a still-alive process. */
107
+ const listenerAlive = () => {
108
+ try {
109
+ const pid = Number((0, fs_1.readFileSync)(LISTENER_PID_FILE, 'utf-8').trim());
110
+ if (!Number.isFinite(pid) || pid <= 0)
111
+ return false;
112
+ // Signal 0 = existence check; throws when the process is gone.
113
+ process.kill(pid, 0);
114
+ return true;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ };
120
+ /**
121
+ * Path to the running cli.js entry. We're invoked AS cli.js (the bin
122
+ * shim defined in package.json), so process.argv[1] is our entry point.
123
+ * Resolves whether the user calls `zeph cc` via the npm-installed shim
124
+ * or directly via `node dist/cli.js cc`.
125
+ */
126
+ const resolveCliPath = () => {
127
+ const entry = process.argv[1];
128
+ if (!entry)
129
+ return null;
130
+ // Sanity check: only autostart when we recognise the entry — refuse
131
+ // to spawn an unknown binary from a weird invocation.
132
+ if (!/cli\.(js|ts|mjs|cjs)$/.test(entry))
133
+ return null;
134
+ return entry;
135
+ };
136
+ /**
137
+ * Spawn `zeph listener` in the background if it isn't already running on
138
+ * this machine. The intent is that the user only ever has to know about
139
+ * `zeph cc` — the phone-to-tmux bridge tags along automatically. Output
140
+ * goes to `~/.zeph/listener.log` so it isn't lost on detach; the listener
141
+ * itself writes its own PID to `~/.zeph/listener.pid` on startup and
142
+ * removes it on graceful exit, so subsequent `zeph cc` invocations skip
143
+ * the spawn when a listener is already up.
144
+ *
145
+ * Failure here is non-fatal — `zeph cc` still launches the agent. The
146
+ * user just loses the phone-bridge feature until they restart.
147
+ */
148
+ const ensureListenerRunning = () => {
149
+ if (listenerAlive())
150
+ return;
151
+ const cliPath = resolveCliPath();
152
+ if (!cliPath || !(0, fs_1.existsSync)(cliPath))
153
+ return;
154
+ try {
155
+ (0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
156
+ const out = (0, fs_1.openSync)(LISTENER_LOG_FILE, 'a');
157
+ const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
158
+ detached: true,
159
+ stdio: ['ignore', out, out],
160
+ env: { ...process.env, ZEPH_LISTENER_AUTOSTART: '1' },
161
+ });
162
+ child.unref();
163
+ console.log(`zeph: listener autostarted in background (log: ${LISTENER_LOG_FILE})`);
164
+ }
165
+ catch (err) {
166
+ console.error(`zeph: listener autostart failed: ${err.message}`);
167
+ }
168
+ };
169
+ /**
170
+ * Launch the agent in a named tmux session (or directly if nested) and
171
+ * forward its exit code. `extra` is appended to the agent invocation, so
172
+ * `zeph cc --resume foo` runs `claude --resume foo` inside the session.
173
+ * Returns when the agent exits.
174
+ */
175
+ const handleAgentSession = (agent, extra = []) => {
176
+ // Best-effort: make sure the phone-bridge daemon is running before we
177
+ // launch the agent. The user shouldn't need to remember a second
178
+ // command for the picker on their phone to work.
179
+ ensureListenerRunning();
180
+ return new Promise((resolve) => {
181
+ const { cmd, args } = targetForAgent(agent, extra);
182
+ const start = Date.now();
183
+ const child = (0, child_process_1.spawn)(cmd, args, { stdio: 'inherit' });
184
+ child.on('exit', (code) => {
185
+ const dur = Date.now() - start;
186
+ // Short-lived non-zero exits are the symptom of "ran from a
187
+ // pane that isn't a real TTY" (iTerm tmux integration pane,
188
+ // some IDE terminals). The user otherwise just sees their
189
+ // shell return with `[exited]` and no clue what went wrong.
190
+ if (code && code !== 0 && dur < 2000) {
191
+ console.error(`zeph: ${cmd} ${args.join(' ')} exited ${code} after ${dur}ms.\n` +
192
+ ` If this terminal is itself inside tmux (or an iTerm/Warp\n` +
193
+ ` tmux-integration pane), run \`zeph cc\` from a plain shell\n` +
194
+ ` pane instead — \`tmux new\` needs a real TTY to attach.`);
195
+ }
196
+ resolve(code ?? 0);
197
+ });
198
+ child.on('error', (err) => {
199
+ if (err.code === 'ENOENT') {
200
+ console.error(`zeph: '${cmd}' not found on PATH`);
201
+ resolve(127);
202
+ }
203
+ else {
204
+ console.error(`zeph: failed to spawn ${cmd}: ${err.message}`);
205
+ resolve(1);
206
+ }
207
+ });
208
+ });
209
+ };
210
+ exports.handleAgentSession = handleAgentSession;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zeph-to/hook-sdk",
3
- "version": "1.8.0",
4
- "description": "Zeph push notification SDK + CLI zero dependencies",
3
+ "version": "1.10.0",
4
+ "description": "Zeph push notification SDK + CLI for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^22.0.0",
31
+ "@types/ws": "^8.18.1",
31
32
  "typescript": "^5.8.0",
32
33
  "vitest": "^2.1.9"
33
34
  },
@@ -62,5 +63,9 @@
62
63
  "claude",
63
64
  "devtools"
64
65
  ],
65
- "license": "Apache-2.0"
66
+ "license": "Apache-2.0",
67
+ "dependencies": {
68
+ "@inquirer/prompts": "^8.4.3",
69
+ "ws": "^8.21.0"
70
+ }
66
71
  }