palmier 0.8.9 → 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.
Files changed (36) hide show
  1. package/README.md +13 -7
  2. package/dist/commands/info.js +5 -0
  3. package/dist/commands/init.js +17 -6
  4. package/dist/commands/pair.js +0 -3
  5. package/dist/network.d.ts +4 -0
  6. package/dist/network.js +100 -0
  7. package/dist/pwa/assets/{index-1gs4vwFo.js → index-B7S0YoMo.js} +45 -45
  8. package/dist/pwa/assets/{index-DQJHVyP6.css → index-DhphickB.css} +1 -1
  9. package/dist/pwa/assets/{web-BqVsIFtP.js → web-4WNPL7z3.js} +1 -1
  10. package/dist/pwa/assets/{web-lefgO9YR.js → web-Bpd2nO1M.js} +1 -1
  11. package/dist/pwa/assets/{web-DrSNtZ3i.js → web-DjwsAB0V.js} +1 -1
  12. package/dist/pwa/index.html +2 -2
  13. package/dist/pwa/service-worker.js +1 -1
  14. package/dist/rpc-handler.js +2 -0
  15. package/dist/transports/http-transport.d.ts +0 -1
  16. package/dist/transports/http-transport.js +8 -11
  17. package/dist/types.d.ts +4 -0
  18. package/package.json +1 -1
  19. package/palmier-server/README.md +1 -1
  20. package/palmier-server/pwa/src/App.css +8 -4
  21. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +54 -13
  22. package/palmier-server/pwa/src/constants.ts +1 -1
  23. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +13 -4
  24. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
  25. package/palmier-server/pwa/src/pages/Dashboard.tsx +6 -2
  26. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -2
  27. package/palmier-server/pwa/src/pages/PairSetup.tsx +5 -2
  28. package/palmier-server/pwa/src/types.ts +1 -1
  29. package/palmier-server/spec.md +7 -4
  30. package/src/commands/info.ts +6 -0
  31. package/src/commands/init.ts +16 -6
  32. package/src/commands/pair.ts +0 -3
  33. package/src/network.ts +96 -0
  34. package/src/rpc-handler.ts +2 -0
  35. package/src/transports/http-transport.ts +8 -11
  36. package/src/types.ts +5 -0
package/README.md CHANGED
@@ -110,14 +110,15 @@ All device tools work while the Palmier Android app is in the background — the
110
110
 
111
111
  ## Access Modes
112
112
 
113
- Two modes Local always works on the host machine; Server pairs other devices through the cloud relay.
113
+ Three ways to reach your host, ordered by setup effort:
114
114
 
