palmier 0.8.9 → 0.8.10

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 i}from"./index-1gs4vwFo.js";function o(){const t=window.navigator.connection||window.navigator.mozConnection||window.navigator.webkitConnection;let n="unknown";const e=t?t.type||t.effectiveType:null;if(e&&typeof e=="string")switch(e){case"bluetooth":case"cellular":n="cellular";break;case"none":n="none";break;case"ethernet":case"wifi":case"wimax":n="wifi";break;case"other":case"unknown":n="unknown";break;case"slow-2g":case"2g":case"3g":n="cellular";break;case"4g":n="wifi";break}return n}class s extends i{constructor(){super(),this.handleOnline=()=>{const e={connected:!0,connectionType:o()};this.notifyListeners("networkStatusChange",e)},this.handleOffline=()=>{const n={connected:!1,connectionType:"none"};this.notifyListeners("networkStatusChange",n)},typeof window<"u"&&(window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline))}async getStatus(){if(!window.navigator)throw this.unavailable("Browser does not support the Network Information API");const n=window.navigator.onLine,e=o();return{connected:n,connectionType:n?e:"none"}}}const r=new s;export{r as Network,s as NetworkWeb};
1
+ import{W as i}from"./index-iL_NTbsT.js";function o(){const t=window.navigator.connection||window.navigator.mozConnection||window.navigator.webkitConnection;let n="unknown";const e=t?t.type||t.effectiveType:null;if(e&&typeof e=="string")switch(e){case"bluetooth":case"cellular":n="cellular";break;case"none":n="none";break;case"ethernet":case"wifi":case"wimax":n="wifi";break;case"other":case"unknown":n="unknown";break;case"slow-2g":case"2g":case"3g":n="cellular";break;case"4g":n="wifi";break}return n}class s extends i{constructor(){super(),this.handleOnline=()=>{const e={connected:!0,connectionType:o()};this.notifyListeners("networkStatusChange",e)},this.handleOffline=()=>{const n={connected:!1,connectionType:"none"};this.notifyListeners("networkStatusChange",n)},typeof window<"u"&&(window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline))}async getStatus(){if(!window.navigator)throw this.unavailable("Browser does not support the Network Information API");const n=window.navigator.onLine,e=o();return{connected:n,connectionType:n?e:"none"}}}const r=new s;export{r as Network,s as NetworkWeb};
@@ -1 +1 @@
1
- import{W as p}from"./index-1gs4vwFo.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-iL_NTbsT.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-1gs4vwFo.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-iL_NTbsT.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-1gs4vwFo.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-iL_NTbsT.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-DQJHVyP6.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":"df5e9cfc35bd18ff9618cc0eeb5b49fb","url":"index.html"},{"revision":null,"url":"assets/web-lefgO9YR.js"},{"revision":null,"url":"assets/web-DrSNtZ3i.js"},{"revision":null,"url":"assets/web-BqVsIFtP.js"},{"revision":null,"url":"assets/index-DQJHVyP6.css"},{"revision":null,"url":"assets/index-1gs4vwFo.js"},{"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":"4085edbe0874a29d092f66d2db59d66b","url":"index.html"},{"revision":null,"url":"assets/web-D4ty3qtI.js"},{"revision":null,"url":"assets/web-CaRUL7Kz.js"},{"revision":null,"url":"assets/web-C9g_YGd8.js"},{"revision":null,"url":"assets/index-iL_NTbsT.js"},{"revision":null,"url":"assets/index-DQJHVyP6.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())});
@@ -14,6 +14,7 @@ import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice } from
14
14
  import { currentVersion, performUpdate } from "./update-checker.js";
15
15
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
16
16
  import { clearTaskQueue } from "./event-queues.js";
17
+ import { buildLanUrl } from "./network.js";
17
18
  export function parseResultFrontmatter(raw) {
18
19
  const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
19
20
  if (!fmMatch)
@@ -133,6 +134,7 @@ export function createRpcHandler(config, nc) {
133
134
  host_platform: process.platform,
134
135
  capability_tokens: capabilities,
135
136
  pending_prompts: listPending(),
137
+ lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
136
138
  };
137
139
  }
