palmier 0.6.9 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{W as p}from"./index-CZejk2al.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
1
+ import{W as p}from"./index-DLxrL0hR.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
@@ -1 +1 @@
1
- import{W as t}from"./index-CZejk2al.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
1
+ import{W as t}from"./index-DLxrL0hR.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
@@ -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-CZejk2al.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-DLxrL0hR.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-C6Lz09EY.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":"7932c200bac35280617b0b0b6fdce381","url":"index.html"},{"revision":null,"url":"assets/web-zj8Blync.js"},{"revision":null,"url":"assets/web-C48txJFl.js"},{"revision":null,"url":"assets/index-CZejk2al.js"},{"revision":null,"url":"assets/index-C6Lz09EY.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"9f51698004b9cc4d787c75695b74de9d","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,d=c.task_id;n.waitUntil(self.registration.getNotifications().then(u=>{var g,P,K;for(const y of u)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}d&&((K=y.data)==null?void 0:K.task_id)===d&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.task_id,i=t.run_id,r=a&&i?`/runs/${encodeURIComponent(a)}/${encodeURIComponent(i)}`:a?`/runs/${encodeURIComponent(a)}/latest`:"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(c=>{for(const o of c)if(o.url.includes(self.location.origin)&&"focus"in o)return o.navigate(r),o.focus();return self.clients.openWindow(r)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
2
+ This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(c)}}}install(e){return v(e,async()=>{const t=new W;this.strategy.plugins.push(t);for(const[i,r]of this._urlsToCacheKeys){const c=this._cacheKeysToIntegrities.get(r),o=this._urlsToCacheModes.get(i),l=new Request(i,{integrity:c,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:r},request:l,event:e}))}const{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return v(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),a=new Set(this._urlsToCacheKeys.values()),i=[];for(const r of s)a.has(r.url)||(await t.delete(r),i.push(r.url));return{deletedURLs:i}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new h("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k;const x=()=>(k||(k=new Q),k);try{self["workbox:routing:7.3.0"]&&_()}catch{}const I="GET",b=n=>n&&typeof n=="object"?n:{handle:n};class R{constructor(e,t,s=I){this.handler=b(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=b(e)}}class z extends R{constructor(e,t,s){const a=({url:i})=>{const r=e.exec(i.href);if(r&&!(i.origin!==location.origin&&r.index!==0))return r.slice(1)};super(a,t,s)}}class X{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(a=>{typeof a=="string"&&(a=[a]);const i=new Request(...a);return this.handleRequest({request:i,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const a=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:t,request:e,sameOrigin:a,url:s});let c=r&&r.handler;const o=e.method;if(!c&&this._defaultHandlerMap.has(o)&&(c=this._defaultHandlerMap.get(o)),!c)return;let l;try{l=c.handle({url:s,request:e,event:t,params:i})}catch(u){l=Promise.reject(u)}const d=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||d)&&(l=l.catch(async u=>{if(d)try{return await d.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(u=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),l}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){const i=this._routes.get(s.method)||[];for(const r of i){let c;const o=r.match({url:e,sameOrigin:t,request:s,event:a});if(o)return c=o,(Array.isArray(c)&&c.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(c=void 0),{route:r,params:c}}return{}}setDefaultHandler(e,t=I){this._defaultHandlerMap.set(t,b(e))}setCatchHandler(e){this._catchHandler=b(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new h("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new h("unregister-route-route-not-registered")}}let m;const Y=()=>(m||(m=new X,m.addFetchListener(),m.addCacheListener()),m);function Z(n,e,t){let s;if(typeof n=="string"){const i=new URL(n,location.href),r=({url:c})=>c.href===i.href;s=new R(r,e,t)}else if(n instanceof RegExp)s=new z(n,e,t);else if(typeof n=="function")s=new R(n,e,t);else if(n instanceof R)s=n;else throw new h("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return Y().registerRoute(s),s}function ee(n,e=[]){for(const t of[...n.searchParams.keys()])e.some(s=>s.test(t))&&n.searchParams.delete(t);return n}function*te(n,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:a}={}){const i=new URL(n,location.href);i.hash="",yield i.href;const r=ee(i,e);if(yield r.href,t&&r.pathname.endsWith("/")){const c=new URL(r.href);c.pathname+=t,yield c.href}if(s){const c=new URL(r.href);c.pathname+=".html",yield c.href}if(a){const c=a({url:i});for(const o of c)yield o.href}}class se extends R{constructor(e,t){const s=({request:a})=>{const i=e.getURLsToCacheKeys();for(const r of te(a.url,t)){const c=i.get(r);if(c){const o=e.getIntegrityForCacheKey(c);return{cacheKey:c,integrity:o}}}};super(s,e.strategy)}}function ne(n){const e=x(),t=new se(e,n);Z(t)}function ae(n){x().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"f65782768cfd2a3d2392e43cf922dde4","url":"index.html"},{"revision":null,"url":"assets/web-HDs03L2B.js"},{"revision":null,"url":"assets/web-CBI458eN.js"},{"revision":null,"url":"assets/index-DLxrL0hR.js"},{"revision":null,"url":"assets/index-C6Lz09EY.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"9f51698004b9cc4d787c75695b74de9d","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,d=c.task_id;n.waitUntil(self.registration.getNotifications().then(u=>{var g,P,K;for(const y of u)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}d&&((K=y.data)==null?void 0:K.task_id)===d&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.task_id,i=t.run_id,r=a&&i?`/runs/${encodeURIComponent(a)}/${encodeURIComponent(i)}`:a?`/runs/${encodeURIComponent(a)}/latest`:"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(c=>{for(const o of c)if(o.url.includes(self.location.origin)&&"focus"in o)return o.navigate(r),o.focus();return self.clients.openWindow(r)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
@@ -5,9 +5,10 @@ import { StringCodec } from "nats";
5
5
  import { validateClient, addClient } from "../client-store.js";
6
6
  import { registerPending } from "../pending-requests.js";
7
7
  import * as fs from "node:fs";
8
- import { agentToolMap, ToolError } from "../mcp-tools.js";
9
- import { handleMcpRequest, getAgentName } from "../mcp-handler.js";
8
+ import { agentToolMap, agentResources, ToolError } from "../mcp-tools.js";
9
+ import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
10
10
  import { getTaskDir } from "../task.js";
11
+ import { onNotificationsChanged } from "../notification-store.js";
11
12
  const assetCache = new Map();
12
13
  const PWA_DIR = path.join(import.meta.dirname, "..", "pwa");
13
14
  const CONTENT_TYPES = {
@@ -73,8 +74,27 @@ export function detectLanIp() {
73
74
  */
74
75
  export async function startHttpTransport(config, handleRpc, port, nc, pairingCode, onReady) {
75
76
  const sseClients = new Set();
77
+ const mcpStreams = new Map();
76
78
  const lanEnabled = config.lanEnabled ?? false;
77
79
  const bindAddress = lanEnabled ? "0.0.0.0" : "127.0.0.1";
80
+ /** Push notifications/resources/updated to all MCP clients subscribed to the given URI. */
81
+ function broadcastResourceUpdated(uri) {
82
+ const subs = getResourceSubscriptions();
83
+ for (const [sessionId, uris] of subs) {
84
+ if (!uris.has(uri))
85
+ continue;
86
+ const stream = mcpStreams.get(sessionId);
87
+ if (!stream)
88
+ continue;
89
+ stream.write(`data: ${JSON.stringify({
90
+ jsonrpc: "2.0",
91
+ method: "notifications/resources/updated",
92
+ params: { uri },
93
+ })}\n\n`);
94
+ }
95
+ }
96
+ // Wire up resource change listeners
97
+ onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
78
98
  // If a pairing code is provided, pre-register it
79
99
  if (pairingCode) {
80
100
  const EXPIRY_MS = 24 * 60 * 60 * 1000;
@@ -146,7 +166,25 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
146
166
  if (result.sessionId) {
147
167
  res.setHeader("Mcp-Session-Id", result.sessionId);
148
168
  }
149
- sendJson(res, 200, result.body);
169
+ if (result.stream && sessionId) {
170
+ // Keep response open as SSE stream for server-initiated notifications
171
+ res.writeHead(200, {
172
+ "Content-Type": "text/event-stream",
173
+ "Cache-Control": "no-cache",
174
+ "Connection": "keep-alive",
175
+ });
176
+ res.write(`data: ${JSON.stringify(result.body)}\n\n`);
177
+ mcpStreams.set(sessionId, res);
178
+ const heartbeat = setInterval(() => { res.write(":heartbeat\n\n"); }, 15_000);
179
+ req.on("close", () => {
180
+ clearInterval(heartbeat);
181
+ mcpStreams.delete(sessionId);
182
+ getResourceSubscriptions().delete(sessionId);
183
+ });
184
+ }
185
+ else {
186
+ sendJson(res, 200, result.body);
187
+ }
150
188
  }
151
189
  catch (err) {
152
190
  sendJson(res, 500, { error: String(err) });
@@ -186,6 +224,16 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
186
224
  }
187
225
  return;
188
226
  }
227
+ // ── Auto-generated REST endpoints from MCP resource registry ────
228
+ const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
229
+ if (matchedResource) {
230
+ if (!isLocalhost(req)) {
231
+ sendJson(res, 403, { error: "localhost only" });
232
+ return;
233
+ }
234
+ sendJson(res, 200, matchedResource.read());
235
+ return;
236
+ }
189
237
  // ── Localhost-only endpoints (no auth) ─────────────────────────────
190
238
  if (req.method === "POST" && pathname === "/event") {
191
239
  if (!isLocalhost(req)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.6.9",
3
+ "version": "0.7.2",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -181,7 +181,7 @@ All endpoints are prefixed with `/api`. No user authentication is required.
181
181
  - **Static file serving** is conditional — Express only serves `pwa/dist/` if the directory exists, so it doesn't interfere during dev when using Vite.
182
182
  - **No CORS** needed — Vite proxy handles same-origin in dev, Express static serving handles it in production.
183
183
  - **Push notifications** — the PWA registers a service worker (`injectManifest` strategy via vite-plugin-pwa) and subscribes the browser for Web Push. The Web Server subscribes to `host-event.>` and sends push notifications for confirmation requests, task completions, and task failures.
184
- - **Markdown rendering** — Generated task plans and task results are rendered as rich formatted text using `react-markdown` with `remark-gfm` (GitHub Flavored Markdown), supporting tables, strikethrough, task lists, and autolinks.
184
+ - **Markdown rendering** — Task results are rendered as rich formatted text using `react-markdown` with `remark-gfm` (GitHub Flavored Markdown), supporting tables, strikethrough, task lists, and autolinks.
185
185
  - **Task confirmation** — the Dashboard discovers pending confirmations from the `task.list` RPC response (tasks with a pending request in the serve daemon's in-memory registry, reported via `task.status`). When found, it shows a full-screen confirmation modal. Push notification action buttons trigger `POST /api/push/respond`, which forwards to the `task.user_input` NATS RPC.
186
186
  - **Task event tracking** — task lifecycle events are persisted to `status.json` on the host (for crash detection) and broadcast via `host-event.<host_id>.<task_id>` pub/sub and HTTP SSE. The PWA loads initial status from `task.list` and subscribes to events for real-time updates.
187
187
  - **NATS config** (`nats.conf`) enables WebSocket on port 9222 (for browser clients) and token-based auth.
@@ -1,25 +1,16 @@
1
- import Markdown from "react-markdown";
2
- import remarkGfm from "remark-gfm";
3
1
  import type { RequiredPermission } from "../types";
4
2
 
5
3
  interface PlanDialogProps {
6
- body: string;
7
4
  permissions?: RequiredPermission[];
8
5
  }
9
6
 
10
- export default function PlanDialog({ body, permissions }: PlanDialogProps) {
7
+ export default function PlanDialog({ permissions }: PlanDialogProps) {
11
8
  return (
12
9
  <div className="plan-dialog">
13
- <h2>Task Execution Plan</h2>
10
+ <h2>Granted Permissions</h2>
14
11
  <div className="plan-dialog-scroll">
15
- {body ? (
16
- <div className="plan-preview"><Markdown remarkPlugins={[remarkGfm]}>{body}</Markdown></div>
17
- ) : (
18
- <p className="plan-empty">No execution plan generated for this task. Your task description will be used directly.</p>
19
- )}
20
- {permissions && permissions.length > 0 && (
12
+ {permissions && permissions.length > 0 ? (
21
13
  <div className="permissions-section">
22
- <h3>Granted Permissions</h3>
23
14
  <ul className="permissions-list">
24
15
  {permissions.map((p, i) => (
25
16
  <li key={i} className="permission-item">
@@ -29,6 +20,8 @@ export default function PlanDialog({ body, permissions }: PlanDialogProps) {
29
20
  ))}
30
21
  </ul>
31
22
  </div>
23
+ ) : (
24
+ <p className="plan-empty">No permissions have been granted for this task.</p>
32
25
  )}
33
26
  </div>
34
27
  <div className="plan-dialog-actions">
@@ -83,16 +83,8 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
83
83
  const [userPrompt, setUserPrompt] = useState(initial?.user_prompt ?? "");
84
84
  const [agent, setAgent] = useState(initial?.agent ?? defaultAgent());
85
85
 
86
- // Plan state for existing tasks only
87
- const [body] = useState(initial?.body ?? "");
88
-
89
- // Track whether prompt or agent diverged from the saved values (for existing tasks)
90
- const promptChanged = !!initial && userPrompt !== (initial.user_prompt ?? "");
91
- const agentChanged = !!initial && agent !== (initial.agent ?? "");
92
- const planInvalidated = promptChanged || agentChanged;
93
-
94
- // Show plan link for existing tasks that have a plan or permissions and haven't been modified
95
- const hasPlan = !!initial && (!!body || !!initial.permissions?.length) && !planInvalidated;
86
+ // Show permissions link for existing tasks that have granted permissions
87
+ const hasPermissions = !!initial?.permissions?.length;
96
88
 
97
89
  // Plan dialog (view-only for existing tasks)
98
90
  const [planDialogOpen, setPlanDialogOpen] = useState(false);
@@ -195,8 +187,8 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
195
187
  if (isEdit) {
196
188
  payload.id = initial!.id;
197
189
  }
198
- // Plan generation happens server-side, allow up to 130s
199
- const result = await request<Task & { error?: string }>(method, payload, { timeout: 130000 });
190
+ // Name generation happens server-side, allow up to 45s
191
+ const result = await request<Task & { error?: string }>(method, payload, { timeout: 45000 });
200
192
  if (result.error) {
201
193
  setError(result.error);
202
194
  return null;
@@ -247,7 +239,6 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
247
239
  <div className="task-form">
248
240
  {planDialogOpen ? (
249
241
  <PlanDialog
250
- body={body}
251
242
  permissions={initial?.permissions}
252
243
  />
253
244
  ) : (<>
@@ -270,12 +261,12 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
270
261
  />
271
262
 
272
263
  <div className="plan-actions">
273
- {hasPlan && (
264
+ {hasPermissions && (
274
265
  <button
275
266
  className="btn btn-link"
276
267
  onClick={() => setPlanDialogOpen(true)}
277
268
  >
278
- Execution Plan
269
+ Granted Permissions
279
270
  </button>
280
271
  )}
281
272
  <div className="agent-picker-section-inline" style={{ marginLeft: "auto" }}>
@@ -1,2 +1,2 @@
1
1
  /** Bump when a breaking host change is made. */
2
- export const MIN_HOST_VERSION = "0.6.8";
2
+ export const MIN_HOST_VERSION = "0.6.9";
@@ -17,7 +17,6 @@ export interface Task {
17
17
  foreground_mode?: boolean;
18
18
  permissions?: RequiredPermission[];
19
19
  command?: string;
20
- body?: string;
21
20
  }
22
21
 
23
22
  export interface Trigger {
@@ -10,6 +10,7 @@ import { StringCodec } from "nats";
10
10
  import hostsRoutes from "./routes/hosts.js";
11
11
  import pushRoutes from "./routes/push.js";
12
12
  import fcmRoutes from "./routes/fcm.js";
13
+ import deviceRoutes from "./routes/device.js";
13
14
  import { notifyClients } from "./notify.js";
14
15
  import { sendFcmToClients, sendFcmToDevice } from "./fcm.js";
15
16
 
@@ -260,6 +261,7 @@ async function main(): Promise<void> {
260
261
  app.use("/api/hosts", hostsRoutes);
261
262
  app.use("/api/push", pushRoutes);
262
263
  app.use("/api/fcm", fcmRoutes);
264
+ app.use("/api/device", deviceRoutes);
263
265
 
264
266
  // Public NATS config endpoint (used by PWA for pairing)
265
267
  app.get("/api/config", (_req, res) => {
@@ -0,0 +1,32 @@
1
+ import { Router, Request, Response } from "express";
2
+ import type { Router as RouterType } from "express";
3
+ import { getNatsConnection } from "../nats.js";
4
+ import { StringCodec } from "nats";
5
+
6
+ const router: RouterType = Router();
7
+
8
+ // POST /api/device/notifications - Receive a notification from Android, relay to host via NATS
9
+ router.post("/notifications", async (req: Request, res: Response) => {
10
+ try {
11
+ const { hostId, notification } = req.body;
12
+
13
+ if (!hostId || !notification?.id) {
14
+ res.status(400).json({ error: "hostId and notification with id are required" });
15
+ return;
16
+ }
17
+
18
+ const conn = await getNatsConnection();
19
+ const sc = StringCodec();
20
+ conn.publish(
21
+ `host.${hostId}.device.notifications`,
22
+ sc.encode(JSON.stringify(notification)),
23
+ );
24
+
25
+ res.json({ ok: true });
26
+ } catch (err) {
27
+ console.error("Device notification relay error:", err);
28
+ res.status(500).json({ error: "Internal server error" });
29
+ }
30
+ });
31
+
32
+ export default router;
@@ -105,10 +105,10 @@ The **RPC method is derived from the NATS subject**, not the message body. The h
105
105
 
106
106
  | Method | Params | Description |
107
107
  |---|---|---|
108
- | `task.list` | *(none)* | List all tasks with frontmatter, body, created_at, and current status. Returns `agents` array of detected CLIs, `host_platform`, and `version`. |
109
- | `task.get` | `id` | Get a single task with frontmatter, body, and current status. |
110
- | `task.create` | `user_prompt`, `agent`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Create a new task with auto-generated plan and name (130s timeout), install system timers if triggers present. If `command` is set, creates a command-triggered task (plan generation is skipped). |
111
- | `task.update` | `id`, `user_prompt?`, `agent?`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Update an existing task. Regenerates plan if `user_prompt` or `agent` changed, or if no plan exists yet (130s timeout). If `command` is set, plan is cleared. Reinstall timers as needed |
108
+ | `task.list` | *(none)* | List all tasks with frontmatter, created_at, and current status. Returns `agents` array of detected CLIs, `host_platform`, and `version`. |
109
+ | `task.get` | `id` | Get a single task with frontmatter and current status. |
110
+ | `task.create` | `user_prompt`, `agent`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Create a new task with auto-generated name (30s timeout for prompts > 50 chars), install system timers if triggers present. |
111
+ | `task.update` | `id`, `user_prompt?`, `agent?`, `triggers?`, `triggers_enabled?`, `requires_confirmation?`, `yolo_mode?`, `foreground_mode?`, `command?` | Update an existing task. Regenerates name if `user_prompt` or `agent` changed. Reinstall timers as needed. |
112
112
  | `task.delete` | `id` | Delete a task and its systemd timers |
113
113
  | `task.run` | `id` | Start a task via system scheduler (`systemctl --user start` / `schtasks /run`) |
114
114
  | `task.abort` | `id` | Stop a running task via system scheduler (`systemctl --user stop` / `schtasks /end`) |
@@ -150,7 +150,7 @@ All tasks are stored locally on the Host machine under a `tasks/` directory rela
150
150
  history.jsonl # Project-level run history index (append-only JSONL: { task_id, run_id })
151
151
  tasks/
152
152
  └── <task-id>/
153
- ├── TASK.md # Current task definition (frontmatter + body)
153
+ ├── TASK.md # Current task definition (YAML frontmatter)
154
154
  ├── status.json # Latest execution status (running_state, time_stamp, pid)
155
155
  └── <timestamp>/ # Run directory (one per run, isolated per agent session)
156
156
  ├── TASKRUN.md # Conversational thread (frontmatter + message entries)
@@ -209,12 +209,13 @@ triggers:
209
209
  triggers_enabled: true
210
210
  requires_confirmation: true
211
211
  ---
212
- [Detailed execution plan generated by the non-interactive generation step]
213
212
  ```
214
213
 
214
+ The `name` field is auto-generated by spawning the configured agent CLI with a short prompt (for prompts > 50 chars). For shorter prompts, the `user_prompt` is used directly as the name.
215
+
215
216
  The `agent` field stores the agent name (e.g., `"claude"`, `"codex"`). The corresponding `AgentTool` implementation is responsible for constructing the full command and arguments at execution time.
216
217
 
217
- The optional `command` field stores a shell command for command-triggered tasks. When set, the task runs in command-triggered mode: the command is spawned with `shell: true`, and each line of its stdout triggers a separate agent invocation with `user_prompt + "\n\nProcess this input:\n" + line`. Plan generation is skipped for command-triggered tasks.
218
+ The optional `command` field stores a shell command for command-triggered tasks. When set, the task runs in command-triggered mode: the command is spawned with `shell: true`, and each line of its stdout triggers a separate agent invocation with `user_prompt + "\n\nProcess this input:\n" + line`.
218
219
 
219
220
  #### Trigger Lifecycle
220
221
 
@@ -263,7 +264,7 @@ The PWA connects to **one host at a time**. A host menu (hamburger drawer) lets
263
264
 
264
265
  3. PWA sends a `task.list` request to `host.<host_id>.rpc.task.list` using NATS request-reply, including the `clientToken` in the payload.
265
266
 
266
- 4. If the host responds, it returns `{ tasks: [...] }` — an array of **flat task objects** (frontmatter fields spread to the top level, plus `body`) and displays the task list. If the request fails with NATS 503 ("no responders"), the PWA shows an empty task list — this is not treated as an error.
267
+ 4. If the host responds, it returns `{ tasks: [...] }` — an array of **flat task objects** (frontmatter fields spread to the top level) and displays the task list. If the request fails with NATS 503 ("no responders"), the PWA shows an empty task list — this is not treated as an error.
267
268
 
268
269
  5. PWA registers the service worker and subscribes the browser for Web Push notifications (via `pushManager.subscribe` with the server's VAPID public key). The push subscription is sent to `POST /api/push/subscribe` with the `hostId` so the server can relay notifications to the device.
269
270
 
@@ -275,13 +276,13 @@ The PWA connects to **one host at a time**. A host menu (hamburger drawer) lets
275
276
 
276
277
  2. User enters a prompt, selects an agent, configures triggers (UI translates human-readable times to cron formats) and confirmation settings, and clicks "Create" (or "Update" for existing tasks).
277
278
 
278
- 3. PWA sends `task.create` (or `task.update`) via NATS request-reply to Host (130s timeout). The host generates the execution plan and task name by running the configured agent CLI in non-interactive mode (e.g., `claude -p "Generate execution plan for: [prompt]"`), then creates the task with the generated plan as its body. The PWA renders the plan markdown as rich formatted text (headings, tables, lists, code blocks) using `react-markdown` with `remark-gfm` for GFM support.
279
+ 3. PWA sends `task.create` (or `task.update`) via NATS request-reply to Host (45s timeout). For prompts > 50 chars, the host generates a concise task name by running the configured agent CLI in non-interactive mode (e.g., `claude -p "Generate a concise 3-6 word name for this task..."`). For shorter prompts, the prompt is used directly as the name.
279
280
 
280
- 4. For updates: if the user changes the `user_prompt` or `agent`, the plan is regenerated. If neither changed, the existing plan is preserved. Existing tasks with a plan show a clickable "Execution Plan" link to view the plan; this link disappears when the user edits the prompt or changes the agent.
281
+ 4. For updates: if the user changes the `user_prompt` or `agent`, the name is regenerated. If neither changed, the existing name is preserved. Existing tasks with granted permissions show a clickable "Granted Permissions" link to view them.
281
282
 
282
283
  5. PWA sends `task.create` (or `task.update` with `id`) with the task fields as the message body. The `id` field is **not sent on create** — the host generates a UUID. The `triggers` field defaults to `[]` if omitted or undefined.
283
284
 
284
- 6. Host creates/updates the `tasks/<task-id>/TASK.md` file and returns the **full flat task object** (all frontmatter fields plus `body` at the top level). The PWA uses this response directly to update the UI.
285
+ 6. Host creates/updates the `tasks/<task-id>/TASK.md` file and returns the **full flat task object** (all frontmatter fields at the top level). The PWA uses this response directly to update the UI.
285
286
 
286
287
  7. **OS Integration:** Host translates triggers into a systemd user timer (`~/.config/systemd/user/palmier-task-<task-id>.timer` and `.service`). The `.service` runs `palmier run <task-id>`, which executes the task as a background process. Host runs `systemctl --user daemon-reload` and enables the timer.
287
288
 
@@ -328,7 +329,7 @@ When `palmier run <task-id>` executes (triggered by a systemd timer, `systemctl
328
329
 
329
330
  * The spawned process inherits the default physical GUI session environment (`DISPLAY=:0`, `XDG_RUNTIME_DIR=/run/user/<uid>`) so that commands requiring a graphical display (e.g., headed browsers) run within the user's desktop session. `PALMIER_HTTP_PORT` is also set so agents can call the serve daemon's HTTP endpoints.
330
331
 
331
- * The agent implementation is responsible for constructing the appropriate arguments (e.g., `--allowedTools` flags for Claude based on the task's permissions). The task plan (body from `TASK.md` or `user_prompt`) is included in the arguments by the agent.
332
+ * The agent implementation is responsible for constructing the appropriate arguments (e.g., `--allowedTools` flags for Claude based on the task's permissions). The task's `user_prompt` is included in the arguments by the agent.
332
333
 
333
334
  3. **Completion:**
334
335
 
@@ -12,7 +12,8 @@ import { detectAgents } from "../agents/agent.js";
12
12
  import { saveConfig } from "../config.js";
13
13
  import type { HostConfig } from "../types.js";
14
14
  import { CONFIG_DIR } from "../config.js";
15
- import type { NatsConnection } from "nats";
15
+ import { StringCodec, type NatsConnection } from "nats";
16
+ import { addNotification } from "../notification-store.js";
16
17
 
17
18
  const POLL_INTERVAL_MS = 30_000;
18
19
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
@@ -130,6 +131,20 @@ export async function serveCommand(): Promise<void> {
130
131
  // Start NATS transport (loops forever, fire-and-forget)
131
132
  if (nc) {
132
133
  startNatsTransport(config, handleRpc, nc);
134
+
135
+ // Subscribe to device notifications from Android
136
+ const sc = StringCodec();
137
+ const notifSub = nc.subscribe(`host.${config.hostId}.device.notifications`);
138
+ (async () => {
139
+ for await (const msg of notifSub) {
140
+ try {
141
+ const data = JSON.parse(sc.decode(msg.data));
142
+ addNotification({ ...data, receivedAt: Date.now() });
143
+ } catch (err) {
144
+ console.error("[nats] Failed to parse device notification:", err);
145
+ }
146
+ }
147
+ })();
133
148
  }
134
149
 
135
150
  // Start HTTP transport (loops forever)
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "crypto";
2
- import { agentTools, agentToolMap, ToolError, type ToolContext } from "./mcp-tools.js";
2
+ import { agentTools, agentToolMap, agentResources, agentResourceMap, ToolError, type ToolContext } from "./mcp-tools.js";
3
3
 
4
4
  interface JsonRpcRequest {
5
5
  jsonrpc: string;
@@ -11,6 +11,15 @@ interface JsonRpcRequest {
11
11
  export interface McpResponse {
12
12
  body: object;
13
13
  sessionId?: string;
14
+ /** If true, the HTTP transport should keep the response open as an SSE stream for server-initiated notifications. */
15
+ stream?: boolean;
16
+ }
17
+
18
+ // Resource subscriptions: sessionId → Set of resource URIs
19
+ const resourceSubscriptions = new Map<string, Set<string>>();
20
+
21
+ export function getResourceSubscriptions(): Map<string, Set<string>> {
22
+ return resourceSubscriptions;
14
23
  }
15
24
 
16
25
  // Session-to-agent name map with 24h TTL
@@ -30,7 +39,10 @@ export function getAgentName(sessionId: string): string | undefined {
30
39
  function pruneExpiredSessions(): void {
31
40
  const now = Date.now();
32
41
  for (const [id, entry] of sessionAgents) {
33
- if (now > entry.expiresAt) sessionAgents.delete(id);
42
+ if (now > entry.expiresAt) {
43
+ sessionAgents.delete(id);
44
+ resourceSubscriptions.delete(id);
45
+ }
34
46
  }
35
47
  }
36
48
 
@@ -78,7 +90,7 @@ export async function handleMcpRequest(body: string, sessionId: string | undefin
78
90
  return {
79
91
  body: rpcResult(id, {
80
92
  protocolVersion: "2025-03-26",
81
- capabilities: { tools: {} },
93
+ capabilities: { tools: {}, resources: { subscribe: true } },
82
94
  serverInfo: { name: "palmier", version: "1.0.0" },
83
95
  }),
84
96
  sessionId: newSessionId,
@@ -126,6 +138,59 @@ export async function handleMcpRequest(body: string, sessionId: string | undefin
126
138
  }
127
139
  }
128
140
 
141
+ case "resources/list": {
142
+ return {
143
+ body: rpcResult(id, {
144
+ resources: agentResources.map((r) => ({
145
+ uri: r.uri,
146
+ name: r.name,
147
+ description: r.description[0],
148
+ mimeType: r.mimeType,
149
+ })),
150
+ }),
151
+ };
152
+ }
153
+
154
+ case "resources/read": {
155
+ const uri = req.params?.uri as string;
156
+ const resource = agentResourceMap.get(uri);
157
+ if (!resource) {
158
+ return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
159
+ }
160
+ return {
161
+ body: rpcResult(id, {
162
+ contents: [{
163
+ uri: resource.uri,
164
+ mimeType: resource.mimeType,
165
+ text: JSON.stringify(resource.read()),
166
+ }],
167
+ }),
168
+ };
169
+ }
170
+
171
+ case "resources/subscribe": {
172
+ const uri = req.params?.uri as string;
173
+ if (!agentResourceMap.has(uri)) {
174
+ return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
175
+ }
176
+ if (!sessionId) {
177
+ return { body: rpcError(id, -32600, "Session required for subscriptions") };
178
+ }
179
+ if (!resourceSubscriptions.has(sessionId)) {
180
+ resourceSubscriptions.set(sessionId, new Set());
181
+ }
182
+ resourceSubscriptions.get(sessionId)!.add(uri);
183
+ return { body: rpcResult(id, {}), stream: true };
184
+ }
185
+
186
+ case "resources/unsubscribe": {
187
+ const uri = req.params?.uri as string;
188
+ if (sessionId) {
189
+ resourceSubscriptions.get(sessionId)?.delete(uri);
190
+ }
191
+ return { body: rpcResult(id, {}) };
192
+ }
193
+
129
194
  default:
130
195
  console.warn(`${logPrefix} Unknown method: ${req.method}`);
131
196
  return { body: rpcError(id, -32601, `Method not found: ${req.method}`) };