palmier 0.6.6 → 0.6.8
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 +15 -1
- package/dist/agents/agent-instructions.md +6 -14
- package/dist/agents/aider.js +1 -1
- package/dist/agents/claude.js +1 -1
- package/dist/agents/cline.js +1 -1
- package/dist/agents/codex.js +1 -1
- package/dist/agents/copilot.js +1 -1
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/droid.js +1 -1
- package/dist/agents/gemini.js +1 -1
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kimi.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/openclaw.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/qwen.js +1 -1
- package/dist/agents/shared-prompt.d.ts +3 -2
- package/dist/agents/shared-prompt.js +6 -4
- package/dist/commands/plan-generation.md +1 -0
- package/dist/commands/run.js +4 -7
- 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 +27 -0
- package/dist/mcp-tools.js +218 -0
- package/dist/pwa/assets/{index-DhvJN8ie.css → index-C6Lz09EY.css} +1 -1
- package/dist/pwa/assets/index-C8vJwUNi.js +118 -0
- package/dist/pwa/assets/web-6UChJFov.js +1 -0
- package/dist/pwa/assets/web-NxTETXZK.js +1 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +20 -8
- package/dist/spawn-command.js +3 -1
- package/dist/transports/http-transport.js +60 -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 +64 -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/TaskCard.tsx +36 -8
- package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
- package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
- package/palmier-server/pwa/src/constants.ts +1 -1
- 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 +6 -14
- package/src/agents/aider.ts +1 -1
- package/src/agents/claude.ts +1 -1
- package/src/agents/cline.ts +1 -1
- package/src/agents/codex.ts +1 -1
- package/src/agents/copilot.ts +1 -1
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/droid.ts +1 -1
- package/src/agents/gemini.ts +1 -1
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kimi.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/openclaw.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/qwen.ts +1 -1
- package/src/agents/shared-prompt.ts +7 -4
- package/src/commands/plan-generation.md +1 -0
- package/src/commands/run.ts +4 -7
- package/src/location-device.ts +35 -0
- package/src/mcp-handler.ts +133 -0
- package/src/mcp-tools.ts +253 -0
- package/src/rpc-handler.ts +21 -8
- package/src/spawn-command.ts +3 -1
- package/src/transports/http-transport.ts +57 -128
- package/test/agent-instructions.test.ts +68 -5
- package/test/fixtures/agent-instructions-snapshot.md +58 -0
- package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{W as t}from"./index-C8vJwUNi.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-C8vJwUNi.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-C8vJwUNi.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C6Lz09EY.css">
|
|
13
13
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
|
14
14
|
<body>
|
|
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":"e99cf000b780427d2b7a5907aa6778ea","url":"index.html"},{"revision":null,"url":"assets/web-NxTETXZK.js"},{"revision":null,"url":"assets/web-6UChJFov.js"},{"revision":null,"url":"assets/index-C8vJwUNi.js"},{"revision":null,"url":"assets/index-C6Lz09EY.css"},{"revision":"fcc457fce855ad0df7178e0786c0d4ef","url":"apple-touch-icon.png"},{"revision":"276650c30bc4effc7d649ec66519aab6","url":"favicon.ico"},{"revision":"2e46512b835c05e17787059909305f22","url":"pwa-192x192.png"},{"revision":"ec5652b5834b4711337743e80e506a41","url":"pwa-512x512.png"},{"revision":"9f51698004b9cc4d787c75695b74de9d","url":"manifest.webmanifest"}]);const re="/api/push/respond";self.addEventListener("message",n=>{});self.addEventListener("push",n=>{var r;if(!n.data)return;let e;try{e=n.data.json()}catch{e={title:"Palmier",body:n.data.text()}}const t=e.type??((r=e.data)==null?void 0:r.type);if(t==="confirm-dismiss"||t==="permission-dismiss"||t==="input-dismiss"){const c=e.data??e,o=c.host_id,l=c.session_id,d=c.task_id;n.waitUntil(self.registration.getNotifications().then(u=>{var g,P,K;for(const y of u)if(((g=y.data)==null?void 0:g.host_id)===o){if(l&&((P=y.data)==null?void 0:P.session_id)===l){y.close();continue}d&&((K=y.data)==null?void 0:K.task_id)===d&&y.close()}}));return}const s=e.title??"Palmier";let a=e.body??"";!a&&t==="confirm"&&(a="A task requires confirmation to run."),!a&&t==="permission"&&(a="A task needs additional permissions to continue."),!a&&t==="input"&&(a="A task needs your input to continue.");const i={body:a,icon:"/pwa-192x192.png",badge:"/pwa-192x192.png",data:e.data??e,vibrate:[100,50,100]};t==="confirm"&&(i.actions=[{action:"confirm",title:"Confirm"},{action:"abort",title:"Abort"}]),n.waitUntil(self.registration.showNotification(s,i))});self.addEventListener("notificationclick",n=>{const e=n.notification;e.close();const t=e.data??{},s=n.action;if(s&&t.type==="confirm"&&t.session_id&&t.host_id){const a=s==="confirm"?"confirmed":"aborted";n.waitUntil(fetch(re,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:t.session_id,host_id:t.host_id,response:a})}).catch(i=>{console.error("Failed to send push response:",i)}))}else{const a=t.task_id,i=t.run_id,r=a&&i?`/runs/${encodeURIComponent(a)}/${encodeURIComponent(i)}`:a?`/runs/${encodeURIComponent(a)}/latest`:"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(c=>{for(const o of c)if(o.url.includes(self.location.origin)&&"focus"in o)return o.navigate(r),o.focus();return self.clients.openWindow(r)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
|
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));
|
|
@@ -104,7 +105,6 @@ async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
|
104
105
|
const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
|
|
105
106
|
const planAgent = getAgent(agentName);
|
|
106
107
|
const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
107
|
-
console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
|
|
108
108
|
const { output } = await spawnCommand(command, args, {
|
|
109
109
|
cwd: projectRoot,
|
|
110
110
|
timeout: 120_000,
|
|
@@ -142,25 +142,27 @@ export function createRpcHandler(config, nc) {
|
|
|
142
142
|
body: task.body,
|
|
143
143
|
status: status ? {
|
|
144
144
|
...status,
|
|
145
|
-
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
146
145
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
147
|
-
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
148
146
|
} : undefined,
|
|
149
147
|
};
|
|
150
148
|
}
|
|
151
149
|
async function handleRpc(request) {
|
|
152
|
-
// Client token validation: skip for trusted localhost requests
|
|
153
|
-
|
|
150
|
+
// Client token validation: skip for trusted localhost requests and
|
|
151
|
+
// task.user_input (server-originated push responses; gated by getPending instead)
|
|
152
|
+
const skipAuth = request.method === "task.user_input";
|
|
153
|
+
if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
154
154
|
return { error: "Unauthorized" };
|
|
155
155
|
}
|
|
156
156
|
switch (request.method) {
|
|
157
157
|
case "task.list": {
|
|
158
158
|
const tasks = listTasks(config.projectRoot);
|
|
159
|
+
const locDevice = getLocationDevice();
|
|
159
160
|
return {
|
|
160
161
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
161
162
|
agents: config.agents ?? [],
|
|
162
163
|
version: currentVersion,
|
|
163
164
|
host_platform: process.platform,
|
|
165
|
+
location_client_token: locDevice?.clientToken ?? null,
|
|
164
166
|
};
|
|
165
167
|
}
|
|
166
168
|
case "task.get": {
|
|
@@ -367,7 +369,7 @@ export function createRpcHandler(config, nc) {
|
|
|
367
369
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
368
370
|
cwd: followupRunDir,
|
|
369
371
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
370
|
-
env: { ...process.env, ...followupAgentEnv
|
|
372
|
+
env: { ...process.env, ...followupAgentEnv },
|
|
371
373
|
windowsHide: true,
|
|
372
374
|
});
|
|
373
375
|
if (stdin != null)
|
|
@@ -498,9 +500,7 @@ export function createRpcHandler(config, nc) {
|
|
|
498
500
|
return {
|
|
499
501
|
task_id: params.id,
|
|
500
502
|
...status,
|
|
501
|
-
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
502
503
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
503
|
-
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
506
|
case "task.result": {
|
|
@@ -604,6 +604,18 @@ export function createRpcHandler(config, nc) {
|
|
|
604
604
|
return { error };
|
|
605
605
|
return { ok: true };
|
|
606
606
|
}
|
|
607
|
+
case "device.location.enable": {
|
|
608
|
+
const params = request.params;
|
|
609
|
+
if (!params.fcmToken)
|
|
610
|
+
return { error: "fcmToken is required" };
|
|
611
|
+
const clientToken = request.clientToken ?? "";
|
|
612
|
+
setLocationDevice(clientToken, params.fcmToken);
|
|
613
|
+
return { ok: true };
|
|
614
|
+
}
|
|
615
|
+
case "device.location.disable": {
|
|
616
|
+
clearLocationDevice();
|
|
617
|
+
return { ok: true };
|
|
618
|
+
}
|
|
607
619
|
default:
|
|
608
620
|
return { error: `Unknown method: ${request.method}` };
|
|
609
621
|
}
|
package/dist/spawn-command.js
CHANGED
|
@@ -42,7 +42,9 @@ export function spawnCommand(command, args, opts) {
|
|
|
42
42
|
const finalArgs = process.platform === "win32"
|
|
43
43
|
? args.map((a) => a.replace(/[\r\n]+/g, " "))
|
|
44
44
|
: args;
|
|
45
|
-
|
|
45
|
+
const truncate = (s, max = 100) => s.length > max ? s.slice(0, max) + "..." : s;
|
|
46
|
+
const displayArgs = finalArgs.map((arg) => truncate(arg));
|
|
47
|
+
console.log(`[spawn] ${command} ${displayArgs.join(" ")}`);
|
|
46
48
|
const child = crossSpawn(command, finalArgs, {
|
|
47
49
|
cwd: opts.cwd,
|
|
48
50
|
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
@@ -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,66 @@ 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 taskId = url.searchParams.get("taskId");
|
|
165
|
+
if (!taskId) {
|
|
166
|
+
sendJson(res, 400, { error: "taskId query parameter is required" });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
170
|
+
if (!fs.existsSync(taskDir)) {
|
|
171
|
+
sendJson(res, 404, { error: `Task not found: ${taskId}` });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const body = await readBody(req);
|
|
175
|
+
const args = body.trim() ? JSON.parse(body) : {};
|
|
176
|
+
const ctx = makeToolContext(taskId);
|
|
177
|
+
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
|
|
178
|
+
const result = await tool.handler(args, ctx);
|
|
179
|
+
console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name} done:`, JSON.stringify(result).slice(0, 200));
|
|
180
|
+
sendJson(res, 200, result);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const status = err instanceof ToolError ? err.statusCode : 500;
|
|
184
|
+
console.error(`[mcp] REST ${tool.name} error:`, err.message ?? String(err));
|
|
185
|
+
sendJson(res, status, { error: err.message ?? String(err) });
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
142
189
|
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
143
190
|
if (req.method === "POST" && pathname === "/event") {
|
|
144
191
|
if (!isLocalhost(req)) {
|
|
@@ -192,122 +239,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
192
239
|
}
|
|
193
240
|
return;
|
|
194
241
|
}
|
|
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
242
|
// ── POST /request-permission — held connection ──────────────────────
|
|
312
243
|
if (req.method === "POST" && pathname === "/request-permission") {
|
|
313
244
|
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
|
|
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 MCP server (`/mcp`, streamable HTTP) and auto-generated REST endpoints for agent-facing tools (`/notify`, `/request-input`, `/request-confirmation`, `/device-geolocation`). 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.
|