palmier 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CLAUDE.md +13 -0
  2. package/README.md +11 -11
  3. package/dist/agents/agent.d.ts +0 -4
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/codex.js +2 -2
  6. package/dist/agents/cursor.js +1 -1
  7. package/dist/agents/deepagents.js +1 -1
  8. package/dist/agents/gemini.js +3 -2
  9. package/dist/agents/goose.js +1 -1
  10. package/dist/agents/hermes.js +1 -1
  11. package/dist/agents/kiro.js +1 -1
  12. package/dist/agents/opencode.js +1 -1
  13. package/dist/agents/qoder.js +1 -1
  14. package/dist/agents/shared-prompt.d.ts +0 -3
  15. package/dist/agents/shared-prompt.js +0 -3
  16. package/dist/app-registry.d.ts +10 -0
  17. package/dist/app-registry.js +44 -0
  18. package/dist/commands/info.d.ts +0 -3
  19. package/dist/commands/info.js +0 -5
  20. package/dist/commands/init.d.ts +0 -3
  21. package/dist/commands/init.js +2 -11
  22. package/dist/commands/pair.d.ts +1 -4
  23. package/dist/commands/pair.js +1 -12
  24. package/dist/commands/restart.d.ts +0 -3
  25. package/dist/commands/restart.js +0 -3
  26. package/dist/commands/run.d.ts +1 -14
  27. package/dist/commands/run.js +18 -61
  28. package/dist/commands/serve.d.ts +0 -3
  29. package/dist/commands/serve.js +33 -27
  30. package/dist/config.d.ts +0 -8
  31. package/dist/config.js +0 -8
  32. package/dist/device-capabilities.d.ts +1 -1
  33. package/dist/event-queues.d.ts +6 -21
  34. package/dist/event-queues.js +6 -21
  35. package/dist/events.d.ts +0 -6
  36. package/dist/events.js +1 -9
  37. package/dist/index.js +0 -1
  38. package/dist/mcp-handler.js +1 -2
  39. package/dist/mcp-tools.d.ts +0 -3
  40. package/dist/mcp-tools.js +12 -16
  41. package/dist/nats-client.d.ts +0 -3
  42. package/dist/nats-client.js +1 -4
  43. package/dist/pending-requests.d.ts +4 -18
  44. package/dist/pending-requests.js +4 -18
  45. package/dist/platform/index.d.ts +1 -4
  46. package/dist/platform/index.js +1 -4
  47. package/dist/platform/linux.d.ts +3 -9
  48. package/dist/platform/linux.js +9 -20
  49. package/dist/platform/platform.d.ts +1 -4
  50. package/dist/platform/windows.d.ts +2 -5
  51. package/dist/platform/windows.js +19 -39
  52. package/dist/pwa/assets/{index-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
  53. package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
  54. package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
  55. package/dist/pwa/assets/{web-DOyOiwsW.js → web-Z1623me-.js} +1 -1
  56. package/dist/pwa/index.html +2 -2
  57. package/dist/pwa/service-worker.js +1 -1
  58. package/dist/rpc-handler.d.ts +0 -6
  59. package/dist/rpc-handler.js +18 -47
  60. package/dist/spawn-command.d.ts +10 -25
  61. package/dist/spawn-command.js +7 -15
  62. package/dist/task.d.ts +6 -64
  63. package/dist/task.js +7 -70
  64. package/dist/transports/http-transport.d.ts +0 -4
  65. package/dist/transports/http-transport.js +6 -28
  66. package/dist/transports/nats-transport.d.ts +0 -4
  67. package/dist/transports/nats-transport.js +3 -9
  68. package/dist/types.d.ts +3 -7
  69. package/dist/update-checker.d.ts +1 -4
  70. package/dist/update-checker.js +2 -5
  71. package/package.json +1 -1
  72. package/palmier-server/pwa/src/App.css +165 -20
  73. package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
  74. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
  75. package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
  76. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
  77. package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
  78. package/palmier-server/pwa/src/constants.ts +1 -1
  79. package/palmier-server/pwa/src/native/Device.ts +20 -2
  80. package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
  81. package/palmier-server/server/src/index.ts +7 -7
  82. package/palmier-server/server/src/routes/device.ts +4 -4
  83. package/palmier-server/spec.md +38 -7
  84. package/src/agents/agent.ts +0 -4
  85. package/src/agents/claude.ts +1 -1
  86. package/src/agents/codex.ts +2 -2
  87. package/src/agents/cursor.ts +1 -1
  88. package/src/agents/deepagents.ts +1 -1
  89. package/src/agents/gemini.ts +3 -2
  90. package/src/agents/goose.ts +1 -1
  91. package/src/agents/hermes.ts +1 -1
  92. package/src/agents/kiro.ts +1 -1
  93. package/src/agents/opencode.ts +1 -1
  94. package/src/agents/qoder.ts +1 -1
  95. package/src/agents/shared-prompt.ts +0 -3
  96. package/src/app-registry.ts +52 -0
  97. package/src/commands/info.ts +0 -5
  98. package/src/commands/init.ts +2 -11
  99. package/src/commands/pair.ts +1 -12
  100. package/src/commands/restart.ts +0 -3
  101. package/src/commands/run.ts +18 -65
  102. package/src/commands/serve.ts +31 -27
  103. package/src/config.ts +0 -8
  104. package/src/device-capabilities.ts +3 -2
  105. package/src/event-queues.ts +6 -21
  106. package/src/events.ts +1 -9
  107. package/src/index.ts +0 -1
  108. package/src/mcp-handler.ts +1 -2
  109. package/src/mcp-tools.ts +12 -18
  110. package/src/nats-client.ts +1 -4
  111. package/src/pending-requests.ts +4 -18
  112. package/src/platform/index.ts +1 -4
  113. package/src/platform/linux.ts +9 -20
  114. package/src/platform/platform.ts +1 -4
  115. package/src/platform/windows.ts +19 -40
  116. package/src/rpc-handler.ts +19 -47
  117. package/src/spawn-command.ts +11 -27
  118. package/src/task.ts +7 -70
  119. package/src/transports/http-transport.ts +6 -39
  120. package/src/transports/nats-transport.ts +3 -9
  121. package/src/types.ts +3 -10
  122. package/src/update-checker.ts +2 -5
  123. package/test/task-parsing.test.ts +2 -3
  124. package/test/windows-xml.test.ts +11 -12
  125. package/dist/pwa/assets/index-DQfOEB03.js +0 -120
@@ -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,10 +12,11 @@ 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
21
  | "send-email"
21
22
  | "dnd";
@@ -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
 
package/src/mcp-tools.ts CHANGED
@@ -462,8 +462,8 @@ const sendSmsTool: ToolDefinition = {
462
462
  async handler(args, ctx) {
463
463
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
464
464
 
465
- const device = getCapabilityDevice("sms");
466
- if (!device) throw new ToolError("No device has SMS access enabled", 400);
465
+ const device = getCapabilityDevice("sms-send");
466
+ if (!device) throw new ToolError("No device has SMS Send enabled", 400);
467
467
 
468
468
  const { to, body } = args as { to: string; body: string };
469
469
  if (!to || !body) throw new ToolError("to and body are required", 400);
@@ -502,10 +502,10 @@ const sendSmsTool: ToolDefinition = {
502
502
  },
503
503
  };
504
504
 
505
- const sendAlertTool: ToolDefinition = {
506
- name: "send-alert",
505
+ const sendAlarmTool: ToolDefinition = {
506
+ name: "send-alarm",
507
507
  description: [
508
- "Send an alert to the user's mobile device with an alarm sound and full-screen popup.",
508
+ "Trigger an alarm on the user's mobile device with an alarm sound and full-screen popup.",
509
509
  "Use this to urgently get the user's attention. The device will play an alarm sound and show a full-screen dialog even on the lock screen.",
510
510
  "Blocks until the device responds (up to 30 seconds).",
511
511
  'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
@@ -513,16 +513,16 @@ const sendAlertTool: ToolDefinition = {
513
513
  inputSchema: {
514
514
  type: "object",
515
515
  properties: {
516
- title: { type: "string", description: "Alert title" },
517
- description: { type: "string", description: "Alert description/details" },
516
+ title: { type: "string", description: "Alarm title" },
517
+ description: { type: "string", description: "Alarm description/details" },
518
518
  },
519
519
  required: ["title"],
520
520
  },
521
521
  async handler(args, ctx) {
522
522
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
523
523
 
524
- const device = getCapabilityDevice("alert");
525
- if (!device) throw new ToolError("No device has alert access enabled", 400);
524
+ const device = getCapabilityDevice("alarm");
525
+ if (!device) throw new ToolError("No device has alarm access enabled", 400);
526
526
 
527
527
  const { title, description } = args as { title: string; description?: string };
528
528
  if (!title) throw new ToolError("title is required", 400);
@@ -536,7 +536,7 @@ const sendAlertTool: ToolDefinition = {
536
536
  if (description) payload.description = description;
537
537
 
538
538
  const ackReply = await ctx.nc.request(
539
- `host.${ctx.config.hostId}.fcm.alert`,
539
+ `host.${ctx.config.hostId}.fcm.alarm`,
540
540
  sc.encode(JSON.stringify(payload)),
541
541
  { timeout: 5_000 },
542
542
  );
@@ -544,7 +544,7 @@ const sendAlertTool: ToolDefinition = {
544
544
  if (ack.error) throw new ToolError(ack.error, 502);
545
545
 
546
546
  const responsePromise = new Promise<string>((resolve, reject) => {
547
- const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.alert.${ctx.sessionId}`, { max: 1 });
547
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.alarm.${ctx.sessionId}`, { max: 1 });
548
548
  const timer = setTimeout(() => {
549
549
  sub.unsubscribe();
550
550
  reject(new ToolError("Device did not respond within 30 seconds", 504));
@@ -733,11 +733,9 @@ const sendEmailTool: ToolDefinition = {
733
733
  },
734
734
  };
735
735
 
736
- export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
736
+ export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
737
737
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
738
738
 
739
- // ── MCP Resources ─────────────────────────────────────────────────────
740
-
741
739
  export interface ResourceDefinition {
742
740
  /** MCP resource URI (e.g. "notifications://device"). */
743
741
  uri: string;
@@ -783,9 +781,6 @@ const deviceSmsResource: ResourceDefinition = {
783
781
  export const agentResources: ResourceDefinition[] = [deviceNotificationsResource, deviceSmsResource];
784
782
  export const agentResourceMap = new Map<string, ResourceDefinition>(agentResources.map((r) => [r.uri, r]));
785
783
 
786
- /**
787
- * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
788
- */
789
784
  export function generateEndpointDocs(
790
785
  port: number,
791
786
  taskId: string,
@@ -803,7 +798,6 @@ export function generateEndpointDocs(
803
798
  const props = schema.properties ?? {};
804
799
  const required = new Set(schema.required ?? []);
805
800
 
806
- // Build example JSON (body only, no taskId)
807
801
  const example: Record<string, unknown> = {};
808
802
  for (const [key, prop] of Object.entries(props)) {
809
803
  if (prop.type === "array") example[key] = ["..."];
@@ -1,9 +1,6 @@
1
1
  import { connect, jwtAuthenticator, type NatsConnection } from "nats";
2
2
  import type { HostConfig } from "./types.js";
3
3
 
4
- /**
5
- * Connect to NATS using the host config's JWT credentials.
6
- */
7
4
  export async function connectNats(config: HostConfig): Promise<NatsConnection> {
8
5
  if (!config.natsJwt || !config.natsNkeySeed) {
9
6
  throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
@@ -17,6 +14,6 @@ export async function connectNats(config: HostConfig): Promise<NatsConnection> {
17
14
  ),
18
15
  });
19
16
 
20
- // Do not log anything as that will pollute stdout for mcp server.
17
+ // Do not log it would pollute stdout for the MCP server.
21
18
  return nc;
22
19
  }
@@ -22,10 +22,9 @@ export interface PendingRequest {
22
22
  const pending = new Map<string, PendingRequest>();
23
23
 
24
24
  /**
25
- * Register a pending request keyed by either a sessionId (confirmation / input)
26
- * or a taskId (permission). The `meta` is surfaced to PWAs that connect after
27
- * the request was opened, so their modals can render without replaying events.
28
- * Only one pending request per key at a time.
25
+ * Key is sessionId for confirmation/input, taskId for permission. Only one
26
+ * pending request per key at a time. `meta` is surfaced via host.info so a
27
+ * freshly-connected PWA can render the modal without replaying events.
29
28
  */
30
29
  export function registerPending(
31
30
  key: string,
@@ -42,10 +41,6 @@ export function registerPending(
42
41
  });
43
42
  }
44
43
 
45
- /**
46
- * Resolve a pending request with the user's response.
47
- * Returns true if a pending request was found and resolved.
48
- */
49
44
  export function resolvePending(key: string, value: string[]): boolean {
50
45
  const entry = pending.get(key);
51
46
  if (!entry) return false;
@@ -54,24 +49,15 @@ export function resolvePending(key: string, value: string[]): boolean {
54
49
  return true;
55
50
  }
56
51
 
57
- /**
58
- * Get the current pending request for a key (if any).
59
- */
60
52
  export function getPending(key: string): PendingRequest | undefined {
61
53
  return pending.get(key);
62
54
  }
63
55
 
64
- /**
65
- * Remove a pending request without resolving it.
66
- */
67
56
  export function removePending(key: string): void {
68
57
  pending.delete(key);
69
58
  }
70
59
 
71
- /**
72
- * List all currently-pending requests, stripped of the unserializable `resolve`
73
- * callback. Used by `host.info` so the PWA can seed its modal state on connect.
74
- */
60
+ /** Pending requests stripped of the unserializable `resolve` callback. */
75
61
  export function listPending(): Array<{
76
62
  key: string;
77
63
  type: PendingRequest["type"];
@@ -2,10 +2,7 @@ import type { PlatformService } from "./platform.js";
2
2
  import { LinuxPlatform } from "./linux.js";
3
3
  import { WindowsPlatform } from "./windows.js";
4
4
 
5
- /**
6
- * On Windows, execSync needs an explicit shell so .cmd shims resolve correctly.
7
- * On Unix, undefined lets Node use the default shell.
8
- */
5
+ /** Windows needs an explicit shell for execSync to resolve .cmd shims. */
9
6
  export const SHELL: string | undefined = process.platform === "win32" ? "cmd.exe" : undefined;
10
7
 
11
8
  let _instance: PlatformService | undefined;
@@ -22,15 +22,9 @@ function getServiceName(taskId: string): string {
22
22
  }
23
23
 
24
24
  /**
25
- * Convert a cron expression to a systemd OnCalendar string.
26
- *
27
- * Only the 4 cron patterns the PWA UI can produce are supported:
28
- * hourly: "0 * * * *"
29
- * daily: "MM HH * * *"
30
- * weekly: "MM HH * * D"
31
- * monthly: "MM HH D * *"
32
- * Arbitrary cron expressions (ranges, lists, steps beyond hourly) are NOT
33
- * handled because the UI never generates them.
25
+ * Only the 4 cron patterns the PWA UI produces are supported:
26
+ * hourly "0 * * * *", daily "MM HH * * *", weekly "MM HH * * D", monthly "MM HH D * *".
27
+ * Arbitrary expressions (ranges, lists, sub-hour steps) are not handled.
34
28
  */
35
29
  export function cronToOnCalendar(cron: string): string {
36
30
  const parts = cron.trim().split(/\s+/);
@@ -40,7 +34,6 @@ export function cronToOnCalendar(cron: string): string {
40
34
 
41
35
  const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
42
36
 
43
- // Map cron day-of-week numbers to systemd abbreviated names
44
37
  const dowMap: Record<string, string> = {
45
38
  "0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed",
46
39
  "4": "Thu", "5": "Fri", "6": "Sat", "7": "Sun",
@@ -73,8 +66,8 @@ export class LinuxPlatform implements PlatformService {
73
66
  fs.mkdirSync(UNIT_DIR, { recursive: true });
74
67
 
75
68
  const palmierBin = process.argv[1] || "palmier";
76
- // Save the user's shell PATH so restartDaemon can use it later
77
- // (the daemon itself runs under systemd with a limited PATH).
69
+ // Save the user's shell PATH so restartDaemon can reuse it later — under
70
+ // systemd the daemon itself runs with a limited PATH.
78
71
  const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
79
72
  fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
80
73
  fs.writeFileSync(PATH_FILE, userPath, "utf-8");
@@ -110,7 +103,7 @@ WantedBy=default.target
110
103
  console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
111
104
  }
112
105
 
113
- // Enable lingering so service runs without active login session
106
+ // Lingering lets the service run without an active login session.
114
107
  try {
115
108
  execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
116
109
  console.log("Login lingering enabled.");
@@ -127,11 +120,9 @@ WantedBy=default.target
127
120
  execSync("systemctl --user disable palmier.service 2>/dev/null", { stdio: "pipe" });
128
121
  } catch { /* service may not exist */ }
129
122
 
130
- // Remove daemon service file
131
123
  const servicePath = path.join(UNIT_DIR, "palmier.service");
132
124
  try { fs.unlinkSync(servicePath); } catch { /* ignore */ }
133
125
 
134
- // Remove all task timers and services
135
126
  try {
136
127
  const files = fs.readdirSync(UNIT_DIR).filter((f) => f.startsWith("palmier-task-"));
137
128
  for (const f of files) {
@@ -148,8 +139,8 @@ WantedBy=default.target
148
139
  }
149
140
 
150
141
  async restartDaemon(): Promise<void> {
151
- // If called from a user's terminal, save the current PATH for future use.
152
- // If called from the daemon (auto-update), read the saved PATH instead.
142
+ // From a TTY, snapshot the current PATH; from the daemon (auto-update),
143
+ // reuse whatever was last saved.
153
144
  if (process.stdin.isTTY) {
154
145
  fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
155
146
  fs.writeFileSync(PATH_FILE, process.env.PATH || "", "utf-8");
@@ -196,7 +187,6 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
196
187
  fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
197
188
  daemonReload();
198
189
 
199
- // Only create and enable a timer if the schedule exists and is enabled
200
190
  if (!task.frontmatter.schedule_enabled) return;
201
191
  const scheduleType = task.frontmatter.schedule_type;
202
192
  const scheduleValues = task.frontmatter.schedule_values;
@@ -260,7 +250,6 @@ WantedBy=timers.target
260
250
  }
261
251
 
262
252
  isTaskRunning(taskId: string): boolean {
263
- // Check systemd first (for scheduled/on-demand runs)
264
253
  const serviceName = getServiceName(taskId);
265
254
  try {
266
255
  const out = execSync(
@@ -271,7 +260,7 @@ WantedBy=timers.target
271
260
  if (state === "active" || state === "activating") return true;
272
261
  } catch { /* service may not exist */ }
273
262
 
274
- // Fall back to PID check (for follow-up runs spawned directly)
263
+ // Follow-up runs are spawned directly, so check PID too.
275
264
  try {
276
265
  const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
277
266
  const status = readTaskStatus(taskDir);
@@ -1,9 +1,6 @@
1
1
  import type { HostConfig, ParsedTask } from "../types.js";
2
2
 
3
- /**
4
- * Abstracts OS-specific daemon, scheduling, and process management.
5
- * Linux uses systemd; Windows uses Task Scheduler; macOS will use launchd.
6
- */
3
+ /** Linux: systemd. Windows: Task Scheduler. macOS: launchd (planned). */
7
4
  export interface PlatformService {
8
5
  /** Install the main `palmier serve` daemon to start at boot. */
9
6
  installDaemon(config: HostConfig): void;