138
140
  case "task.list": {
@@ -1,5 +1,4 @@
1
1
  import { type NatsConnection } from "nats";
2
2
  import type { HostConfig, RpcMessage } from "../types.js";
3
- export declare function detectLanIp(): string;
4
3
  export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, port: number, nc: NatsConnection | undefined, pairingCode?: string, onReady?: () => void): Promise<void>;
5
4
  //# sourceMappingURL=http-transport.d.ts.map
@@ -5,6 +5,8 @@ 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 { saveConfig } from "../config.js";
9
+ import { detectDefaultInterface } from "../network.js";
8
10
  import { agentToolMap, agentResources, ToolError } from "../mcp-tools.js";
9
11
  import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
10
12
  import { getTaskDir } from "../task.js";
@@ -53,16 +55,12 @@ function getAsset(urlPath) {
53
55
  }
54
56
  }
55
57
  const pendingPairs = new Map();
56
- export function detectLanIp() {
57
- const interfaces = os.networkInterfaces();
58
- for (const name of Object.keys(interfaces)) {
59
- for (const iface of interfaces[name] ?? []) {
60
- if (iface.family === "IPv4" && !iface.internal) {
61
- return iface.address;
62
- }
63
- }
58
+ async function persistDefaultInterface(config) {
59
+ const iface = await detectDefaultInterface();
60
+ if (iface && iface !== config.defaultInterface) {
61
+ config.defaultInterface = iface;
62
+ saveConfig(config);
64
63
  }
65
- return "127.0.0.1";
66
64
  }
67
65
  export async function startHttpTransport(config, handleRpc, port, nc, pairingCode, onReady) {
68
66
  const sseClients = new Set();
@@ -361,11 +359,10 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
361
359
  return;
362
360
  }
363
361
  const client = addClient(label);
364
- const ip = detectLanIp();
362
+ await persistDefaultInterface(config);
365
363
  const response = {
366
364
  hostId: config.hostId,
367
365
  clientToken: client.token,
368
- directUrl: `http://${ip}:${port}`,
369
366
  hostName: os.hostname(),
370
367
  };
371
368
  clearTimeout(pending.timer);
package/dist/types.d.ts CHANGED
@@ -12,6 +12,10 @@ export interface HostConfig {
12
12
  supportsYolo: boolean;
13
13
  }>;
14
14
  httpPort?: number;
15
+ /** OS network interface name of the IPv4 default route, captured on the
16
+ * most recent `palmier pair`. Used to resolve the host's LAN URL live for
17
+ * `host.info` so the client can track IP changes (laptop roaming, DHCP). */
18
+ defaultInterface?: string;
15
19
  }
16
20
  export interface TaskFrontmatter {
17
21
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -28,6 +28,7 @@
28
28
  "dependencies": {
29
29
  "commander": "^13.1.0",
30
30
  "cross-spawn": "^7.0.6",
31
+ "default-gateway": "^7.2.2",
31
32
  "dotenv": "^16.4.7",
32
33
  "nats": "^2.29.1",
33
34
  "yaml": "^2.7.0"
@@ -56,7 +56,6 @@ export default function ConnectionStatusIcon() {
56
56
  type="button"
57
57
  className="conn-status-btn"
58
58
  aria-label={label}
59
- title={label}
60
59
  onClick={() => setPopoverOpen((v) => !v)}
61
60
  >
62
61
  <span aria-hidden="true">{icon}</span>
@@ -1,2 +1,2 @@
1
1
  /** Bump when a breaking host change is made. */
2
- export const MIN_HOST_VERSION = "0.8.8";
2
+ export const MIN_HOST_VERSION = "0.8.9";
@@ -103,18 +103,27 @@ export function HostConnectionProvider({ children }: { children: ReactNode }) {
103
103
  const expectedHostId = activeHost.hostId;
104
104
 
105
105
  async function probe() {
106
+ console.log("[HOST/LAN] probing", lanUrl);
106
107
  try {
107
108
  const res = await fetch(`${lanUrl}/health`, {
108
109
  signal: AbortSignal.timeout(LAN_PROBE_TIMEOUT_MS),
109
110
  });
110
111
  if (cancelled) return;
111
- if (!res.ok) { setLanReachable(false); return; }
112
+ if (!res.ok) {
113
+ console.log("[HOST/LAN] probe got non-ok:", res.status);
114
+ setLanReachable(false);
115
+ return;
116
+ }
112
117
  const data = await res.json() as { hostId?: string };
113
118
  const reachable = data.hostId === expectedHostId;
119
+ if (!reachable) console.log("[HOST/LAN] hostId mismatch — got:", data.hostId, "expected:", expectedHostId);
120
+ else console.log("[HOST/LAN] reachable");
114
121
  setLanReachable(reachable);
115
- if (reachable) console.log("[HOST/LAN] reachable:", lanUrl);
116
- } catch {
117
- if (!cancelled) setLanReachable(false);
122
+ } catch (err) {
123
+ if (!cancelled) {
124
+ console.log("[HOST/LAN] probe failed:", err instanceof Error ? err.message : err);
125
+ setLanReachable(false);
126
+ }
118
127
  }
119
128
  }
120
129
 
@@ -14,6 +14,7 @@ interface HostStoreContextValue {
14
14
  addPairedHost(host: PairedHost): void;
15
15
  removePairedHost(hostId: string): void;
16
16
  renamePairedHost(hostId: string, name: string): void;
17
+ setHostLanUrl(hostId: string, lanUrl: string | undefined): void;
17
18
  setActiveHostId(hostId: string): void;
18
19
  getActiveHost(): PairedHost | null;
19
20
  }
@@ -113,6 +114,16 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
113
114
  );
114
115
  }, []);
115
116
 
117
+ const setHostLanUrl = useCallback((hostId: string, lanUrl: string | undefined) => {
118
+ setPairedHosts((prev) =>
119
+ prev.map((h) => {
120
+ if (h.hostId !== hostId) return h;
121
+ if (h.lanUrl === lanUrl) return h;
122
+ return { ...h, lanUrl };
123
+ })
124
+ );
125
+ }, []);
126
+
116
127
  const setActiveHostId = useCallback((hostId: string) => {
117
128
  setActiveHostIdState(hostId);
118
129
  }, []);
@@ -128,6 +139,7 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
128
139
  addPairedHost,
129
140
  removePairedHost,
130
141
  renamePairedHost,
142
+ setHostLanUrl,
131
143
  setActiveHostId,
132
144
  getActiveHost,
133
145
  }}>
