@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # @zeph-to/hook-sdk
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@zeph-to/hook-sdk.svg)](https://www.npmjs.com/package/@zeph-to/hook-sdk)
4
+ [![downloads](https://img.shields.io/npm/dm/@zeph-to/hook-sdk.svg)](https://www.npmjs.com/package/@zeph-to/hook-sdk)
5
+ [![node](https://img.shields.io/node/v/@zeph-to/hook-sdk.svg)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/@zeph-to/hook-sdk.svg)](./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.
@@ -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 {
@@ -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;AA6QF;;;;;;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;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAkB5D,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,aA0DzC,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,CA6E3F,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
- * Locate the most recent Claude Code session UUID for the working
383
- * directory of a tmux pane. Mirrors `mcp-server/config.ts`'s
384
- * detectClaudeSessionId: CC writes per-session jsonl files at
385
- * `~/.claude/projects/<projectHash>/<UUID>.jsonl` where the hash is
386
- * the cwd with `/` replaced by `-`.
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 detectClaudeSessionId = (cwd) => {
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) => {
@@ -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;AAkGF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,QAAO,MAAM,EAAO,KAAG,OAAO,CAAC,MAAM,CAmCtF,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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/hook-sdk",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Zeph push notification SDK + CLI for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",