palmier 0.7.8 → 0.8.0

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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/run.js +55 -0
  3. package/dist/commands/serve.js +22 -2
  4. package/dist/event-queues.d.ts +36 -0
  5. package/dist/event-queues.js +53 -0
  6. package/dist/mcp-tools.d.ts +2 -0
  7. package/dist/mcp-tools.js +4 -2
  8. package/dist/platform/linux.js +11 -8
  9. package/dist/platform/windows.d.ts +5 -6
  10. package/dist/platform/windows.js +19 -13
  11. package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
  12. package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
  13. package/dist/pwa/assets/{web-BNr628AV.js → web-BpM3fNCn.js} +1 -1
  14. package/dist/pwa/assets/{web-DyQPewAi.js → web-CF-N8Di6.js} +1 -1
  15. package/dist/pwa/index.html +2 -2
  16. package/dist/pwa/service-worker.js +1 -1
  17. package/dist/rpc-handler.js +25 -9
  18. package/dist/task.js +1 -1
  19. package/dist/transports/http-transport.js +18 -5
  20. package/dist/types.d.ts +10 -6
  21. package/package.json +1 -1
  22. package/palmier-server/README.md +3 -3
  23. package/palmier-server/pwa/src/App.css +117 -36
  24. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
  25. package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
  26. package/palmier-server/pwa/src/components/SessionComposer.tsx +20 -10
  27. package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +33 -25
  28. package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
  29. package/palmier-server/pwa/src/components/TaskForm.tsx +274 -293
  30. package/palmier-server/pwa/src/components/{TaskListView.tsx → TasksView.tsx} +20 -13
  31. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
  32. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
  33. package/palmier-server/pwa/src/pages/Dashboard.tsx +9 -26
  34. package/palmier-server/pwa/src/types.ts +5 -9
  35. package/palmier-server/spec.md +23 -23
  36. package/src/commands/run.ts +61 -0
  37. package/src/commands/serve.ts +22 -2
  38. package/src/event-queues.ts +56 -0
  39. package/src/mcp-tools.ts +6 -2
  40. package/src/platform/linux.ts +10 -8
  41. package/src/platform/windows.ts +19 -13
  42. package/src/rpc-handler.ts +28 -11
  43. package/src/task.ts +1 -1
  44. package/src/transports/http-transport.ts +17 -5
  45. package/src/types.ts +10 -7
  46. package/dist/pwa/assets/index-8cTctVnD.js +0 -120
  47. package/dist/pwa/assets/index-CSUkBBsQ.css +0 -1
package/src/mcp-tools.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { StringCodec, type NatsConnection } from "nats";
2
2
  import { registerPending } from "./pending-requests.js";
3
3
  import { getCapabilityDevice } from "./device-capabilities.js";
4
- import { getNotifications } from "./notification-store.js";
5
- import { getSmsMessages } from "./sms-store.js";
4
+ import { getNotifications, onNotificationsChanged } from "./notification-store.js";
5
+ import { getSmsMessages, onSmsChanged } from "./sms-store.js";
6
6
  import type { HostConfig } from "./types.js";
7
7
 
