palmier 0.8.10 → 0.9.2

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 (49) hide show
  1. package/README.md +8 -1
  2. package/dist/commands/init.js +13 -2
  3. package/dist/commands/pair.js +3 -9
  4. package/dist/linked-device.d.ts +9 -0
  5. package/dist/linked-device.js +45 -0
  6. package/dist/mcp-tools.js +19 -19
  7. package/dist/network.d.ts +0 -5
  8. package/dist/network.js +75 -9
  9. package/dist/pwa/assets/index-BLCVzS_l.js +120 -0
  10. package/dist/pwa/assets/{index-DQJHVyP6.css → index-Cjjw24Ok.css} +1 -1
  11. package/dist/pwa/assets/{web-CaRUL7Kz.js → web-C2AU9S9n.js} +1 -1
  12. package/dist/pwa/assets/{web-C9g_YGd8.js → web-CfD_ah7K.js} +1 -1
  13. package/dist/pwa/assets/{web-D4ty3qtI.js → web-DugGj1t8.js} +1 -1
  14. package/dist/pwa/index.html +2 -2
  15. package/dist/pwa/service-worker.js +2 -2
  16. package/dist/rpc-handler.js +17 -23
  17. package/package.json +1 -2
  18. package/palmier-server/README.md +3 -2
  19. package/palmier-server/pwa/src/App.css +45 -4
  20. package/palmier-server/pwa/src/App.tsx +36 -15
  21. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +65 -225
  22. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +54 -12
  23. package/palmier-server/pwa/src/components/HostMenu.tsx +110 -21
  24. package/palmier-server/pwa/src/components/RunDetailView.tsx +2 -2
  25. package/palmier-server/pwa/src/components/SessionComposer.tsx +9 -8
  26. package/palmier-server/pwa/src/components/SessionsView.tsx +5 -3
  27. package/palmier-server/pwa/src/components/TabBar.tsx +7 -5
  28. package/palmier-server/pwa/src/components/TaskForm.tsx +5 -3
  29. package/palmier-server/pwa/src/constants.ts +1 -1
  30. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +41 -41
  31. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +17 -60
  32. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +6 -7
  33. package/palmier-server/pwa/src/native/Device.ts +23 -38
  34. package/palmier-server/pwa/src/pages/Dashboard.tsx +36 -41
  35. package/palmier-server/pwa/src/pages/PairHost.tsx +20 -1
  36. package/palmier-server/pwa/src/pages/PairSetup.tsx +98 -39
  37. package/palmier-server/pwa/src/service-worker.ts +9 -6
  38. package/palmier-server/pwa/src/types.ts +2 -0
  39. package/palmier-server/spec.md +44 -15
  40. package/src/commands/init.ts +13 -2
  41. package/src/commands/pair.ts +3 -9
  42. package/src/linked-device.ts +52 -0
  43. package/src/mcp-tools.ts +19 -19
  44. package/src/network.ts +73 -9
  45. package/src/rpc-handler.ts +14 -22
  46. package/dist/device-capabilities.d.ts +0 -9
  47. package/dist/device-capabilities.js +0 -36
  48. package/dist/pwa/assets/index-iL_NTbsT.js +0 -120
  49. package/src/device-capabilities.ts +0 -57
package/src/network.ts CHANGED
@@ -1,19 +1,83 @@
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
  /**
6
5
  * Resolve the name of the network interface used for the IPv4 default route.
7
- * Returns null when no default route is found (e.g. fully offline host) or
8
- * when the OS platform isn't supported by `default-gateway`.
6
+ * Falls back to the first non-internal IPv4 interface when the gateway lookup
7
+ * fails `default-gateway` shells out to `wmic` on Windows, which was removed
8
+ * in Windows 11 24H2.
9
9
  */
