palmier 0.4.5 → 0.4.7

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 (62) hide show
  1. package/README.md +29 -31
  2. package/dist/agents/agent-instructions.md +9 -9
  3. package/dist/agents/claude.js +3 -3
  4. package/dist/agents/codex.js +3 -3
  5. package/dist/agents/copilot.js +3 -6
  6. package/dist/agents/gemini.js +4 -5
  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/plan-generation.md +12 -15
  14. package/dist/commands/run.js +23 -44
  15. package/dist/commands/serve.d.ts +1 -1
  16. package/dist/commands/serve.js +9 -2
  17. package/dist/events.d.ts +2 -2
  18. package/dist/events.js +15 -16
  19. package/dist/index.js +0 -25
  20. package/dist/pending-requests.d.ts +27 -0
  21. package/dist/pending-requests.js +39 -0
  22. package/dist/rpc-handler.js +18 -10
  23. package/dist/task.d.ts +1 -1
  24. package/dist/task.js +3 -2
  25. package/dist/transports/http-transport.d.ts +4 -2
  26. package/dist/transports/http-transport.js +218 -77
  27. package/dist/types.d.ts +7 -16
  28. package/package.json +1 -1
  29. package/src/agents/agent-instructions.md +9 -9
  30. package/src/agents/claude.ts +3 -3
  31. package/src/agents/codex.ts +3 -3
  32. package/src/agents/copilot.ts +3 -6
  33. package/src/agents/gemini.ts +5 -5
  34. package/src/agents/openclaw.ts +2 -2
  35. package/src/agents/shared-prompt.ts +12 -6
  36. package/src/commands/init.ts +34 -3
  37. package/src/commands/pair.ts +11 -14
  38. package/src/commands/plan-generation.md +12 -15
  39. package/src/commands/run.ts +21 -58
  40. package/src/commands/serve.ts +11 -2
  41. package/src/events.ts +14 -15
  42. package/src/index.ts +0 -26
  43. package/src/pending-requests.ts +55 -0
  44. package/src/rpc-handler.ts +18 -11
  45. package/src/task.ts +3 -1
  46. package/src/transports/http-transport.ts +232 -133
  47. package/src/types.ts +10 -16
  48. package/dist/commands/lan.d.ts +0 -8
  49. package/dist/commands/lan.js +0 -44
  50. package/dist/commands/notify.d.ts +0 -9
  51. package/dist/commands/notify.js +0 -43
  52. package/dist/commands/request-input.d.ts +0 -10
  53. package/dist/commands/request-input.js +0 -49
  54. package/dist/lan-lock.d.ts +0 -7
  55. package/dist/lan-lock.js +0 -18
  56. package/dist/user-input.d.ts +0 -15
  57. package/dist/user-input.js +0 -50
  58. package/src/commands/lan.ts +0 -48
  59. package/src/commands/notify.ts +0 -44
  60. package/src/commands/request-input.ts +0 -51
  61. package/src/lan-lock.ts +0 -16
  62. package/src/user-input.ts +0 -67
@@ -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();
@@ -1,24 +1,21 @@
1
- You are a task planning assistant. Given a task description, produce a Markdown execution plan for an agent. **Do not execute any part of the plan yourself.**
1
+ You are a task planning assistant. Given a task description, produce a Markdown execution plan for an AI agent to follow. Do not execute any part of the plan yourself.
2
2
 
3
- Output a raw YAML frontmatter block (delimited by `---`) followed by the plan body. Do NOT wrap frontmatter in code fences. The first line of output must be `---`.
3
+ ## Output Format
4
+
5
+ Start with a YAML frontmatter block (no code fences), then the plan body:
4
6
 
5
7
  ---
6
- task_name: <short name, 3-6 words>
8
+ task_name: <concise label, 3-6 words>
7
9
  ---
8
10
 
9
- **Frontmatter:** `task_name` — concise label (e.g., "Clean up temp files", "Backup database daily").
10
-
11
- **Plan body:**
12
-
13
- ### 1. Goal
14
- What the task accomplishes and the expected end state.
11
+ <plan body>
15
12
 
16
- ### 2. Plan
17
- Numbered sequence of concrete, actionable steps. Include conditional branches where behavior may vary. Each step must be unambiguous.
13
+ ## Plan Body Guidelines
18
14
 
