zas-agent 0.1.0 → 0.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +53 -28
  3. package/dist/cli.js +394 -114
  4. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-09-03
11
+
12
+ ### Changed
13
+
14
+ - Pairing no longer prints a code to type. The terminal listens on
15
+ `127.0.0.1`, the approval page hands it a one-time claim code, and the
16
+ agent is created only when the terminal claims. A page that cannot reach
17
+ the terminal shows the code, and the terminal asks for it. `pair` opens
18
+ the browser; `--no-open` and `ZAS_NO_OPEN=1` keep it closed.
19
+ - `zas_pair` takes an optional `code` for the case where the page shows one.
20
+ - Older servers are not supported by this release: the claim route is
21
+ required. Older CLIs keep working against the server.
22
+
23
+ ## [0.1.1] - 2026-09-02
24
+
25
+ ### Changed
26
+
27
+ - Every line the agent prints and every tool result is in English. The pairing
28
+ prompt, the status report and the error sentences were in Spanish.
29
+ - The README opens with what Zas is. The README, the package keywords and the
30
+ registry entry no longer call the design end-to-end encryption: the
31
+ product's own privacy page does not, because a separate service derives
32
+ account keys.
33
+
10
34
  ## [0.1.0] - 2026-09-02
11
35
 
12
36
  ### Added
@@ -25,5 +49,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
25
49
  two separate identities that cannot read each other's keys.
26
50
  - Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
27
51
 
