palmier 0.4.5 → 0.4.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.
Files changed (62) hide show
  1. package/README.md +29 -31
  2. package/dist/agents/agent-instructions.md +9 -9
  3. package/dist/agents/claude.js +3 -3
  4. package/dist/agents/codex.js +3 -3
  5. package/dist/agents/copilot.js +3 -6
  6. package/dist/agents/gemini.js +4 -5
  7. package/dist/agents/openclaw.js +2 -2
  8. package/dist/agents/shared-prompt.d.ts +2 -4
  9. package/dist/agents/shared-prompt.js +9 -4
  10. package/dist/commands/init.js +31 -2
  11. package/dist/commands/pair.d.ts +1 -1
  12. package/dist/commands/pair.js +12 -15
  13. package/dist/commands/plan-generation.md +12 -15
  14. package/dist/commands/run.js +23 -44
  15. package/dist/commands/serve.d.ts +1 -1
  16. package/dist/commands/serve.js +9 -2
  17. package/dist/events.d.ts +2 -2
  18. package/dist/events.js +15 -16
  19. package/dist/index.js +0 -25
  20. package/dist/pending-requests.d.ts +27 -0
  21. package/dist/pending-requests.js +39 -0
  22. package/dist/rpc-handler.js +18 -10
  23. package/dist/task.d.ts +1 -1
  24. package/dist/task.js +3 -2
  25. package/dist/transports/http-transport.d.ts +4 -2
  26. package/dist/transports/http-transport.js +218 -77
  27. package/dist/types.d.ts +7 -16
  28. package/package.json +1 -1
  29. package/src/agents/agent-instructions.md +9 -9
  30. package/src/agents/claude.ts +3 -3
  31. package/src/agents/codex.ts +3 -3
  32. package/src/agents/copilot.ts +3 -6
  33. package/src/agents/gemini.ts +5 -5
  34. package/src/agents/openclaw.ts +2 -2
  35. package/src/agents/shared-prompt.ts +12 -6
  36. package/src/commands/init.ts +34 -3
  37. package/src/commands/pair.ts +11 -14
  38. package/src/commands/plan-generation.md +12 -15
  39. package/src/commands/run.ts +21 -58
  40. package/src/commands/serve.ts +11 -2
  41. package/src/events.ts +14 -15
  42. package/src/index.ts +0 -26
  43. package/src/pending-requests.ts +55 -0
  44. package/src/rpc-handler.ts +18 -11
  45. package/src/task.ts +3 -1
  46. package/src/transports/http-transport.ts +232 -133
  47. package/src/types.ts +10 -16
  48. package/dist/commands/lan.d.ts +0 -8
  49. package/dist/commands/lan.js +0 -44
  50. package/dist/commands/notify.d.ts +0 -9
  51. package/dist/commands/notify.js +0 -43
  52. package/dist/commands/request-input.d.ts +0 -10
  53. package/dist/commands/request-input.js +0 -49
  54. package/dist/lan-lock.d.ts +0 -7
  55. package/dist/lan-lock.js +0 -18
  56. package/dist/user-input.d.ts +0 -15
  57. package/dist/user-input.js +0 -50
  58. package/src/commands/lan.ts +0 -48
  59. package/src/commands/notify.ts +0 -44
  60. package/src/commands/request-input.ts +0 -51
  61. package/src/lan-lock.ts +0 -16
  62. package/src/user-input.ts +0 -67
@@ -1,7 +1,13 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "os";
3
+ import { StringCodec } from "nats";
3
4
  import { validateSession, addSession } from "../session-store.js";
5
+ import { registerPending } from "../pending-requests.js";
6
+ import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
4
7
  const PWA_ORIGIN = "https://app.palmier.me";
