palmier 0.8.1 → 0.8.4

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 (133) hide show
  1. package/CLAUDE.md +13 -0
  2. package/README.md +16 -14
  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/commands/info.d.ts +0 -3
  17. package/dist/commands/info.js +0 -5
  18. package/dist/commands/init.d.ts +0 -3
  19. package/dist/commands/init.js +2 -11
  20. package/dist/commands/pair.d.ts +1 -4
  21. package/dist/commands/pair.js +3 -12
  22. package/dist/commands/restart.d.ts +0 -3
  23. package/dist/commands/restart.js +0 -3
  24. package/dist/commands/run.d.ts +1 -14
  25. package/dist/commands/run.js +18 -61
  26. package/dist/commands/serve.d.ts +0 -3
  27. package/dist/commands/serve.js +29 -27
  28. package/dist/config.d.ts +0 -8
  29. package/dist/config.js +0 -8
  30. package/dist/device-capabilities.d.ts +1 -1
  31. package/dist/event-queues.d.ts +6 -21
  32. package/dist/event-queues.js +6 -21
  33. package/dist/events.d.ts +0 -6
  34. package/dist/events.js +1 -9
  35. package/dist/index.js +0 -1
  36. package/dist/mcp-handler.js +1 -2
  37. package/dist/mcp-tools.d.ts +0 -3
  38. package/dist/mcp-tools.js +12 -16
  39. package/dist/nats-client.d.ts +0 -3
  40. package/dist/nats-client.js +1 -4
  41. package/dist/pending-requests.d.ts +4 -18
  42. package/dist/pending-requests.js +4 -18
  43. package/dist/platform/index.d.ts +1 -4
  44. package/dist/platform/index.js +8 -7
  45. package/dist/platform/linux.d.ts +3 -9
  46. package/dist/platform/linux.js +9 -20
  47. package/dist/platform/macos.d.ts +32 -0
  48. package/dist/platform/macos.js +287 -0
  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-499vYQvR.js +120 -0
  53. package/dist/pwa/assets/{index-CQxcuDhM.css → index-UaZFu6XL.css} +1 -1
  54. package/dist/pwa/assets/{web-DOyOiwsW.js → web-Bp48ONY3.js} +1 -1
  55. package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-CyJutAy4.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 +14 -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 +7 -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 +325 -22
  73. package/palmier-server/pwa/src/App.tsx +2 -0
  74. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +288 -0
  75. package/palmier-server/pwa/src/components/HostMenu.tsx +20 -207
  76. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
  77. package/palmier-server/pwa/src/components/SessionComposer.tsx +11 -2
  78. package/palmier-server/pwa/src/components/SessionsView.tsx +60 -32
  79. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
  80. package/palmier-server/pwa/src/components/TaskCard.tsx +1 -1
  81. package/palmier-server/pwa/src/components/TaskForm.tsx +207 -5
  82. package/palmier-server/pwa/src/components/TasksView.tsx +3 -1
  83. package/palmier-server/pwa/src/constants.ts +1 -1
  84. package/palmier-server/pwa/src/native/Device.ts +18 -2
  85. package/palmier-server/pwa/src/pages/Dashboard.tsx +13 -6
  86. package/palmier-server/pwa/src/pages/PairHost.tsx +3 -1
  87. package/palmier-server/pwa/src/pages/PairSetup.tsx +70 -0
  88. package/palmier-server/server/src/index.ts +7 -7
  89. package/palmier-server/server/src/routes/device.ts +4 -4
  90. package/palmier-server/spec.md +38 -7
  91. package/src/agents/agent.ts +0 -4
  92. package/src/agents/claude.ts +1 -1
  93. package/src/agents/codex.ts +2 -2
  94. package/src/agents/cursor.ts +1 -1
  95. package/src/agents/deepagents.ts +1 -1
  96. package/src/agents/gemini.ts +3 -2
  97. package/src/agents/goose.ts +1 -1
  98. package/src/agents/hermes.ts +1 -1
  99. package/src/agents/kiro.ts +1 -1
  100. package/src/agents/opencode.ts +1 -1
  101. package/src/agents/qoder.ts +1 -1
  102. package/src/agents/shared-prompt.ts +0 -3
  103. package/src/commands/info.ts +0 -5
  104. package/src/commands/init.ts +2 -11
  105. package/src/commands/pair.ts +3 -12
  106. package/src/commands/restart.ts +0 -3
  107. package/src/commands/run.ts +18 -65
  108. package/src/commands/serve.ts +28 -27
  109. package/src/config.ts +0 -8
  110. package/src/device-capabilities.ts +3 -2
  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 +12 -18
  116. package/src/nats-client.ts +1 -4
  117. package/src/pending-requests.ts +4 -18
  118. package/src/platform/index.ts +5 -7
  119. package/src/platform/linux.ts +9 -20
  120. package/src/platform/macos.ts +310 -0
  121. package/src/platform/platform.ts +1 -4
  122. package/src/platform/windows.ts +19 -40
  123. package/src/rpc-handler.ts +14 -47
  124. package/src/spawn-command.ts +11 -27
  125. package/src/task.ts +7 -70
  126. package/src/transports/http-transport.ts +7 -39
  127. package/src/transports/nats-transport.ts +3 -9
  128. package/src/types.ts +3 -10
  129. package/src/update-checker.ts +2 -5
  130. package/test/macos-plist.test.ts +112 -0
  131. package/test/task-parsing.test.ts +2 -3
  132. package/test/windows-xml.test.ts +11 -12
  133. package/dist/pwa/assets/index-DQfOEB03.js +0 -120
