zas-agent 0.6.2 → 0.7.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 +77 -1
- package/README.md +25 -10
- package/dist/cli.js +240 -77
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `expires_in_days` on `zas_send_file` and `zas_send_note`. An agent can ask
|
|
15
|
+
an item to live for as little as one whole day — worth setting for output
|
|
16
|
+
that is stale tomorrow, such as a build log, a test run, or a screenshot of
|
|
17
|
+
a bug already fixed, so a channel is not left holding five days of noise.
|
|
18
|
+
It can only ever shorten an item. The server clamps the request to what the
|
|
19
|
+
account's plan grants, so asking for longer is answered with the plan's own
|
|
20
|
+
life rather than refused.
|
|
21
|
+
- The lifetime is part of a send's identity. Both replay guards — the receipt
|
|
22
|
+
on disk and the server's idempotency key — hash it in, so "send that again,
|
|
23
|
+
but for one day" no longer matches the five-day receipt and answers with the
|
|
24
|
+
old item's id, having created nothing. A send that asks for nothing hashes
|
|
25
|
+
exactly as it did before, so every receipt already on disk still matches.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- A Directo send of a file of 4 GiB or more delivered only its first
|
|
30
|
+
gigabytes, and both ends agreed the transfer was whole. Node's `openAsBlob`
|
|
31
|
+
truncates `Blob.size` to 32 bits and its own `slice()` clamps to that
|
|
32
|
+
truncated size, so a 9 GiB file offered 1 GiB and sent 1 GiB. The sender now
|
|
33
|
+
trusts the on-disk size and reads true 64-bit offsets. A file under 4 GiB is
|
|
34
|
+
handed to the engine exactly as it was.
|
|
35
|
+
- A receiver restricted to relay candidates could not gather at all, so a
|
|
36
|
+
browser on a `relay` transport policy never received from an agent. Its ICE
|
|
37
|
+
is trimmed only when it genuinely cannot allocate over UDP.
|
|
38
|
+
- Chromium 152 returns no candidate from an ICE url on port 53, and that url
|
|
39
|
+
also suppresses the UDP relay and the server-reflexive candidate, so a
|
|
40
|
+
browser on that version could not reach an agent at all. The port-53 urls
|
|
41
|
+
are dropped for Chromium 152 and later, under every transport policy.
|
|
42
|
+
- The selected candidate pair is read twice — when ICE connects and when the
|
|
43
|
+
data channel opens — and a local `srflx` is re-identified as `prflx` once
|
|
44
|
+
the peer's check reveals the same address. The summary kept the last read
|
|
45
|
+
and the trace kept the first, so one pair was named two ways. The trace now
|
|
46
|
+
says `pair now …` when the read changes.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The install and register commands know which shell they are pasted into.
|
|
51
|
+
Windows takes `npx.cmd` and a `cmd /d /c ` prefix, because npm installs
|
|
52
|
+
`claude` and `codex` as PowerShell script shims that keep a bare `--` for
|
|
53
|
+
themselves. Every command now names `@latest`, so npm requests the published
|
|
54
|
+
package instead of selecting an unbuilt local workspace copy. The README
|
|
55
|
+
gives both platforms, and the prerequisites, in order.
|
|
56
|
+
|
|
57
|
+
## [0.6.3] - 2026-09-04
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- The selected candidate pair is read from the transport, which names its
|
|
62
|
+
own pair on every engine. A transfer that connected and carried its whole
|
|
63
|
+
file could report `pair=none`, because the browser at the other end does
|
|
64
|
+
not mark a pair `nominated` on the controlled side. The empty field was
|
|
65
|
+
read as a missing pair and sent two rounds of diagnosis after a
|
|
66
|
+
connection that had been there all along.
|
|
67
|
+
- A peer connection state of `failed` has to hold before the engine acts on
|
|
68
|
+
it. Chrome reports `failed` while it is still gathering and returns to
|
|
69
|
+
`connecting` in the same millisecond; acting on the first one killed a
|
|
70
|
+
receiver at 0.88 s with pairs still waiting to be tried. The settle window
|
|
71
|
+
is shorter than the ICE grace, so a connection that really is dead still
|
|
72
|
+
gives up sooner than it did before.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- The trace counts candidate pairs by check state just before the verdict:
|
|
77
|
+
`checks failed=24 in-progress=4`, or `checks none`. No pair selected could
|
|
78
|
+
not tell a run that formed no pairs from one that formed pairs and lost
|
|
79
|
+
every check, and those are different faults.
|
|
80
|
+
- A candidate the peer refuses is traced as `ice apply rejected <type>
|
|
81
|
+
<ErrorName>`. The rejection used to be swallowed, so a refused candidate
|
|
82
|
+
looked exactly like one the network lost.
|
|
83
|
+
|
|
10
84
|
## [0.6.2] - 2026-09-04
|
|
11
85
|
|
|
12
86
|
### Added
|
|
@@ -205,7 +279,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
205
279
|
two separate identities that cannot read each other's keys.
|
|
206
280
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
207
281
|
|
|
208
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.
|
|
282
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.7.0...HEAD
|
|
283
|
+
[0.7.0]: https://github.com/soke1556/zas-agent/compare/v0.6.3...v0.7.0
|
|
284
|
+
[0.6.3]: https://github.com/soke1556/zas-agent/compare/v0.6.2...v0.6.3
|
|
209
285
|
[0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
|
|
210
286
|
[0.6.1]: https://github.com/soke1556/zas-agent/compare/v0.6.0...v0.6.1
|
|
211
287
|
[0.6.0]: https://github.com/soke1556/zas-agent/compare/v0.5.1...v0.6.0
|
package/README.md
CHANGED
|
@@ -36,33 +36,48 @@ get a short-lived session.
|
|
|
36
36
|
|
|
37
37
|
## Install
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Install [Node.js LTS](https://nodejs.org/en/download) (22 or newer) and your
|
|
40
|
+
agent's command-line tool first. Reopen your terminal after installing.
|
|
41
|
+
Run these commands on the computer where you use the agent.
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Choose your agent and pair it. On Windows (PowerShell or CMD):
|
|
44
|
+
|
|
45
|
+
**Codex**
|
|
42
46
|
|
|
43
47
|
```
|
|
44
|
-
npx -y zas-agent pair --profile
|
|
48
|
+
npx.cmd -y zas-agent@latest pair --profile codex
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
**Claude Code**
|
|
48
52
|
|
|
49
53
|
```
|
|
50
|
-
|
|
54
|
+
npx.cmd -y zas-agent@latest pair --profile claude-code
|
|
51
55
|
```
|
|
52
56
|
|
|
57
|
+
Open the printed link, approve the channels, and enter the code in the terminal
|
|
58
|
+
if the page shows one. Leave the terminal open until pairing finishes.
|
|
59
|
+
Then run the matching registration command:
|
|
60
|
+
|
|
53
61
|
**Codex**
|
|
54
62
|
|
|
55
63
|
```
|
|
56
|
-
codex mcp add zas "--" npx -y zas-agent --profile codex
|
|
64
|
+
cmd /d /c codex mcp add zas "--" npx.cmd -y zas-agent@latest --profile codex
|
|
57
65
|
```
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
**Claude Code**
|
|
60
68
|
|
|
61
|
-
```toml
|
|
62
|
-
[mcp_servers.zas]
|
|
63
|
-
command = "npx"
|
|
64
|
-
args = ["-y", "zas-agent", "--profile", "codex"]
|
|
65
69
|
```
|
|
70
|
+
cmd /d /c claude mcp add zas "--" npx.cmd -y zas-agent@latest --profile claude-code
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
On macOS, Linux, or WSL, use `npx` in place of `npx.cmd` and omit
|
|
74
|
+
`cmd /d /c `. Pair and register in the same environment. Restart your agent
|
|
75
|
+
and ask it to send something to Zas.
|
|
76
|
+
|
|
77
|
+
If a command is not recognized, install Node.js or the named agent's CLI and
|
|
78
|
+
reopen the terminal. The first download needs internet access and may take a
|
|
79
|
+
few minutes. Use `@latest` as shown so npm requests the published package
|
|
80
|
+
instead of selecting an unbuilt local workspace.
|
|
66
81
|
|
|
67
82
|
### What pairing does
|
|
68
83
|
|
package/dist/cli.js
CHANGED
|
@@ -598,15 +598,27 @@ var ZasClient = class _ZasClient {
|
|
|
598
598
|
}
|
|
599
599
|
};
|
|
600
600
|
|
|
601
|
+
// src/shared/agent-snippets.ts
|
|
602
|
+
function agentSnippets(profile, windows = false, pkg = "zas-agent") {
|
|
603
|
+
const runner = windows ? "npx.cmd" : "npx";
|
|
604
|
+
const command = `${runner} -y ${pkg}@latest`;
|
|
605
|
+
const shell = windows ? "cmd /d /c " : "";
|
|
606
|
+
return {
|
|
607
|
+
pair: `${command} pair --profile ${profile}`,
|
|
608
|
+
claude: `${shell}claude mcp add zas "--" ${command} --profile ${profile}`,
|
|
609
|
+
codex: `${shell}codex mcp add zas "--" ${command} --profile ${profile}`
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
601
613
|
// src/snippets.ts
|
|
602
614
|
function packageName() {
|
|
603
615
|
return true ? "zas-agent" : "zas-agent";
|
|
604
616
|
}
|
|
605
|
-
function claudeSnippet(profile) {
|
|
606
|
-
return
|
|
617
|
+
function claudeSnippet(profile, windows = process.platform === "win32") {
|
|
618
|
+
return agentSnippets(profile, windows, packageName()).claude;
|
|
607
619
|
}
|
|
608
|
-
function codexSnippet(profile) {
|
|
609
|
-
return
|
|
620
|
+
function codexSnippet(profile, windows = process.platform === "win32") {
|
|
621
|
+
return agentSnippets(profile, windows, packageName()).codex;
|
|
610
622
|
}
|
|
611
623
|
function kindForProfile(profile) {
|
|
612
624
|
if (profile === "claude-code") return "claude_code";
|
|
@@ -830,6 +842,41 @@ async function runPair(opts) {
|
|
|
830
842
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
831
843
|
import { z } from "zod";
|
|
832
844
|
|
|
845
|
+
// src/shared/constants.ts
|
|
846
|
+
var CHUNKER_VERSION = "chunker_v1";
|
|
847
|
+
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
848
|
+
var CHUNK_AVG_BITS = 22;
|
|
849
|
+
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
850
|
+
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
851
|
+
var GEAR_SEED = "zas-gear-v1:";
|
|
852
|
+
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
853
|
+
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
854
|
+
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
855
|
+
var MANIFEST_VERSION = 1;
|
|
856
|
+
var MIN_EXPIRY_DAYS = 1;
|
|
857
|
+
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
858
|
+
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
859
|
+
var SAFE_STORED_FILE_LIMITS = {
|
|
860
|
+
anon: 30 * 1024 * 1024,
|
|
861
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
862
|
+
pro: 200 * 1024 * 1024,
|
|
863
|
+
max: 200 * 1024 * 1024
|
|
864
|
+
};
|
|
865
|
+
var STORED_FILE_LIMITS_V2 = {
|
|
866
|
+
anon: 30 * 1024 * 1024,
|
|
867
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
868
|
+
pro: 5 * 1024 * 1024 * 1024,
|
|
869
|
+
max: 20 * 1024 * 1024 * 1024
|
|
870
|
+
};
|
|
871
|
+
var STORAGE_LIMITS = {
|
|
872
|
+
anon: 30 * 1024 * 1024,
|
|
873
|
+
free: 100 * 1024 * 1024,
|
|
874
|
+
pro: null,
|
|
875
|
+
max: null
|
|
876
|
+
};
|
|
877
|
+
var THUMBNAIL_MAX_EDGE = 640;
|
|
878
|
+
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
879
|
+
|
|
833
880
|
// src/shared/direct.ts
|
|
834
881
|
var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
|
|
835
882
|
var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -854,6 +901,8 @@ function directFallbackCipherSize(size) {
|
|
|
854
901
|
}
|
|
855
902
|
var DIRECT_CONNECT_TIMEOUT_MS = 30 * 1e3;
|
|
856
903
|
var DIRECT_STALL_GRACE_MS = 5 * 1e3;
|
|
904
|
+
var DIRECT_CENSUS_WAIT_MS = 250;
|
|
905
|
+
var DIRECT_CONN_FAILED_SETTLE_MS = 1500;
|
|
857
906
|
var DIRECT_SIGNAL_WAIT_MS = 90 * 1e3;
|
|
858
907
|
|
|
859
908
|
// src/shared/direct-engine.ts
|
|
@@ -969,6 +1018,29 @@ function traceAdd(diag, line) {
|
|
|
969
1018
|
const at = t0 === void 0 ? 0 : (Date.now() - t0) / 1e3;
|
|
970
1019
|
diag.trace.push(`${at.toFixed(2)} ${line}`);
|
|
971
1020
|
}
|
|
1021
|
+
function usableForRelay(url) {
|
|
1022
|
+
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase();
|
|
1023
|
+
if (scheme !== "turn" && scheme !== "turns") return false;
|
|
1024
|
+
return !onPort53(url);
|
|
1025
|
+
}
|
|
1026
|
+
function onPort53(url) {
|
|
1027
|
+
return /^(stuns?|turns?):[^?]*:53(\?|$)/i.test(url);
|
|
1028
|
+
}
|
|
1029
|
+
function withoutPort53(servers) {
|
|
1030
|
+
return keepUrls(servers, (url) => !onPort53(url));
|
|
1031
|
+
}
|
|
1032
|
+
function relayIceServers(servers) {
|
|
1033
|
+
return keepUrls(servers, usableForRelay);
|
|
1034
|
+
}
|
|
1035
|
+
function keepUrls(servers, keep) {
|
|
1036
|
+
const kept = [];
|
|
1037
|
+
for (const server of servers) {
|
|
1038
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls ?? [];
|
|
1039
|
+
const usable = urls.filter(keep);
|
|
1040
|
+
if (usable.length > 0) kept.push({ ...server, urls: usable });
|
|
1041
|
+
}
|
|
1042
|
+
return kept;
|
|
1043
|
+
}
|
|
972
1044
|
function urlShape(url) {
|
|
973
1045
|
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
|
|
974
1046
|
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
@@ -1022,17 +1094,36 @@ function countTurnUrls(servers) {
|
|
|
1022
1094
|
}
|
|
1023
1095
|
function pairFromStats(stats) {
|
|
1024
1096
|
let pair;
|
|
1097
|
+
let selectedId;
|
|
1025
1098
|
stats.forEach((s) => {
|
|
1099
|
+
if (s.type === "transport" && typeof s.selectedCandidatePairId === "string") {
|
|
1100
|
+
selectedId = s.selectedCandidatePairId;
|
|
1101
|
+
}
|
|
1026
1102
|
if (s.type === "candidate-pair" && s.state === "succeeded" && (s.nominated || s.selected)) {
|
|
1027
1103
|
pair = s;
|
|
1028
1104
|
}
|
|
1029
1105
|
});
|
|
1106
|
+
if (selectedId) {
|
|
1107
|
+
const named = stats.get(selectedId);
|
|
1108
|
+
if (named?.localCandidateId && named.remoteCandidateId) pair = named;
|
|
1109
|
+
}
|
|
1030
1110
|
if (!pair?.localCandidateId || !pair.remoteCandidateId) return void 0;
|
|
1031
1111
|
const local = stats.get(pair.localCandidateId);
|
|
1032
1112
|
const remote = stats.get(pair.remoteCandidateId);
|
|
1033
1113
|
if (!local?.candidateType || !remote?.candidateType) return void 0;
|
|
1034
1114
|
return { local: local.candidateType, remote: remote.candidateType };
|
|
1035
1115
|
}
|
|
1116
|
+
function checkCensus(stats) {
|
|
1117
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1118
|
+
stats.forEach((s) => {
|
|
1119
|
+
if (s.type !== "candidate-pair") return;
|
|
1120
|
+
const state = String(s.state ?? "?");
|
|
1121
|
+
counts.set(state, (counts.get(state) ?? 0) + 1);
|
|
1122
|
+
});
|
|
1123
|
+
if (counts.size === 0) return "checks none";
|
|
1124
|
+
const parts = [...counts].map(([state, n]) => `${state}=${n}`);
|
|
1125
|
+
return `checks ${parts.join(" ")}`;
|
|
1126
|
+
}
|
|
1036
1127
|
function pathOf(pair) {
|
|
1037
1128
|
if (pair.local === "relay" || pair.remote === "relay") return "relay";
|
|
1038
1129
|
return pair.local === "host" && pair.remote === "host" ? "lan" : "wan";
|
|
@@ -1040,8 +1131,14 @@ function pathOf(pair) {
|
|
|
1040
1131
|
function hasRemoteDescription(pc) {
|
|
1041
1132
|
return !!pc.remoteDescription?.sdp;
|
|
1042
1133
|
}
|
|
1043
|
-
function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
1044
|
-
const
|
|
1134
|
+
function session(ice, onPhase, onDiag, iceTransportPolicy = "all", skipPort53Urls = false) {
|
|
1135
|
+
const trimIce = (servers) => {
|
|
1136
|
+
if (!skipPort53Urls) return servers;
|
|
1137
|
+
if (iceTransportPolicy !== "relay") return withoutPort53(servers);
|
|
1138
|
+
const trimmed = relayIceServers(servers);
|
|
1139
|
+
return countTurnUrls(trimmed) > 0 ? trimmed : servers;
|
|
1140
|
+
};
|
|
1141
|
+
const initialIce = trimIce(ice ?? DIRECT_ICE);
|
|
1045
1142
|
const pc = new RTCPeerConnection({
|
|
1046
1143
|
iceServers: initialIce,
|
|
1047
1144
|
iceTransportPolicy,
|
|
@@ -1059,6 +1156,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1059
1156
|
let phase = "connecting";
|
|
1060
1157
|
let disconnectTimer;
|
|
1061
1158
|
let restartTimer;
|
|
1159
|
+
let connFailTimer;
|
|
1062
1160
|
let stallState = "idle";
|
|
1063
1161
|
const startedAt = Date.now();
|
|
1064
1162
|
const diag = {
|
|
@@ -1081,12 +1179,32 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1081
1179
|
};
|
|
1082
1180
|
traceStart.set(diag, startedAt);
|
|
1083
1181
|
traceAdd(diag, `ice servers ${iceShapes(initialIce)} policy=${iceTransportPolicy}`);
|
|
1182
|
+
const notePair = (pair) => {
|
|
1183
|
+
if (diag.pairLocal === pair.local && diag.pairRemote === pair.remote) return;
|
|
1184
|
+
const changed = diag.pairLocal !== void 0;
|
|
1185
|
+
diag.pairLocal = pair.local;
|
|
1186
|
+
diag.pairRemote = pair.remote;
|
|
1187
|
+
traceAdd(diag, `${changed ? "pair now" : "pair"} ${pair.local}/${pair.remote}`);
|
|
1188
|
+
};
|
|
1084
1189
|
pc.onicegatheringstatechange = () => {
|
|
1085
1190
|
traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
|
|
1086
1191
|
};
|
|
1087
1192
|
const connState = () => String(pc.connectionState ?? "");
|
|
1088
1193
|
pc.onconnectionstatechange = () => {
|
|
1089
|
-
|
|
1194
|
+
const state = connState();
|
|
1195
|
+
traceAdd(diag, `conn ${state}`);
|
|
1196
|
+
if (state !== "failed") {
|
|
1197
|
+
clearTimeout(connFailTimer);
|
|
1198
|
+
connFailTimer = void 0;
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (connFailTimer !== void 0) return;
|
|
1202
|
+
connFailTimer = setTimeout(() => {
|
|
1203
|
+
connFailTimer = void 0;
|
|
1204
|
+
if (connState() !== "failed") return;
|
|
1205
|
+
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1206
|
+
else failWithCensus("ice_failed");
|
|
1207
|
+
}, DIRECT_CONN_FAILED_SETTLE_MS);
|
|
1090
1208
|
};
|
|
1091
1209
|
let channel;
|
|
1092
1210
|
pc.onicecandidateerror = (e) => {
|
|
@@ -1095,6 +1213,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1095
1213
|
};
|
|
1096
1214
|
const cleanup = () => {
|
|
1097
1215
|
clearTimeout(waitTimer);
|
|
1216
|
+
clearTimeout(connFailTimer);
|
|
1098
1217
|
clearTimeout(connectTimer);
|
|
1099
1218
|
clearTimeout(disconnectTimer);
|
|
1100
1219
|
clearTimeout(restartTimer);
|
|
@@ -1134,6 +1253,20 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1134
1253
|
}
|
|
1135
1254
|
setPhase("failed");
|
|
1136
1255
|
};
|
|
1256
|
+
const failWithCensus = (reason) => {
|
|
1257
|
+
if (phase === "done" || phase === "failed") return;
|
|
1258
|
+
let reported = false;
|
|
1259
|
+
const report2 = () => {
|
|
1260
|
+
if (reported) return;
|
|
1261
|
+
reported = true;
|
|
1262
|
+
fail(reason);
|
|
1263
|
+
};
|
|
1264
|
+
const guard = setTimeout(report2, DIRECT_CENSUS_WAIT_MS);
|
|
1265
|
+
void pc.getStats().then((stats) => traceAdd(diag, checkCensus(stats))).catch(() => void 0).then(() => {
|
|
1266
|
+
clearTimeout(guard);
|
|
1267
|
+
report2();
|
|
1268
|
+
});
|
|
1269
|
+
};
|
|
1137
1270
|
const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
|
|
1138
1271
|
let connectTimer;
|
|
1139
1272
|
let heard = false;
|
|
@@ -1143,7 +1276,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1143
1276
|
clearTimeout(waitTimer);
|
|
1144
1277
|
connectTimer = setTimeout(() => {
|
|
1145
1278
|
const iceUp = pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed";
|
|
1146
|
-
|
|
1279
|
+
failWithCensus(iceUp ? "channel_never_opened" : "connect_timeout");
|
|
1147
1280
|
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1148
1281
|
};
|
|
1149
1282
|
let onStall;
|
|
@@ -1153,7 +1286,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1153
1286
|
clearTimeout(disconnectTimer);
|
|
1154
1287
|
if (stallState === "restarting") return;
|
|
1155
1288
|
if (diag.msConnect === void 0 || !onStall) {
|
|
1156
|
-
|
|
1289
|
+
failWithCensus(diag.msConnect === void 0 ? "ice_failed" : "disconnected");
|
|
1157
1290
|
return;
|
|
1158
1291
|
}
|
|
1159
1292
|
stallState = "restarting";
|
|
@@ -1168,7 +1301,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1168
1301
|
traceAdd(diag, `ice ${String(s ?? "")}`);
|
|
1169
1302
|
if (s === "failed") {
|
|
1170
1303
|
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1171
|
-
else
|
|
1304
|
+
else failWithCensus("ice_failed");
|
|
1172
1305
|
}
|
|
1173
1306
|
if (s === "disconnected" && stallState === "idle") {
|
|
1174
1307
|
stallState = "grace";
|
|
@@ -1180,10 +1313,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1180
1313
|
stallState = "idle";
|
|
1181
1314
|
void pc.getStats().then((stats) => {
|
|
1182
1315
|
const pair = pairFromStats(stats);
|
|
1183
|
-
if (
|
|
1184
|
-
diag.pairLocal = pair.local;
|
|
1185
|
-
diag.pairRemote = pair.remote;
|
|
1186
|
-
traceAdd(diag, `pair ${pair.local}/${pair.remote}`);
|
|
1316
|
+
if (pair) notePair(pair);
|
|
1187
1317
|
}).catch(() => void 0);
|
|
1188
1318
|
}
|
|
1189
1319
|
};
|
|
@@ -1208,10 +1338,12 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1208
1338
|
onStall = fn;
|
|
1209
1339
|
stallWindowMs = windowMs;
|
|
1210
1340
|
},
|
|
1341
|
+
notePair,
|
|
1211
1342
|
setIceServers: (iceServers) => {
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1343
|
+
const next = trimIce(iceServers);
|
|
1344
|
+
diag.turnUrlsSupplied = countTurnUrls(next);
|
|
1345
|
+
traceAdd(diag, `ice servers replaced ${iceShapes(next)}`);
|
|
1346
|
+
pc.setConfiguration({ iceServers: next });
|
|
1215
1347
|
}
|
|
1216
1348
|
};
|
|
1217
1349
|
}
|
|
@@ -1231,8 +1363,14 @@ function signalPlumbing(pc, send, onSendError, diag) {
|
|
|
1231
1363
|
generation: localGeneration
|
|
1232
1364
|
}).catch(onSendError);
|
|
1233
1365
|
};
|
|
1366
|
+
let applyRejects = 0;
|
|
1234
1367
|
const apply = (candidate) => {
|
|
1235
|
-
void pc.addIceCandidate(candidate ?? void 0).catch(() =>
|
|
1368
|
+
void pc.addIceCandidate(candidate ?? void 0).catch((e) => {
|
|
1369
|
+
if (applyRejects >= 3) return;
|
|
1370
|
+
applyRejects++;
|
|
1371
|
+
const name = e?.name || "Error";
|
|
1372
|
+
traceAdd(diag, `ice apply rejected ${candidate ? candType(candidate) : "end"} ${name}`);
|
|
1373
|
+
});
|
|
1236
1374
|
};
|
|
1237
1375
|
const applyIce = (candidate, generation) => {
|
|
1238
1376
|
countCand(diag, "remote", candidate);
|
|
@@ -1273,7 +1411,13 @@ function signalPlumbing(pc, send, onSendError, diag) {
|
|
|
1273
1411
|
};
|
|
1274
1412
|
}
|
|
1275
1413
|
function startSender(opts) {
|
|
1276
|
-
const s = session(
|
|
1414
|
+
const s = session(
|
|
1415
|
+
opts.ice,
|
|
1416
|
+
opts.onPhase,
|
|
1417
|
+
opts.onDiag,
|
|
1418
|
+
opts.iceTransportPolicy,
|
|
1419
|
+
opts.skipPort53Urls
|
|
1420
|
+
);
|
|
1277
1421
|
opts.onPhase("connecting");
|
|
1278
1422
|
const { pc } = s;
|
|
1279
1423
|
const plumbing = signalPlumbing(pc, opts.send, () => s.fail("signal_send"), s.diag);
|
|
@@ -1355,8 +1499,7 @@ function startSender(opts) {
|
|
|
1355
1499
|
void pc.getStats().then((stats) => {
|
|
1356
1500
|
const pair = pairFromStats(stats);
|
|
1357
1501
|
if (!pair) return;
|
|
1358
|
-
s.
|
|
1359
|
-
s.diag.pairRemote = pair.remote;
|
|
1502
|
+
s.notePair(pair);
|
|
1360
1503
|
opts.onPath?.(pathOf(pair));
|
|
1361
1504
|
}).catch(() => void 0);
|
|
1362
1505
|
void pump().catch((e) => s.fail("transfer", e));
|
|
@@ -1426,7 +1569,8 @@ function startReceiver(opts) {
|
|
|
1426
1569
|
opts.onPhase(p);
|
|
1427
1570
|
},
|
|
1428
1571
|
opts.onDiag,
|
|
1429
|
-
opts.iceTransportPolicy
|
|
1572
|
+
opts.iceTransportPolicy,
|
|
1573
|
+
opts.skipPort53Urls
|
|
1430
1574
|
);
|
|
1431
1575
|
s.setStall(() => void 0, DIRECT_CONNECT_TIMEOUT_MS * 2);
|
|
1432
1576
|
opts.onPhase("connecting");
|
|
@@ -1452,8 +1596,7 @@ function startReceiver(opts) {
|
|
|
1452
1596
|
void pc.getStats().then((stats) => {
|
|
1453
1597
|
const pair = pairFromStats(stats);
|
|
1454
1598
|
if (!pair) return;
|
|
1455
|
-
s.
|
|
1456
|
-
s.diag.pairRemote = pair.remote;
|
|
1599
|
+
s.notePair(pair);
|
|
1457
1600
|
opts.onPath?.(pathOf(pair));
|
|
1458
1601
|
}).catch(() => void 0);
|
|
1459
1602
|
};
|
|
@@ -1568,42 +1711,6 @@ import { basename as basename2 } from "node:path";
|
|
|
1568
1711
|
|
|
1569
1712
|
// src/shared/manifest.ts
|
|
1570
1713
|
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
1571
|
-
|
|
1572
|
-
// src/shared/constants.ts
|
|
1573
|
-
var CHUNKER_VERSION = "chunker_v1";
|
|
1574
|
-
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
1575
|
-
var CHUNK_AVG_BITS = 22;
|
|
1576
|
-
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
1577
|
-
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
1578
|
-
var GEAR_SEED = "zas-gear-v1:";
|
|
1579
|
-
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
1580
|
-
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
1581
|
-
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
1582
|
-
var MANIFEST_VERSION = 1;
|
|
1583
|
-
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
1584
|
-
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
1585
|
-
var SAFE_STORED_FILE_LIMITS = {
|
|
1586
|
-
anon: 30 * 1024 * 1024,
|
|
1587
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
1588
|
-
pro: 200 * 1024 * 1024,
|
|
1589
|
-
max: 200 * 1024 * 1024
|
|
1590
|
-
};
|
|
1591
|
-
var STORED_FILE_LIMITS_V2 = {
|
|
1592
|
-
anon: 30 * 1024 * 1024,
|
|
1593
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
1594
|
-
pro: 5 * 1024 * 1024 * 1024,
|
|
1595
|
-
max: 20 * 1024 * 1024 * 1024
|
|
1596
|
-
};
|
|
1597
|
-
var STORAGE_LIMITS = {
|
|
1598
|
-
anon: 30 * 1024 * 1024,
|
|
1599
|
-
free: 100 * 1024 * 1024,
|
|
1600
|
-
pro: null,
|
|
1601
|
-
max: null
|
|
1602
|
-
};
|
|
1603
|
-
var THUMBNAIL_MAX_EDGE = 640;
|
|
1604
|
-
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
1605
|
-
|
|
1606
|
-
// src/shared/manifest.ts
|
|
1607
1714
|
function newManifest(partial) {
|
|
1608
1715
|
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
1609
1716
|
}
|
|
@@ -1644,6 +1751,46 @@ function decryptChannelName(channelKey, sealed) {
|
|
|
1644
1751
|
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
1645
1752
|
}
|
|
1646
1753
|
|
|
1754
|
+
// src/file-source.ts
|
|
1755
|
+
import { promises as fsp, statSync } from "node:fs";
|
|
1756
|
+
async function readFileRange(path, start, length) {
|
|
1757
|
+
const fh = await fsp.open(path, "r");
|
|
1758
|
+
try {
|
|
1759
|
+
const buf = Buffer.allocUnsafe(Math.max(0, length));
|
|
1760
|
+
let read = 0;
|
|
1761
|
+
while (read < length) {
|
|
1762
|
+
const { bytesRead } = await fh.read(buf, read, length - read, start + read);
|
|
1763
|
+
if (bytesRead === 0) break;
|
|
1764
|
+
read += bytesRead;
|
|
1765
|
+
}
|
|
1766
|
+
const out = new Uint8Array(read);
|
|
1767
|
+
out.set(buf.subarray(0, read));
|
|
1768
|
+
return out.buffer;
|
|
1769
|
+
} finally {
|
|
1770
|
+
await fh.close();
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function directFileFrom(path, raw) {
|
|
1774
|
+
let statSize;
|
|
1775
|
+
try {
|
|
1776
|
+
statSize = statSync(path).size;
|
|
1777
|
+
} catch {
|
|
1778
|
+
return raw;
|
|
1779
|
+
}
|
|
1780
|
+
if (statSize <= raw.size) return raw;
|
|
1781
|
+
const type = raw.type;
|
|
1782
|
+
const file = {
|
|
1783
|
+
size: statSize,
|
|
1784
|
+
type,
|
|
1785
|
+
slice(start, end) {
|
|
1786
|
+
const from = Math.max(0, Math.min(start, statSize));
|
|
1787
|
+
const to = Math.max(from, Math.min(end, statSize));
|
|
1788
|
+
return { arrayBuffer: () => readFileRange(path, from, to - from) };
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
return file;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1647
1794
|
// src/shared/direct-fallback.ts
|
|
1648
1795
|
function createFallbackMeta(file) {
|
|
1649
1796
|
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -1950,7 +2097,7 @@ function resolveChannel(identity, grants, channel) {
|
|
|
1950
2097
|
}
|
|
1951
2098
|
|
|
1952
2099
|
// src/send.ts
|
|
1953
|
-
import { promises as
|
|
2100
|
+
import { promises as fsp2 } from "node:fs";
|
|
1954
2101
|
import { basename, extname } from "node:path";
|
|
1955
2102
|
|
|
1956
2103
|
// src/shared/chunker.ts
|
|
@@ -2226,6 +2373,10 @@ async function receiptKey(ctx, channelId, contentHash, title) {
|
|
|
2226
2373
|
const raw = `${ctx.identity.agent_uid}\0${channelId}\0${contentHash}\0${title}`;
|
|
2227
2374
|
return blake3Hex(new TextEncoder().encode(raw));
|
|
2228
2375
|
}
|
|
2376
|
+
async function sendIdentity(contentHash, days) {
|
|
2377
|
+
if (days === void 0) return contentHash;
|
|
2378
|
+
return blake3Hex(new TextEncoder().encode(`${contentHash}\0expires_in_days:${days}`));
|
|
2379
|
+
}
|
|
2229
2380
|
function receiptFor(ctx, key) {
|
|
2230
2381
|
const entries = loadFingerprints(ctx.profile).entries;
|
|
2231
2382
|
if (!Object.prototype.hasOwnProperty.call(entries, key)) return void 0;
|
|
@@ -2374,7 +2525,7 @@ async function placeChunks(ctx, encs) {
|
|
|
2374
2525
|
};
|
|
2375
2526
|
});
|
|
2376
2527
|
}
|
|
2377
|
-
async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
2528
|
+
async function postLink(ctx, grant, manifest, placed, idempotencyKey, expiresInDays) {
|
|
2378
2529
|
const channelKey = channelKeyOf(ctx.identity, grant);
|
|
2379
2530
|
const sealed = sealManifest(channelKey, manifest, grant.key_version);
|
|
2380
2531
|
const created = await ctx.client.api("POST", "/links", {
|
|
@@ -2385,7 +2536,10 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
|
2385
2536
|
// nothing left for the server to verify here. The field stays because the
|
|
2386
2537
|
// server reads it, and an absent array is not the same as an empty one.
|
|
2387
2538
|
proofs: [],
|
|
2388
|
-
idempotency_key: idempotencyKey
|
|
2539
|
+
idempotency_key: idempotencyKey,
|
|
2540
|
+
// Absent unless the caller asked. The server clamps whatever arrives to
|
|
2541
|
+
// the account's own plan, so this can only ever shorten the item's life.
|
|
2542
|
+
...expiresInDays === void 0 ? {} : { expires_in_days: expiresInDays }
|
|
2389
2543
|
});
|
|
2390
2544
|
const bound = created.caps ?? {};
|
|
2391
2545
|
let patched = false;
|
|
@@ -2404,7 +2558,7 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
|
2404
2558
|
return created.link_id;
|
|
2405
2559
|
}
|
|
2406
2560
|
async function sendFile(ctx, input, onPhase) {
|
|
2407
|
-
const stat = await
|
|
2561
|
+
const stat = await fsp2.stat(input.path).catch(() => {
|
|
2408
2562
|
throw new ZasError("upload_failed", 400);
|
|
2409
2563
|
});
|
|
2410
2564
|
if (!stat.isFile()) throw new ZasError("upload_failed", 400);
|
|
@@ -2412,10 +2566,10 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2412
2566
|
const grant = await grantFor(ctx, input.channel);
|
|
2413
2567
|
const name = basename(input.path);
|
|
2414
2568
|
const title = input.title ?? name;
|
|
2415
|
-
const file = await
|
|
2569
|
+
const file = await fsp2.readFile(input.path);
|
|
2416
2570
|
if (file.byteLength > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
|
|
2417
2571
|
const bytes = new Uint8Array(file.buffer, file.byteOffset, file.byteLength);
|
|
2418
|
-
const contentHash = await blake3Hex(bytes);
|
|
2572
|
+
const contentHash = await sendIdentity(await blake3Hex(bytes), input.expires_in_days);
|
|
2419
2573
|
const key = await receiptKey(ctx, grant.channel_id, contentHash, title);
|
|
2420
2574
|
const stored = receiptFor(ctx, key);
|
|
2421
2575
|
if (stored) {
|
|
@@ -2466,7 +2620,8 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2466
2620
|
grant,
|
|
2467
2621
|
manifest,
|
|
2468
2622
|
placed,
|
|
2469
|
-
agentSendIdempotencyKey(grant.channel_id, contentHash, title)
|
|
2623
|
+
agentSendIdempotencyKey(grant.channel_id, contentHash, title),
|
|
2624
|
+
input.expires_in_days
|
|
2470
2625
|
);
|
|
2471
2626
|
const deduplicated = placed.filter((p) => p.proven).length;
|
|
2472
2627
|
remember(ctx, key, {
|
|
@@ -2493,9 +2648,12 @@ async function sendNote(ctx, input) {
|
|
|
2493
2648
|
const grant = await grantFor(ctx, input.channel);
|
|
2494
2649
|
const name = input.title ?? noteName(input.text);
|
|
2495
2650
|
const encoded = new TextEncoder().encode(input.text);
|
|
2496
|
-
const contentHash = await
|
|
2497
|
-
|
|
2498
|
-
|
|
2651
|
+
const contentHash = await sendIdentity(
|
|
2652
|
+
await blake3Hex(new TextEncoder().encode(
|
|
2653
|
+
`${input.lang ?? ""}\0${input.secret ? "1" : "0"}\0${input.text}`
|
|
2654
|
+
)),
|
|
2655
|
+
input.expires_in_days
|
|
2656
|
+
);
|
|
2499
2657
|
const key = await receiptKey(ctx, grant.channel_id, contentHash, name);
|
|
2500
2658
|
const stored = receiptFor(ctx, key);
|
|
2501
2659
|
if (stored) {
|
|
@@ -2528,7 +2686,8 @@ async function sendNote(ctx, input) {
|
|
|
2528
2686
|
grant,
|
|
2529
2687
|
manifest,
|
|
2530
2688
|
[],
|
|
2531
|
-
agentSendIdempotencyKey(grant.channel_id, contentHash, name)
|
|
2689
|
+
agentSendIdempotencyKey(grant.channel_id, contentHash, name),
|
|
2690
|
+
input.expires_in_days
|
|
2532
2691
|
);
|
|
2533
2692
|
remember(ctx, key, {
|
|
2534
2693
|
link_id: linkId,
|
|
@@ -2603,9 +2762,10 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2603
2762
|
const grant = await directGrantFor(ctx, input.channel);
|
|
2604
2763
|
const key = channelKeyOf(ctx.identity, grant);
|
|
2605
2764
|
const channelName = channelLabel(ctx, grant);
|
|
2606
|
-
const
|
|
2765
|
+
const raw = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
|
|
2607
2766
|
throw new ZasError("upload_failed", 400);
|
|
2608
2767
|
});
|
|
2768
|
+
const file = directFileFrom(input.path, raw);
|
|
2609
2769
|
if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
|
|
2610
2770
|
const name = basename2(input.path);
|
|
2611
2771
|
const cid = grant.channel_id;
|
|
@@ -2826,7 +2986,7 @@ import {
|
|
|
2826
2986
|
openSync,
|
|
2827
2987
|
renameSync as renameSync2,
|
|
2828
2988
|
rmdirSync,
|
|
2829
|
-
statSync,
|
|
2989
|
+
statSync as statSync2,
|
|
2830
2990
|
unlinkSync,
|
|
2831
2991
|
writeSync
|
|
2832
2992
|
} from "node:fs";
|
|
@@ -2999,7 +3159,7 @@ function destinationOf(dest, name, fallback) {
|
|
|
2999
3159
|
const created = mkdtempSync(join2(tmpdir(), DOWNLOAD_PREFIX));
|
|
3000
3160
|
return { target: freeName(join2(created, base)), created };
|
|
3001
3161
|
}
|
|
3002
|
-
const at =
|
|
3162
|
+
const at = statSync2(dest, { throwIfNoEntry: false })?.isDirectory() ? join2(dest, base) : dest;
|
|
3003
3163
|
return { target: freeName(at) };
|
|
3004
3164
|
}
|
|
3005
3165
|
async function getItem(ctx, channel, id, dest) {
|
|
@@ -3928,11 +4088,12 @@ async function report(client, properties) {
|
|
|
3928
4088
|
|
|
3929
4089
|
// src/server.ts
|
|
3930
4090
|
function agentVersion() {
|
|
3931
|
-
return true ? "0.
|
|
4091
|
+
return true ? "0.7.0" : "0.0.0-dev";
|
|
3932
4092
|
}
|
|
3933
4093
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3934
4094
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
3935
4095
|
var NOTE_LABEL_MAX = 40;
|
|
4096
|
+
var EXPIRES_IN_DAYS = z.number().int().min(MIN_EXPIRY_DAYS).optional().describe("How many whole days the item should live for, at least 1. Leave it out for the account's normal life (5 days). It can only shorten an item, never extend one: a longer request is clamped to what the plan grants. Worth setting for output that is stale tomorrow, such as a build log or a test run.");
|
|
3936
4097
|
var delay = (ms) => new Promise((resolve2) => {
|
|
3937
4098
|
setTimeout(resolve2, ms);
|
|
3938
4099
|
});
|
|
@@ -4109,7 +4270,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4109
4270
|
inputSchema: {
|
|
4110
4271
|
path: z.string().describe("Absolute or relative path of the file to send."),
|
|
4111
4272
|
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4112
|
-
title: z.string().optional().describe("Label for the item. Defaults to the file name.")
|
|
4273
|
+
title: z.string().optional().describe("Label for the item. Defaults to the file name."),
|
|
4274
|
+
expires_in_days: EXPIRES_IN_DAYS
|
|
4113
4275
|
}
|
|
4114
4276
|
}, async (input) => {
|
|
4115
4277
|
try {
|
|
@@ -4238,7 +4400,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4238
4400
|
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4239
4401
|
title: z.string().optional().describe("Label for the item. Defaults to the first line."),
|
|
4240
4402
|
lang: z.string().optional().describe('Language of the snippet, for highlighting (for example "ts", "py").'),
|
|
4241
|
-
secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it.")
|
|
4403
|
+
secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it."),
|
|
4404
|
+
expires_in_days: EXPIRES_IN_DAYS
|
|
4242
4405
|
}
|
|
4243
4406
|
}, async (input) => {
|
|
4244
4407
|
try {
|
package/package.json
CHANGED