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.
Files changed (96) hide show
  1. package/README.md +15 -1
  2. package/dist/agents/agent-instructions.md +6 -14
  3. package/dist/agents/aider.js +1 -1
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/cline.js +1 -1
  6. package/dist/agents/codex.js +1 -1
  7. package/dist/agents/copilot.js +1 -1
  8. package/dist/agents/cursor.js +1 -1
  9. package/dist/agents/deepagents.js +1 -1
  10. package/dist/agents/droid.js +1 -1
  11. package/dist/agents/gemini.js +1 -1
  12. package/dist/agents/goose.js +1 -1
  13. package/dist/agents/hermes.js +1 -1
  14. package/dist/agents/kimi.js +1 -1
  15. package/dist/agents/kiro.js +1 -1
  16. package/dist/agents/openclaw.js +1 -1
  17. package/dist/agents/opencode.js +1 -1
  18. package/dist/agents/qoder.js +1 -1
  19. package/dist/agents/qwen.js +1 -1
  20. package/dist/agents/shared-prompt.d.ts +3 -2
  21. package/dist/agents/shared-prompt.js +6 -4
  22. package/dist/commands/plan-generation.md +1 -0
  23. package/dist/commands/run.js +4 -7
  24. package/dist/location-device.d.ts +8 -0
  25. package/dist/location-device.js +32 -0
  26. package/dist/mcp-handler.d.ts +8 -0
  27. package/dist/mcp-handler.js +110 -0
  28. package/dist/mcp-tools.d.ts +27 -0
  29. package/dist/mcp-tools.js +218 -0
  30. package/dist/pwa/assets/{index-DhvJN8ie.css → index-C6Lz09EY.css} +1 -1
  31. package/dist/pwa/assets/index-C8vJwUNi.js +118 -0
  32. package/dist/pwa/assets/web-6UChJFov.js +1 -0
  33. package/dist/pwa/assets/web-NxTETXZK.js +1 -0
  34. package/dist/pwa/index.html +3 -3
  35. package/dist/pwa/service-worker.js +2 -2
  36. package/dist/rpc-handler.js +20 -8
  37. package/dist/spawn-command.js +3 -1
  38. package/dist/transports/http-transport.js +60 -129
  39. package/package.json +1 -1
  40. package/palmier-server/README.md +6 -1
  41. package/palmier-server/package.json +7 -1
  42. package/palmier-server/pnpm-lock.yaml +1025 -1
  43. package/palmier-server/pwa/index.html +1 -1
  44. package/palmier-server/pwa/package.json +3 -0
  45. package/palmier-server/pwa/src/App.css +64 -0
  46. package/palmier-server/pwa/src/api.ts +8 -2
  47. package/palmier-server/pwa/src/components/HostMenu.tsx +102 -1
  48. package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
  49. package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
  50. package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
  51. package/palmier-server/pwa/src/constants.ts +1 -1
  52. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +2 -1
  53. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +3 -0
  54. package/palmier-server/pwa/src/pages/Dashboard.tsx +5 -2
  55. package/palmier-server/pwa/src/pages/PairHost.tsx +10 -1
  56. package/palmier-server/pwa/src/service-worker.ts +7 -7
  57. package/palmier-server/server/.env.example +4 -0
  58. package/palmier-server/server/package.json +1 -0
  59. package/palmier-server/server/src/db.ts +10 -0
  60. package/palmier-server/server/src/fcm.ts +74 -0
  61. package/palmier-server/server/src/index.ts +101 -21
  62. package/palmier-server/server/src/notify.ts +34 -0
  63. package/palmier-server/server/src/push.ts +1 -1
  64. package/palmier-server/server/src/routes/fcm.ts +64 -0
  65. package/palmier-server/server/src/routes/push.ts +6 -5
  66. package/palmier-server/spec.md +4 -2
  67. package/src/agents/agent-instructions.md +6 -14
  68. package/src/agents/aider.ts +1 -1
  69. package/src/agents/claude.ts +1 -1
  70. package/src/agents/cline.ts +1 -1
  71. package/src/agents/codex.ts +1 -1
  72. package/src/agents/copilot.ts +1 -1
  73. package/src/agents/cursor.ts +1 -1
  74. package/src/agents/deepagents.ts +1 -1
  75. package/src/agents/droid.ts +1 -1
  76. package/src/agents/gemini.ts +1 -1
  77. package/src/agents/goose.ts +1 -1
  78. package/src/agents/hermes.ts +1 -1
  79. package/src/agents/kimi.ts +1 -1
  80. package/src/agents/kiro.ts +1 -1
  81. package/src/agents/openclaw.ts +1 -1
  82. package/src/agents/opencode.ts +1 -1
  83. package/src/agents/qoder.ts +1 -1
  84. package/src/agents/qwen.ts +1 -1
  85. package/src/agents/shared-prompt.ts +7 -4
  86. package/src/commands/plan-generation.md +1 -0
  87. package/src/commands/run.ts +4 -7
  88. package/src/location-device.ts +35 -0
  89. package/src/mcp-handler.ts +133 -0
  90. package/src/mcp-tools.ts +253 -0
  91. package/src/rpc-handler.ts +21 -8
  92. package/src/spawn-command.ts +3 -1
  93. package/src/transports/http-transport.ts +57 -128
  94. package/test/agent-instructions.test.ts +68 -5
  95. package/test/fixtures/agent-instructions-snapshot.md +58 -0
  96. package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
