palmier 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +29 -31
  2. package/dist/agents/agent-instructions.md +4 -11
  3. package/dist/agents/claude.js +3 -3
  4. package/dist/agents/codex.js +2 -2
  5. package/dist/agents/copilot.js +3 -3
  6. package/dist/agents/gemini.js +3 -3
  7. package/dist/agents/openclaw.js +2 -2
  8. package/dist/agents/shared-prompt.d.ts +2 -4
  9. package/dist/agents/shared-prompt.js +9 -4
  10. package/dist/commands/init.js +31 -2
  11. package/dist/commands/pair.d.ts +1 -1
  12. package/dist/commands/pair.js +12 -15
  13. package/dist/commands/run.js +19 -43
  14. package/dist/commands/serve.d.ts +1 -1
  15. package/dist/commands/serve.js +9 -2
  16. package/dist/events.d.ts +2 -2
  17. package/dist/events.js +15 -16
  18. package/dist/index.js +0 -25
  19. package/dist/pending-requests.d.ts +27 -0
  20. package/dist/pending-requests.js +39 -0
  21. package/dist/rpc-handler.js +15 -8
  22. package/dist/transports/http-transport.d.ts +4 -2
  23. package/dist/transports/http-transport.js +226 -77
  24. package/dist/types.d.ts +7 -16
  25. package/package.json +1 -1
  26. package/src/agents/agent-instructions.md +4 -11
  27. package/src/agents/claude.ts +3 -3
  28. package/src/agents/codex.ts +2 -2
  29. package/src/agents/copilot.ts +3 -3
  30. package/src/agents/gemini.ts +3 -3
  31. package/src/agents/openclaw.ts +2 -2
  32. package/src/agents/shared-prompt.ts +12 -6
  33. package/src/commands/init.ts +34 -3
  34. package/src/commands/pair.ts +11 -14
  35. package/src/commands/run.ts +17 -57
  36. package/src/commands/serve.ts +11 -2
  37. package/src/events.ts +14 -15
  38. package/src/index.ts +0 -26
  39. package/src/pending-requests.ts +55 -0
  40. package/src/rpc-handler.ts +15 -9
  41. package/src/transports/http-transport.ts +235 -135
  42. package/src/types.ts +10 -16
  43. package/dist/commands/lan.d.ts +0 -8
  44. package/dist/commands/lan.js +0 -44
  45. package/dist/commands/notify.d.ts +0 -9
  46. package/dist/commands/notify.js +0 -43
  47. package/dist/commands/request-input.d.ts +0 -10
  48. package/dist/commands/request-input.js +0 -49
  49. package/dist/lan-lock.d.ts +0 -7
  50. package/dist/lan-lock.js +0 -18
  51. package/dist/user-input.d.ts +0 -15
  52. package/dist/user-input.js +0 -50
  53. package/src/commands/lan.ts +0 -48
  54. package/src/commands/notify.ts +0 -44
  55. package/src/commands/request-input.ts +0 -51
  56. package/src/lan-lock.ts +0 -16
  57. package/src/user-input.ts +0 -67
package/README.md CHANGED
@@ -10,18 +10,21 @@ A Node.js CLI that lets you dispatch your own AI agents from your phone. It runs
10
10
 
