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.
- package/README.md +8 -7
- package/dist/commands/info.js +5 -0
- package/dist/commands/init.js +6 -6
- package/dist/commands/pair.js +9 -6
- package/dist/network.d.ts +9 -0
- package/dist/network.js +34 -0
- package/dist/pwa/assets/{index-1gs4vwFo.js → index-iL_NTbsT.js} +36 -36
- package/dist/pwa/assets/{web-DrSNtZ3i.js → web-C9g_YGd8.js} +1 -1
- package/dist/pwa/assets/{web-BqVsIFtP.js → web-CaRUL7Kz.js} +1 -1
- package/dist/pwa/assets/{web-lefgO9YR.js → web-D4ty3qtI.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +2 -0
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +8 -11
- package/dist/types.d.ts +4 -0
- package/package.json +2 -1
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -1
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +13 -4
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +6 -2
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -2
- package/palmier-server/pwa/src/pages/PairSetup.tsx +5 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/src/commands/info.ts +6 -0
- package/src/commands/init.ts +6 -7
- package/src/commands/pair.ts +9 -6
- package/src/network.ts +32 -0
- package/src/rpc-handler.ts +2 -0
- package/src/transports/http-transport.ts +8 -11
- package/src/types.ts +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as i}from"./index-
|
|
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-
|
|
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-
|
|
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};
|
package/dist/pwa/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<title>Palmier</title>
|
|
10
10
|
<meta name="description" content="Remote control for AI agents running on your own machine. Schedule tasks, approve permissions, and get push notifications." />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-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":"
|
|
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())});
|
package/dist/rpc-handler.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Bump when a breaking host change is made. */
|
|
2
|
-
export const MIN_HOST_VERSION = "0.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) {
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
}
|
package/src/commands/info.ts
CHANGED
|
@@ -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(", ")}`);
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
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
|
|
53
|
-
console.log(` Pair
|
|
54
|
-
console.log(`
|
|
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());
|
package/src/commands/pair.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
+
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -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
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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 {
|