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
@@ -67,7 +67,7 @@ async function invokeAgentWithRetries(
67
67
  );
68
68
  const result = await spawnCommand(command, args, {
69
69
  cwd: getRunDir(ctx.taskDir, ctx.runId),
70
- env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
70
+ env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
71
71
  echoStdout: true,
72
72
  resolveOnFailure: true,
73
73
  stdin,
@@ -321,7 +321,7 @@ async function runCommandTriggeredMode(
321
321
 
322
322
  const child = spawnStreamingCommand(commandStr, {
323
323
  cwd: getRunDir(ctx.taskDir, ctx.runId),
324
- env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
324
+ env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
325
325
  });
326
326
 
327
327
  let linesProcessed = 0;
@@ -483,7 +483,7 @@ async function requestPermission(
483
483
  taskDir: string,
484
484
  requiredPermissions: RequiredPermission[],
485
485
  ): Promise<"granted" | "granted_all" | "aborted"> {
486
- const port = config.httpPort ?? 9966;
486
+ const port = config.httpPort ?? 7256;
487
487
  const res = await fetch(`http://localhost:${port}/request-permission`, {
488
488
  method: "POST",
489
489
  headers: { "Content-Type": "application/json" },
@@ -511,7 +511,7 @@ async function requestConfirmation(
511
511
  task: ParsedTask,
512
512
  taskDir: string,
513
513
  ): Promise<boolean> {
514
- const port = config.httpPort ?? 9966;
514
+ const port = config.httpPort ?? 7256;
515
515
  const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
516
516
  method: "POST",
517
517
  headers: { "Content-Type": "application/json" },
@@ -127,7 +127,7 @@ export async function serveCommand(): Promise<void> {
127
127
  }, POLL_INTERVAL_MS);
128
128
 
129
129
  const handleRpc = createRpcHandler(config, nc);
130
- const httpPort = config.httpPort ?? 9966;
130
+ const httpPort = config.httpPort ?? 7256;
131
131
 
132
132
  // Start NATS transport (loops forever, fire-and-forget)
133
133
  if (nc) {
package/src/config.ts CHANGED
@@ -25,8 +25,8 @@ export function loadConfig(): HostConfig {
25
25
  throw new Error("Invalid host config: missing hostId");
26
26
  }
27
27
 
28
- if (!config.natsUrl || !config.natsToken) {
29
- throw new Error("Invalid host config: missing natsUrl or natsToken");
28
+ if (!config.natsUrl || !config.natsJwt || !config.natsNkeySeed) {
29
+ throw new Error("Invalid host config: missing NATS JWT credentials. Re-run palmier init.");
30
30
  }
31
31
 
32
32
  return config;
@@ -17,6 +17,7 @@ export type DeviceCapability =
17
17
  | "calendar"
18
18
  | "alert"
19
19
  | "battery"
20
+ | "email"
20
21
  | "dnd";
21
22
 
22
23
  type CapabilityMap = Partial<Record<DeviceCapability, RegisteredDevice>>;
package/src/events.ts CHANGED
@@ -23,7 +23,7 @@ export async function publishHostEvent(
23
23
  }
24
24
 
25
25
  const config = loadConfig();
26
- const port = config.httpPort ?? 9966;
26
+ const port = config.httpPort ?? 7256;
27
27
  try {
28
28
  await fetch(`http://localhost:${port}/event`, {
29
29
  method: "POST",
package/src/mcp-tools.ts CHANGED
@@ -657,7 +657,74 @@ const setRingerModeTool: ToolDefinition = {
657
657
  },
658
658
  };
659
659
 
660
- export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendAlertTool, readBatteryTool, setRingerModeTool];
660
+ const sendEmailTool: ToolDefinition = {
661
+ name: "send-email",
662
+ description: [
663
+ "Send an email from the user's mobile device.",
664
+ "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.",
665
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
666
+ ],
667
+ inputSchema: {
668
+ type: "object",
669
+ properties: {
670
+ to: { type: "string", description: "Recipient email address" },
671
+ subject: { type: "string", description: "Email subject" },
672
+ body: { type: "string", description: "Email body text" },
673
+ cc: { type: "string", description: "CC recipient(s)" },
674
+ bcc: { type: "string", description: "BCC recipient(s)" },
675
+ },
676
+ required: ["to"],
677
+ },
678
+ async handler(args, ctx) {
679
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
680
+
681
+ const device = getCapabilityDevice("email");
682
+ if (!device) throw new ToolError("No device has email access enabled", 400);
683
+
684
+ const { to, subject, body, cc, bcc } = args as { to: string; subject?: string; body?: string; cc?: string; bcc?: string };
685
+ if (!to) throw new ToolError("to is required", 400);
686
+
687
+ const sc = StringCodec();
688
+
689
+ const payload: Record<string, string> = {
690
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
691
+ to,
692
+ };
693
+ if (subject) payload.subject = subject;
694
+ if (body) payload.body = body;
695
+ if (cc) payload.cc = cc;
696
+ if (bcc) payload.bcc = bcc;
697
+
698
+ const ackReply = await ctx.nc.request(
699
+ `host.${ctx.config.hostId}.fcm.email`,
700
+ sc.encode(JSON.stringify(payload)),
701
+ { timeout: 5_000 },
702
+ );
703
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
704
+ if (ack.error) throw new ToolError(ack.error, 502);
705
+
706
+ const responsePromise = new Promise<string>((resolve, reject) => {
707
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.email.${ctx.sessionId}`, { max: 1 });
708
+ const timer = setTimeout(() => {
709
+ sub.unsubscribe();
710
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
711
+ }, 30_000);
712
+
713
+ (async () => {
714
+ for await (const msg of sub) {
715
+ clearTimeout(timer);
716
+ resolve(sc.decode(msg.data));
717
+ }
718
+ })();
719
+ });
720
+
721
+ const result = JSON.parse(await responsePromise);
722
+ if (result.error) return { error: result.error };
723
+ return result;
724
+ },
725
+ };
726
+
727
+ export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
661
728
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
662
729
 
663
730
  // ── MCP Resources ─────────────────────────────────────────────────────
@@ -1,13 +1,20 @@
1
- import { connect, type NatsConnection } from "nats";
1
+ import { connect, jwtAuthenticator, type NatsConnection } from "nats";
2
2
  import type { HostConfig } from "./types.js";
3
3
 
4
4
  /**
5
- * Connect to NATS using the host config's TCP URL and token auth.
5
+ * Connect to NATS using the host config's JWT credentials.
6
6
  */
7
7
  export async function connectNats(config: HostConfig): Promise<NatsConnection> {
8
+ if (!config.natsJwt || !config.natsNkeySeed) {
9
+ throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
10
+ }
11
+
8
12
  const nc = await connect({
9
13
  servers: config.natsUrl,
10
- token: config.natsToken,
14
+ authenticator: jwtAuthenticator(
15
+ config.natsJwt,
16
+ new TextEncoder().encode(config.natsNkeySeed),
17
+ ),
11
18
  });
12
19
 
13
20
  // Do not log anything as that will pollute stdout for mcp server.
package/src/types.ts CHANGED
@@ -4,12 +4,13 @@ export interface HostConfig {
4
4
 
5
5
  natsUrl?: string;
6
6
  natsWsUrl?: string;
7
- natsToken?: string;
7
+ natsJwt?: string;
8
+ natsNkeySeed?: string;
8
9
 
9
10
  // Detected agent CLIs
10
11
  agents?: Array<{ key: string; label: string }>;
11
12
 
12
- // HTTP server port (default 9966)
13
+ // HTTP server port (default 7256)
13
14
  httpPort?: number;
14
15
  // Whether to accept non-localhost HTTP connections
15
16
  lanEnabled?: boolean;
@@ -66,7 +66,7 @@ const mockResources: ResourceDefinition[] = [
66
66
  /** Minimal replica of getAgentInstructions that doesn't need host.json */
67
67
  function buildInstructions(taskId: string, opts?: { skipPermissions?: boolean }): string {
68
68
  let instructions = template
69
- .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(9966, taskId, mockTools, mockResources))
69
+ .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(7256, taskId, mockTools, mockResources))
70
70
  .replace(/\{\{TASK_DESCRIPTION\}\}/g, "Test task prompt");
71
71
  if (opts?.skipPermissions) {
72
72
  instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
@@ -107,7 +107,7 @@ describe("getAgentInstructions", () => {
107
107
 
108
108
  it("includes port in endpoint URL", () => {
109
109
  const result = buildInstructions("test");
110
- assert.match(result, /localhost:9966/);
110
+ assert.match(result, /localhost:7256/);
111
111
  });
112
112
 
113
113
  it("includes task description", () => {
@@ -119,13 +119,13 @@ describe("getAgentInstructions", () => {
119
119
 
120
120
 
121
121
  describe("generateEndpointDocs", () => {
122
- const docs = generateEndpointDocs(9966, "test-id", mockTools, mockResources);
122
+ const docs = generateEndpointDocs(7256, "test-id", mockTools, mockResources);
123
123
 
124
124
  it("matches expected full output", () => {
125
125
  const expected = [
126
126
  "The following HTTP endpoints are available during task execution. Use curl to call them.",
127
127
  "",
128
- "**`POST http://localhost:9966/mock-action?taskId=test-id`** — Perform a mock action.",
128
+ "**`POST http://localhost:7256/mock-action?taskId=test-id`** — Perform a mock action.",
129
129
  "```json",
130
130
  '{"title":"...","detail":"..."}',
131
131
  "```",
@@ -133,7 +133,7 @@ describe("generateEndpointDocs", () => {
133
133
  "- `detail` (optional, string): Optional detail",
134
134
  '- Response: `{"ok": true}` on success.',
135
135
  "",
136
- "**`POST http://localhost:9966/mock-query?taskId=test-id`** — Query mock data from the device.",
136
+ "**`POST http://localhost:7256/mock-query?taskId=test-id`** — Query mock data from the device.",
137
137
  "```json",
138
138
  '{"tags":["..."]}',
139
139
  "```",
@@ -141,7 +141,7 @@ describe("generateEndpointDocs", () => {
141
141
  "- Blocks until the device responds.",
142
142
  '- Response: `{"data": ...}` on success.',
143
143
  "",
144
- "**`GET http://localhost:9966/mock-data?taskId=test-id`** — Get mock data from the device.",
144
+ "**`GET http://localhost:7256/mock-data?taskId=test-id`** — Get mock data from the device.",
145
145
  "- Response: JSON array of data objects.",
146
146
  ].join("\n");
147
147
  assert.equal(docs, expected);
@@ -149,7 +149,7 @@ describe("generateEndpointDocs", () => {
149
149
 
150
150
  it("generates docs for all provided tools", () => {
151
151
  for (const tool of mockTools) {
152
- assert.match(docs, new RegExp(`POST http://localhost:9966/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
152
+ assert.match(docs, new RegExp(`POST http://localhost:7256/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
153
153
  }
154
154
  });
155
155
 
@@ -162,7 +162,7 @@ describe("generateEndpointDocs", () => {
162
162
  });
163
163
 
164
164
  it("includes port in the header", () => {
165
- assert.match(docs, /localhost:9966/);
165
+ assert.match(docs, /localhost:7256/);
166
166
  });
167
167
 
168
168
  it("includes task ID in query parameters", () => {
@@ -194,7 +194,7 @@ describe("generateEndpointDocs", () => {
194
194
 
195
195
  it("generates GET endpoints for all provided resources", () => {
196
196
  for (const resource of mockResources) {
197
- assert.match(docs, new RegExp(`GET http://localhost:9966${resource.restPath}`), `Missing endpoint for ${resource.uri}`);
197
+ assert.match(docs, new RegExp(`GET http://localhost:7256${resource.restPath}`), `Missing endpoint for ${resource.uri}`);
198
198
  }
199
199
  });
200
200
 
@@ -203,7 +203,7 @@ describe("generateEndpointDocs", () => {
203
203
  });
204
204
 
205
205
  it("generates no resource endpoints when resources array is empty", () => {
206
- const docsNoResources = generateEndpointDocs(9966, "test-id", mockTools, []);
206
+ const docsNoResources = generateEndpointDocs(7256, "test-id", mockTools, []);
207
207
  assert.doesNotMatch(docsNoResources, /GET http/);
208
208
  });
209
209
  });