8
+ const assetCache = new Map();
9
+ /** Paths currently being fetched (dedup concurrent requests). */
10
+ const assetInflight = new Map();
5
11
  const CONTENT_TYPES = {
6
12
  ".html": "text/html; charset=utf-8",
7
13
  ".js": "application/javascript",
@@ -14,6 +20,8 @@ const CONTENT_TYPES = {
14
20
  ".svg": "image/svg+xml",
15
21
  };
16
22
  function guessContentType(urlPath) {
23
+ if (urlPath === "/")
24
+ return "text/html; charset=utf-8";
17
25
  const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
18
26
  return CONTENT_TYPES[ext] ?? "application/octet-stream";
19
27
  }
@@ -24,55 +32,39 @@ async function fetchBuffer(url) {
24
32
  return Buffer.from(await res.arrayBuffer());
25
33
  }
26
34
  /**
27
- * Download the PWA from palmier.me into memory.
28
- * Parses index.html for asset references, then fetches each one.
35
+ * Fetch a PWA asset on-the-fly, caching in memory.
36
+ * Returns null if the asset cannot be fetched.
29
37
  */
30
- async function downloadPwaAssets() {
31
- const assets = new Map();
32
- // 1. Fetch index.html
33
- const html = await fetchBuffer(`${PWA_ORIGIN}/`);
34
- assets.set("/", { data: html, contentType: "text/html; charset=utf-8" });
35
- const htmlStr = html.toString("utf-8");
36
- // 2. Extract references from HTML (src="..." and href="...")
37
- // Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
38
- const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
39
- const refRegex = /(?:src|href)="([^"]+)"/g;
40
- const htmlRefs = new Set();
41
- let match;
42
- while ((match = refRegex.exec(htmlStr)) !== null) {
43
- const ref = match[1];
44
- if (ref.startsWith("/") && !ref.startsWith("//") && !SKIP.has(ref)) {
45
- htmlRefs.add(ref);
46
- }
47
- }
48
- // 3. Fetch all HTML-referenced assets
49
- for (const ref of htmlRefs) {
38
+ async function getAsset(urlPath) {
39
+ const cached = assetCache.get(urlPath);
40
+ if (cached)
41
+ return cached;
42
+ // Dedup concurrent requests for the same path
43
+ const inflight = assetInflight.get(urlPath);
44
+ if (inflight)
45
+ return inflight;
46
+ const promise = (async () => {
50
47
  try {
51
- const data = await fetchBuffer(`${PWA_ORIGIN}${ref}`);
52
- assets.set(ref, { data, contentType: guessContentType(ref) });
53
- // 4. Parse CSS for font url() references
54
- if (ref.endsWith(".css")) {
55
- const cssStr = data.toString("utf-8");
56
- const urlRegex = /url\(["']?([^"')]+)["']?\)/g;
57
- let cssMatch;
58
- while ((cssMatch = urlRegex.exec(cssStr)) !== null) {
59
- let fontRef = cssMatch[1];
60
- if (fontRef.startsWith("data:"))
61
- continue;
62
- // Resolve relative URLs against the CSS file's directory
63
- if (!fontRef.startsWith("/")) {
64
- const cssDir = ref.substring(0, ref.lastIndexOf("/") + 1);
65
- fontRef = cssDir + fontRef;
66
- }
67
- htmlRefs.add(fontRef);
68
- }
48
+ let data = await fetchBuffer(`${PWA_ORIGIN}${urlPath}`);
49
+ // Inject LAN mode marker into index HTML so the PWA can detect it's served by palmier
50
+ if (urlPath === "/") {
51
+ const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
52
+ data = Buffer.from(html, "utf-8");
69
53
  }
54
+ const asset = { data, contentType: guessContentType(urlPath) };
55
+ assetCache.set(urlPath, asset);
56
+ return asset;
70
57
  }
71
58
  catch (err) {
72
- console.warn(`[pwa] Failed to fetch ${ref}: ${err}`);
59
+ console.warn(`[pwa] Failed to fetch ${urlPath}: ${err}`);
60
+ return null;
73
61
  }
74
- }
75
- return assets;
62
+ finally {
63
+ assetInflight.delete(urlPath);
64
+ }
65
+ })();
66
+ assetInflight.set(urlPath, promise);
67
+ return promise;
76
68
  }
77
69
  const pendingPairs = new Map();
78
70
  export function detectLanIp() {
@@ -87,22 +79,18 @@ export function detectLanIp() {
87
79
  return "127.0.0.1";
88
80
  }
89
81
  /**
90
- * Start the HTTP transport: Express-like server with RPC, SSE, and health endpoints.
82
+ * Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
83
+ * localhost-only agent endpoints (notify, request-input, confirmation, permission).
91
84
  */
92
- export async function startHttpTransport(config, handleRpc, port, pairingCode, onReady) {
93
- // Download PWA assets into memory before starting the server
94
- console.log("[http] Downloading PWA assets...");
95
- const pwaAssets = await downloadPwaAssets();
96
- console.log(`[http] Cached ${pwaAssets.size} PWA assets in memory.`);
85
+ export async function startHttpTransport(config, handleRpc, port, nc, pairingCode, onReady) {
97
86
  const sseClients = new Set();
98
- // If a pairing code is provided (from `palmier lan`), pre-register it
87
+ const lanEnabled = config.lanEnabled ?? false;
88
+ const bindAddress = lanEnabled ? "0.0.0.0" : "127.0.0.1";
89
+ // If a pairing code is provided, pre-register it
99
90
  if (pairingCode) {
100
- const EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours — stays valid while lan server runs
91
+ const EXPIRY_MS = 24 * 60 * 60 * 1000;
101
92
  const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
102
- pendingPairs.set(pairingCode, {
103
- resolve: () => { },
104
- timer,
105
- });
93
+ pendingPairs.set(pairingCode, { resolve: () => { }, timer });
106
94
  }
107
95
  function broadcastSseEvent(data) {
108
96
  const payload = `data: ${JSON.stringify(data)}\n\n`;
@@ -114,8 +102,7 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
114
102
  const auth = req.headers.authorization;
115
103
  if (!auth || !auth.startsWith("Bearer "))
116
104
  return false;
117
- const token = auth.slice(7);
118
- return validateSession(token);
105
+ return validateSession(auth.slice(7));
119
106
  }
120
107
  function extractSessionToken(req) {
121
108
  const auth = req.headers.authorization;
@@ -139,11 +126,22 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
139
126
  const addr = req.socket.remoteAddress;
140
127
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
141
128
  }
129
+ /**
130
+ * Publish an event via NATS and SSE.
131
+ */
132
+ async function publishEvent(taskId, payload) {
133
+ const sc = StringCodec();
134
+ const subject = `host-event.${config.hostId}.${taskId}`;
135
+ if (nc) {
136
+ nc.publish(subject, sc.encode(JSON.stringify(payload)));
137
+ }
138
+ broadcastSseEvent({ task_id: taskId, ...payload });
139
+ }
142
140
  const server = http.createServer(async (req, res) => {
143
141
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
144
142
  const pathname = url.pathname;
145
- // Internal event endpoint — localhost only, no auth
146
- if (req.method === "POST" && pathname === "/internal/event") {
143
+ // ── Localhost-only endpoints (no auth) ─────────────────────────────
144
+ if (req.method === "POST" && pathname === "/event") {
147
145
  if (!isLocalhost(req)) {
148
146
  sendJson(res, 403, { error: "localhost only" });
149
147
  return;
@@ -159,9 +157,7 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
159
157
  }
160
158
  return;
161
159
  }
162
- // Internal pair-register endpoint localhost only, long-poll
163
- // The pair CLI posts here and blocks until paired or expired.
164
- if (req.method === "POST" && pathname === "/internal/pair-register") {
160
+ if (req.method === "POST" && pathname === "/pair-register") {
165
161
  if (!isLocalhost(req)) {
166
162
  sendJson(res, 403, { error: "localhost only" });
167
163
  return;
@@ -183,7 +179,6 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
183
179
  resolve({ paired: false });
184
180
  }, expiryMs ?? 5 * 60 * 1000);
185
181
  pendingPairs.set(code, { resolve, timer });
186
- // Clean up if the CLI disconnects early
187
182
  req.on("close", () => {
188
183
  if (pendingPairs.has(code)) {
189
184
  clearTimeout(timer);
@@ -198,7 +193,148 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
198
193
  }
199
194
  return;
200
195
  }
201
- // Public pair endpointno auth required, PWA posts OTP code here
196
+ // ── POST /notifysend push notification via NATS ─────────────────
197
+ if (req.method === "POST" && pathname === "/notify") {
198
+ if (!isLocalhost(req)) {
199
+ sendJson(res, 403, { error: "localhost only" });
200
+ return;
201
+ }
202
+ if (!nc) {
203
+ sendJson(res, 503, { error: "NATS not connected — push notifications require server mode" });
204
+ return;
205
+ }
206
+ try {
207
+ const body = await readBody(req);
208
+ const { title, body: notifBody } = JSON.parse(body);
209
+ if (!title || !notifBody) {
210
+ sendJson(res, 400, { error: "title and body are required" });
211
+ return;
212
+ }
213
+ const sc = StringCodec();
214
+ const payload = { hostId: config.hostId, title, body: notifBody };
215
+ const subject = `host.${config.hostId}.push.send`;
216
+ const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 15_000 });
217
+ const result = JSON.parse(sc.decode(reply.data));
218
+ if (result.ok) {
219
+ sendJson(res, 200, { ok: true });
220
+ }
221
+ else {
222
+ sendJson(res, 502, { error: result.error ?? "Push notification failed" });
223
+ }
224
+ }
225
+ catch (err) {
226
+ sendJson(res, 500, { error: `Failed to send notification: ${err}` });
227
+ }
228
+ return;
229
+ }
230
+ // ── POST /request-input — held connection until user responds ────────
231
+ if (req.method === "POST" && pathname === "/request-input") {
232
+ if (!isLocalhost(req)) {
233
+ sendJson(res, 403, { error: "localhost only" });
234
+ return;
235
+ }
236
+ try {
237
+ const body = await readBody(req);
238
+ const { taskId, runId, descriptions } = JSON.parse(body);
239
+ if (!taskId || !descriptions?.length) {
240
+ sendJson(res, 400, { error: "taskId and descriptions are required" });
241
+ return;
242
+ }
243
+ const taskDir = getTaskDir(config.projectRoot, taskId);
244
+ const task = parseTaskFile(taskDir);
245
+ await publishEvent(taskId, {
246
+ event_type: "input-request",
247
+ host_id: config.hostId,
248
+ input_descriptions: descriptions,
249
+ name: task.frontmatter.name,
250
+ });
251
+ const response = await registerPending(taskId, "input", descriptions);
252
+ if (response.length === 1 && response[0] === "aborted") {
253
+ await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
254
+ if (runId) {
255
+ appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
256
+ }
257
+ sendJson(res, 200, { aborted: true });
258
+ }
259
+ else {
260
+ await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
261
+ if (runId) {
262
+ const lines = descriptions.map((desc, i) => `**${desc}** ${response[i]}`);
263
+ appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
264
+ }
265
+ sendJson(res, 200, { values: response });
266
+ }
267
+ }
268
+ catch (err) {
269
+ sendJson(res, 500, { error: String(err) });
270
+ }
271
+ return;
272
+ }
273
+ // ── POST /request-confirmation — held connection ────────────────────
274
+ if (req.method === "POST" && pathname === "/request-confirmation") {
275
+ if (!isLocalhost(req)) {
276
+ sendJson(res, 403, { error: "localhost only" });
277
+ return;
278
+ }
279
+ try {
280
+ const body = await readBody(req);
281
+ const { taskId } = JSON.parse(body);
282
+ if (!taskId) {
283
+ sendJson(res, 400, { error: "taskId is required" });
284
+ return;
285
+ }
286
+ await publishEvent(taskId, {
287
+ event_type: "confirm-request",
288
+ host_id: config.hostId,
289
+ });
290
+ const response = await registerPending(taskId, "confirmation");
291
+ const confirmed = response[0] === "confirmed";
292
+ await publishEvent(taskId, {
293
+ event_type: "confirm-resolved",
294
+ host_id: config.hostId,
295
+ status: confirmed ? "confirmed" : "aborted",
296
+ });
297
+ sendJson(res, 200, { confirmed });
298
+ }
299
+ catch (err) {
300
+ sendJson(res, 500, { error: String(err) });
301
+ }
302
+ return;
303
+ }
304
+ // ── POST /request-permission — held connection ──────────────────────
305
+ if (req.method === "POST" && pathname === "/request-permission") {
306
+ if (!isLocalhost(req)) {
307
+ sendJson(res, 403, { error: "localhost only" });
308
+ return;
309
+ }
310
+ try {
311
+ const body = await readBody(req);
312
+ const { taskId, taskName, permissions } = JSON.parse(body);
313
+ if (!taskId || !permissions?.length) {
314
+ sendJson(res, 400, { error: "taskId and permissions are required" });
315
+ return;
316
+ }
317
+ await publishEvent(taskId, {
318
+ event_type: "permission-request",
319
+ host_id: config.hostId,
320
+ required_permissions: permissions,
321
+ name: taskName,
322
+ });
323
+ const response = await registerPending(taskId, "permission", permissions);
324
+ const status = response[0];
325
+ await publishEvent(taskId, {
326
+ event_type: "permission-resolved",
327
+ host_id: config.hostId,
328
+ status,
329
+ });
330
+ sendJson(res, 200, { response: status });
331
+ }
332
+ catch (err) {
333
+ sendJson(res, 500, { error: String(err) });
334
+ }
335
+ return;
336
+ }
337
+ // ── Public pair endpoint — no auth, PWA posts OTP code here ────────
202
338
  if (req.method === "POST" && pathname === "/pair") {
203
339
  try {
204
340
  const body = await readBody(req);
@@ -212,7 +348,6 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
212
348
  sendJson(res, 401, { error: "Invalid code" });
213
349
  return;
214
350
  }
215
- // Create session and build response
216
351
  const session = addSession(label);
217
352
  const ip = detectLanIp();
218
353
  const response = {
@@ -220,7 +355,6 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
220
355
  sessionToken: session.token,
221
356
  directUrl: `http://${ip}:${port}`,
222
357
  };
223
- // Resolve the long-poll and clean up
224
358
  clearTimeout(pending.timer);
225
359
  pendingPairs.delete(code);
226
360
  pending.resolve({ paired: true });
@@ -231,22 +365,31 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
231
365
  }
232
366
  return;
233
367
  }
234
- // Serve cached PWA assets for non-API routes (no auth required)
368
+ // ── PWA assets (on-the-fly, cached) ────────────────────────────────
369
+ // Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
370
+ const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
235
371
  const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
236
372
  if (!isApiRoute) {
237
- // SPA fallback: serve index.html for unrecognized paths
238
- const asset = pwaAssets.get(pathname) ?? (pathname !== "/" ? pwaAssets.get("/") : undefined);
373
+ if (SKIP.has(pathname)) {
374
+ sendJson(res, 404, { error: "Not found" });
375
+ return;
376
+ }
377
+ // Try exact path, then fall back to index.html (SPA routing)
378
+ let asset = await getAsset(pathname);
379
+ if (!asset && pathname !== "/") {
380
+ asset = await getAsset("/");
381
+ }
239
382
  if (asset) {
240
383
  res.writeHead(200, { "Content-Type": asset.contentType });
241
384
  res.end(asset.data);
242
385
  }
243
386
  else {
244
- sendJson(res, 404, { error: "Not found" });
387
+ sendJson(res, 502, { error: "Failed to fetch PWA assets" });
245
388
  }
246
389
  return;
247
390
  }
248
- // API endpoints require auth
249
- if (!checkAuth(req)) {
391
+ // ── API endpoints require auth (localhost is trusted) ───────────────
392
+ if (!isLocalhost(req) && !checkAuth(req)) {
250
393
  sendJson(res, 401, { error: "Unauthorized" });
251
394
  return;
252
395
  }
@@ -258,7 +401,6 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
258
401
  Connection: "keep-alive",
259
402
  });
260
403
  res.write(":ok\n\n");
261
- // Send heartbeat every 5 seconds
262
404
  const heartbeat = setInterval(() => {
263
405
  res.write("data: {\"heartbeat\":true}\n\n");
264
406
  }, 5000);
@@ -290,7 +432,7 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
290
432
  const sessionToken = extractSessionToken(req);
291
433
  console.log(`[http] RPC: ${method}`);
292
434
  try {
293
- const response = await handleRpc({ method, params, sessionToken });
435
+ const response = await handleRpc({ method, params, sessionToken, localhost: isLocalhost(req) });
294
436
  console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
295
437
  sendJson(res, 200, response);
296
438
  }
@@ -303,10 +445,9 @@ export async function startHttpTransport(config, handleRpc, port, pairingCode, o
303
445
  sendJson(res, 404, { error: "Not found" });
304
446
  });
305
447
  return new Promise((resolve, reject) => {
306
- server.listen(port, () => {
307
- console.log(`[http] Listening on port ${port}`);
448
+ server.listen(port, bindAddress, () => {
449
+ console.log(`[http] Listening on ${bindAddress}:${port}`);
308
450
  onReady?.();
309
- // Graceful shutdown
310
451
  const shutdown = () => {
311
452
  console.log("[http] Shutting down...");
312
453
  for (const client of sseClients) {
package/dist/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface HostConfig {
8
8
  key: string;
9
9
  label: string;
10
10
  }>;
11
+ httpPort?: number;
12
+ lanEnabled?: boolean;
11
13
  }
12
14
  export interface TaskFrontmatter {
13
15
  id: string;
@@ -29,8 +31,6 @@ export interface ParsedTask {
29
31
  body: string;
30
32
  }
31
33
  /**
32
- * State machine: started → (pending_confirmation | pending_permission | pending_input) → finished | aborted | failed
33
- *
34
34
  * - `started`: task is actively running
35
35
  * - `finished`: agent completed successfully
36
36
  * - `aborted`: user declined confirmation, permission, or input
@@ -38,26 +38,15 @@ export interface ParsedTask {
38
38
  */
39
39
  export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
40
40
  /**
41
- * Persisted to `status.json` in the task directory. Updated by the run process
42
- * and read by the RPC handler + PWA to track live task state.
43
- *
44
- * Interactive request flow: the run process sets a `pending_*` field and waits
45
- * for `user_input` to be populated by an RPC call (task.user_input). Only one
46
- * `pending_*` field is set at a time.
41
+ * Persisted to `status.json` in the task directory. Used for crash detection
42
+ * (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
43
+ * permission, input) are handled via held HTTP connections on the serve daemon.
47
44
  */
48
45
  export interface TaskStatus {
49
46
  running_state: TaskRunningState;
50
47
  time_stamp: number;
51
48
  /** PID of the palmier run process (used on Windows to kill the process tree). */
52
49
  pid?: number;
53
- /** Set when the task has `requires_confirmation` and is awaiting user approval. */
54
- pending_confirmation?: boolean;
55
- /** Set when the agent requests permissions not yet granted. Contains the permissions needed. */
56
- pending_permission?: RequiredPermission[];
57
- /** Set when the agent requests user input. Contains descriptions of each requested value. */
58
- pending_input?: string[];
59
- /** Written by the RPC handler to deliver the user's response to the waiting run process. */
60
- user_input?: string[];
61
50
  }
62
51
  export interface HistoryEntry {
63
52
  task_id: string;
@@ -78,5 +67,7 @@ export interface RpcMessage {
78
67
  method: string;
79
68
  params: Record<string, unknown>;
80
69
  sessionToken?: string;
70
+ /** Trusted localhost request — skip session validation. */
71
+ localhost?: boolean;
81
72
  }
82
73
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -20,19 +20,19 @@ If the task fails because a tool was denied or you lack the required permissions
20
20
  [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
21
  [PALMIER_PERMISSION] Write | Write generated output files
22
22
 
23
- ## CLI Commands
23
+ ## HTTP Endpoints
24
24
 
25
- You have access to the following palmier CLI commands:
25
+ The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
26
26
 
27
- **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, request it:
27
+ **Requesting user input** — When you need information from the user (credentials, questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout. Instead, POST to `/request-input` with:
28
+ ```json
29
+ {"taskId":"{{TASK_ID}}","descriptions":["question 1","question 2"]}
28
30
  ```
29
- palmier request-input --description "What is the database connection string?" --description "What is the API key?"
30
- ```
31
- The command blocks until the user responds and prints each value on its own line. If the user aborts, the command exits with a non-zero status.
31
+ The request blocks until the user responds. Response: `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user declines.
32
32
 
33
- **Sending push notifications** — If you need to send a push notification to the user:
34
- ```
35
- palmier notify --title "Task Complete" --body "The deployment finished successfully."
33
+ **Sending push notifications** — To notify the user, POST to `/notify` with:
34
+ ```json
35
+ {"title":"...","body":"..."}
36
36
  ```
37
37
 
38
38
  ---
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class ClaudeAgent implements AgentTool {
@@ -13,8 +13,8 @@ export class ClaudeAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
- const args = ["--permission-mode", "acceptEdits", "-p"];
16
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
+ const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
20
  for (const p of allPerms) {
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CodexAgent implements AgentTool {
@@ -13,7 +13,7 @@ export class CodexAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
16
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
17
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
18
18
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
19
19
 
@@ -22,9 +22,9 @@ export class CodexAgent implements AgentTool {
22
22
  args.push("--config");
23
23
  args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
24
24
  }
25
+ if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
25
26
  args.push("-"); // read prompt from stdin
26
27
 
27
- if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
28
28
  return { command: "codex", args, stdin: prompt };
29
29
  }
30
30
 
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
@@ -13,14 +13,11 @@ export class CopilotAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
16
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
17
  const args = ["-p", prompt];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- if (allPerms.length > 0) {
21
- args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);;
22
- }
23
-
20
+ args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
24
21
  if (followupPrompt) { args.push("--continue"); }
25
22
  return { command: "copilot", args};
26
23
  }
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
@@ -13,19 +13,19 @@ export class GeminiAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
17
- const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
18
- const args = ["--prompt", "-"];
16
+ const fullPrompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
+ const args = ["--allowed-tools", "web_fetch"];
19
18
 
20
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
20
  if (allPerms.length > 0) {
22
- args.push("--allowed-tools");
23
21
  for (const p of allPerms) {
24
22
  args.push(p.name);
25
23
  }
26
24
  }
27
25
 
28
26
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
27
+ args.push("--prompt", "-"); // read prompt from stdin
28
+
29
29
  return { command: "gemini", args, stdin: fullPrompt };
30
30
  }
31
31
 
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
7
  getPlanGenerationCommandLine(prompt: string): CommandLine {
@@ -12,7 +12,7 @@ export class OpenClawAgent implements AgentTool {
12
12
  }
13
13
 
14
14
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
15
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
16
  // OpenClaw does not support stdin as prompt.
17
17
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
18
18