@zeph-to/hook-sdk 1.10.0 → 1.10.2
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 +5 -0
- package/dist/listener.d.ts +11 -1
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +69 -7
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @zeph-to/hook-sdk
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@zeph-to/hook-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/@zeph-to/hook-sdk)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
3
8
|
Push notification SDK + CLI for [Zeph](https://zeph.to), with an optional
|
|
4
9
|
resident listener that **drives Claude Code / Codex / Gemini sessions
|
|
5
10
|
from your phone** by injecting messages into named tmux sessions.
|
package/dist/listener.d.ts
CHANGED
|
@@ -34,6 +34,15 @@ interface AgentSession {
|
|
|
34
34
|
export declare const checkRateLimit: (session: string, now?: number) => boolean;
|
|
35
35
|
/** Read the foreground command in the named tmux session's active pane. */
|
|
36
36
|
export declare const paneCurrentCommand: (session: string) => string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Mark the cached socket as no longer trustworthy — call this when a
|
|
39
|
+
* tmux command fails against the cached path. The next findTmuxSocket()
|
|
40
|
+
* will redo full discovery (default probe → /var/folders walk → lsof
|
|
41
|
+
* fallback) instead of returning a stale answer. Without this the
|
|
42
|
+
* listener wedged at "reported 0 session(s)" forever after a tmux
|
|
43
|
+
* server restart, even when a new server was live and discoverable.
|
|
44
|
+
*/
|
|
45
|
+
export declare const invalidateTmuxSocketCache: () => void;
|
|
37
46
|
/**
|
|
38
47
|
* Parse a `zeph-*` tmux session name into `{project, label}`. For
|
|
39
48
|
* Phase 1 the wrapper only emits `zeph-<project>` (no labels), so the
|
|
@@ -50,7 +59,8 @@ export declare const parseSessionName: (name: string) => {
|
|
|
50
59
|
* directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
|
|
51
60
|
* detectClaudeSessionId: CC writes per-session jsonl files at
|
|
52
61
|
* `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
|
|
53
|
-
* the cwd with `/` replaced by `-`.
|
|
62
|
+
* the cwd with `/` replaced by `-`. Cached for 60s — see
|
|
63
|
+
* claudeSessionCache.
|
|
54
64
|
*/
|
|
55
65
|
export declare const detectClaudeSessionId: (cwd: string) => string | null;
|
|
56
66
|
export interface CollectResult {
|
package/dist/listener.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuBH,KAAK,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAG/C,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,MAAK,MAAmB,KAAG,OAgB1E,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAO7D,CAAC;
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAuBH,KAAK,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAG/C,UAAU,YAAY;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,MAAK,MAAmB,KAAG,OAgB1E,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAO7D,CAAC;AAiDF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAG5C,CAAC;AA8NF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAK3F,CAAC;AAuCF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,CAAC;AAoEF,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAO,aA+DzC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,QAAO,YAAY,EAAuC,CAAC;AAIvF,UAAU,QAAQ;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,UAAU,cAAc;IACpB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAgCD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACnB,MAAM,QAAQ,EACd,OAAM,cAAmB,KAC1B,OAQF,CAAC;AA2BF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GAAI,OAAM,MAAmB,KAAG,MAGnE,CAAC;AAyLF,eAAO,MAAM,cAAc,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAyF3F,CAAC"}
|
package/dist/listener.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.handleListener = exports.computeListenerDeviceId = exports.handlePush = exports.collectSessions = exports.collectSessionsVerbose = exports.detectClaudeSessionId = exports.parseSessionName = exports.paneCurrentCommand = exports.checkRateLimit = void 0;
|
|
28
|
+
exports.handleListener = exports.computeListenerDeviceId = exports.handlePush = exports.collectSessions = exports.collectSessionsVerbose = exports.detectClaudeSessionId = exports.parseSessionName = exports.invalidateTmuxSocketCache = exports.paneCurrentCommand = exports.checkRateLimit = void 0;
|
|
29
29
|
const child_process_1 = require("child_process");
|
|
30
30
|
const crypto_1 = require("crypto");
|
|
31
31
|
const fs_1 = require("fs");
|
|
@@ -135,6 +135,19 @@ let cachedSocketPath = null;
|
|
|
135
135
|
* removes the ambiguity so we don't re-probe every collectSessions
|
|
136
136
|
* cycle (which was spamming the log with "tmux: default socket OK"). */
|
|
137
137
|
let cacheValid = false;
|
|
138
|
+
/**
|
|
139
|
+
* Mark the cached socket as no longer trustworthy — call this when a
|
|
140
|
+
* tmux command fails against the cached path. The next findTmuxSocket()
|
|
141
|
+
* will redo full discovery (default probe → /var/folders walk → lsof
|
|
142
|
+
* fallback) instead of returning a stale answer. Without this the
|
|
143
|
+
* listener wedged at "reported 0 session(s)" forever after a tmux
|
|
144
|
+
* server restart, even when a new server was live and discoverable.
|
|
145
|
+
*/
|
|
146
|
+
const invalidateTmuxSocketCache = () => {
|
|
147
|
+
cacheValid = false;
|
|
148
|
+
cachedSocketPath = null;
|
|
149
|
+
};
|
|
150
|
+
exports.invalidateTmuxSocketCache = invalidateTmuxSocketCache;
|
|
138
151
|
const probeTmuxSocketDetail = (socketPath) => {
|
|
139
152
|
const args = socketPath ? ['-S', socketPath, 'list-sessions'] : ['list-sessions'];
|
|
140
153
|
const r = (0, child_process_1.spawnSync)('tmux', args, {
|
|
@@ -379,13 +392,20 @@ const parseSessionName = (name) => {
|
|
|
379
392
|
exports.parseSessionName = parseSessionName;
|
|
380
393
|
const CLAUDE_PROJECTS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'projects');
|
|
381
394
|
/**
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
395
|
+
* Cache for detectClaudeSessionId. The function walks every jsonl file
|
|
396
|
+
* in `~/.claude/projects/<hash>/` on each call — after weeks of CC use
|
|
397
|
+
* that directory holds hundreds of session files, and we were calling
|
|
398
|
+
* this per tmux session per 5-second report cycle. Heavy disk I/O
|
|
399
|
+
* compounded with multiple sessions caused the report cycle to spike
|
|
400
|
+
* CPU and starve the host shell.
|
|
401
|
+
*
|
|
402
|
+
* The current-session UUID only changes when a new CC session starts
|
|
403
|
+
* in that directory (rare, on the order of hours), so a 60-second TTL
|
|
404
|
+
* is safe and cuts the per-cycle stat count by ~12×.
|
|
387
405
|
*/
|
|
388
|
-
const
|
|
406
|
+
const claudeSessionCache = new Map();
|
|
407
|
+
const CLAUDE_SESSION_CACHE_TTL_MS = 60_000;
|
|
408
|
+
const doDetectClaudeSessionId = (cwd) => {
|
|
389
409
|
try {
|
|
390
410
|
const projectHash = cwd.replace(/\//g, '-');
|
|
391
411
|
const sessionsDir = (0, path_1.join)(CLAUDE_PROJECTS_DIR, projectHash);
|
|
@@ -407,6 +427,32 @@ const detectClaudeSessionId = (cwd) => {
|
|
|
407
427
|
return null;
|
|
408
428
|
}
|
|
409
429
|
};
|
|
430
|
+
/**
|
|
431
|
+
* Locate the most recent Claude Code session UUID for the working
|
|
432
|
+
* directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
|
|
433
|
+
* detectClaudeSessionId: CC writes per-session jsonl files at
|
|
434
|
+
* `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
|
|
435
|
+
* the cwd with `/` replaced by `-`. Cached for 60s — see
|
|
436
|
+
* claudeSessionCache.
|
|
437
|
+
*/
|
|
438
|
+
const detectClaudeSessionId = (cwd) => {
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
const cached = claudeSessionCache.get(cwd);
|
|
441
|
+
if (cached && cached.expiresAt > now)
|
|
442
|
+
return cached.sessionId;
|
|
443
|
+
// Cap cache size so a long-lived listener that's seen many cwds
|
|
444
|
+
// doesn't grow unbounded. 64 is plenty for any realistic setup.
|
|
445
|
+
if (claudeSessionCache.size >= 64) {
|
|
446
|
+
// Evict the oldest-expiring entry — Map iteration order is
|
|
447
|
+
// insertion order, so the first key we hit is the oldest.
|
|
448
|
+
const firstKey = claudeSessionCache.keys().next().value;
|
|
449
|
+
if (firstKey !== undefined)
|
|
450
|
+
claudeSessionCache.delete(firstKey);
|
|
451
|
+
}
|
|
452
|
+
const sessionId = doDetectClaudeSessionId(cwd);
|
|
453
|
+
claudeSessionCache.set(cwd, { sessionId, expiresAt: now + CLAUDE_SESSION_CACHE_TTL_MS });
|
|
454
|
+
return sessionId;
|
|
455
|
+
};
|
|
410
456
|
exports.detectClaudeSessionId = detectClaudeSessionId;
|
|
411
457
|
// U+241F "Symbol for Unit Separator" — a *printable* Unicode glyph
|
|
412
458
|
// (3-byte UTF-8) that visually represents the C0 Unit Separator but is
|
|
@@ -485,6 +531,11 @@ const collectSessionsVerbose = () => {
|
|
|
485
531
|
if (list.status !== 0) {
|
|
486
532
|
const stderr = (list.stderr ?? '').toString().trim();
|
|
487
533
|
log(` tmux list-sessions failed: status=${list.status}${stderr ? ', stderr=' + stderr : ''}`);
|
|
534
|
+
// Tmux call failed against the cached socket — the server it
|
|
535
|
+
// pointed at is gone (died, restarted at a different path, etc).
|
|
536
|
+
// Invalidate so the next cycle re-runs full discovery instead of
|
|
537
|
+
// wedging the listener at "reported 0 session(s)" forever.
|
|
538
|
+
(0, exports.invalidateTmuxSocketCache)();
|
|
488
539
|
return { sessions: [], rejected: [] };
|
|
489
540
|
}
|
|
490
541
|
const rawLines = (list.stdout ?? '').split('\n').filter(Boolean);
|
|
@@ -842,6 +893,17 @@ const handleListener = async (args) => {
|
|
|
842
893
|
log(`zeph listener starting — ${wsUrl}`);
|
|
843
894
|
log(`device=${(0, exports.computeListenerDeviceId)()} host=${(0, os_1.hostname)()} pid=${process.pid}`);
|
|
844
895
|
log("Waiting for 'agent.command' pushes from the phone picker. Ctrl-C to stop.");
|
|
896
|
+
// Heartbeat memory log — once an hour. Lets the user (and us) spot
|
|
897
|
+
// gradual growth in a long-running daemon before it gets bad enough
|
|
898
|
+
// to make the host shell unresponsive. The MB counter is human-
|
|
899
|
+
// readable and tiny enough not to bloat the log.
|
|
900
|
+
const HEAP_LOG_INTERVAL_MS = 60 * 60 * 1000;
|
|
901
|
+
const heapLogTimer = setInterval(() => {
|
|
902
|
+
const m = process.memoryUsage();
|
|
903
|
+
const mb = (n) => Math.round(n / 1024 / 1024);
|
|
904
|
+
log(`heap: rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB external=${mb(m.external)}MB`);
|
|
905
|
+
}, HEAP_LOG_INTERVAL_MS);
|
|
906
|
+
heapLogTimer.unref();
|
|
845
907
|
let shuttingDown = false;
|
|
846
908
|
let activeHandle = null;
|
|
847
909
|
const stop = (sig) => {
|
package/dist/wrapper.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAmHF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCtF,CAAC"}
|
package/dist/wrapper.js
CHANGED
|
@@ -145,6 +145,23 @@ const resolveCliPath = () => {
|
|
|
145
145
|
* Failure here is non-fatal — `zeph cc` still launches the agent. The
|
|
146
146
|
* user just loses the phone-bridge feature until they restart.
|
|
147
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* Rotate the listener log once it grows past 5 MB. The daemon runs for
|
|
150
|
+
* days and writes 2-3 lines per 5-s cycle, so without rotation the file
|
|
151
|
+
* climbs into the tens of megabytes range pretty quickly. We keep the
|
|
152
|
+
* previous run's tail under `.old` for post-mortem and start fresh.
|
|
153
|
+
*/
|
|
154
|
+
const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
155
|
+
const rotateListenerLogIfLarge = () => {
|
|
156
|
+
try {
|
|
157
|
+
if (!(0, fs_1.existsSync)(LISTENER_LOG_FILE))
|
|
158
|
+
return;
|
|
159
|
+
if ((0, fs_1.statSync)(LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
|
|
160
|
+
return;
|
|
161
|
+
(0, fs_1.renameSync)(LISTENER_LOG_FILE, LISTENER_LOG_FILE + '.old');
|
|
162
|
+
}
|
|
163
|
+
catch { /* best-effort */ }
|
|
164
|
+
};
|
|
148
165
|
const ensureListenerRunning = () => {
|
|
149
166
|
if (listenerAlive())
|
|
150
167
|
return;
|
|
@@ -153,6 +170,7 @@ const ensureListenerRunning = () => {
|
|
|
153
170
|
return;
|
|
154
171
|
try {
|
|
155
172
|
(0, fs_1.mkdirSync)(ZEPH_DIR, { recursive: true });
|
|
173
|
+
rotateListenerLogIfLarge();
|
|
156
174
|
const out = (0, fs_1.openSync)(LISTENER_LOG_FILE, 'a');
|
|
157
175
|
const child = (0, child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
|
|
158
176
|
detached: true,
|