@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.
@@ -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
+ };