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