palmier 0.2.3 → 0.2.5
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 +21 -30
- package/dist/agents/claude.js +5 -2
- package/dist/agents/codex.js +4 -2
- package/dist/agents/gemini.js +4 -2
- package/dist/commands/info.js +17 -10
- package/dist/commands/init.d.ts +2 -10
- package/dist/commands/init.js +102 -154
- package/dist/commands/pair.js +3 -3
- package/dist/commands/run.js +2 -13
- package/dist/commands/sessions.js +1 -4
- package/dist/config.js +1 -1
- package/dist/index.js +7 -7
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/linux.d.ts +11 -0
- package/dist/platform/linux.js +186 -0
- package/dist/platform/platform.d.ts +20 -0
- package/dist/platform/platform.js +2 -0
- package/dist/platform/windows.d.ts +11 -0
- package/dist/platform/windows.js +201 -0
- package/dist/rpc-handler.js +13 -30
- package/dist/spawn-command.d.ts +5 -2
- package/dist/spawn-command.js +2 -0
- package/dist/transports/http-transport.js +2 -7
- package/package.json +1 -1
- package/src/agents/claude.ts +6 -2
- package/src/agents/codex.ts +5 -2
- package/src/agents/gemini.ts +5 -2
- package/src/commands/info.ts +18 -10
- package/src/commands/init.ts +131 -180
- package/src/commands/pair.ts +4 -3
- package/src/commands/run.ts +3 -15
- package/src/commands/sessions.ts +1 -4
- package/src/config.ts +1 -1
- package/src/index.ts +8 -7
- package/src/platform/index.ts +16 -0
- package/src/platform/linux.ts +207 -0
- package/src/platform/platform.ts +25 -0
- package/src/platform/windows.ts +223 -0
- package/src/rpc-handler.ts +13 -37
- package/src/spawn-command.ts +7 -2
- package/src/transports/http-transport.ts +2 -5
- package/src/systemd.ts +0 -164
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ The host supports three connection modes:
|
|
|
8
8
|
|
|
9
9
|
| Mode | Transport | Setup | Features |
|
|
10
10
|
|------|-----------|-------|----------|
|
|
11
|
-
| **`nats`** | NATS only | `palmier init
|
|
12
|
-
| **`auto`** | Both NATS + HTTP | `palmier init
|
|
13
|
-
| **`lan`** | HTTP only | `palmier init
|
|
11
|
+
| **`nats`** | NATS only | `palmier init` → Full access, skip LAN | All features |
|
|
12
|
+
| **`auto`** | Both NATS + HTTP | `palmier init` → Full access + LAN | All features. PWA auto-detects best route. |
|
|
13
|
+
| **`lan`** | HTTP only | `palmier init` → LAN only | No push notifications or email MCP. No server needed. |
|
|
14
14
|
|
|
15
15
|
In **auto** mode, the PWA connects directly to the host via HTTP when on the same LAN (lower latency), and falls back to NATS when the host is unreachable. Push notifications and email relay always flow through NATS/server, so no features are lost.
|
|
16
16
|
|
|
@@ -32,8 +32,7 @@ npm install -g palmier
|
|
|
32
32
|
|
|
33
33
|
| Command | Description |
|
|
34
34
|
|---|---|
|
|
35
|
-
| `palmier init
|
|
36
|
-
| `palmier init --lan` | Provision standalone LAN host (no server needed) |
|
|
35
|
+
| `palmier init` | Interactive setup wizard (full access or LAN only) |
|
|
37
36
|
| `palmier pair` | Generate an OTP code to pair a new device |
|
|
38
37
|
| `palmier sessions list` | List active session tokens |
|
|
39
38
|
| `palmier sessions revoke <token>` | Revoke a specific session token |
|
|
@@ -46,20 +45,17 @@ npm install -g palmier
|
|
|
46
45
|
|
|
47
46
|
## Setup
|
|
48
47
|
|
|
49
|
-
###
|
|
48
|
+
### Quick Start
|
|
50
49
|
|
|
51
50
|
1. Install the host: `npm install -g palmier`
|
|
52
|
-
2. Run `palmier init
|
|
53
|
-
3.
|
|
54
|
-
|
|
51
|
+
2. Run `palmier init` in your project directory.
|
|
52
|
+
3. The wizard walks you through:
|
|
53
|
+
- **Full access** or **LAN only** mode
|
|
54
|
+
- Server URL (for full access, defaults to `https://www.palmier.me`)
|
|
55
|
+
- Optional LAN access (for full access — auto-detects best route when enabled)
|
|
56
|
+
4. The host saves config, installs a systemd service, and generates a pairing code.
|
|
55
57
|
5. Enter the pairing code in the Palmier PWA to connect your device.
|
|
56
58
|
|
|
57
|
-
### Standalone LAN mode
|
|
58
|
-
|
|
59
|
-
1. Install the host: `npm install -g palmier`
|
|
60
|
-
2. Run `palmier init --lan` in your project directory.
|
|
61
|
-
3. A pairing code and address are displayed. Enter both in the PWA.
|
|
62
|
-
|
|
63
59
|
### Pairing additional devices
|
|
64
60
|
|
|
65
61
|
Run `palmier pair` on the host to generate a new OTP code. Each paired device gets its own session token.
|
|
@@ -112,20 +108,15 @@ cat ~/.config/palmier/host.json
|
|
|
112
108
|
|
|
113
109
|
## How It Works
|
|
114
110
|
|
|
115
|
-
- The host runs as a **systemd user service**, staying alive in the background
|
|
116
|
-
-
|
|
117
|
-
- **
|
|
118
|
-
- **
|
|
119
|
-
-
|
|
120
|
-
- **
|
|
121
|
-
- **Run history** — each
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
- **Triggers can be enabled/disabled** via the `triggers_enabled` frontmatter field (default `true`). When disabled, systemd timers are removed; when re-enabled, they are reinstalled. Tasks can still be run manually regardless.
|
|
125
|
-
- Incoming tasks are stored as `TASK.md` files in a local `tasks/` directory.
|
|
126
|
-
- Task execution is abstracted through an **`AgentTool` interface** (`src/agents/agent.ts`). Each task stores an `agent` field (e.g., `"claude"`) that selects which agent implementation constructs the full command line and arguments. The agent's `getTaskRunCommandLine(task)` method builds the appropriate flags (e.g., `--allowedTools` for Claude based on task permissions). The process is spawned without a shell, and stdin is closed to prevent tools from hanging on an open pipe. The spawned process inherits the default physical GUI session environment (`DISPLAY=:0`) so commands that launch graphical applications (e.g., headed browsers) run within the user's desktop session. Task lifecycle status (`start`, `finish`, `abort`, `fail`) is persisted to a `status.json` file in the task directory. Status changes are broadcast on a unified `host-event.<host_id>.<task_id>` subject via NATS pub/sub (when available) and/or pushed through SSE (in LAN/auto mode) by `run.ts` POSTing to the serve process's `/internal/event` endpoint. All events carry an `event_type` field (`"running-state"`, `"confirm-request"`, or `"confirm-resolved"`) with the same payload shape on both transports. Consumers (PWA, Web Server) subscribe to these notifications and fetch full status from the host via the `task.status` RPC.
|
|
127
|
-
- **Task confirmation** — tasks with `requires_confirmation: true` set `pending_confirmation: true` in `status.json` before execution. A `confirm-request` event is published on `host-event` so the Web Server can send push notifications and connected PWA clients show a confirmation dialog. The user confirms or aborts via the PWA, which calls the `task.user_input` RPC on the host. The `serve` process sets `user_input` to the user's response (e.g., `"confirmed"` or `"aborted"`), and `run` watches the file for `user_input` to become defined, then updates `running_state` accordingly.
|
|
128
|
-
- **MCP server** (`palmier mcpserver`) — starts an MCP server over stdio that exposes platform tools to AI agents (e.g., Claude Code). In NATS/auto mode, connects to NATS on startup. Currently exposes a `send-email` tool that relays email requests to the Web Server via NATS (`host.<host_id>.email.send`).
|
|
111
|
+
- The host runs as a **systemd user service**, staying alive in the background via `palmier serve`.
|
|
112
|
+
- **Paired devices** communicate with the host via NATS (cloud-routed) and/or direct HTTP (LAN), depending on the connection mode. Each paired device gets a session token that authenticates all requests.
|
|
113
|
+
- **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional triggers (cron schedules or one-time dates).
|
|
114
|
+
- **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.
|
|
115
|
+
- **Triggers** are backed by systemd timers. You can enable/disable them without deleting the task, and any task can still be run manually at any time.
|
|
116
|
+
- **Task confirmation** — tasks can optionally require your approval before running. You'll get a push notification (NATS mode) or a prompt in the PWA to confirm or abort.
|
|
117
|
+
- **Run history** — each run produces a timestamped result file. You can view results and reports from the PWA.
|
|
118
|
+
- **Real-time updates** — task status changes (started, finished, failed) are pushed to connected PWA clients via NATS pub/sub or SSE.
|
|
119
|
+
- **MCP server** (`palmier mcpserver`) exposes platform tools (e.g., `send-email`) to AI agents like Claude Code over stdio.
|
|
129
120
|
|
|
130
121
|
## Project Structure
|
|
131
122
|
|
|
@@ -147,7 +138,7 @@ src/
|
|
|
147
138
|
codex.ts # Codex CLI agent implementation
|
|
148
139
|
openclaw.ts # OpenClaw agent implementation
|
|
149
140
|
commands/
|
|
150
|
-
init.ts #
|
|
141
|
+
init.ts # Interactive setup wizard (auto-pair)
|
|
151
142
|
pair.ts # OTP code generation and pairing handler
|
|
152
143
|
sessions.ts # Session token management CLI (list, revoke, revoke-all)
|
|
153
144
|
info.ts # Print host connection info
|
package/dist/agents/claude.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
3
|
+
// execSync's shell option takes a string (shell path), not boolean.
|
|
4
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
5
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
3
6
|
export class ClaudeAgent {
|
|
4
7
|
getPlanGenerationCommandLine(prompt) {
|
|
5
8
|
return {
|
|
@@ -18,13 +21,13 @@ export class ClaudeAgent {
|
|
|
18
21
|
}
|
|
19
22
|
async init() {
|
|
20
23
|
try {
|
|
21
|
-
execSync("claude --version");
|
|
24
|
+
execSync("claude --version", { shell: SHELL });
|
|
22
25
|
}
|
|
23
26
|
catch {
|
|
24
27
|
return false;
|
|
25
28
|
}
|
|
26
29
|
try {
|
|
27
|
-
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver");
|
|
30
|
+
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { shell: SHELL });
|
|
28
31
|
}
|
|
29
32
|
catch (err) {
|
|
30
33
|
console.warn("Warning: failed to install MCP for Claude:", err instanceof Error ? err.message : err);
|
package/dist/agents/codex.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
3
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
4
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
3
5
|
export class CodexAgent {
|
|
4
6
|
getPlanGenerationCommandLine(prompt) {
|
|
5
7
|
// TODO: fill in
|
|
@@ -24,13 +26,13 @@ export class CodexAgent {
|
|
|
24
26
|
}
|
|
25
27
|
async init() {
|
|
26
28
|
try {
|
|
27
|
-
execSync("codex --version");
|
|
29
|
+
execSync("codex --version", { shell: SHELL });
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
32
|
return false;
|
|
31
33
|
}
|
|
32
34
|
try {
|
|
33
|
-
execSync("codex mcp add palmier palmier mcpserver");
|
|
35
|
+
execSync("codex mcp add palmier palmier mcpserver", { shell: SHELL });
|
|
34
36
|
}
|
|
35
37
|
catch (err) {
|
|
36
38
|
console.warn("Warning: failed to install MCP for Codex:", err instanceof Error ? err.message : err);
|
package/dist/agents/gemini.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
3
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
4
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
3
5
|
export class GeminiAgent {
|
|
4
6
|
getPlanGenerationCommandLine(prompt) {
|
|
5
7
|
// TODO: fill in
|
|
@@ -22,13 +24,13 @@ export class GeminiAgent {
|
|
|
22
24
|
}
|
|
23
25
|
async init() {
|
|
24
26
|
try {
|
|
25
|
-
execSync("gemini --version");
|
|
27
|
+
execSync("gemini --version", { shell: SHELL });
|
|
26
28
|
}
|
|
27
29
|
catch {
|
|
28
30
|
return false;
|
|
29
31
|
}
|
|
30
32
|
try {
|
|
31
|
-
execSync("gemini mcp add --scope user palmier palmier mcpserver");
|
|
33
|
+
execSync("gemini mcp add --scope user palmier palmier mcpserver", { shell: SHELL });
|
|
32
34
|
}
|
|
33
35
|
catch (err) {
|
|
34
36
|
console.warn("Warning: failed to install MCP for Gemini:", err instanceof Error ? err.message : err);
|
package/dist/commands/info.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as os from "os";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
|
+
import { loadSessions } from "../session-store.js";
|
|
3
4
|
/**
|
|
4
5
|
* Detect the first non-internal IPv4 address.
|
|
5
6
|
*/
|
|
@@ -20,21 +21,27 @@ function detectLanIp() {
|
|
|
20
21
|
export async function infoCommand() {
|
|
21
22
|
const config = loadConfig();
|
|
22
23
|
const mode = config.mode ?? "nats";
|
|
23
|
-
|
|
24
|
-
console.log(`Host ID:
|
|
24
|
+
const sessions = loadSessions();
|
|
25
|
+
console.log(`Host ID: ${config.hostId}`);
|
|
26
|
+
console.log(`Mode: ${mode}`);
|
|
27
|
+
console.log(`Project root: ${config.projectRoot}`);
|
|
25
28
|
if (mode === "lan" || mode === "auto") {
|
|
26
29
|
const lanIp = detectLanIp();
|
|
27
30
|
const port = config.directPort ?? 7400;
|
|
28
|
-
console.log(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
console.log(`LAN address: ${lanIp}:${port}`);
|
|
32
|
+
}
|
|
33
|
+
// Detected agents
|
|
34
|
+
if (config.agents && config.agents.length > 0) {
|
|
35
|
+
console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(`Agents: (none detected — run \`palmier agents\`)`);
|
|
32
39
|
}
|
|
33
|
-
|
|
40
|
+
// Sessions
|
|
41
|
+
console.log(`Sessions: ${sessions.length} active`);
|
|
42
|
+
if (sessions.length === 0) {
|
|
34
43
|
console.log("");
|
|
35
|
-
console.log("
|
|
36
|
-
console.log(` URL: ${config.natsUrl}`);
|
|
37
|
-
console.log(` WS: ${config.natsWsUrl}`);
|
|
44
|
+
console.log("No paired clients. Run `palmier pair` to connect a device.");
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
//# sourceMappingURL=info.js.map
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
export interface InitOptions {
|
|
2
|
-
server?: string;
|
|
3
|
-
lan?: boolean;
|
|
4
|
-
host?: string;
|
|
5
|
-
port?: number;
|
|
6
|
-
}
|
|
7
1
|
/**
|
|
8
|
-
*
|
|
9
|
-
* - palmier init --lan → standalone LAN mode, no server needed
|
|
10
|
-
* - palmier init --server <url> → register with server, prompts for nats/auto mode
|
|
2
|
+
* Interactive wizard to provision this host.
|
|
11
3
|
*/
|
|
12
|
-
export declare function initCommand(
|
|
4
|
+
export declare function initCommand(): Promise<void>;
|
|
13
5
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/commands/init.js
CHANGED
|
@@ -1,63 +1,91 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as os from "os";
|
|
4
1
|
import * as readline from "readline";
|
|
5
2
|
import { randomUUID, randomBytes } from "crypto";
|
|
6
|
-
import { execSync } from "child_process";
|
|
7
|
-
import { homedir } from "os";
|
|
8
3
|
import { saveConfig } from "../config.js";
|
|
9
4
|
import { detectAgents } from "../agents/agent.js";
|
|
5
|
+
import { detectLanIp } from "../transports/http-transport.js";
|
|
6
|
+
import { getPlatform } from "../platform/index.js";
|
|
10
7
|
import { pairCommand } from "./pair.js";
|
|
11
8
|
/**
|
|
12
|
-
*
|
|
13
|
-
* - palmier init --lan → standalone LAN mode, no server needed
|
|
14
|
-
* - palmier init --server <url> → register with server, prompts for nats/auto mode
|
|
9
|
+
* Interactive wizard to provision this host.
|
|
15
10
|
*/
|
|
16
|
-
export async function initCommand(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
export async function initCommand() {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
14
|
+
try {
|
|
15
|
+
console.log("\n=== Palmier Host Setup ===\n");
|
|
16
|
+
let step = 1;
|
|
17
|
+
// Step 1: Access mode
|
|
18
|
+
const accessMode = await promptAccessMode(ask, step++);
|
|
19
|
+
let registerResponse;
|
|
20
|
+
if (accessMode === "full") {
|
|
21
|
+
// Step 2: Server URL
|
|
22
|
+
const serverUrl = await promptServerUrl(ask, step++);
|
|
23
|
+
// Register with server
|
|
24
|
+
console.log(`\nRegistering host with ${serverUrl}...`);
|
|
25
|
+
registerResponse = await registerHost(serverUrl);
|
|
26
|
+
console.log("Host registered successfully.");
|
|
27
|
+
}
|
|
28
|
+
let enableLan = accessMode === "lan";
|
|
29
|
+
if (accessMode === "full") {
|
|
30
|
+
// Step N: Optional LAN
|
|
31
|
+
enableLan = await promptEnableLan(ask, step++);
|
|
32
|
+
}
|
|
33
|
+
// Determine mode
|
|
34
|
+
const mode = accessMode === "lan" ? "lan" : enableLan ? "auto" : "nats";
|
|
35
|
+
// Build config
|
|
36
|
+
const config = {
|
|
37
|
+
hostId: registerResponse?.hostId ?? randomUUID(),
|
|
38
|
+
projectRoot: process.cwd(),
|
|
39
|
+
mode,
|
|
40
|
+
};
|
|
41
|
+
if (registerResponse) {
|
|
42
|
+
config.natsUrl = registerResponse.natsUrl;
|
|
43
|
+
config.natsWsUrl = registerResponse.natsWsUrl;
|
|
44
|
+
config.natsToken = registerResponse.natsToken;
|
|
45
|
+
}
|
|
46
|
+
if (enableLan) {
|
|
47
|
+
setupLan(config, step++);
|
|
48
|
+
}
|
|
49
|
+
console.log("\nDetecting installed agents...");
|
|
50
|
+
config.agents = await detectAgents();
|
|
51
|
+
saveConfig(config);
|
|
52
|
+
console.log(`\nHost provisioned (${mode} mode). ID: ${config.hostId}`);
|
|
53
|
+
console.log("Config saved to ~/.config/palmier/host.json");
|
|
54
|
+
getPlatform().installDaemon(config);
|
|
55
|
+
console.log("\nStarting pairing...");
|
|
56
|
+
rl.close();
|
|
57
|
+
await pairCommand();
|
|
22
58
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
catch (err) {
|
|
60
|
+
rl.close();
|
|
61
|
+
throw err;
|
|
26
62
|
}
|
|
27
63
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const hostId = randomUUID();
|
|
33
|
-
const directToken = randomBytes(32).toString("hex");
|
|
34
|
-
const directPort = options.port ?? 7400;
|
|
35
|
-
const lanIp = options.host ?? detectLanIp();
|
|
36
|
-
const config = {
|
|
37
|
-
hostId,
|
|
38
|
-
mode: "lan",
|
|
39
|
-
directPort,
|
|
40
|
-
directToken,
|
|
41
|
-
projectRoot: process.cwd(),
|
|
42
|
-
};
|
|
43
|
-
console.log("Detecting installed agents...");
|
|
44
|
-
config.agents = await detectAgents();
|
|
45
|
-
saveConfig(config);
|
|
46
|
-
console.log(`Host provisioned (LAN mode). ID: ${hostId}`);
|
|
47
|
-
console.log("Config saved to ~/.config/palmier/host.json");
|
|
48
|
-
installSystemdService(config);
|
|
49
|
-
// Auto-enter pair mode so user can connect their PWA immediately
|
|
64
|
+
async function promptAccessMode(ask, step) {
|
|
65
|
+
console.log(`Step ${step}: Choose access mode\n`);
|
|
66
|
+
console.log(" 1) Full access — built-in email and push notification support");
|
|
67
|
+
console.log(" 2) LAN only — no data relayed by palmier server, requires same network");
|
|
50
68
|
console.log("");
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
const answer = await ask("Select [1]: ");
|
|
70
|
+
const trimmed = answer.trim();
|
|
71
|
+
if (trimmed === "2") {
|
|
72
|
+
return "lan";
|
|
73
|
+
}
|
|
74
|
+
return "full";
|
|
53
75
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
async function promptServerUrl(ask, step) {
|
|
77
|
+
const defaultUrl = "https://www.palmier.me";
|
|
78
|
+
console.log(`\nStep ${step}: Enter your Palmier server URL\n`);
|
|
79
|
+
while (true) {
|
|
80
|
+
const answer = await ask(`Server URL [${defaultUrl}]: `);
|
|
81
|
+
const trimmed = (answer.trim() || defaultUrl).replace(/\/+$/, "");
|
|
82
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
|
|
83
|
+
return trimmed;
|
|
84
|
+
}
|
|
85
|
+
console.log(" URL must start with http:// or https://. Please try again.\n");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function registerHost(serverUrl) {
|
|
61
89
|
try {
|
|
62
90
|
const res = await fetch(`${serverUrl}/api/hosts/register`, {
|
|
63
91
|
method: "POST",
|
|
@@ -69,121 +97,41 @@ async function initWithServer(options) {
|
|
|
69
97
|
console.error(`Failed to register host: ${res.status} ${res.statusText}\n${body}`);
|
|
70
98
|
process.exit(1);
|
|
71
99
|
}
|
|
72
|
-
|
|
100
|
+
return (await res.json());
|
|
73
101
|
}
|
|
74
102
|
catch (err) {
|
|
75
103
|
console.error(`Failed to reach server: ${err}`);
|
|
76
104
|
process.exit(1);
|
|
77
105
|
}
|
|
78
|
-
// Prompt for connection mode
|
|
79
|
-
const mode = await promptMode();
|
|
80
|
-
// Build config
|
|
81
|
-
const config = {
|
|
82
|
-
hostId: registerResponse.hostId,
|
|
83
|
-
projectRoot: process.cwd(),
|
|
84
|
-
mode,
|
|
85
|
-
natsUrl: registerResponse.natsUrl,
|
|
86
|
-
natsWsUrl: registerResponse.natsWsUrl,
|
|
87
|
-
natsToken: registerResponse.natsToken,
|
|
88
|
-
};
|
|
89
|
-
if (mode === "auto") {
|
|
90
|
-
const directToken = randomBytes(32).toString("hex");
|
|
91
|
-
const directPort = options.port ?? 7400;
|
|
92
|
-
const lanIp = options.host ?? detectLanIp();
|
|
93
|
-
config.directPort = directPort;
|
|
94
|
-
config.directToken = directToken;
|
|
95
|
-
console.log("");
|
|
96
|
-
console.log("Direct connection info (for LAN clients):");
|
|
97
|
-
console.log(` Address: ${lanIp}:${directPort}`);
|
|
98
|
-
console.log(` Token: ${directToken}`);
|
|
99
|
-
}
|
|
100
|
-
console.log("Detecting installed agents...");
|
|
101
|
-
config.agents = await detectAgents();
|
|
102
|
-
saveConfig(config);
|
|
103
|
-
console.log(`Host provisioned (${mode} mode). ID: ${config.hostId}`);
|
|
104
|
-
console.log("Config saved to ~/.config/palmier/host.json");
|
|
105
|
-
installSystemdService(config);
|
|
106
|
-
// Auto-enter pair mode so user can connect their PWA immediately
|
|
107
|
-
console.log("");
|
|
108
|
-
console.log("Starting pairing...");
|
|
109
|
-
await pairCommand();
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Prompt user to select connection mode.
|
|
113
|
-
*/
|
|
114
|
-
async function promptMode() {
|
|
115
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
-
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
117
|
-
console.log("");
|
|
118
|
-
console.log("Connection mode:");
|
|
119
|
-
console.log(" 1) auto — Both LAN and NATS (recommended)");
|
|
120
|
-
console.log(" 2) nats — NATS only");
|
|
121
|
-
console.log("");
|
|
122
|
-
const answer = await question("Select [1]: ");
|
|
123
|
-
rl.close();
|
|
124
|
-
if (answer.trim() === "2" || answer.trim().toLowerCase() === "nats") {
|
|
125
|
-
return "nats";
|
|
126
|
-
}
|
|
127
|
-
return "auto";
|
|
128
106
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
for (const iface of interfaces[name] ?? []) {
|
|
136
|
-
if (iface.family === "IPv4" && !iface.internal) {
|
|
137
|
-
return iface.address;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return "127.0.0.1";
|
|
107
|
+
async function promptEnableLan(ask, step) {
|
|
108
|
+
console.log(`\nStep ${step}: Enable LAN access?\n`);
|
|
109
|
+
console.log(" When enabled, the app will automatically use a direct LAN connection");
|
|
110
|
+
console.log(" when on the same network, and fall back to the server otherwise.\n");
|
|
111
|
+
const answer = await ask("Enable LAN access? (Y/n): ");
|
|
112
|
+
return answer.trim().toLowerCase() !== "n";
|
|
142
113
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
ExecStart=${palmierBin} serve
|
|
158
|
-
WorkingDirectory=${config.projectRoot}
|
|
159
|
-
Restart=on-failure
|
|
160
|
-
RestartSec=5
|
|
161
|
-
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
162
|
-
|
|
163
|
-
[Install]
|
|
164
|
-
WantedBy=default.target
|
|
165
|
-
`;
|
|
166
|
-
const servicePath = path.join(unitDir, "palmier.service");
|
|
167
|
-
fs.writeFileSync(servicePath, serviceContent, "utf-8");
|
|
168
|
-
console.log("Systemd service installed at:", servicePath);
|
|
169
|
-
// Enable and start the service
|
|
170
|
-
try {
|
|
171
|
-
execSync("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
172
|
-
execSync("systemctl --user enable --now palmier.service", { stdio: "inherit" });
|
|
173
|
-
console.log("Palmier host service enabled and started.");
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
console.error(`Warning: failed to enable systemd service: ${err}`);
|
|
177
|
-
console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
|
|
114
|
+
function setupLan(config, step) {
|
|
115
|
+
const directPort = 7400;
|
|
116
|
+
const directToken = randomBytes(32).toString("hex");
|
|
117
|
+
const lanIp = detectLanIp();
|
|
118
|
+
config.directPort = directPort;
|
|
119
|
+
config.directToken = directToken;
|
|
120
|
+
console.log(`\nStep ${step}: LAN setup\n`);
|
|
121
|
+
console.log(` LAN IP: ${lanIp}`);
|
|
122
|
+
console.log(` Port: ${directPort}`);
|
|
123
|
+
console.log("");
|
|
124
|
+
const platform = process.platform;
|
|
125
|
+
if (platform === "win32") {
|
|
126
|
+
console.log(" Firewall: You may need to allow incoming connections on this port:");
|
|
127
|
+
console.log(` netsh advfirewall firewall add rule name="Palmier" dir=in action=allow protocol=TCP localport=${directPort}`);
|
|
178
128
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
182
|
-
console.log("Login lingering enabled.");
|
|
129
|
+
else if (platform === "darwin") {
|
|
130
|
+
console.log(" Firewall: macOS will prompt you to allow incoming connections automatically.");
|
|
183
131
|
}
|
|
184
|
-
|
|
185
|
-
console.
|
|
132
|
+
else {
|
|
133
|
+
console.log(" Firewall: You may need to allow incoming connections on this port:");
|
|
134
|
+
console.log(` sudo ufw allow ${directPort}/tcp`);
|
|
186
135
|
}
|
|
187
|
-
console.log("\nHost initialization complete!");
|
|
188
136
|
}
|
|
189
137
|
//# sourceMappingURL=init.js.map
|
package/dist/commands/pair.js
CHANGED
|
@@ -92,9 +92,9 @@ export async function pairCommand() {
|
|
|
92
92
|
const sc = StringCodec();
|
|
93
93
|
const subject = `pair.${code}`;
|
|
94
94
|
const sub = nc.subscribe(subject, { max: 1 });
|
|
95
|
-
cleanups.push(
|
|
95
|
+
cleanups.push(() => {
|
|
96
96
|
sub.unsubscribe();
|
|
97
|
-
|
|
97
|
+
nc.close();
|
|
98
98
|
});
|
|
99
99
|
(async () => {
|
|
100
100
|
for await (const msg of sub) {
|
|
@@ -141,7 +141,7 @@ export async function pairCommand() {
|
|
|
141
141
|
}
|
|
142
142
|
if (!paired) {
|
|
143
143
|
console.log("Code expired. Run `palmier pair` to try again.");
|
|
144
|
-
process.exit(1);
|
|
145
144
|
}
|
|
145
|
+
process.exit(paired ? 0 : 1);
|
|
146
146
|
}
|
|
147
147
|
//# sourceMappingURL=pair.js.map
|
package/dist/commands/run.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadConfig } from "../config.js";
|
|
|
5
5
|
import { connectNats } from "../nats-client.js";
|
|
6
6
|
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory } from "../task.js";
|
|
7
7
|
import { getAgent } from "../agents/agent.js";
|
|
8
|
+
import { getPlatform } from "../platform/index.js";
|
|
8
9
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
9
10
|
import { StringCodec } from "nats";
|
|
10
11
|
/**
|
|
@@ -76,7 +77,7 @@ export async function runCommand(taskId) {
|
|
|
76
77
|
console.log("Task confirmed by user.");
|
|
77
78
|
}
|
|
78
79
|
// Execution loop: retry on permission failure if user grants
|
|
79
|
-
const guiEnv = getGuiEnv();
|
|
80
|
+
const guiEnv = getPlatform().getGuiEnv();
|
|
80
81
|
const agent = getAgent(task.frontmatter.agent);
|
|
81
82
|
let lastOutput = "";
|
|
82
83
|
let lastOutcome = "fail";
|
|
@@ -300,16 +301,4 @@ function parseTaskOutcome(output) {
|
|
|
300
301
|
return "finish";
|
|
301
302
|
return "finish";
|
|
302
303
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Return env vars for the default physical GUI session (:0).
|
|
305
|
-
*/
|
|
306
|
-
function getGuiEnv() {
|
|
307
|
-
const uid = process.getuid?.();
|
|
308
|
-
const runtimeDir = process.env.XDG_RUNTIME_DIR ||
|
|
309
|
-
(uid !== undefined ? `/run/user/${uid}` : "");
|
|
310
|
-
return {
|
|
311
|
-
DISPLAY: ":0",
|
|
312
|
-
...(runtimeDir ? { XDG_RUNTIME_DIR: runtimeDir } : {}),
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
304
|
//# sourceMappingURL=run.js.map
|
|
@@ -7,12 +7,9 @@ export async function sessionsListCommand() {
|
|
|
7
7
|
}
|
|
8
8
|
console.log(`${sessions.length} active session(s):\n`);
|
|
9
9
|
for (const s of sessions) {
|
|
10
|
-
const truncated = s.token.slice(0, 8) + "...";
|
|
11
10
|
const label = s.label ? ` (${s.label})` : "";
|
|
12
|
-
console.log(` ${
|
|
11
|
+
console.log(` ${s.token}${label} created ${s.createdAt}`);
|
|
13
12
|
}
|
|
14
|
-
console.log("");
|
|
15
|
-
console.log("To revoke, use the full token: palmier sessions revoke <token>");
|
|
16
13
|
}
|
|
17
14
|
export async function sessionsRevokeCommand(token) {
|
|
18
15
|
if (revokeSession(token)) {
|
package/dist/config.js
CHANGED
|
@@ -9,7 +9,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "host.json");
|
|
|
9
9
|
*/
|
|
10
10
|
export function loadConfig() {
|
|
11
11
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
12
|
-
throw new Error("Host not provisioned. Run `palmier init
|
|
12
|
+
throw new Error("Host not provisioned. Run `palmier init` first.\n" +
|
|
13
13
|
`Expected config at: ${CONFIG_FILE}`);
|
|
14
14
|
}
|
|
15
15
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
package/dist/index.js
CHANGED
|
@@ -22,12 +22,8 @@ program
|
|
|
22
22
|
program
|
|
23
23
|
.command("init")
|
|
24
24
|
.description("Provision this host")
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
.option("--host <ip>", "Override detected LAN IP address")
|
|
28
|
-
.option("--port <port>", "Direct HTTP port (default: 7400)", parseInt)
|
|
29
|
-
.action(async (options) => {
|
|
30
|
-
await initCommand(options);
|
|
25
|
+
.action(async () => {
|
|
26
|
+
await initCommand();
|
|
31
27
|
});
|
|
32
28
|
program
|
|
33
29
|
.command("info")
|
|
@@ -42,7 +38,7 @@ program
|
|
|
42
38
|
await runCommand(taskId);
|
|
43
39
|
});
|
|
44
40
|
program
|
|
45
|
-
.command("serve"
|
|
41
|
+
.command("serve")
|
|
46
42
|
.description("Start the persistent RPC handler")
|
|
47
43
|
.action(async () => {
|
|
48
44
|
await serveCommand();
|
|
@@ -86,6 +82,10 @@ sessionsCmd
|
|
|
86
82
|
.action(async () => {
|
|
87
83
|
await sessionsRevokeAllCommand();
|
|
88
84
|
});
|
|
85
|
+
// No subcommand → default to serve
|
|
86
|
+
if (process.argv.length <= 2) {
|
|
87
|
+
process.argv.push("serve");
|
|
88
|
+
}
|
|
89
89
|
program.parseAsync(process.argv).catch((err) => {
|
|
90
90
|
console.error(err);
|
|
91
91
|
process.exit(1);
|