srift-transfer 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +33 -20
  2. package/dist/daemon.js +349 -158
  3. package/dist/index.js +650 -426
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1289,12 +1289,13 @@ function withQuery(url, q) {
1289
1289
  return `${url}${url.includes("?") ? "&" : "?"}${q}`;
1290
1290
  }
1291
1291
  function explainStatus(status, body) {
1292
- const msg = body.replace(/^SRIFT:\s*/, "").trim();
1292
+ let msg = body.replace(/^SRIFT:\s*/, "").trim();
1293
+ if (/^\s*<(!doctype|html)/i.test(msg)) msg = status >= 500 ? "The SRIFT server or a proxy in front of it is temporarily unavailable \u2014 retry shortly" : "";
1294
+ if (status === 424 || status === 502 && /sender error: (the shared file|cannot read the shared file)/i.test(msg)) return new GetError(msg || "The sender can no longer serve this file.", "ESENDERFILE", status);
1293
1295
  if (status === 404) return new GetError(msg || "Link not found \u2014 it expired, was revoked, or the sender went offline.", "ENOTFOUND_LINK", 404);
1294
1296
  if (status === 410) return new GetError(msg || "Link expired or reached its download limit.", "EGONE", 410);
1295
1297
  if (status === 503) return new GetError(msg || "Sender is offline \u2014 their daemon must be running for relay links.", "EOFFLINE", 503);
1296
1298
  if (status === 409) return new GetError(msg || "The sender is still uploading \u2014 retry shortly.", "ENOTREADY", 409);
1297
- if (status === 502 && /sender error: (the shared file|cannot read the shared file)/i.test(msg)) return new GetError(msg, "ESENDERFILE", 502);
1298
1299
  return new GetError(`${msg || "Download failed"} (HTTP ${status})`, "EHTTP", status);
1299
1300
  }
