@zeph-to/mcp-server 1.13.0 → 1.14.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 +26 -5
- package/dist/api-client.d.ts +3 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/crypto.d.ts +2 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +13 -2
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +2 -0
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +2 -0
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +10 -2
- package/dist/tools/prompt.d.ts.map +1 -1
- package/dist/tools/prompt.js +2 -0
- package/dist/ws-wait.d.ts.map +1 -1
- package/dist/ws-wait.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
Zeph MCP server
|
|
8
|
+
Zeph MCP server — the agent side of the round trip. Your agent finishes a build or hits a decision, calls `zeph_ask`, and the question lands on the user's phone as buttons + a text field; the reply comes back into the same tool call and the agent keeps going. Also: one-way notifications, clipboard, files, and channel broadcasts — all via the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
9
|
+
|
|
10
|
+
Works with Claude Code, Cursor, Windsurf, Gemini CLI, and any MCP client. Part of the Zeph toolchain: [`@zeph-to/cli`](https://github.com/zeph-to/cli) (installer, push CLI, tmux remote control) · [`zeph-to/plugin`](https://github.com/zeph-to/plugin) (Claude Code plugin bundling this server) · the [Zeph app](https://zeph.to) on your phone.
|
|
9
11
|
|
|
10
12
|
## Setup
|
|
11
13
|
|
|
@@ -57,12 +59,17 @@ Add to `~/.claude/settings.json`:
|
|
|
57
59
|
| `ZEPH_HOOK_ID` | No | Hook ID (optional — only needed for interactive tools like `zeph_ask`/`zeph_prompt`/`zeph_input`) |
|
|
58
60
|
| `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 |
|
|
59
61
|
| `ZEPH_BASE_URL` | No | API base URL (default: `https://api.zeph.to/v1`) |
|
|
62
|
+
| `ZEPH_WS_URL` | No | WebSocket endpoint for the hook-response fast path — `zeph_ask`/`zeph_prompt`/`zeph_input` answers arrive the moment the user submits them instead of on the next poll. Falls back to pure polling when unset. Also read from `wsUrl` in `~/.zeph/config.json` |
|
|
60
63
|
| `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). |
|
|
64
|
+
| `ZEPH_SESSION_ID` | No | Override the session id attached to pushes (grouping in the app). Auto-detected from the newest Claude Code transcript when unset |
|
|
65
|
+
| `ZEPH_DISABLE_ENCRYPTION` | No | Set to `1`/`true` to force E2E-style push encryption off, even when the account has keys. Useful while cleaning up legacy key state |
|
|
61
66
|
|
|
62
67
|
\* If env vars are not set, the server reads from `~/.zeph/config.json` (created by `npx @zeph-to/cli install`). Unresolved `${...}` interpolations are also treated as unset.
|
|
63
68
|
|
|
64
69
|
## Tools
|
|
65
70
|
|
|
71
|
+
Push titles are automatically prefixed with the project directory name — `myapp · Build complete` — so the phone feed stays scannable when several sessions push at once.
|
|
72
|
+
|
|
66
73
|
### zeph_notify
|
|
67
74
|
|
|
68
75
|
Send a one-way push notification. Supports optional URL (auto-switches to link type).
|
|
@@ -188,7 +195,7 @@ Returns: `{ value: "feat: add clipboard sync", timedOut: false }`
|
|
|
188
195
|
|
|
189
196
|
### Client timeouts
|
|
190
197
|
|
|
191
|
-
`zeph_ask`, `zeph_prompt`, and `zeph_input` block until the user responds, up to their `timeout` (max 600s).
|
|
198
|
+
`zeph_ask`, `zeph_prompt`, and `zeph_input` block until the user responds, up to their `timeout` (max 600s). With `ZEPH_WS_URL` configured the response arrives over WebSocket the instant it's submitted; otherwise the server polls. Either way the MCP request stays open the whole time. To keep the client from giving up early, the server emits a `notifications/progress` every 5s while waiting. Clients must either set a per-request timeout above the tool's `timeout`, or reset their timeout on progress notifications. Claude Code does the latter by default.
|
|
192
199
|
|
|
193
200
|
## Resources
|
|
194
201
|
|
|
@@ -207,13 +214,27 @@ Lists channels the user owns or subscribes to. Use to find `channelId` for `zeph
|
|
|
207
214
|
| Situation | Tool | Example |
|
|
208
215
|
|-----------|------|---------|
|
|
209
216
|
| Long task finished | `zeph_notify` | Build complete, test results, deploy done |
|
|
210
|
-
| Need
|
|
211
|
-
|
|
|
217
|
+
| Need a decision (buttons + optional free text) | `zeph_ask` | "Tests green. Deploy?" with a custom-instruction escape hatch |
|
|
218
|
+
| Decision from fixed options only | `zeph_prompt` | Choose deploy target, confirm destructive action |
|
|
219
|
+
| Free-form input only | `zeph_input` | Commit message, env var value, description |
|
|
212
220
|
| Share code/logs | `zeph_file` | Error logs, test reports, generated config |
|
|
213
221
|
| Share snippet | `zeph_clipboard` | API key, URL, shell command |
|
|
214
222
|
|
|
215
223
|
### Recommended patterns
|
|
216
224
|
|
|
225
|
+
**Decision gate with an escape hatch (preferred):**
|
|
226
|
+
```
|
|
227
|
+
zeph_ask(
|
|
228
|
+
title: "Tests green. Deploy to production?",
|
|
229
|
+
actions: [
|
|
230
|
+
{ id: "deploy", label: "Deploy", style: "primary" },
|
|
231
|
+
{ id: "hold", label: "Hold", style: "secondary" }
|
|
232
|
+
],
|
|
233
|
+
placeholder: "Or tell me what to do instead...",
|
|
234
|
+
fallback: "hold"
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
217
238
|
**Task completion notification:**
|
|
218
239
|
```
|
|
219
240
|
zeph_notify(
|
|
@@ -271,7 +292,7 @@ The API key needs the following scopes:
|
|
|
271
292
|
|
|
272
293
|
- `push:read` — for `zeph_list`
|
|
273
294
|
- `push:write` — for `zeph_notify`, `zeph_clipboard`, `zeph_dismiss`, `zeph_dismiss_all`, `zeph_file`
|
|
274
|
-
- `hook:write` — for `zeph_prompt
|
|
295
|
+
- `hook:write` — for `zeph_ask`, `zeph_prompt`, and `zeph_input`
|
|
275
296
|
- `channel:read` — for `zeph://channels` resource
|
|
276
297
|
|
|
277
298
|
Create an API key with the **MCP** preset in Settings > API Keys for the correct permissions.
|
package/dist/api-client.d.ts
CHANGED
|
@@ -36,6 +36,9 @@ export declare class ZephApiClient {
|
|
|
36
36
|
hookType?: 'one-way' | 'interactive' | 'input' | 'combo';
|
|
37
37
|
files?: AttachedFile[];
|
|
38
38
|
sessionId?: string;
|
|
39
|
+
/** Stable session grouping so the hook joins the agent chat. */
|
|
40
|
+
agentDeviceId?: string;
|
|
41
|
+
agentSessionName?: string;
|
|
39
42
|
}): Promise<HookTriggerResponse>;
|
|
40
43
|
getHookEvent(hookId: string, eventId: string): Promise<HookEventResponse>;
|
|
41
44
|
listDevices(): Promise<DevicesResponse>;
|
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAEV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,qBAAa,QAAS,SAAQ,KAAK;aAGf,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,MAAM;gBAF9B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM;CAKjC;AAKD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,eAAe;IAK7B,QAAQ,CAAC,MAAM,EAAE;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,GAAG,OAAO,CAAC,YAAY,CAAC;IAInB,WAAW,CACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,QAAQ,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,OAAO,CAAC;QACzD,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface McpServerConfig {
|
|
|
7
7
|
hookId?: string;
|
|
8
8
|
deviceId?: string;
|
|
9
9
|
sessionId?: string;
|
|
10
|
+
/** Stable agent-session key parts so hook pushes join the session chat:
|
|
11
|
+
* the listener's per-host device id (mirrors cli computeListenerDeviceId)
|
|
12
|
+
* + the tmux session name. The Claude sessionId above rotates on
|
|
13
|
+
* compact/resume; these don't. */
|
|
14
|
+
agentDeviceId?: string;
|
|
15
|
+
agentSessionName?: string;
|
|
10
16
|
/** Last path segment of the project directory — prefixed onto push titles. */
|
|
11
17
|
projectName: string;
|
|
12
18
|
}
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,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;AAuGF,eAAO,MAAM,UAAU,QAAO,
|
|
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;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;uCAGmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACvB;AAiBD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,KAAG,MAGpE,CAAC;AAuGF,eAAO,MAAM,UAAU,QAAO,eAyB7B,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -140,7 +140,26 @@ const loadConfig = () => {
|
|
|
140
140
|
hookId: resolvedEnv('ZEPH_HOOK_ID') ?? fileConfig.hookId,
|
|
141
141
|
deviceId: resolvedEnv('ZEPH_DEVICE_ID') ?? fileConfig.deviceId,
|
|
142
142
|
sessionId,
|
|
143
|
+
agentDeviceId: listenerDeviceId(),
|
|
144
|
+
agentSessionName: detectTmuxSessionName(),
|
|
143
145
|
projectName: projectNameFromDir(projectDir),
|
|
144
146
|
};
|
|
145
147
|
};
|
|
146
148
|
exports.loadConfig = loadConfig;
|
|
149
|
+
/** Per-host listener device id — MUST match cli `computeListenerDeviceId`
|
|
150
|
+
* (`dev_listener_<sha8(hostname)>`) so hooks land under the same session key
|
|
151
|
+
* as the listener's agent.command/state pushes. */
|
|
152
|
+
const listenerDeviceId = () => `dev_listener_${(0, crypto_1.createHash)('sha256').update((0, os_1.hostname)()).digest('hex').slice(0, 8)}`;
|
|
153
|
+
/** The tmux session name the agent runs in (`zeph-<project>`) — stable half of
|
|
154
|
+
* the session key. Undefined outside tmux; the hook then stays feed-only. */
|
|
155
|
+
const detectTmuxSessionName = () => {
|
|
156
|
+
if (!process.env.TMUX)
|
|
157
|
+
return undefined;
|
|
158
|
+
try {
|
|
159
|
+
const name = (0, child_process_1.execFileSync)('tmux', ['display-message', '-p', '#S'], { encoding: 'utf-8' }).trim();
|
|
160
|
+
return name || undefined;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
};
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device-shared encryption for MCP server — self-contained ECDH P-256 +
|
|
3
3
|
* AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
|
|
4
|
-
* dependency). Uses Web Crypto API
|
|
4
|
+
* dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
|
|
5
|
+
* (the `crypto` global only exists unflagged from Node 19, so we import it).
|
|
5
6
|
*
|
|
6
7
|
* Threat model honesty (do not call this "E2E" without a footnote):
|
|
7
8
|
*
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA0JH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AAmCF,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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Device-shared encryption for MCP server — self-contained ECDH P-256 +
|
|
4
4
|
* AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
|
|
5
|
-
* dependency). Uses Web Crypto API
|
|
5
|
+
* dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
|
|
6
|
+
* (the `crypto` global only exists unflagged from Node 19, so we import it).
|
|
6
7
|
*
|
|
7
8
|
* Threat model honesty (do not call this "E2E" without a footnote):
|
|
8
9
|
*
|
|
@@ -33,6 +34,11 @@ exports.encryptFileForSelf = exports.encryptPushBodyForSelf = exports.getPublicK
|
|
|
33
34
|
const fs_1 = require("fs");
|
|
34
35
|
const os_1 = require("os");
|
|
35
36
|
const path_1 = require("path");
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
// Node 18 has no `crypto` global (unflagged only from 19.0.0) — resolve the
|
|
39
|
+
// Web Crypto implementation explicitly so encryption works on the declared
|
|
40
|
+
// minimum runtime instead of silently staying disabled.
|
|
41
|
+
const crypto = node_crypto_1.webcrypto;
|
|
36
42
|
// ─── Base64 helpers ───
|
|
37
43
|
const toBase64 = (buffer) => Buffer.from(buffer).toString('base64');
|
|
38
44
|
const fromBase64 = (base64) => {
|
|
@@ -208,7 +214,12 @@ exports.initCrypto = initCrypto;
|
|
|
208
214
|
const fetchServerKeys = async (apiKey, baseUrl) => {
|
|
209
215
|
try {
|
|
210
216
|
const url = `${baseUrl.replace(/\/$/, '')}/users/me/keys`;
|
|
211
|
-
|
|
217
|
+
// Bounded: index.ts awaits initCrypto before connecting the MCP
|
|
218
|
+
// transport, so a hanging fetch here would block server startup.
|
|
219
|
+
const res = await fetch(url, {
|
|
220
|
+
headers: { 'X-API-Key': apiKey },
|
|
221
|
+
signal: AbortSignal.timeout(5_000),
|
|
222
|
+
});
|
|
212
223
|
if (!res.ok)
|
|
213
224
|
return null;
|
|
214
225
|
const json = await res.json();
|
package/dist/tools/ask.d.ts.map
CHANGED
|
@@ -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;AAItD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAwBxD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,
|
|
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;AAItD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAwBxD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SAmI7H,CAAC"}
|
package/dist/tools/ask.js
CHANGED
|
@@ -112,6 +112,8 @@ const registerAskTool = (server, client, config, waiter) => {
|
|
|
112
112
|
metadata: { placeholder, inputType, files },
|
|
113
113
|
files,
|
|
114
114
|
sessionId: config.sessionId,
|
|
115
|
+
agentDeviceId: config.agentDeviceId,
|
|
116
|
+
agentSessionName: config.agentSessionName,
|
|
115
117
|
});
|
|
116
118
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
117
119
|
if (!event) {
|
|
@@ -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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,
|
|
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,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SA2D/H,CAAC"}
|
package/dist/tools/input.js
CHANGED
|
@@ -40,6 +40,8 @@ const registerInputTool = (server, client, config, waiter) => {
|
|
|
40
40
|
hookType: 'input',
|
|
41
41
|
metadata: { placeholder, inputType },
|
|
42
42
|
sessionId: config.sessionId,
|
|
43
|
+
agentDeviceId: config.agentDeviceId,
|
|
44
|
+
agentSessionName: config.agentSessionName,
|
|
43
45
|
});
|
|
44
46
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
45
47
|
if (!event)
|
|
@@ -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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AASrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,
|
|
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;AASrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2HnG,CAAC"}
|
package/dist/tools/notify.js
CHANGED
|
@@ -48,6 +48,7 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
48
48
|
let uploadContentType = fileType;
|
|
49
49
|
let fileIv;
|
|
50
50
|
let fileEncryptedKey;
|
|
51
|
+
let fileEncrypted = false;
|
|
51
52
|
if (canEncrypt) {
|
|
52
53
|
try {
|
|
53
54
|
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(fileMarkdown);
|
|
@@ -55,6 +56,7 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
55
56
|
uploadContentType = 'application/octet-stream';
|
|
56
57
|
fileIv = encrypted.iv;
|
|
57
58
|
fileEncryptedKey = encrypted.encryptedKey;
|
|
59
|
+
fileEncrypted = true;
|
|
58
60
|
}
|
|
59
61
|
catch (err) {
|
|
60
62
|
console.error('[Crypto] File encryption failed, sending plaintext:', err);
|
|
@@ -74,17 +76,21 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
74
76
|
targetDeviceId: deviceId,
|
|
75
77
|
sessionId: config.sessionId,
|
|
76
78
|
};
|
|
79
|
+
let pushEncrypted = false;
|
|
77
80
|
if (canEncrypt) {
|
|
78
81
|
try {
|
|
79
82
|
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body: preview, url });
|
|
80
83
|
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
84
|
+
pushEncrypted = true;
|
|
81
85
|
}
|
|
82
86
|
catch (err) {
|
|
83
87
|
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
const result = await client.sendPush(pushPayload);
|
|
87
|
-
|
|
91
|
+
// Report what actually went out — an encryption failure above falls
|
|
92
|
+
// back to plaintext, so `canEncrypt` alone would over-claim.
|
|
93
|
+
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, fileKey: upload.data.fileKey, autoFile: true, encrypted: fileEncrypted && pushEncrypted });
|
|
88
94
|
}
|
|
89
95
|
// Short body — encrypt push only
|
|
90
96
|
let pushPayload = {
|
|
@@ -96,17 +102,19 @@ const registerNotifyTool = (server, client, config) => {
|
|
|
96
102
|
targetDeviceId: deviceId,
|
|
97
103
|
sessionId: config.sessionId,
|
|
98
104
|
};
|
|
105
|
+
let pushEncrypted = false;
|
|
99
106
|
if (canEncrypt) {
|
|
100
107
|
try {
|
|
101
108
|
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body: cleanBody, url });
|
|
102
109
|
pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
110
|
+
pushEncrypted = true;
|
|
103
111
|
}
|
|
104
112
|
catch (err) {
|
|
105
113
|
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
const result = await client.sendPush(pushPayload);
|
|
109
|
-
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, encrypted:
|
|
117
|
+
return (0, error_format_js_1.textResult)({ pushId: result.data.pushId, encrypted: pushEncrypted });
|
|
110
118
|
}
|
|
111
119
|
catch (err) {
|
|
112
120
|
return (0, error_format_js_1.formatToolError)(err);
|
|
@@ -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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,
|
|
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,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SA0EhI,CAAC"}
|
package/dist/tools/prompt.js
CHANGED
|
@@ -50,6 +50,8 @@ const registerPromptTool = (server, client, config, waiter) => {
|
|
|
50
50
|
fallback,
|
|
51
51
|
hookType: 'interactive',
|
|
52
52
|
sessionId: config.sessionId,
|
|
53
|
+
agentDeviceId: config.agentDeviceId,
|
|
54
|
+
agentSessionName: config.agentSessionName,
|
|
53
55
|
});
|
|
54
56
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
55
57
|
if (!event) {
|
package/dist/ws-wait.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-wait.d.ts","sourceRoot":"","sources":["../src/ws-wait.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,MAAM;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CACxF;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;AAMhD,MAAM,WAAW,gBAAgB;IAC7B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,+DAA+D;IAC/D,WAAW,IAAI,OAAO,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,OAAO,CAAC,EAAE,SAAS,CAAC;CACvB;AAQD,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAY;IACrC,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,SAAS,CAA8C;gBAEnD,IAAI,EAAE,yBAAyB;IAM3C,uEAAuE;IACvE,OAAO,IAAI,OAAO;IAIlB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB;IAgB5C,OAAO,CAAC,YAAY;
|
|
1
|
+
{"version":3,"file":"ws-wait.d.ts","sourceRoot":"","sources":["../src/ws-wait.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,MAAM;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CACxF;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;AAMhD,MAAM,WAAW,gBAAgB;IAC7B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,+DAA+D;IAC/D,WAAW,IAAI,OAAO,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,OAAO,CAAC,EAAE,SAAS,CAAC;CACvB;AAQD,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAY;IACrC,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,SAAS,CAA8C;gBAEnD,IAAI,EAAE,yBAAyB;IAM3C,uEAAuE;IACvE,OAAO,IAAI,OAAO;IAIlB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB;IAgB5C,OAAO,CAAC,YAAY;IAmCpB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,cAAc;CAUzB"}
|
package/dist/ws-wait.js
CHANGED
|
@@ -72,9 +72,15 @@ class HookResponseWaiter {
|
|
|
72
72
|
return;
|
|
73
73
|
try {
|
|
74
74
|
const sep = this.wsUrl.includes('?') ? '&' : '?';
|
|
75
|
+
// KNOWN TRADEOFF: apiKey rides the query string — the WHATWG
|
|
76
|
+
// WebSocket (Node 21+ global) can't set headers, and the API GW
|
|
77
|
+
// $connect route authenticates on the query param. It can land in
|
|
78
|
+
// gateway access logs; first-message auth needs a server change.
|
|
75
79
|
const sock = this.factory(`${this.wsUrl}${sep}apiKey=${encodeURIComponent(this.apiKey)}`);
|
|
76
80
|
this.sock = sock;
|
|
77
81
|
sock.addEventListener('open', () => {
|
|
82
|
+
if (this.sock !== sock)
|
|
83
|
+
return; // stale socket — already replaced
|
|
78
84
|
this.connected = true;
|
|
79
85
|
this.pingTimer = setInterval(() => {
|
|
80
86
|
try {
|
|
@@ -88,7 +94,12 @@ class HookResponseWaiter {
|
|
|
88
94
|
this.pingTimer.unref?.();
|
|
89
95
|
});
|
|
90
96
|
sock.addEventListener('message', (event) => this.handleMessage(event.data));
|
|
91
|
-
|
|
97
|
+
// Guard against stale events: 'error' and 'close' both fire on a
|
|
98
|
+
// dying socket, and a new subscribe() may have already created a
|
|
99
|
+
// replacement between them — a late event from the old socket
|
|
100
|
+
// must not tear down the new one.
|
|
101
|
+
const drop = () => { if (this.sock === sock)
|
|
102
|
+
this.teardownSocket(); };
|
|
92
103
|
sock.addEventListener('close', drop);
|
|
93
104
|
sock.addEventListener('error', drop);
|
|
94
105
|
}
|
package/package.json
CHANGED