19
- ### 3. Output Format (if applicable)
20
- If the task produces formatted output (report, email, etc.), specify structure, sections, tone, and templates.
15
+ - Write a numbered sequence of concrete, actionable steps.
16
+ - If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
17
+ - When a step requires user input, simply state what information is needed from the user. Do not specify how to obtain it — the agent has its own tool for requesting user input.
18
+ - Relative times in the task description (e.g., "yesterday", "last week") refer to execution time, not plan generation time.
21
19
 
22
- Relative times in the task description (e.g., "yesterday") are relative to execution time, not plan generation time.
20
+ ## Task Description
23
21
 
24
- **Task description:**
@@ -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, {
@@ -174,7 +172,7 @@ export async function runCommand(taskId: string): Promise<void> {
174
172
 
175
173
  // Use existing run dir if just created by RPC, otherwise create a new one
176
174
  const existingRunId = findLatestPendingRunId(taskDir);
177
- const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
175
+ const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
178
176
  if (!existingRunId) {
179
177
  appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
180
178
  }
@@ -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,24 @@ 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 res = await fetch(`http://localhost:${port}/request-permission`, {
416
+ method: "POST",
417
+ headers: { "Content-Type": "application/json" },
418
+ body: JSON.stringify({
419
+ taskId: task.frontmatter.id,
420
+ taskName: task.frontmatter.name,
421
+ permissions: requiredPermissions,
422
+ }),
442
423
  });
443
-
444
- const userInput = await waitForUserInput(taskDir);
445
- const response = userInput[0] as "granted" | "granted_all" | "aborted";
424
+ const { response } = await res.json() as { response: "granted" | "granted_all" | "aborted" };
446
425
  writeTaskStatus(taskDir, {
447
426
  running_state: response === "aborted" ? "aborted" : "started",
448
427
  time_stamp: Date.now(),
@@ -450,35 +429,19 @@ async function requestPermission(
450
429
  return response;
451
430
  }
452
431
 
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
432
 
466
433
  async function requestConfirmation(
467
- nc: NatsConnection | undefined,
468
434
  config: HostConfig,
469
435
  task: ParsedTask,
470
436
  taskDir: string,
471
437
  ): 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,
438
+ const port = config.httpPort ?? 7400;
439
+ const res = await fetch(`http://localhost:${port}/request-confirmation`, {
440
+ method: "POST",
441
+ headers: { "Content-Type": "application/json" },
442
+ body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
478
443
  });
479
-
480
- const userInput = await waitForUserInput(taskDir);
481
- const confirmed = userInput[0] === "confirmed";
444
+ const { confirmed } = await res.json() as { confirmed: boolean };
482
445
  writeTaskStatus(taskDir, {
483
446
  running_state: confirmed ? "started" : "aborted",
484
447
  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")
@@ -0,0 +1,55 @@
1
+ import type { RequiredPermission } from "./types.js";
2
+
3
+ export interface PendingRequest {
4
+ type: "confirmation" | "permission" | "input";
5
+ resolve: (value: string[]) => void;
6
+ /** Permission list (for 'permission') or input descriptions (for 'input'). */
7
+ params?: RequiredPermission[] | string[];
8
+ }
9
+
10
+ const pending = new Map<string, PendingRequest>();
11
+
12
+ /**
13
+ * Register a pending request for a task. Returns a Promise that resolves
14
+ * when `resolvePending` is called with the user's response.
15
+ * Only one pending request per task at a time.
16
+ */
17
+ export function registerPending(
18
+ taskId: string,
19
+ type: PendingRequest["type"],
20
+ params?: PendingRequest["params"],
21
+ ): Promise<string[]> {
22
+ if (pending.has(taskId)) {
23
+ return Promise.reject(new Error(`Task ${taskId} already has a pending request`));
24
+ }
25
+
26
+ return new Promise<string[]>((resolve) => {
27
+ pending.set(taskId, { type, resolve, params });
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Resolve a pending request with the user's response.
33
+ * Returns true if a pending request was found and resolved.
34
+ */
35
+ export function resolvePending(taskId: string, value: string[]): boolean {
36
+ const entry = pending.get(taskId);
37
+ if (!entry) return false;
38
+ pending.delete(taskId);
39
+ entry.resolve(value);
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * Get the current pending request for a task (if any).
45
+ */
46
+ export function getPending(taskId: string): PendingRequest | undefined {
47
+ return pending.get(taskId);
48
+ }
49
+
50
+ /**
51
+ * Remove a pending request without resolving it.
52
+ */
53
+ export function removePending(taskId: string): void {
54
+ pending.delete(taskId);
55
+ }
@@ -6,6 +6,7 @@ import { spawn, type ChildProcess } from "child_process";
6
6
  import { parse as parseYaml } from "yaml";
7
7
  import { type NatsConnection } from "nats";
8
8
  import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
9
+ import { resolvePending, getPending } from "./pending-requests.js";
9
10
  import { getPlatform } from "./platform/index.js";
10
11
  import { spawnCommand } from "./spawn-command.js";
11
12
  import crossSpawn from "cross-spawn";
@@ -57,6 +58,7 @@ function parseResultFrontmatter(raw: string): Record<string, unknown> {
57
58
  return {
58
59
  messages,
59
60
  task_name: meta.task_name,
61
+ agent: meta.agent,
60
62
  running_state: runningState,
61
63
  start_time: startedMsg?.time || undefined,
62
64
  end_time: terminalMsg?.time || undefined,
@@ -157,8 +159,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
157
159
  }
158
160
 
159
161
  async function handleRpc(request: RpcMessage): Promise<unknown> {
160
- // Session token validation: always require a valid session token
161
- if (!request.sessionToken || !validateSession(request.sessionToken)) {
162
+ // Session token validation: skip for trusted localhost requests
163
+ if (!request.localhost && (!request.sessionToken || !validateSession(request.sessionToken))) {
162
164
  return { error: "Unauthorized" };
163
165
  }
164
166
 
@@ -317,7 +319,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
317
319
  // Do NOT append to tasks.jsonl — this is a one-off run
318
320
 
319
321
  // Create initial result file so it appears in runs list immediately
320
- const runId = createRunDir(taskDir, name, Date.now());
322
+ const runId = createRunDir(taskDir, name, Date.now(), params.agent);
321
323
  appendHistory(config.projectRoot, { task_id: id, run_id: runId });
322
324
 
323
325
  // Spawn `palmier run <id>` directly as a detached process
@@ -338,7 +340,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
338
340
  // Create initial result file so it appears in runs list immediately
339
341
  const runTaskDir = getTaskDir(config.projectRoot, params.id);
340
342
  const runTask = parseTaskFile(runTaskDir);
341
- const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
343
+ const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
342
344
  appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
343
345
 
344
346
  await getPlatform().startTask(params.id);
@@ -521,7 +523,14 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
521
523
  if (!status) {
522
524
  return { task_id: params.id, error: "No status found" };
523
525
  }
524
- return { task_id: params.id, ...status };
526
+ const pending = getPending(params.id);
527
+ return {
528
+ task_id: params.id,
529
+ ...status,
530
+ ...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
531
+ ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
532
+ ...(pending?.type === "input" ? { pending_input: pending.params } : {}),
533
+ };
525
534
  }
526
535
 
527
536
  case "task.result": {
@@ -570,17 +579,15 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
570
579
 
571
580
  case "task.user_input": {
572
581
  const params = request.params as { id: string; value: string[] };
573
- const taskDir = getTaskDir(config.projectRoot, params.id);
574
582
 
575
- const currentStatus = readTaskStatus(taskDir);
576
- if (!currentStatus?.pending_confirmation && !currentStatus?.pending_permission?.length && !currentStatus?.pending_input?.length) {
583
+ const pending = getPending(params.id);
584
+ if (!pending) {
577
585
  return { ok: false, error: "not pending" };
578
586
  }
579
587
 
580
- writeTaskStatus(taskDir, { ...currentStatus, user_input: params.value });
581
-
588
+ const resolved = resolvePending(params.id, params.value);
582
589
  console.log(`[task.user_input] ${params.id} → ${params.value}`);
583
- return { ok: true };
590
+ return { ok: resolved };
584
591
  }
585
592
 
586
593
  case "taskrun.list": {
package/src/task.ts CHANGED
@@ -155,11 +155,13 @@ export function createRunDir(
155
155
  taskDir: string,
156
156
  taskName: string,
157
157
  startTime: number,
158
+ agent?: string,
158
159
  ): string {
159
160
  const runId = String(startTime);
160
161
  const runDir = path.join(taskDir, runId);
161
162
  fs.mkdirSync(runDir, { recursive: true });
162
- const content = `---\ntask_name: ${taskName}\n---\n\n`;
163
+ const agentLine = agent ? `\nagent: ${agent}` : "";
164
+ const content = `---\ntask_name: ${taskName}${agentLine}\n---\n\n`;
163
165
  fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
164
166
  return runId;
165
167
  }