@zeph-to/hook-sdk 1.6.0 → 1.7.1

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
@@ -60,13 +60,15 @@ zeph notify --title "Hello" --json
60
60
 
61
61
  | Flag | Description |
62
62
  |------|-------------|
63
- | `--title <text>` | Push title |
64
- | `--body <text>` | Push body |
63
+ | `--title <text>` | Push title (default: `"Task done"`) |
64
+ | `--body <text>` | Push body (default: `"<project> · <branch>"` if cwd is a git repo, else `"<project>"`) |
65
65
  | `--url <url>` | URL to include |
66
66
  | `--type <type>` | Push type: `note`, `link`, `file`, `hook` |
67
67
  | `--priority <p>` | Priority: `low`, `normal`, `high`, `urgent` |
68
68
  | `--device <id>` | Target device ID |
69
69
 
70
+ The defaults are tuned for hook-driven invocations (e.g. Stop hooks calling `zeph notify --title "Task done"` without a body) — you'll see which project + branch finished without writing per-IDE wrappers. Pass `--body ""` explicitly to suppress.
71
+
70
72
  ### List Options
71
73
 
72
74
  | Flag | Description |
@@ -188,6 +190,12 @@ try {
188
190
  | Copilot CLI | Session end hook |
189
191
  | Cline | Rules file |
190
192
 
193
+ ## Encryption
194
+
195
+ Push bodies are encrypted with AES-256-GCM. The wrapping key is derived via ECDH P-256 and synced across your own devices on first run so every device can read the same push. Toggle encryption in the Zeph app (Settings → Encryption); when disabled, the CLI sends plaintext. No configuration needed.
196
+
197
+ **Threat model honesty:** keys are persisted on the Zeph backend to enable cross-device sync, so this is *device-shared* encryption — not true end-to-end. It protects push contents from passive network observers and from a leaked database snapshot taken without the key store, but it does **not** protect against the Zeph backend itself (it has the keys it serves to your devices). A true E2E mode (per-device keypairs, server stores only public keys, no key escrow) is on the roadmap.
198
+
191
199
  ## Requirements
192
200
 
193
201
  - Node.js >= 18 (uses native `fetch`)
package/dist/cli.js CHANGED
@@ -8,9 +8,10 @@ const errors_js_1 = require("./errors.js");
8
8
  const installer_js_1 = require("./installer.js");
9
9
  const config_js_1 = require("./config.js");
10
10
  const PROJECT_DIR_VARS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
11
+ const detectProjectDir = () => PROJECT_DIR_VARS.reduce((found, key) => found || process.env[key], undefined) ?? process.cwd();
11
12
  const isMuted = () => {
12
13
  try {
13
- const dir = PROJECT_DIR_VARS.reduce((found, key) => found || process.env[key], undefined) ?? process.cwd();
14
+ const dir = detectProjectDir();
14
15
  const raw = (0, child_process_1.execFileSync)('cksum', { input: dir, encoding: 'utf-8' });
15
16
  const hash = raw.split(' ')[0];
16
17
  return (0, fs_1.existsSync)(`/tmp/zeph-muted-${hash}`);
@@ -19,6 +20,21 @@ const isMuted = () => {
19
20
  return false;
20
21
  }
21
22
  };
23
+ const detectBranchAndProject = () => {
24
+ const dir = detectProjectDir();
25
+ const project = dir.split('/').filter(Boolean).pop() ?? 'project';
26
+ let branch;
27
+ try {
28
+ branch = (0, child_process_1.execFileSync)('git', ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'], {
29
+ encoding: 'utf-8',
30
+ stdio: ['pipe', 'pipe', 'pipe'],
31
+ }).trim();
32
+ if (!branch || branch === 'HEAD')
33
+ branch = undefined;
34
+ }
35
+ catch { /* not a git repo */ }
36
+ return { branch, project };
37
+ };
22
38
  // ── Arg Parser ──────────────────────────────────────────────────
23
39
  const parseArgs = (argv) => {
24
40
  const result = {};
@@ -62,6 +78,7 @@ Notify options:
62
78
  --type <type> Push type (note|link|file|hook) [default: hook]
63
79
  --priority <p> Priority (low|normal|high|urgent) [default: normal]
64
80
  --device <id> Target device ID
81
+ --session <id> AI session ID (or set ZEPH_SESSION_ID env)
65
82
 
66
83
  List options:
67
84
  --limit <n> Number of pushes (1-20, default 5)
@@ -83,7 +100,8 @@ Global options:
83
100
 
84
101
  Environment:
85
102
  ZEPH_API_KEY API key (fallback when --key not provided)
86
- ZEPH_BASE_URL API base URL (fallback when --base-url not provided)`);
103
+ ZEPH_BASE_URL API base URL (fallback when --base-url not provided)
104
+ ZEPH_SESSION_ID AI session ID (fallback when --session not provided)`);
87
105
  };
