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.
- package/README.md +8 -1
- package/dist/commands/init.js +13 -2
- package/dist/commands/pair.js +3 -9
- package/dist/linked-device.d.ts +9 -0
- package/dist/linked-device.js +45 -0
- package/dist/mcp-tools.js +19 -19
- package/dist/network.d.ts +0 -5
- package/dist/network.js +75 -9
- package/dist/pwa/assets/index-BLCVzS_l.js +120 -0
- package/dist/pwa/assets/{index-DQJHVyP6.css → index-Cjjw24Ok.css} +1 -1
- package/dist/pwa/assets/{web-CaRUL7Kz.js → web-C2AU9S9n.js} +1 -1
- package/dist/pwa/assets/{web-C9g_YGd8.js → web-CfD_ah7K.js} +1 -1
- package/dist/pwa/assets/{web-D4ty3qtI.js → web-DugGj1t8.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +17 -23
- package/package.json +1 -2
- package/palmier-server/README.md +3 -2
- package/palmier-server/pwa/src/App.css +45 -4
- package/palmier-server/pwa/src/App.tsx +36 -15
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +65 -225
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +54 -12
- package/palmier-server/pwa/src/components/HostMenu.tsx +110 -21
- package/palmier-server/pwa/src/components/RunDetailView.tsx +2 -2
- package/palmier-server/pwa/src/components/SessionComposer.tsx +9 -8
- package/palmier-server/pwa/src/components/SessionsView.tsx +5 -3
- package/palmier-server/pwa/src/components/TabBar.tsx +7 -5
- package/palmier-server/pwa/src/components/TaskForm.tsx +5 -3
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +41 -41
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +17 -60
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +6 -7
- package/palmier-server/pwa/src/native/Device.ts +23 -38
- package/palmier-server/pwa/src/pages/Dashboard.tsx +36 -41
- package/palmier-server/pwa/src/pages/PairHost.tsx +20 -1
- package/palmier-server/pwa/src/pages/PairSetup.tsx +98 -39
- package/palmier-server/pwa/src/service-worker.ts +9 -6
- package/palmier-server/pwa/src/types.ts +2 -0
- package/palmier-server/spec.md +44 -15
- package/src/commands/init.ts +13 -2
- package/src/commands/pair.ts +3 -9
- package/src/linked-device.ts +52 -0
- package/src/mcp-tools.ts +19 -19
- package/src/network.ts +73 -9
- package/src/rpc-handler.ts +14 -22
- package/dist/device-capabilities.d.ts +0 -9
- package/dist/device-capabilities.js +0 -36
- package/dist/pwa/assets/index-iL_NTbsT.js +0 -120
- 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
|
-
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 {
|
package/src/rpc-handler.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
638
|
+
if (!clientToken) return { error: "Unauthorized" };
|
|
639
|
+
setLinkedDevice(clientToken, params.fcmToken);
|
|
643
640
|
return { ok: true };
|
|
644
641
|
}
|
|
645
642
|
|
|
646
|
-
case "device.
|
|
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
|
-
|
|
645
|
+
const current = getLinkedDevice();
|
|
646
|
+
if (current?.clientToken === clientToken) clearLinkedDevice();
|
|
656
647
|
return { ok: true };
|
|
657
648
|
}
|
|
658
649
|
|
|
659
|
-
case "
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
662
|
-
|
|
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
|