palmier 0.9.4 → 0.9.6

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 (44) hide show
  1. package/README.md +4 -2
  2. package/dist/commands/init.js +2 -2
  3. package/dist/pwa/apple-touch-icon.png +0 -0
  4. package/dist/pwa/assets/index-D1bIhEbd.css +1 -0
  5. package/dist/pwa/assets/index-MLEFUP3r.js +120 -0
  6. package/dist/pwa/assets/{web-Eg0A6HEi.js → web-B1sKCc7e.js} +1 -1
  7. package/dist/pwa/assets/{web-Dcldtodb.js → web-B4xEa6WO.js} +1 -1
  8. package/dist/pwa/assets/{web-DdVpqhvX.js → web-ETD-8ZHd.js} +1 -1
  9. package/dist/pwa/favicon.ico +0 -0
  10. package/dist/pwa/index.html +3 -3
  11. package/dist/pwa/manifest.webmanifest +1 -1
  12. package/dist/pwa/pwa-192x192.png +0 -0
  13. package/dist/pwa/pwa-512x512.png +0 -0
  14. package/dist/pwa/service-worker.js +1 -1
  15. package/dist/rpc-handler.js +15 -1
  16. package/dist/task.d.ts +1 -0
  17. package/dist/task.js +14 -0
  18. package/package.json +1 -1
  19. package/palmier-server/pwa/index.html +1 -1
  20. package/palmier-server/pwa/logo/logo_20260421.png +0 -0
  21. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  22. package/palmier-server/pwa/public/favicon.ico +0 -0
  23. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  24. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  25. package/palmier-server/pwa/src/App.css +14 -0
  26. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +1 -1
  27. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +5 -6
  28. package/palmier-server/pwa/src/components/HostMenu.tsx +28 -1
  29. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -2
  30. package/palmier-server/pwa/src/components/SessionsView.tsx +2 -1
  31. package/palmier-server/pwa/src/components/TaskCard.tsx +14 -6
  32. package/palmier-server/pwa/src/components/TaskForm.tsx +12 -6
  33. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
  34. package/palmier-server/pwa/src/formatTime.ts +38 -4
  35. package/palmier-server/pwa/src/pages/Dashboard.tsx +4 -2
  36. package/palmier-server/pwa/src/types.ts +2 -0
  37. package/palmier-server/pwa/vite.config.ts +1 -1
  38. package/src/commands/init.ts +2 -2
  39. package/src/rpc-handler.ts +12 -1
  40. package/src/task.ts +13 -0
  41. package/dist/pwa/assets/index-BsB1tIsn.css +0 -1
  42. package/dist/pwa/assets/index-DX5qJgHZ.js +0 -120
  43. package/palmier-server/pwa/logo/logo-prompt.md +0 -28
  44. package/palmier-server/pwa/logo/logo_20260330.png +0 -0
@@ -1 +1 @@
1
- import{W as t}from"./index-DX5qJgHZ.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-MLEFUP3r.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{s as AppWeb};
@@ -1 +1 @@
1
- import{W as p}from"./index-DX5qJgHZ.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-MLEFUP3r.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 i}from"./index-DX5qJgHZ.js";function o(){const t=window.navigator.connection||window.navigator.mozConnection||window.navigator.webkitConnection;let n="unknown";const e=t?t.type||t.effectiveType:null;if(e&&typeof e=="string")switch(e){case"bluetooth":case"cellular":n="cellular";break;case"none":n="none";break;case"ethernet":case"wifi":case"wimax":n="wifi";break;case"other":case"unknown":n="unknown";break;case"slow-2g":case"2g":case"3g":n="cellular";break;case"4g":n="wifi";break}return n}class s extends i{constructor(){super(),this.handleOnline=()=>{const e={connected:!0,connectionType:o()};this.notifyListeners("networkStatusChange",e)},this.handleOffline=()=>{const n={connected:!1,connectionType:"none"};this.notifyListeners("networkStatusChange",n)},typeof window<"u"&&(window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline))}async getStatus(){if(!window.navigator)throw this.unavailable("Browser does not support the Network Information API");const n=window.navigator.onLine,e=o();return{connected:n,connectionType:n?e:"none"}}}const r=new s;export{r as Network,s as NetworkWeb};
1
+ import{W as i}from"./index-MLEFUP3r.js";function o(){const t=window.navigator.connection||window.navigator.mozConnection||window.navigator.webkitConnection;let n="unknown";const e=t?t.type||t.effectiveType:null;if(e&&typeof e=="string")switch(e){case"bluetooth":case"cellular":n="cellular";break;case"none":n="none";break;case"ethernet":case"wifi":case"wimax":n="wifi";break;case"other":case"unknown":n="unknown";break;case"slow-2g":case"2g":case"3g":n="cellular";break;case"4g":n="wifi";break}return n}class s extends i{constructor(){super(),this.handleOnline=()=>{const e={connected:!0,connectionType:o()};this.notifyListeners("networkStatusChange",e)},this.handleOffline=()=>{const n={connected:!1,connectionType:"none"};this.notifyListeners("networkStatusChange",n)},typeof window<"u"&&(window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline))}async getStatus(){if(!window.navigator)throw this.unavailable("Browser does not support the Network Information API");const n=window.navigator.onLine,e=o();return{connected:n,connectionType:n?e:"none"}}}const r=new s;export{r as Network,s as NetworkWeb};
Binary file
@@ -7,9 +7,9 @@
7
7
  <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8