@@ -45,7 +45,7 @@ interface PermissionPrompt { permissions: RequiredPermission[]; sessionName?: st
45
45
  interface InputPrompt { questions: string[]; description?: string; sessionName?: string }
46
46
 
47
47
  export default function Dashboard() {
48
- const { pairedHosts, activeHostId, removePairedHost } = useHostStore();
48
+ const { pairedHosts, activeHostId, removePairedHost, setHostLanUrl } = useHostStore();
49
49
  const { connected, request, subscribeEvents, unauthorized } = useHostConnection();
50
50
  const navigate = useNavigate();
51
51
  const location = useLocation();
@@ -90,6 +90,7 @@ export default function Dashboard() {
90
90
  host_platform?: string;
91
91
  capability_tokens?: Record<string, string | null>;
92
92
  pending_prompts?: PendingPrompt[];
93
+ lan_url?: string | null;
93
94
  }>("host.info")
94
95
  .then((result) => {
95
96
  setAgents(result.agents ?? []);
@@ -99,6 +100,9 @@ export default function Dashboard() {
99
100
  const version = result.version ?? null;
100
101
  setDaemonVersion(version);
101
102
  setUpdateRequired(!!version && isOlderThan(version, MIN_HOST_VERSION));
103
+ if (activeHostId) {
104
+ setHostLanUrl(activeHostId, result.lan_url ?? undefined);
105
+ }
102
106
 
103
107
  // Seed modal state from already-pending prompts.
104
108
  const confirms = new Map<string, ConfirmPrompt>();
@@ -132,7 +136,7 @@ export default function Dashboard() {
132
136
  setInputValues(inputVals);
133
137
  })
134
138
  .catch(() => { /* silent — update-required prompt guards the broken case */ });
135
- }, [connected, activeHostId, request]);
139
+ }, [connected, activeHostId, request, setHostLanUrl]);
136
140
 
