@hellcoder/companion 0.116.1-preview.20260814050532.93fba26 → 0.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentsPage-DPt9YKyQ.js → AgentsPage-DJIGGU86.js} +1 -1
- package/dist/assets/{CronManager-BxRWyTp3.js → CronManager-D7aAMVgs.js} +1 -1
- package/dist/assets/{DashboardPage-BSnJNNNL.js → DashboardPage-Bporj7Vr.js} +1 -1
- package/dist/assets/{IntegrationsPage-DWrnBrBa.js → IntegrationsPage-BSY0-fsO.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-DZJpyUr3.js → LinearOAuthSettingsPage-Cl5dP05g.js} +1 -1
- package/dist/assets/{LinearSettingsPage-CUV6wc7-.js → LinearSettingsPage-dwMpnpxL.js} +1 -1
- package/dist/assets/{MagicUIDashboard-CbruKd65.js → MagicUIDashboard-Bpa4AupX.js} +1 -1
- package/dist/assets/{MagicUIView-NSITfXce.js → MagicUIView-BoHMBELi.js} +1 -1
- package/dist/assets/{Playground-BoyZfARc.js → Playground-By2aXWQF.js} +1 -1
- package/dist/assets/{PromptsPage-D3aJBO9h.js → PromptsPage-DVv0Qn3d.js} +1 -1
- package/dist/assets/{RunsPage-C6YY6SGq.js → RunsPage-bu3TZ6ai.js} +1 -1
- package/dist/assets/{SandboxManager-_mggajaE.js → SandboxManager-CDjGbD5N.js} +1 -1
- package/dist/assets/{SettingsPage-DBjt4SLp.js → SettingsPage-D0v_BZTU.js} +1 -1
- package/dist/assets/{TailscalePage-BaAGOCMu.js → TailscalePage-B8SJQpMF.js} +1 -1
- package/dist/assets/index-BqOdTpct.css +1 -0
- package/dist/assets/index-DqIZ_Ol5.js +140 -0
- package/dist/assets/{sw-register-BH5kpgnP.js → sw-register-CvyRJNug.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/linear-agent-bridge.ts +0 -1
- package/server/magic-ui-watcher.test.ts +85 -0
- package/server/magic-ui-watcher.ts +0 -1
- package/server/routes/system-routes.test.ts +401 -0
- package/server/routes/system-routes.ts +10 -0
- package/server/system-disk.test.ts +48 -0
- package/server/system-disk.ts +55 -0
- package/server/system-memory.test.ts +32 -0
- package/server/system-memory.ts +32 -0
- package/dist/assets/index-BaDsUsyb.js +0 -140
- package/dist/assets/index-DSCRqqpK.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-DqIZ_Ol5.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
|
package/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="Companion ME" />
|
|
12
12
|
<meta name="theme-color" content="#d97757" />
|
|
13
13
|
<title>The Companion — Moritz Edition</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DqIZ_Ol5.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BqOdTpct.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
|
18
18
|
<div id="root"></div>
|
package/dist/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.4.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(e,t);super(s),this.name=e,this.details=t}}const E=new Set,f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},b=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),O=a=>{for(const e of Object.keys(f))a(e)},C={updateDetails:a=>{O(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||b(f.googleAnalytics),getPrecacheName:a=>a||b(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||b(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function I(a,e,t,s){const n=P(e.url,t);if(e.url===n)return a.match(e,s);const r=Object.assign(Object.assign({},s),{ignoreSearch:!0}),c=await a.keys(e,r);for(const i of c){const o=P(i.url,t);if(n===o)return a.match(i,s)}}let y;function M(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}async function S(){for(const a of E)await a()}const W=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function A(a){return new Promise(e=>setTimeout(e,a))}function K(a,e){const t=e();return a.waitUntil(t),t}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),r={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},c=M()?s.body:await s.blob();return new Response(c,r)}function j(){self.addEventListener("activate",()=>self.clients.claim())}try{self["workbox:precaching:7.4.0"]&&_()}catch{}const H="__WB_REVISION__";function F(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const r=new URL(a,location.href);return{cacheKey:r.href,url:r.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const r=new URL(t,location.href);return{cacheKey:r.href,url:r.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(H,e),{cacheKey:s.href,url:n.href}}class B{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 n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class ${constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}try{self["workbox:strategies:7.4.0"]&&_()}catch{}function m(a){return typeof a=="string"?new Request(a):a}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,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=m(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const c=await t.preloadResponse;if(c)return c}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const c of this.iterateCallbacks("requestWillFetch"))s=await c({request:s.clone(),event:t})}catch(c){if(c instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:c.message})}const r=s.clone();try{let c;c=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))c=await i({event:t,request:r,response:c});return c}catch(c){throw n&&await this.runCallbacks("fetchDidFail",{error:c,event:t,originalRequest:n.clone(),request:r.clone()}),c}}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=m(e);let s;const{cacheName:n,matchOptions:r}=this._strategy,c=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(c,i);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:r,cachedResponse:s,request:c,event:this.event})||void 0;return s}async cachePut(e,t){const s=m(e);await A(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:W(n.url)});const r=await this._ensureResponseSafeToCache(t);if(!r)return!1;const{cacheName:c,matchOptions:i}=this._strategy,o=await self.caches.open(c),h=this.hasCallback("cacheDidUpdate"),p=h?await I(o,n.clone(),["__WB_REVISION__"],i):null;try{await o.put(n,h?r.clone():r)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await S(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:c,oldResponse:p,newResponse:r.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const r of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await r({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}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 r=>{const c=Object.assign(Object.assign({},r),{state:s});return t[e](c)}}}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(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({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 V{constructor(e={}){this.cacheName=C.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,n="params"in e?e.params:void 0,r=new G(this,{event:t,request:s,params:n}),c=this._getResponse(r,s,t),i=this._awaitComplete(c,r,s,t);return[c,i]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(const c of e.iterateCallbacks("handlerDidError"))if(n=await c({error:r,event:s,request:t}),n)break}if(!n)throw r}for(const r of e.iterateCallbacks("handlerWillRespond"))n=await r({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let r,c;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await t.doneWaiting()}catch(i){i instanceof Error&&(c=i)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:c}),t.destroy(),c)throw c}}class d extends V{constructor(e={}){e.cacheName=C.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.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 n=t.params||{};if(this._fallbackToNetwork){const r=n.integrity,c=e.integrity,i=!c||c===r;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?c||r:void 0})),r&&i&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("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 l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};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 d({cacheName:C.getPrecacheName(e),plugins:[...t,new $({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:n,url:r}=F(s),c=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(r)&&this._urlsToCacheKeys.get(r)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(r),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:r});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(r,n),this._urlsToCacheModes.set(r,c),t.length>0){const i=`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(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}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 l("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 U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{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(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,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 n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(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 l("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 l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}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 l("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 U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{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(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,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 n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(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 l("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 l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"e1fd399a6dc88dfc55dc2d27a6bc45fc","url":"index.html"},{"revision":"1421e9c6cad8218ef5689b85034d3a95","url":"icon-512.png"},{"revision":"b96da5a0b6636012517f54b0e69c304d","url":"icon-192.png"},{"revision":"c64ca8ae8596d4ffe13d0f591beb2274","url":"favicon.svg"},{"revision":"6481face04bd08e6f0bfc2a633840b57","url":"apple-touch-icon.png"},{"revision":"83304194a3c8be2b5b61242eeb1c1046","url":"fonts/MesloLGSNerdFontMono-Regular.woff2"},{"revision":"60fafe18cbcb717c51cdabf87f9490f0","url":"fonts/MesloLGSNerdFontMono-Bold.woff2"},{"revision":null,"url":"assets/workbox-window.prod.es5-BBnX5xw4.js"},{"revision":null,"url":"assets/sw-register-CvyRJNug.js"},{"revision":null,"url":"assets/index-DqIZ_Ol5.js"},{"revision":null,"url":"assets/index-BqOdTpct.css"},{"revision":null,"url":"assets/TailscalePage-B8SJQpMF.js"},{"revision":null,"url":"assets/SettingsPage-D0v_BZTU.js"},{"revision":null,"url":"assets/SandboxManager-CDjGbD5N.js"},{"revision":null,"url":"assets/RunsPage-bu3TZ6ai.js"},{"revision":null,"url":"assets/PromptsPage-DVv0Qn3d.js"},{"revision":null,"url":"assets/Playground-By2aXWQF.js"},{"revision":null,"url":"assets/MagicUIView-BoHMBELi.js"},{"revision":null,"url":"assets/MagicUIDashboard-Bpa4AupX.js"},{"revision":null,"url":"assets/LinearSettingsPage-dwMpnpxL.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-Cl5dP05g.js"},{"revision":null,"url":"assets/IntegrationsPage-BSY0-fsO.js"},{"revision":null,"url":"assets/DashboardPage-Bporj7Vr.js"},{"revision":null,"url":"assets/CronManager-D7aAMVgs.js"},{"revision":null,"url":"assets/AgentsPage-DJIGGU86.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hellcoder/companion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.117.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web UI for launching and interacting with Claude Code agents — Moritz Edition (fork of the-companion)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AgentExecutor } from "./agent-executor.js";
|
|
12
12
|
import type { WsBridge } from "./ws-bridge.js";
|
|
13
|
-
import type { BrowserIncomingMessage } from "./session-types.js";
|
|
14
13
|
import type { AgentConfig } from "./agent-types.js";
|
|
15
14
|
import * as agentStore from "./agent-store.js";
|
|
16
15
|
import * as linearAgent from "./linear-agent.js";
|
|
@@ -286,3 +286,88 @@ describe("MagicUiWatcherManager", () => {
|
|
|
286
286
|
manager.shutdown();
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
// The watcher's query can die at any time (SDK error, auth failure, killed
|
|
291
|
+
// subprocess). handleCrash retries it with exponential backoff and gives up
|
|
292
|
+
// into a "degraded" status after MAX_CRASH_RETRIES, so a permanently broken
|
|
293
|
+
// watcher stops burning tokens instead of hot-looping forever.
|
|
294
|
+
describe("MagicUiWatcher crash handling", () => {
|
|
295
|
+
/** A query that always throws, counting how many times it was started. */
|
|
296
|
+
function makeCrashingQuery() {
|
|
297
|
+
let calls = 0;
|
|
298
|
+
const fn = (() => {
|
|
299
|
+
calls += 1;
|
|
300
|
+
// eslint-disable-next-line require-yield
|
|
301
|
+
async function* gen(): AsyncGenerator<never> {
|
|
302
|
+
throw new Error("query exploded");
|
|
303
|
+
}
|
|
304
|
+
return gen();
|
|
305
|
+
}) as unknown as typeof import("@anthropic-ai/claude-agent-sdk").query;
|
|
306
|
+
return { fn, calls: () => calls };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function makeCrashWatcher() {
|
|
310
|
+
const { fn, calls } = makeCrashingQuery();
|
|
311
|
+
const watcher = new MagicUiWatcher("sess-1", {
|
|
312
|
+
broadcast: (sessionId, msg) => broadcasts.push({ sessionId, msg }),
|
|
313
|
+
store: store as unknown as MagicUiStore,
|
|
314
|
+
model: "claude-haiku-4-5",
|
|
315
|
+
queryFn: fn,
|
|
316
|
+
});
|
|
317
|
+
return { watcher, calls };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
it("retries a crashed query with exponential backoff, then degrades", async () => {
|
|
321
|
+
vi.useFakeTimers();
|
|
322
|
+
try {
|
|
323
|
+
const { watcher, calls } = makeCrashWatcher();
|
|
324
|
+
watcher.start();
|
|
325
|
+
|
|
326
|
+
// First attempt crashes almost immediately.
|
|
327
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
328
|
+
expect(calls()).toBe(1);
|
|
329
|
+
|
|
330
|
+
// Backoff is 2s, then 4s, then 8s — capped at 30s. Nothing should
|
|
331
|
+
// restart before the delay elapses.
|
|
332
|
+
await vi.advanceTimersByTimeAsync(1_999);
|
|
333
|
+
expect(calls()).toBe(1);
|
|
334
|
+
|
|
335
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
336
|
+
expect(calls()).toBe(2);
|
|
337
|
+
|
|
338
|
+
await vi.advanceTimersByTimeAsync(4_000);
|
|
339
|
+
expect(calls()).toBe(3);
|
|
340
|
+
|
|
341
|
+
await vi.advanceTimersByTimeAsync(8_000);
|
|
342
|
+
expect(calls()).toBe(4);
|
|
343
|
+
|
|
344
|
+
// The 4th crash exceeds MAX_CRASH_RETRIES (3): give up rather than
|
|
345
|
+
// retry a 5th time, and surface the failure to the browser.
|
|
346
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
347
|
+
expect(calls()).toBe(4);
|
|
348
|
+
|
|
349
|
+
const last = broadcasts.at(-1);
|
|
350
|
+
expect((last?.msg as { state: MagicUiDashboardState }).state.status).toBe("degraded");
|
|
351
|
+
} finally {
|
|
352
|
+
vi.useRealTimers();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("does not retry after the watcher has been stopped", async () => {
|
|
357
|
+
vi.useFakeTimers();
|
|
358
|
+
try {
|
|
359
|
+
const { watcher, calls } = makeCrashWatcher();
|
|
360
|
+
watcher.start();
|
|
361
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
362
|
+
expect(calls()).toBe(1);
|
|
363
|
+
|
|
364
|
+
// A stop() racing a pending backoff must win — otherwise a stopped
|
|
365
|
+
// session keeps spawning Haiku queries in the background.
|
|
366
|
+
watcher.stop();
|
|
367
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
368
|
+
expect(calls()).toBe(1);
|
|
369
|
+
} finally {
|
|
370
|
+
vi.useRealTimers();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
@@ -30,6 +30,55 @@ vi.mock("../service.js", () => ({
|
|
|
30
30
|
refreshServiceDefinition: vi.fn(),
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
|
+
// ─── Mock claude-compat checker ────────────────────────────────────────────
|
|
34
|
+
// The compat routes gate the post-2.1.121 --sdk-url lockdown workarounds.
|
|
35
|
+
// Everything here touches the real Claude binary on disk, so it is mocked
|
|
36
|
+
// wholesale — these tests exercise the route logic, never the filesystem.
|
|
37
|
+
vi.mock("../claude-compat-checker.js", () => ({
|
|
38
|
+
checkCompat: vi.fn(async () => {}),
|
|
39
|
+
getCompatState: vi.fn(() => ({
|
|
40
|
+
installedVersion: "2.1.130",
|
|
41
|
+
installedPath: "/usr/local/bin/claude",
|
|
42
|
+
isIncompatible: true,
|
|
43
|
+
isPatched: false,
|
|
44
|
+
availableKnownGood: ["2.1.119", "2.1.120"],
|
|
45
|
+
suggestedPinTarget: "2.1.120",
|
|
46
|
+
lastChecked: 0,
|
|
47
|
+
error: null,
|
|
48
|
+
})),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// ─── Mock claude-patcher ───────────────────────────────────────────────────
|
|
52
|
+
vi.mock("../claude-patcher.js", () => ({
|
|
53
|
+
pinToVersion: vi.fn(async () => ({ ok: true })),
|
|
54
|
+
patchBinary: vi.fn(async () => ({
|
|
55
|
+
ok: true,
|
|
56
|
+
patchedPath: "/usr/local/bin/claude",
|
|
57
|
+
replacements: 3,
|
|
58
|
+
})),
|
|
59
|
+
unpatch: vi.fn(async () => ({ ok: true, target: "2.1.130" })),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// ─── Mock settings-manager ─────────────────────────────────────────────────
|
|
63
|
+
vi.mock("../settings-manager.js", () => ({
|
|
64
|
+
getSettings: vi.fn(() => ({
|
|
65
|
+
claudeBridgeMode: "none",
|
|
66
|
+
claudeBridgeIngressUrl: "",
|
|
67
|
+
claudeCompatBannerDismissedVersion: "",
|
|
68
|
+
dockerAutoUpdate: false,
|
|
69
|
+
})),
|
|
70
|
+
updateSettings: vi.fn(),
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// ─── Mock cli-ingress-server ───────────────────────────────────────────────
|
|
74
|
+
// Starting the real one binds a TLS listener.
|
|
75
|
+
vi.mock("../cli-ingress-server.js", () => ({
|
|
76
|
+
startCliIngressServer: vi.fn(async () => ({
|
|
77
|
+
urlPrefix: "wss://[::1]:8443",
|
|
78
|
+
stop: vi.fn(),
|
|
79
|
+
})),
|
|
80
|
+
}));
|
|
81
|
+
|
|
33
82
|
import { Hono } from "hono";
|
|
34
83
|
import { getUsageLimits } from "../usage-limits.js";
|
|
35
84
|
import {
|
|
@@ -39,6 +88,9 @@ import {
|
|
|
39
88
|
setUpdateInProgress,
|
|
40
89
|
} from "../update-checker.js";
|
|
41
90
|
import { registerSystemRoutes } from "./system-routes.js";
|
|
91
|
+
import { checkCompat, getCompatState } from "../claude-compat-checker.js";
|
|
92
|
+
import { pinToVersion, patchBinary, unpatch } from "../claude-patcher.js";
|
|
93
|
+
import { getSettings, updateSettings } from "../settings-manager.js";
|
|
42
94
|
|
|
43
95
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
44
96
|
|
|
@@ -75,8 +127,39 @@ let launcher: ReturnType<typeof createMockLauncher>;
|
|
|
75
127
|
let wsBridge: ReturnType<typeof createMockWsBridge>;
|
|
76
128
|
let terminalManager: ReturnType<typeof createMockTerminalManager>;
|
|
77
129
|
|
|
130
|
+
/** Default compat state: an incompatible CLI with a known-good pin target. */
|
|
131
|
+
function defaultCompatState() {
|
|
132
|
+
return {
|
|
133
|
+
installedVersion: "2.1.130",
|
|
134
|
+
installedPath: "/usr/local/bin/claude",
|
|
135
|
+
isIncompatible: true,
|
|
136
|
+
isPatched: false,
|
|
137
|
+
availableKnownGood: ["2.1.119", "2.1.120"],
|
|
138
|
+
suggestedPinTarget: "2.1.120",
|
|
139
|
+
lastChecked: 0,
|
|
140
|
+
error: null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
78
144
|
beforeEach(() => {
|
|
145
|
+
// clearAllMocks() resets call history but NOT implementations, so a
|
|
146
|
+
// mockReturnValue set inside one test leaks into every later one. Restoring
|
|
147
|
+
// the defaults here keeps the suite order-independent.
|
|
79
148
|
vi.clearAllMocks();
|
|
149
|
+
vi.mocked(getCompatState).mockReturnValue(defaultCompatState() as any);
|
|
150
|
+
vi.mocked(pinToVersion).mockResolvedValue({ ok: true } as any);
|
|
151
|
+
vi.mocked(patchBinary).mockResolvedValue({
|
|
152
|
+
ok: true,
|
|
153
|
+
patchedPath: "/usr/local/bin/claude",
|
|
154
|
+
replacements: 3,
|
|
155
|
+
} as any);
|
|
156
|
+
vi.mocked(unpatch).mockResolvedValue({ ok: true, target: "2.1.130" } as any);
|
|
157
|
+
vi.mocked(getSettings).mockReturnValue({
|
|
158
|
+
claudeBridgeMode: "none",
|
|
159
|
+
claudeBridgeIngressUrl: "",
|
|
160
|
+
claudeCompatBannerDismissedVersion: "",
|
|
161
|
+
dockerAutoUpdate: false,
|
|
162
|
+
} as any);
|
|
80
163
|
|
|
81
164
|
launcher = createMockLauncher();
|
|
82
165
|
wsBridge = createMockWsBridge();
|
|
@@ -635,3 +718,321 @@ describe("POST /api/sessions/:id/message", () => {
|
|
|
635
718
|
expect(json2.error).toMatch(/content/i);
|
|
636
719
|
});
|
|
637
720
|
});
|
|
721
|
+
|
|
722
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
723
|
+
// GET /api/system/memory and GET /api/system/disk
|
|
724
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
725
|
+
|
|
726
|
+
describe("GET /api/system/memory", () => {
|
|
727
|
+
it("returns a memory snapshot including swap fields", async () => {
|
|
728
|
+
const res = await app.request("/api/system/memory");
|
|
729
|
+
expect(res.status).toBe(200);
|
|
730
|
+
const json = await res.json();
|
|
731
|
+
|
|
732
|
+
expect(json.total_bytes).toBeGreaterThan(0);
|
|
733
|
+
expect(json.used_bytes).toBeLessThanOrEqual(json.total_bytes);
|
|
734
|
+
// Swap must always be present, even on a host with none configured —
|
|
735
|
+
// the UI keys off swap_total_bytes === 0 to hide the meter.
|
|
736
|
+
expect(typeof json.swap_total_bytes).toBe("number");
|
|
737
|
+
expect(typeof json.swap_used_bytes).toBe("number");
|
|
738
|
+
expect(typeof json.swap_used_percent).toBe("number");
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe("GET /api/system/disk", () => {
|
|
743
|
+
it("returns a disk snapshot for the Companion data volume", async () => {
|
|
744
|
+
const res = await app.request("/api/system/disk");
|
|
745
|
+
expect(res.status).toBe(200);
|
|
746
|
+
const json = await res.json();
|
|
747
|
+
|
|
748
|
+
// getSystemDisk returns null when statfs is unavailable; the route passes
|
|
749
|
+
// that through as JSON null rather than an empty 204 body.
|
|
750
|
+
if (json === null) return;
|
|
751
|
+
|
|
752
|
+
expect(json.total_bytes).toBeGreaterThan(0);
|
|
753
|
+
expect(json.used_bytes).toBe(json.total_bytes - json.available_bytes);
|
|
754
|
+
expect(json.used_percent).toBeGreaterThanOrEqual(0);
|
|
755
|
+
expect(json.used_percent).toBeLessThanOrEqual(100);
|
|
756
|
+
expect(typeof json.path).toBe("string");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("always responds 200 with a JSON body, never an empty 204", async () => {
|
|
760
|
+
// The browser client calls res.json() unconditionally — an empty body
|
|
761
|
+
// would throw and log a spurious API failure on every poll.
|
|
762
|
+
const res = await app.request("/api/system/disk");
|
|
763
|
+
expect(res.status).toBe(200);
|
|
764
|
+
await expect(res.json()).resolves.not.toThrow();
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
769
|
+
// POST /api/update — guard branches only
|
|
770
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
771
|
+
|
|
772
|
+
// The success path spawns `bun install -g` and calls process.exit(), so these
|
|
773
|
+
// tests deliberately cover only the three refusal branches.
|
|
774
|
+
describe("POST /api/update (guards)", () => {
|
|
775
|
+
it("returns 400 when not running as a service", async () => {
|
|
776
|
+
vi.mocked(getUpdateState).mockReturnValue({
|
|
777
|
+
currentVersion: "1.0.0", latestVersion: "1.1.0", lastChecked: 0,
|
|
778
|
+
isServiceMode: false, checking: false, updateInProgress: false, channel: "stable",
|
|
779
|
+
} as any);
|
|
780
|
+
|
|
781
|
+
const res = await app.request("/api/update", { method: "POST" });
|
|
782
|
+
expect(res.status).toBe(400);
|
|
783
|
+
expect((await res.json()).error).toMatch(/service mode/i);
|
|
784
|
+
expect(setUpdateInProgress).not.toHaveBeenCalled();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it("returns 400 when no update is available", async () => {
|
|
788
|
+
vi.mocked(getUpdateState).mockReturnValue({
|
|
789
|
+
currentVersion: "1.0.0", latestVersion: "1.0.0", lastChecked: 0,
|
|
790
|
+
isServiceMode: true, checking: false, updateInProgress: false, channel: "stable",
|
|
791
|
+
} as any);
|
|
792
|
+
vi.mocked(isUpdateAvailable).mockReturnValue(false);
|
|
793
|
+
|
|
794
|
+
const res = await app.request("/api/update", { method: "POST" });
|
|
795
|
+
expect(res.status).toBe(400);
|
|
796
|
+
expect((await res.json()).error).toMatch(/no update/i);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("returns 409 when an update is already in progress", async () => {
|
|
800
|
+
vi.mocked(getUpdateState).mockReturnValue({
|
|
801
|
+
currentVersion: "1.0.0", latestVersion: "1.1.0", lastChecked: 0,
|
|
802
|
+
isServiceMode: true, checking: false, updateInProgress: true, channel: "stable",
|
|
803
|
+
} as any);
|
|
804
|
+
vi.mocked(isUpdateAvailable).mockReturnValue(true);
|
|
805
|
+
|
|
806
|
+
const res = await app.request("/api/update", { method: "POST" });
|
|
807
|
+
expect(res.status).toBe(409);
|
|
808
|
+
expect((await res.json()).error).toMatch(/already in progress/i);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
813
|
+
// Terminal routes
|
|
814
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
815
|
+
|
|
816
|
+
describe("terminal routes", () => {
|
|
817
|
+
it("GET /api/terminal reports inactive when no terminal exists", async () => {
|
|
818
|
+
terminalManager.getInfo.mockReturnValue(null);
|
|
819
|
+
const res = await app.request("/api/terminal");
|
|
820
|
+
expect(res.status).toBe(200);
|
|
821
|
+
expect(await res.json()).toEqual({ active: false });
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it("GET /api/terminal returns the terminal id and cwd when active", async () => {
|
|
825
|
+
terminalManager.getInfo.mockReturnValue({ id: "t-1", cwd: "/repo" });
|
|
826
|
+
const res = await app.request("/api/terminal?terminalId=t-1");
|
|
827
|
+
expect(await res.json()).toEqual({ active: true, terminalId: "t-1", cwd: "/repo" });
|
|
828
|
+
expect(terminalManager.getInfo).toHaveBeenCalledWith("t-1");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("POST /api/terminal/spawn requires a cwd", async () => {
|
|
832
|
+
const res = await app.request("/api/terminal/spawn", {
|
|
833
|
+
method: "POST",
|
|
834
|
+
headers: { "Content-Type": "application/json" },
|
|
835
|
+
body: JSON.stringify({}),
|
|
836
|
+
});
|
|
837
|
+
expect(res.status).toBe(400);
|
|
838
|
+
expect((await res.json()).error).toMatch(/cwd/i);
|
|
839
|
+
expect(terminalManager.spawn).not.toHaveBeenCalled();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it("POST /api/terminal/spawn passes dimensions and container through", async () => {
|
|
843
|
+
const res = await app.request("/api/terminal/spawn", {
|
|
844
|
+
method: "POST",
|
|
845
|
+
headers: { "Content-Type": "application/json" },
|
|
846
|
+
body: JSON.stringify({ cwd: "/repo", cols: 120, rows: 40, containerId: "c-1" }),
|
|
847
|
+
});
|
|
848
|
+
expect(res.status).toBe(200);
|
|
849
|
+
expect(await res.json()).toEqual({ terminalId: "terminal-123" });
|
|
850
|
+
expect(terminalManager.spawn).toHaveBeenCalledWith("/repo", 120, 40, { containerId: "c-1" });
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it("POST /api/terminal/kill requires a terminalId", async () => {
|
|
854
|
+
// Covers both a missing body and a whitespace-only id.
|
|
855
|
+
const res1 = await app.request("/api/terminal/kill", { method: "POST" });
|
|
856
|
+
expect(res1.status).toBe(400);
|
|
857
|
+
|
|
858
|
+
const res2 = await app.request("/api/terminal/kill", {
|
|
859
|
+
method: "POST",
|
|
860
|
+
headers: { "Content-Type": "application/json" },
|
|
861
|
+
body: JSON.stringify({ terminalId: " " }),
|
|
862
|
+
});
|
|
863
|
+
expect(res2.status).toBe(400);
|
|
864
|
+
expect(terminalManager.kill).not.toHaveBeenCalled();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("POST /api/terminal/kill kills the named terminal", async () => {
|
|
868
|
+
const res = await app.request("/api/terminal/kill", {
|
|
869
|
+
method: "POST",
|
|
870
|
+
headers: { "Content-Type": "application/json" },
|
|
871
|
+
body: JSON.stringify({ terminalId: "t-1" }),
|
|
872
|
+
});
|
|
873
|
+
expect(res.status).toBe(200);
|
|
874
|
+
expect(terminalManager.kill).toHaveBeenCalledWith("t-1");
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
879
|
+
// Claude compatibility routes (--sdk-url lockdown workarounds)
|
|
880
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
881
|
+
|
|
882
|
+
describe("GET /api/claude-compat", () => {
|
|
883
|
+
it("refreshes when the cached state is stale, then returns the payload", async () => {
|
|
884
|
+
// lastChecked === 0 means "never checked" — must trigger a refresh.
|
|
885
|
+
const res = await app.request("/api/claude-compat");
|
|
886
|
+
expect(res.status).toBe(200);
|
|
887
|
+
expect(checkCompat).toHaveBeenCalled();
|
|
888
|
+
|
|
889
|
+
const json = await res.json();
|
|
890
|
+
expect(json.installedVersion).toBe("2.1.130");
|
|
891
|
+
expect(json.isIncompatible).toBe(true);
|
|
892
|
+
expect(json.suggestedPinTarget).toBe("2.1.120");
|
|
893
|
+
// Settings-derived fields are merged into the same payload.
|
|
894
|
+
expect(json.bridgeMode).toBe("none");
|
|
895
|
+
expect(json.bannerDismissedVersion).toBe("");
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it("skips the refresh when the cached state is fresh", async () => {
|
|
899
|
+
vi.mocked(getCompatState).mockReturnValue({
|
|
900
|
+
installedVersion: "2.1.130", installedPath: "/usr/local/bin/claude",
|
|
901
|
+
isIncompatible: false, isPatched: false, availableKnownGood: [],
|
|
902
|
+
suggestedPinTarget: "", lastChecked: Date.now(), error: null,
|
|
903
|
+
} as any);
|
|
904
|
+
|
|
905
|
+
await app.request("/api/claude-compat");
|
|
906
|
+
expect(checkCompat).not.toHaveBeenCalled();
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
describe("POST /api/claude-compat/refresh", () => {
|
|
911
|
+
it("always re-checks and returns the fresh payload", async () => {
|
|
912
|
+
const res = await app.request("/api/claude-compat/refresh", { method: "POST" });
|
|
913
|
+
expect(res.status).toBe(200);
|
|
914
|
+
expect(checkCompat).toHaveBeenCalled();
|
|
915
|
+
expect((await res.json()).installedVersion).toBe("2.1.130");
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
describe("POST /api/claude-compat/pin", () => {
|
|
920
|
+
it("pins to the suggested target and disables patched bridge mode", async () => {
|
|
921
|
+
const res = await app.request("/api/claude-compat/pin", { method: "POST" });
|
|
922
|
+
expect(res.status).toBe(200);
|
|
923
|
+
expect(pinToVersion).toHaveBeenCalledWith("2.1.120");
|
|
924
|
+
|
|
925
|
+
// Pinning returns to a non-validator binary, so the bridge must be turned
|
|
926
|
+
// off — otherwise the CLI keeps being pointed at wss://[::1].
|
|
927
|
+
expect(updateSettings).toHaveBeenCalledWith({
|
|
928
|
+
claudeBridgeMode: "none",
|
|
929
|
+
claudeBridgeIngressUrl: "",
|
|
930
|
+
});
|
|
931
|
+
expect((await res.json()).pinnedTo).toBe("2.1.120");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it("honours an explicit version from the body", async () => {
|
|
935
|
+
await app.request("/api/claude-compat/pin", {
|
|
936
|
+
method: "POST",
|
|
937
|
+
headers: { "Content-Type": "application/json" },
|
|
938
|
+
body: JSON.stringify({ version: "2.1.119" }),
|
|
939
|
+
});
|
|
940
|
+
expect(pinToVersion).toHaveBeenCalledWith("2.1.119");
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it("returns 400 when no known-good version is available to pin to", async () => {
|
|
944
|
+
vi.mocked(getCompatState).mockReturnValue({
|
|
945
|
+
installedVersion: "2.1.130", installedPath: "/usr/local/bin/claude",
|
|
946
|
+
isIncompatible: true, isPatched: false, availableKnownGood: [],
|
|
947
|
+
suggestedPinTarget: "", lastChecked: 0, error: null,
|
|
948
|
+
} as any);
|
|
949
|
+
|
|
950
|
+
const res = await app.request("/api/claude-compat/pin", { method: "POST" });
|
|
951
|
+
expect(res.status).toBe(400);
|
|
952
|
+
expect((await res.json()).error).toMatch(/known-good/i);
|
|
953
|
+
expect(pinToVersion).not.toHaveBeenCalled();
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("surfaces a pin failure as 400", async () => {
|
|
957
|
+
vi.mocked(pinToVersion).mockResolvedValue({ ok: false, error: "download failed" } as any);
|
|
958
|
+
const res = await app.request("/api/claude-compat/pin", { method: "POST" });
|
|
959
|
+
expect(res.status).toBe(400);
|
|
960
|
+
expect((await res.json()).error).toBe("download failed");
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
describe("POST /api/claude-compat/patch", () => {
|
|
965
|
+
it("patches the binary, starts the ingress listener and persists the URL", async () => {
|
|
966
|
+
const res = await app.request("/api/claude-compat/patch", { method: "POST" });
|
|
967
|
+
expect(res.status).toBe(200);
|
|
968
|
+
expect(patchBinary).toHaveBeenCalled();
|
|
969
|
+
|
|
970
|
+
const json = await res.json();
|
|
971
|
+
expect(json.replacements).toBe(3);
|
|
972
|
+
expect(updateSettings).toHaveBeenCalledWith({
|
|
973
|
+
claudeBridgeMode: "patched",
|
|
974
|
+
claudeBridgeIngressUrl: "wss://[::1]:8443",
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it("returns 400 when the binary cannot be patched", async () => {
|
|
979
|
+
vi.mocked(patchBinary).mockResolvedValue({ ok: false, error: "unknown layout" } as any);
|
|
980
|
+
const res = await app.request("/api/claude-compat/patch", { method: "POST" });
|
|
981
|
+
expect(res.status).toBe(400);
|
|
982
|
+
expect((await res.json()).error).toBe("unknown layout");
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
describe("POST /api/claude-compat/unpatch", () => {
|
|
987
|
+
it("restores the binary and clears bridge settings", async () => {
|
|
988
|
+
const res = await app.request("/api/claude-compat/unpatch", { method: "POST" });
|
|
989
|
+
expect(res.status).toBe(200);
|
|
990
|
+
expect(unpatch).toHaveBeenCalled();
|
|
991
|
+
expect(updateSettings).toHaveBeenCalledWith({
|
|
992
|
+
claudeBridgeMode: "none",
|
|
993
|
+
claudeBridgeIngressUrl: "",
|
|
994
|
+
});
|
|
995
|
+
expect((await res.json()).target).toBe("2.1.130");
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it("returns 400 when unpatching fails", async () => {
|
|
999
|
+
vi.mocked(unpatch).mockResolvedValue({ ok: false, error: "no backup" } as any);
|
|
1000
|
+
const res = await app.request("/api/claude-compat/unpatch", { method: "POST" });
|
|
1001
|
+
expect(res.status).toBe(400);
|
|
1002
|
+
expect((await res.json()).error).toBe("no backup");
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
describe("POST /api/claude-compat/dismiss-banner", () => {
|
|
1007
|
+
it("records the explicitly supplied version", async () => {
|
|
1008
|
+
const res = await app.request("/api/claude-compat/dismiss-banner", {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: { "Content-Type": "application/json" },
|
|
1011
|
+
body: JSON.stringify({ version: "2.1.131" }),
|
|
1012
|
+
});
|
|
1013
|
+
expect(res.status).toBe(200);
|
|
1014
|
+
expect(updateSettings).toHaveBeenCalledWith({
|
|
1015
|
+
claudeCompatBannerDismissedVersion: "2.1.131",
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it("falls back to the currently installed version", async () => {
|
|
1020
|
+
const res = await app.request("/api/claude-compat/dismiss-banner", { method: "POST" });
|
|
1021
|
+
expect(res.status).toBe(200);
|
|
1022
|
+
expect(updateSettings).toHaveBeenCalledWith({
|
|
1023
|
+
claudeCompatBannerDismissedVersion: "2.1.130",
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it("returns 400 when there is no version to record", async () => {
|
|
1028
|
+
vi.mocked(getCompatState).mockReturnValue({
|
|
1029
|
+
installedVersion: null, installedPath: null, isIncompatible: false,
|
|
1030
|
+
isPatched: false, availableKnownGood: [], suggestedPinTarget: "",
|
|
1031
|
+
lastChecked: 0, error: null,
|
|
1032
|
+
} as any);
|
|
1033
|
+
|
|
1034
|
+
const res = await app.request("/api/claude-compat/dismiss-banner", { method: "POST" });
|
|
1035
|
+
expect(res.status).toBe(400);
|
|
1036
|
+
expect((await res.json()).error).toMatch(/version/i);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
@@ -4,6 +4,7 @@ import type { WsBridge } from "../ws-bridge.js";
|
|
|
4
4
|
import type { TerminalManager } from "../terminal-manager.js";
|
|
5
5
|
import { getUsageLimits } from "../usage-limits.js";
|
|
6
6
|
import { getSystemMemory } from "../system-memory.js";
|
|
7
|
+
import { getSystemDisk } from "../system-disk.js";
|
|
7
8
|
import {
|
|
8
9
|
getUpdateState,
|
|
9
10
|
checkForUpdate,
|
|
@@ -49,6 +50,15 @@ export function registerSystemRoutes(
|
|
|
49
50
|
return c.json(getSystemMemory());
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
// Free space on the volume holding COMPANION_HOME. Backed by a single
|
|
54
|
+
// statfs(2) call, so it is cheap enough to poll without caching.
|
|
55
|
+
api.get("/system/disk", (c) => {
|
|
56
|
+
// JSON null rather than 204: the browser client calls res.json()
|
|
57
|
+
// unconditionally, and an empty body would throw and register a spurious
|
|
58
|
+
// API failure on every poll.
|
|
59
|
+
return c.json(getSystemDisk());
|
|
60
|
+
});
|
|
61
|
+
|
|
52
62
|
api.get("/sessions/:id/usage-limits", async (c) => {
|
|
53
63
|
const sessionId = c.req.param("id");
|
|
54
64
|
const session = deps.wsBridge.getSession(sessionId);
|