@decentnetwork/lan 0.1.16 → 0.1.18

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.
@@ -159,12 +159,29 @@ export class PeerManager extends EventEmitter {
159
159
  if (!this.peer) {
160
160
  throw new Error("Peer not created. Call create() first.");
161
161
  }
162
- return this.peer.friends().map((friend) => ({
163
- pubkey: friend.pubkey,
164
- name: friend.name,
165
- status: friend.status === "online" ? "online" : "offline",
166
- lastSeen: friend.acceptedAt,
167
- }));
162
+ return this.peer.friends().map((friend) => {
163
+ // Preserve the SDK's three-state status ("requested" matters —
164
+ // collapsing it into "offline" hid the "did my friend-request
165
+ // make it across?" diagnostic from diag output).
166
+ const status = friend.status === "online"
167
+ ? "online"
168
+ : friend.status === "requested"
169
+ ? "requested"
170
+ : "offline";
171
+ // Both pubkey and userid are populated with the base58 userid in
172
+ // every current SDK code path, but persisted records from
173
+ // pre-1.7.x clients may only have pubkey. Keep both forms.
174
+ const carrierId = friend.userid || friend.pubkey;
175
+ return {
176
+ pubkey: friend.pubkey,
177
+ carrierId,
178
+ name: friend.name,
179
+ status,
180
+ address: friend.address,
181
+ lastSeen: friend.acceptedAt,
182
+ acceptedAt: friend.acceptedAt,
183
+ };
184
+ });
168
185
  }
169
186
  /**
170
187
  * Check if a specific friend is currently online (direct UDP path established).
@@ -90,13 +90,16 @@ export declare function cmdFriendRequest(args: {
90
90
  * (this takes ~45s), then wait for the request to arrive.
91
91
  */
92
92
  export declare function cmdFriendAccept(args: {
93
- pubkey?: string;
93
+ userid?: string;
94
94
  waitForRequest?: boolean;
95
95
  waitMs?: number;
96
96
  configDir?: string;
97
97
  }): Promise<void>;
98
98
  /**
99
- * List friends in the friend store.
99
+ * List friends in the friend store. Prefers the live daemon over IPC —
100
+ * opening a second Peer with the same keyfile would stomp on the
101
+ * daemon's Carrier session, so we only fall back to that path when the
102
+ * daemon isn't running. Same pattern as `peers list` and `resolve`.
100
103
  */
