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.
- package/README.md +29 -31
- package/dist/agents/agent-instructions.md +4 -11
- package/dist/agents/claude.js +3 -3
- package/dist/agents/codex.js +2 -2
- package/dist/agents/copilot.js +3 -3
- package/dist/agents/gemini.js +3 -3
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/shared-prompt.d.ts +2 -4
- package/dist/agents/shared-prompt.js +9 -4
- package/dist/commands/init.js +31 -2
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +12 -15
- package/dist/commands/run.js +19 -43
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +9 -2
- package/dist/events.d.ts +2 -2
- package/dist/events.js +15 -16
- package/dist/index.js +0 -25
- package/dist/pending-requests.d.ts +27 -0
- package/dist/pending-requests.js +39 -0
- package/dist/rpc-handler.js +15 -8
- package/dist/transports/http-transport.d.ts +4 -2
- package/dist/transports/http-transport.js +226 -77
- package/dist/types.d.ts +7 -16
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +4 -11
- package/src/agents/claude.ts +3 -3
- package/src/agents/codex.ts +2 -2
- package/src/agents/copilot.ts +3 -3
- package/src/agents/gemini.ts +3 -3
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/shared-prompt.ts +12 -6
- package/src/commands/init.ts +34 -3
- package/src/commands/pair.ts +11 -14
- package/src/commands/run.ts +17 -57
- package/src/commands/serve.ts +11 -2
- package/src/events.ts +14 -15
- package/src/index.ts +0 -26
- package/src/pending-requests.ts +55 -0
- package/src/rpc-handler.ts +15 -9
- package/src/transports/http-transport.ts +235 -135
- package/src/types.ts +10 -16
- package/dist/commands/lan.d.ts +0 -8
- package/dist/commands/lan.js +0 -44
- package/dist/commands/notify.d.ts +0 -9
- package/dist/commands/notify.js +0 -43
- package/dist/commands/request-input.d.ts +0 -10
- package/dist/commands/request-input.js +0 -49
- package/dist/lan-lock.d.ts +0 -7
- package/dist/lan-lock.js +0 -18
- package/dist/user-input.d.ts +0 -15
- package/dist/user-input.js +0 -50
- package/src/commands/lan.ts +0 -48
- package/src/commands/notify.ts +0 -44
- package/src/commands/request-input.ts +0 -51
- package/src/lan-lock.ts +0 -16
- 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
|
-
##
|
|
13
|
+
## Access Modes
|
|
14
14
|
|
|
15
|
-
The
|
|
15
|
+
The serve daemon always runs a local HTTP server. Three access modes are available:
|
|
16
16
|
|
|
17
|
-
| Mode | Transport |
|
|
18
|
-
|
|
19
|
-
| **
|
|
20
|
-
| **LAN** | HTTP (direct
|
|
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
|
-
**
|
|
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
|
|
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
|
|
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
|
|
64
|
-
4.
|
|
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
|
|
67
|
+
### Pairing devices
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
Local access (`http://localhost:<port>`) works immediately — no pairing needed.
|
|
69
70
|
|
|
70
|
-
|
|
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
|
-
- **
|
|
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
|
|
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
|
-
|
|
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 #
|
|
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
|
|
190
|
+
## Agent HTTP Endpoints
|
|
193
191
|
|
|
194
|
-
|
|
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
|
-
|
|
|
194
|
+
| Endpoint | Method | Description |
|
|
197
195
|
|---|---|---|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
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
|
-
|
|
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
|
-
##
|
|
23
|
+
## HTTP Endpoints
|
|
24
24
|
|
|
25
|
-
|
|
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
|
|
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** —
|
|
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
|
|
package/dist/agents/claude.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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 =
|
|
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);
|
package/dist/agents/codex.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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 =
|
|
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 ?? [])];
|
package/dist/agents/copilot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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 =
|
|
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(",")}'`);
|
package/dist/agents/gemini.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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 =
|
|
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");
|
package/dist/agents/openclaw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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]";
|
package/dist/commands/init.js
CHANGED
|
@@ -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...");
|
package/dist/commands/pair.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/commands/pair.js
CHANGED
|
@@ -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
|
|
22
|
+
* POST to the running serve daemon and long-poll until paired or expired.
|
|
24
23
|
*/
|
|
25
|
-
function
|
|
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: "/
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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) => {
|
package/dist/commands/run.js
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
359
|
-
const response =
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
381
|
-
const 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(),
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|