palmier 0.2.6 → 0.2.7
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 +24 -26
- package/dist/commands/info.js +0 -22
- package/dist/commands/init.js +27 -123
- package/dist/commands/lan.d.ts +8 -0
- package/dist/commands/lan.js +51 -0
- package/dist/commands/mcpserver.js +2 -7
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +52 -56
- package/dist/commands/run.js +36 -41
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +14 -33
- package/dist/config.js +3 -17
- package/dist/events.d.ts +3 -4
- package/dist/events.js +25 -8
- package/dist/index.js +8 -0
- package/dist/rpc-handler.js +5 -29
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +103 -18
- package/dist/types.d.ts +1 -3
- package/package.json +1 -1
- package/src/commands/info.ts +0 -24
- package/src/commands/init.ts +29 -150
- package/src/commands/lan.ts +58 -0
- package/src/commands/mcpserver.ts +2 -10
- package/src/commands/pair.ts +50 -63
- package/src/commands/run.ts +29 -43
- package/src/commands/serve.ts +14 -31
- package/src/config.ts +3 -18
- package/src/events.ts +23 -10
- package/src/index.ts +9 -0
- package/src/rpc-handler.ts +5 -31
- package/src/transports/http-transport.ts +123 -19
- package/src/types.ts +2 -6
package/README.md
CHANGED
|
@@ -8,17 +8,16 @@ A Node.js CLI that runs on your machine as a persistent daemon. It manages tasks
|
|
|
8
8
|
|
|
9
9
|
## Connection Modes
|
|
10
10
|
|
|
11
|
-
The host supports
|
|
11
|
+
The host supports two independent connection modes, enabled during `palmier init`. Both can be active at the same time.
|
|
12
12
|
|
|
13
|
-
| Mode | Transport |
|
|
14
|
-
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
| **`lan`** | HTTP only | `palmier init` → LAN only | No push notifications. No server needed. |
|
|
13
|
+
| Mode | Transport | PWA URL | Features |
|
|
14
|
+
|------|-----------|---------|----------|
|
|
15
|
+
| **Server** | NATS (cloud relay) | `https://app.palmier.me` | Push notifications, remote access |
|
|
16
|
+
| **LAN** | HTTP (direct, on-demand) | `http://<host-ip>:7400` | Low-latency, no external server needed |
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
**Server mode** relays communication through the Palmier server via NATS. All features including push notifications are available. The PWA is served over HTTPS.
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
**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.
|
|
22
21
|
|
|
23
22
|
## Prerequisites
|
|
24
23
|
|
|
@@ -36,8 +35,9 @@ npm install -g palmier
|
|
|
36
35
|
|
|
37
36
|
| Command | Description |
|
|
38
37
|
|---|---|
|
|
39
|
-
| `palmier init` | Interactive setup wizard
|
|
40
|
-
| `palmier pair` | Generate an OTP code to pair a new device |
|
|
38
|
+
| `palmier init` | Interactive setup wizard |
|
|
39
|
+
| `palmier pair` | Generate an OTP code to pair a new device (server mode) |
|
|
40
|
+
| `palmier lan` | Start an on-demand LAN server with built-in pairing |
|
|
41
41
|
| `palmier sessions list` | List active session tokens |
|
|
42
42
|
| `palmier sessions revoke <token>` | Revoke a specific session token |
|
|
43
43
|
| `palmier sessions revoke-all` | Revoke all session tokens |
|
|
@@ -54,16 +54,14 @@ npm install -g palmier
|
|
|
54
54
|
|
|
55
55
|
1. Install the host: `npm install -g palmier`
|
|
56
56
|
2. Run `palmier init` in your project directory.
|
|
57
|
-
3. The wizard
|
|
58
|
-
|
|
59
|
-
- Server URL (for full access, used for registration only — not stored in config)
|
|
60
|
-
- Optional LAN access (for full access — auto-detects best route when enabled)
|
|
61
|
-
4. The host saves config, installs a background daemon (systemd on Linux, Registry Run key on Windows), and generates a pairing code.
|
|
62
|
-
5. Enter the pairing code in the Palmier PWA to connect your device.
|
|
57
|
+
3. The wizard detects installed agents, registers with the Palmier server, installs a background daemon, and generates a pairing code.
|
|
58
|
+
4. Enter the pairing code in the Palmier PWA to connect your device.
|
|
63
59
|
|
|
64
60
|
### Pairing additional devices
|
|
65
61
|
|
|
66
|
-
Run `palmier pair` on the host to generate a new OTP code.
|
|
62
|
+
**Server mode:** Run `palmier pair` on the host to generate a new OTP code. Enter it in the PWA at `https://app.palmier.me`.
|
|
63
|
+
|
|
64
|
+
**LAN mode:** Run `palmier lan` — it displays both the URL and a pairing code. Open the URL on your device and enter the code.
|
|
67
65
|
|
|
68
66
|
### Managing sessions
|
|
69
67
|
|
|
@@ -119,15 +117,15 @@ palmier restart
|
|
|
119
117
|
## How It Works
|
|
120
118
|
|
|
121
119
|
- The host runs as a **background daemon** (systemd user service on Linux, Registry Run key on Windows), staying alive via `palmier serve`.
|
|
122
|
-
- **Paired devices** communicate with the host via NATS (
|
|
123
|
-
- **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional
|
|
120
|
+
- **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.
|
|
121
|
+
- **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).
|
|
124
122
|
- **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.
|
|
125
|
-
- **
|
|
123
|
+
- **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.
|
|
126
124
|
- **Task execution** uses the system scheduler on both platforms — `systemctl --user start` on Linux, `schtasks /run` on Windows. The daemon polls every 30 seconds to detect crashed tasks (processes that exited without updating status) and marks them as failed, broadcasting the failure to connected clients.
|
|
127
125
|
- **Command-triggered tasks** — optionally specify a shell command (e.g., `tail -f /var/log/app.log`). Palmier runs the command continuously and invokes the agent for each line of stdout, passing it alongside your prompt. Useful for log monitoring, event-driven automation, and reactive workflows.
|
|
128
|
-
- **Task confirmation** — tasks can optionally require your approval before running. You'll get a push notification (
|
|
126
|
+
- **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.
|
|
129
127
|
- **Run history** — each run produces a timestamped result file. You can view results and reports from the PWA.
|
|
130
|
-
- **Real-time updates** — task status changes (started, finished, failed) are pushed to connected PWA clients via NATS pub/sub or SSE
|
|
128
|
+
- **Real-time updates** — task status changes (started, finished, failed) are pushed to connected PWA clients via NATS pub/sub (server mode) and/or SSE (LAN mode).
|
|
131
129
|
- **MCP server** (`palmier mcpserver`) exposes platform tools (e.g., `send-push-notification`) to AI agents like Claude Code over stdio.
|
|
132
130
|
|
|
133
131
|
## Project Structure
|
|
@@ -148,7 +146,7 @@ src/
|
|
|
148
146
|
gemini.ts # Gemini CLI agent implementation
|
|
149
147
|
codex.ts # Codex CLI agent implementation
|
|
150
148
|
openclaw.ts # OpenClaw agent implementation
|
|
151
|
-
events.ts #
|
|
149
|
+
events.ts # Event broadcasting (NATS pub/sub or HTTP SSE)
|
|
152
150
|
commands/
|
|
153
151
|
init.ts # Interactive setup wizard (auto-pair)
|
|
154
152
|
pair.ts # OTP code generation and pairing handler
|
|
@@ -166,7 +164,7 @@ src/
|
|
|
166
164
|
windows.ts # Windows: Registry Run key, Task Scheduler, schtasks-based task control
|
|
167
165
|
transports/
|
|
168
166
|
nats-transport.ts # NATS subscription loop (host.<hostId>.rpc.>)
|
|
169
|
-
http-transport.ts # HTTP server with RPC, SSE, and internal event endpoints
|
|
167
|
+
http-transport.ts # HTTP server with RPC, SSE, PWA reverse proxy, and internal event endpoints
|
|
170
168
|
```
|
|
171
169
|
|
|
172
170
|
## MCP Server
|
|
@@ -188,7 +186,7 @@ Add to your Claude Code MCP settings:
|
|
|
188
186
|
}
|
|
189
187
|
```
|
|
190
188
|
|
|
191
|
-
Requires a provisioned host (`palmier init`)
|
|
189
|
+
Requires a provisioned host (`palmier init`) with server mode enabled.
|
|
192
190
|
|
|
193
191
|
### Available Tools
|
|
194
192
|
|
|
@@ -273,7 +271,7 @@ Tasks can be configured to run on schedules (cron) or in response to events with
|
|
|
273
271
|
|
|
274
272
|
Task prompts and execution data may be transmitted to third-party AI service providers (Anthropic, Google, OpenAI, etc.) according to their respective terms and privacy policies. Palmier does not control how these services process your data.
|
|
275
273
|
|
|
276
|
-
When using
|
|
274
|
+
When using server mode, communication between your device and the host is relayed through the Palmier server. See the [Privacy Policy](https://www.palmier.me/privacy) for details on what data is collected.
|
|
277
275
|
|
|
278
276
|
### Limitation of Liability
|
|
279
277
|
|
package/dist/commands/info.js
CHANGED
|
@@ -1,35 +1,13 @@
|
|
|
1
|
-
import * as os from "os";
|
|
2
1
|
import { loadConfig } from "../config.js";
|
|
3
2
|
import { loadSessions } from "../session-store.js";
|
|
4
|
-
/**
|
|
5
|
-
* Detect the first non-internal IPv4 address.
|
|
6
|
-
*/
|
|
7
|
-
function detectLanIp() {
|
|
8
|
-
const interfaces = os.networkInterfaces();
|
|
9
|
-
for (const name of Object.keys(interfaces)) {
|
|
10
|
-
for (const iface of interfaces[name] ?? []) {
|
|
11
|
-
if (iface.family === "IPv4" && !iface.internal) {
|
|
12
|
-
return iface.address;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return "127.0.0.1";
|
|
17
|
-
}
|
|
18
3
|
/**
|
|
19
4
|
* Print host connection info for setting up clients.
|
|
20
5
|
*/
|
|
21
6
|
export async function infoCommand() {
|
|
22
7
|
const config = loadConfig();
|
|
23
|
-
const mode = config.mode ?? "nats";
|
|
24
8
|
const sessions = loadSessions();
|
|
25
9
|
console.log(`Host ID: ${config.hostId}`);
|
|
26
|
-
console.log(`Mode: ${mode}`);
|
|
27
10
|
console.log(`Project root: ${config.projectRoot}`);
|
|
28
|
-
if (mode === "lan" || mode === "auto") {
|
|
29
|
-
const lanIp = detectLanIp();
|
|
30
|
-
const port = config.directPort ?? 7400;
|
|
31
|
-
console.log(`LAN address: ${lanIp}:${port}`);
|
|
32
|
-
}
|
|
33
11
|
// Detected agents
|
|
34
12
|
if (config.agents && config.agents.length > 0) {
|
|
35
13
|
console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
-
import { randomUUID, randomBytes } from "crypto";
|
|
3
2
|
import { loadConfig, saveConfig } from "../config.js";
|
|
4
3
|
import { detectAgents } from "../agents/agent.js";
|
|
5
|
-
import { detectLanIp } from "../transports/http-transport.js";
|
|
6
4
|
import { getPlatform } from "../platform/index.js";
|
|
7
5
|
import { pairCommand } from "./pair.js";
|
|
8
6
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
9
7
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
10
8
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
11
9
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
12
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
13
10
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
14
11
|
/**
|
|
15
12
|
* Interactive wizard to provision this host.
|
|
@@ -25,112 +22,50 @@ export async function initCommand() {
|
|
|
25
22
|
console.log("Detecting installed agents...");
|
|
26
23
|
const agents = await detectAgents();
|
|
27
24
|
if (agents.length === 0) {
|
|
28
|
-
console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one
|
|
29
|
-
console.log(`
|
|
30
|
-
console.log(` - ${bold("Gemini CLI")} npm install -g @google/gemini-cli`);
|
|
31
|
-
console.log(` - ${bold("Codex CLI")} npm install -g @openai/codex`);
|
|
32
|
-
console.log(` - ${bold("OpenClaw")} See https://github.com/openclaw/openclaw\n`);
|
|
25
|
+
console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one supported agent CLI.\n`);
|
|
26
|
+
console.log(`See supported agents: https://www.palmier.me/agents\n`);
|
|
33
27
|
console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
|
|
34
28
|
rl.close();
|
|
35
29
|
process.exit(1);
|
|
36
30
|
}
|
|
37
31
|
console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (accessMode === "full") {
|
|
43
|
-
// Step 2: Server URL
|
|
44
|
-
serverUrl = await promptServerUrl(ask, step++);
|
|
32
|
+
// Register with server
|
|
33
|
+
let existingHostId;
|
|
34
|
+
try {
|
|
35
|
+
existingHostId = loadConfig().hostId;
|
|
45
36
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Step N: Optional LAN
|
|
49
|
-
enableLan = await promptEnableLan(ask, step++);
|
|
50
|
-
}
|
|
51
|
-
// Determine mode
|
|
52
|
-
const mode = accessMode === "lan" ? "lan" : enableLan ? "auto" : "nats";
|
|
53
|
-
// Prepare LAN details for summary
|
|
54
|
-
let lanIp;
|
|
55
|
-
const directPort = 7400;
|
|
56
|
-
if (enableLan) {
|
|
57
|
-
lanIp = detectLanIp();
|
|
58
|
-
}
|
|
59
|
-
// Summary before committing
|
|
60
|
-
console.log(`\n${bold("--- Setup Summary ---")}\n`);
|
|
61
|
-
console.log(` Mode: ${cyan(mode)}${dim(mode === "auto" ? " (NATS + LAN)" : mode === "nats" ? " (NATS only)" : " (LAN only)")}`);
|
|
62
|
-
if (serverUrl) {
|
|
63
|
-
console.log(` Server: ${cyan(serverUrl)}`);
|
|
64
|
-
}
|
|
65
|
-
if (enableLan && lanIp) {
|
|
66
|
-
console.log(` LAN: ${cyan(`${lanIp}:${directPort}`)}`);
|
|
67
|
-
}
|
|
68
|
-
console.log(` Agents: ${green(agents.map((a) => a.label).join(", "))}`);
|
|
69
|
-
console.log("");
|
|
70
|
-
const confirm = await ask("Proceed? (Y/n): ");
|
|
71
|
-
if (confirm.trim().toLowerCase() === "n") {
|
|
72
|
-
console.log("\nSetup cancelled.");
|
|
73
|
-
rl.close();
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
// Register with server (after confirmation)
|
|
37
|
+
catch { /* first init */ }
|
|
38
|
+
const serverUrl = "https://app.palmier.me";
|
|
77
39
|
let registerResponse;
|
|
78
|
-
|
|
79
|
-
|
|
40
|
+
while (true) {
|
|
41
|
+
console.log(`\nRegistering host...`);
|
|
80
42
|
try {
|
|
81
|
-
|
|
43
|
+
registerResponse = await registerHost(serverUrl, existingHostId);
|
|
44
|
+
console.log(green("Host registered successfully."));
|
|
45
|
+
break;
|
|
82
46
|
}
|
|
83
|
-
catch {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
|
|
93
|
-
const retry = await ask("\nRetry? (Y/n): ");
|
|
94
|
-
if (retry.trim().toLowerCase() === "n") {
|
|
95
|
-
console.log("\nSetup cancelled.");
|
|
96
|
-
rl.close();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
|
|
49
|
+
const retry = await ask("\nRetry? (Y/n): ");
|
|
50
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
51
|
+
console.log("\nSetup cancelled.");
|
|
52
|
+
rl.close();
|
|
53
|
+
return;
|
|
99
54
|
}
|
|
100
55
|
}
|
|
101
56
|
}
|
|
102
57
|
// Build config
|
|
103
58
|
const config = {
|
|
104
|
-
hostId: registerResponse
|
|
59
|
+
hostId: registerResponse.hostId,
|
|
105
60
|
projectRoot: process.cwd(),
|
|
106
|
-
|
|
61
|
+
nats: true,
|
|
62
|
+
natsUrl: registerResponse.natsUrl,
|
|
63
|
+
natsWsUrl: registerResponse.natsWsUrl,
|
|
64
|
+
natsToken: registerResponse.natsToken,
|
|
65
|
+
agents,
|
|
107
66
|
};
|
|
108
|
-
if (registerResponse) {
|
|
109
|
-
config.natsUrl = registerResponse.natsUrl;
|
|
110
|
-
config.natsWsUrl = registerResponse.natsWsUrl;
|
|
111
|
-
config.natsToken = registerResponse.natsToken;
|
|
112
|
-
}
|
|
113
|
-
if (enableLan) {
|
|
114
|
-
const directToken = randomBytes(32).toString("hex");
|
|
115
|
-
config.directPort = directPort;
|
|
116
|
-
config.directToken = directToken;
|
|
117
|
-
console.log(`\n LAN IP: ${cyan(lanIp)}`);
|
|
118
|
-
console.log(` Port: ${cyan(String(directPort))}`);
|
|
119
|
-
if (process.platform === "win32") {
|
|
120
|
-
console.log(`\n ${yellow("Firewall:")} You may need to allow incoming connections on this port:`);
|
|
121
|
-
console.log(dim(` netsh advfirewall firewall add rule name="Palmier" dir=in action=allow protocol=TCP localport=${directPort}`));
|
|
122
|
-
}
|
|
123
|
-
else if (process.platform === "darwin") {
|
|
124
|
-
console.log(`\n ${yellow("Firewall:")} macOS will prompt you to allow incoming connections automatically.`);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
console.log(`\n ${yellow("Firewall:")} You may need to allow incoming connections on this port:`);
|
|
128
|
-
console.log(dim(` sudo ufw allow ${directPort}/tcp`));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
config.agents = agents;
|
|
132
67
|
saveConfig(config);
|
|
133
|
-
console.log(`\n${green("Host provisioned")}
|
|
68
|
+
console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
|
|
134
69
|
console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
|
|
135
70
|
getPlatform().installDaemon(config);
|
|
136
71
|
console.log("\nStarting pairing...");
|
|
@@ -142,30 +77,6 @@ export async function initCommand() {
|
|
|
142
77
|
throw err;
|
|
143
78
|
}
|
|
144
79
|
}
|
|
145
|
-
async function promptAccessMode(ask, step) {
|
|
146
|
-
console.log(`${bold(`Step ${step}:`)} Choose access mode\n`);
|
|
147
|
-
console.log(` ${bold("1)")} Full access ${dim("— built-in email and push notification support")}`);
|
|
148
|
-
console.log(` ${bold("2)")} LAN only ${dim("— no data relayed by palmier server, requires same network")}`);
|
|
149
|
-
console.log("");
|
|
150
|
-
const answer = await ask("Select [1]: ");
|
|
151
|
-
const trimmed = answer.trim();
|
|
152
|
-
if (trimmed === "2") {
|
|
153
|
-
return "lan";
|
|
154
|
-
}
|
|
155
|
-
return "full";
|
|
156
|
-
}
|
|
157
|
-
async function promptServerUrl(ask, step) {
|
|
158
|
-
const defaultUrl = "https://www.palmier.me";
|
|
159
|
-
console.log(`\n${bold(`Step ${step}:`)} Enter your Palmier server URL\n`);
|
|
160
|
-
while (true) {
|
|
161
|
-
const answer = await ask(`Server URL [${defaultUrl}]: `);
|
|
162
|
-
const trimmed = (answer.trim() || defaultUrl).replace(/\/+$/, "");
|
|
163
|
-
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
164
|
-
return trimmed;
|
|
165
|
-
}
|
|
166
|
-
console.log(" URL must start with http:// or https://. Please try again.\n");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
80
|
async function registerHost(serverUrl, existingHostId) {
|
|
170
81
|
try {
|
|
171
82
|
const res = await fetch(`${serverUrl}/api/hosts/register`, {
|
|
@@ -187,11 +98,4 @@ async function registerHost(serverUrl, existingHostId) {
|
|
|
187
98
|
throw new Error(`Failed to register host: ${message}`);
|
|
188
99
|
}
|
|
189
100
|
}
|
|
190
|
-
async function promptEnableLan(ask, step) {
|
|
191
|
-
console.log(`\n${bold(`Step ${step}:`)} Enable LAN access?\n`);
|
|
192
|
-
console.log(` ${dim("When enabled, the app will automatically use a direct LAN connection")}`);
|
|
193
|
-
console.log(` ${dim("when on the same network, and fall back to the server otherwise.")}\n`);
|
|
194
|
-
const answer = await ask("Enable LAN access? (Y/n): ");
|
|
195
|
-
return answer.trim().toLowerCase() !== "n";
|
|
196
|
-
}
|
|
197
101
|
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start an on-demand LAN server for direct HTTP connections.
|
|
3
|
+
* Generates a pairing code and displays it — no separate `palmier pair` needed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function lanCommand(opts: {
|
|
6
|
+
port: number;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=lan.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { loadConfig, CONFIG_DIR } from "../config.js";
|
|
4
|
+
import { createRpcHandler } from "../rpc-handler.js";
|
|
5
|
+
import { startHttpTransport, detectLanIp } from "../transports/http-transport.js";
|
|
6
|
+
const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
|
|
7
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
8
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
9
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
10
|
+
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
11
|
+
const CODE_LENGTH = 6;
|
|
12
|
+
function generateCode() {
|
|
13
|
+
const bytes = new Uint8Array(CODE_LENGTH);
|
|
14
|
+
crypto.getRandomValues(bytes);
|
|
15
|
+
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
16
|
+
}
|
|
17
|
+
function writeLockfile(port) {
|
|
18
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
fs.writeFileSync(LAN_LOCKFILE, JSON.stringify({ port, pid: process.pid }), "utf-8");
|
|
20
|
+
}
|
|
21
|
+
function removeLockfile() {
|
|
22
|
+
try {
|
|
23
|
+
fs.unlinkSync(LAN_LOCKFILE);
|
|
24
|
+
}
|
|
25
|
+
catch { /* ignore */ }
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start an on-demand LAN server for direct HTTP connections.
|
|
29
|
+
* Generates a pairing code and displays it — no separate `palmier pair` needed.
|
|
30
|
+
*/
|
|
31
|
+
export async function lanCommand(opts) {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
const port = opts.port;
|
|
34
|
+
const ip = detectLanIp();
|
|
35
|
+
const code = generateCode();
|
|
36
|
+
const handleRpc = createRpcHandler(config);
|
|
37
|
+
// Write lockfile so other palmier processes can discover us
|
|
38
|
+
writeLockfile(port);
|
|
39
|
+
// Clean up on exit
|
|
40
|
+
process.on("SIGINT", () => { removeLockfile(); process.exit(0); });
|
|
41
|
+
process.on("SIGTERM", () => { removeLockfile(); process.exit(0); });
|
|
42
|
+
process.on("exit", removeLockfile);
|
|
43
|
+
// Start the HTTP transport with the pre-generated pairing code
|
|
44
|
+
await startHttpTransport(config, handleRpc, port, code, () => {
|
|
45
|
+
console.log(`\n${bold("Palmier LAN Server")}\n`);
|
|
46
|
+
console.log(` ${cyan("Open the app at:")} ${bold(`http://${ip}:${port}`)}\n`);
|
|
47
|
+
console.log(` ${cyan("Pairing code:")} ${bold(code)}\n`);
|
|
48
|
+
console.log(` ${dim("Press Ctrl+C to stop.")}\n`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=lan.js.map
|
|
@@ -6,15 +6,10 @@ import { loadConfig } from "../config.js";
|
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
7
|
export async function mcpserverCommand() {
|
|
8
8
|
const config = loadConfig();
|
|
9
|
-
const
|
|
10
|
-
const useNats = mode === "nats" || mode === "auto";
|
|
11
|
-
let nc;
|
|
12
|
-
if (useNats) {
|
|
13
|
-
nc = await connectNats(config);
|
|
14
|
-
}
|
|
9
|
+
const nc = await connectNats(config);
|
|
15
10
|
const sc = StringCodec();
|
|
16
11
|
const server = new McpServer({ name: "palmier", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
17
|
-
// send-push-notification requires NATS — only register
|
|
12
|
+
// send-push-notification requires NATS — only register when server mode is enabled
|
|
18
13
|
if (nc) {
|
|
19
14
|
server.registerTool("send-push-notification", {
|
|
20
15
|
description: "Send a push notification to all paired devices via the Palmier platform",
|
package/dist/commands/pair.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate an OTP code and wait for a PWA client to pair.
|
|
3
|
-
* Listens on NATS
|
|
3
|
+
* Listens on NATS always, and also on the LAN server if `palmier lan` is running.
|
|
4
4
|
*/
|
|
5
5
|
export declare function pairCommand(): Promise<void>;
|
|
6
6
|
//# sourceMappingURL=pair.d.ts.map
|
package/dist/commands/pair.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import * as http from "node:http";
|
|
2
4
|
import { StringCodec } from "nats";
|
|
3
|
-
import { loadConfig } from "../config.js";
|
|
5
|
+
import { loadConfig, CONFIG_DIR } from "../config.js";
|
|
4
6
|
import { connectNats } from "../nats-client.js";
|
|
5
|
-
import { detectLanIp } from "../transports/http-transport.js";
|
|
6
7
|
import { addSession } from "../session-store.js";
|
|
7
8
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
8
9
|
const CODE_LENGTH = 6;
|
|
9
10
|
const EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
+
const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
|
|
10
12
|
function generateCode() {
|
|
11
13
|
const bytes = new Uint8Array(CODE_LENGTH);
|
|
12
14
|
crypto.getRandomValues(bytes);
|
|
@@ -14,15 +16,13 @@ function generateCode() {
|
|
|
14
16
|
}
|
|
15
17
|
function buildPairResponse(config, label) {
|
|
16
18
|
const session = addSession(label);
|
|
17
|
-
|
|
19
|
+
return {
|
|
18
20
|
hostId: config.hostId,
|
|
19
21
|
sessionToken: session.token,
|
|
20
22
|
};
|
|
21
|
-
return response;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
|
-
* POST to the running
|
|
25
|
-
* Returns true if paired, false if expired/failed.
|
|
25
|
+
* POST to the running LAN server and long-poll until paired or expired.
|
|
26
26
|
*/
|
|
27
27
|
function lanPairRegister(port, code) {
|
|
28
28
|
const body = JSON.stringify({ code, expiryMs: EXPIRY_MS });
|
|
@@ -33,7 +33,7 @@ function lanPairRegister(port, code) {
|
|
|
33
33
|
path: "/internal/pair-register",
|
|
34
34
|
method: "POST",
|
|
35
35
|
headers: { "Content-Type": "application/json" },
|
|
36
|
-
timeout: EXPIRY_MS + 5000,
|
|
36
|
+
timeout: EXPIRY_MS + 5000,
|
|
37
37
|
}, (res) => {
|
|
38
38
|
const chunks = [];
|
|
39
39
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -47,26 +47,30 @@ function lanPairRegister(port, code) {
|
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
-
req.on("error", (
|
|
51
|
-
|
|
52
|
-
console.error("Make sure `palmier serve` is running first.");
|
|
53
|
-
resolve(false);
|
|
54
|
-
});
|
|
55
|
-
req.on("timeout", () => {
|
|
56
|
-
req.destroy();
|
|
57
|
-
resolve(false);
|
|
58
|
-
});
|
|
50
|
+
req.on("error", () => resolve(false));
|
|
51
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
59
52
|
req.end(body);
|
|
60
53
|
});
|
|
61
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Read the LAN lockfile to check if `palmier lan` is running.
|
|
57
|
+
*/
|
|
58
|
+
function getLanPort() {
|
|
59
|
+
try {
|
|
60
|
+
const raw = fs.readFileSync(LAN_LOCKFILE, "utf-8");
|
|
61
|
+
return JSON.parse(raw).port;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
62
67
|
/**
|
|
63
68
|
* Generate an OTP code and wait for a PWA client to pair.
|
|
64
|
-
* Listens on NATS
|
|
69
|
+
* Listens on NATS always, and also on the LAN server if `palmier lan` is running.
|
|
65
70
|
*/
|
|
66
71
|
export async function pairCommand() {
|
|
67
72
|
const config = loadConfig();
|
|
68
73
|
const code = generateCode();
|
|
69
|
-
const mode = config.mode ?? "nats";
|
|
70
74
|
let paired = false;
|
|
71
75
|
function onPaired() {
|
|
72
76
|
paired = true;
|
|
@@ -79,48 +83,40 @@ export async function pairCommand() {
|
|
|
79
83
|
console.log("");
|
|
80
84
|
console.log(` ${code}`);
|
|
81
85
|
console.log("");
|
|
82
|
-
if (mode === "lan" || mode === "auto") {
|
|
83
|
-
const ip = detectLanIp();
|
|
84
|
-
const port = config.directPort ?? 7400;
|
|
85
|
-
console.log(` LAN Address: ${ip}:${port}`);
|
|
86
|
-
console.log("");
|
|
87
|
-
}
|
|
88
86
|
console.log("Code expires in 5 minutes.");
|
|
89
|
-
// NATS pairing (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
label = body.label;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch { /* empty body is fine */ }
|
|
111
|
-
const response = buildPairResponse(config, label);
|
|
112
|
-
if (msg.reply) {
|
|
113
|
-
msg.respond(sc.encode(JSON.stringify(response)));
|
|
87
|
+
// NATS pairing (always active)
|
|
88
|
+
const nc = await connectNats(config);
|
|
89
|
+
const sc = StringCodec();
|
|
90
|
+
const subject = `pair.${code}`;
|
|
91
|
+
const sub = nc.subscribe(subject, { max: 1 });
|
|
92
|
+
cleanups.push(() => {
|
|
93
|
+
sub.unsubscribe();
|
|
94
|
+
nc.close();
|
|
95
|
+
});
|
|
96
|
+
(async () => {
|
|
97
|
+
for await (const msg of sub) {
|
|
98
|
+
if (paired)
|
|
99
|
+
break;
|
|
100
|
+
let label;
|
|
101
|
+
try {
|
|
102
|
+
if (msg.data && msg.data.length > 0) {
|
|
103
|
+
const body = JSON.parse(sc.decode(msg.data));
|
|
104
|
+
label = body.label;
|
|
114
105
|
}
|
|
115
|
-
onPaired();
|
|
116
106
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
catch { /* empty body is fine */ }
|
|
108
|
+
const response = buildPairResponse(config, label);
|
|
109
|
+
if (msg.reply) {
|
|
110
|
+
msg.respond(sc.encode(JSON.stringify(response)));
|
|
111
|
+
}
|
|
112
|
+
onPaired();
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
// LAN pairing — if `palmier lan` is running, also register with it
|
|
116
|
+
const lanPort = getLanPort();
|
|
117
|
+
if (lanPort) {
|
|
122
118
|
(async () => {
|
|
123
|
-
const result = await lanPairRegister(
|
|
119
|
+
const result = await lanPairRegister(lanPort, code);
|
|
124
120
|
if (result)
|
|
125
121
|
onPaired();
|
|
126
122
|
})();
|