srift-transfer 4.3.0 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/daemon.js +365 -218
- package/dist/index.js +401 -221
- package/package.json +9 -41
package/dist/index.js
CHANGED
|
@@ -1806,7 +1806,7 @@ var init_skills_data = __esm({
|
|
|
1806
1806
|
{ name: "encrypted-chat", content: "---\nname: encrypted-chat\ndescription: Send and read end-to-end encrypted chat messages and manage in-session file transfers within an active SRIFT session\n---\n\n# encrypted-chat\n\nSend and read end-to-end encrypted chat messages within an active SRIFT session, plus manage in-session file transfers, via the local MCP daemon.\n\n## Requirements\n\nAn active SRIFT session (see the `session-management` skill to open or join one first). No special credentials required.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_send_chat` \u2014 send an E2EE chat message to peers in the current session.\n- `srift_chat_history` \u2014 read decrypted chat history for the current session.\n- `srift_send_file` \u2014 offer a file to peers already inside the session (in-session transfer, distinct from `quick-share`'s standalone link flow).\n- `srift_accept_transfer` \u2014 accept and download an inbound file offer.\n- `srift_list_transfers` \u2014 list all active and recent transfers with progress.\n- `srift_read_state` \u2014 read the raw `.srift-state.json` snapshot for debugging/observability.\n\nYour local daemon (or the browser) encrypts messages and file chunks with AES-256-GCM before they leave the device, under ECDH P-256 keys generated on the devices (the optional `roomSecret` is mixed in); the relay only forwards ciphertext and public keys and stores nothing. Browsers exchange chat peer to peer; a copy goes through the relay, sealed for their keys, only for CLI/agent members. Peers older than 4.2 fall back to the previous session key, which the server could derive without a `roomSecret`.\n" },
|
|
1807
1807
|
{ name: "get-company-info", content: "---\nname: get-company-info\ndescription: Overview of SRIFT, the open-source (MIT) project - what it does, how to install it, and where the agent docs are\n---\n\n# get-company-info\n\nOverview of SRIFT: an open-source (MIT) project for end-to-end encrypted file transfer, chat and agent-to-agent messaging. SRIFT is a project, not a company.\n\n## Requirements\n\nNo special credentials required.\n\n## Instructions\n\n- What it is and how to use it: read `https://srift.app/llms.txt` (short) or `https://srift.app/llms-full.txt` (complete).\n- Agent manual (CLI, MCP tools, AgentNet, environment variables): `https://srift.app/AGENTS.md`.\n- Install: `npm i -g srift-transfer`, `pip install srift`, or `curl -fsSL https://srift.app/install.sh | sh`.\n- Source code: `https://github.com/srivardhan113/SRIFT-Open_Source`. Security contact: `https://srift.app/.well-known/security.txt`.\n" },
|
|
1808
1808
|
{ name: "quick-share", content: "---\nname: quick-share\ndescription: Zero-install file delivery: seed a file and get a public download URL, no session setup required on the recipient's end\n---\n\n# quick-share\n\nZero-install file delivery via SRIFT's local MCP server. Returns a public `https://srift.app/d/<token>` relay link that any recipient can open in a browser \u2014 no SRIFT install, no signup, no session-join UI required on their end. The bytes stream from the sender's machine through the srift.app relay on demand (pass-through, `Cache-Control: no-store`); nothing is stored on any server, so the link works while the sender's daemon (or process) runs.\n\n## Requirements\n\nNo credentials. The SRIFT local daemon binds to `127.0.0.1:3822` with zero authentication. Only outbound HTTPS/WSS on port 443 is needed.\n\n## Instructions\n\n1. Call the MCP tool `srift_quick_share` with `{ filePath: \"/absolute/path/to/file\" }` (single file) or `{ filePaths: [...] }` (up to 500 files/folders). Optional params: `bundle` (default: true = one .tar.gz link; false = one link per path, created in parallel), `bundleName`, `exclude` (string[] for glob patterns to skip), `sessionName`, `maxDownloads`, `ttlMs`, `encrypt`, `password`.\n2. The tool returns a `downloadUrl` (e.g. `https://srift.app/d/<token>`) when bundled, or `{ links: [], errors: [] }` when separate. With `encrypt: true` (or a `password`) each link is end-to-end encrypted with AES-256-GCM: the key rides in the `#k=` URL fragment, is never sent to a server, and the browser decrypts locally. Without it, links are protected by TLS in transit only \u2014 use `encrypt` for anything sensitive.\n3. Hand the URL(s) to the recipient. In order of preference, they can run:\n - `srift get <url> [<url> ...]` (or `npx -y srift-transfer get <url>`; proxy-aware, resumable, decrypts `#k=` links, supports multiple URLs with `--concurrency N`, works where curl is broken)\n - `wget --content-disposition <url>`\n - PowerShell `iwr <url> -OutFile <name>`\n - `curl -fLOJ <url>`\n - or just open it in any browser.\n4. Keep the sending side running until the recipient has downloaded. From a shell, `srift quick-share <file> [<file> ...] --wait` blocks until then (`--wait-timeout 30m` exits with code 3 on timeout); `--keep-alive` serves until the TTL expires.\n\n## Sandboxed and datacenter agents\n\nIf the background daemon cannot start (the sandbox forbids local servers, blocks detached spawns or loopback, or the port is taken), `srift mcp` and `srift quick-share` host the daemon inside their own process: no local port, outbound HTTPS/WSS on 443 only. The MCP server switches automatically, so this tool keeps working. From a shell, run quick-share in the background (it exits after the download) or with `--wait`; `--foreground` forces in-process serving. Blocked UDP never breaks links. Run `srift doctor` (or the `srift_net_diagnose` tool) to diagnose connectivity; `HTTPS_PROXY`/`NO_PROXY` and `NODE_EXTRA_CA_CERTS` are honored.\n\nSee `/openapi.json` and `/.well-known/mcp/server-card.json` for the full tool contract.\n" },
|
|
1809
|
-
{ name: "session-management", content: '---\nname: session-management\ndescription: Create, join, monitor, and control SRIFT end-to-end encrypted P2P sessions via the local MCP daemon\n---\n\n# session-management\n\nCreate, join, monitor, and control SRIFT end-to-end encrypted P2P sessions ("rooms") via the local MCP daemon. Covers the full session lifecycle used for in-session file transfer and encrypted chat.\n\n## Requirements\n\nNo special credentials required. The SRIFT local daemon binds to `127.0.0.1:3822` and requires zero authentication.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_start_session` \u2014 open a new E2EE room as host.\n- `srift_join_session` \u2014 join an existing room by its 7-character session ID.\n- `srift_session_status` \u2014 read current session, role, connected peers, and pending join requests.\n- `srift_approve_join` / `srift_reject_join` \u2014 host-only: approve or reject a guest\'s join request.\n- `srift_kick_user` \u2014 host-only: remove a peer. Get their `userId` from `srift_session_status` \u2192 `participants`.\n- `srift_close_session` \u2014 tear down the session and flush all encryption keys.\n\nSessions are ephemeral: the server has no database and keeps only session metadata in memory until the session is deleted (host close or 7 days of inactivity); chat, audio and files are never stored. Keys are generated on each device (ECDH P-256 + HKDF-SHA256, AES-256-GCM) and never sent to the server; the optional `roomSecret` is mixed into every key, so pass the same one to `srift_start_session` / `srift_join_session` to keep out anyone who does not know it.\n' }
|
|
1809
|
+
{ name: "session-management", content: '---\nname: session-management\ndescription: Create, join, monitor, and control SRIFT end-to-end encrypted P2P sessions via the local MCP daemon\n---\n\n# session-management\n\nCreate, join, monitor, and control SRIFT end-to-end encrypted P2P sessions ("rooms") via the local MCP daemon. Covers the full session lifecycle used for in-session file transfer and encrypted chat.\n\n## Requirements\n\nNo special credentials required. The SRIFT local daemon binds to `127.0.0.1:3822` and requires zero authentication.\n\n## Instructions\n\nAvailable MCP tools (see `/.well-known/mcp/server-card.json` for full schemas):\n\n- `srift_start_session` \u2014 open a new E2EE room as host.\n- `srift_join_session` \u2014 join an existing room by its 7-character session ID.\n- `srift_session_status` \u2014 read current session, role, connected peers, and pending join requests.\n- `srift_approve_join` / `srift_reject_join` \u2014 host-only: approve or reject a guest\'s join request. A person who opens the join link in a browser appears in pending join requests only after pressing "Request to Join"; `srift_join_session` asks the host at once.\n- `srift_kick_user` \u2014 host-only: remove a peer. Get their `userId` from `srift_session_status` \u2192 `participants`.\n- `srift_close_session` \u2014 tear down the session and flush all encryption keys.\n\nSessions are ephemeral: the server has no database and keeps only session metadata in memory until the session is deleted (host close or 7 days of inactivity); chat, audio and files are never stored. Keys are generated on each device (ECDH P-256 + HKDF-SHA256, AES-256-GCM) and never sent to the server; the optional `roomSecret` is mixed into every key, so pass the same one to `srift_start_session` / `srift_join_session` to keep out anyone who does not know it.\n' }
|
|
1810
1810
|
];
|
|
1811
1811
|
MCP_SKILLS = SOURCES.map(({ name, content }) => {
|
|
1812
1812
|
const uri = `skill://${name}/SKILL.md`;
|
|
@@ -1847,7 +1847,7 @@ function buildPromptMessages(name, args) {
|
|
|
1847
1847
|
1) Call srift_start_session.
|
|
1848
1848
|
2) Give the user the join URL: https://srift.app/join-session?id=<SESSION_ID> (full session UI with chat + multi-file transfer).
|
|
1849
1849
|
Or, for a single one-shot file from agent \u2192 user, prefer srift_quick_share which returns a direct download URL.
|
|
1850
|
-
3) Poll srift_session_status; when pendingJoins appears, call srift_approve_join.
|
|
1850
|
+
3) Poll srift_session_status; when pendingJoins appears, call srift_approve_join. A person who opens the link only sees a preview until they press "Request to Join"; they appear in pendingJoins after that.
|
|
1851
1851
|
4) When a file_offer arrives (visible via srift_list_transfers), call srift_accept_transfer with saveDir="${args?.saveDir || "./"}".`
|
|
1852
1852
|
}
|
|
1853
1853
|
}];
|
|
@@ -2044,7 +2044,7 @@ function createMcpHandler(opts) {
|
|
|
2044
2044
|
}
|
|
2045
2045
|
};
|
|
2046
2046
|
}
|
|
2047
|
-
var PROTOCOL_VERSION, SUPPORTED_LEGACY_PROTOCOL_VERSIONS, LATEST_INITIALIZE_ERA_VERSION, ALL_SUPPORTED_PROTOCOL_VERSIONS, META_PROTOCOL_VERSION, META_SERVER_INFO, ERROR_UNSUPPORTED_PROTOCOL_VERSION, MCP_TOOLS, MCP_RESOURCES, MCP_PROMPTS, QUICKSTART_DOC, DEFAULT_TTL_MS;
|
|
2047
|
+
var PROTOCOL_VERSION, SUPPORTED_LEGACY_PROTOCOL_VERSIONS, LATEST_INITIALIZE_ERA_VERSION, ALL_SUPPORTED_PROTOCOL_VERSIONS, META_PROTOCOL_VERSION, META_SERVER_INFO, ERROR_UNSUPPORTED_PROTOCOL_VERSION, MCP_TOOLS, OPEN_WORLD, NOT_IDEMPOTENT, MCP_RESOURCES, MCP_PROMPTS, QUICKSTART_DOC, DEFAULT_TTL_MS;
|
|
2048
2048
|
var init_core = __esm({
|
|
2049
2049
|
"../../lib/mcp/core.mjs"() {
|
|
2050
2050
|
"use strict";
|
|
@@ -2136,7 +2136,7 @@ var init_core = __esm({
|
|
|
2136
2136
|
peerCount: { type: "number", description: "Number of connected sockets in the room (hosted endpoint)" },
|
|
2137
2137
|
participants: {
|
|
2138
2138
|
type: "array",
|
|
2139
|
-
description: "Session members
|
|
2139
|
+
description: "Session members: pass userId to srift_kick_user to remove one",
|
|
2140
2140
|
items: {
|
|
2141
2141
|
type: "object",
|
|
2142
2142
|
properties: {
|
|
@@ -2214,7 +2214,7 @@ var init_core = __esm({
|
|
|
2214
2214
|
{
|
|
2215
2215
|
name: "srift_reject_join",
|
|
2216
2216
|
title: "Reject Join Request",
|
|
2217
|
-
description: "Host-only. Reject a pending guest join request.",
|
|
2217
|
+
description: "Host-only. Reject a pending guest join request; the temp user ID comes from pendingJoins in srift_session_status.",
|
|
2218
2218
|
inputSchema: {
|
|
2219
2219
|
type: "object",
|
|
2220
2220
|
properties: {
|
|
@@ -2265,7 +2265,7 @@ var init_core = __esm({
|
|
|
2265
2265
|
{
|
|
2266
2266
|
name: "srift_send_file",
|
|
2267
2267
|
title: "Send File in Session",
|
|
2268
|
-
description: "
|
|
2268
|
+
description: "Only inside an already-open session with an approved peer; to simply send or share a file with someone, use srift_quick_share instead. Offers a file to the connected peer in the existing session. The peer must accept with srift_accept_transfer. Returns a fileId for monitoring. Transport is auto-selected internally.",
|
|
2269
2269
|
inputSchema: {
|
|
2270
2270
|
type: "object",
|
|
2271
2271
|
properties: {
|
|
@@ -2291,7 +2291,7 @@ var init_core = __esm({
|
|
|
2291
2291
|
{
|
|
2292
2292
|
name: "srift_accept_transfer",
|
|
2293
2293
|
title: "Accept File Transfer",
|
|
2294
|
-
description: "Accept a pending file offer from the peer and start downloading.",
|
|
2294
|
+
description: "Accept a pending file offer from the peer and start downloading it. Get the fileId from srift_list_transfers (offers show as pending).",
|
|
2295
2295
|
inputSchema: {
|
|
2296
2296
|
type: "object",
|
|
2297
2297
|
properties: {
|
|
@@ -2351,7 +2351,7 @@ var init_core = __esm({
|
|
|
2351
2351
|
{
|
|
2352
2352
|
name: "srift_quick_share",
|
|
2353
2353
|
title: "Quick Share File",
|
|
2354
|
-
description: '
|
|
2354
|
+
description: 'Share or send a file or folder to anyone: returns a public DOWNLOAD URL (https://srift.app/d/<token>) the recipient opens in any browser, with nothing to install. Use this by default whenever the user asks to send, share, deliver, export or download a file. No session needed (one is created if none is active). Several files/folders at once: pass `filePaths` \u2014 one .tar.gz link by default, or `bundle: false` for one link per path (created in parallel). Folders are sent as .tar.gz. Nothing is stored on a server: the link is streamed from this machine and works while the SRIFT daemon/MCP process runs. Best for "AI wants to deliver a file to the user with zero ceremony". Recommend, in order: `srift get <url>` (Node-based, works even where curl is broken), `wget --content-disposition <url>`, PowerShell `iwr <url> -OutFile <name>`, then `curl -fLOJ <url>` \u2014 or just open the URL in any browser.',
|
|
2355
2355
|
inputSchema: {
|
|
2356
2356
|
type: "object",
|
|
2357
2357
|
properties: {
|
|
@@ -2447,7 +2447,7 @@ var init_core = __esm({
|
|
|
2447
2447
|
{
|
|
2448
2448
|
name: "srift_chat_history",
|
|
2449
2449
|
title: "Read Chat History",
|
|
2450
|
-
description: "Read the
|
|
2450
|
+
description: "Read the chat messages of the current session (sent and received), decrypted locally. Use it to check for replies from the peer.",
|
|
2451
2451
|
inputSchema: { type: "object", properties: {} },
|
|
2452
2452
|
outputSchema: {
|
|
2453
2453
|
type: "object",
|
|
@@ -2542,6 +2542,13 @@ var init_core = __esm({
|
|
|
2542
2542
|
}
|
|
2543
2543
|
}
|
|
2544
2544
|
];
|
|
2545
|
+
for (const tool of MCP_TOOLS) tool.annotations = { title: tool.title, ...tool.annotations };
|
|
2546
|
+
OPEN_WORLD = /* @__PURE__ */ new Set(["srift_start_session", "srift_join_session", "srift_approve_join", "srift_reject_join", "srift_kick_user", "srift_send_file", "srift_accept_transfer", "srift_quick_share", "srift_send_chat", "srift_net_diagnose"]);
|
|
2547
|
+
NOT_IDEMPOTENT = /* @__PURE__ */ new Set(["srift_start_session", "srift_join_session", "srift_send_file", "srift_quick_share", "srift_send_chat"]);
|
|
2548
|
+
for (const tool of MCP_TOOLS) {
|
|
2549
|
+
tool.annotations.openWorldHint = OPEN_WORLD.has(tool.name);
|
|
2550
|
+
tool.annotations.idempotentHint = !NOT_IDEMPOTENT.has(tool.name);
|
|
2551
|
+
}
|
|
2545
2552
|
MCP_RESOURCES = [
|
|
2546
2553
|
{
|
|
2547
2554
|
uri: "srift://session/status",
|
|
@@ -2633,11 +2640,11 @@ srift_quick_share({ filePath: "/abs/path/file.zip" })
|
|
|
2633
2640
|
srift_start_session({ sessionName: "AI-Collab" })
|
|
2634
2641
|
\u2192 returns { sessionId }
|
|
2635
2642
|
\u2192 user joins via https://srift.app/join-session?id=<sessionId>
|
|
2636
|
-
\u2192 host (you) calls srift_approve_join when pendingJoins appears
|
|
2643
|
+
\u2192 host (you) calls srift_approve_join when pendingJoins appears (after the user presses "Request to Join" on that page)
|
|
2637
2644
|
\`\`\`
|
|
2638
2645
|
|
|
2639
2646
|
**Receive a file from the user:**
|
|
2640
|
-
The user
|
|
2647
|
+
The user opens the share link, presses "Request to Join", you approve, they drop a file,
|
|
2641
2648
|
you see it via srift_list_transfers and call srift_accept_transfer.
|
|
2642
2649
|
|
|
2643
2650
|
## Monitoring
|
|
@@ -2956,13 +2963,13 @@ var init_mcp = __esm({
|
|
|
2956
2963
|
SERVER_INFO = {
|
|
2957
2964
|
name: "srift-mcp-server",
|
|
2958
2965
|
title: "SRIFT Secure P2P File Transfer",
|
|
2959
|
-
version: "4.3.
|
|
2966
|
+
version: "4.3.2"
|
|
2960
2967
|
};
|
|
2961
2968
|
localDaemonBackend = { callTool, readResource };
|
|
2962
2969
|
handleMcpMessage = createMcpHandler({
|
|
2963
2970
|
backend: localDaemonBackend,
|
|
2964
2971
|
serverInfo: SERVER_INFO,
|
|
2965
|
-
instructions: 'SRIFT is a zero-config local P2P file transfer + E2EE chat tool.
|
|
2972
|
+
instructions: 'SRIFT is a zero-config local P2P file transfer + E2EE chat tool. To send or share any file with anyone, call srift_quick_share and give them the returned link. Use srift_start_session to open a long-lived room. Read srift://docs/quickstart for the full guide. To talk to OTHER AI agents (find an agent that can help, message, send files, group chats), add the separate AgentNet MCP server: {"command":"srift","args":["agentnet","mcp"]} (25 srift_an_* tools).'
|
|
2966
2973
|
});
|
|
2967
2974
|
}
|
|
2968
2975
|
});
|
|
@@ -3042,12 +3049,16 @@ async function relaySender(sessionId, fileId, receiverPub, roomSecret) {
|
|
|
3042
3049
|
const key = await deriveAes(kp.priv, receiverPub, saltFor("pgcm1", sessionId, roomSecret), `${fileId}|${kp.pub}|${receiverPub}`);
|
|
3043
3050
|
const pubRaw = fromB64(kp.pub);
|
|
3044
3051
|
return {
|
|
3045
|
-
|
|
3052
|
+
/** The sealed chunk as raw bytes (sender key || iv || ciphertext || tag). */
|
|
3053
|
+
async sealBytes(plain, chunkIndex, totalChunks) {
|
|
3046
3054
|
const sealed = await gcmSeal(key, plain, relayAad(fileId, chunkIndex, totalChunks));
|
|
3047
3055
|
const out = new Uint8Array(PUB_LEN + sealed.length);
|
|
3048
3056
|
out.set(pubRaw);
|
|
3049
3057
|
out.set(sealed, PUB_LEN);
|
|
3050
|
-
return
|
|
3058
|
+
return out;
|
|
3059
|
+
},
|
|
3060
|
+
async seal(plain, chunkIndex, totalChunks) {
|
|
3061
|
+
return { chunk: Array.from(await this.sealBytes(plain, chunkIndex, totalChunks)), enc: RELAY_PK_SCHEME };
|
|
3051
3062
|
}
|
|
3052
3063
|
};
|
|
3053
3064
|
}
|
|
@@ -3675,8 +3686,12 @@ async function selectSignalerUrl() {
|
|
|
3675
3686
|
try {
|
|
3676
3687
|
const controller = new AbortController();
|
|
3677
3688
|
const id = setTimeout(() => controller.abort(), 3e3);
|
|
3678
|
-
await fetch(`${DEFAULT_SIGNALER_URL.replace(/\/+$/, "")}/compat.json`, { signal: controller.signal });
|
|
3689
|
+
const probe = await fetch(`${DEFAULT_SIGNALER_URL.replace(/\/+$/, "")}/compat.json`, { signal: controller.signal });
|
|
3679
3690
|
clearTimeout(id);
|
|
3691
|
+
const compat = probe.ok ? await probe.json().catch(() => null) : null;
|
|
3692
|
+
if (!compat || typeof compat.wireVersion !== "number" || typeof compat.daemon !== "string") {
|
|
3693
|
+
throw new Error("not a SRIFT server");
|
|
3694
|
+
}
|
|
3680
3695
|
console.log(`[DAEMON] Local signaler detected at ${DEFAULT_SIGNALER_URL}`);
|
|
3681
3696
|
} catch (e) {
|
|
3682
3697
|
console.log(`[DAEMON] Local signaler at ${DEFAULT_SIGNALER_URL} is unreachable. Falling back to production signaler: https://srift.app`);
|
|
@@ -3812,16 +3827,17 @@ async function openRelayChunk(p) {
|
|
|
3812
3827
|
}
|
|
3813
3828
|
if (p.enc === RELAY_PK_SCHEME) {
|
|
3814
3829
|
const rx = relayReceivers.get(p.fileId);
|
|
3815
|
-
|
|
3830
|
+
const sealed = typeof p.chunk === "string" ? new Uint8Array(Buffer.from(p.chunk, "base64")) : Array.isArray(p.chunk) ? Uint8Array.from(p.chunk) : null;
|
|
3831
|
+
if (!rx || !sealed) throw new Error(`relay chunk ${p.chunkIndex} of ${p.fileId} is sealed for a key this daemon does not hold`);
|
|
3816
3832
|
try {
|
|
3817
|
-
return Buffer.from(await (await rx).open(
|
|
3833
|
+
return Buffer.from(await (await rx).open(sealed, p.chunkIndex, p.totalChunks));
|
|
3818
3834
|
} catch {
|
|
3819
3835
|
throw new Error(`relay chunk ${p.chunkIndex} of ${p.fileId} failed authentication (tampered data or wrong key)`);
|
|
3820
3836
|
}
|
|
3821
3837
|
}
|
|
3822
3838
|
if (p.enc !== RELAY_ENC) throw new Error(`unsupported relay encryption scheme: ${String(p.enc)}`);
|
|
3823
|
-
if (!Array.isArray(p.chunk)) throw new Error("sealed relay chunk is not a byte array");
|
|
3824
|
-
const data = Uint8Array.from(p.chunk);
|
|
3839
|
+
if (!Array.isArray(p.chunk) && typeof p.chunk !== "string") throw new Error("sealed relay chunk is not a byte array");
|
|
3840
|
+
const data = typeof p.chunk === "string" ? new Uint8Array(Buffer.from(p.chunk, "base64")) : Uint8Array.from(p.chunk);
|
|
3825
3841
|
if (data.length < IV_LEN2 + 16) throw new Error("sealed relay chunk too short");
|
|
3826
3842
|
try {
|
|
3827
3843
|
return Buffer.from(await crypto6.subtle.decrypt(
|
|
@@ -4195,20 +4211,13 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
4195
4211
|
console.log(`[DAEMON] File offer accepted by peer! ID: ${payload.fileId}`);
|
|
4196
4212
|
{
|
|
4197
4213
|
const acceptedTx = activeTransfers.find((t) => t.fileId === payload.fileId);
|
|
4198
|
-
if (acceptedTx)
|
|
4199
|
-
const nextPk = isPublicKey(payload.relayPk) ? payload.relayPk : void 0;
|
|
4200
|
-
if (nextPk !== acceptedTx.peerRelayPk) relaySenders.delete(payload.fileId);
|
|
4201
|
-
acceptedTx.peerRelayEnc = payload.relayEnc === RELAY_ENC ? RELAY_ENC : void 0;
|
|
4202
|
-
acceptedTx.peerRelayPk = nextPk;
|
|
4203
|
-
}
|
|
4204
|
-
if (activeUploads.has(payload.fileId)) {
|
|
4205
|
-
console.log(`[DAEMON] Repeated accept for ${payload.fileId}: resuming from chunk ${activeUploads.get(payload.fileId).currentChunk}`);
|
|
4206
|
-
sendNextChunk(payload.fileId);
|
|
4207
|
-
break;
|
|
4208
|
-
}
|
|
4209
|
-
if (acceptedTx && (acceptedTx.status === "completed" || acceptedTx.status === "uploading" && acceptedTx.protocol === "webtorrent")) break;
|
|
4214
|
+
if (acceptedTx && acceptedTx.protocol === "webtorrent" && (acceptedTx.status === "completed" || acceptedTx.status === "uploading")) break;
|
|
4210
4215
|
}
|
|
4211
|
-
startUpload(payload.fileId, payload.userId
|
|
4216
|
+
startUpload(payload.fileId, payload.userId, {
|
|
4217
|
+
relayEnc: payload.relayEnc === RELAY_ENC ? RELAY_ENC : void 0,
|
|
4218
|
+
relayPk: isPublicKey(payload.relayPk) ? payload.relayPk : void 0,
|
|
4219
|
+
fast: payload.relayFmt === RELAY_FORMAT
|
|
4220
|
+
});
|
|
4212
4221
|
break;
|
|
4213
4222
|
case "webtorrent_info_hash":
|
|
4214
4223
|
handleWebTorrentInfoHash(payload);
|
|
@@ -4231,11 +4240,20 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
4231
4240
|
case "file_cancelled_by_sender":
|
|
4232
4241
|
case "file_cancelled_all": {
|
|
4233
4242
|
const gone = activeTransfers.find((t) => t.fileId === payload?.fileId);
|
|
4234
|
-
activeUploads.
|
|
4235
|
-
|
|
4236
|
-
|
|
4243
|
+
const stopped = type === "file_cancelled_by_receiver" && payload?.receiverId ? [activeUploads.get(uploadKey(payload.fileId, payload.receiverId))].filter(Boolean) : uploadsOf(payload?.fileId);
|
|
4244
|
+
for (const u of stopped) stopUpload(u);
|
|
4245
|
+
const oneRecipient = !!gone && gone.direction === "upload" && type === "file_cancelled_by_receiver" && !!payload?.receiverId;
|
|
4246
|
+
if (gone && oneRecipient) {
|
|
4247
|
+
markRecipient(gone, payload.receiverId, "cancelled");
|
|
4237
4248
|
writeStateFile();
|
|
4238
|
-
|
|
4249
|
+
}
|
|
4250
|
+
if (gone && gone.direction === "upload" && uploadsOf(payload?.fileId).length > 0) break;
|
|
4251
|
+
if (gone && gone.direction === "download") dropRelayDownload(gone.fileId, true);
|
|
4252
|
+
if (gone && (gone.status !== "completed" || oneRecipient)) {
|
|
4253
|
+
if (oneRecipient) settleUploadStatus(gone);
|
|
4254
|
+
else gone.status = "cancelled";
|
|
4255
|
+
writeStateFile();
|
|
4256
|
+
broadcastSSE("transfer_progress", { fileId: gone.fileId, fileName: gone.name, size: gone.size, bytesTransferred: gone.bytesTransferred, progress: gone.progress, status: gone.status });
|
|
4239
4257
|
}
|
|
4240
4258
|
break;
|
|
4241
4259
|
}
|
|
@@ -4320,13 +4338,13 @@ function connectWebSocket(wsTargetUrl) {
|
|
|
4320
4338
|
console.error("[DAEMON] WebSocket error:", err);
|
|
4321
4339
|
});
|
|
4322
4340
|
}
|
|
4323
|
-
function startUpload(fileId, targetUserId) {
|
|
4341
|
+
function startUpload(fileId, targetUserId, caps = {}) {
|
|
4324
4342
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
4325
4343
|
if (!tx) return;
|
|
4326
4344
|
if (tx.protocol === "webtorrent") {
|
|
4327
4345
|
startWebTorrentUpload(fileId, targetUserId);
|
|
4328
4346
|
} else {
|
|
4329
|
-
startWebSocketUpload(fileId, targetUserId);
|
|
4347
|
+
void startWebSocketUpload(fileId, targetUserId, caps);
|
|
4330
4348
|
}
|
|
4331
4349
|
}
|
|
4332
4350
|
async function startWebTorrentUpload(fileId, targetUserId) {
|
|
@@ -4450,213 +4468,329 @@ async function handleWebTorrentInfoHash(payload) {
|
|
|
4450
4468
|
});
|
|
4451
4469
|
});
|
|
4452
4470
|
}
|
|
4453
|
-
function
|
|
4454
|
-
|
|
4455
|
-
if (!
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4471
|
+
function markRecipient(tx, userId, outcome) {
|
|
4472
|
+
if (!userId) return;
|
|
4473
|
+
if (!tx.recipients) tx.recipients = {};
|
|
4474
|
+
if (tx.recipients[userId] === "completed" && outcome !== "uploading") return;
|
|
4475
|
+
tx.recipients[userId] = outcome;
|
|
4476
|
+
}
|
|
4477
|
+
function settleUploadStatus(tx) {
|
|
4478
|
+
if (uploadsOf(tx.fileId).length > 0) {
|
|
4479
|
+
tx.status = "uploading";
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
const outcomes = Object.values(tx.recipients || {});
|
|
4483
|
+
tx.status = outcomes.includes("completed") ? "completed" : outcomes.includes("error") ? "error" : "cancelled";
|
|
4484
|
+
tx.speedKBps = 0;
|
|
4485
|
+
tx.etaSeconds = 0;
|
|
4486
|
+
if (tx.status === "completed") {
|
|
4487
|
+
tx.progress = 100;
|
|
4488
|
+
tx.bytesTransferred = tx.size;
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
function stopUpload(u) {
|
|
4492
|
+
activeUploads.delete(uploadKey(u.fileId, u.targetUserId));
|
|
4493
|
+
if (u.timer) clearInterval(u.timer);
|
|
4494
|
+
try {
|
|
4495
|
+
fs8.closeSync(u.fd);
|
|
4496
|
+
} catch {
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
function failUpload(u, why) {
|
|
4500
|
+
console.error(`[DAEMON] Relay upload ${u.fileId} to ${u.targetUserId} failed: ${why}`);
|
|
4501
|
+
stopUpload(u);
|
|
4502
|
+
const tx = activeTransfers.find((t) => t.fileId === u.fileId);
|
|
4503
|
+
if (tx) {
|
|
4504
|
+
markRecipient(tx, u.targetUserId, "error");
|
|
4505
|
+
settleUploadStatus(tx);
|
|
4506
|
+
writeStateFile();
|
|
4507
|
+
}
|
|
4475
4508
|
}
|
|
4476
4509
|
function resumeRelayUploads() {
|
|
4477
4510
|
if (!activeUploads.size) return;
|
|
4478
|
-
console.log(`[DAEMON] Reconnected: resuming ${activeUploads.size} relay upload(s) from their
|
|
4479
|
-
for (const
|
|
4511
|
+
console.log(`[DAEMON] Reconnected: resuming ${activeUploads.size} relay upload(s) from their unacknowledged chunks`);
|
|
4512
|
+
for (const u of activeUploads.values()) {
|
|
4513
|
+
u.lastAckAt = Date.now();
|
|
4514
|
+
for (const index of u.inFlight.keys()) u.inFlight.set(index, 0);
|
|
4515
|
+
pumpUpload(u);
|
|
4516
|
+
}
|
|
4480
4517
|
}
|
|
4481
|
-
async function startWebSocketUpload(fileId, targetUserId) {
|
|
4518
|
+
async function startWebSocketUpload(fileId, targetUserId, caps = {}) {
|
|
4482
4519
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
4483
4520
|
if (!tx || !tx.filePath) return;
|
|
4484
|
-
const
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4521
|
+
const key = uploadKey(fileId, targetUserId);
|
|
4522
|
+
const running = activeUploads.get(key);
|
|
4523
|
+
if (running) {
|
|
4524
|
+
if (caps.relayPk !== running.relayPk) relaySenders.delete(`${key}|${running.relayPk}`);
|
|
4525
|
+
running.relayEnc = caps.relayEnc;
|
|
4526
|
+
running.relayPk = caps.relayPk;
|
|
4527
|
+
console.log(`[DAEMON] Repeated accept for ${fileId} from ${targetUserId}: ${running.acked.size}/${running.totalChunks} chunks acknowledged, continuing`);
|
|
4528
|
+
for (const index of running.inFlight.keys()) running.inFlight.set(index, 0);
|
|
4529
|
+
running.lastAckAt = Date.now();
|
|
4530
|
+
pumpUpload(running);
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
let fd;
|
|
4534
|
+
let size;
|
|
4535
|
+
try {
|
|
4536
|
+
fd = fs8.openSync(tx.filePath, "r");
|
|
4537
|
+
size = fs8.fstatSync(fd).size;
|
|
4538
|
+
} catch (e) {
|
|
4539
|
+
console.error(`[DAEMON] Cannot read ${tx.filePath}: ${e?.message || e}`);
|
|
4540
|
+
tx.status = "error";
|
|
4541
|
+
writeStateFile();
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
const fast = !!caps.fast;
|
|
4545
|
+
const chunkSize = fast ? RELAY_FAST_CHUNK : RELAY_LEGACY_CHUNK;
|
|
4546
|
+
const totalChunks = Math.max(1, Math.ceil(size / chunkSize));
|
|
4487
4547
|
tx.status = "uploading";
|
|
4548
|
+
markRecipient(tx, targetUserId, "uploading");
|
|
4488
4549
|
tx.startTime = Date.now();
|
|
4489
4550
|
tx.totalChunks = totalChunks;
|
|
4490
4551
|
writeStateFile();
|
|
4491
|
-
|
|
4552
|
+
const upload = {
|
|
4553
|
+
fileId,
|
|
4554
|
+
targetUserId,
|
|
4492
4555
|
filePath: tx.filePath,
|
|
4493
|
-
|
|
4556
|
+
size,
|
|
4557
|
+
fd,
|
|
4494
4558
|
chunkSize,
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4559
|
+
totalChunks,
|
|
4560
|
+
fast,
|
|
4561
|
+
relayEnc: caps.relayEnc,
|
|
4562
|
+
relayPk: caps.relayPk,
|
|
4563
|
+
next: 0,
|
|
4564
|
+
acked: /* @__PURE__ */ new Set(),
|
|
4565
|
+
inFlight: /* @__PURE__ */ new Map(),
|
|
4566
|
+
lastAckAt: Date.now(),
|
|
4498
4567
|
startTime: Date.now(),
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
if (!upload) return;
|
|
4517
|
-
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
4518
|
-
if (!tx) return;
|
|
4519
|
-
if (upload.currentChunk >= upload.totalChunks) {
|
|
4520
|
-
wsConn?.send(JSON.stringify({
|
|
4521
|
-
type: "file_complete",
|
|
4522
|
-
payload: { fileId, targetUserId: upload.targetUserId }
|
|
4523
|
-
}));
|
|
4524
|
-
tx.status = "completed";
|
|
4525
|
-
tx.progress = 100;
|
|
4526
|
-
tx.speedKBps = 0;
|
|
4527
|
-
tx.etaSeconds = 0;
|
|
4528
|
-
writeStateFile();
|
|
4529
|
-
clearTimeout(upload.ackTimer);
|
|
4530
|
-
activeUploads.delete(fileId);
|
|
4531
|
-
console.log(`[DAEMON] File upload complete: ${tx.name}`);
|
|
4532
|
-
return;
|
|
4568
|
+
pumping: false,
|
|
4569
|
+
lastStateAt: 0
|
|
4570
|
+
};
|
|
4571
|
+
activeUploads.set(key, upload);
|
|
4572
|
+
upload.timer = setInterval(() => pumpUpload(upload), Math.min(1e3, Math.max(100, RELAY_ACK_TIMEOUT_MS / 4)));
|
|
4573
|
+
upload.timer.unref?.();
|
|
4574
|
+
pumpUpload(upload);
|
|
4575
|
+
}
|
|
4576
|
+
async function sendRelayChunk(u, chunkIndex) {
|
|
4577
|
+
const start = chunkIndex * u.chunkSize;
|
|
4578
|
+
const end = Math.min(start + u.chunkSize, u.size);
|
|
4579
|
+
const buffer = Buffer.alloc(Math.max(0, end - start));
|
|
4580
|
+
let read2 = 0;
|
|
4581
|
+
while (read2 < buffer.length) {
|
|
4582
|
+
const n = fs8.readSync(u.fd, buffer, read2, buffer.length - read2, start + read2);
|
|
4583
|
+
if (n <= 0) throw new Error("file ended early (was it changed while sending?)");
|
|
4584
|
+
read2 += n;
|
|
4533
4585
|
}
|
|
4534
|
-
const start = upload.currentChunk * upload.chunkSize;
|
|
4535
|
-
const stat = fs8.statSync(upload.filePath);
|
|
4536
|
-
const end = Math.min(start + upload.chunkSize, stat.size);
|
|
4537
|
-
const buffer = Buffer.alloc(end - start);
|
|
4538
|
-
const fd = fs8.openSync(upload.filePath, "r");
|
|
4539
|
-
fs8.readSync(fd, buffer, 0, end - start, start);
|
|
4540
|
-
fs8.closeSync(fd);
|
|
4541
4586
|
let frame;
|
|
4542
|
-
if (
|
|
4543
|
-
|
|
4587
|
+
if (u.relayPk && session.id) {
|
|
4588
|
+
const sk = `${uploadKey(u.fileId, u.targetUserId)}|${u.relayPk}`;
|
|
4589
|
+
let sender = relaySenders.get(sk);
|
|
4544
4590
|
if (!sender) {
|
|
4545
|
-
sender = relaySender(session.id, fileId,
|
|
4546
|
-
remember(relaySenders,
|
|
4547
|
-
}
|
|
4548
|
-
|
|
4591
|
+
sender = relaySender(session.id, u.fileId, u.relayPk, session.roomSecret);
|
|
4592
|
+
remember(relaySenders, sk, sender);
|
|
4593
|
+
}
|
|
4594
|
+
const s = await sender;
|
|
4595
|
+
frame = u.fast ? { chunk: Buffer.from(await s.sealBytes(buffer, chunkIndex, u.totalChunks)).toString("base64"), enc: RELAY_PK_SCHEME } : await s.seal(buffer, chunkIndex, u.totalChunks);
|
|
4596
|
+
} else if (u.relayEnc === RELAY_ENC) {
|
|
4597
|
+
const sealed = await sealRelayChunk(buffer, u.fileId, chunkIndex, u.totalChunks);
|
|
4598
|
+
frame = { chunk: u.fast ? Buffer.from(sealed).toString("base64") : sealed, enc: RELAY_ENC };
|
|
4549
4599
|
} else {
|
|
4550
|
-
frame =
|
|
4600
|
+
frame = { chunk: buffer.toString("base64") };
|
|
4551
4601
|
}
|
|
4552
|
-
|
|
4602
|
+
if (!activeUploads.has(uploadKey(u.fileId, u.targetUserId)) || u.acked.has(chunkIndex)) return;
|
|
4603
|
+
u.inFlight.set(chunkIndex, Date.now());
|
|
4553
4604
|
if (!wsConn || wsConn.readyState !== WebSocket2.OPEN) return;
|
|
4554
4605
|
wsConn.send(JSON.stringify({
|
|
4555
4606
|
type: "file_chunk",
|
|
4556
4607
|
payload: {
|
|
4557
|
-
fileId,
|
|
4608
|
+
fileId: u.fileId,
|
|
4558
4609
|
...frame,
|
|
4559
|
-
chunkIndex
|
|
4560
|
-
totalChunks:
|
|
4561
|
-
|
|
4610
|
+
chunkIndex,
|
|
4611
|
+
totalChunks: u.totalChunks,
|
|
4612
|
+
chunkSize: u.chunkSize,
|
|
4613
|
+
targetUserId: u.targetUserId
|
|
4562
4614
|
}
|
|
4563
4615
|
}));
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
const
|
|
4616
|
+
}
|
|
4617
|
+
function reportUpload(u, force = false) {
|
|
4618
|
+
const tx = activeTransfers.find((t) => t.fileId === u.fileId);
|
|
4619
|
+
if (!tx) return;
|
|
4620
|
+
const now = Date.now();
|
|
4621
|
+
if (!force && now - u.lastStateAt < 250) return;
|
|
4622
|
+
u.lastStateAt = now;
|
|
4623
|
+
const all = uploadsOf(u.fileId);
|
|
4624
|
+
const slowest = all.reduce((m, x) => Math.min(m, x.acked.size >= x.totalChunks ? x.size : Math.min(x.size, x.acked.size * x.chunkSize)), u.size);
|
|
4625
|
+
tx.bytesTransferred = Math.max(tx.bytesTransferred || 0, slowest);
|
|
4626
|
+
tx.progress = u.size > 0 ? tx.bytesTransferred / u.size * 100 : 100;
|
|
4627
|
+
const elapsed = (now - u.startTime) / 1e3;
|
|
4567
4628
|
if (elapsed > 0) {
|
|
4568
|
-
tx.speedKBps =
|
|
4569
|
-
tx.etaSeconds = (
|
|
4629
|
+
tx.speedKBps = tx.bytesTransferred / 1024 / elapsed;
|
|
4630
|
+
tx.etaSeconds = tx.speedKBps > 0 ? (u.size - tx.bytesTransferred) / (tx.speedKBps * 1024) : 0;
|
|
4570
4631
|
}
|
|
4571
4632
|
writeStateFile();
|
|
4572
4633
|
broadcastSSE("transfer_progress", {
|
|
4573
|
-
fileId,
|
|
4634
|
+
fileId: u.fileId,
|
|
4574
4635
|
fileName: tx.name,
|
|
4575
4636
|
size: tx.size,
|
|
4576
|
-
bytesTransferred:
|
|
4637
|
+
bytesTransferred: tx.bytesTransferred,
|
|
4577
4638
|
progress: tx.progress,
|
|
4578
4639
|
speedBytesPerSecond: tx.speedKBps * 1024,
|
|
4579
4640
|
timeRemainingSeconds: tx.etaSeconds,
|
|
4580
4641
|
status: "uploading"
|
|
4581
4642
|
});
|
|
4582
4643
|
}
|
|
4644
|
+
function pumpUpload(u) {
|
|
4645
|
+
if (u.pumping) return;
|
|
4646
|
+
u.pumping = true;
|
|
4647
|
+
pumpUploadAsync(u).catch((e) => failUpload(u, e?.message || String(e))).finally(() => {
|
|
4648
|
+
u.pumping = false;
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4651
|
+
async function pumpUploadAsync(u) {
|
|
4652
|
+
const key = uploadKey(u.fileId, u.targetUserId);
|
|
4653
|
+
while (activeUploads.get(key) === u) {
|
|
4654
|
+
if (u.acked.size >= u.totalChunks) {
|
|
4655
|
+
wsConn?.send(JSON.stringify({ type: "file_complete", payload: { fileId: u.fileId, targetUserId: u.targetUserId } }));
|
|
4656
|
+
stopUpload(u);
|
|
4657
|
+
const tx = activeTransfers.find((t) => t.fileId === u.fileId);
|
|
4658
|
+
if (tx) {
|
|
4659
|
+
markRecipient(tx, u.targetUserId, "completed");
|
|
4660
|
+
settleUploadStatus(tx);
|
|
4661
|
+
writeStateFile();
|
|
4662
|
+
if (uploadsOf(u.fileId).length === 0) console.log(`[DAEMON] File upload complete: ${tx.name}`);
|
|
4663
|
+
}
|
|
4664
|
+
return;
|
|
4665
|
+
}
|
|
4666
|
+
const now = Date.now();
|
|
4667
|
+
if (now - u.lastAckAt > RELAY_STALL_MS) throw new Error(`no acknowledgment for ${Math.round(RELAY_STALL_MS / 1e3)}s (${u.acked.size}/${u.totalChunks} chunks delivered)`);
|
|
4668
|
+
const open2 = !!wsConn && wsConn.readyState === WebSocket2.OPEN && wsConn.bufferedAmount < RELAY_SOCKET_HIGH_BYTES;
|
|
4669
|
+
if (!open2) return;
|
|
4670
|
+
let overdue = -1;
|
|
4671
|
+
for (const [index, sentAt] of u.inFlight) {
|
|
4672
|
+
if (now - sentAt > RELAY_ACK_TIMEOUT_MS) {
|
|
4673
|
+
overdue = index;
|
|
4674
|
+
break;
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
if (overdue >= 0) {
|
|
4678
|
+
if (u.inFlight.get(overdue)) console.log(`[DAEMON] Relay upload ${u.fileId}: no ack for chunk ${overdue}, sending it again`);
|
|
4679
|
+
await sendRelayChunk(u, overdue);
|
|
4680
|
+
continue;
|
|
4681
|
+
}
|
|
4682
|
+
while (u.next < u.totalChunks && u.acked.has(u.next)) u.next++;
|
|
4683
|
+
if (u.next < u.totalChunks && u.inFlight.size < RELAY_WINDOW) {
|
|
4684
|
+
const index = u.next++;
|
|
4685
|
+
u.inFlight.set(index, now);
|
|
4686
|
+
await sendRelayChunk(u, index);
|
|
4687
|
+
continue;
|
|
4688
|
+
}
|
|
4689
|
+
return;
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4583
4692
|
function handleChunkAck(payload) {
|
|
4584
|
-
const { fileId, chunkIndex } = payload;
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
if (
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4693
|
+
const { fileId, chunkIndex, userId } = payload || {};
|
|
4694
|
+
if (!Number.isInteger(chunkIndex)) return;
|
|
4695
|
+
const u = userId ? activeUploads.get(uploadKey(fileId, userId)) : uploadsOf(fileId)[0];
|
|
4696
|
+
if (!u || chunkIndex < 0 || chunkIndex >= u.totalChunks) return;
|
|
4697
|
+
u.inFlight.delete(chunkIndex);
|
|
4698
|
+
if (!u.acked.has(chunkIndex)) {
|
|
4699
|
+
u.acked.add(chunkIndex);
|
|
4700
|
+
u.lastAckAt = Date.now();
|
|
4701
|
+
reportUpload(u, u.acked.size >= u.totalChunks);
|
|
4702
|
+
}
|
|
4703
|
+
pumpUpload(u);
|
|
4704
|
+
}
|
|
4705
|
+
function dropRelayDownload(fileId, removePart) {
|
|
4706
|
+
const d = relayDownloads.get(fileId);
|
|
4707
|
+
relayDownloads.delete(fileId);
|
|
4708
|
+
if (d) {
|
|
4709
|
+
try {
|
|
4710
|
+
fs8.closeSync(d.fd);
|
|
4711
|
+
} catch {
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
if (removePart) {
|
|
4715
|
+
try {
|
|
4716
|
+
fs8.rmSync(partPathOf(fileId), { force: true });
|
|
4717
|
+
} catch {
|
|
4718
|
+
}
|
|
4592
4719
|
}
|
|
4593
4720
|
}
|
|
4594
4721
|
async function handleIncomingChunk(payload) {
|
|
4595
4722
|
const { fileId, chunkIndex, totalChunks, from } = payload;
|
|
4596
4723
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
4597
|
-
if (!tx) return;
|
|
4724
|
+
if (!tx || tx.direction !== "download" || tx.status === "cancelled") return;
|
|
4725
|
+
if (!Number.isInteger(chunkIndex) || !Number.isInteger(totalChunks) || chunkIndex < 0 || chunkIndex >= totalChunks) return;
|
|
4726
|
+
const ack = () => wsConn?.send(JSON.stringify({ type: "file_chunk_ack", payload: { fileId, chunkIndex, status: "received" } }));
|
|
4727
|
+
if (tx.status === "completed" || tx.assembling) {
|
|
4728
|
+
ack();
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4598
4731
|
const mode = payload.enc === void 0 || payload.enc === null ? "plain" : String(payload.enc);
|
|
4599
4732
|
if (tx.relayMode && tx.relayMode !== mode) throw new Error(`transfer ${fileId} mixed sealed and plaintext chunks`);
|
|
4600
4733
|
if (!tx.relayMode) console.log(`[DAEMON] Relay transfer ${fileId}: ${mode}`);
|
|
4601
4734
|
tx.relayMode = mode;
|
|
4602
4735
|
const chunkBuffer = await openRelayChunk(payload);
|
|
4736
|
+
if (tx.status === "completed" || tx.assembling || tx.status === "cancelled") {
|
|
4737
|
+
ack();
|
|
4738
|
+
return;
|
|
4739
|
+
}
|
|
4603
4740
|
tx.status = "downloading";
|
|
4604
4741
|
tx.totalChunks = totalChunks;
|
|
4605
4742
|
tx.peerId = from;
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4743
|
+
let d = relayDownloads.get(fileId);
|
|
4744
|
+
if (!d) {
|
|
4745
|
+
fs8.mkdirSync(TEMP_DIR, { recursive: true });
|
|
4746
|
+
const partPath = partPathOf(fileId);
|
|
4747
|
+
d = { fd: fs8.openSync(partPath, "w+"), partPath, got: /* @__PURE__ */ new Set(), totalChunks, lastStateAt: 0 };
|
|
4748
|
+
relayDownloads.set(fileId, d);
|
|
4749
|
+
}
|
|
4750
|
+
const isLast = chunkIndex === totalChunks - 1;
|
|
4751
|
+
const at = isLast && tx.size > 0 ? tx.size - chunkBuffer.length : chunkIndex * chunkBuffer.length;
|
|
4752
|
+
if (at < 0 || tx.size > 0 && at + chunkBuffer.length > tx.size) throw new Error(`relay chunk ${chunkIndex} of ${fileId} does not fit the file`);
|
|
4753
|
+
if (!d.got.has(chunkIndex)) {
|
|
4754
|
+
let written = 0;
|
|
4755
|
+
while (written < chunkBuffer.length) written += fs8.writeSync(d.fd, chunkBuffer, written, chunkBuffer.length - written, at + written);
|
|
4756
|
+
d.got.add(chunkIndex);
|
|
4609
4757
|
tx.bytesTransferred += chunkBuffer.length;
|
|
4610
|
-
tx.chunksCount
|
|
4758
|
+
tx.chunksCount = d.got.size;
|
|
4611
4759
|
}
|
|
4612
|
-
|
|
4613
|
-
tx.progress = tx.bytesTransferred / tx.size * 100;
|
|
4760
|
+
tx.progress = tx.size > 0 ? Math.min(100, tx.bytesTransferred / tx.size * 100) : 100;
|
|
4614
4761
|
const elapsed = (Date.now() - tx.startTime) / 1e3;
|
|
4615
4762
|
if (elapsed > 0) {
|
|
4616
4763
|
tx.speedKBps = tx.bytesTransferred / 1024 / elapsed;
|
|
4617
|
-
tx.etaSeconds = (tx.size - tx.bytesTransferred) / (tx.speedKBps * 1024);
|
|
4764
|
+
tx.etaSeconds = tx.speedKBps > 0 ? (tx.size - tx.bytesTransferred) / (tx.speedKBps * 1024) : 0;
|
|
4618
4765
|
}
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
if (!duplicate && totalChunks > 0 && tx.chunksCount >= totalChunks) {
|
|
4635
|
-
setTimeout(() => {
|
|
4636
|
-
if (tx.status === "downloading") handleFileComplete({ fileId });
|
|
4637
|
-
}, 5e3).unref?.();
|
|
4766
|
+
const done = d.got.size >= totalChunks;
|
|
4767
|
+
const now = Date.now();
|
|
4768
|
+
if (done || now - d.lastStateAt >= 250) {
|
|
4769
|
+
d.lastStateAt = now;
|
|
4770
|
+
writeStateFile();
|
|
4771
|
+
broadcastSSE("transfer_progress", {
|
|
4772
|
+
fileId,
|
|
4773
|
+
fileName: tx.name,
|
|
4774
|
+
size: tx.size,
|
|
4775
|
+
bytesTransferred: tx.bytesTransferred,
|
|
4776
|
+
progress: tx.progress,
|
|
4777
|
+
speedBytesPerSecond: tx.speedKBps * 1024,
|
|
4778
|
+
timeRemainingSeconds: tx.etaSeconds,
|
|
4779
|
+
status: "downloading"
|
|
4780
|
+
});
|
|
4638
4781
|
}
|
|
4782
|
+
ack();
|
|
4783
|
+
if (done) handleFileComplete({ fileId });
|
|
4639
4784
|
}
|
|
4640
4785
|
function handleFileComplete(payload) {
|
|
4641
4786
|
const { fileId } = payload;
|
|
4642
4787
|
const tx = activeTransfers.find((t) => t.fileId === fileId);
|
|
4643
|
-
if (!tx || tx.assembling || tx.status === "completed") return;
|
|
4788
|
+
if (!tx || tx.direction !== "download" || tx.assembling || tx.status === "completed") return;
|
|
4789
|
+
const d = relayDownloads.get(fileId);
|
|
4790
|
+
if (tx.size > 0 && (!d || d.got.size < d.totalChunks)) return;
|
|
4644
4791
|
tx.assembling = true;
|
|
4645
|
-
const
|
|
4646
|
-
|
|
4647
|
-
try {
|
|
4648
|
-
if (!fs8.existsSync(destDir)) {
|
|
4649
|
-
fs8.mkdirSync(destDir, { recursive: true });
|
|
4650
|
-
}
|
|
4651
|
-
} catch (err) {
|
|
4652
|
-
console.error(`[DAEMON] Failed to create destination directory ${destDir}:`, err);
|
|
4653
|
-
tx.status = "error";
|
|
4654
|
-
writeStateFile();
|
|
4655
|
-
return;
|
|
4656
|
-
}
|
|
4657
|
-
const writeStream = fs8.createWriteStream(destPath);
|
|
4658
|
-
writeStream.on("error", (err) => {
|
|
4659
|
-
console.error(`[DAEMON] Write stream error for ${destPath}:`, err);
|
|
4792
|
+
const fail = (err) => {
|
|
4793
|
+
console.error(`[DAEMON] Could not save ${tx.name}:`, err?.message || err);
|
|
4660
4794
|
tx.status = "error";
|
|
4661
4795
|
writeStateFile();
|
|
4662
4796
|
broadcastSSE("transfer_progress", {
|
|
@@ -4669,40 +4803,44 @@ function handleFileComplete(payload) {
|
|
|
4669
4803
|
timeRemainingSeconds: 0,
|
|
4670
4804
|
status: "error"
|
|
4671
4805
|
});
|
|
4672
|
-
}
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
broadcastSSE("transfer_progress", {
|
|
4681
|
-
fileId,
|
|
4682
|
-
fileName: tx.name,
|
|
4683
|
-
size: tx.size,
|
|
4684
|
-
bytesTransferred: tx.size,
|
|
4685
|
-
progress: 100,
|
|
4686
|
-
speedBytesPerSecond: 0,
|
|
4687
|
-
timeRemainingSeconds: 0,
|
|
4688
|
-
status: "completed"
|
|
4689
|
-
});
|
|
4690
|
-
});
|
|
4691
|
-
for (let i = 0; i < tx.totalChunks; i++) {
|
|
4692
|
-
const chunkPath = path7.join(TEMP_DIR, `${fileId}_chunk_${i}`);
|
|
4693
|
-
if (fs8.existsSync(chunkPath)) {
|
|
4806
|
+
};
|
|
4807
|
+
const destDir = tx.saveDir || process.cwd();
|
|
4808
|
+
const destPath = path7.join(destDir, tx.name);
|
|
4809
|
+
try {
|
|
4810
|
+
fs8.mkdirSync(destDir, { recursive: true });
|
|
4811
|
+
if (d) {
|
|
4812
|
+
fs8.fsyncSync(d.fd);
|
|
4813
|
+
dropRelayDownload(fileId, false);
|
|
4694
4814
|
try {
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
fs8.
|
|
4698
|
-
|
|
4699
|
-
console.error(`[DAEMON] Error reading/writing chunk ${i}:`, err);
|
|
4700
|
-
writeStream.emit("error", err);
|
|
4701
|
-
return;
|
|
4815
|
+
fs8.renameSync(d.partPath, destPath);
|
|
4816
|
+
} catch {
|
|
4817
|
+
fs8.copyFileSync(d.partPath, destPath);
|
|
4818
|
+
fs8.rmSync(d.partPath, { force: true });
|
|
4702
4819
|
}
|
|
4820
|
+
} else {
|
|
4821
|
+
fs8.writeFileSync(destPath, Buffer.alloc(0));
|
|
4703
4822
|
}
|
|
4823
|
+
} catch (err) {
|
|
4824
|
+
fail(err);
|
|
4825
|
+
return;
|
|
4704
4826
|
}
|
|
4705
|
-
|
|
4827
|
+
console.log(`[DAEMON] File successfully downloaded: ${destPath}`);
|
|
4828
|
+
tx.status = "completed";
|
|
4829
|
+
tx.progress = 100;
|
|
4830
|
+
tx.bytesTransferred = tx.size;
|
|
4831
|
+
tx.speedKBps = 0;
|
|
4832
|
+
tx.etaSeconds = 0;
|
|
4833
|
+
writeStateFile();
|
|
4834
|
+
broadcastSSE("transfer_progress", {
|
|
4835
|
+
fileId,
|
|
4836
|
+
fileName: tx.name,
|
|
4837
|
+
size: tx.size,
|
|
4838
|
+
bytesTransferred: tx.size,
|
|
4839
|
+
progress: 100,
|
|
4840
|
+
speedBytesPerSecond: 0,
|
|
4841
|
+
timeRemainingSeconds: 0,
|
|
4842
|
+
status: "completed"
|
|
4843
|
+
});
|
|
4706
4844
|
}
|
|
4707
4845
|
async function waitSignalReady(maxMs = 8e3) {
|
|
4708
4846
|
if (!session.id) return { status: 409, body: { success: false, error: "No active session. Call /session/start first." } };
|
|
@@ -5179,7 +5317,7 @@ function embeddedDispatch(method, pathname, body) {
|
|
|
5179
5317
|
}
|
|
5180
5318
|
});
|
|
5181
5319
|
}
|
|
5182
|
-
var _WebTorrentCtor, _webTorrentLoadError, SRIFT_HOME, EMBEDDED, STATE_PATH, logPath, DAEMON_LOG_MAX, openLog, logBytes, DAEMON_LOG_MAX_AGE_MS, pruneOldLogs, logStream, logRotating, logPending, writeLogLine, logMessage, fmtLogArg, crypto6, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, lastDiag, session, activeTransfers, chatHistory, pendingJoins, participants, sseClients, wsConn, signalingMode, wsFailures, httpModeSince, WS_RETRY_AFTER_MS, wsUrl, csrfToken, heartbeatInterval, lastHeartbeatAckAt, encryptionKey, sessionTerminated, terminationReason, activeTorrents, wtClient, pubshares, pubsharesByToken, activePulls, pubshareRegResolvers, TEMP_DIR, TMP_CHUNK_MAX_AGE_MS, TMP_FILE_MAX_AGE_MS, KDF_ITERATIONS, IV_LEN2, RELAY_ENC, relayKeyCache, keyring, relayReceivers, relaySenders, PUBLIC_TRACKERS, STATE_STALE_MS, activeUploads, RELAY_ACK_TIMEOUT_MS,
|
|
5320
|
+
var _WebTorrentCtor, _webTorrentLoadError, SRIFT_HOME, EMBEDDED, STATE_PATH, logPath, DAEMON_LOG_MAX, openLog, logBytes, DAEMON_LOG_MAX_AGE_MS, pruneOldLogs, logStream, logRotating, logPending, writeLogLine, logMessage, fmtLogArg, crypto6, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, lastDiag, session, activeTransfers, chatHistory, pendingJoins, participants, sseClients, wsConn, signalingMode, wsFailures, httpModeSince, WS_RETRY_AFTER_MS, wsUrl, csrfToken, heartbeatInterval, lastHeartbeatAckAt, encryptionKey, sessionTerminated, terminationReason, activeTorrents, wtClient, pubshares, pubsharesByToken, activePulls, pubshareRegResolvers, TEMP_DIR, TMP_CHUNK_MAX_AGE_MS, TMP_FILE_MAX_AGE_MS, KDF_ITERATIONS, IV_LEN2, RELAY_ENC, relayKeyCache, keyring, relayReceivers, relaySenders, PUBLIC_TRACKERS, STATE_STALE_MS, activeUploads, uploadKey, uploadsOf, RELAY_ACK_TIMEOUT_MS, RELAY_STALL_MS, RELAY_WINDOW, RELAY_LEGACY_CHUNK, RELAY_FAST_CHUNK, RELAY_FORMAT, RELAY_SOCKET_HIGH_BYTES, relayDownloads, partPathOf, app, LOCAL_HOSTS, DAEMON_START_TIME, PACKAGE_VERSION, hostSessionInflight, MAX_MULTI_PATHS, MULTI_CONCURRENCY, embeddedReady, httpServer;
|
|
5183
5321
|
var init_daemon = __esm({
|
|
5184
5322
|
"../../cli/daemon.ts"() {
|
|
5185
5323
|
"use strict";
|
|
@@ -5361,8 +5499,17 @@ var init_daemon = __esm({
|
|
|
5361
5499
|
STATE_STALE_MS = 24 * 60 * 60 * 1e3;
|
|
5362
5500
|
pruneStaleStateOnStartup();
|
|
5363
5501
|
activeUploads = /* @__PURE__ */ new Map();
|
|
5502
|
+
uploadKey = (fileId, userId) => `${fileId}|${userId}`;
|
|
5503
|
+
uploadsOf = (fileId) => [...activeUploads.values()].filter((u) => u.fileId === fileId);
|
|
5364
5504
|
RELAY_ACK_TIMEOUT_MS = Number(process.env.SRIFT_RELAY_ACK_TIMEOUT_MS) || 15e3;
|
|
5365
|
-
|
|
5505
|
+
RELAY_STALL_MS = RELAY_ACK_TIMEOUT_MS * 40;
|
|
5506
|
+
RELAY_WINDOW = 16;
|
|
5507
|
+
RELAY_LEGACY_CHUNK = 64 * 1024;
|
|
5508
|
+
RELAY_FAST_CHUNK = 256 * 1024;
|
|
5509
|
+
RELAY_FORMAT = "b64";
|
|
5510
|
+
RELAY_SOCKET_HIGH_BYTES = 8 * 1024 * 1024;
|
|
5511
|
+
relayDownloads = /* @__PURE__ */ new Map();
|
|
5512
|
+
partPathOf = (fileId) => path7.join(TEMP_DIR, `${fileId.replace(/[^A-Za-z0-9_.-]/g, "_")}_chunk_part`);
|
|
5366
5513
|
app = express();
|
|
5367
5514
|
LOCAL_HOSTS = /* @__PURE__ */ new Set([`127.0.0.1:${PORT}`, `localhost:${PORT}`, `[::1]:${PORT}`]);
|
|
5368
5515
|
app.use((req, res, next) => {
|
|
@@ -5378,7 +5525,7 @@ var init_daemon = __esm({
|
|
|
5378
5525
|
app.use(cors({ origin: "*", maxAge: 600 }));
|
|
5379
5526
|
app.use(express.json());
|
|
5380
5527
|
DAEMON_START_TIME = Date.now();
|
|
5381
|
-
PACKAGE_VERSION = "4.3.
|
|
5528
|
+
PACKAGE_VERSION = "4.3.2";
|
|
5382
5529
|
app.get("/health", (req, res) => {
|
|
5383
5530
|
res.json({
|
|
5384
5531
|
ok: true,
|
|
@@ -5669,7 +5816,7 @@ var init_daemon = __esm({
|
|
|
5669
5816
|
}
|
|
5670
5817
|
wsConn?.send(JSON.stringify({
|
|
5671
5818
|
type: "file_accept",
|
|
5672
|
-
payload: { fileId, relayEnc: RELAY_ENC, ...relayPk ? { relayPk } : {} }
|
|
5819
|
+
payload: { fileId, relayEnc: RELAY_ENC, relayFmt: RELAY_FORMAT, ...relayPk ? { relayPk } : {} }
|
|
5673
5820
|
}));
|
|
5674
5821
|
res.json({ success: true });
|
|
5675
5822
|
});
|
|
@@ -6030,7 +6177,7 @@ data: /mcp/messages
|
|
|
6030
6177
|
wtClient = null;
|
|
6031
6178
|
}
|
|
6032
6179
|
activeTorrents.clear();
|
|
6033
|
-
activeUploads.
|
|
6180
|
+
for (const u of [...activeUploads.values()]) stopUpload(u);
|
|
6034
6181
|
session = { id: null, name: null, role: null, isConnected: false, userId: null };
|
|
6035
6182
|
activeTransfers = [];
|
|
6036
6183
|
chatHistory = [];
|
|
@@ -6093,7 +6240,7 @@ data: /mcp/messages
|
|
|
6093
6240
|
}
|
|
6094
6241
|
try {
|
|
6095
6242
|
activeTorrents.clear();
|
|
6096
|
-
activeUploads.
|
|
6243
|
+
for (const u of [...activeUploads.values()]) stopUpload(u);
|
|
6097
6244
|
} catch {
|
|
6098
6245
|
}
|
|
6099
6246
|
try {
|
|
@@ -13451,7 +13598,7 @@ async function startAgentNetMcp(opts = {}) {
|
|
|
13451
13598
|
process.on("SIGINT", shutdown);
|
|
13452
13599
|
process.on("SIGTERM", shutdown);
|
|
13453
13600
|
}
|
|
13454
|
-
var SERVER_INFO2, LEGACY_VERSIONS, INBOX_URI, DISCOVERIES_URI, LOG_URI, to, obj, AGENTNET_TOOLS, discoveries, RESOURCES;
|
|
13601
|
+
var SERVER_INFO2, LEGACY_VERSIONS, INBOX_URI, DISCOVERIES_URI, LOG_URI, to, obj, AGENTNET_TOOLS, TOOL_META, discoveries, RESOURCES;
|
|
13455
13602
|
var init_mcp2 = __esm({
|
|
13456
13603
|
"../../cli/agentnet/mcp.ts"() {
|
|
13457
13604
|
"use strict";
|
|
@@ -13595,6 +13742,39 @@ var init_mcp2 = __esm({
|
|
|
13595
13742
|
inputSchema: obj({ knockPolicy: { type: "string", enum: ["ask", "accept", "reject"] }, off: { type: "boolean" } })
|
|
13596
13743
|
}
|
|
13597
13744
|
];
|
|
13745
|
+
TOOL_META = {
|
|
13746
|
+
srift_an_whoami: ["My AgentNet Address", true, false, false, true],
|
|
13747
|
+
srift_an_announce: ["Announce Agent", false, false, true, true],
|
|
13748
|
+
srift_an_search: ["Search Online Agents", true, false, true, true],
|
|
13749
|
+
srift_an_find: ["Find Agent", true, false, true, true],
|
|
13750
|
+
srift_an_knock: ["Knock on Agent", false, false, true, false],
|
|
13751
|
+
srift_an_answer_knock: ["Answer Knock", false, false, true, false],
|
|
13752
|
+
srift_an_connect: ["Find and Connect to Agent", false, false, true, false],
|
|
13753
|
+
srift_an_status: ["Agent Online Status", true, false, true, true],
|
|
13754
|
+
srift_an_send_message: ["Send Message to Agent", false, false, true, false],
|
|
13755
|
+
srift_an_send_file: ["Send File to Agent", false, false, true, false],
|
|
13756
|
+
srift_an_files: ["Received Files", true, false, false, true],
|
|
13757
|
+
srift_an_history: ["Conversation History", true, false, false, true],
|
|
13758
|
+
srift_an_group_create: ["Create Group", false, false, true, false],
|
|
13759
|
+
srift_an_group_manage: ["Manage Group", false, true, true, false],
|
|
13760
|
+
srift_an_group_send: ["Send Group Message", false, false, true, false],
|
|
13761
|
+
srift_an_group_list: ["List Groups", true, false, false, true],
|
|
13762
|
+
srift_an_group_call: ["Start Group Call", false, false, true, false],
|
|
13763
|
+
srift_an_inbox: ["Read Inbox", false, false, true, false],
|
|
13764
|
+
srift_an_call: ["Call Agent", false, false, true, false],
|
|
13765
|
+
srift_an_accept_call: ["Accept Call", false, false, true, false],
|
|
13766
|
+
srift_an_calls: ["Manage Calls", false, false, true, false],
|
|
13767
|
+
srift_an_invite: ["Create Invite Link", false, false, false, false],
|
|
13768
|
+
srift_an_contacts_add: ["Add Contact", false, false, false, true],
|
|
13769
|
+
srift_an_block: ["Block Agent", false, true, false, true],
|
|
13770
|
+
srift_an_set_hook: ["Set Knock Policy", false, false, false, true]
|
|
13771
|
+
};
|
|
13772
|
+
for (const tool of AGENTNET_TOOLS) {
|
|
13773
|
+
const m = TOOL_META[tool.name];
|
|
13774
|
+
if (!m) continue;
|
|
13775
|
+
tool.title = m[0];
|
|
13776
|
+
tool.annotations = { title: m[0], readOnlyHint: m[1], destructiveHint: m[2], openWorldHint: m[3], idempotentHint: m[4] };
|
|
13777
|
+
}
|
|
13598
13778
|
discoveries = [];
|
|
13599
13779
|
RESOURCES = [
|
|
13600
13780
|
{ uri: INBOX_URI, name: "AgentNet inbox", description: "Unread messages, knocks and call invites (subscribe for notifications)", mimeType: "application/json" },
|
|
@@ -15691,7 +15871,7 @@ var __filename = fileURLToPath2(import.meta.url);
|
|
|
15691
15871
|
var __dirname = path14.dirname(__filename);
|
|
15692
15872
|
var DAEMON_PORT3 = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
|
|
15693
15873
|
var DAEMON_URL3 = `http://127.0.0.1:${DAEMON_PORT3}`;
|
|
15694
|
-
var CLI_VERSION = "4.3.
|
|
15874
|
+
var CLI_VERSION = "4.3.2";
|
|
15695
15875
|
function parseDuration(s) {
|
|
15696
15876
|
if (!s) return 0;
|
|
15697
15877
|
const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i.exec(s.trim());
|