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.
Files changed (98) hide show
  1. package/README.md +9 -2
  2. package/dist/agents/agent.d.ts +2 -2
  3. package/dist/agents/aider.d.ts +1 -1
  4. package/dist/agents/aider.js +2 -5
  5. package/dist/agents/claude.d.ts +1 -1
  6. package/dist/agents/claude.js +2 -5
  7. package/dist/agents/cline.d.ts +1 -1
  8. package/dist/agents/cline.js +2 -5
  9. package/dist/agents/codex.d.ts +1 -1
  10. package/dist/agents/codex.js +2 -5
  11. package/dist/agents/copilot.d.ts +1 -1
  12. package/dist/agents/copilot.js +2 -5
  13. package/dist/agents/cursor.d.ts +1 -1
  14. package/dist/agents/cursor.js +2 -5
  15. package/dist/agents/deepagents.d.ts +1 -1
  16. package/dist/agents/deepagents.js +2 -5
  17. package/dist/agents/droid.d.ts +1 -1
  18. package/dist/agents/droid.js +2 -5
  19. package/dist/agents/gemini.d.ts +1 -1
  20. package/dist/agents/gemini.js +2 -5
  21. package/dist/agents/goose.d.ts +1 -1
  22. package/dist/agents/goose.js +2 -5
  23. package/dist/agents/hermes.d.ts +1 -1
  24. package/dist/agents/hermes.js +2 -5
  25. package/dist/agents/kimi.d.ts +1 -1
  26. package/dist/agents/kimi.js +2 -5
  27. package/dist/agents/kiro.d.ts +1 -1
  28. package/dist/agents/kiro.js +2 -5
  29. package/dist/agents/openclaw.d.ts +1 -1
  30. package/dist/agents/openclaw.js +2 -5
  31. package/dist/agents/opencode.d.ts +1 -1
  32. package/dist/agents/opencode.js +2 -5
  33. package/dist/agents/qoder.d.ts +1 -1
  34. package/dist/agents/qoder.js +2 -5
  35. package/dist/agents/qwen.d.ts +1 -1
  36. package/dist/agents/qwen.js +2 -5
  37. package/dist/agents/shared-prompt.js +1 -1
  38. package/dist/commands/run.js +1 -2
  39. package/dist/commands/serve.js +16 -0
  40. package/dist/mcp-handler.d.ts +3 -0
  41. package/dist/mcp-handler.js +59 -3
  42. package/dist/mcp-tools.d.ts +16 -1
  43. package/dist/mcp-tools.js +24 -2
  44. package/dist/notification-store.d.ts +13 -0
  45. package/dist/notification-store.js +19 -0
  46. package/dist/pwa/assets/{index-C8vJwUNi.js → index-DLxrL0hR.js} +42 -42
  47. package/dist/pwa/assets/{web-NxTETXZK.js → web-CBI458eN.js} +1 -1
  48. package/dist/pwa/assets/{web-6UChJFov.js → web-HDs03L2B.js} +1 -1
  49. package/dist/pwa/index.html +1 -1
  50. package/dist/pwa/service-worker.js +1 -1
  51. package/dist/rpc-handler.js +27 -67
  52. package/dist/task.js +2 -3
  53. package/dist/transports/http-transport.js +51 -3
  54. package/dist/types.d.ts +0 -1
  55. package/package.json +2 -2
  56. package/palmier-server/README.md +1 -1
  57. package/palmier-server/pwa/src/components/PlanDialog.tsx +5 -12
  58. package/palmier-server/pwa/src/components/TaskForm.tsx +6 -15
  59. package/palmier-server/pwa/src/constants.ts +1 -1
  60. package/palmier-server/pwa/src/types.ts +0 -1
  61. package/palmier-server/server/src/index.ts +2 -0
  62. package/palmier-server/server/src/routes/device.ts +32 -0
  63. package/palmier-server/spec.md +13 -12
  64. package/src/agents/agent.ts +2 -2
  65. package/src/agents/aider.ts +2 -5
  66. package/src/agents/claude.ts +2 -5
  67. package/src/agents/cline.ts +2 -5
  68. package/src/agents/codex.ts +2 -5
  69. package/src/agents/copilot.ts +2 -5
  70. package/src/agents/cursor.ts +2 -5
  71. package/src/agents/deepagents.ts +2 -5
  72. package/src/agents/droid.ts +2 -5
  73. package/src/agents/gemini.ts +2 -5
  74. package/src/agents/goose.ts +2 -5
  75. package/src/agents/hermes.ts +2 -5
  76. package/src/agents/kimi.ts +2 -5
  77. package/src/agents/kiro.ts +2 -5
  78. package/src/agents/openclaw.ts +2 -5
  79. package/src/agents/opencode.ts +2 -5
  80. package/src/agents/qoder.ts +2 -5
  81. package/src/agents/qwen.ts +2 -5
  82. package/src/agents/shared-prompt.ts +1 -1
  83. package/src/commands/run.ts +1 -2
  84. package/src/commands/serve.ts +16 -1
  85. package/src/mcp-handler.ts +68 -3
  86. package/src/mcp-tools.ts +48 -2
  87. package/src/notification-store.ts +30 -0
  88. package/src/rpc-handler.ts +29 -71
  89. package/src/task.ts +2 -3
  90. package/src/transports/http-transport.ts +49 -3
  91. package/src/types.ts +0 -1
  92. package/test/agent-instructions.test.ts +117 -19
  93. package/test/agent-output-parsing.test.ts +1 -0
  94. package/test/notification-store.test.ts +57 -0
  95. package/test/task-parsing.test.ts +3 -3
  96. package/dist/commands/plan-generation.md +0 -22
  97. package/src/commands/plan-generation.md +0 -22
  98. package/test/fixtures/agent-instructions-snapshot.md +0 -58
