palmier 0.6.8 → 0.7.0

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 (98) hide show
  1. package/README.md +9 -2
  2. package/dist/agents/agent.d.ts +2 -2
  3. package/dist/agents/aider.d.ts +1 -1
  4. package/dist/agents/aider.js +2 -5
  5. package/dist/agents/claude.d.ts +1 -1
  6. package/dist/agents/claude.js +2 -5
  7. package/dist/agents/cline.d.ts +1 -1
  8. package/dist/agents/cline.js +2 -5
  9. package/dist/agents/codex.d.ts +1 -1
  10. package/dist/agents/codex.js +2 -5
  11. package/dist/agents/copilot.d.ts +1 -1
  12. package/dist/agents/copilot.js +2 -5
  13. package/dist/agents/cursor.d.ts +1 -1
  14. package/dist/agents/cursor.js +2 -5
  15. package/dist/agents/deepagents.d.ts +1 -1
  16. package/dist/agents/deepagents.js +2 -5
  17. package/dist/agents/droid.d.ts +1 -1
  18. package/dist/agents/droid.js +2 -5
  19. package/dist/agents/gemini.d.ts +1 -1
  20. package/dist/agents/gemini.js +2 -5
  21. package/dist/agents/goose.d.ts +1 -1
  22. package/dist/agents/goose.js +2 -5
  23. package/dist/agents/hermes.d.ts +1 -1
  24. package/dist/agents/hermes.js +2 -5
  25. package/dist/agents/kimi.d.ts +1 -1
  26. package/dist/agents/kimi.js +2 -5
  27. package/dist/agents/kiro.d.ts +1 -1
  28. package/dist/agents/kiro.js +2 -5
  29. package/dist/agents/openclaw.d.ts +1 -1
  30. package/dist/agents/openclaw.js +2 -5
  31. package/dist/agents/opencode.d.ts +1 -1
  32. package/dist/agents/opencode.js +2 -5
  33. package/dist/agents/qoder.d.ts +1 -1
  34. package/dist/agents/qoder.js +2 -5
  35. package/dist/agents/qwen.d.ts +1 -1
  36. package/dist/agents/qwen.js +2 -5
  37. package/dist/agents/shared-prompt.js +1 -1
  38. package/dist/commands/run.js +1 -2
  39. package/dist/commands/serve.js +16 -0
  40. package/dist/mcp-handler.d.ts +3 -0
  41. package/dist/mcp-handler.js +59 -3
  42. package/dist/mcp-tools.d.ts +16 -1
  43. package/dist/mcp-tools.js +24 -2
  44. package/dist/notification-store.d.ts +13 -0
  45. package/dist/notification-store.js +19 -0
  46. package/dist/pwa/assets/{index-C8vJwUNi.js → index-DLxrL0hR.js} +42 -42
  47. package/dist/pwa/assets/{web-NxTETXZK.js → web-CBI458eN.js} +1 -1
  48. package/dist/pwa/assets/{web-6UChJFov.js → web-HDs03L2B.js} +1 -1
  49. package/dist/pwa/index.html +1 -1
  50. package/dist/pwa/service-worker.js +1 -1
  51. package/dist/rpc-handler.js +27 -67
  52. package/dist/task.js +2 -3
  53. package/dist/transports/http-transport.js +51 -3
  54. package/dist/types.d.ts +0 -1
  55. package/package.json +2 -2
  56. package/palmier-server/README.md +1 -1
  57. package/palmier-server/pwa/src/components/PlanDialog.tsx +5 -12
  58. package/palmier-server/pwa/src/components/TaskForm.tsx +6 -15
  59. package/palmier-server/pwa/src/constants.ts +1 -1
  60. package/palmier-server/pwa/src/types.ts +0 -1
  61. package/palmier-server/server/src/index.ts +2 -0
  62. package/palmier-server/server/src/routes/device.ts +32 -0
  63. package/palmier-server/spec.md +13 -12
  64. package/src/agents/agent.ts +2 -2
  65. package/src/agents/aider.ts +2 -5
  66. package/src/agents/claude.ts +2 -5
  67. package/src/agents/cline.ts +2 -5
  68. package/src/agents/codex.ts +2 -5
  69. package/src/agents/copilot.ts +2 -5
  70. package/src/agents/cursor.ts +2 -5
  71. package/src/agents/deepagents.ts +2 -5
  72. package/src/agents/droid.ts +2 -5
  73. package/src/agents/gemini.ts +2 -5
  74. package/src/agents/goose.ts +2 -5
  75. package/src/agents/hermes.ts +2 -5
  76. package/src/agents/kimi.ts +2 -5
  77. package/src/agents/kiro.ts +2 -5
  78. package/src/agents/openclaw.ts +2 -5
  79. package/src/agents/opencode.ts +2 -5
  80. package/src/agents/qoder.ts +2 -5
  81. package/src/agents/qwen.ts +2 -5
  82. package/src/agents/shared-prompt.ts +1 -1
  83. package/src/commands/run.ts +1 -2
  84. package/src/commands/serve.ts +16 -1
  85. package/src/mcp-handler.ts +68 -3
  86. package/src/mcp-tools.ts +48 -2
  87. package/src/notification-store.ts +30 -0
  88. package/src/rpc-handler.ts +29 -71
  89. package/src/task.ts +2 -3
  90. package/src/transports/http-transport.ts +49 -3
  91. package/src/types.ts +0 -1
  92. package/test/agent-instructions.test.ts +117 -19
  93. package/test/agent-output-parsing.test.ts +1 -0
  94. package/test/notification-store.test.ts +57 -0
  95. package/test/task-parsing.test.ts +3 -3
  96. package/dist/commands/plan-generation.md +0 -22
  97. package/src/commands/plan-generation.md +0 -22
  98. package/test/fixtures/agent-instructions-snapshot.md +0 -58
