@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.
- package/README.md +283 -14
- package/dist/cli.js +33 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/installer.d.ts +7 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +84 -27
- package/dist/listener.d.ts +116 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +878 -0
- package/dist/wrapper.d.ts +26 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +210 -0
- package/package.json +8 -3
|
@@ -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"}
|
package/dist/wrapper.js
ADDED
|
@@ -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.
|
|
4
|
-
"description": "Zeph push notification SDK + CLI
|
|
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
|
}
|