101
104
  export declare function cmdFriendsList(args: {
102
105
  configDir?: string;
@@ -460,6 +460,21 @@ export async function cmdFriendRequest(args) {
460
460
  export async function cmdFriendAccept(args) {
461
461
  const dir = args.configDir || ConfigLoader.defaultConfigDir();
462
462
  const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
463
+ // When the daemon is up, route the accept through IPC — no need
464
+ // to bring the daemon down. This is what 'friends accept' does
465
+ // and 'friend-accept' is just an alias for that path now.
466
+ if (daemonPid(config) !== null) {
467
+ if (!args.userid) {
468
+ throw new Error("userid is required when the daemon is up. Use 'agentnet friends pending' to see queued requests, then 'agentnet friend-accept <userid>'.");
469
+ }
470
+ await cmdFriendsAccept({ userid: args.userid, configDir: args.configDir });
471
+ return;
472
+ }
473
+ // Daemon-down fallback: open a standalone peer. This path stays
474
+ // because it's the only way to accept a request when the daemon
475
+ // can't run (e.g. the operator hasn't set up the helper binary
476
+ // yet). The --wait interactive mode is also useful for one-shot
477
+ // bootstrapping.
463
478
  assertDaemonNotRunning(config, "friend-accept");
464
479
  const { Peer } = await import("@decentnetwork/peer");
465
480
  const keyFile = resolve(config.carrier.dataDir, "keypair.json");
@@ -487,7 +502,10 @@ export async function cmdFriendAccept(args) {
487
502
  if (stored.length === 0) {
488
503
  console.warn(`Self-announce got 0 storage nodes — request may still arrive via express relay`);
489
504
  }
490
- let pubkey = args.pubkey;
505
+ // The SDK uses `pubkey` as the local-var name historically; semantically
506
+ // this is the sender's userid (base58). Keep the variable name to
507
+ // minimize churn but document the mapping.
508
+ let pubkey = args.userid;
491
509
  const wait = args.waitForRequest;
492
510
  if (!pubkey || wait) {
493
511
  const waitMs = args.waitMs ?? 120000;
@@ -500,7 +518,7 @@ export async function cmdFriendAccept(args) {
500
518
  catch (err) {
501
519
  if (!pubkey)
502
520
  throw err;
503
- console.warn(`No incoming request — will try acceptance with provided pubkey ${pubkey}`);
521
+ console.warn(`No incoming request — will try acceptance with provided userid ${pubkey}`);
504
522
  }
505
523
  }
506
524
  console.log(`Accepting friend request from ${pubkey}...`);
@@ -511,46 +529,63 @@ export async function cmdFriendAccept(args) {
511
529
  console.log(`Friend accepted: ${pubkey}`);
512
530
  }
513
531
  /**
514
- * List friends in the friend store.
532
+ * List friends in the friend store. Prefers the live daemon over IPC —
533
+ * opening a second Peer with the same keyfile would stomp on the
534
+ * daemon's Carrier session, so we only fall back to that path when the
535
+ * daemon isn't running. Same pattern as `peers list` and `resolve`.
515
536
  */
516
537
  export async function cmdFriendsList(args) {
517
538
  const dir = args.configDir || ConfigLoader.defaultConfigDir();
518
539
  const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
519
- assertDaemonNotRunning(config, "friends list");
520
- const { Peer } = await import("@decentnetwork/peer");
521
- const keyFile = resolve(config.carrier.dataDir, "keypair.json");
522
- if (!existsSync(keyFile)) {
523
- console.log("No identity yet. Start the daemon at least once to generate one.");
524
- return;
540
+ let friends;
541
+ let source;
542
+ if (daemonPid(config) !== null) {
543
+ const res = await ipcCall(config, { op: "diag" });
544
+ if (!res.ok)
545
+ throw new Error(`Daemon diag failed: ${res.error}`);
546
+ friends = (res.data.friends ?? []);
547
+ source = "daemon";
548
+ }
549
+ else {
550
+ const { Peer } = await import("@decentnetwork/peer");
551
+ const keyFile = resolve(config.carrier.dataDir, "keypair.json");
552
+ if (!existsSync(keyFile)) {
553
+ console.log("No identity yet. Start the daemon at least once to generate one.");
554
+ return;
555
+ }
556
+ const peer = await Peer.create({
557
+ keyFile,
558
+ compatibilityMode: "legacy",
559
+ bootstrapNodes: config.carrier.bootstrapNodes,
560
+ expressNodes: config.carrier.expressNodes,
561
+ });
562
+ await peer.start();
563
+ friends = peer.friends().map((f) => ({
564
+ pubkey: f.pubkey,
565
+ carrierId: f.userid || f.pubkey,
566
+ name: f.name,
567
+ status: f.status,
568
+ address: f.address,
569
+ acceptedAt: f.acceptedAt,
570
+ }));
571
+ await peer.stop();
572
+ source = "disk";
525
573
  }
526
- // Open peer and start (start() loads friends from disk)
527
- const peer = await Peer.create({
528
- keyFile,
529
- compatibilityMode: "legacy",
530
- bootstrapNodes: config.carrier.bootstrapNodes,
531
- expressNodes: config.carrier.expressNodes,
532
- });
533
- await peer.start();
534
- const friends = peer.friends();
535
574
  if (friends.length === 0) {
536
575
  console.log("No friends. Use 'agentnet friend-request --address <addr>' to add one.");
537
- await peer.stop();
538
576
  return;
539
577
  }
540
- console.log(`Friends (${friends.length}):`);
578
+ console.log(`Friends (${friends.length}) ${source === "daemon" ? "[live]" : "[disk]"}:`);
541
579
  for (const friend of friends) {
542
580
  console.log(``);
543
581
  console.log(` ${friend.name || "(unnamed)"} status=${friend.status}`);
544
- // userid is Carrier's first-class identifier; show that, hide
545
- // the raw hex pubkey unless someone explicitly needs it.
546
- const userid = friend.userid && friend.userid !== friend.pubkey ? friend.userid : friend.pubkey;
582
+ const userid = friend.carrierId || friend.pubkey;
547
583
  console.log(` userid: ${userid}`);
548
584
  if (friend.address)
549
585
  console.log(` address: ${friend.address}`);
550
586
  if (friend.acceptedAt)
551
587
  console.log(` accepted: ${new Date(friend.acceptedAt).toISOString()}`);
552
588
  }
553
- await peer.stop();
554
589
  }
555
590
  /**
556
591
  * List queued friend-requests held by the running daemon (the ones
package/dist/cli/index.js CHANGED
@@ -112,8 +112,8 @@ async function main() {
112
112
  configDir: argv["config-dir"],
113
113
  });
114
114
  })
115
- .command("friend-request", "Send a friend request (run while daemon is down)", (y) => y
116
- .option("address", { type: "string", demandOption: true, describe: "Recipient Carrier address" })
115
+ .command("friend-request <address>", "Send a friend request (routes through the running daemon when up; opens a standalone peer when down)", (y) => y
116
+ .positional("address", { type: "string", demandOption: true, describe: "Recipient Carrier address" })
117
117
  .option("hello", { type: "string", describe: "Greeting message" })
118
118
  .option("wait-ms", { type: "number", default: 8000, describe: "Wait time for relay delivery" })
119
119
  .option("config-dir", { type: "string" }), async (argv) => {
@@ -124,13 +124,27 @@ async function main() {
124
124
  configDir: argv["config-dir"],
125
125
  });
126
126
  })
127
- .command("friend-accept", "Accept a friend request (run while daemon is down)", (y) => y
128
- .option("pubkey", { type: "string", describe: "Sender pubkey (omit with --wait to receive interactively)" })
129
- .option("wait", { type: "boolean", default: false, describe: "Wait for incoming request" })
127
+ // `friend-accept` is the legacy daemon-down command. With autoAccept
128
+ // on by default (the standard config), this is rarely used but we
129
+ // keep it as an alias for `friends accept` so an operator running
130
+ // an old recipe doesn't dead-end.
131
+ //
132
+ // Positional userid is supported. The flag is named --userid (NOT
133
+ // --pubkey) because Carrier's identifier model is address + userid;
134
+ // "pubkey" only exists as a hex-encoded internal representation
135
+ // operators never need to touch.
136
+ .command("friend-accept [userid]", "Accept a queued friend-request by userid (alias of 'friends accept')", (y) => y
137
+ .positional("userid", { type: "string", describe: "Sender's Carrier userid (base58)" })
138
+ .option("userid", { type: "string", describe: "Sender's Carrier userid (base58)" })
139
+ // --wait is kept for the daemon-down interactive path
140
+ // (rarely used now; only matters when autoAccept is off
141
+ // AND the operator wants to run a one-shot accept without
142
+ // bringing the daemon up).
143
+ .option("wait", { type: "boolean", default: false, describe: "Daemon-down mode: wait for an incoming request" })
130
144
  .option("wait-ms", { type: "number", default: 120000, describe: "Time to wait for request (ms)" })
131
145
  .option("config-dir", { type: "string" }), async (argv) => {
132
146
  await cmdFriendAccept({
133
- pubkey: argv.pubkey,
147
+ userid: argv.userid,
134
148
  waitForRequest: argv.wait,
135
149
  waitMs: argv["wait-ms"],
136
150
  configDir: argv["config-dir"],
@@ -149,14 +163,24 @@ async function main() {
149
163
  .command("pending", "List queued friend-requests (over IPC; daemon must be up)", (yy) => yy.option("config-dir", { type: "string" }), async (argv) => {
150
164
  await cmdFriendsPending({ configDir: argv["config-dir"] });
151
165
  })
152
- .command("accept", "Accept a queued friend-request by userid (over IPC; daemon stays up)", (yy) => yy
153
- .option("userid", { type: "string", demandOption: true, describe: "Sender's Carrier userid (base58)" })
166
+ // Accept positional userid OR `--userid X`. Operators expect
167
+ // `agentnet friends accept <userid>` to just work without
168
+ // a positional that fails with "Unknown argument: <userid>"
169
+ // because yargs treats the unflagged value as junk.
170
+ .command("accept [userid]", "Accept a queued friend-request by userid (over IPC; daemon stays up)", (yy) => yy
171
+ .positional("userid", { type: "string", describe: "Sender's Carrier userid (base58)" })
172
+ .option("userid", { type: "string", describe: "Same as positional; either form works" })
154
173
  .option("config-dir", { type: "string" }), async (argv) => {
174
+ if (!argv.userid)
175
+ throw new Error("userid is required (positional or --userid)");
155
176
  await cmdFriendsAccept({ userid: argv.userid, configDir: argv["config-dir"] });
156
177
  })
157
- .command("reject", "Drop a queued friend-request by userid (over IPC; daemon stays up)", (yy) => yy
158
- .option("userid", { type: "string", demandOption: true })
178
+ .command("reject [userid]", "Drop a queued friend-request by userid (over IPC; daemon stays up)", (yy) => yy
179
+ .positional("userid", { type: "string", describe: "Sender's Carrier userid (base58)" })
180
+ .option("userid", { type: "string", describe: "Same as positional; either form works" })
159
181
  .option("config-dir", { type: "string" }), async (argv) => {
182
+ if (!argv.userid)
183
+ throw new Error("userid is required (positional or --userid)");
160
184
  await cmdFriendsReject({ userid: argv.userid, configDir: argv["config-dir"] });
161
185
  })
162
186
  .demandCommand(1, "Specify a friends subcommand (run 'agentnet friends --help')"), () => {
@@ -170,6 +170,8 @@ export class DaemonServer {
170
170
  carrierId: f.carrierId,
171
171
  name: f.name,
172
172
  status: f.status,
173
+ address: f.address,
174
+ acceptedAt: f.acceptedAt,
173
175
  })),
174
176
  ipam: ipamPeers,
175
177
  };
package/dist/types.d.ts CHANGED
@@ -8,11 +8,21 @@ export interface PeerIdentity {
8
8
  }
9
9
  export interface RemotePeer {
10
10
  pubkey: string;
11
- status: "online" | "offline" | "connecting";
11
+ /**
12
+ * Mirrors the SDK's FriendRecord.status verbatim:
13
+ * - "online": crypto session established
14
+ * - "offline": was a friend, currently disconnected
15
+ * - "requested": we sent a friend-request; peer hasn't accepted
16
+ * Collapsing "requested" into "offline" loses the operator signal
17
+ * ("did my friend-request go through?") so keep them distinct.
18
+ */
19
+ status: "online" | "offline" | "requested";
12
20
  name?: string;
13
21
  carrierId?: string;
14
22
  virtualIp?: string;
23
+ address?: string;
15
24
  lastSeen?: number;
25
+ acceptedAt?: number;
16
26
  }
17
27
  export interface FriendConnectionEvent {
18
28
  pubkey: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",