1300
1301
  async function readSmallBody(res, max = 4096) {
@@ -1481,7 +1482,7 @@ async function getLink(input, opts = {}) {
1481
1482
  }
1482
1483
  const serverName = filenameFromDisposition(head["content-disposition"]) || "download.bin";
1483
1484
  const mode = head["x-srift-mode"] ? String(head["x-srift-mode"]) : null;
1484
- const total = parseInt(String(head["content-length"] || ""), 10) || 0;
1485
+ const total = parseInt(String(head["x-srift-size"] || head["content-length"] || ""), 10) || 0;
1485
1486
  let received = 0;
1486
1487
  let lastPrint = 0;
1487
1488
  const progress = (n) => {
@@ -1744,10 +1745,10 @@ var init_skills_data = __esm({
1744
1745
  "../../lib/mcp/skills-data.mjs"() {
1745
1746
  "use strict";
1746
1747
  SOURCES = [
1747
- { 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\nAll messages and file payloads are encrypted client-side with AES-256-GCM before leaving the device; the daemon never has access to plaintext.\n" },
1748
+ { 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, using keys derived from the session (plus the optional `roomSecret`); the relay server only forwards ciphertext and never receives the key. Without a `roomSecret` the key is derivable from the session ID (which the server sees); set one on both sides to make it participant-only.\n" },
1748
1749
  { name: "get-company-info", content: "---\nname: get-company-info\ndescription: Retrieve overview and core metadata for SRIFT and SRIPTO Corporation\n---\n\n# get-company-info\n\nRetrieve high-level overview and specifications about SRIFT and SRIPTO Corporation.\n\n## Requirements\n\nNo special credentials required.\n\n## Instructions\n\nRead the home page or `llms.txt` to find details about Srift (secure P2P transport), LetMeTeach, SRIX, and A\u039EI.\n" },
1749
1750
  { 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 -OJ <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 honoured.\n\nSee `/openapi.json` and `/.well-known/mcp/server-card.json` for the full tool contract.\n" },
1750
- { 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: disconnect and ban a peer.\n- `srift_close_session` \u2014 tear down the session and flush all encryption keys.\n\nAll session state is ephemeral and in-memory; nothing is persisted server-side. Keys are derived locally via PBKDF2-SHA256 and never leave the client device.\n' }
1751
+ { 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: session metadata and chat ciphertext are kept server-side only until the session is deleted (host close or 7 days of inactivity), and files are never stored. Keys are derived locally via PBKDF2-SHA256 from the session ID (plus the optional `roomSecret`) and are never sent to the server; without a `roomSecret` the key is derivable from the session ID, which the server sees, so pass one to `srift_start_session` / `srift_join_session` to make it participant-only.\n' }
1751
1752
  ];
1752
1753
  MCP_SKILLS = SOURCES.map(({ name, content }) => {
1753
1754
  const uri = `skill://${name}/SKILL.md`;
@@ -2006,7 +2007,7 @@ var init_core = __esm({
2006
2007
  type: "object",
2007
2008
  properties: {
2008
2009
  sessionName: { type: "string", description: 'Human-readable session name (optional, e.g. "Project Collab")' },
2009
- roomSecret: { type: "string", description: "Optional shared secret for extra-strong E2EE key derivation. Never sent to server." }
2010
+ roomSecret: { type: "string", description: "Optional shared secret mixed into key derivation. Never sent to server. Without it the key is derivable from the session ID (which the server sees); set it to make the key participant-only." }
2010
2011
  }
2011
2012
  },
2012
2013
  outputSchema: {
@@ -2057,15 +2058,37 @@ var init_core = __esm({
2057
2058
  {
2058
2059
  name: "srift_session_status",
2059
2060
  title: "Get Session Status",
2060
- description: "Get current session details: id, role (host/guest), connection state, pending join requests, peer count.",
2061
+ description: "Get current session details: session { id, role (host/guest), connection state, your userId }, pending join requests (approve with srift_approve_join), and members with their userIds (remove one with srift_kick_user).",
2061
2062
  inputSchema: { type: "object", properties: {} },
2062
2063
  outputSchema: {
2063
2064
  type: "object",
2064
2065
  properties: {
2065
- sessionId: { type: "string", description: "Active session ID, if any" },
2066
- role: { type: "string", description: 'Active role: "host", "guest", or null' },
2067
- isConnected: { type: "boolean", description: "True if connected to signaling server" },
2068
- peerCount: { type: "number", description: "Number of connected peers in the room" },
2066
+ session: {
2067
+ type: "object",
2068
+ description: "The active session (id is null when there is none)",
2069
+ properties: {
2070
+ id: { type: ["string", "null"], description: "Active session ID" },
2071
+ name: { type: ["string", "null"], description: "Session name" },
2072
+ role: { type: ["string", "null"], description: '"host", "guest", or null' },
2073
+ isConnected: { type: "boolean", description: "True if connected to the signaling server" },
2074
+ userId: { type: ["string", "null"], description: "Your user id in the session" },
2075
+ status: { type: "string", description: 'Guest only: "pending_approval" or "approved" (hosted endpoint)' }
2076
+ }
2077
+ },
2078
+ peerCount: { type: "number", description: "Number of connected sockets in the room (hosted endpoint)" },
2079
+ participants: {
2080
+ type: "array",
2081
+ description: "Session members (local daemon): pass userId to srift_kick_user to remove one",
2082
+ items: {
2083
+ type: "object",
2084
+ properties: {
2085
+ userId: { type: "string" },
2086
+ username: { type: "string" },
2087
+ isHost: { type: "boolean" },
2088
+ online: { type: "boolean" }
2089
+ }
2090
+ }
2091
+ },
2069
2092
  pendingJoins: {
2070
2093
  type: "array",
2071
2094
  description: "List of pending guest join requests waiting for host approval",
@@ -2159,7 +2182,7 @@ var init_core = __esm({
2159
2182
  {
2160
2183
  name: "srift_kick_user",
2161
2184
  title: "Kick Peer",
2162
- description: "Host-only. Disconnect and ban a peer from the active room.",
2185
+ description: "Host-only. Disconnect and remove a peer from the active room. Get the peer's userId from srift_session_status (participants).",
2163
2186
  inputSchema: {
2164
2187
  type: "object",
2165
2188
  properties: {
@@ -2427,6 +2450,8 @@ var init_core = __esm({
2427
2450
  type: "object",
2428
2451
  properties: {
2429
2452
  ok: { type: "boolean", description: "True if the diagnostic ran successfully (independent of what it found)" },
2453
+ base: { type: "string", description: "The SRIFT server that was probed" },
2454
+ checkedAt: { type: "string", description: "When the probe ran (ISO 8601); cached results show an older time" },
2430
2455
  verdict: { type: "string", description: '"ok", "degraded", or "blocked"' },
2431
2456
  rungs: {
2432
2457
  type: "array",
@@ -2572,8 +2597,13 @@ session-management, and get-company-info, \`skills/get\` to fetch one by URI, an
2572
2597
  ## Security model
2573
2598
  - AES-256-GCM, 12-byte random IV per message.
2574
2599
  - PBKDF2-SHA256, 100,000 iterations.
2575
- - All keys derive locally from sessionId (+ optional roomSecret). Server never sees them.
2576
- - File chunks transit P2P (WebTorrent for >10MB, WebSocket-relay fallback otherwise).
2600
+ - All keys derive locally from sessionId (+ optional roomSecret). The key is never sent to the
2601
+ server, but the server sees the sessionId: without a roomSecret the key is derivable from it.
2602
+ Pass roomSecret to srift_start_session / srift_join_session to make the key participant-only.
2603
+ - Quick-share links are end-to-end encrypted only with encrypt: true (key in the #k= fragment);
2604
+ otherwise TLS in transit only, zero retention.
2605
+ - The CLI daemon moves file chunks over the AES-256-GCM WebSocket relay (browsers use WebRTC for
2606
+ files >5 MB, with the relay as fallback).
2577
2607
 
2578
2608
  ## Local daemon vs hosted MCP
2579
2609
  If you are talking to the hosted endpoint (https://srift.app/mcp) instead of a local
@@ -2590,6 +2620,8 @@ for full file-transfer + chat capability.
2590
2620
 
2591
2621
  // ../../cli/mcp.ts
2592
2622
  import http3 from "http";
2623
+ import fs6 from "fs";
2624
+ import path5 from "path";
2593
2625
  function callDaemon(endpoint, method, body) {
2594
2626
  const emb = embeddedCall(method, endpoint, body);
2595
2627
  if (emb) return emb.then((r) => {
@@ -2640,43 +2672,60 @@ async function callTool(name, args) {
2640
2672
  sessionName: args.sessionName,
2641
2673
  roomSecret: args.roomSecret
2642
2674
  });
2643
- return `Session created.
2675
+ const joinUrl = res.joinUrl || `https://srift.app/join-session?id=${res.sessionId}`;
2676
+ return {
2677
+ text: `Session created.
2644
2678
  Session ID: ${res.sessionId}
2645
- Share URL: https://srift.app/join-session?id=${res.sessionId}
2646
- You are the HOST. Approve incoming joins with srift_approve_join.`;
2679
+ Share URL: ${joinUrl}
2680
+ You are the HOST. Approve incoming joins with srift_approve_join.`,
2681
+ structured: { sessionId: res.sessionId, url: joinUrl, role: "host" }
2682
+ };
2647
2683
  }
2648
2684
  case "srift_join_session": {
2649
2685
  const res = await callDaemon("/session/join", "POST", args);
2650
- return `Join requested for session ${args.sessionId}. Status: ${res.success ? "awaiting host approval" : "failed"}.`;
2686
+ return {
2687
+ text: `Join requested for session ${args.sessionId}. Status: ${res.success ? "awaiting host approval" : "failed"}.`,
2688
+ structured: { sessionId: args.sessionId, status: res.success ? "awaiting_approval" : "failed" }
2689
+ };
2651
2690
  }
2652
2691
  case "srift_session_status": {
2653
2692
  const res = await callDaemon("/status", "GET");
2654
- return JSON.stringify({ session: res.session, pendingJoins: res.pendingJoins }, null, 2);
2693
+ return JSON.stringify({ session: res.session, pendingJoins: res.pendingJoins, participants: res.participants || [] }, null, 2);
2655
2694
  }
2656
2695
  case "srift_close_session":
2657
2696
  await callDaemon("/session/close", "POST");
2658
- return "Session closed. Keys flushed.";
2697
+ return { text: "Session closed. Keys flushed.", structured: { success: true } };
2659
2698
  case "srift_approve_join":
2660
2699
  await callDaemon("/session/approve", "POST", args);
2661
- return `Approved ${args.tempUserId}.`;
2700
+ return { text: `Approved ${args.tempUserId}.`, structured: { success: true, tempUserId: args.tempUserId } };
2662
2701
  case "srift_reject_join":
2663
2702
  await callDaemon("/session/reject", "POST", args);
2664
- return `Rejected ${args.tempUserId}.`;
2703
+ return { text: `Rejected ${args.tempUserId}.`, structured: { success: true, tempUserId: args.tempUserId } };
2665
2704
  case "srift_kick_user":
2666
2705
  await callDaemon("/session/kick", "POST", args);
2667
- return `Kicked ${args.userId}.`;
2706
+ return { text: `Kicked ${args.userId}.`, structured: { success: true, userId: args.userId } };
2668
2707
  case "srift_send_file": {
2669
2708
  const res = await callDaemon("/send", "POST", args);
2670
- return `File offered.
2709
+ return {
2710
+ text: `File offered.
2671
2711
  File ID: ${res.fileId}
2672
- The peer must call srift_accept_transfer with this fileId.`;
2712
+ The peer must call srift_accept_transfer with this fileId.`,
2713
+ structured: { fileId: res.fileId, fileName: path5.basename(String(args.filePath || "")), fileSize: (() => {
2714
+ try {
2715
+ return fs6.statSync(args.filePath).size;
2716
+ } catch {
2717
+ return void 0;
2718
+ }
2719
+ })() }
2720
+ };
2673
2721
  }
2674
2722
  case "srift_accept_transfer":
2675
2723
  await callDaemon("/receive", "POST", args);
2676
- return `Transfer ${args.fileId} accepted. Downloading\u2026`;
2724
+ return { text: `Transfer ${args.fileId} accepted. Downloading\u2026`, structured: { success: true, fileId: args.fileId } };
2677
2725
  case "srift_list_transfers": {
2678
2726
  const res = await callDaemon("/status", "GET");
2679
- return JSON.stringify(res.activeTransfers || [], null, 2);
2727
+ const transfers = res.activeTransfers || [];
2728
+ return { text: JSON.stringify(transfers, null, 2), structured: { transfers } };
2680
2729
  }
2681
2730
  case "srift_quick_share": {
2682
2731
  const multi = Array.isArray(args?.filePaths) && args.filePaths.length > 0;
@@ -2770,10 +2819,11 @@ The peer must call srift_accept_transfer with this fileId.`;
2770
2819
  }
2771
2820
  case "srift_send_chat":
2772
2821
  await callDaemon("/chat/send", "POST", args);
2773
- return `Sent (E2EE): "${args.message}"`;
2822
+ return { text: `Sent (E2EE): "${args.message}"`, structured: { success: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() } };
2774
2823
  case "srift_chat_history": {
2775
2824
  const res = await callDaemon("/chat/history", "GET");
2776
- return JSON.stringify(res, null, 2);
2825
+ const messages = Array.isArray(res) ? res : res?.messages || [];
2826
+ return { text: JSON.stringify(res, null, 2), structured: { messages } };
2777
2827
  }
2778
2828
  case "srift_read_state": {
2779
2829
  const res = await callDaemon("/state", "GET");
@@ -2846,7 +2896,7 @@ var init_mcp = __esm({
2846
2896
  SERVER_INFO = {
2847
2897
  name: "srift-mcp-server",
2848
2898
  title: "SRIFT Secure P2P File Transfer",
2849
- version: "4.0.0"
2899
+ version: "4.1.0"
2850
2900
  };
2851
2901
  localDaemonBackend = { callTool, readResource };
2852
2902
  handleMcpMessage = createMcpHandler({
@@ -2858,8 +2908,8 @@ var init_mcp = __esm({
2858
2908
  });
2859
2909
 
2860
2910
  // ../../cli/pack.ts
2861
- import fs6 from "fs";
2862
- import path5 from "path";
2911
+ import fs7 from "fs";
2912
+ import path6 from "path";
2863
2913
  import zlib from "zlib";
2864
2914
  import { pipeline } from "stream/promises";
2865
2915
  import { Readable } from "stream";
@@ -2883,16 +2933,16 @@ function normFileMode(m) {
2883
2933
  }
2884
2934
  function listFiles(root, excludes = DEFAULT_EXCLUDES) {
2885
2935
  const pats = excludes.map(globToRegExp);
2886
- const excluded = (rel) => pats.some((p) => p.test(rel) || p.test(path5.posix.basename(rel)));
2936
+ const excluded = (rel) => pats.some((p) => p.test(rel) || p.test(path6.posix.basename(rel)));
2887
2937
  const out = [];
2888
2938
  const walk = (dirAbs, dirRel) => {
2889
- const ents = fs6.readdirSync(dirAbs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
2939
+ const ents = fs7.readdirSync(dirAbs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
2890
2940
  for (const e of ents) {
2891
2941
  const rel = dirRel ? `${dirRel}/${e.name}` : e.name;
2892
2942
  if (excluded(rel)) continue;
2893
- const abs = path5.join(dirAbs, e.name);
2943
+ const abs = path6.join(dirAbs, e.name);
2894
2944
  if (e.isSymbolicLink()) continue;
2895
- const st = fs6.lstatSync(abs);
2945
+ const st = fs7.lstatSync(abs);
2896
2946
  if (e.isDirectory()) {
2897
2947
  out.push({ abs, rel, size: 0, mode: 493, mtime: Math.floor(st.mtimeMs / 1e3), dir: true });
2898
2948
  walk(abs, rel);
@@ -2954,7 +3004,7 @@ async function* tarStream(entries, prefix) {
2954
3004
  yield* entryHeaders(e, prefix);
2955
3005
  if (e.dir) continue;
2956
3006
  let written = 0;
2957
- const stream = fs6.createReadStream(e.abs);
3007
+ const stream = fs7.createReadStream(e.abs);
2958
3008
  stream.on("error", () => {
2959
3009
  });
2960
3010
  try {
@@ -2975,17 +3025,17 @@ async function* tarStream(entries, prefix) {
2975
3025
  yield Buffer.alloc(BLOCK * 2);
2976
3026
  }
2977
3027
  async function packDirectory(dir, outFile, excludes = DEFAULT_EXCLUDES) {
2978
- const root = path5.resolve(dir);
3028
+ const root = path6.resolve(dir);
2979
3029
  const entries = listFiles(root, excludes);
2980
3030
  const files = entries.filter((e) => !e.dir);
2981
3031
  if (!files.length) throw new Error(`Nothing to share in ${root} (empty, or everything was excluded)`);
2982
- const prefix = path5.basename(root) || "folder";
2983
- await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }), fs6.createWriteStream(outFile, { mode: 384 }));
3032
+ const prefix = path6.basename(root) || "folder";
3033
+ await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }), fs7.createWriteStream(outFile, { mode: 384 }));
2984
3034
  return { files: files.length, bytes: files.reduce((a, e) => a + e.size, 0) };
2985
3035
  }
2986
3036
  function readable(p) {
2987
3037
  try {
2988
- fs6.accessSync(p, fs6.constants.R_OK);
3038
+ fs7.accessSync(p, fs7.constants.R_OK);
2989
3039
  return null;
2990
3040
  } catch (e) {
2991
3041
  return e?.code || "unreadable";
@@ -2999,7 +3049,7 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
2999
3049
  used.add(name.toLowerCase());
3000
3050
  return name;
3001
3051
  }
3002
- const ext = path5.extname(name);
3052
+ const ext = path6.extname(name);
3003
3053
  const stem2 = name.slice(0, name.length - ext.length);
3004
3054
  for (let i = 2; ; i++) {
3005
3055
  const c = `${stem2} (${i})${ext}`;
@@ -3010,10 +3060,10 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
3010
3060
  }
3011
3061
  };
3012
3062
  for (const p of paths) {
3013
- const abs = path5.resolve(p);
3063
+ const abs = path6.resolve(p);
3014
3064
  let st;
3015
3065
  try {
3016
- st = fs6.statSync(abs);
3066
+ st = fs7.statSync(abs);
3017
3067
  } catch (e) {
3018
3068
  skipped.push({ path: abs, reason: e?.code === "ENOENT" ? "not found" : e?.code || "stat failed" });
3019
3069
  continue;
@@ -3026,7 +3076,7 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
3026
3076
  skipped.push({ path: abs, reason: e?.code || "unreadable folder" });
3027
3077
  continue;
3028
3078
  }
3029
- const name = unique(path5.basename(abs) || "item");
3079
+ const name = unique(path6.basename(abs) || "item");
3030
3080
  out.push({ abs, rel: name, size: 0, mode: 493, mtime: Math.floor(st.mtimeMs / 1e3), dir: true });
3031
3081
  for (const e of inner) {
3032
3082
  const why = e.dir ? null : readable(e.abs);
@@ -3042,7 +3092,7 @@ function listPaths(paths, excludes = DEFAULT_EXCLUDES, skipped = []) {
3042
3092
  skipped.push({ path: abs, reason: why });
3043
3093
  continue;
3044
3094
  }
3045
- out.push({ abs, rel: unique(path5.basename(abs) || "item"), size: st.size, mode: normFileMode(st.mode), mtime: Math.floor(st.mtimeMs / 1e3), dir: false });
3095
+ out.push({ abs, rel: unique(path6.basename(abs) || "item"), size: st.size, mode: normFileMode(st.mode), mtime: Math.floor(st.mtimeMs / 1e3), dir: false });
3046
3096
  } else {
3047
3097
  skipped.push({ path: abs, reason: "not a regular file or folder" });
3048
3098
  }
@@ -3059,7 +3109,7 @@ async function packPaths(paths, outFile, prefix, excludes = DEFAULT_EXCLUDES) {
3059
3109
  throw new Error(`Nothing to share${skipped.length ? ` \u2014 skipped: ${skipped.map((x) => `${x.path} (${x.reason})`).join(", ")}` : " (empty folders, or everything was excluded)"}`);
3060
3110
  }
3061
3111
  try {
3062
- await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }), fs6.createWriteStream(outFile, { mode: 384 }));
3112
+ await pipeline(Readable.from(tarStream(entries, prefix)), zlib.createGzip({ level: 6 }), fs7.createWriteStream(outFile, { mode: 384 }));
3063
3113
  return { files: files.length, bytes: files.reduce((a, e) => a + e.size, 0), skipped };
3064
3114
  } catch (err) {
3065
3115
  const bad = err?.packEntry;
@@ -3100,10 +3150,10 @@ import express from "express";
3100
3150
  import cors from "cors";
3101
3151
  import { WebSocket as WebSocket2 } from "ws";
3102
3152
  import { webcrypto } from "crypto";
3103
- import fs7 from "fs";
3153
+ import fs8 from "fs";
3104
3154
  import http4 from "http";
3105
3155
  import { Duplex } from "stream";
3106
- import path6 from "path";
3156
+ import path7 from "path";
3107
3157
  import os4 from "os";
3108
3158
  import { v4 as uuidv4 } from "uuid";
3109
3159
  import dotenv from "dotenv";
@@ -3234,6 +3284,46 @@ async function decrypt(encB64) {
3234
3284
  const decrypted = await crypto5.subtle.decrypt({ name: "AES-GCM", iv: iv2 }, encryptionKey, ciphertext);
3235
3285
  return new TextDecoder().decode(decrypted);
3236
3286
  }
3287
+ function relayKey() {
3288
+ if (!session.id) return Promise.reject(new Error("No active session"));
3289
+ if (relayKeyCache?.sessionId !== session.id) relayKeyCache = { sessionId: session.id, key: deriveKey(session.id) };
3290
+ return relayKeyCache.key;
3291
+ }
3292
+ function relayAad(fileId, chunkIndex, totalChunks) {
3293
+ return new TextEncoder().encode(`${RELAY_ENC}|${fileId}|${chunkIndex}|${totalChunks}`);
3294
+ }
3295
+ async function sealRelayChunk(plain, fileId, chunkIndex, totalChunks) {
3296
+ const iv2 = crypto5.getRandomValues(new Uint8Array(IV_LEN));
3297
+ const ct = new Uint8Array(await crypto5.subtle.encrypt(
3298
+ { name: "AES-GCM", iv: iv2, additionalData: relayAad(fileId, chunkIndex, totalChunks) },
3299
+ await relayKey(),
3300
+ plain
3301
+ ));
3302
+ const out = new Uint8Array(IV_LEN + ct.length);
3303
+ out.set(iv2);
3304
+ out.set(ct, IV_LEN);
3305
+ return Array.from(out);
3306
+ }
3307
+ async function openRelayChunk(p) {
3308
+ if (p.enc === void 0 || p.enc === null) {
3309
+ if (typeof p.chunk === "string") return Buffer.from(p.chunk, "base64");
3310
+ if (Array.isArray(p.chunk)) return Buffer.from(p.chunk);
3311
+ throw new Error("relay chunk is neither base64 nor a byte array");
3312
+ }
3313
+ if (p.enc !== RELAY_ENC) throw new Error(`unsupported relay encryption scheme: ${String(p.enc)}`);
3314
+ if (!Array.isArray(p.chunk)) throw new Error("sealed relay chunk is not a byte array");
3315
+ const data = Uint8Array.from(p.chunk);
3316
+ if (data.length < IV_LEN + 16) throw new Error("sealed relay chunk too short");
3317
+ try {
3318
+ return Buffer.from(await crypto5.subtle.decrypt(
3319
+ { name: "AES-GCM", iv: data.slice(0, IV_LEN), additionalData: relayAad(p.fileId, p.chunkIndex, p.totalChunks) },
3320
+ await relayKey(),
3321
+ data.slice(IV_LEN)
3322
+ ));
3323
+ } catch {
3324
+ throw new Error(`relay chunk ${p.chunkIndex} of ${p.fileId} failed authentication (wrong session key or tampered data)`);
3325
+ }
3326
+ }
3237
3327
  function getTrackers(sessionId) {
3238
3328
  const list = [];
3239
3329
  try {
@@ -3272,13 +3362,13 @@ function writeStateFile() {
3272
3362
  })),
3273
3363
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
3274
3364
  };
3275
- fs7.writeFileSync(STATE_PATH, JSON.stringify(stateData, null, 2), "utf-8");
3365
+ fs8.writeFileSync(STATE_PATH, JSON.stringify(stateData, null, 2), "utf-8");
3276
3366
  }
3277
3367
  function pruneStaleStateOnStartup() {
3278
- if (!fs7.existsSync(STATE_PATH)) return;
3368
+ if (!fs8.existsSync(STATE_PATH)) return;
3279
3369
  let prunedCount = 0;
3280
3370
  try {
3281
- const raw = fs7.readFileSync(STATE_PATH, "utf-8");
3371
+ const raw = fs8.readFileSync(STATE_PATH, "utf-8");
3282
3372
  const parsed = JSON.parse(raw);
3283
3373
  const now = Date.now();
3284
3374
  const lastUpdatedMs = parsed?.lastUpdated ? Date.parse(parsed.lastUpdated) : NaN;
@@ -3297,7 +3387,7 @@ function pruneStaleStateOnStartup() {
3297
3387
  if (sessionStale || freshTransfers.length !== transfers.length) {
3298
3388
  parsed.activeTransfers = freshTransfers;
3299
3389
  parsed.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
3300
- fs7.writeFileSync(STATE_PATH, JSON.stringify(parsed, null, 2), "utf-8");
3390
+ fs8.writeFileSync(STATE_PATH, JSON.stringify(parsed, null, 2), "utf-8");
3301
3391
  }
3302
3392
  if (prunedCount > 0) {
3303
3393
  console.log(
@@ -3307,7 +3397,7 @@ function pruneStaleStateOnStartup() {
3307
3397
  } catch (err) {
3308
3398
  console.warn("[DAEMON] Stale state file was unreadable, removing it:", err?.message || err);
3309
3399
  try {
3310
- fs7.unlinkSync(STATE_PATH);
3400
+ fs8.unlinkSync(STATE_PATH);
3311
3401
  } catch {
3312
3402
  }
3313
3403
  }
@@ -3328,8 +3418,11 @@ function connectWebSocket(wsTargetUrl) {
3328
3418
  }
3329
3419
  csrfToken = null;
3330
3420
  console.log(`[DAEMON] Connecting to Signaler WS: ${wsTargetUrl}`);
3331
- wsConn = new WebSocket2(wsTargetUrl, { agent: agentFor(wsTargetUrl) });
3332
- wsConn.on("open", () => {
3421
+ const sock = new WebSocket2(wsTargetUrl, { agent: agentFor(wsTargetUrl) });
3422
+ wsConn = sock;
3423
+ const current = () => wsConn === sock;
3424
+ sock.on("open", () => {
3425
+ if (!current()) return;
3333
3426
  console.log("[DAEMON] WebSocket connection open");
3334
3427
  session.isConnected = true;
3335
3428
  writeStateFile();
@@ -3367,7 +3460,8 @@ function connectWebSocket(wsTargetUrl) {
3367
3460
  }
3368
3461
  }, 3e4);
3369
3462
  });
3370
- wsConn.on("message", async (dataStr) => {
3463
+ sock.on("message", async (dataStr) => {
3464
+ if (!current()) return;
3371
3465
  try {
3372
3466
  const msg = JSON.parse(dataStr);
3373
3467
  const { type, payload } = msg;
@@ -3400,6 +3494,30 @@ function connectWebSocket(wsTargetUrl) {
3400
3494
  case "heartbeat_ack":
3401
3495
  lastHeartbeatAckAt = Date.now();
3402
3496
  break;
3497
+ case "user_list": {
3498
+ const users = Array.isArray(payload?.users) ? payload.users : [];
3499
+ participants = users.map((u) => ({ userId: String(u.id), username: String(u.username || ""), isHost: !!u.is_host, online: !!u.isOnline }));
3500
+ broadcastSSE("participants", { participants });
3501
+ if (payload?.messageId) {
3502
+ try {
3503
+ sock.send(JSON.stringify({ type: "user_list_ack", payload: { messageId: payload.messageId } }));
3504
+ } catch {
3505
+ }
3506
+ }
3507
+ break;
3508
+ }
3509
+ case "kicked_from_session":
3510
+ console.log(`[DAEMON] Kicked from session: ${payload?.reason || ""}`);
3511
+ sessionTerminated = true;
3512
+ terminationReason = payload?.reason || "You were removed from the session by the host";
3513
+ session.isConnected = false;
3514
+ session.userId = null;
3515
+ csrfToken = null;
3516
+ pendingJoins = [];
3517
+ participants = [];
3518
+ writeStateFile();
3519
+ broadcastSSE("session_terminated", { reason: terminationReason });
3520
+ break;
3403
3521
  case "session_deleted":
3404
3522
  console.log(`[DAEMON] Session deleted by host: ${payload.reason || ""}`);
3405
3523
  sessionTerminated = true;
@@ -3407,6 +3525,7 @@ function connectWebSocket(wsTargetUrl) {
3407
3525
  session = { id: null, name: null, role: null, isConnected: false, userId: null };
3408
3526
  activeTransfers = [];
3409
3527
  pendingJoins = [];
3528
+ participants = [];
3410
3529
  encryptionKey = null;
3411
3530
  writeStateFile();
3412
3531
  broadcastSSE("session_terminated", { reason: terminationReason });
@@ -3423,6 +3542,7 @@ function connectWebSocket(wsTargetUrl) {
3423
3542
  session.isConnected = false;
3424
3543
  session.userId = null;
3425
3544
  pendingJoins = [];
3545
+ participants = [];
3426
3546
  writeStateFile();
3427
3547
  broadcastSSE("session_terminated", { reason: terminationReason });
3428
3548
  }
@@ -3477,14 +3597,41 @@ function connectWebSocket(wsTargetUrl) {
3477
3597
  break;
3478
3598
  case "file_accept":
3479
3599
  console.log(`[DAEMON] File offer accepted by peer! ID: ${payload.fileId}`);
3600
+ {
3601
+ const acceptedTx = activeTransfers.find((t) => t.fileId === payload.fileId);
3602
+ if (acceptedTx) acceptedTx.peerRelayEnc = payload.relayEnc === RELAY_ENC ? RELAY_ENC : void 0;
3603
+ }
3480
3604
  startUpload(payload.fileId, payload.userId);
3481
3605
  break;
3482
3606
  case "webtorrent_info_hash":
3483
3607
  handleWebTorrentInfoHash(payload);
3484
3608
  break;
3485
3609
  case "file_chunk":
3486
- handleIncomingChunk(payload);
3610
+ handleIncomingChunk(payload).catch((e) => {
3611
+ console.error(`[DAEMON] Relay chunk rejected: ${e?.message || e}`);
3612
+ const bad = activeTransfers.find((t) => t.fileId === payload?.fileId);
3613
+ if (bad) {
3614
+ bad.status = "error";
3615
+ writeStateFile();
3616
+ }
3617
+ try {
3618
+ sock.send(JSON.stringify({ type: "file_cancel_by_receiver", payload: { fileId: payload?.fileId } }));
3619
+ } catch {
3620
+ }
3621
+ });
3487
3622
  break;
3623
+ case "file_cancelled_by_receiver":
3624
+ case "file_cancelled_by_sender":
3625
+ case "file_cancelled_all": {
3626
+ const gone = activeTransfers.find((t) => t.fileId === payload?.fileId);
3627
+ activeUploads.delete(payload?.fileId);
3628
+ if (gone && gone.status !== "completed") {
3629
+ gone.status = "cancelled";
3630
+ writeStateFile();
3631
+ broadcastSSE("transfer_progress", { fileId: gone.fileId, fileName: gone.name, size: gone.size, bytesTransferred: gone.bytesTransferred, progress: gone.progress, status: "cancelled" });
3632
+ }
3633
+ break;
3634
+ }
3488
3635
  case "file_chunk_ack":
3489
3636
  handleChunkAck(payload);
3490
3637
  break;
@@ -3496,6 +3643,7 @@ function connectWebSocket(wsTargetUrl) {
3496
3643
  if (entry) {
3497
3644
  entry.token = payload.token;
3498
3645
  entry.downloadUrl = payload.downloadUrl;
3646
+ if (typeof payload.claim === "string") entry.claim = payload.claim;
3499
3647
  pubsharesByToken.set(payload.token, entry);
3500
3648
  const resolver = pubshareRegResolvers.get(payload.fileId);
3501
3649
  if (resolver) {
@@ -3529,7 +3677,8 @@ function connectWebSocket(wsTargetUrl) {
3529
3677
  console.error("[DAEMON] Error processing WS message:", err);
3530
3678
  }
3531
3679
  });
3532
- wsConn.on("close", () => {
3680
+ sock.on("close", () => {
3681
+ if (!current()) return;
3533
3682
  console.log("[DAEMON] WebSocket connection closed");
3534
3683
  session.isConnected = false;
3535
3684
  csrfToken = null;
@@ -3549,7 +3698,8 @@ function connectWebSocket(wsTargetUrl) {
3549
3698
  }
3550
3699
  }, 3e3);
3551
3700
  });
3552
- wsConn.on("error", (err) => {
3701
+ sock.on("error", (err) => {
3702
+ if (!current()) return;
3553
3703
  console.error("[DAEMON] WebSocket error:", err);
3554
3704
  });
3555
3705
  }
@@ -3687,7 +3837,7 @@ async function startWebSocketUpload(fileId, targetUserId) {
3687
3837
  const tx = activeTransfers.find((t) => t.fileId === fileId);
3688
3838
  if (!tx || !tx.filePath) return;
3689
3839
  const chunkSize = 64 * 1024;
3690
- const stat = fs7.statSync(tx.filePath);
3840
+ const stat = fs8.statSync(tx.filePath);
3691
3841
  const totalChunks = Math.max(1, Math.ceil(stat.size / chunkSize));
3692
3842
  tx.status = "uploading";
3693
3843
  tx.startTime = Date.now();
@@ -3705,6 +3855,17 @@ async function startWebSocketUpload(fileId, targetUserId) {
3705
3855
  sendNextChunk(fileId);
3706
3856
  }
3707
3857
  function sendNextChunk(fileId) {
3858
+ sendNextChunkAsync(fileId).catch((e) => {
3859
+ console.error(`[DAEMON] Relay upload failed for ${fileId}: ${e?.message || e}`);
3860
+ activeUploads.delete(fileId);
3861
+ const failed = activeTransfers.find((t) => t.fileId === fileId);
3862
+ if (failed) {
3863
+ failed.status = "error";
3864
+ writeStateFile();
3865
+ }
3866
+ });
3867
+ }
3868
+ async function sendNextChunkAsync(fileId) {
3708
3869
  const upload = activeUploads.get(fileId);
3709
3870
  if (!upload) return;
3710
3871
  const tx = activeTransfers.find((t) => t.fileId === fileId);
@@ -3724,18 +3885,18 @@ function sendNextChunk(fileId) {
3724
3885
  return;
3725
3886
  }
3726
3887
  const start = upload.currentChunk * upload.chunkSize;
3727
- const stat = fs7.statSync(upload.filePath);
3888
+ const stat = fs8.statSync(upload.filePath);
3728
3889
  const end = Math.min(start + upload.chunkSize, stat.size);
3729
3890
  const buffer = Buffer.alloc(end - start);
3730
- const fd = fs7.openSync(upload.filePath, "r");
3731
- fs7.readSync(fd, buffer, 0, end - start, start);
3732
- fs7.closeSync(fd);
3733
- const chunkBase64 = buffer.toString("base64");
3891
+ const fd = fs8.openSync(upload.filePath, "r");
3892
+ fs8.readSync(fd, buffer, 0, end - start, start);
3893
+ fs8.closeSync(fd);
3894
+ const frame = tx.peerRelayEnc === RELAY_ENC ? { chunk: await sealRelayChunk(buffer, fileId, upload.currentChunk, upload.totalChunks), enc: RELAY_ENC } : { chunk: buffer.toString("base64") };
3734
3895
  wsConn?.send(JSON.stringify({
3735
3896
  type: "file_chunk",
3736
3897
  payload: {
3737
3898
  fileId,
3738
- chunk: chunkBase64,
3899
+ ...frame,
3739
3900
  chunkIndex: upload.currentChunk,
3740
3901
  totalChunks: upload.totalChunks,
3741
3902
  targetUserId: upload.targetUserId
@@ -3769,18 +3930,21 @@ function handleChunkAck(payload) {
3769
3930
  sendNextChunk(fileId);
3770
3931
  }
3771
3932
  }
3772
- function handleIncomingChunk(payload) {
3773
- const { fileId, chunk, chunkIndex, totalChunks, from } = payload;
3933
+ async function handleIncomingChunk(payload) {
3934
+ const { fileId, chunkIndex, totalChunks, from } = payload;
3774
3935
  const tx = activeTransfers.find((t) => t.fileId === fileId);
3775
3936
  if (!tx) return;
3937
+ const mode = payload.enc === void 0 || payload.enc === null ? "plain" : "sealed";
3938
+ if (tx.relayMode && tx.relayMode !== mode) throw new Error(`transfer ${fileId} mixed sealed and plaintext chunks`);
3939
+ tx.relayMode = mode;
3940
+ const chunkBuffer = await openRelayChunk(payload);
3776
3941
  tx.status = "downloading";
3777
3942
  tx.totalChunks = totalChunks;
3778
3943
  tx.peerId = from;
3779
- const chunkBuffer = Buffer.from(chunk, "base64");
3780
3944
  tx.bytesTransferred += chunkBuffer.length;
3781
3945
  tx.chunksCount++;
3782
- const chunkPath = path6.join(TEMP_DIR, `${fileId}_chunk_${chunkIndex}`);
3783
- fs7.writeFileSync(chunkPath, chunkBuffer);
3946
+ const chunkPath = path7.join(TEMP_DIR, `${fileId}_chunk_${chunkIndex}`);
3947
+ fs8.writeFileSync(chunkPath, chunkBuffer);
3784
3948
  tx.progress = tx.bytesTransferred / tx.size * 100;
3785
3949
  const elapsed = (Date.now() - tx.startTime) / 1e3;
3786
3950
  if (elapsed > 0) {
@@ -3808,10 +3972,10 @@ function handleFileComplete(payload) {
3808
3972
  const tx = activeTransfers.find((t) => t.fileId === fileId);
3809
3973
  if (!tx) return;
3810
3974
  const destDir = tx.saveDir || process.cwd();
3811
- const destPath = path6.join(destDir, tx.name);
3975
+ const destPath = path7.join(destDir, tx.name);
3812
3976
  try {
3813
- if (!fs7.existsSync(destDir)) {
3814
- fs7.mkdirSync(destDir, { recursive: true });
3977
+ if (!fs8.existsSync(destDir)) {
3978
+ fs8.mkdirSync(destDir, { recursive: true });
3815
3979
  }
3816
3980
  } catch (err) {
3817
3981
  console.error(`[DAEMON] Failed to create destination directory ${destDir}:`, err);
@@ -3819,7 +3983,7 @@ function handleFileComplete(payload) {
3819
3983
  writeStateFile();
3820
3984
  return;
3821
3985
  }
3822
- const writeStream = fs7.createWriteStream(destPath);
3986
+ const writeStream = fs8.createWriteStream(destPath);
3823
3987
  writeStream.on("error", (err) => {
3824
3988
  console.error(`[DAEMON] Write stream error for ${destPath}:`, err);
3825
3989
  tx.status = "error";
@@ -3854,12 +4018,12 @@ function handleFileComplete(payload) {
3854
4018
  });
3855
4019
  });
3856
4020
  for (let i = 0; i < tx.totalChunks; i++) {
3857
- const chunkPath = path6.join(TEMP_DIR, `${fileId}_chunk_${i}`);
3858
- if (fs7.existsSync(chunkPath)) {
4021
+ const chunkPath = path7.join(TEMP_DIR, `${fileId}_chunk_${i}`);
4022
+ if (fs8.existsSync(chunkPath)) {
3859
4023
  try {
3860
- const chunkBuf = fs7.readFileSync(chunkPath);
4024
+ const chunkBuf = fs8.readFileSync(chunkPath);
3861
4025
  writeStream.write(chunkBuf);
3862
- fs7.unlinkSync(chunkPath);
4026
+ fs8.unlinkSync(chunkPath);
3863
4027
  } catch (err) {
3864
4028
  console.error(`[DAEMON] Error reading/writing chunk ${i}:`, err);
3865
4029
  writeStream.emit("error", err);
@@ -3869,6 +4033,20 @@ function handleFileComplete(payload) {
3869
4033
  }
3870
4034
  writeStream.end();
3871
4035
  }
4036
+ async function waitSignalReady(maxMs = 8e3) {
4037
+ if (!session.id) return { status: 409, body: { success: false, error: "No active session. Call /session/start first." } };
4038
+ if (sessionTerminated) return { status: 410, body: { success: false, error: terminationReason || "The session ended." } };
4039
+ if (session.role === "guest" && !session.userId) {
4040
+ return { status: 409, body: { success: false, error: "Waiting for the host to approve your join request.", retryAfterMs: 2e3 } };
4041
+ }
4042
+ const ready = () => !!encryptionKey && !!csrfToken && wsConn?.readyState === WebSocket2.OPEN;
4043
+ for (let waited = 0; waited < maxMs && !ready() && session.id; waited += 100) {
4044
+ await new Promise((r) => setTimeout(r, 100));
4045
+ }
4046
+ if (ready()) return null;
4047
+ if (!encryptionKey) return { status: 409, body: { success: false, error: "Encryption key not ready. Session still initialising." } };
4048
+ return { status: 503, body: { success: false, error: "Signaling connection not ready (WebSocket is connecting). Retry in 1\u20132 seconds.", retryAfterMs: 1500 } };
4049
+ }
3872
4050
  async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}, timeoutMs = 5e3) {
3873
4051
  if (!wsConn || wsConn.readyState !== WebSocket2.OPEN) {
3874
4052
  throw new Error("Signaling WebSocket not connected");
@@ -3891,7 +4069,8 @@ async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}
3891
4069
  linkKey: opts.linkKey,
3892
4070
  tempFile: opts.tempFile,
3893
4071
  displayName: opts.displayName || filename,
3894
- prefixLen: opts.prefixLen
4072
+ prefixLen: opts.prefixLen,
4073
+ claim: opts.preferClaim
3895
4074
  };
3896
4075
  pubshares.set(fileId, entry);
3897
4076
  const ackP = new Promise((resolve, reject) => {
@@ -3916,6 +4095,7 @@ async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}
3916
4095
  maxDownloads,
3917
4096
  expiresAt,
3918
4097
  preferToken: opts.preferToken || void 0,
4098
+ preferClaim: opts.preferToken && opts.preferClaim ? opts.preferClaim : void 0,
3919
4099
  encrypted: opts.encrypted ? true : void 0,
3920
4100
  prefixLen: opts.encrypted && opts.prefixLen ? opts.prefixLen : void 0
3921
4101
  }
@@ -3932,7 +4112,7 @@ async function reregisterPubsharesAfterReconnect() {
3932
4112
  pubsharesByToken.delete(entry.token);
3933
4113
  if (entry.tempFile) {
3934
4114
  try {
3935
- fs7.unlinkSync(entry.tempFile);
4115
+ fs8.unlinkSync(entry.tempFile);
3936
4116
  } catch {
3937
4117
  }
3938
4118
  }
@@ -3943,7 +4123,7 @@ async function reregisterPubsharesAfterReconnect() {
3943
4123
  pubsharesByToken.delete(entry.token);
3944
4124
  if (entry.tempFile) {
3945
4125
  try {
3946
- fs7.unlinkSync(entry.tempFile);
4126
+ fs8.unlinkSync(entry.tempFile);
3947
4127
  } catch {
3948
4128
  }
3949
4129
  }
@@ -3962,6 +4142,7 @@ async function reregisterPubsharesAfterReconnect() {
3962
4142
  maxDownloads: remainingMax,
3963
4143
  ttlMs: remainingTtl,
3964
4144
  preferToken: entry.token,
4145
+ preferClaim: entry.claim,
3965
4146
  encrypted: entry.encrypted,
3966
4147
  linkKey: entry.linkKey,
3967
4148
  tempFile: entry.tempFile,
@@ -3987,16 +4168,16 @@ async function handlePubsharePull(payload) {
3987
4168
  }
3988
4169
  let fd;
3989
4170
  try {
3990
- fd = fs7.openSync(entry.filePath, "r");
4171
+ fd = fs8.openSync(entry.filePath, "r");
3991
4172
  } catch (e) {
3992
4173
  throw new Error(e?.code === "ENOENT" ? "the shared file no longer exists on the sender" : `cannot read the shared file (${e?.code || e?.message})`);
3993
4174
  }
3994
4175
  try {
3995
- const size = fs7.fstatSync(fd).size;
4176
+ const size = fs8.fstatSync(fd).size;
3996
4177
  if (size !== entry.size) throw new Error("the shared file changed after the link was created");
3997
4178
  } catch (e) {
3998
4179
  try {
3999
- fs7.closeSync(fd);
4180
+ fs8.closeSync(fd);
4000
4181
  } catch {
4001
4182
  }
4002
4183
  throw e;
@@ -4016,7 +4197,7 @@ async function handlePubsharePull(payload) {
4016
4197
  }
4017
4198
  const len = Math.min(CHUNK, end - pos + 1);
4018
4199
  const buf = Buffer.alloc(len);
4019
- const got = fs7.readSync(fd, buf, 0, len, pos);
4200
+ const got = fs8.readSync(fd, buf, 0, len, pos);
4020
4201
  if (got !== len) throw new Error("the shared file changed while it was being sent");
4021
4202
  const dataB64 = buf.toString("base64");
4022
4203
  wsConn.send(JSON.stringify({
@@ -4040,7 +4221,7 @@ async function handlePubsharePull(payload) {
4040
4221
  }
4041
4222
  } finally {
4042
4223
  try {
4043
- fs7.closeSync(fd);
4224
+ fs8.closeSync(fd);
4044
4225
  } catch {
4045
4226
  }
4046
4227
  activePulls.delete(requestId);
@@ -4093,7 +4274,7 @@ async function ensureHostSessionOnce(sessionName) {
4093
4274
  }
4094
4275
  async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
4095
4276
  await ensureHostSession(reqBody.sessionName);
4096
- const filename = reqBody.name || path6.basename(absPath);
4277
+ const filename = reqBody.name || path7.basename(absPath);
4097
4278
  const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
4098
4279
  let servePath = absPath;
4099
4280
  let serveSize = stat.size;
@@ -4103,13 +4284,13 @@ async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
4103
4284
  if (encrypt2) {
4104
4285
  const key = newLinkKey();
4105
4286
  linkKey = b64url(key);
4106
- tempFile = path6.join(TEMP_DIR, `${fileId}.sre1`);
4287
+ tempFile = path7.join(TEMP_DIR, `${fileId}.sre1`);
4107
4288
  const meta = { name: filename, size: stat.size, mime: "application/octet-stream" };
4108
- const fd = fs7.openSync(tempFile, "w", 384);
4289
+ const fd = fs8.openSync(tempFile, "w", 384);
4109
4290
  try {
4110
- for await (const part of encryptFile(absPath, key, meta, { password: reqBody.password })) fs7.writeSync(fd, part);
4291
+ for await (const part of encryptFile(absPath, key, meta, { password: reqBody.password })) fs8.writeSync(fd, part);
4111
4292
  } finally {
4112
- fs7.closeSync(fd);
4293
+ fs8.closeSync(fd);
4113
4294
  }
4114
4295
  servePath = tempFile;
4115
4296
  serveSize = encryptedSize(stat.size, meta);
@@ -4141,7 +4322,7 @@ async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
4141
4322
  if (reqBody._ownedPath) {
4142
4323
  if (encrypt2) {
4143
4324
  try {
4144
- fs7.unlinkSync(reqBody._ownedPath);
4325
+ fs8.unlinkSync(reqBody._ownedPath);
4145
4326
  } catch {
4146
4327
  }
4147
4328
  } else tempFile = reqBody._ownedPath;
@@ -4180,7 +4361,7 @@ async function shareViaRelay(absPath, stat, reqBody, encrypt2) {
4180
4361
  } catch (e) {
4181
4362
  if (tempFile) {
4182
4363
  try {
4183
- fs7.unlinkSync(tempFile);
4364
+ fs8.unlinkSync(tempFile);
4184
4365
  } catch {
4185
4366
  }
4186
4367
  }
@@ -4212,26 +4393,26 @@ function safeArchiveBase(name, fallback) {
4212
4393
  async function createShare(reqBody) {
4213
4394
  if (!reqBody?.filePath) throw Object.assign(new Error("filePath (or filePaths) required"), { status: 400 });
4214
4395
  assertRelayMode(reqBody);
4215
- const absPath = path6.resolve(reqBody.filePath);
4396
+ const absPath = path7.resolve(reqBody.filePath);
4216
4397
  let stat;
4217
4398
  try {
4218
- stat = fs7.statSync(absPath);
4399
+ stat = fs8.statSync(absPath);
4219
4400
  } catch {
4220
4401
  throw Object.assign(new Error(`File not found: ${absPath}`), { status: 404 });
4221
4402
  }
4222
4403
  if (stat.isDirectory()) {
4223
- const base = safeArchiveBase(path6.basename(absPath), "folder");
4224
- const out = path6.join(TEMP_DIR, `${uuidv4()}-${base}.tar.gz`);
4404
+ const base = safeArchiveBase(path7.basename(absPath), "folder");
4405
+ const out = path7.join(TEMP_DIR, `${uuidv4()}-${base}.tar.gz`);
4225
4406
  try {
4226
4407
  await packDirectory(absPath, out, excludesFor(reqBody));
4227
4408
  } catch (e) {
4228
4409
  try {
4229
- fs7.unlinkSync(out);
4410
+ fs8.unlinkSync(out);
4230
4411
  } catch {
4231
4412
  }
4232
4413
  throw Object.assign(new Error(e?.message || String(e)), { status: 400 });
4233
4414
  }
4234
- return shareViaRelay(out, fs7.statSync(out), { ...reqBody, name: reqBody.name || `${base}.tar.gz`, _ownedPath: out }, shareEncrypt(reqBody));
4415
+ return shareViaRelay(out, fs8.statSync(out), { ...reqBody, name: reqBody.name || `${base}.tar.gz`, _ownedPath: out }, shareEncrypt(reqBody));
4235
4416
  }
4236
4417
  if (!stat.isFile()) throw Object.assign(new Error(`Not a regular file or folder: ${absPath}`), { status: 400 });
4237
4418
  return shareViaRelay(absPath, stat, reqBody, shareEncrypt(reqBody));
@@ -4239,26 +4420,26 @@ async function createShare(reqBody) {
4239
4420
  async function createMultiShare(reqBody) {
4240
4421
  assertRelayMode(reqBody);
4241
4422
  const raw = Array.isArray(reqBody.filePaths) ? reqBody.filePaths : [];
4242
- const paths = Array.from(new Set(raw.filter((p) => typeof p === "string" && p.trim()).map((p) => path6.resolve(p))));
4423
+ const paths = Array.from(new Set(raw.filter((p) => typeof p === "string" && p.trim()).map((p) => path7.resolve(p))));
4243
4424
  if (!paths.length) throw Object.assign(new Error("filePaths must be a non-empty array of paths"), { status: 400 });
4244
4425
  if (paths.length > MAX_MULTI_PATHS) throw Object.assign(new Error(`Too many paths (${paths.length}); max ${MAX_MULTI_PATHS}. Share a folder instead.`), { status: 400 });
4245
- const missing = paths.filter((p) => !fs7.existsSync(p));
4426
+ const missing = paths.filter((p) => !fs8.existsSync(p));
4246
4427
  const bundle = reqBody.bundle !== false;
4247
4428
  if (bundle) {
4248
4429
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[-:T]/g, "");
4249
4430
  const base = safeArchiveBase(reqBody.bundleName, `srift-${paths.length}-items-${stamp}`);
4250
- const out = path6.join(TEMP_DIR, `${uuidv4()}-${base}.tar.gz`);
4431
+ const out = path7.join(TEMP_DIR, `${uuidv4()}-${base}.tar.gz`);
4251
4432
  let packed;
4252
4433
  try {
4253
4434
  packed = await packPaths(paths, out, base, excludesFor(reqBody));
4254
4435
  } catch (e) {
4255
4436
  try {
4256
- fs7.unlinkSync(out);
4437
+ fs8.unlinkSync(out);
4257
4438
  } catch {
4258
4439
  }
4259
4440
  throw Object.assign(new Error(e?.message || String(e)), { status: 400 });
4260
4441
  }
4261
- const r = await shareViaRelay(out, fs7.statSync(out), { ...reqBody, name: `${base}.tar.gz`, _ownedPath: out }, shareEncrypt(reqBody));
4442
+ const r = await shareViaRelay(out, fs8.statSync(out), { ...reqBody, name: `${base}.tar.gz`, _ownedPath: out }, shareEncrypt(reqBody));
4262
4443
  if (packed.skipped.length) console.warn(`[DAEMON] bundle skipped ${packed.skipped.length} item(s): ${packed.skipped.map((x) => x.path).join(", ")}`);
4263
4444
  return { ...r, bundle: true, files: packed.files, bytes: packed.bytes, paths: paths.length, skipped: packed.skipped };
4264
4445
  }
@@ -4327,7 +4508,7 @@ function embeddedDispatch(method, pathname, body) {
4327
4508
  }
4328
4509
  });
4329
4510
  }
4330
- var _WebTorrentCtor, _webTorrentLoadError, SRIFT_HOME, EMBEDDED, STATE_PATH, logPath, logStream, logMessage, fmtLogArg, crypto5, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, lastDiag, session, activeTransfers, chatHistory, pendingJoins, sseClients, wsConn, wsUrl, csrfToken, heartbeatInterval, lastHeartbeatAckAt, encryptionKey, sessionTerminated, terminationReason, activeTorrents, wtClient, pubshares, pubsharesByToken, activePulls, pubshareRegResolvers, TEMP_DIR, KDF_ITERATIONS, IV_LEN, STATE_STALE_MS, activeUploads, app, LOCAL_HOSTS, DAEMON_START_TIME, PACKAGE_VERSION, hostSessionInflight, MAX_MULTI_PATHS, MULTI_CONCURRENCY, embeddedReady, httpServer;
4511
+ var _WebTorrentCtor, _webTorrentLoadError, SRIFT_HOME, EMBEDDED, STATE_PATH, logPath, logStream, logMessage, fmtLogArg, crypto5, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, lastDiag, session, activeTransfers, chatHistory, pendingJoins, participants, sseClients, wsConn, wsUrl, csrfToken, heartbeatInterval, lastHeartbeatAckAt, encryptionKey, sessionTerminated, terminationReason, activeTorrents, wtClient, pubshares, pubsharesByToken, activePulls, pubshareRegResolvers, TEMP_DIR, KDF_ITERATIONS, IV_LEN, RELAY_ENC, relayKeyCache, STATE_STALE_MS, activeUploads, app, LOCAL_HOSTS, DAEMON_START_TIME, PACKAGE_VERSION, hostSessionInflight, MAX_MULTI_PATHS, MULTI_CONCURRENCY, embeddedReady, httpServer;
4331
4512
  var init_daemon = __esm({
4332
4513
  "../../cli/daemon.ts"() {
4333
4514
  "use strict";
@@ -4339,25 +4520,25 @@ var init_daemon = __esm({
4339
4520
  init_pack();
4340
4521
  _WebTorrentCtor = null;
4341
4522
  _webTorrentLoadError = null;
4342
- SRIFT_HOME = path6.join(os4.homedir(), ".srift");
4523
+ SRIFT_HOME = path7.join(os4.homedir(), ".srift");
4343
4524
  try {
4344
- fs7.mkdirSync(SRIFT_HOME, { recursive: true, mode: 448 });
4525
+ fs8.mkdirSync(SRIFT_HOME, { recursive: true, mode: 448 });
4345
4526
  try {
4346
- fs7.chmodSync(SRIFT_HOME, 448);
4527
+ fs8.chmodSync(SRIFT_HOME, 448);
4347
4528
  } catch {
4348
4529
  }
4349
4530
  } catch (e) {
4350
4531
  }
4351
4532
  EMBEDDED = process.env.SRIFT_DAEMON_EMBEDDED === "1";
4352
- STATE_PATH = path6.join(SRIFT_HOME, EMBEDDED ? "state-embedded.json" : "state.json");
4353
- logPath = path6.join(SRIFT_HOME, "daemon.log");
4533
+ STATE_PATH = path7.join(SRIFT_HOME, EMBEDDED ? "state-embedded.json" : "state.json");
4534
+ logPath = path7.join(SRIFT_HOME, "daemon.log");
4354
4535
  try {
4355
- if (fs7.statSync(logPath).size > 5 * 1024 * 1024) fs7.renameSync(logPath, `${logPath}.1`);
4536
+ if (fs8.statSync(logPath).size > 5 * 1024 * 1024) fs8.renameSync(logPath, `${logPath}.1`);
4356
4537
  } catch {
4357
4538
  }
4358
- logStream = fs7.createWriteStream(logPath, { flags: "a", mode: 384 });
4539
+ logStream = fs8.createWriteStream(logPath, { flags: "a", mode: 384 });
4359
4540
  try {
4360
- fs7.chmodSync(logPath, 384);
4541
+ fs8.chmodSync(logPath, 384);
4361
4542
  } catch {
4362
4543
  }
4363
4544
  logMessage = (level, message) => {
@@ -4404,6 +4585,7 @@ var init_daemon = __esm({
4404
4585
  activeTransfers = [];
4405
4586
  chatHistory = [];
4406
4587
  pendingJoins = [];
4588
+ participants = [];
4407
4589
  sseClients = [];
4408
4590
  wsConn = null;
4409
4591
  wsUrl = null;
@@ -4419,15 +4601,15 @@ var init_daemon = __esm({
4419
4601
  pubsharesByToken = /* @__PURE__ */ new Map();
4420
4602
  activePulls = /* @__PURE__ */ new Map();
4421
4603
  pubshareRegResolvers = /* @__PURE__ */ new Map();
4422
- TEMP_DIR = path6.join(SRIFT_HOME, "tmp");
4423
- if (!fs7.existsSync(TEMP_DIR)) {
4424
- fs7.mkdirSync(TEMP_DIR, { recursive: true, mode: 448 });
4604
+ TEMP_DIR = path7.join(SRIFT_HOME, "tmp");
4605
+ if (!fs8.existsSync(TEMP_DIR)) {
4606
+ fs8.mkdirSync(TEMP_DIR, { recursive: true, mode: 448 });
4425
4607
  }
4426
4608
  try {
4427
- for (const f of fs7.readdirSync(TEMP_DIR)) {
4609
+ for (const f of fs8.readdirSync(TEMP_DIR)) {
4428
4610
  if (f.endsWith(".sre1")) {
4429
4611
  try {
4430
- fs7.unlinkSync(path6.join(TEMP_DIR, f));
4612
+ fs8.unlinkSync(path7.join(TEMP_DIR, f));
4431
4613
  } catch {
4432
4614
  }
4433
4615
  }
@@ -4436,6 +4618,8 @@ var init_daemon = __esm({
4436
4618
  }
4437
4619
  KDF_ITERATIONS = 1e5;
4438
4620
  IV_LEN = 12;
4621
+ RELAY_ENC = "sgcm1";
4622
+ relayKeyCache = null;
4439
4623
  STATE_STALE_MS = 24 * 60 * 60 * 1e3;
4440
4624
  pruneStaleStateOnStartup();
4441
4625
  activeUploads = /* @__PURE__ */ new Map();
@@ -4454,7 +4638,7 @@ var init_daemon = __esm({
4454
4638
  app.use(cors({ origin: "*", maxAge: 600 }));
4455
4639
  app.use(express.json());
4456
4640
  DAEMON_START_TIME = Date.now();
4457
- PACKAGE_VERSION = "4.0.0";
4641
+ PACKAGE_VERSION = "4.1.0";
4458
4642
  app.get("/health", (req, res) => {
4459
4643
  res.json({
4460
4644
  ok: true,
@@ -4478,6 +4662,7 @@ var init_daemon = __esm({
4478
4662
  },
4479
4663
  activeTransfers,
4480
4664
  pendingJoins,
4665
+ participants,
4481
4666
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4482
4667
  });
4483
4668
  });
@@ -4505,7 +4690,7 @@ var init_daemon = __esm({
4505
4690
  wsUrl = data.wsUrl || `${activeSignalerUrl.replace(/^http/, "ws")}/ws`;
4506
4691
  if (wsUrl) connectWebSocket(wsUrl);
4507
4692
  writeStateFile();
4508
- res.json({ success: true, sessionId: data.sessionId });
4693
+ res.json({ success: true, sessionId: data.sessionId, joinUrl: `${activeSignalerUrl.replace(/\/+$/, "")}/join-session?id=${data.sessionId}` });
4509
4694
  } catch (err) {
4510
4695
  console.error("[DAEMON] Failed to start session:", err);
4511
4696
  res.status(500).json({ success: false, error: err.message });
@@ -4550,7 +4735,7 @@ var init_daemon = __esm({
4550
4735
  res.status(500).json({ success: false, error: err.message });
4551
4736
  }
4552
4737
  });
4553
- app.post("/session/approve", (req, res) => {
4738
+ app.post("/session/approve", async (req, res) => {
4554
4739
  const { tempUserId } = req.body;
4555
4740
  if (!tempUserId) {
4556
4741
  return res.status(400).json({ success: false, error: "tempUserId required" });
@@ -4568,7 +4753,13 @@ var init_daemon = __esm({
4568
4753
  payload: { sessionId: session.id, tempUserId }
4569
4754
  }));
4570
4755
  pendingJoins = pendingJoins.filter((j) => j.tempUserId !== tempUserId);
4571
- res.json({ success: true });
4756
+ const before = new Set(participants.filter((p) => p.online).map((p) => p.userId));
4757
+ const joined = () => participants.find((p) => p.online && !p.isHost && p.username === pending.username && !before.has(p.userId));
4758
+ for (let waited = 0; waited < 8e3 && !joined() && session.id; waited += 100) {
4759
+ await new Promise((r) => setTimeout(r, 100));
4760
+ }
4761
+ const member = joined();
4762
+ res.json({ success: true, joined: !!member, userId: member?.userId ?? null, username: pending.username });
4572
4763
  });
4573
4764
  app.post("/session/reject", (req, res) => {
4574
4765
  const { tempUserId, reason } = req.body;
@@ -4645,13 +4836,14 @@ var init_daemon = __esm({
4645
4836
  activeTransfers = [];
4646
4837
  chatHistory = [];
4647
4838
  pendingJoins = [];
4839
+ participants = [];
4648
4840
  csrfToken = null;
4649
4841
  encryptionKey = null;
4650
4842
  try {
4651
- if (fs7.existsSync(TEMP_DIR)) {
4652
- for (const f of fs7.readdirSync(TEMP_DIR)) {
4843
+ if (fs8.existsSync(TEMP_DIR)) {
4844
+ for (const f of fs8.readdirSync(TEMP_DIR)) {
4653
4845
  try {
4654
- fs7.unlinkSync(path6.join(TEMP_DIR, f));
4846
+ fs8.unlinkSync(path7.join(TEMP_DIR, f));
4655
4847
  } catch {
4656
4848
  }
4657
4849
  }
@@ -4667,14 +4859,16 @@ var init_daemon = __esm({
4667
4859
  if (!filePath) {
4668
4860
  return res.status(400).json({ success: false, error: "filePath is required" });
4669
4861
  }
4670
- const absPath = path6.resolve(filePath);
4671
- if (!fs7.existsSync(absPath)) {
4862
+ const absPath = path7.resolve(filePath);
4863
+ if (!fs8.existsSync(absPath)) {
4672
4864
  return res.status(404).json({ success: false, error: `File not found: ${absPath}` });
4673
4865
  }
4674
- const stat = fs7.statSync(absPath);
4675
- const filename = path6.basename(absPath);
4866
+ const stat = fs8.statSync(absPath);
4867
+ const filename = path7.basename(absPath);
4676
4868
  const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
4677
4869
  const targetProtocol = webTorrentAllowed() && (protocol === "webtorrent" || stat.size > 10 * 1024 * 1024 && protocol !== "websocket") ? "webtorrent" : "websocket";
4870
+ const notReady = await waitSignalReady();
4871
+ if (notReady) return res.status(notReady.status).json(notReady.body);
4678
4872
  const newTx = {
4679
4873
  fileId,
4680
4874
  name: filename,
@@ -4720,13 +4914,14 @@ var init_daemon = __esm({
4720
4914
  if (!tx) {
4721
4915
  return res.status(404).json({ success: false, error: "No offer found for this fileId" });
4722
4916
  }
4723
- tx.saveDir = saveDir ? path6.resolve(saveDir) : process.cwd();
4917
+ tx.saveDir = saveDir ? path7.resolve(saveDir) : process.cwd();
4724
4918
  tx.status = "downloading";
4725
4919
  tx.startTime = Date.now();
4726
4920
  console.log(`[DAEMON] Accepting file offer ${fileId}, saving to ${tx.saveDir}`);
4727
4921
  wsConn?.send(JSON.stringify({
4728
4922
  type: "file_accept",
4729
- payload: { fileId }
4923
+ // relayEnc: this daemon decrypts sealed relay chunks (see openRelayChunk)
4924
+ payload: { fileId, relayEnc: RELAY_ENC }
4730
4925
  }));
4731
4926
  res.json({ success: true });
4732
4927
  });
@@ -4739,16 +4934,8 @@ var init_daemon = __esm({
4739
4934
  if (!session.id) {
4740
4935
  return res.status(409).json({ success: false, error: "No active session. Call /session/start first." });
4741
4936
  }
4742
- if (!encryptionKey) {
4743
- return res.status(409).json({ success: false, error: "Encryption key not ready. Session still initialising." });
4744
- }
4745
- if (!wsConn || wsConn.readyState !== 1) {
4746
- return res.status(503).json({
4747
- success: false,
4748
- error: "Signaling connection not ready (WebSocket is connecting). Retry in 1\u20132 seconds.",
4749
- retryAfterMs: 1500
4750
- });
4751
- }
4937
+ const notReady = await waitSignalReady();
4938
+ if (notReady) return res.status(notReady.status).json(notReady.body);
4752
4939
  const encContent = await encrypt(message);
4753
4940
  wsConn.send(JSON.stringify({
4754
4941
  type: "chat",
@@ -4771,8 +4958,8 @@ var init_daemon = __esm({
4771
4958
  });
4772
4959
  app.get("/state", (req, res) => {
4773
4960
  try {
4774
- if (fs7.existsSync(STATE_PATH)) {
4775
- const raw = fs7.readFileSync(STATE_PATH, "utf-8");
4961
+ if (fs8.existsSync(STATE_PATH)) {
4962
+ const raw = fs8.readFileSync(STATE_PATH, "utf-8");
4776
4963
  res.type("application/json").send(raw);
4777
4964
  } else {
4778
4965
  res.json({ session: { id: null, isConnected: false }, activeTransfers: [], lastUpdated: null });
@@ -4852,7 +5039,7 @@ var init_daemon = __esm({
4852
5039
  pubshares.delete(entry.fileId);
4853
5040
  if (entry.tempFile) {
4854
5041
  try {
4855
- fs7.unlinkSync(entry.tempFile);
5042
+ fs8.unlinkSync(entry.tempFile);
4856
5043
  } catch {
4857
5044
  }
4858
5045
  }
@@ -4964,7 +5151,7 @@ data: /mcp/messages
4964
5151
  servers: [{ url: `http://127.0.0.1:${PORT}` }],
4965
5152
  paths: {
4966
5153
  "/health": { get: { summary: "Liveness probe", description: "Returns {ok,version,uptime_ms,mcp,webrtc,webtorrent}", responses: { "200": { description: "OK" } } } },
4967
- "/status": { get: { summary: "Get session + transfers + pending joins", responses: { "200": { description: "OK" } } } },
5154
+ "/status": { get: { summary: "Get session, transfers, pending joins and participants (members with userId, for /session/kick)", responses: { "200": { description: "OK" } } } },
4968
5155
  "/state": { get: { summary: "Workspace state snapshot (.srift-state.json)", responses: { "200": { description: "OK" } } } },
4969
5156
  "/transfers": { get: { summary: "Live transfer list with speed and ETA. Optional ?fileId= to filter.", responses: { "200": { description: "OK" } } } },
4970
5157
  "/transfers/{fileId}": { get: { summary: "Per-transfer drill-down", parameters: [{ name: "fileId", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "OK" }, "404": { description: "Transfer not found" } } } },
@@ -4972,9 +5159,11 @@ data: /mcp/messages
4972
5159
  "/metrics": { get: { summary: "Prometheus-format counters (no auth)", responses: { "200": { description: "text/plain Prometheus format" } } } },
4973
5160
  "/logs": { get: { summary: "NDJSON daemon log tail. ?lines=N (default 100)", responses: { "200": { description: "application/x-ndjson" } } } },
4974
5161
  "/reset": { post: { summary: "Wipe all state and flush encryption keys", responses: { "200": { description: "{ok:true}" } } } },
4975
- "/session/start": { post: { summary: "Create new session (becomes host)", requestBody: { content: { "application/json": { schema: { type: "object", properties: { sessionName: { type: "string" }, roomSecret: { type: "string" } } } } } }, responses: { "200": { description: "{success:true,sessionId}" } } } },
5162
+ "/v1/diag": { get: { summary: "Network diagnosis (same as `srift doctor --json`); ?fresh=1 re-probes", responses: { "200": { description: "Diagnosis report" } } } },
5163
+ "/daemon/stop": { post: { summary: "Stop this daemon", responses: { "200": { description: "OK" } } } },
5164
+ "/session/start": { post: { summary: "Create new session (becomes host); returns sessionId and joinUrl", requestBody: { content: { "application/json": { schema: { type: "object", properties: { sessionName: { type: "string" }, roomSecret: { type: "string" } } } } } }, responses: { "200": { description: "{success:true,sessionId}" } } } },
4976
5165
  "/session/join": { post: { summary: "Join existing session", requestBody: { content: { "application/json": { schema: { type: "object", required: ["sessionId"], properties: { sessionId: { type: "string" }, username: { type: "string" }, roomSecret: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "Missing sessionId" }, "404": { description: "Session not found" } } } },
4977
- "/session/approve": { post: { summary: "Host approves a pending join request", requestBody: { content: { "application/json": { schema: { type: "object", required: ["tempUserId"], properties: { tempUserId: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "tempUserId required" }, "403": { description: "Not host" }, "404": { description: "No such pending request" } } } },
5166
+ "/session/approve": { post: { summary: "Host approves a pending join request; returns {success, joined, userId} once the guest is in the room (max 8 s)", requestBody: { content: { "application/json": { schema: { type: "object", required: ["tempUserId"], properties: { tempUserId: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "tempUserId required" }, "403": { description: "Not host" }, "404": { description: "No such pending request" } } } },
4978
5167
  "/session/reject": { post: { summary: "Host rejects a pending join request", requestBody: { content: { "application/json": { schema: { type: "object", required: ["tempUserId"], properties: { tempUserId: { type: "string" }, reason: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "tempUserId required" }, "403": { description: "Not host" } } } },
4979
5168
  "/session/kick": { post: { summary: "Host kicks a peer", requestBody: { content: { "application/json": { schema: { type: "object", required: ["userId"], properties: { userId: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "userId required" }, "403": { description: "Not host" } } } },
4980
5169
  "/session/close": { post: { summary: "Tear down the session and flush keys", responses: { "200": { description: "OK" } } } },
@@ -5055,8 +5244,8 @@ data: /mcp/messages
5055
5244
  app.get("/logs", (req, res) => {
5056
5245
  const lines = parseInt(req.query.lines || "100", 10);
5057
5246
  try {
5058
- if (!fs7.existsSync(logPath)) return res.json([]);
5059
- const content = fs7.readFileSync(logPath, "utf-8");
5247
+ if (!fs8.existsSync(logPath)) return res.json([]);
5248
+ const content = fs8.readFileSync(logPath, "utf-8");
5060
5249
  const all = content.split("\n").filter(Boolean);
5061
5250
  const tail = all.slice(-Math.min(lines, all.length));
5062
5251
  res.set("Content-Type", "application/x-ndjson");
@@ -5091,15 +5280,16 @@ data: /mcp/messages
5091
5280
  activeTransfers = [];
5092
5281
  chatHistory = [];
5093
5282
  pendingJoins = [];
5283
+ participants = [];
5094
5284
  csrfToken = null;
5095
5285
  encryptionKey = null;
5096
5286
  wsUrl = null;
5097
5287
  let cleanedChunks = 0;
5098
5288
  try {
5099
- if (fs7.existsSync(TEMP_DIR)) {
5100
- for (const f of fs7.readdirSync(TEMP_DIR)) {
5289
+ if (fs8.existsSync(TEMP_DIR)) {
5290
+ for (const f of fs8.readdirSync(TEMP_DIR)) {
5101
5291
  try {
5102
- fs7.unlinkSync(path6.join(TEMP_DIR, f));
5292
+ fs8.unlinkSync(path7.join(TEMP_DIR, f));
5103
5293
  cleanedChunks++;
5104
5294
  } catch {
5105
5295
  }
@@ -5161,16 +5351,17 @@ data: /mcp/messages
5161
5351
  activeTransfers = [];
5162
5352
  chatHistory = [];
5163
5353
  pendingJoins = [];
5354
+ participants = [];
5164
5355
  encryptionKey = null;
5165
5356
  wsUrl = null;
5166
5357
  writeStateFile();
5167
5358
  } catch {
5168
5359
  }
5169
5360
  try {
5170
- if (fs7.existsSync(TEMP_DIR)) {
5171
- for (const f of fs7.readdirSync(TEMP_DIR)) {
5361
+ if (fs8.existsSync(TEMP_DIR)) {
5362
+ for (const f of fs8.readdirSync(TEMP_DIR)) {
5172
5363
  try {
5173
- fs7.unlinkSync(path6.join(TEMP_DIR, f));
5364
+ fs8.unlinkSync(path7.join(TEMP_DIR, f));
5174
5365
  } catch {
5175
5366
  }
5176
5367
  }
@@ -5185,7 +5376,7 @@ data: /mcp/messages
5185
5376
  process.on("exit", () => {
5186
5377
  for (const e of pubshares.values()) if (e.tempFile) {
5187
5378
  try {
5188
- fs7.unlinkSync(e.tempFile);
5379
+ fs8.unlinkSync(e.tempFile);
5189
5380
  } catch {
5190
5381
  }
5191
5382
  }
@@ -6837,12 +7028,12 @@ var init_card = __esm({
6837
7028
  });
6838
7029
 
6839
7030
  // ../../cli/agentnet/describe.ts
6840
- import fs9 from "node:fs";
7031
+ import fs10 from "node:fs";
6841
7032
  import os6 from "node:os";
6842
- import path8 from "node:path";
7033
+ import path9 from "node:path";
6843
7034
  function read(file2) {
6844
7035
  try {
6845
- return fs9.readFileSync(file2, "utf8");
7036
+ return fs10.readFileSync(file2, "utf8");
6846
7037
  } catch {
6847
7038
  return null;
6848
7039
  }
@@ -6855,7 +7046,7 @@ function firstSentence(s) {
6855
7046
  function detectProject(start = process.cwd()) {
6856
7047
  let dir = start;
6857
7048
  for (let i = 0; i < 4; i++) {
6858
- const pkg = read(path8.join(dir, "package.json"));
7049
+ const pkg = read(path9.join(dir, "package.json"));
6859
7050
  if (pkg) {
6860
7051
  try {
6861
7052
  const j = JSON.parse(pkg);
@@ -6864,7 +7055,7 @@ function detectProject(start = process.cwd()) {
6864
7055
  } catch {
6865
7056
  }
6866
7057
  }
6867
- const py = read(path8.join(dir, "pyproject.toml"));
7058
+ const py = read(path9.join(dir, "pyproject.toml"));
6868
7059
  if (py) {
6869
7060
  return {
6870
7061
  name: py.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1],
@@ -6872,7 +7063,7 @@ function detectProject(start = process.cwd()) {
6872
7063
  stack: "Python"
6873
7064
  };
6874
7065
  }
6875
- const cargo = read(path8.join(dir, "Cargo.toml"));
7066
+ const cargo = read(path9.join(dir, "Cargo.toml"));
6876
7067
  if (cargo) {
6877
7068
  return {
6878
7069
  name: cargo.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1],
@@ -6880,9 +7071,9 @@ function detectProject(start = process.cwd()) {
6880
7071
  stack: "Rust"
6881
7072
  };
6882
7073
  }
6883
- const gomod = read(path8.join(dir, "go.mod"));
7074
+ const gomod = read(path9.join(dir, "go.mod"));
6884
7075
  if (gomod) return { name: gomod.match(/^module\s+(\S+)/m)?.[1]?.split("/").pop(), stack: "Go" };
6885
- const parent = path8.dirname(dir);
7076
+ const parent = path9.dirname(dir);
6886
7077
  if (parent === dir) break;
6887
7078
  dir = parent;
6888
7079
  }
@@ -6890,7 +7081,7 @@ function detectProject(start = process.cwd()) {
6890
7081
  }
6891
7082
  function runtimeLabel() {
6892
7083
  if (process.env.KUBERNETES_SERVICE_HOST) return "Kubernetes";
6893
- if (fs9.existsSync("/.dockerenv")) return "container";
7084
+ if (fs10.existsSync("/.dockerenv")) return "container";
6894
7085
  if (process.env.GITHUB_ACTIONS) return "GitHub Actions";
6895
7086
  if (process.env.CODESPACES) return "Codespace";
6896
7087
  if (process.env.AWS_LAMBDA_FUNCTION_NAME) return "AWS Lambda";
@@ -6926,20 +7117,20 @@ var init_describe = __esm({
6926
7117
 
6927
7118
  // ../../cli/agentnet/local.ts
6928
7119
  import { AsyncLocalStorage } from "node:async_hooks";
6929
- import fs10 from "node:fs";
7120
+ import fs11 from "node:fs";
6930
7121
  import os7 from "node:os";
6931
- import path9 from "node:path";
7122
+ import path10 from "node:path";
6932
7123
  function inHome(home, fn) {
6933
7124
  return homeCtx.run(home, fn);
6934
7125
  }
6935
7126
  function anDir() {
6936
- return homeCtx.getStore() || process.env.SRIFT_AN_HOME || path9.join(os7.homedir(), ".srift", "agentnet");
7127
+ return homeCtx.getStore() || process.env.SRIFT_AN_HOME || path10.join(os7.homedir(), ".srift", "agentnet");
6937
7128
  }
6938
7129
  function file(name) {
6939
- return path9.join(anDir(), name);
7130
+ return path10.join(anDir(), name);
6940
7131
  }
6941
7132
  function ensureDir(dir = anDir()) {
6942
- fs10.mkdirSync(dir, { recursive: true, mode: 448 });
7133
+ fs11.mkdirSync(dir, { recursive: true, mode: 448 });
6943
7134
  }
6944
7135
  function ramFor() {
6945
7136
  const k = anDir();
@@ -6959,7 +7150,7 @@ function readJson(name, fallback) {
6959
7150
  const v = ramFor().json.get(name);
6960
7151
  return v === void 0 ? fallback : JSON.parse(v);
6961
7152
  }
6962
- return JSON.parse(fs10.readFileSync(file(name), "utf8"));
7153
+ return JSON.parse(fs11.readFileSync(file(name), "utf8"));
6963
7154
  } catch {
6964
7155
  return fallback;
6965
7156
  }
@@ -6972,20 +7163,20 @@ function writeJson(name, data) {
6972
7163
  ensureDir();
6973
7164
  const target = file(name);
6974
7165
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
6975
- fs10.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
7166
+ fs11.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
6976
7167
  try {
6977
- fs10.renameSync(tmp, target);
7168
+ fs11.renameSync(tmp, target);
6978
7169
  } catch (e) {
6979
7170
  for (let i = 0; i < 20; i++) {
6980
7171
  sleepSync(25);
6981
7172
  try {
6982
- fs10.renameSync(tmp, target);
7173
+ fs11.renameSync(tmp, target);
6983
7174
  return;
6984
7175
  } catch {
6985
7176
  }
6986
7177
  }
6987
7178
  try {
6988
- fs10.unlinkSync(tmp);
7179
+ fs11.unlinkSync(tmp);
6989
7180
  } catch {
6990
7181
  }
6991
7182
  throw e;
@@ -7002,15 +7193,15 @@ function withLock(name, fn) {
7002
7193
  let fd = null;
7003
7194
  while (fd === null) {
7004
7195
  try {
7005
- fd = fs10.openSync(lock, "wx");
7006
- fs10.writeSync(fd, String(process.pid));
7196
+ fd = fs11.openSync(lock, "wx");
7197
+ fs11.writeSync(fd, String(process.pid));
7007
7198
  } catch (e) {
7008
7199
  if (e?.code !== "EEXIST" && e?.code !== "EPERM" && e?.code !== "EACCES" && e?.code !== "EBUSY") throw e;
7009
7200
  try {
7010
- const holder = Number(fs10.readFileSync(lock, "utf8"));
7011
- const age = Date.now() - fs10.statSync(lock).mtimeMs;
7201
+ const holder = Number(fs11.readFileSync(lock, "utf8"));
7202
+ const age = Date.now() - fs11.statSync(lock).mtimeMs;
7012
7203
  if (holder && holder !== process.pid && !pidAlive(holder) && age > 200 || age > 6e4) {
7013
- fs10.unlinkSync(lock);
7204
+ fs11.unlinkSync(lock);
7014
7205
  continue;
7015
7206
  }
7016
7207
  } catch {
@@ -7023,11 +7214,11 @@ function withLock(name, fn) {
7023
7214
  return fn();
7024
7215
  } finally {
7025
7216
  try {
7026
- fs10.closeSync(fd);
7217
+ fs11.closeSync(fd);
7027
7218
  } catch {
7028
7219
  }
7029
7220
  try {
7030
- fs10.unlinkSync(lock);
7221
+ fs11.unlinkSync(lock);
7031
7222
  } catch {
7032
7223
  }
7033
7224
  }
@@ -7040,13 +7231,13 @@ function mutateJson(name, fallback, fn) {
7040
7231
  });
7041
7232
  }
7042
7233
  function hasIdentity() {
7043
- return fs10.existsSync(file("identity.json"));
7234
+ return fs11.existsSync(file("identity.json"));
7044
7235
  }
7045
7236
  function loadIdentity() {
7046
7237
  const p = file("identity.json");
7047
7238
  let text2;
7048
7239
  try {
7049
- text2 = fs10.readFileSync(p, "utf8");
7240
+ text2 = fs11.readFileSync(p, "utf8");
7050
7241
  } catch (e) {
7051
7242
  if (e?.code === "ENOENT") return null;
7052
7243
  throw new Error(`Cannot read ${p} (${e?.code || e?.message}). Refusing to continue so your address is never replaced.`);
@@ -7101,7 +7292,7 @@ function ensureIdentity() {
7101
7292
  ensureDir();
7102
7293
  let fd;
7103
7294
  try {
7104
- fd = fs10.openSync(file("identity.json.creating"), "wx");
7295
+ fd = fs11.openSync(file("identity.json.creating"), "wx");
7105
7296
  } catch {
7106
7297
  for (let i = 0; i < 100; i++) {
7107
7298
  sleepSync(20);
@@ -7109,7 +7300,7 @@ function ensureIdentity() {
7109
7300
  if (again) return { identity: again, created: false };
7110
7301
  }
7111
7302
  try {
7112
- fs10.unlinkSync(file("identity.json.creating"));
7303
+ fs11.unlinkSync(file("identity.json.creating"));
7113
7304
  } catch {
7114
7305
  }
7115
7306
  return ensureIdentity();
@@ -7121,9 +7312,9 @@ function ensureIdentity() {
7121
7312
  saveIdentity(identity);
7122
7313
  return { identity, created: true };
7123
7314
  } finally {
7124
- fs10.closeSync(fd);
7315
+ fs11.closeSync(fd);
7125
7316
  try {
7126
- fs10.unlinkSync(file("identity.json.creating"));
7317
+ fs11.unlinkSync(file("identity.json.creating"));
7127
7318
  } catch {
7128
7319
  }
7129
7320
  }
@@ -7133,7 +7324,7 @@ function saveProfile(address, profile) {
7133
7324
  withLock("identity.json", () => {
7134
7325
  let raw;
7135
7326
  try {
7136
- raw = JSON.parse(fs10.readFileSync(file("identity.json"), "utf8").replace(/^\uFEFF/, ""));
7327
+ raw = JSON.parse(fs11.readFileSync(file("identity.json"), "utf8").replace(/^\uFEFF/, ""));
7137
7328
  } catch {
7138
7329
  return;
7139
7330
  }
@@ -7274,7 +7465,7 @@ function inboxPath() {
7274
7465
  function readInbox() {
7275
7466
  if (isEphemeral()) return ramFor().inbox.slice();
7276
7467
  try {
7277
- return fs10.readFileSync(inboxPath(), "utf8").split("\n").filter(Boolean).map((l) => {
7468
+ return fs11.readFileSync(inboxPath(), "utf8").split("\n").filter(Boolean).map((l) => {
7278
7469
  try {
7279
7470
  return JSON.parse(l);
7280
7471
  } catch {
@@ -7299,7 +7490,7 @@ function appendInbox(rec) {
7299
7490
  if (r.inbox.length > 2e4) r.inbox.splice(0, r.inbox.length - 2e4);
7300
7491
  } else {
7301
7492
  ensureDir();
7302
- withLock("inbox.jsonl", () => fs10.appendFileSync(inboxPath(), JSON.stringify(rec) + "\n", { mode: 384 }));
7493
+ withLock("inbox.jsonl", () => fs11.appendFileSync(inboxPath(), JSON.stringify(rec) + "\n", { mode: 384 }));
7303
7494
  }
7304
7495
  ids.add(rec.id);
7305
7496
  if (ids.size > 2e4) recentIds.set(key, new Set([...ids].slice(-1e4)));
@@ -7322,14 +7513,14 @@ function outboxAdd(item) {
7322
7513
  return;
7323
7514
  }
7324
7515
  ensureDir(outboxDir());
7325
- fs10.writeFileSync(path9.join(outboxDir(), `${item.id}.json`), JSON.stringify(item), { mode: 384 });
7516
+ fs11.writeFileSync(path10.join(outboxDir(), `${item.id}.json`), JSON.stringify(item), { mode: 384 });
7326
7517
  }
7327
7518
  function outboxList() {
7328
7519
  if (isEphemeral()) return [...ramFor().outbox.values()].map((v) => JSON.parse(v));
7329
7520
  try {
7330
- return fs10.readdirSync(outboxDir()).filter((f) => f.endsWith(".json")).map((f) => {
7521
+ return fs11.readdirSync(outboxDir()).filter((f) => f.endsWith(".json")).map((f) => {
7331
7522
  try {
7332
- return JSON.parse(fs10.readFileSync(path9.join(outboxDir(), f), "utf8"));
7523
+ return JSON.parse(fs11.readFileSync(path10.join(outboxDir(), f), "utf8"));
7333
7524
  } catch {
7334
7525
  return null;
7335
7526
  }
@@ -7343,7 +7534,7 @@ function outboxUpdate(item) {
7343
7534
  if (ramFor().outbox.has(item.id)) ramFor().outbox.set(item.id, JSON.stringify(item));
7344
7535
  return;
7345
7536
  }
7346
- if (OUTBOX_ID.test(item.id) && fs10.existsSync(path9.join(outboxDir(), `${item.id}.json`))) outboxAdd(item);
7537
+ if (OUTBOX_ID.test(item.id) && fs11.existsSync(path10.join(outboxDir(), `${item.id}.json`))) outboxAdd(item);
7347
7538
  }
7348
7539
  function outboxRemove(id) {
7349
7540
  if (isEphemeral()) {
@@ -7352,7 +7543,7 @@ function outboxRemove(id) {
7352
7543
  }
7353
7544
  if (!OUTBOX_ID.test(id)) return;
7354
7545
  try {
7355
- fs10.unlinkSync(path9.join(outboxDir(), `${id}.json`));
7546
+ fs11.unlinkSync(path10.join(outboxDir(), `${id}.json`));
7356
7547
  } catch {
7357
7548
  }
7358
7549
  }
@@ -7365,14 +7556,14 @@ function readLock() {
7365
7556
  const p = file("node.lock");
7366
7557
  let l = null;
7367
7558
  try {
7368
- l = JSON.parse(fs10.readFileSync(p, "utf8"));
7559
+ l = JSON.parse(fs11.readFileSync(p, "utf8"));
7369
7560
  } catch {
7370
7561
  return null;
7371
7562
  }
7372
7563
  if (!l || !Number.isInteger(l.pid)) return null;
7373
7564
  if (l.pid === process.pid && l.nonce && l.nonce === myLockNonce) return l;
7374
7565
  try {
7375
- if (Date.now() - fs10.statSync(p).mtimeMs > LOCK_STALE_MS) return null;
7566
+ if (Date.now() - fs11.statSync(p).mtimeMs > LOCK_STALE_MS) return null;
7376
7567
  } catch {
7377
7568
  return null;
7378
7569
  }
@@ -7383,16 +7574,16 @@ function acquireLock() {
7383
7574
  const p = file("node.lock");
7384
7575
  for (let attempt2 = 0; attempt2 < 2; attempt2++) {
7385
7576
  try {
7386
- const fd = fs10.openSync(p, "wx", 384);
7577
+ const fd = fs11.openSync(p, "wx", 384);
7387
7578
  myLockNonce = randomToken(12);
7388
- fs10.writeSync(fd, JSON.stringify({ pid: process.pid, startedAt: nowSec(), nonce: myLockNonce }));
7389
- fs10.closeSync(fd);
7579
+ fs11.writeSync(fd, JSON.stringify({ pid: process.pid, startedAt: nowSec(), nonce: myLockNonce }));
7580
+ fs11.closeSync(fd);
7390
7581
  return true;
7391
7582
  } catch (e) {
7392
7583
  if (e?.code !== "EEXIST") throw e;
7393
7584
  if (readLock()) return false;
7394
7585
  try {
7395
- fs10.unlinkSync(p);
7586
+ fs11.unlinkSync(p);
7396
7587
  } catch {
7397
7588
  }
7398
7589
  }
@@ -7403,20 +7594,20 @@ function heartbeatLock() {
7403
7594
  if (!myLockNonce) return;
7404
7595
  const now = /* @__PURE__ */ new Date();
7405
7596
  try {
7406
- fs10.utimesSync(file("node.lock"), now, now);
7597
+ fs11.utimesSync(file("node.lock"), now, now);
7407
7598
  } catch {
7408
7599
  }
7409
7600
  }
7410
7601
  function requestStop() {
7411
7602
  ensureDir();
7412
- fs10.writeFileSync(file("node.stop"), String(Date.now()), { mode: 384 });
7603
+ fs11.writeFileSync(file("node.stop"), String(Date.now()), { mode: 384 });
7413
7604
  }
7414
7605
  function stopRequested() {
7415
- return fs10.existsSync(file("node.stop"));
7606
+ return fs11.existsSync(file("node.stop"));
7416
7607
  }
7417
7608
  function clearStopRequest() {
7418
7609
  try {
7419
- fs10.unlinkSync(file("node.stop"));
7610
+ fs11.unlinkSync(file("node.stop"));
7420
7611
  } catch {
7421
7612
  }
7422
7613
  }
@@ -7425,7 +7616,7 @@ function clearLock() {
7425
7616
  if (l && l.pid === process.pid && (!l.nonce || l.nonce === myLockNonce)) {
7426
7617
  myLockNonce = null;
7427
7618
  try {
7428
- fs10.unlinkSync(file("node.lock"));
7619
+ fs11.unlinkSync(file("node.lock"));
7429
7620
  } catch {
7430
7621
  }
7431
7622
  }
@@ -7471,12 +7662,12 @@ function appendSent(rec) {
7471
7662
  return;
7472
7663
  }
7473
7664
  ensureDir();
7474
- withLock("sent.jsonl", () => fs10.appendFileSync(file("sent.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 }));
7665
+ withLock("sent.jsonl", () => fs11.appendFileSync(file("sent.jsonl"), JSON.stringify(rec) + "\n", { mode: 384 }));
7475
7666
  }
7476
7667
  function readSent() {
7477
7668
  if (isEphemeral()) return ramFor().sent.slice();
7478
7669
  try {
7479
- return fs10.readFileSync(file("sent.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => {
7670
+ return fs11.readFileSync(file("sent.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => {
7480
7671
  try {
7481
7672
  return JSON.parse(l);
7482
7673
  } catch {
@@ -7498,11 +7689,11 @@ function pidAlive(pid) {
7498
7689
  function cleanStaleEphemeral() {
7499
7690
  let n = 0;
7500
7691
  try {
7501
- for (const d of fs10.readdirSync(os7.tmpdir())) {
7692
+ for (const d of fs11.readdirSync(os7.tmpdir())) {
7502
7693
  const m = d.match(/^srift-an-(\d+)-/);
7503
7694
  if (m && Number(m[1]) !== process.pid && !pidAlive(Number(m[1]))) {
7504
7695
  try {
7505
- fs10.rmSync(path9.join(os7.tmpdir(), d), { recursive: true, force: true });
7696
+ fs11.rmSync(path10.join(os7.tmpdir(), d), { recursive: true, force: true });
7506
7697
  n++;
7507
7698
  } catch {
7508
7699
  }
@@ -7518,23 +7709,23 @@ function filesRoot() {
7518
7709
  let root = ephemeralRoots.get(key);
7519
7710
  if (!root) {
7520
7711
  cleanStaleEphemeral();
7521
- root = fs10.mkdtempSync(path9.join(os7.tmpdir(), `srift-an-${process.pid}-`));
7712
+ root = fs11.mkdtempSync(path10.join(os7.tmpdir(), `srift-an-${process.pid}-`));
7522
7713
  ephemeralRoots.set(key, root);
7523
7714
  const r = root;
7524
7715
  process.once("exit", () => {
7525
7716
  try {
7526
- fs10.rmSync(r, { recursive: true, force: true });
7717
+ fs11.rmSync(r, { recursive: true, force: true });
7527
7718
  } catch {
7528
7719
  }
7529
7720
  });
7530
7721
  }
7531
7722
  return root;
7532
7723
  }
7533
- return path9.join(anDir(), "files");
7724
+ return path10.join(anDir(), "files");
7534
7725
  }
7535
7726
  function filesDir(...parts) {
7536
- const d = path9.join(filesRoot(), ...parts);
7537
- fs10.mkdirSync(d, { recursive: true, mode: 448 });
7727
+ const d = path10.join(filesRoot(), ...parts);
7728
+ fs11.mkdirSync(d, { recursive: true, mode: 448 });
7538
7729
  return d;
7539
7730
  }
7540
7731
  function rewriteJsonl(name, keep) {
@@ -7543,7 +7734,7 @@ function rewriteJsonl(name, keep) {
7543
7734
  withLock(name, () => {
7544
7735
  let lines;
7545
7736
  try {
7546
- lines = fs10.readFileSync(p, "utf8").split("\n").filter(Boolean);
7737
+ lines = fs11.readFileSync(p, "utf8").split("\n").filter(Boolean);
7547
7738
  } catch {
7548
7739
  return;
7549
7740
  }
@@ -7557,8 +7748,8 @@ function rewriteJsonl(name, keep) {
7557
7748
  });
7558
7749
  if (!removed) return;
7559
7750
  const tmp = `${p}.${process.pid}.tmp`;
7560
- fs10.writeFileSync(tmp, kept.map((l) => l + "\n").join(""), { mode: 384 });
7561
- fs10.renameSync(tmp, p);
7751
+ fs11.writeFileSync(tmp, kept.map((l) => l + "\n").join(""), { mode: 384 });
7752
+ fs11.renameSync(tmp, p);
7562
7753
  });
7563
7754
  if (removed) recentIds.delete("disk:" + anDir());
7564
7755
  return removed;
@@ -7602,11 +7793,11 @@ function prune(cfg = loadConfig(), now = nowSec()) {
7602
7793
  return k;
7603
7794
  });
7604
7795
  try {
7605
- const inc = path9.join(filesRoot(), ".incoming");
7606
- for (const f of fs10.readdirSync(inc)) {
7607
- const fp = path9.join(inc, f);
7608
- if (Date.now() - fs10.statSync(fp).mtimeMs > 36e5) {
7609
- fs10.rmSync(fp, { force: true });
7796
+ const inc = path10.join(filesRoot(), ".incoming");
7797
+ for (const f of fs11.readdirSync(inc)) {
7798
+ const fp = path10.join(inc, f);
7799
+ if (Date.now() - fs11.statSync(fp).mtimeMs > 36e5) {
7800
+ fs11.rmSync(fp, { force: true });
7610
7801
  r.partials++;
7611
7802
  }
7612
7803
  }
@@ -7617,19 +7808,19 @@ function prune(cfg = loadConfig(), now = nowSec()) {
7617
7808
  const walk = (d) => {
7618
7809
  let entries = [];
7619
7810
  try {
7620
- entries = fs10.readdirSync(d, { withFileTypes: true });
7811
+ entries = fs11.readdirSync(d, { withFileTypes: true });
7621
7812
  } catch {
7622
7813
  return;
7623
7814
  }
7624
7815
  for (const e of entries) {
7625
- const fp = path9.join(d, e.name);
7816
+ const fp = path10.join(d, e.name);
7626
7817
  if (e.isDirectory()) {
7627
7818
  if (e.name !== ".incoming") walk(fp);
7628
7819
  continue;
7629
7820
  }
7630
7821
  try {
7631
- if (fs10.statSync(fp).mtimeMs < limit) {
7632
- fs10.rmSync(fp, { force: true });
7822
+ if (fs11.statSync(fp).mtimeMs < limit) {
7823
+ fs11.rmSync(fp, { force: true });
7633
7824
  r.files++;
7634
7825
  }
7635
7826
  } catch {
@@ -7644,12 +7835,12 @@ function prune(cfg = loadConfig(), now = nowSec()) {
7644
7835
  function rotateLog(maxBytes = 5 * 1024 * 1024) {
7645
7836
  const p = file("node.log");
7646
7837
  try {
7647
- if (fs10.statSync(p).size <= maxBytes) return false;
7838
+ if (fs11.statSync(p).size <= maxBytes) return false;
7648
7839
  try {
7649
- fs10.rmSync(p + ".1", { force: true });
7840
+ fs11.rmSync(p + ".1", { force: true });
7650
7841
  } catch {
7651
7842
  }
7652
- fs10.renameSync(p, p + ".1");
7843
+ fs11.renameSync(p, p + ".1");
7653
7844
  return true;
7654
7845
  } catch {
7655
7846
  return false;
@@ -7663,14 +7854,14 @@ function wipe(all = false) {
7663
7854
  if (all) names.push("identity.json", "contacts.json", "groups.json", "config.json", "node.lock");
7664
7855
  for (const n of names) {
7665
7856
  const p = file(n);
7666
- if (fs10.existsSync(p)) {
7667
- fs10.rmSync(p, { recursive: true, force: true });
7857
+ if (fs11.existsSync(p)) {
7858
+ fs11.rmSync(p, { recursive: true, force: true });
7668
7859
  removed.push(n);
7669
7860
  }
7670
7861
  }
7671
7862
  if (all) {
7672
7863
  try {
7673
- for (const f of fs10.readdirSync(anDir())) if (f.endsWith(".lock") || f.endsWith(".tmp")) fs10.rmSync(path9.join(anDir(), f), { force: true });
7864
+ for (const f of fs11.readdirSync(anDir())) if (f.endsWith(".lock") || f.endsWith(".tmp")) fs11.rmSync(path10.join(anDir(), f), { force: true });
7674
7865
  } catch {
7675
7866
  }
7676
7867
  }
@@ -7821,8 +8012,8 @@ var init_group = __esm({
7821
8012
 
7822
8013
  // ../../cli/agentnet/files.ts
7823
8014
  import crypto7 from "node:crypto";
7824
- import fs11 from "node:fs";
7825
- import path10 from "node:path";
8015
+ import fs12 from "node:fs";
8016
+ import path11 from "node:path";
7826
8017
  function safeFileName(name) {
7827
8018
  const raw = String(name || "file").split(/[\\/]/).pop() || "file";
7828
8019
  let base = raw.replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^\.+/, "_").replace(/[. ]+$/, "").trim();
@@ -7830,33 +8021,33 @@ function safeFileName(name) {
7830
8021
  return (base || "file").slice(0, 180);
7831
8022
  }
7832
8023
  function uniquePath2(dir, name) {
7833
- let p = path10.join(dir, name);
7834
- if (!fs11.existsSync(p)) return p;
7835
- const ext = path10.extname(name);
8024
+ let p = path11.join(dir, name);
8025
+ if (!fs12.existsSync(p)) return p;
8026
+ const ext = path11.extname(name);
7836
8027
  const stem2 = name.slice(0, name.length - ext.length);
7837
8028
  for (let i = 1; ; i++) {
7838
- p = path10.join(dir, `${stem2} (${i})${ext}`);
7839
- if (!fs11.existsSync(p)) return p;
8029
+ p = path11.join(dir, `${stem2} (${i})${ext}`);
8030
+ if (!fs12.existsSync(p)) return p;
7840
8031
  }
7841
8032
  }
7842
8033
  function sha256File(p) {
7843
8034
  const h = crypto7.createHash("sha256");
7844
- const fd = fs11.openSync(p, "r");
8035
+ const fd = fs12.openSync(p, "r");
7845
8036
  try {
7846
8037
  const buf = Buffer.alloc(1 << 20);
7847
8038
  let n;
7848
- while ((n = fs11.readSync(fd, buf, 0, buf.length, null)) > 0) h.update(buf.subarray(0, n));
8039
+ while ((n = fs12.readSync(fd, buf, 0, buf.length, null)) > 0) h.update(buf.subarray(0, n));
7849
8040
  } finally {
7850
- fs11.closeSync(fd);
8041
+ fs12.closeSync(fd);
7851
8042
  }
7852
8043
  return h.digest("hex");
7853
8044
  }
7854
8045
  function makeOffer(filePath, groupId) {
7855
- const st = fs11.statSync(filePath);
8046
+ const st = fs12.statSync(filePath);
7856
8047
  if (!st.isFile()) throw new Error(`Not a file: ${filePath}`);
7857
8048
  return {
7858
8049
  fileId: newId(),
7859
- name: safeFileName(path10.basename(filePath)),
8050
+ name: safeFileName(path11.basename(filePath)),
7860
8051
  size: st.size,
7861
8052
  sha256: sha256File(filePath),
7862
8053
  chunkSize: CHUNK_SIZE,
@@ -7867,7 +8058,7 @@ function makeOffer(filePath, groupId) {
7867
8058
  function readChunk(fd, offer, index) {
7868
8059
  const len = Math.min(offer.chunkSize, offer.size - index * offer.chunkSize);
7869
8060
  const buf = Buffer.alloc(Math.max(0, len));
7870
- if (len > 0) fs11.readSync(fd, buf, 0, len, index * offer.chunkSize);
8061
+ if (len > 0) fs12.readSync(fd, buf, 0, len, index * offer.chunkSize);
7871
8062
  return buf.toString("base64");
7872
8063
  }
7873
8064
  var CHUNK_SIZE, FILE_WINDOW, IDLE_MS, MAX_PER_SENDER, MAX_TOTAL, MAX_TOTAL_BYTES, WIN_RESERVED2, FileReceiver;
@@ -7910,8 +8101,8 @@ var init_files = __esm({
7910
8101
  }
7911
8102
  begin(offer, from, fromName) {
7912
8103
  const dir = filesDir(".incoming");
7913
- const part = path10.join(dir, `${offer.fileId}.part`);
7914
- const fd = fs11.openSync(part, "w", 384);
8104
+ const part = path11.join(dir, `${offer.fileId}.part`);
8105
+ const fd = fs12.openSync(part, "w", 384);
7915
8106
  this.incoming.set(offer.fileId, { ...offer, name: safeFileName(offer.name), from, fromName, part, fd, received: /* @__PURE__ */ new Set(), bytes: 0, lastAt: Date.now() });
7916
8107
  }
7917
8108
  /** Write one chunk. `complete` = every chunk is on disk (call finalize() after acking). */
@@ -7926,7 +8117,7 @@ var init_files = __esm({
7926
8117
  const data = Buffer.from(body.data, "base64");
7927
8118
  const expected = Math.min(t.chunkSize, t.size - index * t.chunkSize);
7928
8119
  if (data.length !== Math.max(0, expected)) return { error: "chunk size mismatch" };
7929
- if (data.length) fs11.writeSync(t.fd, data, 0, data.length, index * t.chunkSize);
8120
+ if (data.length) fs12.writeSync(t.fd, data, 0, data.length, index * t.chunkSize);
7930
8121
  t.received.add(index);
7931
8122
  t.bytes += data.length;
7932
8123
  t.lastAt = Date.now();
@@ -7937,7 +8128,7 @@ var init_files = __esm({
7937
8128
  const t = this.incoming.get(fileId);
7938
8129
  if (!t) return { error: "unknown transfer" };
7939
8130
  try {
7940
- fs11.closeSync(t.fd);
8131
+ fs12.closeSync(t.fd);
7941
8132
  } catch {
7942
8133
  }
7943
8134
  this.incoming.delete(t.fileId);
@@ -7947,26 +8138,26 @@ var init_files = __esm({
7947
8138
  const actual = sha256File(t.part);
7948
8139
  if (actual !== t.sha256) {
7949
8140
  try {
7950
- fs11.unlinkSync(t.part);
8141
+ fs12.unlinkSync(t.part);
7951
8142
  } catch {
7952
8143
  }
7953
8144
  return { error: "hash mismatch" };
7954
8145
  }
7955
8146
  const dir = t.groupId ? filesDir("groups", t.groupId) : filesDir(compactAddress(t.from));
7956
8147
  const dest = uniquePath2(dir, t.name);
7957
- const root = path10.resolve(filesRoot()) + path10.sep;
7958
- if (!path10.resolve(dest).startsWith(root)) {
8148
+ const root = path11.resolve(filesRoot()) + path11.sep;
8149
+ if (!path11.resolve(dest).startsWith(root)) {
7959
8150
  try {
7960
- fs11.unlinkSync(t.part);
8151
+ fs12.unlinkSync(t.part);
7961
8152
  } catch {
7962
8153
  }
7963
8154
  return { error: "invalid destination" };
7964
8155
  }
7965
- fs11.renameSync(t.part, dest);
7966
- return { done: { fileId: t.fileId, from: t.from, fromName: t.fromName, name: path10.basename(dest), size: t.size, sha256: actual, path: dest, groupId: t.groupId } };
8156
+ fs12.renameSync(t.part, dest);
8157
+ return { done: { fileId: t.fileId, from: t.from, fromName: t.fromName, name: path11.basename(dest), size: t.size, sha256: actual, path: dest, groupId: t.groupId } };
7967
8158
  } catch (e) {
7968
8159
  try {
7969
- fs11.unlinkSync(t.part);
8160
+ fs12.unlinkSync(t.part);
7970
8161
  } catch {
7971
8162
  }
7972
8163
  return { error: `could not save file: ${e?.code || e?.message}` };
@@ -7976,11 +8167,11 @@ var init_files = __esm({
7976
8167
  const t = this.incoming.get(fileId);
7977
8168
  if (!t) return;
7978
8169
  try {
7979
- fs11.closeSync(t.fd);
8170
+ fs12.closeSync(t.fd);
7980
8171
  } catch {
7981
8172
  }
7982
8173
  try {
7983
- fs11.unlinkSync(t.part);
8174
+ fs12.unlinkSync(t.part);
7984
8175
  } catch {
7985
8176
  }
7986
8177
  this.incoming.delete(fileId);
@@ -8264,10 +8455,9 @@ var init_relay_client = __esm({
8264
8455
  reject(new Error("closed before auth"));
8265
8456
  return;
8266
8457
  }
8267
- if (this.ws === ws) {
8268
- this.ws = null;
8269
- this.transport = null;
8270
- }
8458
+ if (this.ws !== ws) return;
8459
+ this.ws = null;
8460
+ this.transport = null;
8271
8461
  for (const [, cb] of this.waiting) cb({ type: "error", error: "connection closed" });
8272
8462
  this.waiting.clear();
8273
8463
  this.emit("disconnected");
@@ -8602,6 +8792,8 @@ async function resolveAll(query) {
8602
8792
  }
8603
8793
  const tag = q.startsWith("@") || /~[a-z2-7]{4,20}$/i.test(q) ? parseHandleTag(q) : null;
8604
8794
  if (tag) {
8795
+ const known = Object.values(loadContacts()).filter((c) => c.policy !== "blocked" && cardMatchesTag({ ...c.card || {}, name: c.card?.name || c.name, address: c.address }, tag));
8796
+ if (known.length === 1) return [{ address: known[0].address, card: checked(known[0].card, known[0].address), source: "contacts" }];
8605
8797
  const res = await liveSearch({ handle: tag.suffix ? `${tag.handle}~${tag.suffix}` : tag.handle, limit: 20 });
8606
8798
  return res.filter((r) => r.card && cardMatchesTag(r.card, tag)).map((r) => ({ ...r, source: "handle" }));
8607
8799
  }
@@ -8712,7 +8904,7 @@ var init_netpolicy = __esm({
8712
8904
 
8713
8905
  // ../../cli/agentnet/node.ts
8714
8906
  import { EventEmitter as EventEmitter2 } from "node:events";
8715
- import fs12 from "node:fs";
8907
+ import fs13 from "node:fs";
8716
8908
  async function pool(items, n, fn) {
8717
8909
  const out = new Array(items.length);
8718
8910
  let i = 0;
@@ -9174,7 +9366,7 @@ var init_node = __esm({
9174
9366
  if (!a) return { ...base, result: "no_answer", reason: "recipient did not answer the offer (is its node running?)" };
9175
9367
  if (!a.body?.accepted) return { ...base, result: "rejected", reason: a.body?.reason };
9176
9368
  const ack = this.waitFor((r) => r.type === "file_ack" && r.from === to2 && r.body?.fileId === offer.fileId, 12e4);
9177
- const fd = fs12.openSync(filePath, "r");
9369
+ const fd = fs13.openSync(filePath, "r");
9178
9370
  let failed = null;
9179
9371
  let sent = 0;
9180
9372
  try {
@@ -9194,7 +9386,7 @@ var init_node = __esm({
9194
9386
  failed = `chunk ${i} not delivered`;
9195
9387
  });
9196
9388
  } finally {
9197
- fs12.closeSync(fd);
9389
+ fs13.closeSync(fd);
9198
9390
  }
9199
9391
  if (failed) {
9200
9392
  ack.cancel();
@@ -9363,9 +9555,11 @@ var init_node = __esm({
9363
9555
  const targets = [...new Set(outboxList().map((i) => i.to))].filter((t) => !this.watchedTargets.has(t));
9364
9556
  if (!targets.length) return;
9365
9557
  for (const t of targets) this.watchedTargets.add(t);
9366
- await this.watch(targets).catch(() => {
9558
+ const states = await this.watch(targets).catch(() => {
9367
9559
  for (const t of targets) this.watchedTargets.delete(t);
9560
+ return {};
9368
9561
  });
9562
+ for (const [addr, st] of Object.entries(states)) if (st === "online" && this.opts.recv) void this.flushOutboxFor(addr);
9369
9563
  }
9370
9564
  expired(i) {
9371
9565
  return nowSec() - i.createdAt > this.cfg.outboxMaxAgeDays * 86400;
@@ -10874,8 +11068,8 @@ __export(mcp_exports, {
10874
11068
  createAgentNetMcpHandler: () => createAgentNetMcpHandler,
10875
11069
  startAgentNetMcp: () => startAgentNetMcp
10876
11070
  });
10877
- import fs13 from "node:fs";
10878
- import path11 from "node:path";
11071
+ import fs14 from "node:fs";
11072
+ import path12 from "node:path";
10879
11073
  function text(o, isError = false) {
10880
11074
  return { content: [{ type: "text", text: typeof o === "string" ? o : JSON.stringify(o, null, 2) }], structuredContent: typeof o === "object" ? o : void 0, isError };
10881
11075
  }
@@ -10897,16 +11091,16 @@ async function resolveTarget(t) {
10897
11091
  const direct = normalizeAddress(t);
10898
11092
  if (direct && t.replace(/^srift:/i, "").replace(/-/g, "").length === 20) return direct;
10899
11093
  const r = await resolveAll(t);
10900
- if (r.length > 1) throw new Error(`"${t}" is ambiguous (${r.length} online agents): ${r.map((x) => "@" + (x.tag || x.address)).join(", ")}. Use the full @name~xxxxxx tag.`);
11094
+ if (r.length > 1) throw new Error(`"${t}" is ambiguous (${r.length} online agents): ${r.map((x) => "@" + (x.tag || x.address)).join(", ")}. Use the full @name~xxxxxxxx tag.`);
10901
11095
  if (!r.length) throw new Error(`Could not resolve "${t}" (it must be online, or use an invite/address). Try srift_an_search.`);
10902
11096
  return r[0].address;
10903
11097
  }
10904
11098
  function allowedFile(p) {
10905
- const abs = path11.resolve(p);
10906
- const roots = (process.env.SRIFT_AN_FILE_ROOTS || process.cwd()).split(path11.delimiter).filter(Boolean).map((r) => path11.resolve(r));
10907
- const inside = roots.some((r) => abs === r || abs.startsWith(r.endsWith(path11.sep) ? r : r + path11.sep));
11099
+ const abs = path12.resolve(p);
11100
+ const roots = (process.env.SRIFT_AN_FILE_ROOTS || process.cwd()).split(path12.delimiter).filter(Boolean).map((r) => path12.resolve(r));
11101
+ const inside = roots.some((r) => abs === r || abs.startsWith(r.endsWith(path12.sep) ? r : r + path12.sep));
10908
11102
  if (!inside) throw new Error(`Refusing to send ${abs}: only files under ${roots.join(", ")} can be sent through MCP (set SRIFT_AN_FILE_ROOTS to allow more).`);
10909
- const rel = path11.relative(roots.find((r) => abs.startsWith(r)) || roots[0], abs);
11103
+ const rel = path12.relative(roots.find((r) => abs.startsWith(r)) || roots[0], abs);
10910
11104
  if (rel.split(/[\\/]/).some((seg) => seg.startsWith("."))) throw new Error(`Refusing to send hidden files or folders (${rel}) through MCP.`);
10911
11105
  return abs;
10912
11106
  }
@@ -11243,13 +11437,13 @@ async function startAgentNetMcp(opts = {}) {
11243
11437
  hooks.onDiscovery = h.onDiscovery;
11244
11438
  let lastSize = 0;
11245
11439
  try {
11246
- lastSize = fs13.statSync(inboxPath()).size;
11440
+ lastSize = fs14.statSync(inboxPath()).size;
11247
11441
  } catch {
11248
11442
  }
11249
11443
  setInterval(() => {
11250
11444
  if (ownsLock) return;
11251
11445
  try {
11252
- const s = fs13.statSync(inboxPath()).size;
11446
+ const s = fs14.statSync(inboxPath()).size;
11253
11447
  if (s !== lastSize) {
11254
11448
  lastSize = s;
11255
11449
  hooks.onInbox();
@@ -11311,7 +11505,7 @@ var init_mcp2 = __esm({
11311
11505
  LEGACY_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
11312
11506
  INBOX_URI = "srift://agentnet/inbox";
11313
11507
  DISCOVERIES_URI = "srift://agentnet/discoveries";
11314
- to = { type: "string", description: "Target: srift: address, @name~xxxxxx tag, @name, @owner/agent, contact name, name@domain, domain, or invite link" };
11508
+ to = { type: "string", description: "Target: srift: address, @name~xxxxxxxx tag, @name, @owner/agent, contact name, name@domain, domain, or invite link" };
11315
11509
  obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
11316
11510
  AGENTNET_TOOLS = [
11317
11511
  { name: "srift_an_whoami", description: "This agent's permanent address (its 'phone number'), unique @tag, one-line description, discoverability and relays.", inputSchema: obj({}) },
@@ -11334,7 +11528,7 @@ var init_mcp2 = __esm({
11334
11528
  },
11335
11529
  {
11336
11530
  name: "srift_an_find",
11337
- description: "Resolve a specific agent: srift: address, @name~xxxxxx, @name, @owner/agent, owner:@owner (all their agents), name@domain, domain, or invite link. Falls back to live search.",
11531
+ description: "Resolve a specific agent: srift: address, @name~xxxxxxxx, @name, @owner/agent, owner:@owner (all their agents), name@domain, domain, or invite link. Falls back to live search.",
11338
11532
  inputSchema: obj({ query: { type: "string" }, limit: { type: "number" } }, ["query"])
11339
11533
  },
11340
11534
  {
@@ -11436,8 +11630,8 @@ __export(cli_exports, {
11436
11630
  inboxPath: () => inboxPath,
11437
11631
  run: () => run
11438
11632
  });
11439
- import fs14 from "node:fs";
11440
- import path12 from "node:path";
11633
+ import fs15 from "node:fs";
11634
+ import path13 from "node:path";
11441
11635
  import { spawn as spawn3 } from "node:child_process";
11442
11636
  import readline from "node:readline";
11443
11637
  function flag(args, name) {
@@ -11577,12 +11771,12 @@ ${r.map((x) => ` @${x.tag || (x.card ? handleTag(x.card) : x.address)} ${x.one
11577
11771
  if (action === "export") {
11578
11772
  const file2 = pos[2] || fail("Usage: srift an id export <file>");
11579
11773
  const { identity: identity2 } = ensureIdentity();
11580
- fs14.writeFileSync(file2, JSON.stringify(identity2, null, 2), { mode: 384 });
11774
+ fs15.writeFileSync(file2, JSON.stringify(identity2, null, 2), { mode: 384 });
11581
11775
  return out(`Exported identity (PRIVATE KEYS) to ${file2}. Keep it secret.`, { exported: file2, address: identity2.address });
11582
11776
  }
11583
11777
  if (action === "import") {
11584
11778
  const file2 = pos[2] || fail("Usage: srift an id import <file>");
11585
- const idt = JSON.parse(fs14.readFileSync(file2, "utf8"));
11779
+ const idt = JSON.parse(fs15.readFileSync(file2, "utf8"));
11586
11780
  if (addressFromEdPub(idt.edPub) !== idt.address || !idt.edPriv || !idt.xPriv) fail("Not a valid AgentNet identity file.");
11587
11781
  if (hasIdentity() && !has(args, "--force")) fail("An identity already exists. Re-run with --force to replace it.");
11588
11782
  saveIdentity(idt);
@@ -11698,12 +11892,12 @@ Home: ${anDir()}`,
11698
11892
  case "up": {
11699
11893
  if (has(args, "--detach")) {
11700
11894
  const childArgs = [...process.execArgv, process.argv[1], ...process.argv.slice(2).filter((a) => a !== "--detach")];
11701
- fs14.mkdirSync(anDir(), { recursive: true });
11895
+ fs15.mkdirSync(anDir(), { recursive: true });
11702
11896
  rotateLog();
11703
- const logFd = fs14.openSync(path12.join(anDir(), "node.log"), "a");
11897
+ const logFd = fs15.openSync(path13.join(anDir(), "node.log"), "a");
11704
11898
  const child = spawn3(process.execPath, childArgs, { detached: true, stdio: ["ignore", logFd, logFd], windowsHide: true });
11705
11899
  child.unref();
11706
- return out(`AgentNet node started in background (pid ${child.pid}). Log: ${path12.join(anDir(), "node.log")}`, { pid: child.pid });
11900
+ return out(`AgentNet node started in background (pid ${child.pid}). Log: ${path13.join(anDir(), "node.log")}`, { pid: child.pid });
11707
11901
  }
11708
11902
  const { identity } = ensureIdentity();
11709
11903
  if (sub === "host" || has(args, "--discoverable")) {
@@ -11903,13 +12097,13 @@ Home: ${anDir()}`,
11903
12097
  }
11904
12098
  try {
11905
12099
  if (t.startsWith("/file ")) {
11906
- const fp = path12.resolve(t.slice(6).trim().replace(/^"|"$/g, ""));
12100
+ const fp = path13.resolve(t.slice(6).trim().replace(/^"|"$/g, ""));
11907
12101
  if (g) {
11908
12102
  const r = await n.groupSendFile(g.state.id, fp);
11909
- console.log(` \u{1F4CE} ${path12.basename(fp)}: ${r.results.filter((x) => x.result === "delivered").length}/${r.results.length} members received it`);
12103
+ console.log(` \u{1F4CE} ${path13.basename(fp)}: ${r.results.filter((x) => x.result === "delivered").length}/${r.results.length} members received it`);
11910
12104
  } else {
11911
12105
  const r = await n.sendFile(peer, fp);
11912
- console.log(` \u{1F4CE} ${path12.basename(fp)}: ${r.result}${r.reason ? ` \u2014 ${r.reason}` : ""}`);
12106
+ console.log(` \u{1F4CE} ${path13.basename(fp)}: ${r.result}${r.reason ? ` \u2014 ${r.reason}` : ""}`);
11913
12107
  }
11914
12108
  } else if (g) {
11915
12109
  const r = await n.groupSend(g.state.id, t);
@@ -11940,8 +12134,8 @@ Home: ${anDir()}`,
11940
12134
  case "file": {
11941
12135
  const a = await target(pos[1]);
11942
12136
  const fp = pos[2] || fail("Usage: srift an file <to> <path>");
11943
- const r = await withNode(true, (n) => n.sendFile(a, path12.resolve(fp), { onProgress: (p) => {
11944
- if (!json) process.stderr.write(`\r[agentnet] sending ${path12.basename(fp)} ${Math.round(p * 100)}%`);
12137
+ const r = await withNode(true, (n) => n.sendFile(a, path13.resolve(fp), { onProgress: (p) => {
12138
+ if (!json) process.stderr.write(`\r[agentnet] sending ${path13.basename(fp)} ${Math.round(p * 100)}%`);
11945
12139
  } }));
11946
12140
  if (!json) process.stderr.write("\n");
11947
12141
  out(r.result === "delivered" ? `Delivered ${r.name} (${r.size} bytes) to ${a} in ${((r.ms || 0) / 1e3).toFixed(1)}s \u2014 SHA-256 verified by the receiver.` : `Not delivered: ${r.result}${r.reason ? ` \u2014 ${r.reason}` : ""}`, r);
@@ -12023,9 +12217,9 @@ Chat: srift an chat ${r.group.id}`, r);
12023
12217
  if (action === "file") {
12024
12218
  const id = gid(pos[2]);
12025
12219
  const fp = pos[3] || fail("Usage: srift an group file <group> <path>");
12026
- const r = await withNode(true, (n) => n.groupSendFile(id, path12.resolve(fp)));
12220
+ const r = await withNode(true, (n) => n.groupSendFile(id, path13.resolve(fp)));
12027
12221
  const ok = r.results.filter((x) => x.result === "delivered").length;
12028
- return out(`${path12.basename(fp)} delivered to ${ok}/${r.results.length} members (SHA-256 verified).
12222
+ return out(`${path13.basename(fp)} delivered to ${ok}/${r.results.length} members (SHA-256 verified).
12029
12223
  ${r.results.map((x) => ` ${x.to}: ${x.result}${x.reason ? ` \u2014 ${x.reason}` : ""}`).join("\n")}`, r);
12030
12224
  }
12031
12225
  if (action === "call") {
@@ -12071,8 +12265,8 @@ ${r.results.map((x) => ` ${x.to}: ${x.result}${x.reason ? ` \u2014 ${x.reason}`
12071
12265
  if (file2 && !has(args, "--link")) {
12072
12266
  const res = await withNode(true, async (n) => {
12073
12267
  const m = textArg ? await n.send(a, "msg", body) : null;
12074
- const f = await n.sendFile(a, path12.resolve(file2), { onProgress: (p) => {
12075
- if (!json) process.stderr.write(`\r[agentnet] sending ${path12.basename(file2)} ${Math.round(p * 100)}%`);
12268
+ const f = await n.sendFile(a, path13.resolve(file2), { onProgress: (p) => {
12269
+ if (!json) process.stderr.write(`\r[agentnet] sending ${path13.basename(file2)} ${Math.round(p * 100)}%`);
12076
12270
  } });
12077
12271
  return { message: m, file: f };
12078
12272
  });
@@ -12084,9 +12278,9 @@ ${r.results.map((x) => ` ${x.to}: ${x.result}${x.reason ? ` \u2014 ${x.reason}`
12084
12278
  if (file2) {
12085
12279
  if (!helpers.callDaemon) fail("File attachments need the SRIFT daemon.");
12086
12280
  await helpers.ensureDaemon?.();
12087
- const share = await helpers.callDaemon("/quick-share", "POST", { filePath: path12.resolve(file2), encrypt: true });
12281
+ const share = await helpers.callDaemon("/quick-share", "POST", { filePath: path13.resolve(file2), encrypt: true });
12088
12282
  if (!share?.downloadUrl) fail(`quick-share failed: ${share?.error || "unknown error"}`);
12089
- body.attachments = [{ name: path12.basename(file2), url: share.downloadUrl, size: share.size }];
12283
+ body.attachments = [{ name: path13.basename(file2), url: share.downloadUrl, size: share.size }];
12090
12284
  }
12091
12285
  const queue = has(args, "--queue");
12092
12286
  const r = await withNode(false, (n) => n.send(a, "msg", body, { queue }));
@@ -12412,7 +12606,7 @@ Hooks fire from the running node ('srift an up' / 'host' / 'mcp').`, { hooks: ma
12412
12606
  sessions,
12413
12607
  quickShare: helpers.callDaemon ? async (filePath) => {
12414
12608
  await helpers.ensureDaemon?.();
12415
- const r = await helpers.callDaemon("/quick-share", "POST", { filePath: path12.resolve(filePath), encrypt: true });
12609
+ const r = await helpers.callDaemon("/quick-share", "POST", { filePath: path13.resolve(filePath), encrypt: true });
12416
12610
  if (!r?.downloadUrl) throw new Error(r?.error || "quick-share failed");
12417
12611
  return { url: r.downloadUrl };
12418
12612
  } : void 0
@@ -12441,7 +12635,7 @@ No database anywhere: relays keep only what is live (RAM) and route only to ONLI
12441
12635
 
12442
12636
  Identity (your "phone number")
12443
12637
  srift an id [--name N] [--description D] [--skills a,b] show/create your address + handle tag
12444
- srift an id handle @name set a handle; your tag is name~xxxxxx (key-derived, unforgeable)
12638
+ srift an id handle @name set a handle; your tag is name~xxxxxxxx (key-derived, unforgeable)
12445
12639
  srift an id owner <srift-own:token> | --clear accept an owner's signed claim
12446
12640
  srift an id link-domain acme.com [--name support] print /.well-known/srift for your domain
12447
12641
  srift an id export <file> | import <file> | rotate
@@ -12453,7 +12647,7 @@ Be found (live)
12453
12647
  srift an up [--discoverable] [--poll] [--detach] go online; srift an down
12454
12648
  Find & reach others
12455
12649
  srift an search "what you need" [--watch] [--limit 10] live search; --watch notifies as matches come online
12456
- srift an find <srift:addr|@name|@name~xxxxxx|@owner/agent|owner:@owner|name@domain|domain|invite|"need">
12650
+ srift an find <srift:addr|@name|@name~xxxxxxxx|@owner/agent|owner:@owner|name@domain|domain|invite|"need">
12457
12651
  srift an knock <to> "hey, it's me \u2014 \u2026" [--wait 60] ask to connect; they accept/reject with a note
12458
12652
  srift an connect "what you need" [--max 5] [--wait 30] [--note "\u2026"] [--message "\u2026" | --no-message]
12459
12653
  search \u2192 knock best matches \u2192 first accept \u2192 conversation starts
@@ -12522,8 +12716,8 @@ Add --json to any command for machine-readable output.`;
12522
12716
  import { spawn as spawn4, execSync } from "child_process";
12523
12717
  import http7 from "http";
12524
12718
  import https3 from "https";
12525
- import fs15 from "fs";
12526
- import path13 from "path";
12719
+ import fs16 from "fs";
12720
+ import path14 from "path";
12527
12721
  import os8 from "os";
12528
12722
  import crypto8 from "crypto";
12529
12723
  import net4 from "net";
@@ -12535,8 +12729,8 @@ init_history();
12535
12729
  init_embedded();
12536
12730
  import http5 from "http";
12537
12731
  import https2 from "https";
12538
- import fs8 from "fs";
12539
- import path7 from "path";
12732
+ import fs9 from "fs";
12733
+ import path8 from "path";
12540
12734
  import os5 from "os";
12541
12735
  import { fileURLToPath } from "url";
12542
12736
  var DAEMON_PORT2 = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
@@ -12602,7 +12796,7 @@ async function handleSessionStart(name, roomSecret, isJson) {
12602
12796
  } else {
12603
12797
  console.log(`[SRIFT] Session created successfully!`);
12604
12798
  console.log(`Session ID: ${res.sessionId}`);
12605
- console.log(`Access Link: https://srift.app/join-session?id=${res.sessionId}`);
12799
+ console.log(`Access Link: ${res.joinUrl || `https://srift.app/join-session?id=${res.sessionId}`}`);
12606
12800
  if (roomSecret) {
12607
12801
  console.log(`Room Secret: ${roomSecret} (derived locally, never sent to server)`);
12608
12802
  }
@@ -12649,6 +12843,14 @@ Pending Join Requests (${res.pendingJoins.length}):`);
12649
12843
  console.log(` - ${j.username} (${j.tempUserId}) [Run 'srift approve ${j.tempUserId}' to let them in]`);
12650
12844
  });
12651
12845
  }
12846
+ if (res.participants && res.participants.length > 0) {
12847
+ console.log(`
12848
+ Members (${res.participants.length}):`);
12849
+ res.participants.forEach((p) => {
12850
+ const kick = s.role === "host" && !p.isHost ? ` [remove: 'srift kick ${p.userId}']` : "";
12851
+ console.log(` - ${p.username}${p.isHost ? " (host)" : ""} ${p.online ? "online" : "offline"} (${p.userId})${kick}`);
12852
+ });
12853
+ }
12652
12854
  }
12653
12855
  } catch (err) {
12654
12856
  console.error(`Error: ${err.message}`);
@@ -13023,8 +13225,25 @@ async function handlePubshareAdd(filePath, isJson, opts = {}) {
13023
13225
  process.exit(1);
13024
13226
  }
13025
13227
  }
13026
- function handleMonitorTransfer(fileId, jsonStream) {
13228
+ var TERMINAL_STATUSES = ["completed", "error", "cancelled"];
13229
+ async function handleMonitorTransfer(fileId, jsonStream) {
13027
13230
  const url = `${DAEMON_URL2}/api/v1/monitor/events`;
13231
+ try {
13232
+ const tx = ((await callDaemon2("/status", "GET")).activeTransfers || []).find((t) => t.fileId === fileId);
13233
+ if (!tx) {
13234
+ console.error(`[SRIFT] No transfer with id ${fileId}. See: srift list`);
13235
+ process.exit(1);
13236
+ }
13237
+ if (jsonStream) console.log(JSON.stringify({ fileId, fileName: tx.name, size: tx.size, progress: tx.progress, status: tx.status }));
13238
+ else renderProgressBar(tx.progress || 0, tx.speedKBps || 0, tx.etaSeconds || 0, tx.status);
13239
+ if (TERMINAL_STATUSES.includes(tx.status)) {
13240
+ if (!jsonStream) console.log("\nTransfer reached terminal state:", tx.status);
13241
+ process.exit(0);
13242
+ }
13243
+ } catch (err) {
13244
+ console.error(`[SRIFT] Monitor error: ${err.message}`);
13245
+ process.exit(1);
13246
+ }
13028
13247
  const req = http5.request(url, (res) => {
13029
13248
  res.on("data", (chunk) => {
13030
13249
  const lines = chunk.toString().split("\n");
@@ -13038,6 +13257,7 @@ function handleMonitorTransfer(fileId, jsonStream) {
13038
13257
  if (currentEvent === "transfer_progress" && dataJson.fileId === fileId) {
13039
13258
  if (jsonStream) {
13040
13259
  console.log(JSON.stringify(dataJson));
13260
+ if (TERMINAL_STATUSES.includes(dataJson.status)) process.exit(0);
13041
13261
  } else {
13042
13262
  renderProgressBar(
13043
13263
  dataJson.progress,
@@ -13045,7 +13265,7 @@ function handleMonitorTransfer(fileId, jsonStream) {
13045
13265
  dataJson.timeRemainingSeconds,
13046
13266
  dataJson.status
13047
13267
  );
13048
- if (dataJson.status === "completed" || dataJson.status === "error" || dataJson.status === "cancelled") {
13268
+ if (TERMINAL_STATUSES.includes(dataJson.status)) {
13049
13269
  console.log("\nTransfer reached terminal state:", dataJson.status);
13050
13270
  process.exit(0);
13051
13271
  }
@@ -13137,16 +13357,16 @@ async function handleAutoInstallMcp(isJson) {
13137
13357
  try {
13138
13358
  let configPath = "";
13139
13359
  if (process.platform === "win32") {
13140
- configPath = path7.join(process.env.APPDATA || path7.join(os5.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
13360
+ configPath = path8.join(process.env.APPDATA || path8.join(os5.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
13141
13361
  } else if (process.platform === "darwin") {
13142
- configPath = path7.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
13362
+ configPath = path8.join(os5.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
13143
13363
  } else {
13144
- configPath = path7.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
13364
+ configPath = path8.join(os5.homedir(), ".config", "Claude", "claude_desktop_config.json");
13145
13365
  }
13146
13366
  let config = { mcpServers: {} };
13147
- if (fs8.existsSync(configPath)) {
13367
+ if (fs9.existsSync(configPath)) {
13148
13368
  try {
13149
- config = JSON.parse(fs8.readFileSync(configPath, "utf8"));
13369
+ config = JSON.parse(fs9.readFileSync(configPath, "utf8"));
13150
13370
  } catch (err) {
13151
13371
  throw new Error(`Failed to parse Claude Desktop config at ${configPath}: ${err.message}`);
13152
13372
  }
@@ -13155,7 +13375,7 @@ async function handleAutoInstallMcp(isJson) {
13155
13375
  config.mcpServers = {};
13156
13376
  }
13157
13377
  const execPath = process.execPath || "";
13158
- const execBase = path7.basename(execPath).toLowerCase();
13378
+ const execBase = path8.basename(execPath).toLowerCase();
13159
13379
  const isCompiledBinary = execBase === "srift" || execBase === "srift.exe" || typeof process.isBun === "boolean" && process.isBun || process.versions && process.versions.bun !== void 0;
13160
13380
  let command = "";
13161
13381
  let mcpArgs = [];
@@ -13165,16 +13385,16 @@ async function handleAutoInstallMcp(isJson) {
13165
13385
  } else {
13166
13386
  command = "node";
13167
13387
  const __filename2 = fileURLToPath(import.meta.url);
13168
- const __dirname2 = path7.dirname(__filename2);
13169
- const scriptPath = path7.resolve(__dirname2, "index.ts").replace(/\\/g, "/");
13388
+ const __dirname2 = path8.dirname(__filename2);
13389
+ const scriptPath = path8.resolve(__dirname2, "index.ts").replace(/\\/g, "/");
13170
13390
  mcpArgs = ["--experimental-strip-types", scriptPath, "mcp"];
13171
13391
  }
13172
13392
  config.mcpServers.srift = {
13173
13393
  command,
13174
13394
  args: mcpArgs
13175
13395
  };
13176
- fs8.mkdirSync(path7.dirname(configPath), { recursive: true });
13177
- fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
13396
+ fs9.mkdirSync(path8.dirname(configPath), { recursive: true });
13397
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
13178
13398
  if (isJson) {
13179
13399
  console.log(JSON.stringify({ success: true, configPath }));
13180
13400
  } else {
@@ -13192,13 +13412,13 @@ IMPORTANT: Please restart Claude Desktop for the changes to take effect.`);
13192
13412
  }
13193
13413
  async function handleBootstrap(targetDirArg, opts = {}) {
13194
13414
  try {
13195
- const targetDir = targetDirArg ? path7.resolve(targetDirArg) : process.cwd();
13415
+ const targetDir = targetDirArg ? path8.resolve(targetDirArg) : process.cwd();
13196
13416
  const createCursorrules = opts.cursorrules || !opts.cursorrules && !opts.agents;
13197
13417
  const createAgents = opts.agents || !opts.cursorrules && !opts.agents;
13198
13418
  console.log(`[SRIFT] Bootstrapping AI Agent instructions in: ${targetDir}`);
13199
- fs8.mkdirSync(targetDir, { recursive: true });
13419
+ fs9.mkdirSync(targetDir, { recursive: true });
13200
13420
  if (createCursorrules) {
13201
- const cursorrulesPath = path7.join(targetDir, ".cursorrules");
13421
+ const cursorrulesPath = path8.join(targetDir, ".cursorrules");
13202
13422
  console.log(` Writing .cursorrules...`);
13203
13423
  let content = DEFAULT_CURSORRULES;
13204
13424
  try {
@@ -13206,11 +13426,11 @@ async function handleBootstrap(targetDirArg, opts = {}) {
13206
13426
  } catch (err) {
13207
13427
  console.log(` (Note: failed to fetch latest .cursorrules online: ${err.message}. Using offline template.)`);
13208
13428
  }
13209
- fs8.writeFileSync(cursorrulesPath, content, "utf8");
13429
+ fs9.writeFileSync(cursorrulesPath, content, "utf8");
13210
13430
  console.log(` \u2705 .cursorrules written to ${cursorrulesPath}`);
13211
13431
  }
13212
13432
  if (createAgents) {
13213
- const agentsPath = path7.join(targetDir, "AGENTS.md");
13433
+ const agentsPath = path8.join(targetDir, "AGENTS.md");
13214
13434
  console.log(` Writing AGENTS.md...`);
13215
13435
  let content = DEFAULT_AGENTS_MD;
13216
13436
  try {
@@ -13218,7 +13438,7 @@ async function handleBootstrap(targetDirArg, opts = {}) {
13218
13438
  } catch (err) {
13219
13439
  console.log(` (Note: failed to fetch latest AGENTS.md online: ${err.message}. Using offline template.)`);
13220
13440
  }
13221
- fs8.writeFileSync(agentsPath, content, "utf8");
13441
+ fs9.writeFileSync(agentsPath, content, "utf8");
13222
13442
  console.log(` \u2705 AGENTS.md written to ${agentsPath}`);
13223
13443
  }
13224
13444
  console.log(`
@@ -13290,10 +13510,10 @@ init_get();
13290
13510
  init_pack();
13291
13511
  init_history();
13292
13512
  var __filename = fileURLToPath2(import.meta.url);
13293
- var __dirname = path13.dirname(__filename);
13513
+ var __dirname = path14.dirname(__filename);
13294
13514
  var DAEMON_PORT3 = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
13295
13515
  var DAEMON_URL3 = `http://127.0.0.1:${DAEMON_PORT3}`;
13296
- var CLI_VERSION = "4.0.0";
13516
+ var CLI_VERSION = "4.1.0";
13297
13517
  function parseDuration(s) {
13298
13518
  if (!s) return 0;
13299
13519
  const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i.exec(s.trim());
@@ -13358,36 +13578,36 @@ function flagValues(args, name) {
13358
13578
  });
13359
13579
  return out;
13360
13580
  }
13361
- var OUTBOX_DIR = path13.join(os8.homedir(), ".srift", "outbox");
13581
+ var OUTBOX_DIR = path14.join(os8.homedir(), ".srift", "outbox");
13362
13582
  function outboxPath(name) {
13363
- fs15.mkdirSync(OUTBOX_DIR, { recursive: true, mode: 448 });
13583
+ fs16.mkdirSync(OUTBOX_DIR, { recursive: true, mode: 448 });
13364
13584
  try {
13365
13585
  const cutoff = Date.now() - 30 * 864e5;
13366
- for (const f of fs15.readdirSync(OUTBOX_DIR)) {
13367
- const p = path13.join(OUTBOX_DIR, f);
13586
+ for (const f of fs16.readdirSync(OUTBOX_DIR)) {
13587
+ const p = path14.join(OUTBOX_DIR, f);
13368
13588
  try {
13369
- if (fs15.statSync(p).mtimeMs < cutoff) fs15.rmSync(p, { recursive: true, force: true });
13589
+ if (fs16.statSync(p).mtimeMs < cutoff) fs16.rmSync(p, { recursive: true, force: true });
13370
13590
  } catch {
13371
13591
  }
13372
13592
  }
13373
13593
  } catch {
13374
13594
  }
13375
- const dir = path13.join(OUTBOX_DIR, `${Date.now()}-${crypto8.randomBytes(4).toString("hex")}`);
13376
- fs15.mkdirSync(dir, { recursive: true, mode: 448 });
13377
- return path13.join(dir, name);
13595
+ const dir = path14.join(OUTBOX_DIR, `${Date.now()}-${crypto8.randomBytes(4).toString("hex")}`);
13596
+ fs16.mkdirSync(dir, { recursive: true, mode: 448 });
13597
+ return path14.join(dir, name);
13378
13598
  }
13379
13599
  async function stdinToFile(dest) {
13380
13600
  if (process.stdin.isTTY) throw new Error("Nothing on stdin. Pipe data in, e.g.: tar cz dir | srift quick-share - --filename dir.tgz");
13381
- await pipeline2(process.stdin, fs15.createWriteStream(dest, { mode: 384 }));
13382
- return fs15.statSync(dest).size;
13601
+ await pipeline2(process.stdin, fs16.createWriteStream(dest, { mode: 384 }));
13602
+ return fs16.statSync(dest).size;
13383
13603
  }
13384
13604
  var SRIFT_BASE_DL_URL = "https://srift.app/dl";
13385
- var CONFIG_DIR = path13.join(os8.homedir(), ".srift");
13386
- var CONFIG_FILE = path13.join(CONFIG_DIR, "config.json");
13605
+ var CONFIG_DIR = path14.join(os8.homedir(), ".srift");
13606
+ var CONFIG_FILE = path14.join(CONFIG_DIR, "config.json");
13387
13607
  function readConfig() {
13388
13608
  try {
13389
- if (fs15.existsSync(CONFIG_FILE)) {
13390
- return JSON.parse(fs15.readFileSync(CONFIG_FILE, "utf8"));
13609
+ if (fs16.existsSync(CONFIG_FILE)) {
13610
+ return JSON.parse(fs16.readFileSync(CONFIG_FILE, "utf8"));
13391
13611
  }
13392
13612
  } catch {
13393
13613
  }
@@ -13395,8 +13615,8 @@ function readConfig() {
13395
13615
  }
13396
13616
  function writeConfig(config) {
13397
13617
  try {
13398
- fs15.mkdirSync(CONFIG_DIR, { recursive: true });
13399
- fs15.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
13618
+ fs16.mkdirSync(CONFIG_DIR, { recursive: true });
13619
+ fs16.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
13400
13620
  } catch {
13401
13621
  }
13402
13622
  }
@@ -13507,9 +13727,9 @@ async function handleVersion(isJson) {
13507
13727
  }
13508
13728
  async function handleSelfUpdate(isJson) {
13509
13729
  const isBinary = !__filename.endsWith(".ts") && !__filename.endsWith(".js");
13510
- const installDir = path13.join(os8.homedir(), ".srift", "bin");
13730
+ const installDir = path14.join(os8.homedir(), ".srift", "bin");
13511
13731
  const binName = process.platform === "win32" ? "srift.exe" : "srift";
13512
- const binPath = path13.join(installDir, binName);
13732
+ const binPath = path14.join(installDir, binName);
13513
13733
  console.log(`[srift] Checking for updates...`);
13514
13734
  let data;
13515
13735
  try {
@@ -13567,15 +13787,15 @@ async function handleSelfUpdate(isJson) {
13567
13787
  const tmpPath = binPath + ".new";
13568
13788
  const stampPath = binPath + ".new.stamp";
13569
13789
  try {
13570
- if (fs15.existsSync(stampPath)) {
13571
- const prevUrl = fs15.readFileSync(stampPath, "utf8").trim();
13572
- if (prevUrl !== binaryUrl && fs15.existsSync(tmpPath)) {
13573
- fs15.unlinkSync(tmpPath);
13790
+ if (fs16.existsSync(stampPath)) {
13791
+ const prevUrl = fs16.readFileSync(stampPath, "utf8").trim();
13792
+ if (prevUrl !== binaryUrl && fs16.existsSync(tmpPath)) {
13793
+ fs16.unlinkSync(tmpPath);
13574
13794
  }
13575
- } else if (fs15.existsSync(tmpPath)) {
13576
- fs15.unlinkSync(tmpPath);
13795
+ } else if (fs16.existsSync(tmpPath)) {
13796
+ fs16.unlinkSync(tmpPath);
13577
13797
  }
13578
- fs15.writeFileSync(stampPath, binaryUrl);
13798
+ fs16.writeFileSync(stampPath, binaryUrl);
13579
13799
  } catch {
13580
13800
  }
13581
13801
  console.log(`[srift] Downloading ${binaryUrl} ...`);
@@ -13587,7 +13807,7 @@ async function handleSelfUpdate(isJson) {
13587
13807
  process.exit(1);
13588
13808
  } finally {
13589
13809
  try {
13590
- if (fs15.existsSync(stampPath)) fs15.unlinkSync(stampPath);
13810
+ if (fs16.existsSync(stampPath)) fs16.unlinkSync(stampPath);
13591
13811
  } catch {
13592
13812
  }
13593
13813
  }
@@ -13597,10 +13817,10 @@ async function handleSelfUpdate(isJson) {
13597
13817
  const line = lines.find((l) => l.trim().endsWith(binName));
13598
13818
  if (line) {
13599
13819
  const expected = line.trim().split(/\s+/)[0].toLowerCase();
13600
- const actual = crypto8.createHash("sha256").update(fs15.readFileSync(tmpPath)).digest("hex");
13820
+ const actual = crypto8.createHash("sha256").update(fs16.readFileSync(tmpPath)).digest("hex");
13601
13821
  if (actual !== expected) {
13602
13822
  try {
13603
- fs15.unlinkSync(tmpPath);
13823
+ fs16.unlinkSync(tmpPath);
13604
13824
  } catch {
13605
13825
  }
13606
13826
  console.error(`[srift] Checksum mismatch! Aborting update.`);
@@ -13617,23 +13837,23 @@ async function handleSelfUpdate(isJson) {
13617
13837
  console.log(`[srift] Could not download checksum (${err?.message || err}). Continuing without verification.`);
13618
13838
  }
13619
13839
  try {
13620
- fs15.chmodSync(tmpPath, 493);
13840
+ fs16.chmodSync(tmpPath, 493);
13621
13841
  } catch {
13622
13842
  }
13623
13843
  const bakPath = binPath + ".bak";
13624
13844
  try {
13625
- if (fs15.existsSync(binPath)) {
13626
- if (fs15.existsSync(bakPath)) fs15.unlinkSync(bakPath);
13627
- fs15.renameSync(binPath, bakPath);
13845
+ if (fs16.existsSync(binPath)) {
13846
+ if (fs16.existsSync(bakPath)) fs16.unlinkSync(bakPath);
13847
+ fs16.renameSync(binPath, bakPath);
13628
13848
  }
13629
- fs15.renameSync(tmpPath, binPath);
13630
- if (fs15.existsSync(bakPath)) {
13849
+ fs16.renameSync(tmpPath, binPath);
13850
+ if (fs16.existsSync(bakPath)) {
13631
13851
  try {
13632
- fs15.unlinkSync(bakPath);
13852
+ fs16.unlinkSync(bakPath);
13633
13853
  } catch {
13634
13854
  }
13635
13855
  }
13636
- const others = findAllSriftBinaries().filter((p) => path13.resolve(p) !== path13.resolve(binPath));
13856
+ const others = findAllSriftBinaries().filter((p) => path14.resolve(p) !== path14.resolve(binPath));
13637
13857
  if (others.length) {
13638
13858
  console.log(`[srift] Cleaning ${others.length} stale srift binar${others.length === 1 ? "y" : "ies"} elsewhere on the system...`);
13639
13859
  for (const o of others) {
@@ -13645,7 +13865,7 @@ async function handleSelfUpdate(isJson) {
13645
13865
  purgeSriftFromPath();
13646
13866
  if (process.platform === "win32") {
13647
13867
  try {
13648
- const installDir2 = path13.dirname(binPath);
13868
+ const installDir2 = path14.dirname(binPath);
13649
13869
  const psCmd = [
13650
13870
  "$p=[System.Environment]::GetEnvironmentVariable('Path','User')",
13651
13871
  "if(-not $p){$p=''}",
@@ -13664,9 +13884,9 @@ async function handleSelfUpdate(isJson) {
13664
13884
  try {
13665
13885
  const winBin = binPath.replace(/\//g, "\\");
13666
13886
  const winTmp = tmpPath.replace(/\//g, "\\");
13667
- const batPath = path13.join(os8.tmpdir(), `srift-update-${process.pid}-${Date.now()}.bat`);
13887
+ const batPath = path14.join(os8.tmpdir(), `srift-update-${process.pid}-${Date.now()}.bat`);
13668
13888
  const batBody = '@echo off\r\ntimeout /t 2 /nobreak >nul 2>&1\r\n:swap\r\ndel /f /q "' + winBin + '" >nul 2>&1\r\nif exist "' + winBin + '" (\r\n timeout /t 1 /nobreak >nul 2>&1\r\n goto :swap\r\n)\r\nmove /y "' + winTmp + '" "' + winBin + '" >nul 2>&1\r\ndel /f /q "%~f0" >nul 2>&1\r\n';
13669
- fs15.writeFileSync(batPath, batBody, "utf8");
13889
+ fs16.writeFileSync(batPath, batBody, "utf8");
13670
13890
  spawn4("cmd.exe", ["/C", batPath], {
13671
13891
  detached: true,
13672
13892
  stdio: "ignore",
@@ -13712,7 +13932,7 @@ function _downloadAttempt(url, dest, resumeFrom, showProgress, socketIdleMs) {
13712
13932
  let received = resumeFrom;
13713
13933
  let lastPrint = 0;
13714
13934
  let lastDataAt = Date.now();
13715
- const file2 = fs15.createWriteStream(dest, { flags: resumeFrom > 0 ? "a" : "w" });
13935
+ const file2 = fs16.createWriteStream(dest, { flags: resumeFrom > 0 ? "a" : "w" });
13716
13936
  const stallTimer = setInterval(() => {
13717
13937
  if (Date.now() - lastDataAt > socketIdleMs) {
13718
13938
  clearInterval(stallTimer);
@@ -13754,8 +13974,8 @@ function _downloadAttempt(url, dest, resumeFrom, showProgress, socketIdleMs) {
13754
13974
  }
13755
13975
  function _findCurl() {
13756
13976
  if (process.platform === "win32") {
13757
- const win = path13.join(process.env.WINDIR || "C:\\Windows", "System32", "curl.exe");
13758
- if (fs15.existsSync(win)) return win;
13977
+ const win = path14.join(process.env.WINDIR || "C:\\Windows", "System32", "curl.exe");
13978
+ if (fs16.existsSync(win)) return win;
13759
13979
  }
13760
13980
  try {
13761
13981
  const out = execSync(process.platform === "win32" ? "where curl.exe" : "which curl", {
@@ -13764,14 +13984,14 @@ function _findCurl() {
13764
13984
  }).trim().split("\n").map((s) => s.trim()).filter(Boolean);
13765
13985
  for (const p of out) {
13766
13986
  if (/powershell/i.test(p)) continue;
13767
- if (fs15.existsSync(p)) return p;
13987
+ if (fs16.existsSync(p)) return p;
13768
13988
  }
13769
13989
  } catch {
13770
13990
  }
13771
13991
  return null;
13772
13992
  }
13773
13993
  async function _runCurl(curlPath, url, dest, showProgress) {
13774
- fs15.mkdirSync(path13.dirname(dest), { recursive: true });
13994
+ fs16.mkdirSync(path14.dirname(dest), { recursive: true });
13775
13995
  const ua = `srift-cli/${CLI_VERSION} (${process.platform}; ${process.arch})`;
13776
13996
  const args = [
13777
13997
  "-fL",
@@ -13808,7 +14028,7 @@ async function _runCurl(curlPath, url, dest, showProgress) {
13808
14028
  });
13809
14029
  }
13810
14030
  async function downloadFile(url, dest) {
13811
- fs15.mkdirSync(path13.dirname(dest), { recursive: true });
14031
+ fs16.mkdirSync(path14.dirname(dest), { recursive: true });
13812
14032
  const curlPath = _findCurl();
13813
14033
  if (curlPath) {
13814
14034
  console.log(`[srift] Downloading via curl (${curlPath}) \u2014 handles 5xx + connection drops natively.`);
@@ -13819,7 +14039,7 @@ async function downloadFile(url, dest) {
13819
14039
  /*showProgress*/
13820
14040
  true
13821
14041
  );
13822
- if (ok && fs15.existsSync(dest) && fs15.statSync(dest).size > 1e6) {
14042
+ if (ok && fs16.existsSync(dest) && fs16.statSync(dest).size > 1e6) {
13823
14043
  console.log("");
13824
14044
  return;
13825
14045
  }
@@ -13830,7 +14050,7 @@ async function downloadFile(url, dest) {
13830
14050
  const socketIdleMs = 3e4;
13831
14051
  let lastErr = null;
13832
14052
  for (let attempt2 = 1; attempt2 <= maxAttempts; attempt2++) {
13833
- const resumeFrom = fs15.existsSync(dest) ? fs15.statSync(dest).size : 0;
14053
+ const resumeFrom = fs16.existsSync(dest) ? fs16.statSync(dest).size : 0;
13834
14054
  try {
13835
14055
  await _downloadAttempt(
13836
14056
  url,
@@ -13843,7 +14063,7 @@ async function downloadFile(url, dest) {
13843
14063
  return;
13844
14064
  } catch (err) {
13845
14065
  lastErr = err;
13846
- const partial = fs15.existsSync(dest) ? fs15.statSync(dest).size : 0;
14066
+ const partial = fs16.existsSync(dest) ? fs16.statSync(dest).size : 0;
13847
14067
  const partialMb = (partial / 1024 / 1024).toFixed(1);
13848
14068
  if (attempt2 >= maxAttempts) break;
13849
14069
  const wait = Math.min(2e3 * attempt2, 8e3);
@@ -14053,7 +14273,7 @@ async function ensureDaemon() {
14053
14273
  void isMcp;
14054
14274
  logFn("[SRIFT] Starting background transfer daemon...");
14055
14275
  const isBunBinary = !!(process.versions && process.versions.bun) || typeof process.isBun === "boolean" && process.isBun;
14056
- const execBase = path13.basename(process.execPath || "").toLowerCase();
14276
+ const execBase = path14.basename(process.execPath || "").toLowerCase();
14057
14277
  const isCompiledBinary = isBunBinary || execBase === "srift" || execBase === "srift.exe";
14058
14278
  let execCmd;
14059
14279
  let spawnArgs;
@@ -14063,7 +14283,7 @@ async function ensureDaemon() {
14063
14283
  } else {
14064
14284
  const isJs = __filename.endsWith(".js");
14065
14285
  const daemonFile = isJs ? "daemon.js" : "daemon.ts";
14066
- const daemonPath = path13.join(__dirname, daemonFile);
14286
+ const daemonPath = path14.join(__dirname, daemonFile);
14067
14287
  execCmd = process.execPath;
14068
14288
  spawnArgs = isJs ? [daemonPath] : ["--experimental-strip-types", daemonPath];
14069
14289
  }
@@ -14319,21 +14539,21 @@ function findAllSriftBinaries() {
14319
14539
  const add = (p) => {
14320
14540
  if (!p) return;
14321
14541
  try {
14322
- const abs = path13.resolve(p);
14323
- if (fs15.existsSync(abs) && fs15.statSync(abs).isFile()) found.add(abs);
14542
+ const abs = path14.resolve(p);
14543
+ if (fs16.existsSync(abs) && fs16.statSync(abs).isFile()) found.add(abs);
14324
14544
  } catch {
14325
14545
  }
14326
14546
  };
14327
- const execBase = path13.basename(process.execPath || "").toLowerCase();
14547
+ const execBase = path14.basename(process.execPath || "").toLowerCase();
14328
14548
  if (execBase === "srift" || execBase === "srift.exe") add(process.execPath);
14329
- add(path13.join(os8.homedir(), ".srift", "bin", binName));
14549
+ add(path14.join(os8.homedir(), ".srift", "bin", binName));
14330
14550
  try {
14331
14551
  const cmd = process.platform === "win32" ? "where srift" : "which -a srift";
14332
14552
  const out = execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
14333
14553
  for (const line of out.split("\n").map((s) => s.trim()).filter(Boolean)) add(line);
14334
14554
  } catch {
14335
14555
  }
14336
- const pathDirs = (process.env.PATH || "").split(path13.delimiter).filter(Boolean);
14556
+ const pathDirs = (process.env.PATH || "").split(path14.delimiter).filter(Boolean);
14337
14557
  if (process.platform === "win32") {
14338
14558
  try {
14339
14559
  const userPath = execSync(
@@ -14346,7 +14566,7 @@ function findAllSriftBinaries() {
14346
14566
  } catch {
14347
14567
  }
14348
14568
  }
14349
- for (const dir of pathDirs) add(path13.join(dir, binName));
14569
+ for (const dir of pathDirs) add(path14.join(dir, binName));
14350
14570
  return Array.from(found);
14351
14571
  }
14352
14572
  function purgeSriftFromPath() {
@@ -14369,19 +14589,19 @@ function purgeSriftFromPath() {
14369
14589
  return;
14370
14590
  }
14371
14591
  const rcFiles = [
14372
- path13.join(os8.homedir(), ".bashrc"),
14373
- path13.join(os8.homedir(), ".zshrc"),
14374
- path13.join(os8.homedir(), ".profile"),
14375
- path13.join(os8.homedir(), ".bash_profile"),
14376
- path13.join(os8.homedir(), ".config", "fish", "config.fish")
14592
+ path14.join(os8.homedir(), ".bashrc"),
14593
+ path14.join(os8.homedir(), ".zshrc"),
14594
+ path14.join(os8.homedir(), ".profile"),
14595
+ path14.join(os8.homedir(), ".bash_profile"),
14596
+ path14.join(os8.homedir(), ".config", "fish", "config.fish")
14377
14597
  ];
14378
14598
  for (const rc of rcFiles) {
14379
- if (!fs15.existsSync(rc)) continue;
14599
+ if (!fs16.existsSync(rc)) continue;
14380
14600
  try {
14381
- const original = fs15.readFileSync(rc, "utf8");
14601
+ const original = fs16.readFileSync(rc, "utf8");
14382
14602
  const filtered = original.split("\n").filter((l) => !/\.srift[\/\\]bin/.test(l) && l.trim() !== "# SRIFT" && !l.includes("fish_add_path ~/.srift")).join("\n");
14383
14603
  if (filtered !== original) {
14384
- fs15.writeFileSync(rc, filtered);
14604
+ fs16.writeFileSync(rc, filtered);
14385
14605
  console.log(`[srift] Cleaned srift PATH entry from: ${rc}`);
14386
14606
  }
14387
14607
  } catch {
@@ -14391,9 +14611,9 @@ function purgeSriftFromPath() {
14391
14611
  function scheduleWindowsDelete(binaryPath) {
14392
14612
  try {
14393
14613
  const winBin = binaryPath.replace(/\//g, "\\");
14394
- const batPath = path13.join(os8.tmpdir(), `srift-uninstall-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.bat`);
14614
+ const batPath = path14.join(os8.tmpdir(), `srift-uninstall-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.bat`);
14395
14615
  const batBody = '@echo off\r\ntimeout /t 2 /nobreak >nul 2>&1\r\nif not exist "' + winBin + '" goto :selfDelete\r\n:retry\r\ndel /f /q "' + winBin + '" >nul 2>&1\r\nif exist "' + winBin + '" (\r\n timeout /t 1 /nobreak >nul 2>&1\r\n goto :retry\r\n)\r\n:selfDelete\r\ndel /f /q "%~f0" >nul 2>&1\r\n';
14396
- fs15.writeFileSync(batPath, batBody, "utf8");
14616
+ fs16.writeFileSync(batPath, batBody, "utf8");
14397
14617
  spawn4("cmd.exe", ["/C", batPath], {
14398
14618
  detached: true,
14399
14619
  stdio: "ignore",
@@ -14407,13 +14627,13 @@ function scheduleWindowsDelete(binaryPath) {
14407
14627
  }
14408
14628
  function removeBinary(binaryPath) {
14409
14629
  try {
14410
- fs15.unlinkSync(binaryPath);
14630
+ fs16.unlinkSync(binaryPath);
14411
14631
  return { ok: true, deferred: false };
14412
14632
  } catch (e) {
14413
14633
  if (process.platform === "win32" && (e?.code === "EPERM" || e?.code === "EBUSY" || e?.code === "EACCES" || e?.code === "ENOTEMPTY")) {
14414
14634
  try {
14415
14635
  const renamed = `${binaryPath}.old-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
14416
- fs15.renameSync(binaryPath, renamed);
14636
+ fs16.renameSync(binaryPath, renamed);
14417
14637
  scheduleWindowsDelete(renamed);
14418
14638
  return { ok: true, deferred: true };
14419
14639
  } catch {
@@ -14461,13 +14681,13 @@ async function handleUninstall(purge) {
14461
14681
  console.error(`[srift] Could not remove ${failed.length}:`);
14462
14682
  failed.forEach((f) => console.error(` ${f}`));
14463
14683
  }
14464
- const standardInstallDir = path13.join(os8.homedir(), ".srift", "bin");
14465
- if (fs15.existsSync(standardInstallDir)) {
14684
+ const standardInstallDir = path14.join(os8.homedir(), ".srift", "bin");
14685
+ if (fs16.existsSync(standardInstallDir)) {
14466
14686
  try {
14467
- for (const f of fs15.readdirSync(standardInstallDir)) {
14468
- const fp = path13.join(standardInstallDir, f);
14687
+ for (const f of fs16.readdirSync(standardInstallDir)) {
14688
+ const fp = path14.join(standardInstallDir, f);
14469
14689
  try {
14470
- fs15.unlinkSync(fp);
14690
+ fs16.unlinkSync(fp);
14471
14691
  } catch {
14472
14692
  }
14473
14693
  }
@@ -14475,18 +14695,18 @@ async function handleUninstall(purge) {
14475
14695
  }
14476
14696
  }
14477
14697
  if (purge) {
14478
- const sriftDir = path13.join(os8.homedir(), ".srift");
14479
- if (fs15.existsSync(sriftDir)) {
14698
+ const sriftDir = path14.join(os8.homedir(), ".srift");
14699
+ if (fs16.existsSync(sriftDir)) {
14480
14700
  try {
14481
- fs15.rmSync(sriftDir, { recursive: true, force: true });
14701
+ fs16.rmSync(sriftDir, { recursive: true, force: true });
14482
14702
  console.log(`[srift] Purged directory: ${sriftDir}`);
14483
14703
  } catch (e) {
14484
14704
  if (process.platform === "win32") {
14485
- const batPath = path13.join(os8.tmpdir(), `srift-purge-${process.pid}-${Date.now()}.bat`);
14705
+ const batPath = path14.join(os8.tmpdir(), `srift-purge-${process.pid}-${Date.now()}.bat`);
14486
14706
  const winDir = sriftDir.replace(/\//g, "\\");
14487
14707
  const batBody = '@echo off\r\ntimeout /t 3 /nobreak >nul 2>&1\r\n:retry\r\nrmdir /s /q "' + winDir + '" >nul 2>&1\r\nif exist "' + winDir + '" (\r\n timeout /t 1 /nobreak >nul 2>&1\r\n goto :retry\r\n)\r\ndel /f /q "%~f0" >nul 2>&1\r\n';
14488
14708
  try {
14489
- fs15.writeFileSync(batPath, batBody, "utf8");
14709
+ fs16.writeFileSync(batPath, batBody, "utf8");
14490
14710
  spawn4("cmd.exe", ["/C", batPath], { detached: true, stdio: "ignore", windowsHide: true, shell: false }).unref();
14491
14711
  console.log(`[srift] Scheduled purge of ${sriftDir} (~3s after exit).`);
14492
14712
  } catch (ee) {
@@ -14502,10 +14722,10 @@ async function handleUninstall(purge) {
14502
14722
  if (process.platform === "win32") {
14503
14723
  try {
14504
14724
  const tmp = os8.tmpdir();
14505
- for (const f of fs15.readdirSync(tmp)) {
14725
+ for (const f of fs16.readdirSync(tmp)) {
14506
14726
  if (/^srift-(uninstall|update|purge)-.*\.bat$/.test(f)) {
14507
14727
  try {
14508
- fs15.unlinkSync(path13.join(tmp, f));
14728
+ fs16.unlinkSync(path14.join(tmp, f));
14509
14729
  } catch {
14510
14730
  }
14511
14731
  }
@@ -14554,7 +14774,10 @@ var ALL_COMMANDS = [
14554
14774
  "status",
14555
14775
  "reset",
14556
14776
  "logs",
14557
- "uninstall"
14777
+ "uninstall",
14778
+ "agentnet",
14779
+ "an",
14780
+ "bootstrap"
14558
14781
  ];
14559
14782
  function suggestCommand(input) {
14560
14783
  const prefixMatch = ALL_COMMANDS.find((c) => c.startsWith(input) || input.startsWith(c.slice(0, 3)));
@@ -14684,7 +14907,7 @@ async function main() {
14684
14907
  }
14685
14908
  const jsonStream = args.includes("--json-stream");
14686
14909
  await ensureDaemon();
14687
- handleMonitorTransfer(fileId, jsonStream);
14910
+ await handleMonitorTransfer(fileId, jsonStream);
14688
14911
  break;
14689
14912
  }
14690
14913
  case "approve": {
@@ -14792,15 +15015,15 @@ async function main() {
14792
15015
  const n = await stdinToFile(filePath);
14793
15016
  if (!isJson) console.error(`[srift] Read ${n} bytes from stdin.`);
14794
15017
  } else {
14795
- const abs = path13.resolve(target);
14796
- if (!fs15.existsSync(abs)) throw new Error(`File not found: ${abs}`);
14797
- if (fs15.statSync(abs).isDirectory()) {
14798
- const base = path13.basename(abs) || "folder";
15018
+ const abs = path14.resolve(target);
15019
+ if (!fs16.existsSync(abs)) throw new Error(`File not found: ${abs}`);
15020
+ if (fs16.statSync(abs).isDirectory()) {
15021
+ const base = path14.basename(abs) || "folder";
14799
15022
  displayName = displayName || `${base}.tar.gz`;
14800
15023
  filePath = outboxPath(displayName.replace(/[\\/]/g, "_"));
14801
15024
  const excludes = [...DEFAULT_EXCLUDES, ...flagValues(args, "--exclude")];
14802
15025
  const r = await packDirectory(abs, filePath, excludes);
14803
- if (!isJson) console.error(`[srift] Packed ${r.files} file(s), ${r.bytes} bytes \u2192 ${path13.basename(filePath)} (excluded: ${excludes.join(", ")})`);
15026
+ if (!isJson) console.error(`[srift] Packed ${r.files} file(s), ${r.bytes} bytes \u2192 ${path14.basename(filePath)} (excluded: ${excludes.join(", ")})`);
14804
15027
  } else {
14805
15028
  filePath = abs;
14806
15029
  }
@@ -14832,17 +15055,17 @@ async function main() {
14832
15055
  console.error("stdin (-) cannot be combined with other paths.");
14833
15056
  process.exit(1);
14834
15057
  }
14835
- const missing = targets.filter((t) => !fs15.existsSync(path13.resolve(t)));
15058
+ const missing = targets.filter((t) => !fs16.existsSync(path14.resolve(t)));
14836
15059
  if (missing.length === targets.length) {
14837
- const msg = `File not found: ${missing.map((m) => path13.resolve(m)).join(", ")}`;
15060
+ const msg = `File not found: ${missing.map((m) => path14.resolve(m)).join(", ")}`;
14838
15061
  if (isJson) console.log(JSON.stringify({ success: false, error: msg }));
14839
15062
  else console.error(`Error: ${msg}`);
14840
15063
  process.exit(1);
14841
15064
  }
14842
- if (missing.length && !isJson) console.error(`[srift] Warning: not found, will be skipped: ${missing.map((m) => path13.resolve(m)).join(", ")}`);
15065
+ if (missing.length && !isJson) console.error(`[srift] Warning: not found, will be skipped: ${missing.map((m) => path14.resolve(m)).join(", ")}`);
14843
15066
  try {
14844
15067
  const where = await ensureDaemonOrEmbedded({ force: args.includes("--foreground") });
14845
- const res = await handleQuickShareMany(targets.map((t) => path13.resolve(t)), isJson, {
15068
+ const res = await handleQuickShareMany(targets.map((t) => path14.resolve(t)), isJson, {
14846
15069
  maxDownloads,
14847
15070
  ttlMs,
14848
15071
  encrypt: encrypt2,
@@ -14888,7 +15111,7 @@ async function main() {
14888
15111
  console.error("Several links cannot all go to stdout; use -o <dir>.");
14889
15112
  process.exit(64);
14890
15113
  }
14891
- fs15.mkdirSync(outDir, { recursive: true });
15114
+ fs16.mkdirSync(outDir, { recursive: true });
14892
15115
  const conc = Math.min(16, Math.max(1, parseInt(flagValue(args, "--concurrency") || "4", 10) || 4));
14893
15116
  const results = await mapLimit(links, conc, async (l) => {
14894
15117
  try {
@@ -14942,8 +15165,10 @@ async function main() {
14942
15165
  break;
14943
15166
  }
14944
15167
  const items = readHistory().reverse();
15168
+ const limitArg = parseInt(flagValue(args, "--limit") || "", 10);
15169
+ const limit = Number.isFinite(limitArg) && limitArg > 0 ? limitArg : null;
14945
15170
  if (isJson) {
14946
- console.log(JSON.stringify({ items }));
15171
+ console.log(JSON.stringify({ items: limit ? items.slice(0, limit) : items }));
14947
15172
  break;
14948
15173
  }
14949
15174
  if (!items.length) {
@@ -14951,8 +15176,7 @@ async function main() {
14951
15176
  break;
14952
15177
  }
14953
15178
  const now = Date.now();
14954
- const limit = parseInt(flagValue(args, "--limit") || "20", 10);
14955
- for (const it of items.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 20)) {
15179
+ for (const it of items.slice(0, limit || 20)) {
14956
15180
  const state = it.expiresAt && it.expiresAt <= now ? "expired" : it.expiresAt ? `expires ${new Date(it.expiresAt).toLocaleString()}` : "no expiry";
14957
15181
  console.log(`${it.at.slice(0, 19).replace("T", " ")} ${it.mode.padEnd(5)} ${it.encrypted ? "e2ee " : "plain"} ${it.fileName} (${state})`);
14958
15182
  console.log(` ${it.downloadUrl}`);
@@ -15181,7 +15405,7 @@ Env vars:
15181
15405
  function printInstallMcpInstructions() {
15182
15406
  const claudeWin = `${process.env.APPDATA || "%APPDATA%"}\\Claude\\claude_desktop_config.json`;
15183
15407
  const execPath = process.execPath || "";
15184
- const execBase = path13.basename(execPath).toLowerCase();
15408
+ const execBase = path14.basename(execPath).toLowerCase();
15185
15409
  const isCompiledBinary = execBase === "srift" || execBase === "srift.exe" || typeof process.isBun === "boolean" && process.isBun || process.versions && process.versions.bun !== void 0;
15186
15410
  let cmd;
15187
15411
  let argList;
@@ -15198,7 +15422,7 @@ function printInstallMcpInstructions() {
15198
15422
  argList = `["mcp"]`;
15199
15423
  cursorCmd = `srift mcp`;
15200
15424
  } else {
15201
- const scriptPath = path13.resolve(__dirname, "index.ts").replace(/\\/g, "/");
15425
+ const scriptPath = path14.resolve(__dirname, "index.ts").replace(/\\/g, "/");
15202
15426
  cmd = `node`;
15203
15427
  argList = `["--experimental-strip-types","${scriptPath}","mcp"]`;
15204
15428
  cursorCmd = `node --experimental-strip-types ${scriptPath} mcp`;
@@ -15232,10 +15456,10 @@ Same command/args as above.
15232
15456
 
15233
15457
  # Cloud / browser agents (ChatGPT, Claude.ai, n8n, Zapier, etc.)
15234
15458
  # These run off your machine and CANNOT reach 127.0.0.1 \u2014 use the hosted endpoint:
15235
- Hosted MCP: POST https://srift.app/mcp (no install, 8 orchestration tools)
15459
+ Hosted MCP: POST https://srift.app/mcp (no install, 9 session/control tools)
15236
15460
  { "mcpServers": { "srift": { "type": "streamable-http", "url": "https://srift.app/mcp" } } }
15237
15461
 
15238
- # Local daemon surfaces (this machine only \u2014 full 14 tools):
15462
+ # Local daemon surfaces (this machine only \u2014 all 15 tools):
15239
15463
  HTTP MCP endpoint: POST http://127.0.0.1:${DAEMON_PORT3}/mcp
15240
15464
  OpenAPI spec: GET http://127.0.0.1:${DAEMON_PORT3}/openapi.json
15241
15465
  AI Plugin: GET http://127.0.0.1:${DAEMON_PORT3}/.well-known/ai-plugin.json