palmier 0.2.5 → 0.2.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 (78) hide show
  1. package/.github/workflows/ci.yml +16 -0
  2. package/LICENSE +190 -0
  3. package/README.md +286 -219
  4. package/dist/agents/agent.d.ts +6 -3
  5. package/dist/agents/agent.js +2 -0
  6. package/dist/agents/claude.d.ts +1 -1
  7. package/dist/agents/claude.js +12 -9
  8. package/dist/agents/codex.d.ts +1 -1
  9. package/dist/agents/codex.js +12 -10
  10. package/dist/agents/gemini.d.ts +1 -1
  11. package/dist/agents/gemini.js +13 -9
  12. package/dist/agents/openclaw.d.ts +2 -2
  13. package/dist/agents/openclaw.js +8 -7
  14. package/dist/agents/shared-prompt.d.ts +5 -4
  15. package/dist/agents/shared-prompt.js +10 -8
  16. package/dist/commands/agents.js +11 -0
  17. package/dist/commands/info.js +0 -22
  18. package/dist/commands/init.js +59 -95
  19. package/dist/commands/lan.d.ts +8 -0
  20. package/dist/commands/lan.js +51 -0
  21. package/dist/commands/mcpserver.js +12 -27
  22. package/dist/commands/pair.d.ts +1 -1
  23. package/dist/commands/pair.js +52 -56
  24. package/dist/commands/plan-generation.md +24 -32
  25. package/dist/commands/restart.d.ts +5 -0
  26. package/dist/commands/restart.js +9 -0
  27. package/dist/commands/run.js +311 -124
  28. package/dist/commands/serve.d.ts +1 -1
  29. package/dist/commands/serve.js +77 -17
  30. package/dist/commands/task-cleanup.d.ts +14 -0
  31. package/dist/commands/task-cleanup.js +84 -0
  32. package/dist/config.js +3 -17
  33. package/dist/events.d.ts +9 -0
  34. package/dist/events.js +46 -0
  35. package/dist/index.js +15 -0
  36. package/dist/platform/linux.d.ts +2 -0
  37. package/dist/platform/linux.js +22 -1
  38. package/dist/platform/platform.d.ts +4 -0
  39. package/dist/platform/windows.d.ts +3 -0
  40. package/dist/platform/windows.js +99 -82
  41. package/dist/rpc-handler.d.ts +2 -1
  42. package/dist/rpc-handler.js +43 -52
  43. package/dist/spawn-command.d.ts +29 -6
  44. package/dist/spawn-command.js +38 -15
  45. package/dist/transports/http-transport.d.ts +1 -1
  46. package/dist/transports/http-transport.js +103 -18
  47. package/dist/transports/nats-transport.d.ts +4 -2
  48. package/dist/transports/nats-transport.js +3 -4
  49. package/dist/types.d.ts +5 -5
  50. package/package.json +5 -3
  51. package/src/agents/agent.ts +8 -3
  52. package/src/agents/claude.ts +44 -43
  53. package/src/agents/codex.ts +11 -12
  54. package/src/agents/gemini.ts +12 -10
  55. package/src/agents/openclaw.ts +8 -7
  56. package/src/agents/shared-prompt.ts +10 -8
  57. package/src/commands/agents.ts +11 -0
  58. package/src/commands/info.ts +0 -24
  59. package/src/commands/init.ts +62 -119
  60. package/src/commands/lan.ts +58 -0
  61. package/src/commands/mcpserver.ts +12 -31
  62. package/src/commands/pair.ts +50 -63
  63. package/src/commands/plan-generation.md +24 -32
  64. package/src/commands/restart.ts +9 -0
  65. package/src/commands/run.ts +375 -143
  66. package/src/commands/serve.ts +96 -17
  67. package/src/config.ts +3 -18
  68. package/src/cross-spawn.d.ts +5 -0
  69. package/src/events.ts +51 -0
  70. package/src/index.ts +17 -0
  71. package/src/platform/linux.ts +25 -1
  72. package/src/platform/platform.ts +6 -0
  73. package/src/platform/windows.ts +100 -89
  74. package/src/rpc-handler.ts +46 -55
  75. package/src/spawn-command.ts +120 -83
  76. package/src/transports/http-transport.ts +123 -19
  77. package/src/transports/nats-transport.ts +4 -4
  78. package/src/types.ts +6 -8