@@ -10,16 +10,13 @@ import { getPlatform } from "../platform/index.js";
10
10
  import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
11
11
  import { publishHostEvent } from "../events.js";
12
12
  /**
13
- * Invoke the agent CLI with a continuation loop for permissions and user input.
14
- *
15
- * Both standard and command-triggered execution use this.
16
- * The `invokeTask` is the ParsedTask whose prompt is passed to the agent
17
- * (for command-triggered mode this is the per-line augmented task).
13
+ * Invoke the agent CLI in a continuation loop to handle permission requests.
14
+ * `invokeTask` is the ParsedTask whose prompt is passed to the agent (in
15
+ * command-triggered mode this is the per-line augmented task).
18
16
  */
19
17
  async function invokeAgentWithRetries(ctx, invokeTask) {
20
18
  // eslint-disable-next-line no-constant-condition
21
19
  while (true) {
22
- // Stream agent output to TASKRUN.md in real-time, throttled to 500ms
23
20
  const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
24
21
  let lineBuf = "";
25
22
  let notifyPending = false;
@@ -56,11 +53,9 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
56
53
  const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
57
54
  const reportFiles = parseReportFiles(result.output);
58
55
  const requiredPermissions = parsePermissions(result.output);
59
- // Flush remaining buffered content
60
56
  if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
61
57
  writer.write(lineBuf);
62
58
  }
63
- // Include permission requests in the assistant message
64
59
  if (requiredPermissions.length > 0) {
65
60
  const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
66
61
  writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
@@ -75,7 +70,6 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
75
70
  report_files: reportFiles,
76
71
  });
77
72
  }
78
- // Permission handling — agent requested permissions
79
73
  if (requiredPermissions.length > 0) {
80
74
  const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
81
75
  if (response === "aborted") {
@@ -103,31 +97,22 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
103
97
  else {
104
98
  ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
105
99
  }
106
- // If the agent actually failed, retry with the new permissions
100
+ // Retry with the new permissions if the agent failed.
107
101
  if (outcome === "failed") {
108
102
  continue;
109
103
  }
110
104
  }
111
- // Normal completion (success or terminal failure)
112
105
  return { outcome };
113
106
  }
114
107
  }
115
- /**
116
- * Strip [PALMIER_*] marker lines from agent output.
117
- */
118
108
  export function stripPalmierMarkers(output) {
119
109
  return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
120
110
  }
