palmier 1.1.18 → 1.1.20
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 +1 -1
- package/dist/agents/agent-instructions.md +2 -0
- package/dist/commands/run.js +16 -22
- package/dist/commands/serve.js +54 -36
- package/dist/events.d.ts +2 -2
- package/dist/events.js +2 -8
- package/dist/nats-client.js +6 -0
- package/dist/platform/windows.d.ts +4 -1
- package/dist/platform/windows.js +10 -4
- package/dist/pwa/assets/index-rqAuAC0q.js +121 -0
- package/dist/pwa/assets/{web-nkGUvLxE.js → web-DQDmufih.js} +1 -1
- package/dist/pwa/assets/{web-CSBCBt51.js → web-Dp5Zj6HJ.js} +1 -1
- package/dist/pwa/assets/{web-BWS1woqb.js → web-ew6AFUex.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.d.ts +1 -2
- package/dist/rpc-handler.js +12 -14
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +5 -7
- package/package.json +1 -1
- package/dist/pwa/assets/index-DztP_wQC.js +0 -121
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Capability access is opt-in per device: each capability is gated behind an Andro
|
|
|
29
29
|
powershell -c "irm https://palmier.me/install.ps1 | iex"
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The one-liner installs Node.js 24+ if needed, installs `palmier` globally, and runs the setup wizard. If you already have Node.js 24+ and npm:
|
|
32
|
+
The one-liner installs Node.js 24+ if needed, installs `palmier` globally, and runs the setup wizard. On Windows it also sets the PowerShell execution policy to `RemoteSigned` for the current user if scripts are blocked, since npm's `palmier.ps1` shim can't run otherwise. If you already have Node.js 24+ and npm:
|
|
33
33
|
```bash
|
|
34
34
|
npm install -g palmier && palmier init
|
|
35
35
|
```
|
|
@@ -31,6 +31,8 @@ Whenever a tool you are trying to use is denied or you lack the required permiss
|
|
|
31
31
|
|
|
32
32
|
When launching a browser with the Playwright CLI `open` command, always pass `--headed` (never run headless) and `--persistent` (reuse a persistent profile so logins and cookies survive across runs).
|
|
33
33
|
|
|
34
|
+
When the task is finished, close any browser you opened with the Playwright CLI `close` command (pass the same `-s=<session>` if you used one) before printing the completion marker.
|
|
35
|
+
|
|
34
36
|
## HTTP Endpoints
|
|
35
37
|
|
|
36
38
|
{{ENDPOINT_DOCS}}
|
package/dist/commands/run.js
CHANGED
|
@@ -2,7 +2,6 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { spawnCommand } from "../spawn-command.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
|
-
import { connectNats } from "../nats-client.js";
|
|
6
5
|
import { parseTaskFile, taskDisplayName, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir, beginStreamingMessage, formatAgentModelComment } from "../task.js";
|
|
7
6
|
import { getAgent } from "../agents/agent.js";
|
|
8
7
|
import { getPlatform } from "../platform/index.js";
|
|
@@ -31,7 +30,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
31
30
|
notifyPending = true;
|
|
32
31
|
notifyTimer = setTimeout(() => {
|
|
33
32
|
notifyPending = false;
|
|
34
|
-
publishHostEvent(ctx.
|
|
33
|
+
publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
35
34
|
}, 500);
|
|
36
35
|
}
|
|
37
36
|
function ensureWriter(stream) {
|
|
@@ -104,9 +103,9 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
104
103
|
ensureWriter("stdout").write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
105
104
|
}
|
|
106
105
|
writer?.end();
|
|
107
|
-
await publishHostEvent(ctx.
|
|
106
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
108
107
|
if (linkedFiles.length > 0) {
|
|
109
|
-
await publishHostEvent(ctx.
|
|
108
|
+
await publishHostEvent(ctx.taskId, {
|
|
110
109
|
event_type: "report-generated",
|
|
111
110
|
run_id: ctx.runId,
|
|
112
111
|
name: taskDisplayName(ctx.task.frontmatter),
|
|
@@ -153,7 +152,7 @@ export function stripPalmierMarkers(output) {
|
|
|
153
152
|
}
|
|
154
153
|
async function appendAndNotify(ctx, msg) {
|
|
155
154
|
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
156
|
-
await publishHostEvent(ctx.
|
|
155
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
157
156
|
}
|
|
158
157
|
/** The latest run dir with no status messages yet — freshly created by the RPC handler. */
|
|
159
158
|
function findLatestPendingRunId(taskDir) {
|
|
@@ -182,7 +181,6 @@ export async function runCommand(taskId) {
|
|
|
182
181
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
183
182
|
const task = parseTaskFile(taskDir);
|
|
184
183
|
console.log(`Running task: ${taskId}`);
|
|
185
|
-
let nc;
|
|
186
184
|
const taskName = taskDisplayName(task.frontmatter);
|
|
187
185
|
const existingRunId = findLatestPendingRunId(taskDir);
|
|
188
186
|
const agentVersion = config.agents?.find((a) => a.key === task.frontmatter.agent)?.version;
|
|
@@ -191,9 +189,6 @@ export async function runCommand(taskId) {
|
|
|
191
189
|
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
192
190
|
}
|
|
193
191
|
const cleanup = async () => {
|
|
194
|
-
if (nc && !nc.isClosed()) {
|
|
195
|
-
await nc.drain();
|
|
196
|
-
}
|
|
197
192
|
if (task.frontmatter.one_off) {
|
|
198
193
|
try {
|
|
199
194
|
getPlatform().removeTaskTimer(taskId);
|
|
@@ -202,32 +197,31 @@ export async function runCommand(taskId) {
|
|
|
202
197
|
}
|
|
203
198
|
};
|
|
204
199
|
try {
|
|
205
|
-
|
|
206
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
200
|
+
await publishTaskEvent(taskDir, taskId, "started", taskName, runId);
|
|
207
201
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
208
|
-
await publishHostEvent(
|
|
202
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
209
203
|
if (task.frontmatter.requires_confirmation) {
|
|
210
204
|
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
211
205
|
const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
|
|
212
206
|
appendRunMessage(taskDir, runId, { role: "assistant", time: Date.now(), content: confirmPrompt, type: "confirmation" });
|
|
213
|
-
await publishHostEvent(
|
|
207
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
214
208
|
if (!confirmed) {
|
|
215
209
|
console.log("Task aborted by user.");
|
|
216
210
|
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Aborted", type: "confirmation" });
|
|
217
211
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
218
|
-
await publishTaskEvent(
|
|
212
|
+
await publishTaskEvent(taskDir, taskId, "aborted", taskName, runId);
|
|
219
213
|
await cleanup();
|
|
220
214
|
return;
|
|
221
215
|
}
|
|
222
216
|
console.log("Task confirmed by user.");
|
|
223
217
|
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Confirmed", type: "confirmation" });
|
|
224
218
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
225
|
-
await publishHostEvent(
|
|
219
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
226
220
|
}
|
|
227
221
|
const guiEnv = getPlatform().getGuiEnv();
|
|
228
222
|
const agent = getAgent(task.frontmatter.agent);
|
|
229
223
|
const ctx = {
|
|
230
|
-
agent, task, taskDir, runId, guiEnv,
|
|
224
|
+
agent, task, taskDir, runId, guiEnv, config, taskId,
|
|
231
225
|
transientPermissions: [],
|
|
232
226
|
};
|
|
233
227
|
// Command-output and on_new_* tasks share the same trigger machinery: the
|
|
@@ -240,7 +234,7 @@ export async function runCommand(taskId) {
|
|
|
240
234
|
const result = await runEventTriggeredMode(ctx);
|
|
241
235
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
242
236
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
243
|
-
await publishTaskEvent(
|
|
237
|
+
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
244
238
|
console.log(`Task ${taskId} completed (triggered).`);
|
|
245
239
|
}
|
|
246
240
|
else {
|
|
@@ -252,7 +246,7 @@ export async function runCommand(taskId) {
|
|
|
252
246
|
const result = await invokeAgentWithRetries(ctx, task);
|
|
253
247
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
254
248
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
255
|
-
await publishTaskEvent(
|
|
249
|
+
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
256
250
|
console.log(`Task ${taskId} completed.`);
|
|
257
251
|
}
|
|
258
252
|
}
|
|
@@ -266,7 +260,7 @@ export async function runCommand(taskId) {
|
|
|
266
260
|
content: errorMsg,
|
|
267
261
|
});
|
|
268
262
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
269
|
-
await publishTaskEvent(
|
|
263
|
+
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
270
264
|
process.exitCode = 1;
|
|
271
265
|
}
|
|
272
266
|
finally {
|
|
@@ -317,12 +311,12 @@ async function runEventTriggeredMode(ctx) {
|
|
|
317
311
|
catch (err) {
|
|
318
312
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
319
313
|
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: errorMsg, type: "error" });
|
|
320
|
-
await publishHostEvent(ctx.
|
|
314
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
321
315
|
return { outcome: "failed", endTime: Date.now() };
|
|
322
316
|
}
|
|
323
317
|
return { outcome: lastOutcome, endTime: Date.now() };
|
|
324
318
|
}
|
|
325
|
-
async function publishTaskEvent(
|
|
319
|
+
async function publishTaskEvent(taskDir, taskId, eventType, taskName, runId) {
|
|
326
320
|
writeTaskStatus(taskDir, {
|
|
327
321
|
running_state: eventType,
|
|
328
322
|
time_stamp: Date.now(),
|
|
@@ -333,7 +327,7 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
|
|
|
333
327
|
payload.name = taskName;
|
|
334
328
|
if (runId)
|
|
335
329
|
payload.run_id = runId;
|
|
336
|
-
await publishHostEvent(
|
|
330
|
+
await publishHostEvent(taskId, payload);
|
|
337
331
|
}
|
|
338
332
|
async function requestPermission(config, task, taskDir, requiredPermissions) {
|
|
339
333
|
const port = config.httpPort ?? 7256;
|
package/dist/commands/serve.js
CHANGED
|
@@ -25,7 +25,7 @@ const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
|
25
25
|
* clean up OS scheduler units for one-off tasks that have already terminated.
|
|
26
26
|
* The system scheduler (Task Scheduler / systemd) is the authoritative source.
|
|
27
27
|
*/
|
|
28
|
-
async function checkStaleTasks(config
|
|
28
|
+
async function checkStaleTasks(config) {
|
|
29
29
|
const tasksRoot = path.join(config.projectRoot, "tasks");
|
|
30
30
|
if (!fs.existsSync(tasksRoot))
|
|
31
31
|
return;
|
|
@@ -59,7 +59,7 @@ async function checkStaleTasks(config, nc) {
|
|
|
59
59
|
type: "failed",
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
-
await publishHostEvent(
|
|
62
|
+
await publishHostEvent(taskId, {
|
|
63
63
|
event_type: "running-state",
|
|
64
64
|
running_state: "failed",
|
|
65
65
|
name: taskDisplayName(task.frontmatter) || taskId,
|
|
@@ -91,7 +91,7 @@ async function checkStaleTasks(config, nc) {
|
|
|
91
91
|
content: "",
|
|
92
92
|
type: "failed",
|
|
93
93
|
});
|
|
94
|
-
await publishHostEvent(
|
|
94
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -112,37 +112,18 @@ export async function serveCommand() {
|
|
|
112
112
|
saveConfig(config);
|
|
113
113
|
console.log(`Detected agents: ${agents.map((a) => a.key).join(", ") || "none"}`);
|
|
114
114
|
let nc;
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
const handleRpc = createRpcHandler(config);
|
|
116
|
+
function attachNats(conn) {
|
|
117
|
+
nc = conn;
|
|
117
118
|
console.log("[nats] Connected");
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for (const task of allTasks) {
|
|
127
|
-
try {
|
|
128
|
-
platform.installTaskTimer(config, task);
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// Spawn the long-running command process for every enabled command task —
|
|
135
|
-
// the daemon owns these the same way it owns the device-event subscriptions.
|
|
136
|
-
startEnabledCommandRunners(config);
|
|
137
|
-
setInterval(() => {
|
|
138
|
-
checkStaleTasks(config, nc).catch((err) => {
|
|
139
|
-
console.error("[monitor] Error checking stale tasks:", err);
|
|
140
|
-
});
|
|
141
|
-
}, POLL_INTERVAL_MS);
|
|
142
|
-
const handleRpc = createRpcHandler(config, nc);
|
|
143
|
-
const httpPort = config.httpPort ?? 7256;
|
|
144
|
-
if (nc) {
|
|
145
|
-
startNatsTransport(config, handleRpc, nc);
|
|
119
|
+
(async () => {
|
|
120
|
+
for await (const s of conn.status()) {
|
|
121
|
+
if (s.type === "pingTimer")
|
|
122
|
+
continue;
|
|
123
|
+
console.log(`[nats] Status: ${s.type}${typeof s.data === "string" ? ` (${s.data})` : ""}`);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
startNatsTransport(config, handleRpc, conn);
|
|
146
127
|
const sc = StringCodec();
|
|
147
128
|
// Match phone numbers regardless of formatting; letters preserved for shortcodes.
|
|
148
129
|
function normalizeSender(raw) {
|
|
@@ -168,7 +149,7 @@ export async function serveCommand() {
|
|
|
168
149
|
dispatchTrigger(task.frontmatter.id, payload);
|
|
169
150
|
}
|
|
170
151
|
}
|
|
171
|
-
const notifSub =
|
|
152
|
+
const notifSub = conn.subscribe(`host.${config.hostId}.device.notifications`);
|
|
172
153
|
(async () => {
|
|
173
154
|
for await (const msg of notifSub) {
|
|
174
155
|
const raw = sc.decode(msg.data);
|
|
@@ -183,7 +164,7 @@ export async function serveCommand() {
|
|
|
183
164
|
dispatchDeviceEvent("on_new_notification", raw, parsed);
|
|
184
165
|
}
|
|
185
166
|
})();
|
|
186
|
-
const smsSub =
|
|
167
|
+
const smsSub = conn.subscribe(`host.${config.hostId}.device.sms`);
|
|
187
168
|
(async () => {
|
|
188
169
|
for await (const msg of smsSub) {
|
|
189
170
|
const raw = sc.decode(msg.data);
|
|
@@ -199,5 +180,42 @@ export async function serveCommand() {
|
|
|
199
180
|
}
|
|
200
181
|
})();
|
|
201
182
|
}
|
|
202
|
-
|
|
183
|
+
// The client's own reconnect only covers a connection that was once up, so a
|
|
184
|
+
// daemon started while offline (e.g. at logon before Wi-Fi) must keep dialing.
|
|
185
|
+
async function connectWithRetry() {
|
|
186
|
+
for (let delayMs = 5_000;; delayMs = Math.min(delayMs * 2, 60_000)) {
|
|
187
|
+
try {
|
|
188
|
+
attachNats(await connectNats(config));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.warn(`[nats] Connection failed, retrying in ${delayMs / 1000}s: ${err}`);
|
|
193
|
+
}
|
|
194
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Don't block startup on the relay: local mode must work offline.
|
|
198
|
+
connectWithRetry();
|
|
199
|
+
await checkStaleTasks(config);
|
|
200
|
+
// Reinstall scheduler entries for all tasks (recovery after init/reinstall)
|
|
201
|
+
const platform = getPlatform();
|
|
202
|
+
const allTasks = listTasks(config.projectRoot);
|
|
203
|
+
for (const task of allTasks) {
|
|
204
|
+
try {
|
|
205
|
+
platform.installTaskTimer(config, task);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Spawn the long-running command process for every enabled command task —
|
|
212
|
+
// the daemon owns these the same way it owns the device-event subscriptions.
|
|
213
|
+
startEnabledCommandRunners(config);
|
|
214
|
+
setInterval(() => {
|
|
215
|
+
checkStaleTasks(config).catch((err) => {
|
|
216
|
+
console.error("[monitor] Error checking stale tasks:", err);
|
|
217
|
+
});
|
|
218
|
+
}, POLL_INTERVAL_MS);
|
|
219
|
+
const httpPort = config.httpPort ?? 7256;
|
|
220
|
+
await startHttpTransport(config, handleRpc, httpPort, () => nc);
|
|
203
221
|
}
|
package/dist/events.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function publishHostEvent(
|
|
1
|
+
/** Hands the event to the daemon, which forwards it to the relay and local SSE clients. */
|
|
2
|
+
export declare function publishHostEvent(taskId: string, payload: Record<string, unknown>): Promise<void>;
|
package/dist/events.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import { StringCodec } from "nats";
|
|
2
1
|
import { loadConfig } from "./config.js";
|
|
3
|
-
|
|
4
|
-
export async function publishHostEvent(
|
|
5
|
-
const subject = `host-event.${hostId}.${taskId}`;
|
|
6
|
-
if (nc) {
|
|
7
|
-
nc.publish(subject, sc.encode(JSON.stringify(payload)));
|
|
8
|
-
console.log(`[nats] ${subject} →`, payload);
|
|
9
|
-
}
|
|
2
|
+
/** Hands the event to the daemon, which forwards it to the relay and local SSE clients. */
|
|
3
|
+
export async function publishHostEvent(taskId, payload) {
|
|
10
4
|
const config = loadConfig();
|
|
11
5
|
const port = config.httpPort ?? 7256;
|
|
12
6
|
try {
|
package/dist/nats-client.js
CHANGED
|
@@ -6,6 +6,12 @@ export async function connectNats(config) {
|
|
|
6
6
|
const nc = await connect({
|
|
7
7
|
servers: config.natsUrl,
|
|
8
8
|
authenticator: jwtAuthenticator(config.natsJwt, new TextEncoder().encode(config.natsNkeySeed)),
|
|
9
|
+
// Sleep or a network switch kills the socket silently; the 2-min default
|
|
10
|
+
// ping leaves the host unreachable for minutes, and the default 10
|
|
11
|
+
// reconnect attempts close the connection for good after a short outage.
|
|
12
|
+
maxReconnectAttempts: -1,
|
|
13
|
+
pingInterval: 20_000,
|
|
14
|
+
maxPingOut: 2,
|
|
9
15
|
});
|
|
10
16
|
return nc;
|
|
11
17
|
}
|
|
@@ -12,7 +12,10 @@ import type { HostConfig, ParsedTask } from "../types.js";
|
|
|
12
12
|
* monthly: "MM HH D * *"
|
|
13
13
|
*/
|
|
14
14
|
export declare function scheduleValueToXml(scheduleType: "crons" | "specific_times", value: string): string;
|
|
15
|
-
export declare function buildTaskXml(tr: string, triggers: string[],
|
|
15
|
+
export declare function buildTaskXml(tr: string, triggers: string[], opts?: {
|
|
16
|
+
foreground?: boolean;
|
|
17
|
+
persistent?: boolean;
|
|
18
|
+
}): string;
|
|
16
19
|
export declare class WindowsPlatform implements PlatformService {
|
|
17
20
|
installDaemon(config: HostConfig): void;
|
|
18
21
|
uninstallDaemon(): void;
|
package/dist/platform/windows.js
CHANGED
|
@@ -40,7 +40,7 @@ export function scheduleValueToXml(scheduleType, value) {
|
|
|
40
40
|
}
|
|
41
41
|
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger>`;
|
|
42
42
|
}
|
|
43
|
-
export function buildTaskXml(tr, triggers,
|
|
43
|
+
export function buildTaskXml(tr, triggers, opts = {}) {
|
|
44
44
|
const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
|
|
45
45
|
const commandStr = command?.replace(/"/g, "") ?? "";
|
|
46
46
|
const argsStr = argParts.map((a) => a.replace(/"/g, "")).join(" ");
|
|
@@ -49,7 +49,7 @@ export function buildTaskXml(tr, triggers, foreground) {
|
|
|
49
49
|
`<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
50
50
|
` <Principals>`,
|
|
51
51
|
` <Principal>`,
|
|
52
|
-
` <LogonType>${foreground ? "InteractiveToken" : "S4U"}</LogonType>`,
|
|
52
|
+
` <LogonType>${opts.foreground ? "InteractiveToken" : "S4U"}</LogonType>`,
|
|
53
53
|
` <RunLevel>LeastPrivilege</RunLevel>`,
|
|
54
54
|
` </Principal>`,
|
|
55
55
|
` </Principals>`,
|
|
@@ -58,6 +58,12 @@ export function buildTaskXml(tr, triggers, foreground) {
|
|
|
58
58
|
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
59
59
|
` <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`,
|
|
60
60
|
` <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>`,
|
|
61
|
+
// Omitting ExecutionTimeLimit means Task Scheduler's 72h default, which
|
|
62
|
+
// kills a long-running process; PT0S disables the limit.
|
|
63
|
+
...(opts.persistent ? [
|
|
64
|
+
` <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>`,
|
|
65
|
+
` <RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure>`,
|
|
66
|
+
] : []),
|
|
61
67
|
` </Settings>`,
|
|
62
68
|
` <Triggers>${triggers.join("")}</Triggers>`,
|
|
63
69
|
` <Actions>`,
|
|
@@ -171,7 +177,7 @@ export class WindowsPlatform {
|
|
|
171
177
|
ensureDaemonTask(script) {
|
|
172
178
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
173
179
|
const tr = `"${process.execPath}" "${script}" serve`;
|
|
174
|
-
const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
|
|
180
|
+
const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`], { persistent: true });
|
|
175
181
|
const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
|
|
176
182
|
try {
|
|
177
183
|
const bom = Buffer.from([0xFF, 0xFE]);
|
|
@@ -234,7 +240,7 @@ export class WindowsPlatform {
|
|
|
234
240
|
// MultipleInstancesPolicy. S4U keeps the console hidden unless
|
|
235
241
|
// foreground_mode is set. Works unelevated because the caller (daemon)
|
|
236
242
|
// runs elevated.
|
|
237
|
-
const xml = buildTaskXml(tr, triggerElements, task.frontmatter.foreground_mode);
|
|
243
|
+
const xml = buildTaskXml(tr, triggerElements, { foreground: task.frontmatter.foreground_mode });
|
|
238
244
|
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
239
245
|
try {
|
|
240
246
|
// schtasks /xml requires UTF-16LE with BOM.
|