palmier 0.7.2 → 0.7.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 (36) hide show
  1. package/README.md +43 -22
  2. package/dist/commands/serve.js +14 -1
  3. package/dist/device-capabilities.d.ts +9 -0
  4. package/dist/device-capabilities.js +36 -0
  5. package/dist/mcp-handler.js +4 -1
  6. package/dist/mcp-tools.js +414 -7
  7. package/dist/pwa/assets/{index-C6Lz09EY.css → index-B-ByUHPS.css} +1 -1
  8. package/dist/pwa/assets/index-BirmfPUC.js +118 -0
  9. package/dist/pwa/assets/{web-HDs03L2B.js → web-Dc9-IiRD.js} +1 -1
  10. package/dist/pwa/assets/{web-CBI458eN.js → web-_b3Dvcvz.js} +1 -1
  11. package/dist/pwa/index.html +2 -2
  12. package/dist/pwa/service-worker.js +1 -1
  13. package/dist/rpc-handler.js +19 -4
  14. package/dist/sms-store.d.ts +11 -0
  15. package/dist/sms-store.js +19 -0
  16. package/dist/transports/http-transport.js +16 -1
  17. package/package.json +1 -1
  18. package/palmier-server/README.md +11 -3
  19. package/palmier-server/pwa/src/App.css +3 -0
  20. package/palmier-server/pwa/src/components/HostMenu.tsx +465 -0
  21. package/palmier-server/pwa/src/constants.ts +1 -1
  22. package/palmier-server/server/src/index.ts +306 -0
  23. package/palmier-server/server/src/routes/device.ts +168 -0
  24. package/palmier-server/spec.md +32 -3
  25. package/src/commands/serve.ts +14 -1
  26. package/src/device-capabilities.ts +55 -0
  27. package/src/mcp-handler.ts +4 -1
  28. package/src/mcp-tools.ts +473 -7
  29. package/src/rpc-handler.ts +19 -4
  30. package/src/sms-store.ts +28 -0
  31. package/src/transports/http-transport.ts +16 -1
  32. package/test/agent-instructions.test.ts +1 -1
  33. package/dist/location-device.d.ts +0 -8
  34. package/dist/location-device.js +0 -32
  35. package/dist/pwa/assets/index-DLxrL0hR.js +0 -118
  36. package/src/location-device.ts +0 -35
@@ -15,9 +15,74 @@ interface LocationPermissionPlugin {
15
15
  check(): Promise<LocationPermissionResult>;
16
16
  }
17
17
 
18
+ interface NotificationListenerResult {
19
+ enabled: boolean;
20
+ }
21
+
22
+ interface NotificationListenerPlugin {
23
+ request(): Promise<NotificationListenerResult>;
24
+ check(): Promise<NotificationListenerResult>;
25
+ }
26
+
18
27
  const LocationPermission = Capacitor.isNativePlatform()
19
28
  ? registerPlugin<LocationPermissionPlugin>("LocationPermission")
20
29
  : null;
30
+
31
+ interface SmsPermissionResult {
32
+ granted: boolean;
33
+ }
34
+
35
+ interface SmsPermissionPlugin {
36
+ request(): Promise<SmsPermissionResult>;
37
+ check(): Promise<SmsPermissionResult>;
38
+ }
39
+
40
+ interface ContactsPermissionResult {
41
+ granted: boolean;
42
+ }
43
+
44
+ interface ContactsPermissionPlugin {
45
+ request(): Promise<ContactsPermissionResult>;
46
+ check(): Promise<ContactsPermissionResult>;
47
+ }
48
+
49
+ interface CalendarPermissionResult {
50
+ granted: boolean;
51
+ }
52
+
53
+ interface CalendarPermissionPlugin {
54
+ request(): Promise<CalendarPermissionResult>;
55
+ check(): Promise<CalendarPermissionResult>;
56
+ }
57
+
58
+ interface DndAccessResult {
59
+ enabled: boolean;
60
+ }
61
+
62
+ interface DndAccessPlugin {
63
+ request(): Promise<DndAccessResult>;
64
+ check(): Promise<DndAccessResult>;
65
+ }
66
+
67
+ const NotificationListener = Capacitor.isNativePlatform()
68
+ ? registerPlugin<NotificationListenerPlugin>("NotificationListener")
69
+ : null;
70
+
71
+ const SmsPermission = Capacitor.isNativePlatform()
72
+ ? registerPlugin<SmsPermissionPlugin>("SmsPermission")
73
+ : null;
74
+
75
+ const ContactsPermission = Capacitor.isNativePlatform()
76
+ ? registerPlugin<ContactsPermissionPlugin>("ContactsPermission")
77
+ : null;
78
+
79
+ const CalendarPermission = Capacitor.isNativePlatform()
80
+ ? registerPlugin<CalendarPermissionPlugin>("CalendarPermission")
81
+ : null;
82
+
83
+ const DndAccess = Capacitor.isNativePlatform()
84
+ ? registerPlugin<DndAccessPlugin>("DndAccess")
85
+ : null;
21
86
  import { useHostStore } from "../contexts/HostStoreContext";
