palmier 0.7.4 → 0.7.6

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 p}from"./index-BirmfPUC.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-uSwkmHBs.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 +1 @@
1
- import{W as t}from"./index-BirmfPUC.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-uSwkmHBs.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};
@@ -8,7 +8,7 @@
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-BirmfPUC.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-uSwkmHBs.js"></script>
12
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>
@@ -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":"1d8c320b1a6217a07e14dc08358f068d","url":"index.html"},{"revision":null,"url":"assets/web-_b3Dvcvz.js"},{"revision":null,"url":"assets/web-Dc9-IiRD.js"},{"revision":null,"url":"assets/index-BirmfPUC.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())});
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":"478ef5ebdaf80fed88fcc601cd2ae175","url":"index.html"},{"revision":null,"url":"assets/web-DnuoxUd4.js"},{"revision":null,"url":"assets/web-7raT3zOZ.js"},{"revision":null,"url":"assets/index-uSwkmHBs.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())});
@@ -143,13 +143,17 @@ export function createRpcHandler(config, nc) {
143
143
  switch (request.method) {
144
144
  case "task.list": {
145
145
  const tasks = listTasks(config.projectRoot);
146
- const locDevice = getCapabilityDevice("location");
146
+ const capabilities = {};
147
+ for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd"]) {
148
+ capabilities[cap] = getCapabilityDevice(cap)?.clientToken ?? null;
149
+ }
147
150
  return {
148
151
  tasks: tasks.map((task) => flattenTask(task)),
149
152
  agents: config.agents ?? [],
150
153
  version: currentVersion,
151
154
  host_platform: process.platform,
152
- location_client_token: locDevice?.clientToken ?? null,
155
+ location_client_token: capabilities.location,
156
+ capability_tokens: capabilities,
153
157
  };
154
158
  }
155
159
  case "task.get": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -92,13 +92,13 @@ const isNative = Capacitor.isNativePlatform();
92
92
 
93
93
  interface HostMenuProps {
94
94
  daemonVersion?: string | null;
95
- locationClientToken?: string | null;
95
+ capabilityTokens?: Record<string, string | null>;
96
96
  activeClientToken?: string | null;
97
97
  request?<T = unknown>(method: string, params?: unknown): Promise<T>;
98
- onLocationClientTokenChange?(token: string | null): void;
98
+ onCapabilityTokensChange?(tokens: Record<string, string | null>): void;
99
99
  }
100
100
 
101
- export default function HostMenu({ daemonVersion, locationClientToken, activeClientToken, request, onLocationClientTokenChange }: HostMenuProps) {
101
+ export default function HostMenu({ daemonVersion, capabilityTokens, activeClientToken, request, onCapabilityTokensChange }: HostMenuProps) {
102
102
  const { pairedHosts, activeHostId, setActiveHostId, removePairedHost, renamePairedHost } = useHostStore();
103
103
  const navigate = useNavigate();
104
104
  const isDesktop = useMediaQuery("(min-width: 768px)");
@@ -109,22 +109,34 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
109
109
  const [renameValue, setRenameValue] = useState("");
110
110
  const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
111
111
  const [togglingLocation, setTogglingLocation] = useState(false);
112
- const [notificationListenerEnabled, setNotificationListenerEnabled] = useState(false);
113
112
  const [togglingNotificationListener, setTogglingNotificationListener] = useState(false);
114
- const [smsEnabled, setSmsEnabled] = useState(false);
115
113
  const [togglingSms, setTogglingSms] = useState(false);
116
- const [contactsEnabled, setContactsEnabled] = useState(false);
117
114
  const [togglingContacts, setTogglingContacts] = useState(false);
118
- const [calendarEnabled, setCalendarEnabled] = useState(false);
119
115
  const [togglingCalendar, setTogglingCalendar] = useState(false);
120
- const [dndEnabled, setDndEnabled] = useState(false);
121
116
  const [togglingDnd, setTogglingDnd] = useState(false);
122
- const [alarmEnabled, setAlarmEnabled] = useState(false);
123
117
  const [togglingAlarm, setTogglingAlarm] = useState(false);
124
- const [batteryEnabled, setBatteryEnabled] = useState(false);
125
118
  const [togglingBattery, setTogglingBattery] = useState(false);
126
119
 
127
- const locationEnabled = !!(activeClientToken && locationClientToken === activeClientToken);
120
+ // Capability enabled = this device's client token matches the registered device for that capability
121
+ function isCapEnabled(cap: string): boolean {
122
+ return !!(activeClientToken && capabilityTokens?.[cap] === activeClientToken);
123
+ }
124
+ const locationEnabled = isCapEnabled("location");
125
+ const notificationListenerEnabled = isCapEnabled("notifications");
126
+ const smsEnabled = isCapEnabled("sms");
127
+ const contactsEnabled = isCapEnabled("contacts");
128
+ const calendarEnabled = isCapEnabled("calendar");
129
+ const dndEnabled = isCapEnabled("dnd");
130
+ const alarmEnabled = isCapEnabled("alert");
131
+ const batteryEnabled = isCapEnabled("battery");
132
+
133
+ /** Update local capability tokens state after a toggle change */
134
+ function setCapEnabled(cap: string, enabled: boolean) {
135
+ const updated: Record<string, string | null> = {};
136
+ for (const [k, v] of Object.entries(capabilityTokens ?? {})) updated[k] = v ?? null;
137
+ updated[cap] = enabled ? (activeClientToken ?? null) : null;
138
+ onCapabilityTokensChange?.(updated);
139
+ }
128
140
 
129
141
  // Sync location toggle with permission state — on mount and when app resumes from background
130
142
  useEffect(() => {
@@ -136,7 +148,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
136
148
  if (!fine) {
137
149
  // Permission revoked — disable on host
138
150
  request!("device.location.disable").then(() => {
139
- onLocationClientTokenChange?.(null);
151
+ setCapEnabled("location", false);
140
152
  }).catch(() => {});
141
153
  }
142
154
  });
@@ -151,29 +163,6 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
151
163
  return () => { listener.then((h) => h.remove()); };
152
164
  }, [locationEnabled, activeClientToken]);
153
165
 
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
166
  async function handleNotificationListenerToggle() {
178
167
  if (!NotificationListener || !request) return;
179
168
  setTogglingNotificationListener(true);
@@ -181,7 +170,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
181
170
  if (notificationListenerEnabled) {
182
171
  await Preferences.set({ key: "notificationListenerEnabled", value: "false" });
183
172
  await request("device.capability.disable", { capability: "notifications" });
184
- setNotificationListenerEnabled(false);
173
+ setCapEnabled("notifications", false);
185
174
  } else {
186
175
  const { enabled: systemEnabled } = await NotificationListener.check();
187
176
  if (!systemEnabled) {
@@ -192,7 +181,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
192
181
  if (!fcmToken) { console.warn("No FCM token available"); return; }
193
182
  await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
194
183
  await request("device.capability.enable", { capability: "notifications", fcmToken });
195
- setNotificationListenerEnabled(true);
184
+ setCapEnabled("notifications", true);
196
185
  }
197
186
  } catch (err) {
198
187
  console.error("Failed to toggle notification listener:", err);
@@ -201,28 +190,6 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
201
190
  }
202
191
  }
203
192
 
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
193
  async function handleSmsToggle() {
227
194
  if (!SmsPermission || !request) return;
228
195
  setTogglingSms(true);
@@ -230,7 +197,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
230
197
  if (smsEnabled) {
231
198
  await Preferences.set({ key: "smsListenerEnabled", value: "false" });
232
199
  await request("device.capability.disable", { capability: "sms" });
233
- setSmsEnabled(false);
200
+ setCapEnabled("sms", false);
234
201
  } else {
235
202
  const { granted } = await SmsPermission.check();
236
203
  if (!granted) {
@@ -241,7 +208,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
241
208
  if (!fcmToken) { console.warn("No FCM token available"); return; }
242
209
  await Preferences.set({ key: "smsListenerEnabled", value: "true" });
243
210
  await request("device.capability.enable", { capability: "sms", fcmToken });
244
- setSmsEnabled(true);
211
+ setCapEnabled("sms", true);
245
212
  }
246
213
  } catch (err) {
247
214
  console.error("Failed to toggle SMS access:", err);
@@ -250,28 +217,6 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
250
217
  }
251
218
  }
252
219
 
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
220
  async function handleContactsToggle() {
276
221
  if (!ContactsPermission || !request) return;
277
222
  setTogglingContacts(true);
@@ -279,7 +224,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
279
224
  if (contactsEnabled) {
280
225
  await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
281
226
  await request("device.capability.disable", { capability: "contacts" });
282
- setContactsEnabled(false);
227
+ setCapEnabled("contacts", false);
283
228
  } else {
284
229
  const { granted } = await ContactsPermission.check();
285
230
  if (!granted) {
@@ -290,7 +235,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
290
235
  if (!fcmToken) { console.warn("No FCM token available"); return; }
291
236
  await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
292
237
  await request("device.capability.enable", { capability: "contacts", fcmToken });
293
- setContactsEnabled(true);
238
+ setCapEnabled("contacts", true);
294
239
  }
295
240
  } catch (err) {
296
241
  console.error("Failed to toggle contacts access:", err);
@@ -299,28 +244,6 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
299
244
  }
300
245
  }
301
246
 
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
247
  async function handleCalendarToggle() {
325
248
  if (!CalendarPermission || !request) return;
326
249
  setTogglingCalendar(true);
@@ -328,7 +251,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
328
251
  if (calendarEnabled) {
329
252
  await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
330
253
  await request("device.capability.disable", { capability: "calendar" });
331
- setCalendarEnabled(false);
254
+ setCapEnabled("calendar", false);
332
255
  } else {
333
256
  const { granted } = await CalendarPermission.check();
334
257
  if (!granted) {
@@ -339,7 +262,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
339
262
  if (!fcmToken) { console.warn("No FCM token available"); return; }
340
263
  await Preferences.set({ key: "calendarAccessEnabled", value: "true" });
341
264
  await request("device.capability.enable", { capability: "calendar", fcmToken });
342
- setCalendarEnabled(true);
265
+ setCapEnabled("calendar", true);
343
266
  }
344
267
  } catch (err) {
345
268
  console.error("Failed to toggle calendar access:", err);
@@ -348,33 +271,13 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
348
271
  }
349
272
  }
350
273
 
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
274
  async function handleDndToggle() {
371
275
  if (!DndAccess || !request) return;
372
276
  setTogglingDnd(true);
373
277
  try {
374
278
  if (dndEnabled) {
375
- // DND access can only be revoked in system settings, but we unregister from host
376
279
  await request("device.capability.disable", { capability: "dnd" });
377
- setDndEnabled(false);
280
+ setCapEnabled("dnd", false);
378
281
  } else {
379
282
  const { enabled: systemEnabled } = await DndAccess.check();
380
283
  if (!systemEnabled) {
@@ -384,7 +287,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
384
287
  const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
385
288
  if (!fcmToken) { console.warn("No FCM token available"); return; }
386
289
  await request("device.capability.enable", { capability: "dnd", fcmToken });
387
- setDndEnabled(true);
290
+ setCapEnabled("dnd", true);
388
291
  }
389
292
  } catch (err) {
390
293
  console.error("Failed to toggle DND access:", err);
@@ -393,58 +296,38 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
393
296
  }
394
297
  }
395
298
 
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
299
  async function handleAlarmToggle() {
405
300
  if (!request) return;
406
301
  setTogglingAlarm(true);
407
302
  try {
408
303
  if (alarmEnabled) {
409
- await Preferences.set({ key: "alertAccessEnabled", value: "false" });
410
304
  await request("device.capability.disable", { capability: "alert" });
411
- setAlarmEnabled(false);
305
+ setCapEnabled("alert", false);
412
306
  } else {
413
307
  const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
414
308
  if (!fcmToken) { console.warn("No FCM token available"); return; }
415
- await Preferences.set({ key: "alertAccessEnabled", value: "true" });
416
309
  await request("device.capability.enable", { capability: "alert", fcmToken });
417
- setAlarmEnabled(true);
310
+ setCapEnabled("alert", true);
418
311
  }
419
312
  } catch (err) {
420
- console.error("Failed to toggle alarm access:", err);
313
+ console.error("Failed to toggle alert access:", err);
421
314
  } finally {
422
315
  setTogglingAlarm(false);
423
316
  }
424
317
  }
425
318
 
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
319
  async function handleBatteryToggle() {
435
320
  if (!request) return;
436
321
  setTogglingBattery(true);
437
322
  try {
438
323
  if (batteryEnabled) {
439
- await Preferences.set({ key: "batteryAccessEnabled", value: "false" });
440
324
  await request("device.capability.disable", { capability: "battery" });
441
- setBatteryEnabled(false);
325
+ setCapEnabled("battery", false);
442
326
  } else {
443
327
  const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
444
328
  if (!fcmToken) { console.warn("No FCM token available"); return; }
445
- await Preferences.set({ key: "batteryAccessEnabled", value: "true" });
446
329
  await request("device.capability.enable", { capability: "battery", fcmToken });
447
- setBatteryEnabled(true);
330
+ setCapEnabled("battery", true);
448
331
  }
449
332
  } catch (err) {
450
333
  console.error("Failed to toggle battery access:", err);
@@ -459,13 +342,12 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
459
342
  try {
460
343
  if (locationEnabled) {
461
344
  await request("device.location.disable");
462
- onLocationClientTokenChange?.(null);
345
+ setCapEnabled("location", false);
463
346
  } else {
464
- // Request location permissions before enabling
465
347
  if (LocationPermission) {
466
348
  const result = await LocationPermission.request();
467
349
  if (!result.fine) {
468
- return; // User denied permission
350
+ return;
469
351
  }
470
352
  }
471
353
 
@@ -475,7 +357,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
475
357
  return;
476
358
  }
477
359
  await request("device.location.enable", { fcmToken });
478
- onLocationClientTokenChange?.(activeClientToken ?? null);
360
+ setCapEnabled("location", true);
479
361
  }
480
362
  } catch (err) {
481
363
  console.error("Failed to toggle location:", err);
@@ -26,10 +26,10 @@ interface TaskListViewProps {
26
26
  onViewRun(taskId: string, runId?: string): void;
27
27
  onUpdateRequired?(required: boolean): void;
28
28
  onVersion?(version: string | null): void;
29
- onLocationClientToken?(token: string | null): void;
29
+ onCapabilityTokens?(tokens: Record<string, string | null>): void;
30
30
  }
31
31
 
32
- export default function TaskListView({ connected, hostId, request, subscribeEvents, onViewRun, onUpdateRequired, onVersion, onLocationClientToken }: TaskListViewProps) {
32
+ export default function TaskListView({ connected, hostId, request, subscribeEvents, onViewRun, onUpdateRequired, onVersion, onCapabilityTokens }: TaskListViewProps) {
33
33
  const [tasks, setTasks] = useState<Task[]>([]);
34
34
  const [loadingTasks, setLoadingTasks] = useState(false);
35
35
  const [taskError, setTaskError] = useState<string | null>(null);
@@ -53,7 +53,7 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
53
53
  setLoadingTasks(true);
54
54
  setTaskError(null);
55
55
  try {
56
- const result = await request<{ tasks?: (Task & { status?: TaskStatus })[]; agents?: AgentInfo[]; version?: string | null; host_platform?: string; location_client_token?: string | null }>("task.list");
56
+ const result = await request<{ tasks?: (Task & { status?: TaskStatus })[]; agents?: AgentInfo[]; version?: string | null; host_platform?: string; location_client_token?: string | null; capability_tokens?: Record<string, string | null> }>("task.list");
57
57
  const taskList = result.tasks ?? [];
58
58
  const initialEvents = new Map<string, TaskStatus>();
59
59
  const initialConfirms = new Map<string, { description: string; agentName?: string }>();
@@ -82,7 +82,7 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
82
82
  setAgentLabels(result.agents ?? []);
83
83
  const version = result.version ?? null;
84
84
  onVersion?.(version);
85
- onLocationClientToken?.(result.location_client_token ?? null);
85
+ onCapabilityTokens?.(result.capability_tokens ?? {});
86
86
  onUpdateRequired?.(!!version && isOlderThan(version, MIN_HOST_VERSION));
87
87
  } catch (err) {
88
88
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1,2 +1,2 @@
1
1
  /** Bump when a breaking host change is made. */
2
- export const MIN_HOST_VERSION = "0.7.3";
2
+ export const MIN_HOST_VERSION = "0.7.4";
@@ -47,7 +47,7 @@ export default function Dashboard() {
47
47
  const [updating, setUpdating] = useState(false);
48
48
  const [updateError, setUpdateError] = useState<string | null>(null);
49
49
  const [daemonVersion, setDaemonVersion] = useState<string | null>(null);
50
- const [locationClientToken, setLocationClientToken] = useState<string | null>(null);
50
+ const [capabilityTokens, setCapabilityTokens] = useState<Record<string, string | null>>({});
51
51
 
52
52
  // Register push subscription for the active host
53
53
  usePushSubscription();
@@ -88,11 +88,11 @@ export default function Dashboard() {
88
88
 
89
89
  return (
90
90
  <div className="dashboard">
91
- {isDesktop && <HostMenu daemonVersion={daemonVersion} locationClientToken={locationClientToken} activeClientToken={activeClientToken} request={request} onLocationClientTokenChange={setLocationClientToken} />}
91
+ {isDesktop && <HostMenu daemonVersion={daemonVersion} capabilityTokens={capabilityTokens} activeClientToken={activeClientToken} request={request} onCapabilityTokensChange={setCapabilityTokens} />}
92
92
 
93
93
  <div className="dashboard-content">
94
94
  <div className="tab-bar">
95
- {!isDesktop && <HostMenu daemonVersion={daemonVersion} locationClientToken={locationClientToken} activeClientToken={activeClientToken} request={request} onLocationClientTokenChange={setLocationClientToken} />}
95
+ {!isDesktop && <HostMenu daemonVersion={daemonVersion} capabilityTokens={capabilityTokens} activeClientToken={activeClientToken} request={request} onCapabilityTokensChange={setCapabilityTokens} />}
96
96
  <TabBar />
97
97
  </div>
98
98
 
@@ -141,7 +141,7 @@ export default function Dashboard() {
141
141
  onViewRun={handleViewRun}
142
142
  onUpdateRequired={setUpdateRequired}
143
143
  onVersion={setDaemonVersion}
144
- onLocationClientToken={setLocationClientToken}
144
+ onCapabilityTokens={setCapabilityTokens}
145
145
  />
146
146
  </div>
147
147
  {isRunDetail ? (
@@ -163,13 +163,17 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
163
163
  switch (request.method) {
164
164
  case "task.list": {
165
165
  const tasks = listTasks(config.projectRoot);
166
- const locDevice = getCapabilityDevice("location");
166
+ const capabilities: Record<string, string | null> = {};
167
+ for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd"] as const) {
168
+ capabilities[cap] = getCapabilityDevice(cap)?.clientToken ?? null;
169
+ }
167
170
  return {
168
171
  tasks: tasks.map((task) => flattenTask(task)),
169
172
  agents: config.agents ?? [],
170
173
  version: currentVersion,
171
174
  host_platform: process.platform,
172
- location_client_token: locDevice?.clientToken ?? null,
175
+ location_client_token: capabilities.location,
176
+ capability_tokens: capabilities,
173
177
  };
174
178
  }
175
179