palmier 0.4.2 → 0.4.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 +13 -27
- package/dist/agents/agent-instructions.md +40 -0
- package/dist/agents/claude.js +2 -8
- package/dist/agents/codex.js +0 -6
- package/dist/agents/copilot.js +0 -20
- package/dist/agents/gemini.js +0 -6
- package/dist/agents/shared-prompt.d.ts +1 -2
- package/dist/agents/shared-prompt.js +5 -18
- package/dist/commands/notify.d.ts +9 -0
- package/dist/commands/notify.js +43 -0
- package/dist/commands/request-input.d.ts +11 -0
- package/dist/commands/request-input.js +63 -0
- package/dist/commands/run.d.ts +0 -5
- package/dist/commands/run.js +67 -72
- package/dist/commands/serve.js +7 -2
- package/dist/index.js +15 -5
- package/dist/platform/windows.js +17 -2
- package/dist/rpc-handler.js +41 -25
- package/dist/spawn-command.d.ts +1 -1
- package/dist/spawn-command.js +13 -1
- package/dist/task.d.ts +12 -1
- package/dist/task.js +36 -1
- package/dist/types.d.ts +9 -0
- package/package.json +2 -3
- package/src/agents/agent-instructions.md +40 -0
- package/src/agents/claude.ts +2 -7
- package/src/agents/codex.ts +0 -5
- package/src/agents/copilot.ts +0 -19
- package/src/agents/gemini.ts +0 -5
- package/src/agents/shared-prompt.ts +10 -18
- package/src/commands/notify.ts +44 -0
- package/src/commands/request-input.ts +65 -0
- package/src/commands/run.ts +78 -96
- package/src/commands/serve.ts +7 -2
- package/src/index.ts +16 -5
- package/src/platform/windows.ts +17 -2
- package/src/rpc-handler.ts +50 -24
- package/src/spawn-command.ts +13 -2
- package/src/task.ts +47 -2
- package/src/types.ts +10 -0
- package/dist/commands/mcpserver.d.ts +0 -2
- package/dist/commands/mcpserver.js +0 -93
- package/src/commands/mcpserver.ts +0 -113
package/dist/commands/serve.js
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
6
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
7
|
-
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } from "../task.js";
|
|
7
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile, appendResultMessage } from "../task.js";
|
|
8
8
|
import { publishHostEvent } from "../events.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { detectAgents } from "../agents/agent.js";
|
|
@@ -31,8 +31,13 @@ async function markTaskFailed(config, nc, taskId, reason) {
|
|
|
31
31
|
}
|
|
32
32
|
catch { /* use taskId as fallback */ }
|
|
33
33
|
const resultFileName = `RESULT-${endTime}.md`;
|
|
34
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n
|
|
34
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n\n`;
|
|
35
35
|
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
36
|
+
appendResultMessage(taskDir, resultFileName, {
|
|
37
|
+
role: "assistant",
|
|
38
|
+
time: endTime,
|
|
39
|
+
content: reason,
|
|
40
|
+
});
|
|
36
41
|
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
37
42
|
const payload = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
38
43
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import { initCommand } from "./commands/init.js";
|
|
|
8
8
|
import { infoCommand } from "./commands/info.js";
|
|
9
9
|
import { runCommand } from "./commands/run.js";
|
|
10
10
|
import { serveCommand } from "./commands/serve.js";
|
|
11
|
-
import {
|
|
11
|
+
import { notifyCommand } from "./commands/notify.js";
|
|
12
|
+
import { requestInputCommand } from "./commands/request-input.js";
|
|
12
13
|
import { pairCommand } from "./commands/pair.js";
|
|
13
14
|
import { lanCommand } from "./commands/lan.js";
|
|
14
15
|
import { restartCommand } from "./commands/restart.js";
|
|
@@ -51,10 +52,19 @@ program
|
|
|
51
52
|
await restartCommand();
|
|
52
53
|
});
|
|
53
54
|
program
|
|
54
|
-
.command("
|
|
55
|
-
.description("
|
|
56
|
-
.
|
|
57
|
-
|
|
55
|
+
.command("notify")
|
|
56
|
+
.description("Send a push notification to the user")
|
|
57
|
+
.requiredOption("--title <title>", "Notification title")
|
|
58
|
+
.requiredOption("--body <body>", "Notification body text")
|
|
59
|
+
.action(async (opts) => {
|
|
60
|
+
await notifyCommand(opts);
|
|
61
|
+
});
|
|
62
|
+
program
|
|
63
|
+
.command("request-input")
|
|
64
|
+
.description("Request input from the user (requires PALMIER_TASK_ID env var)")
|
|
65
|
+
.requiredOption("--description <desc...>", "Input descriptions to show the user")
|
|
66
|
+
.action(async (opts) => {
|
|
67
|
+
await requestInputCommand(opts);
|
|
58
68
|
});
|
|
59
69
|
program
|
|
60
70
|
.command("pair")
|
package/dist/platform/windows.js
CHANGED
|
@@ -2,7 +2,8 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
4
|
import { spawn as nodeSpawn } from "child_process";
|
|
5
|
-
import { CONFIG_DIR } from "../config.js";
|
|
5
|
+
import { CONFIG_DIR, loadConfig } from "../config.js";
|
|
6
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
6
7
|
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
7
8
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
8
9
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
@@ -118,7 +119,7 @@ export class WindowsPlatform {
|
|
|
118
119
|
// Kill old daemon first, then spawn new one.
|
|
119
120
|
if (oldPid) {
|
|
120
121
|
try {
|
|
121
|
-
execFileSync("taskkill", ["/pid", oldPid, "/f"], { windowsHide: true, stdio: "pipe" });
|
|
122
|
+
execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
122
123
|
}
|
|
123
124
|
catch {
|
|
124
125
|
// Process may have already exited
|
|
@@ -203,6 +204,20 @@ export class WindowsPlatform {
|
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
async stopTask(taskId) {
|
|
207
|
+
// Try to kill the entire process tree via the PID recorded in status.json.
|
|
208
|
+
// schtasks /end only kills the top-level process, leaving agent children orphaned.
|
|
209
|
+
try {
|
|
210
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
211
|
+
const status = readTaskStatus(taskDir);
|
|
212
|
+
if (status?.pid) {
|
|
213
|
+
execFileSync("taskkill", ["/pid", String(status.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// PID may be stale or config unavailable; fall through to schtasks /end
|
|
219
|
+
}
|
|
220
|
+
// Fallback: schtasks /end (kills top-level process only)
|
|
206
221
|
const tn = schtasksTaskName(taskId);
|
|
207
222
|
try {
|
|
208
223
|
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
package/dist/rpc-handler.js
CHANGED
|
@@ -14,47 +14,63 @@ import { currentVersion, performUpdate } from "./update-checker.js";
|
|
|
14
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
16
16
|
/**
|
|
17
|
-
* Parse RESULT frontmatter
|
|
17
|
+
* Parse RESULT frontmatter and conversation messages.
|
|
18
18
|
*/
|
|
19
19
|
function parseResultFrontmatter(raw) {
|
|
20
20
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
21
21
|
if (!fmMatch)
|
|
22
|
-
return {
|
|
22
|
+
return { messages: [] };
|
|
23
23
|
const meta = {};
|
|
24
|
-
const requiredPermissions = [];
|
|
25
24
|
for (const line of fmMatch[1].split("\n")) {
|
|
26
25
|
const sep = line.indexOf(": ");
|
|
27
26
|
if (sep === -1)
|
|
28
27
|
continue;
|
|
29
|
-
|
|
30
|
-
const value = line.slice(sep + 2).trim();
|
|
31
|
-
if (key === "required_permission") {
|
|
32
|
-
const pipeSep = value.indexOf("|");
|
|
33
|
-
if (pipeSep !== -1) {
|
|
34
|
-
requiredPermissions.push({ name: value.slice(0, pipeSep).trim(), description: value.slice(pipeSep + 1).trim() });
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
requiredPermissions.push({ name: value, description: "" });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
meta[key] = value;
|
|
42
|
-
}
|
|
28
|
+
meta[line.slice(0, sep).trim()] = line.slice(sep + 2).trim();
|
|
43
29
|
}
|
|
44
|
-
const
|
|
45
|
-
? meta.report_files.split(",").map((f) => f.trim()).filter(Boolean)
|
|
46
|
-
: [];
|
|
30
|
+
const messages = parseConversationMessages(fmMatch[2]);
|
|
47
31
|
return {
|
|
48
|
-
|
|
32
|
+
messages,
|
|
49
33
|
task_name: meta.task_name,
|
|
50
34
|
running_state: meta.running_state,
|
|
51
35
|
start_time: meta.start_time ? Number(meta.start_time) : undefined,
|
|
52
36
|
end_time: meta.end_time ? Number(meta.end_time) : undefined,
|
|
53
37
|
task_file: meta.task_file,
|
|
54
|
-
report_files: reportFiles.length > 0 ? reportFiles : undefined,
|
|
55
|
-
required_permissions: requiredPermissions.length > 0 ? requiredPermissions : undefined,
|
|
56
38
|
};
|
|
57
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse conversation messages from the body of a RESULT file.
|
|
42
|
+
*/
|
|
43
|
+
function parseConversationMessages(body) {
|
|
44
|
+
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
45
|
+
const messages = [];
|
|
46
|
+
const matches = [...body.matchAll(delimiterRegex)];
|
|
47
|
+
if (matches.length === 0) {
|
|
48
|
+
// No delimiters — treat entire body as single assistant message if non-empty
|
|
49
|
+
const content = body.trim();
|
|
50
|
+
if (content) {
|
|
51
|
+
messages.push({ role: "assistant", time: 0, content });
|
|
52
|
+
}
|
|
53
|
+
return messages;
|
|
54
|
+
}
|
|
55
|
+
for (let i = 0; i < matches.length; i++) {
|
|
56
|
+
const match = matches[i];
|
|
57
|
+
const attrs = match[1];
|
|
58
|
+
const start = match.index + match[0].length;
|
|
59
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
|
|
60
|
+
const content = body.slice(start, end).trim();
|
|
61
|
+
const role = (parseAttr(attrs, "role") ?? "assistant");
|
|
62
|
+
const time = Number(parseAttr(attrs, "time") ?? "0");
|
|
63
|
+
const type = parseAttr(attrs, "type");
|
|
64
|
+
const attachmentsRaw = parseAttr(attrs, "attachments");
|
|
65
|
+
const attachments = attachmentsRaw ? attachmentsRaw.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
66
|
+
messages.push({ role, time, content, ...(type ? { type } : {}), ...(attachments ? { attachments } : {}) });
|
|
67
|
+
}
|
|
68
|
+
return messages;
|
|
69
|
+
}
|
|
70
|
+
function parseAttr(attrs, name) {
|
|
71
|
+
const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
|
|
72
|
+
return match ? match[1] : undefined;
|
|
73
|
+
}
|
|
58
74
|
/**
|
|
59
75
|
* Run plan generation for a task prompt using the given agent.
|
|
60
76
|
* Returns the generated plan body and task name.
|
|
@@ -351,8 +367,8 @@ export function createRpcHandler(config, nc) {
|
|
|
351
367
|
try {
|
|
352
368
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
353
369
|
const meta = parseResultFrontmatter(raw);
|
|
354
|
-
// Exclude
|
|
355
|
-
const {
|
|
370
|
+
// Exclude messages from list response
|
|
371
|
+
const { messages: _, ...rest } = meta;
|
|
356
372
|
return { ...entry, ...rest };
|
|
357
373
|
}
|
|
358
374
|
catch {
|
package/dist/spawn-command.d.ts
CHANGED
package/dist/spawn-command.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import crossSpawn from "cross-spawn";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
/** Kill a child process and its entire tree on Windows; plain kill elsewhere. */
|
|
4
|
+
function treeKill(child) {
|
|
5
|
+
if (process.platform === "win32" && child.pid) {
|
|
6
|
+
try {
|
|
7
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
catch { /* fall through */ }
|
|
11
|
+
}
|
|
12
|
+
child.kill();
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* Spawn a command with shell interpretation, returning the ChildProcess
|
|
4
16
|
* with stdout piped for line-by-line reading.
|
|
@@ -50,7 +62,7 @@ export function spawnCommand(command, args, opts) {
|
|
|
50
62
|
let timer;
|
|
51
63
|
if (opts.timeout) {
|
|
52
64
|
timer = setTimeout(() => {
|
|
53
|
-
child
|
|
65
|
+
treeKill(child);
|
|
54
66
|
reject(new Error("command timed out"));
|
|
55
67
|
}, opts.timeout);
|
|
56
68
|
}
|
package/dist/task.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ParsedTask, TaskStatus, HistoryEntry } from "./types.js";
|
|
1
|
+
import type { ParsedTask, TaskStatus, HistoryEntry, ConversationMessage } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Parse a TASK.md file from the given task directory.
|
|
4
4
|
*/
|
|
@@ -44,6 +44,17 @@ export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
|
|
|
44
44
|
* Returns the result file name.
|
|
45
45
|
*/
|
|
46
46
|
export declare function createResultFile(taskDir: string, taskName: string, startTime: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Append a conversation message to a RESULT file.
|
|
49
|
+
*/
|
|
50
|
+
export declare function appendResultMessage(taskDir: string, resultFile: string, msg: ConversationMessage): void;
|
|
51
|
+
/**
|
|
52
|
+
* Update frontmatter fields in a RESULT file without touching the body.
|
|
53
|
+
*/
|
|
54
|
+
export declare function finalizeResultFrontmatter(taskDir: string, resultFile: string, updates: {
|
|
55
|
+
end_time?: number;
|
|
56
|
+
running_state?: string;
|
|
57
|
+
}): void;
|
|
47
58
|
/**
|
|
48
59
|
* Append a history entry to the project-level history.jsonl file.
|
|
49
60
|
*/
|
package/dist/task.js
CHANGED
|
@@ -137,10 +137,45 @@ export function readTaskStatus(taskDir) {
|
|
|
137
137
|
export function createResultFile(taskDir, taskName, startTime) {
|
|
138
138
|
const resultFileName = `RESULT-${startTime}.md`;
|
|
139
139
|
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
140
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: started\nstart_time: ${startTime}\ntask_file: ${taskSnapshotName}\n---\n`;
|
|
140
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: started\nstart_time: ${startTime}\ntask_file: ${taskSnapshotName}\n---\n\n`;
|
|
141
141
|
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
142
142
|
return resultFileName;
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Append a conversation message to a RESULT file.
|
|
146
|
+
*/
|
|
147
|
+
export function appendResultMessage(taskDir, resultFile, msg) {
|
|
148
|
+
const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
|
|
149
|
+
if (msg.type)
|
|
150
|
+
attrs.push(`type="${msg.type}"`);
|
|
151
|
+
if (msg.attachments?.length)
|
|
152
|
+
attrs.push(`attachments="${msg.attachments.join(",")}"`);
|
|
153
|
+
const delimiter = `<!-- palmier:message ${attrs.join(" ")} -->`;
|
|
154
|
+
const entry = `${delimiter}\n\n${msg.content}\n\n`;
|
|
155
|
+
fs.appendFileSync(path.join(taskDir, resultFile), entry, "utf-8");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Update frontmatter fields in a RESULT file without touching the body.
|
|
159
|
+
*/
|
|
160
|
+
export function finalizeResultFrontmatter(taskDir, resultFile, updates) {
|
|
161
|
+
const filePath = path.join(taskDir, resultFile);
|
|
162
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
163
|
+
const fmEnd = raw.indexOf("\n---\n", 4); // skip opening ---
|
|
164
|
+
if (fmEnd === -1)
|
|
165
|
+
return;
|
|
166
|
+
let frontmatter = raw.slice(0, fmEnd);
|
|
167
|
+
const body = raw.slice(fmEnd);
|
|
168
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
169
|
+
const regex = new RegExp(`^${key}:.*$`, "m");
|
|
170
|
+
if (regex.test(frontmatter)) {
|
|
171
|
+
frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
frontmatter += `\n${key}: ${value}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(filePath, frontmatter + body, "utf-8");
|
|
178
|
+
}
|
|
144
179
|
/**
|
|
145
180
|
* Append a history entry to the project-level history.jsonl file.
|
|
146
181
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
|
48
48
|
export interface TaskStatus {
|
|
49
49
|
running_state: TaskRunningState;
|
|
50
50
|
time_stamp: number;
|
|
51
|
+
/** PID of the palmier run process (used on Windows to kill the process tree). */
|
|
52
|
+
pid?: number;
|
|
51
53
|
/** Set when the task has `requires_confirmation` and is awaiting user approval. */
|
|
52
54
|
pending_confirmation?: boolean;
|
|
53
55
|
/** Set when the agent requests permissions not yet granted. Contains the permissions needed. */
|
|
@@ -65,6 +67,13 @@ export interface RequiredPermission {
|
|
|
65
67
|
name: string;
|
|
66
68
|
description: string;
|
|
67
69
|
}
|
|
70
|
+
export interface ConversationMessage {
|
|
71
|
+
role: "assistant" | "user" | "status";
|
|
72
|
+
time: number;
|
|
73
|
+
content: string;
|
|
74
|
+
type?: "input" | "permission" | "confirmation" | "started" | "finished" | "failed" | "aborted";
|
|
75
|
+
attachments?: string[];
|
|
76
|
+
}
|
|
68
77
|
export interface RpcMessage {
|
|
69
78
|
method: string;
|
|
70
79
|
params: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palmier",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hongxu Cai",
|
|
@@ -20,13 +20,12 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"dev": "tsx src/index.ts",
|
|
23
|
-
"build": "tsc && node -e \"require('fs').cpSync('src/commands/plan-generation.md','dist/commands/plan-generation.md')\"",
|
|
23
|
+
"build": "tsc && node -e \"require('fs').cpSync('src/commands/plan-generation.md','dist/commands/plan-generation.md');require('fs').cpSync('src/agents/agent-instructions.md','dist/agents/agent-instructions.md')\"",
|
|
24
24
|
"test": "tsx --test test/**/*.test.ts",
|
|
25
25
|
"prepare": "npm run build",
|
|
26
26
|
"start": "node dist/index.js"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
29
|
"commander": "^13.1.0",
|
|
31
30
|
"cross-spawn": "^7.0.6",
|
|
32
31
|
"dotenv": "^16.4.7",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
You are an AI agent executing a task on behalf of the user via the Palmier platform. 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 prefixed with [PALMIER_REPORT]:
|
|
6
|
+
[PALMIER_REPORT] report.md
|
|
7
|
+
[PALMIER_REPORT] summary.md
|
|
8
|
+
|
|
9
|
+
## Completion
|
|
10
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
## Permissions
|
|
17
|
+
|
|
18
|
+
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]:
|
|
19
|
+
[PALMIER_PERMISSION] Read | Read file contents from the repository
|
|
20
|
+
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
21
|
+
[PALMIER_PERMISSION] Write | Write generated output files
|
|
22
|
+
|
|
23
|
+
## CLI Commands
|
|
24
|
+
|
|
25
|
+
You have access to the following palmier CLI commands:
|
|
26
|
+
|
|
27
|
+
**Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, request it:
|
|
28
|
+
```
|
|
29
|
+
palmier request-input --description "What is the database connection string?" --description "What is the API key?"
|
|
30
|
+
```
|
|
31
|
+
The command blocks until the user responds and prints each value on its own line. If the user aborts, the command exits with a non-zero status.
|
|
32
|
+
|
|
33
|
+
**Sending push notifications** — If you need to send a push notification to the user:
|
|
34
|
+
```
|
|
35
|
+
palmier notify --title "Task Complete" --body "The deployment finished successfully."
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
The task to execute follows below.
|
package/src/agents/claude.ts
CHANGED
|
@@ -13,8 +13,8 @@ export class ClaudeAgent implements AgentTool {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
17
|
-
const args = ["--permission-mode", "acceptEdits", "
|
|
16
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
|
+
const args = ["--permission-mode", "acceptEdits", "-p"];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
20
20
|
for (const p of allPerms) {
|
|
@@ -31,11 +31,6 @@ export class ClaudeAgent implements AgentTool {
|
|
|
31
31
|
} catch {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
|
-
try {
|
|
35
|
-
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
36
|
-
} catch {
|
|
37
|
-
// MCP registration is best-effort; agent still works without it
|
|
38
|
-
}
|
|
39
34
|
return true;
|
|
40
35
|
}
|
|
41
36
|
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -34,11 +34,6 @@ export class CodexAgent implements AgentTool {
|
|
|
34
34
|
} catch {
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
|
-
try {
|
|
38
|
-
execSync("codex mcp add palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
39
|
-
} catch {
|
|
40
|
-
// MCP registration is best-effort; agent still works without it
|
|
41
|
-
}
|
|
42
37
|
return true;
|
|
43
38
|
}
|
|
44
39
|
}
|
package/src/agents/copilot.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
1
|
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
5
2
|
import { execSync } from "child_process";
|
|
6
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
@@ -34,22 +31,6 @@ export class CopilotAgent implements AgentTool {
|
|
|
34
31
|
} catch {
|
|
35
32
|
return false;
|
|
36
33
|
}
|
|
37
|
-
// Register Palmier MCP server in ~/.copilot/mcp-config.json
|
|
38
|
-
try {
|
|
39
|
-
const configDir = path.join(homedir(), ".copilot");
|
|
40
|
-
const configFile = path.join(configDir, "mcp-config.json");
|
|
41
|
-
let config: Record<string, unknown> = {};
|
|
42
|
-
if (fs.existsSync(configFile)) {
|
|
43
|
-
config = JSON.parse(fs.readFileSync(configFile, "utf-8")) as Record<string, unknown>;
|
|
44
|
-
}
|
|
45
|
-
const servers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
46
|
-
servers.palmier = { command: "palmier", args: ["mcpserver"] };
|
|
47
|
-
config.mcpServers = servers;
|
|
48
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
49
|
-
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
50
|
-
} catch {
|
|
51
|
-
// MCP registration is best-effort
|
|
52
|
-
}
|
|
53
34
|
return true;
|
|
54
35
|
}
|
|
55
36
|
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -35,11 +35,6 @@ export class GeminiAgent implements AgentTool {
|
|
|
35
35
|
} catch {
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
-
try {
|
|
39
|
-
execSync("gemini mcp add --scope user palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
40
|
-
} catch {
|
|
41
|
-
// MCP registration is best-effort; agent still works without it
|
|
42
|
-
}
|
|
43
38
|
return true;
|
|
44
39
|
}
|
|
45
40
|
}
|
|
@@ -1,28 +1,20 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Instructions prepended or injected as system prompt for every task invocation.
|
|
3
9
|
* Instructs the agent to output structured markers so palmier can determine
|
|
4
10
|
* the task outcome, report files, and permission/input requests.
|
|
5
11
|
*/
|
|
6
|
-
export const AGENT_INSTRUCTIONS =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
When you are done, output exactly one of these markers as the very last line:
|
|
11
|
-
- Success: [PALMIER_TASK_SUCCESS]
|
|
12
|
-
- Failure: [PALMIER_TASK_FAILURE]
|
|
13
|
-
Do not wrap them in code blocks or add text on the same line.
|
|
14
|
-
|
|
15
|
-
If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]: e.g.
|
|
16
|
-
[PALMIER_PERMISSION] Read | Read file contents from the repository
|
|
17
|
-
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
18
|
-
[PALMIER_PERMISSION] Write | Write generated output files
|
|
19
|
-
|
|
20
|
-
If the task requires information from the user that you do not have (such as credentials, connection strings, API keys, or configuration values), print each required input on its own line prefixed with [PALMIER_INPUT]: e.g.
|
|
21
|
-
[PALMIER_INPUT] What is the database connection string?
|
|
22
|
-
[PALMIER_INPUT] What is the API key for the external service?`;
|
|
12
|
+
export const AGENT_INSTRUCTIONS = fs.readFileSync(
|
|
13
|
+
path.join(__dirname, "agent-instructions.md"),
|
|
14
|
+
"utf-8",
|
|
15
|
+
);
|
|
23
16
|
|
|
24
17
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
25
18
|
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
26
19
|
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
27
20
|
export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
28
|
-
export const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { connectNats } from "../nats-client.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Send a push notification to the user via NATS.
|
|
7
|
+
* Usage: palmier notify --title "Title" --body "Body text"
|
|
8
|
+
*/
|
|
9
|
+
export async function notifyCommand(opts: { title: string; body: string }): Promise<void> {
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const nc = await connectNats(config);
|
|
12
|
+
|
|
13
|
+
if (!nc) {
|
|
14
|
+
console.error("Error: NATS connection required for push notifications.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sc = StringCodec();
|
|
19
|
+
const payload = {
|
|
20
|
+
hostId: config.hostId,
|
|
21
|
+
title: opts.title,
|
|
22
|
+
body: opts.body,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const subject = `host.${config.hostId}.push.send`;
|
|
27
|
+
const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), {
|
|
28
|
+
timeout: 15_000,
|
|
29
|
+
});
|
|
30
|
+
const result = JSON.parse(sc.decode(reply.data)) as { ok?: boolean; error?: string };
|
|
31
|
+
|
|
32
|
+
if (result.ok) {
|
|
33
|
+
console.log("Push notification sent successfully.");
|
|
34
|
+
} else {
|
|
35
|
+
console.error(`Failed to send push notification: ${result.error}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`Error sending push notification: ${err}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
} finally {
|
|
42
|
+
await nc.drain();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { connectNats } from "../nats-client.js";
|
|
3
|
+
import { getTaskDir, parseTaskFile, appendResultMessage } from "../task.js";
|
|
4
|
+
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Request input from the user and print responses to stdout.
|
|
8
|
+
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
9
|
+
*
|
|
10
|
+
* Requires PALMIER_TASK_ID environment variable to be set.
|
|
11
|
+
* Outputs each response on its own line: "description: value"
|
|
12
|
+
*/
|
|
13
|
+
export async function requestInputCommand(opts: { description: string[] }): Promise<void> {
|
|
14
|
+
const taskId = process.env.PALMIER_TASK_ID;
|
|
15
|
+
if (!taskId) {
|
|
16
|
+
console.error("Error: PALMIER_TASK_ID environment variable is not set.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const nc = await connectNats(config);
|
|
22
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
23
|
+
const task = parseTaskFile(taskDir);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, opts.description);
|
|
27
|
+
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
28
|
+
|
|
29
|
+
if (response === "aborted") {
|
|
30
|
+
// Write abort as user message if RESULT file is available
|
|
31
|
+
const resultFile = process.env.PALMIER_RESULT_FILE;
|
|
32
|
+
if (resultFile) {
|
|
33
|
+
appendResultMessage(taskDir, resultFile, {
|
|
34
|
+
role: "user",
|
|
35
|
+
time: Date.now(),
|
|
36
|
+
content: "Input request aborted.",
|
|
37
|
+
type: "input",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
console.error("User aborted the input request.");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Write user input as a conversation message
|
|
45
|
+
const resultFile = process.env.PALMIER_RESULT_FILE;
|
|
46
|
+
if (resultFile) {
|
|
47
|
+
const lines = opts.description.map((desc, i) => `**${desc}** ${response[i]}`);
|
|
48
|
+
appendResultMessage(taskDir, resultFile, {
|
|
49
|
+
role: "user",
|
|
50
|
+
time: Date.now(),
|
|
51
|
+
content: lines.join("\n"),
|
|
52
|
+
type: "input",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < opts.description.length; i++) {
|
|
57
|
+
console.log(response[i]);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`Error requesting user input: ${err}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
} finally {
|
|
63
|
+
if (nc) await nc.drain();
|
|
64
|
+
}
|
|
65
|
+
}
|