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.
- package/CHANGELOG.md +26 -1
- package/README.md +53 -28
- package/dist/cli.js +394 -114
- 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.
|
|
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
|
[](https://scorecard.dev/viewer/?uri=github.com/soke1556/zas-agent)
|
|
11
11
|
[](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
|
|
66
|
+
### What pairing does
|
|
54
67
|
|
|
55
|
-
`zas-agent pair` mints the key pair, registers the public halves,
|
|
56
|
-
terminal shows
|
|
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
|
-
|
|
60
|
-
https://zas.red/agents/pair?p
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
89
|
-
fingerprint and
|
|
90
|
-
|
|
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 →
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
222
|
-
|
|
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 →
|
|
261
|
+
Settings → Agents → Revoke.
|
|
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:
|
|
140
|
-
not_paired:
|
|
141
|
-
internal:
|
|
142
|
-
agent_forbidden:
|
|
143
|
-
agent_revoked:
|
|
144
|
-
grant_missing:
|
|
145
|
-
not_found:
|
|
146
|
-
invalid_cap:
|
|
147
|
-
write_failed:
|
|
148
|
-
send_forbidden:
|
|
149
|
-
read_forbidden:
|
|
150
|
-
direct_mode:
|
|
151
|
-
not_direct_mode:
|
|
152
|
-
key_stale:
|
|
153
|
-
quota_exceeded:
|
|
154
|
-
rate_limited:
|
|
155
|
-
file_too_big:
|
|
156
|
-
duplicate:
|
|
157
|
-
pairing_expired:
|
|
158
|
-
pairing_cancelled:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
168
|
-
const sentence = SENTENCES[error.code]
|
|
184
|
+
function humanSentence(error) {
|
|
185
|
+
const sentence = SENTENCES[error.code];
|
|
169
186
|
if (sentence) return sentence.replace("{path}", () => error.message);
|
|
170
|
-
return
|
|
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, `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
444
|
-
|
|
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
|
|
447
|
-
return
|
|
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
|
-
`
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
485
|
-
|
|
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
|
-
|
|
500
|
-
opts.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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:
|
|
526
|
-
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("
|
|
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
|
-
`
|
|
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.
|
|
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("
|
|
1645
|
-
if (grant.read) rights.push("
|
|
1646
|
-
return rights.length > 0 ? rights.join(" \xB7 ") : "
|
|
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
|
|
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)
|
|
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
|
-
`
|
|
1696
|
-
grants.length > 0 ? "
|
|
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
|
|
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
|
|
1707
|
-
|
|
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
|
-
|
|
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") || "
|
|
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: `
|
|
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 === "--
|
|
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(`
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
21
|
+
"encryption"
|
|
22
22
|
],
|
|
23
23
|
"mcpName": "io.github.soke1556/zas-agent",
|
|
24
24
|
"bin": {
|