srift-transfer 2.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4545 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+
7
+ // ../../lib/mcp/core.mjs
8
+ function buildPromptMessages(name, args) {
9
+ switch (name) {
10
+ case "send_file_to_user":
11
+ return [{
12
+ role: "user",
13
+ content: {
14
+ type: "text",
15
+ text: `Deliver the file at "${args?.filePath || "<filePath>"}" to the user via SRIFT.
16
+
17
+ 1) Call srift_quick_share with filePath="${args?.filePath || "<filePath>"}".
18
+ 2) Read the returned share URL aloud to the user.
19
+ 3) Poll srift_list_transfers (or read srift://transfers/active) until status is "completed".`
20
+ }
21
+ }];
22
+ case "receive_file_from_user":
23
+ return [{
24
+ role: "user",
25
+ content: {
26
+ type: "text",
27
+ text: `Open a SRIFT room and wait for the user to send you files.
28
+
29
+ 1) Call srift_start_session.
30
+ 2) Give the user the join URL: https://srift.app/join-session?id=<SESSION_ID> (full session UI with chat + multi-file transfer).
31
+ Or, for a single one-shot file from agent \u2192 user, prefer srift_quick_share which returns a direct download URL.
32
+ 3) Poll srift_session_status; when pendingJoins appears, call srift_approve_join.
33
+ 4) When a file_offer arrives (visible via srift_list_transfers), call srift_accept_transfer with saveDir="${args?.saveDir || "./"}".`
34
+ }
35
+ }];
36
+ case "start_collab_session":
37
+ return [{
38
+ role: "user",
39
+ content: {
40
+ type: "text",
41
+ text: `Open a SRIFT collaboration room named "${args?.sessionName || "AI-Collab"}" and use it for E2EE chat + ad-hoc file transfer with the user.
42
+
43
+ 1) srift_start_session({ sessionName: "${args?.sessionName || "AI-Collab"}" })
44
+ 2) Share the join URL.
45
+ 3) If srift_send_chat / srift_send_file are available on this transport, use them directly. Otherwise (hosted/remote MCP), tell the user to open the join URL in their browser or CLI \u2014 chat and file transfer happen E2EE between the real peers, never through this server.
46
+ 4) Use srift_accept_transfer as needed when running locally.`
47
+ }
48
+ }];
49
+ }
50
+ return [];
51
+ }
52
+ function resp(id, result) {
53
+ return { jsonrpc: "2.0", id, result };
54
+ }
55
+ function errResp(id, code, message) {
56
+ return { jsonrpc: "2.0", id, error: { code, message } };
57
+ }
58
+ function createMcpHandler(opts) {
59
+ const { backend, serverInfo, instructions } = opts;
60
+ const tools = opts.toolNames ? MCP_TOOLS.filter((t) => opts.toolNames.has(t.name)) : MCP_TOOLS;
61
+ const resources = opts.resourceUris ? MCP_RESOURCES.filter((r) => opts.resourceUris.has(r.uri)) : MCP_RESOURCES;
62
+ const prompts = opts.promptNames ? MCP_PROMPTS.filter((p) => opts.promptNames.has(p.name)) : MCP_PROMPTS;
63
+ const toolSet = new Set(tools.map((t) => t.name));
64
+ const resourceSet = new Set(resources.map((r) => r.uri));
65
+ const promptSet = new Set(prompts.map((p) => p.name));
66
+ return async function handleMcpMessage2(message, ctx = {}) {
67
+ if (!message || typeof message !== "object" || Array.isArray(message)) {
68
+ return errResp(null, -32600, "Invalid Request");
69
+ }
70
+ const { id, method, params } = message;
71
+ if (id === void 0 || id === null) {
72
+ return null;
73
+ }
74
+ if (typeof method !== "string") {
75
+ return errResp(id, -32600, "Invalid Request: method must be a string");
76
+ }
77
+ try {
78
+ if (method === "initialize") {
79
+ return resp(id, {
80
+ protocolVersion: PROTOCOL_VERSION,
81
+ capabilities: {
82
+ tools: { listChanged: false },
83
+ resources: { listChanged: false, subscribe: false },
84
+ prompts: { listChanged: false },
85
+ logging: {}
86
+ },
87
+ serverInfo,
88
+ instructions
89
+ });
90
+ }
91
+ if (method === "ping") {
92
+ return resp(id, {});
93
+ }
94
+ if (method === "tools/list") {
95
+ return resp(id, { tools });
96
+ }
97
+ if (method === "tools/call") {
98
+ const { name, arguments: args = {} } = params || {};
99
+ if (typeof name !== "string") {
100
+ return errResp(id, -32602, "Invalid params: tool name required");
101
+ }
102
+ if (!toolSet.has(name)) {
103
+ return resp(id, {
104
+ isError: true,
105
+ content: [{ type: "text", text: `Tool "${name}" is not available on this transport.` }]
106
+ });
107
+ }
108
+ const text = await backend.callTool(name, args, ctx);
109
+ return resp(id, { content: [{ type: "text", text }] });
110
+ }
111
+ if (method === "resources/list") {
112
+ return resp(id, { resources });
113
+ }
114
+ if (method === "resources/read") {
115
+ const { uri } = params || {};
116
+ if (typeof uri !== "string" || !resourceSet.has(uri)) {
117
+ return errResp(id, -32602, `Resource not found: ${uri}`);
118
+ }
119
+ const content = await backend.readResource(uri, ctx);
120
+ return resp(id, {
121
+ contents: [{
122
+ uri,
123
+ mimeType: uri === "srift://docs/quickstart" ? "text/markdown" : "application/json",
124
+ text: content
125
+ }]
126
+ });
127
+ }
128
+ if (method === "prompts/list") {
129
+ return resp(id, { prompts });
130
+ }
131
+ if (method === "prompts/get") {
132
+ const { name, arguments: args = {} } = params || {};
133
+ if (typeof name !== "string" || !promptSet.has(name)) {
134
+ return errResp(id, -32602, `Prompt not found: ${name}`);
135
+ }
136
+ const messages = buildPromptMessages(name, args);
137
+ return resp(id, {
138
+ description: prompts.find((p) => p.name === name)?.description || "",
139
+ messages
140
+ });
141
+ }
142
+ if (method === "notifications/initialized" || method === "notifications/cancelled") {
143
+ return null;
144
+ }
145
+ return errResp(id, -32601, `Method not found: ${method}`);
146
+ } catch (err) {
147
+ if (method === "tools/call") {
148
+ return resp(id, { isError: true, content: [{ type: "text", text: `Error: ${err.message}` }] });
149
+ }
150
+ return errResp(id, -32603, err.message || "Internal error");
151
+ }
152
+ };
153
+ }
154
+ var PROTOCOL_VERSION, MCP_TOOLS, MCP_RESOURCES, MCP_PROMPTS, QUICKSTART_DOC;
155
+ var init_core = __esm({
156
+ "../../lib/mcp/core.mjs"() {
157
+ "use strict";
158
+ PROTOCOL_VERSION = "2025-06-18";
159
+ MCP_TOOLS = [
160
+ {
161
+ name: "srift_start_session",
162
+ description: "Create a new SRIFT secure session. Returns the 7-character session ID and a shareable URL. The host approves all future joins. E2EE keys derive locally from the session ID + optional roomSecret.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ sessionName: { type: "string", description: "Human-readable session name (optional)" },
167
+ roomSecret: { type: "string", description: "Optional shared secret for extra-strong E2EE key derivation. Never sent to server." }
168
+ }
169
+ }
170
+ },
171
+ {
172
+ name: "srift_join_session",
173
+ description: "Request to join an existing SRIFT session by its 7-character session ID. The host must approve before transfers/chat work.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ sessionId: { type: "string", description: "7-character session ID (e.g. ABC1234)" },
178
+ username: { type: "string", description: "Display name (optional, defaults to AI-Agent)" },
179
+ roomSecret: { type: "string", description: "Optional shared secret matching the host's" }
180
+ },
181
+ required: ["sessionId"]
182
+ }
183
+ },
184
+ {
185
+ name: "srift_session_status",
186
+ description: "Get current session details: id, role (host/guest), connection state, pending join requests, peer count.",
187
+ inputSchema: { type: "object", properties: {} }
188
+ },
189
+ {
190
+ name: "srift_close_session",
191
+ description: "Close the active session. Flushes E2EE keys, disconnects signaling, clears chat history.",
192
+ inputSchema: { type: "object", properties: {} }
193
+ },
194
+ {
195
+ name: "srift_approve_join",
196
+ description: "Host-only. Approve a pending guest join request by their temp user ID.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ tempUserId: { type: "string", description: "Temp user ID from session_status pendingJoins" }
201
+ },
202
+ required: ["tempUserId"]
203
+ }
204
+ },
205
+ {
206
+ name: "srift_reject_join",
207
+ description: "Host-only. Reject a pending guest join request.",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ tempUserId: { type: "string" },
212
+ reason: { type: "string", description: "Optional rejection reason" }
213
+ },
214
+ required: ["tempUserId"]
215
+ }
216
+ },
217
+ {
218
+ name: "srift_kick_user",
219
+ description: "Host-only. Disconnect and ban a peer from the active room.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: { userId: { type: "string" } },
223
+ required: ["userId"]
224
+ }
225
+ },
226
+ {
227
+ name: "srift_send_file",
228
+ description: "Offer a file to the connected peer in an existing session. The peer must accept with srift_accept_transfer. Returns a fileId for monitoring. Transport is auto-selected internally.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ filePath: { type: "string", description: "Absolute path to the file on disk" }
233
+ },
234
+ required: ["filePath"]
235
+ }
236
+ },
237
+ {
238
+ name: "srift_accept_transfer",
239
+ description: "Accept a pending file offer from the peer and start downloading.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: {
243
+ fileId: { type: "string", description: "fileId from the inbound file_offer event" },
244
+ saveDir: { type: "string", description: "Absolute path to destination directory (defaults to CWD)" }
245
+ },
246
+ required: ["fileId"]
247
+ }
248
+ },
249
+ {
250
+ name: "srift_list_transfers",
251
+ description: "List all active and recent transfers with progress, speed, ETA, and status.",
252
+ inputSchema: { type: "object", properties: {} }
253
+ },
254
+ {
255
+ name: "srift_quick_share",
256
+ description: 'One-shot helper: if no session is active, creates one; then seeds the file and returns a public DOWNLOAD URL (https://srift.app/d/<token>) the recipient can open in any browser or curl. No install needed on their side. Best for "AI wants to deliver a file to the user with zero ceremony".',
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ filePath: { type: "string" },
261
+ sessionName: { type: "string" }
262
+ },
263
+ required: ["filePath"]
264
+ }
265
+ },
266
+ {
267
+ name: "srift_send_chat",
268
+ description: "Send an end-to-end encrypted (AES-256-GCM) chat message to the connected peer.",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: { message: { type: "string" } },
272
+ required: ["message"]
273
+ }
274
+ },
275
+ {
276
+ name: "srift_chat_history",
277
+ description: "Read the locally-decrypted chat log for the current session.",
278
+ inputSchema: { type: "object", properties: {} }
279
+ },
280
+ {
281
+ name: "srift_read_state",
282
+ description: "Return the raw .srift-state.json contents (session + active transfers snapshot). Useful for one-shot polling without subscribing to SSE.",
283
+ inputSchema: { type: "object", properties: {} }
284
+ }
285
+ ];
286
+ MCP_RESOURCES = [
287
+ {
288
+ uri: "srift://session/status",
289
+ name: "Session Status",
290
+ description: "Current session ID, role, connection status, pending joins",
291
+ mimeType: "application/json"
292
+ },
293
+ {
294
+ uri: "srift://transfers/active",
295
+ name: "Active Transfers",
296
+ description: "Uploads and downloads with live progress, speed, ETA",
297
+ mimeType: "application/json"
298
+ },
299
+ {
300
+ uri: "srift://chat/messages",
301
+ name: "Chat History",
302
+ description: "E2EE chat log decrypted locally",
303
+ mimeType: "application/json"
304
+ },
305
+ {
306
+ uri: "srift://workspace/state",
307
+ name: "Workspace State Snapshot",
308
+ description: "Raw .srift-state.json contents",
309
+ mimeType: "application/json"
310
+ },
311
+ {
312
+ uri: "srift://docs/quickstart",
313
+ name: "Quickstart Instructions",
314
+ description: "Inline guide for AI agents on how to use this MCP server",
315
+ mimeType: "text/markdown"
316
+ }
317
+ ];
318
+ MCP_PROMPTS = [
319
+ {
320
+ name: "send_file_to_user",
321
+ description: "Workflow: create a session, seed a file, hand the user a share URL.",
322
+ arguments: [
323
+ { name: "filePath", description: "Absolute path to the file you want to deliver", required: true }
324
+ ]
325
+ },
326
+ {
327
+ name: "receive_file_from_user",
328
+ description: "Workflow: open a session and wait for the user to drop files into it.",
329
+ arguments: [
330
+ { name: "saveDir", description: "Where to save incoming files", required: false }
331
+ ]
332
+ },
333
+ {
334
+ name: "start_collab_session",
335
+ description: "Workflow: open a long-lived E2EE chat + file room with the user.",
336
+ arguments: [
337
+ { name: "sessionName", description: "Label for the room", required: false }
338
+ ]
339
+ }
340
+ ];
341
+ QUICKSTART_DOC = `# SRIFT MCP Quickstart for AI Agents
342
+
343
+ You are connected to SRIFT \u2014 a zero-config, zero-token, peer-to-peer secure file transfer + E2EE chat layer.
344
+
345
+ ## Common workflows
346
+
347
+ **Send a file to the user (fastest path \u2014 zero install on their side):**
348
+ \`\`\`
349
+ srift_quick_share({ filePath: "/abs/path/file.zip" })
350
+ \u2192 returns { downloadUrl, fileName, fileSize }
351
+ \u2192 paste downloadUrl to the user (https://srift.app/d/<token>)
352
+ \u2192 they open it in any browser, or run:
353
+ curl -OJ <downloadUrl>
354
+ wget --content-disposition <downloadUrl>
355
+ \u2192 keep the daemon running while the download is in progress
356
+ \`\`\`
357
+
358
+ **Open a long-lived collaboration room:**
359
+ \`\`\`
360
+ srift_start_session({ sessionName: "AI-Collab" })
361
+ \u2192 returns { sessionId }
362
+ \u2192 user joins via https://srift.app/join-session?id=<sessionId>
363
+ \u2192 host (you) calls srift_approve_join when pendingJoins appears
364
+ \`\`\`
365
+
366
+ **Receive a file from the user:**
367
+ The user clicks the share link, requests join, you approve, they drop a file,
368
+ you see it via srift_list_transfers and call srift_accept_transfer.
369
+
370
+ ## Monitoring
371
+ - Poll \`srift_session_status\` or read resource \`srift://session/status\`.
372
+ - Poll \`srift_list_transfers\` or read \`srift://transfers/active\`.
373
+ - Live state snapshot: \`srift_read_state\` or \`srift://workspace/state\` (local daemon only).
374
+
375
+ ## Security model
376
+ - AES-256-GCM, 12-byte random IV per message.
377
+ - PBKDF2-SHA256, 100,000 iterations.
378
+ - All keys derive locally from sessionId (+ optional roomSecret). Server never sees them.
379
+ - File chunks transit P2P (WebTorrent for >10MB, WebSocket-relay fallback otherwise).
380
+
381
+ ## Local daemon vs hosted MCP
382
+ If you are talking to the hosted endpoint (https://srift.app/mcp) instead of a local
383
+ \`srift mcp\` daemon, file-transfer tools that require a local filesystem
384
+ (srift_send_file, srift_accept_transfer, srift_quick_share) and E2EE chat tools
385
+ (srift_send_chat, srift_chat_history) are NOT available \u2014 the hosted server
386
+ never touches plaintext file bytes or derives E2EE keys, only session/peer
387
+ orchestration. Install the CLI (\`curl -fsSL https://srift.app/install.sh | sh\`)
388
+ for full file-transfer + chat capability.
389
+ `;
390
+ }
391
+ });
392
+
393
+ // ../../cli/mcp.ts
394
+ import http2 from "http";
395
+ function callDaemon2(endpoint, method, body) {
396
+ return new Promise((resolve, reject) => {
397
+ const url = `${DAEMON_URL2}${endpoint}`;
398
+ const options = {
399
+ method,
400
+ headers: { "Content-Type": "application/json" }
401
+ };
402
+ const req = http2.request(url, options, (res) => {
403
+ let data = "";
404
+ res.on("data", (chunk) => data += chunk);
405
+ res.on("end", () => {
406
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
407
+ try {
408
+ resolve(JSON.parse(data));
409
+ } catch {
410
+ resolve({ success: true });
411
+ }
412
+ } else {
413
+ try {
414
+ const errJson = JSON.parse(data);
415
+ reject(new Error(errJson.error || `HTTP ${res.statusCode}`));
416
+ } catch {
417
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
418
+ }
419
+ }
420
+ });
421
+ });
422
+ req.on("error", (err) => reject(new Error(`Daemon connection failed: ${err.message}`)));
423
+ if (body) req.write(JSON.stringify(body));
424
+ req.end();
425
+ });
426
+ }
427
+ async function callTool(name, args) {
428
+ switch (name) {
429
+ case "srift_start_session": {
430
+ const res = await callDaemon2("/session/start", "POST", {
431
+ sessionName: args.sessionName,
432
+ roomSecret: args.roomSecret
433
+ });
434
+ return `Session created.
435
+ Session ID: ${res.sessionId}
436
+ Share URL: https://srift.app/join-session?id=${res.sessionId}
437
+ You are the HOST. Approve incoming joins with srift_approve_join.`;
438
+ }
439
+ case "srift_join_session": {
440
+ const res = await callDaemon2("/session/join", "POST", args);
441
+ return `Join requested for session ${args.sessionId}. Status: ${res.success ? "awaiting host approval" : "failed"}.`;
442
+ }
443
+ case "srift_session_status": {
444
+ const res = await callDaemon2("/status", "GET");
445
+ return JSON.stringify({ session: res.session, pendingJoins: res.pendingJoins }, null, 2);
446
+ }
447
+ case "srift_close_session":
448
+ await callDaemon2("/session/close", "POST");
449
+ return "Session closed. Keys flushed.";
450
+ case "srift_approve_join":
451
+ await callDaemon2("/session/approve", "POST", args);
452
+ return `Approved ${args.tempUserId}.`;
453
+ case "srift_reject_join":
454
+ await callDaemon2("/session/reject", "POST", args);
455
+ return `Rejected ${args.tempUserId}.`;
456
+ case "srift_kick_user":
457
+ await callDaemon2("/session/kick", "POST", args);
458
+ return `Kicked ${args.userId}.`;
459
+ case "srift_send_file": {
460
+ const res = await callDaemon2("/send", "POST", args);
461
+ return `File offered.
462
+ File ID: ${res.fileId}
463
+ The peer must call srift_accept_transfer with this fileId.`;
464
+ }
465
+ case "srift_accept_transfer":
466
+ await callDaemon2("/receive", "POST", args);
467
+ return `Transfer ${args.fileId} accepted. Downloading\u2026`;
468
+ case "srift_list_transfers": {
469
+ const res = await callDaemon2("/status", "GET");
470
+ return JSON.stringify(res.activeTransfers || [], null, 2);
471
+ }
472
+ case "srift_quick_share": {
473
+ const res = await callDaemon2("/quick-share", "POST", args);
474
+ const url = res.downloadUrl || res.shareUrl;
475
+ return [
476
+ `Quick share ready.`,
477
+ `Download URL: ${url}`,
478
+ `File: ${res.fileName} (${res.fileSize} bytes)`,
479
+ ``,
480
+ `Give the URL to the user \u2014 they open it in any browser, or run:`,
481
+ ` curl -OJ "${url}"`,
482
+ `Keep this srift daemon running while they download.`
483
+ ].join("\n");
484
+ }
485
+ case "srift_send_chat":
486
+ await callDaemon2("/chat/send", "POST", args);
487
+ return `Sent (E2EE): "${args.message}"`;
488
+ case "srift_chat_history": {
489
+ const res = await callDaemon2("/chat/history", "GET");
490
+ return JSON.stringify(res, null, 2);
491
+ }
492
+ case "srift_read_state": {
493
+ const res = await callDaemon2("/state", "GET");
494
+ return JSON.stringify(res, null, 2);
495
+ }
496
+ default:
497
+ throw new Error(`Unknown tool: ${name}`);
498
+ }
499
+ }
500
+ async function readResource(uri) {
501
+ if (uri === "srift://session/status") {
502
+ const res = await callDaemon2("/status", "GET");
503
+ return JSON.stringify(res.session, null, 2);
504
+ }
505
+ if (uri === "srift://transfers/active") {
506
+ const res = await callDaemon2("/status", "GET");
507
+ return JSON.stringify(res.activeTransfers || [], null, 2);
508
+ }
509
+ if (uri === "srift://chat/messages") {
510
+ const res = await callDaemon2("/chat/history", "GET");
511
+ return JSON.stringify(res, null, 2);
512
+ }
513
+ if (uri === "srift://workspace/state") {
514
+ const res = await callDaemon2("/state", "GET");
515
+ return JSON.stringify(res, null, 2);
516
+ }
517
+ if (uri === "srift://docs/quickstart") {
518
+ return QUICKSTART_DOC;
519
+ }
520
+ throw new Error(`Resource not found: ${uri}`);
521
+ }
522
+ function startMcpServer() {
523
+ let buffer = "";
524
+ process.stdin.on("data", (chunk) => {
525
+ buffer += chunk.toString();
526
+ let boundary = buffer.indexOf("\n");
527
+ while (boundary !== -1) {
528
+ const line = buffer.substring(0, boundary).trim();
529
+ buffer = buffer.substring(boundary + 1);
530
+ boundary = buffer.indexOf("\n");
531
+ if (line) {
532
+ try {
533
+ const message = JSON.parse(line);
534
+ handleMcpMessage(message).then((response) => {
535
+ if (response) process.stdout.write(JSON.stringify(response) + "\n");
536
+ });
537
+ } catch (err) {
538
+ process.stdout.write(JSON.stringify({
539
+ jsonrpc: "2.0",
540
+ id: null,
541
+ error: { code: -32700, message: "Parse error: " + err.message }
542
+ }) + "\n");
543
+ }
544
+ }
545
+ }
546
+ });
547
+ process.on("SIGINT", () => process.exit(0));
548
+ }
549
+ var DAEMON_PORT2, DAEMON_URL2, SERVER_INFO, localDaemonBackend, handleMcpMessage;
550
+ var init_mcp = __esm({
551
+ "../../cli/mcp.ts"() {
552
+ "use strict";
553
+ init_core();
554
+ DAEMON_PORT2 = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
555
+ DAEMON_URL2 = `http://127.0.0.1:${DAEMON_PORT2}`;
556
+ SERVER_INFO = {
557
+ name: "srift-mcp-server",
558
+ title: "SRIFT Secure P2P File Transfer",
559
+ version: "2.0.0"
560
+ };
561
+ localDaemonBackend = { callTool, readResource };
562
+ handleMcpMessage = createMcpHandler({
563
+ backend: localDaemonBackend,
564
+ serverInfo: SERVER_INFO,
565
+ instructions: "SRIFT is a zero-config local P2P file transfer + E2EE chat tool. Use srift_quick_share to deliver files to the user in one step. Use srift_start_session to open a long-lived room. Read srift://docs/quickstart for the full guide."
566
+ });
567
+ }
568
+ });
569
+
570
+ // ../../cli/daemon.ts
571
+ var daemon_exports = {};
572
+ import express from "express";
573
+ import cors from "cors";
574
+ import { WebSocket } from "ws";
575
+ import { webcrypto } from "crypto";
576
+ import fs2 from "fs";
577
+ import path2 from "path";
578
+ import { v4 as uuidv4 } from "uuid";
579
+ import dotenv from "dotenv";
580
+ async function loadWebTorrent() {
581
+ if (_WebTorrentCtor) return _WebTorrentCtor;
582
+ if (_webTorrentLoadError) throw new Error(_webTorrentLoadError);
583
+ try {
584
+ const mod = await import("webtorrent");
585
+ _WebTorrentCtor = mod.default || mod;
586
+ return _WebTorrentCtor;
587
+ } catch (err) {
588
+ _webTorrentLoadError = `WebTorrent unavailable (likely missing native module): ${err?.message || err}`;
589
+ console.error("[DAEMON]", _webTorrentLoadError);
590
+ throw new Error(_webTorrentLoadError);
591
+ }
592
+ }
593
+ async function selectSignalerUrl() {
594
+ if (DEFAULT_SIGNALER_URL.includes("127.0.0.1") || DEFAULT_SIGNALER_URL.includes("localhost")) {
595
+ try {
596
+ const controller = new AbortController();
597
+ const id = setTimeout(() => controller.abort(), 1e3);
598
+ await fetch(DEFAULT_SIGNALER_URL, { signal: controller.signal });
599
+ clearTimeout(id);
600
+ console.log(`[DAEMON] Local signaler detected at ${DEFAULT_SIGNALER_URL}`);
601
+ } catch (e) {
602
+ console.log(`[DAEMON] Local signaler at ${DEFAULT_SIGNALER_URL} is unreachable. Falling back to production signaler: https://srift.app`);
603
+ activeSignalerUrl = "https://srift.app";
604
+ }
605
+ }
606
+ }
607
+ async function getWtClient() {
608
+ if (!wtClient) {
609
+ const WTCtor = await loadWebTorrent();
610
+ wtClient = new WTCtor();
611
+ }
612
+ return wtClient;
613
+ }
614
+ function bytesToBase64(bytes) {
615
+ let binary = "";
616
+ const CH = 32768;
617
+ for (let i = 0; i < bytes.length; i += CH) {
618
+ binary += String.fromCharCode.apply(
619
+ null,
620
+ Array.from(bytes.subarray(i, i + CH))
621
+ );
622
+ }
623
+ return Buffer.from(binary, "binary").toString("base64");
624
+ }
625
+ function base64ToBytes(b64) {
626
+ const binary = Buffer.from(b64, "base64").toString("binary");
627
+ const out = new Uint8Array(binary.length);
628
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
629
+ return out;
630
+ }
631
+ async function deriveKey(sessionId, roomSecret) {
632
+ const encoder = new TextEncoder();
633
+ const salt = encoder.encode(
634
+ "srift-salt-" + sessionId + (roomSecret ? "|rs|" + roomSecret : "")
635
+ );
636
+ const ikm = encoder.encode(sessionId + (roomSecret ? "::" + roomSecret : ""));
637
+ const keyMaterial = await crypto.subtle.importKey(
638
+ "raw",
639
+ ikm,
640
+ { name: "PBKDF2" },
641
+ false,
642
+ ["deriveBits", "deriveKey"]
643
+ );
644
+ return crypto.subtle.deriveKey(
645
+ { name: "PBKDF2", salt, iterations: KDF_ITERATIONS, hash: "SHA-256" },
646
+ keyMaterial,
647
+ { name: "AES-GCM", length: 256 },
648
+ false,
649
+ ["encrypt", "decrypt"]
650
+ );
651
+ }
652
+ async function encrypt(message) {
653
+ if (!encryptionKey) throw new Error("Key not derived");
654
+ const data = new TextEncoder().encode(message);
655
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LEN));
656
+ const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, encryptionKey, data);
657
+ const ct = new Uint8Array(encrypted);
658
+ const combined = new Uint8Array(iv.length + ct.length);
659
+ combined.set(iv);
660
+ combined.set(ct, iv.length);
661
+ return bytesToBase64(combined);
662
+ }
663
+ async function decrypt(encB64) {
664
+ if (!encryptionKey) throw new Error("Key not derived");
665
+ const combined = base64ToBytes(encB64);
666
+ const iv = combined.slice(0, IV_LEN);
667
+ const ciphertext = combined.slice(IV_LEN);
668
+ const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, encryptionKey, ciphertext);
669
+ return new TextDecoder().decode(decrypted);
670
+ }
671
+ function getTrackers(sessionId) {
672
+ const list = [];
673
+ try {
674
+ const signalerUrl = wsUrl || activeSignalerUrl;
675
+ const trackerUrl = signalerUrl.replace(/^http/, "ws").replace(/\/ws$/, "/announce");
676
+ if (trackerUrl) list.push(trackerUrl);
677
+ } catch {
678
+ }
679
+ list.push("wss://tracker.webtorrent.dev");
680
+ return list;
681
+ }
682
+ function writeStateFile() {
683
+ const statePath = path2.join(process.cwd(), ".srift-state.json");
684
+ const stateData = {
685
+ session: {
686
+ id: session.id,
687
+ name: session.name,
688
+ role: session.role,
689
+ isConnected: session.isConnected,
690
+ peerCount: session.role ? 1 : 0
691
+ // simplified peer calculation
692
+ },
693
+ activeTransfers: activeTransfers.map((t) => ({
694
+ fileId: t.fileId,
695
+ name: t.name,
696
+ size: t.size,
697
+ progress: parseFloat(t.progress.toFixed(2)),
698
+ speedKBps: parseFloat(t.speedKBps.toFixed(2)),
699
+ etaSeconds: Math.ceil(t.etaSeconds),
700
+ protocol: t.protocol,
701
+ status: t.status
702
+ })),
703
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
704
+ };
705
+ fs2.writeFileSync(statePath, JSON.stringify(stateData, null, 2), "utf-8");
706
+ }
707
+ function broadcastSSE(event, data) {
708
+ const payload = `event: ${event}
709
+ data: ${JSON.stringify(data)}
710
+
711
+ `;
712
+ sseClients.forEach((res) => res.write(payload));
713
+ }
714
+ function connectWebSocket(wsTargetUrl) {
715
+ if (wsConn) {
716
+ try {
717
+ wsConn.close();
718
+ } catch {
719
+ }
720
+ }
721
+ console.log(`[DAEMON] Connecting to Signaler WS: ${wsTargetUrl}`);
722
+ wsConn = new WebSocket(wsTargetUrl);
723
+ wsConn.on("open", () => {
724
+ console.log("[DAEMON] WebSocket connection open");
725
+ session.isConnected = true;
726
+ writeStateFile();
727
+ broadcastSSE("connection_state", {
728
+ sessionId: session.id,
729
+ isConnected: true,
730
+ signaling: "connected"
731
+ });
732
+ setTimeout(() => {
733
+ if (session.role === "host") {
734
+ wsConn?.send(JSON.stringify({
735
+ type: "init_host",
736
+ payload: { userId: session.userId, sessionId: session.id }
737
+ }));
738
+ } else if (session.role === "guest") {
739
+ if (session.userId) {
740
+ wsConn?.send(JSON.stringify({
741
+ type: "init_joiner",
742
+ payload: { userId: session.userId, sessionId: session.id }
743
+ }));
744
+ } else {
745
+ const username = process.env.SRIFT_USERNAME || "CLI-Agent";
746
+ wsConn?.send(JSON.stringify({
747
+ type: "request_join",
748
+ payload: { sessionId: session.id, tempUserId: session.tempUserId, username }
749
+ }));
750
+ }
751
+ }
752
+ }, 100);
753
+ lastHeartbeatAckAt = Date.now();
754
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
755
+ heartbeatInterval = setInterval(() => {
756
+ if (wsConn?.readyState === WebSocket.OPEN) {
757
+ wsConn.send(JSON.stringify({ type: "heartbeat", payload: { ts: Date.now() } }));
758
+ }
759
+ }, 3e4);
760
+ });
761
+ wsConn.on("message", async (dataStr) => {
762
+ try {
763
+ const msg = JSON.parse(dataStr);
764
+ const { type, payload } = msg;
765
+ switch (type) {
766
+ case "init_host_ack":
767
+ if (payload.status === "ok") {
768
+ csrfToken = payload.csrfToken;
769
+ console.log("[DAEMON] Host authentication OK, CSRF Token obtained");
770
+ reregisterPubsharesAfterReconnect().catch((e) => console.warn("[DAEMON] pubshare replay failed:", e?.message));
771
+ }
772
+ break;
773
+ case "request_join_ack":
774
+ console.log("[DAEMON] Join request received by server. Waiting for host approval...");
775
+ break;
776
+ case "join_approved":
777
+ console.log("[DAEMON] Host approved join request!");
778
+ session.userId = payload.userId;
779
+ wsConn?.send(JSON.stringify({
780
+ type: "init_joiner",
781
+ payload: { userId: session.userId, sessionId: session.id }
782
+ }));
783
+ writeStateFile();
784
+ break;
785
+ case "init_joiner_ack":
786
+ if (payload.status === "ok") {
787
+ csrfToken = payload.csrfToken;
788
+ console.log("[DAEMON] Approved joiner authentication OK");
789
+ }
790
+ break;
791
+ case "heartbeat_ack":
792
+ lastHeartbeatAckAt = Date.now();
793
+ break;
794
+ case "session_deleted":
795
+ console.log(`[DAEMON] Session deleted by host: ${payload.reason || ""}`);
796
+ sessionTerminated = true;
797
+ terminationReason = payload.message || "Session was deleted by the host";
798
+ session = { id: null, name: null, role: null, isConnected: false, userId: null };
799
+ activeTransfers = [];
800
+ pendingJoins = [];
801
+ encryptionKey = null;
802
+ writeStateFile();
803
+ broadcastSSE("session_terminated", { reason: terminationReason });
804
+ try {
805
+ if (wsConn) wsConn.close();
806
+ } catch {
807
+ }
808
+ break;
809
+ case "error":
810
+ console.error(`[DAEMON] Server error: ${payload.msg} (severity=${payload.severity || "recoverable"})`);
811
+ if (payload.severity === "critical") {
812
+ sessionTerminated = true;
813
+ terminationReason = payload.msg || "Connection terminated by host";
814
+ session.isConnected = false;
815
+ session.userId = null;
816
+ pendingJoins = [];
817
+ writeStateFile();
818
+ broadcastSSE("session_terminated", { reason: terminationReason });
819
+ }
820
+ break;
821
+ case "join_request":
822
+ console.log(`[DAEMON] Peer join request: tempUserId=${payload.tempUserId}, username=${payload.username}`);
823
+ pendingJoins.push({ tempUserId: payload.tempUserId, username: payload.username });
824
+ broadcastSSE("join_request", payload);
825
+ break;
826
+ case "chat":
827
+ try {
828
+ const decContent = await decrypt(payload.content);
829
+ const chatMsg = {
830
+ messageId: payload.messageId || uuidv4(),
831
+ sender: payload.username || "Peer",
832
+ content: decContent,
833
+ timestamp: payload.timestamp || (/* @__PURE__ */ new Date()).toISOString()
834
+ };
835
+ chatHistory.push(chatMsg);
836
+ broadcastSSE("chat_received", chatMsg);
837
+ console.log(`[DAEMON] Chat from ${chatMsg.sender}: ${chatMsg.content}`);
838
+ } catch (decErr) {
839
+ console.error("[DAEMON] Decryption failed for chat message:", decErr);
840
+ }
841
+ break;
842
+ case "file_offer":
843
+ console.log(`[DAEMON] File offer received: ${payload.filename} (${payload.size} bytes)`);
844
+ const newTx = {
845
+ fileId: payload.fileId,
846
+ name: payload.filename,
847
+ size: payload.size,
848
+ progress: 0,
849
+ speedKBps: 0,
850
+ etaSeconds: 0,
851
+ protocol: payload.transferType === "webtorrent" ? "webtorrent" : "websocket",
852
+ status: "pending",
853
+ direction: "download",
854
+ bytesTransferred: 0,
855
+ chunksCount: 0,
856
+ totalChunks: 0,
857
+ startTime: Date.now()
858
+ };
859
+ activeTransfers.push(newTx);
860
+ writeStateFile();
861
+ broadcastSSE("file_offer", {
862
+ fileId: payload.fileId,
863
+ filename: payload.filename,
864
+ size: payload.size,
865
+ from: payload.from,
866
+ protocol: newTx.protocol
867
+ });
868
+ break;
869
+ case "file_accept":
870
+ console.log(`[DAEMON] File offer accepted by peer! ID: ${payload.fileId}`);
871
+ startUpload(payload.fileId, payload.userId);
872
+ break;
873
+ case "webtorrent_info_hash":
874
+ handleWebTorrentInfoHash(payload);
875
+ break;
876
+ case "file_chunk":
877
+ handleIncomingChunk(payload);
878
+ break;
879
+ case "file_chunk_ack":
880
+ handleChunkAck(payload);
881
+ break;
882
+ case "file_complete":
883
+ handleFileComplete(payload);
884
+ break;
885
+ case "pubshare_register_ack": {
886
+ const entry = pubshares.get(payload.fileId);
887
+ if (entry) {
888
+ entry.token = payload.token;
889
+ entry.downloadUrl = payload.downloadUrl;
890
+ pubsharesByToken.set(payload.token, entry);
891
+ const resolver = pubshareRegResolvers.get(payload.fileId);
892
+ if (resolver) {
893
+ pubshareRegResolvers.delete(payload.fileId);
894
+ resolver(entry);
895
+ }
896
+ console.log(`[DAEMON] pubshare registered: ${entry.filename} -> ${entry.downloadUrl}`);
897
+ }
898
+ break;
899
+ }
900
+ case "pubshare_pull": {
901
+ handlePubsharePull(payload).catch((err) => {
902
+ console.error("[DAEMON] pubshare_pull failed:", err);
903
+ try {
904
+ wsConn?.send(JSON.stringify({
905
+ type: "pubshare_end",
906
+ payload: { requestId: payload.requestId, ok: false, error: String(err?.message || err) }
907
+ }));
908
+ } catch {
909
+ }
910
+ });
911
+ break;
912
+ }
913
+ case "pubshare_cancel": {
914
+ const a = activePulls.get(payload.requestId);
915
+ if (a) a.cancelled = true;
916
+ break;
917
+ }
918
+ }
919
+ } catch (err) {
920
+ console.error("[DAEMON] Error processing WS message:", err);
921
+ }
922
+ });
923
+ wsConn.on("close", () => {
924
+ console.log("[DAEMON] WebSocket connection closed");
925
+ session.isConnected = false;
926
+ writeStateFile();
927
+ broadcastSSE("connection_state", {
928
+ sessionId: session.id,
929
+ isConnected: false,
930
+ signaling: "disconnected"
931
+ });
932
+ if (sessionTerminated) {
933
+ console.log(`[DAEMON] Not reconnecting \u2014 session terminated: ${terminationReason}`);
934
+ return;
935
+ }
936
+ setTimeout(() => {
937
+ if (session.id && wsTargetUrl && !sessionTerminated) {
938
+ connectWebSocket(wsTargetUrl);
939
+ }
940
+ }, 3e3);
941
+ });
942
+ wsConn.on("error", (err) => {
943
+ console.error("[DAEMON] WebSocket error:", err);
944
+ });
945
+ }
946
+ function startUpload(fileId, targetUserId) {
947
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
948
+ if (!tx) return;
949
+ if (tx.protocol === "webtorrent") {
950
+ startWebTorrentUpload(fileId, targetUserId);
951
+ } else {
952
+ startWebSocketUpload(fileId, targetUserId);
953
+ }
954
+ }
955
+ async function startWebTorrentUpload(fileId, targetUserId) {
956
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
957
+ if (!tx || !tx.filePath) return;
958
+ console.log(`[DAEMON] Starting WebTorrent seeding for ${tx.name}...`);
959
+ tx.status = "uploading";
960
+ tx.startTime = Date.now();
961
+ writeStateFile();
962
+ let client;
963
+ try {
964
+ client = await getWtClient();
965
+ } catch (err) {
966
+ console.error(`[DAEMON] WebTorrent unavailable, falling back to WebSocket for ${tx.name}: ${err?.message}`);
967
+ tx.protocol = "websocket";
968
+ writeStateFile();
969
+ return;
970
+ }
971
+ const trackers = getTrackers(session.id || "");
972
+ client.seed(tx.filePath, { announce: trackers }, (torrent) => {
973
+ console.log(`[DAEMON] WebTorrent seeding active. Info Hash: ${torrent.infoHash}`);
974
+ activeTorrents.set(fileId, torrent);
975
+ wsConn?.send(JSON.stringify({
976
+ type: "webtorrent_info_hash",
977
+ payload: {
978
+ fileId,
979
+ infoHash: torrent.infoHash,
980
+ fileName: tx.name,
981
+ fileSize: tx.size,
982
+ targetUserId
983
+ }
984
+ }));
985
+ torrent.on("upload", () => {
986
+ tx.bytesTransferred = torrent.uploaded;
987
+ tx.progress = Math.min(99.9, torrent.uploaded / tx.size * 100);
988
+ tx.speedKBps = torrent.uploadSpeed / 1024;
989
+ writeStateFile();
990
+ broadcastSSE("transfer_progress", {
991
+ fileId,
992
+ fileName: tx.name,
993
+ size: tx.size,
994
+ bytesTransferred: torrent.uploaded,
995
+ progress: tx.progress,
996
+ speedBytesPerSecond: torrent.uploadSpeed,
997
+ timeRemainingSeconds: -1,
998
+ status: "uploading"
999
+ });
1000
+ });
1001
+ });
1002
+ }
1003
+ async function handleWebTorrentInfoHash(payload) {
1004
+ const { fileId, infoHash, fileName, fileSize, senderId } = payload;
1005
+ console.log(`[DAEMON] Received WebTorrent info hash: ${infoHash} for file: ${fileName}`);
1006
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
1007
+ if (!tx) return;
1008
+ tx.protocol = "webtorrent";
1009
+ tx.status = "downloading";
1010
+ tx.peerId = senderId;
1011
+ tx.startTime = Date.now();
1012
+ writeStateFile();
1013
+ let client;
1014
+ try {
1015
+ client = await getWtClient();
1016
+ } catch (err) {
1017
+ console.error(`[DAEMON] WebTorrent unavailable for download of ${fileName}: ${err?.message}. Ask the sender to retry with --protocol websocket.`);
1018
+ tx.status = "error";
1019
+ writeStateFile();
1020
+ broadcastSSE("transfer_progress", {
1021
+ fileId,
1022
+ fileName: tx.name,
1023
+ size: tx.size,
1024
+ bytesTransferred: 0,
1025
+ progress: 0,
1026
+ speedBytesPerSecond: 0,
1027
+ timeRemainingSeconds: -1,
1028
+ status: "error"
1029
+ });
1030
+ return;
1031
+ }
1032
+ const trackers = getTrackers(session.id || "");
1033
+ const downloadDir = tx.saveDir || process.cwd();
1034
+ client.add(infoHash, { announce: trackers, path: downloadDir }, (torrent) => {
1035
+ console.log(`[DAEMON] WebTorrent download started: ${torrent.name}`);
1036
+ activeTorrents.set(fileId, torrent);
1037
+ torrent.on("download", () => {
1038
+ tx.bytesTransferred = torrent.downloaded;
1039
+ tx.progress = torrent.progress * 100;
1040
+ tx.speedKBps = torrent.downloadSpeed / 1024;
1041
+ tx.etaSeconds = torrent.timeRemaining / 1e3;
1042
+ writeStateFile();
1043
+ broadcastSSE("transfer_progress", {
1044
+ fileId,
1045
+ fileName: tx.name,
1046
+ size: tx.size,
1047
+ bytesTransferred: torrent.downloaded,
1048
+ progress: tx.progress,
1049
+ speedBytesPerSecond: torrent.downloadSpeed,
1050
+ timeRemainingSeconds: tx.etaSeconds,
1051
+ status: "downloading"
1052
+ });
1053
+ });
1054
+ torrent.on("done", () => {
1055
+ console.log(`[DAEMON] WebTorrent download finished: ${tx.name}`);
1056
+ tx.status = "completed";
1057
+ tx.progress = 100;
1058
+ tx.speedKBps = 0;
1059
+ tx.etaSeconds = 0;
1060
+ writeStateFile();
1061
+ broadcastSSE("transfer_progress", {
1062
+ fileId,
1063
+ fileName: tx.name,
1064
+ size: tx.size,
1065
+ bytesTransferred: tx.size,
1066
+ progress: 100,
1067
+ speedBytesPerSecond: 0,
1068
+ timeRemainingSeconds: 0,
1069
+ status: "completed"
1070
+ });
1071
+ torrent.destroy();
1072
+ activeTorrents.delete(fileId);
1073
+ });
1074
+ });
1075
+ }
1076
+ async function startWebSocketUpload(fileId, targetUserId) {
1077
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
1078
+ if (!tx || !tx.filePath) return;
1079
+ const chunkSize = 64 * 1024;
1080
+ const stat = fs2.statSync(tx.filePath);
1081
+ const totalChunks = Math.max(1, Math.ceil(stat.size / chunkSize));
1082
+ tx.status = "uploading";
1083
+ tx.startTime = Date.now();
1084
+ tx.totalChunks = totalChunks;
1085
+ writeStateFile();
1086
+ activeUploads.set(fileId, {
1087
+ filePath: tx.filePath,
1088
+ totalChunks,
1089
+ chunkSize,
1090
+ targetUserId,
1091
+ currentChunk: 0,
1092
+ bytesSent: 0,
1093
+ startTime: Date.now()
1094
+ });
1095
+ sendNextChunk(fileId);
1096
+ }
1097
+ function sendNextChunk(fileId) {
1098
+ const upload = activeUploads.get(fileId);
1099
+ if (!upload) return;
1100
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
1101
+ if (!tx) return;
1102
+ if (upload.currentChunk >= upload.totalChunks) {
1103
+ wsConn?.send(JSON.stringify({
1104
+ type: "file_complete",
1105
+ payload: { fileId, targetUserId: upload.targetUserId }
1106
+ }));
1107
+ tx.status = "completed";
1108
+ tx.progress = 100;
1109
+ tx.speedKBps = 0;
1110
+ tx.etaSeconds = 0;
1111
+ writeStateFile();
1112
+ activeUploads.delete(fileId);
1113
+ console.log(`[DAEMON] File upload complete: ${tx.name}`);
1114
+ return;
1115
+ }
1116
+ const start = upload.currentChunk * upload.chunkSize;
1117
+ const stat = fs2.statSync(upload.filePath);
1118
+ const end = Math.min(start + upload.chunkSize, stat.size);
1119
+ const buffer = Buffer.alloc(end - start);
1120
+ const fd = fs2.openSync(upload.filePath, "r");
1121
+ fs2.readSync(fd, buffer, 0, end - start, start);
1122
+ fs2.closeSync(fd);
1123
+ const chunkBase64 = buffer.toString("base64");
1124
+ wsConn?.send(JSON.stringify({
1125
+ type: "file_chunk",
1126
+ payload: {
1127
+ fileId,
1128
+ chunk: chunkBase64,
1129
+ chunkIndex: upload.currentChunk,
1130
+ totalChunks: upload.totalChunks,
1131
+ targetUserId: upload.targetUserId
1132
+ }
1133
+ }));
1134
+ tx.bytesTransferred = end;
1135
+ tx.progress = end / stat.size * 100;
1136
+ const elapsed = (Date.now() - upload.startTime) / 1e3;
1137
+ if (elapsed > 0) {
1138
+ tx.speedKBps = end / 1024 / elapsed;
1139
+ tx.etaSeconds = (stat.size - end) / (tx.speedKBps * 1024);
1140
+ }
1141
+ writeStateFile();
1142
+ broadcastSSE("transfer_progress", {
1143
+ fileId,
1144
+ fileName: tx.name,
1145
+ size: tx.size,
1146
+ bytesTransferred: end,
1147
+ progress: tx.progress,
1148
+ speedBytesPerSecond: tx.speedKBps * 1024,
1149
+ timeRemainingSeconds: tx.etaSeconds,
1150
+ status: "uploading"
1151
+ });
1152
+ }
1153
+ function handleChunkAck(payload) {
1154
+ const { fileId, chunkIndex } = payload;
1155
+ const upload = activeUploads.get(fileId);
1156
+ if (!upload) return;
1157
+ if (upload.currentChunk === chunkIndex) {
1158
+ upload.currentChunk++;
1159
+ sendNextChunk(fileId);
1160
+ }
1161
+ }
1162
+ function handleIncomingChunk(payload) {
1163
+ const { fileId, chunk, chunkIndex, totalChunks, from } = payload;
1164
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
1165
+ if (!tx) return;
1166
+ tx.status = "downloading";
1167
+ tx.totalChunks = totalChunks;
1168
+ tx.peerId = from;
1169
+ const chunkBuffer = Buffer.from(chunk, "base64");
1170
+ tx.bytesTransferred += chunkBuffer.length;
1171
+ tx.chunksCount++;
1172
+ const chunkPath = path2.join(TEMP_DIR, `${fileId}_chunk_${chunkIndex}`);
1173
+ fs2.writeFileSync(chunkPath, chunkBuffer);
1174
+ tx.progress = tx.bytesTransferred / tx.size * 100;
1175
+ const elapsed = (Date.now() - tx.startTime) / 1e3;
1176
+ if (elapsed > 0) {
1177
+ tx.speedKBps = tx.bytesTransferred / 1024 / elapsed;
1178
+ tx.etaSeconds = (tx.size - tx.bytesTransferred) / (tx.speedKBps * 1024);
1179
+ }
1180
+ writeStateFile();
1181
+ broadcastSSE("transfer_progress", {
1182
+ fileId,
1183
+ fileName: tx.name,
1184
+ size: tx.size,
1185
+ bytesTransferred: tx.bytesTransferred,
1186
+ progress: tx.progress,
1187
+ speedBytesPerSecond: tx.speedKBps * 1024,
1188
+ timeRemainingSeconds: tx.etaSeconds,
1189
+ status: "downloading"
1190
+ });
1191
+ wsConn?.send(JSON.stringify({
1192
+ type: "file_chunk_ack",
1193
+ payload: { fileId, chunkIndex, status: "received" }
1194
+ }));
1195
+ }
1196
+ function handleFileComplete(payload) {
1197
+ const { fileId } = payload;
1198
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
1199
+ if (!tx) return;
1200
+ const destDir = tx.saveDir || process.cwd();
1201
+ const destPath = path2.join(destDir, tx.name);
1202
+ try {
1203
+ if (!fs2.existsSync(destDir)) {
1204
+ fs2.mkdirSync(destDir, { recursive: true });
1205
+ }
1206
+ } catch (err) {
1207
+ console.error(`[DAEMON] Failed to create destination directory ${destDir}:`, err);
1208
+ tx.status = "error";
1209
+ writeStateFile();
1210
+ return;
1211
+ }
1212
+ const writeStream = fs2.createWriteStream(destPath);
1213
+ writeStream.on("error", (err) => {
1214
+ console.error(`[DAEMON] Write stream error for ${destPath}:`, err);
1215
+ tx.status = "error";
1216
+ writeStateFile();
1217
+ broadcastSSE("transfer_progress", {
1218
+ fileId,
1219
+ fileName: tx.name,
1220
+ size: tx.size,
1221
+ bytesTransferred: tx.bytesTransferred,
1222
+ progress: tx.progress,
1223
+ speedBytesPerSecond: 0,
1224
+ timeRemainingSeconds: 0,
1225
+ status: "error"
1226
+ });
1227
+ });
1228
+ writeStream.on("finish", () => {
1229
+ console.log(`[DAEMON] File successfully downloaded and assembled: ${destPath}`);
1230
+ tx.status = "completed";
1231
+ tx.progress = 100;
1232
+ tx.speedKBps = 0;
1233
+ tx.etaSeconds = 0;
1234
+ writeStateFile();
1235
+ broadcastSSE("transfer_progress", {
1236
+ fileId,
1237
+ fileName: tx.name,
1238
+ size: tx.size,
1239
+ bytesTransferred: tx.size,
1240
+ progress: 100,
1241
+ speedBytesPerSecond: 0,
1242
+ timeRemainingSeconds: 0,
1243
+ status: "completed"
1244
+ });
1245
+ });
1246
+ for (let i = 0; i < tx.totalChunks; i++) {
1247
+ const chunkPath = path2.join(TEMP_DIR, `${fileId}_chunk_${i}`);
1248
+ if (fs2.existsSync(chunkPath)) {
1249
+ try {
1250
+ const chunkBuf = fs2.readFileSync(chunkPath);
1251
+ writeStream.write(chunkBuf);
1252
+ fs2.unlinkSync(chunkPath);
1253
+ } catch (err) {
1254
+ console.error(`[DAEMON] Error reading/writing chunk ${i}:`, err);
1255
+ writeStream.emit("error", err);
1256
+ return;
1257
+ }
1258
+ }
1259
+ }
1260
+ writeStream.end();
1261
+ }
1262
+ async function registerPubshare(fileId, absPath, filename, size, mime, opts = {}, timeoutMs = 5e3) {
1263
+ if (!wsConn || wsConn.readyState !== WebSocket.OPEN) {
1264
+ throw new Error("Signaling WebSocket not connected");
1265
+ }
1266
+ const maxDownloads = Math.max(0, Math.floor(opts.maxDownloads || 0));
1267
+ const expiresAt = opts.ttlMs && opts.ttlMs > 0 ? Date.now() + opts.ttlMs : null;
1268
+ const entry = {
1269
+ token: opts.preferToken || null,
1270
+ filePath: absPath,
1271
+ filename,
1272
+ size,
1273
+ mime,
1274
+ fileId,
1275
+ downloadUrl: null,
1276
+ maxDownloads,
1277
+ expiresAt,
1278
+ downloadCount: 0,
1279
+ createdAt: Date.now()
1280
+ };
1281
+ pubshares.set(fileId, entry);
1282
+ const ackP = new Promise((resolve, reject) => {
1283
+ pubshareRegResolvers.set(fileId, resolve);
1284
+ setTimeout(() => {
1285
+ if (pubshareRegResolvers.has(fileId)) {
1286
+ pubshareRegResolvers.delete(fileId);
1287
+ pubshares.delete(fileId);
1288
+ reject(new Error(
1289
+ "Signaler does not recognise pubshare_register \u2014 likely running an older server build. Until srift.app picks up the new release, the recipient must join the session in a browser."
1290
+ ));
1291
+ }
1292
+ }, timeoutMs);
1293
+ });
1294
+ wsConn.send(JSON.stringify({
1295
+ type: "pubshare_register",
1296
+ payload: {
1297
+ fileId,
1298
+ filename,
1299
+ size,
1300
+ mime,
1301
+ maxDownloads,
1302
+ expiresAt,
1303
+ preferToken: opts.preferToken || void 0
1304
+ }
1305
+ }));
1306
+ return ackP;
1307
+ }
1308
+ async function reregisterPubsharesAfterReconnect() {
1309
+ if (!pubshares.size) return;
1310
+ console.log(`[DAEMON] Re-registering ${pubshares.size} pubshare(s) after WS reconnect\u2026`);
1311
+ for (const entry of Array.from(pubshares.values())) {
1312
+ if (!entry.token) continue;
1313
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
1314
+ pubshares.delete(entry.fileId);
1315
+ pubsharesByToken.delete(entry.token);
1316
+ continue;
1317
+ }
1318
+ if (entry.maxDownloads && entry.downloadCount >= entry.maxDownloads) {
1319
+ pubshares.delete(entry.fileId);
1320
+ pubsharesByToken.delete(entry.token);
1321
+ continue;
1322
+ }
1323
+ try {
1324
+ const remainingTtl = entry.expiresAt ? Math.max(0, entry.expiresAt - Date.now()) : 0;
1325
+ const remainingMax = entry.maxDownloads ? Math.max(1, entry.maxDownloads - entry.downloadCount) : 0;
1326
+ await registerPubshare(
1327
+ entry.fileId,
1328
+ entry.filePath,
1329
+ entry.filename,
1330
+ entry.size,
1331
+ entry.mime,
1332
+ { maxDownloads: remainingMax, ttlMs: remainingTtl, preferToken: entry.token }
1333
+ );
1334
+ } catch (e) {
1335
+ console.warn(`[DAEMON] Failed to re-register pubshare ${entry.fileId}: ${e?.message}`);
1336
+ }
1337
+ }
1338
+ }
1339
+ async function handlePubsharePull(payload) {
1340
+ const { requestId, token, start, end, isFullDownload } = payload || {};
1341
+ if (!requestId || !token) return;
1342
+ const entry = pubsharesByToken.get(token);
1343
+ if (!entry) {
1344
+ wsConn?.send(JSON.stringify({
1345
+ type: "pubshare_end",
1346
+ payload: { requestId, ok: false, error: "unknown token" }
1347
+ }));
1348
+ return;
1349
+ }
1350
+ const cancelFlag = { cancelled: false };
1351
+ activePulls.set(requestId, cancelFlag);
1352
+ const CHUNK = 64 * 1024;
1353
+ let pos = start;
1354
+ let seq = 0;
1355
+ let completedOk = false;
1356
+ const fd = fs2.openSync(entry.filePath, "r");
1357
+ try {
1358
+ while (pos <= end) {
1359
+ if (cancelFlag.cancelled) break;
1360
+ if (!wsConn || wsConn.readyState !== WebSocket.OPEN) {
1361
+ throw new Error("Signaling WebSocket disconnected mid-stream");
1362
+ }
1363
+ const len = Math.min(CHUNK, end - pos + 1);
1364
+ const buf = Buffer.allocUnsafe(len);
1365
+ fs2.readSync(fd, buf, 0, len, pos);
1366
+ const dataB64 = buf.toString("base64");
1367
+ wsConn.send(JSON.stringify({
1368
+ type: "pubshare_chunk",
1369
+ payload: { requestId, seq, dataB64 }
1370
+ }));
1371
+ pos += len;
1372
+ seq++;
1373
+ if (wsConn.bufferedAmount > 8 * 1024 * 1024) {
1374
+ await new Promise((r) => setTimeout(r, 20));
1375
+ }
1376
+ }
1377
+ if (!cancelFlag.cancelled) {
1378
+ wsConn?.send(JSON.stringify({
1379
+ type: "pubshare_end",
1380
+ payload: { requestId, ok: true }
1381
+ }));
1382
+ completedOk = true;
1383
+ }
1384
+ } finally {
1385
+ try {
1386
+ fs2.closeSync(fd);
1387
+ } catch {
1388
+ }
1389
+ activePulls.delete(requestId);
1390
+ }
1391
+ if (completedOk && isFullDownload) {
1392
+ entry.downloadCount++;
1393
+ broadcastSSE("pubshare_download", {
1394
+ token: entry.token,
1395
+ fileId: entry.fileId,
1396
+ filename: entry.filename,
1397
+ downloadCount: entry.downloadCount,
1398
+ maxDownloads: entry.maxDownloads
1399
+ });
1400
+ if (entry.maxDownloads && entry.downloadCount >= entry.maxDownloads) {
1401
+ console.log(`[DAEMON] pubshare exhausted (max ${entry.maxDownloads} reached): ${entry.filename}`);
1402
+ }
1403
+ }
1404
+ }
1405
+ var _WebTorrentCtor, _webTorrentLoadError, logPath, logStream, logMessage, crypto, PORT, DEFAULT_SIGNALER_URL, activeSignalerUrl, 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, activeUploads, app, DAEMON_START_TIME, PACKAGE_VERSION;
1406
+ var init_daemon = __esm({
1407
+ "../../cli/daemon.ts"() {
1408
+ "use strict";
1409
+ init_mcp();
1410
+ _WebTorrentCtor = null;
1411
+ _webTorrentLoadError = null;
1412
+ logPath = path2.join(process.cwd(), ".srift-daemon.log");
1413
+ logStream = fs2.createWriteStream(logPath, { flags: "a" });
1414
+ logMessage = (level, message) => {
1415
+ logStream.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${message}
1416
+ `);
1417
+ };
1418
+ console.log = (...args) => {
1419
+ logMessage("INFO", args.map((arg) => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" "));
1420
+ };
1421
+ console.error = (...args) => {
1422
+ logMessage("ERROR", args.map((arg) => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" "));
1423
+ };
1424
+ console.warn = (...args) => {
1425
+ logMessage("WARN", args.map((arg) => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" "));
1426
+ };
1427
+ crypto = globalThis.crypto || webcrypto;
1428
+ dotenv.config({ path: ".env.local" });
1429
+ dotenv.config();
1430
+ process.on("uncaughtException", (err) => {
1431
+ console.error("[DAEMON] Uncaught Exception:", err);
1432
+ });
1433
+ process.on("unhandledRejection", (reason, promise) => {
1434
+ console.error("[DAEMON] Unhandled Rejection at:", promise, "reason:", reason);
1435
+ });
1436
+ PORT = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
1437
+ DEFAULT_SIGNALER_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8080";
1438
+ activeSignalerUrl = DEFAULT_SIGNALER_URL;
1439
+ session = {
1440
+ id: null,
1441
+ name: null,
1442
+ role: null,
1443
+ isConnected: false,
1444
+ userId: null
1445
+ };
1446
+ activeTransfers = [];
1447
+ chatHistory = [];
1448
+ pendingJoins = [];
1449
+ sseClients = [];
1450
+ wsConn = null;
1451
+ wsUrl = null;
1452
+ csrfToken = null;
1453
+ heartbeatInterval = null;
1454
+ lastHeartbeatAckAt = 0;
1455
+ encryptionKey = null;
1456
+ sessionTerminated = false;
1457
+ terminationReason = null;
1458
+ activeTorrents = /* @__PURE__ */ new Map();
1459
+ wtClient = null;
1460
+ pubshares = /* @__PURE__ */ new Map();
1461
+ pubsharesByToken = /* @__PURE__ */ new Map();
1462
+ activePulls = /* @__PURE__ */ new Map();
1463
+ pubshareRegResolvers = /* @__PURE__ */ new Map();
1464
+ TEMP_DIR = path2.join(process.cwd(), ".srift-temp");
1465
+ if (!fs2.existsSync(TEMP_DIR)) {
1466
+ fs2.mkdirSync(TEMP_DIR, { recursive: true });
1467
+ }
1468
+ KDF_ITERATIONS = 1e5;
1469
+ IV_LEN = 12;
1470
+ activeUploads = /* @__PURE__ */ new Map();
1471
+ app = express();
1472
+ app.use(cors());
1473
+ app.use(express.json());
1474
+ DAEMON_START_TIME = Date.now();
1475
+ PACKAGE_VERSION = "2.1.8";
1476
+ app.get("/health", (req, res) => {
1477
+ res.json({
1478
+ ok: true,
1479
+ version: PACKAGE_VERSION,
1480
+ uptime_ms: Date.now() - DAEMON_START_TIME,
1481
+ mcp: true,
1482
+ webrtc: false,
1483
+ // daemon uses WebSocket relay + WebTorrent, not raw WebRTC
1484
+ webtorrent: _webTorrentLoadError ? false : true,
1485
+ webtorrent_error: _webTorrentLoadError || void 0
1486
+ });
1487
+ });
1488
+ app.get("/status", (req, res) => {
1489
+ res.json({
1490
+ session: {
1491
+ id: session.id,
1492
+ name: session.name,
1493
+ role: session.role,
1494
+ isConnected: session.isConnected,
1495
+ userId: session.userId
1496
+ },
1497
+ activeTransfers,
1498
+ pendingJoins,
1499
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1500
+ });
1501
+ });
1502
+ app.post("/session/start", async (req, res) => {
1503
+ try {
1504
+ const { sessionName, username, roomSecret } = req.body;
1505
+ const sName = sessionName || "cli-session";
1506
+ const uName = username || "CLI-Host";
1507
+ sessionTerminated = false;
1508
+ terminationReason = null;
1509
+ console.log(`[DAEMON] Starting session: "${sName}" by "${uName}"`);
1510
+ const response = await fetch(`${activeSignalerUrl}/create-session`, {
1511
+ method: "POST",
1512
+ headers: { "Content-Type": "application/json" },
1513
+ body: JSON.stringify({ username: uName, name: sName })
1514
+ });
1515
+ if (!response.ok) {
1516
+ throw new Error(`Signaler returned status ${response.status}`);
1517
+ }
1518
+ const data = await response.json();
1519
+ console.log("[DAEMON] Session created successfully on server", data);
1520
+ session = {
1521
+ id: data.sessionId,
1522
+ name: data.name,
1523
+ role: "host",
1524
+ isConnected: false,
1525
+ userId: data.userId,
1526
+ wsToken: data.wsToken,
1527
+ roomSecret: roomSecret || null
1528
+ };
1529
+ encryptionKey = await deriveKey(data.sessionId, roomSecret);
1530
+ console.log("[DAEMON] E2EE Cryptographic key derived");
1531
+ wsUrl = data.wsUrl || `${activeSignalerUrl.replace(/^http/, "ws")}/ws`;
1532
+ if (wsUrl) connectWebSocket(wsUrl);
1533
+ writeStateFile();
1534
+ res.json({ success: true, sessionId: data.sessionId });
1535
+ } catch (err) {
1536
+ console.error("[DAEMON] Failed to start session:", err);
1537
+ res.status(500).json({ success: false, error: err.message });
1538
+ }
1539
+ });
1540
+ app.post("/session/join", async (req, res) => {
1541
+ try {
1542
+ const { sessionId, username, roomSecret } = req.body;
1543
+ if (!sessionId) {
1544
+ return res.status(400).json({ success: false, error: "sessionId required" });
1545
+ }
1546
+ sessionTerminated = false;
1547
+ terminationReason = null;
1548
+ const uName = username || "CLI-Guest";
1549
+ console.log(`[DAEMON] Joining session: "${sessionId}" as "${uName}"`);
1550
+ const response = await fetch(`${activeSignalerUrl}/join-session`, {
1551
+ method: "POST",
1552
+ headers: { "Content-Type": "application/json" },
1553
+ body: JSON.stringify({ sessionId, username: uName })
1554
+ });
1555
+ if (!response.ok) {
1556
+ let errBody = {};
1557
+ try {
1558
+ errBody = await response.json();
1559
+ } catch {
1560
+ }
1561
+ const errMsg = errBody.error || `Signaler returned status ${response.status}`;
1562
+ const statusCode = response.status >= 400 && response.status < 500 ? response.status : 502;
1563
+ return res.status(statusCode).json({ success: false, error: errMsg });
1564
+ }
1565
+ const data = await response.json();
1566
+ console.log("[DAEMON] Join initialized on server", data);
1567
+ session = {
1568
+ id: data.sessionId,
1569
+ name: data.name,
1570
+ role: "guest",
1571
+ isConnected: false,
1572
+ userId: null,
1573
+ tempUserId: data.tempUserId,
1574
+ wsToken: data.wsToken,
1575
+ roomSecret: roomSecret || null
1576
+ };
1577
+ encryptionKey = await deriveKey(data.sessionId, roomSecret);
1578
+ console.log("[DAEMON] E2EE Cryptographic key derived");
1579
+ wsUrl = data.wsUrl || `${activeSignalerUrl.replace(/^http/, "ws")}/ws`;
1580
+ if (wsUrl) connectWebSocket(wsUrl);
1581
+ writeStateFile();
1582
+ res.json({ success: true, sessionId: data.sessionId });
1583
+ } catch (err) {
1584
+ console.error("[DAEMON] Failed to join session:", err);
1585
+ res.status(500).json({ success: false, error: err.message });
1586
+ }
1587
+ });
1588
+ app.post("/session/approve", (req, res) => {
1589
+ const { tempUserId } = req.body;
1590
+ if (!tempUserId) {
1591
+ return res.status(400).json({ success: false, error: "tempUserId required" });
1592
+ }
1593
+ if (session.role !== "host") {
1594
+ return res.status(403).json({ success: false, error: "Only host can approve joins" });
1595
+ }
1596
+ const pending = pendingJoins.find((j) => j.tempUserId === tempUserId);
1597
+ if (!pending) {
1598
+ return res.status(404).json({ success: false, error: `No pending join request found for tempUserId: ${tempUserId}` });
1599
+ }
1600
+ console.log(`[DAEMON] Approving join request: ${tempUserId}`);
1601
+ wsConn?.send(JSON.stringify({
1602
+ type: "approve_join",
1603
+ payload: { sessionId: session.id, tempUserId }
1604
+ }));
1605
+ pendingJoins = pendingJoins.filter((j) => j.tempUserId !== tempUserId);
1606
+ res.json({ success: true });
1607
+ });
1608
+ app.post("/session/reject", (req, res) => {
1609
+ const { tempUserId, reason } = req.body;
1610
+ if (!tempUserId) {
1611
+ return res.status(400).json({ success: false, error: "tempUserId required" });
1612
+ }
1613
+ if (session.role !== "host") {
1614
+ return res.status(403).json({ success: false, error: "Only host can reject joins" });
1615
+ }
1616
+ console.log(`[DAEMON] Rejecting join request: ${tempUserId}`);
1617
+ wsConn?.send(JSON.stringify({
1618
+ type: "reject_join",
1619
+ payload: { tempUserId, reason: reason || "Kicked by host CLI" }
1620
+ }));
1621
+ pendingJoins = pendingJoins.filter((j) => j.tempUserId !== tempUserId);
1622
+ res.json({ success: true });
1623
+ });
1624
+ app.post("/session/kick", (req, res) => {
1625
+ const { userId } = req.body;
1626
+ if (!userId) {
1627
+ return res.status(400).json({ success: false, error: "userId required" });
1628
+ }
1629
+ if (session.role !== "host") {
1630
+ return res.status(403).json({ success: false, error: "Only host can kick users" });
1631
+ }
1632
+ console.log(`[DAEMON] Kicking user: ${userId}`);
1633
+ wsConn?.send(JSON.stringify({
1634
+ type: "kick",
1635
+ payload: { targetId: userId }
1636
+ }));
1637
+ res.json({ success: true });
1638
+ });
1639
+ app.post("/session/close", (req, res) => {
1640
+ console.log("[DAEMON] Closing active session \u2014 full teardown");
1641
+ sessionTerminated = true;
1642
+ try {
1643
+ console.log(`[DAEMON] close: role=${session.role} id=${session.id} wsState=${wsConn ? wsConn.readyState : "no-ws"}`);
1644
+ if (wsConn && wsConn.readyState === 1 && session.id) {
1645
+ if (session.role === "host") {
1646
+ wsConn.send(JSON.stringify({ type: "delete_session", payload: { sessionId: session.id } }));
1647
+ console.log("[DAEMON] close: sent delete_session");
1648
+ } else {
1649
+ wsConn.send(JSON.stringify({ type: "leave_session", payload: { sessionId: session.id, userId: session.userId } }));
1650
+ console.log("[DAEMON] close: sent leave_session");
1651
+ }
1652
+ }
1653
+ } catch (e) {
1654
+ console.error("[DAEMON] close: send failed", e?.message);
1655
+ }
1656
+ if (wsConn) {
1657
+ const _ws = wsConn;
1658
+ setTimeout(() => {
1659
+ try {
1660
+ _ws.close();
1661
+ } catch {
1662
+ }
1663
+ }, 700);
1664
+ wsConn = null;
1665
+ }
1666
+ wsUrl = null;
1667
+ if (heartbeatInterval) {
1668
+ clearInterval(heartbeatInterval);
1669
+ heartbeatInterval = null;
1670
+ }
1671
+ if (wtClient) {
1672
+ try {
1673
+ wtClient.destroy();
1674
+ } catch {
1675
+ }
1676
+ wtClient = null;
1677
+ }
1678
+ activeTorrents.clear();
1679
+ session = { id: null, name: null, role: null, isConnected: false, userId: null };
1680
+ activeTransfers = [];
1681
+ chatHistory = [];
1682
+ pendingJoins = [];
1683
+ csrfToken = null;
1684
+ encryptionKey = null;
1685
+ try {
1686
+ if (fs2.existsSync(TEMP_DIR)) {
1687
+ for (const f of fs2.readdirSync(TEMP_DIR)) {
1688
+ try {
1689
+ fs2.unlinkSync(path2.join(TEMP_DIR, f));
1690
+ } catch {
1691
+ }
1692
+ }
1693
+ }
1694
+ } catch {
1695
+ }
1696
+ writeStateFile();
1697
+ res.json({ success: true });
1698
+ });
1699
+ app.post("/send", async (req, res) => {
1700
+ try {
1701
+ const { filePath, protocol } = req.body;
1702
+ if (!filePath) {
1703
+ return res.status(400).json({ success: false, error: "filePath is required" });
1704
+ }
1705
+ const absPath = path2.resolve(filePath);
1706
+ if (!fs2.existsSync(absPath)) {
1707
+ return res.status(404).json({ success: false, error: `File not found: ${absPath}` });
1708
+ }
1709
+ const stat = fs2.statSync(absPath);
1710
+ const filename = path2.basename(absPath);
1711
+ const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1712
+ const targetProtocol = protocol === "webtorrent" || stat.size > 10 * 1024 * 1024 && protocol !== "websocket" ? "webtorrent" : "websocket";
1713
+ const newTx = {
1714
+ fileId,
1715
+ name: filename,
1716
+ size: stat.size,
1717
+ progress: 0,
1718
+ speedKBps: 0,
1719
+ etaSeconds: 0,
1720
+ protocol: targetProtocol,
1721
+ status: "pending",
1722
+ direction: "upload",
1723
+ filePath: absPath,
1724
+ bytesTransferred: 0,
1725
+ chunksCount: 0,
1726
+ totalChunks: 0,
1727
+ startTime: Date.now()
1728
+ };
1729
+ activeTransfers.push(newTx);
1730
+ writeStateFile();
1731
+ console.log(`[DAEMON] Offering file via ${targetProtocol}: ${filename} (${stat.size} bytes)`);
1732
+ wsConn?.send(JSON.stringify({
1733
+ type: "file_offer",
1734
+ payload: {
1735
+ fileId,
1736
+ filename,
1737
+ size: stat.size,
1738
+ mime: "application/octet-stream",
1739
+ transferType: targetProtocol,
1740
+ csrfToken
1741
+ }
1742
+ }));
1743
+ res.json({ success: true, fileId, protocol: targetProtocol });
1744
+ } catch (err) {
1745
+ console.error("[DAEMON] Send offer failed:", err);
1746
+ res.status(500).json({ success: false, error: err.message });
1747
+ }
1748
+ });
1749
+ app.post("/receive", (req, res) => {
1750
+ const { fileId, saveDir } = req.body;
1751
+ if (!fileId) {
1752
+ return res.status(400).json({ success: false, error: "fileId is required" });
1753
+ }
1754
+ const tx = activeTransfers.find((t) => t.fileId === fileId);
1755
+ if (!tx) {
1756
+ return res.status(404).json({ success: false, error: "No offer found for this fileId" });
1757
+ }
1758
+ tx.saveDir = saveDir ? path2.resolve(saveDir) : process.cwd();
1759
+ tx.status = "downloading";
1760
+ tx.startTime = Date.now();
1761
+ console.log(`[DAEMON] Accepting file offer ${fileId}, saving to ${tx.saveDir}`);
1762
+ wsConn?.send(JSON.stringify({
1763
+ type: "file_accept",
1764
+ payload: { fileId }
1765
+ }));
1766
+ res.json({ success: true });
1767
+ });
1768
+ app.post("/chat/send", async (req, res) => {
1769
+ try {
1770
+ const { message } = req.body;
1771
+ if (!message) {
1772
+ return res.status(400).json({ success: false, error: "message required" });
1773
+ }
1774
+ if (!session.id) {
1775
+ return res.status(409).json({ success: false, error: "No active session. Call /session/start first." });
1776
+ }
1777
+ if (!encryptionKey) {
1778
+ return res.status(409).json({ success: false, error: "Encryption key not ready. Session still initialising." });
1779
+ }
1780
+ if (!wsConn || wsConn.readyState !== 1) {
1781
+ return res.status(503).json({
1782
+ success: false,
1783
+ error: "Signaling connection not ready (WebSocket is connecting). Retry in 1\u20132 seconds.",
1784
+ retryAfterMs: 1500
1785
+ });
1786
+ }
1787
+ const encContent = await encrypt(message);
1788
+ wsConn.send(JSON.stringify({
1789
+ type: "chat",
1790
+ payload: { content: encContent, encrypted: true }
1791
+ }));
1792
+ const chatMsg = {
1793
+ messageId: uuidv4(),
1794
+ sender: "Me",
1795
+ content: message,
1796
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1797
+ };
1798
+ chatHistory.push(chatMsg);
1799
+ res.json({ success: true });
1800
+ } catch (err) {
1801
+ res.status(500).json({ success: false, error: err.message });
1802
+ }
1803
+ });
1804
+ app.get("/chat/history", (req, res) => {
1805
+ res.json(chatHistory);
1806
+ });
1807
+ app.get("/state", (req, res) => {
1808
+ const statePath = path2.join(process.cwd(), ".srift-state.json");
1809
+ try {
1810
+ if (fs2.existsSync(statePath)) {
1811
+ const raw = fs2.readFileSync(statePath, "utf-8");
1812
+ res.type("application/json").send(raw);
1813
+ } else {
1814
+ res.json({ session: { id: null, isConnected: false }, activeTransfers: [], lastUpdated: null });
1815
+ }
1816
+ } catch (err) {
1817
+ res.status(500).json({ error: err.message });
1818
+ }
1819
+ });
1820
+ app.post("/quick-share", async (req, res) => {
1821
+ try {
1822
+ const { filePath, sessionName, maxDownloads, ttlMs } = req.body;
1823
+ if (!filePath) return res.status(400).json({ success: false, error: "filePath required" });
1824
+ if (!session.id) {
1825
+ const sName = sessionName || "AI-QuickShare";
1826
+ const createRes = await fetch(`${activeSignalerUrl}/create-session`, {
1827
+ method: "POST",
1828
+ headers: { "Content-Type": "application/json" },
1829
+ body: JSON.stringify({ username: "AI-Agent", name: sName })
1830
+ });
1831
+ if (!createRes.ok) throw new Error(`Signaler returned ${createRes.status}`);
1832
+ const data = await createRes.json();
1833
+ session = {
1834
+ id: data.sessionId,
1835
+ name: data.name,
1836
+ role: "host",
1837
+ isConnected: false,
1838
+ userId: data.userId,
1839
+ wsToken: data.wsToken,
1840
+ roomSecret: null
1841
+ };
1842
+ encryptionKey = await deriveKey(data.sessionId);
1843
+ wsUrl = data.wsUrl || `${activeSignalerUrl.replace(/^http/, "ws")}/ws`;
1844
+ if (wsUrl) connectWebSocket(wsUrl);
1845
+ writeStateFile();
1846
+ await new Promise((r) => setTimeout(r, 800));
1847
+ }
1848
+ const absPath = path2.resolve(filePath);
1849
+ if (!fs2.existsSync(absPath)) return res.status(404).json({ success: false, error: `File not found: ${absPath}` });
1850
+ const stat = fs2.statSync(absPath);
1851
+ const filename = path2.basename(absPath);
1852
+ const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1853
+ const targetProtocol = stat.size > 10 * 1024 * 1024 ? "webtorrent" : "websocket";
1854
+ activeTransfers.push({
1855
+ fileId,
1856
+ name: filename,
1857
+ size: stat.size,
1858
+ progress: 0,
1859
+ speedKBps: 0,
1860
+ etaSeconds: 0,
1861
+ protocol: targetProtocol,
1862
+ status: "pending",
1863
+ direction: "upload",
1864
+ filePath: absPath,
1865
+ bytesTransferred: 0,
1866
+ chunksCount: 0,
1867
+ totalChunks: 0,
1868
+ startTime: Date.now()
1869
+ });
1870
+ writeStateFile();
1871
+ wsConn?.send(JSON.stringify({
1872
+ type: "file_offer",
1873
+ payload: { fileId, filename, size: stat.size, mime: "application/octet-stream", transferType: targetProtocol, csrfToken }
1874
+ }));
1875
+ let downloadUrl = null;
1876
+ let expiresAt = null;
1877
+ let cappedMaxDownloads = 0;
1878
+ try {
1879
+ const entry = await registerPubshare(
1880
+ fileId,
1881
+ absPath,
1882
+ filename,
1883
+ stat.size,
1884
+ "application/octet-stream",
1885
+ {
1886
+ maxDownloads: typeof maxDownloads === "number" ? maxDownloads : 0,
1887
+ ttlMs: typeof ttlMs === "number" ? ttlMs : 0
1888
+ }
1889
+ );
1890
+ downloadUrl = entry.downloadUrl;
1891
+ expiresAt = entry.expiresAt;
1892
+ cappedMaxDownloads = entry.maxDownloads;
1893
+ } catch (e) {
1894
+ console.warn("[DAEMON] pubshare registration failed (link unavailable):", e?.message);
1895
+ }
1896
+ const publicBase = process.env.SRIFT_PUBLIC_BASE || "https://srift.app";
1897
+ res.json({
1898
+ success: true,
1899
+ sessionId: session.id,
1900
+ fileId,
1901
+ downloadUrl,
1902
+ // ← preferred (zero-install)
1903
+ shareUrl: downloadUrl || `${publicBase}/join-session?id=${session.id}`,
1904
+ // back-compat
1905
+ fileName: filename,
1906
+ fileSize: stat.size,
1907
+ maxDownloads: cappedMaxDownloads,
1908
+ // 0 = unlimited
1909
+ expiresAt
1910
+ // epoch ms or null
1911
+ });
1912
+ } catch (err) {
1913
+ console.error("[DAEMON] /quick-share failed:", err);
1914
+ res.status(500).json({ success: false, error: err.message });
1915
+ }
1916
+ });
1917
+ app.post("/pubshare", async (req, res) => {
1918
+ try {
1919
+ const { filePath, maxDownloads, ttlMs } = req.body;
1920
+ if (!filePath) return res.status(400).json({ success: false, error: "filePath required" });
1921
+ if (!session.id || !session.isConnected) {
1922
+ return res.status(409).json({ success: false, error: "No active session \u2014 run `srift session start` or `srift quick-share` first" });
1923
+ }
1924
+ const absPath = path2.resolve(filePath);
1925
+ if (!fs2.existsSync(absPath)) return res.status(404).json({ success: false, error: `File not found: ${absPath}` });
1926
+ const stat = fs2.statSync(absPath);
1927
+ const filename = path2.basename(absPath);
1928
+ const fileId = `cli_file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1929
+ const entry = await registerPubshare(
1930
+ fileId,
1931
+ absPath,
1932
+ filename,
1933
+ stat.size,
1934
+ "application/octet-stream",
1935
+ {
1936
+ maxDownloads: typeof maxDownloads === "number" ? maxDownloads : 0,
1937
+ ttlMs: typeof ttlMs === "number" ? ttlMs : 0
1938
+ }
1939
+ );
1940
+ res.json({
1941
+ success: true,
1942
+ sessionId: session.id,
1943
+ fileId,
1944
+ token: entry.token,
1945
+ downloadUrl: entry.downloadUrl,
1946
+ fileName: filename,
1947
+ fileSize: stat.size,
1948
+ maxDownloads: entry.maxDownloads,
1949
+ expiresAt: entry.expiresAt
1950
+ });
1951
+ } catch (err) {
1952
+ console.error("[DAEMON] /pubshare failed:", err);
1953
+ res.status(500).json({ success: false, error: err.message });
1954
+ }
1955
+ });
1956
+ app.get("/pubshare/list", (_req, res) => {
1957
+ const now = Date.now();
1958
+ const items = Array.from(pubshares.values()).filter((e) => !e.expiresAt || e.expiresAt > now).map((e) => ({
1959
+ token: e.token,
1960
+ downloadUrl: e.downloadUrl,
1961
+ fileId: e.fileId,
1962
+ fileName: e.filename,
1963
+ fileSize: e.size,
1964
+ downloadCount: e.downloadCount,
1965
+ maxDownloads: e.maxDownloads,
1966
+ expiresAt: e.expiresAt,
1967
+ createdAt: e.createdAt
1968
+ }));
1969
+ res.json({ success: true, items });
1970
+ });
1971
+ app.post("/pubshare/revoke", (req, res) => {
1972
+ const { token } = req.body || {};
1973
+ if (!token) return res.status(400).json({ success: false, error: "token required" });
1974
+ const entry = pubsharesByToken.get(token);
1975
+ if (!entry) return res.status(404).json({ success: false, error: "Unknown token" });
1976
+ try {
1977
+ wsConn?.send(JSON.stringify({ type: "pubshare_unregister", payload: { token } }));
1978
+ } catch {
1979
+ }
1980
+ pubsharesByToken.delete(token);
1981
+ pubshares.delete(entry.fileId);
1982
+ res.json({ success: true });
1983
+ });
1984
+ app.post("/mcp", async (req, res) => {
1985
+ try {
1986
+ const message = req.body;
1987
+ if (Array.isArray(message)) {
1988
+ const responses = await Promise.all(message.map((m) => handleMcpMessage(m)));
1989
+ res.json(responses.filter(Boolean));
1990
+ } else {
1991
+ const response = await handleMcpMessage(message);
1992
+ if (response) res.json(response);
1993
+ else res.status(202).end();
1994
+ }
1995
+ } catch (err) {
1996
+ res.status(500).json({ jsonrpc: "2.0", id: null, error: { code: -32603, message: err.message } });
1997
+ }
1998
+ });
1999
+ app.get("/mcp", (req, res) => {
2000
+ res.setHeader("Content-Type", "text/event-stream");
2001
+ res.setHeader("Cache-Control", "no-cache");
2002
+ res.setHeader("Connection", "keep-alive");
2003
+ res.flushHeaders();
2004
+ res.write(`event: endpoint
2005
+ data: ${JSON.stringify({ uri: "/mcp" })}
2006
+
2007
+ `);
2008
+ sseClients.push(res);
2009
+ req.on("close", () => {
2010
+ sseClients = sseClients.filter((c) => c !== res);
2011
+ });
2012
+ });
2013
+ app.get("/mcp/sse", (req, res) => {
2014
+ res.setHeader("Content-Type", "text/event-stream");
2015
+ res.setHeader("Cache-Control", "no-cache");
2016
+ res.setHeader("Connection", "keep-alive");
2017
+ res.flushHeaders();
2018
+ res.write(`event: endpoint
2019
+ data: /mcp/messages
2020
+
2021
+ `);
2022
+ sseClients.push(res);
2023
+ req.on("close", () => {
2024
+ sseClients = sseClients.filter((c) => c !== res);
2025
+ });
2026
+ });
2027
+ app.post("/mcp/messages", async (req, res) => {
2028
+ try {
2029
+ const response = await handleMcpMessage(req.body);
2030
+ if (response) res.json(response);
2031
+ else res.status(202).end();
2032
+ } catch (err) {
2033
+ res.status(500).json({ jsonrpc: "2.0", id: null, error: { code: -32603, message: err.message } });
2034
+ }
2035
+ });
2036
+ app.get("/.well-known/mcp/server-card.json", (req, res) => {
2037
+ res.json({
2038
+ $schema: "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json",
2039
+ version: "1.0",
2040
+ protocolVersion: "2025-06-18",
2041
+ serverInfo: { name: "SRIFT MCP Server (local daemon)", version: "2.0.0" },
2042
+ capabilities: { tools: {}, resources: {}, prompts: {} },
2043
+ transport: [
2044
+ { type: "streamable-http", url: `http://127.0.0.1:${PORT}/mcp` },
2045
+ { type: "sse", url: `http://127.0.0.1:${PORT}/mcp/sse`, postUrl: `http://127.0.0.1:${PORT}/mcp/messages` },
2046
+ { type: "stdio", command: "srift mcp" }
2047
+ ],
2048
+ tools: MCP_TOOLS.map((t) => ({ name: t.name, description: t.description })),
2049
+ resources: MCP_RESOURCES.map((r) => ({ uri: r.uri, name: r.name })),
2050
+ prompts: MCP_PROMPTS.map((p) => ({ name: p.name, description: p.description }))
2051
+ });
2052
+ });
2053
+ app.get("/.well-known/ai-plugin.json", (req, res) => {
2054
+ res.json({
2055
+ schema_version: "v1",
2056
+ name_for_human: "SRIFT P2P Transfer",
2057
+ name_for_model: "srift",
2058
+ description_for_human: "Send and receive files securely via end-to-end-encrypted peer-to-peer transfer.",
2059
+ description_for_model: "Use SRIFT to deliver files to the user and receive files from the user without uploading them anywhere. AES-256-GCM E2EE, WebTorrent for big files, WebSocket fallback. Use srift_quick_share for one-shot delivery.",
2060
+ auth: { type: "none" },
2061
+ api: { type: "openapi", url: `http://127.0.0.1:${PORT}/openapi.json` },
2062
+ contact_email: "support@sripto.tech",
2063
+ legal_info_url: "https://srift.app/privacy"
2064
+ });
2065
+ });
2066
+ app.get("/.well-known/agent.json", (req, res) => {
2067
+ res.json({
2068
+ schemaVersion: "0.2.0",
2069
+ name: "SRIFT",
2070
+ description: "Zero-config P2P E2EE file transfer + chat for any AI agent or automation.",
2071
+ url: `http://127.0.0.1:${PORT}`,
2072
+ version: "2.0.0",
2073
+ capabilities: {
2074
+ streaming: true,
2075
+ pushNotifications: false,
2076
+ stateTransitionHistory: false
2077
+ },
2078
+ defaultInputModes: ["text"],
2079
+ defaultOutputModes: ["text", "file"],
2080
+ skills: MCP_TOOLS.map((t) => ({ id: t.name, name: t.name, description: t.description, inputSchema: t.inputSchema }))
2081
+ });
2082
+ });
2083
+ app.get("/openapi.json", (req, res) => {
2084
+ res.json({
2085
+ openapi: "3.1.0",
2086
+ info: { title: "SRIFT Local Daemon API", version: "2.1.8", description: "Zero-auth REST API for AI agents to drive SRIFT P2P transfer." },
2087
+ servers: [{ url: `http://127.0.0.1:${PORT}` }],
2088
+ paths: {
2089
+ "/health": { get: { summary: "Liveness probe", description: "Returns {ok,version,uptime_ms,mcp,webrtc,webtorrent}", responses: { "200": { description: "OK" } } } },
2090
+ "/status": { get: { summary: "Get session + transfers + pending joins", responses: { "200": { description: "OK" } } } },
2091
+ "/state": { get: { summary: "Workspace state snapshot (.srift-state.json)", responses: { "200": { description: "OK" } } } },
2092
+ "/transfers": { get: { summary: "Live transfer list with speed and ETA. Optional ?fileId= to filter.", responses: { "200": { description: "OK" } } } },
2093
+ "/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" } } } },
2094
+ "/peers": { get: { summary: "Peer connection state, RTT, ICE type", responses: { "200": { description: "OK" } } } },
2095
+ "/metrics": { get: { summary: "Prometheus-format counters (no auth)", responses: { "200": { description: "text/plain Prometheus format" } } } },
2096
+ "/logs": { get: { summary: "NDJSON daemon log tail. ?lines=N (default 100)", responses: { "200": { description: "application/x-ndjson" } } } },
2097
+ "/reset": { post: { summary: "Wipe all state and flush encryption keys", responses: { "200": { description: "{ok:true}" } } } },
2098
+ "/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}" } } } },
2099
+ "/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" } } } },
2100
+ "/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" } } } },
2101
+ "/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" } } } },
2102
+ "/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" } } } },
2103
+ "/session/close": { post: { summary: "Tear down the session and flush keys", responses: { "200": { description: "OK" } } } },
2104
+ "/send": { post: { summary: "Offer a file to peers (requires active session)", requestBody: { content: { "application/json": { schema: { type: "object", required: ["filePath"], properties: { filePath: { type: "string" } } } } } }, responses: { "200": { description: "{success:true,fileId}" } } } },
2105
+ "/receive": { post: { summary: "Accept an incoming file offer", requestBody: { content: { "application/json": { schema: { type: "object", required: ["fileId"], properties: { fileId: { type: "string" }, saveDir: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "404": { description: "No offer for this fileId" } } } },
2106
+ "/quick-share": { post: { summary: "ONE-SHOT: create session if needed, register the file for public HTTPS download, and return a direct downloadUrl (https://srift.app/d/<token>) the recipient can curl/wget/browse with zero install.", requestBody: { content: { "application/json": { schema: { type: "object", required: ["filePath"], properties: { filePath: { type: "string", description: "Absolute path to file" }, sessionName: { type: "string" } } } } } }, responses: { "200": { description: "{success,sessionId,fileId,downloadUrl,shareUrl,fileName,fileSize}" } } } },
2107
+ "/chat/send": { post: { summary: "Send E2EE chat message", requestBody: { content: { "application/json": { schema: { type: "object", required: ["message"], properties: { message: { type: "string" } } } } } }, responses: { "200": { description: "OK" }, "400": { description: "message required" }, "409": { description: "No session" }, "503": { description: "WS not ready \u2014 retry in 1\u20132s" } } } },
2108
+ "/chat/history": { get: { summary: "Decrypted chat log", responses: { "200": { description: "Array of {messageId,sender,content,timestamp}" } } } },
2109
+ "/api/v1/monitor/events": { get: { summary: "SSE event stream: connection_state, join_request, file_offer, transfer_progress, chat_received", responses: { "200": { description: "text/event-stream" } } } },
2110
+ "/mcp": { post: { summary: "MCP JSON-RPC (Streamable HTTP, spec 2025-06-18)", responses: { "200": { description: "JSON-RPC response" } } }, get: { summary: "SSE stream for server\u2192client MCP notifications", responses: { "200": { description: "text/event-stream" } } } },
2111
+ "/mcp/sse": { get: { summary: "Legacy SSE MCP endpoint (older clients)", responses: { "200": { description: "text/event-stream" } } } },
2112
+ "/mcp/messages": { post: { summary: "Legacy SSE MCP message submission endpoint", responses: { "200": { description: "OK" } } } }
2113
+ }
2114
+ });
2115
+ });
2116
+ app.get("/transfers", (req, res) => {
2117
+ const fileId = req.query.fileId;
2118
+ const list = fileId ? activeTransfers.filter((t) => t.fileId === fileId) : activeTransfers;
2119
+ res.json(list.map((t) => ({
2120
+ fileId: t.fileId,
2121
+ name: t.name,
2122
+ size: t.size,
2123
+ progress: parseFloat(t.progress.toFixed(2)),
2124
+ speedKBps: parseFloat(t.speedKBps.toFixed(2)),
2125
+ etaSeconds: Math.ceil(t.etaSeconds),
2126
+ protocol: t.protocol,
2127
+ status: t.status,
2128
+ direction: t.direction,
2129
+ bytesTransferred: t.bytesTransferred
2130
+ })));
2131
+ });
2132
+ app.get("/transfers/:fileId", (req, res) => {
2133
+ const tx = activeTransfers.find((t) => t.fileId === req.params.fileId);
2134
+ if (!tx) return res.status(404).json({ error: "Transfer not found" });
2135
+ res.json(tx);
2136
+ });
2137
+ app.get("/peers", (req, res) => {
2138
+ if (!session.id) return res.json([]);
2139
+ const state = wsConn ? wsConn.readyState : 3;
2140
+ const stateNames = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
2141
+ const peer = {
2142
+ sessionId: session.id,
2143
+ userId: session.userId,
2144
+ role: session.role,
2145
+ connectionState: stateNames[state] || "UNKNOWN",
2146
+ transport: "websocket",
2147
+ rttMs: null,
2148
+ iceType: "relay",
2149
+ isConnected: session.isConnected
2150
+ };
2151
+ res.json(session.id ? [peer] : []);
2152
+ });
2153
+ app.get("/metrics", (req, res) => {
2154
+ const uptime = ((Date.now() - DAEMON_START_TIME) / 1e3).toFixed(0);
2155
+ const lines = [
2156
+ "# HELP srift_uptime_seconds Daemon uptime in seconds",
2157
+ "# TYPE srift_uptime_seconds gauge",
2158
+ `srift_uptime_seconds ${uptime}`,
2159
+ "# HELP srift_active_transfers Number of active file transfers",
2160
+ "# TYPE srift_active_transfers gauge",
2161
+ `srift_active_transfers ${activeTransfers.filter((t) => t.status === "uploading" || t.status === "downloading").length}`,
2162
+ "# HELP srift_total_transfers Total transfers (all statuses)",
2163
+ "# TYPE srift_total_transfers counter",
2164
+ `srift_total_transfers ${activeTransfers.length}`,
2165
+ "# HELP srift_chat_messages_total Total chat messages in history",
2166
+ "# TYPE srift_chat_messages_total counter",
2167
+ `srift_chat_messages_total ${chatHistory.length}`,
2168
+ "# HELP srift_session_active Whether a session is currently active (1 = yes)",
2169
+ "# TYPE srift_session_active gauge",
2170
+ `srift_session_active ${session.id ? 1 : 0}`,
2171
+ "# HELP srift_ws_connected Whether the WebSocket is connected (1 = yes)",
2172
+ "# TYPE srift_ws_connected gauge",
2173
+ `srift_ws_connected ${session.isConnected ? 1 : 0}`
2174
+ ];
2175
+ res.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
2176
+ res.send(lines.join("\n") + "\n");
2177
+ });
2178
+ app.get("/logs", (req, res) => {
2179
+ const lines = parseInt(req.query.lines || "100", 10);
2180
+ try {
2181
+ if (!fs2.existsSync(logPath)) return res.json([]);
2182
+ const content = fs2.readFileSync(logPath, "utf-8");
2183
+ const all = content.split("\n").filter(Boolean);
2184
+ const tail = all.slice(-Math.min(lines, all.length));
2185
+ res.set("Content-Type", "application/x-ndjson");
2186
+ res.send(tail.join("\n"));
2187
+ } catch (err) {
2188
+ res.status(500).json({ error: err.message });
2189
+ }
2190
+ });
2191
+ app.post("/reset", (req, res) => {
2192
+ console.log("[DAEMON] /reset: full wipe (memory + disk)");
2193
+ if (wsConn) {
2194
+ try {
2195
+ wsConn.close();
2196
+ } catch {
2197
+ }
2198
+ wsConn = null;
2199
+ }
2200
+ if (heartbeatInterval) {
2201
+ clearInterval(heartbeatInterval);
2202
+ heartbeatInterval = null;
2203
+ }
2204
+ if (wtClient) {
2205
+ try {
2206
+ wtClient.destroy();
2207
+ } catch {
2208
+ }
2209
+ wtClient = null;
2210
+ }
2211
+ activeTorrents.clear();
2212
+ activeUploads.clear();
2213
+ session = { id: null, name: null, role: null, isConnected: false, userId: null };
2214
+ activeTransfers = [];
2215
+ chatHistory = [];
2216
+ pendingJoins = [];
2217
+ csrfToken = null;
2218
+ encryptionKey = null;
2219
+ wsUrl = null;
2220
+ let cleanedChunks = 0;
2221
+ try {
2222
+ if (fs2.existsSync(TEMP_DIR)) {
2223
+ for (const f of fs2.readdirSync(TEMP_DIR)) {
2224
+ try {
2225
+ fs2.unlinkSync(path2.join(TEMP_DIR, f));
2226
+ cleanedChunks++;
2227
+ } catch {
2228
+ }
2229
+ }
2230
+ }
2231
+ } catch {
2232
+ }
2233
+ writeStateFile();
2234
+ res.json({ ok: true, message: "State wiped, keys flushed, temp chunks cleaned", cleanedChunks });
2235
+ });
2236
+ app.get("/api/v1/monitor/events", (req, res) => {
2237
+ res.setHeader("Content-Type", "text/event-stream");
2238
+ res.setHeader("Cache-Control", "no-cache");
2239
+ res.setHeader("Connection", "keep-alive");
2240
+ res.flushHeaders();
2241
+ sseClients.push(res);
2242
+ console.log(`[DAEMON] SSE Client connected. Active clients: ${sseClients.length}`);
2243
+ req.on("close", () => {
2244
+ sseClients = sseClients.filter((c) => c !== res);
2245
+ console.log(`[DAEMON] SSE Client disconnected. Active clients: ${sseClients.length}`);
2246
+ });
2247
+ });
2248
+ app.post("/daemon/stop", (req, res) => {
2249
+ console.log("[DAEMON] Stop request received. Exiting daemon process gracefully...");
2250
+ res.json({ success: true });
2251
+ try {
2252
+ if (wsConn) {
2253
+ wsConn.close();
2254
+ wsConn = null;
2255
+ }
2256
+ } catch {
2257
+ }
2258
+ try {
2259
+ if (heartbeatInterval) {
2260
+ clearInterval(heartbeatInterval);
2261
+ heartbeatInterval = null;
2262
+ }
2263
+ } catch {
2264
+ }
2265
+ try {
2266
+ if (wtClient) {
2267
+ wtClient.destroy();
2268
+ wtClient = null;
2269
+ }
2270
+ } catch {
2271
+ }
2272
+ try {
2273
+ activeTorrents.clear();
2274
+ activeUploads.clear();
2275
+ } catch {
2276
+ }
2277
+ try {
2278
+ session = { id: null, name: null, role: null, isConnected: false, userId: null };
2279
+ activeTransfers = [];
2280
+ chatHistory = [];
2281
+ pendingJoins = [];
2282
+ encryptionKey = null;
2283
+ wsUrl = null;
2284
+ writeStateFile();
2285
+ } catch {
2286
+ }
2287
+ try {
2288
+ if (fs2.existsSync(TEMP_DIR)) {
2289
+ for (const f of fs2.readdirSync(TEMP_DIR)) {
2290
+ try {
2291
+ fs2.unlinkSync(path2.join(TEMP_DIR, f));
2292
+ } catch {
2293
+ }
2294
+ }
2295
+ }
2296
+ } catch {
2297
+ }
2298
+ setTimeout(() => {
2299
+ process.exit(0);
2300
+ }, 500);
2301
+ });
2302
+ app.listen(PORT, "127.0.0.1", async () => {
2303
+ await selectSignalerUrl();
2304
+ console.log(`[DAEMON] Background daemon listening on http://127.0.0.1:${PORT}`);
2305
+ writeStateFile();
2306
+ });
2307
+ }
2308
+ });
2309
+
2310
+ // ../../cli/index.ts
2311
+ import { spawn, execSync } from "child_process";
2312
+ import http3 from "http";
2313
+ import https2 from "https";
2314
+ import fs3 from "fs";
2315
+ import path3 from "path";
2316
+ import os2 from "os";
2317
+ import crypto2 from "crypto";
2318
+ import { fileURLToPath as fileURLToPath2 } from "url";
2319
+
2320
+ // ../../cli/client.ts
2321
+ import http from "http";
2322
+ import https from "https";
2323
+ import fs from "fs";
2324
+ import path from "path";
2325
+ import os from "os";
2326
+ import { fileURLToPath } from "url";
2327
+ var DAEMON_PORT = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
2328
+ var DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
2329
+ function callDaemon(endpoint, method, body) {
2330
+ return new Promise((resolve, reject) => {
2331
+ const url = `${DAEMON_URL}${endpoint}`;
2332
+ const options = {
2333
+ method,
2334
+ headers: {
2335
+ "Content-Type": "application/json"
2336
+ }
2337
+ };
2338
+ const req = http.request(url, options, (res) => {
2339
+ let data = "";
2340
+ res.on("data", (chunk) => data += chunk);
2341
+ res.on("end", () => {
2342
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
2343
+ try {
2344
+ resolve(JSON.parse(data));
2345
+ } catch {
2346
+ resolve({ success: true });
2347
+ }
2348
+ } else {
2349
+ try {
2350
+ const errJson = JSON.parse(data);
2351
+ reject(new Error(errJson.error || `HTTP Error ${res.statusCode}`));
2352
+ } catch {
2353
+ reject(new Error(`HTTP Error ${res.statusCode}: ${data}`));
2354
+ }
2355
+ }
2356
+ });
2357
+ });
2358
+ req.on("error", (err) => {
2359
+ reject(new Error(`Daemon connection failed: ${err.message}. Is the daemon running?`));
2360
+ });
2361
+ if (body) {
2362
+ req.write(JSON.stringify(body));
2363
+ }
2364
+ req.end();
2365
+ });
2366
+ }
2367
+ function renderProgressBar(percentage, speedKBps, eta, status) {
2368
+ const width = 40;
2369
+ const completed = Math.min(width, Math.max(0, Math.floor(percentage / 100 * width)));
2370
+ const remaining = width - completed;
2371
+ const bar = "\u2588".repeat(completed) + "\u2591".repeat(remaining);
2372
+ const percentageStr = `${percentage.toFixed(1)}%`;
2373
+ const speedStr = speedKBps > 1024 ? `${(speedKBps / 1024).toFixed(2)} MB/s` : `${speedKBps.toFixed(1)} KB/s`;
2374
+ const etaStr = eta > 0 && eta < 3600 ? `${Math.ceil(eta)}s` : "unknown";
2375
+ process.stdout.write(`\r[SRIFT] [${bar}] ${percentageStr} | Speed: ${speedStr} | ETA: ${etaStr} | Status: ${status}`);
2376
+ }
2377
+ async function handleSessionStart(name, roomSecret, isJson) {
2378
+ try {
2379
+ const res = await callDaemon("/session/start", "POST", { sessionName: name, roomSecret });
2380
+ if (isJson) {
2381
+ console.log(JSON.stringify(res));
2382
+ } else {
2383
+ console.log(`[SRIFT] Session created successfully!`);
2384
+ console.log(`Session ID: ${res.sessionId}`);
2385
+ console.log(`Access Link: https://srift.app/join-session?id=${res.sessionId}`);
2386
+ if (roomSecret) {
2387
+ console.log(`Room Secret: ${roomSecret} (derived locally, never sent to server)`);
2388
+ }
2389
+ }
2390
+ } catch (err) {
2391
+ console.error(`Error: ${err.message}`);
2392
+ process.exit(1);
2393
+ }
2394
+ }
2395
+ async function handleSessionJoin(sessionId, username, roomSecret, isJson) {
2396
+ try {
2397
+ const res = await callDaemon("/session/join", "POST", { sessionId, username, roomSecret });
2398
+ if (isJson) {
2399
+ console.log(JSON.stringify(res));
2400
+ } else {
2401
+ console.log(`[SRIFT] Joining session ${sessionId}...`);
2402
+ console.log(`Waiting for host approval. Check browser / client terminal.`);
2403
+ }
2404
+ } catch (err) {
2405
+ console.error(`Error: ${err.message}`);
2406
+ process.exit(1);
2407
+ }
2408
+ }
2409
+ async function handleSessionStatus(isJson) {
2410
+ try {
2411
+ const res = await callDaemon("/status", "GET");
2412
+ if (isJson) {
2413
+ console.log(JSON.stringify(res));
2414
+ } else {
2415
+ const s = res.session;
2416
+ if (!s.id) {
2417
+ console.log("[SRIFT] No active session.");
2418
+ return;
2419
+ }
2420
+ console.log(`Session ID: ${s.id}`);
2421
+ console.log(`Session Name: ${s.name}`);
2422
+ console.log(`Role: ${s.role}`);
2423
+ console.log(`Status: ${s.isConnected ? "Connected" : "Disconnected"}`);
2424
+ console.log(`User ID: ${s.userId}`);
2425
+ if (res.pendingJoins && res.pendingJoins.length > 0) {
2426
+ console.log(`
2427
+ Pending Join Requests (${res.pendingJoins.length}):`);
2428
+ res.pendingJoins.forEach((j) => {
2429
+ console.log(` - ${j.username} (${j.tempUserId}) [Run 'srift approve ${j.tempUserId}' to let them in]`);
2430
+ });
2431
+ }
2432
+ }
2433
+ } catch (err) {
2434
+ console.error(`Error: ${err.message}`);
2435
+ process.exit(1);
2436
+ }
2437
+ }
2438
+ async function handleSessionClose(isJson) {
2439
+ try {
2440
+ const res = await callDaemon("/session/close", "POST");
2441
+ if (isJson) {
2442
+ console.log(JSON.stringify({ success: true }));
2443
+ } else {
2444
+ console.log("[SRIFT] Active session closed successfully.");
2445
+ }
2446
+ } catch (err) {
2447
+ console.error(`Error: ${err.message}`);
2448
+ process.exit(1);
2449
+ }
2450
+ }
2451
+ async function handleSendFile(filePath, protocol, isJson) {
2452
+ try {
2453
+ const res = await callDaemon("/send", "POST", { filePath, protocol });
2454
+ if (isJson) {
2455
+ console.log(JSON.stringify(res));
2456
+ } else {
2457
+ console.log(`[SRIFT] Seeding file: ${filePath}`);
2458
+ console.log(`File ID: ${res.fileId}`);
2459
+ console.log(`Offer sent. Waiting for peers to accept...`);
2460
+ }
2461
+ } catch (err) {
2462
+ console.error(`Error: ${err.message}`);
2463
+ process.exit(1);
2464
+ }
2465
+ }
2466
+ async function handleReceiveFile(fileId, saveDir, isJson) {
2467
+ try {
2468
+ const res = await callDaemon("/receive", "POST", { fileId, saveDir });
2469
+ if (isJson) {
2470
+ console.log(JSON.stringify(res));
2471
+ } else {
2472
+ console.log(`[SRIFT] Accepted file offer: ${fileId}`);
2473
+ console.log(`Download started. Saving to: ${saveDir || process.cwd()}`);
2474
+ }
2475
+ } catch (err) {
2476
+ console.error(`Error: ${err.message}`);
2477
+ process.exit(1);
2478
+ }
2479
+ }
2480
+ async function handleListTransfers(isJson) {
2481
+ try {
2482
+ const res = await callDaemon("/status", "GET");
2483
+ if (isJson) {
2484
+ console.log(JSON.stringify(res.activeTransfers || []));
2485
+ } else {
2486
+ const txs = res.activeTransfers || [];
2487
+ if (txs.length === 0) {
2488
+ console.log("[SRIFT] No active or past transfers.");
2489
+ return;
2490
+ }
2491
+ console.log("Active/Completed Transfers:");
2492
+ txs.forEach((t) => {
2493
+ console.log(` - [${t.direction}] ${t.name} (${(t.size / 1024 / 1024).toFixed(2)} MB)`);
2494
+ console.log(` ID: ${t.fileId}`);
2495
+ console.log(` Status: ${t.status}`);
2496
+ console.log(` Progress: ${t.progress.toFixed(1)}% | Speed: ${t.speedKBps.toFixed(1)} KB/s`);
2497
+ });
2498
+ }
2499
+ } catch (err) {
2500
+ console.error(`Error: ${err.message}`);
2501
+ process.exit(1);
2502
+ }
2503
+ }
2504
+ async function handleApproveJoin(tempUserId, isJson) {
2505
+ try {
2506
+ const res = await callDaemon("/session/approve", "POST", { tempUserId });
2507
+ if (isJson) {
2508
+ console.log(JSON.stringify({ success: true }));
2509
+ } else {
2510
+ console.log(`[SRIFT] Approved join request for ${tempUserId}`);
2511
+ }
2512
+ } catch (err) {
2513
+ console.error(`Error: ${err.message}`);
2514
+ process.exit(1);
2515
+ }
2516
+ }
2517
+ async function handleRejectJoin(tempUserId, reason, isJson) {
2518
+ try {
2519
+ const res = await callDaemon("/session/reject", "POST", { tempUserId, reason });
2520
+ if (isJson) {
2521
+ console.log(JSON.stringify({ success: true }));
2522
+ } else {
2523
+ console.log(`[SRIFT] Rejected join request for ${tempUserId}`);
2524
+ }
2525
+ } catch (err) {
2526
+ console.error(`Error: ${err.message}`);
2527
+ process.exit(1);
2528
+ }
2529
+ }
2530
+ async function handleKickUser(userId, isJson) {
2531
+ try {
2532
+ const res = await callDaemon("/session/kick", "POST", { userId });
2533
+ if (isJson) {
2534
+ console.log(JSON.stringify({ success: true }));
2535
+ } else {
2536
+ console.log(`[SRIFT] Kicked user ${userId} from session.`);
2537
+ }
2538
+ } catch (err) {
2539
+ console.error(`Error: ${err.message}`);
2540
+ process.exit(1);
2541
+ }
2542
+ }
2543
+ async function handleChatSend(message, isJson) {
2544
+ try {
2545
+ const res = await callDaemon("/chat/send", "POST", { message });
2546
+ if (isJson) {
2547
+ console.log(JSON.stringify({ success: true }));
2548
+ } else {
2549
+ console.log(`[SRIFT] Sent message: "${message}"`);
2550
+ }
2551
+ } catch (err) {
2552
+ console.error(`Error: ${err.message}`);
2553
+ process.exit(1);
2554
+ }
2555
+ }
2556
+ async function handleChatHistory(isJson) {
2557
+ try {
2558
+ const res = await callDaemon("/chat/history", "GET");
2559
+ if (isJson) {
2560
+ console.log(JSON.stringify(res));
2561
+ } else {
2562
+ console.log("Chat History:");
2563
+ res.forEach((m) => {
2564
+ console.log(`[${m.timestamp}] ${m.sender}: ${m.content}`);
2565
+ });
2566
+ }
2567
+ } catch (err) {
2568
+ console.error(`Error: ${err.message}`);
2569
+ process.exit(1);
2570
+ }
2571
+ }
2572
+ async function handleDaemonStop(isJson) {
2573
+ try {
2574
+ const res = await callDaemon("/daemon/stop", "POST");
2575
+ if (isJson) {
2576
+ console.log(JSON.stringify({ success: true }));
2577
+ } else {
2578
+ console.log("[SRIFT] Stopped background transfer daemon.");
2579
+ }
2580
+ } catch (err) {
2581
+ console.error(`Error: ${err.message}`);
2582
+ process.exit(1);
2583
+ }
2584
+ }
2585
+ function _formatSize(bytes) {
2586
+ if (!bytes && bytes !== 0) return "?";
2587
+ if (bytes < 1024) return `${bytes} B`;
2588
+ const units = ["KB", "MB", "GB", "TB"];
2589
+ let v = bytes / 1024, i = 0;
2590
+ while (v >= 1024 && i < units.length - 1) {
2591
+ v /= 1024;
2592
+ i++;
2593
+ }
2594
+ return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
2595
+ }
2596
+ function _formatExpiry(expiresAt) {
2597
+ if (!expiresAt) return "never";
2598
+ const remainingMs = expiresAt - Date.now();
2599
+ if (remainingMs <= 0) return "expired";
2600
+ const s = Math.floor(remainingMs / 1e3);
2601
+ if (s < 60) return `${s}s`;
2602
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
2603
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
2604
+ return `${Math.floor(s / 86400)}d`;
2605
+ }
2606
+ async function handleQuickShare(filePath, sessionName, isJson, opts = {}) {
2607
+ try {
2608
+ const body = { filePath, sessionName };
2609
+ if (opts.maxDownloads) body.maxDownloads = opts.maxDownloads;
2610
+ if (opts.ttlMs) body.ttlMs = opts.ttlMs;
2611
+ const res = await callDaemon("/quick-share", "POST", body);
2612
+ if (isJson) {
2613
+ console.log(JSON.stringify(res));
2614
+ return;
2615
+ }
2616
+ const directUrl = res.downloadUrl;
2617
+ const fallbackUrl = res.shareUrl;
2618
+ if (directUrl) {
2619
+ const limitLine = res.maxDownloads ? `${res.maxDownloads} download${res.maxDownloads === 1 ? "" : "s"}` : "unlimited downloads";
2620
+ const ttlLine = `expires ${_formatExpiry(res.expiresAt)}`;
2621
+ console.log("[SRIFT] Quick share ready.");
2622
+ console.log("");
2623
+ console.log(` File: ${res.fileName} (${_formatSize(res.fileSize)})`);
2624
+ console.log(` Download URL: ${directUrl}`);
2625
+ console.log(` Limits: ${limitLine}, ${ttlLine}`);
2626
+ console.log("");
2627
+ console.log("Recipient can open it in any browser, or download from the terminal:");
2628
+ console.log(` curl -OJ "${directUrl}"`);
2629
+ console.log(` wget --content-disposition "${directUrl}"`);
2630
+ console.log("");
2631
+ console.log("Keep this terminal running while they download.");
2632
+ console.log("Revoke any time with: srift pubshare revoke <token>");
2633
+ } else if (fallbackUrl) {
2634
+ console.log("[SRIFT] Quick share ready (legacy session-join mode).");
2635
+ console.log("");
2636
+ console.log(` File: ${res.fileName} (${_formatSize(res.fileSize)})`);
2637
+ console.log(` Share URL: ${fallbackUrl}`);
2638
+ console.log("");
2639
+ console.log("NOTE: the signaler is running an older build and direct download links");
2640
+ console.log(" are unavailable. Recipient must open the URL in a browser, enter a");
2641
+ console.log(" username, and you will need to approve their join from this terminal.");
2642
+ console.log(" Try `srift self-update` and ask srift.app to redeploy if this persists.");
2643
+ } else {
2644
+ console.error("[SRIFT] Quick share failed \u2014 no link returned.");
2645
+ process.exit(1);
2646
+ }
2647
+ } catch (err) {
2648
+ console.error(`Error: ${err.message}`);
2649
+ process.exit(1);
2650
+ }
2651
+ }
2652
+ async function handlePubshareList(isJson) {
2653
+ try {
2654
+ const res = await callDaemon("/pubshare/list", "GET");
2655
+ if (isJson) {
2656
+ console.log(JSON.stringify(res));
2657
+ return;
2658
+ }
2659
+ const items = res.items || [];
2660
+ if (!items.length) {
2661
+ console.log("[SRIFT] No active public download links.");
2662
+ console.log(" Create one with: srift quick-share <filepath>");
2663
+ return;
2664
+ }
2665
+ console.log(`[SRIFT] Active public download links (${items.length}):`);
2666
+ items.forEach((it, i) => {
2667
+ const limit = it.maxDownloads ? `${it.downloadCount}/${it.maxDownloads}` : `${it.downloadCount}/\u221E`;
2668
+ console.log("");
2669
+ console.log(` ${i + 1}. ${it.fileName} (${_formatSize(it.fileSize)})`);
2670
+ console.log(` URL: ${it.downloadUrl}`);
2671
+ console.log(` Downloads: ${limit}`);
2672
+ console.log(` Expires: ${_formatExpiry(it.expiresAt)}`);
2673
+ console.log(` Token: ${it.token}`);
2674
+ });
2675
+ } catch (err) {
2676
+ console.error(`Error: ${err.message}`);
2677
+ process.exit(1);
2678
+ }
2679
+ }
2680
+ async function handlePubshareRevoke(token, isJson) {
2681
+ try {
2682
+ await callDaemon("/pubshare/revoke", "POST", { token });
2683
+ if (isJson) {
2684
+ console.log(JSON.stringify({ success: true }));
2685
+ return;
2686
+ }
2687
+ console.log(`[SRIFT] Revoked link: ${token}`);
2688
+ } catch (err) {
2689
+ console.error(`Error: ${err.message}`);
2690
+ process.exit(1);
2691
+ }
2692
+ }
2693
+ async function handlePubshareAdd(filePath, isJson, opts = {}) {
2694
+ try {
2695
+ const body = { filePath };
2696
+ if (opts.maxDownloads) body.maxDownloads = opts.maxDownloads;
2697
+ if (opts.ttlMs) body.ttlMs = opts.ttlMs;
2698
+ const res = await callDaemon("/pubshare", "POST", body);
2699
+ if (isJson) {
2700
+ console.log(JSON.stringify(res));
2701
+ return;
2702
+ }
2703
+ const limitLine = res.maxDownloads ? `${res.maxDownloads} download${res.maxDownloads === 1 ? "" : "s"}` : "unlimited downloads";
2704
+ console.log("[SRIFT] Public download link added.");
2705
+ console.log("");
2706
+ console.log(` File: ${res.fileName} (${_formatSize(res.fileSize)})`);
2707
+ console.log(` Download URL: ${res.downloadUrl}`);
2708
+ console.log(` Limits: ${limitLine}, expires ${_formatExpiry(res.expiresAt)}`);
2709
+ } catch (err) {
2710
+ console.error(`Error: ${err.message}`);
2711
+ process.exit(1);
2712
+ }
2713
+ }
2714
+ function handleMonitorTransfer(fileId, jsonStream) {
2715
+ const url = `${DAEMON_URL}/api/v1/monitor/events`;
2716
+ const req = http.request(url, (res) => {
2717
+ res.on("data", (chunk) => {
2718
+ const lines = chunk.toString().split("\n");
2719
+ let currentEvent = "";
2720
+ for (const line of lines) {
2721
+ if (line.startsWith("event: ")) {
2722
+ currentEvent = line.substring(7).trim();
2723
+ } else if (line.startsWith("data: ")) {
2724
+ try {
2725
+ const dataJson = JSON.parse(line.substring(6).trim());
2726
+ if (currentEvent === "transfer_progress" && dataJson.fileId === fileId) {
2727
+ if (jsonStream) {
2728
+ console.log(JSON.stringify(dataJson));
2729
+ } else {
2730
+ renderProgressBar(
2731
+ dataJson.progress,
2732
+ dataJson.speedBytesPerSecond / 1024,
2733
+ dataJson.timeRemainingSeconds,
2734
+ dataJson.status
2735
+ );
2736
+ if (dataJson.status === "completed" || dataJson.status === "error" || dataJson.status === "cancelled") {
2737
+ console.log("\nTransfer reached terminal state:", dataJson.status);
2738
+ process.exit(0);
2739
+ }
2740
+ }
2741
+ }
2742
+ } catch {
2743
+ }
2744
+ }
2745
+ }
2746
+ });
2747
+ });
2748
+ req.on("error", (err) => {
2749
+ console.error(`[SRIFT] Monitor error: ${err.message}`);
2750
+ process.exit(1);
2751
+ });
2752
+ req.end();
2753
+ }
2754
+ async function fetchText(url) {
2755
+ return new Promise((resolve, reject) => {
2756
+ https.get(url, { headers: { "User-Agent": "srift-cli" } }, (res) => {
2757
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
2758
+ let data = "";
2759
+ res.on("data", (chunk) => data += chunk);
2760
+ res.on("end", () => resolve(data));
2761
+ } else {
2762
+ reject(new Error(`HTTP ${res.statusCode}`));
2763
+ }
2764
+ }).on("error", reject);
2765
+ });
2766
+ }
2767
+ var DEFAULT_CURSORRULES = `# SRIFT \u2014 Universal AI Agent Rules (Cursor / Aider / Codex / Continue / Cline / Goose / Zed / Devin / Replit / Copilot)
2768
+
2769
+ You are working inside a project where SRIFT is available. SRIFT is a zero-config, zero-token, peer-to-peer file transfer and encrypted chat tool.
2770
+ Always use SRIFT to send files to the user or receive files from the user, rather than pasting large base64 blobs in chat or committing binaries to git.
2771
+
2772
+ ## Install (standalone binary \u2014 no Node.js required)
2773
+ \`\`\`bash
2774
+ # macOS / Linux / WSL / Termux / any POSIX sh
2775
+ curl -fsSL https://srift.app/install.sh | sh
2776
+ # Windows PowerShell (PS 5.1+ or pwsh 7+)
2777
+ irm https://srift.app/install.ps1 | iex
2778
+ \`\`\`
2779
+
2780
+ ## The 1-line workflow
2781
+ \`\`\`bash
2782
+ srift quick-share /abs/path/to/file
2783
+ # \u2192 prints a https://srift.app/d/<token> URL.
2784
+ # Recipient downloads with ANY HTTP client \u2014 browser, curl, wget, mobile Safari.
2785
+ # They do NOT need SRIFT installed.
2786
+ \`\`\`
2787
+
2788
+ ## Limits
2789
+ - Single use link: \`srift quick-share <file> --once\`
2790
+ - Expiry time: \`srift quick-share <file> --ttl 15m\` (e.g., 30s, 15m, 2h, 1d)
2791
+ - Cap downloads: \`srift quick-share <file> --max-downloads 5\`
2792
+ `;
2793
+ var DEFAULT_AGENTS_MD = `# AGENTS.md \u2014 Universal AI Agent Manual for SRIFT
2794
+
2795
+ SRIFT is a peer-to-peer secure file transfer and encrypted communications platform.
2796
+ It runs as both a public web app at https://srift.app and a local headless daemon (http://127.0.0.1:3822) that AI agents can drive directly.
2797
+
2798
+ ## Quick Share Command:
2799
+ \`\`\`bash
2800
+ srift quick-share /abs/path/to/file
2801
+ # Returns a public download URL: https://srift.app/d/<token>
2802
+ \`\`\`
2803
+
2804
+ Optional limits:
2805
+ - \`--once\` (single-use link)
2806
+ - \`--ttl 15m\` (expires in 15 minutes)
2807
+ - \`--max-downloads 5\` (expires after 5 downloads)
2808
+
2809
+ For long-lived interactive collaboration sessions, use:
2810
+ \`\`\`bash
2811
+ srift session start
2812
+ \`\`\`
2813
+ `;
2814
+ async function handleAutoInstallMcp(isJson) {
2815
+ try {
2816
+ let configPath = "";
2817
+ if (process.platform === "win32") {
2818
+ configPath = path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2819
+ } else if (process.platform === "darwin") {
2820
+ configPath = path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
2821
+ } else {
2822
+ configPath = path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
2823
+ }
2824
+ let config = { mcpServers: {} };
2825
+ if (fs.existsSync(configPath)) {
2826
+ try {
2827
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
2828
+ } catch (err) {
2829
+ throw new Error(`Failed to parse Claude Desktop config at ${configPath}: ${err.message}`);
2830
+ }
2831
+ }
2832
+ if (!config.mcpServers) {
2833
+ config.mcpServers = {};
2834
+ }
2835
+ const execPath = process.execPath || "";
2836
+ const execBase = path.basename(execPath).toLowerCase();
2837
+ const isCompiledBinary = execBase === "srift" || execBase === "srift.exe" || typeof process.isBun === "boolean" && process.isBun || process.versions && process.versions.bun !== void 0;
2838
+ let command = "";
2839
+ let mcpArgs = [];
2840
+ if (isCompiledBinary) {
2841
+ command = execPath.replace(/\\/g, "/");
2842
+ mcpArgs = ["mcp"];
2843
+ } else {
2844
+ command = "node";
2845
+ const __filename2 = fileURLToPath(import.meta.url);
2846
+ const __dirname2 = path.dirname(__filename2);
2847
+ const scriptPath = path.resolve(__dirname2, "index.ts").replace(/\\/g, "/");
2848
+ mcpArgs = ["--experimental-strip-types", scriptPath, "mcp"];
2849
+ }
2850
+ config.mcpServers.srift = {
2851
+ command,
2852
+ args: mcpArgs
2853
+ };
2854
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
2855
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
2856
+ if (isJson) {
2857
+ console.log(JSON.stringify({ success: true, configPath }));
2858
+ } else {
2859
+ console.log(`[SRIFT] MCP Server successfully installed into Claude Desktop!`);
2860
+ console.log(`Config path: ${configPath}`);
2861
+ console.log(`Command: ${command}`);
2862
+ console.log(`Args: ${JSON.stringify(mcpArgs)}`);
2863
+ console.log(`
2864
+ IMPORTANT: Please restart Claude Desktop for the changes to take effect.`);
2865
+ }
2866
+ } catch (err) {
2867
+ console.error(`Error: ${err.message}`);
2868
+ process.exit(1);
2869
+ }
2870
+ }
2871
+ async function handleBootstrap(targetDirArg, opts = {}) {
2872
+ try {
2873
+ const targetDir = targetDirArg ? path.resolve(targetDirArg) : process.cwd();
2874
+ const createCursorrules = opts.cursorrules || !opts.cursorrules && !opts.agents;
2875
+ const createAgents = opts.agents || !opts.cursorrules && !opts.agents;
2876
+ console.log(`[SRIFT] Bootstrapping AI Agent instructions in: ${targetDir}`);
2877
+ fs.mkdirSync(targetDir, { recursive: true });
2878
+ if (createCursorrules) {
2879
+ const cursorrulesPath = path.join(targetDir, ".cursorrules");
2880
+ console.log(` Writing .cursorrules...`);
2881
+ let content = DEFAULT_CURSORRULES;
2882
+ try {
2883
+ content = await fetchText("https://srift.app/.cursorrules");
2884
+ } catch (err) {
2885
+ console.log(` (Note: failed to fetch latest .cursorrules online: ${err.message}. Using offline template.)`);
2886
+ }
2887
+ fs.writeFileSync(cursorrulesPath, content, "utf8");
2888
+ console.log(` \u2705 .cursorrules written to ${cursorrulesPath}`);
2889
+ }
2890
+ if (createAgents) {
2891
+ const agentsPath = path.join(targetDir, "AGENTS.md");
2892
+ console.log(` Writing AGENTS.md...`);
2893
+ let content = DEFAULT_AGENTS_MD;
2894
+ try {
2895
+ content = await fetchText("https://srift.app/AGENTS.md");
2896
+ } catch (err) {
2897
+ console.log(` (Note: failed to fetch latest AGENTS.md online: ${err.message}. Using offline template.)`);
2898
+ }
2899
+ fs.writeFileSync(agentsPath, content, "utf8");
2900
+ console.log(` \u2705 AGENTS.md written to ${agentsPath}`);
2901
+ }
2902
+ console.log(`
2903
+ [SRIFT] Bootstrap complete! AI agents in this project will now automatically use SRIFT for file sharing.`);
2904
+ } catch (err) {
2905
+ console.error(`Error: ${err.message}`);
2906
+ process.exit(1);
2907
+ }
2908
+ }
2909
+
2910
+ // ../../cli/index.ts
2911
+ init_mcp();
2912
+ var __filename = fileURLToPath2(import.meta.url);
2913
+ var __dirname = path3.dirname(__filename);
2914
+ var DAEMON_PORT3 = parseInt(process.env.SRIFT_DAEMON_PORT || "3822", 10);
2915
+ var DAEMON_URL3 = `http://127.0.0.1:${DAEMON_PORT3}`;
2916
+ var CLI_VERSION = "2.2.12";
2917
+ function parseDuration(s) {
2918
+ if (!s) return 0;
2919
+ const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i.exec(s.trim());
2920
+ if (!m) return 0;
2921
+ const n = parseFloat(m[1]);
2922
+ const unit = (m[2] || "s").toLowerCase();
2923
+ const mult = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
2924
+ return Math.floor(n * (mult[unit] || 0));
2925
+ }
2926
+ var VERSION_CHECK_URL = "https://srift.app/cli/version.json";
2927
+ var SRIFT_BASE_DL_URL = "https://srift.app/dl";
2928
+ var CONFIG_DIR = path3.join(os2.homedir(), ".srift");
2929
+ var CONFIG_FILE = path3.join(CONFIG_DIR, "config.json");
2930
+ function readConfig() {
2931
+ try {
2932
+ if (fs3.existsSync(CONFIG_FILE)) {
2933
+ return JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf8"));
2934
+ }
2935
+ } catch {
2936
+ }
2937
+ return {};
2938
+ }
2939
+ function writeConfig(config) {
2940
+ try {
2941
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
2942
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
2943
+ } catch {
2944
+ }
2945
+ }
2946
+ function fetchJson(url) {
2947
+ return new Promise((resolve, reject) => {
2948
+ https2.get(url, { headers: { "User-Agent": `srift-cli/${CLI_VERSION}` } }, (res) => {
2949
+ let data = "";
2950
+ res.on("data", (c) => data += c);
2951
+ res.on("end", () => {
2952
+ try {
2953
+ resolve(JSON.parse(data));
2954
+ } catch {
2955
+ reject(new Error("Invalid JSON"));
2956
+ }
2957
+ });
2958
+ }).on("error", reject);
2959
+ });
2960
+ }
2961
+ async function checkForUpdate(silent = true) {
2962
+ if (process.env.SRIFT_NO_UPDATE_CHECK === "1") return null;
2963
+ const cfg = readConfig();
2964
+ if (cfg.updateCheck === false) return null;
2965
+ const intervalHours = cfg.updateCheckIntervalHours ?? 24;
2966
+ if (cfg.lastUpdateCheck) {
2967
+ const since = Date.now() - new Date(cfg.lastUpdateCheck).getTime();
2968
+ if (since < intervalHours * 36e5) return null;
2969
+ }
2970
+ try {
2971
+ const data = await fetchJson(VERSION_CHECK_URL);
2972
+ writeConfig({ ...cfg, lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString() });
2973
+ const latest = data.latest;
2974
+ const update = compareVersions(latest, CLI_VERSION) > 0;
2975
+ if (!silent && update) {
2976
+ console.error(`
2977
+ \u26A1 srift ${latest} is available (you have ${CLI_VERSION})`);
2978
+ console.error(` Run: srift self-update or curl -fsSL https://srift.app/install.sh | sh
2979
+ `);
2980
+ }
2981
+ return { update, latest, current: CLI_VERSION };
2982
+ } catch {
2983
+ return null;
2984
+ }
2985
+ }
2986
+ function compareVersions(a, b) {
2987
+ const pa = a.split(".").map(Number);
2988
+ const pb = b.split(".").map(Number);
2989
+ for (let i = 0; i < 3; i++) {
2990
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
2991
+ if (diff !== 0) return diff;
2992
+ }
2993
+ return 0;
2994
+ }
2995
+ async function handleVersion(isJson) {
2996
+ let daemonVersion = null;
2997
+ let daemonOk = false;
2998
+ try {
2999
+ const result = await new Promise((resolve, reject) => {
3000
+ const req = http3.get(`${DAEMON_URL3}/health`, (res) => {
3001
+ let d = "";
3002
+ res.on("data", (c) => d += c);
3003
+ res.on("end", () => {
3004
+ try {
3005
+ resolve(JSON.parse(d));
3006
+ } catch {
3007
+ reject(new Error("Invalid"));
3008
+ }
3009
+ });
3010
+ });
3011
+ req.on("error", reject);
3012
+ req.setTimeout(1e3, () => req.destroy());
3013
+ });
3014
+ daemonVersion = result.version ?? result.daemonVersion ?? null;
3015
+ daemonOk = result.ok ?? true;
3016
+ } catch {
3017
+ }
3018
+ let remoteLatest = null;
3019
+ try {
3020
+ const data = await fetchJson(VERSION_CHECK_URL);
3021
+ remoteLatest = data.latest;
3022
+ } catch {
3023
+ }
3024
+ if (isJson) {
3025
+ console.log(JSON.stringify({
3026
+ cli: CLI_VERSION,
3027
+ daemon: daemonVersion,
3028
+ daemonRunning: daemonOk,
3029
+ latest: remoteLatest,
3030
+ updateAvailable: remoteLatest ? compareVersions(remoteLatest, CLI_VERSION) > 0 : null,
3031
+ nodeVersion: process.version,
3032
+ platform: process.platform,
3033
+ arch: process.arch
3034
+ }, null, 2));
3035
+ return;
3036
+ }
3037
+ console.log(`srift ${CLI_VERSION}`);
3038
+ if (daemonVersion) {
3039
+ console.log(`daemon ${daemonVersion} (${daemonOk ? "running" : "not ok"} on :${DAEMON_PORT3})`);
3040
+ } else {
3041
+ console.log(`daemon \u2014 not running`);
3042
+ }
3043
+ console.log(`node ${process.version}`);
3044
+ console.log(`os ${process.platform} ${process.arch}`);
3045
+ if (remoteLatest && compareVersions(remoteLatest, CLI_VERSION) > 0) {
3046
+ console.log(`
3047
+ \u26A1 Update available: ${CLI_VERSION} \u2192 ${remoteLatest}`);
3048
+ console.log(` srift self-update`);
3049
+ }
3050
+ }
3051
+ async function handleSelfUpdate(isJson) {
3052
+ const isBinary = !__filename.endsWith(".ts") && !__filename.endsWith(".js");
3053
+ const installDir = path3.join(os2.homedir(), ".srift", "bin");
3054
+ const binName = process.platform === "win32" ? "srift.exe" : "srift";
3055
+ const binPath = path3.join(installDir, binName);
3056
+ console.log(`[srift] Checking for updates...`);
3057
+ let data;
3058
+ try {
3059
+ data = await fetchJson(VERSION_CHECK_URL);
3060
+ } catch (e) {
3061
+ console.error(`[srift] Cannot reach update server: ${e}`);
3062
+ if (isJson) console.log(JSON.stringify({ ok: false, error: String(e) }));
3063
+ process.exit(1);
3064
+ }
3065
+ const latest = data.latest;
3066
+ if (compareVersions(latest, CLI_VERSION) <= 0) {
3067
+ console.log(`[srift] Already up to date (${CLI_VERSION}).`);
3068
+ if (isJson) console.log(JSON.stringify({ ok: true, alreadyLatest: true, version: CLI_VERSION }));
3069
+ return;
3070
+ }
3071
+ console.log(`[srift] Updating ${CLI_VERSION} \u2192 ${latest} ...`);
3072
+ if (!isBinary) {
3073
+ console.log(`[srift] Running as Node script \u2014 delegating to install script.`);
3074
+ if (process.platform === "win32") {
3075
+ console.log(`[srift] Run: irm https://srift.app/install.ps1 | iex`);
3076
+ } else {
3077
+ console.log(`[srift] Run: curl -fsSL https://srift.app/install.sh | sh`);
3078
+ }
3079
+ if (isJson) console.log(JSON.stringify({ ok: false, error: "not-a-binary", installScript: `https://srift.app/install.${process.platform === "win32" ? "ps1" : "sh"}` }));
3080
+ return;
3081
+ }
3082
+ const archMap = {
3083
+ x64: "x64",
3084
+ arm64: "arm64",
3085
+ ia32: "x86"
3086
+ };
3087
+ const osMap = {
3088
+ linux: "linux",
3089
+ darwin: "darwin",
3090
+ win32: "win"
3091
+ };
3092
+ const osKey = osMap[process.platform];
3093
+ const archKey = archMap[process.arch];
3094
+ if (!osKey || !archKey) {
3095
+ console.error(`[srift] Unsupported platform: ${process.platform}/${process.arch}`);
3096
+ process.exit(1);
3097
+ }
3098
+ const target = `${osKey}-${archKey}`;
3099
+ const binaryUrl = `${SRIFT_BASE_DL_URL}/${latest}/${target}/${binName}`;
3100
+ const sumsUrl = `${SRIFT_BASE_DL_URL}/${latest}/SHA256SUMS`;
3101
+ const tmpPath = binPath + ".new";
3102
+ const stampPath = binPath + ".new.stamp";
3103
+ try {
3104
+ if (fs3.existsSync(stampPath)) {
3105
+ const prevUrl = fs3.readFileSync(stampPath, "utf8").trim();
3106
+ if (prevUrl !== binaryUrl && fs3.existsSync(tmpPath)) {
3107
+ fs3.unlinkSync(tmpPath);
3108
+ }
3109
+ } else if (fs3.existsSync(tmpPath)) {
3110
+ fs3.unlinkSync(tmpPath);
3111
+ }
3112
+ fs3.writeFileSync(stampPath, binaryUrl);
3113
+ } catch {
3114
+ }
3115
+ console.log(`[srift] Downloading ${binaryUrl} ...`);
3116
+ try {
3117
+ await downloadFile(binaryUrl, tmpPath);
3118
+ } catch (err) {
3119
+ if (isJson) console.log(JSON.stringify({ ok: false, error: String(err?.message || err) }));
3120
+ console.error(`[srift] ${err?.message || err}`);
3121
+ process.exit(1);
3122
+ } finally {
3123
+ try {
3124
+ if (fs3.existsSync(stampPath)) fs3.unlinkSync(stampPath);
3125
+ } catch {
3126
+ }
3127
+ }
3128
+ try {
3129
+ const sumsContent = await fetchRaw(sumsUrl);
3130
+ const lines = sumsContent.split("\n");
3131
+ const line = lines.find((l) => l.trim().endsWith(binName));
3132
+ if (line) {
3133
+ const expected = line.trim().split(/\s+/)[0].toLowerCase();
3134
+ const actual = crypto2.createHash("sha256").update(fs3.readFileSync(tmpPath)).digest("hex");
3135
+ if (actual !== expected) {
3136
+ try {
3137
+ fs3.unlinkSync(tmpPath);
3138
+ } catch {
3139
+ }
3140
+ console.error(`[srift] Checksum mismatch! Aborting update.`);
3141
+ console.error(` Expected: ${expected}`);
3142
+ console.error(` Got: ${actual}`);
3143
+ if (isJson) console.log(JSON.stringify({ ok: false, error: "checksum mismatch" }));
3144
+ process.exit(1);
3145
+ }
3146
+ console.log(`[srift] Checksum verified.`);
3147
+ } else {
3148
+ console.log(`[srift] No checksum entry for ${binName} in SHA256SUMS (continuing without verification).`);
3149
+ }
3150
+ } catch (err) {
3151
+ console.log(`[srift] Could not download checksum (${err?.message || err}). Continuing without verification.`);
3152
+ }
3153
+ try {
3154
+ fs3.chmodSync(tmpPath, 493);
3155
+ } catch {
3156
+ }
3157
+ const bakPath = binPath + ".bak";
3158
+ try {
3159
+ if (fs3.existsSync(binPath)) {
3160
+ if (fs3.existsSync(bakPath)) fs3.unlinkSync(bakPath);
3161
+ fs3.renameSync(binPath, bakPath);
3162
+ }
3163
+ fs3.renameSync(tmpPath, binPath);
3164
+ if (fs3.existsSync(bakPath)) {
3165
+ try {
3166
+ fs3.unlinkSync(bakPath);
3167
+ } catch {
3168
+ }
3169
+ }
3170
+ const others = findAllSriftBinaries().filter((p) => path3.resolve(p) !== path3.resolve(binPath));
3171
+ if (others.length) {
3172
+ console.log(`[srift] Cleaning ${others.length} stale srift binar${others.length === 1 ? "y" : "ies"} elsewhere on the system...`);
3173
+ for (const o of others) {
3174
+ const r = removeBinary(o);
3175
+ if (r.ok && !r.deferred) console.log(` \u2705 removed: ${o}`);
3176
+ else if (r.deferred) console.log(` \u23F1 scheduled (locked): ${o}`);
3177
+ else console.log(` \u26A0\uFE0F could not remove: ${o}`);
3178
+ }
3179
+ purgeSriftFromPath();
3180
+ if (process.platform === "win32") {
3181
+ try {
3182
+ const installDir2 = path3.dirname(binPath);
3183
+ const psCmd = [
3184
+ "$p=[System.Environment]::GetEnvironmentVariable('Path','User')",
3185
+ "if(-not $p){$p=''}",
3186
+ `$d='${installDir2.replace(/'/g, "''")}'`,
3187
+ `if($p -notlike "*$d*"){[System.Environment]::SetEnvironmentVariable('Path',"$d;$p",'User')}`
3188
+ ].join(";");
3189
+ execSync(`powershell -NoProfile -Command "${psCmd}"`, { stdio: "ignore" });
3190
+ } catch {
3191
+ }
3192
+ }
3193
+ }
3194
+ console.log(`[srift] \u2705 Updated to ${latest}. Restart your terminal.`);
3195
+ if (isJson) console.log(JSON.stringify({ ok: true, from: CLI_VERSION, to: latest, cleanedStale: others.length }));
3196
+ } catch (e) {
3197
+ if (process.platform === "win32" && (e?.code === "EPERM" || e?.code === "EBUSY" || e?.code === "EACCES")) {
3198
+ try {
3199
+ const winBin = binPath.replace(/\//g, "\\");
3200
+ const winTmp = tmpPath.replace(/\//g, "\\");
3201
+ const batPath = path3.join(os2.tmpdir(), `srift-update-${process.pid}-${Date.now()}.bat`);
3202
+ 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';
3203
+ fs3.writeFileSync(batPath, batBody, "utf8");
3204
+ spawn("cmd.exe", ["/C", batPath], {
3205
+ detached: true,
3206
+ stdio: "ignore",
3207
+ windowsHide: true,
3208
+ shell: false
3209
+ }).unref();
3210
+ console.log(`[srift] \u2705 Update staged. The new binary will be activated within ~2-3 seconds`);
3211
+ console.log(` after you exit this command. Then restart your terminal.`);
3212
+ if (isJson) console.log(JSON.stringify({ ok: true, from: CLI_VERSION, to: latest, deferred: true }));
3213
+ } catch (delErr) {
3214
+ console.error(`[srift] Could not stage update: ${delErr?.message || delErr}`);
3215
+ if (isJson) console.log(JSON.stringify({ ok: false, error: String(delErr?.message || delErr) }));
3216
+ process.exit(1);
3217
+ }
3218
+ } else {
3219
+ console.error(`[srift] Could not replace binary: ${e?.message || e}`);
3220
+ if (isJson) console.log(JSON.stringify({ ok: false, error: String(e?.message || e) }));
3221
+ process.exit(1);
3222
+ }
3223
+ }
3224
+ }
3225
+ function _downloadAttempt(url, dest, resumeFrom, showProgress, socketIdleMs) {
3226
+ return new Promise((resolve, reject) => {
3227
+ const headers = {
3228
+ "User-Agent": `srift-cli/${CLI_VERSION}`,
3229
+ "Accept-Encoding": "identity"
3230
+ // don't gzip a binary
3231
+ };
3232
+ if (resumeFrom > 0) headers["Range"] = `bytes=${resumeFrom}-`;
3233
+ const req = https2.get(url, { headers }, (res) => {
3234
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
3235
+ res.resume();
3236
+ return _downloadAttempt(res.headers.location, dest, resumeFrom, showProgress, socketIdleMs).then(resolve).catch(reject);
3237
+ }
3238
+ const ok = res.statusCode === 200 || res.statusCode === 206 && resumeFrom > 0;
3239
+ if (!ok) {
3240
+ res.resume();
3241
+ reject(new Error(`HTTP ${res.statusCode}`));
3242
+ return;
3243
+ }
3244
+ const cl = res.headers["content-length"];
3245
+ const total = cl ? parseInt(cl, 10) + resumeFrom : 0;
3246
+ let received = resumeFrom;
3247
+ let lastPrint = 0;
3248
+ let lastDataAt = Date.now();
3249
+ const file = fs3.createWriteStream(dest, { flags: resumeFrom > 0 ? "a" : "w" });
3250
+ const stallTimer = setInterval(() => {
3251
+ if (Date.now() - lastDataAt > socketIdleMs) {
3252
+ clearInterval(stallTimer);
3253
+ req.destroy(new Error(`Stalled \u2014 no data for ${Math.round(socketIdleMs / 1e3)}s`));
3254
+ }
3255
+ }, 2e3);
3256
+ res.on("data", (chunk) => {
3257
+ lastDataAt = Date.now();
3258
+ received += chunk.length;
3259
+ if (showProgress && Date.now() - lastPrint > 500) {
3260
+ const mb = (received / 1024 / 1024).toFixed(1);
3261
+ const pct = total > 0 ? ` (${(received / total * 100).toFixed(1)}%)` : "";
3262
+ process.stdout.write(`\r[srift] Downloaded ${mb} MB${pct} `);
3263
+ lastPrint = Date.now();
3264
+ }
3265
+ });
3266
+ res.pipe(file);
3267
+ file.on("finish", () => {
3268
+ clearInterval(stallTimer);
3269
+ if (showProgress) process.stdout.write("\n");
3270
+ file.close(() => resolve());
3271
+ });
3272
+ const onErr = (err) => {
3273
+ clearInterval(stallTimer);
3274
+ try {
3275
+ file.close();
3276
+ } catch {
3277
+ }
3278
+ reject(err);
3279
+ };
3280
+ file.on("error", onErr);
3281
+ res.on("error", onErr);
3282
+ });
3283
+ req.setTimeout(socketIdleMs, () => {
3284
+ req.destroy(new Error(`Connect timeout (${Math.round(socketIdleMs / 1e3)}s)`));
3285
+ });
3286
+ req.on("error", reject);
3287
+ });
3288
+ }
3289
+ function _findCurl() {
3290
+ if (process.platform === "win32") {
3291
+ const win = path3.join(process.env.WINDIR || "C:\\Windows", "System32", "curl.exe");
3292
+ if (fs3.existsSync(win)) return win;
3293
+ }
3294
+ try {
3295
+ const out = execSync(process.platform === "win32" ? "where curl.exe" : "which curl", {
3296
+ encoding: "utf8",
3297
+ stdio: ["ignore", "pipe", "ignore"]
3298
+ }).trim().split("\n").map((s) => s.trim()).filter(Boolean);
3299
+ for (const p of out) {
3300
+ if (/powershell/i.test(p)) continue;
3301
+ if (fs3.existsSync(p)) return p;
3302
+ }
3303
+ } catch {
3304
+ }
3305
+ return null;
3306
+ }
3307
+ async function _runCurl(curlPath, url, dest, showProgress) {
3308
+ fs3.mkdirSync(path3.dirname(dest), { recursive: true });
3309
+ const ua = `srift-cli/${CLI_VERSION} (${process.platform}; ${process.arch})`;
3310
+ const args = [
3311
+ "-fL",
3312
+ "--proto",
3313
+ "=https",
3314
+ "--tlsv1.2",
3315
+ "-A",
3316
+ ua,
3317
+ "--compressed",
3318
+ "-C",
3319
+ "-",
3320
+ "--retry",
3321
+ "15",
3322
+ "--retry-delay",
3323
+ "4",
3324
+ "--retry-all-errors",
3325
+ "--connect-timeout",
3326
+ "15",
3327
+ "--max-time",
3328
+ "900",
3329
+ showProgress ? "--progress-bar" : "-s",
3330
+ "-o",
3331
+ dest,
3332
+ url
3333
+ ];
3334
+ return new Promise((resolve) => {
3335
+ try {
3336
+ const r = spawn(curlPath, args, { stdio: ["ignore", "inherit", "inherit"] });
3337
+ r.on("exit", (code) => resolve(code === 0));
3338
+ r.on("error", () => resolve(false));
3339
+ } catch {
3340
+ resolve(false);
3341
+ }
3342
+ });
3343
+ }
3344
+ async function downloadFile(url, dest) {
3345
+ fs3.mkdirSync(path3.dirname(dest), { recursive: true });
3346
+ const curlPath = _findCurl();
3347
+ if (curlPath) {
3348
+ console.log(`[srift] Downloading via curl (${curlPath}) \u2014 handles 5xx + connection drops natively.`);
3349
+ const ok = await _runCurl(
3350
+ curlPath,
3351
+ url,
3352
+ dest,
3353
+ /*showProgress*/
3354
+ true
3355
+ );
3356
+ if (ok && fs3.existsSync(dest) && fs3.statSync(dest).size > 1e6) {
3357
+ console.log("");
3358
+ return;
3359
+ }
3360
+ console.log(`
3361
+ [srift] curl download failed (or file too small) \u2014 falling back to Node https.get with retry/resume.`);
3362
+ }
3363
+ const maxAttempts = 5;
3364
+ const socketIdleMs = 3e4;
3365
+ let lastErr = null;
3366
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3367
+ const resumeFrom = fs3.existsSync(dest) ? fs3.statSync(dest).size : 0;
3368
+ try {
3369
+ await _downloadAttempt(
3370
+ url,
3371
+ dest,
3372
+ resumeFrom,
3373
+ /*showProgress*/
3374
+ true,
3375
+ socketIdleMs
3376
+ );
3377
+ return;
3378
+ } catch (err) {
3379
+ lastErr = err;
3380
+ const partial = fs3.existsSync(dest) ? fs3.statSync(dest).size : 0;
3381
+ const partialMb = (partial / 1024 / 1024).toFixed(1);
3382
+ if (attempt >= maxAttempts) break;
3383
+ const wait = Math.min(2e3 * attempt, 8e3);
3384
+ console.log(
3385
+ `
3386
+ [srift] Download attempt ${attempt}/${maxAttempts} failed: ${err?.message || err}`
3387
+ );
3388
+ console.log(
3389
+ `[srift] Retrying in ${wait / 1e3}s (resuming from ${partialMb} MB)...`
3390
+ );
3391
+ await new Promise((r) => setTimeout(r, wait));
3392
+ }
3393
+ }
3394
+ throw new Error(
3395
+ `Download failed after ${maxAttempts} attempts: ${lastErr?.message || lastErr}
3396
+ Workaround:
3397
+ Windows: irm https://srift.app/install.ps1 | iex
3398
+ macOS/Linux: curl -fsSL https://srift.app/install.sh | sh`
3399
+ );
3400
+ }
3401
+ function fetchRaw(url) {
3402
+ return new Promise((resolve, reject) => {
3403
+ const req = https2.get(url, { headers: { "User-Agent": `srift-cli/${CLI_VERSION}` } }, (res) => {
3404
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
3405
+ res.resume();
3406
+ return fetchRaw(res.headers.location).then(resolve).catch(reject);
3407
+ }
3408
+ if (!res.statusCode || res.statusCode >= 400) {
3409
+ res.resume();
3410
+ reject(new Error(`HTTP ${res.statusCode}`));
3411
+ return;
3412
+ }
3413
+ let d = "";
3414
+ res.setEncoding("utf8");
3415
+ res.on("data", (c) => d += c);
3416
+ res.on("end", () => resolve(d));
3417
+ res.on("error", reject);
3418
+ });
3419
+ req.setTimeout(15e3, () => req.destroy(new Error("Request timeout (15s)")));
3420
+ req.on("error", reject);
3421
+ });
3422
+ }
3423
+ async function handleDoctor(isJson) {
3424
+ const checks = [];
3425
+ try {
3426
+ const r = await new Promise((resolve, reject) => {
3427
+ const req = http3.get(`${DAEMON_URL3}/health`, (res) => {
3428
+ let d = "";
3429
+ res.on("data", (c) => d += c);
3430
+ res.on("end", () => {
3431
+ try {
3432
+ resolve(JSON.parse(d));
3433
+ } catch {
3434
+ reject(new Error("Invalid JSON"));
3435
+ }
3436
+ });
3437
+ });
3438
+ req.on("error", reject);
3439
+ req.setTimeout(2e3, () => req.destroy(new Error("timeout")));
3440
+ });
3441
+ checks.push({ name: "Daemon running", ok: r.ok ?? true, detail: `v${r.version} uptime=${r.uptime_ms}ms mcp=${r.mcp} webtorrent=${r.webtorrent}` });
3442
+ } catch (e) {
3443
+ checks.push({ name: "Daemon running", ok: false, detail: `ECONNREFUSED \u2014 run: srift daemon start` });
3444
+ }
3445
+ try {
3446
+ await fetchJson("https://srift.app/compat.json");
3447
+ checks.push({ name: "srift.app reachable", ok: true, detail: "HTTP 200" });
3448
+ } catch (e) {
3449
+ checks.push({ name: "srift.app reachable", ok: false, detail: `Cannot reach srift.app: ${e}` });
3450
+ }
3451
+ try {
3452
+ const data = await fetchJson(VERSION_CHECK_URL);
3453
+ const hasUpdate = compareVersions(data.latest, CLI_VERSION) > 0;
3454
+ checks.push({ name: "CLI up to date", ok: !hasUpdate, detail: hasUpdate ? `Update available: ${data.latest}` : `${CLI_VERSION} is latest` });
3455
+ } catch {
3456
+ checks.push({ name: "CLI up to date", ok: true, detail: "Could not check (offline?)" });
3457
+ }
3458
+ const nodeVer = parseInt(process.version.slice(1));
3459
+ checks.push({ name: "Node.js version", ok: nodeVer >= 18, detail: `${process.version} (requires \u226518)` });
3460
+ try {
3461
+ readConfig();
3462
+ checks.push({ name: "Config file", ok: true, detail: CONFIG_FILE });
3463
+ } catch (e) {
3464
+ checks.push({ name: "Config file", ok: false, detail: `Cannot read config: ${e}` });
3465
+ }
3466
+ if (isJson) {
3467
+ console.log(JSON.stringify({ checks, allOk: checks.every((c) => c.ok) }, null, 2));
3468
+ return;
3469
+ }
3470
+ const allOk = checks.every((c) => c.ok);
3471
+ console.log(`
3472
+ srift doctor \u2014 ${allOk ? "\u2705 All checks passed" : "\u26A0\uFE0F Issues found"}
3473
+ `);
3474
+ for (const c of checks) {
3475
+ console.log(` ${c.ok ? "\u2713" : "\u2717"} ${c.name.padEnd(24)} ${c.detail}`);
3476
+ }
3477
+ if (!allOk) {
3478
+ console.log(`
3479
+ Docs: https://srift.app/ai-agents#troubleshooting`);
3480
+ }
3481
+ console.log("");
3482
+ }
3483
+ async function handleConfig(args, isJson) {
3484
+ const subCmd = args[0];
3485
+ const key = args[1];
3486
+ const value = args[2];
3487
+ if (subCmd === "get") {
3488
+ const cfg = readConfig();
3489
+ if (key) {
3490
+ const val = cfg[key];
3491
+ if (isJson) console.log(JSON.stringify({ [key]: val }));
3492
+ else console.log(`${key} = ${val ?? "(not set)"}`);
3493
+ } else {
3494
+ if (isJson) console.log(JSON.stringify(cfg, null, 2));
3495
+ else {
3496
+ for (const [k, v] of Object.entries(cfg)) {
3497
+ console.log(`${k} = ${v}`);
3498
+ }
3499
+ if (Object.keys(cfg).length === 0) console.log("(no config values set)");
3500
+ }
3501
+ }
3502
+ } else if (subCmd === "set") {
3503
+ if (!key || value === void 0) {
3504
+ console.error("Usage: srift config set <key> <value>");
3505
+ process.exit(1);
3506
+ }
3507
+ const cfg = readConfig();
3508
+ let parsed = value;
3509
+ if (value === "true") parsed = true;
3510
+ else if (value === "false") parsed = false;
3511
+ else if (!isNaN(Number(value))) parsed = Number(value);
3512
+ cfg[key] = parsed;
3513
+ writeConfig(cfg);
3514
+ if (isJson) console.log(JSON.stringify({ ok: true, [key]: parsed }));
3515
+ else console.log(`\u2713 ${key} = ${parsed}`);
3516
+ } else if (subCmd === "delete" || subCmd === "unset") {
3517
+ if (!key) {
3518
+ console.error("Usage: srift config delete <key>");
3519
+ process.exit(1);
3520
+ }
3521
+ const cfg = readConfig();
3522
+ delete cfg[key];
3523
+ writeConfig(cfg);
3524
+ if (isJson) console.log(JSON.stringify({ ok: true, deleted: key }));
3525
+ else console.log(`\u2713 Deleted ${key}`);
3526
+ } else if (!subCmd) {
3527
+ const cfg = readConfig();
3528
+ console.log(`Config file: ${CONFIG_FILE}`);
3529
+ if (isJson) {
3530
+ console.log(JSON.stringify(cfg, null, 2));
3531
+ return;
3532
+ }
3533
+ for (const [k, v] of Object.entries(cfg)) console.log(` ${k} = ${v}`);
3534
+ if (Object.keys(cfg).length === 0) console.log(" (no config values set)");
3535
+ console.log("\nKeys: updateCheck (bool), updateCheckIntervalHours (number)");
3536
+ } else {
3537
+ console.error("Usage: srift config [get|set|delete] [key] [value]");
3538
+ process.exit(1);
3539
+ }
3540
+ }
3541
+ function daemonRequest(method, urlPath, body) {
3542
+ return new Promise((resolve, reject) => {
3543
+ const bodyStr = body ? JSON.stringify(body) : void 0;
3544
+ const opts = {
3545
+ hostname: "127.0.0.1",
3546
+ port: DAEMON_PORT3,
3547
+ path: urlPath,
3548
+ method,
3549
+ headers: {
3550
+ "Content-Type": "application/json",
3551
+ ...bodyStr ? { "Content-Length": Buffer.byteLength(bodyStr) } : {}
3552
+ }
3553
+ };
3554
+ const req = http3.request(opts, (res) => {
3555
+ let d = "";
3556
+ res.on("data", (c) => d += c);
3557
+ res.on("end", () => {
3558
+ try {
3559
+ resolve(JSON.parse(d));
3560
+ } catch {
3561
+ resolve({ raw: d, statusCode: res.statusCode });
3562
+ }
3563
+ });
3564
+ });
3565
+ req.on("error", reject);
3566
+ req.setTimeout(5e3, () => req.destroy(new Error("timeout")));
3567
+ if (bodyStr) req.write(bodyStr);
3568
+ req.end();
3569
+ });
3570
+ }
3571
+ function isDaemonRunning() {
3572
+ return new Promise((resolve) => {
3573
+ const req = http3.get(`${DAEMON_URL3}/status`, (res) => {
3574
+ resolve(res.statusCode === 200);
3575
+ });
3576
+ req.on("error", () => resolve(false));
3577
+ req.end();
3578
+ });
3579
+ }
3580
+ async function ensureDaemon() {
3581
+ const running = await isDaemonRunning();
3582
+ if (running) return;
3583
+ const isMcp = process.argv.includes("mcp");
3584
+ const logFn = isMcp ? console.error : console.log;
3585
+ logFn("[SRIFT] Starting background transfer daemon...");
3586
+ const isBunBinary = !!(process.versions && process.versions.bun) || typeof process.isBun === "boolean" && process.isBun;
3587
+ const execBase = path3.basename(process.execPath || "").toLowerCase();
3588
+ const isCompiledBinary = isBunBinary || execBase === "srift" || execBase === "srift.exe";
3589
+ let execCmd;
3590
+ let spawnArgs;
3591
+ if (isCompiledBinary) {
3592
+ execCmd = process.execPath;
3593
+ spawnArgs = ["daemon", "start"];
3594
+ } else {
3595
+ const isJs = __filename.endsWith(".js");
3596
+ const daemonFile = isJs ? "daemon.js" : "daemon.ts";
3597
+ const daemonPath = path3.join(__dirname, daemonFile);
3598
+ execCmd = process.execPath;
3599
+ spawnArgs = isJs ? [daemonPath] : ["--experimental-strip-types", daemonPath];
3600
+ }
3601
+ const child = spawn(
3602
+ execCmd,
3603
+ spawnArgs,
3604
+ {
3605
+ detached: true,
3606
+ stdio: "ignore",
3607
+ windowsHide: true,
3608
+ env: { ...process.env, SRIFT_DAEMON_PORT: DAEMON_PORT3.toString() }
3609
+ }
3610
+ );
3611
+ child.unref();
3612
+ for (let i = 0; i < 50; i++) {
3613
+ await new Promise((r) => setTimeout(r, 100));
3614
+ if (await isDaemonRunning()) {
3615
+ logFn("[SRIFT] Daemon started successfully.");
3616
+ return;
3617
+ }
3618
+ }
3619
+ const portInUse = await new Promise((resolve) => {
3620
+ const req = http3.get(`http://127.0.0.1:${DAEMON_PORT3}/health`, (res) => {
3621
+ resolve(res.statusCode !== void 0);
3622
+ });
3623
+ req.on("error", () => resolve(false));
3624
+ req.setTimeout(500, () => {
3625
+ req.destroy();
3626
+ resolve(false);
3627
+ });
3628
+ });
3629
+ if (portInUse) {
3630
+ throw new Error(
3631
+ `Port ${DAEMON_PORT3} is already in use by another process.
3632
+ Try: SRIFT_DAEMON_PORT=3823 srift daemon start
3633
+ Or stop whatever is using port ${DAEMON_PORT3}.`
3634
+ );
3635
+ }
3636
+ throw new Error(
3637
+ `Failed to start daemon background process.
3638
+ Try manually: srift daemon start
3639
+ Check logs: srift logs
3640
+ Diagnose: srift doctor`
3641
+ );
3642
+ }
3643
+ async function handleStatus(isJson) {
3644
+ let daemonInfo = null;
3645
+ let sessionInfo = null;
3646
+ let transfersInfo = [];
3647
+ let daemonRunning = false;
3648
+ try {
3649
+ daemonInfo = await new Promise((resolve, reject) => {
3650
+ const req = http3.get(`${DAEMON_URL3}/health`, (res) => {
3651
+ let d = "";
3652
+ res.on("data", (c) => d += c);
3653
+ res.on("end", () => {
3654
+ try {
3655
+ resolve(JSON.parse(d));
3656
+ } catch {
3657
+ reject(new Error("bad json"));
3658
+ }
3659
+ });
3660
+ });
3661
+ req.on("error", reject);
3662
+ req.setTimeout(2e3, () => req.destroy(new Error("timeout")));
3663
+ });
3664
+ daemonRunning = daemonInfo.ok ?? true;
3665
+ } catch {
3666
+ }
3667
+ if (daemonRunning) {
3668
+ try {
3669
+ sessionInfo = await daemonRequest("GET", "/status");
3670
+ } catch {
3671
+ }
3672
+ try {
3673
+ const t = await daemonRequest("GET", "/transfers");
3674
+ if (Array.isArray(t)) transfersInfo = t;
3675
+ else if (t && Array.isArray(t.transfers)) transfersInfo = t.transfers;
3676
+ } catch {
3677
+ }
3678
+ }
3679
+ if (isJson) {
3680
+ console.log(JSON.stringify({
3681
+ daemon: daemonRunning ? { running: true, version: daemonInfo?.version, uptime_ms: daemonInfo?.uptime_ms, port: DAEMON_PORT3, mcp: daemonInfo?.mcp, webtorrent: daemonInfo?.webtorrent } : { running: false },
3682
+ session: sessionInfo ?? null,
3683
+ transfers: { count: transfersInfo.length, items: transfersInfo }
3684
+ }, null, 2));
3685
+ return;
3686
+ }
3687
+ console.log(`
3688
+ srift status
3689
+ `);
3690
+ if (daemonRunning) {
3691
+ const uptimeSec = Math.round((daemonInfo?.uptime_ms ?? 0) / 1e3);
3692
+ console.log(` Daemon: running v${daemonInfo?.version ?? "?"} uptime=${uptimeSec}s port=${DAEMON_PORT3}`);
3693
+ console.log(` mcp=${daemonInfo?.mcp ?? "?"} webtorrent=${daemonInfo?.webtorrent ?? "?"}`);
3694
+ } else {
3695
+ console.log(` Daemon: not running`);
3696
+ console.log(` Start with: srift daemon start`);
3697
+ }
3698
+ if (daemonRunning) {
3699
+ const s = sessionInfo?.session ?? sessionInfo;
3700
+ if (s?.id) {
3701
+ const peers = s.peerCount ?? s.peers ?? 0;
3702
+ console.log(`
3703
+ Session: ${s.id} role=${s.role ?? "?"} peers=${peers} connected=${s.isConnected ?? s.connected ?? "?"}`);
3704
+ } else {
3705
+ console.log(`
3706
+ Session: none (start one: srift session start)`);
3707
+ }
3708
+ if (transfersInfo.length > 0) {
3709
+ console.log(`
3710
+ Transfers: ${transfersInfo.length} active`);
3711
+ for (const t of transfersInfo.slice(0, 5)) {
3712
+ const pct = typeof t.progress === "number" ? `${t.progress.toFixed(1)}%` : "?%";
3713
+ console.log(` ${(t.fileId ?? t.id ?? "?").padEnd(32)} ${(t.name ?? "?").padEnd(24)} ${pct.padStart(6)} ${t.status ?? ""}`);
3714
+ }
3715
+ if (transfersInfo.length > 5) console.log(` \u2026 and ${transfersInfo.length - 5} more`);
3716
+ } else {
3717
+ console.log(`
3718
+ Transfers: none`);
3719
+ }
3720
+ }
3721
+ console.log("");
3722
+ }
3723
+ async function handleDaemonStatus(isJson) {
3724
+ let info = null;
3725
+ let running = false;
3726
+ try {
3727
+ info = await new Promise((resolve, reject) => {
3728
+ const req = http3.get(`${DAEMON_URL3}/health`, (res) => {
3729
+ let d = "";
3730
+ res.on("data", (c) => d += c);
3731
+ res.on("end", () => {
3732
+ try {
3733
+ resolve(JSON.parse(d));
3734
+ } catch {
3735
+ reject(new Error("bad json"));
3736
+ }
3737
+ });
3738
+ });
3739
+ req.on("error", reject);
3740
+ req.setTimeout(2e3, () => req.destroy(new Error("timeout")));
3741
+ });
3742
+ running = info.ok ?? true;
3743
+ } catch {
3744
+ }
3745
+ if (isJson) {
3746
+ console.log(JSON.stringify(running ? { running: true, ...info } : { running: false }, null, 2));
3747
+ return;
3748
+ }
3749
+ if (running) {
3750
+ const uptimeSec = Math.round((info?.uptime_ms ?? 0) / 1e3);
3751
+ console.log(`
3752
+ Daemon status: running`);
3753
+ console.log(` version ${info?.version ?? "?"}`);
3754
+ console.log(` port ${DAEMON_PORT3}`);
3755
+ console.log(` uptime ${uptimeSec}s`);
3756
+ console.log(` mcp ${info?.mcp ?? "?"}`);
3757
+ console.log(` webtorrent ${info?.webtorrent ?? "?"}`);
3758
+ } else {
3759
+ console.log(`
3760
+ Daemon status: not running`);
3761
+ console.log(` Start with: srift daemon start`);
3762
+ }
3763
+ console.log("");
3764
+ }
3765
+ async function handleDaemonRestart() {
3766
+ console.log("[srift] Restarting daemon...");
3767
+ try {
3768
+ await daemonRequest("POST", "/reset");
3769
+ } catch {
3770
+ }
3771
+ try {
3772
+ await handleDaemonStop(false);
3773
+ } catch {
3774
+ }
3775
+ await new Promise((r) => setTimeout(r, 500));
3776
+ await ensureDaemon();
3777
+ console.log("[srift] Daemon restarted successfully.");
3778
+ }
3779
+ async function handleReset(isJson) {
3780
+ let result;
3781
+ try {
3782
+ result = await daemonRequest("POST", "/reset");
3783
+ } catch (e) {
3784
+ if (isJson) {
3785
+ console.log(JSON.stringify({ ok: false, error: String(e) }));
3786
+ } else {
3787
+ console.error(`[srift] Reset failed: ${e}`);
3788
+ console.error(` Is the daemon running? Start it: srift daemon start`);
3789
+ }
3790
+ process.exit(1);
3791
+ }
3792
+ if (isJson) {
3793
+ console.log(JSON.stringify({ ok: true, ...result ?? {} }));
3794
+ } else {
3795
+ console.log(`[srift] Reset complete. Session state wiped, encryption keys flushed.`);
3796
+ }
3797
+ }
3798
+ async function handleLogs(tail, jsonStream) {
3799
+ let result;
3800
+ try {
3801
+ result = await daemonRequest("GET", `/logs?lines=${tail}&n=${tail}`);
3802
+ } catch (e) {
3803
+ console.error(`[srift] Cannot retrieve logs: ${e}`);
3804
+ console.error(` Is the daemon running? Start it: srift daemon start`);
3805
+ process.exit(1);
3806
+ }
3807
+ let lines = [];
3808
+ if (Array.isArray(result)) {
3809
+ lines = result.map((l) => typeof l === "string" ? l : JSON.stringify(l));
3810
+ } else if (result?.logs && Array.isArray(result.logs)) {
3811
+ lines = result.logs.map((l) => typeof l === "string" ? l : JSON.stringify(l));
3812
+ } else if (result?.raw && typeof result.raw === "string") {
3813
+ lines = result.raw.split("\n").filter(Boolean);
3814
+ } else {
3815
+ lines = [JSON.stringify(result)];
3816
+ }
3817
+ for (const line of lines) {
3818
+ if (jsonStream) {
3819
+ console.log(line);
3820
+ } else {
3821
+ try {
3822
+ const obj = JSON.parse(line);
3823
+ const ts = obj.ts ?? obj.time ?? obj.timestamp ?? "";
3824
+ const level = (obj.level ?? obj.severity ?? "info").toUpperCase();
3825
+ const msg = obj.msg ?? obj.message ?? obj.text ?? line;
3826
+ console.log(`${ts ? ts + " " : ""}[${level}] ${msg}`);
3827
+ } catch {
3828
+ console.log(line);
3829
+ }
3830
+ }
3831
+ }
3832
+ }
3833
+ function findAllSriftBinaries() {
3834
+ const binName = process.platform === "win32" ? "srift.exe" : "srift";
3835
+ const found = /* @__PURE__ */ new Set();
3836
+ const add = (p) => {
3837
+ if (!p) return;
3838
+ try {
3839
+ const abs = path3.resolve(p);
3840
+ if (fs3.existsSync(abs) && fs3.statSync(abs).isFile()) found.add(abs);
3841
+ } catch {
3842
+ }
3843
+ };
3844
+ const execBase = path3.basename(process.execPath || "").toLowerCase();
3845
+ if (execBase === "srift" || execBase === "srift.exe") add(process.execPath);
3846
+ add(path3.join(os2.homedir(), ".srift", "bin", binName));
3847
+ try {
3848
+ const cmd = process.platform === "win32" ? "where srift" : "which -a srift";
3849
+ const out = execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
3850
+ for (const line of out.split("\n").map((s) => s.trim()).filter(Boolean)) add(line);
3851
+ } catch {
3852
+ }
3853
+ const pathDirs = (process.env.PATH || "").split(path3.delimiter).filter(Boolean);
3854
+ if (process.platform === "win32") {
3855
+ try {
3856
+ const userPath = execSync(
3857
+ `powershell -NoProfile -Command "[System.Environment]::GetEnvironmentVariable('Path','User')"`,
3858
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
3859
+ ).trim();
3860
+ if (userPath) {
3861
+ for (const d of userPath.split(";")) if (d) pathDirs.push(d);
3862
+ }
3863
+ } catch {
3864
+ }
3865
+ }
3866
+ for (const dir of pathDirs) add(path3.join(dir, binName));
3867
+ return Array.from(found);
3868
+ }
3869
+ function purgeSriftFromPath() {
3870
+ if (process.platform === "win32") {
3871
+ try {
3872
+ const psCmd = [
3873
+ "$p=[System.Environment]::GetEnvironmentVariable('Path','User')",
3874
+ "if(-not $p){$p=''}",
3875
+ // Drop any entry containing 'srift' (case-insensitive). The CLI lives
3876
+ // at ~/.srift/bin so the substring is a reliable marker.
3877
+ "$new=($p -split ';' | Where-Object { $_ -and ($_ -notmatch '[Ss]rift') }) -join ';'",
3878
+ "[System.Environment]::SetEnvironmentVariable('Path',$new,'User')",
3879
+ // Broadcast so newly-opened cmd/PowerShell windows see the change
3880
+ `try{$s='[DllImport(\\"user32.dll\\")] public static extern IntPtr SendMessageTimeout(IntPtr h,uint m,UIntPtr w,string l,uint f,uint t,out UIntPtr r);';if(-not('W.M' -as [type])){Add-Type -MemberDefinition $s -Namespace W -Name M | Out-Null};$r=[UIntPtr]::Zero;[W.M]::SendMessageTimeout([IntPtr]0xffff,0x1A,[UIntPtr]::Zero,'Environment',0x0002,5000,[ref]$r) | Out-Null}catch{}`
3881
+ ].join(";");
3882
+ execSync(`powershell -NoProfile -Command "${psCmd}"`, { stdio: "ignore" });
3883
+ console.log("[srift] Cleaned all srift entries from Windows user PATH (+ broadcast to open windows).");
3884
+ } catch {
3885
+ }
3886
+ return;
3887
+ }
3888
+ const rcFiles = [
3889
+ path3.join(os2.homedir(), ".bashrc"),
3890
+ path3.join(os2.homedir(), ".zshrc"),
3891
+ path3.join(os2.homedir(), ".profile"),
3892
+ path3.join(os2.homedir(), ".bash_profile"),
3893
+ path3.join(os2.homedir(), ".config", "fish", "config.fish")
3894
+ ];
3895
+ for (const rc of rcFiles) {
3896
+ if (!fs3.existsSync(rc)) continue;
3897
+ try {
3898
+ const original = fs3.readFileSync(rc, "utf8");
3899
+ const filtered = original.split("\n").filter((l) => !/\.srift[\/\\]bin/.test(l) && l.trim() !== "# SRIFT" && !l.includes("fish_add_path ~/.srift")).join("\n");
3900
+ if (filtered !== original) {
3901
+ fs3.writeFileSync(rc, filtered);
3902
+ console.log(`[srift] Cleaned srift PATH entry from: ${rc}`);
3903
+ }
3904
+ } catch {
3905
+ }
3906
+ }
3907
+ }
3908
+ function scheduleWindowsDelete(binaryPath) {
3909
+ try {
3910
+ const winBin = binaryPath.replace(/\//g, "\\");
3911
+ const batPath = path3.join(os2.tmpdir(), `srift-uninstall-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.bat`);
3912
+ 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';
3913
+ fs3.writeFileSync(batPath, batBody, "utf8");
3914
+ spawn("cmd.exe", ["/C", batPath], {
3915
+ detached: true,
3916
+ stdio: "ignore",
3917
+ windowsHide: true,
3918
+ shell: false
3919
+ }).unref();
3920
+ return true;
3921
+ } catch {
3922
+ return false;
3923
+ }
3924
+ }
3925
+ function removeBinary(binaryPath) {
3926
+ try {
3927
+ fs3.unlinkSync(binaryPath);
3928
+ return { ok: true, deferred: false };
3929
+ } catch (e) {
3930
+ if (process.platform === "win32" && (e?.code === "EPERM" || e?.code === "EBUSY" || e?.code === "EACCES" || e?.code === "ENOTEMPTY")) {
3931
+ try {
3932
+ const renamed = `${binaryPath}.old-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3933
+ fs3.renameSync(binaryPath, renamed);
3934
+ scheduleWindowsDelete(renamed);
3935
+ return { ok: true, deferred: true };
3936
+ } catch {
3937
+ const scheduled = scheduleWindowsDelete(binaryPath);
3938
+ return { ok: scheduled, deferred: scheduled };
3939
+ }
3940
+ }
3941
+ return { ok: false, deferred: false };
3942
+ }
3943
+ }
3944
+ async function handleUninstall(purge) {
3945
+ console.log("[srift] Starting uninstall...");
3946
+ const running = await isDaemonRunning();
3947
+ if (running) {
3948
+ console.log("[srift] Stopping daemon...");
3949
+ try {
3950
+ await handleDaemonStop(false);
3951
+ } catch {
3952
+ }
3953
+ await new Promise((r) => setTimeout(r, 500));
3954
+ }
3955
+ const binaries = findAllSriftBinaries();
3956
+ if (binaries.length === 0) {
3957
+ console.log("[srift] No srift binaries found on disk.");
3958
+ } else {
3959
+ console.log(`[srift] Found ${binaries.length} srift binar${binaries.length === 1 ? "y" : "ies"} on disk:`);
3960
+ for (const b of binaries) console.log(` ${b}`);
3961
+ }
3962
+ let removedNow = 0;
3963
+ let deferred = 0;
3964
+ let failed = [];
3965
+ for (const b of binaries) {
3966
+ const r = removeBinary(b);
3967
+ if (r.ok && !r.deferred) {
3968
+ removedNow++;
3969
+ } else if (r.deferred) {
3970
+ deferred++;
3971
+ } else {
3972
+ failed.push(b);
3973
+ }
3974
+ }
3975
+ if (removedNow > 0) console.log(`[srift] Removed ${removedNow} binar${removedNow === 1 ? "y" : "ies"} immediately.`);
3976
+ if (deferred > 0) console.log(`[srift] Scheduled ${deferred} locked binar${deferred === 1 ? "y" : "ies"} for deletion ~2-3s after this command exits.`);
3977
+ if (failed.length) console.error(`[srift] Could not remove ${failed.length}:`), failed.forEach((f) => console.error(` ${f}`));
3978
+ const standardInstallDir = path3.join(os2.homedir(), ".srift", "bin");
3979
+ if (fs3.existsSync(standardInstallDir)) {
3980
+ try {
3981
+ for (const f of fs3.readdirSync(standardInstallDir)) {
3982
+ const fp = path3.join(standardInstallDir, f);
3983
+ try {
3984
+ fs3.unlinkSync(fp);
3985
+ } catch {
3986
+ }
3987
+ }
3988
+ } catch {
3989
+ }
3990
+ }
3991
+ if (purge) {
3992
+ const sriftDir = path3.join(os2.homedir(), ".srift");
3993
+ if (fs3.existsSync(sriftDir)) {
3994
+ try {
3995
+ fs3.rmSync(sriftDir, { recursive: true, force: true });
3996
+ console.log(`[srift] Purged directory: ${sriftDir}`);
3997
+ } catch (e) {
3998
+ if (process.platform === "win32") {
3999
+ const batPath = path3.join(os2.tmpdir(), `srift-purge-${process.pid}-${Date.now()}.bat`);
4000
+ const winDir = sriftDir.replace(/\//g, "\\");
4001
+ 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';
4002
+ try {
4003
+ fs3.writeFileSync(batPath, batBody, "utf8");
4004
+ spawn("cmd.exe", ["/C", batPath], { detached: true, stdio: "ignore", windowsHide: true, shell: false }).unref();
4005
+ console.log(`[srift] Scheduled purge of ${sriftDir} (~3s after exit).`);
4006
+ } catch (ee) {
4007
+ console.error(`[srift] Could not schedule purge: ${ee?.message || ee}`);
4008
+ }
4009
+ } else {
4010
+ console.error(`[srift] Could not purge ${sriftDir}: ${e?.message || e}`);
4011
+ }
4012
+ }
4013
+ }
4014
+ }
4015
+ purgeSriftFromPath();
4016
+ if (process.platform === "win32") {
4017
+ try {
4018
+ const tmp = os2.tmpdir();
4019
+ for (const f of fs3.readdirSync(tmp)) {
4020
+ if (/^srift-(uninstall|update|purge)-.*\.bat$/.test(f)) {
4021
+ try {
4022
+ fs3.unlinkSync(path3.join(tmp, f));
4023
+ } catch {
4024
+ }
4025
+ }
4026
+ }
4027
+ } catch {
4028
+ }
4029
+ }
4030
+ console.log(`
4031
+ [srift] Uninstall complete.`);
4032
+ if (!purge) {
4033
+ console.log(` Config + data at ~/.srift/ preserved. Re-run with --purge to also delete them.`);
4034
+ }
4035
+ console.log(` Reinstall:`);
4036
+ console.log(` macOS/Linux/WSL: curl -fsSL https://srift.app/install.sh | sh`);
4037
+ console.log(` Windows PS: irm https://srift.app/install.ps1 | iex`);
4038
+ console.log(` Windows cmd: powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://srift.app/install.ps1 | iex"`);
4039
+ }
4040
+ var ALL_COMMANDS = [
4041
+ "daemon",
4042
+ "session",
4043
+ "send",
4044
+ "receive",
4045
+ "list",
4046
+ "monitor",
4047
+ "approve",
4048
+ "reject",
4049
+ "kick",
4050
+ "chat",
4051
+ "mcp",
4052
+ "quick-share",
4053
+ "share",
4054
+ "pubshare",
4055
+ "links",
4056
+ "install-mcp",
4057
+ "install",
4058
+ "info",
4059
+ "version",
4060
+ "self-update",
4061
+ "update",
4062
+ "doctor",
4063
+ "config",
4064
+ "status",
4065
+ "reset",
4066
+ "logs",
4067
+ "uninstall"
4068
+ ];
4069
+ function suggestCommand(input) {
4070
+ const prefixMatch = ALL_COMMANDS.find((c) => c.startsWith(input) || input.startsWith(c.slice(0, 3)));
4071
+ if (prefixMatch) return prefixMatch;
4072
+ for (const cmd of ALL_COMMANDS) {
4073
+ let diffs = 0;
4074
+ const shorter = input.length < cmd.length ? input : cmd;
4075
+ const longer = input.length < cmd.length ? cmd : input;
4076
+ for (let i = 0; i < shorter.length; i++) {
4077
+ if (shorter[i] !== longer[i]) diffs++;
4078
+ }
4079
+ diffs += Math.abs(input.length - cmd.length);
4080
+ if (diffs <= 2) return cmd;
4081
+ }
4082
+ return null;
4083
+ }
4084
+ async function main() {
4085
+ const args = process.argv.slice(2);
4086
+ const command = args[0];
4087
+ if (!command || command === "--help" || command === "-h") {
4088
+ printHelp();
4089
+ return;
4090
+ }
4091
+ const isJson = args.includes("--json");
4092
+ const noDaemon = args.includes("--no-daemon");
4093
+ switch (command) {
4094
+ case "status": {
4095
+ await handleStatus(isJson);
4096
+ break;
4097
+ }
4098
+ case "reset": {
4099
+ await handleReset(isJson);
4100
+ break;
4101
+ }
4102
+ case "logs": {
4103
+ const tailIdx = args.indexOf("--tail");
4104
+ const tail = tailIdx !== -1 ? parseInt(args[tailIdx + 1] ?? "50", 10) : 50;
4105
+ const jsonStream = args.includes("--json-stream");
4106
+ await handleLogs(tail, jsonStream);
4107
+ break;
4108
+ }
4109
+ case "uninstall": {
4110
+ const purge = args.includes("--purge");
4111
+ await handleUninstall(purge);
4112
+ break;
4113
+ }
4114
+ case "daemon": {
4115
+ const subCommand = args[1];
4116
+ if (subCommand === "start") {
4117
+ await Promise.resolve().then(() => (init_daemon(), daemon_exports));
4118
+ } else if (subCommand === "stop") {
4119
+ await handleDaemonStop(isJson);
4120
+ } else if (subCommand === "restart") {
4121
+ await handleDaemonRestart();
4122
+ } else if (subCommand === "status") {
4123
+ await handleDaemonStatus(isJson);
4124
+ } else {
4125
+ console.error("Usage: srift daemon [start|stop|restart|status]");
4126
+ process.exit(1);
4127
+ }
4128
+ break;
4129
+ }
4130
+ case "session": {
4131
+ const subCommand = args[1];
4132
+ await ensureDaemon();
4133
+ if (subCommand === "start") {
4134
+ const nameIdx = args.indexOf("--name");
4135
+ const name = nameIdx !== -1 ? args[nameIdx + 1] : void 0;
4136
+ const secretIdx = args.indexOf("--room-secret");
4137
+ const secret = secretIdx !== -1 ? args[secretIdx + 1] : void 0;
4138
+ await handleSessionStart(name, secret, isJson);
4139
+ } else if (subCommand === "join") {
4140
+ const sessionId = args[2];
4141
+ if (!sessionId) {
4142
+ console.error("Usage: srift session join <session-id> [--username <username>] [--room-secret <secret>]");
4143
+ process.exit(1);
4144
+ }
4145
+ const userIdx = args.indexOf("--username");
4146
+ const username = userIdx !== -1 ? args[userIdx + 1] : void 0;
4147
+ const secretIdx = args.indexOf("--room-secret");
4148
+ const secret = secretIdx !== -1 ? args[secretIdx + 1] : void 0;
4149
+ await handleSessionJoin(sessionId, username, secret, isJson);
4150
+ } else if (subCommand === "status") {
4151
+ await handleSessionStatus(isJson);
4152
+ } else if (subCommand === "close") {
4153
+ await handleSessionClose(isJson);
4154
+ } else {
4155
+ console.error("Unknown session command. Try: start, join, status, close");
4156
+ process.exit(1);
4157
+ }
4158
+ break;
4159
+ }
4160
+ case "send": {
4161
+ const filePath = args[1];
4162
+ if (!filePath) {
4163
+ console.error("Usage: srift send <filepath> [--json]");
4164
+ process.exit(1);
4165
+ }
4166
+ const protocolIdx = args.indexOf("--protocol");
4167
+ const protocol = protocolIdx !== -1 ? args[protocolIdx + 1] : void 0;
4168
+ await ensureDaemon();
4169
+ await handleSendFile(filePath, protocol, isJson);
4170
+ break;
4171
+ }
4172
+ case "receive": {
4173
+ const fileId = args[1];
4174
+ if (!fileId) {
4175
+ console.error("Usage: srift receive <file-id> [--save-dir <dir>] [--json]");
4176
+ process.exit(1);
4177
+ }
4178
+ const dirIdx = args.indexOf("--save-dir");
4179
+ const saveDir = dirIdx !== -1 ? args[dirIdx + 1] : void 0;
4180
+ await ensureDaemon();
4181
+ await handleReceiveFile(fileId, saveDir, isJson);
4182
+ break;
4183
+ }
4184
+ case "list": {
4185
+ await ensureDaemon();
4186
+ await handleListTransfers(isJson);
4187
+ break;
4188
+ }
4189
+ case "monitor": {
4190
+ const fileId = args[1];
4191
+ if (!fileId) {
4192
+ console.error("Usage: srift monitor <file-id> [--json-stream]");
4193
+ process.exit(1);
4194
+ }
4195
+ const jsonStream = args.includes("--json-stream");
4196
+ await ensureDaemon();
4197
+ handleMonitorTransfer(fileId, jsonStream);
4198
+ break;
4199
+ }
4200
+ case "approve": {
4201
+ const tempUserId = args[1];
4202
+ if (!tempUserId) {
4203
+ console.error("Usage: srift approve <temp-user-id> [--json]");
4204
+ process.exit(1);
4205
+ }
4206
+ await ensureDaemon();
4207
+ await handleApproveJoin(tempUserId, isJson);
4208
+ break;
4209
+ }
4210
+ case "reject": {
4211
+ const tempUserId = args[1];
4212
+ if (!tempUserId) {
4213
+ console.error("Usage: srift reject <temp-user-id> [--reason <reason>] [--json]");
4214
+ process.exit(1);
4215
+ }
4216
+ const reasonIdx = args.indexOf("--reason");
4217
+ const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : void 0;
4218
+ await ensureDaemon();
4219
+ await handleRejectJoin(tempUserId, reason, isJson);
4220
+ break;
4221
+ }
4222
+ case "kick": {
4223
+ const userId = args[1];
4224
+ if (!userId) {
4225
+ console.error("Usage: srift kick <user-id> [--json]");
4226
+ process.exit(1);
4227
+ }
4228
+ await ensureDaemon();
4229
+ await handleKickUser(userId, isJson);
4230
+ break;
4231
+ }
4232
+ case "chat": {
4233
+ const subCommand = args[1];
4234
+ await ensureDaemon();
4235
+ if (subCommand === "send") {
4236
+ const msg = args[2];
4237
+ if (!msg) {
4238
+ console.error('Usage: srift chat send "<message>" [--json]');
4239
+ process.exit(1);
4240
+ }
4241
+ await handleChatSend(msg, isJson);
4242
+ } else if (subCommand === "history") {
4243
+ await handleChatHistory(isJson);
4244
+ } else {
4245
+ console.error("Unknown chat command. Try: send, history");
4246
+ process.exit(1);
4247
+ }
4248
+ break;
4249
+ }
4250
+ case "mcp": {
4251
+ await ensureDaemon();
4252
+ startMcpServer();
4253
+ break;
4254
+ }
4255
+ case "quick-share":
4256
+ case "share": {
4257
+ const filePath = args[1];
4258
+ if (!filePath) {
4259
+ console.error("Usage: srift quick-share <filepath> [--name <s>] [--max-downloads <N>] [--ttl <dur>] [--once] [--json]");
4260
+ console.error(" --ttl examples: 30s, 15m, 2h, 1d");
4261
+ process.exit(1);
4262
+ }
4263
+ const nameIdx = args.indexOf("--name");
4264
+ const sessionName = nameIdx !== -1 ? args[nameIdx + 1] : void 0;
4265
+ const maxIdx = args.indexOf("--max-downloads");
4266
+ let maxDownloads = maxIdx !== -1 ? parseInt(args[maxIdx + 1], 10) : 0;
4267
+ if (args.includes("--once")) maxDownloads = 1;
4268
+ const ttlIdx = args.indexOf("--ttl");
4269
+ const ttlMs = ttlIdx !== -1 ? parseDuration(args[ttlIdx + 1]) : 0;
4270
+ if (ttlIdx !== -1 && ttlMs <= 0) {
4271
+ console.error(`Invalid --ttl: "${args[ttlIdx + 1]}". Use 30s, 15m, 2h, 1d.`);
4272
+ process.exit(1);
4273
+ }
4274
+ await ensureDaemon();
4275
+ await handleQuickShare(filePath, sessionName, isJson, { maxDownloads, ttlMs });
4276
+ break;
4277
+ }
4278
+ case "pubshare":
4279
+ case "links": {
4280
+ const sub = args[1];
4281
+ await ensureDaemon();
4282
+ if (sub === "list" || !sub) {
4283
+ await handlePubshareList(isJson);
4284
+ } else if (sub === "revoke") {
4285
+ const token = args[2];
4286
+ if (!token) {
4287
+ console.error("Usage: srift pubshare revoke <token> [--json]");
4288
+ process.exit(1);
4289
+ }
4290
+ await handlePubshareRevoke(token, isJson);
4291
+ } else if (sub === "add") {
4292
+ const fp = args[2];
4293
+ if (!fp) {
4294
+ console.error("Usage: srift pubshare add <filepath> [--max-downloads N] [--ttl dur] [--once] [--json]");
4295
+ process.exit(1);
4296
+ }
4297
+ const maxIdx = args.indexOf("--max-downloads");
4298
+ let maxDownloads = maxIdx !== -1 ? parseInt(args[maxIdx + 1], 10) : 0;
4299
+ if (args.includes("--once")) maxDownloads = 1;
4300
+ const ttlIdx = args.indexOf("--ttl");
4301
+ const ttlMs = ttlIdx !== -1 ? parseDuration(args[ttlIdx + 1]) : 0;
4302
+ await handlePubshareAdd(fp, isJson, { maxDownloads, ttlMs });
4303
+ } else {
4304
+ console.error("Usage: srift pubshare [list|add <file>|revoke <token>]");
4305
+ process.exit(1);
4306
+ }
4307
+ break;
4308
+ }
4309
+ case "install-mcp":
4310
+ case "install": {
4311
+ if (args.includes("--auto")) {
4312
+ await handleAutoInstallMcp(isJson);
4313
+ } else {
4314
+ printInstallMcpInstructions();
4315
+ }
4316
+ break;
4317
+ }
4318
+ case "bootstrap": {
4319
+ const pathArg = args[1] && !args[1].startsWith("--") ? args[1] : void 0;
4320
+ const cursorrules = args.includes("--cursorrules");
4321
+ const agents = args.includes("--agents");
4322
+ await handleBootstrap(pathArg, { cursorrules, agents });
4323
+ break;
4324
+ }
4325
+ case "info": {
4326
+ await ensureDaemon();
4327
+ printAgentInfo();
4328
+ break;
4329
+ }
4330
+ case "version":
4331
+ case "--version":
4332
+ case "-v": {
4333
+ await handleVersion(isJson);
4334
+ if (!isJson) {
4335
+ checkForUpdate(false).catch(() => {
4336
+ });
4337
+ }
4338
+ break;
4339
+ }
4340
+ case "self-update":
4341
+ case "update": {
4342
+ await handleSelfUpdate(isJson);
4343
+ break;
4344
+ }
4345
+ case "doctor": {
4346
+ await handleDoctor(isJson);
4347
+ break;
4348
+ }
4349
+ case "config": {
4350
+ await handleConfig(args.slice(1), isJson);
4351
+ break;
4352
+ }
4353
+ default: {
4354
+ const suggestion = suggestCommand(command);
4355
+ if (suggestion) {
4356
+ console.error(`Unknown command: "${command}". Did you mean: srift ${suggestion}`);
4357
+ } else {
4358
+ console.error(`Unknown command: "${command}"`);
4359
+ }
4360
+ console.error(`Run "srift --help" for usage.`);
4361
+ process.exit(1);
4362
+ }
4363
+ }
4364
+ checkForUpdate(false).catch(() => {
4365
+ });
4366
+ }
4367
+ function printHelp() {
4368
+ console.log(`
4369
+ SRIFT v${CLI_VERSION} \u2014 Headless P2P file transfer + MCP server
4370
+
4371
+ Install: curl -fsSL https://srift.app/install.sh | sh
4372
+ Update: srift self-update
4373
+ Docs: https://srift.app/ai-agents
4374
+
4375
+ \u2500\u2500\u2500 Daemon \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4376
+ srift daemon start Start in foreground (port ${DAEMON_PORT3})
4377
+ srift daemon stop Stop background daemon
4378
+ srift daemon restart Stop then re-start daemon
4379
+ srift daemon status [--json] Show daemon health (version, uptime, mcp)
4380
+
4381
+ \u2500\u2500\u2500 Status & Diagnostics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4382
+ srift status [--json] Unified: daemon + session + transfers
4383
+ srift doctor [--json] Full health check (daemon, network, version)
4384
+ srift logs [--tail <n>] [--json-stream] View daemon logs (default: last 50 lines)
4385
+
4386
+ \u2500\u2500\u2500 Sessions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4387
+ srift session start [--name <name>] [--room-secret <secret>] [--json]
4388
+ srift session join <session-id> [--username <u>] [--room-secret <s>] [--json]
4389
+ srift session status [--json]
4390
+ srift session close [--json]
4391
+
4392
+ \u2500\u2500\u2500 Transfers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4393
+ srift quick-share <filepath> Public download link \u2014 open in
4394
+ [--name <session>] any browser / curl / wget.
4395
+ [--max-downloads <N>] Cap after N successful downloads
4396
+ [--ttl <30s|15m|2h|1d>] Auto-expire link after duration
4397
+ [--once] Shorthand for --max-downloads 1
4398
+ [--json]
4399
+ srift pubshare list [--json] Active public download links
4400
+ srift pubshare add <filepath> [--max-downloads N] [--ttl dur] [--once]
4401
+ Add another public link in the
4402
+ current session
4403
+ srift pubshare revoke <token> [--json] Invalidate a link immediately
4404
+ srift send <filepath> [--json] In-session offer for joined peers
4405
+ srift receive <file-id> [--save-dir <dir>] [--json]
4406
+ srift list [--json]
4407
+ srift monitor <file-id> [--json-stream]
4408
+
4409
+ \u2500\u2500\u2500 Host Controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4410
+ srift approve <temp-user-id> [--json]
4411
+ srift reject <temp-user-id> [--reason <reason>] [--json]
4412
+ srift kick <user-id> [--json]
4413
+
4414
+ \u2500\u2500\u2500 Chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4415
+ srift chat send "<message>" [--json]
4416
+ srift chat history [--json]
4417
+
4418
+ \u2500\u2500\u2500 AI / Agent Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4419
+ srift install-mcp Print config snippets for IDEs
4420
+ srift install-mcp --auto Automatically install to Claude Desktop
4421
+ srift bootstrap [dir] Bootstrap .cursorrules & AGENTS.md in [dir]
4422
+ [--cursorrules] Only bootstrap .cursorrules
4423
+ [--agents] Only bootstrap AGENTS.md
4424
+ srift info Zero-config quick reference
4425
+
4426
+ \u2500\u2500\u2500 MCP Server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4427
+ srift mcp stdio transport
4428
+ HTTP: POST http://127.0.0.1:${DAEMON_PORT3}/mcp streamable HTTP (MCP 2025-06-18)
4429
+ SSE: GET http://127.0.0.1:${DAEMON_PORT3}/mcp/sse
4430
+
4431
+ \u2500\u2500\u2500 Maintenance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4432
+ srift version [--json]
4433
+ srift self-update [--json] Atomic in-place binary update
4434
+ srift reset [--json] Wipe daemon session state + flush keys
4435
+ srift config [get|set|delete] [key] [value] Manage ~/.srift/config.json
4436
+ srift uninstall [--purge] Remove srift binary (--purge also deletes ~/.srift/)
4437
+
4438
+ Flags (global):
4439
+ --json Machine-readable JSON output
4440
+ --no-daemon Skip auto-starting daemon (status-only commands)
4441
+
4442
+ Env vars:
4443
+ SRIFT_DAEMON_PORT=3822 Change daemon port
4444
+ SRIFT_NO_UPDATE_CHECK=1 Disable background update checks
4445
+ `);
4446
+ }
4447
+ function printInstallMcpInstructions() {
4448
+ const claudeWin = `${process.env.APPDATA || "%APPDATA%"}\\Claude\\claude_desktop_config.json`;
4449
+ const execPath = process.execPath || "";
4450
+ const execBase = path3.basename(execPath).toLowerCase();
4451
+ const isCompiledBinary = execBase === "srift" || execBase === "srift.exe" || typeof process.isBun === "boolean" && process.isBun || process.versions && process.versions.bun !== void 0;
4452
+ let cmd;
4453
+ let argList;
4454
+ let cursorCmd;
4455
+ if (isCompiledBinary) {
4456
+ const sriftPath = execPath.replace(/\\/g, "/");
4457
+ cmd = sriftPath;
4458
+ argList = `["mcp"]`;
4459
+ cursorCmd = `${sriftPath} mcp`;
4460
+ } else {
4461
+ const scriptPath = path3.resolve(__dirname, "index.ts").replace(/\\/g, "/");
4462
+ cmd = `node`;
4463
+ argList = `["--experimental-strip-types","${scriptPath}","mcp"]`;
4464
+ cursorCmd = `node --experimental-strip-types ${scriptPath} mcp`;
4465
+ }
4466
+ console.log(`
4467
+ SRIFT MCP \u2014 Universal Install Snippets
4468
+ ======================================
4469
+
4470
+ # Claude Desktop (Windows: ${claudeWin})
4471
+ # macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
4472
+ # Linux: ~/.config/Claude/claude_desktop_config.json
4473
+ {
4474
+ "mcpServers": {
4475
+ "srift": {
4476
+ "command": "${cmd}",
4477
+ "args": ${argList}
4478
+ }
4479
+ }
4480
+ }
4481
+
4482
+ # Cursor IDE (Settings \u2192 Features \u2192 MCP \u2192 Add)
4483
+ Name: srift
4484
+ Type: stdio
4485
+ Command: ${cursorCmd}
4486
+
4487
+ # Continue.dev (~/.continue/config.json \u2192 "mcpServers")
4488
+ { "command": "${cmd}", "args": ${argList} }
4489
+
4490
+ # Codex CLI / Zed / Aider / any MCP-aware client
4491
+ Same command/args as above.
4492
+
4493
+ # Cloud agents (ChatGPT Custom GPTs, OpenAI Apps, n8n, Zapier, etc.):
4494
+ HTTP MCP endpoint: POST http://127.0.0.1:${DAEMON_PORT3}/mcp
4495
+ OpenAPI spec: GET http://127.0.0.1:${DAEMON_PORT3}/openapi.json
4496
+ AI Plugin: GET http://127.0.0.1:${DAEMON_PORT3}/.well-known/ai-plugin.json
4497
+ A2A discovery: GET http://127.0.0.1:${DAEMON_PORT3}/.well-known/agent.json
4498
+ MCP server card: GET http://127.0.0.1:${DAEMON_PORT3}/.well-known/mcp/server-card.json
4499
+
4500
+ No tokens. No auth. Localhost-only by default.
4501
+ `);
4502
+ }
4503
+ function printAgentInfo() {
4504
+ console.log(`
4505
+ SRIFT for AI Agents \u2014 Zero-Config Quick Reference
4506
+ =================================================
4507
+
4508
+ You are running inside the SRIFT workspace. A local daemon is up at:
4509
+ http://127.0.0.1:${DAEMON_PORT3}
4510
+
4511
+ Everything below works without tokens, OAuth, or API keys.
4512
+
4513
+ \u2500\u2500\u2500\u2500 Fastest path to deliver a file to the user \u2500\u2500\u2500\u2500
4514
+ srift quick-share /abs/path/to/file
4515
+ \u2192 prints a direct download URL: https://srift.app/d/<token>
4516
+ \u2192 the user opens it in any browser, or downloads via:
4517
+ curl -OJ https://srift.app/d/<token>
4518
+ wget --content-disposition https://srift.app/d/<token>
4519
+ \u2192 keep the daemon running while they download
4520
+
4521
+ \u2500\u2500\u2500\u2500 Open a long-lived collaboration room \u2500\u2500\u2500\u2500
4522
+ srift session start --name "AI-Collab"
4523
+ (give the user the share URL; approve their join with 'srift approve')
4524
+
4525
+ \u2500\u2500\u2500\u2500 Watch progress without polling \u2500\u2500\u2500\u2500
4526
+ Read .srift-state.json (auto-updated on every event)
4527
+ Or subscribe to SSE: GET http://127.0.0.1:${DAEMON_PORT3}/api/v1/monitor/events
4528
+
4529
+ \u2500\u2500\u2500\u2500 Connect any AI / MCP client \u2500\u2500\u2500\u2500
4530
+ Stdio: srift mcp
4531
+ HTTP MCP: POST http://127.0.0.1:${DAEMON_PORT3}/mcp
4532
+ REST/OpenAPI: http://127.0.0.1:${DAEMON_PORT3}/openapi.json
4533
+
4534
+ \u2500\u2500\u2500\u2500 Discovery files this project ships \u2500\u2500\u2500\u2500
4535
+ ./AGENTS.md \u2014 universal instructions for any AI
4536
+ ./.cursorrules \u2014 Cursor-specific
4537
+ ./.agents/AGENTS.md \u2014 fallback path some clients check
4538
+ ./ai-instructions.md \u2014 full spec
4539
+ ./public/llms.txt \u2014 for LLM crawlers
4540
+ ./public/.well-known/ \u2014 MCP card, AI plugin manifest, A2A, etc.
4541
+
4542
+ Crypto: AES-256-GCM + PBKDF2-SHA256 (100k iter). Keys never leave the device.
4543
+ `);
4544
+ }
4545
+ main();