@@ -105,10 +105,10 @@ The **RPC method is derived from the NATS subject**, not the message body. The h
105
105
 
106
106
  | Method | Params | Description |
107
107
  |---|---|---|
108
- | `task.list` | *(none)* | List all tasks with frontmatter, body, created_at, and current status. Returns `agents` array of detected CLIs, `host_platform`, and `version`. |
109
- | `task.get` | `id` | Get a single task with frontmatter, body, and current status. |
110
- | `task.create` | `user_prompt`, `agent`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Create a new task with auto-generated plan and name (130s timeout), install system timers if triggers present. If `command` is set, creates a command-triggered task (plan generation is skipped). |
111
- | `task.update` | `id`, `user_prompt?`, `agent?`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Update an existing task. Regenerates plan if `user_prompt` or `agent` changed, or if no plan exists yet (130s timeout). If `command` is set, plan is cleared. Reinstall timers as needed |
108
+ | `task.list` | *(none)* | List all tasks with frontmatter, created_at, and current status. Returns `agents` array of detected CLIs, `host_platform`, and `version`. |
109
+ | `task.get` | `id` | Get a single task with frontmatter and current status. |
110
+ | `task.create` | `user_prompt`, `agent`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Create a new task with auto-generated name (30s timeout for prompts > 50 chars), install system timers if triggers present. |
111
+ | `task.update` | `id`, `user_prompt?`, `agent?`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Update an existing task. Regenerates name if `user_prompt` or `agent` changed. Reinstall timers as needed. |
112
112
  | `task.delete` | `id` | Delete a task and its systemd timers |
113
113
  | `task.run` | `id` | Start a task via system scheduler (`systemctl --user start` / `schtasks /run`) |
114
114
  | `task.abort` | `id` | Stop a running task via system scheduler (`systemctl --user stop` / `schtasks /end`) |
@@ -150,7 +150,7 @@ All tasks are stored locally on the Host machine under a `tasks/` directory rela
150
150
  history.jsonl # Project-level run history index (append-only JSONL: { task_id, run_id })
151
151
  tasks/
152
152
  └── <task-id>/
153
- ├── TASK.md # Current task definition (frontmatter + body)
153
+ ├── TASK.md # Current task definition (YAML frontmatter)
154
154
  ├── status.json # Latest execution status (running_state, time_stamp, pid)