@@ -1 +1 @@
1
- import{W as p}from"./index-C8vJwUNi.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
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-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
+ 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};
@@ -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-C8vJwUNi.js"></script>
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":"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())});
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())});
@@ -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
- * Run plan generation for a task prompt using the given agent.
102
- * Returns the generated plan body and task name.
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 generatePlan(projectRoot, userPrompt, agentName) {
105
- const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
106
- const planAgent = getAgent(agentName);
107
- const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
108
- const { output } = await spawnCommand(command, args, {
109
- cwd: projectRoot,
110
- timeout: 120_000,
111
- stdin,
112
- ...(agentEnv ? { env: agentEnv } : {}),
113
- });
114
- let name = "";
115
- const trimmed = output.trim();
116
- let body = trimmed;
117
- const fmMatch = trimmed.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
118
- if (fmMatch) {
119
- try {
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
- // Only generate a plan for longer prompts that benefit from it
182
- let name = "";
183
- let body = "";
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 plan needs regeneration
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 plan if needed (only for longer prompts)
255
- if (existing.frontmatter.user_prompt.length <= 50) {
256
- existing.frontmatter.name = existing.frontmatter.user_prompt;
257
- existing.body = "";
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, body };
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${task.body}\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
- sendJson(res, 200, result.body);
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
@@ -30,7 +30,6 @@ export interface Trigger {
30
30
  }
31
31
  export interface ParsedTask {
32
32
  frontmatter: TaskFrontmatter;
33
- body: string;
34
33
  }
35
34
  /**
36
35
  * - `started`: task is actively running
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.6.8",
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/commands/plan-generation.md','dist/commands/plan-generation.md');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')\"",
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"
@@ -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** — Generated task plans and task results are rendered as rich formatted text using `react-markdown` with `remark-gfm` (GitHub Flavored Markdown), supporting tables, strikethrough, task lists, and autolinks.
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({ body, permissions }: PlanDialogProps) {
7
+ export default function PlanDialog({ permissions }: PlanDialogProps) {
11
8
  return (
12
9
  <div className="plan-dialog">
13
- <h2>Task Execution Plan</h2>
10
+ <h2>Granted Permissions</h2>
14
11
  <div className="plan-dialog-scroll">
15
- {body ? (
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
- // Plan state for existing tasks only
87
- const [body] = useState(initial?.body ?? "");
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
- // Plan generation happens server-side, allow up to 130s
199
- const result = await request<Task & { error?: string }>(method, payload, { timeout: 130000 });
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
- {hasPlan && (
264
+ {hasPermissions && (
274
265
  <button
275
266
  className="btn btn-link"
276
267
  onClick={() => setPlanDialogOpen(true)}
277
268
  >
278
- Execution Plan
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.7";
2
+ export const MIN_HOST_VERSION = "0.6.9";
@@ -17,7 +17,6 @@ export interface Task {
17
17
  foreground_mode?: boolean;
18
18
  permissions?: RequiredPermission[];
19
19
  command?: string;
20
- body?: string;
21
20
  }
22
21
 
23
22
  export interface Trigger {
@@ -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;