137
141
  // Always-on event subscription for modal lifecycle. Independent of which tab
138
142
  // is active. Task-card status updates happen inside TasksView while mounted.
@@ -11,7 +11,6 @@ import type { PairedHost } from "../types";
11
11
  interface PairResponse {
12
12
  hostId: string;
13
13
  clientToken: string;
14
- directUrl?: string;
15
14
  hostName?: string;
16
15
  }
17
16
 
@@ -82,7 +81,6 @@ export default function PairHost() {
82
81
  hostId: response.hostId,
83
82
  clientToken: response.clientToken,
84
83
  directUrl: isLoopback ? window.location.origin : undefined,
85
- lanUrl: !isLoopback ? response.directUrl : undefined,
86
84
  ...(response.hostName ? { name: response.hostName } : {}),
87
85
  };
88
86
 
@@ -6,12 +6,13 @@ import CapabilityToggles from "../components/CapabilityToggles";
6
6
 
7
7
  interface HostInfoResponse {
8
8
  capability_tokens?: Record<string, string | null>;
9
+ lan_url?: string | null;
9
10
  }
10
11
 
11
12
  export default function PairSetup() {
12
13
  const navigate = useNavigate();
13
14
  const { connected, request } = useHostConnection();
14
- const { getActiveHost } = useHostStore();
15
+ const { getActiveHost, setHostLanUrl } = useHostStore();
15
16
  const activeHost = getActiveHost();
16
17
  const activeClientToken = activeHost?.clientToken ?? null;
17
18
 
@@ -26,16 +27,18 @@ export default function PairSetup() {
26
27
 
27
28
  useEffect(() => {
28
29
  if (!connected || !activeHost) return;
30
+ const activeHostId = activeHost.hostId;
29
31
  let cancelled = false;
30
32
  request<HostInfoResponse>("host.info")
31
33
  .then((res) => {
32
34
  if (cancelled) return;
33
35
  setCapabilityTokens(res.capability_tokens ?? {});
36
+ setHostLanUrl(activeHostId, res.lan_url ?? undefined);
34
37
  setLoaded(true);
35
38
  })
36
39
  .catch(() => { if (!cancelled) setLoaded(true); });
37
40
  return () => { cancelled = true; };
38
- }, [connected, activeHost, request]);
41
+ }, [connected, activeHost, request, setHostLanUrl]);
39
42
 
