palmier 1.1.19 → 1.1.21
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.d.ts +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/run.js +32 -31
- 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 +1 -1
- 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
|
@@ -182,7 +182,7 @@ The wizard:
|
|
|
182
182
|
- Offers to install the **Playwright CLI** for browser automation (see [Browser Automation](#browser-automation)). Skipped on re-init if it's already managed.
|
|
183
183
|
- Asks for the HTTP port
|
|
184
184
|
- Detects the default network interface (used for auto-LAN)
|
|
185
|
-
- Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
|
|
185
|
+
- Shows a summary (including the Palmier daemon version and any existing scheduled tasks to recover) and asks for confirmation
|
|
186
186
|
- Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
|
|
187
187
|
- Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows). On re-init, also restarts the daemon so the running process picks up the new agents/versions for `host.info`.
|
|
188
188
|
- Auto-enters pair mode to connect your first device
|
package/dist/agents/agent.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface AgentTool {
|
|
|
30
30
|
supportsPermissions?: boolean;
|
|
31
31
|
/** Whether this agent supports yolo mode (auto-approve all tools). */
|
|
32
32
|
supportsYolo?: boolean;
|
|
33
|
-
/** When true, the run loop
|
|
33
|
+
/** When true, the run loop persists the agent's stderr output only if the invocation fails. */
|
|
34
34
|
suppressStdErr?: boolean;
|
|
35
35
|
/** Matches the model name (capture group 1) in the banner the CLI prints on stderr.
|
|
36
36
|
* Takes precedence over the agent's self-reported model marker. */
|
package/dist/commands/init.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getPlatform } from "../platform/index.js";
|
|
|
10
10
|
import { pairCommand } from "./pair.js";
|
|
11
11
|
import { detectDefaultInterface, getInterfaceIpv4 } from "../network.js";
|
|
12
12
|
import { listTasks, taskDisplayName } from "../task.js";
|
|
13
|
+
import { currentVersion } from "../update-checker.js";
|
|
13
14
|
const { bold, dim, green, cyan, red } = colors;
|
|
14
15
|
export async function initCommand() {
|
|
15
16
|
console.log(`\n${bold("=== Palmier Host Setup ===")}\n`);
|
|
@@ -74,6 +75,7 @@ export async function initCommand() {
|
|
|
74
75
|
const defaultInterface = (await detectDefaultInterface()) ?? undefined;
|
|
75
76
|
const lanIp = defaultInterface ? getInterfaceIpv4(defaultInterface) : null;
|
|
76
77
|
console.log(`\n${bold("Setup summary:")}\n`);
|
|
78
|
+
console.log(` ${dim("Daemon:")} Palmier v${currentVersion}\n`);
|
|
77
79
|
console.log(` ${dim("Task storage:")} ${bold(projectRoot)}`);
|
|
78
80
|
console.log(` All tasks and execution data will be stored here.\n`);
|
|
79
81
|
console.log(` ${dim("Local:")} ${cyan(`http://localhost:${httpPort}`)}`);
|
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";
|
|
@@ -20,6 +19,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
20
19
|
let writer;
|
|
21
20
|
let activeStream;
|
|
22
21
|
const lineBufs = { stdout: "", stderr: "" };
|
|
22
|
+
let suppressedStderr = "";
|
|
23
23
|
let notifyPending = false;
|
|
24
24
|
let notifyTimer;
|
|
25
25
|
let agentModel;
|
|
@@ -31,7 +31,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
31
31
|
notifyPending = true;
|
|
32
32
|
notifyTimer = setTimeout(() => {
|
|
33
33
|
notifyPending = false;
|
|
34
|
-
publishHostEvent(ctx.
|
|
34
|
+
publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
35
35
|
}, 500);
|
|
36
36
|
}
|
|
37
37
|
function ensureWriter(stream) {
|
|
@@ -51,7 +51,9 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
51
51
|
throttledNotify();
|
|
52
52
|
}
|
|
53
53
|
function onStderr(chunk) {
|
|
54
|
-
if (
|
|
54
|
+
if (ctx.agent.suppressStdErr)
|
|
55
|
+
suppressedStderr += chunk;
|
|
56
|
+
else
|
|
55
57
|
emit("stderr", chunk);
|
|
56
58
|
const cliModel = captureCliModel(chunk);
|
|
57
59
|
if (cliModel && !agentModelWritten)
|
|
@@ -72,6 +74,12 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
72
74
|
}
|
|
73
75
|
flushAgentModel();
|
|
74
76
|
}
|
|
77
|
+
function flushTrailingLine(stream) {
|
|
78
|
+
const trailing = lineBufs[stream];
|
|
79
|
+
if (trailing && !trailing.startsWith("[PALMIER")) {
|
|
80
|
+
ensureWriter(stream).write(trailing);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
75
83
|
const { args, stdin, env: agentEnv, files } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions);
|
|
76
84
|
const command = ctx.agent.command;
|
|
77
85
|
const runDir = getRunDir(ctx.taskDir, ctx.runId);
|
|
@@ -88,25 +96,23 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
88
96
|
onStdout: (chunk) => emit("stdout", chunk),
|
|
89
97
|
onStderr,
|
|
90
98
|
});
|
|
91
|
-
if (notifyTimer)
|
|
92
|
-
clearTimeout(notifyTimer);
|
|
93
99
|
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
94
100
|
const linkedFiles = listLinkedTaskFiles(ctx.taskDir, ctx.runId, result.output);
|
|
95
101
|
const requiredPermissions = parsePermissions(result.output);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
+
flushTrailingLine("stdout");
|
|
103
|
+
if (outcome === "failed")
|
|
104
|
+
emit("stderr", suppressedStderr);
|
|
105
|
+
flushTrailingLine("stderr");
|
|
102
106
|
if (requiredPermissions.length > 0) {
|
|
103
107
|
const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
|
|
104
108
|
ensureWriter("stdout").write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
105
109
|
}
|
|
110
|
+
if (notifyTimer)
|
|
111
|
+
clearTimeout(notifyTimer);
|
|
106
112
|
writer?.end();
|
|
107
|
-
await publishHostEvent(ctx.
|
|
113
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
108
114
|
if (linkedFiles.length > 0) {
|
|
109
|
-
await publishHostEvent(ctx.
|
|
115
|
+
await publishHostEvent(ctx.taskId, {
|
|
110
116
|
event_type: "report-generated",
|
|
111
117
|
run_id: ctx.runId,
|
|
112
118
|
name: taskDisplayName(ctx.task.frontmatter),
|
|
@@ -153,7 +159,7 @@ export function stripPalmierMarkers(output) {
|
|
|
153
159
|
}
|
|
154
160
|
async function appendAndNotify(ctx, msg) {
|
|
155
161
|
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
156
|
-
await publishHostEvent(ctx.
|
|
162
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
157
163
|
}
|
|
158
164
|
/** The latest run dir with no status messages yet — freshly created by the RPC handler. */
|
|
159
165
|
function findLatestPendingRunId(taskDir) {
|
|
@@ -182,7 +188,6 @@ export async function runCommand(taskId) {
|
|
|
182
188
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
183
189
|
const task = parseTaskFile(taskDir);
|
|
184
190
|
console.log(`Running task: ${taskId}`);
|
|
185
|
-
let nc;
|
|
186
191
|
const taskName = taskDisplayName(task.frontmatter);
|
|
187
192
|
const existingRunId = findLatestPendingRunId(taskDir);
|
|
188
193
|
const agentVersion = config.agents?.find((a) => a.key === task.frontmatter.agent)?.version;
|
|
@@ -191,9 +196,6 @@ export async function runCommand(taskId) {
|
|
|
191
196
|
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
192
197
|
}
|
|
193
198
|
const cleanup = async () => {
|
|
194
|
-
if (nc && !nc.isClosed()) {
|
|
195
|
-
await nc.drain();
|
|
196
|
-
}
|
|
197
199
|
if (task.frontmatter.one_off) {
|
|
198
200
|
try {
|
|
199
201
|
getPlatform().removeTaskTimer(taskId);
|
|
@@ -202,32 +204,31 @@ export async function runCommand(taskId) {
|
|
|
202
204
|
}
|
|
203
205
|
};
|
|
204
206
|
try {
|
|
205
|
-
|
|
206
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
207
|
+
await publishTaskEvent(taskDir, taskId, "started", taskName, runId);
|
|
207
208
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
208
|
-
await publishHostEvent(
|
|
209
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
209
210
|
if (task.frontmatter.requires_confirmation) {
|
|
210
211
|
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
211
212
|
const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
|
|
212
213
|
appendRunMessage(taskDir, runId, { role: "assistant", time: Date.now(), content: confirmPrompt, type: "confirmation" });
|
|
213
|
-
await publishHostEvent(
|
|
214
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
214
215
|
if (!confirmed) {
|
|
215
216
|
console.log("Task aborted by user.");
|
|
216
217
|
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Aborted", type: "confirmation" });
|
|
217
218
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
218
|
-
await publishTaskEvent(
|
|
219
|
+
await publishTaskEvent(taskDir, taskId, "aborted", taskName, runId);
|
|
219
220
|
await cleanup();
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
222
223
|
console.log("Task confirmed by user.");
|
|
223
224
|
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Confirmed", type: "confirmation" });
|
|
224
225
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
225
|
-
await publishHostEvent(
|
|
226
|
+
await publishHostEvent(taskId, { event_type: "result-updated", run_id: runId });
|
|
226
227
|
}
|
|
227
228
|
const guiEnv = getPlatform().getGuiEnv();
|
|
228
229
|
const agent = getAgent(task.frontmatter.agent);
|
|
229
230
|
const ctx = {
|
|
230
|
-
agent, task, taskDir, runId, guiEnv,
|
|
231
|
+
agent, task, taskDir, runId, guiEnv, config, taskId,
|
|
231
232
|
transientPermissions: [],
|
|
232
233
|
};
|
|
233
234
|
// Command-output and on_new_* tasks share the same trigger machinery: the
|
|
@@ -240,7 +241,7 @@ export async function runCommand(taskId) {
|
|
|
240
241
|
const result = await runEventTriggeredMode(ctx);
|
|
241
242
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
242
243
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
243
|
-
await publishTaskEvent(
|
|
244
|
+
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
244
245
|
console.log(`Task ${taskId} completed (triggered).`);
|
|
245
246
|
}
|
|
246
247
|
else {
|
|
@@ -252,7 +253,7 @@ export async function runCommand(taskId) {
|
|
|
252
253
|
const result = await invokeAgentWithRetries(ctx, task);
|
|
253
254
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
254
255
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
255
|
-
await publishTaskEvent(
|
|
256
|
+
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
256
257
|
console.log(`Task ${taskId} completed.`);
|
|
257
258
|
}
|
|
258
259
|
}
|
|
@@ -266,7 +267,7 @@ export async function runCommand(taskId) {
|
|
|
266
267
|
content: errorMsg,
|
|
267
268
|
});
|
|
268
269
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
269
|
-
await publishTaskEvent(
|
|
270
|
+
await publishTaskEvent(taskDir, taskId, outcome, taskName, runId);
|
|
270
271
|
process.exitCode = 1;
|
|
271
272
|
}
|
|
272
273
|
finally {
|
|
@@ -317,12 +318,12 @@ async function runEventTriggeredMode(ctx) {
|
|
|
317
318
|
catch (err) {
|
|
318
319
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
319
320
|
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: errorMsg, type: "error" });
|
|
320
|
-
await publishHostEvent(ctx.
|
|
321
|
+
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
321
322
|
return { outcome: "failed", endTime: Date.now() };
|
|
322
323
|
}
|
|
323
324
|
return { outcome: lastOutcome, endTime: Date.now() };
|
|
324
325
|
}
|
|
325
|
-
async function publishTaskEvent(
|
|
326
|
+
async function publishTaskEvent(taskDir, taskId, eventType, taskName, runId) {
|
|
326
327
|
writeTaskStatus(taskDir, {
|
|
327
328
|
running_state: eventType,
|
|
328
329
|
time_stamp: Date.now(),
|
|
@@ -333,7 +334,7 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
|
|
|
333
334
|
payload.name = taskName;
|
|
334
335
|
if (runId)
|
|
335
336
|
payload.run_id = runId;
|
|
336
|
-
await publishHostEvent(
|
|
337
|
+
await publishHostEvent(taskId, payload);
|
|
337
338
|
}
|
|
338
339
|
async function requestPermission(config, task, taskDir, requiredPermissions) {
|
|
339
340
|
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.
|