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.
- package/README.md +9 -2
- package/dist/agents/agent.d.ts +2 -2
- package/dist/agents/aider.d.ts +1 -1
- package/dist/agents/aider.js +2 -5
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +2 -5
- package/dist/agents/cline.d.ts +1 -1
- package/dist/agents/cline.js +2 -5
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +2 -5
- package/dist/agents/copilot.d.ts +1 -1
- package/dist/agents/copilot.js +2 -5
- package/dist/agents/cursor.d.ts +1 -1
- package/dist/agents/cursor.js +2 -5
- package/dist/agents/deepagents.d.ts +1 -1
- package/dist/agents/deepagents.js +2 -5
- package/dist/agents/droid.d.ts +1 -1
- package/dist/agents/droid.js +2 -5
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +2 -5
- package/dist/agents/goose.d.ts +1 -1
- package/dist/agents/goose.js +2 -5
- package/dist/agents/hermes.d.ts +1 -1
- package/dist/agents/hermes.js +2 -5
- package/dist/agents/kimi.d.ts +1 -1
- package/dist/agents/kimi.js +2 -5
- package/dist/agents/kiro.d.ts +1 -1
- package/dist/agents/kiro.js +2 -5
- package/dist/agents/openclaw.d.ts +1 -1
- package/dist/agents/openclaw.js +2 -5
- package/dist/agents/opencode.d.ts +1 -1
- package/dist/agents/opencode.js +2 -5
- package/dist/agents/qoder.d.ts +1 -1
- package/dist/agents/qoder.js +2 -5
- package/dist/agents/qwen.d.ts +1 -1
- package/dist/agents/qwen.js +2 -5
- package/dist/agents/shared-prompt.js +1 -1
- package/dist/commands/run.js +1 -2
- package/dist/commands/serve.js +16 -0
- package/dist/mcp-handler.d.ts +3 -0
- package/dist/mcp-handler.js +59 -3
- package/dist/mcp-tools.d.ts +16 -1
- package/dist/mcp-tools.js +24 -2
- package/dist/notification-store.d.ts +13 -0
- package/dist/notification-store.js +19 -0
- package/dist/pwa/assets/{index-C8vJwUNi.js → index-DLxrL0hR.js} +42 -42
- package/dist/pwa/assets/{web-NxTETXZK.js → web-CBI458eN.js} +1 -1
- package/dist/pwa/assets/{web-6UChJFov.js → web-HDs03L2B.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +27 -67
- package/dist/task.js +2 -3
- package/dist/transports/http-transport.js +51 -3
- package/dist/types.d.ts +0 -1
- package/package.json +2 -2
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/components/PlanDialog.tsx +5 -12
- package/palmier-server/pwa/src/components/TaskForm.tsx +6 -15
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/types.ts +0 -1
- package/palmier-server/server/src/index.ts +2 -0
- package/palmier-server/server/src/routes/device.ts +32 -0
- package/palmier-server/spec.md +13 -12
- package/src/agents/agent.ts +2 -2
- package/src/agents/aider.ts +2 -5
- package/src/agents/claude.ts +2 -5
- package/src/agents/cline.ts +2 -5
- package/src/agents/codex.ts +2 -5
- package/src/agents/copilot.ts +2 -5
- package/src/agents/cursor.ts +2 -5
- package/src/agents/deepagents.ts +2 -5
- package/src/agents/droid.ts +2 -5
- package/src/agents/gemini.ts +2 -5
- package/src/agents/goose.ts +2 -5
- package/src/agents/hermes.ts +2 -5
- package/src/agents/kimi.ts +2 -5
- package/src/agents/kiro.ts +2 -5
- package/src/agents/openclaw.ts +2 -5
- package/src/agents/opencode.ts +2 -5
- package/src/agents/qoder.ts +2 -5
- package/src/agents/qwen.ts +2 -5
- package/src/agents/shared-prompt.ts +1 -1
- package/src/commands/run.ts +1 -2
- package/src/commands/serve.ts +16 -1
- package/src/mcp-handler.ts +68 -3
- package/src/mcp-tools.ts +48 -2
- package/src/notification-store.ts +30 -0
- package/src/rpc-handler.ts +29 -71
- package/src/task.ts +2 -3
- package/src/transports/http-transport.ts +49 -3
- package/src/types.ts +0 -1
- package/test/agent-instructions.test.ts +117 -19
- package/test/agent-output-parsing.test.ts +1 -0
- package/test/notification-store.test.ts +57 -0
- package/test/task-parsing.test.ts +3 -3
- package/dist/commands/plan-generation.md +0 -22
- package/src/commands/plan-generation.md +0 -22
- package/test/fixtures/agent-instructions-snapshot.md +0 -58
package/palmier-server/spec.md
CHANGED
|
@@ -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,
|
|
109
|
-
| `task.get` | `id` | Get a single task with frontmatter
|
|
110
|
-
| `task.create` | `user_prompt`, `agent`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Create a new task with auto-generated
|
|
111
|
-
| `task.update` | `id`, `user_prompt?`, `agent?`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Update an existing task. Regenerates
|
|
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
|
|
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`.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/src/agents/agent.ts
CHANGED
|
@@ -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
|
|
35
|
-
|
|
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).
|
package/src/agents/aider.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Aider implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/claude.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class ClaudeAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = true;
|
|
9
|
-
|
|
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 {
|
package/src/agents/cline.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Cline implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/codex.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class CodexAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = true;
|
|
9
|
-
|
|
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 {
|
package/src/agents/copilot.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class CopilotAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/cursor.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Cursor implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/deepagents.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class DeepAgents implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/droid.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class DroidAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/gemini.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class GeminiAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = true;
|
|
9
|
-
|
|
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 {
|
package/src/agents/goose.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class GooseAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/hermes.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Hermes implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/kimi.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class KimiAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/kiro.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Kiro implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -5,11 +5,8 @@ import { getAgentInstructions } from "./shared-prompt.js";
|
|
|
5
5
|
|
|
6
6
|
export class OpenClawAgent implements AgentTool {
|
|
7
7
|
supportsPermissions = false;
|
|
8
|
-
|
|
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 {
|
package/src/agents/opencode.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class OpenCodeAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/qoder.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Qoder implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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 {
|
package/src/agents/qwen.ts
CHANGED
|
@@ -6,11 +6,8 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class QwenAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
-
|
|
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.
|
|
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);
|
package/src/commands/run.ts
CHANGED
|
@@ -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.
|
|
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);
|
package/src/commands/serve.ts
CHANGED
|
@@ -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
|
|
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)
|
package/src/mcp-handler.ts
CHANGED
|
@@ -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)
|
|
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(
|
|
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
|
|
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
|
+
}
|