@zeph-to/mcp-server 2.1.0 → 2.2.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 +7 -4
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -3
- package/dist/remote-state.d.ts +34 -0
- package/dist/remote-state.d.ts.map +1 -0
- package/dist/remote-state.js +79 -0
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +42 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@zeph-to/mcp-server)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
+
[](https://docs.zeph.to)
|
|
7
8
|
|
|
8
9
|
**Your agent calls `zeph_ask`; the question lands on your phone as buttons + a text field; your reply comes back into the same tool call and the agent keeps going.**
|
|
9
10
|
|
|
@@ -16,6 +17,8 @@ Zeph's MCP server is the agent side of that round trip — plus one-way notifica
|
|
|
16
17
|
|
|
17
18
|
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.
|
|
18
19
|
|
|
20
|
+
> **New here?** [docs.zeph.to](https://docs.zeph.to) walks the whole setup — one command on your machine, the app on your phone, and a restart. The reference below assumes that is already done.
|
|
21
|
+
|
|
19
22
|
## Setup
|
|
20
23
|
|
|
21
24
|
The easiest way to set up for all agents at once:
|
|
@@ -70,7 +73,7 @@ e.g. a second account:
|
|
|
70
73
|
| `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` |
|
|
71
74
|
| `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). |
|
|
72
75
|
| `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 |
|
|
73
|
-
| `ZEPH_DISABLE_ENCRYPTION` | No | Set to `1`/`true` to force
|
|
76
|
+
| `ZEPH_DISABLE_ENCRYPTION` | No | Set to `1`/`true` to force push encryption off even when the account has it enabled. A local override for debugging what the server actually received — encryption is already off unless the account opted in (see [Encryption](#encryption)) |
|
|
74
77
|
|
|
75
78
|
\* If env vars are not set, the server reads from `~/.zeph/config.json` (created by `zeph install`). Unresolved `${...}` interpolations are also treated as unset.
|
|
76
79
|
|
|
@@ -339,12 +342,12 @@ Create an API key with the **MCP** preset in Settings > API Keys for the correct
|
|
|
339
342
|
|
|
340
343
|
## Encryption
|
|
341
344
|
|
|
342
|
-
|
|
345
|
+
End-to-end encryption is **off by default** and turning it on needs Zeph Pro. The switch is in the app under Settings → E2E Encryption; until you flip it, every push leaves this server in plaintext. If the account later loses Pro the server answers `PRO_REQUIRED` and this one drops back to plaintext for the rest of the process. No configuration either way — but the opt-in is read **once at startup**, so turning it on while this server is running takes effect only after a restart.
|
|
343
346
|
|
|
344
|
-
|
|
347
|
+
With it on, push bodies and file attachments are encrypted with AES-256-GCM. This server holds its own ECDH P-256 keypair, generated on first use and stored in `~/.config/zeph/device-keys.json` — the private half never leaves the machine, and the backend stores public keys only and rejects a private-key upload. Each push is encrypted once, and its AES key is wrapped separately for every device on your account using ECDH against that device's public key.
|
|
345
348
|
|
|
346
349
|
**Threat model:** against a passive backend — a leaked snapshot, an operator reading the table — the stored ciphertext and wrapped keys are useless, so push contents stay private. Three limits worth knowing:
|
|
347
|
-
- **No protection from an active malicious operator.** Recipient public keys come from `GET /devices` on that same server, unsigned and unpinned. A backend that injects a device record carrying its own key gets the message key wrapped for it, and reads everything.
|
|
350
|
+
- **No protection from an active malicious operator.** Recipient public keys come from `GET /devices` on that same server, unsigned and unpinned. A backend that injects a device record carrying its own key gets the message key wrapped for it, and reads everything. The Zeph app ships the counter-measure — compare device fingerprints, mark a device verified, and strict mode then wraps only for verified devices — but it defaults off, its verified list is per browser profile, and this server does not consult it: `selectRecipients` asks only whether a device has a public key, and whether that key is the legacy account-wide one (ADR-0007 Phase 4).
|
|
348
351
|
- **No forward secrecy.** The ECDH secret for a given sender/device pair is static, so compromising either private key opens every past push wrapped for that pair.
|
|
349
352
|
- **`senderPublicKey` is unsigned**, so a swapped one makes a push undecryptable — that direction fails closed rather than leaking.
|
|
350
353
|
|
package/dist/config.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export interface McpServerConfig {
|
|
|
16
16
|
/** Last path segment of the project directory — prefixed onto push titles. */
|
|
17
17
|
projectName: string;
|
|
18
18
|
}
|
|
19
|
+
/** The project directory the agent runs in, across supported agents. */
|
|
20
|
+
/**
|
|
21
|
+
* The key every per-project zeph file is named by. Shelling out to `cksum`
|
|
22
|
+
* (rather than a pure-JS CRC) is what guarantees it matches the files the bash
|
|
23
|
+
* hooks have already written — they key off `printf '%s' "$dir" | cksum`. Null
|
|
24
|
+
* when `cksum` is unavailable, which means no per-project file can be resolved.
|
|
25
|
+
*/
|
|
26
|
+
export declare const projectHash: (dir: string) => string | null;
|
|
27
|
+
export declare const detectProjectDir: () => string;
|
|
19
28
|
/**
|
|
20
29
|
* Prefix a push title with the project name so the device feed stays
|
|
21
30
|
* scannable — "zeph · Build finished" instead of a bare "Build finished".
|
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;;;uCAGmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACvB;
|
|
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;AAID,wEAAwE;AACxE;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAMlD,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,MAMnC,CAAC;AAMF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,KAAG,MAGpE,CAAC;AAwGF,eAAO,MAAM,UAAU,QAAO,eAiC7B,CAAC;AA4BF;;;;;sEAKsE;AACtE,eAAO,MAAM,gBAAgB,QAAO,MAQnC,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.listenerDeviceId = exports.loadConfig = exports.formatPushTitle = void 0;
|
|
3
|
+
exports.listenerDeviceId = exports.loadConfig = exports.formatPushTitle = exports.detectProjectDir = exports.projectHash = void 0;
|
|
4
4
|
const fs_1 = require("fs");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
@@ -9,6 +9,21 @@ const path_1 = require("path");
|
|
|
9
9
|
const DEFAULT_BASE_URL = 'https://api.zeph.to/v1';
|
|
10
10
|
const PROJECT_DIR_ENV_KEYS = ['CLAUDE_PROJECT_DIR', 'CURSOR_PROJECT_DIR', 'WINDSURF_PROJECT_DIR'];
|
|
11
11
|
/** The project directory the agent runs in, across supported agents. */
|
|
12
|
+
/**
|
|
13
|
+
* The key every per-project zeph file is named by. Shelling out to `cksum`
|
|
14
|
+
* (rather than a pure-JS CRC) is what guarantees it matches the files the bash
|
|
15
|
+
* hooks have already written — they key off `printf '%s' "$dir" | cksum`. Null
|
|
16
|
+
* when `cksum` is unavailable, which means no per-project file can be resolved.
|
|
17
|
+
*/
|
|
18
|
+
const projectHash = (dir) => {
|
|
19
|
+
try {
|
|
20
|
+
return (0, child_process_1.execFileSync)('cksum', { input: dir, encoding: 'utf-8' }).split(' ')[0] || null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
exports.projectHash = projectHash;
|
|
12
27
|
const detectProjectDir = () => {
|
|
13
28
|
for (const key of PROJECT_DIR_ENV_KEYS) {
|
|
14
29
|
const val = process.env[key];
|
|
@@ -17,6 +32,7 @@ const detectProjectDir = () => {
|
|
|
17
32
|
}
|
|
18
33
|
return process.cwd();
|
|
19
34
|
};
|
|
35
|
+
exports.detectProjectDir = detectProjectDir;
|
|
20
36
|
/** Last path segment of a directory: "/Users/me/code/zeph" -> "zeph". */
|
|
21
37
|
const projectNameFromDir = (dir) => dir.split('/').filter(Boolean).pop() ?? 'project';
|
|
22
38
|
/**
|
|
@@ -106,7 +122,9 @@ const writeSessionCache = (sessionId, projectDir) => {
|
|
|
106
122
|
if (envIsTrue('ZEPH_DISABLE_SESSION_CACHE'))
|
|
107
123
|
return;
|
|
108
124
|
try {
|
|
109
|
-
const hash = (0,
|
|
125
|
+
const hash = (0, exports.projectHash)(projectDir);
|
|
126
|
+
if (!hash)
|
|
127
|
+
return;
|
|
110
128
|
const cacheDir = (0, path_1.join)(process.env.XDG_CACHE_HOME ?? (0, path_1.join)((0, os_1.homedir)(), '.cache'), 'zeph');
|
|
111
129
|
(0, fs_1.mkdirSync)(cacheDir, { recursive: true, mode: 0o700 });
|
|
112
130
|
const cachePath = (0, path_1.join)(cacheDir, `session-${hash}`);
|
|
@@ -130,7 +148,7 @@ const loadConfig = () => {
|
|
|
130
148
|
if (!apiKey) {
|
|
131
149
|
throw new Error('ZEPH_API_KEY not found. Run "npx @zeph-to/cli install" or set ZEPH_API_KEY env var.');
|
|
132
150
|
}
|
|
133
|
-
const projectDir = detectProjectDir();
|
|
151
|
+
const projectDir = (0, exports.detectProjectDir)();
|
|
134
152
|
// Claude Code names the running session outright; take it over the
|
|
135
153
|
// transcript scan, which picks the newest file in the project directory and
|
|
136
154
|
// so returns a sibling agent's id whenever two run in one project — the
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when this button ends the session. Case-insensitive by contract — the
|
|
3
|
+
* rules have always said so, but until now nothing enforced it, so a skill
|
|
4
|
+
* emitting `Done` would have read as a non-exit answer and left the user in
|
|
5
|
+
* REMOTE for the whole TTL.
|
|
6
|
+
*/
|
|
7
|
+
export declare const isSessionExitId: (actionId: string) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* What a `zeph_ask` outcome does to the mode.
|
|
10
|
+
*
|
|
11
|
+
* - **exit** — a Done-like id, whether the user tapped it or a timeout
|
|
12
|
+
* resolved to it. This is the one case a fallback ends the session.
|
|
13
|
+
* - **enter** — the user answered with anything else: another button, or free
|
|
14
|
+
* text (no id at all). Free text the server cannot judge — "thanks, that's
|
|
15
|
+
* it" is a meaning call that stays with the model — so it counts as staying.
|
|
16
|
+
* - **keep** — the ask timed out onto a non-exit fallback. Rule 5 recommends
|
|
17
|
+
* `wait`/`review` there, and ask.ts returns the fallback id verbatim, so
|
|
18
|
+
* reading it as an exit would drop a user out of REMOTE over an ask they
|
|
19
|
+
* simply had not answered yet. Nor is it an entry: a timeout is not a user
|
|
20
|
+
* action, and a NORMAL session must not become remote because nobody
|
|
21
|
+
* replied. The mode is left exactly as it was.
|
|
22
|
+
*/
|
|
23
|
+
export type RemoteTransition = 'enter' | 'exit' | 'keep';
|
|
24
|
+
export declare const remoteTransitionFor: (answer: {
|
|
25
|
+
actionId?: string;
|
|
26
|
+
timedOut: boolean;
|
|
27
|
+
}) => RemoteTransition;
|
|
28
|
+
/** `<stateDir>/remote-active-<projectHash>`, or null when the key can't be built. */
|
|
29
|
+
export declare const remoteStatePath: () => string | null;
|
|
30
|
+
/** Enter REMOTE, or push its expiry back. Best-effort: never throws. */
|
|
31
|
+
export declare const enterRemote: () => void;
|
|
32
|
+
/** Leave REMOTE. Best-effort: never throws, and a missing file is success. */
|
|
33
|
+
export declare const exitRemote: () => void;
|
|
34
|
+
//# sourceMappingURL=remote-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-state.d.ts","sourceRoot":"","sources":["../src/remote-state.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,KAAG,OAC8B,CAAC;AAElF;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzD,eAAO,MAAM,mBAAmB,GAC9B,QAAQ;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,KAC/C,gBAGF,CAAC;AAKF,qFAAqF;AACrF,eAAO,MAAM,eAAe,QAAO,MAAM,GAAG,IAG3C,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,WAAW,QAAO,IAS9B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,UAAU,QAAO,IAQ7B,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exitRemote = exports.enterRemote = exports.remoteStatePath = exports.remoteTransitionFor = exports.isSessionExitId = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Sticky REMOTE state — the server's half.
|
|
6
|
+
*
|
|
7
|
+
* REMOTE used to have nowhere to live: the model re-derived it every turn by
|
|
8
|
+
* rescanning the conversation. It now lives in a file, and this module is one
|
|
9
|
+
* of the two places that owns a transition. The other is the plugin's
|
|
10
|
+
* UserPromptSubmit hook, which enters REMOTE when a prompt matches a
|
|
11
|
+
* phone-injection marker; this module handles the transitions the server
|
|
12
|
+
* already knows about — a `zeph_ask` that came back answered.
|
|
13
|
+
*
|
|
14
|
+
* **Three implementations share the file format** — plugin/hooks/gate.sh
|
|
15
|
+
* (`zeph_remote_active` / `zeph_remote_touch`), cli/src/gate.ts
|
|
16
|
+
* (`isRemoteActive` / `touchRemoteActive`), and this file. Nothing checks them
|
|
17
|
+
* against each other at build time, so the format is pinned by literal
|
|
18
|
+
* assertions in remote-state.test.ts. The bash reader sweeps any non-numeric
|
|
19
|
+
* body on sight (`case "$ts" in *[!0-9]*)`), which means a JSON or ISO-8601
|
|
20
|
+
* body written here would enter REMOTE and be deleted by the very next prompt
|
|
21
|
+
* hook — silently, and invisibly from either repo's tests.
|
|
22
|
+
*
|
|
23
|
+
* There is no TTL logic here on purpose: expiry is the readers' job, and
|
|
24
|
+
* duplicating the window would give it two definitions.
|
|
25
|
+
*/
|
|
26
|
+
const fs_1 = require("fs");
|
|
27
|
+
const os_1 = require("os");
|
|
28
|
+
const path_1 = require("path");
|
|
29
|
+
const config_js_1 = require("./config.js");
|
|
30
|
+
/** Action ids that end a remote session (CORE_RULES: case-insensitive). */
|
|
31
|
+
const SESSION_EXIT_IDS = ['done', 'stop', 'exit'];
|
|
32
|
+
/**
|
|
33
|
+
* True when this button ends the session. Case-insensitive by contract — the
|
|
34
|
+
* rules have always said so, but until now nothing enforced it, so a skill
|
|
35
|
+
* emitting `Done` would have read as a non-exit answer and left the user in
|
|
36
|
+
* REMOTE for the whole TTL.
|
|
37
|
+
*/
|
|
38
|
+
const isSessionExitId = (actionId) => SESSION_EXIT_IDS.includes(actionId.trim().toLowerCase());
|
|
39
|
+
exports.isSessionExitId = isSessionExitId;
|
|
40
|
+
const remoteTransitionFor = (answer) => {
|
|
41
|
+
if (answer.actionId !== undefined && (0, exports.isSessionExitId)(answer.actionId))
|
|
42
|
+
return 'exit';
|
|
43
|
+
return answer.timedOut ? 'keep' : 'enter';
|
|
44
|
+
};
|
|
45
|
+
exports.remoteTransitionFor = remoteTransitionFor;
|
|
46
|
+
const stateDir = () => (0, path_1.join)(process.env.XDG_STATE_HOME || (0, path_1.join)((0, os_1.homedir)(), '.local', 'state'), 'zeph');
|
|
47
|
+
/** `<stateDir>/remote-active-<projectHash>`, or null when the key can't be built. */
|
|
48
|
+
const remoteStatePath = () => {
|
|
49
|
+
const hash = (0, config_js_1.projectHash)((0, config_js_1.detectProjectDir)());
|
|
50
|
+
return hash ? (0, path_1.join)(stateDir(), `remote-active-${hash}`) : null;
|
|
51
|
+
};
|
|
52
|
+
exports.remoteStatePath = remoteStatePath;
|
|
53
|
+
/** Enter REMOTE, or push its expiry back. Best-effort: never throws. */
|
|
54
|
+
const enterRemote = () => {
|
|
55
|
+
const file = (0, exports.remoteStatePath)();
|
|
56
|
+
if (!file)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
(0, fs_1.mkdirSync)(stateDir(), { recursive: true });
|
|
60
|
+
(0, fs_1.writeFileSync)(file, `${Math.floor(Date.now() / 1000)}\n`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* an answered ask is still an answer — state IO must not reach the caller */
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
exports.enterRemote = enterRemote;
|
|
67
|
+
/** Leave REMOTE. Best-effort: never throws, and a missing file is success. */
|
|
68
|
+
const exitRemote = () => {
|
|
69
|
+
const file = (0, exports.remoteStatePath)();
|
|
70
|
+
if (!file)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
(0, fs_1.unlinkSync)(file);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* already gone, or unwritable — either way the caller has an answer */
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
exports.exitRemote = exitRemote;
|
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;
|
|
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;AAkDxD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SA+I7H,CAAC"}
|
package/dist/tools/ask.js
CHANGED
|
@@ -8,6 +8,7 @@ const config_js_1 = require("../config.js");
|
|
|
8
8
|
const mime_js_1 = require("../mime.js");
|
|
9
9
|
const sanitize_js_1 = require("../sanitize.js");
|
|
10
10
|
const response_files_js_1 = require("../response-files.js");
|
|
11
|
+
const remote_state_js_1 = require("../remote-state.js");
|
|
11
12
|
// The device feed shows a short preview of the body. Anything longer than
|
|
12
13
|
// this gets truncated there, so we attach the full text as a file — the
|
|
13
14
|
// user can always open the complete content instead of squinting at a
|
|
@@ -21,6 +22,28 @@ const buildAskMarkdown = (title, body, actions) => {
|
|
|
21
22
|
}
|
|
22
23
|
return parts.join('\n');
|
|
23
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Apply the mode transition this answer implies, and tell the model where it
|
|
27
|
+
* landed. The state itself lives in a file the prompt hooks also read, so the
|
|
28
|
+
* two carriers agree without either re-deriving REMOTE from the transcript.
|
|
29
|
+
*
|
|
30
|
+
* `zephState` is reported only when the server actually decided something. A
|
|
31
|
+
* timeout onto a safe fallback changes nothing, and naming a state there would
|
|
32
|
+
* mean re-reading the file — a fourth parser of a format three already share,
|
|
33
|
+
* to answer a question the server was not asked.
|
|
34
|
+
*/
|
|
35
|
+
const settleRemoteState = (answer) => {
|
|
36
|
+
switch ((0, remote_state_js_1.remoteTransitionFor)(answer)) {
|
|
37
|
+
case 'exit':
|
|
38
|
+
(0, remote_state_js_1.exitRemote)();
|
|
39
|
+
return { zephState: 'NORMAL' };
|
|
40
|
+
case 'enter':
|
|
41
|
+
(0, remote_state_js_1.enterRemote)();
|
|
42
|
+
return { zephState: 'REMOTE' };
|
|
43
|
+
case 'keep':
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
24
47
|
const registerAskTool = (server, client, config, waiter) => {
|
|
25
48
|
server.registerTool('zeph_ask', {
|
|
26
49
|
description: 'Ask the user a question with optional quick-reply buttons and a text input field. Combines prompt (buttons) and input (text) in a single notification. The user can either tap a button or type a response. Blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable. The user may also attach screenshots or files to the answer: those arrive as local absolute paths in the `attachments` field of the result, and reading them is part of reading the answer. NOTE: unlike zeph_notify and zeph_file, this tool is never end-to-end encrypted — the hook route it uses cannot carry the sender key — so do not put secrets in the question or expect a private answer.',
|
|
@@ -108,8 +131,13 @@ const registerAskTool = (server, client, config, waiter) => {
|
|
|
108
131
|
});
|
|
109
132
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
110
133
|
if (!event) {
|
|
111
|
-
if (fallback)
|
|
112
|
-
return (0, error_format_js_1.textResult)({
|
|
134
|
+
if (fallback) {
|
|
135
|
+
return (0, error_format_js_1.textResult)({
|
|
136
|
+
actionId: fallback,
|
|
137
|
+
timedOut: true,
|
|
138
|
+
...settleRemoteState({ actionId: fallback, timedOut: true }),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
113
141
|
return (0, error_format_js_1.timeoutError)(timeout, 'Try again or use zeph_notify for one-way communication');
|
|
114
142
|
}
|
|
115
143
|
const response = event.data.response;
|
|
@@ -118,9 +146,19 @@ const registerAskTool = (server, client, config, waiter) => {
|
|
|
118
146
|
// silently for the one caller that never looks at `value`.
|
|
119
147
|
const attachments = await (0, response_files_js_1.saveResponseFiles)(client, trigger.data.eventId, response?.files);
|
|
120
148
|
if (response?.actionId) {
|
|
121
|
-
return (0, error_format_js_1.textResult)({
|
|
149
|
+
return (0, error_format_js_1.textResult)({
|
|
150
|
+
actionId: response.actionId,
|
|
151
|
+
timedOut: false,
|
|
152
|
+
...settleRemoteState({ actionId: response.actionId, timedOut: false }),
|
|
153
|
+
...(0, response_files_js_1.attachmentNote)(attachments),
|
|
154
|
+
});
|
|
122
155
|
}
|
|
123
|
-
return (0, error_format_js_1.textResult)({
|
|
156
|
+
return (0, error_format_js_1.textResult)({
|
|
157
|
+
value: response?.value ?? '',
|
|
158
|
+
timedOut: false,
|
|
159
|
+
...settleRemoteState({ timedOut: false }),
|
|
160
|
+
...(0, response_files_js_1.attachmentNote)(attachments),
|
|
161
|
+
});
|
|
124
162
|
}
|
|
125
163
|
catch (err) {
|
|
126
164
|
return (0, error_format_js_1.formatToolError)(err);
|