@zeph-to/mcp-server 1.9.2 → 1.11.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 CHANGED
@@ -52,6 +52,7 @@ Add to `~/.claude/settings.json`:
52
52
  | `ZEPH_HOOK_ID` | No | Hook ID (optional — only needed for interactive tools like `zeph_ask`/`zeph_prompt`/`zeph_input`) |
53
53
  | `ZEPH_DEVICE_ID` | No | Target device ID (optional — only needed for interactive tools like `zeph_ask`/`zeph_prompt`/`zeph_input`). Omit to send to all devices |
54
54
  | `ZEPH_BASE_URL` | No | API base URL (default: `https://api.zeph.to/v1`) |
55
+ | `ZEPH_DISABLE_SESSION_CACHE` | No | Set to `1`/`true` to skip writing the session-id handoff file under `~/.cache/zeph/`. Useful for read-only filesystems, ephemeral CI runners, or sandboxed envs that audit filesystem writes. The plugin's stop hook still works without it (transcript-path UUID extraction is the primary path; the cache is a fallback for older Claude Code versions). |
55
56
 
56
57
  \* If env vars are not set, the server reads from `~/.zeph/config.json` (created by `npx @zeph-to/hook-sdk install`). Unresolved `${...}` interpolations are also treated as unset.
57
58
 
@@ -266,9 +267,11 @@ The API key needs the following scopes:
266
267
 
267
268
  Create an API key with the **MCP** preset in Settings > API Keys for the correct permissions.
268
269
 
269
- ## E2E Encryption
270
+ ## Encryption
270
271
 
271
- Push notifications are encrypted end-to-end by default (AES-256-GCM + ECDH P-256). Keys are synced with the server on startup. When encryption is disabled in the Zeph app, the server sends plaintext. No configuration needed — encryption is automatic.
272
+ 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 server startup so every device can read the same push. Toggle encryption in the Zeph app (Settings → Encryption); when disabled, the server sends plaintext. No configuration needed.
273
+
274
+ **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.
272
275
 
273
276
  ## License
274
277
 
package/dist/config.d.ts CHANGED
@@ -4,6 +4,15 @@ export interface McpServerConfig {
4
4
  hookId?: string;
5
5
  deviceId?: string;
6
6
  sessionId?: string;
7
+ /** Last path segment of the project directory — prefixed onto push titles. */
8
+ projectName: string;
7
9
  }
10
+ /**
11
+ * Prefix a push title with the project name so the device feed stays
12
+ * scannable — "zeph · Build finished" instead of a bare "Build finished".
13
+ * Idempotent: a title already carrying the project segment is returned
14
+ * unchanged (guards against double-prefixing on retries).
15
+ */
16
+ export declare const formatPushTitle: (projectName: string, title: string) => string;
8
17
  export declare const loadConfig: () => McpServerConfig;
9
18
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwCD,eAAO,MAAM,UAAU,QAAO,eA0B7B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACvB;AAiBD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,KAAG,MAGpE,CAAC;AAgGF,eAAO,MAAM,UAAU,QAAO,eAsB7B,CAAC"}
package/dist/config.js CHANGED
@@ -1,18 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadConfig = void 0;
3
+ exports.loadConfig = exports.formatPushTitle = void 0;
4
4
  const fs_1 = require("fs");
5
+ const os_1 = require("os");
5
6
  const crypto_1 = require("crypto");
6
7
  const child_process_1 = require("child_process");
7
8
  const path_1 = require("path");
8
9
  const DEFAULT_BASE_URL = 'https://api.zeph.to/v1';
