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
@@ -151,10 +151,17 @@ const activeFollowups = new Map<string, ChildProcess>();
151
151
  export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
152
152
  function flattenTask(task: ParsedTask) {
153
153
  const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
154
+ const status = readTaskStatus(taskDir);
155
+ const pending = getPending(task.frontmatter.id);
154
156
  return {
155
157
  ...task.frontmatter,
156
158
  body: task.body,
157
- status: readTaskStatus(taskDir),
159
+ status: status ? {
160
+ ...status,
161
+ ...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
162
+ ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
163
+ ...(pending?.type === "input" ? { pending_input: pending.params } : {}),
164
+ } : undefined,
158
165
  };
159
166
  }
160
167
 
@@ -181,6 +188,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
181
188
  triggers?: Array<{ type: "cron" | "once"; value: string }>;
182
189
  triggers_enabled?: boolean;
183
190
  requires_confirmation?: boolean;
191
+ yolo_mode?: boolean;
184
192
  command?: string;
185
193
  };
186
194
 
@@ -211,6 +219,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
211
219
  triggers: params.triggers ?? [],
212
220
  triggers_enabled: params.triggers_enabled ?? true,
213
221
  requires_confirmation: params.requires_confirmation ?? true,
222
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
214
223
  ...(params.command ? { command: params.command } : {}),
215
224
  },
216
225
  body,
@@ -231,6 +240,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
231
240
  triggers?: Array<{ type: "cron" | "once"; value: string }>;
232
241
  triggers_enabled?: boolean;
233
242
  requires_confirmation?: boolean;
243
+ yolo_mode?: boolean;
234
244
  command?: string;
235
245
  };
236
246
 
@@ -249,6 +259,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
249
259
  if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
250
260
  if (params.requires_confirmation !== undefined)
251
261
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
262
+ if (params.yolo_mode !== undefined) existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
252
263
  if (params.command !== undefined) {
253
264
  if (params.command) {
254
265
  existing.frontmatter.command = params.command;
@@ -295,6 +306,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
295
306
  user_prompt: string;
296
307
  agent: string;
297
308
  requires_confirmation?: boolean;
309
+ yolo_mode?: boolean;
298
310
  command?: string;
299
311
  };
300
312
 
@@ -310,6 +322,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
310
322
  triggers: [],
311
323
  triggers_enabled: false,
312
324
  requires_confirmation: params.requires_confirmation ?? false,
325
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
313
326
  ...(params.command ? { command: params.command } : {}),
314
327
  },
315
328
  body: "",
@@ -383,7 +396,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
383
396
  // Fire-and-forget: invoke agent inline as a child of the serve process
384
397
  const followupAgent = getAgent(followupTask.frontmatter.agent);
385
398
  const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(
386
- followupTask, params.message, followupTask.frontmatter.permissions,
399
+ followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
387
400
  );
388
401
 
389
402
  // Spawn directly via crossSpawn so we can track and kill the child
@@ -557,8 +570,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
557
570
  const reports: Array<{ file: string; content?: string; error?: string }> = [];
558
571
  const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