28
- [Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.1.0...HEAD
52
+ [Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.1.1...HEAD
53
+ [0.1.1]: https://github.com/soke1556/zas-agent/compare/v0.1.0...v0.1.1
29
54
  [0.1.0]: https://github.com/soke1556/zas-agent/releases/tag/v0.1.0
package/README.md CHANGED
@@ -10,6 +10,19 @@ read items back out of them.
10
10
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/soke1556/zas-agent/badge)](https://scorecard.dev/viewer/?uri=github.com/soke1556/zas-agent)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
 
13
+ ## What is Zas
14
+
15
+ [Zas](https://zas.red) moves things between your devices and the people you
16
+ choose: files, photos, text, code. You put something into a channel on one
17
+ device and use it on another. Whatever you do not pin leaves on its own after a
18
+ few days, so there is nothing to tidy up later. Everything is encrypted on the
19
+ device before it is uploaded: the database and the object storage receive
20
+ encrypted bytes, not filenames, content or previews.
21
+
22
+ This package is the piece that lets a coding agent use your channels the way
23
+ your other devices do, under an identity of its own that you approve, that you
24
+ scope to the channels you choose, and that you can revoke.
25
+
13
26
  ## What it does
14
27
 
15
28
  Your coding agent gets seven tools. It can send a file or a note into a channel
@@ -50,28 +63,37 @@ command = "npx"
50
63
  args = ["-y", "zas-agent", "--profile", "codex"]
51
64
  ```
52
65
 
53
- ### What pairing prints
66
+ ### What pairing does
54
67
 
55
- `zas-agent pair` mints the key pair, registers the public halves, and waits. The
56
- terminal shows four things:
68
+ `zas-agent pair` mints the key pair, registers the public halves, opens the
69
+ approval page in your browser, and waits. The terminal shows:
57
70
 
58
71
  ```
59
- Abrí esta página con tu cuenta de Zas:
60
- https://zas.red/agents/pair?p=...
61
- Código: ABCD-EFGH
62
- Huella: 1a2b 3c4d 5e6f 7a8b
63
- Esperando la aprobación… (vence en 10 minutos)
72
+ Open this page signed in to your Zas account:
73
+ https://zas.red/agents/pair?p=...#port=53211
74
+ Fingerprint: 1a2b 3c4d 5e6f 7a8b
75
+ Waiting for approval… (expires in 10 minutes)
64
76
  ```
65
77
 
66
- Open the link signed in to your Zas account. Check that the fingerprint on the
67
- page is the one in your terminal, name the agent, and tick the channels it may
68
- use sending is the default, reading is a separate switch. The code is eight
69
- characters in two groups, and it is what you compare before you approve. The
70
- pairing is good for ten minutes; past that, run the command again.
71
-
72
- You can also start the flow from the coding agent with the `zas_pair` tool: the
73
- first call hands back the URL and the code, and a later call says whether the
74
- approval landed.
78
+ Signed in, name the agent and tick the channels it may use sending is the
79
+ default, reading is a separate switch and approve. Approval creates
80
+ nothing by itself: the page hands a one-time claim code to this terminal
81
+ over `127.0.0.1` (the port in the link), and the agent exists only once the
82
+ terminal claims with that code and the secret it holds. A link that reached
83
+ somebody else is approved on their machine, where nothing listens, and
84
+ expires with nothing created.
85
+
86
+ If the browser cannot reach the terminal — the link was opened on a phone,
87
+ or the browser refused the local connection — the page shows the code and
88
+ the terminal asks for it. Type it there and nowhere else: with that code,
89
+ another terminal that started a pairing could claim it. Pass `--no-open` or
90
+ set `ZAS_NO_OPEN=1` to keep the browser closed; the link is printed either
91
+ way. The pairing is good for ten minutes before approval and five after;
92
+ past that, run the command again.
93
+
94
+ You can also start the flow from the coding agent with the `zas_pair` tool:
95
+ the first call hands back the URL, a later call says whether the approval
96
+ landed, and if the page shows a code, a call with `code` claims with it.
75
97
 
76
98
  ## Why you can trust it
77
99
 
@@ -85,9 +107,11 @@ source, the sentence says so.
85
107
  account key, and the key-derivation service refuses account-key derivation to
86
108
  an agent *(server-side)*.
87
109
  - **You approve it, and you choose the channels.** Pairing never
88
- auto-approves. The approval page shows the harness, the host, the key
89
- fingerprint and the code, and you compare the code with your terminal before
90
- anything is granted.
110
+ auto-approves. The approval page shows the harness, the host and the key
111
+ fingerprint, and nothing exists until the terminal that started the
112
+ pairing claims it with a code only the approving page received
113
+ *(server-side)*. A pairing link sent to you by someone else creates
114
+ nothing on your account.
91
115
  - **It holds one key per granted channel, and no key for any other.** Each
92
116
  grant carries that channel's key sealed to the agent's X25519 public key. A
93
117
  channel you did not grant has no key here to decrypt with, and the server
@@ -98,11 +122,12 @@ source, the sentence says so.
98
122
  - **Everything it sends is visible as its work.** Every item carries the `>_`
99
123
  agent mark and the agent's name, in the channel, on every device you read Zas
100
124
  from.
101
- - **You can revoke it at any time.** Settings → Agentes → the agent → Revocar.
125
+ - **You can revoke it at any time.** Settings → Agents → the agent → Revoke.
102
126
  The session stops, its refresh tokens are revoked *(server-side)*, and the
103
127
  next tool call answers "the owner revoked this agent". What it already sent
104
128
  stays where it is. You can also drop a single channel and keep the rest.
105
- - **Content is end-to-end encrypted, the same way the app does it.** The
129
+ - **Content is encrypted on your machine before it leaves, the same way the
130
+ app does it.** The
106
131
  chunking, the manifest and the envelope formats under `src/shared/` are the
107
132
  same modules the Zas web app ships. The server stores ciphertext and never
108
133
  sees a channel key, a channel name or item plaintext *(server-side)*.
@@ -131,7 +156,7 @@ ones marked *(server-side)* are enforced by the Zas server.
131
156
  - **Rate limited by the server**, on its own buckets, with the key-derivation
132
157
  budget charged to your account so ten agents are not ten times your own
133
158
  allowance *(server-side)*.
134
- - **Ten agents per account** *(server-side)*.
159
+ - **As many agents as your plan or your organization allows** *(server-side)*.
135
160
  - **Files up to 5 GiB**, and in practice less: the agent reads a file into
136
161
  memory to hash it, so the machine's memory is the real ceiling.
137
162
 
@@ -171,7 +196,7 @@ string ever reaches a terminal.
171
196
  | `missing_token` | The session token is missing; pair the agent again. |
172
197
  | `internal` | Something failed inside the agent. |
173
198
 
174
- Every code comes back as one sentence in Spanish and one in English, never as a
199
+ Every code comes back as one sentence in English, never as a
175
200
  stack trace.
176
201
 
177
202
  ## Tools
@@ -179,7 +204,7 @@ stack trace.
179
204
  | Tool | What it does |
180
205
  | --- | --- |
181
206
  | `zas_status` | Says whether this machine is paired with a Zas account, and lists the owner's channels this agent may send to or read from. |
182
- | `zas_pair` | Pairs this machine with a Zas account. The first call returns a URL and a code for the owner to approve; a later call says whether they did. |
207
+ | `zas_pair` | Pairs this machine with a Zas account. The first call returns a URL for the owner to open; a later call says whether they approved. If the page shows a code, a call with `code` claims with it. |
183
208
  | `zas_send_file` | Sends a file from this machine into one of the owner's channels. Returns the item id, or a job id when the upload takes longer than a minute. |
184
209
  | `zas_send_note` | Sends a note — plain text, or a code snippet with its language — into one of the owner's channels. |
185
210
  | `zas_list_items` | Lists the most recent items in one of the owner's channels. Needs a grant that includes reading. |
@@ -218,8 +243,8 @@ crash mid-write cannot leave half a file behind:
218
243
 
219
244
  - `identity.json` — the agent uid, the owner uid, the name, and the two key
220
245
  pairs. Back it up like a private key, or delete it and pair again.
221
- - `pending.json` — a pairing that has not been approved yet. Removed on
222
- approval, and on a pairing that expired or was cancelled.
246
+ - `pending.json` — a pairing that has not been claimed yet. Removed on
247
+ completion, and on a pairing that expired or was cancelled.
223
248
  - `grants.json` — a one-minute cache of `GET /v1/agents/me`: which channels,
224
249
  and the sealed key for each. The channel name stays encrypted here.
225
250
  Disposable.
@@ -233,7 +258,7 @@ profile they live in, and the package does not try to set any others.
233
258
 
234
259
  Deleting the directory makes this machine forget the agent. It does not revoke
235
260
  anything: the account side is closed from the web app, under
236
- Settings → AgentesRevocar.
261
+ Settings → AgentsRevoke.
237
262
 
238
263
  ## Configuration
239
264
 
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { createRequire as __cr } from "node:module"; const require = __cr(import
3
3
 
4
4
  // src/cli.ts
5
5
  import { realpathSync } from "node:fs";
6
+ import { createInterface } from "node:readline";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
9
 
@@ -76,6 +77,17 @@ var AGENT_TOKEN_TTL_SEC = 3600;
76
77
  var AGENT_LAST_SEEN_BUCKET_MS = 5 * 60 * 1e3;
77
78
  var AGENT_HOST_MAX = 60;
78
79
  var AGENT_CHALLENGE_DOMAIN = "ZAS-AGENT-CHALLENGE-V1";
80
+ var AGENT_PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
81
+ var AGENT_PAIRING_CODE_LENGTH = 8;
82
+ var AGENT_CLAIM_TTL_MS = 5 * 60 * 1e3;
83
+ function keepPairingCodeChars(raw) {
84
+ return raw.toUpperCase().split("").filter((c) => AGENT_PAIRING_CODE_ALPHABET.includes(c)).join("");
85
+ }
86
+ function normalizePairingCode(raw) {
87
+ if (typeof raw !== "string") return "";
88
+ const kept = keepPairingCodeChars(raw);
89
+ return kept.length === AGENT_PAIRING_CODE_LENGTH ? kept : "";
90
+ }
79
91
  var text = new TextEncoder();
80
92
  function field(bytes) {
81
93
  const length = new Uint8Array(4);
@@ -136,38 +148,43 @@ function errorFromResponse(status, body) {
136
148
  return new ZasError(code, status, raw, retryAfterMs, raw);
137
149
  }
138
150
  var SENTENCES = {
139
- identity_corrupt: { es: "El archivo de identidad del agente est\xE1 da\xF1ado: {path}. Restauralo o borralo y volv\xE9 a emparejar.", en: "The agent identity file is damaged: {path}. Restore it, or delete it and pair again." },
140
- not_paired: { es: "Este agente todav\xEDa no est\xE1 emparejado. Corr\xE9 \xABnpx -y zas-agent pair\xBB en la terminal.", en: "This agent is not paired yet. Run \u201Cnpx -y zas-agent pair\u201D in the terminal." },
141
- internal: { es: "Algo fall\xF3 dentro del agente. Prob\xE1 de nuevo.", en: "Something failed inside the agent. Try again." },
142
- agent_forbidden: { es: "Este agente no puede hacer eso; solo el due\xF1o de la cuenta.", en: "This agent cannot do that; only the account owner can." },
143
- agent_revoked: { es: "El due\xF1o revoc\xF3 este agente. Volv\xE9 a emparejar con \xABzas-agent pair\xBB.", en: "The owner revoked this agent. Pair again with \u201Czas-agent pair\u201D." },
144
- grant_missing: { es: "Este agente no tiene acceso a ese canal. El due\xF1o lo agrega desde Ajustes \u2192 Agentes.", en: "This agent has no access to that channel. The owner adds it from Settings \u2192 Agents." },
145
- not_found: { es: "Ese \xEDtem no est\xE1 en el canal.", en: "That item is not in the channel." },
146
- invalid_cap: { es: "Ese archivo ya no est\xE1 disponible.", en: "That file is no longer available." },
147
- write_failed: { es: "No se pudo guardar el archivo en el destino.", en: "The file could not be saved to the destination." },
148
- send_forbidden: { es: "Este agente no puede enviar a ese canal.", en: "This agent cannot send to that channel." },
149
- read_forbidden: { es: "Este agente no puede leer ese canal.", en: "This agent cannot read that channel." },
150
- direct_mode: { es: "Ese canal est\xE1 en modo Directo. Us\xE1 zas_send_direct.", en: "That channel is in Directo mode. Use zas_send_direct." },
151
- not_direct_mode: { es: "Ese canal no est\xE1 en modo Directo. Us\xE1 zas_send_file.", en: "That channel is not in Directo mode. Use zas_send_file." },
152
- key_stale: { es: "La clave del canal cambi\xF3. El due\xF1o la renueva al abrir Zas.", en: "The channel key changed. The owner refreshes it by opening Zas." },
153
- quota_exceeded: { es: "La cuenta lleg\xF3 a su l\xEDmite de almacenamiento.", en: "The account reached its storage limit." },
154
- rate_limited: { es: "Demasiados env\xEDos seguidos. Esper\xE1 un momento.", en: "Too many sends in a row. Wait a moment." },
155
- file_too_big: { es: "El archivo supera el m\xE1ximo del plan.", en: "The file is over the plan limit." },
156
- duplicate: { es: "Ese \xEDtem ya est\xE1 en el canal.", en: "That item is already in the channel." },
157
- pairing_expired: { es: "El emparejamiento venci\xF3. Corr\xE9 \xABzas-agent pair\xBB de nuevo.", en: "The pairing expired. Run \u201Czas-agent pair\u201D again." },
158
- pairing_cancelled: { es: "El due\xF1o cancel\xF3 el emparejamiento.", en: "The owner cancelled the pairing." },
159
- feature_disabled: { es: "Los agentes todav\xEDa no est\xE1n habilitados para esta cuenta.", en: "Agents are not enabled for this account yet." },
160
- upload_failed: { es: "No se pudo subir el archivo. Prob\xE1 de nuevo.", en: "The upload failed. Try again." },
161
- oprf_failed: { es: "Zas no respondi\xF3 bien al preparar el archivo. Prob\xE1 de nuevo.", en: "Zas did not answer correctly while preparing the file. Try again." },
162
- network: { es: "No hay conexi\xF3n con Zas. Prob\xE1 de nuevo en un momento.", en: "Zas cannot be reached. Try again in a moment." },
163
- sign_in_failed: { es: "Zas no acept\xF3 la sesi\xF3n de este agente. Prob\xE1 de nuevo o volv\xE9 a emparejar.", en: "Zas did not accept this agent session. Try again, or pair again." },
164
- bad_signature: { es: "Zas rechaz\xF3 la firma de este agente. Emparejalo de nuevo.", en: "Zas rejected this agent's signature. Pair it again." },
165
- missing_token: { es: "Falta el token de sesi\xF3n. Emparej\xE1 el agente de nuevo.", en: "The session token is missing. Pair the agent again." }
151
+ identity_corrupt: "The agent identity file is damaged: {path}. Restore it, or delete it and pair again.",
152
+ not_paired: "This agent is not paired yet. Run \u201Cnpx -y zas-agent pair\u201D in the terminal.",
153
+ internal: "Something failed inside the agent. Try again.",
154
+ agent_forbidden: "This agent cannot do that; only the account owner can.",
155
+ agent_revoked: "The owner revoked this agent. Pair again with \u201Czas-agent pair\u201D.",
156
+ grant_missing: "This agent has no access to that channel. The owner adds it from Settings \u2192 Agents.",
157
+ not_found: "That item is not in the channel.",
158
+ invalid_cap: "That file is no longer available.",
159
+ write_failed: "The file could not be saved to the destination.",
160
+ send_forbidden: "This agent cannot send to that channel.",
161
+ read_forbidden: "This agent cannot read that channel.",
162
+ direct_mode: "That channel is in Directo mode. Use zas_send_direct.",
163
+ not_direct_mode: "That channel is not in Directo mode. Use zas_send_file.",
164
+ key_stale: "The channel key changed. The owner refreshes it by opening Zas.",
165
+ quota_exceeded: "The account reached its storage limit.",
166
+ rate_limited: "Too many sends in a row. Wait a moment.",
167
+ file_too_big: "The file is over the plan limit.",
168
+ duplicate: "That item is already in the channel.",
169
+ pairing_expired: "The pairing expired. Run \u201Czas-agent pair\u201D again.",
170
+ pairing_cancelled: "The owner cancelled the pairing.",
171
+ claim_mismatch: "The code does not match. Try again.",
172
+ pairing_not_approved: "Nobody has approved this pairing yet.",
173
+ pairing_claimed: "This pairing was already claimed. Pair again with \u201Czas-agent pair\u201D.",
174
+ agent_limit: "The account cannot take another agent. The owner deletes one from Settings \u2192 Agents.",
175
+ grant_limit: "The plan allows fewer channels per agent than this pairing grants.",
176
+ feature_disabled: "Agents are not enabled for this account yet.",
177
+ upload_failed: "The upload failed. Try again.",
178
+ oprf_failed: "Zas did not answer correctly while preparing the file. Try again.",
179
+ network: "Zas cannot be reached. Try again in a moment.",
180
+ sign_in_failed: "Zas did not accept this agent session. Try again, or pair again.",
181
+ bad_signature: "Zas rejected this agent's signature. Pair it again.",
182
+ missing_token: "The session token is missing. Pair the agent again."
166
183
  };
167
- function humanSentence(error, locale = "es") {
168
- const sentence = SENTENCES[error.code]?.[locale];
184
+ function humanSentence(error) {
185
+ const sentence = SENTENCES[error.code];
169
186
  if (sentence) return sentence.replace("{path}", () => error.message);
170
- return locale === "es" ? `Zas respondi\xF3 ${error.code} (${error.status}).` : `Zas answered ${error.code} (${error.status}).`;
187
+ return `Zas answered ${error.code} (${error.status}).`;
171
188
  }
172
189
 
173
190
  // src/identity.ts
@@ -185,7 +202,7 @@ function agentHome() {
185
202
  }
186
203
  var PROFILE_RE = /^(?!\.)[A-Za-z0-9._-]{1,64}$/;
187
204
  function profileDir(profile) {
188
- if (!PROFILE_RE.test(profile)) throw new ZasError("internal", 0, `Perfil inv\xE1lido: ${profile}`);
205
+ if (!PROFILE_RE.test(profile)) throw new ZasError("internal", 0, `Invalid profile: ${profile}`);
189
206
  return join(agentHome(), profile);
190
207
  }
191
208
  function ensureDir(dir) {
@@ -278,12 +295,138 @@ function defaultEndpoints() {
278
295
  };
279
296
  }
280
297
 
298
+ // src/open.ts
299
+ import { spawn } from "node:child_process";
300
+ function openInBrowser(url, platform = process.platform) {
301
+ try {
302
+ const [command, args] = platform === "win32" ? ["cmd", ["/c", "start", "", url]] : platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
303
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
304
+ child.on("error", () => void 0);
305
+ child.unref();
306
+ } catch {
307
+ }
308
+ }
309
+
281
310
  // src/pair.ts
282
311
  import { hostname } from "node:os";
283
312
 
313
+ // src/claim-listener.ts
314
+ import { createServer } from "node:http";
315
+ var BODY_MAX = 1024;
316
+ function readBody(req) {
317
+ return new Promise((resolve2) => {
318
+ let size = 0;
319
+ let resolved = false;
320
+ const chunks = [];
321
+ req.on("data", (chunk) => {
322
+ size += chunk.length;
323
+ if (size > BODY_MAX) {
324
+ if (!resolved) {
325
+ resolved = true;
326
+ resolve2(null);
327
+ }
328
+ return;
329
+ }
330
+ chunks.push(chunk);
331
+ });
332
+ req.on("end", () => {
333
+ if (!resolved) {
334
+ resolved = true;
335
+ resolve2(Buffer.concat(chunks).toString("utf8"));
336
+ }
337
+ });
338
+ req.on("error", () => {
339
+ if (!resolved) {
340
+ resolved = true;
341
+ resolve2(null);
342
+ }
343
+ });
344
+ });
345
+ }
346
+ async function startClaimListener(opts) {
347
+ let taken = false;
348
+ const server = createServer(async (req, res) => {
349
+ const origin = String(req.headers.origin ?? "");
350
+ const json = (status, body2) => {
351
+ res.writeHead(status, { "Content-Type": "application/json" });
352
+ res.end(JSON.stringify(body2));
353
+ };
354
+ if (req.url !== "/claim") {
355
+ json(404, { error: "not_found" });
356
+ return;
357
+ }
358
+ if (req.method === "OPTIONS") {
359
+ if (origin !== opts.webOrigin) {
360
+ res.writeHead(403);
361
+ res.end();
362
+ return;
363
+ }
364
+ res.writeHead(204, {
365
+ "Access-Control-Allow-Origin": origin,
366
+ "Access-Control-Allow-Methods": "POST",
367
+ "Access-Control-Allow-Headers": "content-type",
368
+ "Access-Control-Allow-Private-Network": "true",
369
+ "Access-Control-Max-Age": "300",
370
+ Vary: "Origin"
371
+ });
372
+ res.end();
373
+ return;
374
+ }
375
+ if (req.method !== "POST") {
376
+ json(404, { error: "not_found" });
377
+ return;
378
+ }
379
+ if (origin !== opts.webOrigin) {
380
+ json(403, { error: "forbidden" });
381
+ return;
382
+ }
383
+ res.setHeader("Access-Control-Allow-Origin", origin);
384
+ res.setHeader("Vary", "Origin");
385
+ if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
386
+ json(400, { error: "bad_request" });
387
+ return;
388
+ }
389
+ const raw = await readBody(req);
390
+ let body = {};
391
+ try {
392
+ body = raw === null ? {} : JSON.parse(raw);
393
+ } catch {
394
+ body = {};
395
+ }
396
+ if (typeof body !== "object" || body === null || body.pairing_id !== opts.pairingId || typeof body.code !== "string") {
397
+ json(400, { error: "bad_request" });
398
+ return;
399
+ }
400
+ if (taken) {
401
+ json(409, { error: "claim_taken" });
402
+ return;
403
+ }
404
+ taken = true;
405
+ const outcome = await opts.onCode(body.code);
406
+ if ("error" in outcome) json(outcome.status, { error: outcome.error });
407
+ else json(200, { status: "claimed" });
408
+ });
409
+ await new Promise((resolve2, reject) => {
410
+ server.once("error", reject);
411
+ server.listen(0, "127.0.0.1", () => {
412
+ server.off("error", reject);
413
+ resolve2();
414
+ });
415
+ });
416
+ const address = server.address();
417
+ return {
418
+ host: address.address,
419
+ port: address.port,
420
+ close: () => new Promise((resolve2) => {
421
+ server.closeAllConnections();
422
+ server.close(() => resolve2());
423
+ })
424
+ };
425
+ }
426
+
284
427
  // src/client.ts
285
428
  var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
286
- async function readBody(res) {
429
+ async function readBody2(res) {
287
430
  const text2 = await res.text();
288
431
  if (!text2) return {};
289
432
  try {
@@ -302,7 +445,7 @@ function jsonInit(method, body, headers) {
302
445
  async function apiPublic(base, method, path, body, headers = {}, fetchImpl) {
303
446
  const call = fetchImpl ?? ((input, init) => fetch(input, init));
304
447
  const res = await call(`${base}${path}`, jsonInit(method, body, headers));
305
- const parsed = res.status === 204 ? void 0 : await readBody(res);
448
+ const parsed = res.status === 204 ? void 0 : await readBody2(res);
306
449
  if (!res.ok) throw errorFromResponse(res.status, parsed);
307
450
  return parsed;
308
451
  }
@@ -368,7 +511,7 @@ var ZasClient = class _ZasClient {
368
511
  }
369
512
  async api(method, path, body, headers = {}) {
370
513
  const res = await this.authedRetrying(`${this.identity.api_base}/v1${path}`, method, body, headers);
371
- const parsed = res.status === 204 ? void 0 : await readBody(res);
514
+ const parsed = res.status === 204 ? void 0 : await readBody2(res);
372
515
  if (!res.ok) throw errorFromResponse(res.status, parsed);
373
516
  return parsed;
374
517
  }
@@ -391,7 +534,7 @@ var ZasClient = class _ZasClient {
391
534
  }
392
535
  async oprfEvaluate(blinded) {
393
536
  const res = await this.authedRetrying(`${this.identity.oprf_base}/evaluate`, "POST", { blinded }, {});
394
- const parsed = await readBody(res);
537
+ const parsed = await readBody2(res);
395
538
  if (!res.ok) throw errorFromResponse(res.status, parsed);
396
539
  return parsed.evaluated ?? [];
397
540
  }
@@ -402,7 +545,7 @@ var ZasClient = class _ZasClient {
402
545
  const base = _ZasClient.firestoreBase(this.identity.firestore_project);
403
546
  const url = `${base}${parentPath ? `/${parentPath}` : ""}:runQuery`;
404
547
  const res = await this.authed(url, "POST", { structuredQuery: query }, {});
405
- const parsed = await readBody(res);
548
+ const parsed = await readBody2(res);
406
549
  if (!res.ok) throw errorFromResponse(res.status, parsed);
407
550
  if (!Array.isArray(parsed)) return [];
408
551
  return parsed.filter((row) => !!row && typeof row === "object" && "document" in row).map((row) => row.document);
@@ -440,20 +583,30 @@ function kindForProfile(profile) {
440
583
 
441
584
  // src/pair.ts
442
585
  var POLL_INTERVAL_MS = 2e3;
443
- function pairUrl(webBase, pairingId) {
444
- return `${webBase}/agents/pair?p=${pairingId}`;
586
+ var NO_TTY_LINE = "This terminal cannot take a code. If the page shows one, run zas-agent pair again from an interactive terminal.";
587
+ var CODE_SHAPE_LINE = "The code is eight letters and digits, like ABCD-EFGH.";
588
+ function pairUrl(webBase, pairingId, port) {
589
+ const link = `${webBase}/agents/pair?p=${pairingId}`;
590
+ return port ? `${link}#port=${port}` : link;
445
591
  }
446
- function formatCode(code) {
447
- return `${code.slice(0, 4)}-${code.slice(4)}`;
592
+ function webOriginOf(webBase) {
593
+ return new URL(webBase).origin;
448
594
  }
449
595
  var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
596
+ function deferred() {
597
+ let resolve2;
598
+ const promise = new Promise((r) => {
599
+ resolve2 = r;
600
+ });
601
+ return { promise, resolve: resolve2 };
602
+ }
450
603
  async function runPair(opts) {
451
604
  const sleep2 = opts.sleep ?? defaultSleep;
452
605
  const now = opts.now ?? (() => Date.now());
453
606
  const existing = loadIdentity(opts.profile);
454
607
  if (existing) {
455
608
  opts.log(
456
- `Este perfil ya est\xE1 emparejado como \xAB${existing.name}\xBB. Se va a crear un agente nuevo; revoc\xE1 el anterior desde Ajustes \u2192 Agentes.`
609
+ `This profile is already paired as \u201C${existing.name}\u201D. A new agent will be created; revoke the old one from Settings \u2192 Agents.`
457
610
  );
458
611
  }
459
612
  const host = (opts.host ?? hostname()).slice(0, AGENT_HOST_MAX);
@@ -463,67 +616,152 @@ async function runPair(opts) {
463
616
  opts.apiBase,
464
617
  "POST",
465
618
  "/v1/agents/pairings",
466
- { kind: opts.kind, host, x25519_public: keys.x25519_public, p256_public: keys.p256_public },
619
+ { kind: opts.kind, host, x25519_public: keys.x25519_public, p256_public: keys.p256_public, claim: true },
467
620
  {},
468
621
  opts.fetch
469
622
  );
470
- const pending = {
471
- version: 1,
472
- profile: opts.profile,
473
- pairing_id: created.pairing_id,
474
- poll_secret: created.poll_secret,
475
- code: created.code,
476
- fingerprint: created.fingerprint,
477
- expires_at: created.expires_at,
478
- kind: opts.kind,
479
- host,
480
- ...keys,
481
- ...endpoints,
482
- created_at: now()
623
+ let claimed = null;
624
+ let inFlight = null;
625
+ const wake = deferred();
626
+ const claimOnce = (code) => {
627
+ if (claimed) return Promise.resolve({ error: "pairing_claimed", status: 409 });
628
+ if (inFlight) return inFlight;
629
+ inFlight = (async () => {
630
+ try {
631
+ const answer = await apiPublic(
632
+ opts.apiBase,
633
+ "POST",
634
+ `/v1/agents/pairings/${created.pairing_id}/claim`,
635
+ { code },
636
+ { "X-Zas-Poll-Secret": created.poll_secret },
637
+ opts.fetch
638
+ );
639
+ claimed = { agent_uid: answer.agent_uid, owner_uid: answer.owner_uid };
640
+ wake.resolve();
641
+ return { status: "claimed" };
642
+ } catch (err) {
643
+ if (err instanceof ZasError) return { error: err.code, status: err.status };
644
+ return { error: "network", status: 502 };
645
+ } finally {
646
+ inFlight = null;
647
+ }
648
+ })();
649
+ return inFlight;
483
650
  };
484
- savePending(opts.profile, pending);
485
- const minutesLeft = Math.max(0, Math.round((created.expires_at - now()) / 6e4));
486
- opts.log(
487
- [
488
- "Abr\xED esta p\xE1gina con tu cuenta de Zas:",
489
- ` ${pairUrl(opts.webBase, created.pairing_id)}`,
490
- `C\xF3digo: ${formatCode(created.code)}`,
491
- `Huella: ${agentFingerprintShort(created.fingerprint)}`,
492
- `Esperando la aprobaci\xF3n\u2026 (vence en ${minutesLeft} minutos)`
493
- ].join("\n")
494
- );
495
- let approved = null;
496
- for (; ; ) {
497
- let answer;
651
+ let listener = null;
652
+ if (opts.listen !== false) {
498
653
  try {
499
- answer = await apiPublic(
500
- opts.apiBase,
501
- "POST",
502
- `/v1/agents/pairings/${created.pairing_id}/poll`,
503
- void 0,
504
- { "X-Zas-Poll-Secret": created.poll_secret },
505
- opts.fetch
506
- );
507
- } catch (err) {
508
- if (!(err instanceof ZasError) || err.code !== "rate_limited") throw err;
509
- if (now() >= created.expires_at) throw new ZasError("pairing_expired", 410);
510
- await sleep2(err.retryAfterMs ?? 1e4);
511
- continue;
512
- }
513
- if (answer.status === "approved") {
514
- approved = { agent_uid: answer.agent_uid, owner_uid: answer.owner_uid };
515
- break;
654
+ listener = await startClaimListener({
655
+ webOrigin: webOriginOf(opts.webBase),
656
+ pairingId: created.pairing_id,
657
+ onCode: (code) => claimOnce(normalizePairingCode(code) || code)
658
+ });
659
+ } catch {
660
+ listener = null;
516
661
  }
517
- if (answer.status === "expired" || answer.status === "cancelled") {
518
- clearPending(opts.profile);
519
- throw new ZasError(`pairing_${answer.status}`, 410);
662
+ }
663
+ let askFailed = null;
664
+ try {
665
+ const port = listener?.port;
666
+ const link = pairUrl(opts.webBase, created.pairing_id, port);
667
+ const pending = {
668
+ version: 1,
669
+ profile: opts.profile,
670
+ pairing_id: created.pairing_id,
671
+ poll_secret: created.poll_secret,
672
+ fingerprint: created.fingerprint,
673
+ expires_at: created.expires_at,
674
+ kind: opts.kind,
675
+ host,
676
+ ...keys,
677
+ ...endpoints,
678
+ created_at: now(),
679
+ protocol: 2,
680
+ ...port ? { port } : {}
681
+ };
682
+ savePending(opts.profile, pending);
683
+ const minutesLeft = Math.max(0, Math.round((created.expires_at - now()) / 6e4));
684
+ opts.log(
685
+ [
686
+ "Open this page signed in to your Zas account:",
687
+ ` ${link}`,
688
+ `Fingerprint: ${agentFingerprintShort(created.fingerprint)}`,
689
+ `Waiting for approval\u2026 (expires in ${minutesLeft} minutes)`
690
+ ].join("\n")
691
+ );
692
+ opts.open?.(link);
693
+ let asking = false;
694
+ let toldNoTty = false;
695
+ const askLoop = async () => {
696
+ try {
697
+ while (!claimed) {
698
+ const code = normalizePairingCode(await opts.askCode());
699
+ if (!code) {
700
+ opts.log(CODE_SHAPE_LINE);
701
+ continue;
702
+ }
703
+ const outcome = await claimOnce(code);
704
+ if (!("error" in outcome) || outcome.error === "pairing_claimed") return;
705
+ if (outcome.error === "claim_mismatch") {
706
+ opts.log(humanSentence(new ZasError("claim_mismatch", 403)));
707
+ continue;
708
+ }
709
+ throw new ZasError(outcome.error, outcome.status);
710
+ }
711
+ } catch (e) {
712
+ askFailed = e;
713
+ wake.resolve();
714
+ }
715
+ };
716
+ for (; ; ) {
717
+ if (claimed) break;
718
+ if (askFailed) throw askFailed;
719
+ let answer;
720
+ try {
721
+ answer = await apiPublic(
722
+ opts.apiBase,
723
+ "POST",
724
+ `/v1/agents/pairings/${created.pairing_id}/poll`,
725
+ void 0,
726
+ { "X-Zas-Poll-Secret": created.poll_secret },
727
+ opts.fetch
728
+ );
729
+ } catch (err) {
730
+ if (!(err instanceof ZasError) || err.code !== "rate_limited") throw err;
731
+ if (now() >= created.expires_at) throw new ZasError("pairing_expired", 410);
732
+ await Promise.race([sleep2(err.retryAfterMs ?? 1e4), wake.promise]);
733
+ continue;
734
+ }
735
+ if (claimed) break;
736
+ if (answer.status === "claimed") {
737
+ claimed = { agent_uid: answer.agent_uid, owner_uid: answer.owner_uid };
738
+ break;
739
+ }
740
+ if (answer.status === "expired" || answer.status === "cancelled") {
741
+ clearPending(opts.profile);
742
+ throw new ZasError(`pairing_${answer.status}`, 410);
743
+ }
744
+ if (answer.status === "approved") {
745
+ if (opts.askCode && !asking) {
746
+ asking = true;
747
+ void askLoop();
748
+ }
749
+ if (!opts.askCode && !toldNoTty) {
750
+ toldNoTty = true;
751
+ opts.log(NO_TTY_LINE);
752
+ }
753
+ }
754
+ await Promise.race([sleep2(POLL_INTERVAL_MS), wake.promise]);
520
755
  }
521
- await sleep2(POLL_INTERVAL_MS);
756
+ } finally {
757
+ await listener?.close();
522
758
  }
759
+ if (askFailed) throw askFailed;
760
+ if (!claimed) throw new ZasError("pairing_expired", 410);
523
761
  const baseIdentity = {
524
762
  version: 1,
525
- agent_uid: approved.agent_uid,
526
- owner_uid: approved.owner_uid,
763
+ agent_uid: claimed.agent_uid,
764
+ owner_uid: claimed.owner_uid,
527
765
  name: "",
528
766
  kind: opts.kind,
529
767
  host,
@@ -538,10 +776,10 @@ async function runPair(opts) {
538
776
  clearPending(opts.profile);
539
777
  const claudeLines = (lead) => [lead, ` ${claudeSnippet(opts.profile)}`];
540
778
  const codexLines = (lead) => [lead, ` ${codexSnippet(opts.profile)}`];
541
- const install = opts.kind === "claude_code" ? claudeLines("Agregalo a Claude Code:") : opts.kind === "codex" ? codexLines("Agregalo a Codex:") : [...claudeLines("Agregalo a Claude Code:"), ...codexLines("O a Codex:")];
779
+ const install = opts.kind === "claude_code" ? claudeLines("Add it to Claude Code:") : opts.kind === "codex" ? codexLines("Add it to Codex:") : [...claudeLines("Add it to Claude Code:"), ...codexLines("Or to Codex:")];
542
780
  opts.log(
543
781
  [
544
- `Listo: el agente \xAB${identity.name}\xBB qued\xF3 emparejado con tu cuenta.`,
782
+ `Done: the agent \u201C${identity.name}\u201D is paired with your account.`,
545
783
  ...install
546
784
  ].join("\n")
547
785
  );
@@ -1631,7 +1869,7 @@ async function sendNote(ctx, input) {
1631
1869
 
1632
1870
  // src/server.ts
1633
1871
  function agentVersion() {
1634
- return true ? "0.1.0" : "0.0.0-dev";
1872
+ return true ? "0.2.0" : "0.0.0-dev";
1635
1873
  }
1636
1874
  var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
1637
1875
  var PAIR_ANNOUNCE_MS = 15e3;
@@ -1641,9 +1879,9 @@ var delay = (ms) => new Promise((resolve2) => {
1641
1879
  });
1642
1880
  function rightsOf(grant) {
1643
1881
  const rights = [];
1644
- if (grant.send && grant.mode !== "view" && !grant.direct_mode) rights.push("env\xEDa");
1645
- if (grant.read) rights.push("lee");
1646
- return rights.length > 0 ? rights.join(" \xB7 ") : "sin permisos";
1882
+ if (grant.send && grant.mode !== "view" && !grant.direct_mode) rights.push("send");
1883
+ if (grant.read) rights.push("read");
1884
+ return rights.length > 0 ? rights.join(" \xB7 ") : "no access";
1647
1885
  }
1648
1886
  function buildServer(profile, deps = {}) {
1649
1887
  const server = new McpServer({ name: "zas", version: agentVersion() });
@@ -1664,7 +1902,7 @@ function buildServer(profile, deps = {}) {
1664
1902
  });
1665
1903
  const failed = (e) => {
1666
1904
  const err = e instanceof ZasError ? e : new ZasError("internal", 0, String(e));
1667
- return { isError: true, ...text2(`${err.code}: ${humanSentence(err, "es")} / ${humanSentence(err, "en")}`) };
1905
+ return { isError: true, ...text2(`${err.code}: ${humanSentence(err)}`) };
1668
1906
  };
1669
1907
  const settled = (job) => {
1670
1908
  if (job.status === "done") return text2(job.result);
@@ -1679,7 +1917,7 @@ function buildServer(profile, deps = {}) {
1679
1917
  }, async () => {
1680
1918
  try {
1681
1919
  if (!(deps.identity ?? loadIdentity(profile))) {
1682
- return text2(humanSentence(new ZasError("not_paired", 0), "es"));
1920
+ return text2(humanSentence(new ZasError("not_paired", 0)));
1683
1921
  }
1684
1922
  const c = ctx();
1685
1923
  const grants = await grantsFor(c.client, profile);
@@ -1692,10 +1930,10 @@ function buildServer(profile, deps = {}) {
1692
1930
  return ` ${name} \xB7 ${rightsOf(grant)}`;
1693
1931
  });
1694
1932
  return text2([
1695
- `Emparejado como \xAB${c.identity.name}\xBB (${c.identity.kind}) con la cuenta ${c.identity.owner_uid}.`,
1696
- grants.length > 0 ? "Canales:" : "Sin canales: el due\xF1o todav\xEDa no le dio acceso a ninguno.",
1933
+ `Paired as \u201C${c.identity.name}\u201D (${c.identity.kind}) with account ${c.identity.owner_uid}.`,
1934
+ grants.length > 0 ? "Channels:" : "No channels: the owner has not granted access to any yet.",
1697
1935
  ...lines,
1698
- `${packageName()} ${agentVersion()} \xB7 perfil ${profile}`
1936
+ `${packageName()} ${agentVersion()} \xB7 profile ${profile}`
1699
1937
  ].join("\n"));
1700
1938
  } catch (e) {
1701
1939
  return failed(e);
@@ -1703,10 +1941,14 @@ function buildServer(profile, deps = {}) {
1703
1941
  });
1704
1942
  let pairing = null;
1705
1943
  server.registerTool("zas_pair", {
1706
- description: "Pair this machine with a Zas account. The first call returns a URL and a code for the owner to approve; a later call says whether they did." + AGENT_CUE
1707
- }, async () => {
1944
+ description: "Pair this machine with a Zas account. The first call returns a URL for the owner to open; a later call says whether they approved. If the page shows a code, call again with `code`." + AGENT_CUE,
1945
+ inputSchema: {
1946
+ code: z.string().optional().describe("The code the pairing page shows when the browser could not reach this machine.")
1947
+ }
1948
+ }, async (input) => {
1708
1949
  if (!pairing) {
1709
- const state = { logs: [], status: "running" };
1950
+ if (input.code !== void 0) return failed(new ZasError("pairing_not_approved", 409));
1951
+ const state = { logs: [], status: "running", ask: null };
1710
1952
  pairing = state;
1711
1953
  (deps.runPair ?? runPair)({
1712
1954
  profile,
@@ -1717,7 +1959,13 @@ function buildServer(profile, deps = {}) {
1717
1959
  apiBase: defaultEndpoints().api_base,
1718
1960
  log: (line) => {
1719
1961
  state.logs.push(line);
1720
- }
1962
+ },
1963
+ // The person cannot type into this process; the coding agent relays
1964
+ // the code as an argument, and this promise is what it answers.
1965
+ askCode: () => new Promise((resolve2) => {
1966
+ state.ask = resolve2;
1967
+ }),
1968
+ ...process.env.ZAS_NO_OPEN === "1" ? {} : { open: openInBrowser }
1721
1969
  }).then(
1722
1970
  (identity) => {
1723
1971
  state.status = "paired";
@@ -1736,7 +1984,7 @@ function buildServer(profile, deps = {}) {
1736
1984
  pairing = null;
1737
1985
  return failed(state.error);
1738
1986
  }
1739
- return text2(state.logs.join("\n") || "Abriendo el emparejamiento\u2026");
1987
+ return text2(state.logs.join("\n") || "Opening the pairing\u2026");
1740
1988
  }
1741
1989
  if (pairing.status === "paired") return text2(`paired as ${pairing.identity?.name ?? ""}`.trim());
1742
1990
  if (pairing.status === "failed") {
@@ -1744,6 +1992,25 @@ function buildServer(profile, deps = {}) {
1744
1992
  pairing = null;
1745
1993
  return failed(error);
1746
1994
  }
1995
+ if (input.code !== void 0) {
1996
+ const state = pairing;
1997
+ const ask = state.ask;
1998
+ if (!ask) return failed(new ZasError("pairing_not_approved", 409));
1999
+ state.ask = null;
2000
+ ask(normalizePairingCode(input.code) || input.code);
2001
+ const until = Date.now() + (deps.announceMs ?? PAIR_ANNOUNCE_MS);
2002
+ while (state.status === "running" && state.ask === null && Date.now() < until) {
2003
+ await delay(10);
2004
+ }
2005
+ if (state.status === "paired") return text2(`paired as ${state.identity?.name ?? ""}`.trim());
2006
+ if (state.status === "failed") {
2007
+ const error = state.error;
2008
+ pairing = null;
2009
+ return failed(error);
2010
+ }
2011
+ if (state.ask !== null) return failed(new ZasError("claim_mismatch", 403));
2012
+ return text2(["pending", ...state.logs].join("\n"));
2013
+ }
1747
2014
  return text2(["pending", ...pairing.logs].join("\n"));
1748
2015
  });
1749
2016
  server.registerTool("zas_send_file", {
@@ -1840,13 +2107,14 @@ function parseArgs(argv) {
1840
2107
  else if (name === "--profile") {
1841
2108
  parsed.profile = take() || DEFAULT_PROFILE;
1842
2109
  if (!PROFILE_RE.test(parsed.profile)) {
1843
- return { ...parsed, command: "invalid", message: `Perfil inv\xE1lido: ${parsed.profile}` };
2110
+ return { ...parsed, command: "invalid", message: `Invalid profile: ${parsed.profile}` };
1844
2111
  }
1845
2112
  } else if (name === "--kind") {
1846
2113
  const kind = take();
1847
2114
  if (!isAgentKind(kind)) return { ...parsed, command: "help", unknown: `--kind ${kind}` };
1848
2115
  parsed.kind = kind;
1849
- } else if (name === "--host") parsed.host = take() || void 0;
2116
+ } else if (name === "--no-open") parsed.noOpen = true;
2117
+ else if (name === "--host") parsed.host = take() || void 0;
1850
2118
  else return { ...parsed, command: "help", unknown: arg };
1851
2119
  }
1852
2120
  return parsed;
@@ -1856,7 +2124,7 @@ var USAGE = [
1856
2124
  "",
1857
2125
  " zas-agent [--profile <name>] serve the MCP tools over stdio",
1858
2126
  " zas-agent pair [--profile <name>] pair this machine with a Zas account",
1859
- " [--kind claude_code|codex|other] [--host <name>]",
2127
+ " [--kind claude_code|codex|other] [--host <name>] [--no-open]",
1860
2128
  " zas-agent --version"
1861
2129
  ].join("\n");
1862
2130
  async function main(argv, log = (l) => console.error(l)) {
@@ -1870,11 +2138,19 @@ async function main(argv, log = (l) => console.error(l)) {
1870
2138
  return 2;
1871
2139
  }
1872
2140
  if (args.command === "help") {
1873
- if (args.unknown) log(`No entiendo \xAB${args.unknown}\xBB.`);
2141
+ if (args.unknown) log(`Unknown argument \u201C${args.unknown}\u201D.`);
1874
2142
  log(USAGE);
1875
2143
  return args.unknown ? 2 : 0;
1876
2144
  }
1877
2145
  if (args.command === "pair") {
2146
+ let rl = null;
2147
+ const askCode = process.stdin.isTTY ? () => {
2148
+ rl ??= createInterface({ input: process.stdin, output: process.stderr });
2149
+ return new Promise((resolve2) => {
2150
+ rl.question("Type the code shown in the browser: ", resolve2);
2151
+ });
2152
+ } : void 0;
2153
+ const noOpen = args.noOpen === true || process.env.ZAS_NO_OPEN === "1";
1878
2154
  try {
1879
2155
  await runPair({
1880
2156
  profile: args.profile,
@@ -1882,12 +2158,16 @@ async function main(argv, log = (l) => console.error(l)) {
1882
2158
  ...args.host !== void 0 ? { host: args.host } : {},
1883
2159
  webBase: process.env.ZAS_WEB_BASE || "https://zas.red",
1884
2160
  apiBase: defaultEndpoints().api_base,
1885
- log
2161
+ log,
2162
+ ...askCode ? { askCode } : {},
2163
+ ...noOpen ? {} : { open: openInBrowser }
1886
2164
  });
1887
2165
  } catch (e) {
1888
2166
  const err = e instanceof ZasError ? e : new ZasError("internal", 0, String(e));
1889
- log(humanSentence(err, "es"));
2167
+ log(humanSentence(err));
1890
2168
  return 1;
2169
+ } finally {
2170
+ rl?.close();
1891
2171
  }
1892
2172
  return 0;
1893
2173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zas-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server that lets a coding agent send files and notes into your Zas channels.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "claude-code",
19
19
  "codex",
20
20
  "zas",
21
- "end-to-end-encryption"
21
+ "encryption"
22
22
  ],
23
23
  "mcpName": "io.github.soke1556/zas-agent",
24
24
  "bin": {