115
- | Mode | URL | Pairing | Notes |
116
- |------|-----|---------|-------|
117
- | **Local** | `http://localhost:<port>` | Not required | Loopback only — open in a browser on the host machine. No internet needed. |
118
- | **Server** | [https://app.palmier.me](https://app.palmier.me) | Required | Pair the device with a one-time code. Push notifications, remote access from anywhere. |
115
+ | Mode | Where | Pairing | Notes |
116
+ |------|-------|---------|-------|
117
+ | **Local** | `http://localhost:<port>` in a browser on the host machine | Not required | Loopback only. No internet needed. |
118
+ | **Remote (web)** | [https://app.palmier.me](https://app.palmier.me) in any browser | Required | Always goes through the cloud relay. |
119
+ | **Remote (app)** | [Android APK](https://github.com/caihongxu/palmier-android/releases) | Required | Push notifications, background device capabilities, and **auto-LAN**. |
119
120
 
120
- **Auto-LAN (native app only).** When the Capacitor Android app is on the same network as the host, it transparently routes RPC over direct LAN HTTP (`http://<host-ip>:<port>/rpc/...`) instead of through the relay — lower latency, no protocol change. Events still flow over the relay. Browser PWAs can't do this and stay on the relay.
121
+ **Auto-LAN (native app only).** When the Android app is on the same network as the host, it transparently routes RPC over direct LAN HTTP (`http://<host-ip>:<port>/rpc/...`) instead of through the relay — lower latency, no protocol change. Events still flow over the relay. Pairing always goes through the relay regardless. Browser PWAs can't do this (Private Network Access / mixed-content restrictions) and stay on the relay.
121
122
 
122
123
  ## Security & Privacy
123
124
 
@@ -135,7 +136,7 @@ In all modes, client tokens are generated and validated entirely on your host. T
135
136
 
136
137
  Local access (`http://localhost:<port>`) works immediately — no pairing needed.
137
138
 
138
- For other devices, run `palmier pair` on the host to generate a code, then enter it at [https://app.palmier.me](https://app.palmier.me). Pairing always goes through the relay; auto-LAN kicks in transparently afterward when the device is on the same network.
139
+ For remote access (web or app), run `palmier pair` on the host to generate a code, then enter it at [https://app.palmier.me](https://app.palmier.me) or in the Android app. Pairing always goes through the relay; auto-LAN kicks in transparently afterward in the native app when on the same network.
139
140
 
140
141
  ### Managing Clients
141
142
 
@@ -155,6 +156,7 @@ palmier clients revoke-all
155
156
  The wizard:
156
157
  - Detects installed agent CLIs and caches the result
157
158
  - Asks for the HTTP port
159
+ - Detects the default network interface (used for auto-LAN)
158
160
  - Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
159
161
  - Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
160
162
  - Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows)
@@ -166,6 +168,10 @@ The daemon automatically recovers existing tasks by reinstalling their system ti
166
168
 
167
169
  Agents are re-detected on every daemon start. Run `palmier restart` after installing or removing a CLI.
168
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
+
169
175
  ## CLI Reference
170
176
 
171
177
  | Command | Description |
@@ -1,10 +1,15 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { loadClients } from "../client-store.js";
3
+ import { buildLanUrl } from "../network.js";
3
4
  export async function infoCommand() {
4
5
  const config = loadConfig();
5
6
  const clients = loadClients();
7
+ const port = config.httpPort ?? 7256;
8
+ const lanUrl = buildLanUrl(port, config.defaultInterface);
6
9
  console.log(`Host ID: ${config.hostId}`);
7
10
  console.log(`Project root: ${config.projectRoot}`);
11
+ console.log(`Local URL: http://localhost:${port}`);
12
+ console.log(`LAN URL: ${lanUrl ?? "(default route interface unavailable — pair a device or check network)"}`);
8
13
  if (config.agents && config.agents.length > 0) {
9
14
  console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
10
15
  }
@@ -3,7 +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 { detectLanIp } from "../transports/http-transport.js";
6
+ import { detectDefaultInterface, getInterfaceIpv4 } from "../network.js";
7
7
  import { listTasks } from "../task.js";
8
8
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
9
9
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -32,15 +32,25 @@ export async function initCommand() {
32
32
  const parsed = parseInt(portAnswer.trim(), 10);
33
33
  if (parsed > 0 && parsed < 65536)
34
34
  httpPort = parsed;
35
- const lanIp = detectLanIp();
35
+ const defaultInterface = (await detectDefaultInterface()) ?? undefined;
36
+ const lanIp = defaultInterface ? getInterfaceIpv4(defaultInterface) : null;
36
37
  console.log(`\n${bold("Setup summary:")}\n`);
37
38
  console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
38
39
  console.log(` All tasks and execution data will be stored here.\n`);
39
- console.log(` ${dim("Local access:")} ${cyan(`http://localhost:${httpPort}`)}`);
40
+ console.log(` ${dim("Local:")} ${cyan(`http://localhost:${httpPort}`)}`);
40
41
  console.log(` Open in a browser on this machine — no internet required.\n`);
41
- console.log(` ${dim("Remote access:")} ${cyan("https://app.palmier.me")}`);
42
- console.log(` Pair the app to your host. The app uses ${cyan(`http://${lanIp}:${httpPort}`)}`);
43
- console.log(` for direct RPC when on the same network, otherwise the relay.\n`);
42
+ console.log(` ${dim("Remote (web):")} ${cyan("https://app.palmier.me")}`);
43
+ console.log(` Pair a browser on any device. Traffic always goes through the relay.\n`);
44
+ console.log(` ${dim("Remote (app):")} ${cyan("https://github.com/caihongxu/palmier-android/releases")}`);
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")}`);
@@ -4,7 +4,6 @@ import { StringCodec } from "nats";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { connectNats } from "../nats-client.js";
6
6
  import { addClient } from "../client-store.js";
7
- import { detectLanIp } from "../transports/http-transport.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
@@ -15,11 +14,9 @@ export function generatePairingCode() {
15
14
  }
16
15
  function buildPairResponse(config, label) {
17
16
  const client = addClient(label);
18
- const port = config.httpPort ?? 7256;
19
17
  return {
20
18
  hostId: config.hostId,
21
19
  clientToken: client.token,
22
- directUrl: `http://${detectLanIp()}:${port}`,
23
20
  hostName: os.hostname(),
24
21
  };
25
22
  }
@@ -0,0 +1,4 @@
1
+ export declare function detectDefaultInterface(): Promise<string | null>;
2
+ export declare function getInterfaceIpv4(interfaceName: string): string | null;
3
+ export declare function buildLanUrl(port: number, interfaceName: string | undefined): string | null;
4
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1,100 @@
1
+ import * as os from "node:os";
2
+ import * as dgram from "node:dgram";
3
+ /**
4
+ * Resolve the name of the network interface used for the IPv4 default route.
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
+ */
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
+ }
62
+ export async function detectDefaultInterface() {
63
+ const probedIp = await probeOutboundIp();
64
+ if (probedIp) {
65
+ const name = findInterfaceByIp(probedIp);
66
+ if (name && !isVirtualName(name))
67
+ return name;
68
+ }
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
+ }
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;
83
+ }
84
+ export function getInterfaceIpv4(interfaceName) {
85
+ const addrs = os.networkInterfaces()[interfaceName];
86
+ if (!addrs)
87
+ return null;
88
+ for (const addr of addrs) {
89
+ if (addr.family === "IPv4" && !addr.internal)
90
+ return addr.address;
91
+ }
92
+ return null;
93
+ }
94
+ export function buildLanUrl(port, interfaceName) {
95
+ if (!interfaceName)
96
+ return null;
97
+ const ip = getInterfaceIpv4(interfaceName);
98
+ return ip ? `http://${ip}:${port}` : null;
99
+ }
100
+ //# sourceMappingURL=network.js.map