palmier 0.6.7 → 0.6.8
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 +14 -0
- package/dist/agents/agent-instructions.md +7 -37
- package/dist/agents/aider.js +1 -1
- package/dist/agents/claude.js +1 -1
- package/dist/agents/cline.js +1 -1
- package/dist/agents/codex.js +1 -1
- package/dist/agents/copilot.js +1 -1
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/droid.js +1 -1
- package/dist/agents/gemini.js +1 -1
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kimi.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/openclaw.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/qwen.js +1 -1
- package/dist/agents/shared-prompt.d.ts +3 -2
- package/dist/agents/shared-prompt.js +6 -4
- package/dist/commands/run.js +2 -5
- package/dist/mcp-handler.js +1 -1
- package/dist/mcp-tools.d.ts +6 -1
- package/dist/mcp-tools.js +72 -6
- package/dist/pwa/assets/{index-DAI3J-jU.css → index-C6Lz09EY.css} +1 -1
- package/dist/pwa/assets/{index-RrJvjqz9.js → index-C8vJwUNi.js} +42 -42
- package/dist/pwa/assets/{web-DQteXlI7.js → web-6UChJFov.js} +1 -1
- package/dist/pwa/assets/{web-EzNEHXEh.js → web-NxTETXZK.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/spawn-command.js +3 -1
- package/dist/transports/http-transport.js +4 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +9 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
- package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/spec.md +1 -1
- package/src/agents/agent-instructions.md +7 -37
- package/src/agents/aider.ts +1 -1
- package/src/agents/claude.ts +1 -1
- package/src/agents/cline.ts +1 -1
- package/src/agents/codex.ts +1 -1
- package/src/agents/copilot.ts +1 -1
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/droid.ts +1 -1
- package/src/agents/gemini.ts +1 -1
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kimi.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/openclaw.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/qwen.ts +1 -1
- package/src/agents/shared-prompt.ts +7 -4
- package/src/commands/run.ts +2 -5
- package/src/mcp-handler.ts +1 -1
- package/src/mcp-tools.ts +78 -7
- package/src/rpc-handler.ts +0 -1
- package/src/spawn-command.ts +3 -1
- package/src/transports/http-transport.ts +4 -5
- package/test/agent-instructions.test.ts +68 -5
- package/test/fixtures/agent-instructions-snapshot.md +58 -0
package/src/agents/kimi.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class KimiAgent implements AgentTool {
|
|
|
15
15
|
|
|
16
16
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
17
|
const yolo = extraPermissions === "yolo";
|
|
18
|
-
const prompt = followupPrompt ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/kiro.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class Kiro implements AgentTool {
|
|
|
15
15
|
|
|
16
16
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
17
|
const yolo = extraPermissions === "yolo";
|
|
18
|
-
const prompt = followupPrompt ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -14,7 +14,7 @@ export class OpenClawAgent implements AgentTool {
|
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
16
16
|
const yolo = extraPermissions === "yolo";
|
|
17
|
-
const prompt = followupPrompt ??
|
|
17
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
18
18
|
// OpenClaw does not support stdin as prompt.
|
|
19
19
|
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
20
20
|
|
package/src/agents/opencode.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class OpenCodeAgent implements AgentTool {
|
|
|
15
15
|
|
|
16
16
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
17
|
const yolo = extraPermissions === "yolo";
|
|
18
|
-
const prompt = followupPrompt ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["run"];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/qoder.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class Qoder implements AgentTool {
|
|
|
15
15
|
|
|
16
16
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
17
|
const yolo = extraPermissions === "yolo";
|
|
18
|
-
const prompt = followupPrompt ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/qwen.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class QwenAgent implements AgentTool {
|
|
|
15
15
|
|
|
16
16
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
17
|
const yolo = extraPermissions === "yolo";
|
|
18
|
-
const prompt = followupPrompt ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
|
|
20
20
|
|
|
21
21
|
if (followupPrompt) { args.push("-c"); }
|
|
@@ -2,6 +2,8 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
|
+
import { generateEndpointDocs } from "../mcp-tools.js";
|
|
6
|
+
import type { ParsedTask } from "../types.js";
|
|
5
7
|
|
|
6
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
|
|
@@ -11,13 +13,14 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
|
11
13
|
);
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
16
|
+
* Build the full agent prompt: instructions + endpoint docs + task description.
|
|
15
17
|
*/
|
|
16
|
-
export function getAgentInstructions(
|
|
18
|
+
export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
|
|
17
19
|
const port = loadConfig().httpPort ?? 9966;
|
|
20
|
+
const taskDescription = task.body || task.frontmatter.user_prompt;
|
|
18
21
|
let instructions = AGENT_INSTRUCTIONS_TEMPLATE
|
|
19
|
-
.replace(/\{\{
|
|
20
|
-
.replace(/\{\{
|
|
22
|
+
.replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
|
|
23
|
+
.replace(/\{\{TASK_DESCRIPTION\}\}/g, taskDescription);
|
|
21
24
|
if (skipPermissions) {
|
|
22
25
|
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
23
26
|
}
|
package/src/commands/run.ts
CHANGED
|
@@ -65,9 +65,6 @@ async function invokeAgentWithRetries(
|
|
|
65
65
|
const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(
|
|
66
66
|
invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
|
|
67
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)})` : ""}`);
|
|
71
68
|
const result = await spawnCommand(command, args, {
|
|
72
69
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
73
70
|
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
@@ -516,10 +513,10 @@ async function requestConfirmation(
|
|
|
516
513
|
taskDir: string,
|
|
517
514
|
): Promise<boolean> {
|
|
518
515
|
const port = config.httpPort ?? 9966;
|
|
519
|
-
const res = await fetch(`http://localhost:${port}/request-confirmation`, {
|
|
516
|
+
const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
|
|
520
517
|
method: "POST",
|
|
521
518
|
headers: { "Content-Type": "application/json" },
|
|
522
|
-
body: JSON.stringify({
|
|
519
|
+
body: JSON.stringify({ description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
|
|
523
520
|
});
|
|
524
521
|
const body = await res.json() as { confirmed?: boolean; error?: string };
|
|
525
522
|
if (typeof body.confirmed !== "boolean") {
|
package/src/mcp-handler.ts
CHANGED
|
@@ -90,7 +90,7 @@ export async function handleMcpRequest(body: string, sessionId: string | undefin
|
|
|
90
90
|
body: rpcResult(id, {
|
|
91
91
|
tools: agentTools.map((t) => ({
|
|
92
92
|
name: t.name,
|
|
93
|
-
description: t.description,
|
|
93
|
+
description: t.description.join(" "),
|
|
94
94
|
inputSchema: t.inputSchema,
|
|
95
95
|
})),
|
|
96
96
|
}),
|
package/src/mcp-tools.ts
CHANGED
|
@@ -19,14 +19,18 @@ export interface ToolContext {
|
|
|
19
19
|
|
|
20
20
|
export interface ToolDefinition {
|
|
21
21
|
name: string;
|
|
22
|
-
|
|
22
|
+
/** First line is the summary (used as endpoint header). Remaining lines become bullet points in docs. */
|
|
23
|
+
description: string[];
|
|
23
24
|
inputSchema: object;
|
|
24
25
|
handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const notifyTool: ToolDefinition = {
|
|
28
29
|
name: "notify",
|
|
29
|
-
description:
|
|
30
|
+
description: [
|
|
31
|
+
"Send a push notification to the user's device.",
|
|
32
|
+
'Response: `{"ok": true}` on success.',
|
|
33
|
+
],
|
|
30
34
|
inputSchema: {
|
|
31
35
|
type: "object",
|
|
32
36
|
properties: {
|
|
@@ -55,7 +59,12 @@ const notifyTool: ToolDefinition = {
|
|
|
55
59
|
|
|
56
60
|
const requestInputTool: ToolDefinition = {
|
|
57
61
|
name: "request-input",
|
|
58
|
-
description:
|
|
62
|
+
description: [
|
|
63
|
+
"Request input from the user.",
|
|
64
|
+
"The request blocks until the user responds.",
|
|
65
|
+
'Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.',
|
|
66
|
+
"When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this endpoint instead.",
|
|
67
|
+
],
|
|
59
68
|
inputSchema: {
|
|
60
69
|
type: "object",
|
|
61
70
|
properties: {
|
|
@@ -87,18 +96,28 @@ const requestInputTool: ToolDefinition = {
|
|
|
87
96
|
const response = await pendingPromise;
|
|
88
97
|
|
|
89
98
|
if (response.length === 1 && response[0] === "aborted") {
|
|
90
|
-
await ctx.publishEvent("_input", {
|
|
99
|
+
await ctx.publishEvent("_input", {
|
|
100
|
+
event_type: "input-resolved", host_id: ctx.config.hostId,
|
|
101
|
+
session_id: ctx.sessionId, status: "aborted",
|
|
102
|
+
});
|
|
91
103
|
return { aborted: true };
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
await ctx.publishEvent("_input", {
|
|
106
|
+
await ctx.publishEvent("_input", {
|
|
107
|
+
event_type: "input-resolved", host_id: ctx.config.hostId,
|
|
108
|
+
session_id: ctx.sessionId, status: "provided",
|
|
109
|
+
});
|
|
95
110
|
return { values: response };
|
|
96
111
|
},
|
|
97
112
|
};
|
|
98
113
|
|
|
99
114
|
const requestConfirmationTool: ToolDefinition = {
|
|
100
115
|
name: "request-confirmation",
|
|
101
|
-
description:
|
|
116
|
+
description: [
|
|
117
|
+
"Request confirmation from the user.",
|
|
118
|
+
"The request blocks until the user confirms or aborts.",
|
|
119
|
+
'Response: `{"confirmed": true}` or `{"confirmed": false}`.',
|
|
120
|
+
],
|
|
102
121
|
inputSchema: {
|
|
103
122
|
type: "object",
|
|
104
123
|
properties: {
|
|
@@ -136,7 +155,12 @@ const requestConfirmationTool: ToolDefinition = {
|
|
|
136
155
|
|
|
137
156
|
const deviceGeolocationTool: ToolDefinition = {
|
|
138
157
|
name: "device-geolocation",
|
|
139
|
-
description:
|
|
158
|
+
description: [
|
|
159
|
+
"Get the GPS location of the user's mobile device.",
|
|
160
|
+
"When you need the user's real-time location, use this endpoint.",
|
|
161
|
+
"Blocks until the device responds (up to 30 seconds).",
|
|
162
|
+
'Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.',
|
|
163
|
+
],
|
|
140
164
|
inputSchema: {
|
|
141
165
|
type: "object",
|
|
142
166
|
properties: {},
|
|
@@ -180,3 +204,50 @@ const deviceGeolocationTool: ToolDefinition = {
|
|
|
180
204
|
|
|
181
205
|
export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
|
|
182
206
|
export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
|
|
210
|
+
*/
|
|
211
|
+
export function generateEndpointDocs(port: number, taskId: string): string {
|
|
212
|
+
const baseUrl = `http://localhost:${port}`;
|
|
213
|
+
const lines: string[] = [
|
|
214
|
+
`The following HTTP endpoints are available during task execution. Use curl to call them.`,
|
|
215
|
+
"",
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
for (const tool of agentTools) {
|
|
219
|
+
const schema = tool.inputSchema as { properties?: Record<string, { type?: string; description?: string; items?: { type?: string } }>; required?: string[] };
|
|
220
|
+
const props = schema.properties ?? {};
|
|
221
|
+
const required = new Set(schema.required ?? []);
|
|
222
|
+
|
|
223
|
+
// Build example JSON (body only, no taskId)
|
|
224
|
+
const example: Record<string, unknown> = {};
|
|
225
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
226
|
+
if (prop.type === "array") example[key] = ["..."];
|
|
227
|
+
else example[key] = "...";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const queryUrl = `${baseUrl}/${tool.name}?taskId=${taskId}`;
|
|
231
|
+
const [header, ...details] = tool.description;
|
|
232
|
+
|
|
233
|
+
lines.push(`**\`POST ${queryUrl}\`** — ${header}`);
|
|
234
|
+
if (Object.keys(example).length > 0) {
|
|
235
|
+
lines.push("```json");
|
|
236
|
+
lines.push(JSON.stringify(example));
|
|
237
|
+
lines.push("```");
|
|
238
|
+
}
|
|
239
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
240
|
+
const req = required.has(key) ? "required" : "optional";
|
|
241
|
+
let typeStr = prop.type ?? "unknown";
|
|
242
|
+
if (prop.type === "array" && prop.items?.type) typeStr = `${prop.items.type} array`;
|
|
243
|
+
lines.push(`- \`${key}\` (${req}, ${typeStr}): ${prop.description ?? ""}`);
|
|
244
|
+
}
|
|
245
|
+
for (const detail of details) {
|
|
246
|
+
lines.push(`- ${detail}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
lines.push("");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return lines.join("\n").trimEnd();
|
|
253
|
+
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -126,7 +126,6 @@ async function generatePlan(
|
|
|
126
126
|
const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
|
|
127
127
|
const planAgent = getAgent(agentName);
|
|
128
128
|
const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
129
|
-
console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
|
|
130
129
|
|
|
131
130
|
const { output } = await spawnCommand(command, args, {
|
|
132
131
|
cwd: projectRoot,
|
package/src/spawn-command.ts
CHANGED
|
@@ -89,8 +89,10 @@ export function spawnCommand(
|
|
|
89
89
|
const finalArgs = process.platform === "win32"
|
|
90
90
|
? args.map((a) => a.replace(/[\r\n]+/g, " "))
|
|
91
91
|
: args;
|
|
92
|
+
const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "..." : s;
|
|
93
|
+
const displayArgs = finalArgs.map((arg) => truncate(arg));
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
console.log(`[spawn] ${command} ${displayArgs.join(" ")}`);
|
|
94
96
|
|
|
95
97
|
const child = crossSpawn(command, finalArgs, {
|
|
96
98
|
cwd: opts.cwd,
|
|
@@ -195,11 +195,9 @@ export async function startHttpTransport(
|
|
|
195
195
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
196
196
|
const tool = agentToolMap.get(pathname.slice(1))!;
|
|
197
197
|
try {
|
|
198
|
-
const
|
|
199
|
-
const args = body.trim() ? JSON.parse(body) : {};
|
|
200
|
-
const { taskId } = args as { taskId?: string };
|
|
198
|
+
const taskId = url.searchParams.get("taskId");
|
|
201
199
|
if (!taskId) {
|
|
202
|
-
sendJson(res, 400, { error: "taskId is required" });
|
|
200
|
+
sendJson(res, 400, { error: "taskId query parameter is required" });
|
|
203
201
|
return;
|
|
204
202
|
}
|
|
205
203
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
@@ -207,7 +205,8 @@ export async function startHttpTransport(
|
|
|
207
205
|
sendJson(res, 404, { error: `Task not found: ${taskId}` });
|
|
208
206
|
return;
|
|
209
207
|
}
|
|
210
|
-
|
|
208
|
+
const body = await readBody(req);
|
|
209
|
+
const args = body.trim() ? JSON.parse(body) : {};
|
|
211
210
|
const ctx = makeToolContext(taskId);
|
|
212
211
|
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
|
|
213
212
|
const result = await tool.handler(args, ctx);
|
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
+
import { generateEndpointDocs, agentTools } from "../src/mcp-tools.js";
|
|
6
7
|
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const templatePath = path.join(__dirname, "..", "src", "agents", "agent-instructions.md");
|
|
@@ -11,8 +12,8 @@ const template = fs.readFileSync(templatePath, "utf-8");
|
|
|
11
12
|
/** Minimal replica of getAgentInstructions that doesn't need host.json */
|
|
12
13
|
function buildInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
13
14
|
let instructions = template
|
|
14
|
-
.replace(/\{\{
|
|
15
|
-
.replace(/\{\{
|
|
15
|
+
.replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(9966, taskId))
|
|
16
|
+
.replace(/\{\{TASK_DESCRIPTION\}\}/g, "Test task prompt");
|
|
16
17
|
if (skipPermissions) {
|
|
17
18
|
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
18
19
|
}
|
|
@@ -39,10 +40,72 @@ describe("getAgentInstructions", () => {
|
|
|
39
40
|
assert.match(result, /## HTTP Endpoints/);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
it("replaces template variables", () => {
|
|
43
|
+
it("replaces all template variables", () => {
|
|
44
|
+
const result = buildInstructions("my-task-123");
|
|
45
|
+
assert.doesNotMatch(result, /\{\{ENDPOINT_DOCS\}\}/);
|
|
46
|
+
assert.doesNotMatch(result, /\{\{TASK_DESCRIPTION\}\}/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("includes task ID in endpoint examples", () => {
|
|
43
50
|
const result = buildInstructions("my-task-123");
|
|
44
51
|
assert.match(result, /my-task-123/);
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("includes port in endpoint URL", () => {
|
|
55
|
+
const result = buildInstructions("test");
|
|
56
|
+
assert.match(result, /localhost:9966/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("includes task description", () => {
|
|
60
|
+
const result = buildInstructions("test");
|
|
61
|
+
assert.match(result, /Test task prompt/);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("full agent instruction snapshot", () => {
|
|
66
|
+
it("matches the expected full text exactly", () => {
|
|
67
|
+
const result = buildInstructions("test-task-id").replace(/\r\n/g, "\n").trimEnd();
|
|
68
|
+
const snapshotPath = path.join(__dirname, "fixtures", "agent-instructions-snapshot.md");
|
|
69
|
+
const expected = fs.readFileSync(snapshotPath, "utf-8").replace(/\r\n/g, "\n").trimEnd();
|
|
70
|
+
assert.equal(result, expected);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("generateEndpointDocs", () => {
|
|
75
|
+
const docs = generateEndpointDocs(9966, "test-id");
|
|
76
|
+
|
|
77
|
+
it("generates docs for all MCP tools", () => {
|
|
78
|
+
for (const tool of agentTools) {
|
|
79
|
+
assert.match(docs, new RegExp(`POST http://localhost:9966/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("includes taskId parameter for every endpoint", () => {
|
|
84
|
+
const endpointBlocks = docs.split("**`POST");
|
|
85
|
+
// First element is the header, skip it
|
|
86
|
+
for (let i = 1; i < endpointBlocks.length; i++) {
|
|
87
|
+
assert.match(endpointBlocks[i], /taskId/, `Missing taskId in endpoint block ${i}`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("includes port in the header", () => {
|
|
92
|
+
assert.match(docs, /localhost:9966/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("includes task ID in query parameters", () => {
|
|
96
|
+
assert.match(docs, /taskId=test-id/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("includes response descriptions", () => {
|
|
100
|
+
for (const tool of agentTools) {
|
|
101
|
+
if (tool.responseDescription) {
|
|
102
|
+
assert.match(docs, /Response:/, `Missing response description for ${tool.name}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("marks required and optional parameters correctly", () => {
|
|
108
|
+
assert.match(docs, /\(required, string\)/);
|
|
109
|
+
assert.match(docs, /\(optional, string\)/);
|
|
47
110
|
});
|
|
48
111
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
You are an AI agent executing a task on behalf of the user. Follow these instructions carefully.
|
|
2
|
+
|
|
3
|
+
## Reporting Output
|
|
4
|
+
|
|
5
|
+
If you generate report or output files, print each file path on its own line using this exact format:
|
|
6
|
+
[PALMIER_REPORT] <filename>
|
|
7
|
+
|
|
8
|
+
## Completion
|
|
9
|
+
|
|
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]
|
|
13
|
+
|
|
14
|
+
## Permissions
|
|
15
|
+
|
|
16
|
+
Whenever a tool you are trying to use is 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>
|
|
18
|
+
|
|
19
|
+
## HTTP Endpoints
|
|
20
|
+
|
|
21
|
+
The following HTTP endpoints are available during task execution. Use curl to call them.
|
|
22
|
+
|
|
23
|
+
**`POST http://localhost:9966/notify?taskId=test-task-id`** — Send a push notification to the user's device.
|
|
24
|
+
```json
|
|
25
|
+
{"title":"...","body":"..."}
|
|
26
|
+
```
|
|
27
|
+
- `title` (required, string): Notification title
|
|
28
|
+
- `body` (required, string): Notification body
|
|
29
|
+
- Response: `{"ok": true}` on success.
|
|
30
|
+
|
|
31
|
+
**`POST http://localhost:9966/request-input?taskId=test-task-id`** — Request input from the user.
|
|
32
|
+
```json
|
|
33
|
+
{"description":"...","questions":["..."]}
|
|
34
|
+
```
|
|
35
|
+
- `description` (optional, string): Context or heading for the input request
|
|
36
|
+
- `questions` (required, string array): Questions to present to the user
|
|
37
|
+
- The request blocks until the user responds.
|
|
38
|
+
- Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.
|
|
39
|
+
- When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this endpoint instead.
|
|
40
|
+
|
|
41
|
+
**`POST http://localhost:9966/request-confirmation?taskId=test-task-id`** — Request confirmation from the user.
|
|
42
|
+
```json
|
|
43
|
+
{"description":"..."}
|
|
44
|
+
```
|
|
45
|
+
- `description` (required, string): What the user is confirming
|
|
46
|
+
- The request blocks until the user confirms or aborts.
|
|
47
|
+
- Response: `{"confirmed": true}` or `{"confirmed": false}`.
|
|
48
|
+
|
|
49
|
+
**`POST http://localhost:9966/device-geolocation?taskId=test-task-id`** — Get the GPS location of the user's mobile device.
|
|
50
|
+
- When you need the user's real-time location, use this endpoint.
|
|
51
|
+
- Blocks until the device responds (up to 30 seconds).
|
|
52
|
+
- Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.
|
|
53
|
+
|
|
54
|
+
The task to execute follows below:
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
Test task prompt
|