palmier 0.2.2 → 0.2.4
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
CHANGED
|
@@ -112,20 +112,15 @@ cat ~/.config/palmier/host.json
|
|
|
112
112
|
|
|
113
113
|
## How It Works
|
|
114
114
|
|
|
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`).
|
|
115
|
+
- The host runs as a **systemd user service**, staying alive in the background via `palmier serve`.
|
|
116
|
+
- **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.
|
|
117
|
+
- **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).
|
|
118
|
+
- **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.
|
|
119
|
+
- **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.
|
|
120
|
+
- **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.
|
|
121
|
+
- **Run history** — each run produces a timestamped result file. You can view results and reports from the PWA.
|
|
122
|
+
- **Real-time updates** — task status changes (started, finished, failed) are pushed to connected PWA clients via NATS pub/sub or SSE.
|
|
123
|
+
- **MCP server** (`palmier mcpserver`) exposes platform tools (e.g., `send-email`) to AI agents like Claude Code over stdio.
|
|
129
124
|
|
|
130
125
|
## Project Structure
|
|
131
126
|
|
package/dist/commands/pair.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as http from "node:http";
|
|
|
2
2
|
import { StringCodec } from "nats";
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
|
+
import { detectLanIp } from "../transports/http-transport.js";
|
|
5
6
|
import { addSession } from "../session-store.js";
|
|
6
7
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
7
8
|
const CODE_LENGTH = 6;
|
|
@@ -78,6 +79,12 @@ export async function pairCommand() {
|
|
|
78
79
|
console.log("");
|
|
79
80
|
console.log(` ${code}`);
|
|
80
81
|
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
|
+
}
|
|
81
88
|
console.log("Code expires in 5 minutes.");
|
|
82
89
|
// NATS pairing (nats or auto mode)
|
|
83
90
|
if (mode === "nats" || mode === "auto") {
|
|
@@ -85,9 +92,9 @@ export async function pairCommand() {
|
|
|
85
92
|
const sc = StringCodec();
|
|
86
93
|
const subject = `pair.${code}`;
|
|
87
94
|
const sub = nc.subscribe(subject, { max: 1 });
|
|
88
|
-
cleanups.push(
|
|
95
|
+
cleanups.push(() => {
|
|
89
96
|
sub.unsubscribe();
|
|
90
|
-
|
|
97
|
+
nc.close();
|
|
91
98
|
});
|
|
92
99
|
(async () => {
|
|
93
100
|
for await (const msg of sub) {
|
|
@@ -134,7 +141,7 @@ export async function pairCommand() {
|
|
|
134
141
|
}
|
|
135
142
|
if (!paired) {
|
|
136
143
|
console.log("Code expired. Run `palmier pair` to try again.");
|
|
137
|
-
process.exit(1);
|
|
138
144
|
}
|
|
145
|
+
process.exit(paired ? 0 : 1);
|
|
139
146
|
}
|
|
140
147
|
//# sourceMappingURL=pair.js.map
|
|
@@ -2,7 +2,7 @@ import * as http from "node:http";
|
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import { validateSession, hasSessions, addSession } from "../session-store.js";
|
|
4
4
|
const pendingPairs = new Map();
|
|
5
|
-
function detectLanIp() {
|
|
5
|
+
export function detectLanIp() {
|
|
6
6
|
const interfaces = os.networkInterfaces();
|
|
7
7
|
for (const name of Object.keys(interfaces)) {
|
|
8
8
|
for (const iface of interfaces[name] ?? []) {
|
package/package.json
CHANGED
package/src/commands/pair.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as http from "node:http";
|
|
|
2
2
|
import { StringCodec } from "nats";
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
|
+
import { detectLanIp } from "../transports/http-transport.js";
|
|
5
6
|
import { addSession } from "../session-store.js";
|
|
6
7
|
import type { HostConfig } from "../types.js";
|
|
7
8
|
|
|
@@ -97,6 +98,14 @@ export async function pairCommand(): Promise<void> {
|
|
|
97
98
|
console.log("");
|
|
98
99
|
console.log(` ${code}`);
|
|
99
100
|
console.log("");
|
|
101
|
+
|
|
102
|
+
if (mode === "lan" || mode === "auto") {
|
|
103
|
+
const ip = detectLanIp();
|
|
104
|
+
const port = config.directPort ?? 7400;
|
|
105
|
+
console.log(` LAN Address: ${ip}:${port}`);
|
|
106
|
+
console.log("");
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
console.log("Code expires in 5 minutes.");
|
|
101
110
|
|
|
102
111
|
// NATS pairing (nats or auto mode)
|
|
@@ -106,9 +115,9 @@ export async function pairCommand(): Promise<void> {
|
|
|
106
115
|
const subject = `pair.${code}`;
|
|
107
116
|
const sub = nc.subscribe(subject, { max: 1 });
|
|
108
117
|
|
|
109
|
-
cleanups.push(
|
|
118
|
+
cleanups.push(() => {
|
|
110
119
|
sub.unsubscribe();
|
|
111
|
-
|
|
120
|
+
nc.close();
|
|
112
121
|
});
|
|
113
122
|
|
|
114
123
|
(async () => {
|
|
@@ -158,6 +167,7 @@ export async function pairCommand(): Promise<void> {
|
|
|
158
167
|
|
|
159
168
|
if (!paired) {
|
|
160
169
|
console.log("Code expired. Run `palmier pair` to try again.");
|
|
161
|
-
process.exit(1);
|
|
162
170
|
}
|
|
171
|
+
|
|
172
|
+
process.exit(paired ? 0 : 1);
|
|
163
173
|
}
|
|
@@ -12,7 +12,7 @@ interface PendingPair {
|
|
|
12
12
|
|
|
13
13
|
const pendingPairs = new Map<string, PendingPair>();
|
|
14
14
|
|
|
15
|
-
function detectLanIp(): string {
|
|
15
|
+
export function detectLanIp(): string {
|
|
16
16
|
const interfaces = os.networkInterfaces();
|
|
17
17
|
for (const name of Object.keys(interfaces)) {
|
|
18
18
|
for (const iface of interfaces[name] ?? []) {
|