package/README.md CHANGED
@@ -34,7 +34,21 @@ It runs on your machine as a background daemon and connects to a mobile-friendly
34
34
 
35
35
  ## How It Works
36
36
 
37
- Palmier runs as a background daemon (systemd on Linux, Task Scheduler on Windows). It invokes your agent CLIs directly, schedules tasks via native OS timers, and exposes an API that the PWA connects to — either directly over HTTP or remotely through a relay server.
37
+ Palmier runs as a background daemon (systemd on Linux, Task Scheduler on Windows). It invokes your agent CLIs directly, schedules tasks via native OS timers, and exposes an API that the PWA connects to — either directly over HTTP or remotely through a relay server. Agents can interact with the user's mobile device during execution — requesting input, sending push notifications, and fetching GPS location.
38
+
39
+ ### MCP Server
40
+
41
+ Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://localhost:<port>/mcp` (streamable HTTP transport). MCP-capable agents can register it to get tool definitions automatically. The same tools are also available as REST endpoints for curl-based agents.
42
+
43
+ **MCP server URL:** `http://localhost:<port>/mcp`
44
+
45
+ **Available tools:**
46
+ | Tool | Description |
47
+ |------|-------------|
48
+ | `notify` | Send a push notification to the user's device |
49
+ | `request-input` | Request input from the user (blocks until response) |
50
+ | `request-confirmation` | Request confirmation from the user (blocks until response) |
51
+ | `device-geolocation` | Get GPS location of the user's mobile device |
38
52
 