155
155
  └── <timestamp>/ # Run directory (one per run, isolated per agent session)
156
156
  ├── TASKRUN.md # Conversational thread (frontmatter + message entries)
@@ -209,12 +209,13 @@ triggers:
209
209
  triggers_enabled: true
210
210
  requires_confirmation: true
211
211
  ---
212
- [Detailed execution plan generated by the non-interactive generation step]
213
212
  ```
214
213
 
214
+ The `name` field is auto-generated by spawning the configured agent CLI with a short prompt (for prompts > 50 chars). For shorter prompts, the `user_prompt` is used directly as the name.
215
+
215
216
  The `agent` field stores the agent name (e.g., `"claude"`, `"codex"`). The corresponding `AgentTool` implementation is responsible for constructing the full command and arguments at execution time.
216
217
 
217
- The optional `command` field stores a shell command for command-triggered tasks. When set, the task runs in command-triggered mode: the command is spawned with `shell: true`, and each line of its stdout triggers a separate agent invocation with `user_prompt + "\n\nProcess this input:\n" + line`. Plan generation is skipped for command-triggered tasks.
218
+ The optional `command` field stores a shell command for command-triggered tasks. When set, the task runs in command-triggered mode: the command is spawned with `shell: true`, and each line of its stdout triggers a separate agent invocation with `user_prompt + "\n\nProcess this input:\n" + line`.
218
219
 
219
220
  #### Trigger Lifecycle
220
221
 
@@ -263,7 +264,7 @@ The PWA connects to **one host at a time**. A host menu (hamburger drawer) lets
263
264
 
264
265
  3. PWA sends a `task.list` request to `host.<host_id>.rpc.task.list` using NATS request-reply, including the `clientToken` in the payload.
265
266
 
266
- 4. If the host responds, it returns `{ tasks: [...] }` — an array of **flat task objects** (frontmatter fields spread to the top level, plus `body`) and displays the task list. If the request fails with NATS 503 ("no responders"), the PWA shows an empty task list — this is not treated as an error.
267
+ 4. If the host responds, it returns `{ tasks: [...] }` — an array of **flat task objects** (frontmatter fields spread to the top level) and displays the task list. If the request fails with NATS 503 ("no responders"), the PWA shows an empty task list — this is not treated as an error.
267
268
 
268
269
  5. PWA registers the service worker and subscribes the browser for Web Push notifications (via `pushManager.subscribe` with the server's VAPID public key). The push subscription is sent to `POST /api/push/subscribe` with the `hostId` so the server can relay notifications to the device.
269
270
 
@@ -275,13 +276,13 @@ The PWA connects to **one host at a time**. A host menu (hamburger drawer) lets
275
276
 
276
277
  2. User enters a prompt, selects an agent, configures triggers (UI translates human-readable times to cron formats) and confirmation settings, and clicks "Create" (or "Update" for existing tasks).
277
278
 
278
- 3. PWA sends `task.create` (or `task.update`) via NATS request-reply to Host (130s timeout). The host generates the execution plan and task name by running the configured agent CLI in non-interactive mode (e.g., `claude -p "Generate execution plan for: [prompt]"`), then creates the task with the generated plan as its body. The PWA renders the plan markdown as rich formatted text (headings, tables, lists, code blocks) using `react-markdown` with `remark-gfm` for GFM support.
279
+ 3. PWA sends `task.create` (or `task.update`) via NATS request-reply to Host (45s timeout). For prompts > 50 chars, the host generates a concise task name by running the configured agent CLI in non-interactive mode (e.g., `claude -p "Generate a concise 3-6 word name for this task..."`). For shorter prompts, the prompt is used directly as the name.
279
280
 
280
- 4. For updates: if the user changes the `user_prompt` or `agent`, the plan is regenerated. If neither changed, the existing plan is preserved. Existing tasks with a plan show a clickable "Execution Plan" link to view the plan; this link disappears when the user edits the prompt or changes the agent.
281
+ 4. For updates: if the user changes the `user_prompt` or `agent`, the name is regenerated. If neither changed, the existing name is preserved. Existing tasks with granted permissions show a clickable "Granted Permissions" link to view them.
281
282
 
282
283
  5. PWA sends `task.create` (or `task.update` with `id`) with the task fields as the message body. The `id` field is **not sent on create** — the host generates a UUID. The `triggers` field defaults to `[]` if omitted or undefined.
283
284
 
284
- 6. Host creates/updates the `tasks/<task-id>/TASK.md` file and returns the **full flat task object** (all frontmatter fields plus `body` at the top level). The PWA uses this response directly to update the UI.
285
+ 6. Host creates/updates the `tasks/<task-id>/TASK.md` file and returns the **full flat task object** (all frontmatter fields at the top level). The PWA uses this response directly to update the UI.
285
286
 
286
287
  7. **OS Integration:** Host translates triggers into a systemd user timer (`~/.config/systemd/user/palmier-task-<task-id>.timer` and `.service`). The `.service` runs `palmier run <task-id>`, which executes the task as a background process. Host runs `systemctl --user daemon-reload` and enables the timer.
287
288
 
@@ -328,7 +329,7 @@ When `palmier run <task-id>` executes (triggered by a systemd timer, `systemctl
328
329
 
