palmier 0.4.1 → 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 -6
- package/dist/index.js +15 -5
- package/dist/platform/windows.js +17 -2
- package/dist/rpc-handler.js +42 -27
- 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/dist/update-checker.d.ts +0 -8
- package/dist/update-checker.js +0 -36
- 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 -7
- package/src/index.ts +16 -5
- package/src/platform/windows.ts +17 -2
- package/src/rpc-handler.ts +51 -26
- package/src/spawn-command.ts +13 -2
- package/src/task.ts +47 -2
- package/src/types.ts +10 -0
- package/src/update-checker.ts +0 -36
- package/dist/commands/mcpserver.d.ts +0 -2
- package/dist/commands/mcpserver.js +0 -93
- package/src/commands/mcpserver.ts +0 -113
|
@@ -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
|
+
}
|
package/src/commands/run.ts
CHANGED
|
@@ -4,42 +4,16 @@ import * as readline from "readline";
|
|
|
4
4
|
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
-
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile } from "../task.js";
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile, appendResultMessage, finalizeResultFrontmatter } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
|
-
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX
|
|
10
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import type { AgentTool } from "../agents/agent.js";
|
|
12
12
|
import { publishHostEvent } from "../events.js";
|
|
13
|
-
import { waitForUserInput
|
|
13
|
+
import { waitForUserInput } from "../user-input.js";
|
|
14
14
|
import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
|
|
15
15
|
import type { NatsConnection } from "nats";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Write a time-stamped RESULT file with frontmatter.
|
|
19
|
-
* Always generated, even for abort/fail.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Update an existing result file with the final outcome.
|
|
24
|
-
*/
|
|
25
|
-
function finalizeResultFile(
|
|
26
|
-
taskDir: string,
|
|
27
|
-
resultFileName: string,
|
|
28
|
-
taskName: string,
|
|
29
|
-
taskSnapshotName: string,
|
|
30
|
-
runningState: string,
|
|
31
|
-
startTime: number,
|
|
32
|
-
endTime: number,
|
|
33
|
-
output: string,
|
|
34
|
-
reportFiles: string[],
|
|
35
|
-
requiredPermissions: RequiredPermission[],
|
|
36
|
-
): void {
|
|
37
|
-
const reportLine = reportFiles.length > 0 ? `\nreport_files: ${reportFiles.join(", ")}` : "";
|
|
38
|
-
const permLines = requiredPermissions.map((p) => `\nrequired_permission: ${p.name} | ${p.description}`).join("");
|
|
39
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskSnapshotName}${reportLine}${permLines}\n---\n${output}`;
|
|
40
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
17
|
/**
|
|
44
18
|
* Shared context for agent invocation retry loops.
|
|
45
19
|
* Passed around to avoid threading many individual parameters.
|
|
@@ -48,6 +22,7 @@ interface InvocationContext {
|
|
|
48
22
|
agent: AgentTool;
|
|
49
23
|
task: ParsedTask;
|
|
50
24
|
taskDir: string;
|
|
25
|
+
resultFileName: string;
|
|
51
26
|
guiEnv: Record<string, string>;
|
|
52
27
|
nc: NatsConnection | undefined;
|
|
53
28
|
config: HostConfig;
|
|
@@ -57,10 +32,7 @@ interface InvocationContext {
|
|
|
57
32
|
}
|
|
58
33
|
|
|
59
34
|
interface InvocationResult {
|
|
60
|
-
output: string;
|
|
61
35
|
outcome: TaskRunningState;
|
|
62
|
-
reportFiles: string[];
|
|
63
|
-
requiredPermissions: RequiredPermission[];
|
|
64
36
|
}
|
|
65
37
|
|
|
66
38
|
/**
|
|
@@ -80,7 +52,7 @@ async function invokeAgentWithRetry(
|
|
|
80
52
|
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
|
|
81
53
|
const result = await spawnCommand(command, args, {
|
|
82
54
|
cwd: ctx.taskDir,
|
|
83
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
55
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RESULT_FILE: ctx.resultFileName },
|
|
84
56
|
echoStdout: true,
|
|
85
57
|
resolveOnFailure: true,
|
|
86
58
|
stdin,
|
|
@@ -90,13 +62,27 @@ async function invokeAgentWithRetry(
|
|
|
90
62
|
const reportFiles = parseReportFiles(result.output);
|
|
91
63
|
const requiredPermissions = parsePermissions(result.output);
|
|
92
64
|
|
|
65
|
+
// Append assistant message for this invocation
|
|
66
|
+
await appendAndNotify(ctx, {
|
|
67
|
+
role: "assistant",
|
|
68
|
+
time: Date.now(),
|
|
69
|
+
content: stripPalmierMarkers(result.output),
|
|
70
|
+
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
71
|
+
});
|
|
72
|
+
|
|
93
73
|
// Permission retry
|
|
94
74
|
if (outcome === "failed" && requiredPermissions.length > 0) {
|
|
95
75
|
const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
96
76
|
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
|
|
97
77
|
|
|
98
78
|
if (response === "aborted") {
|
|
99
|
-
|
|
79
|
+
await appendAndNotify(ctx, {
|
|
80
|
+
role: "user",
|
|
81
|
+
time: Date.now(),
|
|
82
|
+
content: "Permissions denied. Task aborted.",
|
|
83
|
+
type: "permission",
|
|
84
|
+
});
|
|
85
|
+
return { outcome: "failed" };
|
|
100
86
|
}
|
|
101
87
|
|
|
102
88
|
const newPerms = requiredPermissions.filter(
|
|
@@ -104,6 +90,14 @@ async function invokeAgentWithRetry(
|
|
|
104
90
|
&& !ctx.transientPermissions.some((ep) => ep.name === rp.name),
|
|
105
91
|
);
|
|
106
92
|
|
|
93
|
+
// Append user message for permission grant
|
|
94
|
+
await appendAndNotify(ctx, {
|
|
95
|
+
role: "user",
|
|
96
|
+
time: Date.now(),
|
|
97
|
+
content: `Permissions granted: ${newPerms.map((p) => p.name).join(", ")}`,
|
|
98
|
+
type: "permission",
|
|
99
|
+
});
|
|
100
|
+
|
|
107
101
|
if (response === "granted_all") {
|
|
108
102
|
ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
|
|
109
103
|
invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
|
|
@@ -116,26 +110,29 @@ async function invokeAgentWithRetry(
|
|
|
116
110
|
continue;
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
// Input retry
|
|
120
|
-
const inputRequests = parseInputRequests(result.output);
|
|
121
|
-
if (outcome === "failed" && inputRequests.length > 0) {
|
|
122
|
-
const response = await requestUserInput(ctx.nc, ctx.config, ctx.taskId, ctx.task.frontmatter.name, ctx.taskDir, inputRequests);
|
|
123
|
-
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided");
|
|
124
|
-
|
|
125
|
-
if (response === "aborted") {
|
|
126
|
-
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const inputLines = inputRequests.map((desc, i) => `- ${desc} → ${response[i]}`).join("\n");
|
|
130
|
-
retryPrompt = `The user provided the following inputs:\n${inputLines}\nPlease continue with these values.`;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
113
|
// Normal completion (success or non-retryable failure)
|
|
135
|
-
return {
|
|
114
|
+
return { outcome };
|
|
136
115
|
}
|
|
137
116
|
}
|
|
138
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Strip [PALMIER_*] marker lines from agent output.
|
|
120
|
+
*/
|
|
121
|
+
function stripPalmierMarkers(output: string): string {
|
|
122
|
+
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Append a conversation message to the RESULT file and notify connected clients.
|
|
127
|
+
*/
|
|
128
|
+
async function appendAndNotify(
|
|
129
|
+
ctx: InvocationContext,
|
|
130
|
+
msg: Parameters<typeof appendResultMessage>[2],
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
appendResultMessage(ctx.taskDir, ctx.resultFileName, msg);
|
|
133
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated" });
|
|
134
|
+
}
|
|
135
|
+
|
|
139
136
|
/**
|
|
140
137
|
* Find an existing RESULT file with running_state=started (created by the RPC handler).
|
|
141
138
|
*/
|
|
@@ -197,6 +194,10 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
197
194
|
// Mark as started immediately
|
|
198
195
|
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, resultFileName);
|
|
199
196
|
|
|
197
|
+
// Status: started
|
|
198
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
199
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
200
|
+
|
|
200
201
|
// If requires_confirmation, notify clients and wait
|
|
201
202
|
if (task.frontmatter.requires_confirmation) {
|
|
202
203
|
const confirmed = await requestConfirmation(nc, config, task, taskDir);
|
|
@@ -204,20 +205,22 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
204
205
|
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
205
206
|
if (!confirmed) {
|
|
206
207
|
console.log("Task aborted by user.");
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
209
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: "aborted" });
|
|
209
210
|
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
210
211
|
await cleanup();
|
|
211
212
|
return;
|
|
212
213
|
}
|
|
213
214
|
console.log("Task confirmed by user.");
|
|
215
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
216
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
// Shared invocation context
|
|
217
220
|
const guiEnv = getPlatform().getGuiEnv();
|
|
218
221
|
const agent = getAgent(task.frontmatter.agent);
|
|
219
222
|
const ctx: InvocationContext = {
|
|
220
|
-
agent, task, taskDir, guiEnv, nc, config, taskId,
|
|
223
|
+
agent, task, taskDir, resultFileName, guiEnv, nc, config, taskId,
|
|
221
224
|
transientPermissions: [],
|
|
222
225
|
};
|
|
223
226
|
|
|
@@ -225,35 +228,36 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
225
228
|
// Command-triggered mode
|
|
226
229
|
const result = await runCommandTriggeredMode(ctx);
|
|
227
230
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
228
|
-
|
|
231
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
232
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: result.endTime, running_state: outcome });
|
|
229
233
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
230
234
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
231
235
|
} else {
|
|
232
|
-
// Standard execution
|
|
236
|
+
// Standard execution — add user prompt as first message
|
|
237
|
+
await appendAndNotify(ctx, {
|
|
238
|
+
role: "user",
|
|
239
|
+
time: Date.now(),
|
|
240
|
+
content: task.body || task.frontmatter.user_prompt,
|
|
241
|
+
});
|
|
242
|
+
|
|
233
243
|
const result = await invokeAgentWithRetry(ctx, task);
|
|
234
244
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
235
|
-
|
|
236
|
-
|
|
245
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
246
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: outcome });
|
|
237
247
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
238
|
-
|
|
239
|
-
if (result.reportFiles.length > 0) {
|
|
240
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
241
|
-
event_type: "report-generated",
|
|
242
|
-
name: taskName,
|
|
243
|
-
report_files: result.reportFiles,
|
|
244
|
-
running_state: outcome,
|
|
245
|
-
result_file: resultFileName,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
248
|
console.log(`Task ${taskId} completed.`);
|
|
250
249
|
}
|
|
251
250
|
} catch (err) {
|
|
252
251
|
console.error(`Task ${taskId} failed:`, err);
|
|
253
|
-
const endTime = Date.now();
|
|
254
252
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
255
253
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
256
|
-
|
|
254
|
+
appendResultMessage(taskDir, resultFileName, {
|
|
255
|
+
role: "assistant",
|
|
256
|
+
time: Date.now(),
|
|
257
|
+
content: errorMsg,
|
|
258
|
+
});
|
|
259
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
260
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: outcome });
|
|
257
261
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
258
262
|
process.exitCode = 1;
|
|
259
263
|
} finally {
|
|
@@ -275,7 +279,7 @@ const MAX_LINE_LENGTH = 200_000;
|
|
|
275
279
|
*/
|
|
276
280
|
async function runCommandTriggeredMode(
|
|
277
281
|
ctx: InvocationContext,
|
|
278
|
-
): Promise<{ outcome: TaskRunningState; endTime: number
|
|
282
|
+
): Promise<{ outcome: TaskRunningState; endTime: number }> {
|
|
279
283
|
const commandStr = ctx.task.frontmatter.command!;
|
|
280
284
|
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
281
285
|
|
|
@@ -331,7 +335,7 @@ async function runCommandTriggeredMode(
|
|
|
331
335
|
} else {
|
|
332
336
|
invocationsFailed++;
|
|
333
337
|
}
|
|
334
|
-
appendLog(line,
|
|
338
|
+
appendLog(line, "", result.outcome);
|
|
335
339
|
}
|
|
336
340
|
|
|
337
341
|
async function drainQueue(): Promise<void> {
|
|
@@ -390,15 +394,7 @@ async function runCommandTriggeredMode(
|
|
|
390
394
|
}
|
|
391
395
|
|
|
392
396
|
const endTime = Date.now();
|
|
393
|
-
|
|
394
|
-
`Command: ${commandStr}`,
|
|
395
|
-
`Exit code: ${exitCode}`,
|
|
396
|
-
`Lines processed: ${linesProcessed}`,
|
|
397
|
-
`Agent invocations succeeded: ${invocationsSucceeded}`,
|
|
398
|
-
`Agent invocations failed: ${invocationsFailed}`,
|
|
399
|
-
].join("\n");
|
|
400
|
-
|
|
401
|
-
return { outcome: "finished", endTime, output: summary };
|
|
397
|
+
return { outcome: "finished", endTime };
|
|
402
398
|
}
|
|
403
399
|
|
|
404
400
|
async function publishTaskEvent(
|
|
@@ -413,6 +409,7 @@ async function publishTaskEvent(
|
|
|
413
409
|
writeTaskStatus(taskDir, {
|
|
414
410
|
running_state: eventType,
|
|
415
411
|
time_stamp: Date.now(),
|
|
412
|
+
...(eventType === "started" ? { pid: process.pid } : {}),
|
|
416
413
|
});
|
|
417
414
|
|
|
418
415
|
const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
|
|
@@ -534,21 +531,6 @@ export function parsePermissions(output: string): RequiredPermission[] {
|
|
|
534
531
|
return perms;
|
|
535
532
|
}
|
|
536
533
|
|
|
537
|
-
/**
|
|
538
|
-
* Extract user input requests from agent output.
|
|
539
|
-
* Looks for lines matching: [PALMIER_INPUT] <description>
|
|
540
|
-
*/
|
|
541
|
-
export function parseInputRequests(output: string): string[] {
|
|
542
|
-
const regex = new RegExp(`^\\${TASK_INPUT_PREFIX}\\s+(.+)$`, "gm");
|
|
543
|
-
const inputs: string[] = [];
|
|
544
|
-
let match;
|
|
545
|
-
while ((match = regex.exec(output)) !== null) {
|
|
546
|
-
const desc = match[1].trim();
|
|
547
|
-
if (desc) inputs.push(desc);
|
|
548
|
-
}
|
|
549
|
-
return inputs;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
534
|
/**
|
|
553
535
|
* Parse the agent's output for success/failure markers.
|
|
554
536
|
* Falls back to "finished" if no marker is found.
|
package/src/commands/serve.ts
CHANGED
|
@@ -4,10 +4,9 @@ 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
|
-
import { checkForUpdate } from "../update-checker.js";
|
|
11
10
|
import { detectAgents } from "../agents/agent.js";
|
|
12
11
|
import { saveConfig } from "../config.js";
|
|
13
12
|
import type { HostConfig } from "../types.js";
|
|
@@ -42,8 +41,13 @@ async function markTaskFailed(
|
|
|
42
41
|
} catch { /* use taskId as fallback */ }
|
|
43
42
|
|
|
44
43
|
const resultFileName = `RESULT-${endTime}.md`;
|
|
45
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n
|
|
44
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n\n`;
|
|
46
45
|
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
46
|
+
appendResultMessage(taskDir, resultFileName, {
|
|
47
|
+
role: "assistant",
|
|
48
|
+
time: endTime,
|
|
49
|
+
content: reason,
|
|
50
|
+
});
|
|
47
51
|
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
48
52
|
|
|
49
53
|
const payload: Record<string, unknown> = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
@@ -109,10 +113,6 @@ export async function serveCommand(): Promise<void> {
|
|
|
109
113
|
});
|
|
110
114
|
}, POLL_INTERVAL_MS);
|
|
111
115
|
|
|
112
|
-
// Check for updates on startup and every 24 hours
|
|
113
|
-
checkForUpdate().catch(() => {});
|
|
114
|
-
setInterval(() => { checkForUpdate().catch(() => {}); }, 24 * 60 * 60 * 1000);
|
|
115
|
-
|
|
116
116
|
const handleRpc = createRpcHandler(config, nc);
|
|
117
117
|
await startNatsTransport(config, handleRpc, nc);
|
|
118
118
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { initCommand } from "./commands/init.js";
|
|
|
9
9
|
import { infoCommand } from "./commands/info.js";
|
|
10
10
|
import { runCommand } from "./commands/run.js";
|
|
11
11
|
import { serveCommand } from "./commands/serve.js";
|
|
12
|
-
import {
|
|
12
|
+
import { notifyCommand } from "./commands/notify.js";
|
|
13
|
+
import { requestInputCommand } from "./commands/request-input.js";
|
|
13
14
|
|
|
14
15
|
import { pairCommand } from "./commands/pair.js";
|
|
15
16
|
import { lanCommand } from "./commands/lan.js";
|
|
@@ -62,10 +63,20 @@ program
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
program
|
|
65
|
-
.command("
|
|
66
|
-
.description("
|
|
67
|
-
.
|
|
68
|
-
|
|
66
|
+
.command("notify")
|
|
67
|
+
.description("Send a push notification to the user")
|
|
68
|
+
.requiredOption("--title <title>", "Notification title")
|
|
69
|
+
.requiredOption("--body <body>", "Notification body text")
|
|
70
|
+
.action(async (opts: { title: string; body: string }) => {
|
|
71
|
+
await notifyCommand(opts);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command("request-input")
|
|
76
|
+
.description("Request input from the user (requires PALMIER_TASK_ID env var)")
|
|
77
|
+
.requiredOption("--description <desc...>", "Input descriptions to show the user")
|
|
78
|
+
.action(async (opts: { description: string[] }) => {
|
|
79
|
+
await requestInputCommand(opts);
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
program
|
package/src/platform/windows.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { execFileSync } from "child_process";
|
|
|
4
4
|
import { spawn as nodeSpawn } from "child_process";
|
|
5
5
|
import type { PlatformService } from "./platform.js";
|
|
6
6
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
7
|
-
import { CONFIG_DIR } from "../config.js";
|
|
7
|
+
import { CONFIG_DIR, loadConfig } from "../config.js";
|
|
8
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
@@ -140,7 +141,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
140
141
|
// Kill old daemon first, then spawn new one.
|
|
141
142
|
if (oldPid) {
|
|
142
143
|
try {
|
|
143
|
-
execFileSync("taskkill", ["/pid", oldPid, "/f"], { windowsHide: true, stdio: "pipe" });
|
|
144
|
+
execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
144
145
|
} catch {
|
|
145
146
|
// Process may have already exited
|
|
146
147
|
}
|
|
@@ -225,6 +226,20 @@ export class WindowsPlatform implements PlatformService {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
async stopTask(taskId: string): Promise<void> {
|
|
229
|
+
// Try to kill the entire process tree via the PID recorded in status.json.
|
|
230
|
+
// schtasks /end only kills the top-level process, leaving agent children orphaned.
|
|
231
|
+
try {
|
|
232
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
233
|
+
const status = readTaskStatus(taskDir);
|
|
234
|
+
if (status?.pid) {
|
|
235
|
+
execFileSync("taskkill", ["/pid", String(status.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// PID may be stale or config unavailable; fall through to schtasks /end
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Fallback: schtasks /end (kills top-level process only)
|
|
228
243
|
const tn = schtasksTaskName(taskId);
|
|
229
244
|
try {
|
|
230
245
|
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
package/src/rpc-handler.ts
CHANGED
|
@@ -11,8 +11,8 @@ import { spawnCommand } from "./spawn-command.js";
|
|
|
11
11
|
import { getAgent } from "./agents/agent.js";
|
|
12
12
|
import { validateSession } from "./session-store.js";
|
|
13
13
|
import { publishHostEvent } from "./events.js";
|
|
14
|
-
import { currentVersion,
|
|
15
|
-
import type { HostConfig, ParsedTask, RpcMessage } from "./types.js";
|
|
14
|
+
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
15
|
+
import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
|
|
16
16
|
|
|
17
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
|
|
@@ -22,46 +22,72 @@ const PLAN_GENERATION_PROMPT = fs.readFileSync(
|
|
|
22
22
|
);
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Parse RESULT frontmatter
|
|
25
|
+
* Parse RESULT frontmatter and conversation messages.
|
|
26
26
|
*/
|
|
27
27
|
function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
28
28
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
29
|
-
if (!fmMatch) return {
|
|
29
|
+
if (!fmMatch) return { messages: [] };
|
|
30
30
|
|
|
31
31
|
const meta: Record<string, string> = {};
|
|
32
|
-
const requiredPermissions: Array<{ name: string; description: string }> = [];
|
|
33
32
|
for (const line of fmMatch[1].split("\n")) {
|
|
34
33
|
const sep = line.indexOf(": ");
|
|
35
34
|
if (sep === -1) continue;
|
|
36
|
-
|
|
37
|
-
const value = line.slice(sep + 2).trim();
|
|
38
|
-
if (key === "required_permission") {
|
|
39
|
-
const pipeSep = value.indexOf("|");
|
|
40
|
-
if (pipeSep !== -1) {
|
|
41
|
-
requiredPermissions.push({ name: value.slice(0, pipeSep).trim(), description: value.slice(pipeSep + 1).trim() });
|
|
42
|
-
} else {
|
|
43
|
-
requiredPermissions.push({ name: value, description: "" });
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
meta[key] = value;
|
|
47
|
-
}
|
|
35
|
+
meta[line.slice(0, sep).trim()] = line.slice(sep + 2).trim();
|
|
48
36
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: [];
|
|
37
|
+
|
|
38
|
+
const messages = parseConversationMessages(fmMatch[2]);
|
|
52
39
|
|
|
53
40
|
return {
|
|
54
|
-
|
|
41
|
+
messages,
|
|
55
42
|
task_name: meta.task_name,
|
|
56
43
|
running_state: meta.running_state,
|
|
57
44
|
start_time: meta.start_time ? Number(meta.start_time) : undefined,
|
|
58
45
|
end_time: meta.end_time ? Number(meta.end_time) : undefined,
|
|
59
46
|
task_file: meta.task_file,
|
|
60
|
-
report_files: reportFiles.length > 0 ? reportFiles : undefined,
|
|
61
|
-
required_permissions: requiredPermissions.length > 0 ? requiredPermissions : undefined,
|
|
62
47
|
};
|
|
63
48
|
}
|
|
64
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Parse conversation messages from the body of a RESULT file.
|
|
52
|
+
*/
|
|
53
|
+
function parseConversationMessages(body: string): ConversationMessage[] {
|
|
54
|
+
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
55
|
+
const messages: ConversationMessage[] = [];
|
|
56
|
+
const matches = [...body.matchAll(delimiterRegex)];
|
|
57
|
+
|
|
58
|
+
if (matches.length === 0) {
|
|
59
|
+
// No delimiters — treat entire body as single assistant message if non-empty
|
|
60
|
+
const content = body.trim();
|
|
61
|
+
if (content) {
|
|
62
|
+
messages.push({ role: "assistant", time: 0, content });
|
|
63
|
+
}
|
|
64
|
+
return messages;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < matches.length; i++) {
|
|
68
|
+
const match = matches[i];
|
|
69
|
+
const attrs = match[1];
|
|
70
|
+
const start = match.index! + match[0].length;
|
|
71
|
+
const end = i + 1 < matches.length ? matches[i + 1].index! : body.length;
|
|
72
|
+
const content = body.slice(start, end).trim();
|
|
73
|
+
|
|
74
|
+
const role = (parseAttr(attrs, "role") ?? "assistant") as "assistant" | "user";
|
|
75
|
+
const time = Number(parseAttr(attrs, "time") ?? "0");
|
|
76
|
+
const type = parseAttr(attrs, "type") as ConversationMessage["type"];
|
|
77
|
+
const attachmentsRaw = parseAttr(attrs, "attachments");
|
|
78
|
+
const attachments = attachmentsRaw ? attachmentsRaw.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
79
|
+
|
|
80
|
+
messages.push({ role, time, content, ...(type ? { type } : {}), ...(attachments ? { attachments } : {}) });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return messages;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseAttr(attrs: string, name: string): string | undefined {
|
|
87
|
+
const match = attrs.match(new RegExp(`${name}="([^"]*)"`));
|
|
88
|
+
return match ? match[1] : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
/**
|
|
66
92
|
* Run plan generation for a task prompt using the given agent.
|
|
67
93
|
* Returns the generated plan body and task name.
|
|
@@ -124,7 +150,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
124
150
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
125
151
|
agents: config.agents ?? [],
|
|
126
152
|
version: currentVersion,
|
|
127
|
-
latest_version: getLatestVersion(),
|
|
128
153
|
};
|
|
129
154
|
}
|
|
130
155
|
|
|
@@ -409,8 +434,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
409
434
|
try {
|
|
410
435
|
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
411
436
|
const meta = parseResultFrontmatter(raw);
|
|
412
|
-
// Exclude
|
|
413
|
-
const {
|
|
437
|
+
// Exclude messages from list response
|
|
438
|
+
const { messages: _, ...rest } = meta;
|
|
414
439
|
return { ...entry, ...rest };
|
|
415
440
|
} catch {
|
|
416
441
|
return { ...entry, error: "Result file not found" };
|
package/src/spawn-command.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import crossSpawn from "cross-spawn";
|
|
2
|
-
import type
|
|
2
|
+
import { execFileSync, type ChildProcess } from "child_process";
|
|
3
|
+
|
|
4
|
+
/** Kill a child process and its entire tree on Windows; plain kill elsewhere. */
|
|
5
|
+
function treeKill(child: ChildProcess): void {
|
|
6
|
+
if (process.platform === "win32" && child.pid) {
|
|
7
|
+
try {
|
|
8
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
9
|
+
return;
|
|
10
|
+
} catch { /* fall through */ }
|
|
11
|
+
}
|
|
12
|
+
child.kill();
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
export interface SpawnStreamingOptions {
|
|
5
16
|
cwd: string;
|
|
@@ -100,7 +111,7 @@ export function spawnCommand(
|
|
|
100
111
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
101
112
|
if (opts.timeout) {
|
|
102
113
|
timer = setTimeout(() => {
|
|
103
|
-
child
|
|
114
|
+
treeKill(child);
|
|
104
115
|
reject(new Error("command timed out"));
|
|
105
116
|
}, opts.timeout);
|
|
106
117
|
}
|