11
11
  > **Important:** By using Palmier, you agree to the [Terms of Service](https://www.palmier.me/terms) and [Privacy Policy](https://www.palmier.me/privacy). See the [Disclaimer](#disclaimer) section below.
12
12
 
13
- ## Connection Modes
13
+ ## Access Modes
14
14
 
15
- The host supports two independent connection modes, enabled during `palmier init`. Both can be active at the same time.
15
+ The serve daemon always runs a local HTTP server. Three access modes are available:
16
16
 
17
- | Mode | Transport | PWA URL | Features |
18
- |------|-----------|---------|----------|
19
- | **Server** | Cloud relay (NATS) | `https://app.palmier.me` | Push notifications, remote access |
20
- | **LAN** | HTTP (direct, on-demand) | `http://<host-ip>:7400` | Low-latency, no external server needed |
17
+ | Mode | Transport | URL | Pairing | Features |
18
+ |------|-----------|-----|---------|----------|
19
+ | **Local** | HTTP (localhost) | `http://localhost:<port>` | Not required | Full access from the host machine, no internet needed |
20
+ | **LAN** | HTTP (direct) | `http://<host-ip>:<port>` | Required | Access from other devices on the local network |
21
+ | **Server** | Cloud relay (NATS) | `https://app.palmier.me` | Required | Push notifications, remote access from anywhere |
21
22
 
22
- **Server mode** relays communication through the Palmier cloud server (via [NATS](https://nats.io), a lightweight messaging system). All features including push notifications are available. The PWA is served over HTTPS.
23
+ **Local mode** is always available. The PWA is served at `http://localhost:<port>` and works without pairing or internet. The daemon binds to `127.0.0.1` by default.
23
24
 
24
- **LAN mode** is started on-demand via `palmier lan`. It runs a local HTTP server that reverse-proxies PWA assets from `app.palmier.me` and serves API endpoints locally. The browser accesses everything at `http://<host-ip>:<port>` (same-origin). Push notifications are not available in LAN mode.
25
+ **LAN mode** is enabled during `palmier init`. The daemon binds to `0.0.0.0` instead, making the PWA and API endpoints accessible from the local network at `http://<host-ip>:<port>`. Devices must pair via OTP to access. Push notifications are not available.
26
+
27
+ **Server mode** relays communication through the Palmier cloud server (via [NATS](https://nats.io), a lightweight messaging system). All features including push notifications are available. The PWA is served over HTTPS. Server mode and LAN mode can be active at the same time.
25
28
 
26
29
  ## Prerequisites
27
30
 
@@ -42,8 +45,7 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
42
45
  | Command | Description |
43
46
  |---|---|
44
47
  | `palmier init` | Interactive setup wizard |
45
- | `palmier pair` | Generate an OTP code to pair a new device (server mode) |
46
- | `palmier lan` | Start an on-demand LAN server with built-in pairing |
48
+ | `palmier pair` | Generate an OTP code to pair a new device |
47
49
  | `palmier sessions list` | List active session tokens |
48
50
  | `palmier sessions revoke <token>` | Revoke a specific session token |
49
51
  | `palmier sessions revoke-all` | Revoke all session tokens |
@@ -51,8 +53,6 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
51
53
  | `palmier serve` | Run the persistent RPC handler (default command) |
52
54
  | `palmier restart` | Restart the palmier serve daemon |
53
55
  | `palmier run <task-id>` | Execute a specific task |
54
- | `palmier notify` | Send a push notification to paired devices |
55
- | `palmier request-input` | Request input from the user during task execution |
56
56
 
57
57
  ## Setup
58
58
 
@@ -60,14 +60,15 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
60
60
 
61
61
  1. Install the host: `npm install -g palmier`
62
62
  2. Run `palmier init` in your Palmier root directory (e.g., `~/palmier`).
63
- 3. The wizard detects installed agents, registers with the Palmier server, installs a background daemon, and generates a pairing code.
64
- 4. Enter the pairing code in the Palmier PWA to connect your device.
63
+ 3. The wizard detects installed agents, configures access modes, registers with the Palmier server, and installs a background daemon.
64
+ 4. Open `http://localhost:<port>` to access the app locally no pairing needed.
65
+ 5. To access from other devices, pair via `palmier pair` (run automatically after init).
65
66
 
66
- ### Pairing additional devices
67
+ ### Pairing devices
67
68
 
68
- **Server mode:** Run `palmier pair` on the host to generate a new OTP code. Enter it in the PWA at `https://app.palmier.me`.
69
+ Local access (`http://localhost:<port>`) works immediately no pairing needed.
69
70
 
70
- **LAN mode:** Run `palmier lan` it displays both the URL and a pairing code. Open the URL on your device and enter the code.
71
+ For LAN or server mode, run `palmier pair` on the host to generate an OTP code. Enter it in the PWA either at `http://<host-ip>:<port>` (LAN mode) or `https://app.palmier.me` (server mode).
71
72
 
72
73
  ### Managing sessions
73
74
 
@@ -123,7 +124,7 @@ palmier restart
123
124
  ## How It Works
124
125
 
125
126
  - The host runs as a **background daemon** (systemd user service on Linux, Registry Run key on Windows), staying alive via `palmier serve`.
126
- - **Paired devices** communicate with the host via NATS (server mode) and/or direct HTTP (LAN mode). Each paired device gets a session token that authenticates all requests.
127
+ - **Device access** localhost is always trusted (no pairing needed). LAN and server mode devices communicate via direct HTTP or NATS respectively, and must pair via OTP to get a session token.
127
128
  - **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional schedules (cron schedules or one-time dates).
128
129
  - **Plan generation** is automatic — when you create or update a task, the host invokes your chosen agent CLI to generate an execution plan and name.
129
130
  - **Schedules** are backed by systemd timers (Linux) or Task Scheduler (Windows). You can enable/disable them without deleting the task, and any task can still be run manually at any time.
@@ -132,8 +133,8 @@ palmier restart
132
133
  - **Task confirmation** — tasks can optionally require your approval before running. You'll get a push notification (server mode) or a prompt in the PWA to confirm or abort.
133
134
  - **Conversational run history** — each run gets its own directory (`tasks/<id>/<timestamp>/`) with a `TASKRUN.md` file containing a conversational thread: assistant messages (agent output), user messages (input responses, permission grants, confirmations), and status entries (started, finished, failed, aborted, stopped). The agent runs inside the run directory, so each run's session files and artifacts are isolated. The PWA displays runs as a chat-like thread with follow-up support.
134
135
  - **Follow-up messages** — after a task run completes, users can send follow-up messages from the run detail view. The agent is invoked inline by the serve daemon (no new process spawning), and the response is appended to the same conversation thread.
135
- - **Real-time updates** — task status changes and result updates are pushed to connected PWA clients via NATS pub/sub (server mode) and/or SSE (LAN mode). The run detail view live-updates as the agent produces output. Events are scoped to specific runs.
136
- - **Agent CLI commands** — `palmier notify` and `palmier request-input` allow agents to send push notifications and request user input during task execution without requiring MCP support.
136
+ - **Real-time updates** — task status changes and result updates are pushed to connected PWA clients via NATS pub/sub (server mode) and/or SSE (local/LAN mode). The run detail view live-updates as the agent produces output. Events are scoped to specific runs.
137
+ - **Agent HTTP endpoints** — the serve daemon exposes localhost-only endpoints (`/notify`, `/request-input`) that agents call to send push notifications and request user input during task execution.
137
138
 
138
139
  ## NATS Subjects
139
140
 
@@ -156,7 +157,7 @@ src/
156
157
  spawn-command.ts # Shared helper for spawning CLI tools
157
158
  task.ts # Task file management
158
159
  types.ts # Shared type definitions
159
- lan-lock.ts # LAN lockfile path and port reader
160
+ pending-requests.ts # In-memory registry for held HTTP connections (confirmation, permission, input)
160
161
  events.ts # Event broadcasting (NATS pub/sub or HTTP SSE)
161
162
  agents/
162
163
  agent.ts # AgentTool interface, registry, and agent detection
@@ -170,15 +171,12 @@ src/
170
171
  commands/
171
172
  init.ts # Interactive setup wizard (auto-pair)
172
173
  pair.ts # OTP code generation and pairing handler
173
- lan.ts # On-demand LAN server
174
174
  sessions.ts # Session token management CLI (list, revoke, revoke-all)
175
175
  info.ts # Print host connection info
176
176
 
177
- serve.ts # Transport selection, startup, and crash detection polling
177
+ serve.ts # NATS + HTTP transport startup, crash detection polling
178
178
  restart.ts # Daemon restart (cross-platform)
179
179
  run.ts # Single task execution
180
- notify.ts # Send push notification to paired devices
181
- request-input.ts # Request user input during task execution
182
180
  platform/
183
181
  platform.ts # PlatformService interface
184
182
  index.ts # Platform factory (Linux vs Windows)
@@ -189,16 +187,16 @@ src/
189
187
  http-transport.ts # HTTP server with RPC, SSE, PWA reverse proxy, and internal event endpoints
190
188
  ```
191
189
 
192
- ## Agent CLI Commands
190
+ ## Agent HTTP Endpoints
193
191
 
194
- These commands are available to agents during task execution. They are included in the agent's system prompt automatically.
192
+ The serve daemon exposes localhost-only HTTP endpoints for agents during task execution. The port is baked into the agent's system prompt automatically.
195
193
 
196
- | Command | Flags | Description |
194
+ | Endpoint | Method | Description |
197
195
  |---|---|---|
198
- | `palmier notify` | `--title <title>` `--body <body>` | Send a push notification to all paired devices |
199
- | `palmier request-input` | `--description <desc...>` | Request input from the user; blocks until a response is provided |
196
+ | `/notify` | GET | Send a push notification (requires server mode) |
197
+ | `/request-input` | GET | Request user input; blocks until a response is provided |
200
198
 
201
- Push notifications require server mode to be enabled. `request-input` requires the `PALMIER_TASK_ID` environment variable (set automatically during task execution).
199
+ See [agent-instructions.md](src/agents/agent-instructions.md) for usage examples.
202
200
 
203
201
  ## Uninstalling
204
202
 
@@ -20,20 +20,13 @@ If the task fails because a tool was denied or you lack the required permissions
20
20
  [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
21
  [PALMIER_PERMISSION] Write | Write generated output files
22
22
 
23
- ## CLI Commands
23
+ ## HTTP Endpoints
24
24
 
25
- You have access to the following palmier CLI commands:
25
+ The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution.
26
26
 
27
- **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, request it:
28
- ```
29
- palmier request-input --description "What is the database connection string?" --description "What is the API key?"
30
- ```
31
- The command blocks until the user responds and prints each value on its own line. If the user aborts, the command exits with a non-zero status.
27
+ **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, GET `/request-input?taskId={{TASK_ID}}&descriptions=question+1&descriptions=question+2`. The request blocks until the user responds. The response is `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user chooses to abort.
32
28
 
33
- **Sending push notifications** — If you need to send a push notification to the user:
34
- ```
35
- palmier notify --title "Task Complete" --body "The deployment finished successfully."
36
- ```
29
+ **Sending push notifications** — GET `/notify?title=...&body=...` to send a push notification to the user's devices.
37
30
 
38
31
  ---
39
32
 
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
2
+ import { getAgentInstructions } from "./shared-prompt.js";
3
3
  import { SHELL } from "../platform/index.js";
4
4
  export class ClaudeAgent {
5
5
  getPlanGenerationCommandLine(prompt) {
@@ -9,8 +9,8 @@ export class ClaudeAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
- const args = ["--permission-mode", "acceptEdits", "-p"];
12
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
+ const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
14
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
15
15
  for (const p of allPerms) {
16
16
  args.push("--allowedTools", p.name);
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
2
+ import { getAgentInstructions } from "./shared-prompt.js";
3
3
  import { SHELL } from "../platform/index.js";
4
4
  export class CodexAgent {
5
5
  getPlanGenerationCommandLine(prompt) {
@@ -9,7 +9,7 @@ export class CodexAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
12
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
13
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
14
14
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
15
15
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
2
+ import { getAgentInstructions } from "./shared-prompt.js";
3
3
  import { SHELL } from "../platform/index.js";
4
4
  export class CopilotAgent {
5
5
  getPlanGenerationCommandLine(prompt) {
@@ -9,8 +9,8 @@ export class CopilotAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
- const args = ["-p", prompt];
12
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
+ const args = ["-p", prompt, "--allowed-tools", "web_fetch"];
14
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
15
15
  if (allPerms.length > 0) {
16
16
  args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
2
+ import { getAgentInstructions } from "./shared-prompt.js";
3
3
  import { SHELL } from "../platform/index.js";
4
4
  export class GeminiAgent {
5
5
  getPlanGenerationCommandLine(prompt) {
@@ -10,8 +10,8 @@ export class GeminiAgent {
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
12
  const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
13
- const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
14
- const args = ["--prompt", "-"];
13
+ const fullPrompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + prompt;
14
+ const args = ["--prompt", "--allowed-tools", "web_fetch", "-"];
15
15
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
16
16
  if (allPerms.length > 0) {
17
17
  args.push("--allowed-tools");
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
2
+ import { getAgentInstructions } from "./shared-prompt.js";
3
3
  export class OpenClawAgent {
4
4
  getPlanGenerationCommandLine(prompt) {
5
5
  return {
@@ -8,7 +8,7 @@ export class OpenClawAgent {
8
8
  };
9
9
  }
10
10
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
11
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
11
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
12
12
  // OpenClaw does not support stdin as prompt.
13
13
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
14
14
  return { command: "openclaw", args };
@@ -1,9 +1,7 @@
1
1
  /**
2
- * Instructions prepended or injected as system prompt for every task invocation.
3
- * Instructs the agent to output structured markers so palmier can determine
4
- * the task outcome, report files, and permission/input requests.
2
+ * Agent instructions with the serve daemon's HTTP port and task ID baked in.
5
3
  */
6
- export declare const AGENT_INSTRUCTIONS: string;
4
+ export declare function getAgentInstructions(taskId: string): string;
7
5
  export declare const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
8
6
  export declare const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
9
7
  export declare const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
@@ -1,13 +1,18 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
+ import { loadConfig } from "../config.js";
4
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(path.join(__dirname, "agent-instructions.md"), "utf-8");
5
7
  /**
6
- * Instructions prepended or injected as system prompt for every task invocation.
7
- * Instructs the agent to output structured markers so palmier can determine
8
- * the task outcome, report files, and permission/input requests.
8
+ * Agent instructions with the serve daemon's HTTP port and task ID baked in.
9
9
  */
10
- export const AGENT_INSTRUCTIONS = fs.readFileSync(path.join(__dirname, "agent-instructions.md"), "utf-8");
10
+ export function getAgentInstructions(taskId) {
11
+ const port = loadConfig().httpPort ?? 7400;
12
+ return AGENT_INSTRUCTIONS_TEMPLATE
13
+ .replace(/\{\{PORT\}\}/g, String(port))
14
+ .replace(/\{\{TASK_ID\}\}/g, taskId);
15
+ }
11
16
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
12
17
  export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
13
18
  export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
@@ -3,6 +3,7 @@ import { loadConfig, saveConfig } from "../config.js";
3
3
  import { detectAgents } from "../agents/agent.js";
4
4
  import { getPlatform } from "../platform/index.js";
5
5
  import { pairCommand } from "./pair.js";
6
+ import { detectLanIp } from "../transports/http-transport.js";
6
7
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
7
8
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
8
9
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -29,6 +30,33 @@ export async function initCommand() {
29
30
  process.exit(1);
30
31
  }
31
32
  console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
33
+ // LAN mode
34
+ const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
35
+ const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
36
+ let httpPort = 7400;
37
+ const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
38
+ const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
39
+ const parsed = parseInt(portAnswer.trim(), 10);
40
+ if (parsed > 0 && parsed < 65536)
41
+ httpPort = parsed;
42
+ // Display summary and ask for confirmation before making any changes
43
+ console.log(`\n${bold("Setup summary:")}\n`);
44
+ console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
45
+ console.log(` All tasks and execution data will be stored here.\n`);
46
+ console.log(` ${dim("Local access:")} ${cyan(`http://localhost:${httpPort}`)}`);
47
+ console.log(` Always available — no internet required.\n`);
48
+ if (lanEnabled) {
49
+ const ip = detectLanIp();
50
+ console.log(` ${dim("LAN access:")} ${cyan(`http://${ip}:${httpPort}`)}`);
51
+ console.log(` Accessible from other devices on your local network. Pairing required.\n`);
52
+ }
53
+ console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
54
+ const confirm = await ask("Proceed? (Y/n): ");
55
+ if (confirm.trim().toLowerCase() === "n") {
56
+ console.log("\nSetup cancelled.");
57
+ rl.close();
58
+ return;
59
+ }
32
60
  // Register with server
33
61
  let existingHostId;
34
62
  try {
@@ -54,7 +82,7 @@ export async function initCommand() {
54
82
  }
55
83
  }
56
84
  }
57
- // Build config
85
+ // Build and save config
58
86
  const config = {
59
87
  hostId: registerResponse.hostId,
60
88
  projectRoot: process.cwd(),
@@ -62,9 +90,10 @@ export async function initCommand() {
62
90
  natsWsUrl: registerResponse.natsWsUrl,
63
91
  natsToken: registerResponse.natsToken,
64
92
  agents,
93
+ httpPort,
94
+ lanEnabled,
65
95
  };
66
96
  saveConfig(config);
67
- console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
68
97
  console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
69
98
  getPlatform().installDaemon(config);
70
99
  console.log("\nStarting pairing...");
@@ -2,7 +2,7 @@ export declare const PAIRING_EXPIRY_MS: number;
2
2
  export declare function generatePairingCode(): string;
3
3
  /**
4
4
  * Generate an OTP code and wait for a PWA client to pair.
5
- * Listens on NATS always, and also on the LAN server if `palmier lan` is running.
5
+ * Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
6
6
  */
7
7
  export declare function pairCommand(): Promise<void>;
8
8
  //# sourceMappingURL=pair.d.ts.map
@@ -3,7 +3,6 @@ import { StringCodec } from "nats";
3
3
  import { loadConfig } from "../config.js";
4
4
  import { connectNats } from "../nats-client.js";
5
5
  import { addSession } from "../session-store.js";
6
- import { getLanPort } from "../lan-lock.js";
7
6
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
8
7
  const CODE_LENGTH = 6;
9
8
  export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
@@ -20,15 +19,15 @@ function buildPairResponse(config, label) {
20
19
  };
21
20
  }
22
21
  /**
23
- * POST to the running LAN server and long-poll until paired or expired.
22
+ * POST to the running serve daemon and long-poll until paired or expired.
24
23
  */
25
- function lanPairRegister(port, code) {
24
+ function httpPairRegister(port, code) {
26
25
  const body = JSON.stringify({ code, expiryMs: PAIRING_EXPIRY_MS });
27
26
  return new Promise((resolve) => {
28
27
  const req = http.request({
29
28
  hostname: "127.0.0.1",
30
29
  port,
31
- path: "/internal/pair-register",
30
+ path: "/pair-register",
32
31
  method: "POST",
33
32
  headers: { "Content-Type": "application/json" },
34
33
  timeout: PAIRING_EXPIRY_MS + 5000,
@@ -52,11 +51,12 @@ function lanPairRegister(port, code) {
52
51
  }
53
52
  /**
54
53
  * Generate an OTP code and wait for a PWA client to pair.
55
- * Listens on NATS always, and also on the LAN server if `palmier lan` is running.
54
+ * Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
56
55
  */
57
56
  export async function pairCommand() {
58
57
  const config = loadConfig();
59
58
  const code = generatePairingCode();
59
+ const httpPort = config.httpPort ?? 7400;
60
60
  let paired = false;
61
61
  function onPaired() {
62
62
  paired = true;
@@ -70,7 +70,7 @@ export async function pairCommand() {
70
70
  console.log(` ${code}`);
71
71
  console.log("");
72
72
  console.log("Code expires in 5 minutes.");
73
- // NATS pairing (always active)
73
+ // NATS pairing (server mode)
74
74
  const nc = await connectNats(config);
75
75
  const sc = StringCodec();
76
76
  const subject = `pair.${code}`;
@@ -98,15 +98,12 @@ export async function pairCommand() {
98
98
  onPaired();
99
99
  }
100
100
  })();
101
- // LAN pairing — if `palmier lan` is running, also register with it
102
- const lanPort = getLanPort();
103
- if (lanPort) {
104
- (async () => {
105
- const result = await lanPairRegister(lanPort, code);
106
- if (result)
107
- onPaired();
108
- })();
109
- }
101
+ // HTTP pairing — register with serve daemon's /pair-register endpoint
102
+ (async () => {
103
+ const result = await httpPairRegister(httpPort, code);
104
+ if (result)
105
+ onPaired();
106
+ })();
110
107
  // Wait for pairing or timeout
111
108
  const start = Date.now();
112
109
  await new Promise((resolve) => {
@@ -9,7 +9,6 @@ import { getAgent } from "../agents/agent.js";
9
9
  import { getPlatform } from "../platform/index.js";
10
10
  import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
11
11
  import { publishHostEvent } from "../events.js";
12
- import { waitForUserInput } from "../user-input.js";
13
12
  /**
14
13
  * Invoke the agent CLI with a continuation loop for permissions and user input.
15
14
  *
@@ -24,7 +23,7 @@ async function invokeAgentWithContinuation(ctx, invokeTask) {
24
23
  const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
25
24
  const result = await spawnCommand(command, args, {
26
25
  cwd: getRunDir(ctx.taskDir, ctx.runId),
27
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
26
+ env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
28
27
  echoStdout: true,
29
28
  resolveOnFailure: true,
30
29
  stdin,
@@ -41,8 +40,7 @@ async function invokeAgentWithContinuation(ctx, invokeTask) {
41
40
  });
42
41
  // Permission handling — agent requested permissions
43
42
  if (requiredPermissions.length > 0) {
44
- const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
45
- await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
43
+ const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
46
44
  if (response === "aborted") {
47
45
  await appendAndNotify(ctx, {
48
46
  role: "user",
@@ -144,9 +142,7 @@ export async function runCommand(taskId) {
144
142
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
145
143
  // If requires_confirmation, notify clients and wait
146
144
  if (task.frontmatter.requires_confirmation) {
147
- const confirmed = await requestConfirmation(nc, config, task, taskDir);
148
- const resolvedStatus = confirmed ? "confirmed" : "aborted";
149
- await publishConfirmResolved(nc, config, taskId, resolvedStatus);
145
+ const confirmed = await requestConfirmation(config, task, taskDir);
150
146
  if (!confirmed) {
151
147
  console.log("Task aborted by user.");
152
148
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
@@ -220,7 +216,7 @@ async function runCommandTriggeredMode(ctx) {
220
216
  console.log(`[command-triggered] Spawning: ${commandStr}`);
221
217
  const child = spawnStreamingCommand(commandStr, {
222
218
  cwd: getRunDir(ctx.taskDir, ctx.runId),
223
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
219
+ env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
224
220
  });
225
221
  let linesProcessed = 0;
226
222
  let invocationsSucceeded = 0;
@@ -336,49 +332,29 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
336
332
  payload.run_id = runId;
337
333
  await publishHostEvent(nc, config.hostId, taskId, payload);
338
334
  }
339
- /**
340
- * Notify clients that a confirmation request has been resolved.
341
- */
342
- async function publishConfirmResolved(nc, config, taskId, status) {
343
- await publishHostEvent(nc, config.hostId, taskId, {
344
- event_type: "confirm-resolved",
345
- host_id: config.hostId,
346
- status,
347
- });
348
- }
349
- async function requestPermission(nc, config, task, taskDir, requiredPermissions) {
350
- const currentStatus = readTaskStatus(taskDir);
351
- writeTaskStatus(taskDir, { ...currentStatus, pending_permission: requiredPermissions });
352
- await publishHostEvent(nc, config.hostId, task.frontmatter.id, {
353
- event_type: "permission-request",
354
- host_id: config.hostId,
355
- required_permissions: requiredPermissions,
356
- name: task.frontmatter.name,
335
+ async function requestPermission(config, task, taskDir, requiredPermissions) {
336
+ const port = config.httpPort ?? 7400;
337
+ const params = new URLSearchParams({
338
+ taskId: task.frontmatter.id,
339
+ taskName: task.frontmatter.name,
340
+ permissions: JSON.stringify(requiredPermissions),
357
341
  });
358
- const userInput = await waitForUserInput(taskDir);
359
- const response = userInput[0];
342
+ const res = await fetch(`http://localhost:${port}/request-permission?${params}`);
343
+ const { response } = await res.json();
360
344
  writeTaskStatus(taskDir, {
361
345
  running_state: response === "aborted" ? "aborted" : "started",
362
346
  time_stamp: Date.now(),
363
347
  });
364
348
  return response;
365
349
  }
366
- async function publishPermissionResolved(nc, config, taskId, status) {
367
- await publishHostEvent(nc, config.hostId, taskId, {
368
- event_type: "permission-resolved",
369
- host_id: config.hostId,
370
- status,
371
- });
372
- }
373
- async function requestConfirmation(nc, config, task, taskDir) {
374
- const currentStatus = readTaskStatus(taskDir);
375
- writeTaskStatus(taskDir, { ...currentStatus, pending_confirmation: true });
376
- await publishHostEvent(nc, config.hostId, task.frontmatter.id, {
377
- event_type: "confirm-request",
378
- host_id: config.hostId,
350
+ async function requestConfirmation(config, task, taskDir) {
351
+ const port = config.httpPort ?? 7400;
352
+ const params = new URLSearchParams({
353
+ taskId: task.frontmatter.id,
354
+ taskName: task.frontmatter.name,
379
355
  });
380
- const userInput = await waitForUserInput(taskDir);
381
- const confirmed = userInput[0] === "confirmed";
356
+ const res = await fetch(`http://localhost:${port}/request-confirmation?${params}`);
357
+ const { confirmed } = await res.json();
382
358
  writeTaskStatus(taskDir, {
383
359
  running_state: confirmed ? "started" : "aborted",
384
360
  time_stamp: Date.now(),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Start the persistent RPC handler (NATS only).
2
+ * Start the persistent RPC handler (NATS + HTTP).
3
3
  */
4
4
  export declare function serveCommand(): Promise<void>;
5
5
  //# sourceMappingURL=serve.d.ts.map
@@ -4,6 +4,7 @@ import { loadConfig } from "../config.js";
4
4
  import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
+ import { startHttpTransport } from "../transports/http-transport.js";
7
8
  import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
8
9
  import { publishHostEvent } from "../events.js";
9
10
  import { getPlatform } from "../platform/index.js";
@@ -69,7 +70,7 @@ async function checkStaleTasks(config, nc) {
69
70
  }
70
71
  }
71
72
  /**
72
- * Start the persistent RPC handler (NATS only).
73
+ * Start the persistent RPC handler (NATS + HTTP).
73
74
  */
74
75
  export async function serveCommand() {
75
76
  const config = loadConfig();
@@ -91,6 +92,12 @@ export async function serveCommand() {
91
92
  });
92
93
  }, POLL_INTERVAL_MS);
93
94
  const handleRpc = createRpcHandler(config, nc);
94
- await startNatsTransport(config, handleRpc, nc);
95
+ const httpPort = config.httpPort ?? 7400;
96
+ // Start NATS transport (loops forever, fire-and-forget)
97
+ if (nc) {
98
+ startNatsTransport(config, handleRpc, nc);
99
+ }
100
+ // Start HTTP transport (loops forever)
101
+ await startHttpTransport(config, handleRpc, httpPort, nc);
95
102
  }
96
103
  //# sourceMappingURL=serve.js.map
package/dist/events.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type NatsConnection } from "nats";
2
2
  /**
3
- * Broadcast an event to connected clients via NATS and HTTP SSE (if LAN server is running).
3
+ * Broadcast an event to connected clients via NATS and HTTP SSE.
4
4
  *
5
5
  * - NATS: publishes to `host-event.{hostId}.{taskId}`
6
- * - HTTP: POSTs to the LAN server's `/internal/event` endpoint (auto-detected via lockfile)
6
+ * - HTTP: POSTs to the serve daemon's `/event` endpoint
7
7
  */
8
8
  export declare function publishHostEvent(nc: NatsConnection | undefined, hostId: string, taskId: string, payload: Record<string, unknown>): Promise<void>;
9
9
  //# sourceMappingURL=events.d.ts.map