palmier 0.6.6 → 0.6.8

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 (96) hide show
  1. package/README.md +15 -1
  2. package/dist/agents/agent-instructions.md +6 -14
  3. package/dist/agents/aider.js +1 -1
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/cline.js +1 -1
  6. package/dist/agents/codex.js +1 -1
  7. package/dist/agents/copilot.js +1 -1
  8. package/dist/agents/cursor.js +1 -1
  9. package/dist/agents/deepagents.js +1 -1
  10. package/dist/agents/droid.js +1 -1
  11. package/dist/agents/gemini.js +1 -1
  12. package/dist/agents/goose.js +1 -1
  13. package/dist/agents/hermes.js +1 -1
  14. package/dist/agents/kimi.js +1 -1
  15. package/dist/agents/kiro.js +1 -1
  16. package/dist/agents/openclaw.js +1 -1
  17. package/dist/agents/opencode.js +1 -1
  18. package/dist/agents/qoder.js +1 -1
  19. package/dist/agents/qwen.js +1 -1
  20. package/dist/agents/shared-prompt.d.ts +3 -2
  21. package/dist/agents/shared-prompt.js +6 -4
  22. package/dist/commands/plan-generation.md +1 -0
  23. package/dist/commands/run.js +4 -7
  24. package/dist/location-device.d.ts +8 -0
  25. package/dist/location-device.js +32 -0
  26. package/dist/mcp-handler.d.ts +8 -0
  27. package/dist/mcp-handler.js +110 -0
  28. package/dist/mcp-tools.d.ts +27 -0
  29. package/dist/mcp-tools.js +218 -0
  30. package/dist/pwa/assets/{index-DhvJN8ie.css → index-C6Lz09EY.css} +1 -1
  31. package/dist/pwa/assets/index-C8vJwUNi.js +118 -0
  32. package/dist/pwa/assets/web-6UChJFov.js +1 -0
  33. package/dist/pwa/assets/web-NxTETXZK.js +1 -0
  34. package/dist/pwa/index.html +3 -3
  35. package/dist/pwa/service-worker.js +2 -2
  36. package/dist/rpc-handler.js +20 -8
  37. package/dist/spawn-command.js +3 -1
  38. package/dist/transports/http-transport.js +60 -129
  39. package/package.json +1 -1
  40. package/palmier-server/README.md +6 -1
  41. package/palmier-server/package.json +7 -1
  42. package/palmier-server/pnpm-lock.yaml +1025 -1
  43. package/palmier-server/pwa/index.html +1 -1
  44. package/palmier-server/pwa/package.json +3 -0
  45. package/palmier-server/pwa/src/App.css +64 -0
  46. package/palmier-server/pwa/src/api.ts +8 -2
  47. package/palmier-server/pwa/src/components/HostMenu.tsx +102 -1
  48. package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
  49. package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
  50. package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
  51. package/palmier-server/pwa/src/constants.ts +1 -1
  52. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +2 -1
  53. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +3 -0
  54. package/palmier-server/pwa/src/pages/Dashboard.tsx +5 -2
  55. package/palmier-server/pwa/src/pages/PairHost.tsx +10 -1
  56. package/palmier-server/pwa/src/service-worker.ts +7 -7
  57. package/palmier-server/server/.env.example +4 -0
  58. package/palmier-server/server/package.json +1 -0
  59. package/palmier-server/server/src/db.ts +10 -0
  60. package/palmier-server/server/src/fcm.ts +74 -0
  61. package/palmier-server/server/src/index.ts +101 -21
  62. package/palmier-server/server/src/notify.ts +34 -0
  63. package/palmier-server/server/src/push.ts +1 -1
  64. package/palmier-server/server/src/routes/fcm.ts +64 -0
  65. package/palmier-server/server/src/routes/push.ts +6 -5
  66. package/palmier-server/spec.md +4 -2
  67. package/src/agents/agent-instructions.md +6 -14
  68. package/src/agents/aider.ts +1 -1
  69. package/src/agents/claude.ts +1 -1
  70. package/src/agents/cline.ts +1 -1
  71. package/src/agents/codex.ts +1 -1
  72. package/src/agents/copilot.ts +1 -1
  73. package/src/agents/cursor.ts +1 -1
  74. package/src/agents/deepagents.ts +1 -1
  75. package/src/agents/droid.ts +1 -1
  76. package/src/agents/gemini.ts +1 -1
  77. package/src/agents/goose.ts +1 -1
  78. package/src/agents/hermes.ts +1 -1
  79. package/src/agents/kimi.ts +1 -1
  80. package/src/agents/kiro.ts +1 -1
  81. package/src/agents/openclaw.ts +1 -1
  82. package/src/agents/opencode.ts +1 -1
  83. package/src/agents/qoder.ts +1 -1
  84. package/src/agents/qwen.ts +1 -1
  85. package/src/agents/shared-prompt.ts +7 -4
  86. package/src/commands/plan-generation.md +1 -0
  87. package/src/commands/run.ts +4 -7
  88. package/src/location-device.ts +35 -0
  89. package/src/mcp-handler.ts +133 -0
  90. package/src/mcp-tools.ts +253 -0
  91. package/src/rpc-handler.ts +21 -8
  92. package/src/spawn-command.ts +3 -1
  93. package/src/transports/http-transport.ts +57 -128
  94. package/test/agent-instructions.test.ts +68 -5
  95. package/test/fixtures/agent-instructions-snapshot.md +58 -0
  96. package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
