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.
Files changed (132) 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 +14 -18
  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-B0F9mtid.css +1 -0
  53. package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
  54. package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
  55. package/dist/pwa/assets/{web-BpM3fNCn.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 +19 -48
  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/README.md +1 -1
  73. package/palmier-server/pwa/src/App.css +170 -20
  74. package/palmier-server/pwa/src/App.tsx +15 -1
  75. package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
  76. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
  77. package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
  78. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
  79. package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
  80. package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
  81. package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
  82. package/palmier-server/pwa/src/constants.ts +1 -1
  83. package/palmier-server/pwa/src/native/Device.ts +66 -0
  84. package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
  85. package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
  86. package/palmier-server/pwa/src/types.ts +1 -1
  87. package/palmier-server/server/src/index.ts +7 -7
  88. package/palmier-server/server/src/routes/device.ts +4 -4
  89. package/palmier-server/spec.md +47 -6
  90. package/src/agents/agent.ts +0 -4
  91. package/src/agents/claude.ts +1 -1
  92. package/src/agents/codex.ts +2 -2
  93. package/src/agents/cursor.ts +1 -1
  94. package/src/agents/deepagents.ts +1 -1
  95. package/src/agents/gemini.ts +3 -2
  96. package/src/agents/goose.ts +1 -1
  97. package/src/agents/hermes.ts +1 -1
  98. package/src/agents/kiro.ts +1 -1
  99. package/src/agents/opencode.ts +1 -1
  100. package/src/agents/qoder.ts +1 -1
  101. package/src/agents/shared-prompt.ts +0 -3
  102. package/src/app-registry.ts +52 -0
  103. package/src/commands/info.ts +0 -5
  104. package/src/commands/init.ts +2 -11
  105. package/src/commands/pair.ts +1 -12
  106. package/src/commands/restart.ts +0 -3
  107. package/src/commands/run.ts +18 -65
  108. package/src/commands/serve.ts +31 -27
  109. package/src/config.ts +0 -8
  110. package/src/device-capabilities.ts +4 -3
  111. package/src/event-queues.ts +6 -21
  112. package/src/events.ts +1 -9
  113. package/src/index.ts +0 -1
  114. package/src/mcp-handler.ts +1 -2
  115. package/src/mcp-tools.ts +14 -20
  116. package/src/nats-client.ts +1 -4
  117. package/src/pending-requests.ts +4 -18
  118. package/src/platform/index.ts +1 -4
  119. package/src/platform/linux.ts +9 -20
  120. package/src/platform/platform.ts +1 -4
  121. package/src/platform/windows.ts +19 -40
  122. package/src/rpc-handler.ts +20 -48
  123. package/src/spawn-command.ts +11 -27
  124. package/src/task.ts +7 -70
  125. package/src/transports/http-transport.ts +6 -39
  126. package/src/transports/nats-transport.ts +3 -9
  127. package/src/types.ts +3 -10
  128. package/src/update-checker.ts +2 -5
  129. package/test/task-parsing.test.ts +2 -3
  130. package/test/windows-xml.test.ts +11 -12
  131. package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
  132. package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
package/dist/task.js CHANGED
@@ -1,9 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
- /**
5
- * Parse a TASK.md file from the given task directory.
6
- */
7
4
  export function parseTaskFile(taskDir) {
8
5
  const filePath = path.join(taskDir, "TASK.md");
9
6
  if (!fs.existsSync(filePath)) {
@@ -12,9 +9,6 @@ export function parseTaskFile(taskDir) {
12
9
  const content = fs.readFileSync(filePath, "utf-8");
13
10
  return parseTaskContent(content);
14
11
  }
15
- /**
16
- * Parse TASK.md content string into frontmatter + body.
17
- */
18
12
  export function parseTaskContent(content) {
19
13
  const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
20
14
  const match = content.match(fmRegex);
@@ -30,10 +24,6 @@ export function parseTaskContent(content) {
30
24
  frontmatter.schedule_enabled ??= true;
31
25
  return { frontmatter };
32
26
  }
33
- /**
34
- * Write a TASK.md file to the given task directory.
35
- * Creates the directory if it doesn't exist.
36
- */
37
27
  export function writeTaskFile(taskDir, task) {
38
28
  fs.mkdirSync(taskDir, { recursive: true });
39
29
  const yamlStr = stringifyYaml(task.frontmatter).trim();
@@ -41,17 +31,10 @@ export function writeTaskFile(taskDir, task) {
41
31
  const filePath = path.join(taskDir, "TASK.md");
42
32
  fs.writeFileSync(filePath, content, "utf-8");
43
33
  }
44
- /**
45
- * Append a task ID to the project-level tasks.jsonl file.
46
- */
47
34
  export function appendTaskList(projectRoot, taskId) {
48
35
  const listPath = path.join(projectRoot, "tasks.jsonl");
49
36
  fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
50
37
  }
51
- /**
52
- * Remove a task ID from the project-level tasks.jsonl file.
53
- * Returns true if the entry was found and removed.
54
- */
55
38
  export function removeFromTaskList(projectRoot, taskId) {
56
39
  const listPath = path.join(projectRoot, "tasks.jsonl");
57
40
  if (!fs.existsSync(listPath))
@@ -75,9 +58,6 @@ export function removeFromTaskList(projectRoot, taskId) {
75
58
  fs.writeFileSync(listPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
76
59
  return true;
77
60
  }
78
- /**
79
- * List all tasks referenced in tasks.jsonl.
80
- */
81
61
  export function listTasks(projectRoot) {
82
62
  const listPath = path.join(projectRoot, "tasks.jsonl");
83
63
  if (!fs.existsSync(listPath))
@@ -102,23 +82,13 @@ export function listTasks(projectRoot) {
102
82
  }
103
83
  return tasks.reverse();
104
84
  }
105
- /**
106
- * Get the directory path for a task by its ID.
107
- */
108
85
  export function getTaskDir(projectRoot, taskId) {
109
86
  return path.join(projectRoot, "tasks", taskId);
110
87
  }
111
- /**
112
- * Write task status to status.json in the task directory.
113
- */
114
88
  export function writeTaskStatus(taskDir, status) {
115
89
  const filePath = path.join(taskDir, "status.json");
116
90
  fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
117
91
  }
118
- /**
119
- * Read task status from status.json in the task directory.
120
- * Returns undefined if the file doesn't exist.
121
- */
122
92
  export function readTaskStatus(taskDir) {
123
93
  const filePath = path.join(taskDir, "status.json");
124
94
  try {
@@ -128,10 +98,7 @@ export function readTaskStatus(taskDir) {
128
98
  return undefined;
129
99
  }
130
100
  }
131
- /**
132
- * Create a run directory with an initial TASKRUN.md file.
133
- * Returns the run ID (timestamp string used as directory name).
134
- */
101
+ /** Returns the run ID (timestamp string used as directory name). */
135
102
  export function createRunDir(taskDir, taskName, startTime, agent) {
136
103
  const runId = String(startTime);
137
104
  const runDir = path.join(taskDir, runId);
@@ -141,15 +108,9 @@ export function createRunDir(taskDir, taskName, startTime, agent) {
141
108
  fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
142
109
  return runId;
143
110
  }
144
- /**
145
- * Get the path to a run directory.
146
- */
147
111
  export function getRunDir(taskDir, runId) {
148
112
  return path.join(taskDir, runId);
149
113
  }
150
- /**
151
- * Append a conversation message to a run's TASKRUN.md file.
152
- */
153
114
  export function appendRunMessage(taskDir, runId, msg) {
154
115
  const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
155
116
  if (msg.type)
@@ -160,10 +121,6 @@ export function appendRunMessage(taskDir, runId, msg) {
160
121
  const entry = `${delimiter}\n\n${msg.content}\n\n`;
161
122
  fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
162
123
  }
163
- /**
164
- * Begin a streaming assistant message — writes the delimiter only.
165
- * Returns a writer that appends content chunks and finalizes the message.
166
- */
167
124
  export function beginStreamingMessage(taskDir, runId, time) {
168
125
  const filePath = path.join(taskDir, runId, "TASKRUN.md");
169
126
  const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
@@ -184,7 +141,7 @@ export class StreamingMessageWriter {
184
141
  fs.appendFileSync(this.filePath, "\n\n", "utf-8");
185
142
  if (attachments?.length) {
186
143
  const raw = fs.readFileSync(this.filePath, "utf-8");
187
- // Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
144
+ // spliceUserMessage may have created a newer assistant delimiter.
188
145
  const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
189
146
  let lastMatch = null;
190
147
  let m;
@@ -200,32 +157,23 @@ export class StreamingMessageWriter {
200
157
  }
201
158
  }
202
159
  /**
203
- * Splice a user message into a running assistant stream.
204
- * Ends the current assistant block, writes the user message,
205
- * then opens a new assistant block all as direct file appends.
206
- * The existing StreamingMessageWriter keeps working because its
207
- * write() is just appendFileSync, so subsequent chunks land in
208
- * the new assistant block.
160
+ * Splice a user message into a running assistant stream: close the current
161
+ * assistant block, write the user message, open a new assistant block. Direct
162
+ * appends only, so an existing StreamingMessageWriter keeps working its
163
+ * subsequent chunks land in the new block.
209
164
  */
210
165
  export function spliceUserMessage(taskDir, runId, userMsg,
211
166
  /** Optional text to append to the current assistant block before ending it. */
212
167
  assistantAppend) {
213
168
  const filePath = path.join(taskDir, runId, "TASKRUN.md");
214
- // 1. Optionally append to the current assistant block (e.g. the input questions)
215
169
  if (assistantAppend) {
216
170
  fs.appendFileSync(filePath, assistantAppend, "utf-8");
217
171
  }
218
- // 2. End the current assistant block
219
172
  fs.appendFileSync(filePath, "\n\n", "utf-8");
220
- // 3. Write the user message
221
173
  appendRunMessage(taskDir, runId, userMsg);
222
- // 4. Open a new assistant block for subsequent agent output
223
174
  const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
224
175
  fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
225
176
  }
226
- /**
227
- * Read conversation messages from a run's TASKRUN.md file.
228
- */
229
177
  export function readRunMessages(taskDir, runId) {
230
178
  const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
231
179
  const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
@@ -257,17 +205,10 @@ export function readRunMessages(taskDir, runId) {
257
205
  }
258
206
  return messages;
259
207
  }
260
- /**
261
- * Append a history entry to the project-level history.jsonl file.
262
- */
263
208
  export function appendHistory(projectRoot, entry) {
264
209
  const historyPath = path.join(projectRoot, "history.jsonl");
265
210
  fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
266
211
  }
267
- /**
268
- * Delete a history entry and its associated run directory.
269
- * Returns true if the entry was found and removed.
270
- */
271
212
  export function deleteHistoryEntry(projectRoot, taskId, runId) {
272
213
  const historyPath = path.join(projectRoot, "history.jsonl");
273
214
  if (!fs.existsSync(historyPath))
@@ -289,17 +230,13 @@ export function deleteHistoryEntry(projectRoot, taskId, runId) {
289
230
  if (!found)
290
231
  return false;
291
232
  fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
292
- // Delete the run directory
293
233
  const runDir = path.join(projectRoot, "tasks", taskId, runId);
294
234
  if (fs.existsSync(runDir)) {
295
235
  fs.rmSync(runDir, { recursive: true, force: true });
296
236
  }
297
237
  return true;
298
238
  }
299
- /**
300
- * Read history entries from history.jsonl with pagination.
301
- * Returns entries sorted most-recent-first.
302
- */
239
+ /** Returns entries most-recent-first. */
303
240
  export function readHistory(projectRoot, opts) {
304
241
  const historyPath = path.join(projectRoot, "history.jsonl");
305
242
  if (!fs.existsSync(historyPath))
@@ -1,9 +1,5 @@
1
1
  import { type NatsConnection } from "nats";
2
2
  import type { HostConfig, RpcMessage } from "../types.js";
3
3
  export declare function detectLanIp(): string;
4
- /**
5
- * Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
6
- * localhost-only agent endpoints (notify, request-input, confirmation, permission).
7
- */
8
4
  export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, port: number, nc: NatsConnection | undefined, pairingCode?: string, onReady?: () => void): Promise<void>;
9
5
  //# sourceMappingURL=http-transport.d.ts.map
@@ -29,21 +29,17 @@ function guessContentType(urlPath) {
29
29
  const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
30
30
  return CONTENT_TYPES[ext] ?? "application/octet-stream";
31
31
  }
32
- /**
33
- * Read a PWA asset from the bundled pwa/ directory, caching in memory.
34
- * Returns null if the file does not exist.
35
- */
36
32
  function getAsset(urlPath) {
37
33
  const cached = assetCache.get(urlPath);
38
34
  if (cached)
39
35
  return cached;
40
36
  const filePath = path.join(PWA_DIR, urlPath === "/" ? "index.html" : urlPath);
41
- // Prevent path traversal
37
+ // Prevent path traversal.
42
38
  if (!filePath.startsWith(PWA_DIR))
43
39
  return null;
44
40
  try {
45
41
  let data = fs.readFileSync(filePath);
46
- // Inject marker into index HTML so the PWA can detect it's served by palmier
42
+ // Marker lets the PWA detect it's served by palmier.
47
43
  if (urlPath === "/") {
48
44
  const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
49
45
  data = Buffer.from(html, "utf-8");
@@ -68,10 +64,6 @@ export function detectLanIp() {
68
64
  }
69
65
  return "127.0.0.1";
70
66
  }
71
- /**
72
- * Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
73
- * localhost-only agent endpoints (notify, request-input, confirmation, permission).
74
- */
75
67
  export async function startHttpTransport(config, handleRpc, port, nc, pairingCode, onReady) {
76
68
  const sseClients = new Set();
77
69
  const mcpStreams = new Map();
@@ -96,7 +88,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
96
88
  for (const resource of agentResources) {
97
89
  resource.subscribe(() => broadcastResourceUpdated(resource.uri));
98
90
  }
99
- // If a pairing code is provided, pre-register it
100
91
  if (pairingCode) {
101
92
  const EXPIRY_MS = 24 * 60 * 60 * 1000;
102
93
  const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
@@ -136,9 +127,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
136
127
  const addr = req.socket.remoteAddress;
137
128
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
138
129
  }
139
- /**
140
- * Publish an event via NATS and SSE.
141
- */
142
130
  async function publishEvent(taskId, payload) {
143
131
  const sc = StringCodec();
144
132
  const subject = `host-event.${config.hostId}.${taskId}`;
@@ -153,7 +141,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
153
141
  const server = http.createServer(async (req, res) => {
154
142
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
155
143
  const pathname = url.pathname;
156
- // ── MCP streamable HTTP endpoint ──────────────────────────────────
157
144
  if (req.method === "POST" && pathname === "/mcp") {
158
145
  if (!isLocalhost(req)) {
159
146
  sendJson(res, 403, { error: "localhost only" });
@@ -168,7 +155,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
168
155
  res.setHeader("Mcp-Session-Id", result.sessionId);
169
156
  }
170
157
  if (result.stream && sessionId) {
171
- // Keep response open as SSE stream for server-initiated notifications
158
+ // Keep the response open as SSE for server-initiated notifications.
172
159
  res.writeHead(200, {
173
160
  "Content-Type": "text/event-stream",
174
161
  "Cache-Control": "no-cache",
@@ -192,7 +179,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
192
179
  }
193
180
  return;
194
181
  }
195
- // ── Auto-generated REST endpoints from MCP tool registry ──────────
196
182
  if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
197
183
  if (!isLocalhost(req)) {
198
184
  sendJson(res, 403, { error: "localhost only" });
@@ -225,7 +211,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
225
211
  }
226
212
  return;
227
213
  }
228
- // ── Auto-generated REST endpoints from MCP resource registry ────
229
214
  const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
230
215
  if (matchedResource) {
231
216
  if (!isLocalhost(req)) {
@@ -248,7 +233,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
248
233
  sendJson(res, 200, result);
249
234
  return;
250
235
  }
251
- // ── Event queue pop (used by event-triggered palmier run) ─────────
252
236
  if (req.method === "POST" && pathname === "/task-event/pop") {
253
237
  if (!isLocalhost(req)) {
254
238
  sendJson(res, 403, { error: "localhost only" });
@@ -262,7 +246,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
262
246
  sendJson(res, 200, popEvent(taskId));
263
247
  return;
264
248
  }
265
- // ── Localhost-only endpoints (no auth) ─────────────────────────────
266
249
  if (req.method === "POST" && pathname === "/event") {
267
250
  if (!isLocalhost(req)) {
268
251
  sendJson(res, 403, { error: "localhost only" });
@@ -315,7 +298,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
315
298
  }
316
299
  return;
317
300
  }
318
- // ── POST /request-permission — held connection ──────────────────────
319
301
  if (req.method === "POST" && pathname === "/request-permission") {
320
302
  if (!isLocalhost(req)) {
321
303
  sendJson(res, 403, { error: "localhost only" });
@@ -352,7 +334,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
352
334
  }
353
335
  return;
354
336
  }
355
- // ── Public pair endpoint — no auth, PWA posts pairing code here ────────
356
337
  if (req.method === "POST" && pathname === "/pair") {
357
338
  try {
358
339
  const body = await readBody(req);
@@ -383,8 +364,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
383
364
  }
384
365
  return;
385
366
  }
386
- // ── PWA assets (on-the-fly, cached) ────────────────────────────────
387
- // Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
367
+ // Service worker and manifest require HTTPS, which LAN mode doesn't use.
388
368
  const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
389
369
  const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
390
370
  if (!isApiRoute) {
@@ -392,7 +372,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
392
372
  sendJson(res, 404, { error: "Not found" });
393
373
  return;
394
374
  }
395
- // Try exact path, then fall back to index.html (SPA routing)
375
+ // Fall back to index.html for SPA routing.
396
376
  let asset = getAsset(pathname);
397
377
  if (!asset && pathname !== "/") {
398
378
  asset = getAsset("/");
@@ -406,12 +386,11 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
406
386
  }
407
387
  return;
408
388
  }
409
- // ── API endpoints require auth (localhost is trusted) ───────────────
389
+ // Localhost is trusted; all other API callers require a client token.
410
390
  if (!isLocalhost(req) && !checkAuth(req)) {
411
391
  sendJson(res, 401, { error: "Unauthorized" });
412
392
  return;
413
393
  }
414
- // SSE event stream
415
394
  if (req.method === "GET" && pathname === "/events") {
416
395
  res.writeHead(200, {
417
396
  "Content-Type": "text/event-stream",
@@ -429,7 +408,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
429
408
  });
430
409
  return;
431
410
  }
432
- // RPC endpoint: POST /rpc/<method>
433
411
  if (req.method === "POST" && pathname.startsWith("/rpc/")) {
434
412
  const method = pathname.slice("/rpc/".length);
435
413
  if (!method) {
@@ -1,8 +1,4 @@
1
1
  import { type NatsConnection } from "nats";
2
2
  import type { HostConfig, RpcMessage } from "../types.js";
3
- /**
4
- * Start the NATS transport using an existing connection.
5
- * Subscribe to RPC subjects and dispatch to handler.
6
- */
7
3
  export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, nc: NatsConnection): Promise<void>;
8
4
  //# sourceMappingURL=nats-transport.d.ts.map
@@ -1,14 +1,9 @@
1
1
  import { StringCodec } from "nats";
2
- /**
3
- * Start the NATS transport using an existing connection.
4
- * Subscribe to RPC subjects and dispatch to handler.
5
- */
6
2
  export async function startNatsTransport(config, handleRpc, nc) {
7
3
  const sc = StringCodec();
8
4
  const subject = `host.${config.hostId}.rpc.>`;
9
5
  console.log(`[nats] Subscribing to: ${subject}`);
10
6
  const sub = nc.subscribe(subject);
11
- // Graceful shutdown
12
7
  const shutdown = async () => {
13
8
  console.log("[nats] Shutting down...");
14
9
  sub.unsubscribe();
@@ -18,11 +13,10 @@ export async function startNatsTransport(config, handleRpc, nc) {
18
13
  process.on("SIGINT", shutdown);
19
14
  process.on("SIGTERM", shutdown);
20
15
  async function processMessage(msg) {
21
- // Derive RPC method from subject: ...rpc.<method parts>
16
+ // Subject format: ...rpc.<method parts>
22
17
  const subjectTokens = msg.subject.split(".");
23
18
  const rpcIdx = subjectTokens.indexOf("rpc");
24
19
  const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
25
- // Parse params from message body
26
20
  let params = {};
27
21
  if (msg.data && msg.data.length > 0) {
28
22
  const raw = sc.decode(msg.data).trim();
@@ -39,7 +33,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
39
33
  }
40
34
  }
41
35
  }
42
- // Extract clientToken from params (PWA includes it in the payload)
36
+ // PWA includes the client token in the payload.
43
37
  const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
44
38
  delete params.clientToken;
45
39
  console.log(`[nats] RPC: ${method}`);
@@ -58,7 +52,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
58
52
  }
59
53
  async function consumeSubscription(subscription) {
60
54
  for await (const msg of subscription) {
61
- // Handle RPC without blocking the message loop so heartbeats keep flowing
55
+ // Don't await heartbeats must keep flowing while RPC runs.
62
56
  processMessage(msg);
63
57
  }
64
58
  }
package/dist/types.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface HostConfig {
12
12
  supportsYolo: boolean;
13
13
  }>;
14
14
  httpPort?: number;
15
+ /** Whether to accept non-localhost HTTP connections. */
15
16
  lanEnabled?: boolean;
16
17
  }
17
18
  export interface TaskFrontmatter {
@@ -23,8 +24,8 @@ export interface TaskFrontmatter {
23
24
  * Task schedule.
24
25
  * - `crons`: `schedule_values` holds cron expressions (e.g. "0 9 * * *")
25
26
  * - `specific_times`: `schedule_values` holds local datetime strings (e.g. "2026-04-20T09:00")
26
- * - `on_new_notification`: fires on each new Android notification from NATS; no `schedule_values`
27
- * - `on_new_sms`: fires on each new SMS from NATS; no `schedule_values`
27
+ * - `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.
28
+ * - `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.
28
29
  */
29
30
  schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
30
31
  schedule_values?: string[];
@@ -45,11 +46,6 @@ export interface ParsedTask {
45
46
  * - `failed`: agent exited with an error
46
47
  */
47
48
  export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
48
- /**
49
- * Persisted to `status.json` in the task directory. Used for crash detection
50
- * (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
51
- * permission, input) are handled via held HTTP connections on the serve daemon.
52
- */
53
49
  export interface TaskStatus {
54
50
  running_state: TaskRunningState;
55
51
  time_stamp: number;
@@ -1,9 +1,6 @@
1
1
  /** True when running from a source checkout (has .git) rather than a global npm install. */
2
2
  export declare const isDevBuild: boolean;
3
3
  export declare const currentVersion: string;
4
- /**
5
- * Run the update and restart the daemon.
6
- * Returns an error message if the update fails.
7
- */
4
+ /** Returns an error message if the update fails. */
8
5
  export declare function performUpdate(): Promise<string | null>;
9
6
  //# sourceMappingURL=update-checker.d.ts.map
@@ -9,10 +9,7 @@ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "
9
9
  /** True when running from a source checkout (has .git) rather than a global npm install. */
10
10
  export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
11
11
  export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
12
- /**
13
- * Run the update and restart the daemon.
14
- * Returns an error message if the update fails.
15
- */
12
+ /** Returns an error message if the update fails. */
16
13
  export async function performUpdate() {
17
14
  try {
18
15
  const { output, exitCode } = await spawnCommand("npm", ["update", "-g", "palmier"], {
@@ -25,7 +22,7 @@ export async function performUpdate() {
25
22
  return `Update failed. Please run manually:\nnpm update -g palmier`;
26
23
  }
27
24
  console.log("[update] Update installed, restarting daemon...");
28
- // Small delay to allow the RPC response to be sent
25
+ // Delay so the RPC response finishes sending first.
29
26
  setTimeout(() => {
30
27
  getPlatform().restartDaemon().catch((err) => {
31
28
  console.error("[update] Restart failed:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -183,7 +183,7 @@ All endpoints are prefixed with `/api`. No user authentication is required.
183
183
  - **NATS RPC** — the RPC method is derived from the NATS subject (e.g., `...rpc.task.list` → `task.list`), not the message body. The body contains request parameters plus `clientToken`. All NATS requests go through a centralized `request()` helper in `HostConnectionContext` that handles encoding/decoding and logging.
184
184
  - **Pairing** — `palmier pair` (or auto-pair after `palmier init`) generates a 6-char pairing code. The PWA enters the code, which routes to the host via NATS (`pair.<CODE>`) or HTTP (`POST /pair`). The host validates the code and returns a client token.
185
185
  - **Task IDs** are generated by the host as UUIDs.
186
- - **Schedule can be enabled/disabled** — the `schedule_enabled` frontmatter field (default `true`) controls whether systemd timers are installed. When disabled, timers are removed but the task can still be run manually. The schedule lives in two flat fields: `schedule_type` (`"crons"` or `"specific_times"`) and `schedule_values` (array of cron expressions or local datetime strings). Both are present together or absent together.
186
+ - **Schedule can be enabled/disabled** — the `schedule_enabled` frontmatter field (default `true`) controls whether the schedule is active. When disabled, timers are removed and device events are ignored for that task, but it can still be run manually. The schedule lives in two flat fields: `schedule_type` (`"crons"`, `"specific_times"`, `"on_new_notification"`, or `"on_new_sms"`) and `schedule_values` (array of cron expressions or local datetime strings — only used by `"crons"` and `"specific_times"`). The two `on_new_*` types have no `schedule_values`; they fire in response to device notifications/SMS relayed over NATS, with the daemon owning a per-task FIFO queue that `palmier run` drains via `POST /task-event/pop`.
187
187
  - **Host responses** return flat task objects (frontmatter fields at the top level, not nested) for `task.list`, `task.create`, and `task.update`.
188
188
  - **NATS "503"** means "no responders" — the dashboard silently handles this when no host is connected, showing an empty task list instead of an error.
189
189
  - **Helmet CSP** is disabled (`contentSecurityPolicy: false`) to allow NATS WebSocket connections and inline Vite scripts during dev.