palmier 0.2.6 → 0.2.7

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.
@@ -1,5 +1,4 @@
1
1
  import { randomUUID } from "crypto";
2
- import * as os from "os";
3
2
  import * as fs from "fs";
4
3
  import * as path from "path";
5
4
  import { fileURLToPath } from "url";
@@ -20,18 +19,6 @@ const PLAN_GENERATION_PROMPT = fs.readFileSync(
20
19
  "utf-8",
21
20
  );
22
21
 
23
- function detectLanIp(): string {
24
- const interfaces = os.networkInterfaces();
25
- for (const name of Object.keys(interfaces)) {
26
- for (const iface of interfaces[name] ?? []) {
27
- if (iface.family === "IPv4" && !iface.internal) {
28
- return iface.address;
29
- }
30
- }
31
- }
32
- return "127.0.0.1";
33
- }
34
-
35
22
  /**
36
23
  * Parse RESULT frontmatter into a metadata object.
37
24
  */
@@ -238,10 +225,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
238
225
 
239
226
  writeTaskFile(taskDir, existing);
240
227
 
241
- // Reinstall service and timers
242
- const platform = getPlatform();
243
- platform.removeTaskTimer(params.id);
244
- platform.installTaskTimer(config, existing);
228
+ // Update timers installTaskTimer overwrites in-place (schtasks /f,
229
+ // systemd unit rewrite) without killing a running task process.
230
+ getPlatform().installTaskTimer(config, existing);
245
231
 
246
232
  return flattenTask(existing);
247
233
  }
@@ -283,10 +269,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
283
269
  console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
284
270
  return { error: `Failed to abort task: ${e.stderr || e.message}` };
285
271
  }
286
- // Notify connected clients (NATS + HTTP SSE)
272
+ // Notify connected clients (NATS + HTTP SSE if LAN server is running)
287
273
  const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
288
- const useHttp = (config.mode ?? "nats") === "lan" || (config.mode ?? "nats") === "auto";
289
- await publishHostEvent(nc, config, params.id, abortPayload, useHttp);
274
+ await publishHostEvent(nc, config.hostId, params.id, abortPayload);
290
275
  return { ok: true, task_id: params.id };
291
276
  }
292
277
 
@@ -394,17 +379,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
394
379
  return { ok: true, task_id: params.task_id, result_file: params.result_file };
395
380
  }
396
381
 
397
- case "host.directInfo": {
398
- if (config.mode === "lan" || config.mode === "auto") {
399
- const ip = detectLanIp();
400
- return {
401
- directUrl: `http://${ip}:${config.directPort ?? 7400}`,
402
- directToken: config.directToken,
403
- };
404
- }
405
- return { directUrl: null, directToken: null };
406
- }
407
-
408
382
  default:
409
383
  return { error: `Unknown method: ${request.method}` };
410
384
  }
@@ -3,6 +3,95 @@ import * as os from "os";
3
3
  import { validateSession, addSession } from "../session-store.js";
4
4
  import type { HostConfig, RpcMessage } from "../types.js";
5
5
 