8
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
9
  <title>Palmier</title>
10
- <meta name="description" content="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you also use your phone as an agent remote." />
11
- <script type="module" crossorigin src="/assets/index-DX5qJgHZ.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-BsB1tIsn.css">
10
+ <meta name="description" content="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote." />
11
+ <script type="module" crossorigin src="/assets/index-MLEFUP3r.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-D1bIhEbd.css">
13
13
  <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -1 +1 @@
1
- {"name":"Palmier","short_name":"Palmier","description":"Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you also use your phone as an agent remote.","start_url":"/","display":"standalone","background_color":"#ffffff","theme_color":"#2E5CE5","lang":"en","scope":"/","icons":[{"src":"pwa-192x192.png","sizes":"192x192","type":"image/png"},{"src":"pwa-512x512.png","sizes":"512x512","type":"image/png"}]}
1
+ {"name":"Palmier","short_name":"Palmier","description":"Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote.","start_url":"/","display":"standalone","background_color":"#ffffff","theme_color":"#2E5CE5","lang":"en","scope":"/","icons":[{"src":"pwa-192x192.png","sizes":"192x192","type":"image/png"},{"src":"pwa-512x512.png","sizes":"512x512","type":"image/png"}]}
Binary file
Binary file
@@ -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 d={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},U=n=>[d.prefix,n,d.suffix].filter(e=>e&&e.length>0).join("-"),O=n=>{for(const e of Object.keys(d))n(e)},L={updateDetails:n=>{O(e=>{typeof n[e]=="string"&&(d[e]=n[e])})},getGoogleAnalyticsName:n=>n||U(d.googleAnalytics),getPrecacheName:n=>n||U(d.precache),getPrefix:()=>d.prefix,getRuntimeName:n=>n||U(d.runtime),getSuffix:()=>d.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 x(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=x(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=x(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 $=new Set;async function B(){for(const n of $)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"),u=l?await H(o,a.clone(),["__WB_REVISION__"],c):null;try{await o.put(a,l?i.clone():i)}catch(f){if(f instanceof Error)throw f.name==="QuotaExceededError"&&await B(),f}for(const f of this.iterateCallbacks("cacheDidUpdate"))await f({cacheName:r,oldResponse:u,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 T=()=>(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(f){l=Promise.reject(f)}const u=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||u)&&(l=l.catch(async f=>{if(u)try{return await u.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(f=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw f})),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=T(),t=new se(e,n);Z(t)}function ae(n){T().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"ad71e8f7662fe1ae7357483fabd4554c","url":"index.html"},{"revision":null,"url":"assets/web-Eg0A6HEi.js"},{"revision":null,"url":"assets/web-DdVpqhvX.js"},{"revision":null,"url":"assets/web-Dcldtodb.js"},{"revision":null,"url":"assets/index-DX5qJgHZ.js"},{"revision":null,"url":"assets/index-BsB1tIsn.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":"1ff98b282488b8509fa6e5c341beda90","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,u=c.task_id;n.waitUntil(self.registration.getNotifications().then(f=>{var g,P,K;for(const y of f)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}u&&((K=y.data)==null?void 0:K.task_id)===u&&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.host_id,i=t.task_id,r=t.run_id,c=a?`/hosts/${encodeURIComponent(a)}`:"",o=c&&i&&r?`${c}/runs/${encodeURIComponent(i)}/${encodeURIComponent(r)}`:c&&i?`${c}/runs/${encodeURIComponent(i)}/latest`:c||"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(l=>{for(const u of l)if(u.url.includes(self.location.origin)&&"focus"in u)return u.navigate(o),u.focus();return self.clients.openWindow(o)}))}});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 T=()=>(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(f){l=Promise.reject(f)}const u=r&&r.catchHandler;return l instanceof Promise&&(this._catchHandler||u)&&(l=l.catch(async f=>{if(u)try{return await u.handle({url:s,request:e,event:t,params:i})}catch(g){g instanceof Error&&(f=g)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw f})),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=T(),t=new se(e,n);Z(t)}function ae(n){T().precache(n)}function ie(n,e){ae(n),ne(e)}ie([{"revision":"38013143dc2183340ede8bc1c5124507","url":"registerSW.js"},{"revision":"c60052b7fa76e3b8737fdd47056f224e","url":"index.html"},{"revision":null,"url":"assets/web-ETD-8ZHd.js"},{"revision":null,"url":"assets/web-B4xEa6WO.js"},{"revision":null,"url":"assets/web-B1sKCc7e.js"},{"revision":null,"url":"assets/index-MLEFUP3r.js"},{"revision":null,"url":"assets/index-D1bIhEbd.css"},{"revision":"52153002d5b9308ba313d435ee6dedcd","url":"apple-touch-icon.png"},{"revision":"438da4214ab645a9c18a515c92cd9dfc","url":"favicon.ico"},{"revision":"7499c65c226796ebfc9674049a757362","url":"pwa-192x192.png"},{"revision":"e0cfcedd3448d33a3e75880e29be3d77","url":"pwa-512x512.png"},{"revision":"50cd09d8b1eba396c10f48577e4e8ae3","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,u=c.task_id;n.waitUntil(self.registration.getNotifications().then(f=>{var g,P,K;for(const y of f)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}u&&((K=y.data)==null?void 0:K.task_id)===u&&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.host_id,i=t.task_id,r=t.run_id,c=a?`/hosts/${encodeURIComponent(a)}`:"",o=c&&i&&r?`${c}/runs/${encodeURIComponent(i)}/${encodeURIComponent(r)}`:c&&i?`${c}/runs/${encodeURIComponent(i)}/latest`:c||"/";n.waitUntil(self.clients.matchAll({type:"window",includeUncontrolled:!0}).then(l=>{for(const u of l)if(u.url.includes(self.location.origin)&&"focus"in u)return u.navigate(o),u.focus();return self.clients.openWindow(o)}))}});self.addEventListener("install",()=>{self.skipWaiting()});self.addEventListener("activate",n=>{n.waitUntil(self.clients.claim())});
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
4
+ import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, isTaskInList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
5
5
  import { resolvePending, getPending, listPending } from "./pending-requests.js";
6
6
  import { getPlatform } from "./platform/index.js";
7
7
  import { spawnCommand } from "./spawn-command.js";
@@ -127,6 +127,7 @@ export function createRpcHandler(config, nc) {
127
127
  agents: config.agents ?? [],
128
128
  version: currentVersion,
129
129
  host_platform: process.platform,
130
+ host_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
130
131
  linked_client_token: getLinkedDevice()?.clientToken ?? null,
131
132
  pending_prompts: listPending(),
132
133
  lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
@@ -555,10 +556,23 @@ export function createRpcHandler(config, nc) {
555
556
  if (!params.task_id || !params.run_id) {
556
557
  return { error: "task_id and run_id are required" };
557
558
  }
559
+ const deleteTaskDir = getTaskDir(config.projectRoot, params.task_id);
558
560
  const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
559
561
  if (!deleted) {
560
562
  return { error: "History entry not found" };
561
563
  }
564
+ const { total: remainingRuns } = readHistory(config.projectRoot, { task_id: params.task_id, limit: 1 });
565
+ if (remainingRuns === 0 && !isTaskInList(config.projectRoot, params.task_id)) {
566
+ try {
567
+ getPlatform().removeTaskTimer(params.task_id);
568
+ }
569
+ catch { /* best-effort */ }
570
+ clearTaskQueue(params.task_id);
571
+ try {
572
+ fs.rmSync(deleteTaskDir, { recursive: true, force: true });
573
+ }
574
+ catch { /* best-effort */ }
575
+ }
562
576
  return { ok: true, task_id: params.task_id, run_id: params.run_id };
563
577
  }
564
578
  case "host.update": {
package/dist/task.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare function parseTaskFile(taskDir: string): ParsedTask;
3
3
  export declare function parseTaskContent(content: string): ParsedTask;
4
4
  export declare function writeTaskFile(taskDir: string, task: ParsedTask): void;
5
5
  export declare function appendTaskList(projectRoot: string, taskId: string): void;
6
+ export declare function isTaskInList(projectRoot: string, taskId: string): boolean;
6
7
  export declare function removeFromTaskList(projectRoot: string, taskId: string): boolean;
7
8
  export declare function listTasks(projectRoot: string): ParsedTask[];
8
9
  export declare function getTaskDir(projectRoot: string, taskId: string): string;
package/dist/task.js CHANGED
@@ -35,6 +35,20 @@ export function appendTaskList(projectRoot, taskId) {
35
35
  const listPath = path.join(projectRoot, "tasks.jsonl");
36
36
  fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
37
37
  }
38
+ export function isTaskInList(projectRoot, taskId) {
39
+ const listPath = path.join(projectRoot, "tasks.jsonl");
40
+ if (!fs.existsSync(listPath))
41
+ return false;
42
+ const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
43
+ for (const line of lines) {
44
+ try {
45
+ if (JSON.parse(line).task_id === taskId)
46
+ return true;
47
+ }
48
+ catch { /* skip malformed */ }
49
+ }
50
+ return false;
51
+ }
38
52
  export function removeFromTaskList(projectRoot, taskId) {
39
53
  const listPath = path.join(projectRoot, "tasks.jsonl");
40
54
  if (!fs.existsSync(listPath))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -7,7 +7,7 @@
7
7
  <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8
8
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
9
  <title>Palmier</title>
10
- <meta name="description" content="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you also use your phone as an agent remote." />
10
+ <meta name="description" content="Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote." />
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1174,6 +1174,14 @@ body {
1174
1174
  margin: 0;
1175
1175
  }
1176
1176
 
1177
+ .schedule-section-hint {
1178
+ font-weight: 400;
1179
+ text-transform: none;
1180
+ letter-spacing: 0;
1181
+ color: var(--color-text-secondary);
1182
+ margin-left: 6px;
1183
+ }
1184
+
1177
1185
  .schedule-reactive {
1178
1186
  display: flex;
1179
1187
  flex-direction: column;
@@ -1895,6 +1903,12 @@ body {
1895
1903
  color: var(--color-muted);
1896
1904
  }
1897
1905
 
1906
+ .drawer-host-time {
1907
+ font-size: 0.75rem;
1908
+ color: var(--color-muted);
1909
+ margin-bottom: var(--space-xs);
1910
+ }
1911
+
1898
1912
  .drawer-legal {
1899
1913
  font-size: 0.75rem;
1900
1914
  color: var(--color-muted);
@@ -19,7 +19,7 @@ interface CapabilityDefinition {
19
19
  const CAPABILITIES: CapabilityDefinition[] = [
20
20
  { capability: "sms-read", label: "Read SMS", group: "Messaging" },
21
21
  { capability: "sms-send", label: "Send SMS", group: "Messaging" },
22
- { capability: "send-email", label: "Send Email", group: "Messaging" },
22
+ { capability: "send-email", label: "Prompt Email to Send", group: "Messaging" },
23
23
  { capability: "notifications", label: "Notifications from Other Apps", group: "Data" },
24
24
  { capability: "contacts", label: "Manage Contacts", group: "Data" },
25
25
  { capability: "calendar", label: "Manage Calendar", group: "Data" },
@@ -15,13 +15,12 @@ const SVG_PROPS = {
15
15
  strokeLinejoin: "round" as const,
16
16
  };
17
17
 
18
- function WifiIcon() {
18
+ function LinkedDevicesIcon() {
19
19
  return (
20
20
  <svg {...SVG_PROPS} aria-hidden="true">
21
- <path d="M5 12.55a11 11 0 0 1 14.08 0" />
22
- <path d="M1.42 9a16 16 0 0 1 21.16 0" />
23
- <path d="M8.53 16.11a6 6 0 0 1 6.95 0" />
24
- <line x1="12" y1="20" x2="12.01" y2="20" />
21
+ <rect x="3" y="6" width="5" height="12" rx="1" />
22
+ <rect x="16" y="6" width="5" height="12" rx="1" />
23
+ <line x1="8" y1="12" x2="16" y2="12" />
25
24
  </svg>
26
25
  );
27
26
  }
@@ -71,7 +70,7 @@ export default function ConnectionStatusIcon() {
71
70
  let modifier: string;
72
71
  switch (mode) {
73
72
  case "lan":
74
- icon = <WifiIcon />;
73
+ icon = <LinkedDevicesIcon />;
75
74
  label = "Connected via LAN";
76
75
  modifier = "lan";
77
76
  break;
@@ -34,6 +34,28 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
34
34
  const isDesktop = useMediaQuery("(min-width: 768px)");
35
35
  const [linkingBusy, setLinkingBusy] = useState(false);
36
36
 
37
+ const [now, setNow] = useState(() => Date.now());
38
+ useEffect(() => {
39
+ const tick = () => setNow(Date.now());
40
+ const msUntilNextMinute = 60_000 - (Date.now() % 60_000);
41
+ const first = setTimeout(() => {
42
+ tick();
43
+ const iv = setInterval(tick, 60_000);
44
+ (first as unknown as { _iv: ReturnType<typeof setInterval> })._iv = iv;
45
+ }, msUntilNextMinute);
46
+ return () => {
47
+ const iv = (first as unknown as { _iv?: ReturnType<typeof setInterval> })._iv;
48
+ if (iv) clearInterval(iv);
49
+ clearTimeout(first);
50
+ };
51
+ }, []);
52
+ const hostClock = activeHost.timezone
53
+ ? new Date(now).toLocaleString(undefined, {
54
+ month: "short", day: "numeric", hour: "numeric", minute: "2-digit",
55
+ timeZone: activeHost.timezone,
56
+ })
57
+ : "";
58
+
37
59
  async function makeThisLinkedDevice() {
38
60
  if (!Device || !request || !activeClientToken) return;
39
61
  setLinkingBusy(true);
@@ -282,9 +304,14 @@ export default function HostMenu({ daemonVersion, linkedClientToken, request, on
282
304
  )}
283
305
 
284
306
  <div className="drawer-footer">
307
+ {activeHost.timezone && (
308
+ <div className="drawer-host-time">
309
+ Host time: {hostClock} · {activeHost.timezone}
310
+ </div>
311
+ )}
285
312
  {daemonVersion && (
286
313
  <div className="drawer-version">
287
- Palmier v{daemonVersion}
314
+ Host version: v{daemonVersion}
288
315
  </div>
289
316
  )}
290
317
  <div className="drawer-legal">
@@ -4,7 +4,7 @@ import Markdown from "react-markdown";
4
4
  import remarkGfm from "remark-gfm";
5
5
  import remarkBreaks from "remark-breaks";
6
6
  import { getAgentLabel } from "../agentLabels";
7
- import { formatTime } from "../formatTime";
7
+ import { useFormatTime } from "../formatTime";
8
8
  import { useBackClose } from "../hooks/useBackClose";
9
9
  import type { ConversationMessage } from "../types";
10
10
 
@@ -19,6 +19,7 @@ interface RunDetailViewProps {
19
19
 
20
20
  export default function RunDetailView({ connected, hostId, request, subscribeEvents, taskId, runId }: RunDetailViewProps) {
21
21
  const navigate = useNavigate();
22
+ const formatTime = useFormatTime();
22
23
  const [loading, setLoading] = useState(true);
23
24
  const [messages, setMessages] = useState<ConversationMessage[]>([]);
24
25
  const [runState, setRunState] = useState<string | undefined>();
@@ -300,7 +301,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
300
301
  ref={followupInputRef}
301
302
  className="chat-input"
302
303
  type="text"
303
- placeholder="Follow up"
304
+ placeholder="Follow-up message"
304
305
  value={followupText}
305
306
  onChange={(e) => setFollowupText(e.target.value)}
306
307
  disabled={sendingFollowup}
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
- import { formatTime } from "../formatTime";
3
+ import { useFormatTime } from "../formatTime";
4
4
  import { confirmLeaveDraft } from "../draftGuard";
5
5
  import SessionComposer from "./SessionComposer";
6
6
  import PullToRefreshIndicator from "./PullToRefreshIndicator";
@@ -22,6 +22,7 @@ interface SessionsViewProps {
22
22
  const PAGE_SIZE = 10;
23
23
 
24
24
  export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, filterTaskId, onClearFilter }: SessionsViewProps) {
25
+ const formatTime = useFormatTime();
25
26
  const [entries, setEntries] = useState<HistoryEntry[]>([]);
26
27
  const [total, setTotal] = useState(0);
27
28
  const [loading, setLoading] = useState(false);
@@ -1,7 +1,7 @@
1
1
  import { useState, useRef, useEffect } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { useHostConnection } from "../contexts/HostConnectionContext";
4
- import { formatTime } from "../formatTime";
4
+ import { useFormatTime } from "../formatTime";
5
5
  import { getAgentLabel } from "../agentLabels";
6
6
  import type { Task, TaskStatus } from "../types";
7
7
 
@@ -16,7 +16,9 @@ interface TaskCardProps {
16
16
  }
17
17
 
18
18
  export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun }: TaskCardProps) {
19
- const { request } = useHostConnection();
19
+ const { request, activeHost } = useHostConnection();
20
+ const formatTime = useFormatTime();
21
+ const timeZone = activeHost.timezone;
20
22
  const [aborting, setAborting] = useState(false);
21
23
  const [menuOpen, setMenuOpen] = useState(false);
22
24
  const [sheetOpen, setSheetOpen] = useState(false);
@@ -105,7 +107,9 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
105
107
  function classifyValue(scheduleType: "crons" | "specific_times", value: string): { kind: string; detail: string } {
106
108
  if (scheduleType === "specific_times") {
107
109
  const d = new Date(value);
108
- const label = isNaN(d.getTime()) ? value : `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })} at ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}`;
110
+ const label = isNaN(d.getTime())
111
+ ? value
112
+ : `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", timeZone })} at ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", timeZone })}`;
109
113
  return { kind: "specific_times", detail: label };
110
114
  }
111
115
  const parts = value.split(" ");
@@ -113,9 +117,13 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
113
117
  const [min, hour, dom, , dow] = parts;
114
118
  const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
115
119
  if (hour === "*") return { kind: "hourly", detail: "" };
116
- const d = new Date();
117
- d.setHours(Number(hour), Number(min), 0, 0);
118
- const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
120
+ // Cron HH:MM is in the host's wall clock. Format directly — no Date
121
+ // conversion to avoid implicit shifting through the viewer's local zone.
122
+ const h = Number(hour);
123
+ const m = Number(min);
124
+ const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
125
+ const ampm = h >= 12 ? "PM" : "AM";
126
+ const time = `${hour12}:${String(m).padStart(2, "0")} ${ampm}`;
119
127
  if (dow !== "*") return { kind: "weekly", detail: `${DAYS[Number(dow)] ?? dow} at ${time}` };
120
128
  if (dom !== "*") return { kind: "monthly", detail: `day ${dom} at ${time}` };
121
129
  return { kind: "daily", detail: time };
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from "react";
2
2
  import { Capacitor } from "@capacitor/core";
3
3
  import { useHostConnection } from "../contexts/HostConnectionContext";
4
4
  import { useHostStore } from "../contexts/HostStoreContext";
5
+ import { hostNowParts } from "../formatTime";
5
6
  import PermissionsDialog from "./PermissionsDialog";
6
7
  import { useBackClose } from "../hooks/useBackClose";
7
8
  import { Device } from "../native/Device";
@@ -234,8 +235,13 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
234
235
  || (scheduleMode === "on_new_notification" && notificationApp.trim() !== initialNotificationApp.trim())
235
236
  || (scheduleMode === "on_new_sms" && smsSender.trim() !== initialSmsSender.trim());
236
237
 
238
+ const hostNow = hostNowParts(activeHost.timezone);
237
239
  const hasInvalidTrigger = modeIsScheduled && triggerRows.some((r) =>
238
- r.schedule === "specific_times" && (!r.onceDate || new Date(`${r.onceDate}T${r.onceTime}`) <= new Date())
240
+ r.schedule === "specific_times" && (
241
+ !r.onceDate
242
+ || r.onceDate < hostNow.date
243
+ || (r.onceDate === hostNow.date && (r.onceTime ?? "") <= hostNow.time)
244
+ )
239
245
  );
240
246
  const canSave = isDirty
241
247
  && !!userPrompt.trim()
@@ -425,7 +431,9 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
425
431
 
426
432
  <div className="toggles-group">
427
433
  <div className="schedule-section">
428
- <h3 className="schedule-section-title">Schedule</h3>
434
+ <h3 className="schedule-section-title">
435
+ Schedule <span className="schedule-section-hint">based on host time</span>
436
+ </h3>
429
437
  <select
430
438
  className="form-select"
431
439
  value={scheduleMode}
@@ -594,16 +602,14 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
594
602
  className="form-input"
595
603
  type="date"
596
604
  value={row.onceDate}
597
- min={new Date().toISOString().split("T")[0]}
605
+ min={hostNow.date}
598
606
  onChange={(e) => updateRow(i, { onceDate: e.target.value })}
599
607
  />
600
608
  <input
601
609
  className="form-input"
602
610
  type="time"
603
611
  value={row.onceTime}
604
- min={row.onceDate === new Date().toISOString().split("T")[0]
605
- ? new Date().toTimeString().slice(0, 5)
606
- : undefined}
612
+ min={row.onceDate === hostNow.date ? hostNow.time : undefined}
607
613
  onChange={(e) => updateRow(i, { onceTime: e.target.value })}
608
614
  />
609
615
  </div>
@@ -15,6 +15,7 @@ interface HostStoreContextValue {
15
15
  renamePairedHost(hostId: string, name: string): void;
16
16
  setHostLanUrl(hostId: string, lanUrl: string | undefined): void;
17
17
  setHostLastAgent(hostId: string, agent: string): void;
18
+ setHostTimezone(hostId: string, timezone: string | undefined): void;
18
19
  }
19
20
 
20
21
  const HostStoreContext = createContext<HostStoreContextValue | null>(null);
@@ -91,6 +92,16 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
91
92
  );
92
93
  }, []);
93
94
 
95
+ const setHostTimezone = useCallback((hostId: string, timezone: string | undefined) => {
96
+ setPairedHosts((prev) =>
97
+ prev.map((h) => {
98
+ if (h.hostId !== hostId) return h;
99
+ if (h.timezone === timezone) return h;
100
+ return { ...h, timezone };
101
+ })
102
+ );
103
+ }, []);
104
+
94
105
  return (
95
106
  <HostStoreContext.Provider value={{
96
107
  pairedHosts,
@@ -99,6 +110,7 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
99
110
  renamePairedHost,
100
111
  setHostLanUrl,
101
112
  setHostLastAgent,
113
+ setHostTimezone,
102
114
  }}>
103
115
  {children}
104
116
  </HostStoreContext.Provider>
@@ -1,10 +1,44 @@
1
+ import { useCallback } from "react";
2
+ import { useHostConnection } from "./contexts/HostConnectionContext";
3
+
1
4
  /**
2
5
  * Format a timestamp for display. Shows time only if today, otherwise includes the date.
6
+ * If `timeZone` is provided, renders the wall-clock time in that IANA zone.
3
7
  */
4
- export function formatTime(ms: number): string {
8
+ export function formatTime(ms: number, timeZone?: string): string {
5
9
  const d = new Date(ms);
6
10
  const now = new Date();
7
- const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
8
- if (d.toDateString() === now.toDateString()) return time;
9
- return `${d.toLocaleDateString(undefined, { month: "short", day: "numeric" })} ${time}`;
11
+ const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", timeZone });
12
+ // toDateString() would compare in the viewer's local zone, masking day
13
+ // boundaries in the host's zone. Format both dates in the target zone and
14
+ // compare the strings instead.
15
+ const dayOpts: Intl.DateTimeFormatOptions = { year: "numeric", month: "2-digit", day: "2-digit", timeZone };
16
+ const sameDay = d.toLocaleDateString(undefined, dayOpts) === now.toLocaleDateString(undefined, dayOpts);
17
+ if (sameDay) return time;
18
+ return `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone })} ${time}`;
19
+ }
20
+
21
+ /** Returns a `formatTime` bound to the active host's timezone. */
22
+ export function useFormatTime(): (ms: number) => string {
23
+ const { activeHost } = useHostConnection();
24
+ const tz = activeHost.timezone;
25
+ return useCallback((ms: number) => formatTime(ms, tz), [tz]);
26
+ }
27
+
28
+ /**
29
+ * Current wall-clock in the host timezone, as ISO-shaped date + HH:MM strings.
30
+ * Used for "not in the past" validation on scheduled triggers so the floor is
31
+ * the host's now, not the viewer's now.
32
+ */
33
+ export function hostNowParts(timeZone?: string): { date: string; time: string } {
34
+ const parts = new Intl.DateTimeFormat("en-CA", {
35
+ timeZone,
36
+ year: "numeric", month: "2-digit", day: "2-digit",
37
+ hour: "2-digit", minute: "2-digit", hour12: false,
38
+ }).formatToParts(new Date());
39
+ const pick = (t: string) => parts.find((p) => p.type === t)?.value ?? "";
40
+ return {
41
+ date: `${pick("year")}-${pick("month")}-${pick("day")}`,
42
+ time: `${pick("hour")}:${pick("minute")}`,
43
+ };
10
44
  }
@@ -46,7 +46,7 @@ interface PermissionPrompt { permissions: RequiredPermission[]; sessionName?: st
46
46
  interface InputPrompt { questions: string[]; description?: string; sessionName?: string }
47
47
 
48
48
  export default function Dashboard() {
49
- const { removePairedHost, setHostLanUrl } = useHostStore();
49
+ const { removePairedHost, setHostLanUrl, setHostTimezone } = useHostStore();
50
50
  const { connected, request, subscribeEvents, unauthorized, activeHost } = useHostConnection();
51
51
  const hostId = activeHost.hostId;
52
52
  const activeClientToken = activeHost.clientToken || null;
@@ -100,6 +100,7 @@ export default function Dashboard() {
100
100
  agents?: AgentInfo[];
101
101
  version?: string | null;
102
102
  host_platform?: string;
103
+ host_timezone?: string;
103
104
  linked_client_token?: string | null;
104
105
  pending_prompts?: PendingPrompt[];
105
106
  lan_url?: string | null;
@@ -113,6 +114,7 @@ export default function Dashboard() {
113
114
  setDaemonVersion(version);
114
115
  setUpdateRequired(!!version && isOlderThan(version, MIN_HOST_VERSION));
115
116
  setHostLanUrl(hostId, result.lan_url ?? undefined);
117
+ setHostTimezone(hostId, result.host_timezone);
116
118
 
117
119
  // Seed modal state from already-pending prompts.
118
120
  const confirms = new Map<string, ConfirmPrompt>();
@@ -146,7 +148,7 @@ export default function Dashboard() {
146
148
  setInputValues(inputVals);
147
149
  })
148
150
  .catch(() => { /* silent — update-required prompt guards the broken case */ });
149
- }, [connected, hostId, request, setHostLanUrl]);
151
+ }, [connected, hostId, request, setHostLanUrl, setHostTimezone]);
150
152
 
151
153
  // Always-on event subscription for modal lifecycle. Independent of which tab
152
154
  // is active. Task-card status updates happen inside TasksView while mounted.
@@ -70,4 +70,6 @@ export interface PairedHost {
70
70
  lanUrl?: string;
71
71
  /** Last-used agent key for this host. Seeds the agent picker on the session composer and task form. */
72
72
  lastAgent?: string;
73
+ /** IANA timezone from the host, refreshed on each `host.info`. Drives all time rendering. */
74
+ timezone?: string;
73
75
  }
@@ -24,7 +24,7 @@ export default defineConfig({
24
24
  manifest: {
25
25
  name: "Palmier",
26
26
  short_name: "Palmier",
27
- description: "Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you also use your phone as an agent remote.",
27
+ description: "Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote.",
28
28
  start_url: "/",
29
29
  display: "standalone",
30
30
  background_color: "#ffffff",