10
+ function findInterfaceByIp(ip: string): string | null {
11
+ for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
12
+ for (const addr of addrs ?? []) {
13
+ if (addr.family === "IPv4" && addr.address === ip) return name;
14
+ }
15
+ }
16
+ return null;
17
+ }
18
+
19
+ /** Ask the kernel which local IPv4 would route to an external address. No packet is sent. */
20
+ function probeOutboundIp(): Promise<string | null> {
21
+ return new Promise((resolve) => {
22
+ const sock = dgram.createSocket("udp4");
23
+ const cleanup = (ip: string | null) => { try { sock.close(); } catch { /* ignore */ } resolve(ip); };
24
+ sock.on("error", () => cleanup(null));
25
+ try {
26
+ sock.connect(80, "8.8.8.8", () => {
27
+ const addr = sock.address();
28
+ cleanup(addr.address && addr.address !== "0.0.0.0" ? addr.address : null);
29
+ });
30
+ } catch {
31
+ cleanup(null);
32
+ }
33
+ });
34
+ }
35
+
36
+ type IpClass = 0 | 1 | 2 | 3;
37
+
38
+ /** Lower score = more preferred. 0=192.168, 1=10.x, 2=172.16-31, 3=everything else. */
39
+ function ipClass(ip: string): IpClass {
40
+ const parts = ip.split(".").map(Number);
41
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return 3;
42
+ const [a, b] = parts;
43
+ if (a === 192 && b === 168) return 0;
44
+ if (a === 10) return 1;
45
+ if (a === 172 && b >= 16 && b <= 31) return 2;
46
+ return 3;
47
+ }
48
+
49
+ /** Names that commonly belong to virtual/VPN adapters we'd rather skip. */
50
+ const VIRTUAL_NAME_PATTERNS = [
51
+ "vethernet", "virtualbox", "vmware", "hyper-v", "docker", "bridge",
52
+ "tailscale", "wireguard", "meta", "vpn", "tun", "tap", "loopback",
53
+ "wsl", "utun",
54
+ ];
55
+
56
+ function isVirtualName(name: string): boolean {
57
+ const lower = name.toLowerCase();
58
+ return VIRTUAL_NAME_PATTERNS.some((p) => lower.includes(p));
59
+ }
60
+
10
61
  export async function detectDefaultInterface(): Promise<string | null> {
11
- try {
12
- const result = await gateway4async() as { int?: string | null };
13
- return result.int ?? null;
14
- } catch {
15
- return null;
62
+ const probedIp = await probeOutboundIp();
63
+ if (probedIp) {
64
+ const name = findInterfaceByIp(probedIp);
65
+ if (name && !isVirtualName(name)) return name;
66
+ }
67
+
68
+ type Candidate = { name: string; klass: IpClass; virtual: boolean };
69
+ const candidates: Candidate[] = [];
70
+ for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
71
+ for (const addr of addrs ?? []) {
72
+ if (addr.family !== "IPv4" || addr.internal) continue;
73
+ candidates.push({ name, klass: ipClass(addr.address), virtual: isVirtualName(name) });
74
+ }
16
75
  }
76
+ candidates.sort((a, b) => {
77
+ if (a.virtual !== b.virtual) return a.virtual ? 1 : -1;
78
+ return a.klass - b.klass;
79
+ });
80
+ return candidates[0]?.name ?? null;
17
81
  }
18
82
 
19
83
  export function getInterfaceIpv4(interfaceName: string): string | null {
@@ -9,9 +9,9 @@ import { getPlatform } from "./platform/index.js";
9
9
  import { spawnCommand } from "./spawn-command.js";
10
10
  import crossSpawn from "cross-spawn";
11
11
  import { getAgent } from "./agents/agent.js";
12
- import { validateClient } from "./client-store.js";
12
+ import { validateClient, revokeClient } from "./client-store.js";
13
13
  import { publishHostEvent } from "./events.js";
14
- import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice, type DeviceCapability } from "./device-capabilities.js";
14
+ import { getLinkedDevice, setLinkedDevice, clearLinkedDevice, clearLinkedDeviceIfMatches } from "./linked-device.js";
15
15
  import { currentVersion, performUpdate } from "./update-checker.js";
16
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
17
17
  import { clearTaskQueue } from "./event-queues.js";
@@ -144,15 +144,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
144
144
 