559
572
  for (const file of params.report_files) {
560
- if (!file.endsWith(".md")) {
561
- reports.push({ file, error: "must end with .md" });
573
+ if (!file.endsWith(".md") && !file.endsWith(".txt")) {
574
+ reports.push({ file, error: "must end with .md or .txt" });
562
575
  continue;
563
576
  }
564
577
  const basename = path.basename(file);
package/src/task.ts CHANGED
@@ -202,31 +202,66 @@ export function beginStreamingMessage(
202
202
  const filePath = path.join(taskDir, runId, "TASKRUN.md");
203
203
  const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
204
204
  fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
205
- return new StreamingMessageWriter(filePath, delimiter);
205
+ return new StreamingMessageWriter(filePath);
206
206
  }
207
207
 
208
208
  export class StreamingMessageWriter {
209
- private delimiter: string;
210
- constructor(private filePath: string, delimiter: string) {
211
- this.delimiter = delimiter;
212
- }
209
+ constructor(private filePath: string) {}
213
210
 
214
211
  /** Append a chunk of content to the current message. */
215
212
  write(chunk: string): void {
216
213
  fs.appendFileSync(this.filePath, chunk, "utf-8");
217
214
  }
218
215
 
219
- /** Finalize the message. If attachments are provided, rewrites the delimiter to include them. */
216
+ /** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
220
217
  end(attachments?: string[]): void {
221
218
  fs.appendFileSync(this.filePath, "\n\n", "utf-8");
222
219
  if (attachments?.length) {
223
220
  const raw = fs.readFileSync(this.filePath, "utf-8");
224
- const updated = raw.replace(this.delimiter, `${this.delimiter.slice(0, -4)} attachments="${attachments.join(",")}" -->`);
225
- fs.writeFileSync(this.filePath, updated, "utf-8");
221
+ // Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
222
+ const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
223
+ let lastMatch: RegExpExecArray | null = null;
224
+ let m;
225
+ while ((m = pattern.exec(raw)) !== null) lastMatch = m;
226
+ if (lastMatch) {
227
+ const before = raw.slice(0, lastMatch.index);
228
+ const after = raw.slice(lastMatch.index + lastMatch[0].length);
229
+ const updated = before + `${lastMatch[0].slice(0, -4)} attachments="${attachments.join(",")}" -->` + after;
230
+ fs.writeFileSync(this.filePath, updated, "utf-8");
231
+ }
226
232
  }
227
233
  }
228
234
  }
229
235
 
236
+ /**
237
+ * Splice a user message into a running assistant stream.
238
+ * Ends the current assistant block, writes the user message,
239
+ * then opens a new assistant block — all as direct file appends.
240
+ * The existing StreamingMessageWriter keeps working because its
241
+ * write() is just appendFileSync, so subsequent chunks land in
242
+ * the new assistant block.
243
+ */
244
+ export function spliceUserMessage(
245
+ taskDir: string,
246
+ runId: string,
247
+ userMsg: ConversationMessage,
248
+ /** Optional text to append to the current assistant block before ending it. */
249
+ assistantAppend?: string,
250
+ ): void {
251
+ const filePath = path.join(taskDir, runId, "TASKRUN.md");
252
+ // 1. Optionally append to the current assistant block (e.g. the input questions)
253
+ if (assistantAppend) {
254
+ fs.appendFileSync(filePath, assistantAppend, "utf-8");
255
+ }
256
+ // 2. End the current assistant block
257
+ fs.appendFileSync(filePath, "\n\n", "utf-8");
258
+ // 3. Write the user message
259
+ appendRunMessage(taskDir, runId, userMsg);
260
+ // 4. Open a new assistant block for subsequent agent output
261
+ const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
262
+ fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
263
+ }
264
+
230
265
  /**
231
266
  * Read conversation messages from a run's TASKRUN.md file.
232
267
  */
@@ -3,7 +3,8 @@ import * as os from "os";
3
3
  import { StringCodec, type NatsConnection } 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
  import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
8
9
 
9
10
  const PWA_ORIGIN = "https://app.palmier.me";
@@ -99,6 +100,18 @@ export function detectLanIp(): string {
99
100
  return "127.0.0.1";
100
101
  }
101
102
 
103
+ /** Find the latest (highest-numbered) run directory for a task. */
104
+ function findLatestRunId(taskDir: string): string | null {
105
+ try {
106
+ const dirs = fs.readdirSync(taskDir)
107
+ .filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
108
+ .sort();
109
+ return dirs.length > 0 ? dirs[dirs.length - 1] : null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
102
115
  /**
103
116
  * Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
104
117
  * localhost-only agent endpoints (notify, request-input, confirmation, permission).
@@ -262,6 +275,11 @@ export async function startHttpTransport(
262
275
  const taskDir = getTaskDir(config.projectRoot, taskId);
263
276
  const task = parseTaskFile(taskDir);
264
277
 
278
+ // Resolve runId: use provided value, otherwise find the latest run directory
279
+ const effectiveRunId = runId ?? findLatestRunId(taskDir);
280
+
281
+ const pendingPromise = registerPending(taskId, "input", descriptions);
282
+
265
283
  await publishEvent(taskId, {
266
284
  event_type: "input-request",
267
285
  host_id: config.hostId,
@@ -269,19 +287,22 @@ export async function startHttpTransport(
269
287
  name: task.frontmatter.name,
270
288
  });
271
289
 
272
- const response = await registerPending(taskId, "input", descriptions);
290
+ const response = await pendingPromise;
291
+
292
+ const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
273
293
 
274
294
  if (response.length === 1 && response[0] === "aborted") {
275
295
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
276
- if (runId) {
277
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
296
+ if (effectiveRunId) {
297
+ spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
298
+ await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
278
299
  }
279
300
  sendJson(res, 200, { aborted: true });
280
301
  } else {
281
302
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
282
- if (runId) {
283
- const lines = descriptions.map((desc, i) => `**${desc}** ${response[i]}`);
284
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
303
+ if (effectiveRunId) {
304
+ spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
305
+ await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
285
306
  }
286
307
  sendJson(res, 200, { values: response });
287
308
  }
@@ -300,12 +321,14 @@ export async function startHttpTransport(
300
321
  const { taskId } = JSON.parse(body) as { taskId: string };
301
322
  if (!taskId) { sendJson(res, 400, { error: "taskId is required" }); return; }
302
323
 
324
+ const pendingPromise = registerPending(taskId, "confirmation");
325
+
303
326
  await publishEvent(taskId, {
304
327
  event_type: "confirm-request",
305
328
  host_id: config.hostId,
306
329
  });
307
330
 
308
- const response = await registerPending(taskId, "confirmation");
331
+ const response = await pendingPromise;
309
332
  const confirmed = response[0] === "confirmed";
310
333
 
311
334
  await publishEvent(taskId, {
@@ -335,6 +358,8 @@ export async function startHttpTransport(
335
358
  return;
336
359
  }
337
360
 
361
+ const pendingPromise = registerPending(taskId, "permission", permissions);
362
+
338
363
  await publishEvent(taskId, {
339
364
  event_type: "permission-request",
340
365
  host_id: config.hostId,
@@ -342,7 +367,7 @@ export async function startHttpTransport(
342
367
  name: taskName,
343
368
  });
344
369
 
345
- const response = await registerPending(taskId, "permission", permissions);
370
+ const response = await pendingPromise;
346
371
  const status = response[0] as "granted" | "granted_all" | "aborted";
347
372
 
348
373
  await publishEvent(taskId, {
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ export interface TaskFrontmatter {
23
23
  triggers: Trigger[];
24
24
  triggers_enabled: boolean;
25
25
  requires_confirmation: boolean;
26
+ yolo_mode?: boolean;
26
27
  permissions?: RequiredPermission[];
27
28
  command?: string;
28
29
  }
@@ -0,0 +1,31 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { getAgentInstructions } from "../src/agents/shared-prompt.js";
4
+
5
+ describe("getAgentInstructions", () => {
6
+ it("includes Permissions section by default", () => {
7
+ const result = getAgentInstructions("test-task-id");
8
+ assert.match(result, /## Permissions/);
9
+ assert.match(result, /PALMIER_PERMISSION/);
10
+ });
11
+
12
+ it("strips Permissions section when skipPermissions is true", () => {
13
+ const result = getAgentInstructions("test-task-id", true);
14
+ assert.doesNotMatch(result, /## Permissions/);
15
+ assert.doesNotMatch(result, /PALMIER_PERMISSION/);
16
+ });
17
+
18
+ it("preserves other sections when Permissions is stripped", () => {
19
+ const result = getAgentInstructions("test-task-id", true);
20
+ assert.match(result, /## Reporting Output/);
21
+ assert.match(result, /## Completion/);
22
+ assert.match(result, /## HTTP Endpoints/);
23
+ });
24
+
25
+ it("replaces template variables", () => {
26
+ const result = getAgentInstructions("my-task-123");
27
+ assert.match(result, /my-task-123/);
28
+ assert.doesNotMatch(result, /\{\{TASK_ID\}\}/);
29
+ assert.doesNotMatch(result, /\{\{PORT\}\}/);
30
+ });
31
+ });