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/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
|
*/
|
|
@@ -3,7 +3,8 @@ import * as os from "os";
|
|
|
3
3
|
import { StringCodec } from "nats";
|
|
4
4
|
import { validateSession, addSession } from "../session-store.js";
|
|
5
5
|
import { registerPending } from "../pending-requests.js";
|
|
6
|
-
import
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
|
|
7
8
|
const PWA_ORIGIN = "https://app.palmier.me";
|
|
8
9
|
const assetCache = new Map();
|
|
9
10
|
/** Paths currently being fetched (dedup concurrent requests). */
|
|
@@ -78,6 +79,18 @@ export function detectLanIp() {
|
|
|
78
79
|
}
|
|
79
80
|
return "127.0.0.1";
|
|
80
81
|
}
|
|
82
|
+
/** Find the latest (highest-numbered) run directory for a task. */
|
|
83
|
+
function findLatestRunId(taskDir) {
|
|
84
|
+
try {
|
|
85
|
+
const dirs = fs.readdirSync(taskDir)
|
|
86
|
+
.filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
|
|
87
|
+
.sort();
|
|
88
|
+
return dirs.length > 0 ? dirs[dirs.length - 1] : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
81
94
|
/**
|
|
82
95
|
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
83
96
|
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
@@ -242,25 +255,30 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
242
255
|
}
|
|
243
256
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
244
257
|
const task = parseTaskFile(taskDir);
|
|
258
|
+
// Resolve runId: use provided value, otherwise find the latest run directory
|
|
259
|
+
const effectiveRunId = runId ?? findLatestRunId(taskDir);
|
|
260
|
+
const pendingPromise = registerPending(taskId, "input", descriptions);
|
|
245
261
|
await publishEvent(taskId, {
|
|
246
262
|
event_type: "input-request",
|
|
247
263
|
host_id: config.hostId,
|
|
248
264
|
input_descriptions: descriptions,
|
|
249
265
|
name: task.frontmatter.name,
|
|
250
266
|
});
|
|
251
|
-
const response = await
|
|
267
|
+
const response = await pendingPromise;
|
|
268
|
+
const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
|
|
252
269
|
if (response.length === 1 && response[0] === "aborted") {
|
|
253
270
|
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
|
|
254
|
-
if (
|
|
255
|
-
|
|
271
|
+
if (effectiveRunId) {
|
|
272
|
+
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
|
|
273
|
+
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
256
274
|
}
|
|
257
275
|
sendJson(res, 200, { aborted: true });
|
|
258
276
|
}
|
|
259
277
|
else {
|
|
260
278
|
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
if (effectiveRunId) {
|
|
280
|
+
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
|
|
281
|
+
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
264
282
|
}
|
|
265
283
|
sendJson(res, 200, { values: response });
|
|
266
284
|
}
|
|
@@ -283,11 +301,12 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
283
301
|
sendJson(res, 400, { error: "taskId is required" });
|
|
284
302
|
return;
|
|
285
303
|
}
|
|
304
|
+
const pendingPromise = registerPending(taskId, "confirmation");
|
|
286
305
|
await publishEvent(taskId, {
|
|
287
306
|
event_type: "confirm-request",
|
|
288
307
|
host_id: config.hostId,
|
|
289
308
|
});
|
|
290
|
-
const response = await
|
|
309
|
+
const response = await pendingPromise;
|
|
291
310
|
const confirmed = response[0] === "confirmed";
|
|
292
311
|
await publishEvent(taskId, {
|
|
293
312
|
event_type: "confirm-resolved",
|
|
@@ -314,13 +333,14 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
314
333
|
sendJson(res, 400, { error: "taskId and permissions are required" });
|
|
315
334
|
return;
|
|
316
335
|
}
|
|
336
|
+
const pendingPromise = registerPending(taskId, "permission", permissions);
|
|
317
337
|
await publishEvent(taskId, {
|
|
318
338
|
event_type: "permission-request",
|
|
319
339
|
host_id: config.hostId,
|
|
320
340
|
required_permissions: permissions,
|
|
321
341
|
name: taskName,
|
|
322
342
|
});
|
|
323
|
-
const response = await
|
|
343
|
+
const response = await pendingPromise;
|
|
324
344
|
const status = response[0];
|
|
325
345
|
await publishEvent(taskId, {
|
|
326
346
|
event_type: "permission-resolved",
|
package/dist/types.d.ts
CHANGED
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 (no code block):
|
|
6
|
+
`[PALMIER_REPORT] <filename>`
|
|
8
7
|
|
|
9
8
|
## Completion
|
|
10
9
|
|
|
11
|
-
When you are done, output exactly one of these markers as the very last line:
|
|
12
|
-
- Success: [PALMIER_TASK_SUCCESS]
|
|
13
|
-
- Failure: [PALMIER_TASK_FAILURE]
|
|
14
|
-
Do not wrap them in code blocks or add text on the same line.
|
|
10
|
+
When you are done, output exactly one of these markers as the very last line (no code block, no other text on the same line):
|
|
11
|
+
- Success: `[PALMIER_TASK_SUCCESS]`
|
|
12
|
+
- Failure: `[PALMIER_TASK_FAILURE]`
|
|
15
13
|
|
|
16
14
|
## Permissions
|
|
17
15
|
|
|
18
|
-
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line
|
|
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 (no code block):
|
|
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
|
package/src/agents/copilot.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 CopilotAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
8
9
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return {
|
|
10
11
|
command: "copilot",
|
|
@@ -12,12 +13,17 @@ export class CopilotAgent implements AgentTool {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
17
19
|
const args = ["-p", prompt];
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--yolo");
|
|
23
|
+
} else {
|
|
24
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
25
|
+
args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
|
|
26
|
+
}
|
|
21
27
|
if (followupPrompt) { args.push("--continue"); }
|
|
22
28
|
return { command: "copilot", args};
|
|
23
29
|
}
|
package/src/agents/gemini.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 GeminiAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = true;
|
|
8
9
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return {
|
|
10
11
|
command: "gemini",
|
|
@@ -12,21 +13,24 @@ export class GeminiAgent implements AgentTool {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const
|
|
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 = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
if (!yolo) {
|
|
22
|
+
const tools = ["run_shell_command", "web_fetch"];
|
|
23
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
21
24
|
for (const p of allPerms) {
|
|
22
|
-
|
|
25
|
+
tools.push(p.name);
|
|
23
26
|
}
|
|
27
|
+
args.push("--allowed-tools", tools.join(","));
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
if (followupPrompt) {args.push("--resume");} // continue mode for followups
|
|
27
|
-
args.push("--prompt",
|
|
31
|
+
args.push("--prompt", "-"); // read prompt from stdin to avoid command line length limits
|
|
28
32
|
|
|
29
|
-
return { command: "gemini", args };
|
|
33
|
+
return { command: "gemini", args, stdin: prompt };
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
async init(): Promise<boolean> {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class KimiAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "kimi",
|
|
12
|
+
args: ["-p", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = [];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--yolo");
|
|
23
|
+
}
|
|
24
|
+
if (followupPrompt) { args.push("--continue"); }
|
|
25
|
+
args.push("-p", prompt);
|
|
26
|
+
return { command: "kimi", args };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async init(): Promise<boolean> {
|
|
30
|
+
try {
|
|
31
|
+
execSync("kimi --version", { stdio: "ignore", shell: SHELL });
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentTool, CommandLine } from "./agent.js";
|
|
|
4
4
|
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
6
|
export class OpenClawAgent implements AgentTool {
|
|
7
|
+
supportsPermissions = false;
|
|
7
8
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
9
|
return {
|
|
9
10
|
command: "openclaw",
|
|
@@ -11,8 +12,9 @@ export class OpenClawAgent implements AgentTool {
|
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
-
const
|
|
15
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
16
|
+
const yolo = extraPermissions === "yolo";
|
|
17
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
16
18
|
// OpenClaw does not support stdin as prompt.
|
|
17
19
|
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
18
20
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class QwenAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "qwen",
|
|
12
|
+
args: ["-p", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
|
|
20
|
+
|
|
21
|
+
if (followupPrompt) { args.push("-c"); }
|
|
22
|
+
args.push("-p", prompt);
|
|
23
|
+
return { command: "qwen", args };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async init(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
execSync("qwen --version", { stdio: "ignore", shell: SHELL });
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -13,11 +13,15 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
|
13
13
|
/**
|
|
14
14
|
* Agent instructions with the serve daemon's HTTP port and task ID baked in.
|
|
15
15
|
*/
|
|
16
|
-
export function getAgentInstructions(taskId: string): string {
|
|
16
|
+
export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
17
17
|
const port = loadConfig().httpPort ?? 7400;
|
|
18
|
-
|
|
18
|
+
let instructions = AGENT_INSTRUCTIONS_TEMPLATE
|
|
19
19
|
.replace(/\{\{PORT\}\}/g, String(port))
|
|
20
20
|
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
21
|
+
if (skipPermissions) {
|
|
22
|
+
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
23
|
+
}
|
|
24
|
+
return instructions;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
package/src/commands/run.ts
CHANGED
|
@@ -62,7 +62,12 @@ async function invokeAgentWithRetries(
|
|
|
62
62
|
}, 500);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
|
|
65
|
+
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
|
|
66
|
+
invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
|
|
67
|
+
);
|
|
68
|
+
const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
|
|
69
|
+
const displayArgs = args.map((a) => truncate(a));
|
|
70
|
+
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
66
71
|
const result = await spawnCommand(command, args, {
|
|
67
72
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
68
73
|
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
|
|
@@ -454,7 +459,11 @@ async function requestPermission(
|
|
|
454
459
|
permissions: requiredPermissions,
|
|
455
460
|
}),
|
|
456
461
|
});
|
|
457
|
-
const
|
|
462
|
+
const body = await res.json() as { response?: string; error?: string };
|
|
463
|
+
const response = body.response as "granted" | "granted_all" | "aborted" | undefined;
|
|
464
|
+
if (!response || !["granted", "granted_all", "aborted"].includes(response)) {
|
|
465
|
+
throw new Error(`Permission request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
|
|
466
|
+
}
|
|
458
467
|
writeTaskStatus(taskDir, {
|
|
459
468
|
running_state: response === "aborted" ? "aborted" : "started",
|
|
460
469
|
time_stamp: Date.now(),
|
|
@@ -474,7 +483,11 @@ async function requestConfirmation(
|
|
|
474
483
|
headers: { "Content-Type": "application/json" },
|
|
475
484
|
body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
|
|
476
485
|
});
|
|
477
|
-
const
|
|
486
|
+
const body = await res.json() as { confirmed?: boolean; error?: string };
|
|
487
|
+
if (typeof body.confirmed !== "boolean") {
|
|
488
|
+
throw new Error(`Confirmation request failed: ${body.error ?? `unexpected response: ${JSON.stringify(body)}`}`);
|
|
489
|
+
}
|
|
490
|
+
const { confirmed } = body;
|
|
478
491
|
writeTaskStatus(taskDir, {
|
|
479
492
|
running_state: confirmed ? "started" : "aborted",
|
|
480
493
|
time_stamp: Date.now(),
|
|
@@ -523,8 +536,14 @@ export function parsePermissions(output: string): RequiredPermission[] {
|
|
|
523
536
|
*/
|
|
524
537
|
export function parseTaskOutcome(output: string): TaskRunningState {
|
|
525
538
|
const lastChunk = output.slice(-500);
|
|
526
|
-
|
|
527
|
-
|
|
539
|
+
const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
|
|
540
|
+
let last: string | null = null;
|
|
541
|
+
let match;
|
|
542
|
+
while ((match = regex.exec(lastChunk)) !== null) {
|
|
543
|
+
last = match[0];
|
|
544
|
+
}
|
|
545
|
+
if (last === TASK_FAILURE_MARKER) return "failed";
|
|
546
|
+
if (last === TASK_SUCCESS_MARKER) return "finished";
|
|
528
547
|
return "finished";
|
|
529
548
|
}
|
|
530
549
|
|
package/src/platform/windows.ts
CHANGED
|
@@ -127,7 +127,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
127
127
|
process.exit(0);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Kill old daemon
|
|
130
|
+
// Kill old daemon by PID
|
|
131
131
|
if (oldPid) {
|
|
132
132
|
try {
|
|
133
133
|
execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
@@ -136,6 +136,19 @@ export class WindowsPlatform implements PlatformService {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Also kill any stale palmier serve processes (e.g. leftover from a previous daemon)
|
|
140
|
+
try {
|
|
141
|
+
const out = execFileSync("wmic", ["process", "where", `CommandLine like '%palmier%serve%' and ProcessId != '${process.pid}'`, "get", "ProcessId"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
142
|
+
for (const line of out.split("\n")) {
|
|
143
|
+
const pid = line.trim();
|
|
144
|
+
if (pid && /^\d+$/.test(pid)) {
|
|
145
|
+
try { execFileSync("taskkill", ["/pid", pid, "/f", "/t"], { windowsHide: true, stdio: "pipe" }); } catch {}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// wmic may not be available on all Windows versions
|
|
150
|
+
}
|
|
151
|
+
|
|
139
152
|
this.startDaemonTask();
|
|
140
153
|
}
|
|
141
154
|
|