palmier 0.5.1 → 0.5.3

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 (70) hide show
  1. package/README.md +9 -9
  2. package/dist/agents/agent-instructions.md +7 -11
  3. package/dist/agents/agent.d.ts +8 -3
  4. package/dist/agents/agent.js +7 -1
  5. package/dist/agents/claude.d.ts +2 -1
  6. package/dist/agents/claude.js +10 -5
  7. package/dist/agents/codex.d.ts +2 -1
  8. package/dist/agents/codex.js +10 -6
  9. package/dist/agents/copilot.d.ts +2 -1
  10. package/dist/agents/copilot.js +10 -3
  11. package/dist/agents/gemini.d.ts +2 -1
  12. package/dist/agents/gemini.js +11 -7
  13. package/dist/agents/kimi.d.ts +9 -0
  14. package/dist/agents/kimi.js +35 -0
  15. package/dist/agents/openclaw.d.ts +2 -1
  16. package/dist/agents/openclaw.js +3 -1
  17. package/dist/agents/qwen.d.ts +9 -0
  18. package/dist/agents/qwen.js +32 -0
  19. package/dist/agents/shared-prompt.d.ts +1 -1
  20. package/dist/agents/shared-prompt.js +7 -3
  21. package/dist/client-store.d.ts +12 -0
  22. package/dist/client-store.js +57 -0
  23. package/dist/commands/clients.d.ts +4 -0
  24. package/dist/commands/clients.js +27 -0
  25. package/dist/commands/info.js +5 -5
  26. package/dist/commands/init.js +1 -1
  27. package/dist/commands/pair.js +4 -4
  28. package/dist/commands/run.js +21 -8
  29. package/dist/commands/serve.js +1 -1
  30. package/dist/events.js +1 -1
  31. package/dist/index.js +13 -13
  32. package/dist/rpc-handler.js +13 -6
  33. package/dist/task.d.ts +13 -3
  34. package/dist/task.js +39 -7
  35. package/dist/transports/http-transport.js +30 -13
  36. package/dist/transports/nats-transport.js +4 -4
  37. package/dist/types.d.ts +3 -2
  38. package/package.json +1 -1
  39. package/src/agents/agent-instructions.md +7 -11
  40. package/src/agents/agent.ts +16 -4
  41. package/src/agents/claude.ts +11 -6
  42. package/src/agents/codex.ts +11 -7
  43. package/src/agents/copilot.ts +10 -4
  44. package/src/agents/gemini.ts +12 -8
  45. package/src/agents/kimi.ts +37 -0
  46. package/src/agents/openclaw.ts +4 -2
  47. package/src/agents/qwen.ts +34 -0
  48. package/src/agents/shared-prompt.ts +7 -3
  49. package/src/client-store.ts +68 -0
  50. package/src/commands/clients.ts +29 -0
  51. package/src/commands/info.ts +5 -5
  52. package/src/commands/init.ts +1 -1
  53. package/src/commands/pair.ts +4 -4
  54. package/src/commands/run.ts +22 -8
  55. package/src/commands/serve.ts +1 -1
  56. package/src/events.ts +1 -1
  57. package/src/index.ts +13 -13
  58. package/src/rpc-handler.ts +15 -6
  59. package/src/task.ts +43 -8
  60. package/src/transports/http-transport.ts +32 -13
  61. package/src/transports/nats-transport.ts +4 -4
  62. package/src/types.ts +4 -3
  63. package/test/agent-instructions.test.ts +48 -0
  64. package/test/agent-output-parsing.test.ts +12 -0
  65. package/dist/commands/sessions.d.ts +0 -4
  66. package/dist/commands/sessions.js +0 -27
  67. package/dist/session-store.d.ts +0 -12
  68. package/dist/session-store.js +0 -57
  69. package/src/commands/sessions.ts +0 -29
  70. package/src/session-store.ts +0 -68
@@ -33,10 +33,13 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
33
33
  publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
34
34
  }, 500);
35
35
  }
36
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
36
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions);
37
+ const truncate = (s, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
38
+ const displayArgs = args.map((a) => truncate(a));
39
+ console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
37
40
  const result = await spawnCommand(command, args, {
38
41
  cwd: getRunDir(ctx.taskDir, ctx.runId),
39
- 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) },
42
+ 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 ?? 9966) },
40
43
  echoStdout: true,
41
44
  resolveOnFailure: true,
42
45
  stdin,
@@ -245,7 +248,7 @@ async function runCommandTriggeredMode(ctx) {
245
248
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
246
249
  const child = spawnStreamingCommand(commandStr, {
247
250
  cwd: getRunDir(ctx.taskDir, ctx.runId),
248
- 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) },
251
+ 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 ?? 9966) },
249
252
  });