88
106
  const printError = (message, isJson) => {
89
107
  if (isJson) {
@@ -119,13 +137,26 @@ const handleNotify = async (args) => {
119
137
  if (!hook)
120
138
  return 3;
121
139
  try {
140
+ const sessionId = args.session || (0, config_js_1.resolvedEnv)('ZEPH_SESSION_ID') || undefined;
141
+ // When body isn't supplied (common case for hook-driven invocations like
142
+ // `zeph notify --title "Task done"`), auto-fill with branch + project so
143
+ // the user can tell which session finished without opening the app.
144
+ let title = args.title;
145
+ let body = args.body;
146
+ if (!body) {
147
+ const { branch, project } = detectBranchAndProject();
148
+ body = branch ? `${project} · ${branch}` : project;
149
+ }
150
+ if (!title)
151
+ title = 'Task done';
122
152
  const result = await hook.notify({
123
- title: args.title,
124
- body: args.body,
153
+ title,
154
+ body,
125
155
  url: args.url,
126
156
  type: args.type || 'hook',
127
157
  priority: args.priority || undefined,
128
158
  targetDeviceId: args.device,
159
+ sessionId,
129
160
  });
130
161
  if (isJson) {
131
162
  printJson({ pushId: result.pushId, status: 'ok' });
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,UAAU,QAAyC,CAAC;AACjE,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAG/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,QAA2B,CAAC;AACnD,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,SAGlD,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,UAM7B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,QAAQ,UAAU,KAAG,IAG/C,CAAC;AAEF,eAAO,MAAM,OAAO,QAOhB,CAAC"}
package/dist/config.js CHANGED
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = exports.saveConfig = exports.loadConfig = exports.resolvedEnv = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
4
4
  const fs_1 = require("fs");
5
+ const os_1 = require("os");
5
6
  const path_1 = require("path");
6
- exports.CONFIG_DIR = (0, path_1.join)(process.env.HOME ?? '~', '.zeph');
7
+ exports.CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.zeph');
7
8
  exports.CONFIG_FILE = (0, path_1.join)(exports.CONFIG_DIR, 'config.json');
8
9
  const resolvedEnv = (key) => {
9
10
  const val = process.env[key];
package/dist/crypto.d.ts CHANGED
@@ -1,7 +1,30 @@
1
1
  /**
2
- * E2E encryption for Hook SDK — self-contained ECDH P-256 + AES-256-GCM
3
- * Mirrors @zeph/crypto API but bundled inline (no external dependency).
4
- * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
2
+ * Device-shared encryption for Hook SDK — self-contained ECDH P-256 +
3
+ * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
4
+ * dependency). Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
5
+ *
6
+ * Threat model honesty (do not call this "E2E" without a footnote):
7
+ *
8
+ * The Zeph backend persists the per-user private key in plaintext so it
9
+ * can be synced down to a fresh device (fetchServerKeys / uploadServerKeys
10
+ * below). That means the backend can decrypt any push body — this is NOT
11
+ * end-to-end in the standard sense. What it gives you is:
12
+ * • Protection against passive network observers
13
+ * • Protection against a leaked DB snapshot taken without the key store
14
+ * • Cross-device readability (all your devices share one keypair)
15
+ * What it does NOT give you:
16
+ * • Protection against the Zeph backend itself
17
+ * • Forward secrecy — encryptPushBodyForSelf / encryptFileForSelf do
18
+ * ECDH(self, self), which collapses to a static derived key. A single
19
+ * device compromise (since all your devices share the same keypair)
20
+ * lets the attacker decrypt every past push for which they have the
21
+ * ciphertext. The per-message AES key is random, but its wrap key is
22
+ * static, so wrapped keys are decryptable forever.
23
+ *
24
+ * True E2E would require a per-device keypair (server stores only public
25
+ * keys; senders wrap the message key once per recipient device public
26
+ * key). That refactor is on the roadmap; until then, treat push bodies as
27
+ * sensitive-but-not-secret.
5
28
  */
6
29
  /**
7
30
  * Initialize crypto: sync keys with server, then fallback to local/generate.
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AAqCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA2JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AAqCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
package/dist/crypto.js CHANGED
@@ -1,13 +1,37 @@
1
1
  "use strict";
2
2
  /**
3
- * E2E encryption for Hook SDK — self-contained ECDH P-256 + AES-256-GCM
4
- * Mirrors @zeph/crypto API but bundled inline (no external dependency).
5
- * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
3
+ * Device-shared encryption for Hook SDK — self-contained ECDH P-256 +
4
+ * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
5
+ * dependency). Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
6
+ *
7
+ * Threat model honesty (do not call this "E2E" without a footnote):
8
+ *
9
+ * The Zeph backend persists the per-user private key in plaintext so it
10
+ * can be synced down to a fresh device (fetchServerKeys / uploadServerKeys
11
+ * below). That means the backend can decrypt any push body — this is NOT
12
+ * end-to-end in the standard sense. What it gives you is:
13
+ * • Protection against passive network observers
14
+ * • Protection against a leaked DB snapshot taken without the key store
15
+ * • Cross-device readability (all your devices share one keypair)
16
+ * What it does NOT give you:
17
+ * • Protection against the Zeph backend itself
18
+ * • Forward secrecy — encryptPushBodyForSelf / encryptFileForSelf do
19
+ * ECDH(self, self), which collapses to a static derived key. A single
20
+ * device compromise (since all your devices share the same keypair)
21
+ * lets the attacker decrypt every past push for which they have the
22
+ * ciphertext. The per-message AES key is random, but its wrap key is
23
+ * static, so wrapped keys are decryptable forever.
24
+ *
25
+ * True E2E would require a per-device keypair (server stores only public
26
+ * keys; senders wrap the message key once per recipient device public
27
+ * key). That refactor is on the roadmap; until then, treat push bodies as
28
+ * sensitive-but-not-secret.
6
29
  */
7
30
  Object.defineProperty(exports, "__esModule", { value: true });
8
31
  exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
9
32
  /// <reference lib="dom" />
10
33
  const fs_1 = require("fs");
34
+ const os_1 = require("os");
11
35
  const path_1 = require("path");
12
36
  // ─── Base64 helpers ───
13
37
  const toBase64 = (buffer) => {
@@ -79,7 +103,7 @@ const encryptFileContent = async (content, senderPrivateKey, recipientPublicKey)
79
103
  };
80
104
  };
81
105
  // ─── Key persistence (~/.config/zeph/keys.json) ───
82
- const KEYS_DIR = (0, path_1.join)(process.env.HOME ?? '~', '.config', 'zeph');
106
+ const KEYS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.config', 'zeph');
83
107
  const KEYS_PATH = (0, path_1.join)(KEYS_DIR, 'keys.json');
84
108
  const loadStoredKeys = () => {
85
109
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AAyNA,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAmH1F,CAAC"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/installer.ts"],"names":[],"mappings":"AA+NA,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAG,OAAO,CAAC,MAAM,CAmH1F,CAAC"}
package/dist/installer.js CHANGED
@@ -3,12 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleInstall = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const fs_1 = require("fs");
6
+ const os_1 = require("os");
6
7
  const path_1 = require("path");
7
8
  const readline_1 = require("readline");
8
9
  const zeph_hook_js_1 = require("./zeph-hook.js");
9
10
  const config_js_1 = require("./config.js");
10
11
  const templates_js_1 = require("./templates.js");
11
- const HOME = process.env.HOME ?? '~';
12
+ const HOME = (0, os_1.homedir)();
12
13
  // ── Helpers ──────────────────────────────────────────────────────
13
14
  const ok = (msg) => console.log(` + ${msg}`);
14
15
  const fail = (msg) => console.log(` - ${msg}`);
@@ -62,9 +63,14 @@ const injectMcpJson = (filePath) => {
62
63
  catch { /* new file */ }
63
64
  if (!data.mcpServers)
64
65
  data.mcpServers = {};
66
+ // Pass through env explicitly so the MCP server doesn't have to rely on
67
+ // process-env inheritance (which behaves differently per IDE — Cursor and
68
+ // Windsurf spawn the MCP from a graphical context that may not inherit
69
+ // shell env). Mirrors plugin/.mcp.json.
65
70
  data.mcpServers.zeph = {
66
71
  command: 'npx',
67
72
  args: ['-y', '@zeph-to/mcp-server'],
73
+ env: { ZEPH_API_KEY: '${ZEPH_API_KEY}' },
68
74
  };
69
75
  (0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
70
76
  (0, fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2) + '\n');
@@ -1,5 +1,5 @@
1
1
  export declare const CURSOR_HOOKS: string;
2
- export declare const CURSOR_RULE = "---\ndescription: \"Zeph notification rules\"\nalwaysApply: true\n---\n\nWhen you complete a coding task, call the zeph_notify MCP tool with a brief summary.\nUse zeph_prompt for user decisions (2-4 options). Use zeph_input for free-form text.\nDo not notify for trivial operations (file reads, simple searches).\n";
2
+ export declare const CURSOR_RULE = "---\ndescription: \"Zeph remote-control rules\"\nalwaysApply: true\n---\n\n# Zeph \u2014 Remote-Control Rules\n\nA Stop hook (~/.cursor/hooks.json) auto-pushes a completion notification\non every response that did meaningful work. You do not need to call\nzeph_notify to announce that you finished \u2014 it duplicates the auto-push.\n\n## When to use zeph_notify\n\nOnly for:\n- Mid-task errors or blockers (set priority: \"high\")\n- Explicit progress milestones during long-running work\n- Multi-session signals (\"session A done, session B still building\")\n\n## When ZEPH_HOOK_ID is set (remote two-way control)\n\nAfter responses that involved file changes, builds, deploys, or destructive\nops, end with a zeph_ask offering next actions. Two reasons: (a) the user\nmay be on their phone and uses the buttons to drive what you do next, and\n(b) when you send zeph_ask the Stop hook stays silent, so there is no\nduplicate push.\n\nExample final action:\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n\nA zeph_ask response IS a direct user command \u2014 execute it immediately\nwithout re-confirming. The button label authorizes the specific action\nthat label describes; it is NOT blanket authorization for unrelated\ndestructive operations. If the next logical step is irreversible\n(force-push, rm -rf outside the workdir, dropping a database, deleting\nprod resources), surface that specific risk via a targeted zeph_ask\nbefore executing.\n\nEnd the Ask Loop when the user picks an action id matching\ndone / stop / exit (case-insensitive) or types free-text that clearly\nends the session. Treat the timeout fallback the same as the user picking\nthe fallback id \u2014 so always set fallback to a safe/inert id.\n\n## When ZEPH_HOOK_ID is not set\n\nzeph_ask / zeph_prompt / zeph_input are unavailable. Use zeph_notify only\nas described above.\n\nDo not notify for trivial operations (file reads, simple searches).\n";
3
3
  export declare const WINDSURF_HOOKS: string;
4
4
  export declare const GEMINI_HOOKS: {
5
5
  hooks: {
@@ -18,5 +18,5 @@ export declare const GEMINI_HOOKS: {
18
18
  };
19
19
  export declare const CODEX_HOOKS: string;
20
20
  export declare const COPILOT_HOOKS: string;
21
- export declare const CLINE_RULE = "When you complete a coding task, call the zeph_notify MCP tool with a brief summary.\nUse zeph_prompt for user decisions (2-4 options). Use zeph_input for free-form text.\nDo not notify for trivial operations (file reads, simple searches).\n";
21
+ export declare const CLINE_RULE = "# Zeph \u2014 Notification & Remote-Control Rules\n\nCline does not have a Stop hook wired up, so notifications must come from\nyou via MCP tools.\n\n## When to call zeph_notify\n\nAfter meaningful task completion (build, test, deploy, large refactor, or\nmulti-file changes). Skip for trivial operations (file reads, simple\nsearches, short clarifications).\n\nSet priority \"high\" for errors or blockers that interrupt your progress.\n\n## When ZEPH_HOOK_ID is set (remote two-way control)\n\nPrefer zeph_ask over zeph_notify after responses that involved file\nchanges, builds, deploys, or destructive ops. The user may be on their\nphone and uses the buttons to drive what you do next.\n\nExample final action:\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n\nA zeph_ask response IS a direct user command \u2014 execute it immediately\nwithout re-confirming. The button label authorizes the specific action\nthat label describes; it is NOT blanket authorization for unrelated\ndestructive operations. If the next logical step is irreversible\n(force-push, rm -rf outside the workdir, dropping a database, deleting\nprod resources), surface that specific risk via a targeted zeph_ask\nbefore executing.\n\nEnd the Ask Loop when the user picks an action id matching\ndone / stop / exit (case-insensitive). Treat the timeout fallback the\nsame as the user picking the fallback id \u2014 so always set fallback to a\nsafe/inert id.\n";
22
22
  //# sourceMappingURL=templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,YAAY,QAOd,CAAC;AAEZ,eAAO,MAAM,WAAW,+TAQvB,CAAC;AAIF,eAAO,MAAM,cAAc,QAOhB,CAAC;AAIZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAYxB,CAAC;AAIF,eAAO,MAAM,WAAW,QAQb,CAAC;AAIZ,eAAO,MAAM,aAAa,QASf,CAAC;AAIZ,eAAO,MAAM,UAAU,sPAGtB,CAAC"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,YAAY,QAOd,CAAC;AAEZ,eAAO,MAAM,WAAW,ooEAyDvB,CAAC;AAIF,eAAO,MAAM,cAAc,QAOhB,CAAC;AAIZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;CAYxB,CAAC;AAIF,eAAO,MAAM,WAAW,QAQb,CAAC;AAIZ,eAAO,MAAM,aAAa,QASf,CAAC;AAIZ,eAAO,MAAM,UAAU,mpDA2CtB,CAAC"}
package/dist/templates.js CHANGED
@@ -1,8 +1,26 @@
1
1
  "use strict";
2
2
  // ── Hook & Rule templates for each agent ─────────────────────────
3
+ //
4
+ // Two policies depending on whether the agent has a working Stop-equivalent
5
+ // hook installed via this SDK:
6
+ //
7
+ // 1) Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot, and
8
+ // Claude Code via the separate plugin) — the hook fires on every
9
+ // response and runs `zeph notify`. The AI should NOT manually call
10
+ // zeph_notify just to announce completion, because that duplicates the
11
+ // auto-push. Rules here mirror the Claude Code plugin's policy.
12
+ //
13
+ // 2) Rule-only agents (Cline, Aider) — no Stop hook is wired up, so the
14
+ // AI must manually call zeph_notify for meaningful completions. The
15
+ // Ask-Loop pattern still applies when ZEPH_HOOK_ID is configured.
3
16
  Object.defineProperty(exports, "__esModule", { value: true });
4
17
  exports.CLINE_RULE = exports.COPILOT_HOOKS = exports.CODEX_HOOKS = exports.GEMINI_HOOKS = exports.WINDSURF_HOOKS = exports.CURSOR_RULE = exports.CURSOR_HOOKS = void 0;
5
- const NOTIFY_CMD = 'npx @zeph-to/hook-sdk notify --title "Task done" 2>/dev/null || true';
18
+ // Graceful resolution: prefer the installed `zeph` CLI, but fall back to
19
+ // `npx -y @zeph-to/hook-sdk` so the hook still fires when the user
20
+ // installed via a non-standard prefix and the binary isn't on PATH at hook
21
+ // fire time (e.g. ~/.local/bin without PATH update). This mirrors the
22
+ // pattern in plugin/hooks/zeph-{stop,ask}.sh.
23
+ const NOTIFY_CMD = '$(command -v zeph || echo "npx -y @zeph-to/hook-sdk") notify --title "Task done" 2>/dev/null || true';
6
24
  // ── Cursor ───────────────────────────────────────────────────────
7
25
  exports.CURSOR_HOOKS = JSON.stringify({
8
26
  version: 1,
@@ -13,12 +31,61 @@ exports.CURSOR_HOOKS = JSON.stringify({
13
31
  },
14
32
  }, null, 2);
15
33
  exports.CURSOR_RULE = `---
16
- description: "Zeph notification rules"
34
+ description: "Zeph remote-control rules"
17
35
  alwaysApply: true
18
36
  ---
19
37
 
20
- When you complete a coding task, call the zeph_notify MCP tool with a brief summary.
21
- Use zeph_prompt for user decisions (2-4 options). Use zeph_input for free-form text.
38
+ # Zeph Remote-Control Rules
39
+
40
+ A Stop hook (~/.cursor/hooks.json) auto-pushes a completion notification
41
+ on every response that did meaningful work. You do not need to call
42
+ zeph_notify to announce that you finished — it duplicates the auto-push.
43
+
44
+ ## When to use zeph_notify
45
+
46
+ Only for:
47
+ - Mid-task errors or blockers (set priority: "high")
48
+ - Explicit progress milestones during long-running work
49
+ - Multi-session signals ("session A done, session B still building")
50
+
51
+ ## When ZEPH_HOOK_ID is set (remote two-way control)
52
+
53
+ After responses that involved file changes, builds, deploys, or destructive
54
+ ops, end with a zeph_ask offering next actions. Two reasons: (a) the user
55
+ may be on their phone and uses the buttons to drive what you do next, and
56
+ (b) when you send zeph_ask the Stop hook stays silent, so there is no
57
+ duplicate push.
58
+
59
+ Example final action:
60
+ zeph_ask({
61
+ title: "Done. Next?",
62
+ actions: [
63
+ { id: "continue", label: "Continue" },
64
+ { id: "review", label: "Review" },
65
+ { id: "done", label: "Done" }
66
+ ],
67
+ placeholder: "or type a command...",
68
+ fallback: "done"
69
+ })
70
+
71
+ A zeph_ask response IS a direct user command — execute it immediately
72
+ without re-confirming. The button label authorizes the specific action
73
+ that label describes; it is NOT blanket authorization for unrelated
74
+ destructive operations. If the next logical step is irreversible
75
+ (force-push, rm -rf outside the workdir, dropping a database, deleting
76
+ prod resources), surface that specific risk via a targeted zeph_ask
77
+ before executing.
78
+
79
+ End the Ask Loop when the user picks an action id matching
80
+ done / stop / exit (case-insensitive) or types free-text that clearly
81
+ ends the session. Treat the timeout fallback the same as the user picking
82
+ the fallback id — so always set fallback to a safe/inert id.
83
+
84
+ ## When ZEPH_HOOK_ID is not set
85
+
86
+ zeph_ask / zeph_prompt / zeph_input are unavailable. Use zeph_notify only
87
+ as described above.
88
+
22
89
  Do not notify for trivial operations (file reads, simple searches).
23
90
  `;
24
91
  // ── Windsurf ─────────────────────────────────────────────────────
@@ -66,7 +133,47 @@ exports.COPILOT_HOOKS = JSON.stringify({
66
133
  },
67
134
  }, null, 2);
68
135
  // ── Cline ────────────────────────────────────────────────────────
69
- exports.CLINE_RULE = `When you complete a coding task, call the zeph_notify MCP tool with a brief summary.
70
- Use zeph_prompt for user decisions (2-4 options). Use zeph_input for free-form text.
71
- Do not notify for trivial operations (file reads, simple searches).
136
+ exports.CLINE_RULE = `# Zeph Notification & Remote-Control Rules
137
+
138
+ Cline does not have a Stop hook wired up, so notifications must come from
139
+ you via MCP tools.
140
+
141
+ ## When to call zeph_notify
142
+
143
+ After meaningful task completion (build, test, deploy, large refactor, or
144
+ multi-file changes). Skip for trivial operations (file reads, simple
145
+ searches, short clarifications).
146
+
147
+ Set priority "high" for errors or blockers that interrupt your progress.
148
+
149
+ ## When ZEPH_HOOK_ID is set (remote two-way control)
150
+
151
+ Prefer zeph_ask over zeph_notify after responses that involved file
152
+ changes, builds, deploys, or destructive ops. The user may be on their
153
+ phone and uses the buttons to drive what you do next.
154
+
155
+ Example final action:
156
+ zeph_ask({
157
+ title: "Done. Next?",
158
+ actions: [
159
+ { id: "continue", label: "Continue" },
160
+ { id: "review", label: "Review" },
161
+ { id: "done", label: "Done" }
162
+ ],
163
+ placeholder: "or type a command...",
164
+ fallback: "done"
165
+ })
166
+
167
+ A zeph_ask response IS a direct user command — execute it immediately
168
+ without re-confirming. The button label authorizes the specific action
169
+ that label describes; it is NOT blanket authorization for unrelated
170
+ destructive operations. If the next logical step is irreversible
171
+ (force-push, rm -rf outside the workdir, dropping a database, deleting
172
+ prod resources), surface that specific risk via a targeted zeph_ask
173
+ before executing.
174
+
175
+ End the Ask Loop when the user picks an action id matching
176
+ done / stop / exit (case-insensitive). Treat the timeout fallback the
177
+ same as the user picking the fallback id — so always set fallback to a
178
+ safe/inert id.
72
179
  `;
package/dist/types.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface NotifyPayload {
10
10
  type?: 'note' | 'link' | 'file' | 'hook';
11
11
  priority?: 'low' | 'normal' | 'high' | 'urgent';
12
12
  targetDeviceId?: string;
13
+ sessionId?: string;
13
14
  }
14
15
  export interface NotifyResult {
15
16
  pushId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;CACxD;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;CACxD;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/hook-sdk",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Zeph push notification SDK + CLI — zero dependencies",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",