39
53
  ```
40
54
  ┌──────────────┐ HTTP ┌──────────────────┐
@@ -1,4 +1,4 @@
1
- You are an AI agent executing a task on behalf of the user via the Palmier platform. Follow these instructions carefully.
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
- If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format:
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
- The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
21
+ {{ENDPOINT_DOCS}}
22
22
 
23
- **Requesting user input** — 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. Instead, POST to `/request-input` with:
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
- The task to execute follows below.
27
+ {{TASK_DESCRIPTION}}
28
+
@@ -11,7 +11,7 @@ export class Aider {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--yes-always");
@@ -11,7 +11,7 @@ export class ClaudeAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["--permission-mode", yolo ? "bypassPermissions" : "acceptEdits", "-p"];
16
16
  if (!yolo) {
17
17
  args.push("--allowedTools", "WebFetch");
@@ -11,7 +11,7 @@ export class Cline {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--yolo");
@@ -11,7 +11,7 @@ export class CodexAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["exec", "--skip-git-repo-check", "--sandbox", yolo ? "danger-full-access" : "workspace-write"];
16
16
  if (!yolo) {
17
17
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -11,7 +11,7 @@ export class CopilotAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["-p", prompt];
16
16
  if (yolo) {
17
17
  args.push("--yolo");
@@ -11,7 +11,7 @@ export class Cursor {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--force");
@@ -11,7 +11,7 @@ export class DeepAgents {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--auto-approve");
@@ -11,7 +11,7 @@ export class DroidAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["exec", "--session-id", task.frontmatter.id];
16
16
  if (yolo) {
17
17
  args.push("--skip-permissions-unsafe");
@@ -11,7 +11,7 @@ export class GeminiAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
16
16
  if (!yolo) {
17
17
  const tools = ["run_shell_command", "web_fetch"];
@@ -11,7 +11,7 @@ export class GooseAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["run"];
16
16
  if (followupPrompt) {
17
17
  args.push("--resume");
@@ -11,7 +11,7 @@ export class Hermes {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["chat"];
16
16
  if (yolo) {
17
17
  args.push("--trust-all-tools");
@@ -11,7 +11,7 @@ export class KimiAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--yolo");
@@ -11,7 +11,7 @@ export class Kiro {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--trust-all-tools");
@@ -10,7 +10,7 @@ export class OpenClawAgent {
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
12
  const yolo = extraPermissions === "yolo";
13
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
13
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
14
14
  // OpenClaw does not support stdin as prompt.
15
15
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
16
16
  return { command: "openclaw", args };
@@ -11,7 +11,7 @@ export class OpenCodeAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["run"];
16
16
  if (yolo) {
17
17
  args.push("--dangerously-skip-permissions");
@@ -11,7 +11,7 @@ export class Qoder {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = [];
16
16
  if (yolo) {
17
17
  args.push("--yolo");
@@ -11,7 +11,7 @@ export class QwenAgent {
11
11
  }
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
15
15
  const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
16
16
  if (followupPrompt) {
17
17
  args.push("-c");
@@ -1,7 +1,8 @@
1
+ import type { ParsedTask } from "../types.js";
1
2
  /**
2
- * Agent instructions with the serve daemon's HTTP port and task ID baked in.
3
+ * Build the full agent prompt: instructions + endpoint docs + task description.
3
4
  */
4
- export declare function getAgentInstructions(taskId: string, skipPermissions?: boolean): string;
5
+ export declare function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string;
5
6
  export declare const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
6
7
  export declare const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
7
8
  export declare const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
@@ -2,16 +2,18 @@ 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";
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7
  const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(path.join(__dirname, "agent-instructions.md"), "utf-8");
7
8
  /**
8
- * Agent instructions with the serve daemon's HTTP port and task ID baked in.
9
+ * Build the full agent prompt: instructions + endpoint docs + task description.
9
10
  */
10
- export function getAgentInstructions(taskId, skipPermissions) {
11
+ export function getAgentInstructions(task, skipPermissions) {
11
12
  const port = loadConfig().httpPort ?? 9966;
13
+ const taskDescription = task.body || task.frontmatter.user_prompt;
12
14
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
13
- .replace(/\{\{PORT\}\}/g, String(port))
14
- .replace(/\{\{TASK_ID\}\}/g, taskId);
15
+ .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
16
+ .replace(/\{\{TASK_DESCRIPTION\}\}/g, taskDescription);
15
17
  if (skipPermissions) {
16
18
  instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
17
19
  }
@@ -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
 
@@ -34,12 +34,9 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
34
34
  }, 500);
35
35
  }
36
36
  const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions);
37
- const truncate = (s, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
38
- const displayArgs = args.map((a) => truncate(a));
39
- console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
40
37
  const result = await spawnCommand(command, args, {
41
38
  cwd: getRunDir(ctx.taskDir, ctx.runId),
42
- env: { ...ctx.guiEnv, ...agentEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, 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 ?? 9966) },
43
40
  echoStdout: true,
44
41
  resolveOnFailure: true,
45
42
  stdin,
@@ -261,7 +258,7 @@ async function runCommandTriggeredMode(ctx) {
261
258
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
262
259
  const child = spawnStreamingCommand(commandStr, {
263
260
  cwd: getRunDir(ctx.taskDir, ctx.runId),
264
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, 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 ?? 9966) },
265
262
  });
266
263
  let linesProcessed = 0;
267
264
  let invocationsSucceeded = 0;
@@ -421,10 +418,10 @@ async function requestPermission(config, task, taskDir, requiredPermissions) {
421
418
  }
422
419
  async function requestConfirmation(config, task, taskDir) {
423
420
  const port = config.httpPort ?? 9966;
424
- const res = await fetch(`http://localhost:${port}/request-confirmation`, {
421
+ const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
425
422
  method: "POST",
