palmier 0.8.9 → 0.8.10

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
@@ -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
 
@@ -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,6 @@ 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";
7
6
  import { listTasks } from "../task.js";
8
7
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
9
8
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -32,15 +31,16 @@ export async function initCommand() {
32
31
  const parsed = parseInt(portAnswer.trim(), 10);
33
32
  if (parsed > 0 && parsed < 65536)
34
33
  httpPort = parsed;
35
- const lanIp = detectLanIp();
36
34
  console.log(`\n${bold("Setup summary:")}\n`);
37
35
  console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
38
36
  console.log(` All tasks and execution data will be stored here.\n`);
39
- console.log(` ${dim("Local access:")} ${cyan(`http://localhost:${httpPort}`)}`);
37
+ console.log(` ${dim("Local:")} ${cyan(`http://localhost:${httpPort}`)}`);
40
38
  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`);
39
+ console.log(` ${dim("Remote (web):")} ${cyan("https://app.palmier.me")}`);
40
+ console.log(` Pair a browser on any device. Traffic always goes through the relay.\n`);
41
+ 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`);
44
44
  console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
45
45
  const existingTasks = listTasks(process.cwd());
46
46
  if (existingTasks.length > 0) {
@@ -1,10 +1,10 @@
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 } from "../config.js";
4
+ import { loadConfig, saveConfig } 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";
7
+ import { detectDefaultInterface } from "../network.js";
8
8
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
9
9
  const CODE_LENGTH = 6;
10
10
  export const PAIRING_EXPIRY_MS = 60 * 1000; // 1 minute
@@ -13,13 +13,16 @@ export function generatePairingCode() {
13
13
  crypto.getRandomValues(bytes);
14
14
  return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
15
15
  }
16
- function buildPairResponse(config, label) {
16
+ async function buildPairResponse(config, label) {
17
17
  const client = addClient(label);
18
- const port = config.httpPort ?? 7256;
18
+ const iface = await detectDefaultInterface();
19
+ if (iface && iface !== config.defaultInterface) {
20
+ config.defaultInterface = iface;
21
+ saveConfig(config);
22
+ }
19
23
  return {
20
24
  hostId: config.hostId,
21
25
  clientToken: client.token,
22
- directUrl: `http://${detectLanIp()}:${port}`,
23
26
  hostName: os.hostname(),
24
27
  };
25
28
  }
@@ -88,7 +91,7 @@ export async function pairCommand() {
88
91
  }
89
92
  }
90
93
  catch { /* empty body is fine */ }
91
- const response = buildPairResponse(config, label);
94
+ const response = await buildPairResponse(config, label);
92
95
  if (msg.reply) {
93
96
  msg.respond(sc.encode(JSON.stringify(response)));
94
97
  }
@@ -0,0 +1,9 @@
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
+ export declare function detectDefaultInterface(): Promise<string | null>;
7
+ export declare function getInterfaceIpv4(interfaceName: string): string | null;
8
+ export declare function buildLanUrl(port: number, interfaceName: string | undefined): string | null;
9
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1,34 @@
1
+ import * as os from "node:os";
2
+ // @ts-expect-error - default-gateway ships no types
3
+ import { gateway4async } from "default-gateway";
4
+ /**
5
+ * 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`.
8
+ */
9
+ export async function detectDefaultInterface() {
10
+ try {
11
+ const result = await gateway4async();
12
+ return result.int ?? null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export function getInterfaceIpv4(interfaceName) {
19
+ const addrs = os.networkInterfaces()[interfaceName];
20
+ if (!addrs)
21
+ return null;
22
+ for (const addr of addrs) {
23
+ if (addr.family === "IPv4" && !addr.internal)
24
+ return addr.address;
25
+ }
26
+ return null;
27
+ }
28
+ export function buildLanUrl(port, interfaceName) {
29
+ if (!interfaceName)
30
+ return null;
31
+ const ip = getInterfaceIpv4(interfaceName);
32
+ return ip ? `http://${ip}:${port}` : null;
33
+ }
34
+ //# sourceMappingURL=network.js.map