palmier 0.8.10 → 0.8.11

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/README.md CHANGED
@@ -156,6 +156,7 @@ palmier clients revoke-all
156
156
  The wizard:
157
157
  - Detects installed agent CLIs and caches the result
158
158
  - Asks for the HTTP port
159
+ - Detects the default network interface (used for auto-LAN)
159
160
  - Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
160
161
  - Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
161
162
  - Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows)
@@ -167,6 +168,10 @@ The daemon automatically recovers existing tasks by reinstalling their system ti
167
168
 
168
169
  Agents are re-detected on every daemon start. Run `palmier restart` after installing or removing a CLI.
169
170
 
171
+ ### Re-detecting the LAN Network
172
+
173
+ The default network interface is detected once during `palmier init` and saved to `host.json`. The daemon derives the current IP live from that interface on each client connect, so DHCP-assigned IP changes on the same adapter are picked up automatically. If you physically switch to a different network adapter (e.g., plug in Ethernet after running on WiFi, or add a new USB-tethered interface), run `palmier init` again to re-detect.
174
+
170
175
  ## CLI Reference
171
176
 
172
177
  | Command | Description |
@@ -3,6 +3,7 @@ import { loadConfig, saveConfig } from "../config.js";
3
3
  import { detectAgents } from "../agents/agent.js";
4
4
  import { getPlatform } from "../platform/index.js";
5
5
  import { pairCommand } from "./pair.js";
6
+ import { detectDefaultInterface, getInterfaceIpv4 } from "../network.js";
6
7
  import { listTasks } from "../task.js";