6
+ const PWA_ORIGIN = "https://app.palmier.me";
7
+
8
+ // ── In-memory PWA asset cache ──────────────────────────────────────────
9
+
10
+ interface CachedAsset {
11
+ data: Buffer;
12
+ contentType: string;
13
+ }
14
+
15
+ const CONTENT_TYPES: Record<string, string> = {
16
+ ".html": "text/html; charset=utf-8",
17
+ ".js": "application/javascript",
18
+ ".css": "text/css",
19
+ ".json": "application/json",
20
+
21
+ ".png": "image/png",
22
+ ".ico": "image/x-icon",
23
+ ".woff2": "font/woff2",
24
+ ".woff": "font/woff",
25
+ ".svg": "image/svg+xml",
26
+ };
27
+
28
+ function guessContentType(urlPath: string): string {
29
+ const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
30
+ return CONTENT_TYPES[ext] ?? "application/octet-stream";
31
+ }
32
+
33
+ async function fetchBuffer(url: string): Promise<Buffer> {
34
+ const res = await fetch(url);
35
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`);
36
+ return Buffer.from(await res.arrayBuffer());
37
+ }
38
+
39
+ /**
40
+ * Download the PWA from palmier.me into memory.
41
+ * Parses index.html for asset references, then fetches each one.
42
+ */
43
+ async function downloadPwaAssets(): Promise<Map<string, CachedAsset>> {
44
+ const assets = new Map<string, CachedAsset>();
45
+
46
+ // 1. Fetch index.html
47
+ const html = await fetchBuffer(`${PWA_ORIGIN}/`);
48
+ assets.set("/", { data: html, contentType: "text/html; charset=utf-8" });
49
+
50
+ const htmlStr = html.toString("utf-8");
51
+
52
+ // 2. Extract references from HTML (src="..." and href="...")
53
+ // Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
54
+ const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
55
+ const refRegex = /(?:src|href)="([^"]+)"/g;
56
+ const htmlRefs = new Set<string>();
57
+ let match;
58
+ while ((match = refRegex.exec(htmlStr)) !== null) {
59
+ const ref = match[1];
60
+ if (ref.startsWith("/") && !ref.startsWith("//") && !SKIP.has(ref)) {
61
+ htmlRefs.add(ref);
62
+ }
63
+ }
64
+
65
+ // 3. Fetch all HTML-referenced assets
66
+ for (const ref of htmlRefs) {
67
+ try {
68
+ const data = await fetchBuffer(`${PWA_ORIGIN}${ref}`);
69
+ assets.set(ref, { data, contentType: guessContentType(ref) });
70
+
71
+ // 4. Parse CSS for font url() references
72
+ if (ref.endsWith(".css")) {
73
+ const cssStr = data.toString("utf-8");
74
+ const urlRegex = /url\(["']?([^"')]+)["']?\)/g;
75
+ let cssMatch;
76
+ while ((cssMatch = urlRegex.exec(cssStr)) !== null) {
77
+ let fontRef = cssMatch[1];
78
+ if (fontRef.startsWith("data:")) continue;
79
+ // Resolve relative URLs against the CSS file's directory
80
+ if (!fontRef.startsWith("/")) {
81
+ const cssDir = ref.substring(0, ref.lastIndexOf("/") + 1);
82
+ fontRef = cssDir + fontRef;
83
+ }
84
+ htmlRefs.add(fontRef);
85
+ }
86
+ }
87
+ } catch (err) {
88
+ console.warn(`[pwa] Failed to fetch ${ref}: ${err}`);
89
+ }
90
+ }
91
+
92
+ return assets;
93
+ }
94
+
6
95
  type SseClient = http.ServerResponse;
7
96
 
8
97
  interface PendingPair {
@@ -30,10 +119,27 @@ export function detectLanIp(): string {
30
119
  export async function startHttpTransport(
31
120
  config: HostConfig,
32
121
  handleRpc: (req: RpcMessage) => Promise<unknown>,
122
+ port: number,
123
+ pairingCode?: string,
124
+ onReady?: () => void,
33
125
  ): Promise<void> {
34
- const port = config.directPort ?? 7400;
126
+ // Download PWA assets into memory before starting the server
127
+ console.log("[http] Downloading PWA assets...");
128
+ const pwaAssets = await downloadPwaAssets();
129
+ console.log(`[http] Cached ${pwaAssets.size} PWA assets in memory.`);
130
+
35
131
  const sseClients = new Set<SseClient>();
36
132
 
133
+ // If a pairing code is provided (from `palmier lan`), pre-register it
134
+ if (pairingCode) {
135
+ const EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours — stays valid while lan server runs
136
+ const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
137
+ pendingPairs.set(pairingCode, {
138
+ resolve: () => {},
139
+ timer,
140
+ });
141
+ }
142
+
37
143
  function broadcastSseEvent(data: unknown) {
38
144
  const payload = `data: ${JSON.stringify(data)}\n\n`;
39
145
  for (const client of sseClients) {
@@ -41,11 +147,6 @@ export async function startHttpTransport(
41
147
  }
42
148
  }
43
149
 
44
- function setCorsHeaders(res: http.ServerResponse) {
45
- res.setHeader("Access-Control-Allow-Origin", "*");
46
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
47
- res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
48
- }
49
150
 
50
151
  function checkAuth(req: http.IncomingMessage): boolean {
51
152
  const auth = req.headers.authorization;
@@ -80,15 +181,6 @@ export async function startHttpTransport(
80
181
  }
81
182
 
82
183
  const server = http.createServer(async (req, res) => {
83
- setCorsHeaders(res);
84
-
85
- // Handle CORS preflight
86
- if (req.method === "OPTIONS") {
87
- res.writeHead(204);
88
- res.end();
89
- return;
90
- }
91
-
92
184
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
93
185
  const pathname = url.pathname;
94
186
 
@@ -184,7 +276,6 @@ export async function startHttpTransport(
184
276
  hostId: config.hostId,
185
277
  sessionToken: session.token,
186
278
  directUrl: `http://${ip}:${port}`,
187
- directToken: config.directToken,
188
279
  };
