palmier 0.7.3 → 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.
- package/README.md +26 -16
- package/dist/device-capabilities.d.ts +9 -0
- package/dist/device-capabilities.js +36 -0
- package/dist/mcp-tools.js +55 -38
- package/dist/pwa/assets/index-uSwkmHBs.js +118 -0
- package/dist/pwa/assets/{web-SlBB3mP3.js → web-7raT3zOZ.js} +1 -1
- package/dist/pwa/assets/{web-Dwi8DLNK.js → web-DnuoxUd4.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +24 -5
- package/dist/transports/http-transport.js +1 -1
- package/package.json +1 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +137 -141
- package/palmier-server/pwa/src/components/TaskListView.tsx +4 -4
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/pages/Dashboard.tsx +4 -4
- package/palmier-server/server/src/index.ts +17 -12
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +2 -2
- package/src/device-capabilities.ts +55 -0
- package/src/mcp-tools.ts +54 -36
- package/src/rpc-handler.ts +24 -5
- package/src/transports/http-transport.ts +1 -1
- package/dist/location-device.d.ts +0 -8
- package/dist/location-device.js +0 -32
- package/dist/pwa/assets/index-CPIqbV9-.js +0 -118
- package/src/location-device.ts +0 -35
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as p}from"./index-
|
|
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-
|
|
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};
|
package/dist/pwa/index.html
CHANGED
|
@@ -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-
|
|
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":"
|
|
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())});
|
package/dist/rpc-handler.js
CHANGED
|
@@ -10,7 +10,7 @@ import crossSpawn from "cross-spawn";
|
|
|
10
10
|
import { getAgent } from "./agents/agent.js";
|
|
11
11
|
import { validateClient } from "./client-store.js";
|
|
12
12
|
import { publishHostEvent } from "./events.js";
|
|
13
|
-
import {
|
|
13
|
+
import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice } from "./device-capabilities.js";
|
|
14
14
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
15
15
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
16
16
|
/**
|
|
@@ -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
|
|
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:
|
|
155
|
+
location_client_token: capabilities.location,
|
|
156
|
+
capability_tokens: capabilities,
|
|
153
157
|
};
|
|
154
158
|
}
|
|
155
159
|
case "task.get": {
|
|
@@ -569,11 +573,26 @@ export function createRpcHandler(config, nc) {
|
|
|
569
573
|
if (!params.fcmToken)
|
|
570
574
|
return { error: "fcmToken is required" };
|
|
571
575
|
const clientToken = request.clientToken ?? "";
|
|
572
|
-
|
|
576
|
+
setCapabilityDevice("location", clientToken, params.fcmToken);
|
|
573
577
|
return { ok: true };
|
|
574
578
|
}
|
|
575
579
|
case "device.location.disable": {
|
|
576
|
-
|
|
580
|
+
clearCapabilityDevice("location");
|
|
581
|
+
return { ok: true };
|
|
582
|
+
}
|
|
583
|
+
case "device.capability.enable": {
|
|
584
|
+
const params = request.params;
|
|
585
|
+
if (!params.capability || !params.fcmToken)
|
|
586
|
+
return { error: "capability and fcmToken are required" };
|
|
587
|
+
const clientToken = request.clientToken ?? "";
|
|
588
|
+
setCapabilityDevice(params.capability, clientToken, params.fcmToken);
|
|
589
|
+
return { ok: true };
|
|
590
|
+
}
|
|
591
|
+
case "device.capability.disable": {
|
|
592
|
+
const params = request.params;
|
|
593
|
+
if (!params.capability)
|
|
594
|
+
return { error: "capability is required" };
|
|
595
|
+
clearCapabilityDevice(params.capability);
|
|
577
596
|
return { ok: true };
|
|
578
597
|
}
|
|
579
598
|
default:
|
|
@@ -96,7 +96,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
96
96
|
}
|
|
97
97
|
// Wire up resource change listeners
|
|
98
98
|
onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
|
|
99
|
-
onSmsChanged(() => broadcastResourceUpdated("sms://device"));
|
|
99
|
+
onSmsChanged(() => broadcastResourceUpdated("sms-messages://device"));
|
|
100
100
|
// If a pairing code is provided, pre-register it
|
|
101
101
|
if (pairingCode) {
|
|
102
102
|
const EXPIRY_MS = 24 * 60 * 60 * 1000;
|
package/package.json
CHANGED
|
@@ -92,13 +92,13 @@ const isNative = Capacitor.isNativePlatform();
|
|
|
92
92
|
|
|
93
93
|
interface HostMenuProps {
|
|
94
94
|
daemonVersion?: string | null;
|
|
95
|
-
|
|
95
|
+
capabilityTokens?: Record<string, string | null>;
|
|
96
96
|
activeClientToken?: string | null;
|
|
97
97
|
request?<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
98
|
-
|
|
98
|
+
onCapabilityTokensChange?(tokens: Record<string, string | null>): void;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export default function HostMenu({ daemonVersion,
|
|
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,18 +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);
|
|
117
|
+
const [togglingAlarm, setTogglingAlarm] = useState(false);
|
|
118
|
+
const [togglingBattery, setTogglingBattery] = useState(false);
|
|
122
119
|
|
|
123
|
-
|
|
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
|
+
}
|
|
124
140
|
|
|
125
141
|
// Sync location toggle with permission state — on mount and when app resumes from background
|
|
126
142
|
useEffect(() => {
|
|
@@ -132,7 +148,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
132
148
|
if (!fine) {
|
|
133
149
|
// Permission revoked — disable on host
|
|
134
150
|
request!("device.location.disable").then(() => {
|
|
135
|
-
|
|
151
|
+
setCapEnabled("location", false);
|
|
136
152
|
}).catch(() => {});
|
|
137
153
|
}
|
|
138
154
|
});
|
|
@@ -147,46 +163,25 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
147
163
|
return () => { listener.then((h) => h.remove()); };
|
|
148
164
|
}, [locationEnabled, activeClientToken]);
|
|
149
165
|
|
|
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
166
|
async function handleNotificationListenerToggle() {
|
|
174
|
-
if (!NotificationListener) return;
|
|
167
|
+
if (!NotificationListener || !request) return;
|
|
175
168
|
setTogglingNotificationListener(true);
|
|
176
169
|
try {
|
|
177
170
|
if (notificationListenerEnabled) {
|
|
178
|
-
// Toggling off — save preference, service checks this before relaying
|
|
179
171
|
await Preferences.set({ key: "notificationListenerEnabled", value: "false" });
|
|
180
|
-
|
|
172
|
+
await request("device.capability.disable", { capability: "notifications" });
|
|
173
|
+
setCapEnabled("notifications", false);
|
|
181
174
|
} else {
|
|
182
|
-
// Toggling on — check system permission first, open settings if needed
|
|
183
175
|
const { enabled: systemEnabled } = await NotificationListener.check();
|
|
184
176
|
if (!systemEnabled) {
|
|
185
177
|
const result = await NotificationListener.request();
|
|
186
|
-
if (!result.enabled) return;
|
|
178
|
+
if (!result.enabled) return;
|
|
187
179
|
}
|
|
180
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
181
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
188
182
|
await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
|
|
189
|
-
|
|
183
|
+
await request("device.capability.enable", { capability: "notifications", fcmToken });
|
|
184
|
+
setCapEnabled("notifications", true);
|
|
190
185
|
}
|
|
191
186
|
} catch (err) {
|
|
192
187
|
console.error("Failed to toggle notification listener:", err);
|
|
@@ -195,43 +190,25 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
195
190
|
}
|
|
196
191
|
}
|
|
197
192
|
|
|
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
193
|
async function handleSmsToggle() {
|
|
221
|
-
if (!SmsPermission) return;
|
|
194
|
+
if (!SmsPermission || !request) return;
|
|
222
195
|
setTogglingSms(true);
|
|
223
196
|
try {
|
|
224
197
|
if (smsEnabled) {
|
|
225
198
|
await Preferences.set({ key: "smsListenerEnabled", value: "false" });
|
|
226
|
-
|
|
199
|
+
await request("device.capability.disable", { capability: "sms" });
|
|
200
|
+
setCapEnabled("sms", false);
|
|
227
201
|
} else {
|
|
228
202
|
const { granted } = await SmsPermission.check();
|
|
229
203
|
if (!granted) {
|
|
230
204
|
const result = await SmsPermission.request();
|
|
231
205
|
if (!result.granted) return;
|
|
232
206
|
}
|
|
207
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
208
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
233
209
|
await Preferences.set({ key: "smsListenerEnabled", value: "true" });
|
|
234
|
-
|
|
210
|
+
await request("device.capability.enable", { capability: "sms", fcmToken });
|
|
211
|
+
setCapEnabled("sms", true);
|
|
235
212
|
}
|
|
236
213
|
} catch (err) {
|
|
237
214
|
console.error("Failed to toggle SMS access:", err);
|
|
@@ -240,43 +217,25 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
240
217
|
}
|
|
241
218
|
}
|
|
242
219
|
|
|
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
220
|
async function handleContactsToggle() {
|
|
266
|
-
if (!ContactsPermission) return;
|
|
221
|
+
if (!ContactsPermission || !request) return;
|
|
267
222
|
setTogglingContacts(true);
|
|
268
223
|
try {
|
|
269
224
|
if (contactsEnabled) {
|
|
270
225
|
await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
|
|
271
|
-
|
|
226
|
+
await request("device.capability.disable", { capability: "contacts" });
|
|
227
|
+
setCapEnabled("contacts", false);
|
|
272
228
|
} else {
|
|
273
229
|
const { granted } = await ContactsPermission.check();
|
|
274
230
|
if (!granted) {
|
|
275
231
|
const result = await ContactsPermission.request();
|
|
276
232
|
if (!result.granted) return;
|
|
277
233
|
}
|
|
234
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
235
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
278
236
|
await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
|
|
279
|
-
|
|
237
|
+
await request("device.capability.enable", { capability: "contacts", fcmToken });
|
|
238
|
+
setCapEnabled("contacts", true);
|
|
280
239
|
}
|
|
281
240
|
} catch (err) {
|
|
282
241
|
console.error("Failed to toggle contacts access:", err);
|
|
@@ -285,43 +244,25 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
285
244
|
}
|
|
286
245
|
}
|
|
287
246
|
|
|
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
247
|
async function handleCalendarToggle() {
|
|
311
|
-
if (!CalendarPermission) return;
|
|
248
|
+
if (!CalendarPermission || !request) return;
|
|
312
249
|
setTogglingCalendar(true);
|
|
313
250
|
try {
|
|
314
251
|
if (calendarEnabled) {
|
|
315
252
|
await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
|
|
316
|
-
|
|
253
|
+
await request("device.capability.disable", { capability: "calendar" });
|
|
254
|
+
setCapEnabled("calendar", false);
|
|
317
255
|
} else {
|
|
318
256
|
const { granted } = await CalendarPermission.check();
|
|
319
257
|
if (!granted) {
|
|
320
258
|
const result = await CalendarPermission.request();
|
|
321
259
|
if (!result.granted) return;
|
|
322
260
|
}
|
|
261
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
262
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
323
263
|
await Preferences.set({ key: "calendarAccessEnabled", value: "true" });
|
|
324
|
-
|
|
264
|
+
await request("device.capability.enable", { capability: "calendar", fcmToken });
|
|
265
|
+
setCapEnabled("calendar", true);
|
|
325
266
|
}
|
|
326
267
|
} catch (err) {
|
|
327
268
|
console.error("Failed to toggle calendar access:", err);
|
|
@@ -330,32 +271,24 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
330
271
|
}
|
|
331
272
|
}
|
|
332
273
|
|
|
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
274
|
async function handleDndToggle() {
|
|
353
|
-
if (!DndAccess) return;
|
|
275
|
+
if (!DndAccess || !request) return;
|
|
354
276
|
setTogglingDnd(true);
|
|
355
277
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
278
|
+
if (dndEnabled) {
|
|
279
|
+
await request("device.capability.disable", { capability: "dnd" });
|
|
280
|
+
setCapEnabled("dnd", false);
|
|
281
|
+
} else {
|
|
282
|
+
const { enabled: systemEnabled } = await DndAccess.check();
|
|
283
|
+
if (!systemEnabled) {
|
|
284
|
+
const result = await DndAccess.request();
|
|
285
|
+
if (!result.enabled) return;
|
|
286
|
+
}
|
|
287
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
288
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
289
|
+
await request("device.capability.enable", { capability: "dnd", fcmToken });
|
|
290
|
+
setCapEnabled("dnd", true);
|
|
291
|
+
}
|
|
359
292
|
} catch (err) {
|
|
360
293
|
console.error("Failed to toggle DND access:", err);
|
|
361
294
|
} finally {
|
|
@@ -363,19 +296,58 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
363
296
|
}
|
|
364
297
|
}
|
|
365
298
|
|
|
299
|
+
async function handleAlarmToggle() {
|
|
300
|
+
if (!request) return;
|
|
301
|
+
setTogglingAlarm(true);
|
|
302
|
+
try {
|
|
303
|
+
if (alarmEnabled) {
|
|
304
|
+
await request("device.capability.disable", { capability: "alert" });
|
|
305
|
+
setCapEnabled("alert", false);
|
|
306
|
+
} else {
|
|
307
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
308
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
309
|
+
await request("device.capability.enable", { capability: "alert", fcmToken });
|
|
310
|
+
setCapEnabled("alert", true);
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error("Failed to toggle alert access:", err);
|
|
314
|
+
} finally {
|
|
315
|
+
setTogglingAlarm(false);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function handleBatteryToggle() {
|
|
320
|
+
if (!request) return;
|
|
321
|
+
setTogglingBattery(true);
|
|
322
|
+
try {
|
|
323
|
+
if (batteryEnabled) {
|
|
324
|
+
await request("device.capability.disable", { capability: "battery" });
|
|
325
|
+
setCapEnabled("battery", false);
|
|
326
|
+
} else {
|
|
327
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
328
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
329
|
+
await request("device.capability.enable", { capability: "battery", fcmToken });
|
|
330
|
+
setCapEnabled("battery", true);
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error("Failed to toggle battery access:", err);
|
|
334
|
+
} finally {
|
|
335
|
+
setTogglingBattery(false);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
366
339
|
async function handleLocationToggle() {
|
|
367
340
|
if (!request) return;
|
|
368
341
|
setTogglingLocation(true);
|
|
369
342
|
try {
|
|
370
343
|
if (locationEnabled) {
|
|
371
344
|
await request("device.location.disable");
|
|
372
|
-
|
|
345
|
+
setCapEnabled("location", false);
|
|
373
346
|
} else {
|
|
374
|
-
// Request location permissions before enabling
|
|
375
347
|
if (LocationPermission) {
|
|
376
348
|
const result = await LocationPermission.request();
|
|
377
349
|
if (!result.fine) {
|
|
378
|
-
return;
|
|
350
|
+
return;
|
|
379
351
|
}
|
|
380
352
|
}
|
|
381
353
|
|
|
@@ -385,7 +357,7 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
385
357
|
return;
|
|
386
358
|
}
|
|
387
359
|
await request("device.location.enable", { fcmToken });
|
|
388
|
-
|
|
360
|
+
setCapEnabled("location", true);
|
|
389
361
|
}
|
|
390
362
|
} catch (err) {
|
|
391
363
|
console.error("Failed to toggle location:", err);
|
|
@@ -645,6 +617,30 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
645
617
|
<span className="toggle-switch-thumb" />
|
|
646
618
|
</button>
|
|
647
619
|
</label>
|
|
620
|
+
<label className="drawer-toggle">
|
|
621
|
+
<span className="drawer-toggle-label">Alert Access</span>
|
|
622
|
+
<button
|
|
623
|
+
className={`toggle-switch ${alarmEnabled ? "toggle-switch-on" : ""}`}
|
|
624
|
+
onClick={handleAlarmToggle}
|
|
625
|
+
disabled={togglingAlarm}
|
|
626
|
+
role="switch"
|
|
627
|
+
aria-checked={alarmEnabled}
|
|
628
|
+
>
|
|
629
|
+
<span className="toggle-switch-thumb" />
|
|
630
|
+
</button>
|
|
631
|
+
</label>
|
|
632
|
+
<label className="drawer-toggle">
|
|
633
|
+
<span className="drawer-toggle-label">Battery Access</span>
|
|
634
|
+
<button
|
|
635
|
+
className={`toggle-switch ${batteryEnabled ? "toggle-switch-on" : ""}`}
|
|
636
|
+
onClick={handleBatteryToggle}
|
|
637
|
+
disabled={togglingBattery}
|
|
638
|
+
role="switch"
|
|
639
|
+
aria-checked={batteryEnabled}
|
|
640
|
+
>
|
|
641
|
+
<span className="toggle-switch-thumb" />
|
|
642
|
+
</button>
|
|
643
|
+
</label>
|
|
648
644
|
</div>
|
|
649
645
|
</>
|
|
650
646
|
)}
|
|
@@ -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
|
-
|
|
29
|
+
onCapabilityTokens?(tokens: Record<string, string | null>): void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export default function TaskListView({ connected, hostId, request, subscribeEvents, onViewRun, onUpdateRequired, onVersion,
|
|
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
|
-
|
|
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.
|
|
2
|
+
export const MIN_HOST_VERSION = "0.7.4";
|