250
253
  let linesProcessed = 0;
251
254
  let invocationsSucceeded = 0;
@@ -362,7 +365,7 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
362
365
  await publishHostEvent(nc, config.hostId, taskId, payload);
363
366
  }
364
367
  async function requestPermission(config, task, taskDir, requiredPermissions) {
365
- const port = config.httpPort ?? 7400;
368
+ const port = config.httpPort ?? 9966;
366
369
  const res = await fetch(`http://localhost:${port}/request-permission`, {
367
370
  method: "POST",
368
371
  headers: { "Content-Type": "application/json" },
@@ -384,7 +387,7 @@ async function requestPermission(config, task, taskDir, requiredPermissions) {
384
387
  return response;
385
388
  }
386
389
  async function requestConfirmation(config, task, taskDir) {
387
- const port = config.httpPort ?? 7400;
390
+ const port = config.httpPort ?? 9966;
388
391
  const res = await fetch(`http://localhost:${port}/request-confirmation`, {
389
392
  method: "POST",
390
393
  headers: { "Content-Type": "application/json" },
@@ -411,7 +414,8 @@ export function parseReportFiles(output) {
411
414
  let match;
412
415
  while ((match = regex.exec(output)) !== null) {
413
416
  const name = match[1].trim();
414
- if (name)
417
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
418
+ if (name && !name.startsWith("<"))
415
419
  files.push(name);
416
420
  }
417
421
  return files;
@@ -426,6 +430,9 @@ export function parsePermissions(output) {
426
430
  let match;
427
431
  while ((match = regex.exec(output)) !== null) {
428
432
  const raw = match[1].trim();
433
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
434
+ if (raw.startsWith("<"))
435
+ continue;
429
436
  const sep = raw.indexOf("|");
430
437
  if (sep !== -1) {
431
438
  perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
@@ -442,9 +449,15 @@ export function parsePermissions(output) {
442
449
  */
443
450
  export function parseTaskOutcome(output) {
444
451
  const lastChunk = output.slice(-500);
445
- if (lastChunk.includes(TASK_FAILURE_MARKER))
452
+ const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
453
+ let last = null;
454
+ let match;
455
+ while ((match = regex.exec(lastChunk)) !== null) {
456
+ last = match[0];
457
+ }
458
+ if (last === TASK_FAILURE_MARKER)
446
459
  return "failed";
447
- if (lastChunk.includes(TASK_SUCCESS_MARKER))
460
+ if (last === TASK_SUCCESS_MARKER)
448
461
  return "finished";
449
462
  return "finished";
450
463
  }
@@ -99,7 +99,7 @@ export async function serveCommand() {
99
99
  });
100
100
  }, POLL_INTERVAL_MS);
101
101
  const handleRpc = createRpcHandler(config, nc);
102
- const httpPort = config.httpPort ?? 7400;
102
+ const httpPort = config.httpPort ?? 9966;
103
103
  // Start NATS transport (loops forever, fire-and-forget)
104
104
  if (nc) {
105
105
  startNatsTransport(config, handleRpc, nc);
package/dist/events.js CHANGED
@@ -14,7 +14,7 @@ export async function publishHostEvent(nc, hostId, taskId, payload) {
14
14
  console.log(`[nats] ${subject} →`, payload);
15
15
  }
16
16
  const config = loadConfig();
17
- const port = config.httpPort ?? 7400;
17
+ const port = config.httpPort ?? 9966;
18
18
  try {
19
19
  await fetch(`http://localhost:${port}/event`, {
20
20
  method: "POST",
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { runCommand } from "./commands/run.js";
10
10
  import { serveCommand } from "./commands/serve.js";
11
11
  import { pairCommand } from "./commands/pair.js";
12
12
  import { restartCommand } from "./commands/restart.js";
13
- import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
13
+ import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
16
16
  const program = new Command();
@@ -54,26 +54,26 @@ program
54
54
  .action(async () => {
55
55
  await pairCommand();
56
56
  });
57
- const sessionsCmd = program
58
- .command("sessions")
59
- .description("Manage paired client sessions");
60
- sessionsCmd
57
+ const clientsCmd = program
58
+ .command("clients")
59
+ .description("Manage paired clients");
60
+ clientsCmd
61
61
  .command("list")
62
- .description("List active sessions")
62
+ .description("List active clients")
63
63
  .action(async () => {
64
- await sessionsListCommand();
64
+ await clientsListCommand();
65
65
  });
66
- sessionsCmd
66
+ clientsCmd
67
67
  .command("revoke <token>")
68
- .description("Revoke a session by token")
68
+ .description("Revoke a client by token")
69
69
  .action(async (token) => {
70
- await sessionsRevokeCommand(token);
70
+ await clientsRevokeCommand(token);
71
71
  });
72
- sessionsCmd
72
+ clientsCmd
73
73
  .command("revoke-all")
74
- .description("Revoke all sessions")
74
+ .description("Revoke all clients")
75
75
  .action(async () => {
76
- await sessionsRevokeAllCommand();
76
+ await clientsRevokeAllCommand();
77
77
  });
78
78
  // No subcommand → default to serve
79
79
  if (process.argv.length <= 2) {
@@ -10,7 +10,7 @@ import { getPlatform } from "./platform/index.js";
10
10
  import { spawnCommand } from "./spawn-command.js";
11
11
  import crossSpawn from "cross-spawn";
12
12
  import { getAgent } from "./agents/agent.js";
13
- import { validateSession } from "./session-store.js";
13
+ import { validateClient } from "./client-store.js";
14
14
  import { publishHostEvent } from "./events.js";
15
15
  import { currentVersion, performUpdate } from "./update-checker.js";
16
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
@@ -140,8 +140,8 @@ export function createRpcHandler(config, nc) {
140
140
  };
141
141
  }
142
142
  async function handleRpc(request) {
143
- // Session token validation: skip for trusted localhost requests
144
- if (!request.localhost && (!request.sessionToken || !validateSession(request.sessionToken))) {
143
+ // Client token validation: skip for trusted localhost requests
144
+ if (!request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
145
145
  return { error: "Unauthorized" };
146
146
  }
147
147
  switch (request.method) {
@@ -183,6 +183,7 @@ export function createRpcHandler(config, nc) {
183
183
  triggers: params.triggers ?? [],
184
184
  triggers_enabled: params.triggers_enabled ?? true,
185
185
  requires_confirmation: params.requires_confirmation ?? true,
186
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
186
187
  ...(params.command ? { command: params.command } : {}),
187
188
  },
188
189
  body,
@@ -211,6 +212,11 @@ export function createRpcHandler(config, nc) {
211
212
  existing.frontmatter.triggers_enabled = params.triggers_enabled;
212
213
  if (params.requires_confirmation !== undefined)
213
214
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
215
+ if (params.yolo_mode !== undefined) {
216
+ existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
217
+ if (params.yolo_mode)
218
+ delete existing.frontmatter.permissions;
219
+ }
214
220
  if (params.command !== undefined) {
215
221
  if (params.command) {
216
222
  existing.frontmatter.command = params.command;
@@ -261,6 +267,7 @@ export function createRpcHandler(config, nc) {
261
267
  triggers: [],
262
268
  triggers_enabled: false,
263
269
  requires_confirmation: params.requires_confirmation ?? false,
270
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
264
271
  ...(params.command ? { command: params.command } : {}),
265
272
  },
266
273
  body: "",
@@ -324,7 +331,7 @@ export function createRpcHandler(config, nc) {
324
331
  await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
325
332
  // Fire-and-forget: invoke agent inline as a child of the serve process
326
333
  const followupAgent = getAgent(followupTask.frontmatter.agent);
327
- const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(followupTask, params.message, followupTask.frontmatter.permissions);
334
+ const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions);
328
335
  // Spawn directly via crossSpawn so we can track and kill the child
329
336
  const child = crossSpawn(cmd, cmdArgs, {
330
337
  cwd: followupRunDir,
@@ -488,8 +495,8 @@ export function createRpcHandler(config, nc) {
488
495
  const reports = [];
489
496
  const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
490
497
  for (const file of params.report_files) {
491
- if (!file.endsWith(".md")) {
492
- reports.push({ file, error: "must end with .md" });
498
+ if (!file.endsWith(".md") && !file.endsWith(".txt")) {
499
+ reports.push({ file, error: "must end with .md or .txt" });
493
500
  continue;
494
501
  }
495
502
  const basename = path.basename(file);
package/dist/task.d.ts CHANGED
@@ -58,13 +58,23 @@ export declare function appendRunMessage(taskDir: string, runId: string, msg: Co
58
58
  export declare function beginStreamingMessage(taskDir: string, runId: string, time: number): StreamingMessageWriter;
59
59
  export declare class StreamingMessageWriter {
60
60
  private filePath;
61
- private delimiter;
62
- constructor(filePath: string, delimiter: string);
61
+ constructor(filePath: string);
63
62
  /** Append a chunk of content to the current message. */
64
63
  write(chunk: string): void;
65
- /** Finalize the message. If attachments are provided, rewrites the delimiter to include them. */
64
+ /** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
66
65
  end(attachments?: string[]): void;
67
66
  }
67
+ /**
68
+ * Splice a user message into a running assistant stream.
69
+ * Ends the current assistant block, writes the user message,
70
+ * then opens a new assistant block — all as direct file appends.
71
+ * The existing StreamingMessageWriter keeps working because its
72
+ * write() is just appendFileSync, so subsequent chunks land in
73
+ * the new assistant block.
74
+ */
75
+ export declare function spliceUserMessage(taskDir: string, runId: string, userMsg: ConversationMessage,
76
+ /** Optional text to append to the current assistant block before ending it. */
77
+ assistantAppend?: string): void;
68
78
  /**
69
79
  * Read conversation messages from a run's TASKRUN.md file.
70
80
  */
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
  */
@@ -1,9 +1,10 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "os";
3
3
  import { StringCodec } from "nats";
4
- import { validateSession, addSession } from "../session-store.js";
4
+ import { validateClient, addClient } from "../client-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).
@@ -102,9 +115,9 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
102
115
  const auth = req.headers.authorization;
103
116
  if (!auth || !auth.startsWith("Bearer "))
104
117
  return false;
105
- return validateSession(auth.slice(7));
118
+ return validateClient(auth.slice(7));
106
119
  }
107
- function extractSessionToken(req) {
120
+ function extractClientToken(req) {
108
121
  const auth = req.headers.authorization;
109
122
  if (!auth || !auth.startsWith("Bearer "))
110
123
  return undefined;
@@ -242,6 +255,8 @@ 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);
245
260
  const pendingPromise = registerPending(taskId, "input", descriptions);
246
261
  await publishEvent(taskId, {
247
262
  event_type: "input-request",
@@ -250,18 +265,20 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
250
265
  name: task.frontmatter.name,
251
266
  });
252
267
  const response = await pendingPromise;
268
+ const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
253
269
  if (response.length === 1 && response[0] === "aborted") {
254
270
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
255
- if (runId) {
256
- 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 });
257
274
  }
258
275
  sendJson(res, 200, { aborted: true });
259
276
  }
260
277
  else {
261
278
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
262
- if (runId) {
263
- const lines = descriptions.map((desc, i) => `**${desc}** ${response[i]}`);
264
- 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 });
265
282
  }
266
283
  sendJson(res, 200, { values: response });
267
284
  }
@@ -351,11 +368,11 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
351
368
  sendJson(res, 401, { error: "Invalid code" });
352
369
  return;
353
370
  }
354
- const session = addSession(label);
371
+ const client = addClient(label);
355
372
  const ip = detectLanIp();
356
373
  const response = {
357
374
  hostId: config.hostId,
358
- sessionToken: session.token,
375
+ clientToken: client.token,
359
376
  directUrl: `http://${ip}:${port}`,
360
377
  };
361
378
  clearTimeout(pending.timer);
@@ -432,10 +449,10 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
432
449
  sendJson(res, 400, { error: "Invalid JSON" });
433
450
  return;
434
451
  }
435
- const sessionToken = extractSessionToken(req);
452
+ const clientToken = extractClientToken(req);
436
453
  console.log(`[http] RPC: ${method}`);
437
454
  try {
438
- const response = await handleRpc({ method, params, sessionToken, localhost: isLocalhost(req) });
455
+ const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
439
456
  console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
440
457
  sendJson(res, 200, response);
441
458
  }
@@ -39,13 +39,13 @@ export async function startNatsTransport(config, handleRpc, nc) {
39
39
  }
40
40
  }
41
41
  }
42
- // Extract sessionToken from params (PWA includes it in the payload)
43
- const sessionToken = typeof params.sessionToken === "string" ? params.sessionToken : undefined;
44
- delete params.sessionToken;
42
+ // Extract clientToken from params (PWA includes it in the payload)
43
+ const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
44
+ delete params.clientToken;
45
45
  console.log(`[nats] RPC: ${method}`);
46
46
  let response;
47
47
  try {
48
- response = await handleRpc({ method, params, sessionToken });
48
+ response = await handleRpc({ method, params, clientToken });
49
49
  }
50
50
  catch (err) {
51
51
  console.error(`[nats] RPC error (${method}):`, err);
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
  }
@@ -66,8 +67,8 @@ export interface ConversationMessage {
66
67
  export interface RpcMessage {
67
68
  method: string;
68
69
  params: Record<string, unknown>;
69
- sessionToken?: string;
70
- /** Trusted localhost request — skip session validation. */
70
+ clientToken?: string;
71
+ /** Trusted localhost request — skip client validation. */
71
72
  localhost?: boolean;
72
73
  }
73
74
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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:
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 other text on the same line):
11
+ [PALMIER_TASK_SUCCESS]
12
+ [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:
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