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.
- package/README.md +14 -0
- package/dist/agents/agent-instructions.md +7 -37
- 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/run.js +2 -5
- package/dist/mcp-handler.js +1 -1
- package/dist/mcp-tools.d.ts +6 -1
- package/dist/mcp-tools.js +72 -6
- package/dist/pwa/assets/{index-DAI3J-jU.css → index-C6Lz09EY.css} +1 -1
- package/dist/pwa/assets/{index-RrJvjqz9.js → index-C8vJwUNi.js} +42 -42
- package/dist/pwa/assets/{web-DQteXlI7.js → web-6UChJFov.js} +1 -1
- package/dist/pwa/assets/{web-EzNEHXEh.js → web-NxTETXZK.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/spawn-command.js +3 -1
- package/dist/transports/http-transport.js +4 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +9 -0
- 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/constants.ts +1 -1
- package/palmier-server/spec.md +1 -1
- package/src/agents/agent-instructions.md +7 -37
- 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/run.ts +2 -5
- package/src/mcp-handler.ts +1 -1
- package/src/mcp-tools.ts +78 -7
- package/src/rpc-handler.ts +0 -1
- package/src/spawn-command.ts +3 -1
- package/src/transports/http-transport.ts +4 -5
- package/test/agent-instructions.test.ts +68 -5
- package/test/fixtures/agent-instructions-snapshot.md +58 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as t}from"./index-
|
|
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-
|
|
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
|
@@ -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-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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":"
|
|
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
|
@@ -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,
|
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"],
|
|
@@ -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
|
|
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
|
-
|
|
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
package/palmier-server/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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
|
|
116
|
-
if (dom !== "*") return
|
|
117
|
-
return
|
|
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
|
|
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(
|
|
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
|
-
|
|
381
|
+
Enable schedule
|
|
374
382
|
</label>
|
|
375
383
|
<div className={`triggers-section-body${triggersEnabled ? "" : " disabled"}`}>
|
|
376
|
-
{triggerRows.
|
|
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
|
-
|
|
380
|
-
<
|
|
381
|
-
className="form-
|
|
382
|
-
|
|
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, {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
{
|
|
445
|
-
<div className="trigger-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
+
{triggerRows.length > 1 && (
|
|
476
|
+
<button
|
|
477
|
+
className="trigger-remove-btn"
|
|
478
|
+
onClick={() => removeRow(i)}
|
|
479
|
+
disabled={!triggersEnabled}
|
|
480
|
+
title="Remove trigger"
|
|
481
|
+
>
|
|
482
|
+
×
|
|
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
|
|
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.
|
|
2
|
+
export const MIN_HOST_VERSION = "0.6.7";
|
package/palmier-server/spec.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
+
{{TASK_DESCRIPTION}}
|
|
28
|
+
|
package/src/agents/aider.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/claude.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/cline.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/codex.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/copilot.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["-p", prompt];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/cursor.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/deepagents.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = [];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|
package/src/agents/droid.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/gemini.ts
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/agents/goose.ts
CHANGED
|
@@ -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 ??
|
|
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
|
package/src/agents/hermes.ts
CHANGED
|
@@ -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 ??
|
|
18
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
19
19
|
const args = ["chat"];
|
|
20
20
|
|
|
21
21
|
if (yolo) {
|