10
+ const PROJECT_DIR_ENV_KEYS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
11
+ /** The project directory the agent runs in, across supported agents. */
12
+ const detectProjectDir = () => {
13
+ for (const key of PROJECT_DIR_ENV_KEYS) {
14
+ const val = process.env[key];
15
+ if (val)
16
+ return val;
17
+ }
18
+ return process.cwd();
19
+ };
20
+ /** Last path segment of a directory: "/Users/me/code/zeph" -> "zeph". */
21
+ const projectNameFromDir = (dir) => dir.split('/').filter(Boolean).pop() ?? 'project';
22
+ /**
23
+ * Prefix a push title with the project name so the device feed stays
24
+ * scannable — "zeph · Build finished" instead of a bare "Build finished".
25
+ * Idempotent: a title already carrying the project segment is returned
26
+ * unchanged (guards against double-prefixing on retries).
27
+ */
28
+ const formatPushTitle = (projectName, title) => {
29
+ const prefix = `${projectName} · `;
30
+ return title.startsWith(prefix) ? title : prefix + title;
31
+ };
32
+ exports.formatPushTitle = formatPushTitle;
9
33
  const resolvedEnv = (key) => {
10
34
  const val = process.env[key];
11
35
  return val && !val.startsWith('${') ? val : undefined;
12
36
  };