@@ -1,6 +1,79 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "os";
3
3
  import { validateSession, addSession } from "../session-store.js";
4
+ const PWA_ORIGIN = "https://app.palmier.me";
5
+ const CONTENT_TYPES = {
6
+ ".html": "text/html; charset=utf-8",
7
+ ".js": "application/javascript",
8
+ ".css": "text/css",
9
+ ".json": "application/json",
10
+ ".png": "image/png",
11
+ ".ico": "image/x-icon",
12
+ ".woff2": "font/woff2",
13
+ ".woff": "font/woff",
14
+ ".svg": "image/svg+xml",
15
+ };
16
+ function guessContentType(urlPath) {
17
+ const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
18
+ return CONTENT_TYPES[ext] ?? "application/octet-stream";
19
+ }
20
+ async function fetchBuffer(url) {
21
+ const res = await fetch(url);
22
+ if (!res.ok)
23
+ throw new Error(`${res.status} ${res.statusText} for ${url}`);
24
+ return Buffer.from(await res.arrayBuffer());
25
+ }
26
+ /**
27
+ * Download the PWA from palmier.me into memory.
28
+ * Parses index.html for asset references, then fetches each one.
29
+ */
30
+ async function downloadPwaAssets() {
31
+ const assets = new Map();
32
+ // 1. Fetch index.html
33
+ const html = await fetchBuffer(`${PWA_ORIGIN}/`);
34
+ assets.set("/", { data: html, contentType: "text/html; charset=utf-8" });
35
+ const htmlStr = html.toString("utf-8");
36
+ // 2. Extract references from HTML (src="..." and href="...")
37
+ // Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
38
+ const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
39
+ const refRegex = /(?:src|href)="([^"]+)"/g;
40
+ const htmlRefs = new Set();
41
+ let match;
42
+ while ((match = refRegex.exec(htmlStr)) !== null) {
43
+ const ref = match[1];
44
+ if (ref.startsWith("/") && !ref.startsWith("//") && !SKIP.has(ref)) {
45
+ htmlRefs.add(ref);
46
+ }
47
+ }
48
+ // 3. Fetch all HTML-referenced assets
49
+ for (const ref of htmlRefs) {
50
+ try {
51
+ const data = await fetchBuffer(`${PWA_ORIGIN}${ref}`);
52
+ assets.set(ref, { data, contentType: guessContentType(ref) });
53
+ // 4. Parse CSS for font url() references
54
+ if (ref.endsWith(".css")) {
55
+ const cssStr = data.toString("utf-8");
56
+ const urlRegex = /url\(["']?([^"')]+)["']?\)/g;
57
+ let cssMatch;
58
+ while ((cssMatch = urlRegex.exec(cssStr)) !== null) {
59
+ let fontRef = cssMatch[1];
60
+ if (fontRef.startsWith("data:"))
61
+ continue;
62
+ // Resolve relative URLs against the CSS file's directory
63
+ if (!fontRef.startsWith("/")) {
64
+ const cssDir = ref.substring(0, ref.lastIndexOf("/") + 1);
65
+ fontRef = cssDir + fontRef;
66
+ }
67
+ htmlRefs.add(fontRef);
68
+ }
69
+ }
70
+ }
71
+ catch (err) {
72
+ console.warn(`[pwa] Failed to fetch ${ref}: ${err}`);
73
+ }
74
+ }
75
+ return assets;
76
+ }
4
77
  const pendingPairs = new Map();