22
87
  import { useMediaQuery } from "../hooks/useMediaQuery";
23
88
 
@@ -44,6 +109,20 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
44
109
  const [renameValue, setRenameValue] = useState("");
45
110
  const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
46
111
  const [togglingLocation, setTogglingLocation] = useState(false);
112
+ const [notificationListenerEnabled, setNotificationListenerEnabled] = useState(false);
113
+ const [togglingNotificationListener, setTogglingNotificationListener] = useState(false);
114
+ const [smsEnabled, setSmsEnabled] = useState(false);
115
+ const [togglingSms, setTogglingSms] = useState(false);
116
+ const [contactsEnabled, setContactsEnabled] = useState(false);
117
+ const [togglingContacts, setTogglingContacts] = useState(false);
118
+ const [calendarEnabled, setCalendarEnabled] = useState(false);
119
+ const [togglingCalendar, setTogglingCalendar] = useState(false);
120
+ const [dndEnabled, setDndEnabled] = useState(false);
121
+ const [togglingDnd, setTogglingDnd] = useState(false);
122
+ const [alarmEnabled, setAlarmEnabled] = useState(false);
123
+ const [togglingAlarm, setTogglingAlarm] = useState(false);
124
+ const [batteryEnabled, setBatteryEnabled] = useState(false);
125
+ const [togglingBattery, setTogglingBattery] = useState(false);
47
126
 
48
127
  const locationEnabled = !!(activeClientToken && locationClientToken === activeClientToken);
49
128
 
@@ -72,6 +151,308 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
72
151
  return () => { listener.then((h) => h.remove()); };
73
152
  }, [locationEnabled, activeClientToken]);
74
153
 