8
8
  export class ToolError extends Error {
@@ -750,6 +750,8 @@ export interface ResourceDefinition {
750
750
  restPath: string;
751
751
  /** Return the current resource content. */
752
752
  read: () => unknown;
753
+ /** Register a listener for content changes. Returns an unsubscribe function. */
754
+ subscribe: (listener: () => void) => () => void;
753
755
  }
754
756
 
755
757
  const deviceNotificationsResource: ResourceDefinition = {
@@ -762,6 +764,7 @@ const deviceNotificationsResource: ResourceDefinition = {
762
764
  mimeType: "application/json",
763
765
  restPath: "/notifications",
764
766
  read: getNotifications,
767
+ subscribe: onNotificationsChanged,
765
768
  };
766
769
 
767
770
  const deviceSmsResource: ResourceDefinition = {
@@ -774,6 +777,7 @@ const deviceSmsResource: ResourceDefinition = {
774
777
  mimeType: "application/json",
775
778
  restPath: "/sms-messages",
776
779
  read: getSmsMessages,
780
+ subscribe: onSmsChanged,
777
781
  };
778
782
 
779
783
  export const agentResources: ResourceDefinition[] = [deviceNotificationsResource, deviceSmsResource];
@@ -196,15 +196,17 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
196
196
  fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
197
197
  daemonReload();
198
198
 
199
- // Only create and enable a timer if triggers exist and are enabled
200
- if (!task.frontmatter.triggers_enabled) return;
201
- const triggers = task.frontmatter.triggers || [];
199
+ // Only create and enable a timer if the schedule exists and is enabled
200
+ if (!task.frontmatter.schedule_enabled) return;
201
+ const scheduleType = task.frontmatter.schedule_type;
202
+ const scheduleValues = task.frontmatter.schedule_values;
203
+ if (!scheduleType || !scheduleValues?.length) return;
202
204
  const onCalendarLines: string[] = [];
203
- for (const trigger of triggers) {
204
- if (trigger.type === "cron") {
205
- onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
206
- } else if (trigger.type === "once") {
207
- onCalendarLines.push(`OnActiveSec=${trigger.value}`);
205
+ for (const value of scheduleValues) {
206
+ if (scheduleType === "crons") {
207
+ onCalendarLines.push(`OnCalendar=${cronToOnCalendar(value)}`);
208
+ } else if (scheduleType === "specific_times") {
209
+ onCalendarLines.push(`OnActiveSec=${value}`);
208
210
  }
209
211
  }
210
212
 
@@ -14,22 +14,23 @@ const DAEMON_TASK_NAME = "PalmierDaemon";
14
14
  const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
15
15
 
16
16
  /**
17
- * Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
17
+ * Convert a single schedule value to a Task Scheduler XML trigger element.
18
18
  *
19
- * Only these cron patterns (produced by the PWA UI) are handled:
19
+ * `specific_times` values are ISO datetime strings like "2026-03-28T09:00".
20
+ *
21
+ * `crons` values are cron expressions. Only these patterns (produced by the PWA UI) are handled:
20
22
  * hourly: "0 * * * *"
21
23
  * daily: "MM HH * * *"
22
24
  * weekly: "MM HH * * D"
23
25
  * monthly: "MM HH D * *"
24
26
  */
25
- export function triggerToXml(trigger: { type: string; value: string }): string {
26
- if (trigger.type === "once") {
27
- // ISO datetime "2026-03-28T09:00"
28
- return `<TimeTrigger><StartBoundary>${trigger.value}:00</StartBoundary></TimeTrigger>`;
27
+ export function scheduleValueToXml(scheduleType: "crons" | "specific_times", value: string): string {
28
+ if (scheduleType === "specific_times") {
29
+ return `<TimeTrigger><StartBoundary>${value}:00</StartBoundary></TimeTrigger>`;
29
30
  }
30
31
 
31
- const parts = trigger.value.trim().split(/\s+/);
32
- if (parts.length !== 5) throw new Error(`Invalid cron expression: ${trigger.value}`);
32
+ const parts = value.trim().split(/\s+/);
33
+ if (parts.length !== 5) throw new Error(`Invalid cron expression: ${value}`);
33
34
  const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
34
35
  const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
35
36
  // StartBoundary needs a full date; use a past date as the anchor
@@ -191,14 +192,19 @@ export class WindowsPlatform implements PlatformService {
191
192
  const script = process.argv[1] || "palmier";
192
193
  const tr = `"${process.execPath}" "${script}" run ${taskId}`;
193
194
 
194
- // Build trigger XML elements
195
+ // Build trigger XML elements. Event-based schedule types (on_new_notification,
196
+ // on_new_sms) carry no values and are driven by the run process, not the OS
197
+ // scheduler — they intentionally produce only the dummy trigger below.
195
198
  const triggerElements: string[] = [];
196
- if (task.frontmatter.triggers_enabled) {
197
- for (const trigger of task.frontmatter.triggers ?? []) {
199
+ const scheduleType = task.frontmatter.schedule_type;
200
+ const scheduleValues = task.frontmatter.schedule_values;
201
+ const isTimerSchedule = scheduleType === "crons" || scheduleType === "specific_times";
202
+ if (task.frontmatter.schedule_enabled && isTimerSchedule && scheduleValues?.length) {
203
+ for (const value of scheduleValues) {
198
204
  try {
199
- triggerElements.push(triggerToXml(trigger));
205
+ triggerElements.push(scheduleValueToXml(scheduleType, value));
200
206
  } catch (err) {
201
- console.error(`Invalid trigger: ${err}`);
207
+ console.error(`Invalid schedule value: ${err}`);
202
208
  }
203
209
  }
204
210
  }
@@ -14,6 +14,7 @@ import { publishHostEvent } from "./events.js";
14
14
  import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice, type DeviceCapability } from "./device-capabilities.js";
15
15
  import { currentVersion, performUpdate } from "./update-checker.js";
16
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
17
+ import { clearTaskQueue } from "./event-queues.js";
17
18
  import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
18
19
 
19
20
  /**
@@ -162,7 +163,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
162
163
  // is active. Includes any prompts already waiting so a reconnecting
163
164
  // PWA can render their modals without replaying events.
164
165
  const capabilities: Record<string, string | null> = {};
165
- for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd"] as const) {
166
+ for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd", "email"] as const) {
166
167
  capabilities[cap] = getCapabilityDevice(cap)?.clientToken ?? null;
167
168
  }
168
169
  return {
@@ -194,8 +195,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
194
195
  const params = request.params as {
195
196
  user_prompt: string;
196
197
  agent: string;
197
- triggers?: Array<{ type: "cron" | "once"; value: string }>;
198
- triggers_enabled?: boolean;
198
+ schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
199
+ schedule_values?: string[];
200
+ schedule_enabled?: boolean;
199
201
  requires_confirmation?: boolean;
200
202
  yolo_mode?: boolean;
201
203
  foreground_mode?: boolean;
@@ -214,9 +216,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
214
216
  name,
215
217
  user_prompt: params.user_prompt,
216
218
  agent: params.agent,
217
- triggers: params.triggers ?? [],
218
- triggers_enabled: params.triggers_enabled ?? true,
219
+ schedule_enabled: params.schedule_enabled ?? true,
219
220
  requires_confirmation: params.requires_confirmation ?? true,
221
+ ...(params.schedule_type ? { schedule_type: params.schedule_type } : {}),
222
+ ...(params.schedule_values?.length ? { schedule_values: params.schedule_values } : {}),
220
223
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
221
224
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
222
225
  ...(params.command ? { command: params.command } : {}),
@@ -235,8 +238,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
235
238
  id: string;
236
239
  user_prompt?: string;
237
240
  agent?: string;
238
- triggers?: Array<{ type: "cron" | "once"; value: string }>;
239
- triggers_enabled?: boolean;
241
+ schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms" | null;
242
+ schedule_values?: string[] | null;
243
+ schedule_enabled?: boolean;
240
244
  requires_confirmation?: boolean;
241
245
  yolo_mode?: boolean;
242
246
  foreground_mode?: boolean;
@@ -253,8 +257,21 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
253
257
  // Merge updates
254
258
  if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
255
259
  if (params.agent !== undefined) existing.frontmatter.agent = params.agent;
256
- if (params.triggers !== undefined) existing.frontmatter.triggers = params.triggers;
257
- if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
260
+ if (params.schedule_type !== undefined) {
261
+ if (params.schedule_type) {
262
+ existing.frontmatter.schedule_type = params.schedule_type;
263
+ } else {
264
+ delete existing.frontmatter.schedule_type;
265
+ }
266
+ }
267
+ if (params.schedule_values !== undefined) {
268
+ if (params.schedule_values && params.schedule_values.length > 0) {
269
+ existing.frontmatter.schedule_values = params.schedule_values;
270
+ } else {
271
+ delete existing.frontmatter.schedule_values;
272
+ }
273
+ }
274
+ if (params.schedule_enabled !== undefined) existing.frontmatter.schedule_enabled = params.schedule_enabled;
258
275
  if (params.requires_confirmation !== undefined)
259
276
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
260
277
  if (params.yolo_mode !== undefined) {
@@ -290,6 +307,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
290
307
  const params = request.params as { id: string };
291
308
 
292
309
  getPlatform().removeTaskTimer(params.id);
310
+ clearTaskQueue(params.id);
293
311
  removeFromTaskList(config.projectRoot, params.id);
294
312
 
295
313
  return { ok: true, task_id: params.id };
@@ -314,8 +332,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
314
332
  name,
315
333
  user_prompt: params.user_prompt,
316
334
  agent: params.agent,
317
- triggers: [],
318
- triggers_enabled: false,
335
+ schedule_enabled: false,
319
336
  requires_confirmation: params.requires_confirmation ?? false,
320
337
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
321
338
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
package/src/task.ts CHANGED
@@ -36,7 +36,7 @@ export function parseTaskContent(content: string): ParsedTask {
36
36
 
37
37
  frontmatter.name ??= frontmatter.user_prompt?.slice(0, 60) ?? "";
38
38
  frontmatter.agent ??= "claude";
39
- frontmatter.triggers_enabled ??= true;
39
+ frontmatter.schedule_enabled ??= true;
40
40
 
41
41
  return { frontmatter };
42
42
  }
@@ -9,8 +9,7 @@ import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
9
9
  import { agentToolMap, agentResources, ToolError, type ToolContext } from "../mcp-tools.js";
10
10
  import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
11
11
  import { getTaskDir } from "../task.js";
12
- import { onNotificationsChanged } from "../notification-store.js";
13
- import { onSmsChanged } from "../sms-store.js";
12
+ import { popEvent } from "../event-queues.js";
14
13
 
15
14
  // ── Bundled PWA asset serving ───────────────────────────────────────────
16
15
 
@@ -123,9 +122,9 @@ export async function startHttpTransport(
123
122
  }
124
123
  }
125
124
 
126
- // Wire up resource change listeners
127
- onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
128
- onSmsChanged(() => broadcastResourceUpdated("sms-messages://device"));
125
+ for (const resource of agentResources) {
126
+ resource.subscribe(() => broadcastResourceUpdated(resource.uri));
127
+ }
129
128
 
130
129
  // If a pairing code is provided, pre-register it
131
130
  if (pairingCode) {
@@ -281,6 +280,19 @@ export async function startHttpTransport(
281
280
  return;
282
281
  }
283
282
 
283
+ // ── Event queue pop (used by event-triggered palmier run) ─────────
284
+
285
+ if (req.method === "POST" && pathname === "/task-event/pop") {
286
+ if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
287
+ const taskId = url.searchParams.get("taskId");
288
+ if (!taskId) {
289
+ sendJson(res, 400, { error: "taskId query parameter is required" });
290
+ return;
291
+ }
292
+ sendJson(res, 200, popEvent(taskId));
293
+ return;
294
+ }
295
+
284
296
  // ── Localhost-only endpoints (no auth) ─────────────────────────────
285
297
 
286
298
  if (req.method === "POST" && pathname === "/event") {
package/src/types.ts CHANGED
@@ -21,8 +21,16 @@ export interface TaskFrontmatter {
21
21
  name: string;
22
22
  user_prompt: string;
23
23
  agent: string;
24
- triggers: Trigger[];
25
- triggers_enabled: boolean;
24
+ /**
25
+ * Task schedule.
26
+ * - `crons`: `schedule_values` holds cron expressions (e.g. "0 9 * * *")
27
+ * - `specific_times`: `schedule_values` holds local datetime strings (e.g. "2026-04-20T09:00")
28
+ * - `on_new_notification`: fires on each new Android notification from NATS; no `schedule_values`
29
+ * - `on_new_sms`: fires on each new SMS from NATS; no `schedule_values`
30
+ */
31
+ schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
32
+ schedule_values?: string[];
33
+ schedule_enabled: boolean;
26
34
  requires_confirmation: boolean;
27
35
  yolo_mode?: boolean;
28
36
  foreground_mode?: boolean;
@@ -30,11 +38,6 @@ export interface TaskFrontmatter {
30
38
  command?: string;
31
39
  }
32
40
 
33
- export interface Trigger {
34
- type: "cron" | "once";
35
- value: string;
36
- }
37
-
38
41
  export interface ParsedTask {
39
42
  frontmatter: TaskFrontmatter;
40
43
  }