palmier 0.7.8 → 0.8.0
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/commands/run.js +55 -0
- package/dist/commands/serve.js +22 -2
- package/dist/event-queues.d.ts +36 -0
- package/dist/event-queues.js +53 -0
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +4 -2
- package/dist/platform/linux.js +11 -8
- package/dist/platform/windows.d.ts +5 -6
- package/dist/platform/windows.js +19 -13
- package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
- package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
- package/dist/pwa/assets/{web-BNr628AV.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-DyQPewAi.js → web-CF-N8Di6.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +25 -9
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +18 -5
- package/dist/types.d.ts +10 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +117 -36
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
- package/palmier-server/pwa/src/components/SessionComposer.tsx +20 -10
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +33 -25
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +274 -293
- package/palmier-server/pwa/src/components/{TaskListView.tsx → TasksView.tsx} +20 -13
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +9 -26
- package/palmier-server/pwa/src/types.ts +5 -9
- package/palmier-server/spec.md +23 -23
- package/src/commands/run.ts +61 -0
- package/src/commands/serve.ts +22 -2
- package/src/event-queues.ts +56 -0
- package/src/mcp-tools.ts +6 -2
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +19 -13
- package/src/rpc-handler.ts +28 -11
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +17 -5
- package/src/types.ts +10 -7
- package/dist/pwa/assets/index-8cTctVnD.js +0 -120
- package/dist/pwa/assets/index-CSUkBBsQ.css +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
You have AI agents on your machine. But you have to sit at your desk to use them. Palmier lets you dispatch, schedule, and monitor them from any device, anywhere.
|
|
10
10
|
|
|
11
|
-
It runs on your machine as a background daemon and connects to a mobile-friendly PWA, so you can
|
|
11
|
+
It runs on your machine as a background daemon and connects to a mobile-friendly PWA, so you can start one-off sessions, schedule recurring tasks, approve permissions, and check results without being at your computer.
|
|
12
12
|
> **Important:** By using Palmier, you agree to the [Terms of Service](https://www.palmier.me/terms) and [Privacy Policy](https://www.palmier.me/privacy). See the [Disclaimer](#disclaimer) section below.
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
package/dist/commands/run.js
CHANGED
|
@@ -209,6 +209,15 @@ export async function runCommand(taskId) {
|
|
|
209
209
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
210
210
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
211
211
|
}
|
|
212
|
+
else if (task.frontmatter.schedule_type === "on_new_notification"
|
|
213
|
+
|| task.frontmatter.schedule_type === "on_new_sms") {
|
|
214
|
+
// Event-triggered mode (driven by NATS pub/sub of device notifications/SMS)
|
|
215
|
+
const result = await runEventTriggeredMode(ctx);
|
|
216
|
+
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
217
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
218
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
219
|
+
console.log(`Task ${taskId} completed (event-triggered).`);
|
|
220
|
+
}
|
|
212
221
|
else {
|
|
213
222
|
// Standard execution — add user prompt as first message
|
|
214
223
|
await appendAndNotify(ctx, {
|
|
@@ -380,6 +389,52 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
380
389
|
}
|
|
381
390
|
return { outcome: "finished", endTime };
|
|
382
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Event-triggered execution mode.
|
|
394
|
+
*
|
|
395
|
+
* Drains the daemon-owned per-task event queue via the local /task-event/pop
|
|
396
|
+
* HTTP endpoint, invoking the agent once per event with the payload spliced
|
|
397
|
+
* into the user prompt. The run process itself holds no NATS subscription;
|
|
398
|
+
* the daemon handles that and atomically clears the active flag when we see
|
|
399
|
+
* an empty pop, so it can fire up a fresh run on the next incoming event.
|
|
400
|
+
*/
|
|
401
|
+
async function runEventTriggeredMode(ctx) {
|
|
402
|
+
const scheduleType = ctx.task.frontmatter.schedule_type;
|
|
403
|
+
const label = scheduleType === "on_new_notification" ? "notification" : "SMS";
|
|
404
|
+
const port = ctx.config.httpPort ?? 7256;
|
|
405
|
+
const popUrl = `http://localhost:${port}/task-event/pop?taskId=${encodeURIComponent(ctx.taskId)}`;
|
|
406
|
+
console.log(`[event-triggered] Draining ${label} queue`);
|
|
407
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
408
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
409
|
+
let eventsProcessed = 0;
|
|
410
|
+
try {
|
|
411
|
+
// eslint-disable-next-line no-constant-condition
|
|
412
|
+
while (true) {
|
|
413
|
+
const res = await fetch(popUrl, { method: "POST" });
|
|
414
|
+
if (!res.ok)
|
|
415
|
+
throw new Error(`pop-event failed: ${res.status} ${res.statusText}`);
|
|
416
|
+
const body = await res.json();
|
|
417
|
+
if (body.empty || !body.event)
|
|
418
|
+
break;
|
|
419
|
+
eventsProcessed++;
|
|
420
|
+
console.log(`[event-triggered] Processing ${label} #${eventsProcessed}`);
|
|
421
|
+
const perEventPrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this new ${label}:\n${body.event}`;
|
|
422
|
+
const perEventTask = {
|
|
423
|
+
frontmatter: { ...ctx.task.frontmatter, user_prompt: perEventPrompt },
|
|
424
|
+
};
|
|
425
|
+
await invokeAgentWithRetries(ctx, perEventTask);
|
|
426
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
427
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
432
|
+
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: errorMsg, type: "error" });
|
|
433
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
434
|
+
return { outcome: "failed", endTime: Date.now() };
|
|
435
|
+
}
|
|
436
|
+
return { outcome: "finished", endTime: Date.now() };
|
|
437
|
+
}
|
|
383
438
|
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName, runId) {
|
|
384
439
|
writeTaskStatus(taskDir, {
|
|
385
440
|
running_state: eventType,
|
package/dist/commands/serve.js
CHANGED
|
@@ -14,6 +14,7 @@ import { CONFIG_DIR } from "../config.js";
|
|
|
14
14
|
import { StringCodec } from "nats";
|
|
15
15
|
import { addNotification } from "../notification-store.js";
|
|
16
16
|
import { addSmsMessage } from "../sms-store.js";
|
|
17
|
+
import { enqueueEvent } from "../event-queues.js";
|
|
17
18
|
const POLL_INTERVAL_MS = 30_000;
|
|
18
19
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
19
20
|
/**
|
|
@@ -119,28 +120,47 @@ export async function serveCommand() {
|
|
|
119
120
|
startNatsTransport(config, handleRpc, nc);
|
|
120
121
|
// Subscribe to device notifications and SMS from Android
|
|
121
122
|
const sc = StringCodec();
|
|
123
|
+
// Dispatch a raw event payload to every task whose schedule matches.
|
|
124
|
+
function dispatchDeviceEvent(scheduleType, payload) {
|
|
125
|
+
for (const task of listTasks(config.projectRoot)) {
|
|
126
|
+
if (task.frontmatter.schedule_type !== scheduleType)
|
|
127
|
+
continue;
|
|
128
|
+
if (!task.frontmatter.schedule_enabled)
|
|
129
|
+
continue;
|
|
130
|
+
const { shouldStart } = enqueueEvent(task.frontmatter.id, payload);
|
|
131
|
+
if (shouldStart) {
|
|
132
|
+
platform.startTask(task.frontmatter.id).catch((err) => {
|
|
133
|
+
console.error(`[event-trigger] Failed to start ${task.frontmatter.id}:`, err);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
122
138
|
const notifSub = nc.subscribe(`host.${config.hostId}.device.notifications`);
|
|
123
139
|
(async () => {
|
|
124
140
|
for await (const msg of notifSub) {
|
|
141
|
+
const raw = sc.decode(msg.data);
|
|
125
142
|
try {
|
|
126
|
-
const data = JSON.parse(
|
|
143
|
+
const data = JSON.parse(raw);
|
|
127
144
|
addNotification({ ...data, receivedAt: Date.now() });
|
|
128
145
|
}
|
|
129
146
|
catch (err) {
|
|
130
147
|
console.error("[nats] Failed to parse device notification:", err);
|
|
131
148
|
}
|
|
149
|
+
dispatchDeviceEvent("on_new_notification", raw);
|
|
132
150
|
}
|
|
133
151
|
})();
|
|
134
152
|
const smsSub = nc.subscribe(`host.${config.hostId}.device.sms`);
|
|
135
153
|
(async () => {
|
|
136
154
|
for await (const msg of smsSub) {
|
|
155
|
+
const raw = sc.decode(msg.data);
|
|
137
156
|
try {
|
|
138
|
-
const data = JSON.parse(
|
|
157
|
+
const data = JSON.parse(raw);
|
|
139
158
|
addSmsMessage({ ...data, receivedAt: Date.now() });
|
|
140
159
|
}
|
|
141
160
|
catch (err) {
|
|
142
161
|
console.error("[nats] Failed to parse device SMS:", err);
|
|
143
162
|
}
|
|
163
|
+
dispatchDeviceEvent("on_new_sms", raw);
|
|
144
164
|
}
|
|
145
165
|
})();
|
|
146
166
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-task in-memory event queues for event-triggered schedules
|
|
3
|
+
* (schedule_type: "on_new_notification" | "on_new_sms").
|
|
4
|
+
*
|
|
5
|
+
* The daemon owns the NATS subscription and populates these queues; the
|
|
6
|
+
* `palmier run` process drains them via the localhost /task-event/pop HTTP
|
|
7
|
+
* endpoint. `activeRuns` tracks whether a run process is currently draining,
|
|
8
|
+
* so we don't race a fresh startTask with a teardown-phase run.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle invariants:
|
|
11
|
+
* - activeRuns is cleared atomically inside popEvent when the queue is
|
|
12
|
+
* drained. At that point the calling run has already finished its last
|
|
13
|
+
* agent invocation and is only tearing down.
|
|
14
|
+
* - enqueueEvent returns shouldStart=true only if the task transitioned
|
|
15
|
+
* from idle (no active run) to active — callers must then startTask.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Queue a raw (JSON-string) event payload for a task. Returns whether the
|
|
19
|
+
* caller should now start the run process.
|
|
20
|
+
*/
|
|
21
|
+
export declare function enqueueEvent(taskId: string, payload: string): {
|
|
22
|
+
shouldStart: boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Pop the oldest queued event for a task. Returns `{ event }` when one is
|
|
26
|
+
* available (keeps the task marked active), or `{ empty: true }` after
|
|
27
|
+
* clearing the active flag atomically.
|
|
28
|
+
*/
|
|
29
|
+
export declare function popEvent(taskId: string): {
|
|
30
|
+
event: string;
|
|
31
|
+
} | {
|
|
32
|
+
empty: true;
|
|
33
|
+
};
|
|
34
|
+
/** Remove any state for a task (called from task.delete). */
|
|
35
|
+
export declare function clearTaskQueue(taskId: string): void;
|
|
36
|
+
//# sourceMappingURL=event-queues.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-task in-memory event queues for event-triggered schedules
|
|
3
|
+
* (schedule_type: "on_new_notification" | "on_new_sms").
|
|
4
|
+
*
|
|
5
|
+
* The daemon owns the NATS subscription and populates these queues; the
|
|
6
|
+
* `palmier run` process drains them via the localhost /task-event/pop HTTP
|
|
7
|
+
* endpoint. `activeRuns` tracks whether a run process is currently draining,
|
|
8
|
+
* so we don't race a fresh startTask with a teardown-phase run.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle invariants:
|
|
11
|
+
* - activeRuns is cleared atomically inside popEvent when the queue is
|
|
12
|
+
* drained. At that point the calling run has already finished its last
|
|
13
|
+
* agent invocation and is only tearing down.
|
|
14
|
+
* - enqueueEvent returns shouldStart=true only if the task transitioned
|
|
15
|
+
* from idle (no active run) to active — callers must then startTask.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_QUEUE_SIZE = 100;
|
|
18
|
+
const queues = new Map();
|
|
19
|
+
const activeRuns = new Set();
|
|
20
|
+
/**
|
|
21
|
+
* Queue a raw (JSON-string) event payload for a task. Returns whether the
|
|
22
|
+
* caller should now start the run process.
|
|
23
|
+
*/
|
|
24
|
+
export function enqueueEvent(taskId, payload) {
|
|
25
|
+
const queue = queues.get(taskId) ?? [];
|
|
26
|
+
if (queue.length >= MAX_QUEUE_SIZE)
|
|
27
|
+
queue.shift();
|
|
28
|
+
queue.push(payload);
|
|
29
|
+
queues.set(taskId, queue);
|
|
30
|
+
if (activeRuns.has(taskId))
|
|
31
|
+
return { shouldStart: false };
|
|
32
|
+
activeRuns.add(taskId);
|
|
33
|
+
return { shouldStart: true };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pop the oldest queued event for a task. Returns `{ event }` when one is
|
|
37
|
+
* available (keeps the task marked active), or `{ empty: true }` after
|
|
38
|
+
* clearing the active flag atomically.
|
|
39
|
+
*/
|
|
40
|
+
export function popEvent(taskId) {
|
|
41
|
+
const queue = queues.get(taskId);
|
|
42
|
+
if (queue && queue.length > 0) {
|
|
43
|
+
return { event: queue.shift() };
|
|
44
|
+
}
|
|
45
|
+
activeRuns.delete(taskId);
|
|
46
|
+
return { empty: true };
|
|
47
|
+
}
|
|
48
|
+
/** Remove any state for a task (called from task.delete). */
|
|
49
|
+
export function clearTaskQueue(taskId) {
|
|
50
|
+
queues.delete(taskId);
|
|
51
|
+
activeRuns.delete(taskId);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=event-queues.js.map
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface ResourceDefinition {
|
|
|
32
32
|
restPath: string;
|
|
33
33
|
/** Return the current resource content. */
|
|
34
34
|
read: () => unknown;
|
|
35
|
+
/** Register a listener for content changes. Returns an unsubscribe function. */
|
|
36
|
+
subscribe: (listener: () => void) => () => void;
|
|
35
37
|
}
|
|
36
38
|
export declare const agentResources: ResourceDefinition[];
|
|
37
39
|
export declare const agentResourceMap: Map<string, ResourceDefinition>;
|
package/dist/mcp-tools.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
2
|
import { registerPending } from "./pending-requests.js";
|
|
3
3
|
import { getCapabilityDevice } from "./device-capabilities.js";
|
|
4
|
-
import { getNotifications } from "./notification-store.js";
|
|
5
|
-
import { getSmsMessages } from "./sms-store.js";
|
|
4
|
+
import { getNotifications, onNotificationsChanged } from "./notification-store.js";
|
|
5
|
+
import { getSmsMessages, onSmsChanged } from "./sms-store.js";
|
|
6
6
|
export class ToolError extends Error {
|
|
7
7
|
statusCode;
|
|
8
8
|
constructor(message, statusCode = 500) {
|
|
@@ -651,6 +651,7 @@ const deviceNotificationsResource = {
|
|
|
651
651
|
mimeType: "application/json",
|
|
652
652
|
restPath: "/notifications",
|
|
653
653
|
read: getNotifications,
|
|
654
|
+
subscribe: onNotificationsChanged,
|
|
654
655
|
};
|
|
655
656
|
const deviceSmsResource = {
|
|
656
657
|
uri: "sms-messages://device",
|
|
@@ -662,6 +663,7 @@ const deviceSmsResource = {
|
|
|
662
663
|
mimeType: "application/json",
|
|
663
664
|
restPath: "/sms-messages",
|
|
664
665
|
read: getSmsMessages,
|
|
666
|
+
subscribe: onSmsChanged,
|
|
665
667
|
};
|
|
666
668
|
export const agentResources = [deviceNotificationsResource, deviceSmsResource];
|
|
667
669
|
export const agentResourceMap = new Map(agentResources.map((r) => [r.uri, r]));
|
package/dist/platform/linux.js
CHANGED
|
@@ -181,17 +181,20 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
|
181
181
|
`;
|
|
182
182
|
fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
|
|
183
183
|
daemonReload();
|
|
184
|
-
// Only create and enable a timer if
|
|
185
|
-
if (!task.frontmatter.
|
|
184
|
+
// Only create and enable a timer if the schedule exists and is enabled
|
|
185
|
+
if (!task.frontmatter.schedule_enabled)
|
|
186
|
+
return;
|
|
187
|
+
const scheduleType = task.frontmatter.schedule_type;
|
|
188
|
+
const scheduleValues = task.frontmatter.schedule_values;
|
|
189
|
+
if (!scheduleType || !scheduleValues?.length)
|
|
186
190
|
return;
|
|
187
|
-
const triggers = task.frontmatter.triggers || [];
|
|
188
191
|
const onCalendarLines = [];
|
|
189
|
-
for (const
|
|
190
|
-
if (
|
|
191
|
-
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(
|
|
192
|
+
for (const value of scheduleValues) {
|
|
193
|
+
if (scheduleType === "crons") {
|
|
194
|
+
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(value)}`);
|
|
192
195
|
}
|
|
193
|
-
else if (
|
|
194
|
-
onCalendarLines.push(`OnActiveSec=${
|
|
196
|
+
else if (scheduleType === "specific_times") {
|
|
197
|
+
onCalendarLines.push(`OnActiveSec=${value}`);
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
if (onCalendarLines.length > 0) {
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import type { PlatformService } from "./platform.js";
|
|
2
2
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Convert a
|
|
4
|
+
* Convert a single schedule value to a Task Scheduler XML trigger element.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* `specific_times` values are ISO datetime strings like "2026-03-28T09:00".
|
|
7
|
+
*
|
|
8
|
+
* `crons` values are cron expressions. Only these patterns (produced by the PWA UI) are handled:
|
|
7
9
|
* hourly: "0 * * * *"
|
|
8
10
|
* daily: "MM HH * * *"
|
|
9
11
|
* weekly: "MM HH * * D"
|
|
10
12
|
* monthly: "MM HH D * *"
|
|
11
13
|
*/
|
|
12
|
-
export declare function
|
|
13
|
-
type: string;
|
|
14
|
-
value: string;
|
|
15
|
-
}): string;
|
|
14
|
+
export declare function scheduleValueToXml(scheduleType: "crons" | "specific_times", value: string): string;
|
|
16
15
|
/**
|
|
17
16
|
* Build a complete Task Scheduler XML definition.
|
|
18
17
|
*/
|
package/dist/platform/windows.js
CHANGED
|
@@ -7,22 +7,23 @@ const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
|
7
7
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
8
8
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
9
9
|
/**
|
|
10
|
-
* Convert a
|
|
10
|
+
* Convert a single schedule value to a Task Scheduler XML trigger element.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* `specific_times` values are ISO datetime strings like "2026-03-28T09:00".
|
|
13
|
+
*
|
|
14
|
+
* `crons` values are cron expressions. Only these patterns (produced by the PWA UI) are handled:
|
|
13
15
|
* hourly: "0 * * * *"
|
|
14
16
|
* daily: "MM HH * * *"
|
|
15
17
|
* weekly: "MM HH * * D"
|
|
16
18
|
* monthly: "MM HH D * *"
|
|
17
19
|
*/
|
|
18
|
-
export function
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
return `<TimeTrigger><StartBoundary>${trigger.value}:00</StartBoundary></TimeTrigger>`;
|
|
20
|
+
export function scheduleValueToXml(scheduleType, value) {
|
|
21
|
+
if (scheduleType === "specific_times") {
|
|
22
|
+
return `<TimeTrigger><StartBoundary>${value}:00</StartBoundary></TimeTrigger>`;
|
|
22
23
|
}
|
|
23
|
-
const parts =
|
|
24
|
+
const parts = value.trim().split(/\s+/);
|
|
24
25
|
if (parts.length !== 5)
|
|
25
|
-
throw new Error(`Invalid cron expression: ${
|
|
26
|
+
throw new Error(`Invalid cron expression: ${value}`);
|
|
26
27
|
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
27
28
|
const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
|
|
28
29
|
// StartBoundary needs a full date; use a past date as the anchor
|
|
@@ -176,15 +177,20 @@ export class WindowsPlatform {
|
|
|
176
177
|
const tn = schtasksTaskName(taskId);
|
|
177
178
|
const script = process.argv[1] || "palmier";
|
|
178
179
|
const tr = `"${process.execPath}" "${script}" run ${taskId}`;
|
|
179
|
-
// Build trigger XML elements
|
|
180
|
+
// Build trigger XML elements. Event-based schedule types (on_new_notification,
|
|
181
|
+
// on_new_sms) carry no values and are driven by the run process, not the OS
|
|
182
|
+
// scheduler — they intentionally produce only the dummy trigger below.
|
|
180
183
|
const triggerElements = [];
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
const scheduleType = task.frontmatter.schedule_type;
|
|
185
|
+
const scheduleValues = task.frontmatter.schedule_values;
|
|
186
|
+
const isTimerSchedule = scheduleType === "crons" || scheduleType === "specific_times";
|
|
187
|
+
if (task.frontmatter.schedule_enabled && isTimerSchedule && scheduleValues?.length) {
|
|
188
|
+
for (const value of scheduleValues) {
|
|
183
189
|
try {
|
|
184
|
-
triggerElements.push(
|
|
190
|
+
triggerElements.push(scheduleValueToXml(scheduleType, value));
|
|
185
191
|
}
|
|
186
192
|
catch (err) {
|
|
187
|
-
console.error(`Invalid
|
|
193
|
+
console.error(`Invalid schedule value: ${err}`);
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
}
|