145
145
  switch (request.method) {
146
146
  case "host.info": {
147
- const capabilities: Record<string, string | null> = {};
148
- for (const capability of ["location", "notifications", "sms-read", "sms-send", "contacts", "calendar", "alarm", "battery", "dnd", "send-email"] as const) {
149
- capabilities[capability] = getCapabilityDevice(capability)?.clientToken ?? null;
150
- }
151
147
  return {
152
148
  agents: config.agents ?? [],
153
149
  version: currentVersion,
154
150
  host_platform: process.platform,
155
- capability_tokens: capabilities,
151
+ linked_client_token: getLinkedDevice()?.clientToken ?? null,
156
152
  pending_prompts: listPending(),
157
153
  lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
158
154
  };
@@ -635,31 +631,27 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
635
631
  return { ok: true };
636
632
  }
637
633
 
638
- case "device.location.enable": {
634
+ case "device.link": {
639
635
  const params = request.params as { fcmToken: string };
640
636
  if (!params.fcmToken) return { error: "fcmToken is required" };
641
637
  const clientToken = request.clientToken ?? "";
642
- setCapabilityDevice("location", clientToken, params.fcmToken);
638
+ if (!clientToken) return { error: "Unauthorized" };
639
+ setLinkedDevice(clientToken, params.fcmToken);
643
640
  return { ok: true };
644
641
  }
645
642
 
646
- case "device.location.disable": {
647
- clearCapabilityDevice("location");
648
- return { ok: true };
649
- }
650
-
651
- case "device.capability.enable": {
652
- const params = request.params as { capability: DeviceCapability; fcmToken: string };
653
- if (!params.capability || !params.fcmToken) return { error: "capability and fcmToken are required" };
643
+ case "device.unlink": {
654
644
  const clientToken = request.clientToken ?? "";
655
- setCapabilityDevice(params.capability, clientToken, params.fcmToken);
645
+ const current = getLinkedDevice();
646
+ if (current?.clientToken === clientToken) clearLinkedDevice();
656
647
  return { ok: true };
657
648
  }
658
649
 
659
- case "device.capability.disable": {
660
- const params = request.params as { capability: DeviceCapability };
661
- if (!params.capability) return { error: "capability is required" };
662
- clearCapabilityDevice(params.capability);
650
+ case "clients.revoke_self": {
651
+ const clientToken = request.clientToken ?? "";
652
+ if (!clientToken) return { error: "Unauthorized" };
653
+ clearLinkedDeviceIfMatches(clientToken);
654
+ revokeClient(clientToken);
663
655
  return { ok: true };
664
656
  }
665
657
 
@@ -1,9 +0,0 @@
1
- export interface RegisteredDevice {
2
- clientToken: string;
3
- fcmToken: string;
4
- }
5
- export type DeviceCapability = "location" | "notifications" | "sms-read" | "sms-send" | "contacts" | "calendar" | "alarm" | "battery" | "send-email" | "dnd";
6
- export declare function getCapabilityDevice(capability: DeviceCapability): RegisteredDevice | null;
7
- export declare function setCapabilityDevice(capability: DeviceCapability, clientToken: string, fcmToken: string): void;
8
- export declare function clearCapabilityDevice(capability: DeviceCapability): void;
9
- //# sourceMappingURL=device-capabilities.d.ts.map
@@ -1,36 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { CONFIG_DIR } from "./config.js";
4
- const CAPABILITIES_FILE = path.join(CONFIG_DIR, "device-capabilities.json");
5
- function readAll() {
6
- try {
7
- if (!fs.existsSync(CAPABILITIES_FILE))
8
- return {};
9
- return JSON.parse(fs.readFileSync(CAPABILITIES_FILE, "utf-8"));
10
- }
11
- catch {
12
- return {};
13
- }
14
- }
15
- function writeAll(map) {
16
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
17
- fs.writeFileSync(CAPABILITIES_FILE, JSON.stringify(map, null, 2), "utf-8");
18
- }
19
- export function getCapabilityDevice(capability) {
20
- const map = readAll();
21
- const device = map[capability];
22
- if (!device?.clientToken || !device?.fcmToken)
23
- return null;
24
- return device;
25
- }
26
- export function setCapabilityDevice(capability, clientToken, fcmToken) {
27
- const map = readAll();
28
- map[capability] = { clientToken, fcmToken };
29
- writeAll(map);
30
- }
31
- export function clearCapabilityDevice(capability) {
32
- const map = readAll();
33
- delete map[capability];
34
- writeAll(map);
35
- }
36
- //# sourceMappingURL=device-capabilities.js.map