palmier 0.4.5 → 0.4.6

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.
Files changed (57) hide show
  1. package/README.md +29 -31
  2. package/dist/agents/agent-instructions.md +4 -11
  3. package/dist/agents/claude.js +3 -3
  4. package/dist/agents/codex.js +2 -2
  5. package/dist/agents/copilot.js +3 -3
  6. package/dist/agents/gemini.js +3 -3
  7. package/dist/agents/openclaw.js +2 -2
  8. package/dist/agents/shared-prompt.d.ts +2 -4
  9. package/dist/agents/shared-prompt.js +9 -4
  10. package/dist/commands/init.js +31 -2
  11. package/dist/commands/pair.d.ts +1 -1
  12. package/dist/commands/pair.js +12 -15
  13. package/dist/commands/run.js +19 -43
  14. package/dist/commands/serve.d.ts +1 -1
  15. package/dist/commands/serve.js +9 -2
  16. package/dist/events.d.ts +2 -2
  17. package/dist/events.js +15 -16
  18. package/dist/index.js +0 -25
  19. package/dist/pending-requests.d.ts +27 -0
  20. package/dist/pending-requests.js +39 -0
  21. package/dist/rpc-handler.js +15 -8
  22. package/dist/transports/http-transport.d.ts +4 -2
  23. package/dist/transports/http-transport.js +226 -77
  24. package/dist/types.d.ts +7 -16
  25. package/package.json +1 -1
  26. package/src/agents/agent-instructions.md +4 -11
  27. package/src/agents/claude.ts +3 -3
  28. package/src/agents/codex.ts +2 -2
  29. package/src/agents/copilot.ts +3 -3
  30. package/src/agents/gemini.ts +3 -3
  31. package/src/agents/openclaw.ts +2 -2
  32. package/src/agents/shared-prompt.ts +12 -6
  33. package/src/commands/init.ts +34 -3
  34. package/src/commands/pair.ts +11 -14
  35. package/src/commands/run.ts +17 -57
  36. package/src/commands/serve.ts +11 -2
  37. package/src/events.ts +14 -15
  38. package/src/index.ts +0 -26
  39. package/src/pending-requests.ts +55 -0
  40. package/src/rpc-handler.ts +15 -9
  41. package/src/transports/http-transport.ts +235 -135
  42. package/src/types.ts +10 -16
  43. package/dist/commands/lan.d.ts +0 -8
  44. package/dist/commands/lan.js +0 -44
  45. package/dist/commands/notify.d.ts +0 -9
  46. package/dist/commands/notify.js +0 -43
  47. package/dist/commands/request-input.d.ts +0 -10
  48. package/dist/commands/request-input.js +0 -49
  49. package/dist/lan-lock.d.ts +0 -7
  50. package/dist/lan-lock.js +0 -18
  51. package/dist/user-input.d.ts +0 -15
  52. package/dist/user-input.js +0 -50
  53. package/src/commands/lan.ts +0 -48
  54. package/src/commands/notify.ts +0 -44
  55. package/src/commands/request-input.ts +0 -51
  56. package/src/lan-lock.ts +0 -16
  57. package/src/user-input.ts +0 -67
package/dist/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface HostConfig {
8
8
  key: string;
9
9
  label: string;
10
10
  }>;
11
+ httpPort?: number;
12
+ lanEnabled?: boolean;
11
13
  }
