palmier 0.8.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +14 -18
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/index-B0F9mtid.css +1 -0
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-BpM3fNCn.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +19 -48
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +170 -20
- package/palmier-server/pwa/src/App.tsx +15 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
- package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
- package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +66 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +47 -6
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +4 -3
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +14 -20
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +20 -48
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
- package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
|
@@ -11,8 +11,6 @@ import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp
|
|
|
11
11
|
import { getTaskDir } from "../task.js";
|
|
12
12
|
import { popEvent } from "../event-queues.js";
|
|
13
13
|
|
|
14
|
-
// ── Bundled PWA asset serving ───────────────────────────────────────────
|
|
15
|
-
|
|
16
14
|
interface CachedAsset {
|
|
17
15
|
data: Buffer;
|
|
18
16
|
contentType: string;
|
|
@@ -41,22 +39,18 @@ function guessContentType(urlPath: string): string {
|
|
|
41
39
|
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
/**
|
|
45
|
-
* Read a PWA asset from the bundled pwa/ directory, caching in memory.
|
|
46
|
-
* Returns null if the file does not exist.
|
|
47
|
-
*/
|
|
48
42
|
function getAsset(urlPath: string): CachedAsset | null {
|
|
49
43
|
const cached = assetCache.get(urlPath);
|
|
50
44
|
if (cached) return cached;
|
|
51
45
|
|
|
52
46
|
const filePath = path.join(PWA_DIR, urlPath === "/" ? "index.html" : urlPath);
|
|
53
47
|
|
|
54
|
-
// Prevent path traversal
|
|
48
|
+
// Prevent path traversal.
|
|
55
49
|
if (!filePath.startsWith(PWA_DIR)) return null;
|
|
56
50
|
|
|
57
51
|
try {
|
|
58
52
|
let data = fs.readFileSync(filePath);
|
|
59
|
-
//
|
|
53
|
+
// Marker lets the PWA detect it's served by palmier.
|
|
60
54
|
if (urlPath === "/") {
|
|
61
55
|
const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
|
|
62
56
|
data = Buffer.from(html, "utf-8");
|
|
@@ -90,10 +84,6 @@ export function detectLanIp(): string {
|
|
|
90
84
|
return "127.0.0.1";
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
/**
|
|
94
|
-
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
95
|
-
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
96
|
-
*/
|
|
97
87
|
export async function startHttpTransport(
|
|
98
88
|
config: HostConfig,
|
|
99
89
|
handleRpc: (req: RpcMessage) => Promise<unknown>,
|
|
@@ -126,7 +116,6 @@ export async function startHttpTransport(
|
|
|
126
116
|
resource.subscribe(() => broadcastResourceUpdated(resource.uri));
|
|
127
117
|
}
|
|
128
118
|
|
|
129
|
-
// If a pairing code is provided, pre-register it
|
|
130
119
|
if (pairingCode) {
|
|
131
120
|
const EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|
132
121
|
const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
|
|
@@ -171,9 +160,6 @@ export async function startHttpTransport(
|
|
|
171
160
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
172
161
|
}
|
|
173
162
|
|
|
174
|
-
/**
|
|
175
|
-
* Publish an event via NATS and SSE.
|
|
176
|
-
*/
|
|
177
163
|
async function publishEvent(taskId: string, payload: Record<string, unknown>): Promise<void> {
|
|
178
164
|
const sc = StringCodec();
|
|
179
165
|
const subject = `host-event.${config.hostId}.${taskId}`;
|
|
@@ -191,8 +177,6 @@ export async function startHttpTransport(
|
|
|
191
177
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
192
178
|
const pathname = url.pathname;
|
|
193
179
|
|
|
194
|
-
// ── MCP streamable HTTP endpoint ──────────────────────────────────
|
|
195
|
-
|
|
196
180
|
if (req.method === "POST" && pathname === "/mcp") {
|
|
197
181
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
198
182
|
try {
|
|
@@ -204,7 +188,7 @@ export async function startHttpTransport(
|
|
|
204
188
|
res.setHeader("Mcp-Session-Id", result.sessionId);
|
|
205
189
|
}
|
|
206
190
|
if (result.stream && sessionId) {
|
|
207
|
-
// Keep response open as SSE
|
|
191
|
+
// Keep the response open as SSE for server-initiated notifications.
|
|
208
192
|
res.writeHead(200, {
|
|
209
193
|
"Content-Type": "text/event-stream",
|
|
210
194
|
"Cache-Control": "no-cache",
|
|
@@ -227,8 +211,6 @@ export async function startHttpTransport(
|
|
|
227
211
|
return;
|
|
228
212
|
}
|
|
229
213
|
|
|
230
|
-
// ── Auto-generated REST endpoints from MCP tool registry ──────────
|
|
231
|
-
|
|
232
214
|
if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
|
|
233
215
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
234
216
|
const tool = agentToolMap.get(pathname.slice(1))!;
|
|
@@ -258,8 +240,6 @@ export async function startHttpTransport(
|
|
|
258
240
|
return;
|
|
259
241
|
}
|
|
260
242
|
|
|
261
|
-
// ── Auto-generated REST endpoints from MCP resource registry ────
|
|
262
|
-
|
|
263
243
|
const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
|
|
264
244
|
if (matchedResource) {
|
|
265
245
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
@@ -280,8 +260,6 @@ export async function startHttpTransport(
|
|
|
280
260
|
return;
|
|
281
261
|
}
|
|
282
262
|
|
|
283
|
-
// ── Event queue pop (used by event-triggered palmier run) ─────────
|
|
284
|
-
|
|
285
263
|
if (req.method === "POST" && pathname === "/task-event/pop") {
|
|
286
264
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
287
265
|
const taskId = url.searchParams.get("taskId");
|
|
@@ -293,8 +271,6 @@ export async function startHttpTransport(
|
|
|
293
271
|
return;
|
|
294
272
|
}
|
|
295
273
|
|
|
296
|
-
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
297
|
-
|
|
298
274
|
if (req.method === "POST" && pathname === "/event") {
|
|
299
275
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
300
276
|
try {
|
|
@@ -334,8 +310,6 @@ export async function startHttpTransport(
|
|
|
334
310
|
return;
|
|
335
311
|
}
|
|
336
312
|
|
|
337
|
-
// ── POST /request-permission — held connection ──────────────────────
|
|
338
|
-
|
|
339
313
|
if (req.method === "POST" && pathname === "/request-permission") {
|
|
340
314
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
341
315
|
try {
|
|
@@ -376,8 +350,6 @@ export async function startHttpTransport(
|
|
|
376
350
|
return;
|
|
377
351
|
}
|
|
378
352
|
|
|
379
|
-
// ── Public pair endpoint — no auth, PWA posts pairing code here ────────
|
|
380
|
-
|
|
381
353
|
if (req.method === "POST" && pathname === "/pair") {
|
|
382
354
|
try {
|
|
383
355
|
const body = await readBody(req);
|
|
@@ -404,16 +376,14 @@ export async function startHttpTransport(
|
|
|
404
376
|
return;
|
|
405
377
|
}
|
|
406
378
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
// Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
|
|
379
|
+
// Service worker and manifest require HTTPS, which LAN mode doesn't use.
|
|
410
380
|
const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
|
|
411
381
|
|
|
412
382
|
const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
|
|
413
383
|
if (!isApiRoute) {
|
|
414
384
|
if (SKIP.has(pathname)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
415
385
|
|
|
416
|
-
//
|
|
386
|
+
// Fall back to index.html for SPA routing.
|
|
417
387
|
let asset = getAsset(pathname);
|
|
418
388
|
if (!asset && pathname !== "/") {
|
|
419
389
|
asset = getAsset("/");
|
|
@@ -428,14 +398,12 @@ export async function startHttpTransport(
|
|
|
428
398
|
return;
|
|
429
399
|
}
|
|
430
400
|
|
|
431
|
-
//
|
|
432
|
-
|
|
401
|
+
// Localhost is trusted; all other API callers require a client token.
|
|
433
402
|
if (!isLocalhost(req) && !checkAuth(req)) {
|
|
434
403
|
sendJson(res, 401, { error: "Unauthorized" });
|
|
435
404
|
return;
|
|
436
405
|
}
|
|
437
406
|
|
|
438
|
-
// SSE event stream
|
|
439
407
|
if (req.method === "GET" && pathname === "/events") {
|
|
440
408
|
res.writeHead(200, {
|
|
441
409
|
"Content-Type": "text/event-stream",
|
|
@@ -456,7 +424,6 @@ export async function startHttpTransport(
|
|
|
456
424
|
return;
|
|
457
425
|
}
|
|
458
426
|
|
|
459
|
-
// RPC endpoint: POST /rpc/<method>
|
|
460
427
|
if (req.method === "POST" && pathname.startsWith("/rpc/")) {
|
|
461
428
|
const method = pathname.slice("/rpc/".length);
|
|
462
429
|
if (!method) { sendJson(res, 400, { error: "Missing RPC method" }); return; }
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { StringCodec, type NatsConnection, type Msg, type Subscription } from "nats";
|
|
2
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Start the NATS transport using an existing connection.
|
|
6
|
-
* Subscribe to RPC subjects and dispatch to handler.
|
|
7
|
-
*/
|
|
8
4
|
export async function startNatsTransport(
|
|
9
5
|
config: HostConfig,
|
|
10
6
|
handleRpc: (req: RpcMessage) => Promise<unknown>,
|
|
@@ -16,7 +12,6 @@ export async function startNatsTransport(
|
|
|
16
12
|
console.log(`[nats] Subscribing to: ${subject}`);
|
|
17
13
|
const sub = nc.subscribe(subject);
|
|
18
14
|
|
|
19
|
-
// Graceful shutdown
|
|
20
15
|
const shutdown = async () => {
|
|
21
16
|
console.log("[nats] Shutting down...");
|
|
22
17
|
sub.unsubscribe();
|
|
@@ -28,12 +23,11 @@ export async function startNatsTransport(
|
|
|
28
23
|
process.on("SIGTERM", shutdown);
|
|
29
24
|
|
|
30
25
|
async function processMessage(msg: Msg) {
|
|
31
|
-
//
|
|
26
|
+
// Subject format: ...rpc.<method parts>
|
|
32
27
|
const subjectTokens = msg.subject.split(".");
|
|
33
28
|
const rpcIdx = subjectTokens.indexOf("rpc");
|
|
34
29
|
const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
|
|
35
30
|
|
|
36
|
-
// Parse params from message body
|
|
37
31
|
let params: Record<string, unknown> = {};
|
|
38
32
|
if (msg.data && msg.data.length > 0) {
|
|
39
33
|
const raw = sc.decode(msg.data).trim();
|
|
@@ -50,7 +44,7 @@ export async function startNatsTransport(
|
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
//
|
|
47
|
+
// PWA includes the client token in the payload.
|
|
54
48
|
const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
|
|
55
49
|
delete params.clientToken;
|
|
56
50
|
|
|
@@ -72,7 +66,7 @@ export async function startNatsTransport(
|
|
|
72
66
|
|
|
73
67
|
async function consumeSubscription(subscription: Subscription) {
|
|
74
68
|
for await (const msg of subscription) {
|
|
75
|
-
//
|
|
69
|
+
// Don't await — heartbeats must keep flowing while RPC runs.
|
|
76
70
|
processMessage(msg);
|
|
77
71
|
}
|
|
78
72
|
}
|
package/src/types.ts
CHANGED
|
@@ -7,12 +7,10 @@ export interface HostConfig {
|
|
|
7
7
|
natsJwt?: string;
|
|
8
8
|
natsNkeySeed?: string;
|
|
9
9
|
|
|
10
|
-
// Detected agent CLIs
|
|
11
10
|
agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
|
|
12
11
|
|
|
13
|
-
// HTTP server port (default 7256)
|
|
14
12
|
httpPort?: number;
|
|
15
|
-
|
|
13
|
+
/** Whether to accept non-localhost HTTP connections. */
|
|
16
14
|
lanEnabled?: boolean;
|
|
17
15
|
}
|
|
18
16
|
|
|
@@ -25,8 +23,8 @@ export interface TaskFrontmatter {
|
|
|
25
23
|
* Task schedule.
|
|
26
24
|
* - `crons`: `schedule_values` holds cron expressions (e.g. "0 9 * * *")
|
|
27
25
|
* - `specific_times`: `schedule_values` holds local datetime strings (e.g. "2026-04-20T09:00")
|
|
28
|
-
* - `on_new_notification`: fires on each new Android notification from NATS
|
|
29
|
-
* - `on_new_sms`: fires on each new SMS from NATS
|
|
26
|
+
* - `on_new_notification`: fires on each new Android notification from NATS. Optional `schedule_values` holds a single-entry packageName filter; empty/unset matches any app.
|
|
27
|
+
* - `on_new_sms`: fires on each new SMS from NATS. Optional `schedule_values` holds a single-entry sender filter; compared after normalization (strip spaces/dashes/parens/plus, lowercase). Empty/unset matches any sender.
|
|
30
28
|
*/
|
|
31
29
|
schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
|
|
32
30
|
schedule_values?: string[];
|
|
@@ -50,11 +48,6 @@ export interface ParsedTask {
|
|
|
50
48
|
*/
|
|
51
49
|
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
52
50
|
|
|
53
|
-
/**
|
|
54
|
-
* Persisted to `status.json` in the task directory. Used for crash detection
|
|
55
|
-
* (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
|
|
56
|
-
* permission, input) are handled via held HTTP connections on the serve daemon.
|
|
57
|
-
*/
|
|
58
51
|
export interface TaskStatus {
|
|
59
52
|
running_state: TaskRunningState;
|
|
60
53
|
time_stamp: number;
|
package/src/update-checker.ts
CHANGED
|
@@ -12,10 +12,7 @@ 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
|
-
/**
|
|
16
|
-
* Run the update and restart the daemon.
|
|
17
|
-
* Returns an error message if the update fails.
|
|
18
|
-
*/
|
|
15
|
+
/** Returns an error message if the update fails. */
|
|
19
16
|
export async function performUpdate(): Promise<string | null> {
|
|
20
17
|
try {
|
|
21
18
|
const { output, exitCode } = await spawnCommand("npm", ["update", "-g", "palmier"], {
|
|
@@ -28,7 +25,7 @@ export async function performUpdate(): Promise<string | null> {
|
|
|
28
25
|
return `Update failed. Please run manually:\nnpm update -g palmier`;
|
|
29
26
|
}
|
|
30
27
|
console.log("[update] Update installed, restarting daemon...");
|
|
31
|
-
//
|
|
28
|
+
// Delay so the RPC response finishes sending first.
|
|
32
29
|
setTimeout(() => {
|
|
33
30
|
getPlatform().restartDaemon().catch((err) => {
|
|
34
31
|
console.error("[update] Restart failed:", err);
|
|
@@ -35,16 +35,15 @@ requires_confirmation: false
|
|
|
35
35
|
assert.equal(result.frontmatter.agent, "claude");
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
it("defaults
|
|
38
|
+
it("defaults schedule_enabled to true", () => {
|
|
39
39
|
const content = `---
|
|
40
40
|
id: abc123
|
|
41
41
|
user_prompt: Do something
|
|
42
|
-
triggers: []
|
|
43
42
|
requires_confirmation: false
|
|
44
43
|
---`;
|
|
45
44
|
|
|
46
45
|
const result = parseTaskContent(content);
|
|
47
|
-
assert.equal(result.frontmatter.
|
|
46
|
+
assert.equal(result.frontmatter.schedule_enabled, true);
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
it("derives name from user_prompt when not specified", () => {
|
package/test/windows-xml.test.ts
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { scheduleValueToXml, buildTaskXml } from "../src/platform/windows.js";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
6
|
-
it("converts a
|
|
7
|
-
const xml =
|
|
5
|
+
describe("scheduleValueToXml", () => {
|
|
6
|
+
it("converts a specific_times value to TimeTrigger", () => {
|
|
7
|
+
const xml = scheduleValueToXml("specific_times", "2026-03-28T09:00");
|
|
8
8
|
assert.equal(xml, "<TimeTrigger><StartBoundary>2026-03-28T09:00:00</StartBoundary></TimeTrigger>");
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it("converts hourly cron to TimeTrigger with PT1H repetition", () => {
|
|
12
|
-
const xml =
|
|
12
|
+
const xml = scheduleValueToXml("crons", "0 * * * *");
|
|
13
13
|
assert.ok(xml.includes("<Interval>PT1H</Interval>"), "should have hourly interval");
|
|
14
14
|
assert.ok(xml.includes("<TimeTrigger>"), "should be a TimeTrigger");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("converts daily cron to CalendarTrigger with DaysInterval", () => {
|
|
18
|
-
const xml =
|
|
18
|
+
const xml = scheduleValueToXml("crons", "30 9 * * *");
|
|
19
19
|
assert.ok(xml.includes("<ScheduleByDay>"), "should use ScheduleByDay");
|
|
20
20
|
assert.ok(xml.includes("<DaysInterval>1</DaysInterval>"), "should have interval 1");
|
|
21
21
|
assert.ok(xml.includes("T09:30:00"), "should encode time as 09:30");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("converts weekly cron to CalendarTrigger with DaysOfWeek", () => {
|
|
25
|
-
const xml =
|
|
25
|
+
const xml = scheduleValueToXml("crons", "0 10 * * 1");
|
|
26
26
|
assert.ok(xml.includes("<ScheduleByWeek>"), "should use ScheduleByWeek");
|
|
27
27
|
assert.ok(xml.includes("<Monday />"), "day 1 should be Monday");
|
|
28
28
|
assert.ok(xml.includes("T10:00:00"), "should encode time as 10:00");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("converts weekly cron for Sunday (day 0)", () => {
|
|
32
|
-
const xml =
|
|
32
|
+
const xml = scheduleValueToXml("crons", "0 8 * * 0");
|
|
33
33
|
assert.ok(xml.includes("<Sunday />"), "day 0 should be Sunday");
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("converts weekly cron for Sunday (day 7)", () => {
|
|
37
|
-
const xml =
|
|
37
|
+
const xml = scheduleValueToXml("crons", "0 8 * * 7");
|
|
38
38
|
assert.ok(xml.includes("<Sunday />"), "day 7 should also be Sunday");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it("converts monthly cron to CalendarTrigger with DaysOfMonth", () => {
|
|
42
|
-
const xml =
|
|
42
|
+
const xml = scheduleValueToXml("crons", "0 14 15 * *");
|
|
43
43
|
assert.ok(xml.includes("<ScheduleByMonth>"), "should use ScheduleByMonth");
|
|
44
44
|
assert.ok(xml.includes("<Day>15</Day>"), "should have day 15");
|
|
45
45
|
assert.ok(xml.includes("T14:00:00"), "should encode time as 14:00");
|
|
46
|
-
// All months should be listed
|
|
47
46
|
assert.ok(xml.includes("<January />"), "should include January");
|
|
48
47
|
assert.ok(xml.includes("<December />"), "should include December");
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
it("throws on invalid cron expression", () => {
|
|
52
|
-
assert.throws(() =>
|
|
51
|
+
assert.throws(() => scheduleValueToXml("crons", "bad"), /Invalid cron/);
|
|
53
52
|
});
|
|
54
53
|
});
|
|
55
54
|
|