7
8
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
8
9
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -31,6 +32,8 @@ export async function initCommand() {
31
32
  const parsed = parseInt(portAnswer.trim(), 10);
32
33
  if (parsed > 0 && parsed < 65536)
33
34
  httpPort = parsed;
35
+ const defaultInterface = (await detectDefaultInterface()) ?? undefined;
36
+ const lanIp = defaultInterface ? getInterfaceIpv4(defaultInterface) : null;
34
37
  console.log(`\n${bold("Setup summary:")}\n`);
35
38
  console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
36
39
  console.log(` All tasks and execution data will be stored here.\n`);
@@ -39,8 +42,15 @@ export async function initCommand() {
39
42
  console.log(` ${dim("Remote (web):")} ${cyan("https://app.palmier.me")}`);
40
43
  console.log(` Pair a browser on any device. Traffic always goes through the relay.\n`);
41
44
  console.log(` ${dim("Remote (app):")} ${cyan("https://github.com/caihongxu/palmier-android/releases")}`);
42
- console.log(` Download the Android APK. The app uses LAN for direct RPC`);
43
- console.log(` when on the same network, otherwise the relay.\n`);
45
+ if (lanIp) {
46
+ console.log(` Download the Android APK. The app uses LAN for direct RPC`);
47
+ console.log(` on the same network (detected ${cyan(`http://${lanIp}:${httpPort}`)}),`);
48
+ console.log(` otherwise the relay.\n`);
49
+ }
50
+ else {
51
+ console.log(` Download the Android APK. Traffic will go through the relay —`);
52
+ console.log(` ${red("could not detect a LAN interface")} for direct RPC.\n`);
53
+ }
44
54
  console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
45
55
  const existingTasks = listTasks(process.cwd());
46
56
  if (existingTasks.length > 0) {
@@ -89,6 +99,7 @@ export async function initCommand() {
89
99
  natsNkeySeed: registerResponse.natsNkeySeed,
90
100
  agents,
91
101
  httpPort,
102
+ defaultInterface,
92
103
  };
93
104
  saveConfig(config);
94
105
  console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
@@ -1,10 +1,9 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "node:os";
3
3
  import { StringCodec } from "nats";
4
- import { loadConfig, saveConfig } from "../config.js";
4
+ import { loadConfig } from "../config.js";
5
5
  import { connectNats } from "../nats-client.js";
6
6
  import { addClient } from "../client-store.js";
7
- import { detectDefaultInterface } from "../network.js";
8
7
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
9
8
  const CODE_LENGTH = 6;
10
9
  export const PAIRING_EXPIRY_MS = 60 * 1000; // 1 minute
@@ -13,13 +12,8 @@ export function generatePairingCode() {
13
12
  crypto.getRandomValues(bytes);
14
13
  return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
15
14
  }
16
- async function buildPairResponse(config, label) {
15
+ function buildPairResponse(config, label) {
17
16
  const client = addClient(label);
18
- const iface = await detectDefaultInterface();
19
- if (iface && iface !== config.defaultInterface) {
20
- config.defaultInterface = iface;
21
- saveConfig(config);
22
- }
23
17
  return {
24
18
  hostId: config.hostId,
25
19
  clientToken: client.token,
@@ -91,7 +85,7 @@ export async function pairCommand() {
91
85
  }
92
86
  }
93
87
  catch { /* empty body is fine */ }
94
- const response = await buildPairResponse(config, label);
88
+ const response = buildPairResponse(config, label);
95
89
  if (msg.reply) {
96
90
  msg.respond(sc.encode(JSON.stringify(response)));
97
91
  }
package/dist/network.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Resolve the name of the network interface used for the IPv4 default route.
3
- * Returns null when no default route is found (e.g. fully offline host) or
4
- * when the OS platform isn't supported by `default-gateway`.
5
- */
6
1
  export declare function detectDefaultInterface(): Promise<string | null>;
7
2
  export declare function getInterfaceIpv4(interfaceName: string): string | null;
8
3
  export declare function buildLanUrl(port: number, interfaceName: string | undefined): string | null;
package/dist/network.js CHANGED
@@ -1,19 +1,85 @@
1
1
  import * as os from "node:os";
2
- // @ts-expect-error - default-gateway ships no types
3
- import { gateway4async } from "default-gateway";
2
+ import * as dgram from "node:dgram";
4
3
  /**
5
4
  * Resolve the name of the network interface used for the IPv4 default route.
6
- * Returns null when no default route is found (e.g. fully offline host) or
7
- * when the OS platform isn't supported by `default-gateway`.
5
+ * Falls back to the first non-internal IPv4 interface when the gateway lookup
6
+ * fails `default-gateway` shells out to `wmic` on Windows, which was removed
7
+ * in Windows 11 24H2.
8
8
  */
9
+ function findInterfaceByIp(ip) {
10
+ for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
11
+ for (const addr of addrs ?? []) {
12
+ if (addr.family === "IPv4" && addr.address === ip)
13
+ return name;
14
+ }
15
+ }
16
+ return null;
17
+ }
18
+ /** Ask the kernel which local IPv4 would route to an external address. No packet is sent. */
19
+ function probeOutboundIp() {
20
+ return new Promise((resolve) => {
21
+ const sock = dgram.createSocket("udp4");
22
+ const cleanup = (ip) => { try {
23
+ sock.close();
24
+ }
25
+ catch { /* ignore */ } resolve(ip); };
26
+ sock.on("error", () => cleanup(null));
27
+ try {
28
+ sock.connect(80, "8.8.8.8", () => {
29
+ const addr = sock.address();
30
+ cleanup(addr.address && addr.address !== "0.0.0.0" ? addr.address : null);
31
+ });
32
+ }
33
+ catch {
34
+ cleanup(null);
35
+ }
36
+ });
37
+ }
38
+ /** Lower score = more preferred. 0=192.168, 1=10.x, 2=172.16-31, 3=everything else. */
39
+ function ipClass(ip) {
40
+ const parts = ip.split(".").map(Number);
41
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p)))
42
+ return 3;
43
+ const [a, b] = parts;
44
+ if (a === 192 && b === 168)
45
+ return 0;
46
+ if (a === 10)
47
+ return 1;
48
+ if (a === 172 && b >= 16 && b <= 31)
49
+ return 2;
50
+ return 3;
51
+ }
52
+ /** Names that commonly belong to virtual/VPN adapters we'd rather skip. */
53
+ const VIRTUAL_NAME_PATTERNS = [
54
+ "vethernet", "virtualbox", "vmware", "hyper-v", "docker", "bridge",
55
+ "tailscale", "wireguard", "meta", "vpn", "tun", "tap", "loopback",
56
+ "wsl", "utun",
57
+ ];
58
+ function isVirtualName(name) {
59
+ const lower = name.toLowerCase();
60
+ return VIRTUAL_NAME_PATTERNS.some((p) => lower.includes(p));
61
+ }
9
62
  export async function detectDefaultInterface() {
10
- try {
11
- const result = await gateway4async();
12
- return result.int ?? null;
63
+ const probedIp = await probeOutboundIp();
64
+ if (probedIp) {
65
+ const name = findInterfaceByIp(probedIp);
66
+ if (name && !isVirtualName(name))
67
+ return name;
13
68
  }
14
- catch {
15
- return null;
69
+ const candidates = [];
70
+ for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
71
+ for (const addr of addrs ?? []) {
72
+ if (addr.family !== "IPv4" || addr.internal)
73
+ continue;
74
+ candidates.push({ name, klass: ipClass(addr.address), virtual: isVirtualName(name) });
75
+ }
16
76
  }
77
+ candidates.sort((a, b) => {
78
+ if (a.virtual !== b.virtual)
79
+ return a.virtual ? 1 : -1;
80
+ return a.klass - b.klass;
81
+ });
82
+ return candidates[0]?.name ?? null;
17
83
  }
18
84
  export function getInterfaceIpv4(interfaceName) {
19
85
  const addrs = os.networkInterfaces()[interfaceName];