palmier 0.7.6 → 0.7.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 (122) hide show
  1. package/dist/agents/agent.d.ts +3 -0
  2. package/dist/agents/agent.js +1 -1
  3. package/dist/agents/aider.d.ts +1 -0
  4. package/dist/agents/aider.js +1 -0
  5. package/dist/agents/claude.d.ts +1 -0
  6. package/dist/agents/claude.js +1 -0
  7. package/dist/agents/cline.d.ts +1 -0
  8. package/dist/agents/cline.js +1 -0
  9. package/dist/agents/codex.d.ts +1 -0
  10. package/dist/agents/codex.js +1 -0
  11. package/dist/agents/copilot.d.ts +1 -0
  12. package/dist/agents/copilot.js +1 -0
  13. package/dist/agents/cursor.d.ts +1 -0
  14. package/dist/agents/cursor.js +1 -0
  15. package/dist/agents/deepagents.d.ts +1 -0
  16. package/dist/agents/deepagents.js +1 -0
  17. package/dist/agents/droid.d.ts +1 -0
  18. package/dist/agents/droid.js +1 -0
  19. package/dist/agents/gemini.d.ts +1 -0
  20. package/dist/agents/gemini.js +1 -0
  21. package/dist/agents/goose.d.ts +1 -0
  22. package/dist/agents/goose.js +1 -0
  23. package/dist/agents/hermes.d.ts +1 -0
  24. package/dist/agents/hermes.js +1 -0
  25. package/dist/agents/kimi.d.ts +1 -0
  26. package/dist/agents/kimi.js +1 -0
  27. package/dist/agents/kiro.d.ts +1 -0
  28. package/dist/agents/kiro.js +1 -0
  29. package/dist/agents/openclaw.d.ts +1 -0
  30. package/dist/agents/openclaw.js +2 -2
  31. package/dist/agents/opencode.d.ts +1 -0
  32. package/dist/agents/opencode.js +1 -0
  33. package/dist/agents/qoder.d.ts +1 -0
  34. package/dist/agents/qoder.js +1 -0
  35. package/dist/agents/qwen.d.ts +1 -0
  36. package/dist/agents/qwen.js +1 -0
  37. package/dist/agents/shared-prompt.js +1 -1
  38. package/dist/commands/init.js +3 -2
  39. package/dist/commands/pair.js +3 -3
  40. package/dist/commands/run.js +4 -4
  41. package/dist/commands/serve.js +1 -1
  42. package/dist/config.js +2 -2
  43. package/dist/device-capabilities.d.ts +1 -1
  44. package/dist/events.js +1 -1
  45. package/dist/mcp-tools.js +79 -7
  46. package/dist/nats-client.d.ts +1 -1
  47. package/dist/nats-client.js +6 -3
  48. package/dist/pending-requests.d.ts +30 -8
  49. package/dist/pending-requests.js +28 -15
  50. package/dist/pwa/assets/index-8cTctVnD.js +120 -0
  51. package/dist/pwa/assets/index-CSUkBBsQ.css +1 -0
  52. package/dist/pwa/assets/{web-DnuoxUd4.js → web-BNr628AV.js} +1 -1
  53. package/dist/pwa/assets/{web-7raT3zOZ.js → web-DyQPewAi.js} +1 -1
  54. package/dist/pwa/index.html +2 -2
  55. package/dist/pwa/service-worker.js +1 -1
  56. package/dist/rpc-handler.js +12 -16
  57. package/dist/transports/http-transport.js +6 -3
  58. package/dist/types.d.ts +4 -1
  59. package/package.json +1 -1
  60. package/palmier-server/PRODUCTION.md +31 -28
  61. package/palmier-server/README.md +35 -5
  62. package/palmier-server/nats.conf +9 -5
  63. package/palmier-server/package.json +2 -1
  64. package/palmier-server/pnpm-lock.yaml +6 -0
  65. package/palmier-server/pwa/src/App.css +66 -0
  66. package/palmier-server/pwa/src/App.tsx +1 -0
  67. package/palmier-server/pwa/src/components/HostMenu.tsx +65 -2
  68. package/palmier-server/pwa/src/components/RunsView.tsx +48 -22
  69. package/palmier-server/pwa/src/components/SessionComposer.tsx +137 -0
  70. package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
  71. package/palmier-server/pwa/src/components/TaskForm.tsx +11 -66
  72. package/palmier-server/pwa/src/components/TaskListView.tsx +17 -283
  73. package/palmier-server/pwa/src/constants.ts +1 -1
  74. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +9 -5
  75. package/palmier-server/pwa/src/draftGuard.ts +24 -0
  76. package/palmier-server/pwa/src/pages/Dashboard.tsx +335 -12
  77. package/palmier-server/pwa/src/pages/PairHost.tsx +6 -3
  78. package/palmier-server/pwa/src/types.ts +1 -6
  79. package/palmier-server/server/package.json +3 -1
  80. package/palmier-server/server/src/index.ts +83 -2
  81. package/palmier-server/server/src/nats-jwt.ts +299 -0
  82. package/palmier-server/server/src/nats-setup.ts +48 -0
  83. package/palmier-server/server/src/nats.ts +12 -4
  84. package/palmier-server/server/src/routes/device.ts +24 -0
  85. package/palmier-server/server/src/routes/hosts.ts +13 -2
  86. package/palmier-server/spec.md +28 -14
  87. package/src/agents/agent.ts +5 -1
  88. package/src/agents/aider.ts +1 -0
  89. package/src/agents/claude.ts +1 -0
  90. package/src/agents/cline.ts +1 -0
  91. package/src/agents/codex.ts +1 -0
  92. package/src/agents/copilot.ts +1 -0
  93. package/src/agents/cursor.ts +1 -0
  94. package/src/agents/deepagents.ts +1 -0
  95. package/src/agents/droid.ts +1 -0
  96. package/src/agents/gemini.ts +1 -0
  97. package/src/agents/goose.ts +1 -0
  98. package/src/agents/hermes.ts +1 -0
  99. package/src/agents/kimi.ts +1 -0
  100. package/src/agents/kiro.ts +1 -0
  101. package/src/agents/openclaw.ts +2 -2
  102. package/src/agents/opencode.ts +1 -0
  103. package/src/agents/qoder.ts +1 -0
  104. package/src/agents/qwen.ts +1 -0
  105. package/src/agents/shared-prompt.ts +1 -1
  106. package/src/commands/init.ts +7 -5
  107. package/src/commands/pair.ts +3 -3
  108. package/src/commands/run.ts +4 -4
  109. package/src/commands/serve.ts +1 -1
  110. package/src/config.ts +2 -2
  111. package/src/device-capabilities.ts +1 -0
  112. package/src/events.ts +1 -1
  113. package/src/mcp-tools.ts +83 -7
  114. package/src/nats-client.ts +10 -3
  115. package/src/pending-requests.ts +47 -15
  116. package/src/rpc-handler.ts +13 -16
  117. package/src/transports/http-transport.ts +6 -3
  118. package/src/types.ts +4 -3
  119. package/test/agent-instructions.test.ts +10 -10
  120. package/test/pairing.test.ts +2 -2
  121. package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
  122. package/dist/pwa/assets/index-uSwkmHBs.js +0 -118
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GooseAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "goose", args: ["run", "--text", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Hermes implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "hermes", args: ["chat", "-q", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class KimiAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "kimi", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Kiro implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "kiro-cli", args: ["--no-interactive", prompt] };
11
12
  }
