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 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
- - Saves host configuration to `~/.config/palmier/host.json`
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
  ```
@@ -5,12 +5,12 @@ export class GeminiAgent {
5
5
  getPlanGenerationCommandLine(prompt) {
6
6
  return {
7
7
  command: "gemini",
8
- args: ["--approval-mode", "auto_edit", "--prompt", prompt],
8
+ args: ["--prompt", prompt],
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const fullPrompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
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", "-"); // read prompt from stdin
24
- return { command: "gemini", args, stdin: fullPrompt };
23
+ args.push("--prompt", prompt);
24
+ return { command: "gemini", args };
25
25
  }
26
26
  async init() {
27
27
  try {
@@ -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 invokeAgentWithContinuation(ctx, invokeTask) {
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 invokeAgentWithContinuation(ctx, task);
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 invokeAgentWithContinuation(ctx, perLineTask);
255
+ const result = await invokeAgentWithRetries(ctx, perLineTask);
256
256
  if (result.outcome === "finished") {
257
257
  invocationsSucceeded++;
258
258
  }
@@ -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
- const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
127
- const child = nodeSpawn(wscript, [DAEMON_VBS_FILE], {
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,
@@ -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) => process.stderr.write(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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -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
  ```
@@ -8,13 +8,13 @@ export class GeminiAgent implements AgentTool {
8
8
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
9
  return {
10
10
  command: "gemini",
11
- args: ["--approval-mode", "auto_edit", "--prompt", prompt],
11
+ args: ["--prompt", prompt],
12
12
  };
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const fullPrompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
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", "-"); // read prompt from stdin
27
+ args.push("--prompt", prompt);
28
28
 
29
- return { command: "gemini", args, stdin: fullPrompt };
29
+ return { command: "gemini", args };
30
30
  }
31
31
 
32
32
  async init(): Promise<boolean> {
@@ -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 invokeAgentWithContinuation(
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 invokeAgentWithContinuation(ctx, task);
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 invokeAgentWithContinuation(ctx, perLineTask);
316
+ const result = await invokeAgentWithRetries(ctx, perLineTask);
317
317
  if (result.outcome === "finished") {
318
318
  invocationsSucceeded++;
319
319
  } else {
@@ -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
- const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
151
- const child = nodeSpawn(wscript, [DAEMON_VBS_FILE], {
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,
@@ -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) => process.stderr.write(d));
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) {