palmier 0.4.2 → 0.4.4
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 +18 -30
- 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 +10 -0
- package/dist/commands/request-input.js +49 -0
- package/dist/commands/run.d.ts +4 -5
- package/dist/commands/run.js +90 -105
- package/dist/commands/serve.js +31 -28
- package/dist/index.js +15 -5
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +54 -14
- package/dist/rpc-handler.js +217 -54
- package/dist/spawn-command.d.ts +1 -1
- package/dist/spawn-command.js +13 -1
- package/dist/task.d.ts +18 -7
- package/dist/task.js +70 -27
- package/dist/types.d.ts +10 -1
- 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 +51 -0
- package/src/commands/run.ts +98 -129
- package/src/commands/serve.ts +34 -36
- package/src/index.ts +16 -5
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +53 -15
- package/src/rpc-handler.ts +244 -57
- package/src/spawn-command.ts +13 -2
- package/src/task.ts +79 -29
- package/src/types.ts +11 -1
- package/dist/commands/mcpserver.d.ts +0 -2
- package/dist/commands/mcpserver.js +0 -93
- package/src/commands/mcpserver.ts +0 -113
package/dist/rpc-handler.js
CHANGED
|
@@ -4,57 +4,88 @@ import * as path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory,
|
|
7
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
8
8
|
import { getPlatform } from "./platform/index.js";
|
|
9
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
|
+
import crossSpawn from "cross-spawn";
|
|
10
11
|
import { getAgent } from "./agents/agent.js";
|
|
11
12
|
import { validateSession } from "./session-store.js";
|
|
12
13
|
import { publishHostEvent } from "./events.js";
|
|
13
14
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
15
|
+
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
14
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
16
18
|
/**
|
|
17
|
-
* Parse RESULT frontmatter
|
|
19
|
+
* Parse RESULT frontmatter and conversation messages.
|
|
18
20
|
*/
|
|
19
21
|
function parseResultFrontmatter(raw) {
|
|
20
22
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
21
23
|
if (!fmMatch)
|
|
22
|
-
return {
|
|
24
|
+
return { messages: [] };
|
|
23
25
|
const meta = {};
|
|
24
|
-
const requiredPermissions = [];
|
|
25
26
|
for (const line of fmMatch[1].split("\n")) {
|
|
26
27
|
const sep = line.indexOf(": ");
|
|
27
28
|
if (sep === -1)
|
|
28
29
|
continue;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
meta[line.slice(0, sep).trim()] = line.slice(sep + 2).trim();
|
|
31
|
+
}
|
|
32
|
+
const messages = parseConversationMessages(fmMatch[2]);
|
|
33
|
+
// Derive state from status messages — just look at the last one
|
|
34
|
+
const statusMessages = messages.filter((m) => m.role === "status");
|
|
35
|
+
const lastStatus = statusMessages[statusMessages.length - 1];
|
|
36
|
+
const startedMsg = statusMessages.find((m) => m.type === "started");
|
|
37
|
+
const terminalStates = ["finished", "failed", "aborted"];
|
|
38
|
+
const terminalMsg = [...statusMessages].reverse().find((m) => terminalStates.includes(m.type ?? ""));
|
|
39
|
+
// If last status is "started", determine if it's a task run or follow-up
|
|
40
|
+
let runningState;
|
|
41
|
+
if (lastStatus?.type === "started") {
|
|
42
|
+
runningState = terminalMsg ? "followup" : "started";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
runningState = lastStatus?.type;
|
|
43
46
|
}
|
|
44
|
-
const reportFiles = meta.report_files
|
|
45
|
-
? meta.report_files.split(",").map((f) => f.trim()).filter(Boolean)
|
|
46
|
-
: [];
|
|
47
47
|
return {
|
|
48
|
-
|
|
48
|
+
messages,
|
|
49
49
|
task_name: meta.task_name,
|
|
50
|
-
running_state:
|
|
51
|
-
start_time:
|
|
52
|
-
end_time:
|
|
53
|
-
task_file: meta.task_file,
|
|
54
|
-
report_files: reportFiles.length > 0 ? reportFiles : undefined,
|
|
55
|
-
required_permissions: requiredPermissions.length > 0 ? requiredPermissions : undefined,
|
|
50
|
+
running_state: runningState,
|
|
51
|
+
start_time: startedMsg?.time || undefined,
|
|
52
|
+
end_time: terminalMsg?.time || undefined,
|
|
56
53
|
};
|
|
57
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse conversation messages from the body of a RESULT file.
|
|
57
|
+
*/
|
|
58
|
+
function parseConversationMessages(body) {
|
|
59
|
+
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
60
|
+
const messages = [];
|
|
61
|
+
const matches = [...body.matchAll(delimiterRegex)];
|
|
62
|
+
if (matches.length === 0) {
|
|
63
|
+
// No delimiters — treat entire body as single assistant message if non-empty
|
|
64
|
+
const content = body.trim();
|
|
65
|
+
if (content) {
|
|
66
|
+
messages.push({ role: "assistant", time: 0, content });
|
|
67
|
+
}
|
|
68
|
+
return messages;
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < matches.length; i++) {
|
|
71
|
+
const match = matches[i];
|
|
72
|
+
const attrs = match[1];
|
|
73
|
+
const start = match.index + match[0].length;
|
|
74
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
|
|
75
|
+
const content = body.slice(start, end).trim();
|
|
76
|
+
const role = (parseAttr(attrs, "role") ?? "assistant");
|
|
77
|
+
const time = Number(parseAttr(attrs, "time") ?? "0");
|
|
78
|
+
const type = parseAttr(attrs, "type");
|
|
79
|
+
const attachmentsRaw = parseAttr(attrs, "attachments");
|
|
80
|
+
const attachments = attachmentsRaw ? attachmentsRaw.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
81
|
+
messages.push({ role, time, content, ...(type ? { type } : {}), ...(attachments ? { attachments } : {}) });
|
|
82
|
+
}
|
|
83
|
+
return messages;
|
|
84
|
+
}
|
|
85
|
+
function parseAttr(attrs, name) {
|
|
86
|
+
const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
|
|
87
|
+
return match ? match[1] : undefined;
|
|
88
|
+
}
|
|
58
89
|
/**
|
|
59
90
|
* Run plan generation for a task prompt using the given agent.
|
|
60
91
|
* Returns the generated plan body and task name.
|
|
@@ -85,6 +116,8 @@ async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
|
85
116
|
}
|
|
86
117
|
return { name, body };
|
|
87
118
|
}
|
|
119
|
+
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
120
|
+
const activeFollowups = new Map();
|
|
88
121
|
/**
|
|
89
122
|
* Create a transport-agnostic RPC handler bound to the given config.
|
|
90
123
|
*/
|
|
@@ -226,8 +259,8 @@ export function createRpcHandler(config, nc) {
|
|
|
226
259
|
writeTaskFile(taskDir, task);
|
|
227
260
|
// Do NOT append to tasks.jsonl — this is a one-off run
|
|
228
261
|
// Create initial result file so it appears in runs list immediately
|
|
229
|
-
const
|
|
230
|
-
appendHistory(config.projectRoot, { task_id: id,
|
|
262
|
+
const runId = createRunDir(taskDir, name, Date.now());
|
|
263
|
+
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
231
264
|
// Spawn `palmier run <id>` directly as a detached process
|
|
232
265
|
const script = process.argv[1] || "palmier";
|
|
233
266
|
const child = spawn(process.execPath, [script, "run", id], {
|
|
@@ -236,7 +269,7 @@ export function createRpcHandler(config, nc) {
|
|
|
236
269
|
windowsHide: true,
|
|
237
270
|
});
|
|
238
271
|
child.unref();
|
|
239
|
-
return { ok: true, task_id: id,
|
|
272
|
+
return { ok: true, task_id: id, run_id: runId };
|
|
240
273
|
}
|
|
241
274
|
case "task.run": {
|
|
242
275
|
const params = request.params;
|
|
@@ -244,10 +277,10 @@ export function createRpcHandler(config, nc) {
|
|
|
244
277
|
// Create initial result file so it appears in runs list immediately
|
|
245
278
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
246
279
|
const runTask = parseTaskFile(runTaskDir);
|
|
247
|
-
const
|
|
248
|
-
appendHistory(config.projectRoot, { task_id: params.id,
|
|
280
|
+
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
|
|
281
|
+
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
249
282
|
await getPlatform().startTask(params.id);
|
|
250
|
-
return { ok: true, task_id: params.id,
|
|
283
|
+
return { ok: true, task_id: params.id, run_id: taskRunId };
|
|
251
284
|
}
|
|
252
285
|
catch (err) {
|
|
253
286
|
const e = err;
|
|
@@ -255,15 +288,145 @@ export function createRpcHandler(config, nc) {
|
|
|
255
288
|
return { error: `Failed to start task: ${e.stderr || e.message}` };
|
|
256
289
|
}
|
|
257
290
|
}
|
|
291
|
+
case "task.followup": {
|
|
292
|
+
const params = request.params;
|
|
293
|
+
if (!params.run_id || !params.message?.trim()) {
|
|
294
|
+
return { error: "run_id and message are required" };
|
|
295
|
+
}
|
|
296
|
+
const followupKey = `${params.id}:${params.run_id}`;
|
|
297
|
+
if (activeFollowups.has(followupKey)) {
|
|
298
|
+
return { error: "A follow-up is already running for this run" };
|
|
299
|
+
}
|
|
300
|
+
const followupTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
301
|
+
const followupTask = parseTaskFile(followupTaskDir);
|
|
302
|
+
const followupRunDir = getRunDir(followupTaskDir, params.run_id);
|
|
303
|
+
// Append user message + started status
|
|
304
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
305
|
+
role: "user",
|
|
306
|
+
time: Date.now(),
|
|
307
|
+
content: params.message,
|
|
308
|
+
});
|
|
309
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
310
|
+
role: "status",
|
|
311
|
+
time: Date.now(),
|
|
312
|
+
content: "",
|
|
313
|
+
type: "started",
|
|
314
|
+
});
|
|
315
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
316
|
+
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
317
|
+
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
318
|
+
const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(followupTask, params.message, followupTask.frontmatter.permissions);
|
|
319
|
+
// Spawn directly via crossSpawn so we can track and kill the child
|
|
320
|
+
const child = crossSpawn(cmd, cmdArgs, {
|
|
321
|
+
cwd: followupRunDir,
|
|
322
|
+
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
323
|
+
env: { ...process.env, PALMIER_TASK_ID: params.id },
|
|
324
|
+
windowsHide: true,
|
|
325
|
+
});
|
|
326
|
+
if (stdin != null)
|
|
327
|
+
child.stdin.end(stdin);
|
|
328
|
+
activeFollowups.set(followupKey, child);
|
|
329
|
+
// Collect output
|
|
330
|
+
const chunks = [];
|
|
331
|
+
child.stdout?.on("data", (d) => chunks.push(d));
|
|
332
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
333
|
+
child.on("close", async (code) => {
|
|
334
|
+
activeFollowups.delete(followupKey);
|
|
335
|
+
// If killed by stop_followup, the stopped status is already written
|
|
336
|
+
if (child.killed)
|
|
337
|
+
return;
|
|
338
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
339
|
+
const outcome = code !== 0 ? "failed" : parseTaskOutcome(output);
|
|
340
|
+
const reportFiles = parseReportFiles(output);
|
|
341
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
342
|
+
role: "assistant",
|
|
343
|
+
time: Date.now(),
|
|
344
|
+
content: stripPalmierMarkers(output),
|
|
345
|
+
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
346
|
+
});
|
|
347
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
348
|
+
role: "status",
|
|
349
|
+
time: Date.now(),
|
|
350
|
+
content: "",
|
|
351
|
+
type: outcome,
|
|
352
|
+
});
|
|
353
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
354
|
+
});
|
|
355
|
+
child.on("error", async (err) => {
|
|
356
|
+
activeFollowups.delete(followupKey);
|
|
357
|
+
console.error(`Follow-up failed for ${followupKey}:`, err);
|
|
358
|
+
appendRunMessage(followupTaskDir, params.run_id, {
|
|
359
|
+
role: "status",
|
|
360
|
+
time: Date.now(),
|
|
361
|
+
content: "",
|
|
362
|
+
type: "failed",
|
|
363
|
+
});
|
|
364
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
365
|
+
});
|
|
366
|
+
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
367
|
+
}
|
|
368
|
+
case "task.stop_followup": {
|
|
369
|
+
const params = request.params;
|
|
370
|
+
if (!params.run_id) {
|
|
371
|
+
return { error: "run_id is required" };
|
|
372
|
+
}
|
|
373
|
+
const stopKey = `${params.id}:${params.run_id}`;
|
|
374
|
+
const child = activeFollowups.get(stopKey);
|
|
375
|
+
if (!child) {
|
|
376
|
+
return { error: "No active follow-up for this run" };
|
|
377
|
+
}
|
|
378
|
+
// Kill the child process tree
|
|
379
|
+
if (process.platform === "win32" && child.pid) {
|
|
380
|
+
try {
|
|
381
|
+
const { execFileSync } = await import("child_process");
|
|
382
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
383
|
+
}
|
|
384
|
+
catch { /* may have already exited */ }
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
child.kill();
|
|
388
|
+
}
|
|
389
|
+
// Append stopped status (child.killed prevents the close handler from writing)
|
|
390
|
+
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
391
|
+
appendRunMessage(stopTaskDir, params.run_id, {
|
|
392
|
+
role: "status",
|
|
393
|
+
time: Date.now(),
|
|
394
|
+
content: "",
|
|
395
|
+
type: "stopped",
|
|
396
|
+
});
|
|
397
|
+
activeFollowups.delete(stopKey);
|
|
398
|
+
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
399
|
+
return { ok: true, task_id: params.id, run_id: params.run_id };
|
|
400
|
+
}
|
|
258
401
|
case "task.abort": {
|
|
259
402
|
const params = request.params;
|
|
403
|
+
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
404
|
+
// Read the PID before overwriting status — stopTask needs it to
|
|
405
|
+
// kill the entire process tree on Windows.
|
|
406
|
+
const abortPrevStatus = readTaskStatus(abortTaskDir);
|
|
260
407
|
// Write abort status BEFORE killing so the dying process's signal
|
|
261
408
|
// handler can detect this was RPC-initiated and skip publishing.
|
|
262
|
-
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
263
409
|
writeTaskStatus(abortTaskDir, {
|
|
264
410
|
running_state: "aborted",
|
|
265
411
|
time_stamp: Date.now(),
|
|
412
|
+
...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
|
|
266
413
|
});
|
|
414
|
+
// Append aborted status to the latest run
|
|
415
|
+
try {
|
|
416
|
+
const runDirs = fs.readdirSync(abortTaskDir)
|
|
417
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
|
|
418
|
+
.sort();
|
|
419
|
+
const latestRunId = runDirs[runDirs.length - 1];
|
|
420
|
+
if (latestRunId) {
|
|
421
|
+
appendRunMessage(abortTaskDir, latestRunId, {
|
|
422
|
+
role: "status",
|
|
423
|
+
time: Date.now(),
|
|
424
|
+
content: "",
|
|
425
|
+
type: "aborted",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch { /* best-effort */ }
|
|
267
430
|
try {
|
|
268
431
|
await getPlatform().stopTask(params.id);
|
|
269
432
|
}
|
|
@@ -288,25 +451,26 @@ export function createRpcHandler(config, nc) {
|
|
|
288
451
|
}
|
|
289
452
|
case "task.result": {
|
|
290
453
|
const params = request.params;
|
|
291
|
-
if (!params.
|
|
292
|
-
return { error: "
|
|
454
|
+
if (!params.run_id) {
|
|
455
|
+
return { error: "run_id is required" };
|
|
293
456
|
}
|
|
294
|
-
const
|
|
457
|
+
const taskrunPath = path.join(config.projectRoot, "tasks", params.id, params.run_id, "TASKRUN.md");
|
|
295
458
|
try {
|
|
296
|
-
const raw = fs.readFileSync(
|
|
459
|
+
const raw = fs.readFileSync(taskrunPath, "utf-8");
|
|
297
460
|
const meta = parseResultFrontmatter(raw);
|
|
298
461
|
return { task_id: params.id, ...meta };
|
|
299
462
|
}
|
|
300
463
|
catch {
|
|
301
|
-
return { task_id: params.id, error: "
|
|
464
|
+
return { task_id: params.id, error: "Run not found" };
|
|
302
465
|
}
|
|
303
466
|
}
|
|
304
467
|
case "task.reports": {
|
|
305
468
|
const params = request.params;
|
|
306
|
-
if (!Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
307
|
-
return { error: "
|
|
469
|
+
if (!params.run_id || !Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
470
|
+
return { error: "run_id and report_files are required" };
|
|
308
471
|
}
|
|
309
472
|
const reports = [];
|
|
473
|
+
const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
|
|
310
474
|
for (const file of params.report_files) {
|
|
311
475
|
if (!file.endsWith(".md")) {
|
|
312
476
|
reports.push({ file, error: "must end with .md" });
|
|
@@ -317,7 +481,7 @@ export function createRpcHandler(config, nc) {
|
|
|
317
481
|
reports.push({ file, error: "must be a plain filename" });
|
|
318
482
|
continue;
|
|
319
483
|
}
|
|
320
|
-
const reportPath = path.join(
|
|
484
|
+
const reportPath = path.join(runDir, basename);
|
|
321
485
|
try {
|
|
322
486
|
const content = fs.readFileSync(reportPath, "utf-8");
|
|
323
487
|
reports.push({ file, content });
|
|
@@ -339,7 +503,7 @@ export function createRpcHandler(config, nc) {
|
|
|
339
503
|
console.log(`[task.user_input] ${params.id} → ${params.value}`);
|
|
340
504
|
return { ok: true };
|
|
341
505
|
}
|
|
342
|
-
case "
|
|
506
|
+
case "taskrun.list": {
|
|
343
507
|
const params = request.params;
|
|
344
508
|
const { entries, total } = readHistory(config.projectRoot, {
|
|
345
509
|
offset: params.offset ?? 0,
|
|
@@ -347,30 +511,29 @@ export function createRpcHandler(config, nc) {
|
|
|
347
511
|
task_id: params.task_id,
|
|
348
512
|
});
|
|
349
513
|
const enriched = entries.map((entry) => {
|
|
350
|
-
const
|
|
514
|
+
const taskrunPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.run_id, "TASKRUN.md");
|
|
351
515
|
try {
|
|
352
|
-
const raw = fs.readFileSync(
|
|
516
|
+
const raw = fs.readFileSync(taskrunPath, "utf-8");
|
|
353
517
|
const meta = parseResultFrontmatter(raw);
|
|
354
|
-
|
|
355
|
-
const { content: _, ...rest } = meta;
|
|
518
|
+
const { messages: _, ...rest } = meta;
|
|
356
519
|
return { ...entry, ...rest };
|
|
357
520
|
}
|
|
358
521
|
catch {
|
|
359
|
-
return { ...entry, error: "
|
|
522
|
+
return { ...entry, error: "Run not found" };
|
|
360
523
|
}
|
|
361
524
|
});
|
|
362
525
|
return { entries: enriched, total };
|
|
363
526
|
}
|
|
364
|
-
case "
|
|
527
|
+
case "taskrun.delete": {
|
|
365
528
|
const params = request.params;
|
|
366
|
-
if (!params.task_id || !params.
|
|
367
|
-
return { error: "task_id and
|
|
529
|
+
if (!params.task_id || !params.run_id) {
|
|
530
|
+
return { error: "task_id and run_id are required" };
|
|
368
531
|
}
|
|
369
|
-
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.
|
|
532
|
+
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
|
|
370
533
|
if (!deleted) {
|
|
371
534
|
return { error: "History entry not found" };
|
|
372
535
|
}
|
|
373
|
-
return { ok: true, task_id: params.task_id,
|
|
536
|
+
return { ok: true, task_id: params.task_id, run_id: params.run_id };
|
|
374
537
|
}
|
|
375
538
|
case "host.update": {
|
|
376
539
|
const error = await performUpdate();
|
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
|
*/
|
|
@@ -39,20 +39,31 @@ export declare function writeTaskStatus(taskDir: string, status: TaskStatus): vo
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
|
|
41
41
|
/**
|
|
42
|
-
* Create
|
|
43
|
-
*
|
|
44
|
-
* Returns the result file name.
|
|
42
|
+
* Create a run directory with an initial TASKRUN.md file.
|
|
43
|
+
* Returns the run ID (timestamp string used as directory name).
|
|
45
44
|
*/
|
|
46
|
-
export declare function
|
|
45
|
+
export declare function createRunDir(taskDir: string, taskName: string, startTime: number): string;
|
|
46
|
+
/**
|
|
47
|
+
* Get the path to a run directory.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getRunDir(taskDir: string, runId: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Append a conversation message to a run's TASKRUN.md file.
|
|
52
|
+
*/
|
|
53
|
+
export declare function appendRunMessage(taskDir: string, runId: string, msg: ConversationMessage): void;
|
|
54
|
+
/**
|
|
55
|
+
* Read conversation messages from a run's TASKRUN.md file.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readRunMessages(taskDir: string, runId: string): ConversationMessage[];
|
|
47
58
|
/**
|
|
48
59
|
* Append a history entry to the project-level history.jsonl file.
|
|
49
60
|
*/
|
|
50
61
|
export declare function appendHistory(projectRoot: string, entry: HistoryEntry): void;
|
|
51
62
|
/**
|
|
52
|
-
* Delete a history entry and its associated
|
|
63
|
+
* Delete a history entry and its associated run directory.
|
|
53
64
|
* Returns true if the entry was found and removed.
|
|
54
65
|
*/
|
|
55
|
-
export declare function deleteHistoryEntry(projectRoot: string, taskId: string,
|
|
66
|
+
export declare function deleteHistoryEntry(projectRoot: string, taskId: string, runId: string): boolean;
|
|
56
67
|
/**
|
|
57
68
|
* Read history entries from history.jsonl with pagination.
|
|
58
69
|
* Returns entries sorted most-recent-first.
|
package/dist/task.js
CHANGED
|
@@ -130,16 +130,69 @@ export function readTaskStatus(taskDir) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
* Create
|
|
134
|
-
*
|
|
135
|
-
* Returns the result file name.
|
|
133
|
+
* Create a run directory with an initial TASKRUN.md file.
|
|
134
|
+
* Returns the run ID (timestamp string used as directory name).
|
|
136
135
|
*/
|
|
137
|
-
export function
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
export function createRunDir(taskDir, taskName, startTime) {
|
|
137
|
+
const runId = String(startTime);
|
|
138
|
+
const runDir = path.join(taskDir, runId);
|
|
139
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
140
|
+
const content = `---\ntask_name: ${taskName}\n---\n\n`;
|
|
141
|
+
fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
|
|
142
|
+
return runId;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the path to a run directory.
|
|
146
|
+
*/
|
|
147
|
+
export function getRunDir(taskDir, runId) {
|
|
148
|
+
return path.join(taskDir, runId);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Append a conversation message to a run's TASKRUN.md file.
|
|
152
|
+
*/
|
|
153
|
+
export function appendRunMessage(taskDir, runId, msg) {
|
|
154
|
+
const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
|
|
155
|
+
if (msg.type)
|
|
156
|
+
attrs.push(`type="${msg.type}"`);
|
|
157
|
+
if (msg.attachments?.length)
|
|
158
|
+
attrs.push(`attachments="${msg.attachments.join(",")}"`);
|
|
159
|
+
const delimiter = `<!-- palmier:message ${attrs.join(" ")} -->`;
|
|
160
|
+
const entry = `${delimiter}\n\n${msg.content}\n\n`;
|
|
161
|
+
fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read conversation messages from a run's TASKRUN.md file.
|
|
165
|
+
*/
|
|
166
|
+
export function readRunMessages(taskDir, runId) {
|
|
167
|
+
const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
168
|
+
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
169
|
+
if (!fmMatch)
|
|
170
|
+
return [];
|
|
171
|
+
const body = fmMatch[1];
|
|
172
|
+
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
173
|
+
const matches = [...body.matchAll(delimiterRegex)];
|
|
174
|
+
if (matches.length === 0)
|
|
175
|
+
return [];
|
|
176
|
+
const messages = [];
|
|
177
|
+
for (let i = 0; i < matches.length; i++) {
|
|
178
|
+
const match = matches[i];
|
|
179
|
+
const attrs = match[1];
|
|
180
|
+
const start = match.index + match[0].length;
|
|
181
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
|
|
182
|
+
const content = body.slice(start, end).trim();
|
|
183
|
+
const roleAttr = attrs.match(/role="([^"]*)"/)?.[1] ?? "assistant";
|
|
184
|
+
const timeAttr = attrs.match(/time="([^"]*)"/)?.[1] ?? "0";
|
|
185
|
+
const typeAttr = attrs.match(/type="([^"]*)"/)?.[1];
|
|
186
|
+
const attachmentsAttr = attrs.match(/attachments="([^"]*)"/)?.[1];
|
|
187
|
+
messages.push({
|
|
188
|
+
role: roleAttr,
|
|
189
|
+
time: Number(timeAttr),
|
|
190
|
+
content,
|
|
191
|
+
...(typeAttr ? { type: typeAttr } : {}),
|
|
192
|
+
...(attachmentsAttr ? { attachments: attachmentsAttr.split(",").map((f) => f.trim()).filter(Boolean) } : {}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return messages;
|
|
143
196
|
}
|
|
144
197
|
/**
|
|
145
198
|
* Append a history entry to the project-level history.jsonl file.
|
|
@@ -149,10 +202,10 @@ export function appendHistory(projectRoot, entry) {
|
|
|
149
202
|
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
150
203
|
}
|
|
151
204
|
/**
|
|
152
|
-
* Delete a history entry and its associated
|
|
205
|
+
* Delete a history entry and its associated run directory.
|
|
153
206
|
* Returns true if the entry was found and removed.
|
|
154
207
|
*/
|
|
155
|
-
export function deleteHistoryEntry(projectRoot, taskId,
|
|
208
|
+
export function deleteHistoryEntry(projectRoot, taskId, runId) {
|
|
156
209
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
157
210
|
if (!fs.existsSync(historyPath))
|
|
158
211
|
return false;
|
|
@@ -162,9 +215,9 @@ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
|
|
|
162
215
|
for (const line of lines) {
|
|
163
216
|
try {
|
|
164
217
|
const entry = JSON.parse(line);
|
|
165
|
-
if (entry.task_id === taskId && entry.
|
|
218
|
+
if (entry.task_id === taskId && entry.run_id === runId) {
|
|
166
219
|
found = true;
|
|
167
|
-
continue;
|
|
220
|
+
continue;
|
|
168
221
|
}
|
|
169
222
|
}
|
|
170
223
|
catch { /* keep malformed lines */ }
|
|
@@ -172,21 +225,11 @@ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
|
|
|
172
225
|
}
|
|
173
226
|
if (!found)
|
|
174
227
|
return false;
|
|
175
|
-
// Rewrite history.jsonl without the deleted entry
|
|
176
228
|
fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
177
|
-
// Delete the
|
|
178
|
-
const
|
|
179
|
-
if (fs.existsSync(
|
|
180
|
-
fs.
|
|
181
|
-
}
|
|
182
|
-
// Delete the corresponding task snapshot (TASK-<timestamp>.md)
|
|
183
|
-
const tsMatch = resultFile.match(/^RESULT-(\d+)\.md$/);
|
|
184
|
-
if (tsMatch) {
|
|
185
|
-
const snapshotFile = `TASK-${tsMatch[1]}.md`;
|
|
186
|
-
const snapshotPath = path.join(projectRoot, "tasks", taskId, snapshotFile);
|
|
187
|
-
if (fs.existsSync(snapshotPath)) {
|
|
188
|
-
fs.unlinkSync(snapshotPath);
|
|
189
|
-
}
|
|
229
|
+
// Delete the run directory
|
|
230
|
+
const runDir = path.join(projectRoot, "tasks", taskId, runId);
|
|
231
|
+
if (fs.existsSync(runDir)) {
|
|
232
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
190
233
|
}
|
|
191
234
|
return true;
|
|
192
235
|
}
|
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. */
|
|
@@ -59,12 +61,19 @@ export interface TaskStatus {
|
|
|
59
61
|
}
|
|
60
62
|
export interface HistoryEntry {
|
|
61
63
|
task_id: string;
|
|
62
|
-
|
|
64
|
+
run_id: string;
|
|
63
65
|
}
|
|
64
66
|
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" | "stopped";
|
|
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.4",
|
|
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",
|