palmier 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-BirmfPUC.js +118 -0
- package/dist/pwa/assets/{web-Dwi8DLNK.js → web-Dc9-IiRD.js} +1 -1
- package/dist/pwa/assets/{web-SlBB3mP3.js → web-_b3Dvcvz.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +19 -4
- package/dist/transports/http-transport.js +1 -1
- package/package.json +1 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +125 -11
- package/palmier-server/pwa/src/constants.ts +1 -1
- 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 +19 -4
- 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 t}from"./index-
|
|
1
|
+
import{W as t}from"./index-BirmfPUC.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as p}from"./index-
|
|
1
|
+
import{W as p}from"./index-BirmfPUC.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
|
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-BirmfPUC.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":"1d8c320b1a6217a07e14dc08358f068d","url":"index.html"},{"revision":null,"url":"assets/web-_b3Dvcvz.js"},{"revision":null,"url":"assets/web-Dc9-IiRD.js"},{"revision":null,"url":"assets/index-BirmfPUC.js"},{"revision":null,"url":"assets/index-B-ByUHPS.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"9f51698004b9cc4d787c75695b74de9d","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,d=c.task_id;n.waitUntil(self.registration.getNotifications().then(u=>{var g,P,K;for(const y of u)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}d&&((K=y.data)==null?void 0:K.task_id)===d&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.task_id,i=t.run_id,r=a&&i?`/runs/${encodeURIComponent(a)}/${encodeURIComponent(i)}`:a?`/runs/${encodeURIComponent(a)}/latest`:"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(c=>{for(const o of c)if(o.url.includes(self.location.origin)&&"focus"in o)return o.navigate(r),o.focus();return self.clients.openWindow(r)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
|
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,7 +143,7 @@ export function createRpcHandler(config, nc) {
|
|
|
143
143
|
switch (request.method) {
|
|
144
144
|
case "task.list": {
|
|
145
145
|
const tasks = listTasks(config.projectRoot);
|
|
146
|
-
const locDevice =
|
|
146
|
+
const locDevice = getCapabilityDevice("location");
|
|
147
147
|
return {
|
|
148
148
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
149
149
|
agents: config.agents ?? [],
|
|
@@ -569,11 +569,26 @@ export function createRpcHandler(config, nc) {
|
|
|
569
569
|
if (!params.fcmToken)
|
|
570
570
|
return { error: "fcmToken is required" };
|
|
571
571
|
const clientToken = request.clientToken ?? "";
|
|
572
|
-
|
|
572
|
+
setCapabilityDevice("location", clientToken, params.fcmToken);
|
|
573
573
|
return { ok: true };
|
|
574
574
|
}
|
|
575
575
|
case "device.location.disable": {
|
|
576
|
-
|
|
576
|
+
clearCapabilityDevice("location");
|
|
577
|
+
return { ok: true };
|
|
578
|
+
}
|
|
579
|
+
case "device.capability.enable": {
|
|
580
|
+
const params = request.params;
|
|
581
|
+
if (!params.capability || !params.fcmToken)
|
|
582
|
+
return { error: "capability and fcmToken are required" };
|
|
583
|
+
const clientToken = request.clientToken ?? "";
|
|
584
|
+
setCapabilityDevice(params.capability, clientToken, params.fcmToken);
|
|
585
|
+
return { ok: true };
|
|
586
|
+
}
|
|
587
|
+
case "device.capability.disable": {
|
|
588
|
+
const params = request.params;
|
|
589
|
+
if (!params.capability)
|
|
590
|
+
return { error: "capability is required" };
|
|
591
|
+
clearCapabilityDevice(params.capability);
|
|
577
592
|
return { ok: true };
|
|
578
593
|
}
|
|
579
594
|
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
|
@@ -119,6 +119,10 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
119
119
|
const [togglingCalendar, setTogglingCalendar] = useState(false);
|
|
120
120
|
const [dndEnabled, setDndEnabled] = useState(false);
|
|
121
121
|
const [togglingDnd, setTogglingDnd] = useState(false);
|
|
122
|
+
const [alarmEnabled, setAlarmEnabled] = useState(false);
|
|
123
|
+
const [togglingAlarm, setTogglingAlarm] = useState(false);
|
|
124
|
+
const [batteryEnabled, setBatteryEnabled] = useState(false);
|
|
125
|
+
const [togglingBattery, setTogglingBattery] = useState(false);
|
|
122
126
|
|
|
123
127
|
const locationEnabled = !!(activeClientToken && locationClientToken === activeClientToken);
|
|
124
128
|
|
|
@@ -171,21 +175,23 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
171
175
|
}, []);
|
|
172
176
|
|
|
173
177
|
async function handleNotificationListenerToggle() {
|
|
174
|
-
if (!NotificationListener) return;
|
|
178
|
+
if (!NotificationListener || !request) return;
|
|
175
179
|
setTogglingNotificationListener(true);
|
|
176
180
|
try {
|
|
177
181
|
if (notificationListenerEnabled) {
|
|
178
|
-
// Toggling off — save preference, service checks this before relaying
|
|
179
182
|
await Preferences.set({ key: "notificationListenerEnabled", value: "false" });
|
|
183
|
+
await request("device.capability.disable", { capability: "notifications" });
|
|
180
184
|
setNotificationListenerEnabled(false);
|
|
181
185
|
} else {
|
|
182
|
-
// Toggling on — check system permission first, open settings if needed
|
|
183
186
|
const { enabled: systemEnabled } = await NotificationListener.check();
|
|
184
187
|
if (!systemEnabled) {
|
|
185
188
|
const result = await NotificationListener.request();
|
|
186
|
-
if (!result.enabled) return;
|
|
189
|
+
if (!result.enabled) return;
|
|
187
190
|
}
|
|
191
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
192
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
188
193
|
await Preferences.set({ key: "notificationListenerEnabled", value: "true" });
|
|
194
|
+
await request("device.capability.enable", { capability: "notifications", fcmToken });
|
|
189
195
|
setNotificationListenerEnabled(true);
|
|
190
196
|
}
|
|
191
197
|
} catch (err) {
|
|
@@ -218,11 +224,12 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
218
224
|
}, []);
|
|
219
225
|
|
|
220
226
|
async function handleSmsToggle() {
|
|
221
|
-
if (!SmsPermission) return;
|
|
227
|
+
if (!SmsPermission || !request) return;
|
|
222
228
|
setTogglingSms(true);
|
|
223
229
|
try {
|
|
224
230
|
if (smsEnabled) {
|
|
225
231
|
await Preferences.set({ key: "smsListenerEnabled", value: "false" });
|
|
232
|
+
await request("device.capability.disable", { capability: "sms" });
|
|
226
233
|
setSmsEnabled(false);
|
|
227
234
|
} else {
|
|
228
235
|
const { granted } = await SmsPermission.check();
|
|
@@ -230,7 +237,10 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
230
237
|
const result = await SmsPermission.request();
|
|
231
238
|
if (!result.granted) return;
|
|
232
239
|
}
|
|
240
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
241
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
233
242
|
await Preferences.set({ key: "smsListenerEnabled", value: "true" });
|
|
243
|
+
await request("device.capability.enable", { capability: "sms", fcmToken });
|
|
234
244
|
setSmsEnabled(true);
|
|
235
245
|
}
|
|
236
246
|
} catch (err) {
|
|
@@ -263,11 +273,12 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
263
273
|
}, []);
|
|
264
274
|
|
|
265
275
|
async function handleContactsToggle() {
|
|
266
|
-
if (!ContactsPermission) return;
|
|
276
|
+
if (!ContactsPermission || !request) return;
|
|
267
277
|
setTogglingContacts(true);
|
|
268
278
|
try {
|
|
269
279
|
if (contactsEnabled) {
|
|
270
280
|
await Preferences.set({ key: "contactsAccessEnabled", value: "false" });
|
|
281
|
+
await request("device.capability.disable", { capability: "contacts" });
|
|
271
282
|
setContactsEnabled(false);
|
|
272
283
|
} else {
|
|
273
284
|
const { granted } = await ContactsPermission.check();
|
|
@@ -275,7 +286,10 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
275
286
|
const result = await ContactsPermission.request();
|
|
276
287
|
if (!result.granted) return;
|
|
277
288
|
}
|
|
289
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
290
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
278
291
|
await Preferences.set({ key: "contactsAccessEnabled", value: "true" });
|
|
292
|
+
await request("device.capability.enable", { capability: "contacts", fcmToken });
|
|
279
293
|
setContactsEnabled(true);
|
|
280
294
|
}
|
|
281
295
|
} catch (err) {
|
|
@@ -308,11 +322,12 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
308
322
|
}, []);
|
|
309
323
|
|
|
310
324
|
async function handleCalendarToggle() {
|
|
311
|
-
if (!CalendarPermission) return;
|
|
325
|
+
if (!CalendarPermission || !request) return;
|
|
312
326
|
setTogglingCalendar(true);
|
|
313
327
|
try {
|
|
314
328
|
if (calendarEnabled) {
|
|
315
329
|
await Preferences.set({ key: "calendarAccessEnabled", value: "false" });
|
|
330
|
+
await request("device.capability.disable", { capability: "calendar" });
|
|
316
331
|
setCalendarEnabled(false);
|
|
317
332
|
} else {
|
|
318
333
|
const { granted } = await CalendarPermission.check();
|
|
@@ -320,7 +335,10 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
320
335
|
const result = await CalendarPermission.request();
|
|
321
336
|
if (!result.granted) return;
|
|
322
337
|
}
|
|
338
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
339
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
323
340
|
await Preferences.set({ key: "calendarAccessEnabled", value: "true" });
|
|
341
|
+
await request("device.capability.enable", { capability: "calendar", fcmToken });
|
|
324
342
|
setCalendarEnabled(true);
|
|
325
343
|
}
|
|
326
344
|
} catch (err) {
|
|
@@ -350,12 +368,24 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
350
368
|
}, []);
|
|
351
369
|
|
|
352
370
|
async function handleDndToggle() {
|
|
353
|
-
if (!DndAccess) return;
|
|
371
|
+
if (!DndAccess || !request) return;
|
|
354
372
|
setTogglingDnd(true);
|
|
355
373
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
374
|
+
if (dndEnabled) {
|
|
375
|
+
// DND access can only be revoked in system settings, but we unregister from host
|
|
376
|
+
await request("device.capability.disable", { capability: "dnd" });
|
|
377
|
+
setDndEnabled(false);
|
|
378
|
+
} else {
|
|
379
|
+
const { enabled: systemEnabled } = await DndAccess.check();
|
|
380
|
+
if (!systemEnabled) {
|
|
381
|
+
const result = await DndAccess.request();
|
|
382
|
+
if (!result.enabled) return;
|
|
383
|
+
}
|
|
384
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
385
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
386
|
+
await request("device.capability.enable", { capability: "dnd", fcmToken });
|
|
387
|
+
setDndEnabled(true);
|
|
388
|
+
}
|
|
359
389
|
} catch (err) {
|
|
360
390
|
console.error("Failed to toggle DND access:", err);
|
|
361
391
|
} finally {
|
|
@@ -363,6 +393,66 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
363
393
|
}
|
|
364
394
|
}
|
|
365
395
|
|
|
396
|
+
// Sync alarm toggle — no permission needed, just device registration
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
if (!isNative) return;
|
|
399
|
+
Preferences.get({ key: "alertAccessEnabled" }).then(({ value }) => {
|
|
400
|
+
setAlarmEnabled(value === "true");
|
|
401
|
+
});
|
|
402
|
+
}, []);
|
|
403
|
+
|
|
404
|
+
async function handleAlarmToggle() {
|
|
405
|
+
if (!request) return;
|
|
406
|
+
setTogglingAlarm(true);
|
|
407
|
+
try {
|
|
408
|
+
if (alarmEnabled) {
|
|
409
|
+
await Preferences.set({ key: "alertAccessEnabled", value: "false" });
|
|
410
|
+
await request("device.capability.disable", { capability: "alert" });
|
|
411
|
+
setAlarmEnabled(false);
|
|
412
|
+
} else {
|
|
413
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
414
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
415
|
+
await Preferences.set({ key: "alertAccessEnabled", value: "true" });
|
|
416
|
+
await request("device.capability.enable", { capability: "alert", fcmToken });
|
|
417
|
+
setAlarmEnabled(true);
|
|
418
|
+
}
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error("Failed to toggle alarm access:", err);
|
|
421
|
+
} finally {
|
|
422
|
+
setTogglingAlarm(false);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Sync battery toggle — no permission needed, just device registration
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (!isNative) return;
|
|
429
|
+
Preferences.get({ key: "batteryAccessEnabled" }).then(({ value }) => {
|
|
430
|
+
setBatteryEnabled(value === "true");
|
|
431
|
+
});
|
|
432
|
+
}, []);
|
|
433
|
+
|
|
434
|
+
async function handleBatteryToggle() {
|
|
435
|
+
if (!request) return;
|
|
436
|
+
setTogglingBattery(true);
|
|
437
|
+
try {
|
|
438
|
+
if (batteryEnabled) {
|
|
439
|
+
await Preferences.set({ key: "batteryAccessEnabled", value: "false" });
|
|
440
|
+
await request("device.capability.disable", { capability: "battery" });
|
|
441
|
+
setBatteryEnabled(false);
|
|
442
|
+
} else {
|
|
443
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
444
|
+
if (!fcmToken) { console.warn("No FCM token available"); return; }
|
|
445
|
+
await Preferences.set({ key: "batteryAccessEnabled", value: "true" });
|
|
446
|
+
await request("device.capability.enable", { capability: "battery", fcmToken });
|
|
447
|
+
setBatteryEnabled(true);
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error("Failed to toggle battery access:", err);
|
|
451
|
+
} finally {
|
|
452
|
+
setTogglingBattery(false);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
366
456
|
async function handleLocationToggle() {
|
|
367
457
|
if (!request) return;
|
|
368
458
|
setTogglingLocation(true);
|
|
@@ -645,6 +735,30 @@ export default function HostMenu({ daemonVersion, locationClientToken, activeCli
|
|
|
645
735
|
<span className="toggle-switch-thumb" />
|
|
646
736
|
</button>
|
|
647
737
|
</label>
|
|
738
|
+
<label className="drawer-toggle">
|
|
739
|
+
<span className="drawer-toggle-label">Alert Access</span>
|
|
740
|
+
<button
|
|
741
|
+
className={`toggle-switch ${alarmEnabled ? "toggle-switch-on" : ""}`}
|
|
742
|
+
onClick={handleAlarmToggle}
|
|
743
|
+
disabled={togglingAlarm}
|
|
744
|
+
role="switch"
|
|
745
|
+
aria-checked={alarmEnabled}
|
|
746
|
+
>
|
|
747
|
+
<span className="toggle-switch-thumb" />
|
|
748
|
+
</button>
|
|
749
|
+
</label>
|
|
750
|
+
<label className="drawer-toggle">
|
|
751
|
+
<span className="drawer-toggle-label">Battery Access</span>
|
|
752
|
+
<button
|
|
753
|
+
className={`toggle-switch ${batteryEnabled ? "toggle-switch-on" : ""}`}
|
|
754
|
+
onClick={handleBatteryToggle}
|
|
755
|
+
disabled={togglingBattery}
|
|
756
|
+
role="switch"
|
|
757
|
+
aria-checked={batteryEnabled}
|
|
758
|
+
>
|
|
759
|
+
<span className="toggle-switch-thumb" />
|
|
760
|
+
</button>
|
|
761
|
+
</label>
|
|
648
762
|
</div>
|
|
649
763
|
</>
|
|
650
764
|
)}
|
|
@@ -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.3";
|
|
@@ -391,19 +391,21 @@ async function main(): Promise<void> {
|
|
|
391
391
|
}
|
|
392
392
|
})();
|
|
393
393
|
|
|
394
|
-
// Subscribe to
|
|
394
|
+
// Subscribe to alert requests from hosts
|
|
395
395
|
(async () => {
|
|
396
396
|
try {
|
|
397
397
|
const conn = await getNatsConnection();
|
|
398
|
-
const sub = conn.subscribe("host.*.fcm.
|
|
399
|
-
console.log("Listening for FCM
|
|
398
|
+
const sub = conn.subscribe("host.*.fcm.alert");
|
|
399
|
+
console.log("Listening for FCM alert requests");
|
|
400
400
|
|
|
401
401
|
for await (const msg of sub) {
|
|
402
402
|
try {
|
|
403
403
|
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
404
404
|
hostId: string;
|
|
405
405
|
requestId: string;
|
|
406
|
-
|
|
406
|
+
fcmToken?: string;
|
|
407
|
+
title?: string;
|
|
408
|
+
description?: string;
|
|
407
409
|
};
|
|
408
410
|
|
|
409
411
|
const subjectHostId = msg.subject.split(".")[1];
|
|
@@ -415,29 +417,32 @@ async function main(): Promise<void> {
|
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
const fcmPayload: Record<string, string> = {
|
|
418
|
-
type: "
|
|
420
|
+
type: "send-alert",
|
|
419
421
|
requestId: data.requestId,
|
|
420
422
|
hostId: data.hostId,
|
|
421
423
|
};
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
424
|
+
if (data.title) fcmPayload.title = data.title;
|
|
425
|
+
if (data.description) fcmPayload.description = data.description;
|
|
425
426
|
|
|
426
|
-
console.log(`[FCM] Sending
|
|
427
|
-
|
|
427
|
+
console.log(`[FCM] Sending alert request for host ${data.hostId}`);
|
|
428
|
+
if (data.fcmToken) {
|
|
429
|
+
await sendFcmToDevice(data.fcmToken, fcmPayload);
|
|
430
|
+
} else {
|
|
431
|
+
await sendFcmToClients(data.hostId, fcmPayload);
|
|
432
|
+
}
|
|
428
433
|
|
|
429
434
|
if (msg.reply) {
|
|
430
435
|
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
431
436
|
}
|
|
432
437
|
} catch (err) {
|
|
433
|
-
console.error("[FCM] Error handling
|
|
438
|
+
console.error("[FCM] Error handling alert request:", err);
|
|
434
439
|
if (msg.reply) {
|
|
435
440
|
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
436
441
|
}
|
|
437
442
|
}
|
|
438
443
|
}
|
|
439
444
|
} catch (err) {
|
|
440
|
-
console.error("Failed to subscribe to FCM
|
|
445
|
+
console.error("Failed to subscribe to FCM alert requests:", err);
|
|
441
446
|
}
|
|
442
447
|
})();
|
|
443
448
|
|
|
@@ -125,8 +125,8 @@ router.post("/sms-response", async (req: Request, res: Response) => {
|
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
-
// POST /api/device/
|
|
129
|
-
router.post("/
|
|
128
|
+
// POST /api/device/alert-response - Receive alert response from Android, relay to host via NATS
|
|
129
|
+
router.post("/alert-response", async (req: Request, res: Response) => {
|
|
130
130
|
try {
|
|
131
131
|
const { requestId, hostId, result } = req.body;
|
|
132
132
|
|
|
@@ -138,13 +138,13 @@ router.post("/alarm-response", async (req: Request, res: Response) => {
|
|
|
138
138
|
const conn = await getNatsConnection();
|
|
139
139
|
const sc = StringCodec();
|
|
140
140
|
conn.publish(
|
|
141
|
-
`host.${hostId}.
|
|
141
|
+
`host.${hostId}.alert.${requestId}`,
|
|
142
142
|
sc.encode(JSON.stringify(result)),
|
|
143
143
|
);
|
|
144
144
|
|
|
145
145
|
res.json({ ok: true });
|
|
146
146
|
} catch (err) {
|
|
147
|
-
console.error("Device
|
|
147
|
+
console.error("Device alert response relay error:", err);
|
|
148
148
|
res.status(500).json({ error: "Internal server error" });
|
|
149
149
|
}
|
|
150
150
|
});
|
package/palmier-server/spec.md
CHANGED
|
@@ -12,7 +12,7 @@ The host supports **Linux** (systemd) and **Windows** (Task Scheduler for both d
|
|
|
12
12
|
|
|
13
13
|
### 1.2 Components
|
|
14
14
|
|
|
15
|
-
* **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Exposes a localhost-only MCP server at `/mcp` (streamable HTTP transport) with tools: `notify`, `request-input`, `request-confirmation`, `device-geolocation`, `read-contacts`, `create-contact`, `read-calendar`, `create-calendar-event`, `send-sms`, `set-alarm`, `read-battery`, `set-ringer-mode`; and resources: `notifications://device` (device notifications), `sms://device` (SMS messages). Tools and resources are auto-generated as REST endpoints from shared registries (`ToolDefinition[]`, `ResourceDefinition[]`) — zero duplication. Tool REST endpoints are POST with `taskId` query param; resource REST endpoints are GET. `/request-permission` remains a separate endpoint (not part of the MCP registries). MCP resources support subscriptions — clients call `resources/subscribe` and the server holds the POST response open as an SSE stream, pushing `notifications/resources/updated` notifications when the resource data changes. MCP sessions track agent names from `initialize` clientInfo for logging and UI display. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPromptCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
|
|
15
|
+
* **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Exposes a localhost-only MCP server at `/mcp` (streamable HTTP transport) with tools: `notify`, `request-input`, `request-confirmation`, `device-geolocation`, `read-contacts`, `create-contact`, `read-calendar`, `create-calendar-event`, `send-sms-message`, `set-alarm`, `read-battery`, `set-ringer-mode`; and resources: `notifications://device` (device notifications), `sms-messages://device` (SMS messages). Tools and resources are auto-generated as REST endpoints from shared registries (`ToolDefinition[]`, `ResourceDefinition[]`) — zero duplication. Tool REST endpoints are POST with `taskId` query param; resource REST endpoints are GET. `/request-permission` remains a separate endpoint (not part of the MCP registries). MCP resources support subscriptions — clients call `resources/subscribe` and the server holds the POST response open as an SSE stream, pushing `notifications/resources/updated` notifications when the resource data changes. MCP sessions track agent names from `initialize` clientInfo for logging and UI display. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPromptCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
|
|
16
16
|
|
|
17
17
|
* **Web Server (Node.js):** Serves the PWA assets (React) via `app.palmier.me` (Cloudflare proxied), manages Web Push VAPID keys, and provides host registration. Uses **PostgreSQL** for persistent storage (host registrations, push subscriptions, FCM tokens). Connects to NATS via TCP to subscribe to `host-event.>` for sending push notifications (confirmations, dismissals, completion/failure). For `POST /api/push/respond` (confirmation responses via push notification action buttons), the Web Server forwards the response to the host via the `task.user_input` NATS RPC. Subscribes to `host.*.push.send` NATS subjects to relay push notification requests from the host CLI. Subscribes to `host.*.fcm.geolocation` to relay device geolocation requests via FCM. Subscribes to `host.*.fcm.contacts`, `host.*.fcm.calendar`, `host.*.fcm.sms`, `host.*.fcm.alarm`, `host.*.fcm.battery`, and `host.*.fcm.ringer` to relay device capability requests via FCM. Provides HTTP endpoints for Android to post responses back (`/api/device/contacts-response`, `/api/device/calendar-response`, `/api/device/sms-response`, `/api/device/alarm-response`, `/api/device/battery-response`, `/api/device/ringer-response`). Co-located with the NATS server on the same machine.
|
|
18
18
|
|
|
@@ -400,7 +400,7 @@ Resource REST endpoints are auto-generated from the `ResourceDefinition[]` regis
|
|
|
400
400
|
|
|
401
401
|
* **`GET /notifications`** — Returns recent notifications from the user's Android device as a JSON array. Each notification contains `{ id, packageName, appName, title, text, timestamp, receivedAt }`. The host maintains a bounded in-memory collection (last 50 notifications) fed by NATS subscription to `host.<host_id>.device.notifications`.
|
|
402
402
|
|
|
403
|
-
* **`GET /sms`** — Returns recent SMS messages from the user's Android device as a JSON array. Each message contains `{ id, sender, body, timestamp, receivedAt }`. The host maintains a bounded in-memory collection (last 50 messages) fed by NATS subscription to `host.<host_id>.device.sms`.
|
|
403
|
+
* **`GET /sms-messages`** — Returns recent SMS messages from the user's Android device as a JSON array. Each message contains `{ id, sender, body, timestamp, receivedAt }`. The host maintains a bounded in-memory collection (last 50 messages) fed by NATS subscription to `host.<host_id>.device.sms`.
|
|
404
404
|
|
|
405
405
|
## 7. Database Schema (PostgreSQL)
|
|
406
406
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
|
|
5
|
+
const CAPABILITIES_FILE = path.join(CONFIG_DIR, "device-capabilities.json");
|
|
6
|
+
|
|
7
|
+
export interface RegisteredDevice {
|
|
8
|
+
clientToken: string;
|
|
9
|
+
fcmToken: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type DeviceCapability =
|
|
13
|
+
| "location"
|
|
14
|
+
| "notifications"
|
|
15
|
+
| "sms"
|
|
16
|
+
| "contacts"
|
|
17
|
+
| "calendar"
|
|
18
|
+
| "alert"
|
|
19
|
+
| "battery"
|
|
20
|
+
| "dnd";
|
|
21
|
+
|
|
22
|
+
type CapabilityMap = Partial<Record<DeviceCapability, RegisteredDevice>>;
|
|
23
|
+
|
|
24
|
+
function readAll(): CapabilityMap {
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(CAPABILITIES_FILE)) return {};
|
|
27
|
+
return JSON.parse(fs.readFileSync(CAPABILITIES_FILE, "utf-8")) as CapabilityMap;
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeAll(map: CapabilityMap): void {
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
fs.writeFileSync(CAPABILITIES_FILE, JSON.stringify(map, null, 2), "utf-8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getCapabilityDevice(capability: DeviceCapability): RegisteredDevice | null {
|
|
39
|
+
const map = readAll();
|
|
40
|
+
const device = map[capability];
|
|
41
|
+
if (!device?.clientToken || !device?.fcmToken) return null;
|
|
42
|
+
return device;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setCapabilityDevice(capability: DeviceCapability, clientToken: string, fcmToken: string): void {
|
|
46
|
+
const map = readAll();
|
|
47
|
+
map[capability] = { clientToken, fcmToken };
|
|
48
|
+
writeAll(map);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function clearCapabilityDevice(capability: DeviceCapability): void {
|
|
52
|
+
const map = readAll();
|
|
53
|
+
delete map[capability];
|
|
54
|
+
writeAll(map);
|
|
55
|
+
}
|