palmier 0.6.6 → 0.6.7
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 +1 -1
- package/dist/agents/agent-instructions.md +28 -6
- package/dist/commands/plan-generation.md +1 -0
- package/dist/commands/run.js +3 -3
- package/dist/location-device.d.ts +8 -0
- package/dist/location-device.js +32 -0
- package/dist/mcp-handler.d.ts +8 -0
- package/dist/mcp-handler.js +110 -0
- package/dist/mcp-tools.d.ts +22 -0
- package/dist/mcp-tools.js +152 -0
- package/dist/pwa/assets/{index-DhvJN8ie.css → index-DAI3J-jU.css} +1 -1
- package/dist/pwa/assets/index-RrJvjqz9.js +118 -0
- package/dist/pwa/assets/web-DQteXlI7.js +1 -0
- package/dist/pwa/assets/web-EzNEHXEh.js +1 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +20 -7
- package/dist/transports/http-transport.js +61 -129
- package/package.json +1 -1
- package/palmier-server/README.md +6 -1
- package/palmier-server/package.json +7 -1
- package/palmier-server/pnpm-lock.yaml +1025 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/package.json +3 -0
- package/palmier-server/pwa/src/App.css +55 -0
- package/palmier-server/pwa/src/api.ts +8 -2
- package/palmier-server/pwa/src/components/HostMenu.tsx +102 -1
- package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +2 -1
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +3 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +5 -2
- package/palmier-server/pwa/src/pages/PairHost.tsx +10 -1
- package/palmier-server/pwa/src/service-worker.ts +7 -7
- package/palmier-server/server/.env.example +4 -0
- package/palmier-server/server/package.json +1 -0
- package/palmier-server/server/src/db.ts +10 -0
- package/palmier-server/server/src/fcm.ts +74 -0
- package/palmier-server/server/src/index.ts +101 -21
- package/palmier-server/server/src/notify.ts +34 -0
- package/palmier-server/server/src/push.ts +1 -1
- package/palmier-server/server/src/routes/fcm.ts +64 -0
- package/palmier-server/server/src/routes/push.ts +6 -5
- package/palmier-server/spec.md +4 -2
- package/src/agents/agent-instructions.md +28 -6
- package/src/commands/plan-generation.md +1 -0
- package/src/commands/run.ts +3 -3
- package/src/location-device.ts +35 -0
- package/src/mcp-handler.ts +133 -0
- package/src/mcp-tools.ts +182 -0
- package/src/rpc-handler.ts +21 -7
- package/src/transports/http-transport.ts +58 -128
- package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{W as t}from"./index-RrJvjqz9.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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{W as p}from"./index-RrJvjqz9.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
|
package/dist/pwa/index.html
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<title>Palmier</title>
|
|
10
|
-
<meta name="description" content="
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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-RrJvjqz9.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DAI3J-jU.css">
|
|
13
13
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
try{self["workbox:core:7.3.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(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=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),I=a=>{for(const e of Object.keys(f))a(e)},L={updateDetails:a=>{I(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||U(f.googleAnalytics),getPrecacheName:a=>a||U(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||U(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=e();return a.waitUntil(t),t}try{self["workbox:precaching:7.3.0"]&&_()}catch{}const E="__WB_REVISION__";function O(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const i=new URL(a,location.href);return{cacheKey:i.href,url:i.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const i=new URL(t,location.href);return{cacheKey:i.href,url:i.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(E,e),{cacheKey:s.href,url:n.href}}class A{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 n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class M{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}let y;function W(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),i={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},r=W()?s.body:await s.blob();return new Response(r,i)}const S=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function K(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function j(a,e,t,s){const n=K(e.url,t);if(e.url===n)return a.match(e,s);const i=Object.assign(Object.assign({},s),{ignoreSearch:!0}),r=await a.keys(e,i);for(const c of r){const o=K(c.url,t);if(n===o)return a.match(c,s)}}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}const H=new Set;async function F(){for(const a of H)await a()}function B(a){return new Promise(e=>setTimeout(e,a))}try{self["workbox:strategies:7.3.0"]&&_()}catch{}function R(a){return typeof a=="string"?new Request(a):a}class ${constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,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=R(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const r=await t.preloadResponse;if(r)return r}const n=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 l("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 n&&await this.runCallbacks("fetchDidFail",{error:r,event:t,originalRequest:n.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=R(e);let s;const{cacheName:n,matchOptions:i}=this._strategy,r=await this.getCacheKey(t,"read"),c=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,c);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(e,t){const s=R(e);await B(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:S(n.url)});const i=await this._ensureResponseSafeToCache(t);if(!i)return!1;const{cacheName:r,matchOptions:c}=this._strategy,o=await self.caches.open(r),h=this.hasCallback("cacheDidUpdate"),p=h?await j(o,n.clone(),["__WB_REVISION__"],c):null;try{await o.put(n,h?i.clone():i)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await F(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:r,oldResponse:p,newResponse:i.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const i of this.iterateCallbacks("cacheKeyWillBeUsed"))n=R(await i({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}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(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({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 V{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,n="params"in e?e.params:void 0,i=new $(this,{event:t,request:s,params:n}),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 n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(i){if(i instanceof Error){for(const r of e.iterateCallbacks("handlerDidError"))if(n=await r({error:i,event:s,request:t}),n)break}if(!n)throw i}for(const i of e.iterateCallbacks("handlerWillRespond"))n=await i({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let i,r;try{i=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await t.doneWaiting()}catch(c){c instanceof Error&&(r=c)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),t.destroy(),r)throw r}}class d extends V{constructor(e={}){e.cacheName=L.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.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 n=t.params||{};if(this._fallbackToNetwork){const i=n.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 l("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 l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};class G{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new d({cacheName:L.getPrecacheName(e),plugins:[...t,new M({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:n,url:i}=O(s),r=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:i});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(i,n),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
|
|
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":"f31d164733f432f4807c770bdfc626b2","url":"index.html"},{"revision":null,"url":"assets/web-EzNEHXEh.js"},{"revision":null,"url":"assets/web-DQteXlI7.js"},{"revision":null,"url":"assets/index-RrJvjqz9.js"},{"revision":null,"url":"assets/index-DAI3J-jU.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
|
@@ -12,6 +12,7 @@ import crossSpawn from "cross-spawn";
|
|
|
12
12
|
import { getAgent } from "./agents/agent.js";
|
|
13
13
|
import { validateClient } from "./client-store.js";
|
|
14
14
|
import { publishHostEvent } from "./events.js";
|
|
15
|
+
import { getLocationDevice, setLocationDevice, clearLocationDevice } from "./location-device.js";
|
|
15
16
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
16
17
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -142,25 +143,27 @@ export function createRpcHandler(config, nc) {
|
|
|
142
143
|
body: task.body,
|
|
143
144
|
status: status ? {
|
|
144
145
|
...status,
|
|
145
|
-
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
146
146
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
147
|
-
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
148
147
|
} : undefined,
|
|
149
148
|
};
|
|
150
149
|
}
|
|
151
150
|
async function handleRpc(request) {
|
|
152
|
-
// Client token validation: skip for trusted localhost requests
|
|
153
|
-
|
|
151
|
+
// Client token validation: skip for trusted localhost requests and
|
|
152
|
+
// task.user_input (server-originated push responses; gated by getPending instead)
|
|
153
|
+
const skipAuth = request.method === "task.user_input";
|
|
154
|
+
if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
154
155
|
return { error: "Unauthorized" };
|
|
155
156
|
}
|
|
156
157
|
switch (request.method) {
|
|
157
158
|
case "task.list": {
|
|
158
159
|
const tasks = listTasks(config.projectRoot);
|
|
160
|
+
const locDevice = getLocationDevice();
|
|
159
161
|
return {
|
|
160
162
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
161
163
|
agents: config.agents ?? [],
|
|
162
164
|
version: currentVersion,
|
|
163
165
|
host_platform: process.platform,
|
|
166
|
+
location_client_token: locDevice?.clientToken ?? null,
|
|
164
167
|
};
|
|
165
168
|
}
|
|
166
169
|
case "task.get": {
|
|
@@ -367,7 +370,7 @@ export function createRpcHandler(config, nc) {
|
|
|
367
370
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
368
371
|
cwd: followupRunDir,
|
|
369
372
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
370
|
-
env: { ...process.env, ...followupAgentEnv
|
|
373
|
+
env: { ...process.env, ...followupAgentEnv },
|
|
371
374
|
windowsHide: true,
|
|
372
375
|
});
|
|
373
376
|
if (stdin != null)
|
|
@@ -498,9 +501,7 @@ export function createRpcHandler(config, nc) {
|
|
|
498
501
|
return {
|
|
499
502
|
task_id: params.id,
|
|
500
503
|
...status,
|
|
501
|
-
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
502
504
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
503
|
-
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
504
505
|
};
|
|
505
506
|
}
|
|
506
507
|
case "task.result": {
|
|
@@ -604,6 +605,18 @@ export function createRpcHandler(config, nc) {
|
|
|
604
605
|
return { error };
|
|
605
606
|
return { ok: true };
|
|
606
607
|
}
|
|
608
|
+
case "device.location.enable": {
|
|
609
|
+
const params = request.params;
|
|
610
|
+
if (!params.fcmToken)
|
|
611
|
+
return { error: "fcmToken is required" };
|
|
612
|
+
const clientToken = request.clientToken ?? "";
|
|
613
|
+
setLocationDevice(clientToken, params.fcmToken);
|
|
614
|
+
return { ok: true };
|
|
615
|
+
}
|
|
616
|
+
case "device.location.disable": {
|
|
617
|
+
clearLocationDevice();
|
|
618
|
+
return { ok: true };
|
|
619
|
+
}
|
|
607
620
|
default:
|
|
608
621
|
return { error: `Unknown method: ${request.method}` };
|
|
609
622
|
}
|
|
@@ -5,7 +5,9 @@ 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 {
|
|
8
|
+
import { agentToolMap, ToolError } from "../mcp-tools.js";
|
|
9
|
+
import { handleMcpRequest, getAgentName } from "../mcp-handler.js";
|
|
10
|
+
import { getTaskDir } from "../task.js";
|
|
9
11
|
const assetCache = new Map();
|
|
10
12
|
const PWA_DIR = path.join(import.meta.dirname, "..", "pwa");
|
|
11
13
|
const CONTENT_TYPES = {
|
|
@@ -65,18 +67,6 @@ export function detectLanIp() {
|
|
|
65
67
|
}
|
|
66
68
|
return "127.0.0.1";
|
|
67
69
|
}
|
|
68
|
-
/** Find the latest (highest-numbered) run directory for a task. */
|
|
69
|
-
function findLatestRunId(taskDir) {
|
|
70
|
-
try {
|
|
71
|
-
const dirs = fs.readdirSync(taskDir)
|
|
72
|
-
.filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
|
|
73
|
-
.sort();
|
|
74
|
-
return dirs.length > 0 ? dirs[dirs.length - 1] : null;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
70
|
/**
|
|
81
71
|
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
82
72
|
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
@@ -136,9 +126,67 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
136
126
|
}
|
|
137
127
|
broadcastSseEvent({ task_id: taskId, ...payload });
|
|
138
128
|
}
|
|
129
|
+
function makeToolContext(sessionId) {
|
|
130
|
+
return { config, nc, publishEvent, sessionId, agentName: getAgentName(sessionId) };
|
|
131
|
+
}
|
|
139
132
|
const server = http.createServer(async (req, res) => {
|
|
140
133
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
141
134
|
const pathname = url.pathname;
|
|
135
|
+
// ── MCP streamable HTTP endpoint ──────────────────────────────────
|
|
136
|
+
if (req.method === "POST" && pathname === "/mcp") {
|
|
137
|
+
if (!isLocalhost(req)) {
|
|
138
|
+
sendJson(res, 403, { error: "localhost only" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const body = await readBody(req);
|
|
143
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
144
|
+
const ctx = makeToolContext(sessionId ?? "");
|
|
145
|
+
const result = await handleMcpRequest(body, sessionId, ctx);
|
|
146
|
+
if (result.sessionId) {
|
|
147
|
+
res.setHeader("Mcp-Session-Id", result.sessionId);
|
|
148
|
+
}
|
|
149
|
+
sendJson(res, 200, result.body);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
sendJson(res, 500, { error: String(err) });
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// ── Auto-generated REST endpoints from MCP tool registry ──────────
|
|
157
|
+
if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
|
|
158
|
+
if (!isLocalhost(req)) {
|
|
159
|
+
sendJson(res, 403, { error: "localhost only" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const tool = agentToolMap.get(pathname.slice(1));
|
|
163
|
+
try {
|
|
164
|
+
const body = await readBody(req);
|
|
165
|
+
const args = body.trim() ? JSON.parse(body) : {};
|
|
166
|
+
const { taskId } = args;
|
|
167
|
+
if (!taskId) {
|
|
168
|
+
sendJson(res, 400, { error: "taskId is required" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
172
|
+
if (!fs.existsSync(taskDir)) {
|
|
173
|
+
sendJson(res, 404, { error: `Task not found: ${taskId}` });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
delete args.taskId;
|
|
177
|
+
const ctx = makeToolContext(taskId);
|
|
178
|
+
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
|
|
179
|
+
const result = await tool.handler(args, ctx);
|
|
180
|
+
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name} done:`, JSON.stringify(result).slice(0, 200));
|
|
181
|
+
sendJson(res, 200, result);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const status = err instanceof ToolError ? err.statusCode : 500;
|
|
185
|
+
console.error(`[mcp] REST ${tool.name} error:`, err.message ?? String(err));
|
|
186
|
+
sendJson(res, status, { error: err.message ?? String(err) });
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
142
190
|
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
143
191
|
if (req.method === "POST" && pathname === "/event") {
|
|
144
192
|
if (!isLocalhost(req)) {
|
|
@@ -192,122 +240,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
192
240
|
}
|
|
193
241
|
return;
|
|
194
242
|
}
|
|
195
|
-
// ── POST /notify — send push notification via NATS ─────────────────
|
|
196
|
-
if (req.method === "POST" && pathname === "/notify") {
|
|
197
|
-
if (!isLocalhost(req)) {
|
|
198
|
-
sendJson(res, 403, { error: "localhost only" });
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (!nc) {
|
|
202
|
-
sendJson(res, 503, { error: "NATS not connected — push notifications require server mode" });
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
try {
|
|
206
|
-
const body = await readBody(req);
|
|
207
|
-
const { taskId: notifTaskId, title, body: notifBody } = JSON.parse(body);
|
|
208
|
-
if (!title || !notifBody) {
|
|
209
|
-
sendJson(res, 400, { error: "title and body are required" });
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const sc = StringCodec();
|
|
213
|
-
const payload = { hostId: config.hostId, title, body: notifBody };
|
|
214
|
-
if (notifTaskId)
|
|
215
|
-
payload.task_id = notifTaskId;
|
|
216
|
-
const subject = `host.${config.hostId}.push.send`;
|
|
217
|
-
const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 15_000 });
|
|
218
|
-
const result = JSON.parse(sc.decode(reply.data));
|
|
219
|
-
if (result.ok) {
|
|
220
|
-
sendJson(res, 200, { ok: true });
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
sendJson(res, 502, { error: result.error ?? "Push notification failed" });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
sendJson(res, 500, { error: `Failed to send notification: ${err}` });
|
|
228
|
-
}
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// ── POST /request-input — held connection until user responds ────────
|
|
232
|
-
if (req.method === "POST" && pathname === "/request-input") {
|
|
233
|
-
if (!isLocalhost(req)) {
|
|
234
|
-
sendJson(res, 403, { error: "localhost only" });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const body = await readBody(req);
|
|
239
|
-
const { taskId, runId, descriptions } = JSON.parse(body);
|
|
240
|
-
if (!taskId || !descriptions?.length) {
|
|
241
|
-
sendJson(res, 400, { error: "taskId and descriptions are required" });
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
245
|
-
const task = parseTaskFile(taskDir);
|
|
246
|
-
// Resolve runId: use provided value, otherwise find the latest run directory
|
|
247
|
-
const effectiveRunId = runId ?? findLatestRunId(taskDir);
|
|
248
|
-
const pendingPromise = registerPending(taskId, "input", descriptions);
|
|
249
|
-
await publishEvent(taskId, {
|
|
250
|
-
event_type: "input-request",
|
|
251
|
-
host_id: config.hostId,
|
|
252
|
-
input_descriptions: descriptions,
|
|
253
|
-
name: task.frontmatter.name,
|
|
254
|
-
});
|
|
255
|
-
const response = await pendingPromise;
|
|
256
|
-
const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
|
|
257
|
-
if (response.length === 1 && response[0] === "aborted") {
|
|
258
|
-
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
|
|
259
|
-
if (effectiveRunId) {
|
|
260
|
-
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
|
|
261
|
-
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
262
|
-
}
|
|
263
|
-
sendJson(res, 200, { aborted: true });
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
|
|
267
|
-
if (effectiveRunId) {
|
|
268
|
-
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
|
|
269
|
-
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
270
|
-
}
|
|
271
|
-
sendJson(res, 200, { values: response });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
sendJson(res, 500, { error: String(err) });
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
// ── POST /request-confirmation — held connection ────────────────────
|
|
280
|
-
if (req.method === "POST" && pathname === "/request-confirmation") {
|
|
281
|
-
if (!isLocalhost(req)) {
|
|
282
|
-
sendJson(res, 403, { error: "localhost only" });
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
const body = await readBody(req);
|
|
287
|
-
const { taskId } = JSON.parse(body);
|
|
288
|
-
if (!taskId) {
|
|
289
|
-
sendJson(res, 400, { error: "taskId is required" });
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
const pendingPromise = registerPending(taskId, "confirmation");
|
|
293
|
-
await publishEvent(taskId, {
|
|
294
|
-
event_type: "confirm-request",
|
|
295
|
-
host_id: config.hostId,
|
|
296
|
-
});
|
|
297
|
-
const response = await pendingPromise;
|
|
298
|
-
const confirmed = response[0] === "confirmed";
|
|
299
|
-
await publishEvent(taskId, {
|
|
300
|
-
event_type: "confirm-resolved",
|
|
301
|
-
host_id: config.hostId,
|
|
302
|
-
status: confirmed ? "confirmed" : "aborted",
|
|
303
|
-
});
|
|
304
|
-
sendJson(res, 200, { confirmed });
|
|
305
|
-
}
|
|
306
|
-
catch (err) {
|
|
307
|
-
sendJson(res, 500, { error: String(err) });
|
|
308
|
-
}
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
243
|
// ── POST /request-permission — held connection ──────────────────────
|
|
312
244
|
if (req.method === "POST" && pathname === "/request-permission") {
|
|
313
245
|
if (!isLocalhost(req)) {
|
package/package.json
CHANGED
package/palmier-server/README.md
CHANGED
|
@@ -28,7 +28,8 @@ Palmier is a platform for remotely scheduling, managing, and executing autonomou
|
|
|
28
28
|
- **PWA** -- React 19 + Vite progressive web app. Connects to NATS over WebSocket for real-time task updates and to the web server for host registration and push notifications. No user accounts — paired hosts are stored in localStorage.
|
|
29
29
|
- **Web Server** -- Express + TypeScript API server. Handles host registration, push notifications (subscribes to `host-event.>` pub/sub for confirmation and completion events), and push notification relay (for host CLI requests via NATS). In production, also serves the built PWA static files.
|
|
30
30
|
- **NATS Server** -- Message broker. Provides pub/sub messaging and request-reply for real-time communication between all components.
|
|
31
|
-
- **Host** -- Runs on remote Linux/Windows machines to execute tasks via pluggable agent tools (e.g., Claude Code, Codex, Gemini). Each agent implements an `AgentTool` interface that handles command construction. Communicates with the platform over NATS and exposes a local HTTP server with agent-facing endpoints (`/notify`, `/request-input`) for task execution flows. See the [palmier](https://github.com/caihongxu/palmier) repo.
|
|
31
|
+
- **Host** -- Runs on remote Linux/Windows machines to execute tasks via pluggable agent tools (e.g., Claude Code, Codex, Gemini). Each agent implements an `AgentTool` interface that handles command construction. Communicates with the platform over NATS and exposes a local HTTP server with agent-facing endpoints (`/notify`, `/request-input`, `/device-geolocation`) for task execution flows. See the [palmier](https://github.com/caihongxu/palmier) repo.
|
|
32
|
+
- **Android App** -- Native Android wrapper (Capacitor) for the PWA. Provides FCM push messaging and background GPS access. The server sends FCM data messages to wake the device for geolocation requests. See the [palmier-android](https://github.com/caihongxu/palmier-android) repo.
|
|
32
33
|
|
|
33
34
|
## Prerequisites
|
|
34
35
|
|
|
@@ -145,6 +146,7 @@ The host runs on a separate Linux machine. See the [palmier README](https://gith
|
|
|
145
146
|
| `VAPID_PUBLIC_KEY` | VAPID public key for web push | *(generated via web-push)* |
|
|
146
147
|
| `VAPID_PRIVATE_KEY` | VAPID private key for web push | *(generated via web-push)* |
|
|
147
148
|
| `VAPID_MAILTO` | Contact email for VAPID | `mailto:admin@example.com` |
|
|
149
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to Firebase service account JSON (for FCM) | `/path/to/service-account.json` |
|
|
148
150
|
|
|
149
151
|
> **LAN setup note:** `NATS_URL` uses `localhost` because the server connects to NATS locally. `NATS_HOST_URL`, `NATS_WS_URL` must use the LAN IP so remote hosts and browsers can reach them. Firewall must allow inbound on ports 3000 (HTTP), 4222 (NATS TCP), 5173 (Vite dev), and 9222 (NATS WebSocket).
|
|
150
152
|
|
|
@@ -160,6 +162,8 @@ All endpoints are prefixed with `/api`. No user authentication is required.
|
|
|
160
162
|
| `DELETE` | `/api/push/subscribe` | Remove a push notification subscription |
|
|
161
163
|
| `GET` | `/api/push/vapid-key` | Get the VAPID public key |
|
|
162
164
|
| `POST` | `/api/push/respond` | Respond to a pending task confirmation via push notification |
|
|
165
|
+
| `POST` | `/api/fcm/register` | Register an FCM token for a host (Android device) |
|
|
166
|
+
| `POST` | `/api/fcm/geolocation-response` | Receive device location from Android, forward via NATS |
|
|
163
167
|
| `GET` | `/health` | Health check |
|
|
164
168
|
|
|
165
169
|
|
|
@@ -185,3 +189,4 @@ All endpoints are prefixed with `/api`. No user authentication is required.
|
|
|
185
189
|
## Related Repositories
|
|
186
190
|
|
|
187
191
|
- [palmier](https://github.com/caihongxu/palmier) -- The host binary, published as `palmier` on npm. Install with `npm install -g palmier`. Uses npm (not pnpm).
|
|
192
|
+
- [palmier-android](https://github.com/caihongxu/palmier-android) -- Native Android wrapper (Capacitor) for the PWA. Provides FCM and background GPS.
|