121
- /**
122
- * Append a conversation message to the RESULT file and notify connected clients.
123
- */
124
111
  async function appendAndNotify(ctx, msg) {
125
112
  appendRunMessage(ctx.taskDir, ctx.runId, msg);
126
113
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
127
114
  }
128
- /**
129
- * Find the latest run dir that has no status messages yet (just created by the RPC handler).
130
- */
115
+ /** The latest run dir with no status messages yet — freshly created by the RPC handler. */
131
116
  function findLatestPendingRunId(taskDir) {
132
117
  const dirs = fs.readdirSync(taskDir)
133
118
  .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
@@ -140,8 +125,8 @@ function findLatestPendingRunId(taskDir) {
140
125
  return hasStatus ? null : latest;
141
126
  }
142
127
  /**
143
- * If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
144
- * respect that instead of overwriting with the process's own outcome.
128
+ * If the RPC handler already wrote "aborted" (via task.abort), respect that
129
+ * instead of overwriting with the process's own outcome.
145
130
  */
146
131
  function resolveOutcome(taskDir, outcome) {
147
132
  const current = readTaskStatus(taskDir);
@@ -149,9 +134,6 @@ function resolveOutcome(taskDir, outcome) {
149
134
  return "aborted";
150
135
  return outcome;
151
136
  }
152
- /**
153
- * Execute a task by ID.
154
- */
155
137
  export async function runCommand(taskId) {
156
138
  const config = loadConfig();
157
139
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -159,7 +141,6 @@ export async function runCommand(taskId) {
159
141
  console.log(`Running task: ${taskId}`);
160
142
  let nc;
161
143
  const taskName = task.frontmatter.name;
162
- // Use existing run dir if just created by RPC, otherwise create a new one
163
144
  const existingRunId = findLatestPendingRunId(taskDir);
164
145
  const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
165
146
  if (!existingRunId) {
@@ -175,7 +156,6 @@ export async function runCommand(taskId) {
175
156
  await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
176
157
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
177
158
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
178
- // If requires_confirmation, notify clients and wait
179
159
  if (task.frontmatter.requires_confirmation) {
180
160
  const confirmed = await requestConfirmation(config, task, taskDir);
181
161
  const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
@@ -194,7 +174,6 @@ export async function runCommand(taskId) {
194
174
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
195
175
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
196
176
  }
197
- // Shared invocation context
198
177
  const guiEnv = getPlatform().getGuiEnv();
199
178
  const agent = getAgent(task.frontmatter.agent);
200
179
  const ctx = {
@@ -202,7 +181,6 @@ export async function runCommand(taskId) {
202
181
  transientPermissions: [],
203
182
  };
204
183
  if (task.frontmatter.command) {
205
- // Command-triggered mode
206
184
  const result = await runCommandTriggeredMode(ctx);
207
185
  const outcome = resolveOutcome(taskDir, result.outcome);
208
186
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
@@ -211,7 +189,6 @@ export async function runCommand(taskId) {
211
189
  }
212
190
  else if (task.frontmatter.schedule_type === "on_new_notification"
213
191
  || task.frontmatter.schedule_type === "on_new_sms") {
214
- // Event-triggered mode (driven by NATS pub/sub of device notifications/SMS)
215
192
  const result = await runEventTriggeredMode(ctx);
216
193
  const outcome = resolveOutcome(taskDir, result.outcome);
217
194
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
@@ -219,7 +196,6 @@ export async function runCommand(taskId) {
219
196
  console.log(`Task ${taskId} completed (event-triggered).`);
220
197
  }
221
198
  else {
222
- // Standard execution — add user prompt as first message
223
199
  await appendAndNotify(ctx, {
224
200
  role: "user",
225
201
  time: Date.now(),
@@ -254,11 +230,9 @@ const MAX_LOG_ENTRIES = 1000;
254
230
  /** Max input line length (chars). Long emails can take up to 200k chars. */
255
231
  const MAX_LINE_LENGTH = 200_000;
256
232
  /**
257
- * Command-triggered execution mode.
258
- *
259
- * Spawns a long-running shell command and, for each line of stdout,
260
- * invokes the agent CLI with the user's prompt augmented by that line.
261
- * Processes lines sequentially with a bounded queue.
233
+ * Spawn a long-running shell command and invoke the agent CLI once per stdout
234
+ * line, with the user's prompt augmented by that line. Sequential with a
235
+ * bounded queue.
262
236
  */
263
237
  async function runCommandTriggeredMode(ctx) {
264
238
  const commandStr = ctx.task.frontmatter.command;
@@ -280,7 +254,6 @@ async function runCommandTriggeredMode(ctx) {
280
254
  function appendLog(line, agentOutput, outcome) {
281
255
  const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
282
256
  fs.appendFileSync(logPath, entry, "utf-8");
283
- // Trim log if too large (keep last MAX_LOG_ENTRIES entries)
284
257
  try {
285
258
  const content = fs.readFileSync(logPath, "utf-8");
286
259
  const entries = content.split("\n---\n").filter(Boolean);
@@ -312,7 +285,7 @@ async function runCommandTriggeredMode(ctx) {
312
285
  invocationsFailed++;
313
286
  }
314
287
  appendLog(line, "", result.outcome);
315
- // Append monitoring status so the UI shows the task is waiting for more input
288
+ // Signal "waiting for more input" in the UI.
316
289
  appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
317
290
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
318
291
  }
@@ -353,7 +326,6 @@ async function runCommandTriggeredMode(ctx) {
353
326
  stderrBuf += chunk;
354
327
  process.stderr.write(d);
355
328
  });
356
- // Wait for command to exit
357
329
  const exitCode = await new Promise((resolve) => {
358
330
  child.on("close", (code) => {
359
331
  commandExited = true;
@@ -368,7 +340,6 @@ async function runCommandTriggeredMode(ctx) {
368
340
  resolve(1);
369
341
  });
370
342
  });
371
- // Wait for any remaining queued lines to finish processing
372
343
  if (lineQueue.length > 0 || processing) {
373
344
  await new Promise((resolve) => {
374
345
  resolveWhenDone = resolve;
@@ -390,13 +361,10 @@ async function runCommandTriggeredMode(ctx) {
390
361
  return { outcome: "finished", endTime };
391
362
  }
392
363
  /**
393
- * Event-triggered execution mode.
394
- *
395
- * Drains the daemon-owned per-task event queue via the local /task-event/pop
396
- * HTTP endpoint, invoking the agent once per event with the payload spliced
397
- * into the user prompt. The run process itself holds no NATS subscription;
398
- * the daemon handles that and atomically clears the active flag when we see
399
- * an empty pop, so it can fire up a fresh run on the next incoming event.
364
+ * Drain the daemon-owned per-task event queue via /task-event/pop, invoking
365
+ * the agent once per event. The run process holds no NATS subscription — the
366
+ * daemon owns that and atomically clears the active flag on empty pop so it
367
+ * can fire a fresh run on the next incoming event.
400
368
  */
401
369
  async function runEventTriggeredMode(ctx) {
402
370
  const scheduleType = ctx.task.frontmatter.schedule_type;
@@ -488,10 +456,6 @@ async function requestConfirmation(config, task, taskDir) {
488
456
  });
489
457
  return confirmed;
490
458
  }
491
- /**
492
- * Extract report file names from agent output.
493
- * Looks for lines matching: [PALMIER_REPORT] <filename>
494
- */
495
459
  const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
496
460
  export function parseReportFiles(output) {
497
461
  const regex = new RegExp(`^\\${TASK_REPORT_PREFIX}\\s+(.+)$`, "gm");
@@ -499,7 +463,7 @@ export function parseReportFiles(output) {
499
463
  let match;
500
464
  while ((match = regex.exec(output)) !== null) {
501
465
  const name = match[1].trim();
502
- // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
466
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>").
503
467
  if (!name || name.startsWith("<"))
504
468
  continue;
505
469
  const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
@@ -509,17 +473,13 @@ export function parseReportFiles(output) {
509
473
  }
510
474
  return files;
511
475
  }
512
- /**
513
- * Extract required permissions from agent output.
514
- * Looks for lines matching: [PALMIER_PERMISSION] <tool> | <description>
515
- */
516
476
  export function parsePermissions(output) {
517
477
  const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
518
478
  const perms = [];
519
479
  let match;
520
480
  while ((match = regex.exec(output)) !== null) {
521
481
  const raw = match[1].trim();
522
- // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
482
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
523
483
  if (raw.startsWith("<"))
524
484
  continue;
525
485
  const sep = raw.indexOf("|");
@@ -532,10 +492,7 @@ export function parsePermissions(output) {
532
492
  }
533
493
  return perms;
534
494
  }
535
- /**
536
- * Parse the agent's output for success/failure markers.
537
- * Falls back to "finished" if no marker is found.
538
- */
495
+ /** Falls back to "finished" if no success/failure marker is found. */
539
496
  export function parseTaskOutcome(output) {
540
497
  const lastChunk = output.slice(-500);
541
498
  const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
@@ -1,5 +1,2 @@
1
- /**
2
- * Start the persistent RPC handler (NATS + HTTP).
3
- */
4
1
  export declare function serveCommand(): Promise<void>;
5
2
  //# sourceMappingURL=serve.d.ts.map
@@ -18,11 +18,8 @@ import { enqueueEvent } from "../event-queues.js";
18
18
  const POLL_INTERVAL_MS = 30_000;
19
19
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
20
20
  /**
21
- * Scan all tasks for any stuck in "started" state whose process is no longer alive.
22
- * Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
23
- *
24
- * Since run.ts creates the RESULT file and history entry at start, we just need to
25
- * finalize the existing RESULT file, append a failed status entry, and broadcast.
21
+ * Reconcile tasks stuck in "started" whose process is no longer alive.
22
+ * The system scheduler (Task Scheduler / systemd) is the authoritative source.
26
23
  */
27
24
  async function checkStaleTasks(config, nc) {
28
25
  const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
@@ -42,13 +39,11 @@ async function checkStaleTasks(config, nc) {
42
39
  const status = readTaskStatus(taskDir);
43
40
  if (!status || status.running_state !== "started")
44
41
  continue;
45
- // Ask the system scheduler if the task is still running
46
42
  if (platform.isTaskRunning(taskId))
47
43
  continue;
48
44
  console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
49
45
  const endTime = Date.now();
50
46
  writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
51
- // Find the latest run directory (created by run.ts at start)
52
47
  const runId = fs.readdirSync(taskDir)
53
48
  .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
54
49
  .sort()
@@ -65,7 +60,7 @@ async function checkStaleTasks(config, nc) {
65
60
  try {
66
61
  taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
67
62
  }
68
- catch { /* use taskId as fallback */ }
63
+ catch { /* fallback to taskId */ }
69
64
  await publishHostEvent(nc, config.hostId, taskId, {
70
65
  event_type: "running-state",
71
66
  running_state: "failed",
@@ -73,15 +68,11 @@ async function checkStaleTasks(config, nc) {
73
68
  });
74
69
  }
75
70
  }
76
- /**
77
- * Start the persistent RPC handler (NATS + HTTP).
78
- */
79
71
  export async function serveCommand() {
80
72
  const config = loadConfig();
81
- // Write PID so `palmier restart` can find us regardless of how we were started
73
+ // PID file lets `palmier restart` find us regardless of how we were started
82
74
  fs.writeFileSync(DAEMON_PID_FILE, String(process.pid), "utf-8");
83
75
  console.log("Starting...");
84
- // Re-detect agents on every daemon start
85
76
  const agents = await detectAgents();
86
77
  config.agents = agents;
87
78
  saveConfig(config);
@@ -94,9 +85,8 @@ export async function serveCommand() {
94
85
  catch (err) {
95
86
  console.warn(`[nats] Connection failed (server mode unavailable): ${err}`);
96
87
  }
97
- // Reconcile any tasks stuck from before daemon started
98
88
  await checkStaleTasks(config, nc);
99
- // Ensure all tasks have their scheduler entries (recovery after init/reinstall)
89
+ // Reinstall scheduler entries for all tasks (recovery after init/reinstall)
100
90
  const platform = getPlatform();
101
91
  const allTasks = listTasks(config.projectRoot);
102
92
  for (const task of allTasks) {
@@ -107,7 +97,6 @@ export async function serveCommand() {
107
97
  console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
108
98
  }
109
99
  }
110
- // Poll for crashed tasks every 30 seconds
111
100
  setInterval(() => {
112
101
  checkStaleTasks(config, nc).catch((err) => {
113
102
  console.error("[monitor] Error checking stale tasks:", err);
@@ -115,18 +104,30 @@ export async function serveCommand() {
115
104
  }, POLL_INTERVAL_MS);
116
105
  const handleRpc = createRpcHandler(config, nc);
117
106
  const httpPort = config.httpPort ?? 7256;
118
- // Start NATS transport (loops forever, fire-and-forget)
119
107
  if (nc) {
120
108
  startNatsTransport(config, handleRpc, nc);
121
- // Subscribe to device notifications and SMS from Android
122
109
  const sc = StringCodec();
123
- // Dispatch a raw event payload to every task whose schedule matches.
124
- function dispatchDeviceEvent(scheduleType, payload) {
110
+ // Match phone numbers regardless of formatting; letters preserved for shortcodes.
111
+ function normalizeSender(raw) {
112
+ return raw.replace(/[\s\-()+]/g, "").toLowerCase();
113
+ }
114
+ function dispatchDeviceEvent(scheduleType, payload, parsed) {
125
115
  for (const task of listTasks(config.projectRoot)) {
126
116
  if (task.frontmatter.schedule_type !== scheduleType)
127
117
  continue;
128
118
  if (!task.frontmatter.schedule_enabled)
129
119
  continue;
120
+ if (scheduleType === "on_new_notification" && task.frontmatter.schedule_values && task.frontmatter.schedule_values.length > 0) {
121
+ const pkg = parsed?.packageName;
122
+ if (!pkg || !task.frontmatter.schedule_values.includes(pkg))
123
+ continue;
124
+ }
125
+ if (scheduleType === "on_new_sms" && task.frontmatter.schedule_values && task.frontmatter.schedule_values.length > 0) {
126
+ const sender = parsed?.sender;
127
+ const normalizedSender = sender ? normalizeSender(sender) : "";
128
+ if (!normalizedSender || !task.frontmatter.schedule_values.some((s) => normalizeSender(s) === normalizedSender))
129
+ continue;
130
+ }
130
131
  const { shouldStart } = enqueueEvent(task.frontmatter.id, payload);
131
132
  if (shouldStart) {
132
133
  platform.startTask(task.frontmatter.id).catch((err) => {
@@ -139,32 +140,33 @@ export async function serveCommand() {
139
140
  (async () => {
140
141
  for await (const msg of notifSub) {
141
142
  const raw = sc.decode(msg.data);
143
+ let parsed;
142
144
  try {
143
- const data = JSON.parse(raw);
144
- addNotification({ ...data, receivedAt: Date.now() });
145
+ parsed = JSON.parse(raw);
146
+ addNotification({ ...parsed, receivedAt: Date.now() });
145
147
  }
146
148
  catch (err) {
147
149
  console.error("[nats] Failed to parse device notification:", err);
148
150
  }
149
- dispatchDeviceEvent("on_new_notification", raw);
151
+ dispatchDeviceEvent("on_new_notification", raw, parsed);
150
152
  }
151
153
  })();
152
154
  const smsSub = nc.subscribe(`host.${config.hostId}.device.sms`);
153
155
  (async () => {
154
156
  for await (const msg of smsSub) {
155
157
  const raw = sc.decode(msg.data);
158
+ let parsed;
156
159
  try {
157
- const data = JSON.parse(raw);
158
- addSmsMessage({ ...data, receivedAt: Date.now() });
160
+ parsed = JSON.parse(raw);
161
+ addSmsMessage({ ...parsed, receivedAt: Date.now() });
159
162
  }
160
163
  catch (err) {
161
164
  console.error("[nats] Failed to parse device SMS:", err);
162
165
  }
163
- dispatchDeviceEvent("on_new_sms", raw);
166
+ dispatchDeviceEvent("on_new_sms", raw, parsed);
164
167
  }
165
168
  })();
166
169
  }
167
- // Start HTTP transport (loops forever)
168
170
  await startHttpTransport(config, handleRpc, httpPort, nc);
169
171
  }
170
172
  //# sourceMappingURL=serve.js.map
package/dist/config.d.ts CHANGED
@@ -1,15 +1,7 @@
1
1
  import type { HostConfig } from "./types.js";
2
2
  declare const CONFIG_DIR: string;
3
3
  declare const CONFIG_FILE: string;
4
- /**
5
- * Load host configuration from ~/.config/palmier/host.json.
6
- * Throws if the file is missing or invalid.
7
- */
8
4
  export declare function loadConfig(): HostConfig;
9
- /**
10
- * Persist host configuration to ~/.config/palmier/host.json.
11
- * Creates parent directories if needed.
12
- */
13
5
  export declare function saveConfig(config: HostConfig): void;
14
6
  export { CONFIG_DIR, CONFIG_FILE };
15
7
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -3,10 +3,6 @@ import * as path from "path";
3
3
  import { homedir } from "os";
4
4
  const CONFIG_DIR = path.join(homedir(), ".config", "palmier");
5
5
  const CONFIG_FILE = path.join(CONFIG_DIR, "host.json");
6
- /**
7
- * Load host configuration from ~/.config/palmier/host.json.
8
- * Throws if the file is missing or invalid.
9
- */
10
6
  export function loadConfig() {
11
7
  if (!fs.existsSync(CONFIG_FILE)) {
12
8
  throw new Error("Host not provisioned. Run `palmier init` first.\n" +
@@ -22,10 +18,6 @@ export function loadConfig() {
22
18
  }
23
19
  return config;
24
20
  }
25
- /**
26
- * Persist host configuration to ~/.config/palmier/host.json.
27
- * Creates parent directories if needed.
28
- */
29
21
  export function saveConfig(config) {
30
22
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
31
23
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
@@ -2,7 +2,7 @@ export interface RegisteredDevice {
2
2
  clientToken: string;
3
3
  fcmToken: string;
4
4
  }
5
- export type DeviceCapability = "location" | "notifications" | "sms" | "contacts" | "calendar" | "alert" | "battery" | "send-email" | "dnd";
5
+ export type DeviceCapability = "location" | "notifications" | "sms-read" | "sms-send" | "contacts" | "calendar" | "alarm" | "battery" | "send-email" | "dnd";
6
6
  export declare function getCapabilityDevice(capability: DeviceCapability): RegisteredDevice | null;
7
7
  export declare function setCapabilityDevice(capability: DeviceCapability, clientToken: string, fcmToken: string): void;
8
8
  export declare function clearCapabilityDevice(capability: DeviceCapability): void;
@@ -1,31 +1,16 @@
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.
16
- */
17
- /**
18
- * Queue a raw (JSON-string) event payload for a task. Returns whether the
19
- * caller should now start the run process.
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.
20
10
  */
21
11
  export declare function enqueueEvent(taskId: string, payload: string): {
22
12
  shouldStart: boolean;
23
13
  };
24
- /**
25
- * Pop the oldest queued event for a task. Returns `{ event }` when one is
26
- * available (keeps the task marked active), or `{ empty: true }` after
27
- * clearing the active flag atomically.
28
- */
29
14
  export declare function popEvent(taskId: string): {
30
15
  event: string;
31
16
  } | {
@@ -1,26 +1,16 @@
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
  const MAX_QUEUE_SIZE = 100;
18
12
  const queues = new Map();
19
13
  const activeRuns = new Set();
20
- /**
21
- * Queue a raw (JSON-string) event payload for a task. Returns whether the
22
- * caller should now start the run process.
23
- */
24
14
  export function enqueueEvent(taskId, payload) {
25
15
  const queue = queues.get(taskId) ?? [];
26
16
  if (queue.length >= MAX_QUEUE_SIZE)
@@ -32,11 +22,6 @@ export function enqueueEvent(taskId, payload) {
32
22
  activeRuns.add(taskId);
33
23
  return { shouldStart: true };
34
24
  }
35
- /**
36
- * Pop the oldest queued event for a task. Returns `{ event }` when one is
37
- * available (keeps the task marked active), or `{ empty: true }` after
38
- * clearing the active flag atomically.
39
- */
40
25
  export function popEvent(taskId) {
41
26
  const queue = queues.get(taskId);
42
27
  if (queue && queue.length > 0) {
package/dist/events.d.ts CHANGED
@@ -1,9 +1,3 @@
1
1
  import { type NatsConnection } from "nats";
2
- /**
3
- * Broadcast an event to connected clients via NATS and HTTP SSE.
4
- *
5
- * - NATS: publishes to `host-event.{hostId}.{taskId}`
6
- * - HTTP: POSTs to the serve daemon's `/event` endpoint
7
- */
8
2
  export declare function publishHostEvent(nc: NatsConnection | undefined, hostId: string, taskId: string, payload: Record<string, unknown>): Promise<void>;
9
3
  //# sourceMappingURL=events.d.ts.map
package/dist/events.js CHANGED
@@ -1,12 +1,6 @@
1
1
  import { StringCodec } from "nats";
2
2
  import { loadConfig } from "./config.js";
3
3
  const sc = StringCodec();
4
- /**
5
- * Broadcast an event to connected clients via NATS and HTTP SSE.
6
- *
7
- * - NATS: publishes to `host-event.{hostId}.{taskId}`
8
- * - HTTP: POSTs to the serve daemon's `/event` endpoint
9
- */
10
4
  export async function publishHostEvent(nc, hostId, taskId, payload) {
11
5
  const subject = `host-event.${hostId}.${taskId}`;
12
6
  if (nc) {
@@ -23,8 +17,6 @@ export async function publishHostEvent(nc, hostId, taskId, payload) {
23
17
  });
24
18
  console.log(`[http] host-event: ${taskId} →`, payload);
25
19
  }
26
- catch {
27
- // Serve HTTP may not be ready yet — ignore
28
- }
20
+ catch { /* serve HTTP may not be ready yet */ }
29
21
  }
30
22
  //# sourceMappingURL=events.js.map
package/dist/index.js CHANGED
@@ -82,7 +82,6 @@ program
82
82
  .action(async () => {
83
83
  await uninstallCommand();
84
84
  });
85
- // No subcommand → default to serve
86
85
  if (process.argv.length <= 2) {
87
86
  process.argv.push("serve");
88
87
  }
@@ -1,11 +1,10 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { agentTools, agentToolMap, agentResources, agentResourceMap, ToolError } from "./mcp-tools.js";
3
- // Resource subscriptions: sessionId → Set of resource URIs
3
+ /** sessionId → subscribed resource URIs */
4
4
  const resourceSubscriptions = new Map();
5
5
  export function getResourceSubscriptions() {
6
6
  return resourceSubscriptions;
7
7
  }
8
- // Session-to-agent name map with 24h TTL
9
8
  const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
10
9
  const sessionAgents = new Map();
11
10
  export function getAgentName(sessionId) {
@@ -37,8 +37,5 @@ export interface ResourceDefinition {
37
37
  }
38
38
  export declare const agentResources: ResourceDefinition[];
39
39
  export declare const agentResourceMap: Map<string, ResourceDefinition>;
40
- /**
41
- * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
42
- */
43
40
  export declare function generateEndpointDocs(port: number, taskId: string, tools?: ToolDefinition[], resources?: ResourceDefinition[]): string;
44
41
  //# sourceMappingURL=mcp-tools.d.ts.map