13
37
  const loadFileConfig = () => {
14
38
  try {
15
- const configPath = (0, path_1.join)(process.env.HOME ?? '~', '.zeph', 'config.json');
39
+ const configPath = (0, path_1.join)((0, os_1.homedir)(), '.zeph', 'config.json');
16
40
  return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
17
41
  }
18
42
  catch {
@@ -24,18 +48,77 @@ const detectClaudeSessionId = () => {
24
48
  try {
25
49
  const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
26
50
  const projectHash = projectDir.replace(/\//g, '-');
27
- const sessionsDir = (0, path_1.join)(process.env.HOME ?? '~', '.claude', 'projects', projectHash);
28
- const entries = (0, fs_1.readdirSync)(sessionsDir)
29
- .filter((name) => /^[0-9a-f]{8}-/.test(name))
30
- .filter((name) => (0, fs_1.statSync)((0, path_1.join)(sessionsDir, name)).isDirectory())
31
- .map((name) => ({ name, mtime: (0, fs_1.statSync)((0, path_1.join)(sessionsDir, name)).mtimeMs }))
32
- .sort((a, b) => b.mtime - a.mtime);
33
- return entries[0]?.name;
51
+ const sessionsDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'projects', projectHash);
52
+ let latest;
53
+ for (const name of (0, fs_1.readdirSync)(sessionsDir)) {
54
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(name))
55
+ continue;
56
+ const fullPath = (0, path_1.join)(sessionsDir, name);
57
+ const stat = (0, fs_1.statSync)(fullPath);
58
+ if (!stat.isDirectory())
59
+ continue;
60
+ if (!latest || stat.mtimeMs > latest.mtime) {
61
+ latest = { name, mtime: stat.mtimeMs };
62
+ }
63
+ }
64
+ return latest?.name;
34
65
  }
35
66
  catch {
36
67
  return undefined;
37
68
  }
38
69
  };
70
+ /**
71
+ * Truthy-env helper — accepts "1", "true", "yes", "on" (case-insensitive).
72
+ */
73
+ const envIsTrue = (key) => {
74
+ const v = process.env[key];
75
+ if (!v)
76
+ return false;
77
+ return /^(1|true|yes|on)$/i.test(v.trim());
78
+ };
79
+ /**
80
+ * Write the resolved session id to a per-user cache file so shell hooks
81
+ * (e.g. plugin/hooks/zeph-stop.sh) can pick it up when their own transcript
82
+ * scrape misses. The previous location was /tmp/zeph-session-<hash>, which
83
+ * on multi-user machines exposed a symlink race — predictable filename,
84
+ * world-writable directory. Living under ~/.cache (or $XDG_CACHE_HOME)
85
+ * removes that — only the owning user can write there. The file is also
86
+ * opened with O_NOFOLLOW so a pre-existing symlink can never redirect us
87
+ * to /etc/passwd or similar.
88
+ *
89
+ * Users can opt out entirely by setting ZEPH_DISABLE_SESSION_CACHE=1.
90
+ * Useful for:
91
+ * - Read-only filesystems (some container runtimes)
92
+ * - CI runners where ~/.cache isn't persisted anyway, so the write is
93
+ * pure overhead
94
+ * - Sandboxed environments where extra filesystem writes trigger audit
95
+ * noise
96
+ * The shell hook still works without the cache — it primarily extracts
97
+ * the session id from the transcript_path UUID; the cache is just a
98
+ * fallback for older Claude Code versions.
99
+ */
100
+ const writeSessionCache = (sessionId, projectDir) => {
101
+ if (envIsTrue('ZEPH_DISABLE_SESSION_CACHE'))
102
+ return;
103
+ try {
104
+ const hash = (0, child_process_1.execFileSync)('cksum', { input: projectDir, encoding: 'utf-8' }).split(' ')[0];
105
+ const cacheDir = (0, path_1.join)(process.env.XDG_CACHE_HOME ?? (0, path_1.join)((0, os_1.homedir)(), '.cache'), 'zeph');
106
+ (0, fs_1.mkdirSync)(cacheDir, { recursive: true, mode: 0o700 });
107
+ const cachePath = (0, path_1.join)(cacheDir, `session-${hash}`);
108
+ const flags = fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_TRUNC | fs_1.constants.O_NOFOLLOW;
109
+ const fd = (0, fs_1.openSync)(cachePath, flags, 0o600);
110
+ try {
111
+ (0, fs_1.writeSync)(fd, sessionId);
112
+ }
113
+ finally {
114
+ (0, fs_1.closeSync)(fd);
115
+ }
116
+ }
117
+ catch {
118
+ /* best-effort — hook stop script also extracts session id from
119
+ * the transcript path, so failing here is non-fatal. */
120
+ }
121
+ };
39
122
  const loadConfig = () => {
40
123
  const fileConfig = loadFileConfig();
41
124
  const apiKey = resolvedEnv('ZEPH_API_KEY') ?? fileConfig.apiKey;
@@ -43,19 +126,15 @@ const loadConfig = () => {
43
126
  throw new Error('ZEPH_API_KEY not found. Run "npx @zeph-to/hook-sdk install" or set ZEPH_API_KEY env var.');
44
127
  }
45
128
  const sessionId = resolvedEnv('ZEPH_SESSION_ID') ?? detectClaudeSessionId() ?? `sess_${(0, crypto_1.randomBytes)(12).toString('base64url')}`;
46
- // Write sessionId to tmp file so shell hooks (zeph-stop.sh) can read it
47
- try {
48
- const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
49
- const hash = (0, child_process_1.execFileSync)('cksum', { input: projectDir, encoding: 'utf-8' }).split(' ')[0];
50
- (0, fs_1.writeFileSync)(`/tmp/zeph-session-${hash}`, sessionId);
51
- }
52
- catch { /* best-effort */ }
129
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
130
+ writeSessionCache(sessionId, projectDir);
53
131
  return {
54
132
  apiKey,
55
133
  baseUrl: (resolvedEnv('ZEPH_BASE_URL') ?? fileConfig.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ''),
56
134
  hookId: resolvedEnv('ZEPH_HOOK_ID') ?? fileConfig.hookId,
57
135
  deviceId: resolvedEnv('ZEPH_DEVICE_ID') ?? fileConfig.deviceId,
58
136
  sessionId,
137
+ projectName: projectNameFromDir(detectProjectDir()),
59
138
  };
60
139
  };
61
140
  exports.loadConfig = loadConfig;
package/dist/crypto.d.ts CHANGED
@@ -1,13 +1,39 @@
1
1
  /**
2
- * E2E encryption for MCP server — 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 MCP server — 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.
8
31
  * Server is source of truth for per-user key pair.
9
32
  * Safe to call concurrently — deduplicates to single init.
10
33
  * Returns the exported public key (Base64 SPKI).
34
+ *
35
+ * NOTE: when `apiKey` is provided, `baseUrl` is required — otherwise a
36
+ * caller in a dev environment would silently upload keys to prod.
11
37
  */
12
38
  export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
13
39
  export declare const getKeyPair: () => CryptoKeyPair | null;
@@ -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,CAiE5E,CAAC;AAqCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;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;;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;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAyE5E,CAAC;AAqCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;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;;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 MCP server — 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 MCP server — 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.encryptPushBodyForSelf = 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)(process.env.XDG_CONFIG_HOME ?? (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 {
@@ -103,15 +127,24 @@ let initPromise = null;
103
127
  * Server is source of truth for per-user key pair.
104
128
  * Safe to call concurrently — deduplicates to single init.
105
129
  * Returns the exported public key (Base64 SPKI).
130
+ *
131
+ * NOTE: when `apiKey` is provided, `baseUrl` is required — otherwise a
132
+ * caller in a dev environment would silently upload keys to prod.
106
133
  */
107
134
  const initCrypto = (apiKey, baseUrl) => {
135
+ if (apiKey && !baseUrl) {
136
+ return Promise.reject(new Error('initCrypto: baseUrl is required when apiKey is provided. ' +
137
+ 'Pass the resolved config.baseUrl to avoid silently syncing dev keys to prod.'));
138
+ }
108
139
  if (initPromise)
109
140
  return initPromise;
141
+ // The check above guarantees baseUrl is defined when apiKey is — narrow once.
142
+ const baseUrlRequired = apiKey ? baseUrl : baseUrl;
110
143
  initPromise = (async () => {
111
144
  const stored = loadStoredKeys();
112
145
  // Try server sync if API key available
113
146
  if (apiKey) {
114
- const serverResult = await fetchServerKeys(apiKey, baseUrl);
147
+ const serverResult = await fetchServerKeys(apiKey, baseUrlRequired);
115
148
  // Server says encryption disabled — skip crypto init
116
149
  if (serverResult && !serverResult.encryptionEnabled) {
117
150
  cachedKeyPair = null;
@@ -129,7 +162,7 @@ const initCrypto = (apiKey, baseUrl) => {
129
162
  return serverResult.keys.publicKey;
130
163
  }
131
164
  if (stored) {
132
- await uploadServerKeys(stored, apiKey, baseUrl);
165
+ await uploadServerKeys(stored, apiKey, baseUrlRequired);
133
166
  cachedKeyPair = await importKeyPair(stored);
134
167
  cachedExportedPublicKey = stored.publicKey;
135
168
  cachedOwnPublicKey = cachedKeyPair.publicKey;
@@ -138,7 +171,7 @@ const initCrypto = (apiKey, baseUrl) => {
138
171
  const keyPair = await generateKeyPair();
139
172
  const exported = await exportKeyPair(keyPair);
140
173
  storeKeys(exported);
141
- await uploadServerKeys(exported, apiKey, baseUrl);
174
+ await uploadServerKeys(exported, apiKey, baseUrlRequired);
142
175
  cachedKeyPair = keyPair;
143
176
  cachedExportedPublicKey = exported.publicKey;
144
177
  cachedOwnPublicKey = keyPair.publicKey;
@@ -167,7 +200,7 @@ const initCrypto = (apiKey, baseUrl) => {
167
200
  exports.initCrypto = initCrypto;
168
201
  const fetchServerKeys = async (apiKey, baseUrl) => {
169
202
  try {
170
- const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
203
+ const url = `${baseUrl.replace(/\/$/, '')}/users/me/keys`;
171
204
  const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
172
205
  if (!res.ok)
173
206
  return null;
@@ -185,7 +218,7 @@ const fetchServerKeys = async (apiKey, baseUrl) => {
185
218
  };
186
219
  const uploadServerKeys = async (keys, apiKey, baseUrl) => {
187
220
  try {
188
- const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
221
+ const url = `${baseUrl.replace(/\/$/, '')}/users/me/keys`;
189
222
  await fetch(url, {
190
223
  method: 'PUT',
191
224
  headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
package/dist/mime.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared MIME-type inference for file/notify/ask payloads.
3
+ *
4
+ * Kept in one place so different tools don't produce inconsistent types
5
+ * for the same extension (e.g. `.csv` ending up as text/plain in one
6
+ * code path and text/csv in another).
7
+ */
8
+ export declare const inferMimeType: (fileName: string) => string;
9
+ //# sourceMappingURL=mime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkBH,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,MAGhD,CAAC"}
package/dist/mime.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Shared MIME-type inference for file/notify/ask payloads.
4
+ *
5
+ * Kept in one place so different tools don't produce inconsistent types
6
+ * for the same extension (e.g. `.csv` ending up as text/plain in one
7
+ * code path and text/csv in another).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.inferMimeType = void 0;
11
+ const EXT_TO_MIME = {
12
+ txt: 'text/plain',
13
+ log: 'text/plain',
14
+ md: 'text/markdown',
15
+ json: 'application/json',
16
+ csv: 'text/csv',
17
+ html: 'text/html',
18
+ xml: 'text/xml',
19
+ yaml: 'text/yaml',
20
+ yml: 'text/yaml',
21
+ ts: 'text/typescript',
22
+ js: 'text/javascript',
23
+ py: 'text/x-python',
24
+ sh: 'text/x-shellscript',
25
+ };
26
+ const inferMimeType = (fileName) => {
27
+ const ext = fileName.split('.').pop()?.toLowerCase();
28
+ return EXT_TO_MIME[ext ?? ''] ?? 'text/plain';
29
+ };
30
+ exports.inferMimeType = inferMimeType;
@@ -1,5 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ZephApiClient } from '../api-client.js';
3
- import type { McpServerConfig } from '../config.js';
3
+ import { type McpServerConfig } from '../config.js';
4
4
  export declare const registerAskTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
5
5
  //# sourceMappingURL=ask.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/tools/ask.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYpD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAmHhG,CAAC"}
1
+ {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/tools/ask.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAuBrE,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwHhG,CAAC"}
package/dist/tools/ask.js CHANGED
@@ -4,13 +4,21 @@ exports.registerAskTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const poll_js_1 = require("../poll.js");
7
+ const config_js_1 = require("../config.js");
7
8
  const crypto_js_1 = require("../crypto.js");
8
- const BODY_FILE_THRESHOLD = 512;
9
+ const mime_js_1 = require("../mime.js");
10
+ // The device feed shows a short preview of the body. Anything longer than
11
+ // this gets truncated there, so we attach the full text as a file — the
12
+ // user can always open the complete content instead of squinting at a
13
+ // clipped preview.
9
14
  const PREVIEW_LENGTH = 200;
10
- const inferMimeType = (fileName) => {
11
- const ext = fileName.split('.').pop()?.toLowerCase();
12
- const map = { md: 'text/markdown', txt: 'text/plain', json: 'application/json' };
13
- return map[ext ?? ''] ?? 'text/plain';
15
+ /** Self-contained Markdown for the attached response.md: heading + body + options. */
16
+ const buildAskMarkdown = (title, body, actions) => {
17
+ const parts = [`# ${title}`, '', body];
18
+ if (actions && actions.length > 0) {
19
+ parts.push('', '---', '', `**Options:** ${actions.map((a) => a.label).join(' · ')}`);
20
+ }
21
+ return parts.join('\n');
14
22
  };
15
23
  const registerAskTool = (server, client, config) => {
16
24
  server.registerTool('zeph_ask', {
@@ -54,21 +62,25 @@ const registerAskTool = (server, client, config) => {
54
62
  if (!config.hookId)
55
63
  return (0, error_format_js_1.hookNotConfiguredError)();
56
64
  try {
57
- const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
58
- const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
65
+ const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title);
66
+ // Attach a file whenever the body would be clipped in the feed preview.
67
+ const exceedsPreview = !!body && body.length > PREVIEW_LENGTH;
59
68
  let triggerBody = body;
60
69
  let files;
61
- if (isLongBody && body) {
70
+ if (exceedsPreview && body) {
62
71
  const fileName = 'response.md';
63
- const fileType = inferMimeType(fileName);
72
+ const fileType = (0, mime_js_1.inferMimeType)(fileName);
64
73
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
65
- let uploadContent = body;
74
+ // Self-contained Markdown so the file alone tells the whole story.
75
+ const fileMarkdown = buildAskMarkdown(title, body, actions);
76
+ const fileBytes = new TextEncoder().encode(fileMarkdown).byteLength;
77
+ let uploadContent = fileMarkdown;
66
78
  let uploadContentType = fileType;
67
79
  let fileIv;
68
80
  let fileEncryptedKey;
69
81
  if (canEncrypt) {
70
82
  try {
71
- const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
83
+ const encrypted = await (0, crypto_js_1.encryptFileForSelf)(fileMarkdown);
72
84
  uploadContent = encrypted.ciphertext;
73
85
  uploadContentType = 'application/octet-stream';
74
86
  fileIv = encrypted.iv;
@@ -78,13 +90,13 @@ const registerAskTool = (server, client, config) => {
78
90
  console.error('[Crypto] File encryption failed, sending plaintext:', err);
79
91
  }
80
92
  }
81
- const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? bodyBytes : uploadContent.length });
93
+ const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileBytes : uploadContent.length });
82
94
  await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
83
- triggerBody = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
84
- files = [{ fileKey: upload.data.fileKey, fileName, fileSize: bodyBytes, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }];
95
+ triggerBody = body.slice(0, PREVIEW_LENGTH) + '...';
96
+ files = [{ fileKey: upload.data.fileKey, fileName, fileSize: fileBytes, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }];
85
97
  }
86
98
  const trigger = await client.triggerHook(config.hookId, {
87
- title,
99
+ title: pushTitle,
88
100
  body: triggerBody,
89
101
  actions,
90
102
  timeout,
@@ -1,5 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ZephApiClient } from '../api-client.js';
3
- import type { McpServerConfig } from '../config.js';
3
+ import { type McpServerConfig } from '../config.js';
4
4
  export declare const registerFileTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
5
5
  //# sourceMappingURL=file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAwBpD,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2EjG,CAAC"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAKrE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2EjG,CAAC"}
@@ -2,27 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerFileTool = void 0;
4
4
  const zod_1 = require("zod");
5
+ const config_js_1 = require("../config.js");
5
6
  const error_format_js_1 = require("../error-format.js");
6
7
  const crypto_js_1 = require("../crypto.js");
7
- const inferMimeType = (fileName) => {
8
- const ext = fileName.split('.').pop()?.toLowerCase();
9
- const map = {
10
- txt: 'text/plain',
11
- json: 'application/json',
12
- csv: 'text/csv',
13
- md: 'text/markdown',
14
- html: 'text/html',
15
- xml: 'text/xml',
16
- yaml: 'text/yaml',
17
- yml: 'text/yaml',
18
- log: 'text/plain',
19
- ts: 'text/typescript',
20
- js: 'text/javascript',
21
- py: 'text/x-python',
22
- sh: 'text/x-shellscript',
23
- };
24
- return map[ext ?? ''] ?? 'text/plain';
25
- };
8
+ const mime_js_1 = require("../mime.js");
26
9
  const registerFileTool = (server, client, config) => {
27
10
  server.registerTool('zeph_file', {
28
11
  description: 'Send a text file to the user\'s device. The content is uploaded and delivered as a file push. Use for logs, reports, code snippets, or any text content.',
@@ -40,7 +23,7 @@ const registerFileTool = (server, client, config) => {
40
23
  }, async ({ fileName, content, title, targetDeviceId }) => {
41
24
  try {
42
25
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
43
- let fileType = inferMimeType(fileName);
26
+ let fileType = (0, mime_js_1.inferMimeType)(fileName);
44
27
  const originalSize = new TextEncoder().encode(content).byteLength;
45
28
  // Step 1: Optionally encrypt file content
46
29
  let uploadContent = content;
@@ -65,11 +48,11 @@ const registerFileTool = (server, client, config) => {
65
48
  // Step 3: Upload content to S3
66
49
  await client.uploadToS3(upload.data.uploadUrl, uploadContent, fileType);
67
50
  // Step 4: Send file push (encrypt push body if possible)
68
- const pushTitle = title ?? fileName;
51
+ const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title ?? fileName);
69
52
  let pushPayload = {
70
53
  title: pushTitle,
71
54
  type: 'file',
72
- files: [{ fileKey: upload.data.fileKey, fileName, fileSize: originalSize, fileType: inferMimeType(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
55
+ files: [{ fileKey: upload.data.fileKey, fileName, fileSize: originalSize, fileType: (0, mime_js_1.inferMimeType)(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
73
56
  targetDeviceId: targetDeviceId ?? config.deviceId,
74
57
  sessionId: config.sessionId,
75
58
  };
@@ -1,5 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ZephApiClient } from '../api-client.js';
3
- import type { McpServerConfig } from '../config.js';
3
+ import { type McpServerConfig } from '../config.js';
4
4
  export declare const registerInputTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
5
5
  //# sourceMappingURL=input.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwDlG,CAAC"}
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwDlG,CAAC"}
@@ -4,6 +4,7 @@ exports.registerInputTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const poll_js_1 = require("../poll.js");
7
+ const config_js_1 = require("../config.js");
7
8
  const registerInputTool = (server, client, config) => {
8
9
  server.registerTool('zeph_input', {
9
10
  description: 'Request text input from the user via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
@@ -32,7 +33,7 @@ const registerInputTool = (server, client, config) => {
32
33
  return (0, error_format_js_1.hookNotConfiguredError)();
33
34
  try {
34
35
  const trigger = await client.triggerHook(config.hookId, {
35
- title,
36
+ title: (0, config_js_1.formatPushTitle)(config.projectName, title),
36
37
  body,
37
38
  timeout,
38
39
  hookType: 'input',
@@ -1,5 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ZephApiClient } from '../api-client.js';
3
- import type { McpServerConfig } from '../config.js';
3
+ import { type McpServerConfig } from '../config.js';
4
4
  export declare const registerNotifyTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
5
5
  //# sourceMappingURL=notify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/tools/notify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYpD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA6GnG,CAAC"}
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/tools/notify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAQrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAiHnG,CAAC"}
@@ -3,17 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerNotifyTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
+ const config_js_1 = require("../config.js");
6
7
  const crypto_js_1 = require("../crypto.js");
7
- const BODY_FILE_THRESHOLD = 512;
8
+ const mime_js_1 = require("../mime.js");
9
+ // The device feed shows a short preview of the body. Anything longer gets
10
+ // truncated there, so we attach the full text as a file for full viewing.
8
11
  const PREVIEW_LENGTH = 200;
9
- const inferMimeType = (fileName) => {
10
- const ext = fileName.split('.').pop()?.toLowerCase();
11
- const map = { md: 'text/markdown', txt: 'text/plain', json: 'application/json' };
12
- return map[ext ?? ''] ?? 'text/plain';
13
- };
14
12
  const registerNotifyTool = (server, client, config) => {
15
13
  server.registerTool('zeph_notify', {
16
- description: 'Send a one-way push notification to the user\'s devices. Use this to inform the user about task completion, errors, or status updates. Long bodies (>512B) are automatically uploaded as a file for full viewing.',
14
+ description: 'Send a one-way push notification to the user\'s devices. Use this to inform the user about task completion, errors, or status updates. Long bodies are automatically uploaded as a file for full viewing.',
17
15
  annotations: {
18
16
  readOnlyHint: false,
19
17
  destructiveHint: false,
@@ -32,21 +30,24 @@ const registerNotifyTool = (server, client, config) => {
32
30
  }, async ({ title, body, url, priority, targetDeviceId }) => {
33
31
  try {
34
32
  const deviceId = targetDeviceId ?? config.deviceId;
35
- const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
36
- const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
33
+ const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title);
34
+ // Attach a file whenever the body would be clipped in the feed preview.
35
+ const isLongBody = !!body && body.length > PREVIEW_LENGTH;
37
36
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
38
37
  if (isLongBody && body) {
39
38
  const fileName = 'response.md';
40
- const fileType = inferMimeType(fileName);
41
- const fileSize = bodyBytes;
39
+ const fileType = (0, mime_js_1.inferMimeType)(fileName);
40
+ // Self-contained Markdown so the file alone carries the full text.
41
+ const fileMarkdown = `# ${title}\n\n${body}`;
42
+ const fileBytes = new TextEncoder().encode(fileMarkdown).byteLength;
42
43
  // Encrypt file content if keys available
43
- let uploadContent = body;
44
+ let uploadContent = fileMarkdown;
44
45
  let uploadContentType = fileType;
45
46
  let fileIv;
46
47
  let fileEncryptedKey;
47
48
  if (canEncrypt) {
48
49
  try {
49
- const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
50
+ const encrypted = await (0, crypto_js_1.encryptFileForSelf)(fileMarkdown);
50
51
  uploadContent = encrypted.ciphertext;
51
52
  uploadContentType = 'application/octet-stream';
52
53
  fileIv = encrypted.iv;
@@ -56,23 +57,23 @@ const registerNotifyTool = (server, client, config) => {
56
57
  console.error('[Crypto] File encryption failed, sending plaintext:', err);
57
58
  }
58
59
  }
59
- const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileSize : uploadContent.length });
60
+ const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileBytes : uploadContent.length });
60
61
  await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
61
- const preview = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
62
+ const preview = body.slice(0, PREVIEW_LENGTH) + '...';
62
63
  // Encrypt push body (title/preview/url) if keys available
63
64
  let pushPayload = {
64
- title,
65
+ title: pushTitle,
65
66
  body: preview,
66
67
  url,
67
68
  type: 'file',
68
69
  priority,
69
- files: [{ fileKey: upload.data.fileKey, fileName, fileSize, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }],
70
+ files: [{ fileKey: upload.data.fileKey, fileName, fileSize: fileBytes, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }],
70
71
  targetDeviceId: deviceId,
71
72
  sessionId: config.sessionId,
72
73
  };
73
74
  if (canEncrypt) {
74
75
  try {
75
- const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title, body: preview, url });
76
+ const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body: preview, url });
76
77
  pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
77
78
  }
78
79
  catch (err) {
@@ -84,7 +85,7 @@ const registerNotifyTool = (server, client, config) => {
84
85
  }
85
86
  // Short body — encrypt push only
86
87
  let pushPayload = {
87
- title,
88
+ title: pushTitle,
88
89
  body,
89
90
  url,
90
91
  type: 'hook',
@@ -94,7 +95,7 @@ const registerNotifyTool = (server, client, config) => {
94
95
  };
95
96
  if (canEncrypt) {
96
97
  try {
97
- const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title, body, url });
98
+ const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body, url });
98
99
  pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
99
100
  }
100
101
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ZephApiClient } from '../api-client.js';
3
- import type { McpServerConfig } from '../config.js';
3
+ import { type McpServerConfig } from '../config.js';
4
4
  export declare const registerPromptTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
5
5
  //# sourceMappingURL=prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/tools/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAuEnG,CAAC"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/tools/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAuEnG,CAAC"}
@@ -4,6 +4,7 @@ exports.registerPromptTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const poll_js_1 = require("../poll.js");
7
+ const config_js_1 = require("../config.js");
7
8
  const registerPromptTool = (server, client, config) => {
8
9
  server.registerTool('zeph_prompt', {
9
10
  description: 'Ask the user to choose from predefined options via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
@@ -41,7 +42,7 @@ const registerPromptTool = (server, client, config) => {
41
42
  return (0, error_format_js_1.hookNotConfiguredError)();
42
43
  try {
43
44
  const trigger = await client.triggerHook(config.hookId, {
44
- title,
45
+ title: (0, config_js_1.formatPushTitle)(config.projectName, title),
45
46
  body,
46
47
  actions,
47
48
  timeout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.9.2",
3
+ "version": "1.11.0",
4
4
  "description": "Zeph MCP server — AI agent notifications, prompts, and input via MCP protocol",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  "zeph-mcp": "./dist/index.js"
9
9
  },
10
10
  "engines": {
11
- "node": ">=18.0.0"
11
+ "node": ">=18.17.0"
12
12
  },
13
13
  "files": [
14
14
  "dist",
@@ -16,6 +16,8 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "tsc",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
19
21
  "prepublishOnly": "npm run build"
20
22
  },
21
23
  "dependencies": {
@@ -23,8 +25,9 @@
23
25
  "zod": "^3.25.1"
24
26
  },
25
27
  "devDependencies": {
28
+ "@types/node": "^22.0.0",
26
29
  "typescript": "^5.8.0",
27
- "@types/node": "^22.0.0"
30
+ "vitest": "^2.1.9"
28
31
  },
29
32
  "release": {
30
33
  "branches": [