5
78
  export function detectLanIp() {
6
79
  const interfaces = os.networkInterfaces();
@@ -16,20 +89,27 @@ export function detectLanIp() {
16
89
  /**
17
90
  * Start the HTTP transport: Express-like server with RPC, SSE, and health endpoints.
18
91
  */
19
- export async function startHttpTransport(config, handleRpc) {
20
- const port = config.directPort ?? 7400;
92
+ export async function startHttpTransport(config, handleRpc, port, pairingCode, onReady) {
93
+ // Download PWA assets into memory before starting the server
94
+ console.log("[http] Downloading PWA assets...");
95
+ const pwaAssets = await downloadPwaAssets();
96
+ console.log(`[http] Cached ${pwaAssets.size} PWA assets in memory.`);
21
97
  const sseClients = new Set();
98
+ // If a pairing code is provided (from `palmier lan`), pre-register it
99
+ if (pairingCode) {
100
+ const EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours — stays valid while lan server runs
101
+ const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
102
+ pendingPairs.set(pairingCode, {
103
+ resolve: () => { },
104
+ timer,
105
+ });
106
+ }
22
107
  function broadcastSseEvent(data) {
23
108
  const payload = `data: ${JSON.stringify(data)}\n\n`;
24
109
  for (const client of sseClients) {
25
110
  client.write(payload);
26
111
  }
27
112
  }
28
- function setCorsHeaders(res) {
29
- res.setHeader("Access-Control-Allow-Origin", "*");
30
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
31
- res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
32
- }
33
113
  function checkAuth(req) {
34
114
  const auth = req.headers.authorization;
35
115
  if (!auth || !auth.startsWith("Bearer "))
@@ -60,13 +140,6 @@ export async function startHttpTransport(config, handleRpc) {
60
140
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
61
141
  }
62
142
  const server = http.createServer(async (req, res) => {
63
- setCorsHeaders(res);
64
- // Handle CORS preflight
65
- if (req.method === "OPTIONS") {
66
- res.writeHead(204);
67
- res.end();
68
- return;
69
- }
70
143
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
71
144
  const pathname = url.pathname;
72
145
  // Internal event endpoint — localhost only, no auth
@@ -146,7 +219,6 @@ export async function startHttpTransport(config, handleRpc) {
146
219
  hostId: config.hostId,
147
220
  sessionToken: session.token,
148
221
  directUrl: `http://${ip}:${port}`,
149
- directToken: config.directToken,
150
222
  };
151
223
  // Resolve the long-poll and clean up
152
224
  clearTimeout(pending.timer);
@@ -159,7 +231,21 @@ export async function startHttpTransport(config, handleRpc) {
159
231
  }
160
232
  return;
161
233
  }
162
- // All other endpoints require auth
234
+ // Serve cached PWA assets for non-API routes (no auth required)
235
+ const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
236
+ if (!isApiRoute) {
237
+ // SPA fallback: serve index.html for unrecognized paths
238
+ const asset = pwaAssets.get(pathname) ?? (pathname !== "/" ? pwaAssets.get("/") : undefined);
239
+ if (asset) {
240
+ res.writeHead(200, { "Content-Type": asset.contentType });
241
+ res.end(asset.data);
242
+ }
243
+ else {
244
+ sendJson(res, 404, { error: "Not found" });
245
+ }
246
+ return;
247
+ }
248
+ // API endpoints require auth
163
249
  if (!checkAuth(req)) {
164
250
  sendJson(res, 401, { error: "Unauthorized" });
165
251
  return;
@@ -170,7 +256,6 @@ export async function startHttpTransport(config, handleRpc) {
170
256
  "Content-Type": "text/event-stream",
171
257
  "Cache-Control": "no-cache",
172
258
  Connection: "keep-alive",
173
- "Access-Control-Allow-Origin": "*",
174
259
  });
175
260
  res.write(":ok\n\n");
176
261
  // Send heartbeat every 5 seconds
@@ -220,7 +305,7 @@ export async function startHttpTransport(config, handleRpc) {
220
305
  return new Promise((resolve, reject) => {
221
306
  server.listen(port, () => {
222
307
  console.log(`[http] Listening on port ${port}`);
223
- console.log(`[http] SSE clients can connect to /events`);
308
+ onReady?.();
224
309
  // Graceful shutdown
225
310
  const shutdown = () => {
226
311
  console.log("[http] Shutting down...");
@@ -1,6 +1,8 @@
1
+ import { type NatsConnection } from "nats";
1
2
  import type { HostConfig, RpcMessage } from "../types.js";
2
3
  /**
3
- * Start the NATS transport: connect, subscribe to RPC subjects, dispatch to handler.
4
+ * Start the NATS transport using an existing connection.
5
+ * Subscribe to RPC subjects and dispatch to handler.
4
6
  */
5
- export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>): Promise<void>;
7
+ export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, nc: NatsConnection): Promise<void>;
6
8
  //# sourceMappingURL=nats-transport.d.ts.map
@@ -1,10 +1,9 @@
1
1
  import { StringCodec } from "nats";
2
- import { connectNats } from "../nats-client.js";
3
2
  /**
4
- * Start the NATS transport: connect, subscribe to RPC subjects, dispatch to handler.
3
+ * Start the NATS transport using an existing connection.
4
+ * Subscribe to RPC subjects and dispatch to handler.
5
5
  */
6
- export async function startNatsTransport(config, handleRpc) {
7
- const nc = await connectNats(config);
6
+ export async function startNatsTransport(config, handleRpc, nc) {
8
7
  const sc = StringCodec();
9
8
  const subject = `host.${config.hostId}.rpc.>`;
10
9
  console.log(`[nats] Subscribing to: ${subject}`);
package/dist/types.d.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  export interface HostConfig {
2
2
  hostId: string;
3
3
  projectRoot: string;
4
- mode: "nats" | "lan" | "auto";
4
+ nats?: boolean;
5
5
  natsUrl?: string;
6
6
  natsWsUrl?: string;
7
7
  natsToken?: string;
8
- directPort?: number;
9
- directToken?: string;
10
8
  agents?: Array<{
11
9
  key: string;
12
10
  label: string;
@@ -21,6 +19,7 @@ export interface TaskFrontmatter {
21
19
  triggers_enabled: boolean;
22
20
  requires_confirmation: boolean;
23
21
  permissions?: RequiredPermission[];
22
+ command?: string;
24
23
  }
25
24
  export interface Trigger {
26
25
  type: "cron" | "once";
@@ -30,13 +29,14 @@ export interface ParsedTask {
30
29
  frontmatter: TaskFrontmatter;
31
30
  body: string;
32
31
  }
33
- export type TaskRunningState = "start" | "finish" | "abort" | "fail";
32
+ export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
34
33
  export interface TaskStatus {
35
34
  running_state: TaskRunningState;
36
35
  time_stamp: number;
37
36
  pending_confirmation?: boolean;
38
37
  pending_permission?: RequiredPermission[];
39
- user_input?: string;
38
+ pending_input?: string[];
39
+ user_input?: string[];
40
40
  }
41
41
  export interface HistoryEntry {
42
42
  task_id: string;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
- "license": "ISC",
5
+ "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
7
7
  "type": "module",
8
8
  "main": "dist/index.js",
@@ -19,16 +19,18 @@
19
19
  "dependencies": {
20
20
  "@modelcontextprotocol/sdk": "^1.27.1",
21
21
  "commander": "^13.1.0",
22
+ "cross-spawn": "^7.0.6",
22
23
  "dotenv": "^16.4.7",
23
24
  "nats": "^2.29.1",
24
25
  "yaml": "^2.7.0"
25
26
  },
26
27
  "devDependencies": {
28
+ "@types/cross-spawn": "^6.0.6",
27
29
  "@types/node": "^22.13.0",
28
30
  "tsx": "^4.19.0",
29
31
  "typescript": "^5.7.0"
30
32
  },
31
33
  "engines": {
32
- "node": ">=20.0.0"
34
+ "node": ">=24.0.0"
33
35
  }
34
36
  }
@@ -2,10 +2,13 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import { ClaudeAgent } from "./claude.js";
3
3
  import { GeminiAgent } from "./gemini.js";
4
4
  import { CodexAgent } from "./codex.js";
5
+ import { OpenClawAgent } from "./openclaw.js";
5
6
 
6
7
  export interface CommandLine {
7
8
  command: string;
8
9
  args: string[];
10
+ /** If provided, the string is written to the process's stdin and then the pipe is closed. */
11
+ stdin?: string;
9
12
  }
10
13
 
11
14
  /**
@@ -16,9 +19,10 @@ export interface AgentTool {
16
19
  /** Return the command and args used to generate a plan from a prompt. */
17
20
  getPlanGenerationCommandLine(prompt: string): CommandLine;
18
21
 
19
- /** Return the command and args used to run a task. If prompt is provided, use it instead of the task's prompt.
20
- * extraPermissions are transient permissions granted for this run only (not persisted in frontmatter). */
21
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
22
+ /** Return the command and args used to run a task. If retryPrompt is provided, use it instead of the task's prompt,
23
+ * and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
24
+ * permissions granted for this run only (not persisted in frontmatter). */
25
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
22
26
 
23
27
  /** Detect whether the agent CLI is available and perform any agent-specific
24
28
  * initialization. Returns true if the agent was detected and initialized successfully. */
@@ -29,6 +33,7 @@ const agentRegistry: Record<string, AgentTool> = {
29
33
  claude: new ClaudeAgent(),
30
34
  gemini: new GeminiAgent(),
31
35
  codex: new CodexAgent(),
36
+ openclaw: new OpenClawAgent(),
32
37
  };
33
38
 
34
39
  const agentLabels: Record<string, string> = {
@@ -1,43 +1,44 @@
1
- import type { ParsedTask, RequiredPermission } from "../types.js";
2
- import { execSync } from "child_process";
3
- import type { AgentTool, CommandLine } from "./agent.js";
4
- import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
5
-
6
- // execSync's shell option takes a string (shell path), not boolean.
7
- // On Windows we need a shell so .cmd shims resolve correctly.
8
- const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
9
-
10
- export class ClaudeAgent implements AgentTool {
11
- getPlanGenerationCommandLine(prompt: string): CommandLine {
12
- return {
13
- command: "claude",
14
- args: ["-p", prompt],
15
- };
16
- }
17
-
18
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
19
- prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
20
- const args = ["-c", "--permission-mode", "acceptEdits", "-p", prompt];
21
-
22
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
23
- for (const p of allPerms) {
24
- args.push("--allowedTools", p.name);
25
- }
26
-
27
- return { command: "claude", args };
28
- }
29
-
30
- async init(): Promise<boolean> {
31
- try {
32
- execSync("claude --version", { shell: SHELL });
33
- } catch {
34
- return false;
35
- }
36
- try {
37
- execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { shell: SHELL });
38
- } catch (err) {
39
- console.warn("Warning: failed to install MCP for Claude:", err instanceof Error ? err.message : err);
40
- }
41
- return true;
42
- }
43
- }
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
5
+
6
+ // execSync's shell option takes a string (shell path), not boolean.
7
+ // On Windows we need a shell so .cmd shims resolve correctly.
8
+ const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
9
+
10
+ export class ClaudeAgent implements AgentTool {
11
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
12
+ return {
13
+ command: "claude",
14
+ args: ["-p", prompt],
15
+ };
16
+ }
17
+
18
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
19
+ const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
20
+ const args = ["--permission-mode", "acceptEdits", "--append-system-prompt", AGENT_INSTRUCTIONS, "-p"];
21
+
22
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
23
+ for (const p of allPerms) {
24
+ args.push("--allowedTools", p.name);
25
+ }
26
+
27
+ if (retryPrompt) {args.push("-c");} // continue mode for retries
28
+ return { command: "claude", args, stdin: prompt };
29
+ }
30
+
31
+ async init(): Promise<boolean> {
32
+ try {
33
+ execSync("claude --version", { stdio: "ignore", shell: SHELL });
34
+ } catch {
35
+ return false;
36
+ }
37
+ try {
38
+ execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { stdio: "ignore", shell: SHELL });
39
+ } catch {
40
+ // MCP registration is best-effort; agent still works without it
41
+ }
42
+ return true;
43
+ }
44
+ }
@@ -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 { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
4
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
5
5
 
6
6
  // On Windows we need a shell so .cmd shims resolve correctly.
7
7
  const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
@@ -11,12 +11,12 @@ export class CodexAgent implements AgentTool {
11
11
  // TODO: fill in
12
12
  return {
13
13
  command: "codex",
14
- args: ["exec", "--skip-git-repo-check",prompt],
14
+ args: ["exec", "--skip-git-repo-check", prompt],
15
15
  };
16
16
  }
17
17
 
18
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
19
- prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
18
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
19
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
20
20
  // TODO: Update sandbox to workspace-write once https://github.com/openai/codex/issues/12572
21
21
  // is fixed.
22
22
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
@@ -26,23 +26,22 @@ export class CodexAgent implements AgentTool {
26
26
  args.push("--config");
27
27
  args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
28
28
  }
29
+ args.push("-"); // read prompt from stdin
29
30
 
30
- args.push(prompt);
31
- args.push("resume", "--last");
32
-
33
- return { command: "codex", args };
31
+ if (retryPrompt) {args.push("resume", "--last");} // continue mode for retries
32
+ return { command: "codex", args, stdin: prompt };
34
33
  }
35
34
 
36
35
  async init(): Promise<boolean> {
37
36
  try {
38
- execSync("codex --version", { shell: SHELL });
37
+ execSync("codex --version", { stdio: "ignore", shell: SHELL });
39
38
  } catch {
40
39
  return false;
41
40
  }
42
41
  try {
43
- execSync("codex mcp add palmier palmier mcpserver", { shell: SHELL });
44
- } catch (err) {
45
- console.warn("Warning: failed to install MCP for Codex:", err instanceof Error ? err.message : err);
42
+ execSync("codex mcp add palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
43
+ } catch {
44
+ // MCP registration is best-effort; agent still works without it
46
45
  }
47
46
  return true;
48
47
  }
@@ -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 { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
4
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
5
5
 
6
6
  // On Windows we need a shell so .cmd shims resolve correctly.
7
7
  const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
@@ -11,13 +11,14 @@ export class GeminiAgent implements AgentTool {
11
11
  // TODO: fill in
12
12
  return {
13
13
  command: "gemini",
14
- args: ["--approval-mode", "auto_edit","--prompt", prompt],
14
+ args: ["--approval-mode", "auto_edit", "--prompt", prompt],
15
15
  };
16
16
  }
17
17
 
18
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
19
- prompt = prompt ?? (task.body || task.frontmatter.user_prompt);
20
- const args = ["--resume", "--prompt", prompt + TASK_OUTCOME_SUFFIX];
18
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
19
+ const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
20
+ const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
21
+ const args = ["--prompt", "-"];
21
22
 
22
23
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
23
24
  if (allPerms.length > 0) {
@@ -27,19 +28,20 @@ export class GeminiAgent implements AgentTool {
27
28
  }
28
29
  }
29
30
 
30
- return { command: "gemini", args };
31
+ if (retryPrompt) {args.push("--resume");} // continue mode for retries
32
+ return { command: "gemini", args, stdin: fullPrompt };
31
33
  }
32
34
 
33
35
  async init(): Promise<boolean> {
34
36
  try {
35
- execSync("gemini --version", { shell: SHELL });
37
+ execSync("gemini --version", { stdio: "ignore", shell: SHELL });
36
38
  } catch {
37
39
  return false;
38
40
  }
39
41
  try {
40
- execSync("gemini mcp add --scope user palmier palmier mcpserver", { shell: SHELL });
41
- } catch (err) {
42
- console.warn("Warning: failed to install MCP for Gemini:", err instanceof Error ? err.message : err);
42
+ execSync("gemini mcp add --scope user palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
43
+ } catch {
44
+ // MCP registration is best-effort; agent still works without it
43
45
  }
44
46
  return true;
45
47
  }
@@ -1,26 +1,27 @@
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 { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
4
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
5
5
 
6
- export class ClaudeAgent implements AgentTool {
6
+ export class OpenClawAgent implements AgentTool {
7
7
  getPlanGenerationCommandLine(prompt: string): CommandLine {
8
8
  return {
9
9
  command: "openclaw",
10
- args: ["agent", "--message", prompt],
10
+ args: ["agent", "--local", "--agent", "main", "--message", prompt],
11
11
  };
12
12
  }
13
13
 
14
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
16
- const args = ["agent", "--message", prompt];
14
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
16
+ // OpenClaw does not support stdin as prompt.
17
+ const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
17
18
 
18
19
  return { command: "openclaw", args };
19
20
  }
20
21
 
21
22
  async init(): Promise<boolean> {
22
23
  try {
23
- execSync("openclaw --version");
24
+ execSync("openclaw --version", { stdio: "ignore" });
24
25
  } catch {
25
26
  return false;
26
27
  }
@@ -1,12 +1,9 @@
1
1
  /**
2
- * Appended to the end of every task prompt.
3
- * Instructs the agent to output a clear success/failure marker
4
- * so that palmier can determine the task outcome.
2
+ * Instructions prepended or injected as system prompt for every task invocation.
3
+ * Instructs the agent to output structured markers so palmier can determine
4
+ * the task outcome, report files, and permission/input requests.
5
5
  */
6
- export const TASK_OUTCOME_SUFFIX = `
7
-
8
- ---
9
- If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.
6
+ export const AGENT_INSTRUCTIONS = `If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.
10
7
  [PALMIER_REPORT] report.md
11
8
  [PALMIER_REPORT] summary.md
12
9
 
@@ -18,9 +15,14 @@ Do not wrap them in code blocks or add text on the same line.
18
15
  If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]: e.g.
19
16
  [PALMIER_PERMISSION] Read | Read file contents from the repository
20
17
  [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
- [PALMIER_PERMISSION] Write | Write generated output files`;
18
+ [PALMIER_PERMISSION] Write | Write generated output files
19
+
20
+ If the task requires information from the user that you do not have (such as credentials, connection strings, API keys, or configuration values), print each required input on its own line prefixed with [PALMIER_INPUT]: e.g.
21
+ [PALMIER_INPUT] What is the database connection string?
22
+ [PALMIER_INPUT] What is the API key for the external service?`;
22
23
 
23
24
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
24
25
  export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
25
26
  export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
26
27
  export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
28
+ export const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
@@ -1,8 +1,10 @@
1
1
  import { loadConfig, saveConfig } from "../config.js";
2
2
  import { detectAgents } from "../agents/agent.js";
3
+ import { getPlatform } from "../platform/index.js";
3
4
 
4
5
  export async function agentsCommand(): Promise<void> {
5
6
  const config = loadConfig();
7
+ const oldKeys = (config.agents ?? []).map((a) => a.key).sort().join(",");
6
8
 
7
9
  console.log("Detecting installed agents...");
8
10
  const agents = await detectAgents();
@@ -17,4 +19,13 @@ export async function agentsCommand(): Promise<void> {
17
19
  console.log(` ${a.key} — ${a.label}`);
18
20
  }
19
21
  }
22
+
23
+ // Restart daemon if agent list changed so the UI picks it up immediately
24
+ const newKeys = agents.map((a) => a.key).sort().join(",");
25
+ if (newKeys !== oldKeys) {
26
+ try {
27
+ console.log("Agent list changed, restarting daemon...");
28
+ await getPlatform().restartDaemon();
29
+ } catch { /* daemon may not be running yet */ }
30
+ }
20
31
  }
@@ -1,40 +1,16 @@
1
- import * as os from "os";
2
1
  import { loadConfig } from "../config.js";
3
2
  import { loadSessions } from "../session-store.js";
4
3
 
5
- /**
6
- * Detect the first non-internal IPv4 address.
7
- */
8
- function detectLanIp(): string {
9
- const interfaces = os.networkInterfaces();
10
- for (const name of Object.keys(interfaces)) {
11
- for (const iface of interfaces[name] ?? []) {
12
- if (iface.family === "IPv4" && !iface.internal) {
13
- return iface.address;
14
- }
15
- }
16
- }
17
- return "127.0.0.1";
18
- }
19
-
20
4
  /**
21
5
  * Print host connection info for setting up clients.
22
6
  */
23
7
  export async function infoCommand(): Promise<void> {
24
8
  const config = loadConfig();
25
- const mode = config.mode ?? "nats";
26
9
  const sessions = loadSessions();
27
10
 
28
11
  console.log(`Host ID: ${config.hostId}`);
29
- console.log(`Mode: ${mode}`);
30
12
  console.log(`Project root: ${config.projectRoot}`);
31
13
 
32
- if (mode === "lan" || mode === "auto") {
33
- const lanIp = detectLanIp();
34
- const port = config.directPort ?? 7400;
35
- console.log(`LAN address: ${lanIp}:${port}`);
36
- }
37
-
38
14
  // Detected agents
39
15
  if (config.agents && config.agents.length > 0) {
40
16
  console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);