palmier 0.6.7 → 0.6.9
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/agent.d.ts +2 -2
- package/dist/agents/aider.d.ts +1 -1
- package/dist/agents/aider.js +3 -6
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +3 -6
- package/dist/agents/cline.d.ts +1 -1
- package/dist/agents/cline.js +3 -6
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +3 -6
- package/dist/agents/copilot.d.ts +1 -1
- package/dist/agents/copilot.js +3 -6
- package/dist/agents/cursor.d.ts +1 -1
- package/dist/agents/cursor.js +3 -6
- package/dist/agents/deepagents.d.ts +1 -1
- package/dist/agents/deepagents.js +3 -6
- package/dist/agents/droid.d.ts +1 -1
- package/dist/agents/droid.js +3 -6
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +3 -6
- package/dist/agents/goose.d.ts +1 -1
- package/dist/agents/goose.js +3 -6
- package/dist/agents/hermes.d.ts +1 -1
- package/dist/agents/hermes.js +3 -6
- package/dist/agents/kimi.d.ts +1 -1
- package/dist/agents/kimi.js +3 -6
- package/dist/agents/kiro.d.ts +1 -1
- package/dist/agents/kiro.js +3 -6
- package/dist/agents/openclaw.d.ts +1 -1
- package/dist/agents/openclaw.js +3 -6
- package/dist/agents/opencode.d.ts +1 -1
- package/dist/agents/opencode.js +3 -6
- package/dist/agents/qoder.d.ts +1 -1
- package/dist/agents/qoder.js +3 -6
- package/dist/agents/qwen.d.ts +1 -1
- package/dist/agents/qwen.js +3 -6
- package/dist/agents/shared-prompt.d.ts +3 -2
- package/dist/agents/shared-prompt.js +6 -4
- package/dist/commands/run.js +3 -7
- 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-CZejk2al.js} +42 -42
- package/dist/pwa/assets/{web-EzNEHXEh.js → web-C48txJFl.js} +1 -1
- package/dist/pwa/assets/{web-DQteXlI7.js → web-zj8Blync.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +27 -68
- package/dist/spawn-command.js +3 -1
- package/dist/task.js +2 -3
- package/dist/transports/http-transport.js +4 -5
- package/dist/types.d.ts +0 -1
- package/package.json +2 -2
- 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/agent.ts +2 -2
- package/src/agents/aider.ts +3 -6
- package/src/agents/claude.ts +3 -6
- package/src/agents/cline.ts +3 -6
- package/src/agents/codex.ts +3 -6
- package/src/agents/copilot.ts +3 -6
- package/src/agents/cursor.ts +3 -6
- package/src/agents/deepagents.ts +3 -6
- package/src/agents/droid.ts +3 -6
- package/src/agents/gemini.ts +3 -6
- package/src/agents/goose.ts +3 -6
- package/src/agents/hermes.ts +3 -6
- package/src/agents/kimi.ts +3 -6
- package/src/agents/kiro.ts +3 -6
- package/src/agents/openclaw.ts +3 -6
- package/src/agents/opencode.ts +3 -6
- package/src/agents/qoder.ts +3 -6
- package/src/agents/qwen.ts +3 -6
- package/src/agents/shared-prompt.ts +7 -4
- package/src/commands/run.ts +3 -7
- package/src/mcp-handler.ts +1 -1
- package/src/mcp-tools.ts +78 -7
- package/src/rpc-handler.ts +29 -72
- package/src/spawn-command.ts +3 -1
- package/src/task.ts +2 -3
- package/src/transports/http-transport.ts +4 -5
- package/src/types.ts +0 -1
- package/test/agent-instructions.test.ts +137 -9
- package/test/agent-output-parsing.test.ts +1 -0
- package/test/task-parsing.test.ts +3 -3
- package/dist/commands/plan-generation.md +0 -22
- package/src/commands/plan-generation.md +0 -22
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, tools: ToolDefinition[] = agentTools): 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 tools) {
|
|
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
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
4
|
import { spawn, type ChildProcess } from "child_process";
|
|
6
|
-
import { parse as parseYaml } from "yaml";
|
|
7
5
|
import { type NatsConnection } from "nats";
|
|
8
6
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
9
7
|
import { resolvePending, getPending } from "./pending-requests.js";
|
|
@@ -18,13 +16,6 @@ import { currentVersion, performUpdate } from "./update-checker.js";
|
|
|
18
16
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
19
17
|
import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
|
|
20
18
|
|
|
21
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
|
|
23
|
-
const PLAN_GENERATION_PROMPT = fs.readFileSync(
|
|
24
|
-
path.join(__dirname, "commands", "plan-generation.md"),
|
|
25
|
-
"utf-8",
|
|
26
|
-
);
|
|
27
|
-
|
|
28
19
|
/**
|
|
29
20
|
* Parse RESULT frontmatter and conversation messages.
|
|
30
21
|
*/
|
|
@@ -115,40 +106,30 @@ function parseAttr(attrs: string, name: string): string | undefined {
|
|
|
115
106
|
}
|
|
116
107
|
|
|
117
108
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
109
|
+
* Generate a concise task name from a user prompt using the given agent.
|
|
110
|
+
* Falls back to the raw prompt on failure.
|
|
120
111
|
*/
|
|
121
|
-
async function
|
|
112
|
+
async function generateName(
|
|
122
113
|
projectRoot: string,
|
|
123
114
|
userPrompt: string,
|
|
124
115
|
agentName: string,
|
|
125
|
-
): Promise<
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const { command, args, stdin, env: agentEnv } =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const fmMatch = trimmed.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
142
|
-
if (fmMatch) {
|
|
143
|
-
try {
|
|
144
|
-
const fm = parseYaml(fmMatch[1]) as { task_name?: string };
|
|
145
|
-
name = fm.task_name ?? "";
|
|
146
|
-
} catch {
|
|
147
|
-
// If frontmatter parsing fails, treat entire output as body
|
|
148
|
-
}
|
|
149
|
-
body = fmMatch[2].trimStart();
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
const prompt = `Generate a concise 3-6 word name for this task. Reply with ONLY the name, nothing else.\n\nTask: ${userPrompt}`;
|
|
118
|
+
const agent = getAgent(agentName);
|
|
119
|
+
const { command, args, stdin, env: agentEnv } = agent.getPromptCommandLine(prompt);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const { output } = await spawnCommand(command, args, {
|
|
123
|
+
cwd: projectRoot,
|
|
124
|
+
timeout: 30_000,
|
|
125
|
+
stdin,
|
|
126
|
+
...(agentEnv ? { env: agentEnv } : {}),
|
|
127
|
+
});
|
|
128
|
+
const name = output.trim().replace(/^["']|["']$/g, "").slice(0, 80);
|
|
129
|
+
return name || userPrompt;
|
|
130
|
+
} catch {
|
|
131
|
+
return userPrompt;
|
|
150
132
|
}
|
|
151
|
-
return { name, body };
|
|
152
133
|
}
|
|
153
134
|
|
|
154
135
|
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
@@ -164,7 +145,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
164
145
|
const pending = getPending(task.frontmatter.id);
|
|
165
146
|
return {
|
|
166
147
|
...task.frontmatter,
|
|
167
|
-
body: task.body,
|
|
168
148
|
status: status ? {
|
|
169
149
|
...status,
|
|
170
150
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
@@ -216,25 +196,13 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
216
196
|
command?: string;
|
|
217
197
|
};
|
|
218
198
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (params.user_prompt.length <= 50) {
|
|
223
|
-
name = params.user_prompt;
|
|
224
|
-
} else {
|
|
225
|
-
try {
|
|
226
|
-
const plan = await generatePlan(config.projectRoot, params.user_prompt, params.agent);
|
|
227
|
-
name = plan.name;
|
|
228
|
-
body = plan.body;
|
|
229
|
-
} catch (err: unknown) {
|
|
230
|
-
const error = err as { stdout?: string; stderr?: string };
|
|
231
|
-
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
199
|
+
const name = params.user_prompt.length <= 50
|
|
200
|
+
? params.user_prompt
|
|
201
|
+
: await generateName(config.projectRoot, params.user_prompt, params.agent);
|
|
234
202
|
|
|
235
203
|
const id = randomUUID();
|
|
236
204
|
const taskDir = getTaskDir(config.projectRoot, id);
|
|
237
|
-
const task = {
|
|
205
|
+
const task: ParsedTask = {
|
|
238
206
|
frontmatter: {
|
|
239
207
|
id,
|
|
240
208
|
name,
|
|
@@ -247,7 +215,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
247
215
|
...(params.foreground_mode ? { foreground_mode: true } : {}),
|
|
248
216
|
...(params.command ? { command: params.command } : {}),
|
|
249
217
|
},
|
|
250
|
-
body,
|
|
251
218
|
};
|
|
252
219
|
|
|
253
220
|
writeTaskFile(taskDir, task);
|
|
@@ -273,10 +240,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
273
240
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
274
241
|
const existing = parseTaskFile(taskDir);
|
|
275
242
|
|
|
276
|
-
// Detect whether
|
|
243
|
+
// Detect whether name needs regeneration
|
|
277
244
|
const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
|
|
278
245
|
const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
|
|
279
|
-
const needsRegeneration = promptChanged || agentChanged || !existing.body;
|
|
280
246
|
|
|
281
247
|
// Merge updates
|
|
282
248
|
if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
|
|
@@ -298,19 +264,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
298
264
|
}
|
|
299
265
|
}
|
|
300
266
|
|
|
301
|
-
// Regenerate
|
|
302
|
-
if (
|
|
303
|
-
existing.frontmatter.name = existing.frontmatter.user_prompt
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
308
|
-
existing.frontmatter.name = plan.name;
|
|
309
|
-
existing.body = plan.body;
|
|
310
|
-
} catch (err: unknown) {
|
|
311
|
-
const error = err as { stdout?: string; stderr?: string };
|
|
312
|
-
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
313
|
-
}
|
|
267
|
+
// Regenerate name when prompt or agent changes
|
|
268
|
+
if (promptChanged || agentChanged) {
|
|
269
|
+
existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
|
|
270
|
+
? existing.frontmatter.user_prompt
|
|
271
|
+
: await generateName(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
314
272
|
}
|
|
315
273
|
|
|
316
274
|
writeTaskFile(taskDir, existing);
|
|
@@ -357,7 +315,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
357
315
|
...(params.foreground_mode ? { foreground_mode: true } : {}),
|
|
358
316
|
...(params.command ? { command: params.command } : {}),
|
|
359
317
|
},
|
|
360
|
-
body: "",
|
|
361
318
|
};
|
|
362
319
|
|
|
363
320
|
writeTaskFile(taskDir, task);
|
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,
|
package/src/task.ts
CHANGED
|
@@ -29,7 +29,6 @@ export function parseTaskContent(content: string): ParsedTask {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const frontmatter = parseYaml(match[1]) as TaskFrontmatter;
|
|
32
|
-
const body = (match[2] || "").trim();
|
|
33
32
|
|
|
34
33
|
if (!frontmatter.id) {
|
|
35
34
|
throw new Error("TASK.md frontmatter must include at least: id");
|
|
@@ -39,7 +38,7 @@ export function parseTaskContent(content: string): ParsedTask {
|
|
|
39
38
|
frontmatter.agent ??= "claude";
|
|
40
39
|
frontmatter.triggers_enabled ??= true;
|
|
41
40
|
|
|
42
|
-
return { frontmatter
|
|
41
|
+
return { frontmatter };
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/**
|
|
@@ -50,7 +49,7 @@ export function writeTaskFile(taskDir: string, task: ParsedTask): void {
|
|
|
50
49
|
fs.mkdirSync(taskDir, { recursive: true });
|
|
51
50
|
|
|
52
51
|
const yamlStr = stringifyYaml(task.frontmatter).trim();
|
|
53
|
-
const content = `---\n${yamlStr}\n---\n
|
|
52
|
+
const content = `---\n${yamlStr}\n---\n`;
|
|
54
53
|
|
|
55
54
|
const filePath = path.join(taskDir, "TASK.md");
|
|
56
55
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
@@ -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);
|
package/src/types.ts
CHANGED
|
@@ -3,17 +3,57 @@ 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, type ToolDefinition } 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");
|
|
9
10
|
const template = fs.readFileSync(templatePath, "utf-8");
|
|
10
11
|
|
|
12
|
+
/** Mock tools with a known, stable shape for testing */
|
|
13
|
+
const mockTools: ToolDefinition[] = [
|
|
14
|
+
{
|
|
15
|
+
name: "mock-action",
|
|
16
|
+
description: [
|
|
17
|
+
"Perform a mock action.",
|
|
18
|
+
'Response: `{"ok": true}` on success.',
|
|
19
|
+
],
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
title: { type: "string", description: "Action title" },
|
|
24
|
+
detail: { type: "string", description: "Optional detail" },
|
|
25
|
+
},
|
|
26
|
+
required: ["title"],
|
|
27
|
+
},
|
|
28
|
+
handler: async () => ({ ok: true }),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "mock-query",
|
|
32
|
+
description: [
|
|
33
|
+
"Query mock data from the device.",
|
|
34
|
+
"Blocks until the device responds.",
|
|
35
|
+
'Response: `{"data": ...}` on success.',
|
|
36
|
+
],
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
tags: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: { type: "string" },
|
|
43
|
+
description: "Filter tags",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
handler: async () => ({ data: [] }),
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
11
51
|
/** Minimal replica of getAgentInstructions that doesn't need host.json */
|
|
12
|
-
function buildInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
52
|
+
function buildInstructions(taskId: string, opts?: { skipPermissions?: boolean }): string {
|
|
13
53
|
let instructions = template
|
|
14
|
-
.replace(/\{\{
|
|
15
|
-
.replace(/\{\{
|
|
16
|
-
if (skipPermissions) {
|
|
54
|
+
.replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(9966, taskId, mockTools))
|
|
55
|
+
.replace(/\{\{TASK_DESCRIPTION\}\}/g, "Test task prompt");
|
|
56
|
+
if (opts?.skipPermissions) {
|
|
17
57
|
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
18
58
|
}
|
|
19
59
|
return instructions;
|
|
@@ -27,22 +67,110 @@ describe("getAgentInstructions", () => {
|
|
|
27
67
|
});
|
|
28
68
|
|
|
29
69
|
it("strips Permissions section when skipPermissions is true", () => {
|
|
30
|
-
const result = buildInstructions("test-task-id", true);
|
|
70
|
+
const result = buildInstructions("test-task-id", { skipPermissions: true });
|
|
31
71
|
assert.doesNotMatch(result, /## Permissions/);
|
|
32
72
|
assert.doesNotMatch(result, /PALMIER_PERMISSION/);
|
|
33
73
|
});
|
|
34
74
|
|
|
35
75
|
it("preserves other sections when Permissions is stripped", () => {
|
|
36
|
-
const result = buildInstructions("test-task-id", true);
|
|
76
|
+
const result = buildInstructions("test-task-id", { skipPermissions: true });
|
|
37
77
|
assert.match(result, /## Reporting Output/);
|
|
38
78
|
assert.match(result, /## Completion/);
|
|
39
79
|
assert.match(result, /## HTTP Endpoints/);
|
|
40
80
|
});
|
|
41
81
|
|
|
42
|
-
it("replaces template variables", () => {
|
|
82
|
+
it("replaces all template variables", () => {
|
|
83
|
+
const result = buildInstructions("my-task-123");
|
|
84
|
+
assert.doesNotMatch(result, /\{\{ENDPOINT_DOCS\}\}/);
|
|
85
|
+
assert.doesNotMatch(result, /\{\{TASK_DESCRIPTION\}\}/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("includes task ID in endpoint examples", () => {
|
|
43
89
|
const result = buildInstructions("my-task-123");
|
|
44
90
|
assert.match(result, /my-task-123/);
|
|
45
|
-
|
|
46
|
-
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("includes port in endpoint URL", () => {
|
|
94
|
+
const result = buildInstructions("test");
|
|
95
|
+
assert.match(result, /localhost:9966/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("includes task description", () => {
|
|
99
|
+
const result = buildInstructions("test");
|
|
100
|
+
assert.match(result, /Test task prompt/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
describe("generateEndpointDocs", () => {
|
|
107
|
+
const docs = generateEndpointDocs(9966, "test-id", mockTools);
|
|
108
|
+
|
|
109
|
+
it("matches expected full output", () => {
|
|
110
|
+
const expected = [
|
|
111
|
+
"The following HTTP endpoints are available during task execution. Use curl to call them.",
|
|
112
|
+
"",
|
|
113
|
+
"**`POST http://localhost:9966/mock-action?taskId=test-id`** — Perform a mock action.",
|
|
114
|
+
"```json",
|
|
115
|
+
'{"title":"...","detail":"..."}',
|
|
116
|
+
"```",
|
|
117
|
+
"- `title` (required, string): Action title",
|
|
118
|
+
"- `detail` (optional, string): Optional detail",
|
|
119
|
+
'- Response: `{"ok": true}` on success.',
|
|
120
|
+
"",
|
|
121
|
+
"**`POST http://localhost:9966/mock-query?taskId=test-id`** — Query mock data from the device.",
|
|
122
|
+
"```json",
|
|
123
|
+
'{"tags":["..."]}',
|
|
124
|
+
"```",
|
|
125
|
+
"- `tags` (optional, string array): Filter tags",
|
|
126
|
+
"- Blocks until the device responds.",
|
|
127
|
+
'- Response: `{"data": ...}` on success.',
|
|
128
|
+
].join("\n");
|
|
129
|
+
assert.equal(docs, expected);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("generates docs for all provided tools", () => {
|
|
133
|
+
for (const tool of mockTools) {
|
|
134
|
+
assert.match(docs, new RegExp(`POST http://localhost:9966/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("includes taskId parameter for every endpoint", () => {
|
|
139
|
+
const endpointBlocks = docs.split("**`POST");
|
|
140
|
+
// First element is the header, skip it
|
|
141
|
+
for (let i = 1; i < endpointBlocks.length; i++) {
|
|
142
|
+
assert.match(endpointBlocks[i], /taskId/, `Missing taskId in endpoint block ${i}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("includes port in the header", () => {
|
|
147
|
+
assert.match(docs, /localhost:9966/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("includes task ID in query parameters", () => {
|
|
151
|
+
assert.match(docs, /taskId=test-id/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("includes response descriptions", () => {
|
|
155
|
+
for (const tool of mockTools) {
|
|
156
|
+
if (tool.description.length > 1) {
|
|
157
|
+
assert.match(docs, /Response:/, `Missing response description for ${tool.name}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("marks required and optional parameters correctly", () => {
|
|
163
|
+
assert.match(docs, /\(required, string\)/);
|
|
164
|
+
// "detail" has no required entry, so it should be optional
|
|
165
|
+
assert.match(docs, /\(optional, string\)/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("handles array-type parameters", () => {
|
|
169
|
+
assert.match(docs, /\(optional, string array\)/);
|
|
170
|
+
assert.match(docs, /Filter tags/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("renders multi-line descriptions as bullet points", () => {
|
|
174
|
+
assert.match(docs, /- Blocks until the device responds\./);
|
|
47
175
|
});
|
|
48
176
|
});
|
|
@@ -19,7 +19,7 @@ This is the task body.`;
|
|
|
19
19
|
assert.equal(result.frontmatter.id, "abc123");
|
|
20
20
|
assert.equal(result.frontmatter.name, "Test Task");
|
|
21
21
|
assert.equal(result.frontmatter.agent, "claude");
|
|
22
|
-
assert.equal(result.
|
|
22
|
+
assert.equal(result.frontmatter.user_prompt, "Do something");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it("defaults agent to claude when not specified", () => {
|
|
@@ -68,7 +68,7 @@ requires_confirmation: false
|
|
|
68
68
|
assert.throws(() => parseTaskContent("---\nname: test\n---\n"), /must include at least: id/);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it("handles empty body", () => {
|
|
71
|
+
it("handles empty body gracefully", () => {
|
|
72
72
|
const content = `---
|
|
73
73
|
id: abc123
|
|
74
74
|
user_prompt: test
|
|
@@ -78,6 +78,6 @@ requires_confirmation: false
|
|
|
78
78
|
---`;
|
|
79
79
|
|
|
80
80
|
const result = parseTaskContent(content);
|
|
81
|
-
assert.equal(result.
|
|
81
|
+
assert.equal(result.frontmatter.id, "abc123");
|
|
82
82
|
});
|
|
83
83
|
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
You are a task planning assistant. Given a task description, produce a Markdown execution plan for an AI agent to follow. Do not execute any part of the plan yourself.
|
|
2
|
-
|
|
3
|
-
## Output Format
|
|
4
|
-
|
|
5
|
-
Start with a YAML frontmatter block (no code fences), then the plan body:
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
task_name: <concise label, 3-6 words>
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
<plan body>
|
|
12
|
-
|
|
13
|
-
## Plan Body Guidelines
|
|
14
|
-
|
|
15
|
-
- Write a numbered sequence of concrete, actionable steps.
|
|
16
|
-
- If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
|
|
17
|
-
- When a step requires user input, simply state what information is needed from the user. Do **not** specify how to obtain it — the agent has its own tool for requesting user input.
|
|
18
|
-
- Preserve relative time expressions (e.g., "today", "yesterday", "last week") exactly as written — do **not** resolve them to specific dates. The plan may be executed on a different day than it was generated.
|
|
19
|
-
- If the task involves opening a web browser or application, include a final step to close it before finishing.
|
|
20
|
-
|
|
21
|
-
## Task Description
|
|
22
|
-
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
You are a task planning assistant. Given a task description, produce a Markdown execution plan for an AI agent to follow. Do not execute any part of the plan yourself.
|
|
2
|
-
|
|
3
|
-
## Output Format
|
|
4
|
-
|
|
5
|
-
Start with a YAML frontmatter block (no code fences), then the plan body:
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
task_name: <concise label, 3-6 words>
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
<plan body>
|
|
12
|
-
|
|
13
|
-
## Plan Body Guidelines
|
|
14
|
-
|
|
15
|
-
- Write a numbered sequence of concrete, actionable steps.
|
|
16
|
-
- If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
|
|
17
|
-
- When a step requires user input, simply state what information is needed from the user. Do **not** specify how to obtain it — the agent has its own tool for requesting user input.
|
|
18
|
-
- Preserve relative time expressions (e.g., "today", "yesterday", "last week") exactly as written — do **not** resolve them to specific dates. The plan may be executed on a different day than it was generated.
|
|
19
|
-
- If the task involves opening a web browser or application, include a final step to close it before finishing.
|
|
20
|
-
|
|
21
|
-
## Task Description
|
|
22
|
-
|