palmier 0.6.0 → 0.6.2
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/.github/workflows/publish.yml +15 -2
- package/CLAUDE.md +2 -2
- package/DISCLAIMER.md +36 -0
- package/README.md +76 -87
- package/dist/agents/agent-instructions.md +1 -1
- package/dist/agents/agent.d.ts +2 -0
- package/dist/agents/agent.js +21 -0
- package/dist/agents/aider.d.ts +9 -0
- package/dist/agents/aider.js +32 -0
- package/dist/agents/cursor.d.ts +9 -0
- package/dist/agents/cursor.js +35 -0
- package/dist/agents/deepagents.d.ts +9 -0
- package/dist/agents/deepagents.js +35 -0
- package/dist/agents/droid.d.ts +9 -0
- package/dist/agents/droid.js +32 -0
- package/dist/agents/goose.d.ts +9 -0
- package/dist/agents/goose.js +32 -0
- package/dist/agents/opencode.d.ts +9 -0
- package/dist/agents/opencode.js +35 -0
- package/dist/agents/openhands.d.ts +9 -0
- package/dist/agents/openhands.js +35 -0
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +1 -1
- package/dist/commands/run.js +2 -2
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/assets/index-ByhOhTz1.js +118 -0
- package/dist/pwa/assets/index-_AmC1Rkn.css +1 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/pwa/favicon.ico +0 -0
- package/dist/pwa/index.html +17 -0
- package/dist/pwa/manifest.webmanifest +1 -0
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +1 -0
- package/dist/pwa/service-worker.js +2 -0
- package/dist/rpc-handler.d.ts +4 -0
- package/dist/rpc-handler.js +5 -4
- package/dist/transports/http-transport.js +29 -41
- package/package.json +2 -2
- package/palmier-server/.github/workflows/ci.yml +21 -0
- package/palmier-server/.github/workflows/deploy.yml +38 -0
- package/palmier-server/CLAUDE.md +13 -0
- package/palmier-server/PRODUCTION.md +355 -0
- package/palmier-server/README.md +187 -0
- package/palmier-server/nats.conf +15 -0
- package/palmier-server/package.json +8 -0
- package/palmier-server/pnpm-lock.yaml +6597 -0
- package/palmier-server/pnpm-workspace.yaml +3 -0
- package/palmier-server/pwa/index.html +16 -0
- package/palmier-server/pwa/logo/logo-prompt.md +28 -0
- package/palmier-server/pwa/logo/logo_20260330.png +0 -0
- package/palmier-server/pwa/package.json +30 -0
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +2387 -0
- package/palmier-server/pwa/src/App.tsx +21 -0
- package/palmier-server/pwa/src/agentLabels.ts +11 -0
- package/palmier-server/pwa/src/api.ts +61 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +289 -0
- package/palmier-server/pwa/src/components/PlanDialog.tsx +41 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +293 -0
- package/palmier-server/pwa/src/components/RunsView.tsx +254 -0
- package/palmier-server/pwa/src/components/TabBar.tsx +31 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +213 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +580 -0
- package/palmier-server/pwa/src/components/TaskListView.tsx +415 -0
- package/palmier-server/pwa/src/constants.ts +2 -0
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +313 -0
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +135 -0
- package/palmier-server/pwa/src/formatTime.ts +10 -0
- package/palmier-server/pwa/src/hooks/useBackClose.ts +75 -0
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +17 -0
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +75 -0
- package/palmier-server/pwa/src/main.tsx +14 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +223 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +178 -0
- package/palmier-server/pwa/src/service-worker.ts +139 -0
- package/palmier-server/pwa/src/types.ts +79 -0
- package/palmier-server/pwa/src/vite-env.d.ts +11 -0
- package/palmier-server/pwa/tsconfig.json +21 -0
- package/palmier-server/pwa/tsconfig.node.json +19 -0
- package/palmier-server/pwa/vite.config.ts +47 -0
- package/palmier-server/server/.env.example +16 -0
- package/palmier-server/server/package.json +33 -0
- package/palmier-server/server/src/db.ts +34 -0
- package/palmier-server/server/src/index.ts +219 -0
- package/palmier-server/server/src/nats.ts +25 -0
- package/palmier-server/server/src/push.ts +68 -0
- package/palmier-server/server/src/routes/hosts.ts +45 -0
- package/palmier-server/server/src/routes/push.ts +100 -0
- package/palmier-server/server/tsconfig.json +20 -0
- package/palmier-server/spec.md +415 -0
- package/src/agents/agent-instructions.md +1 -1
- package/src/agents/agent.ts +23 -0
- package/src/agents/aider.ts +37 -0
- package/src/agents/cursor.ts +38 -0
- package/src/agents/deepagents.ts +38 -0
- package/src/agents/droid.ts +37 -0
- package/src/agents/goose.ts +35 -0
- package/src/agents/opencode.ts +38 -0
- package/src/agents/openhands.ts +38 -0
- package/src/commands/pair.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/rpc-handler.ts +5 -4
- package/src/transports/http-transport.ts +31 -43
- package/test/result-state.test.ts +110 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# Project Palmier: Architecture & Implementation Plan
|
|
2
|
+
|
|
3
|
+
Palmier is a platform enabling end-users to remotely schedule, manage, and execute autonomous tasks on their host machines via a Progressive Web App (PWA). It acts as a secure, distributed bridge between a user's mobile device/browser and a local host daemon running on their hardware.
|
|
4
|
+
|
|
5
|
+
## 1. System Architecture & Components
|
|
6
|
+
|
|
7
|
+
The system relies on a publish-subscribe model utilizing NATS to bypass firewall restrictions and enable real-time, bi-directional communication. All infrastructure runs on a single VPS (DigitalOcean), with automated CI/CD via GitHub Actions.
|
|
8
|
+
|
|
9
|
+
### 1.1 Platform Support
|
|
10
|
+
|
|
11
|
+
The host supports **Linux** (systemd) and **Windows** (Task Scheduler for both daemon and task triggers). macOS support (launchd) is planned. OS-specific details in this spec use Linux examples unless noted otherwise; the `PlatformService` abstraction handles cross-platform differences.
|
|
12
|
+
|
|
13
|
+
### 1.2 Components
|
|
14
|
+
|
|
15
|
+
* **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Localhost-only HTTP endpoints (`/notify`, `/request-input`, `/request-confirmation`, `/request-permission`) are used by agents and the `palmier run` process for interactive flows via held HTTP connections. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPlanGenerationCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
|
|
16
|
+
|
|
17
|
+
* **Web Server (Node.js):** Serves the PWA assets (React) via `app.palmier.me` (Cloudflare proxied), manages Web Push VAPID keys, and provides host registration. Uses **PostgreSQL** for persistent storage (host registrations, push subscriptions). Connects to NATS via TCP to subscribe to `host-event.>` for sending push notifications (confirmations, dismissals, completion/failure). For `POST /api/push/respond` (confirmation responses via push notification action buttons), the Web Server forwards the response to the host via the `task.user_input` NATS RPC. Subscribes to `host.*.push.send` NATS subjects to relay push notification requests from the host CLI. Co-located with the NATS server on the same machine.
|
|
18
|
+
|
|
19
|
+
* **PWA (React):** The user-facing frontend, primarily targeting mobile devices. Connects to the NATS server via **WebSockets** at `nats.palmier.me` (DNS only, not Cloudflare proxied, to avoid interference with persistent connections). No user accounts — paired hosts are stored in localStorage.
|
|
20
|
+
|
|
21
|
+
* **NATS Server:** The central message broker. Runs in Docker on the same machine as the Web Server.
|
|
22
|
+
|
|
23
|
+
### 1.3 Security & Authentication
|
|
24
|
+
|
|
25
|
+
* **NATS authentication:** The NATS server uses **token-based authentication** with a single shared token. Subject scoping (e.g., `host.<host_id>.>`) is enforced at the application layer.
|
|
26
|
+
|
|
27
|
+
* **Client tokens:** Each PWA device paired with a host receives a unique client token, generated and stored on the host. Client tokens are included in every RPC request and validated by the host before processing. Tokens do not expire and can be revoked via the `palmier clients` CLI.
|
|
28
|
+
|
|
29
|
+
* **Pairing:** Devices pair with hosts using a 6-character alphanumeric pairing code. The code serves as a routing key — the PWA sends the code to NATS subject `pair.<CODE>` or to the host's HTTP `POST /pair` endpoint. The host validates the code and returns a client token. Codes expire after 5 minutes or first successful use.
|
|
30
|
+
|
|
31
|
+
* **Future migration:** Token-based auth can be migrated to full **JWT/NKey Authentication** for finer-grained access control and dynamic credential issuance without restarting the NATS server.
|
|
32
|
+
|
|
33
|
+
### 1.4 Repository Structure
|
|
34
|
+
|
|
35
|
+
The project is split across two repositories:
|
|
36
|
+
|
|
37
|
+
* **`palmier`**: The host binary. A standalone Node.js CLI that runs on the user's machine.
|
|
38
|
+
* **`palmier-server`**: Contains both the Web Server (`server/`) and the PWA (`pwa/`, built with Vite + React). Uses **pnpm** for package management with a pnpm workspace.
|
|
39
|
+
|
|
40
|
+
## 2. Host Provisioning & Device Pairing
|
|
41
|
+
|
|
42
|
+
### 2.1 Host Provisioning
|
|
43
|
+
|
|
44
|
+
Each host machine is provisioned via `palmier init`, an interactive wizard that registers the host with the Palmier server.
|
|
45
|
+
|
|
46
|
+
`palmier init` is an interactive wizard that:
|
|
47
|
+
|
|
48
|
+
1. Detects installed agent CLIs.
|
|
49
|
+
2. Asks whether to enable LAN access and which HTTP port to use (default 9966).
|
|
50
|
+
3. Shows a summary of task storage directory, local access URL, LAN URL (if enabled), detected agents, and any existing tasks to recover. Asks for confirmation before proceeding.
|
|
51
|
+
4. Registers with the Palmier server via `POST <url>/api/hosts/register` — server returns `{ hostId, natsUrl, natsWsUrl, natsToken }`.
|
|
52
|
+
5. Saves config to `~/.config/palmier/host.json` (includes `httpPort`, `lanEnabled`, NATS credentials).
|
|
53
|
+
6. Installs a systemd user service (Linux) or Task Scheduler entry (Windows) and auto-enters pair mode.
|
|
54
|
+
|
|
55
|
+
The daemon automatically recovers existing tasks by reinstalling their system timers on startup.
|
|
56
|
+
|
|
57
|
+
The `serve` daemon always starts an HTTP server on the configured port. Three access modes are available:
|
|
58
|
+
|
|
59
|
+
**Local mode** (always available):
|
|
60
|
+
- HTTP server binds to `127.0.0.1:<port>`. The PWA is accessible at `http://localhost:<port>` without pairing or internet. The PWA is bundled with the host package. The serve daemon injects `window.__PALMIER_SERVE__=true` into the HTML; the PWA detects this and auto-connects.
|
|
61
|
+
|
|
62
|
+
**LAN mode** (enabled during init):
|
|
63
|
+
- HTTP server binds to `0.0.0.0:<port>`, making the PWA accessible from the local network at `http://<host-ip>:<port>`. Non-localhost access requires pairing via a pairing code. Push notifications are not available.
|
|
64
|
+
|
|
65
|
+
**Server mode** (NATS cloud relay, always on):
|
|
66
|
+
- Communication is relayed through the Palmier cloud server via NATS. PWA is accessed at `https://app.palmier.me`. Enables push notifications and remote access.
|
|
67
|
+
|
|
68
|
+
### 2.2 Device Pairing
|
|
69
|
+
|
|
70
|
+
Local access (`http://localhost:<port>`) requires no pairing — the PWA auto-connects with a placeholder host ID.
|
|
71
|
+
|
|
72
|
+
For LAN and server mode, `palmier pair` generates a 6-character pairing code from the charset `ABCDEFGHJKMNPQRSTUVWXYZ23456789` (excludes ambiguous O/0/I/1/L) and listens on both transports in parallel:
|
|
73
|
+
|
|
74
|
+
**Server mode (NATS):**
|
|
75
|
+
1. Host subscribes to `pair.<CODE>` on NATS with a 5-minute timeout.
|
|
76
|
+
2. User enters the code in the PWA at `https://app.palmier.me`.
|
|
77
|
+
3. Host validates the code, generates a client token via `addClient()`, and responds with `{ hostId, clientToken }`.
|
|
78
|
+
|
|
79
|
+
**LAN mode (HTTP):**
|
|
80
|
+
1. Host registers the code with the serve daemon via `POST /pair-register`.
|
|
81
|
+
2. User opens `http://<host-ip>:<port>` and enters the code. No host address field is shown since the request is same-origin.
|
|
82
|
+
3. PWA posts `POST /pair` with `{ code }` to the same origin. Host responds with `{ hostId, clientToken, directUrl }`.
|
|
83
|
+
|
|
84
|
+
In both cases, the PWA stores the paired host in localStorage and navigates to the dashboard. Codes expire after 5 minutes or first successful use.
|
|
85
|
+
|
|
86
|
+
### 2.3 Client Management
|
|
87
|
+
|
|
88
|
+
Client tokens are stored on the host in `~/.config/palmier/clients.json`. Each token is a 32-byte hex string.
|
|
89
|
+
|
|
90
|
+
* `palmier clients list` — shows tokens (truncated), labels, creation dates
|
|
91
|
+
* `palmier clients revoke <token>` — removes a client token
|
|
92
|
+
* `palmier clients revoke-all` — clears all clients
|
|
93
|
+
|
|
94
|
+
If no clients exist, the host skips client validation (backward compatibility for unpaired hosts).
|
|
95
|
+
|
|
96
|
+
### 2.4 NATS Communication
|
|
97
|
+
|
|
98
|
+
All communication is scoped per host. **Request-reply** is used for RPC-style calls (task CRUD, status queries) — the PWA publishes a request and receives a response on an auto-generated inbox, eliminating the need for separate response subjects.
|
|
99
|
+
|
|
100
|
+
The **RPC method is derived from the NATS subject**, not the message body. The host subscribes to `host.<host_id>.rpc.>` and extracts the method by splitting the subject at `rpc.` (e.g., `...rpc.task.create` → `task.create`). The message body contains the request parameters as JSON, including the `clientToken` field for authentication.
|
|
101
|
+
|
|
102
|
+
**Host RPC endpoints** (request-reply, subject: `host.<host_id>.rpc.<method>`):
|
|
103
|
+
|
|
104
|
+
| Method | Params | Description |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `task.list` | *(none)* | List all tasks with frontmatter, body, created_at, and current status. Returns `agents` array of detected CLIs, `host_platform`, and `version`. |
|
|
107
|
+
| `task.get` | `id` | Get a single task with frontmatter, body, and current status. |
|
|
108
|
+
| `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). |
|
|
109
|
+
| `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 |
|
|
110
|
+
| `task.delete` | `id` | Delete a task and its systemd timers |
|
|
111
|
+
| `task.run` | `id` | Start a task via system scheduler (`systemctl --user start` / `schtasks /run`) |
|
|
112
|
+
| `task.abort` | `id` | Stop a running task via system scheduler (`systemctl --user stop` / `schtasks /end`) |
|
|
113
|
+
| `task.user_input` | `id`, `value` | Respond to a pending request (confirmation, permission, or input). Resolves an in-memory pending request held by the serve daemon's HTTP endpoint. |
|
|
114
|
+
| `task.status` | `id` | Read current status from `status.json`, enriched with pending request state from in-memory registry |
|
|
115
|
+
| `task.result` | `id`, `run_id` | Read a run's TASKRUN.md conversational messages and metadata. Returns `{ messages: ConversationMessage[], task_name, agent, running_state, start_time, end_time }`. |
|
|
116
|
+
| `task.followup` | `id`, `run_id`, `message` | Send a follow-up message to an existing run. Appends user message + started status, invokes agent inline, appends result. |
|
|
117
|
+
| `task.stop_followup` | `id`, `run_id` | Stop an active follow-up. Kills the agent child process and appends a stopped status. |
|
|
118
|
+
| `task.reports` | `id`, `run_id`, `report_files` | Read one or more report files from the run directory. Supports `.md`, `.txt`, and image files (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp`). Text files return `{ file, content }`, images return `{ file, data_url }` (base64). |
|
|
119
|
+
| `task.logs` | `id` | Read recent journalctl logs for the task's systemd service |
|
|
120
|
+
| `taskrun.list` | `offset?`, `limit?`, `task_id?` | Read paginated run history from `history.jsonl` (default limit: 10). Optional `task_id` filter. Returns `{ entries, total }` where each entry is enriched with TASKRUN.md metadata. |
|
|
121
|
+
| `taskrun.delete` | `task_id`, `run_id` | Delete a run and its directory. |
|
|
122
|
+
|
|
123
|
+
All RPC requests include a `clientToken` field in the JSON payload. The host validates the token before processing the request.
|
|
124
|
+
|
|
125
|
+
**Host CLI → Web Server** (request-reply):
|
|
126
|
+
|
|
127
|
+
| Subject | Payload | Description |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `host.<host_id>.push.send` | `{ hostId, title, body }` | Send push notification to all paired devices (15s timeout) |
|
|
130
|
+
|
|
131
|
+
**Pub/Sub** (fire-and-forget, published by `palmier run`):
|
|
132
|
+
|
|
133
|
+
| Subject | Payload | Subscriber | Description |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| `host-event.<host_id>.<task_id>` | `{ event_type, ... }` | PWA, Web Server | Unified event subject. `event_type` is one of `"running-state"`, `"confirm-request"`, `"confirm-resolved"`, `"permission-request"`, `"permission-resolved"`, or `"report-generated"`. Payloads: running-state includes `{ running_state, name? }`, confirm-request includes `{ host_id }`, confirm-resolved includes `{ host_id, status }`, report-generated includes `{ name?, run_id, report_files }`. Same payload shape is used for both NATS and HTTP SSE. |
|
|
136
|
+
|
|
137
|
+
### 2.5 Push Subscription Management
|
|
138
|
+
|
|
139
|
+
Push notification subscriptions are stored in PostgreSQL, keyed by host ID and device endpoint. A host may have multiple paired devices (e.g., phone + tablet). All push notifications for a host are delivered to **all registered devices** for that host.
|
|
140
|
+
|
|
141
|
+
## 3. Data Model: The Task Directory
|
|
142
|
+
|
|
143
|
+
All tasks are stored locally on the Host machine under a `tasks/` directory relative to the project root (the directory where `palmier init` was run).
|
|
144
|
+
|
|
145
|
+
### Structure
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
history.jsonl # Project-level run history index (append-only JSONL: { task_id, run_id })
|
|
149
|
+
tasks/
|
|
150
|
+
└── <task-id>/
|
|
151
|
+
├── TASK.md # Current task definition (frontmatter + body)
|
|
152
|
+
├── status.json # Latest execution status (running_state, time_stamp, pid)
|
|
153
|
+
└── <timestamp>/ # Run directory (one per run, isolated per agent session)
|
|
154
|
+
├── TASKRUN.md # Conversational thread (frontmatter + message entries)
|
|
155
|
+
└── ... # Agent session files, reports, artifacts
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `TASKRUN.md` Format
|
|
159
|
+
|
|
160
|
+
TASKRUN files use a conversational format with YAML frontmatter and HTML comment delimiters separating messages:
|
|
161
|
+
|
|
162
|
+
```markdown
|
|
163
|
+
---
|
|
164
|
+
task_name: My Task
|
|
165
|
+
agent: claude
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
<!-- palmier:message role="status" time="1712282400000" type="started" -->
|
|
169
|
+
|
|
170
|
+
<!-- palmier:message role="user" time="1712282400100" -->
|
|
171
|
+
|
|
172
|
+
Run the audit and generate a report.
|
|
173
|
+
|
|
174
|
+
<!-- palmier:message role="assistant" time="1712282430000" attachments="report.md" -->
|
|
175
|
+
|
|
176
|
+
Audit complete. Generated report.
|
|
177
|
+
|
|
178
|
+
<!-- palmier:message role="status" time="1712282450000" type="finished" -->
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Frontmatter** contains `task_name` and `agent` (snapshotted at run creation time). Timing and state are derived from status messages:
|
|
182
|
+
- **running_state**: derived from the last status message. `"started"` with no prior terminal = task running. `"started"` with prior terminal = follow-up running (`"followup"`). Terminal types: `"finished"`, `"failed"`, `"aborted"`, `"stopped"`.
|
|
183
|
+
- **start_time**: `time` of the first `type="started"` status message
|
|
184
|
+
- **end_time**: `time` of the last terminal status message
|
|
185
|
+
|
|
186
|
+
**Message delimiter:** `<!-- palmier:message role="{role}" time="{ms}" [type="{type}"] [attachments="{files}"] -->`
|
|
187
|
+
|
|
188
|
+
- **role**: `"assistant"` (agent output), `"user"` (user input/permissions/confirmations), or `"status"` (lifecycle events)
|
|
189
|
+
- **time**: Unix timestamp in milliseconds
|
|
190
|
+
- **type** (optional): `"input"`, `"permission"`, `"confirmation"`, `"started"`, `"finished"`, `"failed"`, `"aborted"`, `"stopped"`
|
|
191
|
+
- **attachments** (optional): comma-separated report filenames
|
|
192
|
+
|
|
193
|
+
Messages are appended incrementally during execution.
|
|
194
|
+
|
|
195
|
+
### `TASK.md` Schema
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
---
|
|
199
|
+
id: "uuid-v4"
|
|
200
|
+
user_prompt: "Run a system audit and summarize large files..."
|
|
201
|
+
agent: "claude"
|
|
202
|
+
triggers:
|
|
203
|
+
- type: "cron"
|
|
204
|
+
value: "0 9 * * 1"
|
|
205
|
+
- type: "once"
|
|
206
|
+
value: "2026-03-20T15:00:00Z"
|
|
207
|
+
triggers_enabled: true
|
|
208
|
+
requires_confirmation: true
|
|
209
|
+
---
|
|
210
|
+
[Detailed execution plan generated by the non-interactive generation step]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
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.
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
#### Trigger Lifecycle
|
|
218
|
+
|
|
219
|
+
* **`triggers_enabled`:** Controls whether systemd timers are installed for the task's triggers. When `false`, all timers are removed; when toggled back to `true`, timers are reinstalled. Defaults to `true`. The task can still be run manually via "Run Now" regardless of this setting. The "Enable Triggers" checkbox only appears in the UI when the task has at least one trigger.
|
|
220
|
+
* **`cron` triggers:** Persist indefinitely. The systemd timer remains active until the task is deleted or triggers are disabled.
|
|
221
|
+
* **`once` triggers:** After firing, the trigger is removed from the `TASK.md` frontmatter and its corresponding systemd timer/service files are cleaned up. The task itself remains in the `tasks/` directory as a manual task (can still be executed on-demand via the PWA or CLI, but will not fire automatically again).
|
|
222
|
+
|
|
223
|
+
### Task Events
|
|
224
|
+
|
|
225
|
+
Task lifecycle status is persisted to a `status.json` file in the task directory on the host. The file contains `{ running_state, time_stamp, pid }` and is used primarily for crash detection. Interactive request flows (confirmation, permission, input) are handled via held HTTP connections on the serve daemon's in-memory pending request registry. The `running_state` is one of:
|
|
226
|
+
|
|
227
|
+
* **`started`** — task execution has begun (set immediately, before confirmation if applicable).
|
|
228
|
+
* **`finished`** — task completed successfully.
|
|
229
|
+
* **`aborted`** — task was aborted by the user (confirmation denied or manual abort via RPC).
|
|
230
|
+
* **`failed`** — task execution failed (the command exited with a non-zero code).
|
|
231
|
+
|
|
232
|
+
`palmier run` writes `status.json` and publishes a notification on `host-event.<host_id>.<task_id>` (payload: `{ event_type: "running-state", running_state }`) via NATS and HTTP SSE. The `time_stamp` field is UTC time in milliseconds since epoch (`Date.now()`).
|
|
233
|
+
|
|
234
|
+
The `task.list` RPC includes each task's current status (read from `status.json`). The `task.status` RPC returns the status for a single task.
|
|
235
|
+
|
|
236
|
+
The PWA receives initial statuses from `task.list` on load. It subscribes to `host-event.<activeHostId>.>` for live updates; on each notification it parses the `event_type` field and calls the host's `task.status` RPC to fetch the current status. Task cards display the `user_prompt` as the title (truncated to 2 lines) and a status indicator: a marching dots animation when running (`started`), a red dot for errors (`aborted` or `failed`), a gray dot when triggers are disabled or absent, and a green dot when idle (no entry or `finished`). When the last run was successful (`finished`), a "View Result" button loads the task's result file in a popup dialog. The "once" trigger date/time picker only allows selecting future dates and times.
|
|
237
|
+
|
|
238
|
+
The Web Server subscribes to `host-event.>` and sends push notifications based on `event_type`: confirmation pushes for `confirm-request`, dismiss pushes for `confirm-resolved`, permission pushes for `permission-request`, dismiss pushes for `permission-resolved`, and report-ready/failure pushes for `report-generated` events.
|
|
239
|
+
|
|
240
|
+
### Logs
|
|
241
|
+
|
|
242
|
+
Task execution logs are managed by systemd's journal. Each task's systemd service unit is tagged with the task ID, allowing logs to be queried with:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
journalctl --user -u palmier-task-<task-id>.service
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The host exposes a `task.logs` RPC handler that runs this query and returns recent log lines to the PWA.
|
|
249
|
+
|
|
250
|
+
## 4. UI & Task Management Flow
|
|
251
|
+
|
|
252
|
+
The PWA (React) provides a responsive CRUD interface.
|
|
253
|
+
|
|
254
|
+
The PWA connects to **one host at a time**. A host menu (hamburger drawer) lets the user switch between paired hosts. All hosts are selectable regardless of online status — the PWA does not probe or display host connectivity in the picker.
|
|
255
|
+
|
|
256
|
+
### 4.1 Initialization
|
|
257
|
+
|
|
258
|
+
1. PWA loads. If no hosts are paired, it shows an empty state with a "Pair Host" button.
|
|
259
|
+
|
|
260
|
+
2. If hosts are paired, PWA fetches NATS credentials from `GET /api/config` (returns `{ natsWsUrl, natsToken }`) and connects to NATS via WebSocket.
|
|
261
|
+
|
|
262
|
+
3. PWA sends a `task.list` request to `host.<host_id>.rpc.task.list` using NATS request-reply, including the `clientToken` in the payload.
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
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.
|
|
267
|
+
|
|
268
|
+
6. PWA discovers pending confirmations from the `task.list` RPC response — tasks with a pending confirmation, permission, or input request are shown as interactive modals. The PWA responds by calling the `task.user_input` RPC on the host, which resolves the in-memory pending request held by the serve daemon. The `run` process (blocked on an HTTP call to the serve daemon) receives the response and proceeds or exits accordingly.
|
|
269
|
+
|
|
270
|
+
### 4.2 Task Creation & Update
|
|
271
|
+
|
|
272
|
+
1. User clicks the "Describe your new task..." placeholder in the task list view, which opens the task form directly.
|
|
273
|
+
|
|
274
|
+
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).
|
|
275
|
+
|
|
276
|
+
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.
|
|
277
|
+
|
|
278
|
+
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.
|
|
279
|
+
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
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.
|
|
283
|
+
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
### 4.3 On-Demand Execution
|
|
287
|
+
|
|
288
|
+
Any task that is not currently running can be executed immediately:
|
|
289
|
+
|
|
290
|
+
* **PWA:** A "Run Now" button is shown on each task card when the task is not already running. Clicking it sends a `task.run` request via NATS request-reply to the Host, which starts execution via the system scheduler (`systemctl --user start` on Linux, `schtasks /run` on Windows).
|
|
291
|
+
* **CLI:** `palmier run <task-id>` executes the task directly (outside the system scheduler).
|
|
292
|
+
|
|
293
|
+
Both paths follow the same execution loop described in §5.2 (including confirmation checks if configured). The system scheduler prevents concurrent runs of the same task — if the service/task is already active, the start command is a no-op.
|
|
294
|
+
|
|
295
|
+
### 4.4 Task Deletion
|
|
296
|
+
|
|
297
|
+
1. PWA sends `task.delete` via NATS request-reply.
|
|
298
|
+
|
|
299
|
+
2. Host runs `systemctl --user stop palmier-task-<task-id>.timer`, disables it, deletes the systemd files, removes the `tasks/<task-id>` directory, and runs `daemon-reload`.
|
|
300
|
+
|
|
301
|
+
## 5. Task Execution & Host Interaction
|
|
302
|
+
|
|
303
|
+
### 5.1 Execution Architecture
|
|
304
|
+
|
|
305
|
+
Task execution is handled by `palmier run <task-id>`, a short-lived process that resolves the task's `agent` field to an `AgentTool` implementation, which constructs the full command line. The `agent` field defaults to `"claude"`. Each execution is its own process — systemd manages its lifecycle via the `.service` unit.
|
|
306
|
+
|
|
307
|
+
The persistent host process monitors running tasks via a **crash detection polling loop**: every 30 seconds, it scans all tasks with `running_state: "started"` and queries the system scheduler (`systemctl --user is-active` on Linux, `schtasks /query` on Windows) to check if the task process is still alive. If the scheduler reports the task is no longer running but `status.json` still says `"started"`, the daemon marks it as failed, writes a RESULT file, appends to history, and broadcasts the failure event. This also runs once at daemon startup to reconcile any tasks that crashed while the daemon was offline. Real-time task events are broadcast via a shared events module (`events.ts`) that publishes to NATS pub/sub and the serve daemon's HTTP SSE endpoint.
|
|
308
|
+
|
|
309
|
+
### 5.2 The Execution Loop
|
|
310
|
+
|
|
311
|
+
When `palmier run <task-id>` executes (triggered by a systemd timer, `systemctl start` from the host's `task.run` RPC handler, or direct CLI invocation):
|
|
312
|
+
|
|
313
|
+
1. **Confirmation Check:**
|
|
314
|
+
|
|
315
|
+
* Reads `TASK.md`. If `requires_confirmation: true`:
|
|
316
|
+
|
|
317
|
+
* `palmier run` publishes a `started` event on `host-event.<host_id>.<task_id>`, then POSTs to the serve daemon's `/request-confirmation` HTTP endpoint. This registers an in-memory pending request and publishes a `confirm-request` event via NATS and SSE. The Web Server subscribes to `host-event.>` and sends a push notification.
|
|
318
|
+
|
|
319
|
+
* The user responds either via the PWA (which calls the `task.user_input` RPC on the host) or via the push notification action buttons (Service Worker calls `POST /api/push/respond`, Web Server forwards to the `task.user_input` RPC). Both paths resolve the in-memory pending request on the serve daemon.
|
|
320
|
+
|
|
321
|
+
* The `/request-confirmation` HTTP response returns to `palmier run` with `{ confirmed: true/false }`. If confirmed, it proceeds. If aborted, it publishes an `aborted` event and exits.
|
|
322
|
+
|
|
323
|
+
2. **Launching the Task Process:**
|
|
324
|
+
|
|
325
|
+
* `palmier run` resolves the task's `agent` field to an `AgentTool` implementation and calls `getTaskRunCommandLine(task)` to obtain the command and arguments. The process is spawned directly (without a shell). stdin is closed (equivalent to `< /dev/null`) to prevent tools from hanging on an open pipe. The working directory is the project root (from `host.json`). The environment variable `PALMIER_TASK_ID=<task-id>` is set for identification.
|
|
326
|
+
|
|
327
|
+
* 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.
|
|
328
|
+
|
|
329
|
+
* 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.
|
|
330
|
+
|
|
331
|
+
3. **Completion:**
|
|
332
|
+
|
|
333
|
+
* When the child process exits successfully, `palmier run` publishes a `finished` event on `host-event` and persists it to `status.json`. If the process exits with a non-zero code, it publishes a `failed` event instead. If report files were generated, `palmier run` also publishes a `report-generated` event; the Web Server sends a push notification when it receives this event.
|
|
334
|
+
|
|
335
|
+
### 5.3 Command-Triggered Execution
|
|
336
|
+
|
|
337
|
+
When a task has a `command` field set, `palmier run` enters command-triggered mode after the confirmation check:
|
|
338
|
+
|
|
339
|
+
1. **Spawn the command** using `shell: true` (allowing pipes, redirects, etc.) with stdout piped. stdin is closed. stderr is forwarded to the palmier process's stderr.
|
|
340
|
+
|
|
341
|
+
2. **Read stdout line by line** using Node's `readline` interface. Empty lines are skipped.
|
|
342
|
+
|
|
343
|
+
3. **For each line**, invoke the agent CLI:
|
|
344
|
+
* Build a per-line prompt: `user_prompt + "\n\nProcess this input:\n" + <line>` + the standard task outcome suffix.
|
|
345
|
+
* Call `agent.getTaskRunCommandLine()` with the augmented prompt.
|
|
346
|
+
* Spawn the agent via `spawnCommand()` and collect output.
|
|
347
|
+
* The standard permission/input retry loop applies: if the agent requests permissions or user input, line processing pauses, the user is prompted, and the invocation retries once resolved. Granted permissions accumulate across lines within the same run.
|
|
348
|
+
|
|
349
|
+
4. **Sequential processing with bounded queue**: lines are processed one at a time. If lines arrive faster than agent invocations complete, they queue up to a max of 100 entries. Overflow drops the oldest unprocessed line.
|
|
350
|
+
|
|
351
|
+
5. **On command exit or signal**: each agent invocation is written as a conversation entry in the RESULT file. Per-line agent outputs are also logged to `command-output.log` in the task directory.
|
|
352
|
+
|
|
353
|
+
6. **Composable with triggers**: cron/once triggers start `palmier run` on schedule, which spawns the command. The command runs until it exits or the task is aborted.
|
|
354
|
+
|
|
355
|
+
### 5.4 Failsafes & Constraints
|
|
356
|
+
|
|
357
|
+
* **Crash Detection:** The `palmier serve` daemon polls every 30 seconds, querying the system scheduler to detect tasks whose process exited without updating `status.json`. Detected crashes append a failed status entry to the existing RESULT file and broadcast the failure. This also runs at daemon startup to catch crashes that occurred while the daemon was offline.
|
|
358
|
+
|
|
359
|
+
* **Process Tracking:** Each `palmier run` process writes its PID to `status.json`. On abort, `taskkill /pid <pid> /f /t` (Windows) or `systemctl --user stop` (Linux) kills the entire process tree. On Windows, tasks use S4U LogonType in Task Scheduler to run without visible console windows.
|
|
360
|
+
|
|
361
|
+
* **No Remote Timeout:** If a confirmation request is sent to the user's devices and the user does not respond, the task continues to wait indefinitely. The user can always respond via the PWA. There is no automatic deny-on-timeout.
|
|
362
|
+
|
|
363
|
+
* **Confirmation Cleanup:** If `palmier run` is killed during a pending confirmation, the in-memory pending request on the serve daemon is orphaned. This is harmless — the `task.user_input` RPC will return `"not pending"` since the pending entry is removed when the HTTP connection closes.
|
|
364
|
+
|
|
365
|
+
* **No Execution Time Limit:** Tasks may be long-running by design. There is no global execution timeout.
|
|
366
|
+
|
|
367
|
+
## 6. Agent HTTP Endpoints
|
|
368
|
+
|
|
369
|
+
The serve daemon exposes localhost-only HTTP endpoints that agents call during task execution. The port and task ID are baked into the agent's system prompt via template variables (`{{PORT}}`, `{{TASK_ID}}`).
|
|
370
|
+
|
|
371
|
+
### 6.1 Endpoints
|
|
372
|
+
|
|
373
|
+
* **`POST /notify`** — Sends a push notification to all paired devices. Body: `{ title, body }`. The serve daemon forwards to NATS `host.<host_id>.push.send`; the Web Server delivers via Web Push. Requires server mode.
|
|
374
|
+
|
|
375
|
+
* **`POST /request-input`** — Requests input from the user during task execution. Body: `{ taskId, descriptions }`. The connection is held open until the user responds via the PWA (`task.user_input` RPC). Returns `{ values: [...] }` on success or `{ aborted: true }` if declined.
|
|
376
|
+
|
|
377
|
+
* **`POST /request-confirmation`** — Requests task confirmation. Body: `{ taskId, taskName }`. Called by `palmier run` (not agents). Returns `{ confirmed: boolean }`.
|
|
378
|
+
|
|
379
|
+
* **`POST /request-permission`** — Requests permission grants. Body: `{ taskId, taskName, permissions }`. Called by `palmier run` (not agents). Returns `{ response: "granted" | "granted_all" | "aborted" }`.
|
|
380
|
+
|
|
381
|
+
## 7. Database Schema (PostgreSQL)
|
|
382
|
+
|
|
383
|
+
```sql
|
|
384
|
+
-- Host registrations
|
|
385
|
+
CREATE TABLE hosts (
|
|
386
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
387
|
+
name VARCHAR(255),
|
|
388
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
-- Push notification subscriptions (Web Push)
|
|
392
|
+
CREATE TABLE push_subscriptions (
|
|
393
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
394
|
+
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
|
|
395
|
+
endpoint TEXT NOT NULL,
|
|
396
|
+
p256dh TEXT NOT NULL,
|
|
397
|
+
auth TEXT NOT NULL,
|
|
398
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
399
|
+
UNIQUE(host_id, endpoint)
|
|
400
|
+
);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## 8. Web Server API Endpoints
|
|
404
|
+
|
|
405
|
+
All endpoints are served over HTTPS. No user authentication is required — the server is stateless with respect to user identity.
|
|
406
|
+
|
|
407
|
+
| Method | Path | Description |
|
|
408
|
+
|--------|------|-------------|
|
|
409
|
+
| `POST` | `/api/hosts/register` | Register a new host. Returns `{ hostId, natsUrl, natsWsUrl, natsToken }`. Rate-limited by IP. |
|
|
410
|
+
| `GET` | `/api/config` | Returns NATS WebSocket credentials for the PWA: `{ natsWsUrl, natsToken }`. |
|
|
411
|
+
| `POST` | `/api/push/subscribe` | Register a push subscription. Body: `{ hostId, endpoint, keys: { p256dh, auth } }`. |
|
|
412
|
+
| `DELETE` | `/api/push/subscribe` | Unregister a push subscription. Body: `{ hostId, endpoint }`. |
|
|
413
|
+
| `GET` | `/api/push/vapid-key` | Returns the server's VAPID public key for push subscription. |
|
|
414
|
+
| `POST` | `/api/push/respond` | Called by Service Worker to relay user responses to task confirmations. Body: `{ type, task_id, host_id, response }`. Web Server forwards the response to the host via the `host.<host_id>.rpc.task.user_input` NATS RPC. |
|
|
415
|
+
| `GET` | `/health` | Health check. |
|
|
@@ -28,7 +28,7 @@ The request blocks until the user responds. Response: `{"values":["answer1","ans
|
|
|
28
28
|
|
|
29
29
|
**Sending push notifications** — To notify the user, POST to `/notify` with:
|
|
30
30
|
```json
|
|
31
|
-
{"title":"...","body":"..."}
|
|
31
|
+
{"taskId":"{{TASK_ID}}","title":"...","body":"..."}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
---
|
package/src/agents/agent.ts
CHANGED
|
@@ -2,16 +2,25 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
|
2
2
|
import { ClaudeAgent } from "./claude.js";
|
|
3
3
|
import { GeminiAgent } from "./gemini.js";
|
|
4
4
|
import { CodexAgent } from "./codex.js";
|
|
5
|
+
import { DroidAgent } from "./droid.js";
|
|
5
6
|
import { OpenClawAgent } from "./openclaw.js";
|
|
6
7
|
import { CopilotAgent } from "./copilot.js";
|
|
7
8
|
import { QwenAgent } from "./qwen.js";
|
|
8
9
|
import { KimiAgent } from "./kimi.js";
|
|
10
|
+
import { GooseAgent } from "./goose.js";
|
|
11
|
+
import { OpenCodeAgent } from "./opencode.js";
|
|
12
|
+
import { DeepAgents } from "./deepagents.js";
|
|
13
|
+
import { Aider } from "./aider.js";
|
|
14
|
+
import { OpenHands } from "./openhands.js";
|
|
15
|
+
import { Cursor } from "./cursor.js";
|
|
9
16
|
|
|
10
17
|
export interface CommandLine {
|
|
11
18
|
command: string;
|
|
12
19
|
args: string[];
|
|
13
20
|
/** If provided, the string is written to the process's stdin and then the pipe is closed. */
|
|
14
21
|
stdin?: string;
|
|
22
|
+
/** Additional environment variables to set for the spawned process. */
|
|
23
|
+
env?: Record<string, string>;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
/**
|
|
@@ -45,16 +54,30 @@ const agentRegistry: Record<string, AgentTool> = {
|
|
|
45
54
|
copilot: new CopilotAgent(),
|
|
46
55
|
qwen: new QwenAgent(),
|
|
47
56
|
kimi: new KimiAgent(),
|
|
57
|
+
droid: new DroidAgent(),
|
|
58
|
+
goose: new GooseAgent(),
|
|
59
|
+
opencode: new OpenCodeAgent(),
|
|
60
|
+
deepagents: new DeepAgents(),
|
|
61
|
+
aider: new Aider(),
|
|
62
|
+
openhands: new OpenHands(),
|
|
63
|
+
cursor: new Cursor(),
|
|
48
64
|
};
|
|
49
65
|
|
|
50
66
|
const agentLabels: Record<string, string> = {
|
|
51
67
|
claude: "Claude Code",
|
|
52
68
|
gemini: "Gemini CLI",
|
|
53
69
|
codex: "Codex CLI",
|
|
70
|
+
droid: "Droid CLI",
|
|
54
71
|
openclaw: "OpenClaw",
|
|
55
72
|
copilot: "Copilot CLI",
|
|
56
73
|
qwen: "Qwen Code",
|
|
57
74
|
kimi: "Kimi Code",
|
|
75
|
+
goose: "Goose CLI",
|
|
76
|
+
opencode: "OpenCode",
|
|
77
|
+
deepagents: "Deep Agents CLI",
|
|
78
|
+
aider: "Aider",
|
|
79
|
+
openhands: "OpenHands",
|
|
80
|
+
cursor: "Cursor CLI",
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
export interface DetectedAgent {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class Aider implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "aider",
|
|
12
|
+
args: ["--message", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = [];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--yes-always");
|
|
23
|
+
}
|
|
24
|
+
args.push("--message", prompt);
|
|
25
|
+
|
|
26
|
+
return { command: "aider", args};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async init(): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
execSync("aider --version", { stdio: "ignore", shell: SHELL });
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class Cursor implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "cursor",
|
|
12
|
+
args: ["-p", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = [];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--force");
|
|
23
|
+
}
|
|
24
|
+
if (followupPrompt) {args.push("--continue");} // continue mode for followups
|
|
25
|
+
args.push("-p", prompt);
|
|
26
|
+
|
|
27
|
+
return { command: "cursor", args};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async init(): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
execSync("cursor --version", { stdio: "ignore", shell: SHELL });
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class DeepAgents implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "deepagents",
|
|
12
|
+
args: ["--non-interactive", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = [];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--auto-approve");
|
|
23
|
+
}
|
|
24
|
+
if (followupPrompt) {args.push("--resume");} // continue mode for followups
|
|
25
|
+
args.push("--non-interactive", prompt);
|
|
26
|
+
|
|
27
|
+
return { command: "deepagents", args};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async init(): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
execSync("deepagents --version", { stdio: "ignore", shell: SHELL });
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class DroidAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "droid",
|
|
12
|
+
args: ["exec", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["exec", "--session-id", task.frontmatter.id];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--skip-permissions-unsafe");
|
|
23
|
+
}
|
|
24
|
+
args.push(prompt);
|
|
25
|
+
|
|
26
|
+
return { command: "droid", args};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async init(): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
execSync("droid --version", { stdio: "ignore", shell: SHELL });
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|