@@ -5,12 +5,13 @@ import express from "express";
5
5
  import helmet from "helmet";
6
6
  import { pool, initDb } from "./db.js";
7
7
  import { connectNats, getNatsConnection } from "./nats.js";
8
- import { sendPushToHost } from "./push.js";
9
-
10
8
  import { StringCodec } from "nats";
11
9
 
12
10
  import hostsRoutes from "./routes/hosts.js";
13
11
  import pushRoutes from "./routes/push.js";
12
+ import fcmRoutes from "./routes/fcm.js";
13
+ import { notifyClients } from "./notify.js";
14
+ import { sendFcmToClients, sendFcmToDevice } from "./fcm.js";
14
15
 
15
16
  const PORT = parseInt(process.env.PORT || "3000", 10);
16
17
 
@@ -42,28 +43,34 @@ async function main(): Promise<void> {
42
43
  running_state?: string;
43
44
  name?: string;
44
45
  run_id?: string;
46
+ session_id?: string;
47
+ description?: string;
48
+ agent_name?: string;
45
49
  required_permissions?: Array<{ name: string; description: string }>;
46
- input_descriptions?: string[];
50
+ input_questions?: string[];
47
51
  result_file?: string;
48
52
  };
49
53
 
50
54
  if (data.event_type === "confirm-request") {
51
- await sendPushToHost(hostId, {
55
+ await notifyClients(hostId, {
52
56
  type: "confirm",
53
- task_id: taskId,
57
+ title: "Confirmation Required",
58
+ body: data.description || "A task requires confirmation to run.",
54
59
  host_id: hostId,
60
+ session_id: data.session_id,
61
+ agent_name: data.agent_name,
55
62
  });
56
63
  } else if (data.event_type === "confirm-resolved") {
57
- await sendPushToHost(hostId, {
64
+ await notifyClients(hostId, {
58
65
  type: "confirm-dismiss",
59
- task_id: taskId,
60
66
  host_id: hostId,
67
+ session_id: data.session_id,
61
68
  });
62
69
  } else if (data.event_type === "permission-request") {
63
70
  const taskLabel = data.name
64
71
  ? data.name.length > 60 ? data.name.slice(0, 60) + "…" : data.name
65
72
  : "A task";
66
- await sendPushToHost(hostId, {
73
+ await notifyClients(hostId, {
67
74
  type: "permission",
68
75
  title: "Permission Required",
69
76
  body: `${taskLabel} needs additional permissions to continue.`,
@@ -71,27 +78,25 @@ async function main(): Promise<void> {
71
78
  host_id: hostId,
72
79
  });
73
80
  } else if (data.event_type === "permission-resolved") {
74
- await sendPushToHost(hostId, {
81
+ await notifyClients(hostId, {
75
82
  type: "permission-dismiss",
76
83
  task_id: taskId,
77
84
  host_id: hostId,
78
85
  });
79
86
  } else if (data.event_type === "input-request") {
80
- const taskLabel = data.name
81
- ? data.name.length > 60 ? data.name.slice(0, 60) + "…" : data.name
82
- : "A task";
83
- await sendPushToHost(hostId, {
87
+ await notifyClients(hostId, {
84
88
  type: "input",
85
89
  title: "Input Required",
86
- body: `${taskLabel} needs your input to continue.`,
87
- task_id: taskId,
90
+ body: "A task needs your input to continue.",
88
91
  host_id: hostId,
92
+ session_id: data.session_id,
93
+ agent_name: data.agent_name,
89
94
  });
90
95
  } else if (data.event_type === "input-resolved") {
91
- await sendPushToHost(hostId, {
96
+ await notifyClients(hostId, {
92
97
  type: "input-dismiss",
93
- task_id: taskId,
94
98
  host_id: hostId,
99
+ session_id: data.session_id,
95
100
  });
96
101
  } else if (data.event_type === "report-generated" || (data.event_type === "running-state" && data.running_state === "failed")) {
97
102
  const label = data.name;
@@ -100,7 +105,7 @@ async function main(): Promise<void> {
100
105
  : "Task";
101
106
  const isFailure = data.running_state === "failed";
102
107
  const body = isFailure ? `${taskLabel} — failed` : `${taskLabel} — report ready`;
103
- await sendPushToHost(hostId, {
108
+ await notifyClients(hostId, {
104
109
  type: isFailure ? "fail" : "complete",
105
110
  title: "Palmier",
106
111
  body,
@@ -132,7 +137,8 @@ async function main(): Promise<void> {
132
137
  hostId: string;
133
138
  title: string;
134
139
  body: string;
135
- task_id?: string;
140
+ session_id?: string;
141
+ agent_name?: string;
136
142
  };
137
143
 
138
144
  // Validate hostId in subject matches payload
@@ -144,12 +150,17 @@ async function main(): Promise<void> {
144
150
  continue;
145
151
  }
146
152
 
153
+ // If no agent_name, session_id is a taskId — include as task_id for deep-linking
154
+ const isTask = data.session_id && !data.agent_name;
155
+
147
156
  console.log(`[Push] Sending notification for host ${data.hostId}`);
148
- await sendPushToHost(data.hostId, {
157
+ await notifyClients(data.hostId, {
149
158
  type: "notification",
159
+ host_id: data.hostId,
150
160
  title: data.title,
151
161
  body: data.body,
152
- ...(data.task_id ? { task_id: data.task_id } : {}),
162
+ ...(isTask ? { task_id: data.session_id } : {}),
163
+ agent_name: data.agent_name,
153
164
  });
154
165
 
155
166
  if (msg.reply) {
@@ -167,19 +178,88 @@ async function main(): Promise<void> {
167
178
  }
168
179
  })();
169
180
 
181
+ // Subscribe to FCM geolocation requests from hosts
182
+ (async () => {
183
+ try {
184
+ const conn = await getNatsConnection();
185
+ const sub = conn.subscribe("host.*.fcm.geolocation");
186
+ console.log("Listening for FCM geolocation requests");
187
+
188
+ for await (const msg of sub) {
189
+ try {
190
+ const data = JSON.parse(sc.decode(msg.data)) as {
191
+ hostId: string;
192
+ requestId: string;
193
+ fcmToken?: string;
194
+ };
195
+
196
+ const subjectHostId = msg.subject.split(".")[1];
197
+ if (data.hostId !== subjectHostId) {
198
+ if (msg.reply) {
199
+ msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
200
+ }
201
+ continue;
202
+ }
203
+
204
+ const fcmPayload = {
205
+ type: "geolocation-request",
206
+ requestId: data.requestId,
207
+ hostId: data.hostId,
208
+ };
209
+
210
+ console.log(`[FCM] Sending geolocation request for host ${data.hostId}`);
211
+ if (data.fcmToken) {
212
+ await sendFcmToDevice(data.fcmToken, fcmPayload);
213
+ } else {
214
+ await sendFcmToClients(data.hostId, fcmPayload);
215
+ }
216
+
217
+ if (msg.reply) {
218
+ msg.respond(sc.encode(JSON.stringify({ ok: true })));
219
+ }
220
+ } catch (err) {
221
+ console.error("[FCM] Error handling geolocation request:", err);
222
+ if (msg.reply) {
223
+ msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
224
+ }
225
+ }
226
+ }
227
+ } catch (err) {
228
+ console.error("Failed to subscribe to FCM geolocation requests:", err);
229
+ }
230
+ })();
231
+
170
232
  // Create Express app
171
233
  const app = express();
172
234
 
173
235
  app.use(
174
236
  helmet({
175
237
  contentSecurityPolicy: false,
238
+ crossOriginResourcePolicy: false,
176
239
  })
177
240
  );
241
+
242
+ // CORS for Capacitor Android app (requests from capacitor://localhost)
243
+ app.use((req, res, next) => {
244
+ const origin = req.headers.origin;
245
+ if (origin && (origin.startsWith("capacitor://") || origin === "https://localhost" || origin.startsWith("http://localhost"))) {
246
+ res.setHeader("Access-Control-Allow-Origin", origin);
247
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
248
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
249
+ }
250
+ if (req.method === "OPTIONS") {
251
+ res.sendStatus(204);
252
+ return;
253
+ }
254
+ next();
255
+ });
256
+
178
257
  app.use(express.json());
179
258
 
180
259
  // Mount routes
181
260
  app.use("/api/hosts", hostsRoutes);
182
261
  app.use("/api/push", pushRoutes);
262
+ app.use("/api/fcm", fcmRoutes);
183
263
 
184
264
  // Public NATS config endpoint (used by PWA for pairing)
185
265
  app.get("/api/config", (_req, res) => {
@@ -0,0 +1,34 @@
1
+ import { sendPushToClients } from "./push.js";
2
+ import { sendFcmToClients } from "./fcm.js";
3
+
4
+ export interface NotificationPayload {
5
+ type: string;
6
+ host_id: string;
7
+ title?: string;
8
+ body?: string;
9
+ task_id?: string;
10
+ session_id?: string;
11
+ run_id?: string;
12
+ result_file?: string;
13
+ agent_name?: string;
14
+ }
15
+
16
+ function stringifyPayload(payload: NotificationPayload): Record<string, string> {
17
+ const result: Record<string, string> = {};
18
+ for (const [key, value] of Object.entries(payload)) {
19
+ if (value !== undefined && value !== null) {
20
+ result[key] = String(value);
21
+ }
22
+ }
23
+ return result;
24
+ }
25
+
26
+ export async function notifyClients(
27
+ hostId: string,
28
+ payload: NotificationPayload
29
+ ): Promise<void> {
30
+ await Promise.allSettled([
31
+ sendPushToClients(hostId, payload),
32
+ sendFcmToClients(hostId, stringifyPayload(payload)),
33
+ ]);
34
+ }
@@ -19,7 +19,7 @@ function ensureConfigured(): void {
19
19
  configured = true;
20
20
  }
21
21
 
22
- export async function sendPushToHost(
22
+ export async function sendPushToClients(
23
23
  hostId: string,
24
24
  payload: object | string
25
25
  ): Promise<void> {
@@ -0,0 +1,64 @@
1
+ import { Router, Request, Response } from "express";
2
+ import type { Router as RouterType } from "express";
3
+ import { pool } from "../db.js";
4
+ import { getNatsConnection } from "../nats.js";
5
+ import { StringCodec } from "nats";
6
+
7
+ const router: RouterType = Router();
8
+
9
+ // POST /api/fcm/register - Register or refresh an FCM token for a host
10
+ router.post("/register", async (req: Request, res: Response) => {
11
+ try {
12
+ const { hostId, fcmToken, deviceLabel } = req.body;
13
+
14
+ if (!hostId || !fcmToken) {
15
+ res.status(400).json({ error: "hostId and fcmToken are required" });
16
+ return;
17
+ }
18
+
19
+ await pool.query(
20
+ `INSERT INTO fcm_tokens (host_id, fcm_token, device_label, updated_at)
21
+ VALUES ($1, $2, $3, NOW())
22
+ ON CONFLICT (host_id, fcm_token)
23
+ DO UPDATE SET device_label = EXCLUDED.device_label, updated_at = NOW()`,
24
+ [hostId, fcmToken, deviceLabel || null]
25
+ );
26
+
27
+ res.status(201).json({ message: "FCM token registered" });
28
+ } catch (err) {
29
+ console.error("FCM register error:", err);
30
+ res.status(500).json({ error: "Internal server error" });
31
+ }
32
+ });
33
+
34
+ // POST /api/fcm/geolocation-response - Receive location from Android device, forward via NATS
35
+ router.post("/geolocation-response", async (req: Request, res: Response) => {
36
+ try {
37
+ const { requestId, hostId, latitude, longitude, accuracy, timestamp, error } = req.body;
38
+
39
+ if (!requestId || !hostId) {
40
+ res.status(400).json({ error: "requestId and hostId are required" });
41
+ return;
42
+ }
43
+
44
+ const conn = await getNatsConnection();
45
+ const sc = StringCodec();
46
+ const subject = `host.${hostId}.geolocation.${requestId}`;
47
+
48
+ if (error) {
49
+ conn.publish(subject, sc.encode(JSON.stringify({ error })));
50
+ } else {
51
+ conn.publish(
52
+ subject,
53
+ sc.encode(JSON.stringify({ latitude, longitude, accuracy, timestamp }))
54
+ );
55
+ }
56
+
57
+ res.json({ message: "Location forwarded" });
58
+ } catch (err) {
59
+ console.error("FCM geolocation-response error:", err);
60
+ res.status(500).json({ error: "Internal server error" });
61
+ }
62
+ });
63
+
64
+ export default router;
@@ -66,14 +66,15 @@ router.get("/vapid-key", (_req, res: Response) => {
66
66
  res.json({ publicKey });
67
67
  });
68
68
 
69
- // POST /api/push/respond - Respond to a pending confirmation via NATS request-reply
69
+ // POST /api/push/respond - Respond to a pending request via NATS request-reply
70
70
  router.post("/respond", async (req: Request, res: Response) => {
71
71
  try {
72
- const { task_id, host_id, response } = req.body;
72
+ const { task_id, session_id, host_id, response } = req.body;
73
+ const id = session_id || task_id;
73
74
 
74
- if (!task_id || !host_id || !response) {
75
+ if (!id || !host_id || !response) {
75
76
  res.status(400).json({
76
- error: "task_id, host_id, and response are required",
77
+ error: "host_id, response, and session_id (or task_id for permissions) are required",
77
78
  });
78
79
  return;
79
80
  }
@@ -81,7 +82,7 @@ router.post("/respond", async (req: Request, res: Response) => {
81
82
  const conn = await getNatsConnection();
82
83
  const sc = StringCodec();
83
84
  const subject = `host.${host_id}.rpc.task.user_input`;
84
- const payload = sc.encode(JSON.stringify({ id: task_id, value: [response] }));
85
+ const payload = sc.encode(JSON.stringify({ id, value: [response] }));
85
86
 
86
87
  const reply = await conn.request(subject, payload, { timeout: 5000 });
87
88
  const result = JSON.parse(sc.decode(reply.data));
@@ -12,9 +12,11 @@ The host supports **Linux** (systemd) and **Windows** (Task Scheduler for both d
12
12
 
13
13
  ### 1.2 Components
14
14
 
15
- * **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Localhost-only HTTP endpoints (`/notify`, `/request-input`, `/request-confirmation`, `/request-permission`) are used by agents and the `palmier run` process for interactive flows via held HTTP connections. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPlanGenerationCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
15
+ * **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Exposes a localhost-only MCP server at `/mcp` (streamable HTTP transport) with tools: `notify`, `request-input`, `request-confirmation`, `device-geolocation`. The same tools are auto-generated as REST endpoints (`/notify`, `/request-input`, etc.) from a shared tool registry zero duplication. REST endpoints require `taskId` in the body for session identification. `/request-permission` remains a separate endpoint (not part of the MCP tool registry). MCP sessions track agent names from `initialize` clientInfo for logging and UI display. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPlanGenerationCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
16
16
 
17
- * **Web Server (Node.js):** Serves the PWA assets (React) via `app.palmier.me` (Cloudflare proxied), manages Web Push VAPID keys, and provides host registration. Uses **PostgreSQL** for persistent storage (host registrations, push subscriptions). Connects to NATS via TCP to subscribe to `host-event.>` for sending push notifications (confirmations, dismissals, completion/failure). For `POST /api/push/respond` (confirmation responses via push notification action buttons), the Web Server forwards the response to the host via the `task.user_input` NATS RPC. Subscribes to `host.*.push.send` NATS subjects to relay push notification requests from the host CLI. Co-located with the NATS server on the same machine.
17
+ * **Web Server (Node.js):** Serves the PWA assets (React) via `app.palmier.me` (Cloudflare proxied), manages Web Push VAPID keys, and provides host registration. Uses **PostgreSQL** for persistent storage (host registrations, push subscriptions, FCM tokens). Connects to NATS via TCP to subscribe to `host-event.>` for sending push notifications (confirmations, dismissals, completion/failure). For `POST /api/push/respond` (confirmation responses via push notification action buttons), the Web Server forwards the response to the host via the `task.user_input` NATS RPC. Subscribes to `host.*.push.send` NATS subjects to relay push notification requests from the host CLI. Subscribes to `host.*.fcm.geolocation` to relay device geolocation requests via FCM. Co-located with the NATS server on the same machine.
18
+
19
+ * **Android App (Capacitor):** Native Android wrapper for the PWA. Provides FCM push messaging for receiving data messages in the background and `FusedLocationProviderClient` for GPS access. When a geolocation request arrives via FCM, a foreground service briefly starts to fetch the GPS fix and POST the result back to the Web Server. See the `palmier-android` repo.
18
20
 
19
21
  * **PWA (React):** The user-facing frontend, primarily targeting mobile devices. Connects to the NATS server via **WebSockets** at `nats.palmier.me` (DNS only, not Cloudflare proxied, to avoid interference with persistent connections). No user accounts — paired hosts are stored in localStorage.
20
22
 
@@ -1,4 +1,4 @@
1
- You are an AI agent executing a task on behalf of the user via the Palmier platform. Follow these instructions carefully.
1
+ You are an AI agent executing a task on behalf of the user. Follow these instructions carefully.
2
2
 
3
3
  ## Reporting Output
4
4
 
@@ -13,24 +13,16 @@ When you are done, output exactly one of these markers as the very last line (no
13
13
 
14
14
  ## Permissions
15
15
 
16
- If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format:
16
+ Whenever a tool you are trying to use is denied or you lack the required permissions, print each required permission on its own line using this exact format:
17
17
  [PALMIER_PERMISSION] <tool_name> | <description>
18
18
 
19
19
  ## HTTP Endpoints
20
20
 
21
- The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
21
+ {{ENDPOINT_DOCS}}
22
22
 
23
- **Requesting user input** — When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment. Instead, POST to `/request-input` with:
24
- ```json
25
- {"taskId":"{{TASK_ID}}","descriptions":["question 1","question 2"]}
26
- ```
27
- The request blocks until the user responds. Response: `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user declines.
28
-
29
- **Sending push notifications** — To notify the user, POST to `/notify` with:
30
- ```json
31
- {"taskId":"{{TASK_ID}}","title":"...","body":"..."}
32
- ```
23
+ The task to execute follows below:
33
24
 
34
25
  ---
35
26
 
36
- The task to execute follows below.
27
+ {{TASK_DESCRIPTION}}
28
+
@@ -15,7 +15,7 @@ export class Aider implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class ClaudeAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["--permission-mode", yolo ? "bypassPermissions" : "acceptEdits", "-p"];
20
20
 
21
21
  if (!yolo) {
@@ -15,7 +15,7 @@ export class Cline implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class CodexAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["exec", "--skip-git-repo-check", "--sandbox", yolo ? "danger-full-access" : "workspace-write"];
20
20
 
21
21
  if (!yolo) {
@@ -15,7 +15,7 @@ export class CopilotAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["-p", prompt];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class Cursor implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class DeepAgents implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class DroidAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["exec", "--session-id", task.frontmatter.id];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class GeminiAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
20
20
 
21
21
  if (!yolo) {
@@ -15,7 +15,7 @@ export class GooseAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["run"];
20
20
 
21
21
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
@@ -15,7 +15,7 @@ export class Hermes implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["chat"];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class KimiAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class Kiro implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -14,7 +14,7 @@ export class OpenClawAgent implements AgentTool {
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
16
16
  const yolo = extraPermissions === "yolo";
17
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
18
18
  // OpenClaw does not support stdin as prompt.
19
19
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
20
20
 
@@ -15,7 +15,7 @@ export class OpenCodeAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["run"];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class Qoder implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class QwenAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
20
20
 
21
21
  if (followupPrompt) { args.push("-c"); }
@@ -2,6 +2,8 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { loadConfig } from "../config.js";
5
+ import { generateEndpointDocs } from "../mcp-tools.js";
6
+ import type { ParsedTask } from "../types.js";
5
7
 
6
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
9
 
@@ -11,13 +13,14 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
11
13
  );
12
14
 
13
15
  /**
14
- * Agent instructions with the serve daemon's HTTP port and task ID baked in.
16
+ * Build the full agent prompt: instructions + endpoint docs + task description.
15
17
  */
16
- export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
18
+ export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
17
19
  const port = loadConfig().httpPort ?? 9966;
20
+ const taskDescription = task.body || task.frontmatter.user_prompt;
18
21
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
19
- .replace(/\{\{PORT\}\}/g, String(port))
20
- .replace(/\{\{TASK_ID\}\}/g, taskId);
22
+ .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
23
+ .replace(/\{\{TASK_DESCRIPTION\}\}/g, taskDescription);
21
24
  if (skipPermissions) {
22
25
  instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
23
26
  }
@@ -16,6 +16,7 @@ task_name: <concise label, 3-6 words>
16
16
  - If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
17
17
  - When a step requires user input, simply state what information is needed from the user. Do **not** specify how to obtain it — the agent has its own tool for requesting user input.
18
18
  - Preserve relative time expressions (e.g., "today", "yesterday", "last week") exactly as written — do **not** resolve them to specific dates. The plan may be executed on a different day than it was generated.
19
+ - If the task involves opening a web browser or application, include a final step to close it before finishing.
19
20
 
20
21
  ## Task Description
21
22