palmier 0.5.0 → 0.5.2

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 (44) hide show
  1. package/dist/agents/agent-instructions.md +7 -11
  2. package/dist/agents/agent.d.ts +8 -3
  3. package/dist/agents/agent.js +7 -1
  4. package/dist/agents/claude.d.ts +2 -1
  5. package/dist/agents/claude.js +10 -5
  6. package/dist/agents/codex.d.ts +2 -1
  7. package/dist/agents/codex.js +10 -6
  8. package/dist/agents/copilot.d.ts +2 -1
  9. package/dist/agents/copilot.js +10 -3
  10. package/dist/agents/gemini.d.ts +2 -1
  11. package/dist/agents/gemini.js +11 -7
  12. package/dist/agents/kimi.d.ts +9 -0
  13. package/dist/agents/kimi.js +35 -0
  14. package/dist/agents/openclaw.d.ts +2 -1
  15. package/dist/agents/openclaw.js +3 -1
  16. package/dist/agents/qwen.d.ts +9 -0
  17. package/dist/agents/qwen.js +32 -0
  18. package/dist/agents/shared-prompt.d.ts +1 -1
  19. package/dist/agents/shared-prompt.js +6 -2
  20. package/dist/commands/run.js +22 -5
  21. package/dist/platform/windows.js +17 -1
  22. package/dist/rpc-handler.js +15 -4
  23. package/dist/task.d.ts +13 -3
  24. package/dist/task.js +39 -7
  25. package/dist/transports/http-transport.js +29 -9
  26. package/dist/types.d.ts +1 -0
  27. package/package.json +1 -1
  28. package/src/agents/agent-instructions.md +7 -11
  29. package/src/agents/agent.ts +16 -4
  30. package/src/agents/claude.ts +11 -6
  31. package/src/agents/codex.ts +11 -7
  32. package/src/agents/copilot.ts +10 -4
  33. package/src/agents/gemini.ts +12 -8
  34. package/src/agents/kimi.ts +37 -0
  35. package/src/agents/openclaw.ts +4 -2
  36. package/src/agents/qwen.ts +34 -0
  37. package/src/agents/shared-prompt.ts +6 -2
  38. package/src/commands/run.ts +24 -5
  39. package/src/platform/windows.ts +14 -1
  40. package/src/rpc-handler.ts +17 -4
  41. package/src/task.ts +43 -8
  42. package/src/transports/http-transport.ts +34 -9
  43. package/src/types.ts +1 -0
  44. package/test/agent-instructions.test.ts +31 -0
package/dist/task.js CHANGED
@@ -169,29 +169,61 @@ export function beginStreamingMessage(taskDir, runId, time) {
169
169
  const filePath = path.join(taskDir, runId, "TASKRUN.md");
170
170
  const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
171
171
  fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
172
- return new StreamingMessageWriter(filePath, delimiter);
172
+ return new StreamingMessageWriter(filePath);
173
173
  }
