palmier 0.7.6 → 0.7.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 (48) hide show
  1. package/dist/agents/shared-prompt.js +1 -1
  2. package/dist/commands/init.js +3 -2
  3. package/dist/commands/pair.js +1 -1
  4. package/dist/commands/run.js +4 -4
  5. package/dist/commands/serve.js +1 -1
  6. package/dist/config.js +2 -2
  7. package/dist/device-capabilities.d.ts +1 -1
  8. package/dist/events.js +1 -1
  9. package/dist/mcp-tools.js +64 -1
  10. package/dist/nats-client.d.ts +1 -1
  11. package/dist/nats-client.js +6 -3
  12. package/dist/pwa/assets/index-Bt8Hhaw3.js +118 -0
  13. package/dist/pwa/assets/{web-DnuoxUd4.js → web-CkWrlNwc.js} +1 -1
  14. package/dist/pwa/assets/{web-7raT3zOZ.js → web-lx34oBi7.js} +1 -1
  15. package/dist/pwa/index.html +1 -1
  16. package/dist/pwa/service-worker.js +1 -1
  17. package/dist/types.d.ts +2 -1
  18. package/package.json +1 -1
  19. package/palmier-server/PRODUCTION.md +31 -28
  20. package/palmier-server/README.md +35 -5
  21. package/palmier-server/nats.conf +9 -5
  22. package/palmier-server/package.json +2 -1
  23. package/palmier-server/pnpm-lock.yaml +6 -0
  24. package/palmier-server/pwa/src/components/HostMenu.tsx +58 -0
  25. package/palmier-server/pwa/src/constants.ts +1 -1
  26. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +9 -5
  27. package/palmier-server/pwa/src/pages/PairHost.tsx +6 -3
  28. package/palmier-server/server/package.json +3 -1
  29. package/palmier-server/server/src/index.ts +83 -2
  30. package/palmier-server/server/src/nats-jwt.ts +299 -0
  31. package/palmier-server/server/src/nats-setup.ts +48 -0
  32. package/palmier-server/server/src/nats.ts +12 -4
  33. package/palmier-server/server/src/routes/device.ts +24 -0
  34. package/palmier-server/server/src/routes/hosts.ts +13 -2
  35. package/palmier-server/spec.md +6 -5
  36. package/src/agents/shared-prompt.ts +1 -1
  37. package/src/commands/init.ts +7 -5
  38. package/src/commands/pair.ts +1 -1
  39. package/src/commands/run.ts +4 -4
  40. package/src/commands/serve.ts +1 -1
  41. package/src/config.ts +2 -2
  42. package/src/device-capabilities.ts +1 -0
  43. package/src/events.ts +1 -1
  44. package/src/mcp-tools.ts +68 -1
  45. package/src/nats-client.ts +10 -3
  46. package/src/types.ts +3 -2
  47. package/test/agent-instructions.test.ts +10 -10
  48. package/dist/pwa/assets/index-uSwkmHBs.js +0 -118
