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.
- package/README.md +24 -26
- package/dist/commands/info.js +0 -22
- package/dist/commands/init.js +27 -123
- package/dist/commands/lan.d.ts +8 -0
- package/dist/commands/lan.js +51 -0
- package/dist/commands/mcpserver.js +2 -7
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +52 -56
- package/dist/commands/run.js +36 -41
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +14 -33
- package/dist/config.js +3 -17
- package/dist/events.d.ts +3 -4
- package/dist/events.js +25 -8
- package/dist/index.js +8 -0
- package/dist/rpc-handler.js +5 -29
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +103 -18
- package/dist/types.d.ts +1 -3
- package/package.json +1 -1
- package/src/commands/info.ts +0 -24
- package/src/commands/init.ts +29 -150
- package/src/commands/lan.ts +58 -0
- package/src/commands/mcpserver.ts +2 -10
- package/src/commands/pair.ts +50 -63
- package/src/commands/run.ts +29 -43
- package/src/commands/serve.ts +14 -31
- package/src/config.ts +3 -18
- package/src/events.ts +23 -10
- package/src/index.ts +9 -0
- package/src/rpc-handler.ts +5 -31
- package/src/transports/http-transport.ts +123 -19
- package/src/types.ts +2 -6
package/src/rpc-handler.ts
CHANGED
|
@@ -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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
}
|