palmier 0.6.7 → 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.
Files changed (68) hide show
  1. package/README.md +14 -0
  2. package/dist/agents/agent-instructions.md +7 -37
  3. package/dist/agents/aider.js +1 -1
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/cline.js +1 -1
  6. package/dist/agents/codex.js +1 -1
  7. package/dist/agents/copilot.js +1 -1
  8. package/dist/agents/cursor.js +1 -1
  9. package/dist/agents/deepagents.js +1 -1
  10. package/dist/agents/droid.js +1 -1
  11. package/dist/agents/gemini.js +1 -1
  12. package/dist/agents/goose.js +1 -1
  13. package/dist/agents/hermes.js +1 -1
  14. package/dist/agents/kimi.js +1 -1
  15. package/dist/agents/kiro.js +1 -1
  16. package/dist/agents/openclaw.js +1 -1
  17. package/dist/agents/opencode.js +1 -1
  18. package/dist/agents/qoder.js +1 -1
  19. package/dist/agents/qwen.js +1 -1
  20. package/dist/agents/shared-prompt.d.ts +3 -2
  21. package/dist/agents/shared-prompt.js +6 -4
  22. package/dist/commands/run.js +2 -5
  23. package/dist/mcp-handler.js +1 -1
  24. package/dist/mcp-tools.d.ts +6 -1
  25. package/dist/mcp-tools.js +72 -6
  26. package/dist/pwa/assets/{index-DAI3J-jU.css → index-C6Lz09EY.css} +1 -1
  27. package/dist/pwa/assets/{index-RrJvjqz9.js → index-C8vJwUNi.js} +42 -42
  28. package/dist/pwa/assets/{web-DQteXlI7.js → web-6UChJFov.js} +1 -1
  29. package/dist/pwa/assets/{web-EzNEHXEh.js → web-NxTETXZK.js} +1 -1
  30. package/dist/pwa/index.html +2 -2
  31. package/dist/pwa/service-worker.js +1 -1
  32. package/dist/rpc-handler.js +0 -1
  33. package/dist/spawn-command.js +3 -1
  34. package/dist/transports/http-transport.js +4 -5
  35. package/package.json +1 -1
  36. package/palmier-server/README.md +1 -1
  37. package/palmier-server/pwa/src/App.css +9 -0
  38. package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
  39. package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
  40. package/palmier-server/pwa/src/constants.ts +1 -1
  41. package/palmier-server/spec.md +1 -1
  42. package/src/agents/agent-instructions.md +7 -37
  43. package/src/agents/aider.ts +1 -1
  44. package/src/agents/claude.ts +1 -1
  45. package/src/agents/cline.ts +1 -1
  46. package/src/agents/codex.ts +1 -1
  47. package/src/agents/copilot.ts +1 -1
  48. package/src/agents/cursor.ts +1 -1
  49. package/src/agents/deepagents.ts +1 -1
  50. package/src/agents/droid.ts +1 -1
  51. package/src/agents/gemini.ts +1 -1
  52. package/src/agents/goose.ts +1 -1
  53. package/src/agents/hermes.ts +1 -1
  54. package/src/agents/kimi.ts +1 -1
  55. package/src/agents/kiro.ts +1 -1
  56. package/src/agents/openclaw.ts +1 -1
  57. package/src/agents/opencode.ts +1 -1
  58. package/src/agents/qoder.ts +1 -1
  59. package/src/agents/qwen.ts +1 -1
  60. package/src/agents/shared-prompt.ts +7 -4
  61. package/src/commands/run.ts +2 -5
  62. package/src/mcp-handler.ts +1 -1
  63. package/src/mcp-tools.ts +78 -7
  64. package/src/rpc-handler.ts +0 -1
  65. package/src/spawn-command.ts +3 -1
  66. package/src/transports/http-transport.ts +4 -5
  67. package/test/agent-instructions.test.ts +68 -5
  68. package/test/fixtures/agent-instructions-snapshot.md +58 -0
@@ -1 +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};
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};
@@ -1 +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};
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};
@@ -8,8 +8,8 @@
8
8
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
9
  <title>Palmier</title>
10
10
  <meta name="description" content="Remote control for AI agents running on your own machine. Schedule tasks, approve permissions, and get push notifications." />
