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
package/dist/mcp-tools.js CHANGED
@@ -400,9 +400,9 @@ const sendSmsTool = {
400
400
  async handler(args, ctx) {
401
401
  if (!ctx.nc)
402
402
  throw new ToolError("Not connected to server (NATS unavailable)", 503);
403
- const device = getCapabilityDevice("sms");
403
+ const device = getCapabilityDevice("sms-send");
404
404
  if (!device)
405
- throw new ToolError("No device has SMS access enabled", 400);
405
+ throw new ToolError("No device has SMS Send enabled", 400);
406
406
  const { to, body } = args;
407
407
  if (!to || !body)
408
408
  throw new ToolError("to and body are required", 400);
@@ -433,10 +433,10 @@ const sendSmsTool = {
433
433
  return result;
434
434
  },
435
435
  };
436
- const sendAlertTool = {
437
- name: "send-alert",
436
+ const sendAlarmTool = {
437
+ name: "send-alarm",
438
438
  description: [
439
- "Send an alert to the user's mobile device with an alarm sound and full-screen popup.",
439
+ "Trigger an alarm on the user's mobile device with an alarm sound and full-screen popup.",
440
440
  "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.",
441
441
  "Blocks until the device responds (up to 30 seconds).",
442
442
  'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
@@ -444,17 +444,17 @@ const sendAlertTool = {
444
444
  inputSchema: {
445
445
  type: "object",
446
446
  properties: {
447
- title: { type: "string", description: "Alert title" },
448
- description: { type: "string", description: "Alert description/details" },
447
+ title: { type: "string", description: "Alarm title" },
448
+ description: { type: "string", description: "Alarm description/details" },
449
449
  },
450
450
  required: ["title"],
451
451
  },
452
452
  async handler(args, ctx) {
453
453
  if (!ctx.nc)
454
454
  throw new ToolError("Not connected to server (NATS unavailable)", 503);
455
- const device = getCapabilityDevice("alert");
455
+ const device = getCapabilityDevice("alarm");
456
456
  if (!device)
457
- throw new ToolError("No device has alert access enabled", 400);
457
+ throw new ToolError("No device has alarm access enabled", 400);
458
458
  const { title, description } = args;
459
459
  if (!title)
460
460
  throw new ToolError("title is required", 400);
@@ -465,12 +465,12 @@ const sendAlertTool = {
465
465
  };
466
466
  if (description)
467
467
  payload.description = description;
468
- const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.alert`, sc.encode(JSON.stringify(payload)), { timeout: 5_000 });
468
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.alarm`, sc.encode(JSON.stringify(payload)), { timeout: 5_000 });
469
469
  const ack = JSON.parse(sc.decode(ackReply.data));
470
470
  if (ack.error)
471
471
  throw new ToolError(ack.error, 502);
472
472
  const responsePromise = new Promise((resolve, reject) => {
473
- const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.alert.${ctx.sessionId}`, { max: 1 });
473
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.alarm.${ctx.sessionId}`, { max: 1 });
474
474
  const timer = setTimeout(() => {
475
475
  sub.unsubscribe();
476
476
  reject(new ToolError("Device did not respond within 30 seconds", 504));
@@ -639,7 +639,7 @@ const sendEmailTool = {
639
639
  return result;
640
640
  },
641
641
  };
642
- export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
642
+ export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlarmTool, readBatteryTool, setRingerModeTool];
643
643
  export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
644
644
  const deviceNotificationsResource = {
645
645
  uri: "notifications://device",
@@ -667,9 +667,6 @@ const deviceSmsResource = {
667
667
  };
668
668
  export const agentResources = [deviceNotificationsResource, deviceSmsResource];
669
669
  export const agentResourceMap = new Map(agentResources.map((r) => [r.uri, r]));
670
- /**
671
- * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
672
- */
673
670
  export function generateEndpointDocs(port, taskId, tools = agentTools, resources = agentResources) {
674
671
  const baseUrl = `http://localhost:${port}`;
675
672
  const lines = [
@@ -680,7 +677,6 @@ export function generateEndpointDocs(port, taskId, tools = agentTools, resources
680
677
  const schema = tool.inputSchema;
681
678
  const props = schema.properties ?? {};
682
679
  const required = new Set(schema.required ?? []);
683
- // Build example JSON (body only, no taskId)
684
680
  const example = {};
685
681
  for (const [key, prop] of Object.entries(props)) {
686
682
  if (prop.type === "array")
@@ -1,7 +1,4 @@
1
1
  import { type NatsConnection } from "nats";
2
2
  import type { HostConfig } from "./types.js";
3
- /**
4
- * Connect to NATS using the host config's JWT credentials.
5
- */
6
3
  export declare function connectNats(config: HostConfig): Promise<NatsConnection>;
7
4
  //# sourceMappingURL=nats-client.d.ts.map
@@ -1,7 +1,4 @@
1
1
  import { connect, jwtAuthenticator } from "nats";
2
- /**
3
- * Connect to NATS using the host config's JWT credentials.
4
- */
5
2
  export async function connectNats(config) {
6
3
  if (!config.natsJwt || !config.natsNkeySeed) {
7
4
  throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
@@ -10,7 +7,7 @@ export async function connectNats(config) {
10
7
  servers: config.natsUrl,
11
8
  authenticator: jwtAuthenticator(config.natsJwt, new TextEncoder().encode(config.natsNkeySeed)),
12
9
  });
13
- // Do not log anything as that will pollute stdout for mcp server.
10
+ // Do not log it would pollute stdout for the MCP server.
14
11
  return nc;
15
12
  }
16
13
  //# sourceMappingURL=nats-client.js.map
@@ -17,29 +17,15 @@ export interface PendingRequest {
17
17
  meta?: PendingRequestMeta;
18
18
  }
19
19
  /**
20
- * Register a pending request keyed by either a sessionId (confirmation / input)
21
- * or a taskId (permission). The `meta` is surfaced to PWAs that connect after
22
- * the request was opened, so their modals can render without replaying events.
23
- * Only one pending request per key at a time.
20
+ * Key is sessionId for confirmation/input, taskId for permission. Only one
21
+ * pending request per key at a time. `meta` is surfaced via host.info so a
22
+ * freshly-connected PWA can render the modal without replaying events.
24
23
  */
25
24
  export declare function registerPending(key: string, type: PendingRequest["type"], params?: PendingRequest["params"], meta?: PendingRequestMeta): Promise<string[]>;
26
- /**
27
- * Resolve a pending request with the user's response.
28
- * Returns true if a pending request was found and resolved.
29
- */
30
25
  export declare function resolvePending(key: string, value: string[]): boolean;
31
- /**
32
- * Get the current pending request for a key (if any).
33
- */
34
26
  export declare function getPending(key: string): PendingRequest | undefined;
35
- /**
36
- * Remove a pending request without resolving it.
37
- */
38
27
  export declare function removePending(key: string): void;
39
- /**
40
- * List all currently-pending requests, stripped of the unserializable `resolve`
41
- * callback. Used by `host.info` so the PWA can seed its modal state on connect.
42
- */
28
+ /** Pending requests stripped of the unserializable `resolve` callback. */
43
29
  export declare function listPending(): Array<{
44
30
  key: string;
45
31
  type: PendingRequest["type"];
@@ -1,9 +1,8 @@
1
1
  const pending = new Map();
2
2
  /**
3
- * Register a pending request keyed by either a sessionId (confirmation / input)
4
- * or a taskId (permission). The `meta` is surfaced to PWAs that connect after
5
- * the request was opened, so their modals can render without replaying events.
6
- * Only one pending request per key at a time.
3
+ * Key is sessionId for confirmation/input, taskId for permission. Only one
4
+ * pending request per key at a time. `meta` is surfaced via host.info so a
5
+ * freshly-connected PWA can render the modal without replaying events.
7
6
  */
8
7
  export function registerPending(key, type, params, meta) {
9
8
  if (pending.has(key)) {
@@ -13,10 +12,6 @@ export function registerPending(key, type, params, meta) {
13
12
  pending.set(key, { type, resolve, params, meta });
14
13
  });
15
14
  }
16
- /**
17
- * Resolve a pending request with the user's response.
18
- * Returns true if a pending request was found and resolved.
19
- */
20
15
  export function resolvePending(key, value) {
21
16
  const entry = pending.get(key);
22
17
  if (!entry)
@@ -25,22 +20,13 @@ export function resolvePending(key, value) {
25
20
  entry.resolve(value);
26
21
  return true;
27
22
  }
28
- /**
29
- * Get the current pending request for a key (if any).
30
- */
31
23
  export function getPending(key) {
32
24
  return pending.get(key);
33
25
  }
34
- /**
35
- * Remove a pending request without resolving it.
36
- */
37
26
  export function removePending(key) {
38
27
  pending.delete(key);
39
28
  }
40
- /**
41
- * List all currently-pending requests, stripped of the unserializable `resolve`
42
- * callback. Used by `host.info` so the PWA can seed its modal state on connect.
43
- */
29
+ /** Pending requests stripped of the unserializable `resolve` callback. */
44
30
  export function listPending() {
45
31
  return [...pending.entries()].map(([key, entry]) => ({
46
32
  key,
@@ -1,8 +1,5 @@
1
1
  import type { PlatformService } from "./platform.js";
2
- /**
3
- * On Windows, execSync needs an explicit shell so .cmd shims resolve correctly.
4
- * On Unix, undefined lets Node use the default shell.
5
- */
2
+ /** Windows needs an explicit shell for execSync to resolve .cmd shims. */
6
3
  export declare const SHELL: string | undefined;
7
4
  export declare function getPlatform(): PlatformService;
8
5
  export type { PlatformService } from "./platform.js";
@@ -1,16 +1,17 @@
1
1
  import { LinuxPlatform } from "./linux.js";
2
2
  import { WindowsPlatform } from "./windows.js";
3
- /**
4
- * On Windows, execSync needs an explicit shell so .cmd shims resolve correctly.
5
- * On Unix, undefined lets Node use the default shell.
6
- */
3
+ import { MacOsPlatform } from "./macos.js";
4
+ /** Windows needs an explicit shell for execSync to resolve .cmd shims. */
7
5
  export const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
8
6
  let _instance;
9
7
  export function getPlatform() {
10
8
  if (!_instance) {
11
- _instance = process.platform === "win32"
12
- ? new WindowsPlatform()
13
- : new LinuxPlatform();
9
+ if (process.platform === "win32")
10
+ _instance = new WindowsPlatform();
11
+ else if (process.platform === "darwin")
12
+ _instance = new MacOsPlatform();
13
+ else
14
+ _instance = new LinuxPlatform();
14
15
  }
15
16
  return _instance;
16
17
  }
@@ -1,15 +1,9 @@
1
1
  import type { PlatformService } from "./platform.js";
2
2
  import type { HostConfig, ParsedTask } from "../types.js";
3
3
  /**
4
- * Convert a cron expression to a systemd OnCalendar string.
5
- *
6
- * Only the 4 cron patterns the PWA UI can produce are supported:
7
- * hourly: "0 * * * *"
8
- * daily: "MM HH * * *"
9
- * weekly: "MM HH * * D"
10
- * monthly: "MM HH D * *"
11
- * Arbitrary cron expressions (ranges, lists, steps beyond hourly) are NOT
12
- * handled because the UI never generates them.
4
+ * Only the 4 cron patterns the PWA UI produces are supported:
5
+ * hourly "0 * * * *", daily "MM HH * * *", weekly "MM HH * * D", monthly "MM HH D * *".
6
+ * Arbitrary expressions (ranges, lists, sub-hour steps) are not handled.
13
7
  */
14
8
  export declare function cronToOnCalendar(cron: string): string;
15
9
  export declare class LinuxPlatform implements PlatformService {
@@ -15,15 +15,9 @@ function getServiceName(taskId) {
15
15
  return `palmier-task-${taskId}.service`;
16
16
  }
17
17
  /**
18
- * Convert a cron expression to a systemd OnCalendar string.
19
- *
20
- * Only the 4 cron patterns the PWA UI can produce are supported:
21
- * hourly: "0 * * * *"
22
- * daily: "MM HH * * *"
23
- * weekly: "MM HH * * D"
24
- * monthly: "MM HH D * *"
25
- * Arbitrary cron expressions (ranges, lists, steps beyond hourly) are NOT
26
- * handled because the UI never generates them.
18
+ * Only the 4 cron patterns the PWA UI produces are supported:
19
+ * hourly "0 * * * *", daily "MM HH * * *", weekly "MM HH * * D", monthly "MM HH D * *".
20
+ * Arbitrary expressions (ranges, lists, sub-hour steps) are not handled.
27
21
  */
28
22
  export function cronToOnCalendar(cron) {
29
23
  const parts = cron.trim().split(/\s+/);
@@ -31,7 +25,6 @@ export function cronToOnCalendar(cron) {
31
25
  throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
32
26
  }
33
27
  const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
34
- // Map cron day-of-week numbers to systemd abbreviated names
35
28
  const dowMap = {
36
29
  "0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed",
37
30
  "4": "Thu", "5": "Fri", "6": "Sat", "7": "Sun",
@@ -59,8 +52,8 @@ export class LinuxPlatform {
59
52
  installDaemon(config) {
60
53
  fs.mkdirSync(UNIT_DIR, { recursive: true });
61
54
  const palmierBin = process.argv[1] || "palmier";
62
- // Save the user's shell PATH so restartDaemon can use it later
63
- // (the daemon itself runs under systemd with a limited PATH).
55
+ // Save the user's shell PATH so restartDaemon can reuse it later — under
56
+ // systemd the daemon itself runs with a limited PATH.
64
57
  const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
65
58
  fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
66
59
  fs.writeFileSync(PATH_FILE, userPath, "utf-8");
@@ -93,7 +86,7 @@ WantedBy=default.target
93
86
  console.error(`Warning: failed to enable systemd service: ${err}`);
94
87
  console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
95
88
  }
96
- // Enable lingering so service runs without active login session
89
+ // Lingering lets the service run without an active login session.
97
90
  try {
98
91
  execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
99
92
  console.log("Login lingering enabled.");
@@ -109,13 +102,11 @@ WantedBy=default.target
109
102
  execSync("systemctl --user disable palmier.service 2>/dev/null", { stdio: "pipe" });
110
103
  }
111
104
  catch { /* service may not exist */ }
112
- // Remove daemon service file
113
105
  const servicePath = path.join(UNIT_DIR, "palmier.service");
114
106
  try {
115
107
  fs.unlinkSync(servicePath);
116
108
  }
117
109
  catch { /* ignore */ }
118
- // Remove all task timers and services
119
110
  try {
120
111
  const files = fs.readdirSync(UNIT_DIR).filter((f) => f.startsWith("palmier-task-"));
121
112
  for (const f of files) {
@@ -142,8 +133,8 @@ WantedBy=default.target
142
133
  console.log("Palmier daemon and tasks uninstalled.");
143
134
  }
144
135
  async restartDaemon() {
145
- // If called from a user's terminal, save the current PATH for future use.
146
- // If called from the daemon (auto-update), read the saved PATH instead.
136
+ // From a TTY, snapshot the current PATH; from the daemon (auto-update),
137
+ // reuse whatever was last saved.
147
138
  if (process.stdin.isTTY) {
148
139
  fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
149
140
  fs.writeFileSync(PATH_FILE, process.env.PATH || "", "utf-8");
@@ -181,7 +172,6 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
181
172
  `;
182
173
  fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
183
174
  daemonReload();
184
- // Only create and enable a timer if the schedule exists and is enabled
185
175
  if (!task.frontmatter.schedule_enabled)
186
176
  return;
187
177
  const scheduleType = task.frontmatter.schedule_type;
@@ -248,7 +238,6 @@ WantedBy=timers.target
248
238
  await execAsync(`systemctl --user stop ${serviceName}`);
249
239
  }
250
240
  isTaskRunning(taskId) {
251
- // Check systemd first (for scheduled/on-demand runs)
252
241
  const serviceName = getServiceName(taskId);
253
242
  try {
254
243
  const out = execSync(`systemctl --user show -p ActiveState --value ${serviceName}`, { encoding: "utf-8" });
@@ -257,7 +246,7 @@ WantedBy=timers.target
257
246
  return true;
258
247
  }
259
248
  catch { /* service may not exist */ }
260
- // Fall back to PID check (for follow-up runs spawned directly)
249
+ // Follow-up runs are spawned directly, so check PID too.
261
250
  try {
262
251
  const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
263
252
  const status = readTaskStatus(taskDir);
@@ -0,0 +1,32 @@
1
+ import type { PlatformService } from "./platform.js";
2
+ import type { HostConfig, ParsedTask } from "../types.js";
3
+ /**
4
+ * Convert one of the four PWA-produced cron patterns to a launchd
5
+ * `StartCalendarInterval` dict.
6
+ * hourly "0 * * * *" → { Minute: 0 }
7
+ * daily "MM HH * * *" → { Minute, Hour }
8
+ * weekly "MM HH * * D" → { Minute, Hour, Weekday }
9
+ * monthly "MM HH D * *" → { Minute, Hour, Day }
10
+ * launchd Weekday: Sunday is 0 (cron 7 → 0).
11
+ */
12
+ export declare function cronToCalendarInterval(cron: string): Record<string, number>;
13
+ /**
14
+ * Convert a PWA `specific_times` value (ISO local datetime like "2026-04-20T09:00")
15
+ * to a `StartCalendarInterval` dict. launchd has no "one-shot at date X" trigger,
16
+ * so we omit Year — the task fires yearly on the same date and time. Sufficient
17
+ * because the PWA regenerates/removes one-off tasks after they run.
18
+ */
19
+ export declare function specificTimeToCalendarInterval(iso: string): Record<string, number>;
20
+ export declare function buildPlist(dict: Record<string, unknown>): string;
21
+ export declare class MacOsPlatform implements PlatformService {
22
+ installDaemon(config: HostConfig): void;
23
+ uninstallDaemon(): void;
24
+ restartDaemon(): Promise<void>;
25
+ installTaskTimer(config: HostConfig, task: ParsedTask): void;
26
+ removeTaskTimer(taskId: string): void;
27
+ startTask(taskId: string): Promise<void>;
28
+ stopTask(taskId: string): Promise<void>;
29
+ isTaskRunning(taskId: string): boolean;
30
+ getGuiEnv(): Record<string, string>;
31
+ }
32
+ //# sourceMappingURL=macos.d.ts.map