426
423
  headers: { "Content-Type": "application/json" },
427
- body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
424
+ body: JSON.stringify({ description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
428
425
  });
429
426
  const body = await res.json();
430
427
  if (typeof body.confirmed !== "boolean") {
@@ -0,0 +1,8 @@
1
+ export interface LocationDevice {
2
+ clientToken: string;
3
+ fcmToken: string;
4
+ }
5
+ export declare function getLocationDevice(): LocationDevice | null;
6
+ export declare function setLocationDevice(clientToken: string, fcmToken: string): void;
7
+ export declare function clearLocationDevice(): void;
8
+ //# sourceMappingURL=location-device.d.ts.map
@@ -0,0 +1,32 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { CONFIG_DIR } from "./config.js";
4
+ const LOCATION_FILE = path.join(CONFIG_DIR, "location-device.json");
5
+ export function getLocationDevice() {
6
+ try {
7
+ if (!fs.existsSync(LOCATION_FILE))
8
+ return null;
9
+ const raw = fs.readFileSync(LOCATION_FILE, "utf-8");
10
+ const data = JSON.parse(raw);
11
+ if (!data.clientToken || !data.fcmToken)
12
+ return null;
13
+ return data;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function setLocationDevice(clientToken, fcmToken) {
20
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
21
+ fs.writeFileSync(LOCATION_FILE, JSON.stringify({ clientToken, fcmToken }, null, 2), "utf-8");
22
+ }
23
+ export function clearLocationDevice() {
24
+ try {
25
+ if (fs.existsSync(LOCATION_FILE))
26
+ fs.unlinkSync(LOCATION_FILE);
27
+ }
28
+ catch {
29
+ // ignore
30
+ }
31
+ }
32
+ //# sourceMappingURL=location-device.js.map
@@ -0,0 +1,8 @@
1
+ import { type ToolContext } from "./mcp-tools.js";
2
+ export interface McpResponse {
3
+ body: object;
4
+ sessionId?: string;
5
+ }
6
+ export declare function getAgentName(sessionId: string): string | undefined;
7
+ export declare function handleMcpRequest(body: string, sessionId: string | undefined, ctx: ToolContext): Promise<McpResponse>;
8
+ //# sourceMappingURL=mcp-handler.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { randomUUID } from "crypto";
2
+ import { agentTools, agentToolMap, ToolError } from "./mcp-tools.js";
3
+ // Session-to-agent name map with 24h TTL
4
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
5
+ const sessionAgents = new Map();
6
+ export function getAgentName(sessionId) {
7
+ const entry = sessionAgents.get(sessionId);
8
+ if (!entry)
9
+ return undefined;
10
+ if (Date.now() > entry.expiresAt) {
11
+ sessionAgents.delete(sessionId);
12
+ return undefined;
13
+ }
14
+ return entry.agentName;
15
+ }
16
+ function pruneExpiredSessions() {
17
+ const now = Date.now();
18
+ for (const [id, entry] of sessionAgents) {
19
+ if (now > entry.expiresAt)
20
+ sessionAgents.delete(id);
21
+ }
22
+ }
23
+ function rpcError(id, code, message) {
24
+ return { jsonrpc: "2.0", id, error: { code, message } };
25
+ }
26
+ function rpcResult(id, result) {
27
+ return { jsonrpc: "2.0", id, result };
28
+ }
29
+ export async function handleMcpRequest(body, sessionId, ctx) {
30
+ let req;
31
+ try {
32
+ req = JSON.parse(body);
33
+ }
34
+ catch {
35
+ return { body: rpcError(null, -32700, "Parse error") };
36
+ }
37
+ const id = req.id ?? null;
38
+ if (req.jsonrpc !== "2.0") {
39
+ return { body: rpcError(id, -32600, "Invalid Request: missing jsonrpc 2.0") };
40
+ }
41
+ const agent = sessionId ? getAgentName(sessionId) : undefined;
42
+ const sid = sessionId?.slice(0, 8) ?? "none";
43
+ const logPrefix = agent ? `[mcp] [${sid}] [${agent}]` : `[mcp] [${sid}]`;
44
+ console.log(`${logPrefix} ${req.method}${req.method === "tools/call" ? ` → ${req.params?.name}` : ""}`);
45
+ switch (req.method) {
46
+ case "initialize": {
47
+ const newSessionId = randomUUID();
48
+ const clientInfo = req.params?.clientInfo;
49
+ const agentName = clientInfo
50
+ ? `${clientInfo.name || "unknown"}${clientInfo.version ? ` ${clientInfo.version}` : ""}`
51
+ : undefined;
52
+ if (agentName) {
53
+ sessionAgents.set(newSessionId, { agentName, expiresAt: Date.now() + SESSION_TTL_MS });
54
+ pruneExpiredSessions();
55
+ }
56
+ console.log(`[mcp] [${newSessionId.slice(0, 8)}] Session initialized${agentName ? ` (${agentName})` : ""}`);
57
+ return {
58
+ body: rpcResult(id, {
59
+ protocolVersion: "2025-03-26",
60
+ capabilities: { tools: {} },
61
+ serverInfo: { name: "palmier", version: "1.0.0" },
62
+ }),
63
+ sessionId: newSessionId,
64
+ };
65
+ }
66
+ case "tools/list": {
67
+ return {
68
+ body: rpcResult(id, {
69
+ tools: agentTools.map((t) => ({
70
+ name: t.name,
71
+ description: t.description.join(" "),
72
+ inputSchema: t.inputSchema,
73
+ })),
74
+ }),
75
+ };
76
+ }
77
+ case "tools/call": {
78
+ const name = req.params?.name;
79
+ const args = (req.params?.arguments ?? {});
80
+ if (!name)
81
+ return { body: rpcError(id, -32602, "Missing params.name") };
82
+ const tool = agentToolMap.get(name);
83
+ if (!tool)
84
+ return { body: rpcError(id, -32602, `Unknown tool: ${name}`) };
85
+ try {
86
+ const result = await tool.handler(args, ctx);
87
+ console.log(`${logPrefix} tools/call ${name} done:`, JSON.stringify(result).slice(0, 200));
88
+ return {
89
+ body: rpcResult(id, {
90
+ content: [{ type: "text", text: JSON.stringify(result) }],
91
+ }),
92
+ };
93
+ }
94
+ catch (err) {
95
+ const message = err instanceof ToolError ? err.message : String(err);
96
+ console.error(`${logPrefix} tools/call ${name} error:`, message);
97
+ return {
98
+ body: rpcResult(id, {
99
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
100
+ isError: true,
101
+ }),
102
+ };
103
+ }
104
+ }
105
+ default:
106
+ console.warn(`${logPrefix} Unknown method: ${req.method}`);
107
+ return { body: rpcError(id, -32601, `Method not found: ${req.method}`) };
108
+ }
109
+ }
110
+ //# sourceMappingURL=mcp-handler.js.map
@@ -0,0 +1,27 @@
1
+ import { type NatsConnection } from "nats";
2
+ import type { HostConfig } from "./types.js";
3
+ export declare class ToolError extends Error {
4
+ statusCode: number;
5
+ constructor(message: string, statusCode?: number);
6
+ }
7
+ export interface ToolContext {
8
+ config: HostConfig;
9
+ nc: NatsConnection | undefined;
10
+ publishEvent: (id: string, payload: Record<string, unknown>) => Promise<void>;
11
+ sessionId: string;
12
+ agentName?: string;
13
+ }
14
+ export interface ToolDefinition {
15
+ name: string;
16
+ /** First line is the summary (used as endpoint header). Remaining lines become bullet points in docs. */
17
+ description: string[];
18
+ inputSchema: object;
19
+ handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
20
+ }
21
+ export declare const agentTools: ToolDefinition[];
22
+ export declare const agentToolMap: Map<string, ToolDefinition>;
23
+ /**
24
+ * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
25
+ */
26
+ export declare function generateEndpointDocs(port: number, taskId: string): string;
27
+ //# sourceMappingURL=mcp-tools.d.ts.map