329
330
  * The spawned process inherits the default physical GUI session environment (`DISPLAY=:0`, `XDG_RUNTIME_DIR=/run/user/<uid>`) so that commands requiring a graphical display (e.g., headed browsers) run within the user's desktop session. `PALMIER_HTTP_PORT` is also set so agents can call the serve daemon's HTTP endpoints.
330
331
 
331
- * The agent implementation is responsible for constructing the appropriate arguments (e.g., `--allowedTools` flags for Claude based on the task's permissions). The task plan (body from `TASK.md` or `user_prompt`) is included in the arguments by the agent.
332
+ * The agent implementation is responsible for constructing the appropriate arguments (e.g., `--allowedTools` flags for Claude based on the task's permissions). The task's `user_prompt` is included in the arguments by the agent.
332
333
 
333
334
  3. **Completion:**
334
335
 
@@ -31,8 +31,8 @@ export interface CommandLine {
31
31
  * Abstracts how plans are generated and tasks are executed across different AI agents.
32
32
  */
33
33
  export interface AgentTool {
34
- /** Return the command and args used to generate a plan from a prompt. */
35
- getPlanGenerationCommandLine(prompt: string): CommandLine;
34
+ /** Return the command and args for a short, non-interactive prompt (e.g. generating a task name). */
35
+ getPromptCommandLine(prompt: string): CommandLine;
36
36
 
37
37
  /** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
38
38
  * and treat it as a continuation of the original run (reuse the same session, etc).
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Aider implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "aider",
12
- args: ["--message", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "aider", args: ["--message", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class ClaudeAgent implements AgentTool {
8
8
  supportsPermissions = true;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "claude",
12
- args: ["-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "claude", args: ["-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Cline implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "cline ",
12
- args: ["--yolo", "-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "cline ", args: ["--yolo", "-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CodexAgent implements AgentTool {
8
8
  supportsPermissions = true;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "codex",
12
- args: ["exec", "--skip-git-repo-check", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "codex", args: ["exec", "--skip-git-repo-check", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "copilot",
12
- args: ["-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "copilot", args: ["-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Cursor implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "cursor",
12
- args: ["-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "cursor", args: ["-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class DeepAgents implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "deepagents",
12
- args: ["--non-interactive", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "deepagents", args: ["--non-interactive", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class DroidAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "droid",
12
- args: ["exec", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "droid", args: ["exec", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
8
8
  supportsPermissions = true;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "gemini",
12
- args: ["--prompt", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "gemini", args: ["--prompt", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GooseAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "goose",
12
- args: ["run", "--text", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "goose", args: ["run", "--text", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Hermes implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "hermes",
12
- args: ["chat", "-q", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "hermes", args: ["chat", "-q", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class KimiAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "kimi",
12
- args: ["-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "kimi", args: ["-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Kiro implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "kiro-cli",
12
- args: ["--no-interactive", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "kiro-cli", args: ["--no-interactive", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -5,11 +5,8 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
7
  supportsPermissions = false;
8
- getPlanGenerationCommandLine(prompt: string): CommandLine {
9
- return {
10
- command: "openclaw",
11
- args: ["agent", "--local", "--agent", "main", "--message", prompt],
12
- };
8
+ getPromptCommandLine(prompt: string): CommandLine {
9
+ return { command: "openclaw", args: ["agent", "--local", "--agent", "main", "--message", prompt] };
13
10
  }
14
11
 
15
12
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class OpenCodeAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "opencode",
12
- args: ["run", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "opencode", args: ["run", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Qoder implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "qodercli",
12
- args: ["-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "qodercli", args: ["-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class QwenAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
- getPlanGenerationCommandLine(prompt: string): CommandLine {
10
- return {
11
- command: "qwen",
12
- args: ["-p", prompt],
13
- };
9
+ getPromptCommandLine(prompt: string): CommandLine {
10
+ return { command: "qwen", args: ["-p", prompt] };
14
11
  }
15
12
 
16
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
@@ -17,7 +17,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
17
17
  */
18
18
  export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
19
19
  const port = loadConfig().httpPort ?? 9966;
20
- const taskDescription = task.body || task.frontmatter.user_prompt;
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))
23
23
  .replace(/\{\{TASK_DESCRIPTION\}\}/g, taskDescription);
@@ -272,7 +272,7 @@ export async function runCommand(taskId: string): Promise<void> {
272
272
  await appendAndNotify(ctx, {
273
273
  role: "user",
274
274
  time: Date.now(),
275
- content: task.body || task.frontmatter.user_prompt,
275
+ content: task.frontmatter.user_prompt,
276
276
  });
277
277
 
278
278
  const result = await invokeAgentWithRetries(ctx, task);
@@ -362,7 +362,6 @@ async function runCommandTriggeredMode(
362
362
  const perLinePrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this input:\n${line}`;
363
363
  const perLineTask: ParsedTask = {
364
364
  frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
365
- body: "",
366
365
  };
367
366
 
368
367
  const result = await invokeAgentWithRetries(ctx, perLineTask);
@@ -12,7 +12,8 @@ import { detectAgents } from "../agents/agent.js";
12
12
  import { saveConfig } from "../config.js";
13
13
  import type { HostConfig } from "../types.js";
14
14
  import { CONFIG_DIR } from "../config.js";
15
- import type { NatsConnection } from "nats";
15
+ import { StringCodec, type NatsConnection } from "nats";
16
+ import { addNotification } from "../notification-store.js";
16
17
 
17
18
  const POLL_INTERVAL_MS = 30_000;
18
19
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
@@ -130,6 +131,20 @@ export async function serveCommand(): Promise<void> {
130
131
  // Start NATS transport (loops forever, fire-and-forget)
131
132
  if (nc) {
132
133
  startNatsTransport(config, handleRpc, nc);
134
+
135
+ // Subscribe to device notifications from Android
136
+ const sc = StringCodec();
137
+ const notifSub = nc.subscribe(`host.${config.hostId}.device.notifications`);
138
+ (async () => {
139
+ for await (const msg of notifSub) {
140
+ try {
141
+ const data = JSON.parse(sc.decode(msg.data));
142
+ addNotification({ ...data, receivedAt: Date.now() });
143
+ } catch (err) {
144
+ console.error("[nats] Failed to parse device notification:", err);
145
+ }
146
+ }
147
+ })();
133
148
  }
134
149
 
135
150
  // Start HTTP transport (loops forever)
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "crypto";
2
- import { agentTools, agentToolMap, ToolError, type ToolContext } from "./mcp-tools.js";
2
+ import { agentTools, agentToolMap, agentResources, agentResourceMap, ToolError, type ToolContext } from "./mcp-tools.js";
3
3
 
4
4
  interface JsonRpcRequest {
5
5
  jsonrpc: string;
@@ -11,6 +11,15 @@ interface JsonRpcRequest {
11
11
  export interface McpResponse {
12
12
  body: object;
13
13
  sessionId?: string;
14
+ /** If true, the HTTP transport should keep the response open as an SSE stream for server-initiated notifications. */
15
+ stream?: boolean;
16
+ }
17
+
18
+ // Resource subscriptions: sessionId → Set of resource URIs
19
+ const resourceSubscriptions = new Map<string, Set<string>>();
20
+
21
+ export function getResourceSubscriptions(): Map<string, Set<string>> {
22
+ return resourceSubscriptions;
14
23
  }
15
24
 
16
25
  // Session-to-agent name map with 24h TTL
@@ -30,7 +39,10 @@ export function getAgentName(sessionId: string): string | undefined {
30
39
  function pruneExpiredSessions(): void {
31
40
  const now = Date.now();
32
41
  for (const [id, entry] of sessionAgents) {
33
- if (now > entry.expiresAt) sessionAgents.delete(id);
42
+ if (now > entry.expiresAt) {
43
+ sessionAgents.delete(id);
44
+ resourceSubscriptions.delete(id);
45
+ }
34
46
  }
35
47
  }
36
48
 
@@ -78,7 +90,7 @@ export async function handleMcpRequest(body: string, sessionId: string | undefin
78
90
  return {
79
91
  body: rpcResult(id, {
80
92
  protocolVersion: "2025-03-26",
81
- capabilities: { tools: {} },
93
+ capabilities: { tools: {}, resources: { subscribe: true } },
82
94
  serverInfo: { name: "palmier", version: "1.0.0" },
83
95
  }),
84
96
  sessionId: newSessionId,
@@ -126,6 +138,59 @@ export async function handleMcpRequest(body: string, sessionId: string | undefin
126
138
  }
127
139
  }
128
140
 
141
+ case "resources/list": {
142
+ return {
143
+ body: rpcResult(id, {
144
+ resources: agentResources.map((r) => ({
145
+ uri: r.uri,
146
+ name: r.name,
147
+ description: r.description[0],
148
+ mimeType: r.mimeType,
149
+ })),
150
+ }),
151
+ };
152
+ }
153
+
154
+ case "resources/read": {
155
+ const uri = req.params?.uri as string;
156
+ const resource = agentResourceMap.get(uri);
157
+ if (!resource) {
158
+ return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
159
+ }
160
+ return {
161
+ body: rpcResult(id, {
162
+ contents: [{
163
+ uri: resource.uri,
164
+ mimeType: resource.mimeType,
165
+ text: JSON.stringify(resource.read()),
166
+ }],
167
+ }),
168
+ };
169
+ }
170
+
171
+ case "resources/subscribe": {
172
+ const uri = req.params?.uri as string;
173
+ if (!agentResourceMap.has(uri)) {
174
+ return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
175
+ }
176
+ if (!sessionId) {
177
+ return { body: rpcError(id, -32600, "Session required for subscriptions") };
178
+ }
179
+ if (!resourceSubscriptions.has(sessionId)) {
180
+ resourceSubscriptions.set(sessionId, new Set());
181
+ }
182
+ resourceSubscriptions.get(sessionId)!.add(uri);
183
+ return { body: rpcResult(id, {}), stream: true };
184
+ }
185
+
186
+ case "resources/unsubscribe": {
187
+ const uri = req.params?.uri as string;
188
+ if (sessionId) {
189
+ resourceSubscriptions.get(sessionId)?.delete(uri);
190
+ }
191
+ return { body: rpcResult(id, {}) };
192
+ }
193
+
129
194
  default:
130
195
  console.warn(`${logPrefix} Unknown method: ${req.method}`);
131
196
  return { body: rpcError(id, -32601, `Method not found: ${req.method}`) };
package/src/mcp-tools.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { StringCodec, type NatsConnection } from "nats";
2
2
  import { registerPending } from "./pending-requests.js";
3
3
  import { getLocationDevice } from "./location-device.js";
4
+ import { getNotifications } from "./notification-store.js";
4
5
  import type { HostConfig } from "./types.js";
5
6
 
6
7
  export class ToolError extends Error {
@@ -205,17 +206,53 @@ const deviceGeolocationTool: ToolDefinition = {
205
206
  export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
206
207
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
207
208
 
209
+ // ── MCP Resources ─────────────────────────────────────────────────────
210
+
211
+ export interface ResourceDefinition {
212
+ /** MCP resource URI (e.g. "notifications://device"). */
213
+ uri: string;
214
+ /** Display name. */
215
+ name: string;
216
+ /** First line is the summary (used as REST endpoint header). Remaining lines become bullet points in docs. */
217
+ description: string[];
218
+ mimeType: string;
219
+ /** REST endpoint path (e.g. "/notifications"). Served as GET. */
220
+ restPath: string;
221
+ /** Return the current resource content. */
222
+ read: () => unknown;
223
+ }
224
+
225
+ const deviceNotificationsResource: ResourceDefinition = {
226
+ uri: "notifications://device",
227
+ name: "Device Notifications",
228
+ description: [
229
+ "Get recent notifications from the user's Android device.",
230
+ "Response: JSON array of notification objects with `id`, `packageName`, `appName`, `title`, `text`, `timestamp`.",
231
+ ],
232
+ mimeType: "application/json",
233
+ restPath: "/notifications",
234
+ read: getNotifications,
235
+ };
236
+
237
+ export const agentResources: ResourceDefinition[] = [deviceNotificationsResource];
238
+ export const agentResourceMap = new Map<string, ResourceDefinition>(agentResources.map((r) => [r.uri, r]));
239
+
208
240
  /**
209
241
  * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
210
242
  */
211
- export function generateEndpointDocs(port: number, taskId: string): string {
243
+ export function generateEndpointDocs(
244
+ port: number,
245
+ taskId: string,
246
+ tools: ToolDefinition[] = agentTools,
247
+ resources: ResourceDefinition[] = agentResources,
248
+ ): string {
212
249
  const baseUrl = `http://localhost:${port}`;
213
250
  const lines: string[] = [
214
251
  `The following HTTP endpoints are available during task execution. Use curl to call them.`,
215
252
  "",
216
253
  ];
217
254
 
218
- for (const tool of agentTools) {
255
+ for (const tool of tools) {
219
256
  const schema = tool.inputSchema as { properties?: Record<string, { type?: string; description?: string; items?: { type?: string } }>; required?: string[] };
220
257
  const props = schema.properties ?? {};
221
258
  const required = new Set(schema.required ?? []);
@@ -249,5 +286,14 @@ export function generateEndpointDocs(port: number, taskId: string): string {
249
286
  lines.push("");
250
287
  }
251
288
 
289
+ for (const resource of resources) {
290
+ const [header, ...details] = resource.description;
291
+ lines.push(`**\`GET ${baseUrl}${resource.restPath}\`** — ${header}`);
292
+ for (const detail of details) {
293
+ lines.push(`- ${detail}`);
294
+ }
295
+ lines.push("");
296
+ }
297
+
252
298
  return lines.join("\n").trimEnd();
253
299
  }
@@ -0,0 +1,30 @@
1
+ export interface DeviceNotification {
2
+ id: string;
3
+ packageName: string;
4
+ appName: string;
5
+ title: string;
6
+ text: string;
7
+ timestamp: number;
8
+ receivedAt: number;
9
+ }
10
+
11
+ const MAX_NOTIFICATIONS = 50;
12
+ const notifications: DeviceNotification[] = [];
13
+ const listeners = new Set<() => void>();
14
+
15
+ export function addNotification(n: DeviceNotification): void {
16
+ notifications.push(n);
17
+ if (notifications.length > MAX_NOTIFICATIONS) {
18
+ notifications.shift();
19
+ }
20
+ for (const cb of listeners) cb();
21
+ }
22
+
23
+ export function getNotifications(): DeviceNotification[] {
24
+ return [...notifications];
25
+ }
26
+
27
+ export function onNotificationsChanged(cb: () => void): () => void {
28
+ listeners.add(cb);
29
+ return () => { listeners.delete(cb); };
30
+ }