174
174
  export class StreamingMessageWriter {
175
175
  filePath;
176
- delimiter;
177
- constructor(filePath, delimiter) {
176
+ constructor(filePath) {
178
177
  this.filePath = filePath;
179
- this.delimiter = delimiter;
180
178
  }
181
179
  /** Append a chunk of content to the current message. */
182
180
  write(chunk) {
183
181
  fs.appendFileSync(this.filePath, chunk, "utf-8");
184
182
  }
185
- /** Finalize the message. If attachments are provided, rewrites the delimiter to include them. */
183
+ /** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
186
184
  end(attachments) {
187
185
  fs.appendFileSync(this.filePath, "\n\n", "utf-8");
188
186
  if (attachments?.length) {
189
187
  const raw = fs.readFileSync(this.filePath, "utf-8");
190
- const updated = raw.replace(this.delimiter, `${this.delimiter.slice(0, -4)} attachments="${attachments.join(",")}" -->`);
191
- fs.writeFileSync(this.filePath, updated, "utf-8");
188
+ // Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
189
+ const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
190
+ let lastMatch = null;
191
+ let m;
192
+ while ((m = pattern.exec(raw)) !== null)
193
+ lastMatch = m;
194
+ if (lastMatch) {
195
+ const before = raw.slice(0, lastMatch.index);
196
+ const after = raw.slice(lastMatch.index + lastMatch[0].length);
197
+ const updated = before + `${lastMatch[0].slice(0, -4)} attachments="${attachments.join(",")}" -->` + after;
198
+ fs.writeFileSync(this.filePath, updated, "utf-8");
199
+ }
192
200
  }
193
201
  }
194
202
  }
203
+ /**
204
+ * Splice a user message into a running assistant stream.
205
+ * Ends the current assistant block, writes the user message,
206
+ * then opens a new assistant block — all as direct file appends.
207
+ * The existing StreamingMessageWriter keeps working because its
208
+ * write() is just appendFileSync, so subsequent chunks land in
209
+ * the new assistant block.
210
+ */
211
+ export function spliceUserMessage(taskDir, runId, userMsg,
212
+ /** Optional text to append to the current assistant block before ending it. */
213
+ assistantAppend) {
214
+ const filePath = path.join(taskDir, runId, "TASKRUN.md");
215
+ // 1. Optionally append to the current assistant block (e.g. the input questions)
216
+ if (assistantAppend) {
217
+ fs.appendFileSync(filePath, assistantAppend, "utf-8");
218
+ }
219
+ // 2. End the current assistant block
220
+ fs.appendFileSync(filePath, "\n\n", "utf-8");
221
+ // 3. Write the user message
222
+ appendRunMessage(taskDir, runId, userMsg);
223
+ // 4. Open a new assistant block for subsequent agent output
224
+ const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
225
+ fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
226
+ }
195
227
  /**
196
228
  * Read conversation messages from a run's TASKRUN.md file.
197
229
  */
@@ -3,7 +3,8 @@ import * as os from "os";
3
3
  import { StringCodec } from "nats";
4
4
  import { validateSession, addSession } from "../session-store.js";
5
5
  import { registerPending } from "../pending-requests.js";
6
- import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
6
+ import * as fs from "node:fs";
7
+ import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
7
8
  const PWA_ORIGIN = "https://app.palmier.me";
8
9
  const assetCache = new Map();
9
10
  /** Paths currently being fetched (dedup concurrent requests). */
@@ -78,6 +79,18 @@ export function detectLanIp() {
78
79
  }
79
80
  return "127.0.0.1";
80
81
  }
82
+ /** Find the latest (highest-numbered) run directory for a task. */
83
+ function findLatestRunId(taskDir) {
84
+ try {
85
+ const dirs = fs.readdirSync(taskDir)
86
+ .filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
87
+ .sort();
88
+ return dirs.length > 0 ? dirs[dirs.length - 1] : null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
81
94
  /**
82
95
  * Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
83
96
  * localhost-only agent endpoints (notify, request-input, confirmation, permission).
@@ -242,25 +255,30 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
242
255
  }
243
256
  const taskDir = getTaskDir(config.projectRoot, taskId);
244
257
  const task = parseTaskFile(taskDir);
258
+ // Resolve runId: use provided value, otherwise find the latest run directory
259
+ const effectiveRunId = runId ?? findLatestRunId(taskDir);
260
+ const pendingPromise = registerPending(taskId, "input", descriptions);
245
261
  await publishEvent(taskId, {
246
262
  event_type: "input-request",
247
263
  host_id: config.hostId,
248
264
  input_descriptions: descriptions,
249
265
  name: task.frontmatter.name,
250
266
  });
251
- const response = await registerPending(taskId, "input", descriptions);
267
+ const response = await pendingPromise;
268
+ const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
252
269
  if (response.length === 1 && response[0] === "aborted") {
253
270
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
254
- if (runId) {
255
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
271
+ if (effectiveRunId) {
272
+ spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
273
+ await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
256
274
  }
257
275
  sendJson(res, 200, { aborted: true });
258
276
  }
259
277
  else {
260
278
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
261
- if (runId) {
262
- const lines = descriptions.map((desc, i) => `**${desc}** ${response[i]}`);
263
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
279
+ if (effectiveRunId) {
280
+ spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
281
+ await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
264
282
  }
265
283
  sendJson(res, 200, { values: response });
266
284
  }
@@ -283,11 +301,12 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
283
301
  sendJson(res, 400, { error: "taskId is required" });
284
302
  return;
285
303
  }
304
+ const pendingPromise = registerPending(taskId, "confirmation");
286
305
  await publishEvent(taskId, {
287
306
  event_type: "confirm-request",
288
307
  host_id: config.hostId,
289
308
  });
290
- const response = await registerPending(taskId, "confirmation");
309
+ const response = await pendingPromise;
291
310
  const confirmed = response[0] === "confirmed";
292
311
  await publishEvent(taskId, {
293
312
  event_type: "confirm-resolved",
@@ -314,13 +333,14 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
314
333
  sendJson(res, 400, { error: "taskId and permissions are required" });
315
334
  return;
316
335
  }
336
+ const pendingPromise = registerPending(taskId, "permission", permissions);
317
337
  await publishEvent(taskId, {
318
338
  event_type: "permission-request",
319
339
  host_id: config.hostId,
320
340
  required_permissions: permissions,
321
341
  name: taskName,
322
342
  });
323
- const response = await registerPending(taskId, "permission", permissions);
343
+ const response = await pendingPromise;
324
344
  const status = response[0];
325
345
  await publishEvent(taskId, {
326
346
  event_type: "permission-resolved",
package/dist/types.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface TaskFrontmatter {
19
19
  triggers: Trigger[];
20
20
  triggers_enabled: boolean;
21
21
  requires_confirmation: boolean;
22
+ yolo_mode?: boolean;
22
23
  permissions?: RequiredPermission[];
23
24
  command?: string;
24
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -2,23 +2,19 @@ You are an AI agent executing a task on behalf of the user via the Palmier platf
2
2
 
3
3
  ## Reporting Output
4
4
 
5
- If you generate report or output files, print each file path on its own line prefixed with [PALMIER_REPORT]:
6
- [PALMIER_REPORT] report.md
7
- [PALMIER_REPORT] summary.md
5
+ If you generate report or output files, print each file path on its own line using this exact format (no code block):
6
+ `[PALMIER_REPORT] <filename>`
8
7
 
9
8
  ## Completion
10
9
 
11
- When you are done, output exactly one of these markers as the very last line:
12
- - Success: [PALMIER_TASK_SUCCESS]
13
- - Failure: [PALMIER_TASK_FAILURE]
14
- Do not wrap them in code blocks or add text on the same line.
10
+ When you are done, output exactly one of these markers as the very last line (no code block, no other text on the same line):
11
+ - Success: `[PALMIER_TASK_SUCCESS]`
12
+ - Failure: `[PALMIER_TASK_FAILURE]`
15
13
 
16
14
  ## Permissions
17
15
 
18
- 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]:
19
- [PALMIER_PERMISSION] Read | Read file contents from the repository
20
- [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
- [PALMIER_PERMISSION] Write | Write generated output files
16
+ If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format (no code block):
17
+ `[PALMIER_PERMISSION] <tool_name> | <description>`
22
18
 
23
19
  ## HTTP Endpoints
24
20
 
@@ -4,6 +4,8 @@ import { GeminiAgent } from "./gemini.js";
4
4
  import { CodexAgent } from "./codex.js";
5
5
  import { OpenClawAgent } from "./openclaw.js";
6
6
  import { CopilotAgent } from "./copilot.js";
7
+ import { QwenAgent } from "./qwen.js";
8
+ import { KimiAgent } from "./kimi.js";
7
9
 
8
10
  export interface CommandLine {
9
11
  command: string;
@@ -21,9 +23,14 @@ export interface AgentTool {
21
23
  getPlanGenerationCommandLine(prompt: string): CommandLine;
22
24
 
23
25
  /** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
24
- * and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
25
- * permissions granted for this run only (not persisted in frontmatter). */
26
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
26
+ * and treat it as a continuation of the original run (reuse the same session, etc).
27
+ * extraPermissions: pass an array of RequiredPermission for transient permissions granted for this run only,
28
+ * or pass `"yolo"` to enable yolo mode (auto-approve all tools, skip permission instructions). */
29
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine;
30
+
31
+ /** Whether this agent supports permission overrides (e.g. --allowedTools).
32
+ * If false, the permissions section is omitted from agent instructions. */
33
+ supportsPermissions: boolean;
27
34
 
28
35
  /** Detect whether the agent CLI is available and perform any agent-specific
29
36
  * initialization. Returns true if the agent was detected and initialized successfully. */
@@ -36,6 +43,8 @@ const agentRegistry: Record<string, AgentTool> = {
36
43
  codex: new CodexAgent(),
37
44
  openclaw: new OpenClawAgent(),
38
45
  copilot: new CopilotAgent(),
46
+ qwen: new QwenAgent(),
47
+ kimi: new KimiAgent(),
39
48
  };
40
49
 
41
50
  const agentLabels: Record<string, string> = {
@@ -44,11 +53,14 @@ const agentLabels: Record<string, string> = {
44
53
  codex: "Codex CLI",
45
54
  openclaw: "OpenClaw",
46
55
  copilot: "Copilot CLI",
56
+ qwen: "Qwen Code",
57
+ kimi: "Kimi Code",
47
58
  };
48
59
 
49
60
  export interface DetectedAgent {
50
61
  key: string;
51
62
  label: string;
63
+ supportsPermissions: boolean;
52
64
  }
53
65
 
54
66
  export async function detectAgents(): Promise<DetectedAgent[]> {
@@ -56,7 +68,7 @@ export async function detectAgents(): Promise<DetectedAgent[]> {
56
68
  for (const [key, agent] of Object.entries(agentRegistry)) {
57
69
  const label = agentLabels[key] ?? key;
58
70
  const ok = await agent.init();
59
- if (ok) detected.push({ key, label });
71
+ if (ok) detected.push({ key, label, supportsPermissions: agent.supportsPermissions });
60
72
  }
61
73
  return detected;
62
74
  }
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class ClaudeAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "claude",
@@ -12,13 +13,17 @@ export class ClaudeAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
- const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--permission-mode", yolo ? "bypassPermissions" : "acceptEdits", "-p"];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- for (const p of allPerms) {
21
- args.push("--allowedTools", p.name);
21
+ if (!yolo) {
22
+ args.push("--allowedTools", "WebFetch");
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
24
+ for (const p of allPerms) {
25
+ args.push("--allowedTools", p.name);
26
+ }
22
27
  }
23
28
 
24
29
  if (followupPrompt) {args.push("-c");} // continue mode for followups
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CodexAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "codex",
@@ -12,15 +13,18 @@ export class CodexAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
19
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
18
- const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
20
+ const args = ["exec", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
19
21
 
20
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
- for (const p of allPerms) {
22
- args.push("--config");
23
- args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
22
+ if (!yolo) {
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
24
+ for (const p of allPerms) {
25
+ args.push("--config");
26
+ args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
27
+ }
24
28
  }
25
29
  if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
26
30
  args.push("-"); // read prompt from stdin
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
8
+ supportsPermissions = false;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "copilot",
@@ -12,12 +13,17 @@ export class CopilotAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
19
  const args = ["-p", prompt];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
21
+ if (yolo) {
22
+ args.push("--yolo");
23
+ } else {
24
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
25
+ args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
26
+ }
21
27
  if (followupPrompt) { args.push("--continue"); }
22
28
  return { command: "copilot", args};
23
29
  }
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "gemini",
@@ -12,21 +13,24 @@ export class GeminiAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
- const args = ["--approval-mode", "auto_edit", "--allowed-tools", "web_fetch"];
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- if (allPerms.length > 0) {
21
+ if (!yolo) {
22
+ const tools = ["run_shell_command", "web_fetch"];
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
24
  for (const p of allPerms) {
22
- args.push(p.name);
25
+ tools.push(p.name);
23
26
  }
27
+ args.push("--allowed-tools", tools.join(","));
24
28
  }
25
29
 
26
30
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
27
- args.push("--prompt", prompt);
31
+ args.push("--prompt", "-"); // read prompt from stdin to avoid command line length limits
28
32
 
29
- return { command: "gemini", args };
33
+ return { command: "gemini", args, stdin: prompt };
30
34
  }
31
35
 
32
36
  async init(): Promise<boolean> {
@@ -0,0 +1,37 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
+ import { SHELL } from "../platform/index.js";
6
+
7
+ export class KimiAgent implements AgentTool {
8
+ supportsPermissions = false;
9
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
10
+ return {
11
+ command: "kimi",
12
+ args: ["-p", prompt],
13
+ };
14
+ }
15
+
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = [];
20
+
21
+ if (yolo) {
22
+ args.push("--yolo");
23
+ }
24
+ if (followupPrompt) { args.push("--continue"); }
25
+ args.push("-p", prompt);
26
+ return { command: "kimi", args };
27
+ }
28
+
29
+ async init(): Promise<boolean> {
30
+ try {
31
+ execSync("kimi --version", { stdio: "ignore", shell: SHELL });
32
+ } catch {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ }
@@ -4,6 +4,7 @@ import type { AgentTool, CommandLine } from "./agent.js";
4
4
  import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
+ supportsPermissions = false;
7
8
  getPlanGenerationCommandLine(prompt: string): CommandLine {
8
9
  return {
9
10
  command: "openclaw",
@@ -11,8 +12,9 @@ export class OpenClawAgent implements AgentTool {
11
12
  };
12
13
  }
13
14
 
14
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
16
+ const yolo = extraPermissions === "yolo";
17
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
18
  // OpenClaw does not support stdin as prompt.
17
19
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
18
20
 
@@ -0,0 +1,34 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
+ import { SHELL } from "../platform/index.js";
6
+
7
+ export class QwenAgent implements AgentTool {
8
+ supportsPermissions = false;
9
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
10
+ return {
11
+ command: "qwen",
12
+ args: ["-p", prompt],
13
+ };
14
+ }
15
+
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
20
+
21
+ if (followupPrompt) { args.push("-c"); }
22
+ args.push("-p", prompt);
23
+ return { command: "qwen", args };
24
+ }
25
+
26
+ async init(): Promise<boolean> {
27
+ try {
28
+ execSync("qwen --version", { stdio: "ignore", shell: SHELL });
29
+ } catch {
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ }
@@ -13,11 +13,15 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
13
13
  /**
14
14
  * Agent instructions with the serve daemon's HTTP port and task ID baked in.
15
15
  */
16
- export function getAgentInstructions(taskId: string): string {
16
+ export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
17
17
  const port = loadConfig().httpPort ?? 7400;
18
- return AGENT_INSTRUCTIONS_TEMPLATE
18
+ let instructions = AGENT_INSTRUCTIONS_TEMPLATE
19
19
  .replace(/\{\{PORT\}\}/g, String(port))
20
20
  .replace(/\{\{TASK_ID\}\}/g, taskId);
21
+ if (skipPermissions) {
22
+ instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
23
+ }
24
+ return instructions;
21
25
  }
22
26
 
23
27
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
@@ -62,7 +62,12 @@ async function invokeAgentWithRetries(
62
62
  }, 500);
63
63
  }
64
64
 
65
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
65
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
66
+ invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
67
+ );
68
+ const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
69
+ const displayArgs = args.map((a) => truncate(a));
70
+ console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
66
71
  const result = await spawnCommand(command, args, {
67
72
  cwd: getRunDir(ctx.taskDir, ctx.runId),
68
73
  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) },
@@ -454,7 +459,11 @@ async function requestPermission(
454
459
  permissions: requiredPermissions,
455
460
  }),
456
461
  });
457
- const { response } = await res.json() as { response: "granted" | "granted_all" | "aborted" };
462
+ const body = await res.json() as { response?: string; error?: string };
463
+ const response = body.response as "granted" | "granted_all" | "aborted" | undefined;
464
+ if (!response || !["granted", "granted_all", "aborted"].includes(response)) {
465
+ throw new Error(`Permission request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
466
+ }
458
467
  writeTaskStatus(taskDir, {
459
468
  running_state: response === "aborted" ? "aborted" : "started",
460
469
  time_stamp: Date.now(),
@@ -474,7 +483,11 @@ async function requestConfirmation(
474
483
  headers: { "Content-Type": "application/json" },
475
484
  body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
476
485
  });
477
- const { confirmed } = await res.json() as { confirmed: boolean };
486
+ const body = await res.json() as { confirmed?: boolean; error?: string };
487
+ if (typeof body.confirmed !== "boolean") {
488
+ throw new Error(`Confirmation request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
489
+ }
490
+ const { confirmed } = body;
478
491
  writeTaskStatus(taskDir, {
479
492
  running_state: confirmed ? "started" : "aborted",
480
493
  time_stamp: Date.now(),
@@ -523,8 +536,14 @@ export function parsePermissions(output: string): RequiredPermission[] {
523
536
  */
524
537
  export function parseTaskOutcome(output: string): TaskRunningState {
525
538
  const lastChunk = output.slice(-500);
526
- if (lastChunk.includes(TASK_FAILURE_MARKER)) return "failed";
527
- if (lastChunk.includes(TASK_SUCCESS_MARKER)) return "finished";
539
+ const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
540
+ let last: string | null = null;
541
+ let match;
542
+ while ((match = regex.exec(lastChunk)) !== null) {
543
+ last = match[0];
544
+ }
545
+ if (last === TASK_FAILURE_MARKER) return "failed";
546
+ if (last === TASK_SUCCESS_MARKER) return "finished";
528
547
  return "finished";
529
548
  }
530
549
 
@@ -127,7 +127,7 @@ export class WindowsPlatform implements PlatformService {
127
127
  process.exit(0);
128
128
  }
129
129
 
130
- // Kill old daemon first, then spawn new one.
130
+ // Kill old daemon by PID
131
131
  if (oldPid) {
132
132
  try {
133
133
  execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
@@ -136,6 +136,19 @@ export class WindowsPlatform implements PlatformService {
136
136
  }
137
137
  }
138
138
 
139
+ // Also kill any stale palmier serve processes (e.g. leftover from a previous daemon)
140
+ try {
141
+ const out = execFileSync("wmic", ["process", "where", `CommandLine like '%palmier%serve%' and ProcessId != '${process.pid}'`, "get", "ProcessId"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
142
+ for (const line of out.split("\n")) {
143
+ const pid = line.trim();
144
+ if (pid && /^\d+$/.test(pid)) {
145
+ try { execFileSync("taskkill", ["/pid", pid, "/f", "/t"], { windowsHide: true, stdio: "pipe" }); } catch {}
146
+ }
147
+ }
148
+ } catch {
149
+ // wmic may not be available on all Windows versions
150
+ }
151
+
139
152
  this.startDaemonTask();
140
153
  }
141
154