@decentnetwork/lan 0.1.107 → 0.1.109
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/cli/commands.js +1 -0
- package/dist/console/console.js +37 -10
- package/dist/daemon/message-store.d.ts +13 -0
- package/dist/daemon/message-store.js +7 -1
- package/dist/daemon/server.js +8 -0
- package/dist/ui/desktop/app.js +112 -34
- package/dist/ui/server.d.ts +3 -0
- package/dist/ui/server.js +64 -0
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/commands.js
CHANGED
|
@@ -1176,6 +1176,7 @@ export async function cmdUi(args) {
|
|
|
1176
1176
|
call: (req) => ipcCall(config, req),
|
|
1177
1177
|
routesPath: resolve(dir, "routes.yaml"),
|
|
1178
1178
|
doraRosterPath,
|
|
1179
|
+
downloadsDir: resolve(dir, "downloads"),
|
|
1179
1180
|
meExtra: {
|
|
1180
1181
|
lanVer: readJsonVer(join(moduleDir, "..", "..", "package.json")),
|
|
1181
1182
|
peerVer,
|
package/dist/console/console.js
CHANGED
|
@@ -5,6 +5,7 @@ import { render } from "ink";
|
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { Box as Box2, Text as Text2, useApp, useInput, useStdout } from "ink";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
|
|
9
10
|
// src/console/theme.ts
|
|
10
11
|
var PALETTES = {
|
|
@@ -274,7 +275,7 @@ function HelpBar({ th, T, focus, overlay }) {
|
|
|
274
275
|
if (focus === "input") {
|
|
275
276
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
276
277
|
/* @__PURE__ */ jsx(Text, { color: th.accent, children: "\u25CF input " }),
|
|
277
|
-
/* @__PURE__ */ jsx(Text, { color: th.dim, children: `type to compose \xB7 \u21B5 ${focusSend(T)} \xB7 esc/< ${T.friends.toLowerCase()} \xB7 Tab
|
|
278
|
+
/* @__PURE__ */ jsx(Text, { color: th.dim, children: `type to compose \xB7 \u21B5 ${focusSend(T)} \xB7 esc/< ${T.friends.toLowerCase()} \xB7 Tab \xB7 /send <path> /q /r /add /alias` })
|
|
278
279
|
] });
|
|
279
280
|
}
|
|
280
281
|
const items = [["\u2191\u2193", T.move], ["\u21B5/>", T.chat], ["r", T.reqShort], ["d", T.remove], ["a", T.alias], ["+", T.add], ["q", T.quit]];
|
|
@@ -520,6 +521,16 @@ function App({ client }) {
|
|
|
520
521
|
clipboardWrite(me.userId);
|
|
521
522
|
flashCopied();
|
|
522
523
|
}
|
|
524
|
+
} else if (c === "send" || c === "file") {
|
|
525
|
+
if (friend && arg) {
|
|
526
|
+
const id = friend.id;
|
|
527
|
+
const path = arg.replace(/^~(?=$|\/)/, homedir());
|
|
528
|
+
const base = path.split("/").pop() || "file";
|
|
529
|
+
setThreads((t) => ({ ...t, [id]: [...t[id] || [], { t: now(), who: "me", text: `\u{1F4CE} ${base} \u2026` }] }));
|
|
530
|
+
void client.sendFile(id, path).then((r) => {
|
|
531
|
+
if (!r.ok) setThreads((t) => ({ ...t, [id]: [...t[id] || [], { t: now(), who: "me", text: `\u26A0 ${r.error || "send failed"}` }] }));
|
|
532
|
+
}).catch(() => void 0);
|
|
533
|
+
}
|
|
523
534
|
}
|
|
524
535
|
};
|
|
525
536
|
const n = friends.length;
|
|
@@ -642,16 +653,16 @@ function now() {
|
|
|
642
653
|
// src/console/data.ts
|
|
643
654
|
import { createConnection } from "node:net";
|
|
644
655
|
import { resolve as resolve2 } from "node:path";
|
|
645
|
-
import { homedir as
|
|
656
|
+
import { homedir as homedir3 } from "node:os";
|
|
646
657
|
import { createRequire } from "node:module";
|
|
647
658
|
|
|
648
659
|
// src/config/loader.ts
|
|
649
660
|
import { readFileSync, existsSync } from "fs";
|
|
650
661
|
import { resolve, dirname } from "path";
|
|
651
662
|
import { mkdirSync } from "fs";
|
|
652
|
-
import { homedir } from "os";
|
|
663
|
+
import { homedir as homedir2 } from "os";
|
|
653
664
|
import yaml from "js-yaml";
|
|
654
|
-
var DEFAULT_CONFIG_DIR = resolve(
|
|
665
|
+
var DEFAULT_CONFIG_DIR = resolve(homedir2(), ".agentnet");
|
|
655
666
|
var DEFAULT_CONFIG_FILE = resolve(DEFAULT_CONFIG_DIR, "config.yaml");
|
|
656
667
|
var DEFAULT_BOOTSTRAP_NODES = [
|
|
657
668
|
// US-East — closest for typical North-American peers.
|
|
@@ -873,6 +884,13 @@ var ConfigLoader = class {
|
|
|
873
884
|
};
|
|
874
885
|
|
|
875
886
|
// src/console/data.ts
|
|
887
|
+
function fileSize(n) {
|
|
888
|
+
if (!n || n < 0) return "0 B";
|
|
889
|
+
if (n < 1024) return `${n} B`;
|
|
890
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
891
|
+
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
892
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
893
|
+
}
|
|
876
894
|
function hhmm(ts) {
|
|
877
895
|
if (!ts) return "";
|
|
878
896
|
const d = new Date(ts);
|
|
@@ -891,7 +909,7 @@ var DaemonClient = class _DaemonClient {
|
|
|
891
909
|
this.sockPath = sockPath;
|
|
892
910
|
}
|
|
893
911
|
static async create(configDir) {
|
|
894
|
-
const dir = configDir ?? resolve2(
|
|
912
|
+
const dir = configDir ?? resolve2(homedir3(), ".agentnet");
|
|
895
913
|
const config = await ConfigLoader.load(resolve2(dir, "config.yaml"));
|
|
896
914
|
const dataDir = (config.carrier.dataDir || resolve2(dir, "carrier")).replace(/\/+$/, "");
|
|
897
915
|
return new _DaemonClient(`${dataDir}/daemon.sock`);
|
|
@@ -1034,15 +1052,24 @@ var DaemonClient = class _DaemonClient {
|
|
|
1034
1052
|
const r = await this.call({ op: "chat-history", userid }).catch(() => ({ ok: false, error: "x" }));
|
|
1035
1053
|
const chats = _DaemonClient.data(r).chats ?? {};
|
|
1036
1054
|
const arr = chats[userid] ?? [];
|
|
1037
|
-
return arr.map((m) =>
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1055
|
+
return arr.map((m) => {
|
|
1056
|
+
const file = m.file;
|
|
1057
|
+
const text = file ? `\u{1F4CE} ${file.name ?? "file"} (${fileSize(typeof file.size === "number" ? file.size : 0)})` : String(m.text ?? "");
|
|
1058
|
+
return {
|
|
1059
|
+
t: hhmm(typeof m.ts === "number" ? m.ts : void 0),
|
|
1060
|
+
who: m.dir === "out" ? "me" : "them",
|
|
1061
|
+
text
|
|
1062
|
+
};
|
|
1063
|
+
});
|
|
1042
1064
|
}
|
|
1043
1065
|
send(userid, text) {
|
|
1044
1066
|
return this.call({ op: "chat-send", userid, text });
|
|
1045
1067
|
}
|
|
1068
|
+
/** Send a local file to a friend (toxcore transfer); recipient auto-saves to
|
|
1069
|
+
* ~/.agentnet/downloads. The 30s timeout covers reading + offering a file. */
|
|
1070
|
+
sendFile(userid, path) {
|
|
1071
|
+
return this.call({ op: "file-send", userid, path }, 3e4);
|
|
1072
|
+
}
|
|
1046
1073
|
markRead(userid) {
|
|
1047
1074
|
return this.call({ op: "chat-mark-read", userid });
|
|
1048
1075
|
}
|
|
@@ -14,6 +14,13 @@ export interface ChatMessage {
|
|
|
14
14
|
ts: number;
|
|
15
15
|
/** Stable per-message id (ts + per-process sequence) for UI keys / dedup. */
|
|
16
16
|
id: string;
|
|
17
|
+
/** Present when this entry is a file transfer rather than a text message.
|
|
18
|
+
* `name`/`size` describe the file; received files (dir:"in") are saved to
|
|
19
|
+
* <configDir>/downloads/<name> and downloadable via the UI. */
|
|
20
|
+
file?: {
|
|
21
|
+
name: string;
|
|
22
|
+
size: number;
|
|
23
|
+
};
|
|
17
24
|
}
|
|
18
25
|
export declare class MessageStore {
|
|
19
26
|
private path;
|
|
@@ -26,6 +33,12 @@ export declare class MessageStore {
|
|
|
26
33
|
private load;
|
|
27
34
|
/** Append a message and schedule a flush. Returns the stored message. */
|
|
28
35
|
append(peer: string, dir: "in" | "out", text: string, ts?: number): ChatMessage;
|
|
36
|
+
/** Append a file-transfer entry (shown as a download chip in the UI). */
|
|
37
|
+
appendFile(peer: string, dir: "in" | "out", file: {
|
|
38
|
+
name: string;
|
|
39
|
+
size: number;
|
|
40
|
+
}, ts?: number): ChatMessage;
|
|
41
|
+
private push;
|
|
29
42
|
/**
|
|
30
43
|
* Return history. With no peer, returns every peer's full thread (the legacy
|
|
31
44
|
* chat-history shape). With a peer, supports pagination: `limit` newest
|
|
@@ -45,7 +45,13 @@ export class MessageStore {
|
|
|
45
45
|
}
|
|
46
46
|
/** Append a message and schedule a flush. Returns the stored message. */
|
|
47
47
|
append(peer, dir, text, ts = Date.now()) {
|
|
48
|
-
|
|
48
|
+
return this.push(peer, { dir, text, ts, id: `${ts}-${this.seq++}` });
|
|
49
|
+
}
|
|
50
|
+
/** Append a file-transfer entry (shown as a download chip in the UI). */
|
|
51
|
+
appendFile(peer, dir, file, ts = Date.now()) {
|
|
52
|
+
return this.push(peer, { dir, text: "", ts, id: `${ts}-${this.seq++}`, file });
|
|
53
|
+
}
|
|
54
|
+
push(peer, msg) {
|
|
49
55
|
let arr = this.byPeer.get(peer);
|
|
50
56
|
if (!arr) {
|
|
51
57
|
arr = [];
|
package/dist/daemon/server.js
CHANGED
|
@@ -310,6 +310,10 @@ export class DaemonServer {
|
|
|
310
310
|
if (!fileId)
|
|
311
311
|
throw new Error("No free transfer slot (or friend unknown)");
|
|
312
312
|
this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
|
|
313
|
+
// Record in chat history (as an outgoing file chip) and push to the UI.
|
|
314
|
+
this.messageStore?.appendFile(userid, "out", { name, size: data.length });
|
|
315
|
+
this.friendMeta?.ensure(userid);
|
|
316
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
313
317
|
return { fileId, name, size: data.length };
|
|
314
318
|
},
|
|
315
319
|
chatMarkRead: async (userid, ts) => {
|
|
@@ -597,6 +601,10 @@ export class DaemonServer {
|
|
|
597
601
|
const safe = (p.name || "file").replace(/[/\\]/g, "_");
|
|
598
602
|
await fs.writeFile(resolve(dir, safe), Buffer.from(p.data));
|
|
599
603
|
this.logger.info(`Saved file "${p.name}" (${p.size}B) from ${p.friendId.slice(0, 8)} → ${resolve(dir, safe)}`);
|
|
604
|
+
// Record in chat history (as a download chip) and push to the UI.
|
|
605
|
+
this.messageStore?.appendFile(p.friendId, "in", { name: safe, size: p.size });
|
|
606
|
+
this.friendMeta?.ensure(p.friendId);
|
|
607
|
+
this.ipcEvents.emit("event", { type: "chat", userid: p.friendId, dir: "in" });
|
|
600
608
|
})().catch((e) => this.logger.warn(`Failed to save file: ${e.message}`));
|
|
601
609
|
});
|
|
602
610
|
this.peerManager.on("friend-request", (req) => {
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -131,6 +131,16 @@ function dkClock(ts) {
|
|
|
131
131
|
const d = new Date(ts);
|
|
132
132
|
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
133
133
|
}
|
|
134
|
+
function dkFileSize(n) {
|
|
135
|
+
if (!n || n < 0) return "0 B";
|
|
136
|
+
if (n < 1024) return n + " B";
|
|
137
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
138
|
+
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
139
|
+
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
|
140
|
+
}
|
|
141
|
+
function dkFileUrl(name) {
|
|
142
|
+
return "/api/file-download?name=" + encodeURIComponent(name);
|
|
143
|
+
}
|
|
134
144
|
function dkDayLabel(ts) {
|
|
135
145
|
if (!ts) return "Today";
|
|
136
146
|
const d = new Date(ts), now = /* @__PURE__ */ new Date();
|
|
@@ -186,7 +196,11 @@ const dkApi = {
|
|
|
186
196
|
remove: (userid) => dkPost("/api/friend-remove", { userid }),
|
|
187
197
|
alias: (userid, alias) => dkPost("/api/friend-alias", { userid, alias }),
|
|
188
198
|
markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
|
|
189
|
-
setProfile: (name, description) => dkPost("/api/set-profile", { name, description })
|
|
199
|
+
setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
|
|
200
|
+
sendFile: (userid, file) => fetch(
|
|
201
|
+
"/api/file-send?userid=" + encodeURIComponent(userid) + "&name=" + encodeURIComponent(file.name),
|
|
202
|
+
{ method: "POST", body: file }
|
|
203
|
+
).then((r) => r.json()).catch((e) => ({ ok: false, error: String(e) }))
|
|
190
204
|
};
|
|
191
205
|
const DK_ME_FALLBACK = {
|
|
192
206
|
name: "\u2026",
|
|
@@ -240,6 +254,7 @@ function useDaemonData() {
|
|
|
240
254
|
from: m.dir === "out" ? "me" : "them",
|
|
241
255
|
time: dkClock(m.ts),
|
|
242
256
|
text: m.text,
|
|
257
|
+
file: m.file ? { name: m.file.name, size: dkFileSize(m.file.size), dir: m.dir } : void 0,
|
|
243
258
|
status: m.dir === "out" ? "read" : void 0
|
|
244
259
|
}));
|
|
245
260
|
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
@@ -987,16 +1002,27 @@ const inputStyle = {
|
|
|
987
1002
|
};
|
|
988
1003
|
function Msg({ m, peer, T }) {
|
|
989
1004
|
const mine = m.from === "me";
|
|
990
|
-
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0" } }, !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ?
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1005
|
+
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: mine ? "flex-end" : "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0" } }, !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ? React.createElement(
|
|
1006
|
+
mine ? "div" : "a",
|
|
1007
|
+
{
|
|
1008
|
+
...mine ? {} : { href: dkFileUrl(m.file.name), download: m.file.name, title: "download" },
|
|
1009
|
+
style: {
|
|
1010
|
+
display: "flex",
|
|
1011
|
+
alignItems: "center",
|
|
1012
|
+
gap: 10,
|
|
1013
|
+
padding: "10px 12px",
|
|
1014
|
+
borderRadius: 10,
|
|
1015
|
+
background: mine ? "var(--bub-me)" : "var(--bub-them)",
|
|
1016
|
+
border: "1px solid " + (mine ? "transparent" : "var(--line)"),
|
|
1017
|
+
minWidth: 200,
|
|
1018
|
+
textDecoration: "none",
|
|
1019
|
+
cursor: mine ? "default" : "pointer"
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
/* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.kind || "file", size: 18, stroke: 1.9 })),
|
|
1023
|
+
/* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.size, mine ? "" : " \xB7 download")),
|
|
1024
|
+
mine ? /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
|
|
1025
|
+
) : /* @__PURE__ */ React.createElement("div", { style: {
|
|
1000
1026
|
padding: "8px 12px",
|
|
1001
1027
|
borderRadius: 12,
|
|
1002
1028
|
borderBottomRightRadius: mine ? 4 : 12,
|
|
@@ -1011,14 +1037,17 @@ function Msg({ m, peer, T }) {
|
|
|
1011
1037
|
wordBreak: "break-word"
|
|
1012
1038
|
} }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" }))));
|
|
1013
1039
|
}
|
|
1014
|
-
function Conversation({ T, peer, lang, thread: threadProp, onSend, onAlias, onRemove, onOpenNet }) {
|
|
1040
|
+
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
|
|
1015
1041
|
const scrollRef = React.useRef(null);
|
|
1042
|
+
const fileRef = React.useRef(null);
|
|
1016
1043
|
const thread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
|
|
1017
1044
|
React.useEffect(() => {
|
|
1018
1045
|
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
1019
1046
|
}, [peer.id, threadProp]);
|
|
1020
1047
|
const [menu, setMenu] = React.useState(false);
|
|
1021
1048
|
const [draft, setDraft] = React.useState("");
|
|
1049
|
+
const [dragOver, setDragOver] = React.useState(false);
|
|
1050
|
+
const [sending, setSending] = React.useState(null);
|
|
1022
1051
|
React.useEffect(() => {
|
|
1023
1052
|
setDraft("");
|
|
1024
1053
|
}, [peer.id]);
|
|
@@ -1028,27 +1057,68 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onAlias, onRe
|
|
|
1028
1057
|
setDraft("");
|
|
1029
1058
|
}
|
|
1030
1059
|
};
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1060
|
+
const sendFiles = (files) => {
|
|
1061
|
+
if (!files || !files.length || !onSendFile) return;
|
|
1062
|
+
const file = files[0];
|
|
1063
|
+
setSending({ name: file.name });
|
|
1064
|
+
Promise.resolve(onSendFile(file)).then((r) => {
|
|
1065
|
+
if (r && r.ok === false) window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
|
|
1066
|
+
}).finally(() => setSending(null));
|
|
1067
|
+
};
|
|
1068
|
+
const onDrop = (e) => {
|
|
1069
|
+
e.preventDefault();
|
|
1070
|
+
setDragOver(false);
|
|
1071
|
+
sendFiles(e.dataTransfer.files);
|
|
1072
|
+
};
|
|
1073
|
+
return /* @__PURE__ */ React.createElement(
|
|
1074
|
+
"div",
|
|
1039
1075
|
{
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (
|
|
1044
|
-
e.preventDefault();
|
|
1045
|
-
sendDraft();
|
|
1046
|
-
}
|
|
1076
|
+
style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column", background: "var(--bg)", position: "relative" },
|
|
1077
|
+
onDragOver: (e) => {
|
|
1078
|
+
e.preventDefault();
|
|
1079
|
+
if (!dragOver) setDragOver(true);
|
|
1047
1080
|
},
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1081
|
+
onDragLeave: (e) => {
|
|
1082
|
+
if (e.currentTarget === e.target) setDragOver(false);
|
|
1083
|
+
},
|
|
1084
|
+
onDrop
|
|
1085
|
+
},
|
|
1086
|
+
dragOver && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 0, zIndex: 30, background: "rgba(91,140,255,0.10)", border: "2px dashed var(--accent)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", fontFamily: "var(--mono)", fontSize: 14, color: "var(--accent)" } }, lang === "zh" ? "\u677E\u5F00\u53D1\u9001\u6587\u4EF6" : "Drop to send file"),
|
|
1087
|
+
/* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
|
|
1088
|
+
setMenu(false);
|
|
1089
|
+
onAlias(peer);
|
|
1090
|
+
} }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
|
|
1091
|
+
setMenu(false);
|
|
1092
|
+
onRemove(peer);
|
|
1093
|
+
} }))))),
|
|
1094
|
+
/* @__PURE__ */ React.createElement("div", { ref: scrollRef, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(Msg, { key: i, m, peer, T })))),
|
|
1095
|
+
/* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
|
|
1096
|
+
"input",
|
|
1097
|
+
{
|
|
1098
|
+
ref: fileRef,
|
|
1099
|
+
type: "file",
|
|
1100
|
+
style: { display: "none" },
|
|
1101
|
+
onChange: (e) => {
|
|
1102
|
+
sendFiles(e.target.files);
|
|
1103
|
+
e.target.value = "";
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
), /* @__PURE__ */ React.createElement(Btn, { icon: "paperclip", title: T.sendFile, onClick: () => fileRef.current && fileRef.current.click() }), /* @__PURE__ */ React.createElement(
|
|
1107
|
+
"input",
|
|
1108
|
+
{
|
|
1109
|
+
value: draft,
|
|
1110
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1111
|
+
onKeyDown: (e) => {
|
|
1112
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
sendDraft();
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
placeholder: `${T.message} ${peer.alias || shortKey(peer.userId, 6, 4)}\u2026`,
|
|
1118
|
+
style: { flex: 1, height: 38, borderRadius: 9, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13.5, padding: "0 14px", outline: "none", minWidth: 0 }
|
|
1119
|
+
}
|
|
1120
|
+
), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "arrowUp", title: T.send, onClick: sendDraft })))
|
|
1121
|
+
);
|
|
1052
1122
|
}
|
|
1053
1123
|
function MenuItem({ icon, label, onClick, danger }) {
|
|
1054
1124
|
return /* @__PURE__ */ React.createElement("button", { onClick, style: {
|
|
@@ -1071,9 +1141,9 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
1071
1141
|
function ChatEmpty({ T }) {
|
|
1072
1142
|
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
|
|
1073
1143
|
}
|
|
1074
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onAlias, onRemove, onOpenNet }) {
|
|
1144
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
|
|
1075
1145
|
const peer = peers.find((p) => p.id === activeId);
|
|
1076
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onAlias, onRemove, onOpenNet }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1146
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1077
1147
|
}
|
|
1078
1148
|
Object.assign(window, { ChatTab });
|
|
1079
1149
|
function StatTile({ label, value, sub, tone }) {
|
|
@@ -1427,6 +1497,14 @@ function DkApp() {
|
|
|
1427
1497
|
if (!activeId || !text || !text.trim()) return;
|
|
1428
1498
|
dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
|
|
1429
1499
|
};
|
|
1500
|
+
const onSendFile = (file) => {
|
|
1501
|
+
if (!activeId || !file) return Promise.resolve({ ok: false });
|
|
1502
|
+
return dkApi.sendFile(activeId, file).then((r) => {
|
|
1503
|
+
data.loadThread(activeId);
|
|
1504
|
+
data.refresh();
|
|
1505
|
+
return r;
|
|
1506
|
+
});
|
|
1507
|
+
};
|
|
1430
1508
|
const onAlias = (peer) => {
|
|
1431
1509
|
const a = window.prompt("Set alias for this peer (empty to clear):", peer.alias || "");
|
|
1432
1510
|
if (a !== null) dkApi.alias(peer.id, a).then(data.refresh);
|
|
@@ -1444,7 +1522,7 @@ function DkApp() {
|
|
|
1444
1522
|
{ id: "network", icon: "network", label: T.network },
|
|
1445
1523
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
1446
1524
|
];
|
|
1447
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "decentlan"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
1525
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "decentlan"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
1448
1526
|
TweakRadio,
|
|
1449
1527
|
{
|
|
1450
1528
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
package/dist/ui/server.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface FriendUiOptions {
|
|
|
17
17
|
wire?: string;
|
|
18
18
|
channel?: string;
|
|
19
19
|
};
|
|
20
|
+
/** Directory where the daemon saves received files (<configDir>/downloads).
|
|
21
|
+
* Used to serve GET /api/file-download. Undefined disables downloads. */
|
|
22
|
+
downloadsDir?: string;
|
|
20
23
|
listenHost?: string;
|
|
21
24
|
listenPort?: number;
|
|
22
25
|
log?: (msg: string) => void;
|
package/dist/ui/server.js
CHANGED
|
@@ -250,6 +250,70 @@ export function startFriendUi(opts) {
|
|
|
250
250
|
});
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
+
// ── File transfer ──────────────────────────────────────────────────
|
|
254
|
+
// Upload: the browser POSTs raw file bytes with ?userid=&name= in the
|
|
255
|
+
// query. We stream them to a temp file (no in-memory buffering of large
|
|
256
|
+
// files) and hand the daemon that path over the existing file-send IPC.
|
|
257
|
+
if (req.method === "POST" && url === "/api/file-send") {
|
|
258
|
+
const q = new URL(req.url || "", "http://x").searchParams;
|
|
259
|
+
const userid = q.get("userid") || "";
|
|
260
|
+
const rawName = q.get("name") || "file";
|
|
261
|
+
const safe = rawName.replace(/[/\\]/g, "_").slice(0, 200) || "file";
|
|
262
|
+
if (!userid) {
|
|
263
|
+
sendJson(res, 400, { ok: false, error: "userid required" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const os = await import("node:os");
|
|
267
|
+
const fs = await import("node:fs");
|
|
268
|
+
const fsp = await import("node:fs/promises");
|
|
269
|
+
const tmpDir = await fsp.mkdtemp(join(os.tmpdir(), "agentnet-up-"));
|
|
270
|
+
const tmpPath = join(tmpDir, safe);
|
|
271
|
+
try {
|
|
272
|
+
await new Promise((resolve2, reject) => {
|
|
273
|
+
const ws = fs.createWriteStream(tmpPath);
|
|
274
|
+
req.pipe(ws);
|
|
275
|
+
req.on("error", reject);
|
|
276
|
+
ws.on("error", reject);
|
|
277
|
+
ws.on("finish", () => resolve2());
|
|
278
|
+
});
|
|
279
|
+
const r = await opts.call({ op: "file-send", userid, path: tmpPath });
|
|
280
|
+
if (!r.ok) {
|
|
281
|
+
sendJson(res, 502, r);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
sendJson(res, 200, { ok: true, ...(r.data ?? {}) });
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// Download a received file by name, served from <configDir>/downloads.
|
|
295
|
+
if (req.method === "GET" && url === "/api/file-download") {
|
|
296
|
+
if (!opts.downloadsDir) {
|
|
297
|
+
res.writeHead(404);
|
|
298
|
+
res.end("downloads disabled");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const q = new URL(req.url || "", "http://x").searchParams;
|
|
302
|
+
const name = (q.get("name") || "").replace(/[/\\]/g, "");
|
|
303
|
+
const file = join(opts.downloadsDir, name);
|
|
304
|
+
// Path-traversal guard: the resolved path must stay under downloadsDir.
|
|
305
|
+
if (!name || !file.startsWith(opts.downloadsDir) || !existsSync(file)) {
|
|
306
|
+
res.writeHead(404);
|
|
307
|
+
res.end("not found");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
res.writeHead(200, {
|
|
311
|
+
"content-type": "application/octet-stream",
|
|
312
|
+
"content-disposition": `attachment; filename="${name.replace(/"/g, "")}"`,
|
|
313
|
+
});
|
|
314
|
+
res.end(readFileSync(file));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
253
317
|
if (req.method === "GET" && url === "/api/state") {
|
|
254
318
|
const [diag, pending] = await Promise.all([
|
|
255
319
|
opts.call({ op: "diag" }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.109",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|