palmier 0.4.8 → 0.4.9
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 +3 -68
- package/dist/agents/agent-instructions.md +1 -1
- package/dist/agents/gemini.js +5 -5
- package/dist/commands/run.js +3 -3
- package/dist/platform/windows.js +3 -2
- package/dist/spawn-command.js +4 -1
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +1 -1
- package/src/agents/gemini.ts +5 -5
- package/src/commands/run.ts +3 -3
- package/src/platform/windows.ts +3 -2
- package/src/spawn-command.ts +4 -1
package/README.md
CHANGED
|
@@ -85,7 +85,9 @@ palmier sessions revoke-all
|
|
|
85
85
|
|
|
86
86
|
The `init` command:
|
|
87
87
|
- Detects installed agent CLIs (Claude Code, Gemini CLI, Codex CLI, GitHub Copilot) and caches the result
|
|
88
|
-
-
|
|
88
|
+
- Configures access modes (HTTP port, LAN access)
|
|
89
|
+
- Shows a summary and asks for confirmation before making changes
|
|
90
|
+
- Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
|
|
89
91
|
- Installs a background daemon (systemd user service on Linux, Registry Run key on Windows)
|
|
90
92
|
- Auto-enters pair mode to connect your first device
|
|
91
93
|
|
|
@@ -128,76 +130,9 @@ palmier restart
|
|
|
128
130
|
- **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).
|
|
129
131
|
- **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.
|
|
130
132
|
- **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.
|
|
131
|
-
- **Task execution** uses the system scheduler on both platforms — `systemctl --user start` on Linux, `schtasks /run` on Windows. On Windows, tasks run via a VBS wrapper (`wscript.exe`) to avoid visible console 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.
|
|
132
133
|
- **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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
134
|
- **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.
|
|
138
135
|
|
|
139
|
-
## NATS Subjects
|
|
140
|
-
|
|
141
|
-
| Subject | Direction | Description |
|
|
142
|
-
|---|---|---|
|
|
143
|
-
| `host.<hostId>.rpc.<method>` | Client → Host | RPC request/reply (e.g., `task.list`, `task.create`) |
|
|
144
|
-
| `host-event.<hostId>.<taskId>` | Host → Client | Real-time task events (`running-state`, `result-updated`, `confirm-request`, `permission-request`, `input-request`) |
|
|
145
|
-
| `host.<hostId>.push.send` | Host → Server | Request server to deliver a push notification |
|
|
146
|
-
| `pair.<code>` | Client → Host | OTP pairing request/reply |
|
|
147
|
-
|
|
148
|
-
## Project Structure
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
src/
|
|
152
|
-
index.ts # CLI entrypoint (commander setup)
|
|
153
|
-
config.ts # Host configuration (read/write ~/.config/palmier)
|
|
154
|
-
rpc-handler.ts # Transport-agnostic RPC handler (with session validation)
|
|
155
|
-
session-store.ts # Session token management (~/.config/palmier/sessions.json)
|
|
156
|
-
nats-client.ts # NATS connection helper
|
|
157
|
-
spawn-command.ts # Shared helper for spawning CLI tools
|
|
158
|
-
task.ts # Task file management
|
|
159
|
-
types.ts # Shared type definitions
|
|
160
|
-
pending-requests.ts # In-memory registry for held HTTP connections (confirmation, permission, input)
|
|
161
|
-
events.ts # Event broadcasting (NATS pub/sub or HTTP SSE)
|
|
162
|
-
agents/
|
|
163
|
-
agent.ts # AgentTool interface, registry, and agent detection
|
|
164
|
-
shared-prompt.ts # Agent instructions loader
|
|
165
|
-
agent-instructions.md # System prompt injected into every agent invocation
|
|
166
|
-
claude.ts # Claude Code agent implementation
|
|
167
|
-
gemini.ts # Gemini CLI agent implementation
|
|
168
|
-
codex.ts # Codex CLI agent implementation
|
|
169
|
-
copilot.ts # GitHub Copilot agent implementation
|
|
170
|
-
openclaw.ts # OpenClaw agent implementation
|
|
171
|
-
commands/
|
|
172
|
-
init.ts # Interactive setup wizard (auto-pair)
|
|
173
|
-
pair.ts # OTP code generation and pairing handler
|
|
174
|
-
sessions.ts # Session token management CLI (list, revoke, revoke-all)
|
|
175
|
-
info.ts # Print host connection info
|
|
176
|
-
|
|
177
|
-
serve.ts # NATS + HTTP transport startup, crash detection polling
|
|
178
|
-
restart.ts # Daemon restart (cross-platform)
|
|
179
|
-
run.ts # Single task execution
|
|
180
|
-
platform/
|
|
181
|
-
platform.ts # PlatformService interface
|
|
182
|
-
index.ts # Platform factory (Linux vs Windows)
|
|
183
|
-
linux.ts # Linux: systemd daemon, timers, systemctl task control
|
|
184
|
-
windows.ts # Windows: Registry Run key, Task Scheduler, schtasks-based task control
|
|
185
|
-
transports/
|
|
186
|
-
nats-transport.ts # NATS subscription loop (host.<hostId>.rpc.>)
|
|
187
|
-
http-transport.ts # HTTP server with RPC, SSE, PWA reverse proxy, and internal event endpoints
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## Agent HTTP Endpoints
|
|
191
|
-
|
|
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.
|
|
193
|
-
|
|
194
|
-
| Endpoint | Method | Description |
|
|
195
|
-
|---|---|---|
|
|
196
|
-
| `/notify` | POST | Send a push notification (requires server mode) |
|
|
197
|
-
| `/request-input` | POST | Request user input; blocks until a response is provided |
|
|
198
|
-
|
|
199
|
-
See [agent-instructions.md](src/agents/agent-instructions.md) for usage examples.
|
|
200
|
-
|
|
201
136
|
## Uninstalling
|
|
202
137
|
|
|
203
138
|
To fully remove Palmier from a machine:
|
|
@@ -24,7 +24,7 @@ If the task fails because a tool was denied or you lack the required permissions
|
|
|
24
24
|
|
|
25
25
|
The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
|
|
26
26
|
|
|
27
|
-
**Requesting user input** — When you need information from the user (credentials, questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout. Instead, POST to `/request-input` with:
|
|
27
|
+
**Requesting user input** — When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment. Instead, POST to `/request-input` with:
|
|
28
28
|
```json
|
|
29
29
|
{"taskId":"{{TASK_ID}}","descriptions":["question 1","question 2"]}
|
|
30
30
|
```
|
package/dist/agents/gemini.js
CHANGED
|
@@ -5,12 +5,12 @@ export class GeminiAgent {
|
|
|
5
5
|
getPlanGenerationCommandLine(prompt) {
|
|
6
6
|
return {
|
|
7
7
|
command: "gemini",
|
|
8
|
-
args: ["--
|
|
8
|
+
args: ["--prompt", prompt],
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
|
|
12
|
-
const
|
|
13
|
-
const args = ["--allowed-tools", "web_fetch"];
|
|
12
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
13
|
+
const args = ["--approval-mode", "auto_edit", "--allowed-tools", "web_fetch"];
|
|
14
14
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
15
15
|
if (allPerms.length > 0) {
|
|
16
16
|
for (const p of allPerms) {
|
|
@@ -20,8 +20,8 @@ export class GeminiAgent {
|
|
|
20
20
|
if (followupPrompt) {
|
|
21
21
|
args.push("--resume");
|
|
22
22
|
} // continue mode for followups
|
|
23
|
-
args.push("--prompt",
|
|
24
|
-
return { command: "gemini", args
|
|
23
|
+
args.push("--prompt", prompt);
|
|
24
|
+
return { command: "gemini", args };
|
|
25
25
|
}
|
|
26
26
|
async init() {
|
|
27
27
|
try {
|
package/dist/commands/run.js
CHANGED
|
@@ -16,7 +16,7 @@ import { publishHostEvent } from "../events.js";
|
|
|
16
16
|
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
17
17
|
* (for command-triggered mode this is the per-line augmented task).
|
|
18
18
|
*/
|
|
19
|
-
async function
|
|
19
|
+
async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
20
20
|
// eslint-disable-next-line no-constant-condition
|
|
21
21
|
while (true) {
|
|
22
22
|
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
|
|
@@ -174,7 +174,7 @@ export async function runCommand(taskId) {
|
|
|
174
174
|
time: Date.now(),
|
|
175
175
|
content: task.body || task.frontmatter.user_prompt,
|
|
176
176
|
});
|
|
177
|
-
const result = await
|
|
177
|
+
const result = await invokeAgentWithRetries(ctx, task);
|
|
178
178
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
179
179
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
180
180
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
@@ -252,7 +252,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
252
252
|
frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
|
|
253
253
|
body: "",
|
|
254
254
|
};
|
|
255
|
-
const result = await
|
|
255
|
+
const result = await invokeAgentWithRetries(ctx, perLineTask);
|
|
256
256
|
if (result.outcome === "finished") {
|
|
257
257
|
invocationsSucceeded++;
|
|
258
258
|
}
|
package/dist/platform/windows.js
CHANGED
|
@@ -123,8 +123,9 @@ export class WindowsPlatform {
|
|
|
123
123
|
// Write a VBS launcher that starts the daemon with no visible console window.
|
|
124
124
|
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
125
125
|
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
// Use `cmd /c start` to break out of the SSH session's job object.
|
|
127
|
+
// Without this, the daemon is killed when the SSH session disconnects.
|
|
128
|
+
const child = nodeSpawn("cmd", ["/c", "start", "/b", "wscript.exe", DAEMON_VBS_FILE], {
|
|
128
129
|
detached: true,
|
|
129
130
|
stdio: "ignore",
|
|
130
131
|
windowsHide: true,
|
package/dist/spawn-command.js
CHANGED
|
@@ -58,7 +58,10 @@ export function spawnCommand(command, args, opts) {
|
|
|
58
58
|
if (opts.echoStdout)
|
|
59
59
|
process.stdout.write(d);
|
|
60
60
|
});
|
|
61
|
-
child.stderr.on("data", (d) =>
|
|
61
|
+
child.stderr.on("data", (d) => {
|
|
62
|
+
chunks.push(d);
|
|
63
|
+
process.stderr.write(d);
|
|
64
|
+
});
|
|
62
65
|
let timer;
|
|
63
66
|
if (opts.timeout) {
|
|
64
67
|
timer = setTimeout(() => {
|
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ If the task fails because a tool was denied or you lack the required permissions
|
|
|
24
24
|
|
|
25
25
|
The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
|
|
26
26
|
|
|
27
|
-
**Requesting user input** — When you need information from the user (credentials, questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout. Instead, POST to `/request-input` with:
|
|
27
|
+
**Requesting user input** — When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment. Instead, POST to `/request-input` with:
|
|
28
28
|
```json
|
|
29
29
|
{"taskId":"{{TASK_ID}}","descriptions":["question 1","question 2"]}
|
|
30
30
|
```
|
package/src/agents/gemini.ts
CHANGED
|
@@ -8,13 +8,13 @@ export class GeminiAgent implements AgentTool {
|
|
|
8
8
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
9
|
return {
|
|
10
10
|
command: "gemini",
|
|
11
|
-
args: ["--
|
|
11
|
+
args: ["--prompt", prompt],
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
17
|
-
const args = ["--allowed-tools", "web_fetch"];
|
|
16
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
17
|
+
const args = ["--approval-mode", "auto_edit", "--allowed-tools", "web_fetch"];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
20
20
|
if (allPerms.length > 0) {
|
|
@@ -24,9 +24,9 @@ export class GeminiAgent implements AgentTool {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (followupPrompt) {args.push("--resume");} // continue mode for followups
|
|
27
|
-
args.push("--prompt",
|
|
27
|
+
args.push("--prompt", prompt);
|
|
28
28
|
|
|
29
|
-
return { command: "gemini", args
|
|
29
|
+
return { command: "gemini", args };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async init(): Promise<boolean> {
|
package/src/commands/run.ts
CHANGED
|
@@ -41,7 +41,7 @@ interface InvocationResult {
|
|
|
41
41
|
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
42
42
|
* (for command-triggered mode this is the per-line augmented task).
|
|
43
43
|
*/
|
|
44
|
-
async function
|
|
44
|
+
async function invokeAgentWithRetries(
|
|
45
45
|
ctx: InvocationContext,
|
|
46
46
|
invokeTask: ParsedTask,
|
|
47
47
|
): Promise<InvocationResult> {
|
|
@@ -226,7 +226,7 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
226
226
|
content: task.body || task.frontmatter.user_prompt,
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
const result = await
|
|
229
|
+
const result = await invokeAgentWithRetries(ctx, task);
|
|
230
230
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
231
231
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
232
232
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
@@ -313,7 +313,7 @@ async function runCommandTriggeredMode(
|
|
|
313
313
|
body: "",
|
|
314
314
|
};
|
|
315
315
|
|
|
316
|
-
const result = await
|
|
316
|
+
const result = await invokeAgentWithRetries(ctx, perLineTask);
|
|
317
317
|
if (result.outcome === "finished") {
|
|
318
318
|
invocationsSucceeded++;
|
|
319
319
|
} else {
|
package/src/platform/windows.ts
CHANGED
|
@@ -147,8 +147,9 @@ export class WindowsPlatform implements PlatformService {
|
|
|
147
147
|
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
148
148
|
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
// Use `cmd /c start` to break out of the SSH session's job object.
|
|
151
|
+
// Without this, the daemon is killed when the SSH session disconnects.
|
|
152
|
+
const child = nodeSpawn("cmd", ["/c", "start", "/b", "wscript.exe", DAEMON_VBS_FILE], {
|
|
152
153
|
detached: true,
|
|
153
154
|
stdio: "ignore",
|
|
154
155
|
windowsHide: true,
|
package/src/spawn-command.ts
CHANGED
|
@@ -106,7 +106,10 @@ export function spawnCommand(
|
|
|
106
106
|
chunks.push(d);
|
|
107
107
|
if (opts.echoStdout) process.stdout.write(d);
|
|
108
108
|
});
|
|
109
|
-
child.stderr!.on("data", (d: Buffer) =>
|
|
109
|
+
child.stderr!.on("data", (d: Buffer) => {
|
|
110
|
+
chunks.push(d);
|
|
111
|
+
process.stderr.write(d);
|
|
112
|
+
});
|
|
110
113
|
|
|
111
114
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
112
115
|
if (opts.timeout) {
|