palmier 0.8.8 → 0.8.9
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 +26 -17
- package/dist/commands/init.js +6 -11
- package/dist/commands/pair.js +3 -0
- package/dist/pwa/assets/index-1gs4vwFo.js +120 -0
- package/dist/pwa/assets/{index-UaZFu6XL.css → index-DQJHVyP6.css} +1 -1
- package/dist/pwa/assets/{web-DYwZE4qa.js → web-BqVsIFtP.js} +1 -1
- package/dist/pwa/assets/web-DrSNtZ3i.js +1 -0
- package/dist/pwa/assets/{web-nSzKzI8x.js → web-lefgO9YR.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/transports/http-transport.js +42 -27
- package/dist/types.d.ts +0 -2
- package/package.json +1 -1
- package/palmier-server/CLAUDE.md +4 -0
- package/palmier-server/PRODUCTION.md +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pnpm-lock.yaml +12 -0
- package/palmier-server/pwa/package.json +1 -0
- package/palmier-server/pwa/src/App.css +61 -0
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +1 -1
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +69 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +5 -4
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +156 -66
- package/palmier-server/pwa/src/pages/Dashboard.tsx +2 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +9 -9
- package/palmier-server/pwa/src/types.ts +3 -1
- package/palmier-server/spec.md +21 -19
- package/src/commands/init.ts +7 -12
- package/src/commands/pair.ts +3 -0
- package/src/transports/http-transport.ts +34 -29
- package/src/types.ts +0 -2
- package/dist/pwa/assets/index-BiAE5qeC.js +0 -120
package/src/commands/pair.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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";
|
|
7
8
|
import type { HostConfig } from "../types.js";
|
|
8
9
|
|
|
9
10
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
@@ -19,9 +20,11 @@ export function generatePairingCode(): string {
|
|
|
19
20
|
|
|
20
21
|
function buildPairResponse(config: HostConfig, label?: string) {
|
|
21
22
|
const client = addClient(label);
|
|
23
|
+
const port = config.httpPort ?? 7256;
|
|
22
24
|
return {
|
|
23
25
|
hostId: config.hostId,
|
|
24
26
|
clientToken: client.token,
|
|
27
|
+
directUrl: `http://${detectLanIp()}:${port}`,
|
|
25
28
|
hostName: os.hostname(),
|
|
26
29
|
};
|
|
27
30
|
}
|
|
@@ -94,8 +94,9 @@ export async function startHttpTransport(
|
|
|
94
94
|
): Promise<void> {
|
|
95
95
|
const sseClients = new Set<SseClient>();
|
|
96
96
|
const mcpStreams = new Map<string, http.ServerResponse>();
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Always bind 0.0.0.0 so other devices on the LAN can reach /rpc and /health.
|
|
98
|
+
// The web UI, /pair, and /events are individually gated to loopback.
|
|
99
|
+
const bindAddress = "0.0.0.0";
|
|
99
100
|
|
|
100
101
|
/** Push notifications/resources/updated to all MCP clients subscribed to the given URI. */
|
|
101
102
|
function broadcastResourceUpdated(uri: string) {
|
|
@@ -177,6 +178,15 @@ export async function startHttpTransport(
|
|
|
177
178
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
178
179
|
const pathname = url.pathname;
|
|
179
180
|
|
|
181
|
+
if (req.method === "GET" && pathname === "/health") {
|
|
182
|
+
res.writeHead(200, {
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"Access-Control-Allow-Origin": "*",
|
|
185
|
+
});
|
|
186
|
+
res.end(JSON.stringify({ ok: true, hostId: config.hostId }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
180
190
|
if (req.method === "POST" && pathname === "/mcp") {
|
|
181
191
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
182
192
|
try {
|
|
@@ -351,6 +361,7 @@ export async function startHttpTransport(
|
|
|
351
361
|
}
|
|
352
362
|
|
|
353
363
|
if (req.method === "POST" && pathname === "/pair") {
|
|
364
|
+
if (!isLocalhost(req)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
354
365
|
try {
|
|
355
366
|
const body = await readBody(req);
|
|
356
367
|
const { code, label } = JSON.parse(body) as { code: string; label?: string };
|
|
@@ -377,35 +388,11 @@ export async function startHttpTransport(
|
|
|
377
388
|
return;
|
|
378
389
|
}
|
|
379
390
|
|
|
380
|
-
// Service worker and manifest require HTTPS, which
|
|
391
|
+
// Service worker and manifest require HTTPS, which loopback HTTP doesn't use.
|
|
381
392
|
const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
|
|
382
393
|
|
|
383
|
-
const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
|
|
384
|
-
if (!isApiRoute) {
|
|
385
|
-
if (SKIP.has(pathname)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
386
|
-
|
|
387
|
-
// Fall back to index.html for SPA routing.
|
|
388
|
-
let asset = getAsset(pathname);
|
|
389
|
-
if (!asset && pathname !== "/") {
|
|
390
|
-
asset = getAsset("/");
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (asset) {
|
|
394
|
-
res.writeHead(200, { "Content-Type": asset.contentType });
|
|
395
|
-
res.end(asset.data);
|
|
396
|
-
} else {
|
|
397
|
-
sendJson(res, 502, { error: "Failed to fetch PWA assets" });
|
|
398
|
-
}
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Localhost is trusted; all other API callers require a client token.
|
|
403
|
-
if (!isLocalhost(req) && !checkAuth(req)) {
|
|
404
|
-
sendJson(res, 401, { error: "Unauthorized" });
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
394
|
if (req.method === "GET" && pathname === "/events") {
|
|
395
|
+
if (!isLocalhost(req)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
409
396
|
res.writeHead(200, {
|
|
410
397
|
"Content-Type": "text/event-stream",
|
|
411
398
|
"Cache-Control": "no-cache",
|
|
@@ -426,6 +413,10 @@ export async function startHttpTransport(
|
|
|
426
413
|
}
|
|
427
414
|
|
|
428
415
|
if (req.method === "POST" && pathname.startsWith("/rpc/")) {
|
|
416
|
+
if (!isLocalhost(req) && !checkAuth(req)) {
|
|
417
|
+
sendJson(res, 401, { error: "Unauthorized" });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
429
420
|
const method = pathname.slice("/rpc/".length);
|
|
430
421
|
if (!method) { sendJson(res, 400, { error: "Missing RPC method" }); return; }
|
|
431
422
|
|
|
@@ -451,7 +442,21 @@ export async function startHttpTransport(
|
|
|
451
442
|
return;
|
|
452
443
|
}
|
|
453
444
|
|
|
454
|
-
|
|
445
|
+
// PWA static assets — loopback only. Other devices must load the PWA from app.palmier.me.
|
|
446
|
+
if (!isLocalhost(req)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
447
|
+
if (SKIP.has(pathname)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
448
|
+
|
|
449
|
+
let asset = getAsset(pathname);
|
|
450
|
+
if (!asset && pathname !== "/") {
|
|
451
|
+
asset = getAsset("/");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (asset) {
|
|
455
|
+
res.writeHead(200, { "Content-Type": asset.contentType });
|
|
456
|
+
res.end(asset.data);
|
|
457
|
+
} else {
|
|
458
|
+
sendJson(res, 404, { error: "Not found" });
|
|
459
|
+
}
|
|
455
460
|
});
|
|
456
461
|
|
|
457
462
|
return new Promise<void>((resolve, reject) => {
|
package/src/types.ts
CHANGED
|
@@ -10,8 +10,6 @@ export interface HostConfig {
|
|
|
10
10
|
agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
|
|
11
11
|
|
|
12
12
|
httpPort?: number;
|
|
13
|
-
/** Whether to accept non-localhost HTTP connections. */
|
|
14
|
-
lanEnabled?: boolean;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
export interface TaskFrontmatter {
|