189
280
 
190
281
  // Resolve the long-poll and clean up
@@ -199,7 +290,21 @@ export async function startHttpTransport(
199
290
  return;
200
291
  }
201
292
 
202
- // All other endpoints require auth
293
+ // Serve cached PWA assets for non-API routes (no auth required)
294
+ const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
295
+ if (!isApiRoute) {
296
+ // SPA fallback: serve index.html for unrecognized paths
297
+ const asset = pwaAssets.get(pathname) ?? (pathname !== "/" ? pwaAssets.get("/") : undefined);
298
+ if (asset) {
299
+ res.writeHead(200, { "Content-Type": asset.contentType });
300
+ res.end(asset.data);
301
+ } else {
302
+ sendJson(res, 404, { error: "Not found" });
303
+ }
304
+ return;
305
+ }
306
+
307
+ // API endpoints require auth
203
308
  if (!checkAuth(req)) {
204
309
  sendJson(res, 401, { error: "Unauthorized" });
205
310
  return;
@@ -211,7 +316,6 @@ export async function startHttpTransport(
211
316
  "Content-Type": "text/event-stream",
212
317
  "Cache-Control": "no-cache",
213
318
  Connection: "keep-alive",
214
- "Access-Control-Allow-Origin": "*",
215
319
  });
216
320
  res.write(":ok\n\n");
217
321
 
@@ -267,7 +371,7 @@ export async function startHttpTransport(
267
371
  return new Promise<void>((resolve, reject) => {
268
372
  server.listen(port, () => {
269
373
  console.log(`[http] Listening on port ${port}`);
270
- console.log(`[http] SSE clients can connect to /events`);
374
+ onReady?.();
271
375
 
272
376
  // Graceful shutdown
273
377
  const shutdown = () => {
package/src/types.ts CHANGED
@@ -1,17 +1,13 @@
1
1
  export interface HostConfig {
2
2
  hostId: string;
3
3
  projectRoot: string;
4
- mode: "nats" | "lan" | "auto";
5
4
 
6
- // NATS fields (required when mode === "nats" or "auto")
5
+ // NATS (always enabled)
6
+ nats?: boolean;
7
7
  natsUrl?: string;
8
8
  natsWsUrl?: string;
9
9
  natsToken?: string;
10
10
 
11
- // Direct/LAN fields (required when mode === "lan" or "auto")
12
- directPort?: number;
13
- directToken?: string;
14
-
15
11
  // Detected agent CLIs
16
12
  agents?: Array<{ key: string; label: string }>;
17
13
  }