@@ -9,7 +9,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(path.join(__dirname, "agent-
9
9
  * Build the full agent prompt: instructions + endpoint docs + task description.
10
10
  */
11
11
  export function getAgentInstructions(task, skipPermissions) {
12
- const port = loadConfig().httpPort ?? 9966;
12
+ const port = loadConfig().httpPort ?? 7256;
13
13
  const taskDescription = task.frontmatter.user_prompt;
14
14
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
15
15
  .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
@@ -34,7 +34,7 @@ export async function initCommand() {
34
34
  // LAN mode
35
35
  const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
36
36
  const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
37
- let httpPort = 9966;
37
+ let httpPort = 7256;
38
38
  const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
39
39
  const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
40
40
  const parsed = parseInt(portAnswer.trim(), 10);
@@ -98,7 +98,8 @@ export async function initCommand() {
98
98
  projectRoot: process.cwd(),
99
99
  natsUrl: registerResponse.natsUrl,
100
100
  natsWsUrl: registerResponse.natsWsUrl,
101
- natsToken: registerResponse.natsToken,
101
+ natsJwt: registerResponse.natsJwt,
102
+ natsNkeySeed: registerResponse.natsNkeySeed,
102
103
  agents,
103
104
  httpPort,
104
105
  lanEnabled,
@@ -56,7 +56,7 @@ function httpPairRegister(port, code) {
56
56
  export async function pairCommand() {
57
57
  const config = loadConfig();
58
58
  const code = generatePairingCode();
59
- const httpPort = config.httpPort ?? 9966;
59
+ const httpPort = config.httpPort ?? 7256;
60
60
  let paired = false;
61
61
  function onPaired() {
62
62
  paired = true;
@@ -36,7 +36,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
36
36
  const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions);
37
37
  const result = await spawnCommand(command, args, {
38
38
  cwd: getRunDir(ctx.taskDir, ctx.runId),
39
- env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
39
+ env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
40
40
  echoStdout: true,
41
41
  resolveOnFailure: true,
42
42
  stdin,
@@ -258,7 +258,7 @@ async function runCommandTriggeredMode(ctx) {
258
258
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
259
259
  const child = spawnStreamingCommand(commandStr, {
260
260
  cwd: getRunDir(ctx.taskDir, ctx.runId),
261
- env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
261
+ env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
262
262
  });
263
263
  let linesProcessed = 0;
264
264
  let invocationsSucceeded = 0;
@@ -394,7 +394,7 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
394
394
  await publishHostEvent(nc, config.hostId, taskId, payload);
395
395
  }
396
396
  async function requestPermission(config, task, taskDir, requiredPermissions) {
397
- const port = config.httpPort ?? 9966;
397
+ const port = config.httpPort ?? 7256;
398
398
  const res = await fetch(`http://localhost:${port}/request-permission`, {
399
399
  method: "POST",
400
400
  headers: { "Content-Type": "application/json" },
@@ -416,7 +416,7 @@ async function requestPermission(config, task, taskDir, requiredPermissions) {
416
416
  return response;
417
417
  }
418
418
  async function requestConfirmation(config, task, taskDir) {
419
- const port = config.httpPort ?? 9966;
419
+ const port = config.httpPort ?? 7256;
420
420
  const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
421
421
  method: "POST",
422
422
  headers: { "Content-Type": "application/json" },
@@ -113,7 +113,7 @@ export async function serveCommand() {
113
113
  });
114
114
  }, POLL_INTERVAL_MS);
115
115
  const handleRpc = createRpcHandler(config, nc);
116
- const httpPort = config.httpPort ?? 9966;
116
+ const httpPort = config.httpPort ?? 7256;
117
117
  // Start NATS transport (loops forever, fire-and-forget)
118
118
  if (nc) {
119
119
  startNatsTransport(config, handleRpc, nc);
package/dist/config.js CHANGED
@@ -17,8 +17,8 @@ export function loadConfig() {
17
17
  if (!config.hostId) {
18
18
  throw new Error("Invalid host config: missing hostId");
19
19
  }
20
- if (!config.natsUrl || !config.natsToken) {
21
- throw new Error("Invalid host config: missing natsUrl or natsToken");
20
+ if (!config.natsUrl || !config.natsJwt || !config.natsNkeySeed) {
21
+ throw new Error("Invalid host config: missing NATS JWT credentials. Re-run palmier init.");
22
22
  }
23
23
  return config;
24
24
  }
@@ -2,7 +2,7 @@ export interface RegisteredDevice {
2
2
  clientToken: string;
3
3
  fcmToken: string;
4
4
  }
5
- export type DeviceCapability = "location" | "notifications" | "sms" | "contacts" | "calendar" | "alert" | "battery" | "dnd";
5
+ export type DeviceCapability = "location" | "notifications" | "sms" | "contacts" | "calendar" | "alert" | "battery" | "email" | "dnd";
6
6
  export declare function getCapabilityDevice(capability: DeviceCapability): RegisteredDevice | null;
7
7
  export declare function setCapabilityDevice(capability: DeviceCapability, clientToken: string, fcmToken: string): void;
8
8
  export declare function clearCapabilityDevice(capability: DeviceCapability): void;
package/dist/events.js CHANGED
@@ -14,7 +14,7 @@ export async function publishHostEvent(nc, hostId, taskId, payload) {
14
14
  console.log(`[nats] ${subject} →`, payload);
15
15
  }
16
16
  const config = loadConfig();
17
- const port = config.httpPort ?? 9966;
17
+ const port = config.httpPort ?? 7256;
18
18
  try {
19
19
  await fetch(`http://localhost:${port}/event`, {
20
20
  method: "POST",
package/dist/mcp-tools.js CHANGED
@@ -567,7 +567,70 @@ const setRingerModeTool = {
567
567
  return result;
568
568
  },
569
569
  };
570
- export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendAlertTool, readBatteryTool, setRingerModeTool];
570
+ const sendEmailTool = {
571
+ name: "send-email",
572
+ description: [
573
+ "Send an email from the user's mobile device.",
574
+ "When you need to send an email, use this tool. The email app opens on the device with the draft pre-filled for the user to review and send.",
575
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
576
+ ],
577
+ inputSchema: {
578
+ type: "object",
579
+ properties: {
580
+ to: { type: "string", description: "Recipient email address" },
581
+ subject: { type: "string", description: "Email subject" },
582
+ body: { type: "string", description: "Email body text" },
583
+ cc: { type: "string", description: "CC recipient(s)" },
584
+ bcc: { type: "string", description: "BCC recipient(s)" },
585
+ },
586
+ required: ["to"],
587
+ },
588
+ async handler(args, ctx) {
589
+ if (!ctx.nc)
590
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
591
+ const device = getCapabilityDevice("email");
592
+ if (!device)
593
+ throw new ToolError("No device has email access enabled", 400);
594
+ const { to, subject, body, cc, bcc } = args;
595
+ if (!to)
596
+ throw new ToolError("to is required", 400);
597
+ const sc = StringCodec();
598
+ const payload = {
599
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
600
+ to,
601
+ };
602
+ if (subject)
603
+ payload.subject = subject;
604
+ if (body)
605
+ payload.body = body;
606
+ if (cc)
607
+ payload.cc = cc;
608
+ if (bcc)
609
+ payload.bcc = bcc;
610
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.email`, sc.encode(JSON.stringify(payload)), { timeout: 5_000 });
611
+ const ack = JSON.parse(sc.decode(ackReply.data));
612
+ if (ack.error)
613
+ throw new ToolError(ack.error, 502);
614
+ const responsePromise = new Promise((resolve, reject) => {
615
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.email.${ctx.sessionId}`, { max: 1 });
616
+ const timer = setTimeout(() => {
617
+ sub.unsubscribe();
618
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
619
+ }, 30_000);
620
+ (async () => {
621
+ for await (const msg of sub) {
622
+ clearTimeout(timer);
623
+ resolve(sc.decode(msg.data));
624
+ }
625
+ })();
626
+ });
627
+ const result = JSON.parse(await responsePromise);
628
+ if (result.error)
629
+ return { error: result.error };
630
+ return result;
631
+ },
632
+ };
633
+ export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
571
634
  export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
572
635
  const deviceNotificationsResource = {
573
636
  uri: "notifications://device",
@@ -1,7 +1,7 @@
1
1
  import { type NatsConnection } from "nats";
2
2
  import type { HostConfig } from "./types.js";
3
3
  /**
4
- * Connect to NATS using the host config's TCP URL and token auth.
4
+ * Connect to NATS using the host config's JWT credentials.
5
5
  */
6
6
  export declare function connectNats(config: HostConfig): Promise<NatsConnection>;
7
7
  //# sourceMappingURL=nats-client.d.ts.map
@@ -1,11 +1,14 @@
1
- import { connect } from "nats";
1
+ import { connect, jwtAuthenticator } from "nats";
2
2
  /**
3
- * Connect to NATS using the host config's TCP URL and token auth.
3
+ * Connect to NATS using the host config's JWT credentials.
4
4
  */
5
5
  export async function connectNats(config) {
6
+ if (!config.natsJwt || !config.natsNkeySeed) {
7
+ throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
8
+ }
6
9
  const nc = await connect({
7
10
  servers: config.natsUrl,
8
- token: config.natsToken,
11
+ authenticator: jwtAuthenticator(config.natsJwt, new TextEncoder().encode(config.natsNkeySeed)),
9
12
  });
10
13
  // Do not log anything as that will pollute stdout for mcp server.
11
14
  return nc;