palmier 0.8.1 → 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.
Files changed (125) hide show
  1. package/CLAUDE.md +13 -0
  2. package/README.md +11 -11
  3. package/dist/agents/agent.d.ts +0 -4
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/codex.js +2 -2
  6. package/dist/agents/cursor.js +1 -1
  7. package/dist/agents/deepagents.js +1 -1
  8. package/dist/agents/gemini.js +3 -2
  9. package/dist/agents/goose.js +1 -1
  10. package/dist/agents/hermes.js +1 -1
  11. package/dist/agents/kiro.js +1 -1
  12. package/dist/agents/opencode.js +1 -1
  13. package/dist/agents/qoder.js +1 -1
  14. package/dist/agents/shared-prompt.d.ts +0 -3
  15. package/dist/agents/shared-prompt.js +0 -3
  16. package/dist/app-registry.d.ts +10 -0
  17. package/dist/app-registry.js +44 -0
  18. package/dist/commands/info.d.ts +0 -3
  19. package/dist/commands/info.js +0 -5
  20. package/dist/commands/init.d.ts +0 -3
  21. package/dist/commands/init.js +2 -11
  22. package/dist/commands/pair.d.ts +1 -4
  23. package/dist/commands/pair.js +1 -12
  24. package/dist/commands/restart.d.ts +0 -3
  25. package/dist/commands/restart.js +0 -3
  26. package/dist/commands/run.d.ts +1 -14
  27. package/dist/commands/run.js +18 -61
  28. package/dist/commands/serve.d.ts +0 -3
  29. package/dist/commands/serve.js +33 -27
  30. package/dist/config.d.ts +0 -8
  31. package/dist/config.js +0 -8
  32. package/dist/device-capabilities.d.ts +1 -1
  33. package/dist/event-queues.d.ts +6 -21
  34. package/dist/event-queues.js +6 -21
  35. package/dist/events.d.ts +0 -6
  36. package/dist/events.js +1 -9
  37. package/dist/index.js +0 -1
  38. package/dist/mcp-handler.js +1 -2
  39. package/dist/mcp-tools.d.ts +0 -3
  40. package/dist/mcp-tools.js +12 -16
  41. package/dist/nats-client.d.ts +0 -3
  42. package/dist/nats-client.js +1 -4
  43. package/dist/pending-requests.d.ts +4 -18
  44. package/dist/pending-requests.js +4 -18
  45. package/dist/platform/index.d.ts +1 -4
  46. package/dist/platform/index.js +1 -4
  47. package/dist/platform/linux.d.ts +3 -9
  48. package/dist/platform/linux.js +9 -20
  49. package/dist/platform/platform.d.ts +1 -4
  50. package/dist/platform/windows.d.ts +2 -5
  51. package/dist/platform/windows.js +19 -39
  52. package/dist/pwa/assets/{index-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
  53. package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
  54. package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
  55. package/dist/pwa/assets/{web-DOyOiwsW.js → web-Z1623me-.js} +1 -1
  56. package/dist/pwa/index.html +2 -2
  57. package/dist/pwa/service-worker.js +1 -1
  58. package/dist/rpc-handler.d.ts +0 -6
  59. package/dist/rpc-handler.js +18 -47
  60. package/dist/spawn-command.d.ts +10 -25
  61. package/dist/spawn-command.js +7 -15
  62. package/dist/task.d.ts +6 -64
  63. package/dist/task.js +7 -70
  64. package/dist/transports/http-transport.d.ts +0 -4
  65. package/dist/transports/http-transport.js +6 -28
  66. package/dist/transports/nats-transport.d.ts +0 -4
  67. package/dist/transports/nats-transport.js +3 -9
  68. package/dist/types.d.ts +3 -7
  69. package/dist/update-checker.d.ts +1 -4
  70. package/dist/update-checker.js +2 -5
  71. package/package.json +1 -1
  72. package/palmier-server/pwa/src/App.css +165 -20
  73. package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
  74. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
  75. package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
  76. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
  77. package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
  78. package/palmier-server/pwa/src/constants.ts +1 -1
  79. package/palmier-server/pwa/src/native/Device.ts +20 -2
  80. package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
  81. package/palmier-server/server/src/index.ts +7 -7
  82. package/palmier-server/server/src/routes/device.ts +4 -4
  83. package/palmier-server/spec.md +38 -7
  84. package/src/agents/agent.ts +0 -4
  85. package/src/agents/claude.ts +1 -1
  86. package/src/agents/codex.ts +2 -2
  87. package/src/agents/cursor.ts +1 -1
  88. package/src/agents/deepagents.ts +1 -1
  89. package/src/agents/gemini.ts +3 -2
  90. package/src/agents/goose.ts +1 -1
  91. package/src/agents/hermes.ts +1 -1
  92. package/src/agents/kiro.ts +1 -1
  93. package/src/agents/opencode.ts +1 -1
  94. package/src/agents/qoder.ts +1 -1
  95. package/src/agents/shared-prompt.ts +0 -3
  96. package/src/app-registry.ts +52 -0
  97. package/src/commands/info.ts +0 -5
  98. package/src/commands/init.ts +2 -11
  99. package/src/commands/pair.ts +1 -12
  100. package/src/commands/restart.ts +0 -3
  101. package/src/commands/run.ts +18 -65
  102. package/src/commands/serve.ts +31 -27
  103. package/src/config.ts +0 -8
  104. package/src/device-capabilities.ts +3 -2
  105. package/src/event-queues.ts +6 -21
  106. package/src/events.ts +1 -9
  107. package/src/index.ts +0 -1
  108. package/src/mcp-handler.ts +1 -2
  109. package/src/mcp-tools.ts +12 -18
  110. package/src/nats-client.ts +1 -4
  111. package/src/pending-requests.ts +4 -18
  112. package/src/platform/index.ts +1 -4
  113. package/src/platform/linux.ts +9 -20
  114. package/src/platform/platform.ts +1 -4
  115. package/src/platform/windows.ts +19 -40
  116. package/src/rpc-handler.ts +19 -47
  117. package/src/spawn-command.ts +11 -27
  118. package/src/task.ts +7 -70
  119. package/src/transports/http-transport.ts +6 -39
  120. package/src/transports/nats-transport.ts +3 -9
  121. package/src/types.ts +3 -10
  122. package/src/update-checker.ts +2 -5
  123. package/test/task-parsing.test.ts +2 -3
  124. package/test/windows-xml.test.ts +11 -12
  125. package/dist/pwa/assets/index-DQfOEB03.js +0 -120
package/src/task.ts CHANGED
@@ -3,9 +3,6 @@ import * as path from "path";
3
3
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
4
  import type { ParsedTask, TaskFrontmatter, TaskStatus, HistoryEntry, ConversationMessage } from "./types.js";
5
5
 
6
- /**
7
- * Parse a TASK.md file from the given task directory.
8
- */
9
6
  export function parseTaskFile(taskDir: string): ParsedTask {
10
7
  const filePath = path.join(taskDir, "TASK.md");
11
8
 
@@ -17,9 +14,6 @@ export function parseTaskFile(taskDir: string): ParsedTask {
17
14
  return parseTaskContent(content);
18
15
  }
19
16
 
20
- /**
21
- * Parse TASK.md content string into frontmatter + body.
22
- */
23
17
  export function parseTaskContent(content: string): ParsedTask {
24
18
  const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
25
19
  const match = content.match(fmRegex);
@@ -41,10 +35,6 @@ export function parseTaskContent(content: string): ParsedTask {
41
35
  return { frontmatter };
42
36
  }
43
37
 
44
- /**
45
- * Write a TASK.md file to the given task directory.
46
- * Creates the directory if it doesn't exist.
47
- */
48
38
  export function writeTaskFile(taskDir: string, task: ParsedTask): void {
49
39
  fs.mkdirSync(taskDir, { recursive: true });
50
40
 
@@ -55,18 +45,11 @@ export function writeTaskFile(taskDir: string, task: ParsedTask): void {
55
45
  fs.writeFileSync(filePath, content, "utf-8");
56
46
  }
57
47
 
58
- /**
59
- * Append a task ID to the project-level tasks.jsonl file.
60
- */
61
48
  export function appendTaskList(projectRoot: string, taskId: string): void {
62
49
  const listPath = path.join(projectRoot, "tasks.jsonl");
63
50
  fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
64
51
  }
65
52
 
66
- /**
67
- * Remove a task ID from the project-level tasks.jsonl file.
68
- * Returns true if the entry was found and removed.
69
- */
70
53
  export function removeFromTaskList(projectRoot: string, taskId: string): boolean {
71
54
  const listPath = path.join(projectRoot, "tasks.jsonl");
72
55
  if (!fs.existsSync(listPath)) return false;
@@ -91,9 +74,6 @@ export function removeFromTaskList(projectRoot: string, taskId: string): boolean
91
74
  return true;
92
75
  }
93
76
 
94
- /**
95
- * List all tasks referenced in tasks.jsonl.
96
- */
97
77
  export function listTasks(projectRoot: string): ParsedTask[] {
98
78
  const listPath = path.join(projectRoot, "tasks.jsonl");
99
79
  if (!fs.existsSync(listPath)) return [];
@@ -118,25 +98,15 @@ export function listTasks(projectRoot: string): ParsedTask[] {
118
98
  return tasks.reverse();
119
99
  }
120
100
 
121
- /**
122
- * Get the directory path for a task by its ID.
123
- */
124
101
  export function getTaskDir(projectRoot: string, taskId: string): string {
125
102
  return path.join(projectRoot, "tasks", taskId);
126
103
  }
127
104
 
128
- /**
129
- * Write task status to status.json in the task directory.
130
- */
131
105
  export function writeTaskStatus(taskDir: string, status: TaskStatus): void {
132
106
  const filePath = path.join(taskDir, "status.json");
133
107
  fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
134
108
  }
135
109
 
136
- /**
137
- * Read task status from status.json in the task directory.
138
- * Returns undefined if the file doesn't exist.
139
- */
140
110
  export function readTaskStatus(taskDir: string): TaskStatus | undefined {
141
111
  const filePath = path.join(taskDir, "status.json");
142
112
  try {
@@ -146,10 +116,7 @@ export function readTaskStatus(taskDir: string): TaskStatus | undefined {
146
116
  }
147
117
  }
148
118
 
149
- /**
150
- * Create a run directory with an initial TASKRUN.md file.
151
- * Returns the run ID (timestamp string used as directory name).
152
- */
119
+ /** Returns the run ID (timestamp string used as directory name). */
153
120
  export function createRunDir(
154
121
  taskDir: string,
155
122
  taskName: string,
@@ -165,16 +132,10 @@ export function createRunDir(
165
132
  return runId;
166
133
  }
167
134
 
168
- /**
169
- * Get the path to a run directory.
170
- */
171
135
  export function getRunDir(taskDir: string, runId: string): string {
172
136
  return path.join(taskDir, runId);
173
137
  }
174
138
 
175
- /**
176
- * Append a conversation message to a run's TASKRUN.md file.
177
- */
178
139
  export function appendRunMessage(
179
140
  taskDir: string,
180
141
  runId: string,
@@ -189,10 +150,6 @@ export function appendRunMessage(
189
150
  fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
190
151
  }
191
152
 
192
- /**
193
- * Begin a streaming assistant message — writes the delimiter only.
194
- * Returns a writer that appends content chunks and finalizes the message.
195
- */
196
153
  export function beginStreamingMessage(
197
154
  taskDir: string,
198
155
  runId: string,
@@ -217,7 +174,7 @@ export class StreamingMessageWriter {
217
174
  fs.appendFileSync(this.filePath, "\n\n", "utf-8");
218
175
  if (attachments?.length) {
219
176
  const raw = fs.readFileSync(this.filePath, "utf-8");
220
- // Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
177
+ // spliceUserMessage may have created a newer assistant delimiter.
221
178
  const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
222
179
  let lastMatch: RegExpExecArray | null = null;
223
180
  let m;
@@ -233,12 +190,10 @@ export class StreamingMessageWriter {
233
190
  }
234
191
 
235
192
  /**
236
- * Splice a user message into a running assistant stream.
237
- * Ends the current assistant block, writes the user message,
238
- * then opens a new assistant block all as direct file appends.
239
- * The existing StreamingMessageWriter keeps working because its
240
- * write() is just appendFileSync, so subsequent chunks land in
241
- * the new assistant block.
193
+ * Splice a user message into a running assistant stream: close the current
194
+ * assistant block, write the user message, open a new assistant block. Direct
195
+ * appends only, so an existing StreamingMessageWriter keeps working its
196
+ * subsequent chunks land in the new block.
242
197
  */
243
198
  export function spliceUserMessage(
244
199
  taskDir: string,
@@ -248,22 +203,15 @@ export function spliceUserMessage(
248
203
  assistantAppend?: string,
249
204
  ): void {
250
205
  const filePath = path.join(taskDir, runId, "TASKRUN.md");
251
- // 1. Optionally append to the current assistant block (e.g. the input questions)
252
206
  if (assistantAppend) {
253
207
  fs.appendFileSync(filePath, assistantAppend, "utf-8");
254
208
  }
255
- // 2. End the current assistant block
256
209
  fs.appendFileSync(filePath, "\n\n", "utf-8");
257
- // 3. Write the user message
258
210
  appendRunMessage(taskDir, runId, userMsg);
259
- // 4. Open a new assistant block for subsequent agent output
260
211
  const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
261
212
  fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
262
213
  }
263
214
 
264
- /**
265
- * Read conversation messages from a run's TASKRUN.md file.
266
- */
267
215
  export function readRunMessages(taskDir: string, runId: string): ConversationMessage[] {
268
216
  const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
269
217
  const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
@@ -298,18 +246,11 @@ export function readRunMessages(taskDir: string, runId: string): ConversationMes
298
246
  return messages;
299
247
  }
300
248
 
301
- /**
302
- * Append a history entry to the project-level history.jsonl file.
303
- */
304
249
  export function appendHistory(projectRoot: string, entry: HistoryEntry): void {
305
250
  const historyPath = path.join(projectRoot, "history.jsonl");
306
251
  fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
307
252
  }
308
253
 
309
- /**
310
- * Delete a history entry and its associated run directory.
311
- * Returns true if the entry was found and removed.
312
- */
313
254
  export function deleteHistoryEntry(
314
255
  projectRoot: string,
315
256
  taskId: string,
@@ -337,7 +278,6 @@ export function deleteHistoryEntry(
337
278
 
338
279
  fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
339
280
 
340
- // Delete the run directory
341
281
  const runDir = path.join(projectRoot, "tasks", taskId, runId);
342
282
  if (fs.existsSync(runDir)) {
343
283
  fs.rmSync(runDir, { recursive: true, force: true });
@@ -346,10 +286,7 @@ export function deleteHistoryEntry(
346
286
  return true;
347
287
  }
348
288
 
349
- /**
350
- * Read history entries from history.jsonl with pagination.
351
- * Returns entries sorted most-recent-first.
352
- */
289
+ /** Returns entries most-recent-first. */
353
290
  export function readHistory(
354
291
  projectRoot: string,
355
292
  opts: { offset?: number; limit?: number; task_id?: string },
@@ -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
- // Inject marker into index HTML so the PWA can detect it's served by palmier
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 stream for server-initiated notifications
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
- // ── PWA assets (on-the-fly, cached) ────────────────────────────────
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
- // Try exact path, then fall back to index.html (SPA routing)
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
- // ── API endpoints require auth (localhost is trusted) ───────────────
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
- // Derive RPC method from subject: ...rpc.<method parts>
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
- // Extract clientToken from params (PWA includes it in the payload)
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
- // Handle RPC without blocking the message loop so heartbeats keep flowing
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
- // Whether to accept non-localhost HTTP connections
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; no `schedule_values`
29
- * - `on_new_sms`: fires on each new SMS from NATS; no `schedule_values`
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;
@@ -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
- // Small delay to allow the RPC response to be sent
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 triggers_enabled to true", () => {
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.triggers_enabled, true);
46
+ assert.equal(result.frontmatter.schedule_enabled, true);
48
47
  });
49
48
 
50
49
  it("derives name from user_prompt when not specified", () => {
@@ -1,55 +1,54 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { triggerToXml, buildTaskXml } from "../src/platform/windows.js";
3
+ import { scheduleValueToXml, buildTaskXml } from "../src/platform/windows.js";
4
4
 
5
- describe("triggerToXml", () => {
6
- it("converts a once trigger to TimeTrigger", () => {
7
- const xml = triggerToXml({ type: "once", value: "2026-03-28T09:00" });
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 = triggerToXml({ type: "cron", value: "0 * * * *" });
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 = triggerToXml({ type: "cron", value: "30 9 * * *" });
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 = triggerToXml({ type: "cron", value: "0 10 * * 1" });
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 = triggerToXml({ type: "cron", value: "0 8 * * 0" });
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 = triggerToXml({ type: "cron", value: "0 8 * * 7" });
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 = triggerToXml({ type: "cron", value: "0 14 15 * *" });
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(() => triggerToXml({ type: "cron", value: "bad" }), /Invalid cron/);
51
+ assert.throws(() => scheduleValueToXml("crons", "bad"), /Invalid cron/);
53
52
  });
54
53
  });
55
54