@decentnetwork/lan 0.1.97 → 0.1.99
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/carrier/peer-manager.d.ts +11 -0
- package/dist/carrier/peer-manager.js +13 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +12 -0
- package/dist/cli/index.js +4 -1
- package/dist/console/console.js +1011 -0
- package/dist/daemon/ipc.d.ts +8 -1
- package/dist/daemon/ipc.js +4 -0
- package/dist/daemon/server.js +14 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/desktop/app.js +32 -6
- package/dist/ui/server.js +9 -1
- package/package.json +7 -5
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
// src/console/index.tsx
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
|
|
4
|
+
// src/console/app.tsx
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box as Box2, Text as Text2, useApp, useInput, useStdout } from "ink";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
// src/console/theme.ts
|
|
10
|
+
var PALETTES = {
|
|
11
|
+
indigo: { accent: "#8B7DF7", selFg: "#0c0e15" },
|
|
12
|
+
green: { accent: "#3FB950", selFg: "#06210f" },
|
|
13
|
+
amber: { accent: "#E3A857", selFg: "#1c1400" }
|
|
14
|
+
};
|
|
15
|
+
function theme(palette, mode) {
|
|
16
|
+
const p = PALETTES[palette] ?? PALETTES.indigo;
|
|
17
|
+
if (mode === "light") {
|
|
18
|
+
return {
|
|
19
|
+
line: "#dde1ea",
|
|
20
|
+
text: "#2b313d",
|
|
21
|
+
dim: "#6c7484",
|
|
22
|
+
faint: "#aab1be",
|
|
23
|
+
ok: "#1a9c3e",
|
|
24
|
+
warn: "#a9741b",
|
|
25
|
+
unread: "#8a6300",
|
|
26
|
+
danger: "#cf3b3b",
|
|
27
|
+
accent: p.accent,
|
|
28
|
+
selFg: p.selFg
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
line: "#252b3b",
|
|
33
|
+
text: "#c9d1e3",
|
|
34
|
+
dim: "#79839b",
|
|
35
|
+
faint: "#4a5266",
|
|
36
|
+
ok: "#3FB950",
|
|
37
|
+
warn: "#E3A857",
|
|
38
|
+
unread: "#E3B341",
|
|
39
|
+
danger: "#F26D6D",
|
|
40
|
+
accent: p.accent,
|
|
41
|
+
selFg: p.selFg
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var BRAILLE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
45
|
+
|
|
46
|
+
// src/console/i18n.ts
|
|
47
|
+
var TSTR = {
|
|
48
|
+
en: {
|
|
49
|
+
friends: "Friends",
|
|
50
|
+
chat: "chat",
|
|
51
|
+
typeMsg: "type a message\u2026",
|
|
52
|
+
requests: "Friend requests",
|
|
53
|
+
reqShort: "requests",
|
|
54
|
+
noReqs: "no pending requests",
|
|
55
|
+
accept: "accept",
|
|
56
|
+
reject: "reject",
|
|
57
|
+
close: "close",
|
|
58
|
+
removeQ: "Remove friend?",
|
|
59
|
+
removeBody: "Drop %s from your friend list. This cannot be undone.",
|
|
60
|
+
yes: "yes",
|
|
61
|
+
no: "no",
|
|
62
|
+
addTitle: "paste a friend's carrier address to send a request",
|
|
63
|
+
addPlaceholder: "AH7Fv8TnxJ\u2026",
|
|
64
|
+
sendReq: "send request",
|
|
65
|
+
cancel: "cancel",
|
|
66
|
+
move: "move",
|
|
67
|
+
open: "open",
|
|
68
|
+
remove: "remove",
|
|
69
|
+
alias: "alias",
|
|
70
|
+
add: "add",
|
|
71
|
+
quit: "quit",
|
|
72
|
+
you: "you",
|
|
73
|
+
copy: "copy",
|
|
74
|
+
copied: "copied!",
|
|
75
|
+
polling: "polling daemon",
|
|
76
|
+
online: "online",
|
|
77
|
+
day: "day",
|
|
78
|
+
night: "night",
|
|
79
|
+
aliasTitle: "set a local alias for %s",
|
|
80
|
+
aliasPlaceholder: "alias\u2026",
|
|
81
|
+
save: "save",
|
|
82
|
+
sent: "request sent",
|
|
83
|
+
noFriends: "no friends \u2014 press + to add"
|
|
84
|
+
},
|
|
85
|
+
zh: {
|
|
86
|
+
friends: "\u597D\u53CB",
|
|
87
|
+
chat: "\u5BF9\u8BDD",
|
|
88
|
+
typeMsg: "\u8F93\u5165\u6D88\u606F\u2026",
|
|
89
|
+
requests: "\u597D\u53CB\u8BF7\u6C42",
|
|
90
|
+
reqShort: "\u8BF7\u6C42",
|
|
91
|
+
noReqs: "\u6CA1\u6709\u5F85\u5904\u7406\u8BF7\u6C42",
|
|
92
|
+
accept: "\u63A5\u53D7",
|
|
93
|
+
reject: "\u62D2\u7EDD",
|
|
94
|
+
close: "\u5173\u95ED",
|
|
95
|
+
removeQ: "\u5220\u9664\u597D\u53CB\uFF1F",
|
|
96
|
+
removeBody: "\u5C06 %s \u4ECE\u597D\u53CB\u5217\u8868\u79FB\u9664\uFF0C\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002",
|
|
97
|
+
yes: "\u786E\u5B9A",
|
|
98
|
+
no: "\u53D6\u6D88",
|
|
99
|
+
addTitle: "\u7C98\u8D34\u597D\u53CB\u7684 carrier \u5730\u5740\u4EE5\u53D1\u9001\u8BF7\u6C42",
|
|
100
|
+
addPlaceholder: "AH7Fv8TnxJ\u2026",
|
|
101
|
+
sendReq: "\u53D1\u9001\u8BF7\u6C42",
|
|
102
|
+
cancel: "\u53D6\u6D88",
|
|
103
|
+
move: "\u9009\u62E9",
|
|
104
|
+
open: "\u6253\u5F00",
|
|
105
|
+
remove: "\u5220\u9664",
|
|
106
|
+
alias: "\u5907\u6CE8",
|
|
107
|
+
add: "\u6DFB\u52A0",
|
|
108
|
+
quit: "\u9000\u51FA",
|
|
109
|
+
you: "\u6211",
|
|
110
|
+
copy: "\u590D\u5236",
|
|
111
|
+
copied: "\u5DF2\u590D\u5236\uFF01",
|
|
112
|
+
polling: "\u8F6E\u8BE2\u5B88\u62A4\u8FDB\u7A0B",
|
|
113
|
+
online: "\u5728\u7EBF",
|
|
114
|
+
day: "\u767D\u5929",
|
|
115
|
+
night: "\u591C\u95F4",
|
|
116
|
+
aliasTitle: "\u4E3A %s \u8BBE\u7F6E\u672C\u5730\u5907\u6CE8",
|
|
117
|
+
aliasPlaceholder: "\u5907\u6CE8\u2026",
|
|
118
|
+
save: "\u4FDD\u5B58",
|
|
119
|
+
sent: "\u8BF7\u6C42\u5DF2\u53D1\u9001",
|
|
120
|
+
noFriends: "\u6682\u65E0\u597D\u53CB \u2014 \u6309 + \u6DFB\u52A0"
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/console/panes.tsx
|
|
125
|
+
import { Box, Text } from "ink";
|
|
126
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
127
|
+
var fnv = (s) => {
|
|
128
|
+
let h = 2166136261;
|
|
129
|
+
for (let i = 0; i < s.length; i++) {
|
|
130
|
+
h ^= s.charCodeAt(i);
|
|
131
|
+
h = Math.imul(h, 16777619);
|
|
132
|
+
}
|
|
133
|
+
return (h >>> 0) % 360;
|
|
134
|
+
};
|
|
135
|
+
var HUES = ["#7FB5E6", "#9CD67F", "#E6B86B", "#D69CE6", "#6BD6C4", "#E68C8C", "#B5A0E6"];
|
|
136
|
+
var nameColor = (s) => HUES[fnv(s) % HUES.length];
|
|
137
|
+
var pad = (s, w) => s.length >= w ? s.slice(0, w) : s + " ".repeat(w - s.length);
|
|
138
|
+
var clip = (s, w) => w <= 0 ? "" : s.length <= w ? s : s.slice(0, Math.max(0, w - 1)) + "\u2026";
|
|
139
|
+
function StatusHeader({ th, T, me, spinner, pollOn, mode, lang, copied }) {
|
|
140
|
+
const uid = me.userId ? `${me.userId.slice(0, 12)}\u2026${me.userId.slice(-5)}` : "\u2014";
|
|
141
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
142
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
143
|
+
/* @__PURE__ */ jsx(Text, { color: th.accent, bold: true, children: "agentnet " }),
|
|
144
|
+
/* @__PURE__ */ jsx(Text, { color: th.text, children: "console" }),
|
|
145
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
146
|
+
/* @__PURE__ */ jsxs(Text, { color: th.faint, children: [
|
|
147
|
+
spinner,
|
|
148
|
+
" ",
|
|
149
|
+
T.polling,
|
|
150
|
+
" \xB7 ",
|
|
151
|
+
pollOn ? "1s" : "\xB7"
|
|
152
|
+
] }),
|
|
153
|
+
/* @__PURE__ */ jsx(Text, { color: th.dim, children: " \u2502 " }),
|
|
154
|
+
/* @__PURE__ */ jsxs(Text, { color: th.ok, children: [
|
|
155
|
+
"\u25CF ",
|
|
156
|
+
T.online
|
|
157
|
+
] }),
|
|
158
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
159
|
+
" ",
|
|
160
|
+
me.ip,
|
|
161
|
+
" \xB7 lan ",
|
|
162
|
+
me.lan
|
|
163
|
+
] })
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
166
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
167
|
+
T.you,
|
|
168
|
+
" "
|
|
169
|
+
] }),
|
|
170
|
+
/* @__PURE__ */ jsx(Text, { color: th.accent, children: me.handle }),
|
|
171
|
+
/* @__PURE__ */ jsx(Text, { color: th.dim, children: " \xB7 " }),
|
|
172
|
+
/* @__PURE__ */ jsx(Text, { color: th.text, children: uid }),
|
|
173
|
+
/* @__PURE__ */ jsxs(Text, { color: copied ? th.ok : th.faint, children: [
|
|
174
|
+
" ",
|
|
175
|
+
copied ? `\u2713 ${T.copied}` : `[c] ${T.copy}`
|
|
176
|
+
] }),
|
|
177
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
178
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
179
|
+
"[F2] ",
|
|
180
|
+
mode === "light" ? `\u2600 ${T.day}` : `\u263E ${T.night}`,
|
|
181
|
+
" [F3] ",
|
|
182
|
+
lang === "en" ? "EN/\u4E2D" : "\u4E2D/EN"
|
|
183
|
+
] })
|
|
184
|
+
] })
|
|
185
|
+
] });
|
|
186
|
+
}
|
|
187
|
+
function FriendList({ th, T, friends, selIdx, width, height, rows, focused }) {
|
|
188
|
+
const online = friends.filter((f) => f.online).length;
|
|
189
|
+
const inner = width - 4;
|
|
190
|
+
const cap = Math.max(1, rows);
|
|
191
|
+
let start = 0;
|
|
192
|
+
if (friends.length > cap) start = Math.min(Math.max(0, selIdx - Math.floor(cap / 2)), friends.length - cap);
|
|
193
|
+
const visible = friends.slice(start, start + cap);
|
|
194
|
+
const up = start > 0, down = start + cap < friends.length;
|
|
195
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, height, borderStyle: "round", borderColor: focused ? th.accent : th.line, paddingX: 1, overflow: "hidden", children: [
|
|
196
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
197
|
+
/* @__PURE__ */ jsx(Text, { color: focused ? th.accent : th.dim, children: T.friends }),
|
|
198
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
199
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
200
|
+
up ? "\u2191" : " ",
|
|
201
|
+
online,
|
|
202
|
+
"/",
|
|
203
|
+
friends.length,
|
|
204
|
+
down ? "\u2193" : " "
|
|
205
|
+
] })
|
|
206
|
+
] }),
|
|
207
|
+
visible.map((f, vi) => {
|
|
208
|
+
const i = start + vi;
|
|
209
|
+
const sel = i === selIdx;
|
|
210
|
+
const dot = f.online ? "\u25CF" : "\u25CB";
|
|
211
|
+
const nm = f.name + (f.agent ? " ~bot" : "");
|
|
212
|
+
const tag = (f.via === "relay" ? "relay " : "") + (f.unread > 0 ? `(${f.unread})` : "");
|
|
213
|
+
if (sel && focused) {
|
|
214
|
+
const left = `${dot} ${nm}`;
|
|
215
|
+
const lc = clip(left, inner - tag.length - 1);
|
|
216
|
+
const line = lc + " ".repeat(Math.max(1, inner - lc.length - tag.length)) + tag;
|
|
217
|
+
return /* @__PURE__ */ jsx(Text, { backgroundColor: th.accent, color: th.selFg, children: pad(line, inner) }, f.id);
|
|
218
|
+
}
|
|
219
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
220
|
+
/* @__PURE__ */ jsxs(Text, { color: sel ? th.accent : f.online ? th.ok : th.faint, children: [
|
|
221
|
+
sel ? "\u203A" : dot,
|
|
222
|
+
" "
|
|
223
|
+
] }),
|
|
224
|
+
/* @__PURE__ */ jsx(Text, { color: sel ? th.accent : th.text, dimColor: !sel && !f.online, bold: sel, children: clip(nm, inner - tag.length - 3) }),
|
|
225
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
226
|
+
f.via === "relay" && /* @__PURE__ */ jsx(Text, { color: th.warn, children: "relay " }),
|
|
227
|
+
f.unread > 0 && /* @__PURE__ */ jsxs(Text, { color: th.unread, bold: true, children: [
|
|
228
|
+
"(",
|
|
229
|
+
f.unread,
|
|
230
|
+
")"
|
|
231
|
+
] })
|
|
232
|
+
] }, f.id);
|
|
233
|
+
})
|
|
234
|
+
] });
|
|
235
|
+
}
|
|
236
|
+
function ChatPane({ th, T, friend, thread, draft, blink, visibleRows, height, focused }) {
|
|
237
|
+
const shown = thread.slice(-Math.max(1, visibleRows));
|
|
238
|
+
const sub = friend.online ? `${friend.via ?? "direct"}${friend.ping != null ? ` ${friend.ping}ms` : ""}` : "offline";
|
|
239
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, height, borderStyle: "round", borderColor: focused ? th.accent : th.line, paddingX: 1, overflow: "hidden", children: [
|
|
240
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
241
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
242
|
+
T.chat,
|
|
243
|
+
": "
|
|
244
|
+
] }),
|
|
245
|
+
/* @__PURE__ */ jsxs(Text, { color: th.text, children: [
|
|
246
|
+
friend.name,
|
|
247
|
+
" "
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ jsx(Text, { color: friend.online ? th.ok : th.faint, children: friend.online ? "\u25CF" : "\u25CB" }),
|
|
250
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
251
|
+
/* @__PURE__ */ jsx(Text, { color: th.dim, children: sub })
|
|
252
|
+
] }),
|
|
253
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, children: shown.map((m, i) => /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
|
|
254
|
+
/* @__PURE__ */ jsxs(Text, { color: th.faint, children: [
|
|
255
|
+
"[",
|
|
256
|
+
m.t,
|
|
257
|
+
"] "
|
|
258
|
+
] }),
|
|
259
|
+
/* @__PURE__ */ jsxs(Text, { color: m.who === "me" ? th.accent : nameColor(friend.name), children: [
|
|
260
|
+
m.who === "me" ? "me" : friend.name,
|
|
261
|
+
"> "
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsx(Text, { color: th.text, children: m.text })
|
|
264
|
+
] }, i)) }),
|
|
265
|
+
/* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: th.line, borderBottom: false, borderLeft: false, borderRight: false, children: [
|
|
266
|
+
/* @__PURE__ */ jsx(Text, { color: th.accent, children: "> " }),
|
|
267
|
+
/* @__PURE__ */ jsx(Text, { color: draft ? th.text : th.faint, children: draft || T.typeMsg }),
|
|
268
|
+
/* @__PURE__ */ jsx(Text, { backgroundColor: blink ? th.accent : void 0, children: " " })
|
|
269
|
+
] })
|
|
270
|
+
] });
|
|
271
|
+
}
|
|
272
|
+
function HelpBar({ th, T, focus, overlay }) {
|
|
273
|
+
if (overlay) return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: th.dim, children: "\u2191\u2193 select \xB7 \u21B5/a accept \xB7 x reject \xB7 y/n confirm \xB7 esc close" }) });
|
|
274
|
+
if (focus === "input") {
|
|
275
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
276
|
+
/* @__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 switch \xB7 /q /r /add /alias` })
|
|
278
|
+
] });
|
|
279
|
+
}
|
|
280
|
+
const items = [["\u2191\u2193", T.move], ["\u21B5/>", T.chat], ["r", T.reqShort], ["d", T.remove], ["a", T.alias], ["+", T.add], ["q", T.quit]];
|
|
281
|
+
return /* @__PURE__ */ jsx(Box, { children: items.map(([k, label]) => /* @__PURE__ */ jsxs(Box, { marginRight: 2, children: [
|
|
282
|
+
/* @__PURE__ */ jsxs(Text, { backgroundColor: th.line, color: th.text, children: [
|
|
283
|
+
" ",
|
|
284
|
+
k,
|
|
285
|
+
" "
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
288
|
+
" ",
|
|
289
|
+
label
|
|
290
|
+
] })
|
|
291
|
+
] }, k)) });
|
|
292
|
+
}
|
|
293
|
+
function focusSend(T) {
|
|
294
|
+
return T.sendReq.split(" ")[0] || "send";
|
|
295
|
+
}
|
|
296
|
+
function Centered({ height, children }) {
|
|
297
|
+
return /* @__PURE__ */ jsx(Box, { height, width: "100%", justifyContent: "center", alignItems: "center", children });
|
|
298
|
+
}
|
|
299
|
+
function RequestsOverlay({ th, T, requests, reqIdx, height }) {
|
|
300
|
+
return /* @__PURE__ */ jsx(Centered, { height, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: 62, borderStyle: "round", borderColor: th.accent, paddingX: 1, children: [
|
|
301
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
302
|
+
/* @__PURE__ */ jsx(Text, { color: th.accent, children: T.requests }),
|
|
303
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
|
|
304
|
+
/* @__PURE__ */ jsx(Text, { color: th.dim, children: requests.length })
|
|
305
|
+
] }),
|
|
306
|
+
requests.length === 0 && /* @__PURE__ */ jsx(Text, { color: th.dim, children: T.noReqs }),
|
|
307
|
+
requests.slice(0, Math.max(1, height - 6)).map((r, i) => {
|
|
308
|
+
const sel = i === reqIdx;
|
|
309
|
+
const key = r.key.length > 30 ? `${r.key.slice(0, 22)}\u2026${r.key.slice(-6)}` : r.key;
|
|
310
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
311
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
312
|
+
/* @__PURE__ */ jsx(Text, { color: sel ? th.accent : th.faint, children: sel ? "\u203A " : " " }),
|
|
313
|
+
/* @__PURE__ */ jsx(Text, { color: th.text, children: key })
|
|
314
|
+
] }),
|
|
315
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
316
|
+
" via ",
|
|
317
|
+
r.via,
|
|
318
|
+
r.time ? ` \xB7 ${r.time} ago` : ""
|
|
319
|
+
] })
|
|
320
|
+
] }, r.id);
|
|
321
|
+
}),
|
|
322
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
323
|
+
"\u21B5/a ",
|
|
324
|
+
T.accept,
|
|
325
|
+
" x ",
|
|
326
|
+
T.reject,
|
|
327
|
+
" esc ",
|
|
328
|
+
T.close
|
|
329
|
+
] }) })
|
|
330
|
+
] }) });
|
|
331
|
+
}
|
|
332
|
+
function ConfirmOverlay({ th, T, name, height }) {
|
|
333
|
+
return /* @__PURE__ */ jsx(Centered, { height, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: 48, borderStyle: "round", borderColor: th.danger, paddingX: 1, children: [
|
|
334
|
+
/* @__PURE__ */ jsx(Text, { color: th.danger, children: T.removeQ }),
|
|
335
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: th.text, children: T.removeBody.replace("%s", name) }) }),
|
|
336
|
+
/* @__PURE__ */ jsxs(Text, { color: th.dim, children: [
|
|
337
|
+
"y ",
|
|
338
|
+
T.yes,
|
|
339
|
+
" n/esc ",
|
|
340
|
+
T.no
|
|
341
|
+
] })
|
|
342
|
+
] }) });
|
|
343
|
+
}
|
|
344
|
+
function AddOverlay({ th, T, draft, blink, alias, height }) {
|
|
345
|
+
const title = alias ? T.aliasTitle.replace("%s", alias.name) : T.addTitle;
|
|
346
|
+
const ph = alias ? T.aliasPlaceholder : T.addPlaceholder;
|
|
347
|
+
const footer = alias ? `\u21B5 ${T.save} esc ${T.cancel}` : `\u21B5 ${T.sendReq} esc ${T.cancel}`;
|
|
348
|
+
return /* @__PURE__ */ jsx(Centered, { height, children: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: 66, borderStyle: "round", borderColor: th.accent, paddingX: 1, children: [
|
|
349
|
+
/* @__PURE__ */ jsx(Text, { color: th.dim, children: title }),
|
|
350
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
351
|
+
/* @__PURE__ */ jsx(Text, { color: th.accent, children: "> " }),
|
|
352
|
+
/* @__PURE__ */ jsx(Text, { color: draft ? th.text : th.faint, wrap: "truncate", children: draft || ph }),
|
|
353
|
+
/* @__PURE__ */ jsx(Text, { backgroundColor: blink ? th.accent : void 0, children: " " })
|
|
354
|
+
] }),
|
|
355
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: th.dim, children: footer }) })
|
|
356
|
+
] }) });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/console/app.tsx
|
|
360
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
361
|
+
function clipboardWrite(text) {
|
|
362
|
+
try {
|
|
363
|
+
const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : process.env.WAYLAND_DISPLAY ? "wl-copy" : "xclip";
|
|
364
|
+
const args = cmd === "xclip" ? ["-selection", "clipboard"] : [];
|
|
365
|
+
const p = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
366
|
+
p.on("error", () => void 0);
|
|
367
|
+
p.stdin.end(text);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function useDimensions() {
|
|
372
|
+
const { stdout } = useStdout();
|
|
373
|
+
const [dim, setDim] = React.useState({ cols: stdout.columns || 100, rows: stdout.rows || 30 });
|
|
374
|
+
React.useEffect(() => {
|
|
375
|
+
const on = () => setDim({ cols: stdout.columns || 100, rows: stdout.rows || 30 });
|
|
376
|
+
stdout.on("resize", on);
|
|
377
|
+
return () => {
|
|
378
|
+
stdout.off("resize", on);
|
|
379
|
+
};
|
|
380
|
+
}, [stdout]);
|
|
381
|
+
return dim;
|
|
382
|
+
}
|
|
383
|
+
function App({ client }) {
|
|
384
|
+
const { exit } = useApp();
|
|
385
|
+
const { cols, rows } = useDimensions();
|
|
386
|
+
const [mode, setMode] = React.useState("dark");
|
|
387
|
+
const [lang, setLang] = React.useState("en");
|
|
388
|
+
const [palette] = React.useState("indigo");
|
|
389
|
+
const T = TSTR[lang];
|
|
390
|
+
const th = theme(palette, mode);
|
|
391
|
+
const [me, setMe] = React.useState({ handle: "you", userId: "", ip: "\u2014", lan: "", channel: "@next" });
|
|
392
|
+
const [friends, setFriends] = React.useState([]);
|
|
393
|
+
const [requests, setRequests] = React.useState([]);
|
|
394
|
+
const [threads, setThreads] = React.useState({});
|
|
395
|
+
const [selIdx, setSelIdx] = React.useState(0);
|
|
396
|
+
const [reqIdx, setReqIdx] = React.useState(0);
|
|
397
|
+
const [view, setView] = React.useState("chat");
|
|
398
|
+
const [focus, setFocus] = React.useState("list");
|
|
399
|
+
const [draft, setDraft] = React.useState("");
|
|
400
|
+
const [field, setField] = React.useState("");
|
|
401
|
+
const [copied, setCopied] = React.useState(false);
|
|
402
|
+
const [blink, setBlink] = React.useState(true);
|
|
403
|
+
const [spin, setSpin] = React.useState(0);
|
|
404
|
+
const [pollOn, setPollOn] = React.useState(true);
|
|
405
|
+
const friend = friends[Math.min(selIdx, Math.max(0, friends.length - 1))];
|
|
406
|
+
const thread = friend && threads[friend.id] || [];
|
|
407
|
+
React.useEffect(() => {
|
|
408
|
+
const b = setInterval(() => setBlink((v) => !v), 530);
|
|
409
|
+
const s = setInterval(() => setSpin((v) => (v + 1) % BRAILLE.length), 110);
|
|
410
|
+
const p = setInterval(() => setPollOn((v) => !v), 1400);
|
|
411
|
+
return () => {
|
|
412
|
+
clearInterval(b);
|
|
413
|
+
clearInterval(s);
|
|
414
|
+
clearInterval(p);
|
|
415
|
+
};
|
|
416
|
+
}, []);
|
|
417
|
+
const selId = friend?.id;
|
|
418
|
+
React.useEffect(() => {
|
|
419
|
+
let alive = true;
|
|
420
|
+
const tick = async () => {
|
|
421
|
+
try {
|
|
422
|
+
const snap = await client.snapshot();
|
|
423
|
+
if (!alive) return;
|
|
424
|
+
setMe(snap.me);
|
|
425
|
+
setFriends(snap.friends);
|
|
426
|
+
setRequests(snap.requests);
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
if (selId) {
|
|
430
|
+
try {
|
|
431
|
+
const h = await client.history(selId);
|
|
432
|
+
if (alive) setThreads((t) => ({ ...t, [selId]: h }));
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
void tick();
|
|
438
|
+
const iv = setInterval(() => void tick(), 1400);
|
|
439
|
+
return () => {
|
|
440
|
+
alive = false;
|
|
441
|
+
clearInterval(iv);
|
|
442
|
+
};
|
|
443
|
+
}, [client, selId]);
|
|
444
|
+
React.useEffect(() => {
|
|
445
|
+
if (selId) void client.markRead(selId).catch(() => void 0);
|
|
446
|
+
}, [client, selId]);
|
|
447
|
+
const flashCopied = () => {
|
|
448
|
+
setCopied(true);
|
|
449
|
+
setTimeout(() => setCopied(false), 1100);
|
|
450
|
+
};
|
|
451
|
+
const send = () => {
|
|
452
|
+
if (!friend || !draft.trim()) return;
|
|
453
|
+
const text = draft.trim();
|
|
454
|
+
setThreads((t) => ({ ...t, [friend.id]: [...t[friend.id] || [], { t: now(), who: "me", text }] }));
|
|
455
|
+
setDraft("");
|
|
456
|
+
void client.send(friend.id, text).catch(() => void 0);
|
|
457
|
+
};
|
|
458
|
+
const actReq = (kind) => {
|
|
459
|
+
const r = requests[reqIdx];
|
|
460
|
+
if (!r) return;
|
|
461
|
+
setRequests((rs) => rs.filter((x) => x.id !== r.id));
|
|
462
|
+
setReqIdx(0);
|
|
463
|
+
void (kind === "accept" ? client.accept(r.userid) : client.reject(r.userid)).catch(() => void 0);
|
|
464
|
+
};
|
|
465
|
+
const removeFriend = () => {
|
|
466
|
+
if (!friend) return;
|
|
467
|
+
const id = friend.id;
|
|
468
|
+
setFriends((fs) => fs.filter((f) => f.id !== id));
|
|
469
|
+
setSelIdx((i) => Math.max(0, i - 1));
|
|
470
|
+
setView("chat");
|
|
471
|
+
void client.remove(id).catch(() => void 0);
|
|
472
|
+
};
|
|
473
|
+
const submitField = () => {
|
|
474
|
+
const v = field.trim();
|
|
475
|
+
if (view === "add" && v) void client.add(v).catch(() => void 0);
|
|
476
|
+
if (view === "alias" && friend) void client.alias(friend.id, v).catch(() => void 0);
|
|
477
|
+
setField("");
|
|
478
|
+
setView("chat");
|
|
479
|
+
};
|
|
480
|
+
const runSlash = (line) => {
|
|
481
|
+
const parts = line.slice(1).trim().split(/\s+/);
|
|
482
|
+
const c = (parts[0] || "").toLowerCase();
|
|
483
|
+
const arg = parts.slice(1).join(" ");
|
|
484
|
+
if (c === "q" || c === "quit") exit();
|
|
485
|
+
else if (c === "r" || c === "requests") {
|
|
486
|
+
setReqIdx(0);
|
|
487
|
+
setView("requests");
|
|
488
|
+
} else if (c === "d" || c === "remove") {
|
|
489
|
+
if (friend) setView("confirm");
|
|
490
|
+
} else if (c === "a" || c === "alias") {
|
|
491
|
+
if (friend) {
|
|
492
|
+
if (arg) void client.alias(friend.id, arg).catch(() => void 0);
|
|
493
|
+
else {
|
|
494
|
+
setField("");
|
|
495
|
+
setView("alias");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} else if (c === "add") {
|
|
499
|
+
if (arg) void client.add(arg).catch(() => void 0);
|
|
500
|
+
else {
|
|
501
|
+
setField("");
|
|
502
|
+
setView("add");
|
|
503
|
+
}
|
|
504
|
+
} else if (c === "c" || c === "copy") {
|
|
505
|
+
if (me.userId) {
|
|
506
|
+
clipboardWrite(me.userId);
|
|
507
|
+
flashCopied();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const n = friends.length;
|
|
512
|
+
const moveUp = () => {
|
|
513
|
+
setSelIdx((i) => Math.max(0, i - 1));
|
|
514
|
+
setDraft("");
|
|
515
|
+
};
|
|
516
|
+
const moveDown = () => {
|
|
517
|
+
setSelIdx((i) => Math.min(n - 1, i + 1));
|
|
518
|
+
setDraft("");
|
|
519
|
+
};
|
|
520
|
+
useInput((input, key) => {
|
|
521
|
+
if (key.ctrl && input === "t" || input === "OQ" || input === "[12~") {
|
|
522
|
+
setMode((m) => m === "dark" ? "light" : "dark");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (key.ctrl && input === "g" || input === "OR" || input === "[13~") {
|
|
526
|
+
setLang((l) => l === "en" ? "zh" : "en");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (view === "requests") {
|
|
530
|
+
if (key.escape) setView("chat");
|
|
531
|
+
else if (key.upArrow) setReqIdx((i) => Math.max(0, i - 1));
|
|
532
|
+
else if (key.downArrow) setReqIdx((i) => Math.min(requests.length - 1, i + 1));
|
|
533
|
+
else if (input === "a" || key.return) actReq("accept");
|
|
534
|
+
else if (input === "x" || key.delete || key.backspace) actReq("reject");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (view === "confirm") {
|
|
538
|
+
if (input === "y") removeFriend();
|
|
539
|
+
else if (input === "n" || key.escape) setView("chat");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (view === "add" || view === "alias") {
|
|
543
|
+
if (key.escape) {
|
|
544
|
+
setField("");
|
|
545
|
+
setView("chat");
|
|
546
|
+
} else if (key.return) submitField();
|
|
547
|
+
else if (key.backspace || key.delete) setField((s) => s.slice(0, -1));
|
|
548
|
+
else if (input && input.length === 1 && !key.ctrl && !key.meta) setField((s) => s + input);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (key.tab) {
|
|
552
|
+
setFocus((f) => f === "list" ? "input" : "list");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (focus === "list") {
|
|
556
|
+
if (key.upArrow) moveUp();
|
|
557
|
+
else if (key.downArrow) moveDown();
|
|
558
|
+
else if (key.return || input === ">") setFocus("input");
|
|
559
|
+
else if (input === "r") {
|
|
560
|
+
setReqIdx(0);
|
|
561
|
+
setView("requests");
|
|
562
|
+
} else if (input === "d") {
|
|
563
|
+
if (n) setView("confirm");
|
|
564
|
+
} else if (input === "a") {
|
|
565
|
+
if (friend) {
|
|
566
|
+
setField("");
|
|
567
|
+
setView("alias");
|
|
568
|
+
}
|
|
569
|
+
} else if (input === "+") {
|
|
570
|
+
setField("");
|
|
571
|
+
setView("add");
|
|
572
|
+
} else if (input === "c") {
|
|
573
|
+
if (me.userId) {
|
|
574
|
+
clipboardWrite(me.userId);
|
|
575
|
+
flashCopied();
|
|
576
|
+
}
|
|
577
|
+
} else if (input === "q") exit();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (key.escape) {
|
|
581
|
+
setFocus("list");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (input === "<" && draft === "") {
|
|
585
|
+
setFocus("list");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (key.upArrow) {
|
|
589
|
+
moveUp();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (key.downArrow) {
|
|
593
|
+
moveDown();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (key.return) {
|
|
597
|
+
if (draft.startsWith("/")) {
|
|
598
|
+
runSlash(draft.trim());
|
|
599
|
+
setDraft("");
|
|
600
|
+
} else send();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (key.backspace || key.delete) {
|
|
604
|
+
setDraft((s) => s.slice(0, -1));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) setDraft((s) => s + input);
|
|
608
|
+
});
|
|
609
|
+
const leftWidth = Math.max(22, Math.min(34, Math.floor(cols * 0.3)));
|
|
610
|
+
const contentRows = Math.max(6, rows - 4);
|
|
611
|
+
const listRows = Math.max(1, contentRows - 3);
|
|
612
|
+
const chatRows = Math.max(1, contentRows - 5);
|
|
613
|
+
const overlay = view !== "chat";
|
|
614
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
615
|
+
/* @__PURE__ */ jsx2(StatusHeader, { th, T, me, spinner: BRAILLE[spin], pollOn, mode, lang, copied }),
|
|
616
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, height: contentRows, children: view === "requests" ? /* @__PURE__ */ jsx2(RequestsOverlay, { th, T, requests, reqIdx, height: contentRows }) : view === "confirm" && friend ? /* @__PURE__ */ jsx2(ConfirmOverlay, { th, T, name: friend.name, height: contentRows }) : view === "add" ? /* @__PURE__ */ jsx2(AddOverlay, { th, T, draft: field, blink, height: contentRows }) : view === "alias" && friend ? /* @__PURE__ */ jsx2(AddOverlay, { th, T, draft: field, blink, alias: { name: friend.name }, height: contentRows }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
617
|
+
/* @__PURE__ */ jsx2(FriendList, { th, T, friends, selIdx, width: leftWidth, height: contentRows, rows: listRows, focused: focus === "list" }),
|
|
618
|
+
friend ? /* @__PURE__ */ jsx2(ChatPane, { th, T, friend, thread, draft, blink: blink && focus === "input", visibleRows: chatRows, height: contentRows, focused: focus === "input" }) : /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, height: contentRows, borderStyle: "round", borderColor: th.line, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx2(Text2, { color: th.faint, children: T.noFriends }) })
|
|
619
|
+
] }) }),
|
|
620
|
+
/* @__PURE__ */ jsx2(HelpBar, { th, T, focus, overlay })
|
|
621
|
+
] });
|
|
622
|
+
}
|
|
623
|
+
function now() {
|
|
624
|
+
const d = /* @__PURE__ */ new Date();
|
|
625
|
+
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/console/data.ts
|
|
629
|
+
import { createConnection } from "node:net";
|
|
630
|
+
import { resolve as resolve2 } from "node:path";
|
|
631
|
+
import { homedir as homedir2 } from "node:os";
|
|
632
|
+
import { createRequire } from "node:module";
|
|
633
|
+
|
|
634
|
+
// src/config/loader.ts
|
|
635
|
+
import { readFileSync, existsSync } from "fs";
|
|
636
|
+
import { resolve, dirname } from "path";
|
|
637
|
+
import { mkdirSync } from "fs";
|
|
638
|
+
import { homedir } from "os";
|
|
639
|
+
import yaml from "js-yaml";
|
|
640
|
+
var DEFAULT_CONFIG_DIR = resolve(homedir(), ".agentnet");
|
|
641
|
+
var DEFAULT_CONFIG_FILE = resolve(DEFAULT_CONFIG_DIR, "config.yaml");
|
|
642
|
+
var DEFAULT_BOOTSTRAP_NODES = [
|
|
643
|
+
// US-East — closest for typical North-American peers.
|
|
644
|
+
{ host: "13.58.208.50", port: 33445, pk: "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh" },
|
|
645
|
+
{ host: "18.216.102.47", port: 33445, pk: "G5z8MqiNDFTadFUPfMdYsYtkUDbX5mNCMVHMZtsCnFeb" },
|
|
646
|
+
{ host: "18.216.6.197", port: 33445, pk: "H8sqhRrQuJZ6iLtP2wanxt4LzdNrN2NNFnpPdq1uJ9n2" },
|
|
647
|
+
// US-West.
|
|
648
|
+
{ host: "54.193.141.205", port: 33445, pk: "7TfZWZNV8vnBxxWzJXuvKgX2QyKkLpg2oXx3LQ5tg8LW" },
|
|
649
|
+
// Unknown / global.
|
|
650
|
+
{ host: "154.64.235.176", port: 33445, pk: "GdNtV2N74fZnLjhH7NhQ18nGdxb1k8jRM9dQaK7WnxmL" },
|
|
651
|
+
// Asia-Pacific (Singapore + China). Kept as fallback so peers
|
|
652
|
+
// actually in CN/SG can use a nearby relay. Peers in the US should
|
|
653
|
+
// not be hitting these for normal traffic.
|
|
654
|
+
{ host: "52.74.215.181", port: 33445, pk: "Xv6d34WaUw9bPn7YihzVAFw7D2igbQJZ3jwmzzfYVFV" },
|
|
655
|
+
{ host: "47.100.103.201", port: 33445, pk: "CX1XH419p4xJ5SV4KvDxBeKYSRdMJW9QpdWJY8owUxHd" },
|
|
656
|
+
{ host: "52.83.171.135", port: 443, pk: "5tuHgK1Q4CYf4K5PutsEPK5E3Z7cbtEBdx7LwmdzqXHL" },
|
|
657
|
+
{ host: "52.83.191.228", port: 33445, pk: "3khtxZo89SBScAMaHhTvD68pPHiKxgZT6hTCSZZVgNEm" }
|
|
658
|
+
];
|
|
659
|
+
var DEFAULT_EXPRESS_NODES = [
|
|
660
|
+
// lens stays PRIMARY for backward-compat: older clients (peer < 0.1.25)
|
|
661
|
+
// only know lens and pull from the first relay, so replies must land on
|
|
662
|
+
// lens for them to see them. tokyo is the dedicated decentlan/dora relay
|
|
663
|
+
// (outside the GFW) kept as a hot standby + redundancy; peer >= 0.1.25
|
|
664
|
+
// pulls from BOTH, so nothing is missed. Flood protection comes from the
|
|
665
|
+
// express server's per-user M-cap, not from isolating onto tokyo.
|
|
666
|
+
{ host: "lens.beagle.chat", port: 443, pk: "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" },
|
|
667
|
+
{ host: "tokyo.fi.chat", port: 8443, pk: "EzpBtoUkjeMQfuLGWwTUbvzCn9rK4J648Ziy21EKxefo" }
|
|
668
|
+
];
|
|
669
|
+
var DEFAULT_DORAS_FALLBACK = [
|
|
670
|
+
{ name: "dora-mac", userid: "98rsHv17h8G6AP9RagyrBiT1kmw4cn8MFPEembS6ZVjv", address: "Jt7w1pKkyLT5GVue9h6ZPkjg1EeuuTbD6JVSLycXLsdm6nvBGSUd" },
|
|
671
|
+
// 10.86.1.10–63.254
|
|
672
|
+
{ name: "dora-beagle", userid: "AxKFEZFLDi23EmnJFNP6gjUM4CaNMPfWUvbFR9ixtMBN", address: "NsuN81dZdEoyvwEFgWaHkT8SPJB6UWeRmdYcCGFV5CdbbPXoK2RM" },
|
|
673
|
+
// 10.86.64.10–127.254
|
|
674
|
+
{ name: "dora-sh", userid: "GMEMLmCWLMBK6BJiMkbLPNkEjF4S2xRf1SqR9hM8fWV3", address: "ajg1ZMBw86UyujmEJzqKSCbi3wwEtg6tdGFTdESakyqujyxmqJZK" },
|
|
675
|
+
// 10.86.128.10–191.254
|
|
676
|
+
{ name: "dora-tokyo", userid: "AB6BZfbrTFWw9eUoVpHdJqhhRnY8bTttp4CHTZ2Xfzxi", address: "MAW2eBqBuQ6SmaXTrnZRRayQjAj3aLatwPy4xmBp7spnJeV569op" }
|
|
677
|
+
// 10.86.192.10–254.254
|
|
678
|
+
];
|
|
679
|
+
function loadDefaultDoras() {
|
|
680
|
+
try {
|
|
681
|
+
const file = resolve(dirname(new URL(import.meta.url).pathname), "../../config/default-doras.yaml");
|
|
682
|
+
const parsed = yaml.load(readFileSync(file, "utf-8"));
|
|
683
|
+
const doras = (parsed?.doras ?? []).filter((d) => d && d.userid && d.address);
|
|
684
|
+
if (doras.length > 0) return doras;
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
return DEFAULT_DORAS_FALLBACK;
|
|
688
|
+
}
|
|
689
|
+
var DEFAULT_DORAS = loadDefaultDoras();
|
|
690
|
+
var DEFAULT_DORA_USERID = DEFAULT_DORAS[0].userid;
|
|
691
|
+
var DEFAULT_DORA_ADDRESS = DEFAULT_DORAS[0].address;
|
|
692
|
+
function loadDefaultExits() {
|
|
693
|
+
try {
|
|
694
|
+
const file = resolve(dirname(new URL(import.meta.url).pathname), "../../config/default-exits.yaml");
|
|
695
|
+
const parsed = yaml.load(readFileSync(file, "utf-8"));
|
|
696
|
+
return (parsed?.exits ?? []).filter((e) => e && e.userid);
|
|
697
|
+
} catch {
|
|
698
|
+
return [];
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
var DEFAULT_EXITS = loadDefaultExits();
|
|
702
|
+
var DEFAULT_AUTOFRIEND = [
|
|
703
|
+
...DEFAULT_DORAS.map((d) => d.userid),
|
|
704
|
+
...DEFAULT_EXITS.map((e) => e.userid)
|
|
705
|
+
];
|
|
706
|
+
var ConfigLoader = class {
|
|
707
|
+
static defaultConfigPath() {
|
|
708
|
+
return resolve(this.defaultConfigDir(), "config.yaml");
|
|
709
|
+
}
|
|
710
|
+
static defaultConfigDir() {
|
|
711
|
+
const sudoUser = process.env.SUDO_USER;
|
|
712
|
+
if (sudoUser && sudoUser !== "root" && !existsSync(DEFAULT_CONFIG_FILE)) {
|
|
713
|
+
for (const home of [`/home/${sudoUser}`, `/Users/${sudoUser}`]) {
|
|
714
|
+
const cand = resolve(home, ".agentnet");
|
|
715
|
+
if (existsSync(resolve(cand, "config.yaml"))) return cand;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return DEFAULT_CONFIG_DIR;
|
|
719
|
+
}
|
|
720
|
+
static async load(filePath) {
|
|
721
|
+
const path = filePath || DEFAULT_CONFIG_FILE;
|
|
722
|
+
try {
|
|
723
|
+
const content = readFileSync(path, "utf-8");
|
|
724
|
+
const config = yaml.load(content);
|
|
725
|
+
return this.normalizeConfig(config);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
`Config file not found: ${path}. Run 'agentnet init' first.`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
static async loadOrCreateDefault(nodeName, configDir) {
|
|
736
|
+
const dir = configDir || DEFAULT_CONFIG_DIR;
|
|
737
|
+
const filePath = resolve(dir, "config.yaml");
|
|
738
|
+
try {
|
|
739
|
+
return await this.load(filePath);
|
|
740
|
+
} catch {
|
|
741
|
+
return this.createDefault(nodeName, dir);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
static createDefault(nodeName, configDir) {
|
|
745
|
+
const dir = configDir || DEFAULT_CONFIG_DIR;
|
|
746
|
+
return {
|
|
747
|
+
node: {
|
|
748
|
+
name: nodeName,
|
|
749
|
+
namespace: "agentnet-main"
|
|
750
|
+
},
|
|
751
|
+
carrier: {
|
|
752
|
+
// Isolated from other Carrier-using apps (Beagle, OpenClaw, etc.)
|
|
753
|
+
// by default. User can point at a shared identity by editing the config.
|
|
754
|
+
dataDir: resolve(dir, "carrier"),
|
|
755
|
+
bootstrapNodes: DEFAULT_BOOTSTRAP_NODES,
|
|
756
|
+
expressNodes: DEFAULT_EXPRESS_NODES
|
|
757
|
+
},
|
|
758
|
+
network: {
|
|
759
|
+
interface: "agentnet0",
|
|
760
|
+
ip: "10.86.1.10",
|
|
761
|
+
subnet: "10.86.0.0/16",
|
|
762
|
+
// .decent is the canonical TLD for the Decent Network's
|
|
763
|
+
// private virtual LAN. Existing configs with `dnsDomain:
|
|
764
|
+
// agentnet` keep working since `ConfigLoader.load` preserves
|
|
765
|
+
// whatever's on disk — only new `agentnet init` runs pick
|
|
766
|
+
// this up.
|
|
767
|
+
dnsDomain: "decent",
|
|
768
|
+
// 5353 is the mDNS convention and collides with avahi /
|
|
769
|
+
// mDNSResponder / openclaw-gateway on a lot of boxes. 5354
|
|
770
|
+
// is adjacent and routinely free.
|
|
771
|
+
dnsPort: 5354
|
|
772
|
+
},
|
|
773
|
+
paths: {
|
|
774
|
+
ipamFile: resolve(dir, "ipam.yaml"),
|
|
775
|
+
policyFile: resolve(dir, "policy.yaml"),
|
|
776
|
+
auditLog: resolve(dir, "audit.log")
|
|
777
|
+
},
|
|
778
|
+
// Proxy is opt-in. Off by default; `agentnet proxy enable` flips it on.
|
|
779
|
+
proxy: {
|
|
780
|
+
enabled: false,
|
|
781
|
+
port: 8888
|
|
782
|
+
},
|
|
783
|
+
// Auto-accept incoming friend requests by default. The Carrier
|
|
784
|
+
// network is already a friend network — if you don't want a peer,
|
|
785
|
+
// don't share your address with them. Disable with
|
|
786
|
+
// `friends.autoAccept: false` for stricter control.
|
|
787
|
+
friends: {
|
|
788
|
+
autoAccept: true
|
|
789
|
+
},
|
|
790
|
+
// Dora integration is ON by default and points at the public
|
|
791
|
+
// canonical dora — `agentnet init` follows up with a one-time
|
|
792
|
+
// friend-request to its address, so a fresh install joins the
|
|
793
|
+
// shared network with zero additional commands. To run in
|
|
794
|
+
// private (no dora) mode: `agentnet dora disable`. To point at
|
|
795
|
+
// your own dora: `agentnet dora enable --address <addr>` (it
|
|
796
|
+
// replaces the default).
|
|
797
|
+
dora: {
|
|
798
|
+
enabled: true,
|
|
799
|
+
userids: DEFAULT_DORAS.map((d) => d.userid),
|
|
800
|
+
refreshIntervalMs: 6e4,
|
|
801
|
+
// Default: auto-friend ONLY infrastructure — the dora registries and
|
|
802
|
+
// the official exit nodes (config/default-exits.yaml) — not every
|
|
803
|
+
// personal/compute box in the roster. Hub-and-spoke: a client connects
|
|
804
|
+
// to doras + exits; exits still serve arbitrary clients because the
|
|
805
|
+
// daemon auto-ACCEPTS incoming friend-requests. The full roster is
|
|
806
|
+
// still learned into IPAM for name/IP resolution. Run a full mesh with
|
|
807
|
+
// `agentnet dora autofriend all`, lock down with `... none`, or curate
|
|
808
|
+
// with `agentnet dora autofriend allow <peer>...`.
|
|
809
|
+
autoFriend: DEFAULT_AUTOFRIEND
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
static async save(config, filePath) {
|
|
814
|
+
const path = filePath || DEFAULT_CONFIG_FILE;
|
|
815
|
+
const dir = dirname(path);
|
|
816
|
+
mkdirSync(dir, { recursive: true });
|
|
817
|
+
const content = yaml.dump(config, { lineWidth: -1 });
|
|
818
|
+
const fs = await import("fs/promises");
|
|
819
|
+
await fs.writeFile(path, content, "utf-8");
|
|
820
|
+
}
|
|
821
|
+
static normalizeConfig(config) {
|
|
822
|
+
const defaults = this.createDefault("unnamed-node");
|
|
823
|
+
return {
|
|
824
|
+
node: {
|
|
825
|
+
name: config.node?.name || defaults.node.name,
|
|
826
|
+
namespace: config.node?.namespace || defaults.node.namespace
|
|
827
|
+
},
|
|
828
|
+
carrier: {
|
|
829
|
+
dataDir: config.carrier?.dataDir || defaults.carrier.dataDir,
|
|
830
|
+
// ALWAYS use the latest DEFAULT_BOOTSTRAP_NODES, even if the
|
|
831
|
+
// user's config.yaml has an older list. Bootstrap nodes are
|
|
832
|
+
// shared public infrastructure — operators who want a private
|
|
833
|
+
// mesh point at their own dora (which handles peer discovery
|
|
834
|
+
// entirely); they don't customize Carrier bootstraps. Without
|
|
835
|
+
// this auto-update, a stale config.yaml from an early install
|
|
836
|
+
// pins a US peer to a China relay forever even after we
|
|
837
|
+
// reorder defaults, and every install upgrade keeps the bad
|
|
838
|
+
// first-relay landing pattern.
|
|
839
|
+
bootstrapNodes: defaults.carrier.bootstrapNodes,
|
|
840
|
+
expressNodes: config.carrier?.expressNodes || defaults.carrier.expressNodes
|
|
841
|
+
},
|
|
842
|
+
network: {
|
|
843
|
+
interface: config.network?.interface || defaults.network.interface,
|
|
844
|
+
ip: config.network?.ip || defaults.network.ip,
|
|
845
|
+
subnet: config.network?.subnet || defaults.network.subnet,
|
|
846
|
+
dnsDomain: config.network?.dnsDomain || defaults.network.dnsDomain,
|
|
847
|
+
dnsPort: config.network?.dnsPort || defaults.network.dnsPort
|
|
848
|
+
},
|
|
849
|
+
paths: {
|
|
850
|
+
ipamFile: config.paths?.ipamFile || defaults.paths.ipamFile,
|
|
851
|
+
policyFile: config.paths?.policyFile || defaults.paths.policyFile,
|
|
852
|
+
auditLog: config.paths?.auditLog || defaults.paths.auditLog
|
|
853
|
+
},
|
|
854
|
+
proxy: config.proxy ?? defaults.proxy,
|
|
855
|
+
friends: config.friends ?? defaults.friends,
|
|
856
|
+
dora: config.dora ?? defaults.dora
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// src/console/data.ts
|
|
862
|
+
function hhmm(ts) {
|
|
863
|
+
if (!ts) return "";
|
|
864
|
+
const d = new Date(ts);
|
|
865
|
+
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
866
|
+
}
|
|
867
|
+
function lanVersion() {
|
|
868
|
+
try {
|
|
869
|
+
const req = createRequire(import.meta.url);
|
|
870
|
+
return req("../../package.json").version ?? "";
|
|
871
|
+
} catch {
|
|
872
|
+
return "";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
var DaemonClient = class _DaemonClient {
|
|
876
|
+
constructor(sockPath) {
|
|
877
|
+
this.sockPath = sockPath;
|
|
878
|
+
}
|
|
879
|
+
static async create(configDir) {
|
|
880
|
+
const dir = configDir ?? resolve2(homedir2(), ".agentnet");
|
|
881
|
+
const config = await ConfigLoader.load(resolve2(dir, "config.yaml"));
|
|
882
|
+
const dataDir = (config.carrier.dataDir || resolve2(dir, "carrier")).replace(/\/+$/, "");
|
|
883
|
+
return new _DaemonClient(`${dataDir}/daemon.sock`);
|
|
884
|
+
}
|
|
885
|
+
call(req, timeoutMs = 5e3) {
|
|
886
|
+
return new Promise((res, rej) => {
|
|
887
|
+
const sock = createConnection(this.sockPath);
|
|
888
|
+
let buf = "";
|
|
889
|
+
const timer = setTimeout(() => {
|
|
890
|
+
sock.destroy();
|
|
891
|
+
rej(new Error("ipc timeout"));
|
|
892
|
+
}, timeoutMs);
|
|
893
|
+
sock.on("connect", () => sock.write(JSON.stringify(req) + "\n"));
|
|
894
|
+
sock.on("data", (c) => {
|
|
895
|
+
buf += c.toString("utf-8");
|
|
896
|
+
const nl = buf.indexOf("\n");
|
|
897
|
+
if (nl < 0) return;
|
|
898
|
+
clearTimeout(timer);
|
|
899
|
+
sock.end();
|
|
900
|
+
try {
|
|
901
|
+
res(JSON.parse(buf.slice(0, nl)));
|
|
902
|
+
} catch (e) {
|
|
903
|
+
rej(e);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
sock.on("error", (e) => {
|
|
907
|
+
clearTimeout(timer);
|
|
908
|
+
rej(e);
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
static data(r) {
|
|
913
|
+
return r.ok && r.data ? r.data : {};
|
|
914
|
+
}
|
|
915
|
+
/** diag + friends-list + friends-pending in one poll. */
|
|
916
|
+
async snapshot() {
|
|
917
|
+
const [diag, list, pending] = await Promise.all([
|
|
918
|
+
this.call({ op: "diag" }).catch(() => ({ ok: false, error: "x" })),
|
|
919
|
+
this.call({ op: "friends-list" }).catch(() => ({ ok: false, error: "x" })),
|
|
920
|
+
this.call({ op: "friends-pending" }).catch(() => ({ ok: false, error: "x" }))
|
|
921
|
+
]);
|
|
922
|
+
const d = _DaemonClient.data(diag);
|
|
923
|
+
const userId = d.identity?.userid ?? "";
|
|
924
|
+
const name = d.node?.name ?? "";
|
|
925
|
+
const me = {
|
|
926
|
+
handle: `${name || (userId ? userId.slice(0, 8) : "peer")}@decentnetwork`,
|
|
927
|
+
userId,
|
|
928
|
+
ip: d.tun?.ip ?? d.allocatedIp ?? "\u2014",
|
|
929
|
+
lan: lanVersion(),
|
|
930
|
+
channel: "@next"
|
|
931
|
+
};
|
|
932
|
+
const rawFriends = _DaemonClient.data(list).friends ?? [];
|
|
933
|
+
const friends = rawFriends.map((f) => {
|
|
934
|
+
const key = String(f.userid ?? f.carrierId ?? f.pubkey ?? "");
|
|
935
|
+
const via = f.via ?? (typeof f.transport === "string" ? f.transport : null);
|
|
936
|
+
const online = f.online === true || f.status === "online";
|
|
937
|
+
return {
|
|
938
|
+
id: key,
|
|
939
|
+
name: String(f.alias ?? f.name ?? (key ? key.slice(0, 10) : "(unnamed)")),
|
|
940
|
+
online,
|
|
941
|
+
agent: f.agent === true || /bot|claw/i.test(String(f.name ?? "")),
|
|
942
|
+
unread: Number(f.unread ?? 0),
|
|
943
|
+
via: via === "udp" || via === "both" || via === "direct" ? "direct" : via === "tcp-relay" || via === "relay" ? "relay" : online ? "direct" : null,
|
|
944
|
+
ping: typeof f.ping === "number" ? f.ping : null,
|
|
945
|
+
key
|
|
946
|
+
};
|
|
947
|
+
});
|
|
948
|
+
const rawPending = _DaemonClient.data(pending).pending ?? _DaemonClient.data(pending).requests ?? [];
|
|
949
|
+
const requests = rawPending.map((r, i) => {
|
|
950
|
+
const userid = String(r.userid ?? r.carrierId ?? "");
|
|
951
|
+
return {
|
|
952
|
+
id: userid || `q${i}`,
|
|
953
|
+
userid,
|
|
954
|
+
key: String(r.address ?? r.carrier ?? r.key ?? userid),
|
|
955
|
+
via: String(r.via ?? "lan"),
|
|
956
|
+
time: String(r.time ?? "")
|
|
957
|
+
};
|
|
958
|
+
});
|
|
959
|
+
return { me, friends, requests };
|
|
960
|
+
}
|
|
961
|
+
async history(userid) {
|
|
962
|
+
const r = await this.call({ op: "chat-history", userid }).catch(() => ({ ok: false, error: "x" }));
|
|
963
|
+
const chats = _DaemonClient.data(r).chats ?? {};
|
|
964
|
+
const arr = chats[userid] ?? [];
|
|
965
|
+
return arr.map((m) => ({
|
|
966
|
+
t: hhmm(typeof m.ts === "number" ? m.ts : void 0),
|
|
967
|
+
who: m.dir === "out" ? "me" : "them",
|
|
968
|
+
text: String(m.text ?? "")
|
|
969
|
+
}));
|
|
970
|
+
}
|
|
971
|
+
send(userid, text) {
|
|
972
|
+
return this.call({ op: "chat-send", userid, text });
|
|
973
|
+
}
|
|
974
|
+
markRead(userid) {
|
|
975
|
+
return this.call({ op: "chat-mark-read", userid });
|
|
976
|
+
}
|
|
977
|
+
accept(userid) {
|
|
978
|
+
return this.call({ op: "friends-accept", userid });
|
|
979
|
+
}
|
|
980
|
+
reject(userid) {
|
|
981
|
+
return this.call({ op: "friends-reject", userid });
|
|
982
|
+
}
|
|
983
|
+
add(address) {
|
|
984
|
+
return this.call({ op: "friend-request", address });
|
|
985
|
+
}
|
|
986
|
+
remove(userid) {
|
|
987
|
+
return this.call({ op: "friend-remove", userid });
|
|
988
|
+
}
|
|
989
|
+
alias(userid, alias) {
|
|
990
|
+
return this.call({ op: "friend-set-alias", userid, alias });
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// src/console/index.tsx
|
|
995
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
996
|
+
async function runConsole(opts = {}) {
|
|
997
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
998
|
+
process.stderr.write("agentnet console needs an interactive terminal (TTY).\n");
|
|
999
|
+
process.exitCode = 1;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const client = await DaemonClient.create(opts.configDir);
|
|
1003
|
+
const { waitUntilExit } = render(/* @__PURE__ */ jsx3(App, { client }));
|
|
1004
|
+
await waitUntilExit();
|
|
1005
|
+
}
|
|
1006
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1007
|
+
void runConsole();
|
|
1008
|
+
}
|
|
1009
|
+
export {
|
|
1010
|
+
runConsole
|
|
1011
|
+
};
|