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/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/daemon.js +2259 -0
- package/dist/index.js +4545 -0
- package/package.json +68 -0
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();
|