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,355 @@
|
|
|
1
|
+
# Production Deployment Guide
|
|
2
|
+
|
|
3
|
+
Single-instance deployment on Ubuntu 24 with TLS support. NATS and PostgreSQL run in Docker; the Palmier web server runs directly on the host via systemd. Caddy handles TLS termination with automatic Let's Encrypt certificates. Deployments are automated via GitHub Actions — pushing to `main` triggers a CI build, then deploys to the VPS over SSH.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Internet
|
|
9
|
+
│
|
|
10
|
+
├── palmier.me → Cloudflare → Caddy → redirect to www
|
|
11
|
+
├── www.palmier.me → Cloudflare Pages (landing site, static)
|
|
12
|
+
├── app.palmier.me → Cloudflare → Caddy → localhost:3000 (Palmier server, HTTPS)
|
|
13
|
+
└── nats.palmier.me → Caddy (direct) → localhost:9222 (NATS WebSocket)
|
|
14
|
+
→ port 4222 direct (NATS TCP for hosts)
|
|
15
|
+
|
|
16
|
+
Caddy (ports 80/443, auto TLS)
|
|
17
|
+
|
|
18
|
+
Docker
|
|
19
|
+
├── NATS (ports 4222 TCP public, 9222 WS localhost)
|
|
20
|
+
└── PostgreSQL (port 5432 localhost)
|
|
21
|
+
|
|
22
|
+
systemd
|
|
23
|
+
└── Palmier Server (port 3000 localhost)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
Three DNS records pointing to your server's public IP:
|
|
29
|
+
|
|
30
|
+
- `palmier.me` — root domain (Cloudflare proxied, redirects to `www`)
|
|
31
|
+
- `www.palmier.me` — Landing site (Cloudflare Pages, static)
|
|
32
|
+
- `app.palmier.me` — PWA + API over HTTPS (Cloudflare proxied)
|
|
33
|
+
- `nats.palmier.me` — NATS WebSocket + TCP (DNS only, no Cloudflare proxy)
|
|
34
|
+
|
|
35
|
+
### Install Node.js 24+
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
|
|
39
|
+
sudo apt install -y nodejs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Install pnpm
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
sudo npm install -g pnpm
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Install Docker
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sudo apt install -y docker.io docker-compose-v2
|
|
52
|
+
sudo usermod -aG docker $USER
|
|
53
|
+
# Log out and back in for group to take effect
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Install Caddy
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sudo apt install -y caddy
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 1. TLS with Caddy
|
|
63
|
+
|
|
64
|
+
Caddy auto-provisions Let's Encrypt certificates with zero configuration.
|
|
65
|
+
|
|
66
|
+
Create `/etc/caddy/Caddyfile`:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
palmier.me {
|
|
70
|
+
redir https://www.palmier.me{uri} permanent
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
app.palmier.me {
|
|
74
|
+
reverse_proxy localhost:3000
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
nats.palmier.me {
|
|
78
|
+
@websocket {
|
|
79
|
+
header Connection *Upgrade*
|
|
80
|
+
header Upgrade websocket
|
|
81
|
+
}
|
|
82
|
+
reverse_proxy @websocket localhost:9222
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This gives you:
|
|
87
|
+
|
|
88
|
+
- `https://app.palmier.me` — PWA + API (auto TLS, Cloudflare CDN for static assets)
|
|
89
|
+
- `wss://nats.palmier.me` — NATS WebSocket (auto TLS, direct to VPS)
|
|
90
|
+
- `https://www.palmier.me` — Landing site (Cloudflare Pages, no VPS involvement)
|
|
91
|
+
|
|
92
|
+
LAN mode is served directly by the palmier host binary (reverse-proxying PWA assets from `app.palmier.me`), so no separate domain is needed.
|
|
93
|
+
|
|
94
|
+
Caddy auto-provisions Let's Encrypt certs for both domains. Set Cloudflare SSL/TLS mode to **Full (strict)** so Cloudflare verifies the origin cert when connecting to your VPS.
|
|
95
|
+
|
|
96
|
+
No need to configure TLS in NATS itself.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
sudo systemctl enable --now caddy
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 2. Docker Compose for NATS + PostgreSQL
|
|
103
|
+
|
|
104
|
+
Create a directory for production Docker config:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
mkdir -p ~/palmier-prod
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### docker-compose.yml
|
|
111
|
+
|
|
112
|
+
Create `~/palmier-prod/docker-compose.yml`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
services:
|
|
116
|
+
nats:
|
|
117
|
+
image: nats:2-alpine
|
|
118
|
+
restart: unless-stopped
|
|
119
|
+
ports:
|
|
120
|
+
- "4222:4222"
|
|
121
|
+
- "127.0.0.1:9222:9222"
|
|
122
|
+
volumes:
|
|
123
|
+
- ./nats.conf:/etc/nats/nats.conf:ro
|
|
124
|
+
- nats-data:/data
|
|
125
|
+
command: ["-c", "/etc/nats/nats.conf"]
|
|
126
|
+
|
|
127
|
+
postgres:
|
|
128
|
+
image: postgres:17-alpine
|
|
129
|
+
restart: unless-stopped
|
|
130
|
+
ports:
|
|
131
|
+
- "127.0.0.1:5432:5432"
|
|
132
|
+
environment:
|
|
133
|
+
POSTGRES_DB: palmier
|
|
134
|
+
POSTGRES_USER: palmier
|
|
135
|
+
POSTGRES_PASSWORD: <generate-a-strong-password>
|
|
136
|
+
volumes:
|
|
137
|
+
- pgdata:/var/lib/postgresql/data
|
|
138
|
+
|
|
139
|
+
volumes:
|
|
140
|
+
nats-data:
|
|
141
|
+
pgdata:
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### nats.conf
|
|
145
|
+
|
|
146
|
+
Create `~/palmier-prod/nats.conf`:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
listen: 0.0.0.0:4222
|
|
150
|
+
|
|
151
|
+
websocket {
|
|
152
|
+
listen: "0.0.0.0:9222"
|
|
153
|
+
no_tls: true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
authorization {
|
|
157
|
+
token: "<generate-a-new-token>"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Generate secrets:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# NATS token
|
|
165
|
+
openssl rand -hex 32
|
|
166
|
+
|
|
167
|
+
# Postgres password
|
|
168
|
+
openssl rand -hex 16
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Start the containers:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
cd ~/palmier-prod
|
|
175
|
+
docker compose up -d
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 3. NATS TCP for Remote Hosts
|
|
179
|
+
|
|
180
|
+
Hosts connect to NATS over TCP (port 4222). Since Caddy only handles HTTP/WebSocket, port 4222 must be open on the firewall for hosts to connect. The NATS token provides authentication.
|
|
181
|
+
|
|
182
|
+
For encrypted host connections, you can add a TLS-terminating TCP proxy (e.g., nginx stream block) in front of port 4222. For a single-user setup, token auth alone is sufficient.
|
|
183
|
+
|
|
184
|
+
## 4. Firewall
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
sudo ufw allow 22/tcp # SSH (must allow before enabling!)
|
|
188
|
+
sudo ufw allow 80/tcp # Caddy HTTP -> HTTPS redirect
|
|
189
|
+
sudo ufw allow 443/tcp # Caddy HTTPS
|
|
190
|
+
sudo ufw allow 4222/tcp # NATS TCP for hosts
|
|
191
|
+
sudo ufw enable
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 5. Clone the Repository
|
|
195
|
+
|
|
196
|
+
Set up VPS git access first (see [VPS Git Access](#vps-git-access)), then clone:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
mkdir -p ~/source
|
|
200
|
+
cd ~/source
|
|
201
|
+
git clone git@github.com:caihongxu/palmier-server.git
|
|
202
|
+
cd palmier-server
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The first push to `main` will build and start the server automatically via GitHub Actions.
|
|
206
|
+
|
|
207
|
+
## 6. Configure Environment
|
|
208
|
+
|
|
209
|
+
Create `server/.env`:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
PORT=3000
|
|
213
|
+
DATABASE_URL=postgresql://palmier:<your-pg-password>@localhost:5432/palmier
|
|
214
|
+
|
|
215
|
+
NATS_URL=nats://localhost:4222
|
|
216
|
+
NATS_HOST_URL=nats://nats.palmier.me:4222
|
|
217
|
+
NATS_WS_URL=wss://nats.palmier.me
|
|
218
|
+
NATS_TOKEN=<your-nats-token-from-nats.conf>
|
|
219
|
+
|
|
220
|
+
VAPID_PUBLIC_KEY=<generated>
|
|
221
|
+
VAPID_PRIVATE_KEY=<generated>
|
|
222
|
+
VAPID_MAILTO=mailto:you@palmier.me
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Key differences from development:
|
|
226
|
+
|
|
227
|
+
- `NATS_WS_URL` uses `wss://` through Caddy (not direct `ws://`)
|
|
228
|
+
- `NATS_HOST_URL` uses your public domain so remote hosts can reach NATS
|
|
229
|
+
|
|
230
|
+
Generate VAPID keys:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npx web-push generate-vapid-keys
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## 7. Palmier systemd Service
|
|
237
|
+
|
|
238
|
+
Create `/etc/systemd/system/palmier-server.service`:
|
|
239
|
+
|
|
240
|
+
```ini
|
|
241
|
+
[Unit]
|
|
242
|
+
Description=Palmier Server
|
|
243
|
+
After=network.target docker.service
|
|
244
|
+
|
|
245
|
+
[Service]
|
|
246
|
+
Type=simple
|
|
247
|
+
User=hongxu
|
|
248
|
+
WorkingDirectory=/home/hongxu/source/palmier-server/server
|
|
249
|
+
ExecStart=/usr/bin/node dist/index.js
|
|
250
|
+
Restart=on-failure
|
|
251
|
+
RestartSec=5
|
|
252
|
+
EnvironmentFile=/home/hongxu/source/palmier-server/server/.env
|
|
253
|
+
|
|
254
|
+
[Install]
|
|
255
|
+
WantedBy=multi-user.target
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Enable and start:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
sudo systemctl daemon-reload
|
|
262
|
+
sudo systemctl enable --now palmier-server
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## 8. Verify
|
|
266
|
+
|
|
267
|
+
At this point, all services should be running (Docker from step 2, Caddy from step 1, Palmier from step 7). Verify:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
sudo systemctl status caddy
|
|
271
|
+
sudo systemctl status palmier-server
|
|
272
|
+
cd ~/palmier-prod && docker compose ps
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- `https://app.palmier.me` — should load the PWA (server mode)
|
|
276
|
+
- `https://app.palmier.me/health` — should return OK
|
|
277
|
+
- Logs: `journalctl -u palmier-server -f`
|
|
278
|
+
- Docker logs: `cd ~/palmier-prod && docker compose logs -f`
|
|
279
|
+
|
|
280
|
+
## CI/CD with GitHub Actions
|
|
281
|
+
|
|
282
|
+
Deployments are automated via two GitHub Actions workflows in `.github/workflows/`:
|
|
283
|
+
|
|
284
|
+
- **`ci.yml`** — runs on every push (except `main`) and pull requests. Builds server + PWA to catch errors early.
|
|
285
|
+
- **`deploy.yml`** — runs on push to `main`. Builds in CI first, then SSHs into the VPS to pull, build, and restart the service.
|
|
286
|
+
|
|
287
|
+
### GitHub Secrets
|
|
288
|
+
|
|
289
|
+
Add these in the repo settings (Settings → Secrets and variables → Actions):
|
|
290
|
+
|
|
291
|
+
| Secret | Description |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `VPS_HOST` | Droplet IP address |
|
|
294
|
+
| `VPS_USER` | SSH username (e.g., `hongxu`) |
|
|
295
|
+
| `SSH_PRIVATE_KEY` | Ed25519 private key for SSH access |
|
|
296
|
+
|
|
297
|
+
### VPS SSH Setup
|
|
298
|
+
|
|
299
|
+
Generate a deploy key pair and add the public key to the VPS:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/palmier-deploy -N ""
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Append the public key to the VPS `~/.ssh/authorized_keys`. Add the private key as the `SSH_PRIVATE_KEY` GitHub secret.
|
|
306
|
+
|
|
307
|
+
### VPS Git Access
|
|
308
|
+
|
|
309
|
+
The VPS needs to pull from the private repo. Add a read-only deploy key:
|
|
310
|
+
|
|
311
|
+
1. Generate a key on the VPS: `ssh-keygen -t ed25519 -C "palmier-server-deploy" -f ~/.ssh/deploy-key -N ""`
|
|
312
|
+
2. Add the public key to the repo (Settings → Deploy keys, read-only)
|
|
313
|
+
3. Configure SSH on the VPS to use the key for GitHub:
|
|
314
|
+
```
|
|
315
|
+
# ~/.ssh/config
|
|
316
|
+
Host github.com
|
|
317
|
+
IdentityFile ~/.ssh/deploy-key
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Passwordless sudo for deploy
|
|
321
|
+
|
|
322
|
+
The deploy workflow needs to restart the systemd service. Allow the deploy user to do this without a password:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
sudo visudo -f /etc/sudoers.d/palmier-deploy
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Add:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
hongxu ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart palmier-server
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Deploying
|
|
335
|
+
|
|
336
|
+
Push to `main` and the deploy workflow handles everything automatically. To deploy manually:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
ssh hongxu@<vps-ip>
|
|
340
|
+
cd ~/source/palmier-server
|
|
341
|
+
git pull
|
|
342
|
+
pnpm install --frozen-lockfile
|
|
343
|
+
cd pwa && pnpm build && cd ..
|
|
344
|
+
cd server && pnpm build && cd ..
|
|
345
|
+
sudo systemctl restart palmier-server
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Component Summary
|
|
349
|
+
|
|
350
|
+
| Component | Runtime | Ports |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| PostgreSQL | Docker | 5432 (localhost only) |
|
|
353
|
+
| NATS | Docker | 4222 (public, for hosts), 9222 (localhost, proxied by Caddy) |
|
|
354
|
+
| Palmier Server | systemd + Node.js | 3000 (localhost only) |
|
|
355
|
+
| Caddy | systemd | 80, 443 (public) |
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Palmier Server
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Palmier is a platform for remotely scheduling, managing, and executing autonomous AI tasks on host machines via a progressive web app. Uses NATS for real-time communication (pub/sub and request-reply) and push notifications for task progress.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
+-----------+ +----------------+ +------------------+
|
|
11
|
+
| | HTTP | | NATS | |
|
|
12
|
+
| PWA |------>| Web Server |<----->| NATS Server |
|
|
13
|
+
| (React) | | (Express) | | |
|
|
14
|
+
| | | | | |
|
|
15
|
+
+-----------+ +----------------+ +------------------+
|
|
16
|
+
| | ^
|
|
17
|
+
| NATS WebSocket | PostgreSQL | NATS
|
|
18
|
+
+-------------------------------------------> |
|
|
19
|
+
| |
|
|
20
|
+
+----v----+ +-------+-------+
|
|
21
|
+
| | | |
|
|
22
|
+
| DB | | Host(s) |
|
|
23
|
+
| (PG) | | (separate |
|
|
24
|
+
| | | repo) |
|
|
25
|
+
+---------+ +---------------+
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **PWA** -- React 19 + Vite progressive web app. Connects to NATS over WebSocket for real-time task updates and to the web server for host registration and push notifications. No user accounts — paired hosts are stored in localStorage.
|
|
29
|
+
- **Web Server** -- Express + TypeScript API server. Handles host registration, push notifications (subscribes to `host-event.>` pub/sub for confirmation and completion events), and push notification relay (for host CLI requests via NATS). In production, also serves the built PWA static files.
|
|
30
|
+
- **NATS Server** -- Message broker. Provides pub/sub messaging and request-reply for real-time communication between all components.
|
|
31
|
+
- **Host** -- Runs on remote Linux/Windows machines to execute tasks via pluggable agent tools (e.g., Claude Code, Codex, Gemini). Each agent implements an `AgentTool` interface that handles command construction. Communicates with the platform over NATS and exposes a local HTTP server with agent-facing endpoints (`/notify`, `/request-input`) for task execution flows. See the [palmier](https://github.com/caihongxu/palmier) repo.
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
- Node.js 24+
|
|
36
|
+
- pnpm (`npm install -g pnpm`)
|
|
37
|
+
- PostgreSQL 17
|
|
38
|
+
- NATS server with WebSocket enabled
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
1. **Clone the repository**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/caihongxu/palmier-server.git
|
|
46
|
+
cd palmier-server
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. **Install dependencies** (uses pnpm workspaces)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
3. **Configure environment variables**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cp server/.env.example server/.env
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Edit `server/.env` and fill in the values (see table below).
|
|
62
|
+
|
|
63
|
+
4. **Generate VAPID keys** for web push notifications
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx web-push generate-vapid-keys
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Copy the public and private keys into `server/.env`.
|
|
70
|
+
|
|
71
|
+
5. **Create the PostgreSQL database**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
createdb palmier
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Update `DATABASE_URL` in `server/.env` with your connection string. Tables are created automatically on server startup.
|
|
78
|
+
|
|
79
|
+
6. **Start the NATS server** with the included config
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
nats-server -c nats.conf
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The config enables WebSocket on port 9222 and token-based auth.
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
Development uses two servers: Vite for the PWA (with hot module replacement) and Express for the API. Vite proxies `/api/*` requests to Express so everything appears same-origin.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Terminal 1 -- API server (port 3000)
|
|
93
|
+
cd server
|
|
94
|
+
pnpm dev
|
|
95
|
+
|
|
96
|
+
# Terminal 2 -- PWA dev server (port 5173, proxies /api to :3000)
|
|
97
|
+
cd pwa
|
|
98
|
+
pnpm dev
|
|
99
|
+
|
|
100
|
+
# To proxy to a remote API server instead of localhost:
|
|
101
|
+
# Linux / macOS:
|
|
102
|
+
API_URL=https://app.palmier.me pnpm dev
|
|
103
|
+
# Windows (PowerShell):
|
|
104
|
+
$env:API_URL="https://app.palmier.me"; pnpm dev
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Open `http://localhost:5173` in your browser.
|
|
108
|
+
|
|
109
|
+
### Production
|
|
110
|
+
|
|
111
|
+
In production, Express serves the built PWA static files directly — one server, one port.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Build the PWA
|
|
115
|
+
cd pwa && pnpm build
|
|
116
|
+
|
|
117
|
+
# Start the server (serves API + PWA on port 3000)
|
|
118
|
+
cd server && pnpm start
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Open `http://localhost:3000`.
|
|
122
|
+
|
|
123
|
+
### Host Setup
|
|
124
|
+
|
|
125
|
+
The host runs on a separate Linux machine. See the [palmier README](https://github.com/caihongxu/palmier) for full details.
|
|
126
|
+
|
|
127
|
+
1. On the host machine, in your project directory, run:
|
|
128
|
+
```bash
|
|
129
|
+
palmier init
|
|
130
|
+
```
|
|
131
|
+
The interactive wizard detects agents, configures access modes (HTTP port, LAN access), shows a summary for confirmation, registers with the server, saves config to `~/.config/palmier/host.json`, installs a background daemon, and generates a pairing code.
|
|
132
|
+
|
|
133
|
+
2. Enter the pairing code in the PWA to connect your device to the host.
|
|
134
|
+
|
|
135
|
+
## Environment Variables
|
|
136
|
+
|
|
137
|
+
| Variable | Description | Example |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `PORT` | HTTP server port | `3000` |
|
|
140
|
+
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:password@localhost:5432/palmier` |
|
|
141
|
+
| `NATS_URL` | NATS server URL (TCP, for server's own connection) | `nats://localhost:4222` |
|
|
142
|
+
| `NATS_HOST_URL` | NATS URL sent to hosts during registration (use LAN IP) | `nats://192.168.1.100:4222` |
|
|
143
|
+
| `NATS_WS_URL` | NATS WebSocket URL sent to PWA clients | `wss://nats.palmier.me` (prod) or `ws://192.168.1.100:9222` (LAN) |
|
|
144
|
+
| `NATS_TOKEN` | NATS authentication token | *(from nats.conf)* |
|
|
145
|
+
| `VAPID_PUBLIC_KEY` | VAPID public key for web push | *(generated via web-push)* |
|
|
146
|
+
| `VAPID_PRIVATE_KEY` | VAPID private key for web push | *(generated via web-push)* |
|
|
147
|
+
| `VAPID_MAILTO` | Contact email for VAPID | `mailto:admin@example.com` |
|
|
148
|
+
|
|
149
|
+
> **LAN setup note:** `NATS_URL` uses `localhost` because the server connects to NATS locally. `NATS_HOST_URL`, `NATS_WS_URL` must use the LAN IP so remote hosts and browsers can reach them. Firewall must allow inbound on ports 3000 (HTTP), 4222 (NATS TCP), 5173 (Vite dev), and 9222 (NATS WebSocket).
|
|
150
|
+
|
|
151
|
+
## API Endpoints
|
|
152
|
+
|
|
153
|
+
All endpoints are prefixed with `/api`. No user authentication is required.
|
|
154
|
+
|
|
155
|
+
| Method | Path | Description |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `POST` | `/api/hosts/register` | Register a new host (returns hostId + NATS config) |
|
|
158
|
+
| `GET` | `/api/config` | Get NATS WebSocket credentials for the PWA |
|
|
159
|
+
| `POST` | `/api/push/subscribe` | Register a push notification subscription |
|
|
160
|
+
| `DELETE` | `/api/push/subscribe` | Remove a push notification subscription |
|
|
161
|
+
| `GET` | `/api/push/vapid-key` | Get the VAPID public key |
|
|
162
|
+
| `POST` | `/api/push/respond` | Respond to a pending task confirmation via push notification |
|
|
163
|
+
| `GET` | `/health` | Health check |
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Key Implementation Notes
|
|
167
|
+
|
|
168
|
+
- **No user accounts** — the PWA stores paired hosts in localStorage. Each device pairs with a host via a pairing code and receives a client token.
|
|
169
|
+
- **Client tokens** are generated and validated on the host, not the server. They are included in every NATS RPC payload and as Bearer tokens for HTTP requests.
|
|
170
|
+
- **NATS RPC** — the RPC method is derived from the NATS subject (e.g., `...rpc.task.list` → `task.list`), not the message body. The body contains request parameters plus `clientToken`. All NATS requests go through a centralized `request()` helper in `HostConnectionContext` that handles encoding/decoding and logging.
|
|
171
|
+
- **Pairing** — `palmier pair` (or auto-pair after `palmier init`) generates a 6-char pairing code. The PWA enters the code, which routes to the host via NATS (`pair.<CODE>`) or HTTP (`POST /pair`). The host validates the code and returns a client token.
|
|
172
|
+
- **Task IDs** are generated by the host as UUIDs.
|
|
173
|
+
- **Triggers can be enabled/disabled** — the `triggers_enabled` frontmatter field (default `true`) controls whether systemd timers are installed. When disabled, timers are removed but the task can still be run manually. The "Enable Triggers" checkbox only appears in the form when triggers exist.
|
|
174
|
+
- **Host responses** return flat task objects (frontmatter fields at the top level, not nested) for `task.list`, `task.create`, and `task.update`.
|
|
175
|
+
- **NATS "503"** means "no responders" — the dashboard silently handles this when no host is connected, showing an empty task list instead of an error.
|
|
176
|
+
- **Helmet CSP** is disabled (`contentSecurityPolicy: false`) to allow NATS WebSocket connections and inline Vite scripts during dev.
|
|
177
|
+
- **Static file serving** is conditional — Express only serves `pwa/dist/` if the directory exists, so it doesn't interfere during dev when using Vite.
|
|
178
|
+
- **No CORS** needed — Vite proxy handles same-origin in dev, Express static serving handles it in production.
|
|
179
|
+
- **Push notifications** — the PWA registers a service worker (`injectManifest` strategy via vite-plugin-pwa) and subscribes the browser for Web Push. The Web Server subscribes to `host-event.>` and sends push notifications for confirmation requests, task completions, and task failures.
|
|
180
|
+
- **Markdown rendering** — Generated task plans and task results are rendered as rich formatted text using `react-markdown` with `remark-gfm` (GitHub Flavored Markdown), supporting tables, strikethrough, task lists, and autolinks.
|
|
181
|
+
- **Task confirmation** — the Dashboard discovers pending confirmations from the `task.list` RPC response (tasks with a pending request in the serve daemon's in-memory registry, reported via `task.status`). When found, it shows a full-screen confirmation modal. Push notification action buttons trigger `POST /api/push/respond`, which forwards to the `task.user_input` NATS RPC.
|
|
182
|
+
- **Task event tracking** — task lifecycle events are persisted to `status.json` on the host (for crash detection) and broadcast via `host-event.<host_id>.<task_id>` pub/sub and HTTP SSE. The PWA loads initial status from `task.list` and subscribes to events for real-time updates.
|
|
183
|
+
- **NATS config** (`nats.conf`) enables WebSocket on port 9222 (for browser clients) and token-based auth.
|
|
184
|
+
|
|
185
|
+
## Related Repositories
|
|
186
|
+
|
|
187
|
+
- [palmier](https://github.com/caihongxu/palmier) -- The host binary, published as `palmier` on npm. Install with `npm install -g palmier`. Uses npm (not pnpm).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# NATS Server Configuration for Palmier
|
|
2
|
+
|
|
3
|
+
# TCP listener (for server + agent)
|
|
4
|
+
listen: 0.0.0.0:4222
|
|
5
|
+
|
|
6
|
+
# WebSocket listener (for PWA browser clients)
|
|
7
|
+
websocket {
|
|
8
|
+
listen: "0.0.0.0:9222"
|
|
9
|
+
no_tls: true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Token-based authentication
|
|
13
|
+
authorization {
|
|
14
|
+
token: "592b229f9df46e9ecaedbfcf9e2e5bcc6432367c114b20a0c7854b6f4542184a"
|
|
15
|
+
}
|