11
- <script type="module" crossorigin src="/assets/index-RrJvjqz9.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DAI3J-jU.css">
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
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())});
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())});
@@ -105,7 +105,6 @@ async function generatePlan(projectRoot, userPrompt, agentName) {
105
105
  const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
106
106
  const planAgent = getAgent(agentName);
107
107
  const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
108
- console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
109
108
  const { output } = await spawnCommand(command, args, {
110
109
  cwd: projectRoot,
111
110
  timeout: 120_000,
@@ -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
- // console.log(`[spawn] ${command} ${finalArgs.join(" ")}`);
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"],
@@ -161,11 +161,9 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
161
161
  }
162
162
  const tool = agentToolMap.get(pathname.slice(1));
163
163
  try {
164
- const body = await readBody(req);
165
- const args = body.trim() ? JSON.parse(body) : {};
166
- const { taskId } = args;
164
+ const taskId = url.searchParams.get("taskId");
167
165
  if (!taskId) {
168
- sendJson(res, 400, { error: "taskId is required" });
166
+ sendJson(res, 400, { error: "taskId query parameter is required" });
169
167
  return;
170
168
  }
171
169
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -173,7 +171,8 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
173
171
  sendJson(res, 404, { error: `Task not found: ${taskId}` });
174
172
  return;
175
173
  }
176
- delete args.taskId;
174
+ const body = await readBody(req);
175
+ const args = body.trim() ? JSON.parse(body) : {};
177
176
  const ctx = makeToolContext(taskId);
178
177
  console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
179
178
  const result = await tool.handler(args, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -28,7 +28,7 @@ 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`, `/device-geolocation`) 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 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
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.
33
33
 
34
34
  ## Prerequisites
@@ -1152,6 +1152,7 @@ body {
1152
1152
  width: auto;
1153
1153
  }
1154
1154
 
1155
+ .triggers-section-body > .form-select,
1155
1156
  .trigger-row-card .form-select,
1156
1157
  .trigger-row-card .form-input {
1157
1158
  margin-bottom: 0;
@@ -1160,6 +1161,14 @@ body {
1160
1161
  height: 32px;
1161
1162
  box-sizing: border-box;
1162
1163
  min-width: 0;
1164
+ }
1165
+
1166
+ .triggers-section-body > .form-select {
1167
+ width: 100%;
1168
+ }
1169
+
1170
+ .trigger-row-card .form-select,
1171
+ .trigger-row-card .form-input {
1163
1172
  flex: 1;
1164
1173
  }
1165
1174
 
@@ -99,25 +99,53 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
99
99
  };
100
100
 
101
101
 
102
- function formatTrigger(t: { type: string; value: string }): string {
102
+ function formatTriggersGrouped(triggers: { type: string; value: string }[]): string {
103
+ if (triggers.length === 0) return "";
104
+ if (triggers.length === 1) return formatSingleTrigger(triggers[0]);
105
+
106
+ // Detect the shared schedule type
107
+ const classified = triggers.map(classifyTrigger);
108
+ const types = new Set(classified.map((c) => c.kind));
109
+
110
+ // If all the same type, group them
111
+ if (types.size === 1) {
112
+ const kind = classified[0].kind;
113
+ if (kind === "hourly") return "Every hour";
114
+ const details = classified.map((c) => c.detail);
115
+ return `${kind.charAt(0).toUpperCase() + kind.slice(1)}: ${details.join(", ")}`;
116
+ }
117
+
118
+ // Mixed types — fall back to listing each
119
+ return triggers.map(formatSingleTrigger).join(", ");
120
+ }
121
+
122
+ function classifyTrigger(t: { type: string; value: string }): { kind: string; detail: string } {
103
123
  if (t.type === "once") {
104
124
  const d = new Date(t.value);
105
- return isNaN(d.getTime()) ? t.value : `Once on ${d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })} at ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}`;
125
+ const label = isNaN(d.getTime()) ? t.value : `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })} at ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}`;
126
+ return { kind: "once", detail: label };
106
127
  }
107
128
  const parts = t.value.split(" ");
108
- if (parts.length !== 5) return t.value;
129
+ if (parts.length !== 5) return { kind: "unknown", detail: t.value };
109
130
  const [min, hour, dom, , dow] = parts;
110
131
  const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
111
- if (hour === "*") return "Every hour";
132
+ if (hour === "*") return { kind: "hourly", detail: "" };
112
133
  const d = new Date();
113
134
  d.setHours(Number(hour), Number(min), 0, 0);
114
135
  const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
115
- if (dow !== "*") return `Weekly on ${DAYS[Number(dow)] ?? dow} at ${time}`;
116
- if (dom !== "*") return `Monthly on day ${dom} at ${time}`;
117
- return `Daily at ${time}`;
136
+ if (dow !== "*") return { kind: "weekly", detail: `${DAYS[Number(dow)] ?? dow} at ${time}` };
137
+ if (dom !== "*") return { kind: "monthly", detail: `day ${dom} at ${time}` };
138
+ return { kind: "daily", detail: time };
139
+ }
140
+
141
+ function formatSingleTrigger(t: { type: string; value: string }): string {
142
+ const c = classifyTrigger(t);
143
+ if (c.kind === "hourly") return "Every hour";
144
+ if (c.kind === "once") return `Once on ${c.detail}`;
145
+ return `${c.kind.charAt(0).toUpperCase() + c.kind.slice(1)}: ${c.detail}`;
118
146
  }
119
147
 
120
- const triggersText = task.triggers.map(formatTrigger).join(", ");
148
+ const triggersText = formatTriggersGrouped(task.triggers);
121
149
 
122
150
  const actionItems = (
123
151
  <>
@@ -104,6 +104,9 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
104
104
  const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
105
105
  () => (initial?.triggers ?? []).map(triggerToRow)
106
106
  );
107
+ const [schedule, setSchedule] = useState<Schedule>(
108
+ () => (initial?.triggers ?? []).map(triggerToRow)[0]?.schedule ?? "daily"
109
+ );
107
110
  const [triggersEnabled, setTriggersEnabled] = useState(
108
111
  initial?.triggers_enabled ?? false
109
112
  );
@@ -152,7 +155,12 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
152
155
  }
153
156
 
154
157
  function addRow() {
155
- setTriggerRows((prev) => [...prev, newRow(prev.length > 0 ? prev[prev.length - 1].schedule : undefined)]);
158
+ setTriggerRows((prev) => [...prev, newRow(schedule)]);
159
+ }
160
+
161
+ function changeSchedule(s: Schedule) {
162
+ setSchedule(s);
163
+ setTriggerRows([newRow(s)]);
156
164
  }
157
165
 
158
166
  function collectTriggers(): Trigger[] {
@@ -370,26 +378,47 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
370
378
  }}
371
379
  disabled={saving}
372
380
  />
373
- Add schedules
381
+ Enable schedule
374
382
  </label>
375
383
  <div className={`triggers-section-body${triggersEnabled ? "" : " disabled"}`}>
376
- {triggerRows.map((row, i) => (
384
+ {triggerRows.length > 0 && (
385
+ <select
386
+ className="form-select"
387
+ value={schedule}
388
+ disabled={!triggersEnabled}
389
+ onChange={(e) => changeSchedule(e.target.value as Schedule)}
390
+ >
391
+ <option value="once">Specific Time</option>
392
+ <option value="hourly">Hourly</option>
393
+ <option value="daily">Daily</option>
394
+ <option value="weekly">Weekly</option>
395
+ <option value="monthly">Monthly</option>
396
+ </select>
397
+ )}
398
+ {schedule !== "hourly" && triggerRows.map((row, i) => (
377
399
  <div key={i} className="trigger-row-card">
378
400
  <div className="trigger-row-content">
379
- <div className="trigger-row-top">
380
- <select
381
- className="form-select"
382
- value={row.schedule}
401
+ {schedule === "daily" && (
402
+ <input
403
+ className="form-input"
404
+ type="time"
405
+ value={row.time}
383
406
  disabled={!triggersEnabled}
384
- onChange={(e) => updateRow(i, { schedule: e.target.value as Schedule })}
385
- >
386
- <option value="once">Specific Time</option>
387
- <option value="hourly">Hourly</option>
388
- <option value="daily">Daily</option>
389
- <option value="weekly">Weekly</option>
390
- <option value="monthly">Monthly</option>
391
- </select>
392
- {row.schedule === "daily" && (
407
+ onChange={(e) => updateRow(i, { time: e.target.value })}
408
+ />
409
+ )}
410
+ {schedule === "weekly" && (
411
+ <div className="trigger-row-top">
412
+ <select
413
+ className="form-select"
414
+ value={row.dayOfWeek}
415
+ disabled={!triggersEnabled}
416
+ onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
417
+ >
418
+ {DAYS_OF_WEEK.map((d, di) => (
419
+ <option key={di} value={String(di)}>{d}</option>
420
+ ))}
421
+ </select>
393
422
  <input
394
423
  className="form-input"
395
424
  type="time"
@@ -397,31 +426,10 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
397
426
  disabled={!triggersEnabled}
398
427
  onChange={(e) => updateRow(i, { time: e.target.value })}
399
428
  />
400
- )}
401
- {row.schedule === "weekly" && (
402
- <>
403
- <select
404
- className="form-select"
405
- value={row.dayOfWeek}
406
- disabled={!triggersEnabled}
407
- onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
408
- >
409
- {DAYS_OF_WEEK.map((d, di) => (
410
- <option key={di} value={String(di)}>{d}</option>
411
- ))}
412
- </select>
413
- <input
414
- className="form-input"
415
- type="time"
416
- value={row.time}
417
- disabled={!triggersEnabled}
418
- onChange={(e) => updateRow(i, { time: e.target.value })}
419
- />
420
- </>
421
- )}
422
- </div>
423
- {row.schedule === "monthly" && (
424
- <div className="trigger-details">
429
+ </div>
430
+ )}
431
+ {schedule === "monthly" && (
432
+ <div className="trigger-row-top">
425
433
  <select
426
434
  className="form-select"
427
435
  value={row.dayOfMonth}
@@ -441,8 +449,8 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
441
449
  />
442
450
  </div>
443
451
  )}
444
- {row.schedule === "once" && (
445
- <div className="trigger-details">
452
+ {schedule === "once" && (
453
+ <div className="trigger-row-top">
446
454
  <input
447
455
  className="form-input"
448
456
  type="date"
@@ -464,19 +472,21 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
464
472
  </div>
465
473
  )}
466
474
  </div>
467
- <button
468
- className="trigger-remove-btn"
469
- onClick={() => removeRow(i)}
470
- disabled={!triggersEnabled}
471
- title="Remove trigger"
472
- >
473
- &times;
474
- </button>
475
+ {triggerRows.length > 1 && (
476
+ <button
477
+ className="trigger-remove-btn"
478
+ onClick={() => removeRow(i)}
479
+ disabled={!triggersEnabled}
480
+ title="Remove trigger"
481
+ >
482
+ &times;
483
+ </button>
484
+ )}
475
485
  </div>
476
486
  ))}
477
- {triggerRows.length > 0 && (
487
+ {triggerRows.length > 0 && schedule !== "hourly" && (
478
488
  <button className="trigger-add-btn" onClick={addRow} disabled={!triggersEnabled}>
479
- + Add Schedule
489
+ + Add
480
490
  </button>
481
491
  )}
482
492
  </div>
@@ -1,2 +1,2 @@
1
1
  /** Bump when a breaking host change is made. */
2
- export const MIN_HOST_VERSION = "0.6.4";
2
+ export const MIN_HOST_VERSION = "0.6.7";
@@ -12,7 +12,7 @@ The host supports **Linux** (systemd) and **Windows** (Task Scheduler for both d
12
12
 
13
13
  ### 1.2 Components
14
14
 
15
- * **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Localhost-only HTTP endpoints (`/notify`, `/request-input`, `/request-confirmation`, `/request-permission`, `/device-geolocation`) are used by agents and the `palmier run` process for interactive flows via held HTTP connections. `/request-input` and `/notify` are task-independent (no `taskId` required) input requests use an internal `requestId` for routing. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPlanGenerationCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
15
+ * **Host Binary (Node.js):** Runs persistently on the user's host machine as a NATS + HTTP RPC handler. Manages file system operations (task CRUD), OS-level scheduling (systemd), and task generation. Provides a CLI with commands: `palmier init` (provisioning), `palmier pair` (generate pairing code for device pairing), `palmier clients` (manage client tokens), `palmier run <task-id>` (executes a task via the configured agent tool), `palmier uninstall` (stop daemon and remove all scheduled tasks), and `palmier serve` (persistent RPC handler, default command). The `serve` process always starts a local HTTP server (bound to `127.0.0.1` by default, or `0.0.0.0` if LAN mode is enabled) alongside the NATS transport. Exposes a localhost-only MCP server at `/mcp` (streamable HTTP transport) with tools: `notify`, `request-input`, `request-confirmation`, `device-geolocation`. The same tools are auto-generated as REST endpoints (`/notify`, `/request-input`, etc.) from a shared tool registry zero duplication. REST endpoints require `taskId` in the body for session identification. `/request-permission` remains a separate endpoint (not part of the MCP tool registry). MCP sessions track agent names from `initialize` clientInfo for logging and UI display. `palmier run` is a short-lived process invoked by systemd. Task execution is abstracted through an `AgentTool` interface (`src/agents/agent.ts`) so different AI CLI tools can be supported — each agent implements `getPlanGenerationCommandLine()`, `getTaskRunCommandLine()`, and `init()`. The task's `agent` field (e.g., `"claude"`) selects which agent is used.
16
16
 
17
17
  * **Web Server (Node.js):** Serves the PWA assets (React) via `app.palmier.me` (Cloudflare proxied), manages Web Push VAPID keys, and provides host registration. Uses **PostgreSQL** for persistent storage (host registrations, push subscriptions, FCM tokens). Connects to NATS via TCP to subscribe to `host-event.>` for sending push notifications (confirmations, dismissals, completion/failure). For `POST /api/push/respond` (confirmation responses via push notification action buttons), the Web Server forwards the response to the host via the `task.user_input` NATS RPC. Subscribes to `host.*.push.send` NATS subjects to relay push notification requests from the host CLI. Subscribes to `host.*.fcm.geolocation` to relay device geolocation requests via FCM. Co-located with the NATS server on the same machine.
18
18
 
@@ -1,4 +1,4 @@
1
- You are an AI agent executing a task on behalf of the user via the Palmier platform. Follow these instructions carefully.
1
+ You are an AI agent executing a task on behalf of the user. Follow these instructions carefully.
2
2
 
3
3
  ## Reporting Output
4
4
 
@@ -13,46 +13,16 @@ When you are done, output exactly one of these markers as the very last line (no
13
13
 
14
14
  ## Permissions
15
15
 
16
- If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format:
16
+ Whenever a tool you are trying to use is denied or you lack the required permissions, print each required permission on its own line using this exact format:
17
17
  [PALMIER_PERMISSION] <tool_name> | <description>
18
18
 
19
19
  ## HTTP Endpoints
20
20
 
21
- The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them. All endpoints require `taskId` in the request body.
22
-
23
- **`POST /request-input`** — Request input from the user. The request blocks until the user responds.
24
- ```json
25
- {"taskId": "{{TASK_ID}}", "description": "optional context", "questions": ["question 1", "question 2"]}
26
- ```
27
- - `taskId` (required, string): The current task ID.
28
- - `questions` (required, string array): Questions to present to the user.
29
- - `description` (optional, string): Context or heading for the input request.
30
- - Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.
31
- - When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment. Use this endpoint instead.
32
-
33
- **`POST /request-confirmation`** — Request confirmation from the user. The request blocks until the user confirms or aborts.
34
- ```json
35
- {"taskId": "{{TASK_ID}}", "description": "What the user is confirming"}
36
- ```
37
- - `taskId` (required, string): The current task ID.
38
- - `description` (required, string): What the user is confirming.
39
- - Response: `{"confirmed": true}` or `{"confirmed": false}`.
40
-
41
- **`POST /device-geolocation`** — Get the GPS location of the user's mobile device. Blocks until the device responds (up to 30 seconds).
42
- ```json
43
- {"taskId": "{{TASK_ID}}"}
44
- ```
45
- - `taskId` (required, string): The current task ID.
46
- - Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.
47
-
48
- **`POST /notify`** — Send a push notification to the user's device.
49
- ```json
50
- {"taskId": "{{TASK_ID}}", "title": "...", "body": "..."}
51
- ```
52
- - `taskId` (required, string): The current task ID.
53
- - `title` (required, string): Notification title.
54
- - `body` (required, string): Notification body.
21
+ {{ENDPOINT_DOCS}}
22
+
23
+ The task to execute follows below:
55
24
 
56
25
  ---
57
26
 
58
- The task to execute follows below.
27
+ {{TASK_DESCRIPTION}}
28
+
@@ -15,7 +15,7 @@ export class Aider implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class ClaudeAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["--permission-mode", yolo ? "bypassPermissions" : "acceptEdits", "-p"];
20
20
 
21
21
  if (!yolo) {
@@ -15,7 +15,7 @@ export class Cline implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class CodexAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["exec", "--skip-git-repo-check", "--sandbox", yolo ? "danger-full-access" : "workspace-write"];
20
20
 
21
21
  if (!yolo) {
@@ -15,7 +15,7 @@ export class CopilotAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["-p", prompt];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class Cursor implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class DeepAgents implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class DroidAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["exec", "--session-id", task.frontmatter.id];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class GeminiAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
20
20
 
21
21
  if (!yolo) {
@@ -15,7 +15,7 @@ export class GooseAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["run"];
20
20
 
21
21
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
@@ -15,7 +15,7 @@ export class Hermes implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["chat"];
20
20
 
21
21
  if (yolo) {