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.
- package/README.md +13 -7
- package/dist/commands/info.js +5 -0
- package/dist/commands/init.js +17 -6
- package/dist/commands/pair.js +0 -3
- package/dist/network.d.ts +4 -0
- package/dist/network.js +100 -0
- package/dist/pwa/assets/{index-1gs4vwFo.js → index-B7S0YoMo.js} +45 -45
- package/dist/pwa/assets/{index-DQJHVyP6.css → index-DhphickB.css} +1 -1
- package/dist/pwa/assets/{web-BqVsIFtP.js → web-4WNPL7z3.js} +1 -1
- package/dist/pwa/assets/{web-lefgO9YR.js → web-Bpd2nO1M.js} +1 -1
- package/dist/pwa/assets/{web-DrSNtZ3i.js → web-DjwsAB0V.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +2 -0
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +8 -11
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +8 -4
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +54 -13
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +13 -4
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +6 -2
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -2
- package/palmier-server/pwa/src/pages/PairSetup.tsx +5 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/spec.md +7 -4
- package/src/commands/info.ts +6 -0
- package/src/commands/init.ts +16 -6
- package/src/commands/pair.ts +0 -3
- package/src/network.ts +96 -0
- package/src/rpc-handler.ts +2 -0
- package/src/transports/http-transport.ts +8 -11
- 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
|
-
|
|
113
|
+
Three ways to reach your host, ordered by setup effort:
|
|
114
114
|
|
|
115
|
-
| Mode |
|
|
116
|
-
|
|
117
|
-
| **Local** | `http://localhost:<port>`
|
|
118
|
-
| **
|
|
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
|
|
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
|
|
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 |
|
package/dist/commands/info.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
42
|
-
console.log(` Pair
|
|
43
|
-
console.log(`
|
|
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")}`);
|
package/dist/commands/pair.js
CHANGED
|
@@ -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
|
package/dist/network.js
ADDED
|
@@ -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
|