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
@@ -13,10 +13,6 @@ import { publishHostEvent } from "../events.js";
13
13
  import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
14
14
  import type { NatsConnection } from "nats";
15
15
 
16
- /**
17
- * Shared context for agent invocation retry loops.
18
- * Passed around to avoid threading many individual parameters.
19
- */
20
16
  interface InvocationContext {
21
17
  agent: AgentTool;
22
18
  task: ParsedTask;
@@ -35,11 +31,9 @@ interface InvocationResult {
35
31
  }
36
32
 
37
33
  /**
38
- * Invoke the agent CLI with a continuation loop for permissions and user input.
39
- *
40
- * Both standard and command-triggered execution use this.
41
- * The `invokeTask` is the ParsedTask whose prompt is passed to the agent
42
- * (for command-triggered mode this is the per-line augmented task).
34
+ * Invoke the agent CLI in a continuation loop to handle permission requests.
35
+ * `invokeTask` is the ParsedTask whose prompt is passed to the agent (in
36
+ * command-triggered mode this is the per-line augmented task).
43
37
  */
44
38
  async function invokeAgentWithRetries(
45
39
  ctx: InvocationContext,
@@ -47,7 +41,6 @@ async function invokeAgentWithRetries(
47
41
  ): Promise<InvocationResult> {
48
42
  // eslint-disable-next-line no-constant-condition
49
43
  while (true) {
50
- // Stream agent output to TASKRUN.md in real-time, throttled to 500ms
51
44
  const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
52
45
  let lineBuf = "";
53
46
  let notifyPending = false;
@@ -89,12 +82,10 @@ async function invokeAgentWithRetries(
89
82
  const reportFiles = parseReportFiles(result.output);
90
83
  const requiredPermissions = parsePermissions(result.output);
91
84
 
92
- // Flush remaining buffered content
93
85
  if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
94
86
  writer.write(lineBuf);
95
87
  }
96
88
 
97
- // Include permission requests in the assistant message
98
89
  if (requiredPermissions.length > 0) {
99
90
  const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
100
91
  writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
@@ -112,7 +103,6 @@ async function invokeAgentWithRetries(
112
103
  });
113
104
  }
114
105
 
115
- // Permission handling — agent requested permissions
116
106
  if (requiredPermissions.length > 0) {
117
107
  const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
118
108
 
@@ -146,27 +136,20 @@ async function invokeAgentWithRetries(
146
136
  ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
147
137
  }
148
138
 
149
- // If the agent actually failed, retry with the new permissions
139
+ // Retry with the new permissions if the agent failed.
150
140
  if (outcome === "failed") {
151
141
  continue;
152
142
  }
153
143
  }
154
144
 
155
- // Normal completion (success or terminal failure)
156
145
  return { outcome };
157
146
  }
158
147
  }
159
148
 
160
- /**
161
- * Strip [PALMIER_*] marker lines from agent output.
162
- */
163
149
  export function stripPalmierMarkers(output: string): string {
164
150
  return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
165
151
  }
166
152
 
167
- /**
168
- * Append a conversation message to the RESULT file and notify connected clients.
169
- */
170
153
  async function appendAndNotify(
171
154
  ctx: InvocationContext,
172
155
  msg: Parameters<typeof appendRunMessage>[2],
@@ -175,9 +158,7 @@ async function appendAndNotify(
175
158
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
176
159
  }
177
160
 
178
- /**
179
- * Find the latest run dir that has no status messages yet (just created by the RPC handler).
180
- */
161
+ /** The latest run dir with no status messages yet — freshly created by the RPC handler. */
181
162
  function findLatestPendingRunId(taskDir: string): string | null {
182
163
  const dirs = fs.readdirSync(taskDir)
183
164
  .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
@@ -190,8 +171,8 @@ function findLatestPendingRunId(taskDir: string): string | null {
190
171
  }
191
172
 
192
173
  /**
193
- * If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
194
- * respect that instead of overwriting with the process's own outcome.
174
+ * If the RPC handler already wrote "aborted" (via task.abort), respect that
175
+ * instead of overwriting with the process's own outcome.
195
176
  */
196
177
  function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunningState {
197
178
  const current = readTaskStatus(taskDir);
@@ -199,9 +180,6 @@ function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunning
199
180
  return outcome;
200
181
  }
201
182
 
202
- /**
203
- * Execute a task by ID.
204
- */
205
183
  export async function runCommand(taskId: string): Promise<void> {
206
184
  const config = loadConfig();
207
185
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -211,7 +189,6 @@ export async function runCommand(taskId: string): Promise<void> {
211
189
  let nc: NatsConnection | undefined;
212
190
  const taskName = task.frontmatter.name;
213
191
 
214
- // Use existing run dir if just created by RPC, otherwise create a new one
215
192
  const existingRunId = findLatestPendingRunId(taskDir);
216
193
  const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
217
194
  if (!existingRunId) {
@@ -231,7 +208,6 @@ export async function runCommand(taskId: string): Promise<void> {
231
208
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
232
209
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
233
210
 
234
- // If requires_confirmation, notify clients and wait
235
211
  if (task.frontmatter.requires_confirmation) {
236
212
  const confirmed = await requestConfirmation(config, task, taskDir);
237
213
  const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
@@ -252,7 +228,6 @@ export async function runCommand(taskId: string): Promise<void> {
252
228
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
253
229
  }
254
230
 
255
- // Shared invocation context
256
231
  const guiEnv = getPlatform().getGuiEnv();
257
232
  const agent = getAgent(task.frontmatter.agent);
258
233
  const ctx: InvocationContext = {
@@ -261,7 +236,6 @@ export async function runCommand(taskId: string): Promise<void> {
261
236
  };
262
237
 
263
238
  if (task.frontmatter.command) {
264
- // Command-triggered mode
265
239
  const result = await runCommandTriggeredMode(ctx);
266
240
  const outcome = resolveOutcome(taskDir, result.outcome);
267
241
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
@@ -269,14 +243,12 @@ export async function runCommand(taskId: string): Promise<void> {
269
243
  console.log(`Task ${taskId} completed (command-triggered).`);
270
244
  } else if (task.frontmatter.schedule_type === "on_new_notification"
271
245
  || task.frontmatter.schedule_type === "on_new_sms") {
272
- // Event-triggered mode (driven by NATS pub/sub of device notifications/SMS)
273
246
  const result = await runEventTriggeredMode(ctx);
274
247
  const outcome = resolveOutcome(taskDir, result.outcome);
275
248
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
276
249
  await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
277
250
  console.log(`Task ${taskId} completed (event-triggered).`);
278
251
  } else {
279
- // Standard execution — add user prompt as first message
280
252
  await appendAndNotify(ctx, {
281
253
  role: "user",
282
254
  time: Date.now(),
@@ -312,11 +284,9 @@ const MAX_LOG_ENTRIES = 1000;
312
284
  const MAX_LINE_LENGTH = 200_000;
313
285
 
314
286
  /**
315
- * Command-triggered execution mode.
316
- *
317
- * Spawns a long-running shell command and, for each line of stdout,
318
- * invokes the agent CLI with the user's prompt augmented by that line.
319
- * Processes lines sequentially with a bounded queue.
287
+ * Spawn a long-running shell command and invoke the agent CLI once per stdout
288
+ * line, with the user's prompt augmented by that line. Sequential with a
289
+ * bounded queue.
320
290
  */
321
291
  async function runCommandTriggeredMode(
322
292
  ctx: InvocationContext,
@@ -346,7 +316,6 @@ async function runCommandTriggeredMode(
346
316
  const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
347
317
  fs.appendFileSync(logPath, entry, "utf-8");
348
318
 
349
- // Trim log if too large (keep last MAX_LOG_ENTRIES entries)
350
319
  try {
351
320
  const content = fs.readFileSync(logPath, "utf-8");
352
321
  const entries = content.split("\n---\n").filter(Boolean);
@@ -380,7 +349,7 @@ async function runCommandTriggeredMode(
380
349
  }
381
350
  appendLog(line, "", result.outcome);
382
351
 
383
- // Append monitoring status so the UI shows the task is waiting for more input
352
+ // Signal "waiting for more input" in the UI.
384
353
  appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
385
354
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
386
355
  }
@@ -422,7 +391,6 @@ async function runCommandTriggeredMode(
422
391
  process.stderr.write(d);
423
392
  });
424
393
 
425
- // Wait for command to exit
426
394
  const exitCode = await new Promise<number | null>((resolve) => {
427
395
  child.on("close", (code: number | null) => {
428
396
  commandExited = true;
@@ -438,7 +406,6 @@ async function runCommandTriggeredMode(
438
406
  });
439
407
  });
440
408
 
441
- // Wait for any remaining queued lines to finish processing
442
409
  if (lineQueue.length > 0 || processing) {
443
410
  await new Promise<void>((resolve) => {
444
411
  resolveWhenDone = resolve;
@@ -464,13 +431,10 @@ async function runCommandTriggeredMode(
464
431
  }
465
432
 
466
433
  /**
467
- * Event-triggered execution mode.
468
- *
469
- * Drains the daemon-owned per-task event queue via the local /task-event/pop
470
- * HTTP endpoint, invoking the agent once per event with the payload spliced
471
- * into the user prompt. The run process itself holds no NATS subscription;
472
- * the daemon handles that and atomically clears the active flag when we see
473
- * an empty pop, so it can fire up a fresh run on the next incoming event.
434
+ * Drain the daemon-owned per-task event queue via /task-event/pop, invoking
435
+ * the agent once per event. The run process holds no NATS subscription — the
436
+ * daemon owns that and atomically clears the active flag on empty pop so it
437
+ * can fire a fresh run on the next incoming event.
474
438
  */
475
439
  async function runEventTriggeredMode(
476
440
  ctx: InvocationContext,
@@ -590,10 +554,6 @@ async function requestConfirmation(
590
554
  return confirmed;
591
555
  }
592
556
 
593
- /**
594
- * Extract report file names from agent output.
595
- * Looks for lines matching: [PALMIER_REPORT] <filename>
596
- */
597
557
  const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
598
558
 
599
559
  export function parseReportFiles(output: string): string[] {
@@ -602,7 +562,7 @@ export function parseReportFiles(output: string): string[] {
602
562
  let match;
603
563
  while ((match = regex.exec(output)) !== null) {
604
564
  const name = match[1].trim();
605
- // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
565
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>").
606
566
  if (!name || name.startsWith("<")) continue;
607
567
  const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
608
568
  if (!ALLOWED_REPORT_EXT.includes(ext)) continue;
@@ -611,17 +571,13 @@ export function parseReportFiles(output: string): string[] {
611
571
  return files;
612
572
  }
613
573
 
614
- /**
615
- * Extract required permissions from agent output.
616
- * Looks for lines matching: [PALMIER_PERMISSION] <tool> | <description>
617
- */
618
574
  export function parsePermissions(output: string): RequiredPermission[] {
619
575
  const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
620
576
  const perms: RequiredPermission[] = [];
621
577
  let match;
622
578
  while ((match = regex.exec(output)) !== null) {
623
579
  const raw = match[1].trim();
624
- // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
580
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
625
581
  if (raw.startsWith("<")) continue;
626
582
  const sep = raw.indexOf("|");
627
583
  if (sep !== -1) {
@@ -633,10 +589,7 @@ export function parsePermissions(output: string): RequiredPermission[] {
633
589
  return perms;
634
590
  }
635
591
 
636
- /**
637
- * Parse the agent's output for success/failure markers.
638
- * Falls back to "finished" if no marker is found.
639
- */
592
+ /** Falls back to "finished" if no success/failure marker is found. */
640
593
  export function parseTaskOutcome(output: string): TaskRunningState {
641
594
  const lastChunk = output.slice(-500);
642
595
  const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
@@ -16,16 +16,14 @@ import { StringCodec, type NatsConnection } from "nats";
16
16
  import { addNotification } from "../notification-store.js";
17
17
  import { addSmsMessage } from "../sms-store.js";
18
18
  import { enqueueEvent } from "../event-queues.js";
19
+ import { recordApp } from "../app-registry.js";
19
20
 
20
21
  const POLL_INTERVAL_MS = 30_000;
21
22
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
22
23
 
23
24
  /**
24
- * Scan all tasks for any stuck in "started" state whose process is no longer alive.
25
- * Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
26
- *
27
- * Since run.ts creates the RESULT file and history entry at start, we just need to
28
- * finalize the existing RESULT file, append a failed status entry, and broadcast.
25
+ * Reconcile tasks stuck in "started" whose process is no longer alive.
26
+ * The system scheduler (Task Scheduler / systemd) is the authoritative source.
29
27
  */
30
28
  async function checkStaleTasks(
31
29
  config: HostConfig,
@@ -46,14 +44,12 @@ async function checkStaleTasks(
46
44
  const status = readTaskStatus(taskDir);
47
45
  if (!status || status.running_state !== "started") continue;
48
46
 
49
- // Ask the system scheduler if the task is still running
50
47
  if (platform.isTaskRunning(taskId)) continue;
51
48
 
52
49
  console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
53
50
  const endTime = Date.now();
54
51
  writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
55
52
 
56
- // Find the latest run directory (created by run.ts at start)
57
53
  const runId = fs.readdirSync(taskDir)
58
54
  .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
59
55
  .sort()
@@ -71,7 +67,7 @@ async function checkStaleTasks(
71
67
  let taskName = taskId;
72
68
  try {
73
69
  taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
74
- } catch { /* use taskId as fallback */ }
70
+ } catch { /* fallback to taskId */ }
75
71
 
76
72
  await publishHostEvent(nc, config.hostId, taskId, {
77
73
  event_type: "running-state",
@@ -81,18 +77,14 @@ async function checkStaleTasks(
81
77
  }
82
78
  }
83
79
 
84
- /**
85
- * Start the persistent RPC handler (NATS + HTTP).
86
- */
87
80
  export async function serveCommand(): Promise<void> {
88
81
  const config = loadConfig();
89
82
 
90
- // Write PID so `palmier restart` can find us regardless of how we were started
83
+ // PID file lets `palmier restart` find us regardless of how we were started
91
84
  fs.writeFileSync(DAEMON_PID_FILE, String(process.pid), "utf-8");
92
85
 
93
86
  console.log("Starting...");
94
87
 
95
- // Re-detect agents on every daemon start
96
88
  const agents = await detectAgents();
97
89
  config.agents = agents;
98
90
  saveConfig(config);
@@ -106,10 +98,9 @@ export async function serveCommand(): Promise<void> {
106
98
  console.warn(`[nats] Connection failed (server mode unavailable): ${err}`);
107
99
  }
108
100
 
109
- // Reconcile any tasks stuck from before daemon started
110
101
  await checkStaleTasks(config, nc);
111
102
 
112
- // Ensure all tasks have their scheduler entries (recovery after init/reinstall)
103
+ // Reinstall scheduler entries for all tasks (recovery after init/reinstall)
113
104
  const platform = getPlatform();
114
105
  const allTasks = listTasks(config.projectRoot);
115
106
  for (const task of allTasks) {
@@ -120,7 +111,6 @@ export async function serveCommand(): Promise<void> {
120
111
  }
121
112
  }
122
113
 
123
- // Poll for crashed tasks every 30 seconds
124
114
  setInterval(() => {
125
115
  checkStaleTasks(config, nc).catch((err) => {
126
116
  console.error("[monitor] Error checking stale tasks:", err);
@@ -130,18 +120,29 @@ export async function serveCommand(): Promise<void> {
130
120
  const handleRpc = createRpcHandler(config, nc);
131
121
  const httpPort = config.httpPort ?? 7256;
132
122
 
133
- // Start NATS transport (loops forever, fire-and-forget)
134
123
  if (nc) {
135
124
  startNatsTransport(config, handleRpc, nc);
136
125
 
137
- // Subscribe to device notifications and SMS from Android
138
126
  const sc = StringCodec();
139
127
 
140
- // Dispatch a raw event payload to every task whose schedule matches.
141
- function dispatchDeviceEvent(scheduleType: "on_new_notification" | "on_new_sms", payload: string): void {
128
+ // Match phone numbers regardless of formatting; letters preserved for shortcodes.
129
+ function normalizeSender(raw: string): string {
130
+ return raw.replace(/[\s\-()+]/g, "").toLowerCase();
131
+ }
132
+
133
+ function dispatchDeviceEvent(scheduleType: "on_new_notification" | "on_new_sms", payload: string, parsed?: unknown): void {
142
134
  for (const task of listTasks(config.projectRoot)) {
143
135
  if (task.frontmatter.schedule_type !== scheduleType) continue;
144
136
  if (!task.frontmatter.schedule_enabled) continue;
137
+ if (scheduleType === "on_new_notification" && task.frontmatter.schedule_values && task.frontmatter.schedule_values.length > 0) {
138
+ const pkg = (parsed as { packageName?: string } | undefined)?.packageName;
139
+ if (!pkg || !task.frontmatter.schedule_values.includes(pkg)) continue;
140
+ }
141
+ if (scheduleType === "on_new_sms" && task.frontmatter.schedule_values && task.frontmatter.schedule_values.length > 0) {
142
+ const sender = (parsed as { sender?: string } | undefined)?.sender;
143
+ const normalizedSender = sender ? normalizeSender(sender) : "";
144
+ if (!normalizedSender || !task.frontmatter.schedule_values.some((s) => normalizeSender(s) === normalizedSender)) continue;
145
+ }
145
146
  const { shouldStart } = enqueueEvent(task.frontmatter.id, payload);
146
147
  if (shouldStart) {
147
148
  platform.startTask(task.frontmatter.id).catch((err) => {
@@ -155,13 +156,16 @@ export async function serveCommand(): Promise<void> {
155
156
  (async () => {
156
157
  for await (const msg of notifSub) {
157
158
  const raw = sc.decode(msg.data);
159
+ let parsed: unknown;
158
160
  try {
159
- const data = JSON.parse(raw);
160
- addNotification({ ...data, receivedAt: Date.now() });
161
+ parsed = JSON.parse(raw);
162
+ const data = parsed as { packageName?: string; appName?: string };
163
+ addNotification({ ...(parsed as object), receivedAt: Date.now() } as Parameters<typeof addNotification>[0]);
164
+ if (data.packageName && data.appName) recordApp(data.packageName, data.appName);
161
165
  } catch (err) {
162
166
  console.error("[nats] Failed to parse device notification:", err);
163
167
  }
164
- dispatchDeviceEvent("on_new_notification", raw);
168
+ dispatchDeviceEvent("on_new_notification", raw, parsed);
165
169
  }
166
170
  })();
167
171
 
@@ -169,17 +173,17 @@ export async function serveCommand(): Promise<void> {
169
173
  (async () => {
170
174
  for await (const msg of smsSub) {
171
175
  const raw = sc.decode(msg.data);
176
+ let parsed: unknown;
172
177
  try {
173
- const data = JSON.parse(raw);
174
- addSmsMessage({ ...data, receivedAt: Date.now() });
178
+ parsed = JSON.parse(raw);
179
+ addSmsMessage({ ...(parsed as object), receivedAt: Date.now() } as Parameters<typeof addSmsMessage>[0]);
175
180
  } catch (err) {
176
181
  console.error("[nats] Failed to parse device SMS:", err);
177
182
  }
178
- dispatchDeviceEvent("on_new_sms", raw);
183
+ dispatchDeviceEvent("on_new_sms", raw, parsed);
179
184
  }
180
185
  })();
181
186
  }
182
187
 
183
- // Start HTTP transport (loops forever)
184
188
  await startHttpTransport(config, handleRpc, httpPort, nc);
185
189
  }
package/src/config.ts CHANGED
@@ -6,10 +6,6 @@ import type { HostConfig } from "./types.js";
6
6
  const CONFIG_DIR = path.join(homedir(), ".config", "palmier");
7
7
  const CONFIG_FILE = path.join(CONFIG_DIR, "host.json");
8
8
 
9
- /**
10
- * Load host configuration from ~/.config/palmier/host.json.
11
- * Throws if the file is missing or invalid.
12
- */
13
9
  export function loadConfig(): HostConfig {
14
10
  if (!fs.existsSync(CONFIG_FILE)) {
15
11
  throw new Error(
@@ -32,10 +28,6 @@ export function loadConfig(): HostConfig {
32
28
  return config;
33
29
  }
34
30
 
35
- /**
36
- * Persist host configuration to ~/.config/palmier/host.json.
37
- * Creates parent directories if needed.
38
- */
39
31
  export function saveConfig(config: HostConfig): void {
40
32
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
41
33
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
@@ -12,12 +12,13 @@ export interface RegisteredDevice {
12
12
  export type DeviceCapability =
13
13
  | "location"
14
14
  | "notifications"
15
- | "sms"
15
+ | "sms-read"
16
+ | "sms-send"
16
17
  | "contacts"
17
18
  | "calendar"
18
- | "alert"
19
+ | "alarm"
19
20
  | "battery"
20
- | "email"
21
+ | "send-email"
21
22
  | "dnd";
22
23
 
23
24
  type CapabilityMap = Partial<Record<DeviceCapability, RegisteredDevice>>;
@@ -1,18 +1,12 @@
1
1
  /**
2
- * Per-task in-memory event queues for event-triggered schedules
3
- * (schedule_type: "on_new_notification" | "on_new_sms").
4
- *
2
+ * Per-task in-memory event queues for on_new_notification / on_new_sms schedules.
5
3
  * 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.
4
+ * `palmier run` process drains via /task-event/pop.
9
5
  *
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.
6
+ * Invariants:
7
+ * - popEvent clears activeRuns atomically when the queue empties, so a
8
+ * fresh startTask cannot race the tearing-down run.
9
+ * - enqueueEvent returns shouldStart=true only on the idle→active edge.
16
10
  */
17
11
 
18
12
  const MAX_QUEUE_SIZE = 100;
@@ -20,10 +14,6 @@ const MAX_QUEUE_SIZE = 100;
20
14
  const queues = new Map<string, string[]>();
21
15
  const activeRuns = new Set<string>();
22
16
 
23
- /**
24
- * Queue a raw (JSON-string) event payload for a task. Returns whether the
25
- * caller should now start the run process.
26
- */
27
17
  export function enqueueEvent(taskId: string, payload: string): { shouldStart: boolean } {
28
18
  const queue = queues.get(taskId) ?? [];
29
19
  if (queue.length >= MAX_QUEUE_SIZE) queue.shift();
@@ -35,11 +25,6 @@ export function enqueueEvent(taskId: string, payload: string): { shouldStart: bo
35
25
  return { shouldStart: true };
36
26
  }
37
27
 
38
- /**
39
- * Pop the oldest queued event for a task. Returns `{ event }` when one is
40
- * available (keeps the task marked active), or `{ empty: true }` after
41
- * clearing the active flag atomically.
42
- */
43
28
  export function popEvent(taskId: string): { event: string } | { empty: true } {
44
29
  const queue = queues.get(taskId);
45
30
  if (queue && queue.length > 0) {
package/src/events.ts CHANGED
@@ -3,12 +3,6 @@ import { loadConfig } from "./config.js";
3
3
 
4
4
  const sc = StringCodec();
5
5
 
6
- /**
7
- * Broadcast an event to connected clients via NATS and HTTP SSE.
8
- *
9
- * - NATS: publishes to `host-event.{hostId}.{taskId}`
10
- * - HTTP: POSTs to the serve daemon's `/event` endpoint
11
- */
12
6
  export async function publishHostEvent(
13
7
  nc: NatsConnection | undefined,
14
8
  hostId: string,
@@ -31,7 +25,5 @@ export async function publishHostEvent(
31
25
  body: JSON.stringify({ task_id: taskId, ...payload }),
32
26
  });
33
27
  console.log(`[http] host-event: ${taskId} →`, payload);
34
- } catch {
35
- // Serve HTTP may not be ready yet — ignore
36
- }
28
+ } catch { /* serve HTTP may not be ready yet */ }
37
29
  }
package/src/index.ts CHANGED
@@ -101,7 +101,6 @@ program
101
101
  await uninstallCommand();
102
102
  });
103
103
 
104
- // No subcommand → default to serve
105
104
  if (process.argv.length <= 2) {
106
105
  process.argv.push("serve");
107
106
  }
@@ -15,14 +15,13 @@ export interface McpResponse {
15
15
  stream?: boolean;
16
16
  }
17
17
 
18
- // Resource subscriptions: sessionId → Set of resource URIs
18
+ /** sessionId → subscribed resource URIs */
19
19
  const resourceSubscriptions = new Map<string, Set<string>>();
20
20
 
21
21
  export function getResourceSubscriptions(): Map<string, Set<string>> {
22
22
  return resourceSubscriptions;
23
23
  }
24
24
 
25
- // Session-to-agent name map with 24h TTL
26
25
  const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
27
26
  const sessionAgents = new Map<string, { agentName: string; expiresAt: number }>();
28
27