zas-agent 0.7.0 → 0.9.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 +57 -1
- package/README.md +25 -3
- package/dist/cli.js +354 -56
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.0] - 2026-09-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `zas_edit_item`. An agent changes an item it sent, under the same id: the
|
|
15
|
+
title of a file or a note, or a note's text, language and secret cover.
|
|
16
|
+
The owner's activity log records the edit, and the channel's devices are
|
|
17
|
+
woken. An item somebody else sent is refused as `not_yours`.
|
|
18
|
+
- `zas_replace_file`. An agent swaps the bytes under a file it sent, keeping
|
|
19
|
+
the item id, its place in the channel and its pin. The old chunks are
|
|
20
|
+
released and the new ones charged in one server transaction; a chunk in
|
|
21
|
+
both versions is left alone. A note, an item with a public share, and an
|
|
22
|
+
item somebody else sent are refused. Agents only: no web or mobile client
|
|
23
|
+
gets a replace.
|
|
24
|
+
- Five codes: `not_yours`, `stale` (the item changed while the agent was
|
|
25
|
+
working on it: read it again and retry), `not_a_note`, `not_a_file`,
|
|
26
|
+
`item_shared`.
|
|
27
|
+
- Both tools drop the local send receipts for the item they changed, so
|
|
28
|
+
"send that again" after an edit makes a new item.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- A machine whose agent home could not be written crashed on the way into
|
|
33
|
+
serving, before the MCP initialize response, because recording that the
|
|
34
|
+
telemetry notice had been shown threw. The notice is printed and the
|
|
35
|
+
failure to record it is logged; the server starts either way. This is the
|
|
36
|
+
crash behind "connection closed: initialize response" from a Codex-style
|
|
37
|
+
host.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## [0.8.0] - 2026-09-06
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- A channel you joined, but did not create, can now be granted to an agent.
|
|
45
|
+
Its owner has to allow the agent first. Until they answer, the grant is
|
|
46
|
+
written and waiting: the key is real and it opens nothing.
|
|
47
|
+
- `grant_pending`. A tool call on a channel whose owner has not answered says
|
|
48
|
+
so, and says to ask them, rather than reporting the grant as missing.
|
|
49
|
+
- `foreign_limit` on pairing. A plan allows an agent a number of channels of
|
|
50
|
+
other people, and the free plan allows fewer of those than of your own.
|
|
51
|
+
- `zas_status` marks such a channel as waiting for its owner.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Reading a channel somebody else created said `read_forbidden` on a grant
|
|
56
|
+
that plainly said `send · read`, while sending into the same channel
|
|
57
|
+
worked. Sending goes through the API, which knows which account holds a
|
|
58
|
+
channel; reading goes to Firestore directly, and the agent looked under
|
|
59
|
+
its own owner, where a shared channel is not kept. The server now says
|
|
60
|
+
where each channel lives, and reads, Directo offers and Directo fallbacks
|
|
61
|
+
all address that account. A channel of your own is unchanged: it is in
|
|
62
|
+
your own account, which is where it was always read from.
|
|
63
|
+
|
|
10
64
|
## [0.7.0] - 2026-09-05
|
|
11
65
|
|
|
12
66
|
### Added
|
|
@@ -279,7 +333,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
279
333
|
two separate identities that cannot read each other's keys.
|
|
280
334
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
281
335
|
|
|
282
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.
|
|
336
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.9.0...HEAD
|
|
337
|
+
[0.9.0]: https://github.com/soke1556/zas-agent/compare/v0.8.0...v0.9.0
|
|
338
|
+
[0.8.0]: https://github.com/soke1556/zas-agent/compare/v0.7.0...v0.8.0
|
|
283
339
|
[0.7.0]: https://github.com/soke1556/zas-agent/compare/v0.6.3...v0.7.0
|
|
284
340
|
[0.6.3]: https://github.com/soke1556/zas-agent/compare/v0.6.2...v0.6.3
|
|
285
341
|
[0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
|
package/README.md
CHANGED
|
@@ -25,9 +25,10 @@ scope to the channels you choose, and that you can revoke.
|
|
|
25
25
|
|
|
26
26
|
## What it does
|
|
27
27
|
|
|
28
|
-
Your coding agent gets
|
|
28
|
+
Your coding agent gets thirteen tools. It can send a file or a note into a channel
|
|
29
29
|
you picked, send or receive a file live through Directo, list what is in that
|
|
30
|
-
channel,
|
|
30
|
+
channel, pull one item back onto disk, and change what it sent: an item's
|
|
31
|
+
title or text, or the bytes under a file, keeping the item id.
|
|
31
32
|
Everything it sends is encrypted on your machine before it leaves, lands in your
|
|
32
33
|
account, and is marked in the channel as sent by that agent. The agent has an
|
|
33
34
|
identity of its own and never holds your account key: pairing mints a key pair
|
|
@@ -166,6 +167,11 @@ ones marked *(server-side)* are enforced by the Zas server.
|
|
|
166
167
|
- **No stored sending into a view-only channel, and none into a channel in
|
|
167
168
|
Directo mode.** Both are refused before a byte is uploaded. A channel in
|
|
168
169
|
Directo mode takes `zas_send_direct`, a live transfer that stores nothing.
|
|
170
|
+
- **No changing what it did not send.** `zas_edit_item` and `zas_replace_file`
|
|
171
|
+
act only on items carrying this agent's mark; anything else is `not_yours`,
|
|
172
|
+
and the server refuses it too *(server-side)*. A replace is for agents only:
|
|
173
|
+
no web or mobile client gets one. An item with a public share is not
|
|
174
|
+
replaced until the share is removed.
|
|
169
175
|
- **No receiving unless the grant says read.** Receiving through Directo
|
|
170
176
|
writes a file onto this machine, so `zas_receive_direct` takes the same
|
|
171
177
|
`read` switch as listing. And it only ever runs inside a tool call: the
|
|
@@ -213,6 +219,11 @@ string ever reaches a terminal.
|
|
|
213
219
|
| `file_too_big` | The file is over the plan limit. |
|
|
214
220
|
| `duplicate` | That item is already in the channel. |
|
|
215
221
|
| `not_found` | That item is not in the channel. |
|
|
222
|
+
| `not_yours` | That item was not sent by this agent; it can only change its own items. |
|
|
223
|
+
| `stale` | That item changed while this agent was working on it; read it again and retry. |
|
|
224
|
+
| `not_a_note` | That item is a file, not a note; only its title can change. |
|
|
225
|
+
| `not_a_file` | That item is a note, not a file. |
|
|
226
|
+
| `item_shared` | That item has a public share; the owner removes the share first. |
|
|
216
227
|
| `invalid_cap` | That file is no longer available. |
|
|
217
228
|
| `write_failed` | The download destination could not be written. |
|
|
218
229
|
| `pairing_expired` | The pairing expired; pair again. |
|
|
@@ -243,11 +254,22 @@ stack trace.
|
|
|
243
254
|
| `zas_receive_direct_fallback` | After a Directo receive failed in flight, downloads the encrypted copy the sender chose to store, and decrypts it to the same destination. |
|
|
244
255
|
| `zas_list_items` | Lists the most recent items in one of the owner's channels. Needs a grant that includes reading. |
|
|
245
256
|
| `zas_get_item` | Fetches one item. A note comes back as text; a file is written to disk. It never overwrites, so the path it answers with can differ from the one you asked for. |
|
|
257
|
+
| `zas_edit_item` | Changes an item this agent sent, keeping its id: the title of a file or a note, or a note's text, language and secret cover. Needs a grant that includes reading and sending. The owner's activity log records it. |
|
|
258
|
+
| `zas_replace_file` | Replaces the bytes of a file this agent sent with a file from this machine, keeping the item id, its place in the channel and its pin. Returns the item id, or a job id after a minute. Refuses notes, shared items, and items sent by anyone else. |
|
|
246
259
|
| `zas_jobs` | Lists the sends and Directo transfers this server started, newest first, with the phase each one reached — and where a `job_id` from a long send is redeemed. |
|
|
247
260
|
|
|
248
261
|
`channel` takes a channel name or a channel id. A name has to match exactly one
|
|
249
262
|
of the channels you granted; with exactly one grant, `zas_send_file`,
|
|
250
|
-
`zas_send_note`, `zas_send_direct
|
|
263
|
+
`zas_send_note`, `zas_send_direct`, `zas_receive_direct`, `zas_edit_item` and
|
|
264
|
+
`zas_replace_file` can leave it out.
|
|
265
|
+
|
|
266
|
+
A replace runs the same pipeline as a send — hash, key derivation, encryption,
|
|
267
|
+
then upload or proof per chunk — and posts the new set with the old one to
|
|
268
|
+
release. The server moves both sets, the account's storage tally and the item
|
|
269
|
+
in one transaction, so a failure leaves everything as it was; a chunk present
|
|
270
|
+
in both versions is neither uploaded nor released. Both tools read the item
|
|
271
|
+
first and write only over the version they read: an item that changed in
|
|
272
|
+
between answers `stale`, and nothing is lost.
|
|
251
273
|
|
|
252
274
|
Directo needs a native module, [node-datachannel](https://github.com/murat-dogan/node-datachannel),
|
|
253
275
|
WebRTC for Node. npm installs a prebuilt binary for Windows, macOS and Linux;
|
package/dist/cli.js
CHANGED
|
@@ -123,7 +123,13 @@ var RENAMES = {
|
|
|
123
123
|
// agent (expired, or cancelled from the receiving side); `direct_too_big`
|
|
124
124
|
// is the plan's Directo ceiling.
|
|
125
125
|
not_live: "direct_cancelled",
|
|
126
|
-
direct_too_big: "file_too_big"
|
|
126
|
+
direct_too_big: "file_too_big",
|
|
127
|
+
// The item-change routes. `not_allowed` there means the item is not this
|
|
128
|
+
// agent's; a share of any kind, armed or not, is the one reason a replace
|
|
129
|
+
// is refused that the owner fixes from the item itself.
|
|
130
|
+
not_allowed: "not_yours",
|
|
131
|
+
shared: "item_shared",
|
|
132
|
+
bar_armed: "item_shared"
|
|
127
133
|
};
|
|
128
134
|
var ZasError = class extends Error {
|
|
129
135
|
constructor(code, status, message, retryAfterMs, serverCode) {
|
|
@@ -159,6 +165,7 @@ var SENTENCES = {
|
|
|
159
165
|
agent_forbidden: "This agent cannot do that; only the account owner can.",
|
|
160
166
|
agent_revoked: "The owner revoked this agent. Pair again with \u201Czas-agent pair\u201D.",
|
|
161
167
|
grant_missing: "This agent has no access to that channel. The owner adds it from Settings \u2192 Agents.",
|
|
168
|
+
grant_pending: "That channel belongs to somebody else, and its owner has not allowed this agent yet. Ask them to allow it in Zas.",
|
|
162
169
|
not_found: "That item is not in the channel.",
|
|
163
170
|
invalid_cap: "That file is no longer available.",
|
|
164
171
|
write_failed: "The file could not be saved to the destination.",
|
|
@@ -180,6 +187,11 @@ var SENTENCES = {
|
|
|
180
187
|
rate_limited: "Too many sends in a row. Wait a moment.",
|
|
181
188
|
file_too_big: "The file is over the plan limit.",
|
|
182
189
|
duplicate: "That item is already in the channel.",
|
|
190
|
+
not_yours: "That item was not sent by this agent. It can only change its own items.",
|
|
191
|
+
stale: "That item changed while this agent was working on it. Read it again and retry.",
|
|
192
|
+
not_a_note: "That item is a file, not a note. Only its title can change; use zas_replace_file for the bytes.",
|
|
193
|
+
not_a_file: "That item is a note, not a file. Use zas_edit_item.",
|
|
194
|
+
item_shared: "That item has a public share. The owner removes the share first.",
|
|
183
195
|
pairing_expired: "The pairing expired. Run \u201Czas-agent pair\u201D again.",
|
|
184
196
|
pairing_cancelled: "The owner cancelled the pairing.",
|
|
185
197
|
claim_mismatch: "The code does not match. Try again.",
|
|
@@ -187,6 +199,7 @@ var SENTENCES = {
|
|
|
187
199
|
pairing_claimed: "This pairing was already claimed. Pair again with \u201Czas-agent pair\u201D.",
|
|
188
200
|
agent_limit: "The account cannot take another agent. The owner deletes one from Settings \u2192 Agents.",
|
|
189
201
|
grant_limit: "The plan allows fewer channels per agent than this pairing grants.",
|
|
202
|
+
foreign_limit: "The plan allows this agent fewer channels of other people than this pairing asks for.",
|
|
190
203
|
feature_disabled: "Agents are not enabled for this account yet.",
|
|
191
204
|
upload_failed: "The upload failed. Try again.",
|
|
192
205
|
oprf_failed: "Zas did not answer correctly while preparing the file. Try again.",
|
|
@@ -1036,8 +1049,8 @@ function keepUrls(servers, keep) {
|
|
|
1036
1049
|
const kept = [];
|
|
1037
1050
|
for (const server of servers) {
|
|
1038
1051
|
const urls = typeof server.urls === "string" ? [server.urls] : server.urls ?? [];
|
|
1039
|
-
const
|
|
1040
|
-
if (
|
|
1052
|
+
const usable2 = urls.filter(keep);
|
|
1053
|
+
if (usable2.length > 0) kept.push({ ...server, urls: usable2 });
|
|
1041
1054
|
}
|
|
1042
1055
|
return kept;
|
|
1043
1056
|
}
|
|
@@ -2075,15 +2088,22 @@ function channelNameOf(identity, grant) {
|
|
|
2075
2088
|
throw new ZasError("key_stale", 0);
|
|
2076
2089
|
}
|
|
2077
2090
|
}
|
|
2091
|
+
function channelAccountOf(identity, grant) {
|
|
2092
|
+
return grant.channel_owner_uid !== void 0 && grant.channel_owner_uid !== "" ? grant.channel_owner_uid : identity.owner_uid;
|
|
2093
|
+
}
|
|
2078
2094
|
var fold = (value) => value.trim().toLowerCase();
|
|
2095
|
+
function usable(grant) {
|
|
2096
|
+
if (grant.pending) throw new ZasError("grant_pending", 0);
|
|
2097
|
+
return grant;
|
|
2098
|
+
}
|
|
2079
2099
|
function resolveChannel(identity, grants, channel) {
|
|
2080
2100
|
if (channel === void 0 || fold(channel) === "") {
|
|
2081
|
-
if (grants.length === 1) return grants[0];
|
|
2101
|
+
if (grants.length === 1) return usable(grants[0]);
|
|
2082
2102
|
throw new ZasError("grant_missing", 0);
|
|
2083
2103
|
}
|
|
2084
2104
|
const wanted = channel.trim();
|
|
2085
2105
|
const byId = grants.find((g) => g.channel_id === wanted);
|
|
2086
|
-
if (byId) return byId;
|
|
2106
|
+
if (byId) return usable(byId);
|
|
2087
2107
|
const folded = fold(wanted);
|
|
2088
2108
|
const byName = grants.filter((g) => {
|
|
2089
2109
|
try {
|
|
@@ -2092,7 +2112,7 @@ function resolveChannel(identity, grants, channel) {
|
|
|
2092
2112
|
return false;
|
|
2093
2113
|
}
|
|
2094
2114
|
});
|
|
2095
|
-
if (byName.length === 1) return byName[0];
|
|
2115
|
+
if (byName.length === 1) return usable(byName[0]);
|
|
2096
2116
|
throw new ZasError("grant_missing", 0);
|
|
2097
2117
|
}
|
|
2098
2118
|
|
|
@@ -2557,32 +2577,20 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey, expiresInD
|
|
|
2557
2577
|
}
|
|
2558
2578
|
return created.link_id;
|
|
2559
2579
|
}
|
|
2560
|
-
async function
|
|
2561
|
-
const stat = await fsp2.stat(
|
|
2580
|
+
async function checkFilePath(path) {
|
|
2581
|
+
const stat = await fsp2.stat(path).catch(() => {
|
|
2562
2582
|
throw new ZasError("upload_failed", 400);
|
|
2563
2583
|
});
|
|
2564
2584
|
if (!stat.isFile()) throw new ZasError("upload_failed", 400);
|
|
2565
2585
|
if (stat.size > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
const file = await fsp2.readFile(
|
|
2586
|
+
}
|
|
2587
|
+
async function readFileChecked(path) {
|
|
2588
|
+
await checkFilePath(path);
|
|
2589
|
+
const file = await fsp2.readFile(path);
|
|
2570
2590
|
if (file.byteLength > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
const stored = receiptFor(ctx, key);
|
|
2575
|
-
if (stored) {
|
|
2576
|
-
return {
|
|
2577
|
-
link_id: stored.link_id,
|
|
2578
|
-
channel_id: grant.channel_id,
|
|
2579
|
-
channel_name: channelNameOf(ctx.identity, grant),
|
|
2580
|
-
bytes: stored.bytes,
|
|
2581
|
-
chunks: stored.chunks,
|
|
2582
|
-
deduplicated: stored.deduplicated,
|
|
2583
|
-
replayed: true
|
|
2584
|
-
};
|
|
2585
|
-
}
|
|
2591
|
+
return { bytes: new Uint8Array(file.buffer, file.byteOffset, file.byteLength), name: basename(path) };
|
|
2592
|
+
}
|
|
2593
|
+
async function placeFile(ctx, bytes, onPhase) {
|
|
2586
2594
|
onPhase?.("hashing");
|
|
2587
2595
|
const plains = [];
|
|
2588
2596
|
for await (const piece of chunkStream([bytes])) plains.push(piece);
|
|
@@ -2599,7 +2607,37 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2599
2607
|
(plain, i) => encryptChunk(oprfFinalize(hashes[i], blinds[i].blind, b64ToBytes(evaluated[i])), plain)
|
|
2600
2608
|
));
|
|
2601
2609
|
onPhase?.("uploading");
|
|
2602
|
-
|
|
2610
|
+
return placeChunks(ctx, encs);
|
|
2611
|
+
}
|
|
2612
|
+
function forgetLink(profile, linkId) {
|
|
2613
|
+
const entries = {};
|
|
2614
|
+
let dropped = false;
|
|
2615
|
+
for (const [k, v] of Object.entries(loadFingerprints(profile).entries)) {
|
|
2616
|
+
if (v && v.link_id === linkId) dropped = true;
|
|
2617
|
+
else entries[k] = v;
|
|
2618
|
+
}
|
|
2619
|
+
if (dropped) saveFingerprints(profile, { entries });
|
|
2620
|
+
}
|
|
2621
|
+
async function sendFile(ctx, input, onPhase) {
|
|
2622
|
+
await checkFilePath(input.path);
|
|
2623
|
+
const grant = await grantFor(ctx, input.channel);
|
|
2624
|
+
const { bytes, name } = await readFileChecked(input.path);
|
|
2625
|
+
const title = input.title ?? name;
|
|
2626
|
+
const contentHash = await sendIdentity(await blake3Hex(bytes), input.expires_in_days);
|
|
2627
|
+
const key = await receiptKey(ctx, grant.channel_id, contentHash, title);
|
|
2628
|
+
const stored = receiptFor(ctx, key);
|
|
2629
|
+
if (stored) {
|
|
2630
|
+
return {
|
|
2631
|
+
link_id: stored.link_id,
|
|
2632
|
+
channel_id: grant.channel_id,
|
|
2633
|
+
channel_name: channelNameOf(ctx.identity, grant),
|
|
2634
|
+
bytes: stored.bytes,
|
|
2635
|
+
chunks: stored.chunks,
|
|
2636
|
+
deduplicated: stored.deduplicated,
|
|
2637
|
+
replayed: true
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
const placed = await placeFile(ctx, bytes, onPhase);
|
|
2603
2641
|
onPhase?.("finishing");
|
|
2604
2642
|
const mime = mimeFor(input.path);
|
|
2605
2643
|
const thumb = await thumbnailFor(bytes, mime);
|
|
@@ -2784,7 +2822,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2784
2822
|
...stamp
|
|
2785
2823
|
});
|
|
2786
2824
|
const route = `/direct/${cid}/${id}`;
|
|
2787
|
-
const offerPath = `accounts/${
|
|
2825
|
+
const offerPath = `accounts/${channelAccountOf(ctx.identity, grant)}/channels/${cid}/direct/${id}`;
|
|
2788
2826
|
const setState = (state) => ctx.client.api("POST", `${route}/state`, { state, ...stamp }).then(() => void 0, () => void 0);
|
|
2789
2827
|
const claimBy = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS);
|
|
2790
2828
|
for (; ; ) {
|
|
@@ -2962,20 +3000,6 @@ async function sendDirectFallback(ctx, record, report2, deps = {}) {
|
|
|
2962
3000
|
};
|
|
2963
3001
|
}
|
|
2964
3002
|
|
|
2965
|
-
// src/receive.ts
|
|
2966
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2967
|
-
import {
|
|
2968
|
-
closeSync as closeSync2,
|
|
2969
|
-
existsSync as existsSync3,
|
|
2970
|
-
mkdirSync as mkdirSync3,
|
|
2971
|
-
openSync as openSync2,
|
|
2972
|
-
renameSync as renameSync3,
|
|
2973
|
-
rmdirSync as rmdirSync2,
|
|
2974
|
-
unlinkSync as unlinkSync2,
|
|
2975
|
-
writeSync as writeSync2
|
|
2976
|
-
} from "node:fs";
|
|
2977
|
-
import { dirname as dirname2 } from "node:path";
|
|
2978
|
-
|
|
2979
3003
|
// src/read.ts
|
|
2980
3004
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2981
3005
|
import {
|
|
@@ -3015,7 +3039,8 @@ function rowOf(doc) {
|
|
|
3015
3039
|
agent: stringOf(fields.agent),
|
|
3016
3040
|
createdAt: timeOf(fields.created_at),
|
|
3017
3041
|
expiresAt: timeOf(fields.expires_at),
|
|
3018
|
-
bar: fields.bar?.booleanValue === true
|
|
3042
|
+
bar: fields.bar?.booleanValue === true,
|
|
3043
|
+
updateTime: timeOf({ timestampValue: document.updateTime })
|
|
3019
3044
|
};
|
|
3020
3045
|
}
|
|
3021
3046
|
function readable(row) {
|
|
@@ -3060,7 +3085,7 @@ function nameFrom(channelKey, grant) {
|
|
|
3060
3085
|
}
|
|
3061
3086
|
}
|
|
3062
3087
|
function linksParent(identity, grant) {
|
|
3063
|
-
return `accounts/${identity
|
|
3088
|
+
return `accounts/${channelAccountOf(identity, grant)}/channels/${grant.channel_id}`;
|
|
3064
3089
|
}
|
|
3065
3090
|
function linkPath(identity, grant, id) {
|
|
3066
3091
|
return `projects/${identity.firestore_project}/databases/(default)/documents/${linksParent(identity, grant)}/links/${id}`;
|
|
@@ -3162,7 +3187,7 @@ function destinationOf(dest, name, fallback) {
|
|
|
3162
3187
|
const at = statSync2(dest, { throwIfNoEntry: false })?.isDirectory() ? join2(dest, base) : dest;
|
|
3163
3188
|
return { target: freeName(at) };
|
|
3164
3189
|
}
|
|
3165
|
-
async function
|
|
3190
|
+
async function loadItem(ctx, channel, id) {
|
|
3166
3191
|
const grant = await readGrant(ctx, channel);
|
|
3167
3192
|
if (!ID_SEGMENT.test(id)) throw new ZasError("not_found", 404);
|
|
3168
3193
|
const channelKey = channelKeyOf(ctx.identity, grant);
|
|
@@ -3181,6 +3206,10 @@ async function getItem(ctx, channel, id, dest) {
|
|
|
3181
3206
|
if (!row || !readable(row)) throw new ZasError("not_found", 404);
|
|
3182
3207
|
const manifest = openFor(channelKey, row);
|
|
3183
3208
|
if (!manifest) throw new ZasError("not_found", 404);
|
|
3209
|
+
return { grant, channelKey, row, manifest };
|
|
3210
|
+
}
|
|
3211
|
+
async function getItem(ctx, channel, id, dest) {
|
|
3212
|
+
const { row, manifest } = await loadItem(ctx, channel, id);
|
|
3184
3213
|
if (manifest.kind === "text") {
|
|
3185
3214
|
const text2 = manifest.text ?? "";
|
|
3186
3215
|
return { text: text2, bytes: new TextEncoder().encode(text2).length };
|
|
@@ -3231,7 +3260,140 @@ async function getItem(ctx, channel, id, dest) {
|
|
|
3231
3260
|
return { path: target, bytes: written };
|
|
3232
3261
|
}
|
|
3233
3262
|
|
|
3263
|
+
// src/edit.ts
|
|
3264
|
+
function writable(grant) {
|
|
3265
|
+
if (!grant.send || grant.mode === "view") throw new ZasError("send_forbidden", 403);
|
|
3266
|
+
if (grant.direct_mode) throw new ZasError("direct_mode", 409);
|
|
3267
|
+
}
|
|
3268
|
+
async function ownItem(ctx, channel, id) {
|
|
3269
|
+
const loaded = await loadItem(ctx, channel, id);
|
|
3270
|
+
writable(loaded.grant);
|
|
3271
|
+
if (loaded.row.agent !== ctx.identity.agent_uid) throw new ZasError("not_yours", 403);
|
|
3272
|
+
return loaded;
|
|
3273
|
+
}
|
|
3274
|
+
function sealedFor(item, manifest) {
|
|
3275
|
+
return bytesToB64(sealManifest(item.channelKey, manifest, item.grant.key_version));
|
|
3276
|
+
}
|
|
3277
|
+
function guardOf(item) {
|
|
3278
|
+
return item.row.updateTime === null ? {} : { expected_update_time: item.row.updateTime };
|
|
3279
|
+
}
|
|
3280
|
+
async function editItem(ctx, input) {
|
|
3281
|
+
const item = await ownItem(ctx, input.channel, input.id);
|
|
3282
|
+
const { grant, row, manifest } = item;
|
|
3283
|
+
const kind = manifest.kind === "text" ? "text" : "file";
|
|
3284
|
+
const noteFields = input.text !== void 0 || input.lang !== void 0 || input.secret !== void 0;
|
|
3285
|
+
if (noteFields && kind !== "text") throw new ZasError("not_a_note", 400);
|
|
3286
|
+
const done = (updateTime) => ({
|
|
3287
|
+
link_id: row.id,
|
|
3288
|
+
channel_id: grant.channel_id,
|
|
3289
|
+
channel_name: channelNameOf(ctx.identity, grant),
|
|
3290
|
+
kind,
|
|
3291
|
+
title: manifest.title ?? manifest.name,
|
|
3292
|
+
update_time: updateTime
|
|
3293
|
+
});
|
|
3294
|
+
if (input.title === void 0 && !noteFields) return done(row.updateTime ?? Date.now());
|
|
3295
|
+
if (input.title !== void 0) {
|
|
3296
|
+
if (input.title === "") delete manifest.title;
|
|
3297
|
+
else manifest.title = input.title;
|
|
3298
|
+
}
|
|
3299
|
+
if (input.text !== void 0) {
|
|
3300
|
+
manifest.text = input.text;
|
|
3301
|
+
manifest.size = new TextEncoder().encode(input.text).length;
|
|
3302
|
+
delete manifest.html;
|
|
3303
|
+
if (manifest.title === void 0) manifest.name = noteName(input.text);
|
|
3304
|
+
}
|
|
3305
|
+
if (input.lang !== void 0) {
|
|
3306
|
+
if (input.lang === "") delete manifest.code;
|
|
3307
|
+
else manifest.code = { lang: input.lang, auto: false };
|
|
3308
|
+
}
|
|
3309
|
+
if (input.secret !== void 0) {
|
|
3310
|
+
if (input.secret) manifest.sensitive = true;
|
|
3311
|
+
else delete manifest.sensitive;
|
|
3312
|
+
}
|
|
3313
|
+
const answer = await ctx.client.api(
|
|
3314
|
+
"PATCH",
|
|
3315
|
+
`/links/${grant.channel_id}/${row.id}`,
|
|
3316
|
+
{
|
|
3317
|
+
manifest_enc: sealedFor(item, manifest),
|
|
3318
|
+
// The caller is changing what the item says: the server records it.
|
|
3319
|
+
edit: true,
|
|
3320
|
+
...guardOf(item)
|
|
3321
|
+
}
|
|
3322
|
+
);
|
|
3323
|
+
forgetLink(ctx.profile, row.id);
|
|
3324
|
+
return done(typeof answer.update_time === "number" ? answer.update_time : Date.now());
|
|
3325
|
+
}
|
|
3326
|
+
async function replaceFile(ctx, input, onPhase) {
|
|
3327
|
+
await checkFilePath(input.path);
|
|
3328
|
+
const item = await ownItem(ctx, input.channel, input.id);
|
|
3329
|
+
const { grant, row, manifest } = item;
|
|
3330
|
+
if (manifest.kind === "text") throw new ZasError("not_a_file", 400);
|
|
3331
|
+
const { bytes, name } = await readFileChecked(input.path);
|
|
3332
|
+
const title = input.title ?? manifest.title;
|
|
3333
|
+
const contentHash = await blake3Hex(bytes);
|
|
3334
|
+
const placed = await placeFile(ctx, bytes, onPhase);
|
|
3335
|
+
onPhase?.("finishing");
|
|
3336
|
+
const mime = mimeFor(input.path);
|
|
3337
|
+
const thumb = await thumbnailFor(bytes, mime);
|
|
3338
|
+
const next = newManifest({
|
|
3339
|
+
kind: "file",
|
|
3340
|
+
name,
|
|
3341
|
+
...title !== void 0 ? { title } : {},
|
|
3342
|
+
mime,
|
|
3343
|
+
size: bytes.length,
|
|
3344
|
+
// The item's own time: a replace changes what the item holds, not when the
|
|
3345
|
+
// owner got it, and the row keeps its place in the channel either way.
|
|
3346
|
+
created_at: manifest.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3347
|
+
...thumb ? { thumb_data: thumb } : {},
|
|
3348
|
+
chunks: placed.map((p) => p.entry)
|
|
3349
|
+
});
|
|
3350
|
+
const release = manifest.chunks.map((c) => c.cap).filter((cap) => typeof cap === "string");
|
|
3351
|
+
const answer = await ctx.client.api("POST", `/links/${grant.channel_id}/${row.id}/replace`, {
|
|
3352
|
+
manifest_enc: sealedFor(item, next),
|
|
3353
|
+
caps: placed.map((p) => p.entry.cap).filter((cap) => typeof cap === "string"),
|
|
3354
|
+
release,
|
|
3355
|
+
// Scoped to the link as well as the channel: the same bytes sent as a new
|
|
3356
|
+
// item are a different request from these bytes replacing this one.
|
|
3357
|
+
idempotency_key: agentSendIdempotencyKey(`${grant.channel_id}/${row.id}`, contentHash, title ?? name),
|
|
3358
|
+
...guardOf(item)
|
|
3359
|
+
});
|
|
3360
|
+
const bound = answer.caps ?? {};
|
|
3361
|
+
let patched = false;
|
|
3362
|
+
for (const p of placed) {
|
|
3363
|
+
const cap = bound[p.entry.blob_id];
|
|
3364
|
+
if (cap) {
|
|
3365
|
+
p.entry.cap = cap;
|
|
3366
|
+
patched = true;
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
if (patched) {
|
|
3370
|
+
await ctx.client.api("PATCH", `/links/${grant.channel_id}/${row.id}`, { manifest_enc: sealedFor(item, next) });
|
|
3371
|
+
}
|
|
3372
|
+
forgetLink(ctx.profile, row.id);
|
|
3373
|
+
return {
|
|
3374
|
+
link_id: row.id,
|
|
3375
|
+
channel_id: grant.channel_id,
|
|
3376
|
+
channel_name: channelNameOf(ctx.identity, grant),
|
|
3377
|
+
bytes: bytes.length,
|
|
3378
|
+
chunks: placed.length,
|
|
3379
|
+
deduplicated: placed.filter((p) => p.proven).length,
|
|
3380
|
+
replayed: answer.replayed === true
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3234
3384
|
// src/receive.ts
|
|
3385
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
3386
|
+
import {
|
|
3387
|
+
closeSync as closeSync2,
|
|
3388
|
+
existsSync as existsSync3,
|
|
3389
|
+
mkdirSync as mkdirSync3,
|
|
3390
|
+
openSync as openSync2,
|
|
3391
|
+
renameSync as renameSync3,
|
|
3392
|
+
rmdirSync as rmdirSync2,
|
|
3393
|
+
unlinkSync as unlinkSync2,
|
|
3394
|
+
writeSync as writeSync2
|
|
3395
|
+
} from "node:fs";
|
|
3396
|
+
import { dirname as dirname2 } from "node:path";
|
|
3235
3397
|
var OFFER_WAIT_MS2 = 9.5 * 60 * 1e3;
|
|
3236
3398
|
var OFFER_POLL_MS2 = 1e3;
|
|
3237
3399
|
var SIGNAL_POLL_MS2 = 400;
|
|
@@ -3341,11 +3503,12 @@ async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
|
3341
3503
|
const channelName = channelLabel(ctx, grant);
|
|
3342
3504
|
const cid = grant.channel_id;
|
|
3343
3505
|
const owner = ctx.identity.owner_uid;
|
|
3506
|
+
const account = channelAccountOf(ctx.identity, grant);
|
|
3344
3507
|
const device = deps.device ?? newDeviceToken();
|
|
3345
3508
|
const { seal, open } = sealer(key, grant.key_version);
|
|
3346
3509
|
const stamp = { device, owner_uid: owner };
|
|
3347
3510
|
const startedAt = now();
|
|
3348
|
-
const channelPath = `accounts/${
|
|
3511
|
+
const channelPath = `accounts/${account}/channels/${cid}`;
|
|
3349
3512
|
report2("waiting");
|
|
3350
3513
|
const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
|
|
3351
3514
|
let id;
|
|
@@ -3480,6 +3643,7 @@ async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
|
3480
3643
|
channel_name: channelName,
|
|
3481
3644
|
offer_id: offerId,
|
|
3482
3645
|
owner_uid: owner,
|
|
3646
|
+
channel_account: account,
|
|
3483
3647
|
device,
|
|
3484
3648
|
...input.dest !== void 0 ? { dest: input.dest } : {},
|
|
3485
3649
|
meta: offered,
|
|
@@ -3495,7 +3659,7 @@ async function receiveDirectFallback(ctx, record, report2, deps = {}) {
|
|
|
3495
3659
|
const { open } = sealer(record.key, record.key_version);
|
|
3496
3660
|
const stamp = { device: record.device, owner_uid: record.owner_uid };
|
|
3497
3661
|
const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
|
|
3498
|
-
const offerPath = `accounts/${record.
|
|
3662
|
+
const offerPath = `accounts/${record.channel_account}/channels/${record.channel_id}/direct/${record.offer_id}`;
|
|
3499
3663
|
const doc = await ctx.client.firestoreGet(offerPath);
|
|
3500
3664
|
const enc = stringField2(doc, "fallback_meta_enc");
|
|
3501
3665
|
let meta;
|
|
@@ -3647,6 +3811,8 @@ var PRODUCT_EVENTS = [
|
|
|
3647
3811
|
"enterprise.invite_attach",
|
|
3648
3812
|
"enterprise.invite_accept",
|
|
3649
3813
|
"enterprise.invite_admin",
|
|
3814
|
+
"enterprise.signin",
|
|
3815
|
+
"enterprise.org_create",
|
|
3650
3816
|
"enterprise.workspace_opened",
|
|
3651
3817
|
"enterprise.workspace_switched",
|
|
3652
3818
|
"enterprise.channel_created",
|
|
@@ -3691,7 +3857,17 @@ var PRODUCT_EVENTS = [
|
|
|
3691
3857
|
"agent.paired",
|
|
3692
3858
|
"agent.revoked",
|
|
3693
3859
|
"agent.send",
|
|
3694
|
-
"agent.
|
|
3860
|
+
"agent.edit",
|
|
3861
|
+
"agent.replace",
|
|
3862
|
+
"agent.tool_call",
|
|
3863
|
+
// The public pages, which until now reported nothing at all: `capture_pageview`
|
|
3864
|
+
// is off, so a visitor who read the enterprise page and left was invisible.
|
|
3865
|
+
// These five are the funnel that ends at `enterprise.org_create`.
|
|
3866
|
+
"marketing.page_viewed",
|
|
3867
|
+
"marketing.section_viewed",
|
|
3868
|
+
"marketing.scroll_depth",
|
|
3869
|
+
"marketing.cta_clicked",
|
|
3870
|
+
"marketing.language_switched"
|
|
3695
3871
|
];
|
|
3696
3872
|
var SERVER_OWNED_PRODUCT_EVENTS = [
|
|
3697
3873
|
"account_created",
|
|
@@ -3704,6 +3880,8 @@ var SERVER_OWNED_PRODUCT_EVENTS = [
|
|
|
3704
3880
|
"share_downloaded",
|
|
3705
3881
|
"share_signin_click",
|
|
3706
3882
|
"agent.send",
|
|
3883
|
+
"agent.edit",
|
|
3884
|
+
"agent.replace",
|
|
3707
3885
|
"agent.tool_call"
|
|
3708
3886
|
];
|
|
3709
3887
|
var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
|
|
@@ -3721,6 +3899,30 @@ var candidateType = oneOf("", "host", "srflx", "relay", "prflx", "other");
|
|
|
3721
3899
|
var enterpriseRole = oneOf("owner", "admin", "billing_admin", "member", "guest");
|
|
3722
3900
|
var agentKind = oneOf("claude_code", "codex", "other");
|
|
3723
3901
|
var pairHandoff = oneOf("loopback", "manual", "unknown");
|
|
3902
|
+
var marketingSurface = oneOf("home", "plans", "enterprise");
|
|
3903
|
+
var marketingSection = oneOf(
|
|
3904
|
+
"enterprise_band",
|
|
3905
|
+
"hero",
|
|
3906
|
+
"what",
|
|
3907
|
+
"benefits",
|
|
3908
|
+
"fit",
|
|
3909
|
+
"billing",
|
|
3910
|
+
"signin"
|
|
3911
|
+
);
|
|
3912
|
+
var marketingCta = oneOf(
|
|
3913
|
+
"see_enterprise",
|
|
3914
|
+
"see_plans",
|
|
3915
|
+
"contact",
|
|
3916
|
+
"start_org",
|
|
3917
|
+
"sign_in",
|
|
3918
|
+
"open_console",
|
|
3919
|
+
"switch_language",
|
|
3920
|
+
"footer_home"
|
|
3921
|
+
);
|
|
3922
|
+
var marketingPlacement = oneOf("nav", "hero", "band", "fit", "billing", "signin", "footer");
|
|
3923
|
+
var arrivalKind = oneOf("direct", "internal", "search", "social", "other");
|
|
3924
|
+
var uiLocale = oneOf("es-AR", "pt-BR", "en");
|
|
3925
|
+
var signinContext = oneOf("page", "handoff_site", "handoff_share");
|
|
3724
3926
|
var AGENT_TOOL_NAMES = [
|
|
3725
3927
|
"status",
|
|
3726
3928
|
"pair",
|
|
@@ -3732,7 +3934,9 @@ var AGENT_TOOL_NAMES = [
|
|
|
3732
3934
|
"receive_direct_fallback",
|
|
3733
3935
|
"list_items",
|
|
3734
3936
|
"get_item",
|
|
3735
|
-
"jobs"
|
|
3937
|
+
"jobs",
|
|
3938
|
+
"edit_item",
|
|
3939
|
+
"replace_file"
|
|
3736
3940
|
];
|
|
3737
3941
|
var AGENT_ERROR_CODES = [
|
|
3738
3942
|
"none",
|
|
@@ -3777,7 +3981,13 @@ var AGENT_ERROR_CODES = [
|
|
|
3777
3981
|
"network",
|
|
3778
3982
|
"sign_in_failed",
|
|
3779
3983
|
"bad_signature",
|
|
3780
|
-
"missing_token"
|
|
3984
|
+
"missing_token",
|
|
3985
|
+
// An agent changing its own items (2026-09-09).
|
|
3986
|
+
"not_yours",
|
|
3987
|
+
"stale",
|
|
3988
|
+
"not_a_note",
|
|
3989
|
+
"not_a_file",
|
|
3990
|
+
"item_shared"
|
|
3781
3991
|
];
|
|
3782
3992
|
function agentDurationBucket(ms) {
|
|
3783
3993
|
if (ms < 1e3) return "lt_1s";
|
|
@@ -3838,6 +4048,24 @@ var EVENT_PROPERTY_RULES = {
|
|
|
3838
4048
|
role: enterpriseRole,
|
|
3839
4049
|
external: bool
|
|
3840
4050
|
},
|
|
4051
|
+
// The workspace sign-in card, wherever it is shown: the public enterprise
|
|
4052
|
+
// page, a `?site=` handoff from a customer domain, or a `?share=` link.
|
|
4053
|
+
"enterprise.signin": {
|
|
4054
|
+
context: signinContext,
|
|
4055
|
+
stage: oneOf(
|
|
4056
|
+
"shown",
|
|
4057
|
+
"email_submitted",
|
|
4058
|
+
"email_sent",
|
|
4059
|
+
"email_failed",
|
|
4060
|
+
"provider_clicked",
|
|
4061
|
+
"link_completed",
|
|
4062
|
+
"link_failed"
|
|
4063
|
+
),
|
|
4064
|
+
provider: oneOf("google.com", "apple.com", "email_link", "none")
|
|
4065
|
+
},
|
|
4066
|
+
// The end of the funnel. `opened` is the create form appearing, which is
|
|
4067
|
+
// what someone signing in with no organization yet is shown.
|
|
4068
|
+
"enterprise.org_create": { stage: oneOf("opened", "submitted", "created", "failed") },
|
|
3841
4069
|
"enterprise.workspace_opened": { role: enterpriseRole, first_visit: bool },
|
|
3842
4070
|
"enterprise.workspace_switched": { destination: oneOf("personal", "organization"), role: enterpriseRole },
|
|
3843
4071
|
"enterprise.channel_created": { member_count: count },
|
|
@@ -3990,9 +4218,32 @@ var EVENT_PROPERTY_RULES = {
|
|
|
3990
4218
|
},
|
|
3991
4219
|
"agent.revoked": { agent_kind: agentKind },
|
|
3992
4220
|
"agent.send": { size_bucket: sizeBucket, agent_kind: agentKind },
|
|
4221
|
+
"agent.edit": { agent_kind: agentKind },
|
|
4222
|
+
"agent.replace": { size_bucket: sizeBucket, agent_kind: agentKind },
|
|
3993
4223
|
// What the CLI reports after every tool call: the tool, whether it worked,
|
|
3994
4224
|
// the code when it did not, and the version that answered. No path, no file
|
|
3995
4225
|
// name, no channel, no sentence.
|
|
4226
|
+
"marketing.page_viewed": {
|
|
4227
|
+
surface: marketingSurface,
|
|
4228
|
+
locale: uiLocale,
|
|
4229
|
+
// Whether the URL carried a language prefix. `/en/enterprise` and
|
|
4230
|
+
// `/enterprise` are the same page in different languages, and which one
|
|
4231
|
+
// search sends people to is worth knowing on its own.
|
|
4232
|
+
prefixed: bool,
|
|
4233
|
+
signed_in: bool,
|
|
4234
|
+
arrival: arrivalKind
|
|
4235
|
+
},
|
|
4236
|
+
"marketing.section_viewed": { surface: marketingSurface, section: marketingSection },
|
|
4237
|
+
"marketing.scroll_depth": {
|
|
4238
|
+
surface: marketingSurface,
|
|
4239
|
+
depth: oneOf("p25", "p50", "p75", "p100")
|
|
4240
|
+
},
|
|
4241
|
+
"marketing.cta_clicked": {
|
|
4242
|
+
surface: marketingSurface,
|
|
4243
|
+
cta: marketingCta,
|
|
4244
|
+
placement: marketingPlacement
|
|
4245
|
+
},
|
|
4246
|
+
"marketing.language_switched": { surface: marketingSurface, from: uiLocale, to: uiLocale },
|
|
3996
4247
|
"agent.tool_call": {
|
|
3997
4248
|
tool: oneOf(...AGENT_TOOL_NAMES),
|
|
3998
4249
|
result: oneOf("ok", "error"),
|
|
@@ -4088,7 +4339,7 @@ async function report(client, properties) {
|
|
|
4088
4339
|
|
|
4089
4340
|
// src/server.ts
|
|
4090
4341
|
function agentVersion() {
|
|
4091
|
-
return true ? "0.
|
|
4342
|
+
return true ? "0.9.0" : "0.0.0-dev";
|
|
4092
4343
|
}
|
|
4093
4344
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
4094
4345
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
@@ -4098,6 +4349,7 @@ var delay = (ms) => new Promise((resolve2) => {
|
|
|
4098
4349
|
setTimeout(resolve2, ms);
|
|
4099
4350
|
});
|
|
4100
4351
|
function rightsOf(grant) {
|
|
4352
|
+
if (grant.pending) return "waiting for the channel\u2019s owner";
|
|
4101
4353
|
const rights = [];
|
|
4102
4354
|
if (grant.send && grant.mode !== "view") rights.push(grant.direct_mode ? "send (Directo)" : "send");
|
|
4103
4355
|
if (grant.read) rights.push(grant.direct_mode ? "receive (Directo)" : "read");
|
|
@@ -4444,6 +4696,45 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4444
4696
|
return failed(e);
|
|
4445
4697
|
}
|
|
4446
4698
|
});
|
|
4699
|
+
server.registerTool("zas_edit_item", {
|
|
4700
|
+
description: "Change an item this agent sent, keeping its id: the title of a file or a note, or a note's text, language and secret cover. Refuses items sent by anyone else. A file's bytes change with zas_replace_file. Needs a grant that includes reading and sending. Pass only the fields to change; an empty title clears it." + AGENT_CUE,
|
|
4701
|
+
inputSchema: {
|
|
4702
|
+
id: z.string().describe("Item id, as `zas_list_items` reports it."),
|
|
4703
|
+
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4704
|
+
title: z.string().optional().describe("New label. An empty string clears it, so the file name or the first line shows again."),
|
|
4705
|
+
text: z.string().optional().describe("New body, for a note."),
|
|
4706
|
+
lang: z.string().optional().describe('Language of the snippet, for highlighting (for example "ts", "py"). An empty string makes it plain text.'),
|
|
4707
|
+
secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it. False removes the cover.")
|
|
4708
|
+
}
|
|
4709
|
+
}, async (input) => {
|
|
4710
|
+
try {
|
|
4711
|
+
return text2(await editItem(ctx(), input));
|
|
4712
|
+
} catch (e) {
|
|
4713
|
+
return failed(e);
|
|
4714
|
+
}
|
|
4715
|
+
});
|
|
4716
|
+
server.registerTool("zas_replace_file", {
|
|
4717
|
+
description: "Replace the bytes of a file this agent sent with a file from this machine, keeping the item id, its place in the channel and its pin. Refuses items sent by anyone else, notes, and an item with a public share. Returns the item id, or a job id when the upload takes longer than a minute. Sends any file this process can read; confirm with the owner before sending secrets, keys or credentials." + AGENT_CUE,
|
|
4718
|
+
inputSchema: {
|
|
4719
|
+
id: z.string().describe("Item id, as `zas_list_items` reports it."),
|
|
4720
|
+
path: z.string().describe("Absolute or relative path of the new file."),
|
|
4721
|
+
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4722
|
+
title: z.string().optional().describe("New label. Defaults to the label the item has.")
|
|
4723
|
+
}
|
|
4724
|
+
}, async (input) => {
|
|
4725
|
+
try {
|
|
4726
|
+
const c = ctx();
|
|
4727
|
+
const job = runner.start(
|
|
4728
|
+
"replace",
|
|
4729
|
+
input.title ?? input.path,
|
|
4730
|
+
input.channel ?? "",
|
|
4731
|
+
(report2) => replaceFile(c, input, report2)
|
|
4732
|
+
);
|
|
4733
|
+
return settled(await runner.wait(job));
|
|
4734
|
+
} catch (e) {
|
|
4735
|
+
return failed(e);
|
|
4736
|
+
}
|
|
4737
|
+
});
|
|
4447
4738
|
server.registerTool("zas_jobs", {
|
|
4448
4739
|
description: "List the sends and Directo transfers this server started, newest first, with the phase each one reached and how it ended \u2014 including any `job_id` a send returned; a finished job keeps its result here." + AGENT_CUE
|
|
4449
4740
|
}, async () => text2(runner.list()));
|
|
@@ -4497,6 +4788,15 @@ var USAGE = [
|
|
|
4497
4788
|
" zas-agent telemetry [on|off] say what this machine reports, or change it",
|
|
4498
4789
|
" zas-agent --version"
|
|
4499
4790
|
].join("\n");
|
|
4791
|
+
function noticeOnServe(log) {
|
|
4792
|
+
try {
|
|
4793
|
+
if (noticeShown()) return;
|
|
4794
|
+
log(TELEMETRY_NOTICE);
|
|
4795
|
+
markNoticeShown();
|
|
4796
|
+
} catch (e) {
|
|
4797
|
+
log(`Could not record that the notice was shown (${String(e?.message ?? e)}). Serving anyway.`);
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4500
4800
|
async function main(argv, log = (l) => console.error(l)) {
|
|
4501
4801
|
const args = parseArgs(argv);
|
|
4502
4802
|
if (args.command === "version") {
|
|
@@ -4551,10 +4851,7 @@ async function main(argv, log = (l) => console.error(l)) {
|
|
|
4551
4851
|
}
|
|
4552
4852
|
return 0;
|
|
4553
4853
|
}
|
|
4554
|
-
|
|
4555
|
-
log(TELEMETRY_NOTICE);
|
|
4556
|
-
markNoticeShown();
|
|
4557
|
-
}
|
|
4854
|
+
noticeOnServe(log);
|
|
4558
4855
|
await buildServer(args.profile).connect(new StdioServerTransport());
|
|
4559
4856
|
return 0;
|
|
4560
4857
|
}
|
|
@@ -4591,5 +4888,6 @@ if (isInvokedDirectly(process.argv[1], import.meta.url)) {
|
|
|
4591
4888
|
export {
|
|
4592
4889
|
isInvokedDirectly,
|
|
4593
4890
|
main,
|
|
4891
|
+
noticeOnServe,
|
|
4594
4892
|
parseArgs
|
|
4595
4893
|
};
|
package/package.json
CHANGED