palmier 0.4.0 → 0.4.2
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/dist/commands/mcpserver.js +34 -1
- package/dist/commands/run.js +42 -61
- package/dist/commands/serve.js +0 -4
- package/dist/platform/windows.js +12 -9
- package/dist/rpc-handler.js +42 -4
- package/dist/task.d.ts +6 -0
- package/dist/task.js +12 -0
- package/dist/update-checker.d.ts +0 -8
- package/dist/update-checker.js +0 -36
- package/dist/user-input.d.ts +15 -0
- package/dist/user-input.js +50 -0
- package/package.json +1 -1
- package/src/commands/mcpserver.ts +41 -1
- package/src/commands/run.ts +46 -79
- package/src/commands/serve.ts +0 -5
- package/src/platform/windows.ts +16 -10
- package/src/rpc-handler.ts +54 -4
- package/src/task.ts +17 -0
- package/src/update-checker.ts +0 -36
- package/src/user-input.ts +67 -0
|
@@ -4,6 +4,8 @@ import { z } from "zod";
|
|
|
4
4
|
import { StringCodec } from "nats";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
+
import { getTaskDir, parseTaskFile } from "../task.js";
|
|
8
|
+
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
7
9
|
export async function mcpserverCommand() {
|
|
8
10
|
const config = loadConfig();
|
|
9
11
|
const nc = await connectNats(config);
|
|
@@ -12,7 +14,7 @@ export async function mcpserverCommand() {
|
|
|
12
14
|
// send-push-notification requires NATS — only register when server mode is enabled
|
|
13
15
|
if (nc) {
|
|
14
16
|
server.registerTool("send-push-notification", {
|
|
15
|
-
description: "Send a push notification to
|
|
17
|
+
description: "Send a push notification to the user",
|
|
16
18
|
inputSchema: {
|
|
17
19
|
title: z.string().describe("Notification title"),
|
|
18
20
|
body: z.string().describe("Notification body text"),
|
|
@@ -49,6 +51,37 @@ export async function mcpserverCommand() {
|
|
|
49
51
|
}
|
|
50
52
|
});
|
|
51
53
|
}
|
|
54
|
+
const taskId = process.env.PALMIER_TASK_ID;
|
|
55
|
+
if (taskId) {
|
|
56
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
57
|
+
const task = parseTaskFile(taskDir);
|
|
58
|
+
server.registerTool("request-user-input", {
|
|
59
|
+
description: "Request input from the user. The user will see the descriptions and can provide values or abort.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
descriptions: z.array(z.string()).describe("List of input descriptions to show the user"),
|
|
62
|
+
},
|
|
63
|
+
}, async (args) => {
|
|
64
|
+
try {
|
|
65
|
+
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, args.descriptions);
|
|
66
|
+
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
67
|
+
if (response === "aborted") {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: "User aborted the input request." }],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const lines = args.descriptions.map((desc, i) => `${desc}: ${response[i]}`).join("\n");
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: lines }],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: `Error requesting user input: ${err}` }],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
52
85
|
const transport = new StdioServerTransport();
|
|
53
86
|
await server.connect(transport);
|
|
54
87
|
// Graceful shutdown
|
package/dist/commands/run.js
CHANGED
|
@@ -4,22 +4,24 @@ 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 } from "../task.js";
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX, TASK_INPUT_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import { publishHostEvent } from "../events.js";
|
|
12
|
+
import { waitForUserInput, requestUserInput, publishInputResolved } from "../user-input.js";
|
|
12
13
|
/**
|
|
13
14
|
* Write a time-stamped RESULT file with frontmatter.
|
|
14
15
|
* Always generated, even for abort/fail.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Update an existing result file with the final outcome.
|
|
19
|
+
*/
|
|
20
|
+
function finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, runningState, startTime, endTime, output, reportFiles, requiredPermissions) {
|
|
18
21
|
const reportLine = reportFiles.length > 0 ? `\nreport_files: ${reportFiles.join(", ")}` : "";
|
|
19
22
|
const permLines = requiredPermissions.map((p) => `\nrequired_permission: ${p.name} | ${p.description}`).join("");
|
|
20
23
|
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskSnapshotName}${reportLine}${permLines}\n---\n${output}`;
|
|
21
24
|
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
22
|
-
return resultFileName;
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* Invoke the agent CLI with a retry loop for permissions and user input.
|
|
@@ -66,7 +68,7 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
|
66
68
|
// Input retry
|
|
67
69
|
const inputRequests = parseInputRequests(result.output);
|
|
68
70
|
if (outcome === "failed" && inputRequests.length > 0) {
|
|
69
|
-
const response = await requestUserInput(ctx.nc, ctx.config, ctx.task, ctx.taskDir, inputRequests);
|
|
71
|
+
const response = await requestUserInput(ctx.nc, ctx.config, ctx.taskId, ctx.task.frontmatter.name, ctx.taskDir, inputRequests);
|
|
70
72
|
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided");
|
|
71
73
|
if (response === "aborted") {
|
|
72
74
|
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
@@ -79,6 +81,18 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
|
79
81
|
return { output: result.output, outcome, reportFiles, requiredPermissions };
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Find an existing RESULT file with running_state=started (created by the RPC handler).
|
|
86
|
+
*/
|
|
87
|
+
function findStartedResultFile(taskDir) {
|
|
88
|
+
const files = fs.readdirSync(taskDir).filter((f) => f.startsWith("RESULT-") && f.endsWith(".md"));
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const content = fs.readFileSync(path.join(taskDir, file), "utf-8");
|
|
91
|
+
if (content.includes("running_state: started"))
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
82
96
|
/**
|
|
83
97
|
* If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
|
|
84
98
|
* respect that instead of overwriting with the process's own outcome.
|
|
@@ -98,20 +112,28 @@ export async function runCommand(taskId) {
|
|
|
98
112
|
const task = parseTaskFile(taskDir);
|
|
99
113
|
console.log(`Running task: ${taskId}`);
|
|
100
114
|
let nc;
|
|
101
|
-
const startTime = Date.now();
|
|
102
115
|
const taskName = task.frontmatter.name;
|
|
116
|
+
// Check for an existing "started" result file (created by the RPC handler)
|
|
117
|
+
const existingResult = findStartedResultFile(taskDir);
|
|
118
|
+
const startTime = existingResult ? parseInt(existingResult.replace("RESULT-", "").replace(".md", ""), 10) : Date.now();
|
|
119
|
+
const resultFileName = existingResult ?? createResultFile(taskDir, taskName, startTime);
|
|
103
120
|
// Snapshot the task file at run time
|
|
104
121
|
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
105
|
-
fs.
|
|
122
|
+
if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
|
|
123
|
+
fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
|
|
124
|
+
}
|
|
106
125
|
const cleanup = async () => {
|
|
107
126
|
if (nc && !nc.isClosed()) {
|
|
108
127
|
await nc.drain();
|
|
109
128
|
}
|
|
110
129
|
};
|
|
130
|
+
if (!existingResult) {
|
|
131
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
132
|
+
}
|
|
111
133
|
try {
|
|
112
134
|
nc = await connectNats(config);
|
|
113
135
|
// Mark as started immediately
|
|
114
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName);
|
|
136
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, resultFileName);
|
|
115
137
|
// If requires_confirmation, notify clients and wait
|
|
116
138
|
if (task.frontmatter.requires_confirmation) {
|
|
117
139
|
const confirmed = await requestConfirmation(nc, config, task, taskDir);
|
|
@@ -120,9 +142,8 @@ export async function runCommand(taskId) {
|
|
|
120
142
|
if (!confirmed) {
|
|
121
143
|
console.log("Task aborted by user.");
|
|
122
144
|
const endTime = Date.now();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName);
|
|
145
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, "aborted", startTime, endTime, "", [], []);
|
|
146
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
126
147
|
await cleanup();
|
|
127
148
|
return;
|
|
128
149
|
}
|
|
@@ -139,24 +160,23 @@ export async function runCommand(taskId) {
|
|
|
139
160
|
// Command-triggered mode
|
|
140
161
|
const result = await runCommandTriggeredMode(ctx);
|
|
141
162
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName);
|
|
163
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, result.endTime, result.output, [], []);
|
|
164
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
145
165
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
146
166
|
}
|
|
147
167
|
else {
|
|
148
168
|
// Standard execution
|
|
149
169
|
const result = await invokeAgentWithRetry(ctx, task);
|
|
150
170
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName);
|
|
171
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, Date.now(), result.output, result.reportFiles, result.requiredPermissions);
|
|
172
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
154
173
|
if (result.reportFiles.length > 0) {
|
|
155
174
|
await publishHostEvent(nc, config.hostId, taskId, {
|
|
156
175
|
event_type: "report-generated",
|
|
157
176
|
name: taskName,
|
|
158
177
|
report_files: result.reportFiles,
|
|
159
178
|
running_state: outcome,
|
|
179
|
+
result_file: resultFileName,
|
|
160
180
|
});
|
|
161
181
|
}
|
|
162
182
|
console.log(`Task ${taskId} completed.`);
|
|
@@ -167,9 +187,8 @@ export async function runCommand(taskId) {
|
|
|
167
187
|
const endTime = Date.now();
|
|
168
188
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
169
189
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName);
|
|
190
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, endTime, errorMsg, [], []);
|
|
191
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
173
192
|
process.exitCode = 1;
|
|
174
193
|
}
|
|
175
194
|
finally {
|
|
@@ -302,7 +321,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
302
321
|
].join("\n");
|
|
303
322
|
return { outcome: "finished", endTime, output: summary };
|
|
304
323
|
}
|
|
305
|
-
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName) {
|
|
324
|
+
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName, resultFile) {
|
|
306
325
|
writeTaskStatus(taskDir, {
|
|
307
326
|
running_state: eventType,
|
|
308
327
|
time_stamp: Date.now(),
|
|
@@ -310,6 +329,8 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
|
|
|
310
329
|
const payload = { event_type: "running-state", running_state: eventType };
|
|
311
330
|
if (taskName)
|
|
312
331
|
payload.name = taskName;
|
|
332
|
+
if (resultFile)
|
|
333
|
+
payload.result_file = resultFile;
|
|
313
334
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
314
335
|
}
|
|
315
336
|
/**
|
|
@@ -322,22 +343,6 @@ async function publishConfirmResolved(nc, config, taskId, status) {
|
|
|
322
343
|
status,
|
|
323
344
|
});
|
|
324
345
|
}
|
|
325
|
-
/**
|
|
326
|
-
* Watch status.json until user_input is populated by an RPC call, then resolve.
|
|
327
|
-
* All interactive request flows (confirmation, permission, user input) share this.
|
|
328
|
-
*/
|
|
329
|
-
function waitForUserInput(taskDir) {
|
|
330
|
-
const statusPath = path.join(taskDir, "status.json");
|
|
331
|
-
return new Promise((resolve) => {
|
|
332
|
-
const watcher = fs.watch(statusPath, () => {
|
|
333
|
-
const status = readTaskStatus(taskDir);
|
|
334
|
-
if (!status || !status.user_input?.length)
|
|
335
|
-
return;
|
|
336
|
-
watcher.close();
|
|
337
|
-
resolve(status.user_input);
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
346
|
async function requestPermission(nc, config, task, taskDir, requiredPermissions) {
|
|
342
347
|
const currentStatus = readTaskStatus(taskDir);
|
|
343
348
|
writeTaskStatus(taskDir, { ...currentStatus, pending_permission: requiredPermissions });
|
|
@@ -362,30 +367,6 @@ async function publishPermissionResolved(nc, config, taskId, status) {
|
|
|
362
367
|
status,
|
|
363
368
|
});
|
|
364
369
|
}
|
|
365
|
-
async function requestUserInput(nc, config, task, taskDir, inputDescriptions) {
|
|
366
|
-
const currentStatus = readTaskStatus(taskDir);
|
|
367
|
-
writeTaskStatus(taskDir, { ...currentStatus, pending_input: inputDescriptions });
|
|
368
|
-
await publishHostEvent(nc, config.hostId, task.frontmatter.id, {
|
|
369
|
-
event_type: "input-request",
|
|
370
|
-
host_id: config.hostId,
|
|
371
|
-
input_descriptions: inputDescriptions,
|
|
372
|
-
name: task.frontmatter.name,
|
|
373
|
-
});
|
|
374
|
-
const userInput = await waitForUserInput(taskDir);
|
|
375
|
-
if (userInput.length === 1 && userInput[0] === "aborted") {
|
|
376
|
-
writeTaskStatus(taskDir, { running_state: "aborted", time_stamp: Date.now() });
|
|
377
|
-
return "aborted";
|
|
378
|
-
}
|
|
379
|
-
writeTaskStatus(taskDir, { running_state: "started", time_stamp: Date.now() });
|
|
380
|
-
return userInput;
|
|
381
|
-
}
|
|
382
|
-
async function publishInputResolved(nc, config, taskId, status) {
|
|
383
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
384
|
-
event_type: "input-resolved",
|
|
385
|
-
host_id: config.hostId,
|
|
386
|
-
status,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
370
|
async function requestConfirmation(nc, config, task, taskDir) {
|
|
390
371
|
const currentStatus = readTaskStatus(taskDir);
|
|
391
372
|
writeTaskStatus(taskDir, { ...currentStatus, pending_confirmation: true });
|
package/dist/commands/serve.js
CHANGED
|
@@ -7,7 +7,6 @@ import { startNatsTransport } from "../transports/nats-transport.js";
|
|
|
7
7
|
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } 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 { CONFIG_DIR } from "../config.js";
|
|
@@ -88,9 +87,6 @@ export async function serveCommand() {
|
|
|
88
87
|
console.error("[monitor] Error checking stale tasks:", err);
|
|
89
88
|
});
|
|
90
89
|
}, POLL_INTERVAL_MS);
|
|
91
|
-
// Check for updates on startup and every 24 hours
|
|
92
|
-
checkForUpdate().catch(() => { });
|
|
93
|
-
setInterval(() => { checkForUpdate().catch(() => { }); }, 24 * 60 * 60 * 1000);
|
|
94
90
|
const handleRpc = createRpcHandler(config, nc);
|
|
95
91
|
await startNatsTransport(config, handleRpc, nc);
|
|
96
92
|
}
|
package/dist/platform/windows.js
CHANGED
|
@@ -110,31 +110,34 @@ export class WindowsPlatform {
|
|
|
110
110
|
const oldPid = fs.existsSync(DAEMON_PID_FILE)
|
|
111
111
|
? fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim()
|
|
112
112
|
: null;
|
|
113
|
-
// Spawn the new daemon before killing the old one.
|
|
114
|
-
this.spawnDaemon(script);
|
|
115
113
|
if (oldPid && oldPid === String(process.pid)) {
|
|
116
|
-
// We ARE the old daemon (auto-update) —
|
|
114
|
+
// We ARE the old daemon (auto-update) — spawn replacement then exit.
|
|
115
|
+
this.spawnDaemon(script);
|
|
117
116
|
process.exit(0);
|
|
118
117
|
}
|
|
119
|
-
|
|
118
|
+
// Kill old daemon first, then spawn new one.
|
|
119
|
+
if (oldPid) {
|
|
120
120
|
try {
|
|
121
|
-
execFileSync("taskkill", ["/pid", oldPid, "/
|
|
121
|
+
execFileSync("taskkill", ["/pid", oldPid, "/f"], { windowsHide: true, stdio: "pipe" });
|
|
122
122
|
}
|
|
123
123
|
catch {
|
|
124
124
|
// Process may have already exited
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
this.spawnDaemon(script);
|
|
127
128
|
}
|
|
128
129
|
spawnDaemon(script) {
|
|
129
|
-
|
|
130
|
+
// Write a VBS launcher that starts the daemon with no visible console window.
|
|
131
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
132
|
+
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
133
|
+
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
134
|
+
const child = nodeSpawn(wscript, [DAEMON_VBS_FILE], {
|
|
130
135
|
detached: true,
|
|
131
136
|
stdio: "ignore",
|
|
132
137
|
windowsHide: true,
|
|
133
138
|
});
|
|
134
|
-
if (child.pid) {
|
|
135
|
-
fs.writeFileSync(DAEMON_PID_FILE, String(child.pid), "utf-8");
|
|
136
|
-
}
|
|
137
139
|
child.unref();
|
|
140
|
+
// PID file will be written by the serve command itself when it starts.
|
|
138
141
|
console.log("Palmier daemon started.");
|
|
139
142
|
}
|
|
140
143
|
installTaskTimer(config, task) {
|
package/dist/rpc-handler.js
CHANGED
|
@@ -2,14 +2,15 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
+
import { spawn } from "child_process";
|
|
5
6
|
import { parse as parseYaml } from "yaml";
|
|
6
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList } from "./task.js";
|
|
7
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createResultFile } from "./task.js";
|
|
7
8
|
import { getPlatform } from "./platform/index.js";
|
|
8
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
9
10
|
import { getAgent } from "./agents/agent.js";
|
|
10
11
|
import { validateSession } from "./session-store.js";
|
|
11
12
|
import { publishHostEvent } from "./events.js";
|
|
12
|
-
import { currentVersion,
|
|
13
|
+
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
13
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
15
16
|
/**
|
|
@@ -108,7 +109,6 @@ export function createRpcHandler(config, nc) {
|
|
|
108
109
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
109
110
|
agents: config.agents ?? [],
|
|
110
111
|
version: currentVersion,
|
|
111
|
-
latest_version: getLatestVersion(),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
case "task.create": {
|
|
@@ -205,11 +205,49 @@ export function createRpcHandler(config, nc) {
|
|
|
205
205
|
removeFromTaskList(config.projectRoot, params.id);
|
|
206
206
|
return { ok: true, task_id: params.id };
|
|
207
207
|
}
|
|
208
|
+
case "task.run_oneoff": {
|
|
209
|
+
const params = request.params;
|
|
210
|
+
const id = randomUUID();
|
|
211
|
+
const taskDir = getTaskDir(config.projectRoot, id);
|
|
212
|
+
const name = params.user_prompt.slice(0, 60);
|
|
213
|
+
const task = {
|
|
214
|
+
frontmatter: {
|
|
215
|
+
id,
|
|
216
|
+
name,
|
|
217
|
+
user_prompt: params.user_prompt,
|
|
218
|
+
agent: params.agent,
|
|
219
|
+
triggers: [],
|
|
220
|
+
triggers_enabled: false,
|
|
221
|
+
requires_confirmation: params.requires_confirmation ?? false,
|
|
222
|
+
...(params.command ? { command: params.command } : {}),
|
|
223
|
+
},
|
|
224
|
+
body: "",
|
|
225
|
+
};
|
|
226
|
+
writeTaskFile(taskDir, task);
|
|
227
|
+
// Do NOT append to tasks.jsonl — this is a one-off run
|
|
228
|
+
// Create initial result file so it appears in runs list immediately
|
|
229
|
+
const resultFileName = createResultFile(taskDir, name, Date.now());
|
|
230
|
+
appendHistory(config.projectRoot, { task_id: id, result_file: resultFileName });
|
|
231
|
+
// Spawn `palmier run <id>` directly as a detached process
|
|
232
|
+
const script = process.argv[1] || "palmier";
|
|
233
|
+
const child = spawn(process.execPath, [script, "run", id], {
|
|
234
|
+
detached: true,
|
|
235
|
+
stdio: "ignore",
|
|
236
|
+
windowsHide: true,
|
|
237
|
+
});
|
|
238
|
+
child.unref();
|
|
239
|
+
return { ok: true, task_id: id, result_file: resultFileName };
|
|
240
|
+
}
|
|
208
241
|
case "task.run": {
|
|
209
242
|
const params = request.params;
|
|
210
243
|
try {
|
|
244
|
+
// Create initial result file so it appears in runs list immediately
|
|
245
|
+
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
246
|
+
const runTask = parseTaskFile(runTaskDir);
|
|
247
|
+
const runResultFileName = createResultFile(runTaskDir, runTask.frontmatter.name, Date.now());
|
|
248
|
+
appendHistory(config.projectRoot, { task_id: params.id, result_file: runResultFileName });
|
|
211
249
|
await getPlatform().startTask(params.id);
|
|
212
|
-
return { ok: true, task_id: params.id };
|
|
250
|
+
return { ok: true, task_id: params.id, result_file: runResultFileName };
|
|
213
251
|
}
|
|
214
252
|
catch (err) {
|
|
215
253
|
const e = err;
|
package/dist/task.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export declare function writeTaskStatus(taskDir: string, status: TaskStatus): vo
|
|
|
38
38
|
* Returns undefined if the file doesn't exist.
|
|
39
39
|
*/
|
|
40
40
|
export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Create the initial result file when a task starts running.
|
|
43
|
+
* Contains only start_time and running_state=started; no end_time or content yet.
|
|
44
|
+
* Returns the result file name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createResultFile(taskDir: string, taskName: string, startTime: number): string;
|
|
41
47
|
/**
|
|
42
48
|
* Append a history entry to the project-level history.jsonl file.
|
|
43
49
|
*/
|
package/dist/task.js
CHANGED
|
@@ -129,6 +129,18 @@ export function readTaskStatus(taskDir) {
|
|
|
129
129
|
return undefined;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Create the initial result file when a task starts running.
|
|
134
|
+
* Contains only start_time and running_state=started; no end_time or content yet.
|
|
135
|
+
* Returns the result file name.
|
|
136
|
+
*/
|
|
137
|
+
export function createResultFile(taskDir, taskName, startTime) {
|
|
138
|
+
const resultFileName = `RESULT-${startTime}.md`;
|
|
139
|
+
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
140
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: started\nstart_time: ${startTime}\ntask_file: ${taskSnapshotName}\n---\n`;
|
|
141
|
+
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
142
|
+
return resultFileName;
|
|
143
|
+
}
|
|
132
144
|
/**
|
|
133
145
|
* Append a history entry to the project-level history.jsonl file.
|
|
134
146
|
*/
|
package/dist/update-checker.d.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/** True when running from a source checkout (has .git) rather than a global npm install. */
|
|
2
2
|
export declare const isDevBuild: boolean;
|
|
3
3
|
export declare const currentVersion: string;
|
|
4
|
-
/**
|
|
5
|
-
* Check the npm registry for the latest version of palmier.
|
|
6
|
-
*/
|
|
7
|
-
export declare function checkForUpdate(): Promise<void>;
|
|
8
|
-
/**
|
|
9
|
-
* Get the latest version from npm, or null if not yet checked.
|
|
10
|
-
*/
|
|
11
|
-
export declare function getLatestVersion(): string | null;
|
|
12
4
|
/**
|
|
13
5
|
* Run the update and restart the daemon.
|
|
14
6
|
* Returns an error message if the update fails.
|
package/dist/update-checker.js
CHANGED
|
@@ -9,41 +9,6 @@ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "
|
|
|
9
9
|
/** True when running from a source checkout (has .git) rather than a global npm install. */
|
|
10
10
|
export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
|
|
11
11
|
export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
|
|
12
|
-
let latestVersion = null;
|
|
13
|
-
let lastCheckTime = 0;
|
|
14
|
-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15
|
-
/**
|
|
16
|
-
* Check the npm registry for the latest version of palmier.
|
|
17
|
-
*/
|
|
18
|
-
export async function checkForUpdate() {
|
|
19
|
-
if (isDevBuild)
|
|
20
|
-
return;
|
|
21
|
-
const now = Date.now();
|
|
22
|
-
if (now - lastCheckTime < CHECK_INTERVAL_MS)
|
|
23
|
-
return;
|
|
24
|
-
lastCheckTime = now;
|
|
25
|
-
try {
|
|
26
|
-
const res = await fetch("https://registry.npmjs.org/palmier/latest", {
|
|
27
|
-
signal: AbortSignal.timeout(10_000),
|
|
28
|
-
});
|
|
29
|
-
if (!res.ok)
|
|
30
|
-
return;
|
|
31
|
-
const data = (await res.json());
|
|
32
|
-
if (data.version) {
|
|
33
|
-
latestVersion = data.version;
|
|
34
|
-
console.log(`[update] Latest version: ${data.version} (current: ${currentVersion})`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// Network errors are expected (offline, etc.)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Get the latest version from npm, or null if not yet checked.
|
|
43
|
-
*/
|
|
44
|
-
export function getLatestVersion() {
|
|
45
|
-
return latestVersion;
|
|
46
|
-
}
|
|
47
12
|
/**
|
|
48
13
|
* Run the update and restart the daemon.
|
|
49
14
|
* Returns an error message if the update fails.
|
|
@@ -60,7 +25,6 @@ export async function performUpdate() {
|
|
|
60
25
|
return `Update failed. Please run manually:\nnpm update -g palmier`;
|
|
61
26
|
}
|
|
62
27
|
console.log("[update] Update installed, restarting daemon...");
|
|
63
|
-
latestVersion = null;
|
|
64
28
|
// Small delay to allow the RPC response to be sent
|
|
65
29
|
setTimeout(() => {
|
|
66
30
|
getPlatform().restartDaemon().catch((err) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HostConfig } from "./types.js";
|
|
2
|
+
import type { NatsConnection } from "nats";
|
|
3
|
+
/**
|
|
4
|
+
* Watch status.json until user_input is populated by an RPC call, then resolve.
|
|
5
|
+
*/
|
|
6
|
+
export declare function waitForUserInput(taskDir: string): Promise<string[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Send an input-request event and wait for the user's response.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestUserInput(nc: NatsConnection | undefined, config: HostConfig, taskId: string, taskName: string, taskDir: string, inputDescriptions: string[]): Promise<string[] | "aborted">;
|
|
11
|
+
/**
|
|
12
|
+
* Notify clients that an input request has been resolved.
|
|
13
|
+
*/
|
|
14
|
+
export declare function publishInputResolved(nc: NatsConnection | undefined, config: HostConfig, taskId: string, status: "provided" | "aborted"): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=user-input.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { readTaskStatus, writeTaskStatus } from "./task.js";
|
|
4
|
+
import { publishHostEvent } from "./events.js";
|
|
5
|
+
/**
|
|
6
|
+
* Watch status.json until user_input is populated by an RPC call, then resolve.
|
|
7
|
+
*/
|
|
8
|
+
export function waitForUserInput(taskDir) {
|
|
9
|
+
const statusPath = path.join(taskDir, "status.json");
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const watcher = fs.watch(statusPath, () => {
|
|
12
|
+
const status = readTaskStatus(taskDir);
|
|
13
|
+
if (!status || !status.user_input?.length)
|
|
14
|
+
return;
|
|
15
|
+
watcher.close();
|
|
16
|
+
resolve(status.user_input);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Send an input-request event and wait for the user's response.
|
|
22
|
+
*/
|
|
23
|
+
export async function requestUserInput(nc, config, taskId, taskName, taskDir, inputDescriptions) {
|
|
24
|
+
const currentStatus = readTaskStatus(taskDir);
|
|
25
|
+
writeTaskStatus(taskDir, { ...currentStatus, pending_input: inputDescriptions });
|
|
26
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
27
|
+
event_type: "input-request",
|
|
28
|
+
host_id: config.hostId,
|
|
29
|
+
input_descriptions: inputDescriptions,
|
|
30
|
+
name: taskName,
|
|
31
|
+
});
|
|
32
|
+
const userInput = await waitForUserInput(taskDir);
|
|
33
|
+
if (userInput.length === 1 && userInput[0] === "aborted") {
|
|
34
|
+
writeTaskStatus(taskDir, { running_state: "aborted", time_stamp: Date.now() });
|
|
35
|
+
return "aborted";
|
|
36
|
+
}
|
|
37
|
+
writeTaskStatus(taskDir, { running_state: "started", time_stamp: Date.now() });
|
|
38
|
+
return userInput;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Notify clients that an input request has been resolved.
|
|
42
|
+
*/
|
|
43
|
+
export async function publishInputResolved(nc, config, taskId, status) {
|
|
44
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
45
|
+
event_type: "input-resolved",
|
|
46
|
+
host_id: config.hostId,
|
|
47
|
+
status,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=user-input.js.map
|
package/package.json
CHANGED
|
@@ -4,6 +4,8 @@ import { z } from "zod";
|
|
|
4
4
|
import { StringCodec } from "nats";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
+
import { getTaskDir, parseTaskFile } from "../task.js";
|
|
8
|
+
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
7
9
|
export async function mcpserverCommand(): Promise<void> {
|
|
8
10
|
const config = loadConfig();
|
|
9
11
|
const nc = await connectNats(config);
|
|
@@ -20,7 +22,7 @@ export async function mcpserverCommand(): Promise<void> {
|
|
|
20
22
|
server.registerTool(
|
|
21
23
|
"send-push-notification",
|
|
22
24
|
{
|
|
23
|
-
description: "Send a push notification to
|
|
25
|
+
description: "Send a push notification to the user",
|
|
24
26
|
inputSchema: {
|
|
25
27
|
title: z.string().describe("Notification title"),
|
|
26
28
|
body: z.string().describe("Notification body text"),
|
|
@@ -63,6 +65,44 @@ export async function mcpserverCommand(): Promise<void> {
|
|
|
63
65
|
);
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
const taskId = process.env.PALMIER_TASK_ID;
|
|
69
|
+
if (taskId) {
|
|
70
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
71
|
+
const task = parseTaskFile(taskDir);
|
|
72
|
+
|
|
73
|
+
server.registerTool(
|
|
74
|
+
"request-user-input",
|
|
75
|
+
{
|
|
76
|
+
description: "Request input from the user. The user will see the descriptions and can provide values or abort.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
descriptions: z.array(z.string()).describe("List of input descriptions to show the user"),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
async (args) => {
|
|
82
|
+
try {
|
|
83
|
+
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, args.descriptions);
|
|
84
|
+
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
85
|
+
|
|
86
|
+
if (response === "aborted") {
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text" as const, text: "User aborted the input request." }],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const lines = args.descriptions.map((desc: string, i: number) => `${desc}: ${response[i]}`).join("\n");
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text" as const, text: lines }],
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text" as const, text: `Error requesting user input: ${err}` }],
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
66
106
|
const transport = new StdioServerTransport();
|
|
67
107
|
await server.connect(transport);
|
|
68
108
|
|
package/src/commands/run.ts
CHANGED
|
@@ -4,12 +4,13 @@ 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 } from "../task.js";
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX, TASK_INPUT_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, requestUserInput, publishInputResolved } from "../user-input.js";
|
|
13
14
|
import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
|
|
14
15
|
import type { NatsConnection } from "nats";
|
|
15
16
|
|
|
@@ -17,8 +18,13 @@ import type { NatsConnection } from "nats";
|
|
|
17
18
|
* Write a time-stamped RESULT file with frontmatter.
|
|
18
19
|
* Always generated, even for abort/fail.
|
|
19
20
|
*/
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Update an existing result file with the final outcome.
|
|
24
|
+
*/
|
|
25
|
+
function finalizeResultFile(
|
|
21
26
|
taskDir: string,
|
|
27
|
+
resultFileName: string,
|
|
22
28
|
taskName: string,
|
|
23
29
|
taskSnapshotName: string,
|
|
24
30
|
runningState: string,
|
|
@@ -27,13 +33,11 @@ function writeResult(
|
|
|
27
33
|
output: string,
|
|
28
34
|
reportFiles: string[],
|
|
29
35
|
requiredPermissions: RequiredPermission[],
|
|
30
|
-
):
|
|
31
|
-
const resultFileName = `RESULT-${endTime}.md`;
|
|
36
|
+
): void {
|
|
32
37
|
const reportLine = reportFiles.length > 0 ? `\nreport_files: ${reportFiles.join(", ")}` : "";
|
|
33
38
|
const permLines = requiredPermissions.map((p) => `\nrequired_permission: ${p.name} | ${p.description}`).join("");
|
|
34
39
|
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskSnapshotName}${reportLine}${permLines}\n---\n${output}`;
|
|
35
40
|
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
36
|
-
return resultFileName;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -115,7 +119,7 @@ async function invokeAgentWithRetry(
|
|
|
115
119
|
// Input retry
|
|
116
120
|
const inputRequests = parseInputRequests(result.output);
|
|
117
121
|
if (outcome === "failed" && inputRequests.length > 0) {
|
|
118
|
-
const response = await requestUserInput(ctx.nc, ctx.config, ctx.task, ctx.taskDir, inputRequests);
|
|
122
|
+
const response = await requestUserInput(ctx.nc, ctx.config, ctx.taskId, ctx.task.frontmatter.name, ctx.taskDir, inputRequests);
|
|
119
123
|
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided");
|
|
120
124
|
|
|
121
125
|
if (response === "aborted") {
|
|
@@ -132,6 +136,18 @@ async function invokeAgentWithRetry(
|
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Find an existing RESULT file with running_state=started (created by the RPC handler).
|
|
141
|
+
*/
|
|
142
|
+
function findStartedResultFile(taskDir: string): string | null {
|
|
143
|
+
const files = fs.readdirSync(taskDir).filter((f) => f.startsWith("RESULT-") && f.endsWith(".md"));
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const content = fs.readFileSync(path.join(taskDir, file), "utf-8");
|
|
146
|
+
if (content.includes("running_state: started")) return file;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
/**
|
|
136
152
|
* If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
|
|
137
153
|
* respect that instead of overwriting with the process's own outcome.
|
|
@@ -152,12 +168,18 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
152
168
|
console.log(`Running task: ${taskId}`);
|
|
153
169
|
|
|
154
170
|
let nc: NatsConnection | undefined;
|
|
155
|
-
const startTime = Date.now();
|
|
156
171
|
const taskName = task.frontmatter.name;
|
|
157
172
|
|
|
173
|
+
// Check for an existing "started" result file (created by the RPC handler)
|
|
174
|
+
const existingResult = findStartedResultFile(taskDir);
|
|
175
|
+
const startTime = existingResult ? parseInt(existingResult.replace("RESULT-", "").replace(".md", ""), 10) : Date.now();
|
|
176
|
+
const resultFileName = existingResult ?? createResultFile(taskDir, taskName, startTime);
|
|
177
|
+
|
|
158
178
|
// Snapshot the task file at run time
|
|
159
179
|
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
160
|
-
fs.
|
|
180
|
+
if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
|
|
181
|
+
fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
|
|
182
|
+
}
|
|
161
183
|
|
|
162
184
|
const cleanup = async () => {
|
|
163
185
|
if (nc && !nc.isClosed()) {
|
|
@@ -165,11 +187,15 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
165
187
|
}
|
|
166
188
|
};
|
|
167
189
|
|
|
190
|
+
if (!existingResult) {
|
|
191
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
192
|
+
}
|
|
193
|
+
|
|
168
194
|
try {
|
|
169
195
|
nc = await connectNats(config);
|
|
170
196
|
|
|
171
197
|
// Mark as started immediately
|
|
172
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName);
|
|
198
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, resultFileName);
|
|
173
199
|
|
|
174
200
|
// If requires_confirmation, notify clients and wait
|
|
175
201
|
if (task.frontmatter.requires_confirmation) {
|
|
@@ -179,9 +205,8 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
179
205
|
if (!confirmed) {
|
|
180
206
|
console.log("Task aborted by user.");
|
|
181
207
|
const endTime = Date.now();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName);
|
|
208
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, "aborted", startTime, endTime, "", [], []);
|
|
209
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
185
210
|
await cleanup();
|
|
186
211
|
return;
|
|
187
212
|
}
|
|
@@ -200,21 +225,16 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
200
225
|
// Command-triggered mode
|
|
201
226
|
const result = await runCommandTriggeredMode(ctx);
|
|
202
227
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
startTime, result.endTime, result.output, [], [],
|
|
206
|
-
);
|
|
207
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
208
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName);
|
|
228
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, result.endTime, result.output, [], []);
|
|
229
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
209
230
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
210
231
|
} else {
|
|
211
232
|
// Standard execution
|
|
212
233
|
const result = await invokeAgentWithRetry(ctx, task);
|
|
213
234
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
214
235
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName);
|
|
236
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, Date.now(), result.output, result.reportFiles, result.requiredPermissions);
|
|
237
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
218
238
|
|
|
219
239
|
if (result.reportFiles.length > 0) {
|
|
220
240
|
await publishHostEvent(nc, config.hostId, taskId, {
|
|
@@ -222,6 +242,7 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
222
242
|
name: taskName,
|
|
223
243
|
report_files: result.reportFiles,
|
|
224
244
|
running_state: outcome,
|
|
245
|
+
result_file: resultFileName,
|
|
225
246
|
});
|
|
226
247
|
}
|
|
227
248
|
|
|
@@ -232,9 +253,8 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
232
253
|
const endTime = Date.now();
|
|
233
254
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
234
255
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName);
|
|
256
|
+
finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, endTime, errorMsg, [], []);
|
|
257
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
238
258
|
process.exitCode = 1;
|
|
239
259
|
} finally {
|
|
240
260
|
await cleanup();
|
|
@@ -388,6 +408,7 @@ async function publishTaskEvent(
|
|
|
388
408
|
taskId: string,
|
|
389
409
|
eventType: TaskRunningState,
|
|
390
410
|
taskName?: string,
|
|
411
|
+
resultFile?: string,
|
|
391
412
|
): Promise<void> {
|
|
392
413
|
writeTaskStatus(taskDir, {
|
|
393
414
|
running_state: eventType,
|
|
@@ -396,6 +417,7 @@ async function publishTaskEvent(
|
|
|
396
417
|
|
|
397
418
|
const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
|
|
398
419
|
if (taskName) payload.name = taskName;
|
|
420
|
+
if (resultFile) payload.result_file = resultFile;
|
|
399
421
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
400
422
|
}
|
|
401
423
|
|
|
@@ -415,22 +437,6 @@ async function publishConfirmResolved(
|
|
|
415
437
|
});
|
|
416
438
|
}
|
|
417
439
|
|
|
418
|
-
/**
|
|
419
|
-
* Watch status.json until user_input is populated by an RPC call, then resolve.
|
|
420
|
-
* All interactive request flows (confirmation, permission, user input) share this.
|
|
421
|
-
*/
|
|
422
|
-
function waitForUserInput(taskDir: string): Promise<string[]> {
|
|
423
|
-
const statusPath = path.join(taskDir, "status.json");
|
|
424
|
-
return new Promise<string[]>((resolve) => {
|
|
425
|
-
const watcher = fs.watch(statusPath, () => {
|
|
426
|
-
const status = readTaskStatus(taskDir);
|
|
427
|
-
if (!status || !status.user_input?.length) return;
|
|
428
|
-
watcher.close();
|
|
429
|
-
resolve(status.user_input);
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
440
|
async function requestPermission(
|
|
435
441
|
nc: NatsConnection | undefined,
|
|
436
442
|
config: HostConfig,
|
|
@@ -470,45 +476,6 @@ async function publishPermissionResolved(
|
|
|
470
476
|
});
|
|
471
477
|
}
|
|
472
478
|
|
|
473
|
-
async function requestUserInput(
|
|
474
|
-
nc: NatsConnection | undefined,
|
|
475
|
-
config: HostConfig,
|
|
476
|
-
task: ParsedTask,
|
|
477
|
-
taskDir: string,
|
|
478
|
-
inputDescriptions: string[],
|
|
479
|
-
): Promise<string[] | "aborted"> {
|
|
480
|
-
const currentStatus = readTaskStatus(taskDir)!;
|
|
481
|
-
writeTaskStatus(taskDir, { ...currentStatus, pending_input: inputDescriptions });
|
|
482
|
-
|
|
483
|
-
await publishHostEvent(nc, config.hostId, task.frontmatter.id, {
|
|
484
|
-
event_type: "input-request",
|
|
485
|
-
host_id: config.hostId,
|
|
486
|
-
input_descriptions: inputDescriptions,
|
|
487
|
-
name: task.frontmatter.name,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
const userInput = await waitForUserInput(taskDir);
|
|
491
|
-
if (userInput.length === 1 && userInput[0] === "aborted") {
|
|
492
|
-
writeTaskStatus(taskDir, { running_state: "aborted", time_stamp: Date.now() });
|
|
493
|
-
return "aborted";
|
|
494
|
-
}
|
|
495
|
-
writeTaskStatus(taskDir, { running_state: "started", time_stamp: Date.now() });
|
|
496
|
-
return userInput;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
async function publishInputResolved(
|
|
500
|
-
nc: NatsConnection | undefined,
|
|
501
|
-
config: HostConfig,
|
|
502
|
-
taskId: string,
|
|
503
|
-
status: "provided" | "aborted",
|
|
504
|
-
): Promise<void> {
|
|
505
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
506
|
-
event_type: "input-resolved",
|
|
507
|
-
host_id: config.hostId,
|
|
508
|
-
status,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
479
|
async function requestConfirmation(
|
|
513
480
|
nc: NatsConnection | undefined,
|
|
514
481
|
config: HostConfig,
|
package/src/commands/serve.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { startNatsTransport } from "../transports/nats-transport.js";
|
|
|
7
7
|
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } 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";
|
|
@@ -109,10 +108,6 @@ export async function serveCommand(): Promise<void> {
|
|
|
109
108
|
});
|
|
110
109
|
}, POLL_INTERVAL_MS);
|
|
111
110
|
|
|
112
|
-
// Check for updates on startup and every 24 hours
|
|
113
|
-
checkForUpdate().catch(() => {});
|
|
114
|
-
setInterval(() => { checkForUpdate().catch(() => {}); }, 24 * 60 * 60 * 1000);
|
|
115
|
-
|
|
116
111
|
const handleRpc = createRpcHandler(config, nc);
|
|
117
112
|
await startNatsTransport(config, handleRpc, nc);
|
|
118
113
|
}
|
package/src/platform/windows.ts
CHANGED
|
@@ -131,31 +131,37 @@ export class WindowsPlatform implements PlatformService {
|
|
|
131
131
|
? fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim()
|
|
132
132
|
: null;
|
|
133
133
|
|
|
134
|
-
// Spawn the new daemon before killing the old one.
|
|
135
|
-
this.spawnDaemon(script);
|
|
136
|
-
|
|
137
134
|
if (oldPid && oldPid === String(process.pid)) {
|
|
138
|
-
// We ARE the old daemon (auto-update) —
|
|
135
|
+
// We ARE the old daemon (auto-update) — spawn replacement then exit.
|
|
136
|
+
this.spawnDaemon(script);
|
|
139
137
|
process.exit(0);
|
|
140
|
-
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Kill old daemon first, then spawn new one.
|
|
141
|
+
if (oldPid) {
|
|
141
142
|
try {
|
|
142
|
-
execFileSync("taskkill", ["/pid", oldPid, "/
|
|
143
|
+
execFileSync("taskkill", ["/pid", oldPid, "/f"], { windowsHide: true, stdio: "pipe" });
|
|
143
144
|
} catch {
|
|
144
145
|
// Process may have already exited
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
|
|
149
|
+
this.spawnDaemon(script);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
private spawnDaemon(script: string): void {
|
|
150
|
-
|
|
153
|
+
// Write a VBS launcher that starts the daemon with no visible console window.
|
|
154
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
155
|
+
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
156
|
+
|
|
157
|
+
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
158
|
+
const child = nodeSpawn(wscript, [DAEMON_VBS_FILE], {
|
|
151
159
|
detached: true,
|
|
152
160
|
stdio: "ignore",
|
|
153
161
|
windowsHide: true,
|
|
154
162
|
});
|
|
155
|
-
if (child.pid) {
|
|
156
|
-
fs.writeFileSync(DAEMON_PID_FILE, String(child.pid), "utf-8");
|
|
157
|
-
}
|
|
158
163
|
child.unref();
|
|
164
|
+
// PID file will be written by the serve command itself when it starts.
|
|
159
165
|
console.log("Palmier daemon started.");
|
|
160
166
|
}
|
|
161
167
|
|
package/src/rpc-handler.ts
CHANGED
|
@@ -2,15 +2,16 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
+
import { spawn } from "child_process";
|
|
5
6
|
import { parse as parseYaml } from "yaml";
|
|
6
7
|
import { type NatsConnection } from "nats";
|
|
7
|
-
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList } from "./task.js";
|
|
8
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createResultFile } from "./task.js";
|
|
8
9
|
import { getPlatform } from "./platform/index.js";
|
|
9
10
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
11
|
import { getAgent } from "./agents/agent.js";
|
|
11
12
|
import { validateSession } from "./session-store.js";
|
|
12
13
|
import { publishHostEvent } from "./events.js";
|
|
13
|
-
import { currentVersion,
|
|
14
|
+
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
14
15
|
import type { HostConfig, ParsedTask, RpcMessage } from "./types.js";
|
|
15
16
|
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -123,7 +124,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
123
124
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
124
125
|
agents: config.agents ?? [],
|
|
125
126
|
version: currentVersion,
|
|
126
|
-
latest_version: getLatestVersion(),
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -243,11 +243,61 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
243
243
|
return { ok: true, task_id: params.id };
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
case "task.run_oneoff": {
|
|
247
|
+
const params = request.params as {
|
|
248
|
+
user_prompt: string;
|
|
249
|
+
agent: string;
|
|
250
|
+
requires_confirmation?: boolean;
|
|
251
|
+
command?: string;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const id = randomUUID();
|
|
255
|
+
const taskDir = getTaskDir(config.projectRoot, id);
|
|
256
|
+
const name = params.user_prompt.slice(0, 60);
|
|
257
|
+
const task: ParsedTask = {
|
|
258
|
+
frontmatter: {
|
|
259
|
+
id,
|
|
260
|
+
name,
|
|
261
|
+
user_prompt: params.user_prompt,
|
|
262
|
+
agent: params.agent,
|
|
263
|
+
triggers: [],
|
|
264
|
+
triggers_enabled: false,
|
|
265
|
+
requires_confirmation: params.requires_confirmation ?? false,
|
|
266
|
+
...(params.command ? { command: params.command } : {}),
|
|
267
|
+
},
|
|
268
|
+
body: "",
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
writeTaskFile(taskDir, task);
|
|
272
|
+
// Do NOT append to tasks.jsonl — this is a one-off run
|
|
273
|
+
|
|
274
|
+
// Create initial result file so it appears in runs list immediately
|
|
275
|
+
const resultFileName = createResultFile(taskDir, name, Date.now());
|
|
276
|
+
appendHistory(config.projectRoot, { task_id: id, result_file: resultFileName });
|
|
277
|
+
|
|
278
|
+
// Spawn `palmier run <id>` directly as a detached process
|
|
279
|
+
const script = process.argv[1] || "palmier";
|
|
280
|
+
const child = spawn(process.execPath, [script, "run", id], {
|
|
281
|
+
detached: true,
|
|
282
|
+
stdio: "ignore",
|
|
283
|
+
windowsHide: true,
|
|
284
|
+
});
|
|
285
|
+
child.unref();
|
|
286
|
+
|
|
287
|
+
return { ok: true, task_id: id, result_file: resultFileName };
|
|
288
|
+
}
|
|
289
|
+
|
|
246
290
|
case "task.run": {
|
|
247
291
|
const params = request.params as { id: string };
|
|
248
292
|
try {
|
|
293
|
+
// Create initial result file so it appears in runs list immediately
|
|
294
|
+
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
295
|
+
const runTask = parseTaskFile(runTaskDir);
|
|
296
|
+
const runResultFileName = createResultFile(runTaskDir, runTask.frontmatter.name, Date.now());
|
|
297
|
+
appendHistory(config.projectRoot, { task_id: params.id, result_file: runResultFileName });
|
|
298
|
+
|
|
249
299
|
await getPlatform().startTask(params.id);
|
|
250
|
-
return { ok: true, task_id: params.id };
|
|
300
|
+
return { ok: true, task_id: params.id, result_file: runResultFileName };
|
|
251
301
|
} catch (err: unknown) {
|
|
252
302
|
const e = err as { stderr?: string; message?: string };
|
|
253
303
|
console.error(`task.run failed for ${params.id}: ${e.stderr || e.message}`);
|
package/src/task.ts
CHANGED
|
@@ -147,6 +147,23 @@ export function readTaskStatus(taskDir: string): TaskStatus | undefined {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Create the initial result file when a task starts running.
|
|
152
|
+
* Contains only start_time and running_state=started; no end_time or content yet.
|
|
153
|
+
* Returns the result file name.
|
|
154
|
+
*/
|
|
155
|
+
export function createResultFile(
|
|
156
|
+
taskDir: string,
|
|
157
|
+
taskName: string,
|
|
158
|
+
startTime: number,
|
|
159
|
+
): string {
|
|
160
|
+
const resultFileName = `RESULT-${startTime}.md`;
|
|
161
|
+
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
162
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: started\nstart_time: ${startTime}\ntask_file: ${taskSnapshotName}\n---\n`;
|
|
163
|
+
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
164
|
+
return resultFileName;
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
/**
|
|
151
168
|
* Append a history entry to the project-level history.jsonl file.
|
|
152
169
|
*/
|
package/src/update-checker.ts
CHANGED
|
@@ -12,41 +12,6 @@ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "
|
|
|
12
12
|
export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
|
|
13
13
|
export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
|
|
14
14
|
|
|
15
|
-
let latestVersion: string | null = null;
|
|
16
|
-
let lastCheckTime = 0;
|
|
17
|
-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Check the npm registry for the latest version of palmier.
|
|
21
|
-
*/
|
|
22
|
-
export async function checkForUpdate(): Promise<void> {
|
|
23
|
-
if (isDevBuild) return;
|
|
24
|
-
const now = Date.now();
|
|
25
|
-
if (now - lastCheckTime < CHECK_INTERVAL_MS) return;
|
|
26
|
-
lastCheckTime = now;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch("https://registry.npmjs.org/palmier/latest", {
|
|
30
|
-
signal: AbortSignal.timeout(10_000),
|
|
31
|
-
});
|
|
32
|
-
if (!res.ok) return;
|
|
33
|
-
const data = (await res.json()) as { version?: string };
|
|
34
|
-
if (data.version) {
|
|
35
|
-
latestVersion = data.version;
|
|
36
|
-
console.log(`[update] Latest version: ${data.version} (current: ${currentVersion})`);
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
// Network errors are expected (offline, etc.)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get the latest version from npm, or null if not yet checked.
|
|
45
|
-
*/
|
|
46
|
-
export function getLatestVersion(): string | null {
|
|
47
|
-
return latestVersion;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
15
|
/**
|
|
51
16
|
* Run the update and restart the daemon.
|
|
52
17
|
* Returns an error message if the update fails.
|
|
@@ -63,7 +28,6 @@ export async function performUpdate(): Promise<string | null> {
|
|
|
63
28
|
return `Update failed. Please run manually:\nnpm update -g palmier`;
|
|
64
29
|
}
|
|
65
30
|
console.log("[update] Update installed, restarting daemon...");
|
|
66
|
-
latestVersion = null;
|
|
67
31
|
// Small delay to allow the RPC response to be sent
|
|
68
32
|
setTimeout(() => {
|
|
69
33
|
getPlatform().restartDaemon().catch((err) => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { readTaskStatus, writeTaskStatus } from "./task.js";
|
|
4
|
+
import { publishHostEvent } from "./events.js";
|
|
5
|
+
import type { HostConfig } from "./types.js";
|
|
6
|
+
import type { NatsConnection } from "nats";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Watch status.json until user_input is populated by an RPC call, then resolve.
|
|
10
|
+
*/
|
|
11
|
+
export function waitForUserInput(taskDir: string): Promise<string[]> {
|
|
12
|
+
const statusPath = path.join(taskDir, "status.json");
|
|
13
|
+
return new Promise<string[]>((resolve) => {
|
|
14
|
+
const watcher = fs.watch(statusPath, () => {
|
|
15
|
+
const status = readTaskStatus(taskDir);
|
|
16
|
+
if (!status || !status.user_input?.length) return;
|
|
17
|
+
watcher.close();
|
|
18
|
+
resolve(status.user_input);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Send an input-request event and wait for the user's response.
|
|
25
|
+
*/
|
|
26
|
+
export async function requestUserInput(
|
|
27
|
+
nc: NatsConnection | undefined,
|
|
28
|
+
config: HostConfig,
|
|
29
|
+
taskId: string,
|
|
30
|
+
taskName: string,
|
|
31
|
+
taskDir: string,
|
|
32
|
+
inputDescriptions: string[],
|
|
33
|
+
): Promise<string[] | "aborted"> {
|
|
34
|
+
const currentStatus = readTaskStatus(taskDir)!;
|
|
35
|
+
writeTaskStatus(taskDir, { ...currentStatus, pending_input: inputDescriptions });
|
|
36
|
+
|
|
37
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
38
|
+
event_type: "input-request",
|
|
39
|
+
host_id: config.hostId,
|
|
40
|
+
input_descriptions: inputDescriptions,
|
|
41
|
+
name: taskName,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const userInput = await waitForUserInput(taskDir);
|
|
45
|
+
if (userInput.length === 1 && userInput[0] === "aborted") {
|
|
46
|
+
writeTaskStatus(taskDir, { running_state: "aborted", time_stamp: Date.now() });
|
|
47
|
+
return "aborted";
|
|
48
|
+
}
|
|
49
|
+
writeTaskStatus(taskDir, { running_state: "started", time_stamp: Date.now() });
|
|
50
|
+
return userInput;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Notify clients that an input request has been resolved.
|
|
55
|
+
*/
|
|
56
|
+
export async function publishInputResolved(
|
|
57
|
+
nc: NatsConnection | undefined,
|
|
58
|
+
config: HostConfig,
|
|
59
|
+
taskId: string,
|
|
60
|
+
status: "provided" | "aborted",
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
63
|
+
event_type: "input-resolved",
|
|
64
|
+
host_id: config.hostId,
|
|
65
|
+
status,
|
|
66
|
+
});
|
|
67
|
+
}
|