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.
- package/README.md +9 -9
- 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 +7 -3
- package/dist/client-store.d.ts +12 -0
- package/dist/client-store.js +57 -0
- package/dist/commands/clients.d.ts +4 -0
- package/dist/commands/clients.js +27 -0
- package/dist/commands/info.js +5 -5
- package/dist/commands/init.js +1 -1
- package/dist/commands/pair.js +4 -4
- package/dist/commands/run.js +21 -8
- package/dist/commands/serve.js +1 -1
- package/dist/events.js +1 -1
- package/dist/index.js +13 -13
- package/dist/rpc-handler.js +13 -6
- package/dist/task.d.ts +13 -3
- package/dist/task.js +39 -7
- package/dist/transports/http-transport.js +30 -13
- package/dist/transports/nats-transport.js +4 -4
- package/dist/types.d.ts +3 -2
- 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 +7 -3
- package/src/client-store.ts +68 -0
- package/src/commands/clients.ts +29 -0
- package/src/commands/info.ts +5 -5
- package/src/commands/init.ts +1 -1
- package/src/commands/pair.ts +4 -4
- package/src/commands/run.ts +22 -8
- package/src/commands/serve.ts +1 -1
- package/src/events.ts +1 -1
- package/src/index.ts +13 -13
- package/src/rpc-handler.ts +15 -6
- package/src/task.ts +43 -8
- package/src/transports/http-transport.ts +32 -13
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -3
- package/test/agent-instructions.test.ts +48 -0
- package/test/agent-output-parsing.test.ts +12 -0
- package/dist/commands/sessions.d.ts +0 -4
- package/dist/commands/sessions.js +0 -27
- package/dist/session-store.d.ts +0 -12
- package/dist/session-store.js +0 -57
- package/src/commands/sessions.ts +0 -29
- package/src/session-store.ts +0 -68
package/dist/commands/run.js
CHANGED
|
@@ -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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
460
|
+
if (last === TASK_SUCCESS_MARKER)
|
|
448
461
|
return "finished";
|
|
449
462
|
return "finished";
|
|
450
463
|
}
|
package/dist/commands/serve.js
CHANGED
|
@@ -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 ??
|
|
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 ??
|
|
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 {
|
|
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
|
|
58
|
-
.command("
|
|
59
|
-
.description("Manage paired
|
|
60
|
-
|
|
57
|
+
const clientsCmd = program
|
|
58
|
+
.command("clients")
|
|
59
|
+
.description("Manage paired clients");
|
|
60
|
+
clientsCmd
|
|
61
61
|
.command("list")
|
|
62
|
-
.description("List active
|
|
62
|
+
.description("List active clients")
|
|
63
63
|
.action(async () => {
|
|
64
|
-
await
|
|
64
|
+
await clientsListCommand();
|
|
65
65
|
});
|
|
66
|
-
|
|
66
|
+
clientsCmd
|
|
67
67
|
.command("revoke <token>")
|
|
68
|
-
.description("Revoke a
|
|
68
|
+
.description("Revoke a client by token")
|
|
69
69
|
.action(async (token) => {
|
|
70
|
-
await
|
|
70
|
+
await clientsRevokeCommand(token);
|
|
71
71
|
});
|
|
72
|
-
|
|
72
|
+
clientsCmd
|
|
73
73
|
.command("revoke-all")
|
|
74
|
-
.description("Revoke all
|
|
74
|
+
.description("Revoke all clients")
|
|
75
75
|
.action(async () => {
|
|
76
|
-
await
|
|
76
|
+
await clientsRevokeAllCommand();
|
|
77
77
|
});
|
|
78
78
|
// No subcommand → default to serve
|
|
79
79
|
if (process.argv.length <= 2) {
|
package/dist/rpc-handler.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
144
|
-
if (!request.localhost && (!request.
|
|
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
|
-
|
|
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
|
|
172
|
+
return new StreamingMessageWriter(filePath);
|
|
173
173
|
}
|
|
174
174
|
export class StreamingMessageWriter {
|
|
175
175
|
filePath;
|
|
176
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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 {
|
|
4
|
+
import { validateClient, addClient } from "../client-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
|
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
|
|
118
|
+
return validateClient(auth.slice(7));
|
|
106
119
|
}
|
|
107
|
-
function
|
|
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 (
|
|
256
|
-
|
|
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 (
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
371
|
+
const client = addClient(label);
|
|
355
372
|
const ip = detectLanIp();
|
|
356
373
|
const response = {
|
|
357
374
|
hostId: config.hostId,
|
|
358
|
-
|
|
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
|
|
452
|
+
const clientToken = extractClientToken(req);
|
|
436
453
|
console.log(`[http] RPC: ${method}`);
|
|
437
454
|
try {
|
|
438
|
-
const response = await handleRpc({ method, params,
|
|
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
|
|
43
|
-
const
|
|
44
|
-
delete params.
|
|
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,
|
|
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
|
-
|
|
70
|
-
/** Trusted localhost request — skip
|
|
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
|
@@ -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
|
|
6
|
-
[PALMIER_REPORT]
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
19
|
-
[PALMIER_PERMISSION]
|
|
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
|
|
package/src/agents/agent.ts
CHANGED
|
@@ -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).
|
|
25
|
-
* permissions granted for this run only
|
|
26
|
-
|
|
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
|
}
|
package/src/agents/claude.ts
CHANGED
|
@@ -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
|
|
17
|
-
const
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
package/src/agents/codex.ts
CHANGED
|
@@ -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
|
|
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", "--
|
|
20
|
+
const args = ["exec", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|