palmier 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agents/agent-instructions.md +28 -6
- package/dist/agents/agent.js +6 -3
- package/dist/agents/hermes.d.ts +9 -0
- package/dist/agents/hermes.js +35 -0
- package/dist/commands/plan-generation.md +1 -0
- package/dist/commands/run.js +3 -3
- 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 +22 -0
- package/dist/mcp-tools.js +152 -0
- package/dist/pwa/assets/{index-DhvJN8ie.css → index-DAI3J-jU.css} +1 -1
- package/dist/pwa/assets/index-RrJvjqz9.js +118 -0
- package/dist/pwa/assets/web-DQteXlI7.js +1 -0
- package/dist/pwa/assets/web-EzNEHXEh.js +1 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +23 -15
- package/dist/transports/http-transport.js +61 -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 +55 -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/TaskListView.tsx +94 -78
- 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 +28 -6
- package/src/agents/agent.ts +6 -3
- package/src/agents/hermes.ts +38 -0
- package/src/commands/plan-generation.md +1 -0
- package/src/commands/run.ts +3 -3
- package/src/location-device.ts +35 -0
- package/src/mcp-handler.ts +133 -0
- package/src/mcp-tools.ts +182 -0
- package/src/rpc-handler.ts +24 -15
- package/src/transports/http-transport.ts +58 -128
- package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
|
@@ -5,8 +5,10 @@ import { StringCodec, type NatsConnection } from "nats";
|
|
|
5
5
|
import { validateClient, addClient } from "../client-store.js";
|
|
6
6
|
import { registerPending } from "../pending-requests.js";
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
-
import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
|
|
9
8
|
import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
|
|
9
|
+
import { agentToolMap, ToolError, type ToolContext } from "../mcp-tools.js";
|
|
10
|
+
import { handleMcpRequest, getAgentName } from "../mcp-handler.js";
|
|
11
|
+
import { getTaskDir } from "../task.js";
|
|
10
12
|
|
|
11
13
|
// ── Bundled PWA asset serving ───────────────────────────────────────────
|
|
12
14
|
|
|
@@ -87,18 +89,6 @@ export function detectLanIp(): string {
|
|
|
87
89
|
return "127.0.0.1";
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
/** Find the latest (highest-numbered) run directory for a task. */
|
|
91
|
-
function findLatestRunId(taskDir: string): string | null {
|
|
92
|
-
try {
|
|
93
|
-
const dirs = fs.readdirSync(taskDir)
|
|
94
|
-
.filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
|
|
95
|
-
.sort();
|
|
96
|
-
return dirs.length > 0 ? dirs[dirs.length - 1] : null;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
92
|
/**
|
|
103
93
|
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
104
94
|
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
@@ -172,10 +162,65 @@ export async function startHttpTransport(
|
|
|
172
162
|
broadcastSseEvent({ task_id: taskId, ...payload });
|
|
173
163
|
}
|
|
174
164
|
|
|
165
|
+
function makeToolContext(sessionId: string): ToolContext {
|
|
166
|
+
return { config, nc, publishEvent, sessionId, agentName: getAgentName(sessionId) };
|
|
167
|
+
}
|
|
168
|
+
|
|
175
169
|
const server = http.createServer(async (req, res) => {
|
|
176
170
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
177
171
|
const pathname = url.pathname;
|
|
178
172
|
|
|
173
|
+
// ── MCP streamable HTTP endpoint ──────────────────────────────────
|
|
174
|
+
|
|
175
|
+
if (req.method === "POST" && pathname === "/mcp") {
|
|
176
|
+
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
177
|
+
try {
|
|
178
|
+
const body = await readBody(req);
|
|
179
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
180
|
+
const ctx = makeToolContext(sessionId ?? "");
|
|
181
|
+
const result = await handleMcpRequest(body, sessionId, ctx);
|
|
182
|
+
if (result.sessionId) {
|
|
183
|
+
res.setHeader("Mcp-Session-Id", result.sessionId);
|
|
184
|
+
}
|
|
185
|
+
sendJson(res, 200, result.body);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
sendJson(res, 500, { error: String(err) });
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Auto-generated REST endpoints from MCP tool registry ──────────
|
|
193
|
+
|
|
194
|
+
if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
|
|
195
|
+
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
196
|
+
const tool = agentToolMap.get(pathname.slice(1))!;
|
|
197
|
+
try {
|
|
198
|
+
const body = await readBody(req);
|
|
199
|
+
const args = body.trim() ? JSON.parse(body) : {};
|
|
200
|
+
const { taskId } = args as { taskId?: string };
|
|
201
|
+
if (!taskId) {
|
|
202
|
+
sendJson(res, 400, { error: "taskId is required" });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
206
|
+
if (!fs.existsSync(taskDir)) {
|
|
207
|
+
sendJson(res, 404, { error: `Task not found: ${taskId}` });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
delete args.taskId;
|
|
211
|
+
const ctx = makeToolContext(taskId);
|
|
212
|
+
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
|
|
213
|
+
const result = await tool.handler(args, ctx);
|
|
214
|
+
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name} done:`, JSON.stringify(result).slice(0, 200));
|
|
215
|
+
sendJson(res, 200, result);
|
|
216
|
+
} catch (err: any) {
|
|
217
|
+
const status = err instanceof ToolError ? err.statusCode : 500;
|
|
218
|
+
console.error(`[mcp] REST ${tool.name} error:`, err.message ?? String(err));
|
|
219
|
+
sendJson(res, status, { error: err.message ?? String(err) });
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
179
224
|
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
180
225
|
|
|
181
226
|
if (req.method === "POST" && pathname === "/event") {
|
|
@@ -217,121 +262,6 @@ export async function startHttpTransport(
|
|
|
217
262
|
return;
|
|
218
263
|
}
|
|
219
264
|
|
|
220
|
-
// ── POST /notify — send push notification via NATS ─────────────────
|
|
221
|
-
|
|
222
|
-
if (req.method === "POST" && pathname === "/notify") {
|
|
223
|
-
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
224
|
-
if (!nc) { sendJson(res, 503, { error: "NATS not connected — push notifications require server mode" }); return; }
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const body = await readBody(req);
|
|
228
|
-
const { taskId: notifTaskId, title, body: notifBody } = JSON.parse(body) as { taskId?: string; title: string; body: string };
|
|
229
|
-
if (!title || !notifBody) { sendJson(res, 400, { error: "title and body are required" }); return; }
|
|
230
|
-
|
|
231
|
-
const sc = StringCodec();
|
|
232
|
-
const payload: Record<string, string> = { hostId: config.hostId, title, body: notifBody };
|
|
233
|
-
if (notifTaskId) payload.task_id = notifTaskId;
|
|
234
|
-
const subject = `host.${config.hostId}.push.send`;
|
|
235
|
-
const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 15_000 });
|
|
236
|
-
const result = JSON.parse(sc.decode(reply.data)) as { ok?: boolean; error?: string };
|
|
237
|
-
|
|
238
|
-
if (result.ok) {
|
|
239
|
-
sendJson(res, 200, { ok: true });
|
|
240
|
-
} else {
|
|
241
|
-
sendJson(res, 502, { error: result.error ?? "Push notification failed" });
|
|
242
|
-
}
|
|
243
|
-
} catch (err) {
|
|
244
|
-
sendJson(res, 500, { error: `Failed to send notification: ${err}` });
|
|
245
|
-
}
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ── POST /request-input — held connection until user responds ────────
|
|
250
|
-
|
|
251
|
-
if (req.method === "POST" && pathname === "/request-input") {
|
|
252
|
-
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
253
|
-
try {
|
|
254
|
-
const body = await readBody(req);
|
|
255
|
-
const { taskId, runId, descriptions } = JSON.parse(body) as {
|
|
256
|
-
taskId: string; runId?: string; descriptions: string[];
|
|
257
|
-
};
|
|
258
|
-
if (!taskId || !descriptions?.length) {
|
|
259
|
-
sendJson(res, 400, { error: "taskId and descriptions are required" });
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
264
|
-
const task = parseTaskFile(taskDir);
|
|
265
|
-
|
|
266
|
-
// Resolve runId: use provided value, otherwise find the latest run directory
|
|
267
|
-
const effectiveRunId = runId ?? findLatestRunId(taskDir);
|
|
268
|
-
|
|
269
|
-
const pendingPromise = registerPending(taskId, "input", descriptions);
|
|
270
|
-
|
|
271
|
-
await publishEvent(taskId, {
|
|
272
|
-
event_type: "input-request",
|
|
273
|
-
host_id: config.hostId,
|
|
274
|
-
input_descriptions: descriptions,
|
|
275
|
-
name: task.frontmatter.name,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const response = await pendingPromise;
|
|
279
|
-
|
|
280
|
-
const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
|
|
281
|
-
|
|
282
|
-
if (response.length === 1 && response[0] === "aborted") {
|
|
283
|
-
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
|
|
284
|
-
if (effectiveRunId) {
|
|
285
|
-
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
|
|
286
|
-
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
287
|
-
}
|
|
288
|
-
sendJson(res, 200, { aborted: true });
|
|
289
|
-
} else {
|
|
290
|
-
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
|
|
291
|
-
if (effectiveRunId) {
|
|
292
|
-
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
|
|
293
|
-
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
294
|
-
}
|
|
295
|
-
sendJson(res, 200, { values: response });
|
|
296
|
-
}
|
|
297
|
-
} catch (err) {
|
|
298
|
-
sendJson(res, 500, { error: String(err) });
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ── POST /request-confirmation — held connection ────────────────────
|
|
304
|
-
|
|
305
|
-
if (req.method === "POST" && pathname === "/request-confirmation") {
|
|
306
|
-
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
307
|
-
try {
|
|
308
|
-
const body = await readBody(req);
|
|
309
|
-
const { taskId } = JSON.parse(body) as { taskId: string };
|
|
310
|
-
if (!taskId) { sendJson(res, 400, { error: "taskId is required" }); return; }
|
|
311
|
-
|
|
312
|
-
const pendingPromise = registerPending(taskId, "confirmation");
|
|
313
|
-
|
|
314
|
-
await publishEvent(taskId, {
|
|
315
|
-
event_type: "confirm-request",
|
|
316
|
-
host_id: config.hostId,
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const response = await pendingPromise;
|
|
320
|
-
const confirmed = response[0] === "confirmed";
|
|
321
|
-
|
|
322
|
-
await publishEvent(taskId, {
|
|
323
|
-
event_type: "confirm-resolved",
|
|
324
|
-
host_id: config.hostId,
|
|
325
|
-
status: confirmed ? "confirmed" : "aborted",
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
sendJson(res, 200, { confirmed });
|
|
329
|
-
} catch (err) {
|
|
330
|
-
sendJson(res, 500, { error: String(err) });
|
|
331
|
-
}
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
265
|
// ── POST /request-permission — held connection ──────────────────────
|
|
336
266
|
|
|
337
267
|
if (req.method === "POST" && pathname === "/request-permission") {
|