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.
- package/README.md +15 -1
- package/dist/agents/agent-instructions.md +6 -14
- package/dist/agents/aider.js +1 -1
- package/dist/agents/claude.js +1 -1
- package/dist/agents/cline.js +1 -1
- package/dist/agents/codex.js +1 -1
- package/dist/agents/copilot.js +1 -1
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/droid.js +1 -1
- package/dist/agents/gemini.js +1 -1
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kimi.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/openclaw.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/qwen.js +1 -1
- package/dist/agents/shared-prompt.d.ts +3 -2
- package/dist/agents/shared-prompt.js +6 -4
- package/dist/commands/plan-generation.md +1 -0
- package/dist/commands/run.js +4 -7
- package/dist/location-device.d.ts +8 -0
- package/dist/location-device.js +32 -0
- package/dist/mcp-handler.d.ts +8 -0
- package/dist/mcp-handler.js +110 -0
- package/dist/mcp-tools.d.ts +27 -0
- package/dist/mcp-tools.js +218 -0
- package/dist/pwa/assets/{index-DhvJN8ie.css → index-C6Lz09EY.css} +1 -1
- package/dist/pwa/assets/index-C8vJwUNi.js +118 -0
- package/dist/pwa/assets/web-6UChJFov.js +1 -0
- package/dist/pwa/assets/web-NxTETXZK.js +1 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +20 -8
- package/dist/spawn-command.js +3 -1
- package/dist/transports/http-transport.js +60 -129
- package/package.json +1 -1
- package/palmier-server/README.md +6 -1
- package/palmier-server/package.json +7 -1
- package/palmier-server/pnpm-lock.yaml +1025 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/package.json +3 -0
- package/palmier-server/pwa/src/App.css +64 -0
- package/palmier-server/pwa/src/api.ts +8 -2
- package/palmier-server/pwa/src/components/HostMenu.tsx +102 -1
- package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
- package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
- package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +2 -1
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +3 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +5 -2
- package/palmier-server/pwa/src/pages/PairHost.tsx +10 -1
- package/palmier-server/pwa/src/service-worker.ts +7 -7
- package/palmier-server/server/.env.example +4 -0
- package/palmier-server/server/package.json +1 -0
- package/palmier-server/server/src/db.ts +10 -0
- package/palmier-server/server/src/fcm.ts +74 -0
- package/palmier-server/server/src/index.ts +101 -21
- package/palmier-server/server/src/notify.ts +34 -0
- package/palmier-server/server/src/push.ts +1 -1
- package/palmier-server/server/src/routes/fcm.ts +64 -0
- package/palmier-server/server/src/routes/push.ts +6 -5
- package/palmier-server/spec.md +4 -2
- package/src/agents/agent-instructions.md +6 -14
- package/src/agents/aider.ts +1 -1
- package/src/agents/claude.ts +1 -1
- package/src/agents/cline.ts +1 -1
- package/src/agents/codex.ts +1 -1
- package/src/agents/copilot.ts +1 -1
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/droid.ts +1 -1
- package/src/agents/gemini.ts +1 -1
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kimi.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/openclaw.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/qwen.ts +1 -1
- package/src/agents/shared-prompt.ts +7 -4
- package/src/commands/plan-generation.md +1 -0
- package/src/commands/run.ts +4 -7
- package/src/location-device.ts +35 -0
- package/src/mcp-handler.ts +133 -0
- package/src/mcp-tools.ts +253 -0
- package/src/rpc-handler.ts +21 -8
- package/src/spawn-command.ts +3 -1
- package/src/transports/http-transport.ts +57 -128
- package/test/agent-instructions.test.ts +68 -5
- package/test/fixtures/agent-instructions-snapshot.md +58 -0
- 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
|
-
|
|
50
|
+
input_questions?: string[];
|
|
47
51
|
result_file?: string;
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
if (data.event_type === "confirm-request") {
|
|
51
|
-
await
|
|
55
|
+
await notifyClients(hostId, {
|
|
52
56
|
type: "confirm",
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
...(
|
|
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
|
+
}
|
|
@@ -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
|
|
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 (!
|
|
75
|
+
if (!id || !host_id || !response) {
|
|
75
76
|
res.status(400).json({
|
|
76
|
-
error: "
|
|
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
|
|
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));
|
package/palmier-server/spec.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
{{ENDPOINT_DOCS}}
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
|
|
27
|
+
{{TASK_DESCRIPTION}}
|
|
28
|
+
|
package/src/agents/aider.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/claude.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/cline.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/codex.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/copilot.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["-p", prompt];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/cursor.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/deepagents.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/droid.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/gemini.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/goose.ts
CHANGED
|
@@ -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 ??
|
|
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
|
package/src/agents/hermes.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["chat"];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/kimi.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/kiro.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -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 ??
|
|
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
|
|
package/src/agents/opencode.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["run"];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/qoder.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/qwen.ts
CHANGED
|
@@ -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 ??
|
|
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
|
-
*
|
|
16
|
+
* Build the full agent prompt: instructions + endpoint docs + task description.
|
|
15
17
|
*/
|
|
16
|
-
export function getAgentInstructions(
|
|
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(/\{\{
|
|
20
|
-
.replace(/\{\{
|
|
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
|
|