40
43
  return (
41
44
  <div className="pair-setup">
@@ -66,6 +66,6 @@ export interface PairedHost {
66
66
  name?: string;
67
67
  /** If set, all communication uses HTTP to this URL instead of NATS. Set only for loopback (Local mode). */
68
68
  directUrl?: string;
69
- /** Host's LAN URL captured at pair time. Native Capacitor app probes for reachability and routes RPC over HTTP when reachable; events stay on NATS. */
69
+ /** Host's LAN URL, refreshed from each `host.info` response so laptop/DHCP IP changes propagate. Native Capacitor app probes for reachability and routes RPC over HTTP when reachable; events stay on NATS. */
70
70
  lanUrl?: string;
71
71
  }
@@ -1,12 +1,18 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { loadClients } from "../client-store.js";
3
+ import { buildLanUrl } from "../network.js";
3
4
 
4
5
  export async function infoCommand(): Promise<void> {
5
6
  const config = loadConfig();
6
7
  const clients = loadClients();
8
+ const port = config.httpPort ?? 7256;
9
+
10
+ const lanUrl = buildLanUrl(port, config.defaultInterface);
7
11
 
8
12
  console.log(`Host ID: ${config.hostId}`);
9
13
  console.log(`Project root: ${config.projectRoot}`);
14
+ console.log(`Local URL: http://localhost:${port}`);
15
+ console.log(`LAN URL: ${lanUrl ?? "(default route interface unavailable — pair a device or check network)"}`);
10
16
 
11
17
  if (config.agents && config.agents.length > 0) {
12
18
  console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
@@ -3,7 +3,6 @@ import { loadConfig, saveConfig } from "../config.js";
3
3
  import { detectAgents } from "../agents/agent.js";
4
4
  import { getPlatform } from "../platform/index.js";
5
5
  import { pairCommand } from "./pair.js";
6
- import { detectLanIp } from "../transports/http-transport.js";
7
6
  import { listTasks } from "../task.js";
8
7
  import type { HostConfig } from "../types.js";
9
8
 
@@ -42,16 +41,16 @@ export async function initCommand(): Promise<void> {
42
41
  const parsed = parseInt(portAnswer.trim(), 10);
43
42
  if (parsed > 0 && parsed < 65536) httpPort = parsed;
44
43
 
45
- const lanIp = detectLanIp();
46
-
47
44
  console.log(`\n${bold("Setup summary:")}\n`);
48
45
  console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
49
46
  console.log(` All tasks and execution data will be stored here.\n`);
50
- console.log(` ${dim("Local access:")} ${cyan(`http://localhost:${httpPort}`)}`);
47
+ console.log(` ${dim("Local:")} ${cyan(`http://localhost:${httpPort}`)}`);
51
48
  console.log(` Open in a browser on this machine — no internet required.\n`);
52
- console.log(` ${dim("Remote access:")} ${cyan("https://app.palmier.me")}`);
53
- console.log(` Pair the app to your host. The app uses ${cyan(`http://${lanIp}:${httpPort}`)}`);
54
- console.log(` for direct RPC when on the same network, otherwise the relay.\n`);
49
+ console.log(` ${dim("Remote (web):")} ${cyan("https://app.palmier.me")}`);
50
+ console.log(` Pair a browser on any device. Traffic always goes through the relay.\n`);
51
+ console.log(` ${dim("Remote (app):")} ${cyan("https://github.com/caihongxu/palmier-android/releases")}`);
52
+ console.log(` Download the Android APK. The app uses LAN for direct RPC`);
53
+ console.log(` when on the same network, otherwise the relay.\n`);
55
54
  console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
56
55
 
57
56
  const existingTasks = listTasks(process.cwd());
@@ -1,10 +1,10 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "node:os";
3
3
  import { StringCodec } from "nats";
4
- import { loadConfig } from "../config.js";
4
+ import { loadConfig, saveConfig } from "../config.js";
5
5
  import { connectNats } from "../nats-client.js";
6
6
  import { addClient } from "../client-store.js";
7
- import { detectLanIp } from "../transports/http-transport.js";
7
+ import { detectDefaultInterface } from "../network.js";
8
8
  import type { HostConfig } from "../types.js";
9
9
 
10
10
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
@@ -18,13 +18,16 @@ export function generatePairingCode(): string {
18
18
  return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
19
19
  }
20
20
 
21
- function buildPairResponse(config: HostConfig, label?: string) {
21
+ async function buildPairResponse(config: HostConfig, label?: string) {
22
22
  const client = addClient(label);
23
- const port = config.httpPort ?? 7256;
23
+ const iface = await detectDefaultInterface();
24
+ if (iface && iface !== config.defaultInterface) {
25
+ config.defaultInterface = iface;
26
+ saveConfig(config);
27
+ }
24
28
  return {
25
29
  hostId: config.hostId,
26
30
  clientToken: client.token,
27
- directUrl: `http://${detectLanIp()}:${port}`,
28
31
  hostName: os.hostname(),
29
32
  };
30
33
  }
@@ -105,7 +108,7 @@ export async function pairCommand(): Promise<void> {
105
108
  }
106
109
  } catch { /* empty body is fine */ }
107
110
 
108
- const response = buildPairResponse(config, label);
111
+ const response = await buildPairResponse(config, label);
109
112
  if (msg.reply) {
110
113
  msg.respond(sc.encode(JSON.stringify(response)));
111
114
  }
package/src/network.ts ADDED
@@ -0,0 +1,32 @@
1
+ import * as os from "node:os";
2
+ // @ts-expect-error - default-gateway ships no types
3
+ import { gateway4async } from "default-gateway";
4
+
5
+ /**
6
+ * Resolve the name of the network interface used for the IPv4 default route.
7
+ * Returns null when no default route is found (e.g. fully offline host) or
8
+ * when the OS platform isn't supported by `default-gateway`.
9
+ */
10
+ export async function detectDefaultInterface(): Promise<string | null> {
11
+ try {
12
+ const result = await gateway4async() as { int?: string | null };
13
+ return result.int ?? null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function getInterfaceIpv4(interfaceName: string): string | null {
20
+ const addrs = os.networkInterfaces()[interfaceName];
21
+ if (!addrs) return null;
22
+ for (const addr of addrs) {
23
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ export function buildLanUrl(port: number, interfaceName: string | undefined): string | null {
29
+ if (!interfaceName) return null;
30
+ const ip = getInterfaceIpv4(interfaceName);
31
+ return ip ? `http://${ip}:${port}` : null;
32
+ }
@@ -15,6 +15,7 @@ import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice, type D
15
15
  import { currentVersion, performUpdate } from "./update-checker.js";
16
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
17
17
  import { clearTaskQueue } from "./event-queues.js";
18
+ import { buildLanUrl } from "./network.js";
18
19
  import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
19
20
 
20
21
  export function parseResultFrontmatter(raw: string): Record<string, unknown> {
@@ -153,6 +154,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
153
154
  host_platform: process.platform,
154
155
  capability_tokens: capabilities,
155
156
  pending_prompts: listPending(),
157
+ lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
156
158
  };
157
159
  }
158
160
 
@@ -6,6 +6,8 @@ import { validateClient, addClient } from "../client-store.js";
6
6
  import { registerPending } from "../pending-requests.js";
7
7
  import * as fs from "node:fs";
8
8
  import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
9
+ import { saveConfig } from "../config.js";
10
+ import { detectDefaultInterface } from "../network.js";
9
11
  import { agentToolMap, agentResources, ToolError, type ToolContext } from "../mcp-tools.js";
10
12
  import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
11
13
  import { getTaskDir } from "../task.js";
@@ -72,16 +74,12 @@ interface PendingPair {
72
74
 
73
75
  const pendingPairs = new Map<string, PendingPair>();
74
76
 
75
- export function detectLanIp(): string {
76
- const interfaces = os.networkInterfaces();
77
- for (const name of Object.keys(interfaces)) {
78
- for (const iface of interfaces[name] ?? []) {
79
- if (iface.family === "IPv4" && !iface.internal) {
80
- return iface.address;
81
- }
82
- }
77
+ async function persistDefaultInterface(config: HostConfig): Promise<void> {
78
+ const iface = await detectDefaultInterface();
79
+ if (iface && iface !== config.defaultInterface) {
80
+ config.defaultInterface = iface;
81
+ saveConfig(config);
83
82
  }
84
- return "127.0.0.1";
85
83
  }
86
84
 
87
85
  export async function startHttpTransport(
@@ -371,11 +369,10 @@ export async function startHttpTransport(
371
369
  if (!pending) { sendJson(res, 401, { error: "Invalid code" }); return; }
372
370
 
373
371
  const client = addClient(label);
374
- const ip = detectLanIp();
372
+ await persistDefaultInterface(config);
375
373
  const response: Record<string, unknown> = {
376
374
  hostId: config.hostId,
377
375
  clientToken: client.token,
378
- directUrl: `http://${ip}:${port}`,
379
376
  hostName: os.hostname(),
380
377
  };
381
378
 
package/src/types.ts CHANGED
@@ -10,6 +10,11 @@ export interface HostConfig {
10
10
  agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
11
11
 
12
12
  httpPort?: number;
13
+
14
+ /** OS network interface name of the IPv4 default route, captured on the
15
+ * most recent `palmier pair`. Used to resolve the host's LAN URL live for
16
+ * `host.info` so the client can track IP changes (laptop roaming, DHCP). */
17
+ defaultInterface?: string;
13
18
  }
14
19
 
15
20
  export interface TaskFrontmatter {