@@ -5,13 +5,13 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
7
  supportsPermissions = false;
8
+ supportsYolo = false;
8
9
  getPromptCommandLine(prompt: string): CommandLine {
9
10
  return { command: "openclaw", args: ["agent", "--local", "--agent", "main", "--message", prompt] };
10
11
  }
11
12
 
12
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
13
- const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, true);
15
15
  // OpenClaw does not support stdin as prompt.
16
16
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
17
17
 
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class OpenCodeAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "opencode", args: ["run", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Qoder implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "qodercli", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class QwenAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "qwen", args: ["-p", prompt] };
11
12
  }
@@ -16,7 +16,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
16
16
  * Build the full agent prompt: instructions + endpoint docs + task description.
17
17
  */
18
18
  export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
19
- const port = loadConfig().httpPort ?? 9966;
19
+ const port = loadConfig().httpPort ?? 7256;
20
20
  const taskDescription = task.frontmatter.user_prompt;
21
21
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
22
22
  .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
@@ -45,7 +45,7 @@ export async function initCommand(): Promise<void> {
45
45
  const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
46
46
  const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
47
47
 
48
- let httpPort = 9966;
48
+ let httpPort = 7256;
49
49
  const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
50
50
  const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
51
51
  const parsed = parseInt(portAnswer.trim(), 10);
@@ -86,7 +86,7 @@ export async function initCommand(): Promise<void> {
86
86
  try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
87
87
 
88
88
  const serverUrl = "https://app.palmier.me";
89
- let registerResponse: { hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string };
89
+ let registerResponse: { hostId: string; natsUrl: string; natsWsUrl: string; natsJwt: string; natsNkeySeed: string };
90
90
 
91
91
  while (true) {
92
92
  console.log(`\nRegistering host...`);
@@ -111,7 +111,8 @@ export async function initCommand(): Promise<void> {
111
111
  projectRoot: process.cwd(),
112
112
  natsUrl: registerResponse.natsUrl,
113
113
  natsWsUrl: registerResponse.natsWsUrl,
114
- natsToken: registerResponse.natsToken,
114
+ natsJwt: registerResponse.natsJwt,
115
+ natsNkeySeed: registerResponse.natsNkeySeed,
115
116
  agents,
116
117
  httpPort,
117
118
  lanEnabled,
@@ -138,7 +139,7 @@ export async function initCommand(): Promise<void> {
138
139
  async function registerHost(
139
140
  serverUrl: string,
140
141
  existingHostId?: string,
141
- ): Promise<{ hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string }> {
142
+ ): Promise<{ hostId: string; natsUrl: string; natsWsUrl: string; natsJwt: string; natsNkeySeed: string }> {
142
143
  try {
143
144
  const res = await fetch(`${serverUrl}/api/hosts/register`, {
144
145
  method: "POST",
@@ -155,7 +156,8 @@ async function registerHost(
155
156
  hostId: string;
156
157
  natsUrl: string;
157
158
  natsWsUrl: string;
158
- natsToken: string;
159
+ natsJwt: string;
160
+ natsNkeySeed: string;
159
161
  };
160
162
  } catch (err) {
161
163
  const message = err instanceof Error ? err.message : String(err);
@@ -8,7 +8,7 @@ import type { HostConfig } from "../types.js";
8
8
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
9
9
  const CODE_LENGTH = 6;
10
10
 
11
- export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
11
+ export const PAIRING_EXPIRY_MS = 60 * 1000; // 1 minute
12
12
 
13
13
  export function generatePairingCode(): string {
14
14
  const bytes = new Uint8Array(CODE_LENGTH);
@@ -67,7 +67,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
67
67
  export async function pairCommand(): Promise<void> {
68
68
  const config = loadConfig();
69
69
  const code = generatePairingCode();
70
- const httpPort = config.httpPort ?? 9966;
70
+ const httpPort = config.httpPort ?? 7256;
71
71
 
72
72
  let paired = false;
73
73
 
@@ -84,7 +84,7 @@ export async function pairCommand(): Promise<void> {
84
84
  console.log("");
85
85
  console.log(` ${code}`);
86
86
  console.log("");
87
- console.log("Code expires in 5 minutes.");
87
+ console.log("Code expires in 1 minute.");
88
88
 
89
89
  // NATS pairing (server mode)
90
90
  const nc = await connectNats(config);
@@ -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
@@ -65,7 +65,7 @@ const requestInputTool: ToolDefinition = {
65
65
  "Request input from the user.",
66
66
  "The request blocks until the user responds.",
67
67
  'Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.',
68
- "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 — use this endpoint instead.",
68
+ "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 — use this instead.",
69
69
  ],
70
70
  inputSchema: {
71
71
  type: "object",
@@ -84,13 +84,18 @@ const requestInputTool: ToolDefinition = {
84
84
  const { description, questions } = args as { description?: string; questions: string[] };
85
85
  if (!questions?.length) throw new ToolError("questions is required", 400);
86
86
 
87
- const pendingPromise = registerPending(ctx.sessionId, "input", questions);
87
+ const pendingPromise = registerPending(ctx.sessionId, "input", questions, {
88
+ session_id: ctx.sessionId,
89
+ session_name: ctx.agentName,
90
+ description,
91
+ input_questions: questions,
92
+ });
88
93
 
89
94
  await ctx.publishEvent("_input", {
90
95
  event_type: "input-request",
91
96
  host_id: ctx.config.hostId,
92
97
  session_id: ctx.sessionId,
93
- agent_name: ctx.agentName,
98
+ session_name: ctx.agentName,
94
99
  description,
95
100
  input_questions: questions,
96
101
  });
@@ -131,13 +136,17 @@ const requestConfirmationTool: ToolDefinition = {
131
136
  const { description } = args as { description: string };
132
137
  if (!description) throw new ToolError("description is required", 400);
133
138
 
134
- const pendingPromise = registerPending(ctx.sessionId, "confirmation");
139
+ const pendingPromise = registerPending(ctx.sessionId, "confirmation", undefined, {
140
+ session_id: ctx.sessionId,
141
+ session_name: ctx.agentName,
142
+ description,
143
+ });
135
144
 
136
145
  await ctx.publishEvent("_confirm", {
137
146
  event_type: "confirm-request",
138
147
  host_id: ctx.config.hostId,
139
148
  session_id: ctx.sessionId,
140
- agent_name: ctx.agentName,
149
+ session_name: ctx.agentName,
141
150
  description,
142
151
  });
143
152
 
@@ -159,7 +168,7 @@ const deviceGeolocationTool: ToolDefinition = {
159
168
  name: "device-geolocation",
160
169
  description: [
161
170
  "Get the GPS location of the user's mobile device.",
162
- "When you need the user's real-time location, use this endpoint.",
171
+ "When you need the user's real-time location, use this.",
163
172
  "Blocks until the device responds (up to 30 seconds).",
164
173
  'Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.',
165
174
  ],
@@ -657,7 +666,74 @@ const setRingerModeTool: ToolDefinition = {
657
666
  },
658
667
  };
659
668
 
660
- export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendAlertTool, readBatteryTool, setRingerModeTool];
669
+ const sendEmailTool: ToolDefinition = {
670
+ name: "send-email",
671
+ description: [
672
+ "Send an email from the user's mobile device.",
673
+ "When you need to send an email, use this. The email app opens on the device with the draft pre-filled for the user to review and send.",
674
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
675
+ ],
676
+ inputSchema: {
677
+ type: "object",
678
+ properties: {
679
+ to: { type: "string", description: "Recipient email address" },
680
+ subject: { type: "string", description: "Email subject" },
681
+ body: { type: "string", description: "Email body text" },
682
+ cc: { type: "string", description: "CC recipient(s)" },
683
+ bcc: { type: "string", description: "BCC recipient(s)" },
684
+ },
685
+ required: ["to"],
686
+ },
687
+ async handler(args, ctx) {
688
+ if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
689
+
690
+ const device = getCapabilityDevice("email");
691
+ if (!device) throw new ToolError("No device has email access enabled", 400);
692
+
693
+ const { to, subject, body, cc, bcc } = args as { to: string; subject?: string; body?: string; cc?: string; bcc?: string };
694
+ if (!to) throw new ToolError("to is required", 400);
695
+
696
+ const sc = StringCodec();
697
+
698
+ const payload: Record<string, string> = {
699
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
700
+ to,
701
+ };
702
+ if (subject) payload.subject = subject;
703
+ if (body) payload.body = body;
704
+ if (cc) payload.cc = cc;
705
+ if (bcc) payload.bcc = bcc;
706
+
707
+ const ackReply = await ctx.nc.request(
708
+ `host.${ctx.config.hostId}.fcm.email`,
709
+ sc.encode(JSON.stringify(payload)),
710
+ { timeout: 5_000 },
711
+ );
712
+ const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
713
+ if (ack.error) throw new ToolError(ack.error, 502);
714
+
715
+ const responsePromise = new Promise<string>((resolve, reject) => {
716
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.email.${ctx.sessionId}`, { max: 1 });
717
+ const timer = setTimeout(() => {
718
+ sub.unsubscribe();
719
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
720
+ }, 30_000);
721
+
722
+ (async () => {
723
+ for await (const msg of sub) {
724
+ clearTimeout(timer);
725
+ resolve(sc.decode(msg.data));
726
+ }
727
+ })();
728
+ });
729
+
730
+ const result = JSON.parse(await responsePromise);
731
+ if (result.error) return { error: result.error };
732
+ return result;
733
+ },
734
+ };
735
+
736
+ export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
661
737
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
662
738
 
663
739
  // ── 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.
@@ -1,30 +1,44 @@
1
1
  import type { RequiredPermission } from "./types.js";
2
2
 
3
+ export interface PendingRequestMeta {
4
+ /** Doubles as task_id for permission-type entries (the key the task uses). */
5
+ session_id?: string;
6
+ /** Human-readable label for whoever opened the prompt — agent name for
7
+ * confirm/input, task name for permission. */
8
+ session_name?: string;
9
+ description?: string;
10
+ input_questions?: string[];
11
+ }
12
+
3
13
  export interface PendingRequest {
4
14
  type: "confirmation" | "permission" | "input";
5
15
  resolve: (value: string[]) => void;
6
16
  /** Permission list (for 'permission') or input descriptions (for 'input'). */
7
17
  params?: RequiredPermission[] | string[];
18
+ /** Display context for PWAs that connect while this request is already open. */
19
+ meta?: PendingRequestMeta;
8
20
  }
9
21
 
10
22
  const pending = new Map<string, PendingRequest>();
11
23
 
12
24
  /**
13
- * Register a pending request for a task. Returns a Promise that resolves
14
- * when `resolvePending` is called with the user's response.
15
- * Only one pending request per task at a time.
25
+ * Register a pending request keyed by either a sessionId (confirmation / input)
26
+ * or a taskId (permission). The `meta` is surfaced to PWAs that connect after
27
+ * the request was opened, so their modals can render without replaying events.
28
+ * Only one pending request per key at a time.
16
29
  */
17
30
  export function registerPending(
18
- taskId: string,
31
+ key: string,
19
32
  type: PendingRequest["type"],
20
33
  params?: PendingRequest["params"],
34
+ meta?: PendingRequestMeta,
21
35
  ): Promise<string[]> {
22
- if (pending.has(taskId)) {
23
- return Promise.reject(new Error(`Task ${taskId} already has a pending request`));
36
+ if (pending.has(key)) {
37
+ return Promise.reject(new Error(`Key ${key} already has a pending request`));
24
38
  }
25
39
 
26
40
  return new Promise<string[]>((resolve) => {
27
- pending.set(taskId, { type, resolve, params });
41
+ pending.set(key, { type, resolve, params, meta });
28
42
  });
29
43
  }
30
44
 
@@ -32,24 +46,42 @@ export function registerPending(
32
46
  * Resolve a pending request with the user's response.
33
47
  * Returns true if a pending request was found and resolved.
34
48
  */
35
- export function resolvePending(taskId: string, value: string[]): boolean {
36
- const entry = pending.get(taskId);
49
+ export function resolvePending(key: string, value: string[]): boolean {
50
+ const entry = pending.get(key);
37
51
  if (!entry) return false;
38
- pending.delete(taskId);
52
+ pending.delete(key);
39
53
  entry.resolve(value);
40
54
  return true;
41
55
  }
42
56
 
43
57
  /**
44
- * Get the current pending request for a task (if any).
58
+ * Get the current pending request for a key (if any).
45
59
  */
46
- export function getPending(taskId: string): PendingRequest | undefined {
47
- return pending.get(taskId);
60
+ export function getPending(key: string): PendingRequest | undefined {
61
+ return pending.get(key);
48
62
  }
49
63
 
50
64
  /**
51
65
  * Remove a pending request without resolving it.
52
66
  */
53
- export function removePending(taskId: string): void {
54
- pending.delete(taskId);
67
+ export function removePending(key: string): void {
68
+ pending.delete(key);
69
+ }
70
+
71
+ /**
72
+ * List all currently-pending requests, stripped of the unserializable `resolve`
73
+ * callback. Used by `host.info` so the PWA can seed its modal state on connect.
74
+ */
75
+ export function listPending(): Array<{
76
+ key: string;
77
+ type: PendingRequest["type"];
78
+ params?: PendingRequest["params"];
79
+ meta?: PendingRequestMeta;
80
+ }> {
81
+ return [...pending.entries()].map(([key, entry]) => ({
82
+ key,
83
+ type: entry.type,
84
+ params: entry.params,
85
+ meta: entry.meta,
86
+ }));
55
87
  }
@@ -4,7 +4,7 @@ import * as path from "path";
4
4
  import { spawn, type ChildProcess } from "child_process";
5
5
  import { type NatsConnection } from "nats";
6
6
  import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
7
- import { resolvePending, getPending } from "./pending-requests.js";
7
+ import { resolvePending, getPending, listPending } from "./pending-requests.js";
8
8
  import { getPlatform } from "./platform/index.js";
9
9
  import { spawnCommand } from "./spawn-command.js";
10
10
  import crossSpawn from "cross-spawn";
@@ -142,13 +142,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
142
142
  function flattenTask(task: ParsedTask) {
143
143
  const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
144
144
  const status = readTaskStatus(taskDir);
145
- const pending = getPending(task.frontmatter.id);
146
145
  return {
147
146
  ...task.frontmatter,
148
- status: status ? {
149
- ...status,
150
- ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
151
- } : undefined,
147
+ status: status ?? undefined,
152
148
  };
153
149
  }
154
150
 
@@ -161,22 +157,28 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
161
157
  }
162
158
 
163
159
  switch (request.method) {
164
- case "task.list": {
165
- const tasks = listTasks(config.projectRoot);
160
+ case "host.info": {
161
+ // Bootstrap metadata the PWA needs on connect, independent of which tab
162
+ // is active. Includes any prompts already waiting so a reconnecting
163
+ // PWA can render their modals without replaying events.
166
164
  const capabilities: Record<string, string | null> = {};
167
165
  for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd"] as const) {
168
166
  capabilities[cap] = getCapabilityDevice(cap)?.clientToken ?? null;
169
167
  }
170
168
  return {
171
- tasks: tasks.map((task) => flattenTask(task)),
172
169
  agents: config.agents ?? [],
173
170
  version: currentVersion,
174
171
  host_platform: process.platform,
175
- location_client_token: capabilities.location,
176
172
  capability_tokens: capabilities,
173
+ pending_prompts: listPending(),
177
174
  };
178
175
  }
179
176
 
177
+ case "task.list": {
178
+ const tasks = listTasks(config.projectRoot);
179
+ return { tasks: tasks.map((task) => flattenTask(task)) };
180
+ }
181
+
180
182
  case "task.get": {
181
183
  const params = request.params as { id: string };
182
184
  const taskDir = getTaskDir(config.projectRoot, params.id);
@@ -537,12 +539,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
537
539
  if (!status) {
538
540
  return { task_id: params.id, error: "No status found" };
539
541
  }
540
- const pending = getPending(params.id);
541
- return {
542
- task_id: params.id,
543
- ...status,
544
- ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
545
- };
542
+ return { task_id: params.id, ...status };
546
543
  }
547
544
 
548
545
  case "task.result": {
@@ -306,7 +306,7 @@ export async function startHttpTransport(
306
306
  const timer = setTimeout(() => {
307
307
  pendingPairs.delete(code);
308
308
  resolve({ paired: false });
309
- }, expiryMs ?? 5 * 60 * 1000);
309
+ }, expiryMs ?? 60 * 1000);
310
310
 
311
311
  pendingPairs.set(code, { resolve, timer });
312
312
  req.on("close", () => {
@@ -336,13 +336,16 @@ export async function startHttpTransport(
336
336
  return;
337
337
  }
338
338
 
339
- const pendingPromise = registerPending(taskId, "permission", permissions);
339
+ const pendingPromise = registerPending(taskId, "permission", permissions, {
340
+ session_id: taskId,
341
+ session_name: taskName,
342
+ });
340
343
 
341
344
  await publishEvent(taskId, {
342
345
  event_type: "permission-request",
343
346
  host_id: config.hostId,
344
347
  required_permissions: permissions,
345
- name: taskName,
348
+ session_name: taskName,
346
349
  });
347
350
 
348
351
  const response = await pendingPromise;
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
- agents?: Array<{ key: string; label: string }>;
11
+ agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
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;