palmier 0.9.3 → 0.9.4

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.
@@ -1 +1 @@
1
- import{W as p}from"./index-CknFGshO.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-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 +1 @@
1
- import{W as i}from"./index-CknFGshO.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-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 +1 @@
1
- import{W as t}from"./index-CknFGshO.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-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};
@@ -7,8 +7,8 @@
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. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere." />
11
- <script type="module" crossorigin src="/assets/index-CknFGshO.js"></script>
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
12
  <link rel="stylesheet" crossorigin href="/assets/index-BsB1tIsn.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 +1 @@
1
- {"name":"Palmier","short_name":"Palmier","description":"Bridge your AI agents and your phone. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere.","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 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,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":"24edcc91783323299757366e29c6a55e","url":"index.html"},{"revision":null,"url":"assets/web-a9jK1xeo.js"},{"revision":null,"url":"assets/web-Dl9aC-Qr.js"},{"revision":null,"url":"assets/web-DdzXb-jW.js"},{"revision":null,"url":"assets/index-CknFGshO.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":"1c5f5d4fb1f74fd2c4cb52075f2643f6","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":"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())});
@@ -1,8 +1,7 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { spawn } from "child_process";
5
- import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
4
+ import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus } from "./task.js";
6
5
  import { resolvePending, getPending, listPending } from "./pending-requests.js";
7
6
  import { getPlatform } from "./platform/index.js";
8
7
  import { spawnCommand } from "./spawn-command.js";
