palmier 0.7.2 → 0.7.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.
@@ -1 +1 @@
1
- import{W as t}from"./index-DLxrL0hR.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
1
+ import{W as t}from"./index-CPIqbV9-.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
@@ -1 +1 @@
1
- import{W as p}from"./index-DLxrL0hR.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
1
+ import{W as p}from"./index-CPIqbV9-.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
@@ -8,8 +8,8 @@
8
8
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
9
  <title>Palmier</title>
10
10
  <meta name="description" content="Remote control for AI agents running on your own machine. Schedule tasks, approve permissions, and get push notifications." />
11
- <script type="module" crossorigin src="/assets/index-DLxrL0hR.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-C6Lz09EY.css">
11
+ <script type="module" crossorigin src="/assets/index-CPIqbV9-.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-B-ByUHPS.css">
13
13
  <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -1,2 +1,2 @@
1
1
  try{self["workbox:core:7.3.0"]&&_()}catch{}const N=(n,...e)=>{let t=n;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},E=N;class h extends Error{constructor(e,t){const s=E(e,t);super(s),this.name=e,this.details=t}}const f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},U=n=>[f.prefix,n,f.suffix].filter(e=>e&&e.length>0).join("-"),O=n=>{for(const e of Object.keys(f))n(e)},L={updateDetails:n=>{O(e=>{typeof n[e]=="string"&&(f[e]=n[e])})},getGoogleAnalyticsName:n=>n||U(f.googleAnalytics),getPrecacheName:n=>n||U(f.precache),getPrefix:()=>f.prefix,getRuntimeName:n=>n||U(f.runtime),getSuffix:()=>f.suffix};function v(n,e){const t=e();return n.waitUntil(t),t}try{self["workbox:precaching:7.3.0"]&&_()}catch{}const A="__WB_REVISION__";function M(n){if(!n)throw new h("add-to-cache-list-unexpected-type",{entry:n});if(typeof n=="string"){const i=new URL(n,location.href);return{cacheKey:i.href,url:i.href}}const{revision:e,url:t}=n;if(!t)throw new h("add-to-cache-list-unexpected-type",{entry:n});if(!e){const i=new URL(t,location.href);return{cacheKey:i.href,url:i.href}}const s=new URL(t,location.href),a=new URL(t,location.href);return s.searchParams.set(A,e),{cacheKey:s.href,url:a.href}}class W{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const a=t.originalRequest.url;s?this.notUpdatedURLs.push(a):this.updatedURLs.push(a)}return s}}}class q{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const a=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return a?new Request(a,{headers:t.headers}):t},this._precacheController=e}}let w;function S(){if(w===void 0){const n=new Response("");if("body"in n)try{new Response(n.body),w=!0}catch{w=!1}w=!1}return w}async function j(n,e){let t=null;if(n.url&&(t=new URL(n.url).origin),t!==self.location.origin)throw new h("cross-origin-copy-response",{origin:t});const s=n.clone(),i={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},r=S()?s.body:await s.blob();return new Response(r,i)}const D=n=>new URL(String(n),location.href).href.replace(new RegExp(`^${location.origin}`),"");function T(n,e){const t=new URL(n);for(const s of e)t.searchParams.delete(s);return t.href}async function H(n,e,t,s){const a=T(e.url,t);if(e.url===a)return n.match(e,s);const i=Object.assign(Object.assign({},s),{ignoreSearch:!0}),r=await n.keys(e,i);for(const c of r){const o=T(c.url,t);if(a===o)return n.match(c,s)}}class F{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}const B=new Set;async function $(){for(const n of B)await n()}function V(n){return new Promise(e=>setTimeout(e,n))}try{self["workbox:strategies:7.3.0"]&&_()}catch{}function C(n){return typeof n=="string"?new Request(n):n}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new F,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=C(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const r=await t.preloadResponse;if(r)return r}const a=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const r of this.iterateCallbacks("requestWillFetch"))s=await r({request:s.clone(),event:t})}catch(r){if(r instanceof Error)throw new h("plugin-error-request-will-fetch",{thrownErrorMessage:r.message})}const i=s.clone();try{let r;r=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const c of this.iterateCallbacks("fetchDidSucceed"))r=await c({event:t,request:i,response:r});return r}catch(r){throw a&&await this.runCallbacks("fetchDidFail",{error:r,event:t,originalRequest:a.clone(),request:i.clone()}),r}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=C(e);let s;const{cacheName:a,matchOptions:i}=this._strategy,r=await this.getCacheKey(t,"read"),c=Object.assign(Object.assign({},i),{cacheName:a});s=await caches.match(r,c);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:a,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(e,t){const s=C(e);await V(0);const a=await this.getCacheKey(s,"write");if(!t)throw new h("cache-put-with-no-response",{url:D(a.url)});const i=await this._ensureResponseSafeToCache(t);if(!i)return!1;const{cacheName:r,matchOptions:c}=this._strategy,o=await self.caches.open(r),l=this.hasCallback("cacheDidUpdate"),d=l?await H(o,a.clone(),["__WB_REVISION__"],c):null;try{await o.put(a,l?i.clone():i)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await $(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:r,oldResponse:d,newResponse:i.clone(),request:a,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let a=e;for(const i of this.iterateCallbacks("cacheKeyWillBeUsed"))a=C(await i({mode:t,request:a,event:this.event,params:this.params}));this._cacheKeys[s]=a}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield i=>{const r=Object.assign(Object.assign({},i),{state:s});return t[e](r)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(a=>a.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const a of this.iterateCallbacks("cacheWillUpdate"))if(t=await a({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class J{constructor(e={}){this.cacheName=L.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,a="params"in e?e.params:void 0,i=new G(this,{event:t,request:s,params:a}),r=this._getResponse(i,s,t),c=this._awaitComplete(r,i,s,t);return[r,c]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let a;try{if(a=await this._handle(t,e),!a||a.type==="error")throw new h("no-response",{url:t.url})}catch(i){if(i instanceof Error){for(const r of e.iterateCallbacks("handlerDidError"))if(a=await r({error:i,event:s,request:t}),a)break}if(!a)throw i}for(const i of e.iterateCallbacks("handlerWillRespond"))a=await i({event:s,request:t,response:a});return a}async _awaitComplete(e,t,s,a){let i,r;try{i=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:a,request:s,response:i}),await t.doneWaiting()}catch(c){c instanceof Error&&(r=c)}if(await t.runCallbacks("handlerDidComplete",{event:a,request:s,response:i,error:r}),t.destroy(),r)throw r}}class p extends J{constructor(e={}){e.cacheName=L.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(p.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const a=t.params||{};if(this._fallbackToNetwork){const i=a.integrity,r=e.integrity,c=!r||r===i;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?r||i:void 0})),i&&c&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new h("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new h("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,a]of this.plugins.entries())a!==p.copyRedirectedCacheableResponsesPlugin&&(a===p.defaultPrecacheCacheabilityPlugin&&(e=s),a.cacheWillUpdate&&t++);t===0?this.plugins.push(p.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}p.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:n}){return!n||n.status>=400?null:n}};p.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:n}){return n.redirected?await j(n):n}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new p({cacheName:L.getPrecacheName(e),plugins:[...t,new q({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:a,url:i}=M(s),r=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==a)throw new h("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:a});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(a)&&this._cacheKeysToIntegrities.get(a)!==s.integrity)throw new h("add-to-cache-list-conflicting-integrities",{url:i});this._cacheKeysToIntegrities.set(a,s.integrity)}if(this._urlsToCacheKeys.set(i,a),this._urlsToCacheModes.set(i,r),t.length>0){const c=`Workbox is precaching URLs without revision info: ${t.join(", ")}
2
- This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(c)}}}install(e){return v(e,async()=>{const t=new W;this.strategy.plugins.push(t);for(const[i,r]of this._urlsToCacheKeys){const c=this._cacheKeysToIntegrities.get(r),o=this._urlsToCacheModes.get(i),l=new Request(i,{integrity:c,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:r},request:l,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return v(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),a=new Set(this._urlsToCacheKeys.values()),i=[];for(const r of s)a.has(r.url)||(await t.delete(r),i.push(r.url));return{deletedURLs:i}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new h("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k;const x=()=>(k||(k=new Q),k);try{self["workbox:routing:7.3.0"]&&_()}catch{}const I="GET",b=n=>n&&typeof n=="object"?n:{handle:n};class R{constructor(e,t,s=I){this.handler=b(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=b(e)}}class z extends R{constructor(e,t,s){const a=({url:i})=>{const r=e.exec(i.href);if(r&&!(i.origin!==location.origin&&r.index!==0))return r.slice(1)};super(a,t,s)}}class X{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(a=>{typeof a=="string"&&(a=[a]);const i=new Request(...a);return this.handleRequest({request:i,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const a=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let c=r&&r.handler;const o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;let l;try{l=c.handle({url:s,request:e,event:t,params:i})}catch(u){l=Promise.reject(u)}const d=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||d)&&(l=l.catch(async u=>{if(d)try{return await d.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(u=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),l}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const i=this._routes.get(s.method)||[];for(const r of i){let c;const o=r.match({url:e,sameOrigin:t,request:s,event:a});if(o)return c=o,(Array.isArray(c)&&c.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(c=void 0),{route:r,params:c}}return{}}setDefaultHandler(e,t=I){this._defaultHandlerMap.set(t,b(e))}setCatchHandler(e){this._catchHandler=b(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new h("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new h("unregister-route-route-not-registered")}}let m;const Y=()=>(m||(m=new X,m.addFetchListener(),m.addCacheListener()),m);function Z(n,e,t){let s;if(typeof n=="string"){const i=new URL(n,location.href),r=({url:c})=>c.href===i.href;s=new R(r,e,t)}else if(n instanceof RegExp)s=new z(n,e,t);else if(typeof n=="function")s=new R(n,e,t);else if(n instanceof R)s=n;else throw new h("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return Y().registerRoute(s),s}function ee(n,e=[]){for(const t of[...n.searchParams.keys()])e.some(s=>s.test(t))&&n.searchParams.delete(t);return n}function*te(n,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:a}={}){const i=new URL(n,location.href);i.hash="",yield i.href;const r=ee(i,e);if(yield r.href,t&&r.pathname.endsWith("/")){const c=new URL(r.href);c.pathname+=t,yield c.href}if(s){const c=new URL(r.href);c.pathname+=".html",yield c.href}if(a){const c=a({url:i});for(const o of c)yield o.href}}class se extends R{constructor(e,t){const s=({request:a})=>{const i=e.getURLsToCacheKeys();for(const r of te(a.url,t)){const c=i.get(r);if(c){const o=e.getIntegrityForCacheKey(c);return{cacheKey:c,integrity:o}}}};super(s,e.strategy)}}function ne(n){const e=x(),t=new se(e,n);Z(t)}function ae(n){x().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"f65782768cfd2a3d2392e43cf922dde4","url":"index.html"},{"revision":null,"url":"assets/web-HDs03L2B.js"},{"revision":null,"url":"assets/web-CBI458eN.js"},{"revision":null,"url":"assets/index-DLxrL0hR.js"},{"revision":null,"url":"assets/index-C6Lz09EY.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"9f51698004b9cc4d787c75695b74de9d","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,d=c.task_id;n.waitUntil(self.registration.getNotifications().then(u=>{var g,P,K;for(const y of u)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}d&&((K=y.data)==null?void 0:K.task_id)===d&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.task_id,i=t.run_id,r=a&&i?`/runs/${encodeURIComponent(a)}/${encodeURIComponent(i)}`:a?`/runs/${encodeURIComponent(a)}/latest`:"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(c=>{for(const o of c)if(o.url.includes(self.location.origin)&&"focus"in o)return o.navigate(r),o.focus();return self.clients.openWindow(r)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
2
+ This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(c)}}}install(e){return v(e,async()=>{const t=new W;this.strategy.plugins.push(t);for(const[i,r]of this._urlsToCacheKeys){const c=this._cacheKeysToIntegrities.get(r),o=this._urlsToCacheModes.get(i),l=new Request(i,{integrity:c,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:r},request:l,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return v(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),a=new Set(this._urlsToCacheKeys.values()),i=[];for(const r of s)a.has(r.url)||(await t.delete(r),i.push(r.url));return{deletedURLs:i}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new h("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k;const x=()=>(k||(k=new Q),k);try{self["workbox:routing:7.3.0"]&&_()}catch{}const I="GET",b=n=>n&&typeof n=="object"?n:{handle:n};class R{constructor(e,t,s=I){this.handler=b(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=b(e)}}class z extends R{constructor(e,t,s){const a=({url:i})=>{const r=e.exec(i.href);if(r&&!(i.origin!==location.origin&&r.index!==0))return r.slice(1)};super(a,t,s)}}class X{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(a=>{typeof a=="string"&&(a=[a]);const i=new Request(...a);return this.handleRequest({request:i,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const a=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let c=r&&r.handler;const o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;let l;try{l=c.handle({url:s,request:e,event:t,params:i})}catch(u){l=Promise.reject(u)}const d=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||d)&&(l=l.catch(async u=>{if(d)try{return await d.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(u=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),l}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const i=this._routes.get(s.method)||[];for(const r of i){let c;const o=r.match({url:e,sameOrigin:t,request:s,event:a});if(o)return c=o,(Array.isArray(c)&&c.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(c=void 0),{route:r,params:c}}return{}}setDefaultHandler(e,t=I){this._defaultHandlerMap.set(t,b(e))}setCatchHandler(e){this._catchHandler=b(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new h("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new h("unregister-route-route-not-registered")}}let m;const Y=()=>(m||(m=new X,m.addFetchListener(),m.addCacheListener()),m);function Z(n,e,t){let s;if(typeof n=="string"){const i=new URL(n,location.href),r=({url:c})=>c.href===i.href;s=new R(r,e,t)}else if(n instanceof RegExp)s=new z(n,e,t);else if(typeof n=="function")s=new R(n,e,t);else if(n instanceof R)s=n;else throw new h("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return Y().registerRoute(s),s}function ee(n,e=[]){for(const t of[...n.searchParams.keys()])e.some(s=>s.test(t))&&n.searchParams.delete(t);return n}function*te(n,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:a}={}){const i=new URL(n,location.href);i.hash="",yield i.href;const r=ee(i,e);if(yield r.href,t&&r.pathname.endsWith("/")){const c=new URL(r.href);c.pathname+=t,yield c.href}if(s){const c=new URL(r.href);c.pathname+=".html",yield c.href}if(a){const c=a({url:i});for(const o of c)yield o.href}}class se extends R{constructor(e,t){const s=({request:a})=>{const i=e.getURLsToCacheKeys();for(const r of te(a.url,t)){const c=i.get(r);if(c){const o=e.getIntegrityForCacheKey(c);return{cacheKey:c,integrity:o}}}};super(s,e.strategy)}}function ne(n){const e=x(),t=new se(e,n);Z(t)}function ae(n){x().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"1afa20efa6a5d1f89603d54bd42459e9","url":"index.html"},{"revision":null,"url":"assets/web-SlBB3mP3.js"},{"revision":null,"url":"assets/web-Dwi8DLNK.js"},{"revision":null,"url":"assets/index-CPIqbV9-.js"},{"revision":null,"url":"assets/index-B-ByUHPS.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"9f51698004b9cc4d787c75695b74de9d","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,d=c.task_id;n.waitUntil(self.registration.getNotifications().then(u=>{var g,P,K;for(const y of u)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}d&&((K=y.data)==null?void 0:K.task_id)===d&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.task_id,i=t.run_id,r=a&&i?`/runs/${encodeURIComponent(a)}/${encodeURIComponent(i)}`:a?`/runs/${encodeURIComponent(a)}/latest`:"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(c=>{for(const o of c)if(o.url.includes(self.location.origin)&&"focus"in o)return o.navigate(r),o.focus();return self.clients.openWindow(r)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
@@ -0,0 +1,11 @@
1
+ export interface SmsMessage {
2
+ id: string;
3
+ sender: string;
4
+ body: string;
5
+ timestamp: number;
6
+ receivedAt: number;
7
+ }
8
+ export declare function addSmsMessage(m: SmsMessage): void;
9
+ export declare function getSmsMessages(): SmsMessage[];
10
+ export declare function onSmsChanged(cb: () => void): () => void;
11
+ //# sourceMappingURL=sms-store.d.ts.map
@@ -0,0 +1,19 @@
1
+ const MAX_MESSAGES = 50;
2
+ const messages = [];
3
+ const listeners = new Set();
4
+ export function addSmsMessage(m) {
5
+ messages.push(m);
6
+ if (messages.length > MAX_MESSAGES) {
7
+ messages.shift();
8
+ }
9
+ for (const cb of listeners)
10
+ cb();
11
+ }
12
+ export function getSmsMessages() {
13
+ return [...messages];
14
+ }
15
+ export function onSmsChanged(cb) {
16
+ listeners.add(cb);
17
+ return () => { listeners.delete(cb); };
18
+ }
19
+ //# sourceMappingURL=sms-store.js.map
@@ -9,6 +9,7 @@ import { agentToolMap, agentResources, ToolError } from "../mcp-tools.js";
9
9
  import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
10
10
  import { getTaskDir } from "../task.js";
11
11
  import { onNotificationsChanged } from "../notification-store.js";
12
+ import { onSmsChanged } from "../sms-store.js";
12
13
  const assetCache = new Map();
13
14
  const PWA_DIR = path.join(import.meta.dirname, "..", "pwa");
14
15
  const CONTENT_TYPES = {
@@ -95,6 +96,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
95
96
  }
96
97
  // Wire up resource change listeners
97
98
  onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
99
+ onSmsChanged(() => broadcastResourceUpdated("sms://device"));
98
100
  // If a pairing code is provided, pre-register it
99
101
  if (pairingCode) {
100
102
  const EXPIRY_MS = 24 * 60 * 60 * 1000;
@@ -231,7 +233,20 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
231
233
  sendJson(res, 403, { error: "localhost only" });
232
234
  return;
233
235
  }
234
- sendJson(res, 200, matchedResource.read());
236
+ const taskId = url.searchParams.get("taskId");
237
+ if (!taskId) {
238
+ sendJson(res, 400, { error: "taskId query parameter is required" });
239
+ return;
240
+ }
241
+ const taskDir = getTaskDir(config.projectRoot, taskId);
242
+ if (!fs.existsSync(taskDir)) {
243
+ sendJson(res, 404, { error: `Task not found: ${taskId}` });
244
+ return;
245
+ }
246
+ console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${matchedResource.name}`);
247
+ const result = matchedResource.read();
248
+ console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${matchedResource.name} done: ${JSON.stringify(result).slice(0, 200)}`);
249
+ sendJson(res, 200, result);
235
250
  return;
236
251
  }
237
252
  // ── Localhost-only endpoints (no auth) ─────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -28,8 +28,8 @@ Palmier is a platform for remotely scheduling, managing, and executing autonomou
28
28
  - **PWA** -- React 19 + Vite progressive web app. Connects to NATS over WebSocket for real-time task updates and to the web server for host registration and push notifications. No user accounts — paired hosts are stored in localStorage.
29
29
  - **Web Server** -- Express + TypeScript API server. Handles host registration, push notifications (subscribes to `host-event.>` pub/sub for confirmation and completion events), and push notification relay (for host CLI requests via NATS). In production, also serves the built PWA static files.
30
30
  - **NATS Server** -- Message broker. Provides pub/sub messaging and request-reply for real-time communication between all components.
31
- - **Host** -- Runs on remote Linux/Windows machines to execute tasks via pluggable agent tools (e.g., Claude Code, Codex, Gemini). Each agent implements an `AgentTool` interface that handles command construction. Communicates with the platform over NATS and exposes a local MCP server (`/mcp`, streamable HTTP) and auto-generated REST endpoints for agent-facing tools (`/notify`, `/request-input`, `/request-confirmation`, `/device-geolocation`). See the [palmier](https://github.com/caihongxu/palmier) repo.
32
- - **Android App** -- Native Android wrapper (Capacitor) for the PWA. Provides FCM push messaging and background GPS access. The server sends FCM data messages to wake the device for geolocation requests. See the [palmier-android](https://github.com/caihongxu/palmier-android) repo.
31
+ - **Host** -- Runs on remote Linux/Windows machines to execute tasks via pluggable agent tools (e.g., Claude Code, Codex, Gemini). Each agent implements an `AgentTool` interface that handles command construction. Communicates with the platform over NATS and exposes a local MCP server (`/mcp`, streamable HTTP) with auto-generated REST endpoints for tools and resources. See the [palmier](https://github.com/caihongxu/palmier) repo.
32
+ - **Android App** -- Native Android wrapper (Capacitor) for the PWA. Provides FCM push messaging and native device capabilities — GPS, notifications, SMS, contacts, calendar, alarms, battery, and ringer control. All capabilities work in the background via FCM data messages. See the [palmier-android](https://github.com/caihongxu/palmier-android) repo.
33
33
 
34
34
  ## Prerequisites
35
35
 
@@ -164,6 +164,14 @@ All endpoints are prefixed with `/api`. No user authentication is required.
164
164
  | `POST` | `/api/push/respond` | Respond to a pending task confirmation via push notification |
165
165
  | `POST` | `/api/fcm/register` | Register an FCM token for a host (Android device) |
166
166
  | `POST` | `/api/fcm/geolocation-response` | Receive device location from Android, forward via NATS |
167
+ | `POST` | `/api/device/notifications` | Relay device notification from Android to host via NATS |
168
+ | `POST` | `/api/device/sms` | Relay incoming SMS from Android to host via NATS |
169
+ | `POST` | `/api/device/contacts-response` | Relay contacts response from Android to host via NATS |
170
+ | `POST` | `/api/device/calendar-response` | Relay calendar response from Android to host via NATS |
171
+ | `POST` | `/api/device/sms-response` | Relay SMS send response from Android to host via NATS |
172
+ | `POST` | `/api/device/alarm-response` | Relay alarm response from Android to host via NATS |
173
+ | `POST` | `/api/device/battery-response` | Relay battery response from Android to host via NATS |
174
+ | `POST` | `/api/device/ringer-response` | Relay ringer mode response from Android to host via NATS |
167
175
  | `GET` | `/health` | Health check |
168
176
 
169
177
 
@@ -189,4 +197,4 @@ All endpoints are prefixed with `/api`. No user authentication is required.
189
197
  ## Related Repositories
190
198
 
191
199
  - [palmier](https://github.com/caihongxu/palmier) -- The host binary, published as `palmier` on npm. Install with `npm install -g palmier`. Uses npm (not pnpm).
192
- - [palmier-android](https://github.com/caihongxu/palmier-android) -- Native Android wrapper (Capacitor) for the PWA. Provides FCM and background GPS.
200
+ - [palmier-android](https://github.com/caihongxu/palmier-android) -- Native Android wrapper (Capacitor) for the PWA. Provides FCM and native device capabilities (GPS, notifications, SMS, contacts, calendar, alarms, battery, ringer).
@@ -1685,6 +1685,9 @@ body {
1685
1685
 
1686
1686
  .drawer-section {
1687
1687
  padding: var(--space-md);
1688
+ display: flex;
1689
+ flex-direction: column;
1690
+ gap: var(--space-sm);
1688
1691
  }
1689
1692
 
1690
1693
  .drawer-section-label {
@@ -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,16 @@ 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);
47
122
 
48
123
  const locationEnabled = !!(activeClientToken && locationClientToken === activeClientToken);
49
124
 
@@ -72,6 +147,222 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
72
147
  return () => { listener.then((h) => h.remove()); };
73
148
  }, [locationEnabled, activeClientToken]);
74
149
 
150
+ // Sync notification listener toggle with system state — on mount and when app resumes
151
+ useEffect(() => {
152
+ if (!isNative || !NotificationListener) return;
153
+
154
+ function syncNotificationListenerState() {
155
+ Promise.all([
156
+ NotificationListener!.check(),
157
+ Preferences.get({ key: "notificationListenerEnabled" }),
158
+ ]).then(([{ enabled: systemEnabled }, { value: prefValue }]) => {
159
+ // Enabled only if both system permission is granted AND user toggled on
160
+ setNotificationListenerEnabled(systemEnabled && prefValue !== "false");
161
+ });
162
+ }
163
+
164
+ syncNotificationListenerState();
165
+
166
+ const listener = CapApp.addListener("resume", () => {
167
+ syncNotificationListenerState();
168
+ });
169
+
170
+ return () => { listener.then((h) => h.remove()); };
171
+ }, []);
172
+
173
+ async function handleNotificationListenerToggle() {
174
+ if (!NotificationListener) return;
175
+ setTogglingNotificationListener(true);
176
+ try {
177
+ if (notificationListenerEnabled) {
178
+ // Toggling off — save preference, service checks this before relaying
179
+ await Preferences.set({ key: "notificationListenerEnabled", value: "false" });
180
+ setNotificationListenerEnabled(false);
181
+ } else {
182
+ // Toggling on — check system permission first, open settings if needed
183
+ const { enabled: systemEnabled } = await NotificationListener.check();
184
+ if (!systemEnabled) {
185
+ const result = await NotificationListener.request();
186
+ if (!result.enabled) return; // User didn't grant access
187
+ }
188
+ await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
189
+ setNotificationListenerEnabled(true);
190
+ }
191
+ } catch (err) {
192
+ console.error("Failed to toggle notification listener:", err);
193
+ } finally {
194
+ setTogglingNotificationListener(false);
195
+ }
196
+ }
197
+
198
+ // Sync SMS toggle with permission state — on mount and when app resumes
199
+ useEffect(() => {
200
+ if (!isNative || !SmsPermission) return;
201
+
202
+ function syncSmsState() {
203
+ Promise.all([
204
+ SmsPermission!.check(),
205
+ Preferences.get({ key: "smsListenerEnabled" }),
206
+ ]).then(([{ granted }, { value: prefValue }]) => {
207
+ setSmsEnabled(granted && prefValue !== "false");
208
+ });
209
+ }
210
+
211
+ syncSmsState();
212
+
213
+ const listener = CapApp.addListener("resume", () => {
214
+ syncSmsState();
215
+ });
216
+
217
+ return () => { listener.then((h) => h.remove()); };
218
+ }, []);
219
+
220
+ async function handleSmsToggle() {
221
+ if (!SmsPermission) return;
222
+ setTogglingSms(true);
223
+ try {
224
+ if (smsEnabled) {
225
+ await Preferences.set({ key: "smsListenerEnabled", value: "false" });
226
+ setSmsEnabled(false);
227
+ } else {
228
+ const { granted } = await SmsPermission.check();
229
+ if (!granted) {
230
+ const result = await SmsPermission.request();
231
+ if (!result.granted) return;
232
+ }
233
+ await Preferences.set({ key: "smsListenerEnabled", value: "true" });
234
+ setSmsEnabled(true);
235
+ }
236
+ } catch (err) {
237
+ console.error("Failed to toggle SMS access:", err);
238
+ } finally {
239
+ setTogglingSms(false);
240
+ }
241
+ }
242
+
243
+ // Sync contacts toggle with permission state — on mount and when app resumes
244
+ useEffect(() => {
245
+ if (!isNative || !ContactsPermission) return;
246
+
247
+ function syncContactsState() {
248
+ Promise.all([
249
+ ContactsPermission!.check(),
250
+ Preferences.get({ key: "contactsAccessEnabled" }),
251
+ ]).then(([{ granted }, { value: prefValue }]) => {
252
+ setContactsEnabled(granted && prefValue !== "false");
253
+ });
254
+ }
255
+
256
+ syncContactsState();
257
+
258
+ const listener = CapApp.addListener("resume", () => {
259
+ syncContactsState();
260
+ });
261
+
262
+ return () => { listener.then((h) => h.remove()); };
263
+ }, []);
264
+
265
+ async function handleContactsToggle() {
266
+ if (!ContactsPermission) return;
267
+ setTogglingContacts(true);
268
+ try {
269
+ if (contactsEnabled) {
270
+ await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
271
+ setContactsEnabled(false);
272
+ } else {
273
+ const { granted } = await ContactsPermission.check();
274
+ if (!granted) {
275
+ const result = await ContactsPermission.request();
276
+ if (!result.granted) return;
277
+ }
278
+ await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
279
+ setContactsEnabled(true);
280
+ }
281
+ } catch (err) {
282
+ console.error("Failed to toggle contacts access:", err);
283
+ } finally {
284
+ setTogglingContacts(false);
285
+ }
286
+ }
287
+
288
+ // Sync calendar toggle with permission state — on mount and when app resumes
289
+ useEffect(() => {
290
+ if (!isNative || !CalendarPermission) return;
291
+
292
+ function syncCalendarState() {
293
+ Promise.all([
294
+ CalendarPermission!.check(),
295
+ Preferences.get({ key: "calendarAccessEnabled" }),
296
+ ]).then(([{ granted }, { value: prefValue }]) => {
297
+ setCalendarEnabled(granted && prefValue !== "false");
298
+ });
299
+ }
300
+
301
+ syncCalendarState();
302
+
303
+ const listener = CapApp.addListener("resume", () => {
304
+ syncCalendarState();
305
+ });
306
+
307
+ return () => { listener.then((h) => h.remove()); };
308
+ }, []);
309
+
310
+ async function handleCalendarToggle() {
311
+ if (!CalendarPermission) return;
312
+ setTogglingCalendar(true);
313
+ try {
314
+ if (calendarEnabled) {
315
+ await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
316
+ setCalendarEnabled(false);
317
+ } else {
318
+ const { granted } = await CalendarPermission.check();
319
+ if (!granted) {
320
+ const result = await CalendarPermission.request();
321
+ if (!result.granted) return;
322
+ }
323
+ await Preferences.set({ key: "calendarAccessEnabled", value: "true" });
324
+ setCalendarEnabled(true);
325
+ }
326
+ } catch (err) {
327
+ console.error("Failed to toggle calendar access:", err);
328
+ } finally {
329
+ setTogglingCalendar(false);
330
+ }
331
+ }
332
+
333
+ // Sync DND access toggle with system state — on mount and when app resumes
334
+ useEffect(() => {
335
+ if (!isNative || !DndAccess) return;
336
+
337
+ function syncDndState() {
338
+ DndAccess!.check().then(({ enabled }) => {
339
+ setDndEnabled(enabled);
340
+ });
341
+ }
342
+
343
+ syncDndState();
344
+
345
+ const listener = CapApp.addListener("resume", () => {
346
+ syncDndState();
347
+ });
348
+
349
+ return () => { listener.then((h) => h.remove()); };
350
+ }, []);
351
+
352
+ async function handleDndToggle() {
353
+ if (!DndAccess) return;
354
+ setTogglingDnd(true);
355
+ try {
356
+ // DND access can only be toggled in system settings
357
+ const result = await DndAccess.request();
358
+ setDndEnabled(result.enabled);
359
+ } catch (err) {
360
+ console.error("Failed to toggle DND access:", err);
361
+ } finally {
362
+ setTogglingDnd(false);
363
+ }
364
+ }
365
+
75
366
  async function handleLocationToggle() {
76
367
  if (!request) return;
77
368
  setTogglingLocation(true);
@@ -294,6 +585,66 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
294
585
  <span className="toggle-switch-thumb" />
295
586
  </button>
296
587
  </label>
588
+ <label className="drawer-toggle">
589
+ <span className="drawer-toggle-label">Notification Access</span>
590
+ <button
591
+ className={`toggle-switch ${notificationListenerEnabled ? "toggle-switch-on" : ""}`}
592
+ onClick={handleNotificationListenerToggle}
593
+ disabled={togglingNotificationListener}
594
+ role="switch"
595
+ aria-checked={notificationListenerEnabled}
596
+ >
597
+ <span className="toggle-switch-thumb" />
598
+ </button>
599
+ </label>
600
+ <label className="drawer-toggle">
601
+ <span className="drawer-toggle-label">SMS Access</span>
602
+ <button
603
+ className={`toggle-switch ${smsEnabled ? "toggle-switch-on" : ""}`}
604
+ onClick={handleSmsToggle}
605
+ disabled={togglingSms}
606
+ role="switch"
607
+ aria-checked={smsEnabled}
608
+ >
609
+ <span className="toggle-switch-thumb" />
610
+ </button>
611
+ </label>
612
+ <label className="drawer-toggle">
613
+ <span className="drawer-toggle-label">Contacts Access</span>
614
+ <button
615
+ className={`toggle-switch ${contactsEnabled ? "toggle-switch-on" : ""}`}
616
+ onClick={handleContactsToggle}
617
+ disabled={togglingContacts}
618
+ role="switch"
619
+ aria-checked={contactsEnabled}
620
+ >
621
+ <span className="toggle-switch-thumb" />
622
+ </button>
623
+ </label>
624
+ <label className="drawer-toggle">
625
+ <span className="drawer-toggle-label">Calendar Access</span>
626
+ <button
627
+ className={`toggle-switch ${calendarEnabled ? "toggle-switch-on" : ""}`}
628
+ onClick={handleCalendarToggle}
629
+ disabled={togglingCalendar}
630
+ role="switch"
631
+ aria-checked={calendarEnabled}
632
+ >
633
+ <span className="toggle-switch-thumb" />
634
+ </button>
635
+ </label>
636
+ <label className="drawer-toggle">
637
+ <span className="drawer-toggle-label">Do Not Disturb Control</span>
638
+ <button
639
+ className={`toggle-switch ${dndEnabled ? "toggle-switch-on" : ""}`}
640
+ onClick={handleDndToggle}
641
+ disabled={togglingDnd}
642
+ role="switch"
643
+ aria-checked={dndEnabled}
644
+ >
645
+ <span className="toggle-switch-thumb" />
646
+ </button>
647
+ </label>
297
648
  </div>
298
649
  </>
299
650
  )}
@@ -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.2";