154
+ // Sync notification listener toggle with system state — on mount and when app resumes
155
+ useEffect(() => {
156
+ if (!isNative || !NotificationListener) return;
157
+
158
+ function syncNotificationListenerState() {
159
+ Promise.all([
160
+ NotificationListener!.check(),
161
+ Preferences.get({ key: "notificationListenerEnabled" }),
162
+ ]).then(([{ enabled: systemEnabled }, { value: prefValue }]) => {
163
+ // Enabled only if both system permission is granted AND user toggled on
164
+ setNotificationListenerEnabled(systemEnabled && prefValue !== "false");
165
+ });
166
+ }
167
+
168
+ syncNotificationListenerState();
169
+
170
+ const listener = CapApp.addListener("resume", () => {
171
+ syncNotificationListenerState();
172
+ });
173
+
174
+ return () => { listener.then((h) => h.remove()); };
175
+ }, []);
176
+
177
+ async function handleNotificationListenerToggle() {
178
+ if (!NotificationListener || !request) return;
179
+ setTogglingNotificationListener(true);
180
+ try {
181
+ if (notificationListenerEnabled) {
182
+ await Preferences.set({ key: "notificationListenerEnabled", value: "false" });
183
+ await request("device.capability.disable", { capability: "notifications" });
184
+ setNotificationListenerEnabled(false);
185
+ } else {
186
+ const { enabled: systemEnabled } = await NotificationListener.check();
187
+ if (!systemEnabled) {
188
+ const result = await NotificationListener.request();
189
+ if (!result.enabled) return;
190
+ }
191
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
192
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
193
+ await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
194
+ await request("device.capability.enable", { capability: "notifications", fcmToken });
195
+ setNotificationListenerEnabled(true);
196
+ }
197
+ } catch (err) {
198
+ console.error("Failed to toggle notification listener:", err);
199
+ } finally {
200
+ setTogglingNotificationListener(false);
201
+ }
202
+ }
203
+
204
+ // Sync SMS toggle with permission state — on mount and when app resumes
205
+ useEffect(() => {
206
+ if (!isNative || !SmsPermission) return;
207
+
208
+ function syncSmsState() {
209
+ Promise.all([
210
+ SmsPermission!.check(),
211
+ Preferences.get({ key: "smsListenerEnabled" }),
212
+ ]).then(([{ granted }, { value: prefValue }]) => {
213
+ setSmsEnabled(granted && prefValue !== "false");
214
+ });
215
+ }
216
+
217
+ syncSmsState();
218
+
219
+ const listener = CapApp.addListener("resume", () => {
220
+ syncSmsState();
221
+ });
222
+
223
+ return () => { listener.then((h) => h.remove()); };
224
+ }, []);
225
+
226
+ async function handleSmsToggle() {
227
+ if (!SmsPermission || !request) return;
228
+ setTogglingSms(true);
229
+ try {
230
+ if (smsEnabled) {
231
+ await Preferences.set({ key: "smsListenerEnabled", value: "false" });
232
+ await request("device.capability.disable", { capability: "sms" });
233
+ setSmsEnabled(false);
234
+ } else {
235
+ const { granted } = await SmsPermission.check();
236
+ if (!granted) {
237
+ const result = await SmsPermission.request();
238
+ if (!result.granted) return;
239
+ }
240
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
241
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
242
+ await Preferences.set({ key: "smsListenerEnabled", value: "true" });
243
+ await request("device.capability.enable", { capability: "sms", fcmToken });
244
+ setSmsEnabled(true);
245
+ }
246
+ } catch (err) {
247
+ console.error("Failed to toggle SMS access:", err);
248
+ } finally {
249
+ setTogglingSms(false);
250
+ }
251
+ }
252
+
253
+ // Sync contacts toggle with permission state — on mount and when app resumes
254
+ useEffect(() => {
255
+ if (!isNative || !ContactsPermission) return;
256
+
257
+ function syncContactsState() {
258
+ Promise.all([
259
+ ContactsPermission!.check(),
260
+ Preferences.get({ key: "contactsAccessEnabled" }),
261
+ ]).then(([{ granted }, { value: prefValue }]) => {
262
+ setContactsEnabled(granted && prefValue !== "false");
263
+ });
264
+ }
265
+
266
+ syncContactsState();
267
+
268
+ const listener = CapApp.addListener("resume", () => {
269
+ syncContactsState();
270
+ });
271
+
272
+ return () => { listener.then((h) => h.remove()); };
273
+ }, []);
274
+
275
+ async function handleContactsToggle() {
276
+ if (!ContactsPermission || !request) return;
277
+ setTogglingContacts(true);
278
+ try {
279
+ if (contactsEnabled) {
280
+ await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
281
+ await request("device.capability.disable", { capability: "contacts" });
282
+ setContactsEnabled(false);
283
+ } else {
284
+ const { granted } = await ContactsPermission.check();
285
+ if (!granted) {
286
+ const result = await ContactsPermission.request();
287
+ if (!result.granted) return;
288
+ }
289
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
290
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
291
+ await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
292
+ await request("device.capability.enable", { capability: "contacts", fcmToken });
293
+ setContactsEnabled(true);
294
+ }
295
+ } catch (err) {
296
+ console.error("Failed to toggle contacts access:", err);
297
+ } finally {
298
+ setTogglingContacts(false);
299
+ }
300
+ }
301
+
302
+ // Sync calendar toggle with permission state — on mount and when app resumes
303
+ useEffect(() => {
304
+ if (!isNative || !CalendarPermission) return;
305
+
306
+ function syncCalendarState() {
307
+ Promise.all([
308
+ CalendarPermission!.check(),
309
+ Preferences.get({ key: "calendarAccessEnabled" }),
310
+ ]).then(([{ granted }, { value: prefValue }]) => {
311
+ setCalendarEnabled(granted && prefValue !== "false");
312
+ });
313
+ }
314
+
315
+ syncCalendarState();
316
+
317
+ const listener = CapApp.addListener("resume", () => {
318
+ syncCalendarState();
319
+ });
320
+
321
+ return () => { listener.then((h) => h.remove()); };
322
+ }, []);
323
+
324
+ async function handleCalendarToggle() {
325
+ if (!CalendarPermission || !request) return;
326
+ setTogglingCalendar(true);
327
+ try {
328
+ if (calendarEnabled) {
329
+ await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
330
+ await request("device.capability.disable", { capability: "calendar" });
331
+ setCalendarEnabled(false);
332
+ } else {
333
+ const { granted } = await CalendarPermission.check();
334
+ if (!granted) {
335
+ const result = await CalendarPermission.request();
336
+ if (!result.granted) return;
337
+ }
338
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
339
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
340
+ await Preferences.set({ key: "calendarAccessEnabled", value: "true" });
341
+ await request("device.capability.enable", { capability: "calendar", fcmToken });
342
+ setCalendarEnabled(true);
343
+ }
344
+ } catch (err) {
345
+ console.error("Failed to toggle calendar access:", err);
346
+ } finally {
347
+ setTogglingCalendar(false);
348
+ }
349
+ }
350
+
351
+ // Sync DND access toggle with system state — on mount and when app resumes
352
+ useEffect(() => {
353
+ if (!isNative || !DndAccess) return;
354
+
355
+ function syncDndState() {
356
+ DndAccess!.check().then(({ enabled }) => {
357
+ setDndEnabled(enabled);
358
+ });
359
+ }
360
+
361
+ syncDndState();
362
+
363
+ const listener = CapApp.addListener("resume", () => {
364
+ syncDndState();
365
+ });
366
+
367
+ return () => { listener.then((h) => h.remove()); };
368
+ }, []);
369
+
370
+ async function handleDndToggle() {
371
+ if (!DndAccess || !request) return;
372
+ setTogglingDnd(true);
373
+ try {
374
+ if (dndEnabled) {
375
+ // DND access can only be revoked in system settings, but we unregister from host
376
+ await request("device.capability.disable", { capability: "dnd" });
377
+ setDndEnabled(false);
378
+ } else {
379
+ const { enabled: systemEnabled } = await DndAccess.check();
380
+ if (!systemEnabled) {
381
+ const result = await DndAccess.request();
382
+ if (!result.enabled) return;
383
+ }
384
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
385
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
386
+ await request("device.capability.enable", { capability: "dnd", fcmToken });
387
+ setDndEnabled(true);
388
+ }
389
+ } catch (err) {
390
+ console.error("Failed to toggle DND access:", err);
391
+ } finally {
392
+ setTogglingDnd(false);
393
+ }
394
+ }
395
+
396
+ // Sync alarm toggle — no permission needed, just device registration
397
+ useEffect(() => {
398
+ if (!isNative) return;
399
+ Preferences.get({ key: "alertAccessEnabled" }).then(({ value }) => {
400
+ setAlarmEnabled(value === "true");
401
+ });
402
+ }, []);
403
+
404
+ async function handleAlarmToggle() {
405
+ if (!request) return;
406
+ setTogglingAlarm(true);
407
+ try {
408
+ if (alarmEnabled) {
409
+ await Preferences.set({ key: "alertAccessEnabled", value: "false" });
410
+ await request("device.capability.disable", { capability: "alert" });
411
+ setAlarmEnabled(false);
412
+ } else {
413
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
414
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
415
+ await Preferences.set({ key: "alertAccessEnabled", value: "true" });
416
+ await request("device.capability.enable", { capability: "alert", fcmToken });
417
+ setAlarmEnabled(true);
418
+ }
419
+ } catch (err) {
420
+ console.error("Failed to toggle alarm access:", err);
421
+ } finally {
422
+ setTogglingAlarm(false);
423
+ }
424
+ }
425
+
426
+ // Sync battery toggle — no permission needed, just device registration
427
+ useEffect(() => {
428
+ if (!isNative) return;
429
+ Preferences.get({ key: "batteryAccessEnabled" }).then(({ value }) => {
430
+ setBatteryEnabled(value === "true");
431
+ });
432
+ }, []);
433
+
434
+ async function handleBatteryToggle() {
435
+ if (!request) return;
436
+ setTogglingBattery(true);
437
+ try {
438
+ if (batteryEnabled) {
439
+ await Preferences.set({ key: "batteryAccessEnabled", value: "false" });
440
+ await request("device.capability.disable", { capability: "battery" });
441
+ setBatteryEnabled(false);
442
+ } else {
443
+ const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
444
+ if (!fcmToken) { console.warn("No FCM token available"); return; }
445
+ await Preferences.set({ key: "batteryAccessEnabled", value: "true" });
446
+ await request("device.capability.enable", { capability: "battery", fcmToken });
447
+ setBatteryEnabled(true);
448
+ }
449
+ } catch (err) {
450
+ console.error("Failed to toggle battery access:", err);
451
+ } finally {
452
+ setTogglingBattery(false);
453
+ }
454
+ }
455
+
75
456
  async function handleLocationToggle() {
76
457
  if (!request) return;
77
458
  setTogglingLocation(true);
@@ -294,6 +675,90 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
294
675
  <span className="toggle-switch-thumb" />
295
676
  </button>
296
677
  </label>
678
+ <label className="drawer-toggle">
679
+ <span className="drawer-toggle-label">Notification Access</span>
680
+ <button
681
+ className={`toggle-switch ${notificationListenerEnabled ? "toggle-switch-on" : ""}`}
682
+ onClick={handleNotificationListenerToggle}
683
+ disabled={togglingNotificationListener}
684
+ role="switch"
685
+ aria-checked={notificationListenerEnabled}
686
+ >
687
+ <span className="toggle-switch-thumb" />
688
+ </button>
689
+ </label>
690
+ <label className="drawer-toggle">
691
+ <span className="drawer-toggle-label">SMS Access</span>
692
+ <button
693
+ className={`toggle-switch ${smsEnabled ? "toggle-switch-on" : ""}`}
694
+ onClick={handleSmsToggle}
695
+ disabled={togglingSms}
696
+ role="switch"
697
+ aria-checked={smsEnabled}
698
+ >
699
+ <span className="toggle-switch-thumb" />
700
+ </button>
701
+ </label>
702
+ <label className="drawer-toggle">
703
+ <span className="drawer-toggle-label">Contacts Access</span>
704
+ <button
705
+ className={`toggle-switch ${contactsEnabled ? "toggle-switch-on" : ""}`}
706
+ onClick={handleContactsToggle}
707
+ disabled={togglingContacts}
708
+ role="switch"
709
+ aria-checked={contactsEnabled}
710
+ >
711
+ <span className="toggle-switch-thumb" />
712
+ </button>
713
+ </label>
714
+ <label className="drawer-toggle">
715
+ <span className="drawer-toggle-label">Calendar Access</span>
716
+ <button
717
+ className={`toggle-switch ${calendarEnabled ? "toggle-switch-on" : ""}`}
718
+ onClick={handleCalendarToggle}
719
+ disabled={togglingCalendar}
720
+ role="switch"
721
+ aria-checked={calendarEnabled}
722
+ >
723
+ <span className="toggle-switch-thumb" />
724
+ </button>
725
+ </label>
726
+ <label className="drawer-toggle">
727
+ <span className="drawer-toggle-label">Do Not Disturb Control</span>
728
+ <button
729
+ className={`toggle-switch ${dndEnabled ? "toggle-switch-on" : ""}`}
730
+ onClick={handleDndToggle}
731
+ disabled={togglingDnd}
732
+ role="switch"
733
+ aria-checked={dndEnabled}
734
+ >
735
+ <span className="toggle-switch-thumb" />
736
+ </button>
737
+ </label>
738
+ <label className="drawer-toggle">
739
+ <span className="drawer-toggle-label">Alert Access</span>
740
+ <button
741
+ className={`toggle-switch ${alarmEnabled ? "toggle-switch-on" : ""}`}
742
+ onClick={handleAlarmToggle}
743
+ disabled={togglingAlarm}
744
+ role="switch"
745
+ aria-checked={alarmEnabled}
746
+ >
747
+ <span className="toggle-switch-thumb" />
748
+ </button>
749
+ </label>
750
+ <label className="drawer-toggle">
751
+ <span className="drawer-toggle-label">Battery Access</span>
752
+ <button
753
+ className={`toggle-switch ${batteryEnabled ? "toggle-switch-on" : ""}`}
754
+ onClick={handleBatteryToggle}
755
+ disabled={togglingBattery}
756
+ role="switch"
757
+ aria-checked={batteryEnabled}
758
+ >
759
+ <span className="toggle-switch-thumb" />
760
+ </button>
761
+ </label>
297
762
  </div>
298
763
  </>
299
764
  )}
@@ -1,2 +1,2 @@
1
1
  /** Bump when a breaking host change is made. */
2
- export const MIN_HOST_VERSION = "0.6.9";
2
+ export const MIN_HOST_VERSION = "0.7.3";