@@ -251,6 +250,7 @@ export function createRpcHandler(config, nc) {
251
250
  agent: params.agent,
252
251
  schedule_enabled: false,
253
252
  requires_confirmation: params.requires_confirmation ?? false,
253
+ one_off: true,
254
254
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
255
255
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
256
256
  ...(params.command ? { command: params.command } : {}),
@@ -260,13 +260,9 @@ export function createRpcHandler(config, nc) {
260
260
  // One-off run: do NOT append to tasks.jsonl.
261
261
  const runId = createRunDir(taskDir, name, Date.now(), params.agent);
262
262
  appendHistory(config.projectRoot, { task_id: id, run_id: runId });
263
- const script = process.argv[1] || "palmier";
264
- const child = spawn(process.execPath, [script, "run", id], {
265
- detached: true,
266
- stdio: "ignore",
267
- windowsHide: true,
268
- });
269
- child.unref();
263
+ const platform = getPlatform();
264
+ platform.installTaskTimer(config, task);
265
+ await platform.startTask(id);
270
266
  return { ok: true, task_id: id, run_id: runId };
271
267
  }
272
268
  case "task.run": {
@@ -325,11 +321,14 @@ export function createRpcHandler(config, nc) {
325
321
  if (stdin != null)
326
322
  child.stdin.end(stdin);
327
323
  activeFollowups.set(followupKey, child);
324
+ if (child.pid)
325
+ writeFollowupStatus(followupRunDir, { pid: child.pid, spawned_at: Date.now() });
328
326
  const chunks = [];
329
327
  child.stdout?.on("data", (d) => chunks.push(d));
330
328
  child.stderr?.on("data", (d) => process.stderr.write(d));
331
329
  child.on("close", async (code) => {
332
330
  activeFollowups.delete(followupKey);
331
+ deleteFollowupStatus(followupRunDir);
333
332
  // stop_followup already wrote the stopped status.
334
333
  if (child.killed)
335
334
  return;
@@ -352,6 +351,7 @@ export function createRpcHandler(config, nc) {
352
351
  });
353
352
  child.on("error", async (err) => {
354
353
  activeFollowups.delete(followupKey);
354
+ deleteFollowupStatus(followupRunDir);
355
355
  console.error(`Follow-up failed for ${followupKey}:`, err);
356
356
  appendRunMessage(followupTaskDir, params.run_id, {
357
357
  role: "status",
@@ -369,22 +369,37 @@ export function createRpcHandler(config, nc) {
369
369
  return { error: "run_id is required" };
370
370
  }
371
371
  const stopKey = `${params.id}:${params.run_id}`;
372
+ const stopTaskDir = getTaskDir(config.projectRoot, params.id);
373
+ const stopRunDir = getRunDir(stopTaskDir, params.run_id);
372
374
  const child = activeFollowups.get(stopKey);
375
+ let pidToKill = child?.pid;
373
376
  if (!child) {
374
- return { error: "No active follow-up for this run" };
377
+ // Daemon restarted since spawn — the in-memory handle is gone but
378
+ // the child may still be running. Fall back to the persisted PID.
379
+ const persisted = readFollowupStatus(stopRunDir);
380
+ if (!persisted)
381
+ return { error: "No active follow-up for this run" };
382
+ pidToKill = persisted.pid;
375
383
  }
376
- if (process.platform === "win32" && child.pid) {
377
- try {
378
- const { execFileSync } = await import("child_process");
379
- execFileSync("taskkill", ["/pid", String(child.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
384
+ if (pidToKill !== undefined) {
385
+ if (process.platform === "win32") {
386
+ try {
387
+ const { execFileSync } = await import("child_process");
388
+ execFileSync("taskkill", ["/pid", String(pidToKill), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
389
+ }
390
+ catch { /* may have already exited */ }
391
+ }
392
+ else if (child) {
393
+ child.kill();
394
+ }
395
+ else {
396
+ try {
397
+ process.kill(pidToKill, "SIGTERM");
398
+ }
399
+ catch { /* already dead */ }
380
400
  }
381
- catch { /* may have already exited */ }
382
- }
383
- else {
384
- child.kill();
385
401
  }
386
402
  // child.killed stops the close handler from double-writing the status.
387
- const stopTaskDir = getTaskDir(config.projectRoot, params.id);
388
403
  appendRunMessage(stopTaskDir, params.run_id, {
389
404
  role: "status",
390
405
  time: Date.now(),
@@ -392,6 +407,7 @@ export function createRpcHandler(config, nc) {
392
407
  type: "stopped",
393
408
  });
394
409
  activeFollowups.delete(stopKey);
410
+ deleteFollowupStatus(stopRunDir);
395
411
  await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
396
412
  return { ok: true, task_id: params.id, run_id: params.run_id };
397
413
  }
@@ -431,6 +447,12 @@ export function createRpcHandler(config, nc) {
431
447
  console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
432
448
  return { error: `Failed to abort task: ${e.stderr || e.message}` };
433
449
  }
450
+ try {
451
+ const aborted = parseTaskFile(abortTaskDir);
452
+ if (aborted.frontmatter.one_off)
453
+ getPlatform().removeTaskTimer(params.id);
454
+ }
455
+ catch { /* best-effort cleanup */ }
434
456
  const abortPayload = { event_type: "running-state", running_state: "aborted" };
435
457
  await publishHostEvent(nc, config.hostId, params.id, abortPayload);
436
458
  return { ok: true, task_id: params.id };
package/dist/task.d.ts CHANGED
@@ -8,6 +8,13 @@ export declare function listTasks(projectRoot: string): ParsedTask[];
8
8
  export declare function getTaskDir(projectRoot: string, taskId: string): string;
9
9
  export declare function writeTaskStatus(taskDir: string, status: TaskStatus): void;
10
10
  export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
11
+ export interface FollowupStatus {
12
+ pid: number;
13
+ spawned_at: number;
14
+ }
15
+ export declare function writeFollowupStatus(runDir: string, status: FollowupStatus): void;
16
+ export declare function readFollowupStatus(runDir: string): FollowupStatus | undefined;
17
+ export declare function deleteFollowupStatus(runDir: string): void;
11
18
  /** Returns the run ID (timestamp string used as directory name). */
12
19
  export declare function createRunDir(taskDir: string, taskName: string, startTime: number, agent?: string): string;
13
20
  export declare function getRunDir(taskDir: string, runId: string): string;
package/dist/task.js CHANGED
@@ -98,6 +98,23 @@ export function readTaskStatus(taskDir) {
98
98
  return undefined;
99
99
  }
100
100
  }
101
+ export function writeFollowupStatus(runDir, status) {
102
+ fs.writeFileSync(path.join(runDir, "followup.json"), JSON.stringify(status), "utf-8");
103
+ }
104
+ export function readFollowupStatus(runDir) {
105
+ try {
106
+ return JSON.parse(fs.readFileSync(path.join(runDir, "followup.json"), "utf-8"));
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ export function deleteFollowupStatus(runDir) {
113
+ try {
114
+ fs.unlinkSync(path.join(runDir, "followup.json"));
115
+ }
116
+ catch { /* ignore */ }
117
+ }
101
118
  /** Returns the run ID (timestamp string used as directory name). */
102
119
  export function createRunDir(taskDir, taskName, startTime, agent) {
103
120
  const runId = String(startTime);
package/dist/types.d.ts CHANGED
@@ -37,6 +37,10 @@ export interface TaskFrontmatter {
37
37
  foreground_mode?: boolean;
38
38
  permissions?: RequiredPermission[];
39
39
  command?: string;
40
+ /** Set when the task was created via task.run_oneoff. Used so the run process
41
+ * can tear down its OS scheduler unit when it finishes — one-off tasks aren't
42
+ * in tasks.jsonl so the daemon's recovery/sweep logic doesn't cover them. */
43
+ one_off?: boolean;
40
44
  }
41
45
  export interface ParsedTask {
42
46
  frontmatter: TaskFrontmatter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
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. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere." />
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
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -300,7 +300,7 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
300
300
  ref={followupInputRef}
301
301
  className="chat-input"
302
302
  type="text"
303
- placeholder="Follow-up message"
303
+ placeholder="Follow up"
304
304
  value={followupText}
305
305
  onChange={(e) => setFollowupText(e.target.value)}
306
306
  disabled={sendingFollowup}
@@ -139,7 +139,9 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
139
139
  const [requiresConfirmation, setRequiresConfirmation] = useState(
140
140
  initial?.requires_confirmation ?? false
141
141
  );
142
- const [scheduleEnabled, setScheduleEnabled] = useState(initial?.schedule_enabled ?? true);
142
+ const [scheduleEnabled, setScheduleEnabled] = useState(
143
+ initial?.schedule_type ? (initial.schedule_enabled ?? true) : true,
144
+ );
143
145
  const [yoloMode, setYoloMode] = useState(initial?.yolo_mode ?? false);
144
146
  const [foregroundMode, setForegroundMode] = useState(initial?.foreground_mode ?? false);
145
147
  const [command, setCommand] = useState(initial?.command ?? "");
@@ -220,7 +222,7 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
220
222
  || agent !== (initial?.agent ?? "")
221
223
  || scheduleMode !== initialMode
222
224
  || requiresConfirmation !== (initial?.requires_confirmation ?? false)
223
- || scheduleEnabled !== (initial?.schedule_enabled ?? true)
225
+ || scheduleEnabled !== (initial?.schedule_type ? (initial.schedule_enabled ?? true) : true)
224
226
  || yoloMode !== (initial?.yolo_mode ?? false)
225
227
  || foregroundMode !== (initial?.foreground_mode ?? false)
226
228
  || (modeIsCommand && command !== (initial?.command ?? ""))
@@ -313,7 +315,7 @@ export default function TaskForm({ initial, agents, hostPlatform, isNotification
313
315
  agent,
314
316
  schedule_type: scheduleType,
315
317
  schedule_values: scheduleValues.length > 0 ? scheduleValues : null,
316
- schedule_enabled: scheduleMode !== "ondemand" && scheduleEnabled,
318
+ schedule_enabled: scheduleMode === "ondemand" ? true : scheduleEnabled,
317
319
  requires_confirmation: modeIsScheduled ? requiresConfirmation : false,
318
320
  yolo_mode: yoloMode,
319
321
  foreground_mode: foregroundMode,
@@ -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. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere.",
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.",
28
28
  start_url: "/",
29
29
  display: "standalone",
30
30
  background_color: "#ffffff",
@@ -199,6 +199,9 @@ export async function runCommand(taskId: string): Promise<void> {
199
199
  if (nc && !nc.isClosed()) {
200
200
  await nc.drain();
201
201
  }
202
+ if (task.frontmatter.one_off) {
203
+ try { getPlatform().removeTaskTimer(taskId); } catch { /* best-effort */ }
204
+ }
202
205
  };
203
206
 
204
207
  try {
@@ -5,7 +5,7 @@ import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
7
  import { startHttpTransport } from "../transports/http-transport.js";
8
- import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage, listTasks } from "../task.js";
8
+ import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage, listTasks, readFollowupStatus, deleteFollowupStatus } from "../task.js";
9
9
  import { publishHostEvent } from "../events.js";
10
10
  import { getPlatform } from "../platform/index.js";
11
11
  import { detectAgents } from "../agents/agent.js";
@@ -21,58 +21,83 @@ const POLL_INTERVAL_MS = 30_000;
21
21
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
22
22
 
23
23
  /**
24
- * Reconcile tasks stuck in "started" whose process is no longer alive.
24
+ * Reconcile tasks stuck in "started" whose process is no longer alive, and
25
+ * clean up OS scheduler units for one-off tasks that have already terminated.
25
26
  * The system scheduler (Task Scheduler / systemd) is the authoritative source.
26
27
  */
27
28
  async function checkStaleTasks(
28
29
  config: HostConfig,
29
30
  nc: NatsConnection | undefined,
30
31
  ): Promise<void> {
31
- const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
32
- if (!fs.existsSync(tasksJsonl)) return;
32
+ const tasksRoot = path.join(config.projectRoot, "tasks");
33
+ if (!fs.existsSync(tasksRoot)) return;
33
34
 
34
35
  const platform = getPlatform();
35
- const lines = fs.readFileSync(tasksJsonl, "utf-8").split("\n").filter(Boolean);
36
- for (const line of lines) {
37
- let taskId: string;
38
- try {
39
- taskId = (JSON.parse(line) as { task_id: string }).task_id;
40
- } catch { continue; }
36
+ const taskIds = fs.readdirSync(tasksRoot).filter((f) =>
37
+ fs.statSync(path.join(tasksRoot, f)).isDirectory()
38
+ );
41
39
 
40
+ for (const taskId of taskIds) {
42
41
  const taskDir = getTaskDir(config.projectRoot, taskId);
43
42
  const status = readTaskStatus(taskDir);
44
- if (!status || status.running_state !== "started") continue;
45
-
46
- if (platform.isTaskRunning(taskId)) continue;
47
-
48
- console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
49
- const endTime = Date.now();
50
- writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
51
-
52
- const runId = fs.readdirSync(taskDir)
53
- .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
54
- .sort()
55
- .pop();
43
+ if (!status) continue;
44
+
45
+ let task;
46
+ try { task = parseTaskFile(taskDir); } catch { continue; }
47
+
48
+ if (status.running_state === "started" && !platform.isTaskRunning(taskId)) {
49
+ console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
50
+ const endTime = Date.now();
51
+ writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
52
+
53
+ const runId = fs.readdirSync(taskDir)
54
+ .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
55
+ .sort()
56
+ .pop();
57
+
58
+ if (runId) {
59
+ appendRunMessage(taskDir, runId, {
60
+ role: "status",
61
+ time: endTime,
62
+ content: "",
63
+ type: "failed",
64
+ });
65
+ }
56
66
 
57
- if (runId) {
58
- appendRunMessage(taskDir, runId, {
59
- role: "status",
60
- time: endTime,
61
- content: "",
62
- type: "failed",
67
+ await publishHostEvent(nc, config.hostId, taskId, {
68
+ event_type: "running-state",
69
+ running_state: "failed",
70
+ name: task.frontmatter.name || taskId,
63
71
  });
64
72
  }
65
73
 
66
- let taskName = taskId;
67
- try {
68
- taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
69
- } catch { /* fallback to taskId */ }
74
+ if (task.frontmatter.one_off && status.running_state !== "started") {
75
+ try { platform.removeTaskTimer(taskId); } catch { /* best-effort */ }
76
+ }
70
77
 
71
- await publishHostEvent(nc, config.hostId, taskId, {
72
- event_type: "running-state",
73
- running_state: "failed",
74
- name: taskName,
75
- });
78
+ // Reconcile orphaned follow-ups: if a run has a persisted follow-up PID
79
+ // but that process is no longer alive, clear the file and mark the run
80
+ // as failed so the UI doesn't claim it's still running.
81
+ const runIds = fs.readdirSync(taskDir).filter((f) =>
82
+ /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md"))
83
+ );
84
+ for (const runId of runIds) {
85
+ const runDir = path.join(taskDir, runId);
86
+ const followup = readFollowupStatus(runDir);
87
+ if (!followup) continue;
88
+ try {
89
+ process.kill(followup.pid, 0);
90
+ } catch {
91
+ deleteFollowupStatus(runDir);
92
+ appendRunMessage(taskDir, runId, {
93
+ role: "status",
94
+ time: Date.now(),
95
+ content: "",
96
+ type: "failed",
97
+ });
98
+ await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
99
+ }
100
+ }
76
101
  }
77
102
  }
78
103