12
14
  export interface TaskFrontmatter {
13
15
  id: string;
@@ -29,8 +31,6 @@ export interface ParsedTask {
29
31
  body: string;
30
32
  }
31
33
  /**
32
- * State machine: started → (pending_confirmation | pending_permission | pending_input) → finished | aborted | failed
33
- *
34
34
  * - `started`: task is actively running
35
35
  * - `finished`: agent completed successfully
36
36
  * - `aborted`: user declined confirmation, permission, or input
@@ -38,26 +38,15 @@ export interface ParsedTask {
38
38
  */
39
39
  export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
40
40
  /**
41
- * Persisted to `status.json` in the task directory. Updated by the run process
42
- * and read by the RPC handler + PWA to track live task state.
43
- *
44
- * Interactive request flow: the run process sets a `pending_*` field and waits
45
- * for `user_input` to be populated by an RPC call (task.user_input). Only one
46
- * `pending_*` field is set at a time.
41
+ * Persisted to `status.json` in the task directory. Used for crash detection
42
+ * (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
43
+ * permission, input) are handled via held HTTP connections on the serve daemon.
47
44
  */
48
45
  export interface TaskStatus {
49
46
  running_state: TaskRunningState;
50
47
  time_stamp: number;
51
48
  /** PID of the palmier run process (used on Windows to kill the process tree). */
52
49
  pid?: number;
53
- /** Set when the task has `requires_confirmation` and is awaiting user approval. */
54
- pending_confirmation?: boolean;
55
- /** Set when the agent requests permissions not yet granted. Contains the permissions needed. */
56
- pending_permission?: RequiredPermission[];
57
- /** Set when the agent requests user input. Contains descriptions of each requested value. */
58
- pending_input?: string[];
59
- /** Written by the RPC handler to deliver the user's response to the waiting run process. */
60
- user_input?: string[];
61
50
  }
62
51
  export interface HistoryEntry {
63
52
  task_id: string;
@@ -78,5 +67,7 @@ export interface RpcMessage {
78
67
  method: string;
79
68
  params: Record<string, unknown>;
80
69
  sessionToken?: string;
70
+ /** Trusted localhost request — skip session validation. */
71
+ localhost?: boolean;
81
72
  }
82
73
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -20,20 +20,13 @@ If the task fails because a tool was denied or you lack the required permissions
20
20
  [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
21
  [PALMIER_PERMISSION] Write | Write generated output files
22
22
 
23
- ## CLI Commands
23
+ ## HTTP Endpoints
24
24
 
25
- You have access to the following palmier CLI commands:
25
+ The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution.
26
26
 
27
- **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, request it:
28
- ```
29
- palmier request-input --description "What is the database connection string?" --description "What is the API key?"
30
- ```
31
- The command blocks until the user responds and prints each value on its own line. If the user aborts, the command exits with a non-zero status.
27
+ **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, GET `/request-input?taskId={{TASK_ID}}&descriptions=question+1&descriptions=question+2`. The request blocks until the user responds. The response is `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user chooses to abort.
32
28
 
33
- **Sending push notifications** — If you need to send a push notification to the user:
34
- ```
35
- palmier notify --title "Task Complete" --body "The deployment finished successfully."
36
- ```
29
+ **Sending push notifications** — GET `/notify?title=...&body=...` to send a push notification to the user's devices.
37
30
 
38
31
  ---
39
32
 
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class ClaudeAgent implements AgentTool {
@@ -13,8 +13,8 @@ export class ClaudeAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
- const args = ["--permission-mode", "acceptEdits", "-p"];
16
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
+ const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
20
  for (const p of allPerms) {
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CodexAgent implements AgentTool {
@@ -13,7 +13,7 @@ export class CodexAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
16
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
17
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
18
18
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
19
19
 
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
@@ -13,8 +13,8 @@ export class CopilotAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
- const args = ["-p", prompt];
16
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
+ const args = ["-p", prompt, "--allowed-tools", "web_fetch"];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
20
  if (allPerms.length > 0) {
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
@@ -14,8 +14,8 @@ export class GeminiAgent implements AgentTool {
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
16
  const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
17
- const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
18
- const args = ["--prompt", "-"];
17
+ const fullPrompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + prompt;
18
+ const args = ["--prompt", "--allowed-tools", "web_fetch", "-"];
19
19
 
20
20
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
21
  if (allPerms.length > 0) {
@@ -1,7 +1,7 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { execSync } from "child_process";
3
3
  import type { AgentTool, CommandLine } from "./agent.js";
4
- import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
7
  getPlanGenerationCommandLine(prompt: string): CommandLine {
@@ -12,7 +12,7 @@ export class OpenClawAgent implements AgentTool {
12
12
  }
13
13
 
14
14
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
15
+ const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
16
16
  // OpenClaw does not support stdin as prompt.
17
17
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
18
18
 
@@ -1,19 +1,25 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
+ import { loadConfig } from "../config.js";
4
5
 
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7
 
7
- /**
8
- * Instructions prepended or injected as system prompt for every task invocation.
9
- * Instructs the agent to output structured markers so palmier can determine
10
- * the task outcome, report files, and permission/input requests.
11
- */
12
- export const AGENT_INSTRUCTIONS = fs.readFileSync(
8
+ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
13
9
  path.join(__dirname, "agent-instructions.md"),
14
10
  "utf-8",
15
11
  );
16
12
 
13
+ /**
14
+ * Agent instructions with the serve daemon's HTTP port and task ID baked in.
15
+ */
16
+ export function getAgentInstructions(taskId: string): string {
17
+ const port = loadConfig().httpPort ?? 7400;
18
+ return AGENT_INSTRUCTIONS_TEMPLATE
19
+ .replace(/\{\{PORT\}\}/g, String(port))
20
+ .replace(/\{\{TASK_ID\}\}/g, taskId);
21
+ }
22
+
17
23
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
18
24
  export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
19
25
  export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
@@ -3,6 +3,7 @@ import { loadConfig, saveConfig } from "../config.js";
3
3
  import { detectAgents } from "../agents/agent.js";
4
4
  import { getPlatform } from "../platform/index.js";
5
5
  import { pairCommand } from "./pair.js";
6
+ import { detectLanIp } from "../transports/http-transport.js";
6
7
  import type { HostConfig } from "../types.js";
7
8
 
8
9
  type AskFn = (q: string) => Promise<string>;
@@ -39,6 +40,36 @@ export async function initCommand(): Promise<void> {
39
40
 
40
41
  console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
41
42
 
43
+ // LAN mode
44
+ const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
45
+ const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
46
+
47
+ let httpPort = 7400;
48
+ const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
49
+ const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
50
+ const parsed = parseInt(portAnswer.trim(), 10);
51
+ if (parsed > 0 && parsed < 65536) httpPort = parsed;
52
+
53
+ // Display summary and ask for confirmation before making any changes
54
+ console.log(`\n${bold("Setup summary:")}\n`);
55
+ console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
56
+ console.log(` All tasks and execution data will be stored here.\n`);
57
+ console.log(` ${dim("Local access:")} ${cyan(`http://localhost:${httpPort}`)}`);
58
+ console.log(` Always available — no internet required.\n`);
59
+ if (lanEnabled) {
60
+ const ip = detectLanIp();
61
+ console.log(` ${dim("LAN access:")} ${cyan(`http://${ip}:${httpPort}`)}`);
62
+ console.log(` Accessible from other devices on your local network. Pairing required.\n`);
63
+ }
64
+ console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
65
+
66
+ const confirm = await ask("Proceed? (Y/n): ");
67
+ if (confirm.trim().toLowerCase() === "n") {
68
+ console.log("\nSetup cancelled.");
69
+ rl.close();
70
+ return;
71
+ }
72
+
42
73
  // Register with server
43
74
  let existingHostId: string | undefined;
44
75
  try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
@@ -63,7 +94,7 @@ export async function initCommand(): Promise<void> {
63
94
  }
64
95
  }
65
96
 
66
- // Build config
97
+ // Build and save config
67
98
  const config: HostConfig = {
68
99
  hostId: registerResponse.hostId,
69
100
  projectRoot: process.cwd(),
@@ -71,11 +102,11 @@ export async function initCommand(): Promise<void> {
71
102
  natsWsUrl: registerResponse.natsWsUrl,
72
103
  natsToken: registerResponse.natsToken,
73
104
  agents,
105
+ httpPort,
106
+ lanEnabled,
74
107
  };
75
108
 
76
109
  saveConfig(config);
77
-
78
- console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
79
110
  console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
80
111
 
81
112
  getPlatform().installDaemon(config);
@@ -3,7 +3,6 @@ import { StringCodec } from "nats";
3
3
  import { loadConfig } from "../config.js";
4
4
  import { connectNats } from "../nats-client.js";
5
5
  import { addSession } from "../session-store.js";
6
- import { getLanPort } from "../lan-lock.js";
7
6
  import type { HostConfig } from "../types.js";
8
7
 
9
8
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
@@ -26,9 +25,9 @@ function buildPairResponse(config: HostConfig, label?: string) {
26
25
  }
27
26
 
28
27
  /**
29
- * POST to the running LAN server and long-poll until paired or expired.
28
+ * POST to the running serve daemon and long-poll until paired or expired.
30
29
  */
31
- function lanPairRegister(port: number, code: string): Promise<boolean> {
30
+ function httpPairRegister(port: number, code: string): Promise<boolean> {
32
31
  const body = JSON.stringify({ code, expiryMs: PAIRING_EXPIRY_MS });
33
32
 
34
33
  return new Promise((resolve) => {
@@ -36,7 +35,7 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
36
35
  {
37
36
  hostname: "127.0.0.1",
38
37
  port,
39
- path: "/internal/pair-register",
38
+ path: "/pair-register",
40
39
  method: "POST",
41
40
  headers: { "Content-Type": "application/json" },
42
41
  timeout: PAIRING_EXPIRY_MS + 5000,
@@ -63,11 +62,12 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
63
62
 
64
63
  /**
65
64
  * Generate an OTP code and wait for a PWA client to pair.
66
- * Listens on NATS always, and also on the LAN server if `palmier lan` is running.
65
+ * Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
67
66
  */
68
67
  export async function pairCommand(): Promise<void> {
69
68
  const config = loadConfig();
70
69
  const code = generatePairingCode();
70
+ const httpPort = config.httpPort ?? 7400;
71
71
 
72
72
  let paired = false;
73
73
 
@@ -86,7 +86,7 @@ export async function pairCommand(): Promise<void> {
86
86
  console.log("");
87
87
  console.log("Code expires in 5 minutes.");
88
88
 
89
- // NATS pairing (always active)
89
+ // NATS pairing (server mode)
90
90
  const nc = await connectNats(config);
91
91
  const sc = StringCodec();
92
92
  const subject = `pair.${code}`;
@@ -116,14 +116,11 @@ export async function pairCommand(): Promise<void> {
116
116
  }
117
117
  })();
118
118
 
119
- // LAN pairing — if `palmier lan` is running, also register with it
120
- const lanPort = getLanPort();
121
- if (lanPort) {
122
- (async () => {
123
- const result = await lanPairRegister(lanPort, code);
124
- if (result) onPaired();
125
- })();
126
- }
119
+ // HTTP pairing — register with serve daemon's /pair-register endpoint
120
+ (async () => {
121
+ const result = await httpPairRegister(httpPort, code);
122
+ if (result) onPaired();
123
+ })();
127
124
 
128
125
  // Wait for pairing or timeout
129
126
  const start = Date.now();
@@ -10,7 +10,6 @@ import { getPlatform } from "../platform/index.js";
10
10
  import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
11
11
  import type { AgentTool } from "../agents/agent.js";
12
12
  import { publishHostEvent } from "../events.js";
13
- import { waitForUserInput } from "../user-input.js";
14
13
  import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
15
14
  import type { NatsConnection } from "nats";
16
15
 
@@ -52,7 +51,7 @@ async function invokeAgentWithContinuation(
52
51
  const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
53
52
  const result = await spawnCommand(command, args, {
54
53
  cwd: getRunDir(ctx.taskDir, ctx.runId),
55
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
54
+ env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
56
55
  echoStdout: true,
57
56
  resolveOnFailure: true,
58
57
  stdin,
@@ -72,8 +71,7 @@ async function invokeAgentWithContinuation(
72
71
 
73
72
  // Permission handling — agent requested permissions
74
73
  if (requiredPermissions.length > 0) {
75
- const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
76
- await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
74
+ const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
77
75
 
78
76
  if (response === "aborted") {
79
77
  await appendAndNotify(ctx, {
@@ -194,9 +192,7 @@ export async function runCommand(taskId: string): Promise<void> {
194
192
 
195
193
  // If requires_confirmation, notify clients and wait
196
194
  if (task.frontmatter.requires_confirmation) {
197
- const confirmed = await requestConfirmation(nc, config, task, taskDir);
198
- const resolvedStatus = confirmed ? "confirmed" : "aborted";
199
- await publishConfirmResolved(nc, config, taskId, resolvedStatus);
195
+ const confirmed = await requestConfirmation(config, task, taskDir);
200
196
  if (!confirmed) {
201
197
  console.log("Task aborted by user.");
202
198
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
@@ -275,7 +271,7 @@ async function runCommandTriggeredMode(
275
271
 
276
272
  const child = spawnStreamingCommand(commandStr, {
277
273
  cwd: getRunDir(ctx.taskDir, ctx.runId),
278
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
274
+ env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
279
275
  });
280
276
 
281
277
  let linesProcessed = 0;
@@ -408,41 +404,21 @@ async function publishTaskEvent(
408
404
  await publishHostEvent(nc, config.hostId, taskId, payload);
409
405
  }
410
406
 
411
- /**
412
- * Notify clients that a confirmation request has been resolved.
413
- */
414
- async function publishConfirmResolved(
415
- nc: NatsConnection | undefined,
416
- config: HostConfig,
417
- taskId: string,
418
- status: "confirmed" | "aborted",
419
- ): Promise<void> {
420
- await publishHostEvent(nc, config.hostId, taskId, {
421
- event_type: "confirm-resolved",
422
- host_id: config.hostId,
423
- status,
424
- });
425
- }
426
407
 
427
408
  async function requestPermission(
428
- nc: NatsConnection | undefined,
429
409
  config: HostConfig,
430
410
  task: ParsedTask,
431
411
  taskDir: string,
432
412
  requiredPermissions: RequiredPermission[],
433
413
  ): Promise<"granted" | "granted_all" | "aborted"> {
434
- const currentStatus = readTaskStatus(taskDir)!;
435
- writeTaskStatus(taskDir, { ...currentStatus, pending_permission: requiredPermissions });
436
-
437
- await publishHostEvent(nc, config.hostId, task.frontmatter.id, {
438
- event_type: "permission-request",
439
- host_id: config.hostId,
440
- required_permissions: requiredPermissions,
441
- name: task.frontmatter.name,
414
+ const port = config.httpPort ?? 7400;
415
+ const params = new URLSearchParams({
416
+ taskId: task.frontmatter.id,
417
+ taskName: task.frontmatter.name,
418
+ permissions: JSON.stringify(requiredPermissions),
442
419
  });
443
-
444
- const userInput = await waitForUserInput(taskDir);
445
- const response = userInput[0] as "granted" | "granted_all" | "aborted";
420
+ const res = await fetch(`http://localhost:${port}/request-permission?${params}`);
421
+ const { response } = await res.json() as { response: "granted" | "granted_all" | "aborted" };
446
422
  writeTaskStatus(taskDir, {
447
423
  running_state: response === "aborted" ? "aborted" : "started",
448
424
  time_stamp: Date.now(),
@@ -450,35 +426,19 @@ async function requestPermission(
450
426
  return response;
451
427
  }
452
428
 
453
- async function publishPermissionResolved(
454
- nc: NatsConnection | undefined,
455
- config: HostConfig,
456
- taskId: string,
457
- status: "granted" | "granted_all" | "aborted",
458
- ): Promise<void> {
459
- await publishHostEvent(nc, config.hostId, taskId, {
460
- event_type: "permission-resolved",
461
- host_id: config.hostId,
462
- status,
463
- });
464
- }
465
429
 
466
430
  async function requestConfirmation(
467
- nc: NatsConnection | undefined,
468
431
  config: HostConfig,
469
432
  task: ParsedTask,
470
433
  taskDir: string,
471
434
  ): Promise<boolean> {
472
- const currentStatus = readTaskStatus(taskDir)!;
473
- writeTaskStatus(taskDir, { ...currentStatus, pending_confirmation: true });
474
-
475
- await publishHostEvent(nc, config.hostId, task.frontmatter.id, {
476
- event_type: "confirm-request",
477
- host_id: config.hostId,
435
+ const port = config.httpPort ?? 7400;
436
+ const params = new URLSearchParams({
437
+ taskId: task.frontmatter.id,
438
+ taskName: task.frontmatter.name,
478
439
  });
479
-
480
- const userInput = await waitForUserInput(taskDir);
481
- const confirmed = userInput[0] === "confirmed";
440
+ const res = await fetch(`http://localhost:${port}/request-confirmation?${params}`);
441
+ const { confirmed } = await res.json() as { confirmed: boolean };
482
442
  writeTaskStatus(taskDir, {
483
443
  running_state: confirmed ? "started" : "aborted",
484
444
  time_stamp: Date.now(),
@@ -4,6 +4,7 @@ import { loadConfig } from "../config.js";
4
4
  import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
+ import { startHttpTransport } from "../transports/http-transport.js";
7
8
  import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
8
9
  import { publishHostEvent } from "../events.js";
9
10
  import { getPlatform } from "../platform/index.js";
@@ -78,7 +79,7 @@ async function checkStaleTasks(
78
79
  }
79
80
 
80
81
  /**
81
- * Start the persistent RPC handler (NATS only).
82
+ * Start the persistent RPC handler (NATS + HTTP).
82
83
  */
83
84
  export async function serveCommand(): Promise<void> {
84
85
  const config = loadConfig();
@@ -107,5 +108,13 @@ export async function serveCommand(): Promise<void> {
107
108
  }, POLL_INTERVAL_MS);
108
109
 
109
110
  const handleRpc = createRpcHandler(config, nc);
110
- await startNatsTransport(config, handleRpc, nc);
111
+ const httpPort = config.httpPort ?? 7400;
112
+
113
+ // Start NATS transport (loops forever, fire-and-forget)
114
+ if (nc) {
115
+ startNatsTransport(config, handleRpc, nc);
116
+ }
117
+
118
+ // Start HTTP transport (loops forever)
119
+ await startHttpTransport(config, handleRpc, httpPort, nc);
111
120
  }
package/src/events.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { StringCodec, type NatsConnection } from "nats";
2
- import { getLanPort } from "./lan-lock.js";
2
+ import { loadConfig } from "./config.js";
3
3
 
4
4
  const sc = StringCodec();
5
5
 
6
6
  /**
7
- * Broadcast an event to connected clients via NATS and HTTP SSE (if LAN server is running).
7
+ * Broadcast an event to connected clients via NATS and HTTP SSE.
8
8
  *
9
9
  * - NATS: publishes to `host-event.{hostId}.{taskId}`
10
- * - HTTP: POSTs to the LAN server's `/internal/event` endpoint (auto-detected via lockfile)
10
+ * - HTTP: POSTs to the serve daemon's `/event` endpoint
11
11
  */
12
12
  export async function publishHostEvent(
13
13
  nc: NatsConnection | undefined,
@@ -22,17 +22,16 @@ export async function publishHostEvent(
22
22
  console.log(`[nats] ${subject} →`, payload);
23
23
  }
24
24
 
25
- const lanPort = getLanPort();
26
- if (lanPort) {
27
- try {
28
- await fetch(`http://localhost:${lanPort}/internal/event`, {
29
- method: "POST",
30
- headers: { "Content-Type": "application/json" },
31
- body: JSON.stringify({ task_id: taskId, ...payload }),
32
- });
33
- console.log(`[http] host-event: ${taskId} →`, payload);
34
- } catch {
35
- // LAN server may have shut down — ignore
36
- }
25
+ const config = loadConfig();
26
+ const port = config.httpPort ?? 7400;
27
+ try {
28
+ await fetch(`http://localhost:${port}/event`, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({ task_id: taskId, ...payload }),
32
+ });
33
+ console.log(`[http] host-event: ${taskId} →`, payload);
34
+ } catch {
35
+ // Serve HTTP may not be ready yet — ignore
37
36
  }
38
37
  }
package/src/index.ts CHANGED
@@ -9,11 +9,8 @@ import { initCommand } from "./commands/init.js";
9
9
  import { infoCommand } from "./commands/info.js";
10
10
  import { runCommand } from "./commands/run.js";
11
11
  import { serveCommand } from "./commands/serve.js";
12
- import { notifyCommand } from "./commands/notify.js";
13
- import { requestInputCommand } from "./commands/request-input.js";
14
12
 
15
13
  import { pairCommand } from "./commands/pair.js";
16
- import { lanCommand } from "./commands/lan.js";
17
14
  import { restartCommand } from "./commands/restart.js";
18
15
  import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
19
16
 
@@ -62,22 +59,6 @@ program
62
59
  await restartCommand();
63
60
  });
64
61
 
65
- program
66
- .command("notify")
67
- .description("Send a push notification to the user")
68
- .requiredOption("--title <title>", "Notification title")
69
- .requiredOption("--body <body>", "Notification body text")
70
- .action(async (opts: { title: string; body: string }) => {
71
- await notifyCommand(opts);
72
- });
73
-
74
- program
75
- .command("request-input")
76
- .description("Request input from the user (requires PALMIER_TASK_ID env var)")
77
- .requiredOption("--description <desc...>", "Input descriptions to show the user")
78
- .action(async (opts: { description: string[] }) => {
79
- await requestInputCommand(opts);
80
- });
81
62
 
82
63
  program
83
64
  .command("pair")
@@ -86,13 +67,6 @@ program
86
67
  await pairCommand();
87
68
  });
88
69
 
89
- program
90
- .command("lan")
91
- .description("Start an on-demand LAN server for direct HTTP connections")
92
- .option("-p, --port <port>", "Port to listen on", "7400")
93
- .action(async (opts: { port: string }) => {
94
- await lanCommand({ port: parseInt(opts.port, 10) });
95
- });
96
70
 
97
71
  const sessionsCmd = program
98
72
  .command("sessions")