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.
Files changed (33) hide show
  1. package/README.md +26 -17
  2. package/dist/commands/init.js +6 -11
  3. package/dist/commands/pair.js +3 -0
  4. package/dist/pwa/assets/index-1gs4vwFo.js +120 -0
  5. package/dist/pwa/assets/{index-UaZFu6XL.css → index-DQJHVyP6.css} +1 -1
  6. package/dist/pwa/assets/{web-DYwZE4qa.js → web-BqVsIFtP.js} +1 -1
  7. package/dist/pwa/assets/web-DrSNtZ3i.js +1 -0
  8. package/dist/pwa/assets/{web-nSzKzI8x.js → web-lefgO9YR.js} +1 -1
  9. package/dist/pwa/index.html +2 -2
  10. package/dist/pwa/service-worker.js +1 -1
  11. package/dist/transports/http-transport.js +42 -27
  12. package/dist/types.d.ts +0 -2
  13. package/package.json +1 -1
  14. package/palmier-server/CLAUDE.md +4 -0
  15. package/palmier-server/PRODUCTION.md +1 -1
  16. package/palmier-server/README.md +1 -1
  17. package/palmier-server/pnpm-lock.yaml +12 -0
  18. package/palmier-server/pwa/package.json +1 -0
  19. package/palmier-server/pwa/src/App.css +61 -0
  20. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +1 -1
  21. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +69 -0
  22. package/palmier-server/pwa/src/components/HostMenu.tsx +5 -4
  23. package/palmier-server/pwa/src/constants.ts +1 -1
  24. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +156 -66
  25. package/palmier-server/pwa/src/pages/Dashboard.tsx +2 -0
  26. package/palmier-server/pwa/src/pages/PairHost.tsx +9 -9
  27. package/palmier-server/pwa/src/types.ts +3 -1
  28. package/palmier-server/spec.md +21 -19
  29. package/src/commands/init.ts +7 -12
  30. package/src/commands/pair.ts +3 -0
  31. package/src/transports/http-transport.ts +34 -29
  32. package/src/types.ts +0 -2
  33. package/dist/pwa/assets/index-BiAE5qeC.js +0 -120
@@ -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
- const lanEnabled = config.lanEnabled ?? false;
98
- const bindAddress = lanEnabled ? "0.0.0.0" : "127.0.0.1";
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 LAN mode doesn't use.
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
- sendJson(res, 404, { error: "Not found" });
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 {