@decentnetwork/lan 0.1.99 → 0.1.100

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.
Binary file
Binary file
Binary file
Binary file
@@ -53,6 +53,10 @@ export declare class PeerManager extends EventEmitter {
53
53
  name?: string;
54
54
  description?: string;
55
55
  }): void;
56
+ /** Offer a file to a friend (toxcore-standard transfer). Returns the fileId. */
57
+ sendFile(userid: string, data: Uint8Array, name: string): string | null;
58
+ /** Accept an incoming file offer. */
59
+ acceptFile(userid: string, fileNumber: number): void;
56
60
  /**
57
61
  * Send an outbound friend request to a Carrier address (NOT a bare
58
62
  * userid — sendFriendRequest needs the address form because it
@@ -106,6 +106,16 @@ export class PeerManager extends EventEmitter {
106
106
  this.peer.setUserInfo(info);
107
107
  this.logger.info(`Profile updated (name="${info.name ?? "(unchanged)"}")`);
108
108
  }
109
+ /** Offer a file to a friend (toxcore-standard transfer). Returns the fileId. */
110
+ sendFile(userid, data, name) {
111
+ if (!this.peer)
112
+ throw new Error("Peer not created. Call create() first.");
113
+ return this.peer.sendFile(userid, data, { name });
114
+ }
115
+ /** Accept an incoming file offer. */
116
+ acceptFile(userid, fileNumber) {
117
+ this.peer?.acceptFile(userid, fileNumber);
118
+ }
109
119
  /**
110
120
  * Send an outbound friend request to a Carrier address (NOT a bare
111
121
  * userid — sendFriendRequest needs the address form because it
@@ -477,6 +487,12 @@ export class PeerManager extends EventEmitter {
477
487
  }
478
488
  this.emit("message", message.pubkey, message.text);
479
489
  });
490
+ // Toxcore-standard file transfer (native-compatible). Forward offers,
491
+ // progress, and completions to the daemon, which auto-accepts and saves.
492
+ this.peer.onFile((o) => this.emit("file-offer", o));
493
+ this.peer.onFileProgress((p) => this.emit("file-progress", p));
494
+ this.peer.onFileComplete((p) => this.emit("file-complete", p));
495
+ this.peer.onFileCancel((p) => this.emit("file-cancel", p));
480
496
  // decentlan custom packets, all lossless so they traverse relay-only
481
497
  // sessions: IP data (163), session handshake (161), dora control (162).
482
498
  // Each on its own Carrier packet id — a chat message can never be mistaken
@@ -254,6 +254,17 @@ export declare function cmdChatSend(args: {
254
254
  text: string;
255
255
  configDir?: string;
256
256
  }): Promise<void>;
257
+ /**
258
+ * Send a file to a friend over toxcore-standard file transfer (direct, over the
259
+ * reliable net_crypto channel — no express, no size cap). The daemon reads the
260
+ * file by absolute path, so it must be readable by the daemon's user. The
261
+ * recipient's daemon auto-accepts and saves to its <configDir>/downloads/.
262
+ */
263
+ export declare function cmdFileSend(args: {
264
+ to: string;
265
+ path: string;
266
+ configDir?: string;
267
+ }): Promise<void>;
257
268
  /**
258
269
  * Print chat history (sent + received). Incoming messages are logged by the
259
270
  * daemon as they arrive, so this is also how you READ messages from friends.
@@ -1083,6 +1083,23 @@ export async function cmdChatSend(args) {
1083
1083
  throw new Error(`Daemon refused: ${res.error}`);
1084
1084
  console.log(`-> ${args.to.slice(0, 16)}… ${args.text}`);
1085
1085
  }
1086
+ /**
1087
+ * Send a file to a friend over toxcore-standard file transfer (direct, over the
1088
+ * reliable net_crypto channel — no express, no size cap). The daemon reads the
1089
+ * file by absolute path, so it must be readable by the daemon's user. The
1090
+ * recipient's daemon auto-accepts and saves to its <configDir>/downloads/.
1091
+ */
1092
+ export async function cmdFileSend(args) {
1093
+ const dir = args.configDir || ConfigLoader.defaultConfigDir();
1094
+ const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
1095
+ const abs = resolve(process.cwd(), args.path);
1096
+ const res = await ipcCall(config, { op: "file-send", userid: args.to, path: abs }, 30000);
1097
+ if (!res.ok)
1098
+ throw new Error(`File send failed: ${res.error}`);
1099
+ const d = (res.data ?? {});
1100
+ console.log(`Offered "${d.name}" (${d.size} bytes) to ${args.to.slice(0, 16)}… — fileId ${d.fileId}`);
1101
+ console.log("Streams once the recipient accepts; their daemon saves it to ~/.agentnet/downloads/.");
1102
+ }
1086
1103
  /**
1087
1104
  * Print chat history (sent + received). Incoming messages are logged by the
1088
1105
  * daemon as they arrive, so this is also how you READ messages from friends.
package/dist/cli/index.js CHANGED
@@ -10,7 +10,7 @@ import { hideBin } from "yargs/helpers";
10
10
  // Belt-and-braces — also raise it here in case the CLI is run directly
11
11
  // (e.g. `node dist/cli/index.js` rather than via dist/index.js).
12
12
  EventEmitter.defaultMaxListeners = 100;
13
- import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdProxyRouter, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
13
+ import { cmdInit, cmdIdentityShow, cmdPeersList, cmdIpamAssign, cmdGrant, cmdRevoke, cmdResolve, cmdStatus, cmdUp, cmdAuditLog, cmdFriendRequest, cmdFriendAccept, cmdFriendsList, cmdFriendsPending, cmdFriendsAccept, cmdFriendsReject, cmdProxyEnable, cmdProxyDisable, cmdProxyStatus, cmdProxyAllowHost, cmdProxyRevokeHost, cmdProxyListHosts, cmdProxyUse, cmdProxyRouter, cmdDoraEnable, cmdDoraDisable, cmdDoraStatus, cmdDoraAutofriend, cmdDiag, cmdDoctor, cmdDnsInstall, cmdDnsHosts, cmdServiceInstall, cmdRestart, cmdServiceStatus, cmdServiceRestart, cmdUi, cmdConsole, cmdFileSend, cmdChatSend, cmdChatHistory, cmdFriendRemove, cmdFriendAlias, } from "./commands.js";
14
14
  async function main() {
15
15
  await yargs(hideBin(process.argv))
16
16
  .scriptName("agentnet")
@@ -155,6 +155,17 @@ async function main() {
155
155
  })
156
156
  .command("console", "Interactive terminal UI (TUI) — friends list + chat, keyboard-driven (q to quit)", (y) => y.option("config-dir", { type: "string" }), async (argv) => {
157
157
  await cmdConsole({ configDir: argv["config-dir"] });
158
+ })
159
+ .command("file <action> <userid> [path]", "Send a file to a friend (toxcore-standard, direct; recipient auto-saves to ~/.agentnet/downloads)", (y) => y
160
+ .positional("action", { type: "string", choices: ["send"], demandOption: true })
161
+ .positional("userid", { type: "string", demandOption: true, describe: "Friend's userid" })
162
+ .positional("path", { type: "string", describe: "Path to the file to send" })
163
+ .option("config-dir", { type: "string" }), async (argv) => {
164
+ if (argv.action === "send") {
165
+ if (!argv.path)
166
+ throw new Error("path is required: agentnet file send <userid> <path>");
167
+ await cmdFileSend({ to: argv.userid, path: argv.path, configDir: argv["config-dir"] });
168
+ }
158
169
  })
159
170
  // Tell the running daemon to re-exec itself with its original argv.
160
171
  // The daemon inherits its own uid (root if it was launched as root)
@@ -56,6 +56,8 @@ export interface IpcHandlers {
56
56
  name?: string;
57
57
  description?: string;
58
58
  }) => Promise<void>;
59
+ /** Offer a local file (by path) to a friend over toxcore file transfer. */
60
+ fileSend: (userid: string, path: string) => Promise<Record<string, unknown>>;
59
61
  /** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
60
62
  chatMarkRead: (userid: string, ts?: number) => Promise<void>;
61
63
  /** Re-read proxy allowlist from config and apply it to the running
@@ -74,7 +76,7 @@ export interface IpcHandlers {
74
76
  selfRestart: () => Promise<Record<string, unknown>>;
75
77
  }
76
78
  export interface IpcRequest {
77
- op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "set-profile" | "chat-mark-read" | "proxy-reload" | "self-restart";
79
+ op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "set-profile" | "file-send" | "chat-mark-read" | "proxy-reload" | "self-restart";
78
80
  address?: string;
79
81
  hello?: string;
80
82
  userid?: string;
@@ -82,6 +84,7 @@ export interface IpcRequest {
82
84
  alias?: string;
83
85
  name?: string;
84
86
  description?: string;
87
+ path?: string;
85
88
  before?: number;
86
89
  limit?: number;
87
90
  ts?: number;
@@ -170,6 +170,13 @@ export class IpcServer {
170
170
  await this.handlers.setProfile({ name: req.name, description: req.description });
171
171
  return;
172
172
  }
173
+ case "file-send": {
174
+ if (!req.userid)
175
+ throw new Error("userid is required");
176
+ if (!req.path)
177
+ throw new Error("path is required");
178
+ return await this.handlers.fileSend(req.userid, req.path);
179
+ }
173
180
  case "chat-mark-read": {
174
181
  if (!req.userid)
175
182
  throw new Error("userid is required");
@@ -295,6 +295,17 @@ export class DaemonServer {
295
295
  this.logger.warn(`Failed to persist profile: ${e.message}`);
296
296
  });
297
297
  },
298
+ fileSend: async (userid, path) => {
299
+ const fs = await import("fs/promises");
300
+ const { basename } = await import("path");
301
+ const data = await fs.readFile(path);
302
+ const name = basename(path);
303
+ const fileId = this.peerManager?.sendFile(userid, new Uint8Array(data), name);
304
+ if (!fileId)
305
+ throw new Error("No free transfer slot (or friend unknown)");
306
+ this.logger.info(`Offering file "${name}" (${data.length}B) to ${userid.slice(0, 8)}`);
307
+ return { fileId, name, size: data.length };
308
+ },
298
309
  chatMarkRead: async (userid, ts) => {
299
310
  this.friendMeta?.markRead(userid, ts);
300
311
  },
@@ -544,6 +555,27 @@ export class DaemonServer {
544
555
  this.peerManager.on("message", (pubkey, text) => {
545
556
  this.logChat(pubkey, "in", text);
546
557
  });
558
+ // File transfer (toxcore-standard): auto-accept offers from friends and
559
+ // save completed files under <configDir>/downloads/.
560
+ this.peerManager.on("file-offer", (o) => {
561
+ this.logger.info(`Incoming file "${o.name}" (${o.size}B) from ${o.friendId.slice(0, 8)} — accepting`);
562
+ this.peerManager?.acceptFile(o.friendId, o.fileNumber);
563
+ });
564
+ this.peerManager.on("file-complete", (p) => {
565
+ if (p.sending || !p.data) {
566
+ if (p.sending)
567
+ this.logger.info(`File "${p.name}" sent to ${p.friendId.slice(0, 8)}`);
568
+ return;
569
+ }
570
+ void (async () => {
571
+ const dir = resolve(this.configDir, "downloads");
572
+ const fs = await import("fs/promises");
573
+ await fs.mkdir(dir, { recursive: true });
574
+ const safe = (p.name || "file").replace(/[/\\]/g, "_");
575
+ await fs.writeFile(resolve(dir, safe), Buffer.from(p.data));
576
+ this.logger.info(`Saved file "${p.name}" (${p.size}B) from ${p.friendId.slice(0, 8)} → ${resolve(dir, safe)}`);
577
+ })().catch((e) => this.logger.warn(`Failed to save file: ${e.message}`));
578
+ });
547
579
  this.peerManager.on("friend-request", (req) => {
548
580
  void pubkeyHexToUserid(req.pubkey).then((userid) => {
549
581
  const who = `${req.name || "(unnamed)"} ${userid}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
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",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@decentnetwork/dora": "^0.1.6",
82
- "@decentnetwork/peer": "^0.1.42",
82
+ "@decentnetwork/peer": "^0.1.43",
83
83
  "ink": "^5.2.1",
84
84
  "js-yaml": "^4.1.0",
85
85
  "react": "^18.3.1",