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.
- package/dist/agents/agent-instructions.md +7 -11
- package/dist/agents/agent.d.ts +8 -3
- package/dist/agents/agent.js +7 -1
- package/dist/agents/claude.d.ts +2 -1
- package/dist/agents/claude.js +10 -5
- package/dist/agents/codex.d.ts +2 -1
- package/dist/agents/codex.js +10 -6
- package/dist/agents/copilot.d.ts +2 -1
- package/dist/agents/copilot.js +10 -3
- package/dist/agents/gemini.d.ts +2 -1
- package/dist/agents/gemini.js +11 -7
- package/dist/agents/kimi.d.ts +9 -0
- package/dist/agents/kimi.js +35 -0
- package/dist/agents/openclaw.d.ts +2 -1
- package/dist/agents/openclaw.js +3 -1
- package/dist/agents/qwen.d.ts +9 -0
- package/dist/agents/qwen.js +32 -0
- package/dist/agents/shared-prompt.d.ts +1 -1
- package/dist/agents/shared-prompt.js +6 -2
- package/dist/commands/run.js +22 -5
- package/dist/platform/windows.js +17 -1
- package/dist/rpc-handler.js +15 -4
- package/dist/task.d.ts +13 -3
- package/dist/task.js +39 -7
- package/dist/transports/http-transport.js +29 -9
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +7 -11
- package/src/agents/agent.ts +16 -4
- package/src/agents/claude.ts +11 -6
- package/src/agents/codex.ts +11 -7
- package/src/agents/copilot.ts +10 -4
- package/src/agents/gemini.ts +12 -8
- package/src/agents/kimi.ts +37 -0
- package/src/agents/openclaw.ts +4 -2
- package/src/agents/qwen.ts +34 -0
- package/src/agents/shared-prompt.ts +6 -2
- package/src/commands/run.ts +24 -5
- package/src/platform/windows.ts +14 -1
- package/src/rpc-handler.ts +17 -4
- package/src/task.ts +43 -8
- package/src/transports/http-transport.ts +34 -9
- package/src/types.ts +1 -0
- package/test/agent-instructions.test.ts +31 -0
package/src/rpc-handler.ts
CHANGED
|
@@ -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:
|
|
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
|
|
205
|
+
return new StreamingMessageWriter(filePath);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
export class StreamingMessageWriter {
|
|
209
|
-
private
|
|
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
|
-
|
|
225
|
-
|
|
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
|
|
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
|
|
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 (
|
|
277
|
-
|
|
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 (
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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
|
+
});
|