@zeph-to/mcp-server 2.0.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 +18 -5
- package/dist/api-client.d.ts +6 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +16 -0
- 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/response-files.d.ts +36 -0
- package/dist/response-files.d.ts.map +1 -0
- package/dist/response-files.js +75 -0
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +50 -6
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +8 -2
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- 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
|
|
|
@@ -205,6 +208,16 @@ fallback: "skip" (auto-select on timeout, optional)
|
|
|
205
208
|
|
|
206
209
|
Returns: `{ actionId: "fix", timedOut: false }` or `{ value: "custom text", timedOut: false }`
|
|
207
210
|
|
|
211
|
+
The user can also attach screenshots or files to their answer. Those are downloaded to `~/.zeph/attachments/hook-<eventId>/` and the result gains an `attachments` array of absolute local paths, alongside the button or the text:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
{ value: "look at this", attachments: ["/Users/you/.zeph/attachments/hook-hevt_1/screen.png"],
|
|
215
|
+
attachmentsNote: "The user attached 1 file(s) to this answer. Read each path above to see them.",
|
|
216
|
+
timedOut: false }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Reading those paths is part of reading the answer. Note that hook attachments are never end-to-end encrypted — the same limitation as the question itself, since the hook route carries no sender key.
|
|
220
|
+
|
|
208
221
|
### zeph_input
|
|
209
222
|
|
|
210
223
|
Request free-form text input from the user. Blocks until response or timeout.
|
|
@@ -219,7 +232,7 @@ inputType: "text" | "password" | "multiline"
|
|
|
219
232
|
timeout: 120 (seconds, default: 120, max: 600)
|
|
220
233
|
```
|
|
221
234
|
|
|
222
|
-
Returns: `{ value: "feat: add clipboard sync", timedOut: false }`
|
|
235
|
+
Returns: `{ value: "feat: add clipboard sync", timedOut: false }` — plus `attachments` when the user attached files, exactly as in `zeph_ask` above.
|
|
223
236
|
|
|
224
237
|
### Client timeouts
|
|
225
238
|
|
|
@@ -329,12 +342,12 @@ Create an API key with the **MCP** preset in Settings > API Keys for the correct
|
|
|
329
342
|
|
|
330
343
|
## Encryption
|
|
331
344
|
|
|
332
|
-
|
|
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.
|
|
333
346
|
|
|
334
|
-
|
|
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.
|
|
335
348
|
|
|
336
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:
|
|
337
|
-
- **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).
|
|
338
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.
|
|
339
352
|
- **`senderPublicKey` is unsigned**, so a swapped one makes a push undecryptable — that direction fails closed rather than leaking.
|
|
340
353
|
|
package/dist/api-client.d.ts
CHANGED
|
@@ -57,6 +57,12 @@ export declare class ZephApiClient {
|
|
|
57
57
|
fileType: string;
|
|
58
58
|
fileSize: number;
|
|
59
59
|
}): Promise<UploadRequestResponse>;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a fileKey to its bytes. Two hops by design: the API hands back a
|
|
62
|
+
* presigned URL (files never travel through Lambda), and that URL is
|
|
63
|
+
* self-authenticating, so the second fetch carries no API key.
|
|
64
|
+
*/
|
|
65
|
+
downloadFile(fileKey: string): Promise<Uint8Array>;
|
|
60
66
|
uploadToS3(url: string, content: string | Buffer, contentType: string): Promise<void>;
|
|
61
67
|
private request;
|
|
62
68
|
}
|
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,0BAA0B,EAC1B,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/api-client.js
CHANGED
|
@@ -59,6 +59,22 @@ class ZephApiClient {
|
|
|
59
59
|
async requestUpload(params) {
|
|
60
60
|
return this.request('POST', '/files/upload-request', params);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a fileKey to its bytes. Two hops by design: the API hands back a
|
|
64
|
+
* presigned URL (files never travel through Lambda), and that URL is
|
|
65
|
+
* self-authenticating, so the second fetch carries no API key.
|
|
66
|
+
*/
|
|
67
|
+
async downloadFile(fileKey) {
|
|
68
|
+
const meta = await this.request('GET', `/files/${encodeURIComponent(fileKey)}`);
|
|
69
|
+
const downloadUrl = meta.data?.downloadUrl;
|
|
70
|
+
if (!downloadUrl)
|
|
71
|
+
throw new ApiError('Download URL missing', 'DOWNLOAD_FAILED', 502);
|
|
72
|
+
const response = await fetch(downloadUrl, { signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) });
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new ApiError(`File download failed with status ${response.status}`, 'DOWNLOAD_FAILED', response.status);
|
|
75
|
+
}
|
|
76
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
77
|
+
}
|
|
62
78
|
async uploadToS3(url, content, contentType) {
|
|
63
79
|
const isText = typeof content === 'string';
|
|
64
80
|
const body = isText ? content : new Uint8Array(content);
|
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;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saving the files a user attached to a hook answer.
|
|
3
|
+
*
|
|
4
|
+
* `zeph_ask` and `zeph_input` hand the agent a JSON result, and an agent
|
|
5
|
+
* cannot look at an S3 key — so an answered screenshot only becomes useful
|
|
6
|
+
* once it is a path on this machine. This module downloads each attachment
|
|
7
|
+
* to `~/.zeph/attachments/hook-<eventId>/` and returns the absolute paths,
|
|
8
|
+
* which the tool then names in its result for the agent to read.
|
|
9
|
+
*
|
|
10
|
+
* The files are plaintext by contract: the hook route carries no sender key,
|
|
11
|
+
* so the server rejects an encrypted attachment on a response rather than
|
|
12
|
+
* let one arrive here as bytes nothing can open.
|
|
13
|
+
*
|
|
14
|
+
* Failure is per file, never fatal. A question that was answered has been
|
|
15
|
+
* answered; losing one image must not turn that into a tool error and make
|
|
16
|
+
* the user answer again.
|
|
17
|
+
*/
|
|
18
|
+
import type { ZephApiClient } from './api-client.js';
|
|
19
|
+
import type { AttachedFile } from './types.js';
|
|
20
|
+
/**
|
|
21
|
+
* The part of a tool result that tells the agent about saved attachments.
|
|
22
|
+
* Spread into the result object; empty when nothing was attached, so a plain
|
|
23
|
+
* answer keeps the shape it has always had.
|
|
24
|
+
*
|
|
25
|
+
* The instruction is explicit because a bare list of paths in a JSON result
|
|
26
|
+
* reads as metadata — the agent has to be told these are the user's answer
|
|
27
|
+
* and that opening them is part of reading it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const attachmentNote: (paths: string[]) => Record<string, unknown>;
|
|
30
|
+
export interface SaveResponseFilesDeps {
|
|
31
|
+
/** Test seam for the two-step fetch (metadata → presigned URL → bytes). */
|
|
32
|
+
fetchBytes?: (fileKey: string) => Promise<Uint8Array>;
|
|
33
|
+
dir?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare const saveResponseFiles: (client: ZephApiClient, eventId: string, files: AttachedFile[] | undefined, deps?: SaveResponseFilesDeps) => Promise<string[]>;
|
|
36
|
+
//# sourceMappingURL=response-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-files.d.ts","sourceRoot":"","sources":["../src/response-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAmB/C;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,EAAE,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAM/D,CAAC;AAET,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,aAAa,EACrB,SAAS,MAAM,EACf,OAAO,YAAY,EAAE,GAAG,SAAS,EACjC,OAAM,qBAA0B,KAC/B,OAAO,CAAC,MAAM,EAAE,CAiBlB,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Saving the files a user attached to a hook answer.
|
|
4
|
+
*
|
|
5
|
+
* `zeph_ask` and `zeph_input` hand the agent a JSON result, and an agent
|
|
6
|
+
* cannot look at an S3 key — so an answered screenshot only becomes useful
|
|
7
|
+
* once it is a path on this machine. This module downloads each attachment
|
|
8
|
+
* to `~/.zeph/attachments/hook-<eventId>/` and returns the absolute paths,
|
|
9
|
+
* which the tool then names in its result for the agent to read.
|
|
10
|
+
*
|
|
11
|
+
* The files are plaintext by contract: the hook route carries no sender key,
|
|
12
|
+
* so the server rejects an encrypted attachment on a response rather than
|
|
13
|
+
* let one arrive here as bytes nothing can open.
|
|
14
|
+
*
|
|
15
|
+
* Failure is per file, never fatal. A question that was answered has been
|
|
16
|
+
* answered; losing one image must not turn that into a tool error and make
|
|
17
|
+
* the user answer again.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.saveResponseFiles = exports.attachmentNote = void 0;
|
|
21
|
+
const node_fs_1 = require("node:fs");
|
|
22
|
+
const node_os_1 = require("node:os");
|
|
23
|
+
const node_path_1 = require("node:path");
|
|
24
|
+
/** Shared with the `zeph listener` daemon, which writes push attachments here. */
|
|
25
|
+
const ATTACHMENTS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.zeph', 'attachments');
|
|
26
|
+
/**
|
|
27
|
+
* Make a filesystem-safe single path segment: take the basename (drops any
|
|
28
|
+
* `../` prefix), strip control characters and embedded separators, remove
|
|
29
|
+
* leading dots, and cap the length. Empty or dot-only names use the fallback.
|
|
30
|
+
*/
|
|
31
|
+
const safeSegment = (raw, fallback) => {
|
|
32
|
+
const cleaned = (0, node_path_1.basename)(raw)
|
|
33
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
34
|
+
.replace(/[/\\]/g, '_')
|
|
35
|
+
.replace(/^\.+/, '')
|
|
36
|
+
.trim();
|
|
37
|
+
return (cleaned || fallback).slice(0, 200);
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The part of a tool result that tells the agent about saved attachments.
|
|
41
|
+
* Spread into the result object; empty when nothing was attached, so a plain
|
|
42
|
+
* answer keeps the shape it has always had.
|
|
43
|
+
*
|
|
44
|
+
* The instruction is explicit because a bare list of paths in a JSON result
|
|
45
|
+
* reads as metadata — the agent has to be told these are the user's answer
|
|
46
|
+
* and that opening them is part of reading it.
|
|
47
|
+
*/
|
|
48
|
+
const attachmentNote = (paths) => paths.length
|
|
49
|
+
? {
|
|
50
|
+
attachments: paths,
|
|
51
|
+
attachmentsNote: `The user attached ${paths.length} file(s) to this answer. Read each path above to see them.`,
|
|
52
|
+
}
|
|
53
|
+
: {};
|
|
54
|
+
exports.attachmentNote = attachmentNote;
|
|
55
|
+
const saveResponseFiles = async (client, eventId, files, deps = {}) => {
|
|
56
|
+
if (!files?.length)
|
|
57
|
+
return [];
|
|
58
|
+
const fetchBytes = deps.fetchBytes ?? ((key) => client.downloadFile(key));
|
|
59
|
+
const dir = (0, node_path_1.join)(deps.dir ?? ATTACHMENTS_DIR, safeSegment(`hook-${eventId}`, 'hook'));
|
|
60
|
+
const paths = [];
|
|
61
|
+
for (const [i, file] of files.entries()) {
|
|
62
|
+
try {
|
|
63
|
+
const bytes = await fetchBytes(file.fileKey);
|
|
64
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
65
|
+
const abs = (0, node_path_1.join)(dir, safeSegment(file.fileName || `file-${i}`, `file-${i}`));
|
|
66
|
+
(0, node_fs_1.writeFileSync)(abs, bytes);
|
|
67
|
+
paths.push(abs);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(`[zeph] attachment "${file.fileName}" failed to save: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return paths;
|
|
74
|
+
};
|
|
75
|
+
exports.saveResponseFiles = saveResponseFiles;
|
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
|
@@ -7,6 +7,8 @@ const poll_js_1 = require("../poll.js");
|
|
|
7
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
|
+
const response_files_js_1 = require("../response-files.js");
|
|
11
|
+
const remote_state_js_1 = require("../remote-state.js");
|
|
10
12
|
// The device feed shows a short preview of the body. Anything longer than
|
|
11
13
|
// this gets truncated there, so we attach the full text as a file — the
|
|
12
14
|
// user can always open the complete content instead of squinting at a
|
|
@@ -20,9 +22,31 @@ const buildAskMarkdown = (title, body, actions) => {
|
|
|
20
22
|
}
|
|
21
23
|
return parts.join('\n');
|
|
22
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
|
+
};
|
|
23
47
|
const registerAskTool = (server, client, config, waiter) => {
|
|
24
48
|
server.registerTool('zeph_ask', {
|
|
25
|
-
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. 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.',
|
|
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.',
|
|
26
50
|
annotations: {
|
|
27
51
|
readOnlyHint: false,
|
|
28
52
|
destructiveHint: false,
|
|
@@ -107,14 +131,34 @@ const registerAskTool = (server, client, config, waiter) => {
|
|
|
107
131
|
});
|
|
108
132
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
109
133
|
if (!event) {
|
|
110
|
-
if (fallback)
|
|
111
|
-
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
|
+
}
|
|
112
141
|
return (0, error_format_js_1.timeoutError)(timeout, 'Try again or use zeph_notify for one-way communication');
|
|
113
142
|
}
|
|
114
143
|
const response = event.data.response;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
// Saved before the button branch returns: a user can tap a quick reply
|
|
145
|
+
// AND attach a screenshot, and dropping the files there would lose them
|
|
146
|
+
// silently for the one caller that never looks at `value`.
|
|
147
|
+
const attachments = await (0, response_files_js_1.saveResponseFiles)(client, trigger.data.eventId, response?.files);
|
|
148
|
+
if (response?.actionId) {
|
|
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
|
+
});
|
|
155
|
+
}
|
|
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
|
+
});
|
|
118
162
|
}
|
|
119
163
|
catch (err) {
|
|
120
164
|
return (0, error_format_js_1.formatToolError)(err);
|
|
@@ -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;
|
|
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;AAGrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SAgE/H,CAAC"}
|
package/dist/tools/input.js
CHANGED
|
@@ -6,9 +6,10 @@ const error_format_js_1 = require("../error-format.js");
|
|
|
6
6
|
const poll_js_1 = require("../poll.js");
|
|
7
7
|
const config_js_1 = require("../config.js");
|
|
8
8
|
const sanitize_js_1 = require("../sanitize.js");
|
|
9
|
+
const response_files_js_1 = require("../response-files.js");
|
|
9
10
|
const registerInputTool = (server, client, config, waiter) => {
|
|
10
11
|
server.registerTool('zeph_input', {
|
|
11
|
-
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.',
|
|
12
|
+
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. The user may also attach screenshots or files: those arrive as local absolute paths in the `attachments` field of the result, and reading them is part of reading the answer.',
|
|
12
13
|
annotations: {
|
|
13
14
|
readOnlyHint: false,
|
|
14
15
|
destructiveHint: false,
|
|
@@ -46,7 +47,12 @@ const registerInputTool = (server, client, config, waiter) => {
|
|
|
46
47
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
47
48
|
if (!event)
|
|
48
49
|
return (0, error_format_js_1.timeoutError)(timeout, 'Try again with a longer timeout');
|
|
49
|
-
|
|
50
|
+
const attachments = await (0, response_files_js_1.saveResponseFiles)(client, trigger.data.eventId, event.data.response?.files);
|
|
51
|
+
return (0, error_format_js_1.textResult)({
|
|
52
|
+
value: event.data.response?.value ?? '',
|
|
53
|
+
timedOut: false,
|
|
54
|
+
...(0, response_files_js_1.attachmentNote)(attachments),
|
|
55
|
+
});
|
|
50
56
|
}
|
|
51
57
|
catch (err) {
|
|
52
58
|
return (0, error_format_js_1.formatToolError)(err);
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface HookEventResponse {
|
|
|
23
23
|
response?: {
|
|
24
24
|
actionId?: string;
|
|
25
25
|
value?: string;
|
|
26
|
+
/** Screenshots and files the user attached to the answer — plaintext,
|
|
27
|
+
* since the hook route carries no key this end could decrypt with. */
|
|
28
|
+
files?: AttachedFile[];
|
|
26
29
|
respondedDeviceId?: string;
|
|
27
30
|
};
|
|
28
31
|
};
|
|
@@ -89,6 +92,11 @@ export interface UploadRequestResponse {
|
|
|
89
92
|
uploadUrl: string;
|
|
90
93
|
};
|
|
91
94
|
}
|
|
95
|
+
export interface DownloadUrlResponse {
|
|
96
|
+
data: {
|
|
97
|
+
downloadUrl: string;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
92
100
|
export interface AttachedFile {
|
|
93
101
|
fileKey: string;
|
|
94
102
|
fileName: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|