palmier 0.6.8 → 0.7.0
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 +9 -2
- package/dist/agents/agent.d.ts +2 -2
- package/dist/agents/aider.d.ts +1 -1
- package/dist/agents/aider.js +2 -5
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +2 -5
- package/dist/agents/cline.d.ts +1 -1
- package/dist/agents/cline.js +2 -5
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +2 -5
- package/dist/agents/copilot.d.ts +1 -1
- package/dist/agents/copilot.js +2 -5
- package/dist/agents/cursor.d.ts +1 -1
- package/dist/agents/cursor.js +2 -5
- package/dist/agents/deepagents.d.ts +1 -1
- package/dist/agents/deepagents.js +2 -5
- package/dist/agents/droid.d.ts +1 -1
- package/dist/agents/droid.js +2 -5
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +2 -5
- package/dist/agents/goose.d.ts +1 -1
- package/dist/agents/goose.js +2 -5
- package/dist/agents/hermes.d.ts +1 -1
- package/dist/agents/hermes.js +2 -5
- package/dist/agents/kimi.d.ts +1 -1
- package/dist/agents/kimi.js +2 -5
- package/dist/agents/kiro.d.ts +1 -1
- package/dist/agents/kiro.js +2 -5
- package/dist/agents/openclaw.d.ts +1 -1
- package/dist/agents/openclaw.js +2 -5
- package/dist/agents/opencode.d.ts +1 -1
- package/dist/agents/opencode.js +2 -5
- package/dist/agents/qoder.d.ts +1 -1
- package/dist/agents/qoder.js +2 -5
- package/dist/agents/qwen.d.ts +1 -1
- package/dist/agents/qwen.js +2 -5
- package/dist/agents/shared-prompt.js +1 -1
- package/dist/commands/run.js +1 -2
- package/dist/commands/serve.js +16 -0
- package/dist/mcp-handler.d.ts +3 -0
- package/dist/mcp-handler.js +59 -3
- package/dist/mcp-tools.d.ts +16 -1
- package/dist/mcp-tools.js +24 -2
- package/dist/notification-store.d.ts +13 -0
- package/dist/notification-store.js +19 -0
- package/dist/pwa/assets/{index-C8vJwUNi.js → index-DLxrL0hR.js} +42 -42
- package/dist/pwa/assets/{web-NxTETXZK.js → web-CBI458eN.js} +1 -1
- package/dist/pwa/assets/{web-6UChJFov.js → web-HDs03L2B.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +27 -67
- package/dist/task.js +2 -3
- package/dist/transports/http-transport.js +51 -3
- package/dist/types.d.ts +0 -1
- package/package.json +2 -2
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/components/PlanDialog.tsx +5 -12
- package/palmier-server/pwa/src/components/TaskForm.tsx +6 -15
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/types.ts +0 -1
- package/palmier-server/server/src/index.ts +2 -0
- package/palmier-server/server/src/routes/device.ts +32 -0
- package/palmier-server/spec.md +13 -12
- package/src/agents/agent.ts +2 -2
- package/src/agents/aider.ts +2 -5
- package/src/agents/claude.ts +2 -5
- package/src/agents/cline.ts +2 -5
- package/src/agents/codex.ts +2 -5
- package/src/agents/copilot.ts +2 -5
- package/src/agents/cursor.ts +2 -5
- package/src/agents/deepagents.ts +2 -5
- package/src/agents/droid.ts +2 -5
- package/src/agents/gemini.ts +2 -5
- package/src/agents/goose.ts +2 -5
- package/src/agents/hermes.ts +2 -5
- package/src/agents/kimi.ts +2 -5
- package/src/agents/kiro.ts +2 -5
- package/src/agents/openclaw.ts +2 -5
- package/src/agents/opencode.ts +2 -5
- package/src/agents/qoder.ts +2 -5
- package/src/agents/qwen.ts +2 -5
- package/src/agents/shared-prompt.ts +1 -1
- package/src/commands/run.ts +1 -2
- package/src/commands/serve.ts +16 -1
- package/src/mcp-handler.ts +68 -3
- package/src/mcp-tools.ts +48 -2
- package/src/notification-store.ts +30 -0
- package/src/rpc-handler.ts +29 -71
- package/src/task.ts +2 -3
- package/src/transports/http-transport.ts +49 -3
- package/src/types.ts +0 -1
- package/test/agent-instructions.test.ts +117 -19
- package/test/agent-output-parsing.test.ts +1 -0
- package/test/notification-store.test.ts +57 -0
- package/test/task-parsing.test.ts +3 -3
- package/dist/commands/plan-generation.md +0 -22
- package/src/commands/plan-generation.md +0 -22
- package/test/fixtures/agent-instructions-snapshot.md +0 -58
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as p}from"./index-
|
|
1
|
+
import{W as p}from"./index-DLxrL0hR.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{W as t}from"./index-
|
|
1
|
+
import{W as t}from"./index-DLxrL0hR.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
|
package/dist/pwa/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<title>Palmier</title>
|
|
10
10
|
<meta name="description" content="Remote control for AI agents running on your own machine. Schedule tasks, approve permissions, and get push notifications." />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DLxrL0hR.js"></script>
|
|
12
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>
|
|
@@ -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":"f65782768cfd2a3d2392e43cf922dde4","url":"index.html"},{"revision":null,"url":"assets/web-HDs03L2B.js"},{"revision":null,"url":"assets/web-CBI458eN.js"},{"revision":null,"url":"assets/index-DLxrL0hR.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
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
4
|
import { spawn } from "child_process";
|
|
6
|
-
import { parse as parseYaml } from "yaml";
|
|
7
5
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
8
6
|
import { resolvePending, getPending } from "./pending-requests.js";
|
|
9
7
|
import { getPlatform } from "./platform/index.js";
|
|
@@ -15,8 +13,6 @@ import { publishHostEvent } from "./events.js";
|
|
|
15
13
|
import { getLocationDevice, setLocationDevice, clearLocationDevice } from "./location-device.js";
|
|
16
14
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
17
15
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
18
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
20
16
|
/**
|
|
21
17
|
* Parse RESULT frontmatter and conversation messages.
|
|
22
18
|
*/
|
|
@@ -98,34 +94,26 @@ function parseAttr(attrs, name) {
|
|
|
98
94
|
return match ? match[1] : undefined;
|
|
99
95
|
}
|
|
100
96
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
97
|
+
* Generate a concise task name from a user prompt using the given agent.
|
|
98
|
+
* Falls back to the raw prompt on failure.
|
|
103
99
|
*/
|
|
104
|
-
async function
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const { command, args, stdin, env: agentEnv } =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const fm = parseYaml(fmMatch[1]);
|
|
121
|
-
name = fm.task_name ?? "";
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// If frontmatter parsing fails, treat entire output as body
|
|
125
|
-
}
|
|
126
|
-
body = fmMatch[2].trimStart();
|
|
100
|
+
async function generateName(projectRoot, userPrompt, agentName) {
|
|
101
|
+
const prompt = `Generate a concise 3-6 word name for this task. Reply with ONLY the name, nothing else.\n\nTask: ${userPrompt}`;
|
|
102
|
+
const agent = getAgent(agentName);
|
|
103
|
+
const { command, args, stdin, env: agentEnv } = agent.getPromptCommandLine(prompt);
|
|
104
|
+
try {
|
|
105
|
+
const { output } = await spawnCommand(command, args, {
|
|
106
|
+
cwd: projectRoot,
|
|
107
|
+
timeout: 30_000,
|
|
108
|
+
stdin,
|
|
109
|
+
...(agentEnv ? { env: agentEnv } : {}),
|
|
110
|
+
});
|
|
111
|
+
const name = output.trim().replace(/^["']|["']$/g, "").slice(0, 80);
|
|
112
|
+
return name || userPrompt;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return userPrompt;
|
|
127
116
|
}
|
|
128
|
-
return { name, body };
|
|
129
117
|
}
|
|
130
118
|
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
131
119
|
const activeFollowups = new Map();
|
|
@@ -139,7 +127,6 @@ export function createRpcHandler(config, nc) {
|
|
|
139
127
|
const pending = getPending(task.frontmatter.id);
|
|
140
128
|
return {
|
|
141
129
|
...task.frontmatter,
|
|
142
|
-
body: task.body,
|
|
143
130
|
status: status ? {
|
|
144
131
|
...status,
|
|
145
132
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
@@ -178,23 +165,9 @@ export function createRpcHandler(config, nc) {
|
|
|
178
165
|
}
|
|
179
166
|
case "task.create": {
|
|
180
167
|
const params = request.params;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (params.user_prompt.length <= 50) {
|
|
185
|
-
name = params.user_prompt;
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
try {
|
|
189
|
-
const plan = await generatePlan(config.projectRoot, params.user_prompt, params.agent);
|
|
190
|
-
name = plan.name;
|
|
191
|
-
body = plan.body;
|
|
192
|
-
}
|
|
193
|
-
catch (err) {
|
|
194
|
-
const error = err;
|
|
195
|
-
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
196
|
-
}
|
|
197
|
-
}
|
|
168
|
+
const name = params.user_prompt.length <= 50
|
|
169
|
+
? params.user_prompt
|
|
170
|
+
: await generateName(config.projectRoot, params.user_prompt, params.agent);
|
|
198
171
|
const id = randomUUID();
|
|
199
172
|
const taskDir = getTaskDir(config.projectRoot, id);
|
|
200
173
|
const task = {
|
|
@@ -210,7 +183,6 @@ export function createRpcHandler(config, nc) {
|
|
|
210
183
|
...(params.foreground_mode ? { foreground_mode: true } : {}),
|
|
211
184
|
...(params.command ? { command: params.command } : {}),
|
|
212
185
|
},
|
|
213
|
-
body,
|
|
214
186
|
};
|
|
215
187
|
writeTaskFile(taskDir, task);
|
|
216
188
|
appendTaskList(config.projectRoot, id);
|
|
@@ -221,10 +193,9 @@ export function createRpcHandler(config, nc) {
|
|
|
221
193
|
const params = request.params;
|
|
222
194
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
223
195
|
const existing = parseTaskFile(taskDir);
|
|
224
|
-
// Detect whether
|
|
196
|
+
// Detect whether name needs regeneration
|
|
225
197
|
const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
|
|
226
198
|
const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
|
|
227
|
-
const needsRegeneration = promptChanged || agentChanged || !existing.body;
|
|
228
199
|
// Merge updates
|
|
229
200
|
if (params.user_prompt !== undefined)
|
|
230
201
|
existing.frontmatter.user_prompt = params.user_prompt;
|
|
@@ -251,21 +222,11 @@ export function createRpcHandler(config, nc) {
|
|
|
251
222
|
delete existing.frontmatter.command;
|
|
252
223
|
}
|
|
253
224
|
}
|
|
254
|
-
// Regenerate
|
|
255
|
-
if (
|
|
256
|
-
existing.frontmatter.name = existing.frontmatter.user_prompt
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
else if (needsRegeneration) {
|
|
260
|
-
try {
|
|
261
|
-
const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
262
|
-
existing.frontmatter.name = plan.name;
|
|
263
|
-
existing.body = plan.body;
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
const error = err;
|
|
267
|
-
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
268
|
-
}
|
|
225
|
+
// Regenerate name when prompt or agent changes
|
|
226
|
+
if (promptChanged || agentChanged) {
|
|
227
|
+
existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
|
|
228
|
+
? existing.frontmatter.user_prompt
|
|
229
|
+
: await generateName(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
269
230
|
}
|
|
270
231
|
writeTaskFile(taskDir, existing);
|
|
271
232
|
// Update timers — installTaskTimer overwrites in-place (schtasks /f,
|
|
@@ -297,7 +258,6 @@ export function createRpcHandler(config, nc) {
|
|
|
297
258
|
...(params.foreground_mode ? { foreground_mode: true } : {}),
|
|
298
259
|
...(params.command ? { command: params.command } : {}),
|
|
299
260
|
},
|
|
300
|
-
body: "",
|
|
301
261
|
};
|
|
302
262
|
writeTaskFile(taskDir, task);
|
|
303
263
|
// Do NOT append to tasks.jsonl — this is a one-off run
|
package/dist/task.js
CHANGED
|
@@ -22,14 +22,13 @@ export function parseTaskContent(content) {
|
|
|
22
22
|
throw new Error("TASK.md is missing valid YAML frontmatter delimiters (---)");
|
|
23
23
|
}
|
|
24
24
|
const frontmatter = parseYaml(match[1]);
|
|
25
|
-
const body = (match[2] || "").trim();
|
|
26
25
|
if (!frontmatter.id) {
|
|
27
26
|
throw new Error("TASK.md frontmatter must include at least: id");
|
|
28
27
|
}
|
|
29
28
|
frontmatter.name ??= frontmatter.user_prompt?.slice(0, 60) ?? "";
|
|
30
29
|
frontmatter.agent ??= "claude";
|
|
31
30
|
frontmatter.triggers_enabled ??= true;
|
|
32
|
-
return { frontmatter
|
|
31
|
+
return { frontmatter };
|
|
33
32
|
}
|
|
34
33
|
/**
|
|
35
34
|
* Write a TASK.md file to the given task directory.
|
|
@@ -38,7 +37,7 @@ export function parseTaskContent(content) {
|
|
|
38
37
|
export function writeTaskFile(taskDir, task) {
|
|
39
38
|
fs.mkdirSync(taskDir, { recursive: true });
|
|
40
39
|
const yamlStr = stringifyYaml(task.frontmatter).trim();
|
|
41
|
-
const content = `---\n${yamlStr}\n---\n
|
|
40
|
+
const content = `---\n${yamlStr}\n---\n`;
|
|
42
41
|
const filePath = path.join(taskDir, "TASK.md");
|
|
43
42
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
44
43
|
}
|
|
@@ -5,9 +5,10 @@ import { StringCodec } from "nats";
|
|
|
5
5
|
import { validateClient, addClient } from "../client-store.js";
|
|
6
6
|
import { registerPending } from "../pending-requests.js";
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
-
import { agentToolMap, ToolError } from "../mcp-tools.js";
|
|
9
|
-
import { handleMcpRequest, getAgentName } from "../mcp-handler.js";
|
|
8
|
+
import { agentToolMap, agentResources, ToolError } from "../mcp-tools.js";
|
|
9
|
+
import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
|
|
10
10
|
import { getTaskDir } from "../task.js";
|
|
11
|
+
import { onNotificationsChanged } from "../notification-store.js";
|
|
11
12
|
const assetCache = new Map();
|
|
12
13
|
const PWA_DIR = path.join(import.meta.dirname, "..", "pwa");
|
|
13
14
|
const CONTENT_TYPES = {
|
|
@@ -73,8 +74,27 @@ export function detectLanIp() {
|
|
|
73
74
|
*/
|
|
74
75
|
export async function startHttpTransport(config, handleRpc, port, nc, pairingCode, onReady) {
|
|
75
76
|
const sseClients = new Set();
|
|
77
|
+
const mcpStreams = new Map();
|
|
76
78
|
const lanEnabled = config.lanEnabled ?? false;
|
|
77
79
|
const bindAddress = lanEnabled ? "0.0.0.0" : "127.0.0.1";
|
|
80
|
+
/** Push notifications/resources/updated to all MCP clients subscribed to the given URI. */
|
|
81
|
+
function broadcastResourceUpdated(uri) {
|
|
82
|
+
const subs = getResourceSubscriptions();
|
|
83
|
+
for (const [sessionId, uris] of subs) {
|
|
84
|
+
if (!uris.has(uri))
|
|
85
|
+
continue;
|
|
86
|
+
const stream = mcpStreams.get(sessionId);
|
|
87
|
+
if (!stream)
|
|
88
|
+
continue;
|
|
89
|
+
stream.write(`data: ${JSON.stringify({
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
method: "notifications/resources/updated",
|
|
92
|
+
params: { uri },
|
|
93
|
+
})}\n\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Wire up resource change listeners
|
|
97
|
+
onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
|
|
78
98
|
// If a pairing code is provided, pre-register it
|
|
79
99
|
if (pairingCode) {
|
|
80
100
|
const EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -146,7 +166,25 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
146
166
|
if (result.sessionId) {
|
|
147
167
|
res.setHeader("Mcp-Session-Id", result.sessionId);
|
|
148
168
|
}
|
|
149
|
-
|
|
169
|
+
if (result.stream && sessionId) {
|
|
170
|
+
// Keep response open as SSE stream for server-initiated notifications
|
|
171
|
+
res.writeHead(200, {
|
|
172
|
+
"Content-Type": "text/event-stream",
|
|
173
|
+
"Cache-Control": "no-cache",
|
|
174
|
+
"Connection": "keep-alive",
|
|
175
|
+
});
|
|
176
|
+
res.write(`data: ${JSON.stringify(result.body)}\n\n`);
|
|
177
|
+
mcpStreams.set(sessionId, res);
|
|
178
|
+
const heartbeat = setInterval(() => { res.write(":heartbeat\n\n"); }, 15_000);
|
|
179
|
+
req.on("close", () => {
|
|
180
|
+
clearInterval(heartbeat);
|
|
181
|
+
mcpStreams.delete(sessionId);
|
|
182
|
+
getResourceSubscriptions().delete(sessionId);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
sendJson(res, 200, result.body);
|
|
187
|
+
}
|
|
150
188
|
}
|
|
151
189
|
catch (err) {
|
|
152
190
|
sendJson(res, 500, { error: String(err) });
|
|
@@ -186,6 +224,16 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
186
224
|
}
|
|
187
225
|
return;
|
|
188
226
|
}
|
|
227
|
+
// ── Auto-generated REST endpoints from MCP resource registry ────
|
|
228
|
+
const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
|
|
229
|
+
if (matchedResource) {
|
|
230
|
+
if (!isLocalhost(req)) {
|
|
231
|
+
sendJson(res, 403, { error: "localhost only" });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
sendJson(res, 200, matchedResource.read());
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
189
237
|
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
190
238
|
if (req.method === "POST" && pathname === "/event") {
|
|
191
239
|
if (!isLocalhost(req)) {
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palmier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hongxu Cai",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"dev": "tsx src/index.ts",
|
|
23
|
-
"build": "tsc && node -e \"const fs=require('fs');fs.cpSync('src/
|
|
23
|
+
"build": "tsc && node -e \"const fs=require('fs');fs.cpSync('src/agents/agent-instructions.md','dist/agents/agent-instructions.md');const p=process.env.PALMIER_PWA_DIST||'../palmier-server/pwa/dist';if(fs.existsSync(p))fs.cpSync(p,'dist/pwa',{recursive:true});else console.warn('PWA dist not found at '+p+', skipping')\"",
|
|
24
24
|
"test": "tsx --test test/**/*.test.ts",
|
|
25
25
|
"prepare": "npm run build",
|
|
26
26
|
"start": "node dist/index.js"
|
package/palmier-server/README.md
CHANGED
|
@@ -181,7 +181,7 @@ All endpoints are prefixed with `/api`. No user authentication is required.
|
|
|
181
181
|
- **Static file serving** is conditional — Express only serves `pwa/dist/` if the directory exists, so it doesn't interfere during dev when using Vite.
|
|
182
182
|
- **No CORS** needed — Vite proxy handles same-origin in dev, Express static serving handles it in production.
|
|
183
183
|
- **Push notifications** — the PWA registers a service worker (`injectManifest` strategy via vite-plugin-pwa) and subscribes the browser for Web Push. The Web Server subscribes to `host-event.>` and sends push notifications for confirmation requests, task completions, and task failures.
|
|
184
|
-
- **Markdown rendering** —
|
|
184
|
+
- **Markdown rendering** — Task results are rendered as rich formatted text using `react-markdown` with `remark-gfm` (GitHub Flavored Markdown), supporting tables, strikethrough, task lists, and autolinks.
|
|
185
185
|
- **Task confirmation** — the Dashboard discovers pending confirmations from the `task.list` RPC response (tasks with a pending request in the serve daemon's in-memory registry, reported via `task.status`). When found, it shows a full-screen confirmation modal. Push notification action buttons trigger `POST /api/push/respond`, which forwards to the `task.user_input` NATS RPC.
|
|
186
186
|
- **Task event tracking** — task lifecycle events are persisted to `status.json` on the host (for crash detection) and broadcast via `host-event.<host_id>.<task_id>` pub/sub and HTTP SSE. The PWA loads initial status from `task.list` and subscribes to events for real-time updates.
|
|
187
187
|
- **NATS config** (`nats.conf`) enables WebSocket on port 9222 (for browser clients) and token-based auth.
|
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
import Markdown from "react-markdown";
|
|
2
|
-
import remarkGfm from "remark-gfm";
|
|
3
1
|
import type { RequiredPermission } from "../types";
|
|
4
2
|
|
|
5
3
|
interface PlanDialogProps {
|
|
6
|
-
body: string;
|
|
7
4
|
permissions?: RequiredPermission[];
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
export default function PlanDialog({
|
|
7
|
+
export default function PlanDialog({ permissions }: PlanDialogProps) {
|
|
11
8
|
return (
|
|
12
9
|
<div className="plan-dialog">
|
|
13
|
-
<h2>
|
|
10
|
+
<h2>Granted Permissions</h2>
|
|
14
11
|
<div className="plan-dialog-scroll">
|
|
15
|
-
{
|
|
16
|
-
<div className="plan-preview"><Markdown remarkPlugins={[remarkGfm]}>{body}</Markdown></div>
|
|
17
|
-
) : (
|
|
18
|
-
<p className="plan-empty">No execution plan generated for this task. Your task description will be used directly.</p>
|
|
19
|
-
)}
|
|
20
|
-
{permissions && permissions.length > 0 && (
|
|
12
|
+
{permissions && permissions.length > 0 ? (
|
|
21
13
|
<div className="permissions-section">
|
|
22
|
-
<h3>Granted Permissions</h3>
|
|
23
14
|
<ul className="permissions-list">
|
|
24
15
|
{permissions.map((p, i) => (
|
|
25
16
|
<li key={i} className="permission-item">
|
|
@@ -29,6 +20,8 @@ export default function PlanDialog({ body, permissions }: PlanDialogProps) {
|
|
|
29
20
|
))}
|
|
30
21
|
</ul>
|
|
31
22
|
</div>
|
|
23
|
+
) : (
|
|
24
|
+
<p className="plan-empty">No permissions have been granted for this task.</p>
|
|
32
25
|
)}
|
|
33
26
|
</div>
|
|
34
27
|
<div className="plan-dialog-actions">
|
|
@@ -83,16 +83,8 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
83
83
|
const [userPrompt, setUserPrompt] = useState(initial?.user_prompt ?? "");
|
|
84
84
|
const [agent, setAgent] = useState(initial?.agent ?? defaultAgent());
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
// Track whether prompt or agent diverged from the saved values (for existing tasks)
|
|
90
|
-
const promptChanged = !!initial && userPrompt !== (initial.user_prompt ?? "");
|
|
91
|
-
const agentChanged = !!initial && agent !== (initial.agent ?? "");
|
|
92
|
-
const planInvalidated = promptChanged || agentChanged;
|
|
93
|
-
|
|
94
|
-
// Show plan link for existing tasks that have a plan or permissions and haven't been modified
|
|
95
|
-
const hasPlan = !!initial && (!!body || !!initial.permissions?.length) && !planInvalidated;
|
|
86
|
+
// Show permissions link for existing tasks that have granted permissions
|
|
87
|
+
const hasPermissions = !!initial?.permissions?.length;
|
|
96
88
|
|
|
97
89
|
// Plan dialog (view-only for existing tasks)
|
|
98
90
|
const [planDialogOpen, setPlanDialogOpen] = useState(false);
|
|
@@ -195,8 +187,8 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
195
187
|
if (isEdit) {
|
|
196
188
|
payload.id = initial!.id;
|
|
197
189
|
}
|
|
198
|
-
//
|
|
199
|
-
const result = await request<Task & { error?: string }>(method, payload, { timeout:
|
|
190
|
+
// Name generation happens server-side, allow up to 45s
|
|
191
|
+
const result = await request<Task & { error?: string }>(method, payload, { timeout: 45000 });
|
|
200
192
|
if (result.error) {
|
|
201
193
|
setError(result.error);
|
|
202
194
|
return null;
|
|
@@ -247,7 +239,6 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
247
239
|
<div className="task-form">
|
|
248
240
|
{planDialogOpen ? (
|
|
249
241
|
<PlanDialog
|
|
250
|
-
body={body}
|
|
251
242
|
permissions={initial?.permissions}
|
|
252
243
|
/>
|
|
253
244
|
) : (<>
|
|
@@ -270,12 +261,12 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
270
261
|
/>
|
|
271
262
|
|
|
272
263
|
<div className="plan-actions">
|
|
273
|
-
{
|
|
264
|
+
{hasPermissions && (
|
|
274
265
|
<button
|
|
275
266
|
className="btn btn-link"
|
|
276
267
|
onClick={() => setPlanDialogOpen(true)}
|
|
277
268
|
>
|
|
278
|
-
|
|
269
|
+
Granted Permissions
|
|
279
270
|
</button>
|
|
280
271
|
)}
|
|
281
272
|
<div className="agent-picker-section-inline" style={{ marginLeft: "auto" }}>
|
|
@@ -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.9";
|
|
@@ -10,6 +10,7 @@ import { StringCodec } from "nats";
|
|
|
10
10
|
import hostsRoutes from "./routes/hosts.js";
|
|
11
11
|
import pushRoutes from "./routes/push.js";
|
|
12
12
|
import fcmRoutes from "./routes/fcm.js";
|
|
13
|
+
import deviceRoutes from "./routes/device.js";
|
|
13
14
|
import { notifyClients } from "./notify.js";
|
|
14
15
|
import { sendFcmToClients, sendFcmToDevice } from "./fcm.js";
|
|
15
16
|
|
|
@@ -260,6 +261,7 @@ async function main(): Promise<void> {
|
|
|
260
261
|
app.use("/api/hosts", hostsRoutes);
|
|
261
262
|
app.use("/api/push", pushRoutes);
|
|
262
263
|
app.use("/api/fcm", fcmRoutes);
|
|
264
|
+
app.use("/api/device", deviceRoutes);
|
|
263
265
|
|
|
264
266
|
// Public NATS config endpoint (used by PWA for pairing)
|
|
265
267
|
app.get("/api/config", (_req, res) => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import type { Router as RouterType } from "express";
|
|
3
|
+
import { getNatsConnection } from "../nats.js";
|
|
4
|
+
import { StringCodec } from "nats";
|
|
5
|
+
|
|
6
|
+
const router: RouterType = Router();
|
|
7
|
+
|
|
8
|
+
// POST /api/device/notifications - Receive a notification from Android, relay to host via NATS
|
|
9
|
+
router.post("/notifications", async (req: Request, res: Response) => {
|
|
10
|
+
try {
|
|
11
|
+
const { hostId, notification } = req.body;
|
|
12
|
+
|
|
13
|
+
if (!hostId || !notification?.id) {
|
|
14
|
+
res.status(400).json({ error: "hostId and notification with id are required" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const conn = await getNatsConnection();
|
|
19
|
+
const sc = StringCodec();
|
|
20
|
+
conn.publish(
|
|
21
|
+
`host.${hostId}.device.notifications`,
|
|
22
|
+
sc.encode(JSON.stringify(notification)),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
res.json({ ok: true });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error("Device notification relay error:", err);
|
|
28
|
+
res.status(500).json({ error: "Internal server error" });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export default router;
|