@hellcoder/companion 0.113.4 → 0.113.5-preview.20260730100656.ecb60c9
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-B2NtxJig.js → AgentsPage-Ct9Hvk1J.js} +1 -1
- package/dist/assets/{CronManager-CES2-E0_.js → CronManager-B4XnCjHZ.js} +1 -1
- package/dist/assets/{DashboardPage-aFf4uQfU.js → DashboardPage-CfgavOrA.js} +1 -1
- package/dist/assets/{IntegrationsPage-C_tZ2P0K.js → IntegrationsPage-CmRcLZ2Y.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-DUQik5kh.js → LinearOAuthSettingsPage-D5fdWchh.js} +1 -1
- package/dist/assets/{LinearSettingsPage-DFh8jfhh.js → LinearSettingsPage-BHrdSu6B.js} +1 -1
- package/dist/assets/{Playground-C4t0-Zia.js → Playground-BDjcdHjZ.js} +1 -1
- package/dist/assets/{PromptsPage-Ma4gA0R7.js → PromptsPage-C4XeLDqH.js} +1 -1
- package/dist/assets/{RunsPage-jk2yW392.js → RunsPage-9_OGNofF.js} +1 -1
- package/dist/assets/{SandboxManager-BA6-RlkR.js → SandboxManager-C0UI27DS.js} +1 -1
- package/dist/assets/SettingsPage-1ggRq3DY.js +1 -0
- package/dist/assets/{TailscalePage-CZ6V3kc7.js → TailscalePage-9EDSWy6L.js} +1 -1
- package/dist/assets/{index-B1Oejky6.js → index-DRO5Cupx.js} +3 -3
- package/dist/assets/{sw-register-B9GK06Mo.js → sw-register-UeoQRzHq.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +2 -1
- package/server/claude-adapter.ts +9 -7
- package/server/claude-sdk-adapter.test.ts +298 -0
- package/server/claude-sdk-adapter.ts +394 -0
- package/server/cli-launcher.ts +110 -0
- package/server/routes/settings-routes.ts +10 -0
- package/server/routes.test.ts +27 -0
- package/server/settings-manager.test.ts +21 -0
- package/server/settings-manager.ts +16 -1
- package/dist/assets/SettingsPage-BehGv-KV.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-DRO5Cupx.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,7 +11,7 @@
|
|
|
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-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DRO5Cupx.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-C86AT0KJ.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
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":"36bcb8b7fbc035bfba60d0d2d3a66e19","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-UeoQRzHq.js"},{"revision":null,"url":"assets/index-DRO5Cupx.js"},{"revision":null,"url":"assets/index-C86AT0KJ.css"},{"revision":null,"url":"assets/TailscalePage-9EDSWy6L.js"},{"revision":null,"url":"assets/SettingsPage-1ggRq3DY.js"},{"revision":null,"url":"assets/SandboxManager-C0UI27DS.js"},{"revision":null,"url":"assets/RunsPage-9_OGNofF.js"},{"revision":null,"url":"assets/PromptsPage-C4XeLDqH.js"},{"revision":null,"url":"assets/Playground-BDjcdHjZ.js"},{"revision":null,"url":"assets/LinearSettingsPage-BHrdSu6B.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-D5fdWchh.js"},{"revision":null,"url":"assets/IntegrationsPage-CmRcLZ2Y.js"},{"revision":null,"url":"assets/DashboardPage-CfgavOrA.js"},{"revision":null,"url":"assets/CronManager-B4XnCjHZ.js"},{"revision":null,"url":"assets/AgentsPage-Ct9Hvk1J.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.113.
|
|
3
|
+
"version": "0.113.5-preview.20260730100656.ecb60c9",
|
|
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",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"generate-token": "bun bin/generate-token.ts"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
|
54
55
|
"@codemirror/lang-cpp": "^6.0.3",
|
|
55
56
|
"@codemirror/lang-css": "^6.3.1",
|
|
56
57
|
"@codemirror/lang-html": "^6.4.11",
|
package/server/claude-adapter.ts
CHANGED
|
@@ -211,13 +211,15 @@ function toolBlockIds(content: unknown): { uses: string[]; results: string[] } {
|
|
|
211
211
|
// --- Claude Code Adapter ------------------------------------------------------
|
|
212
212
|
|
|
213
213
|
export class ClaudeAdapter implements IBackendAdapter {
|
|
214
|
-
|
|
214
|
+
protected sessionId: string;
|
|
215
215
|
|
|
216
216
|
// Transport selector. "websocket" is the legacy `--sdk-url` transport where
|
|
217
217
|
// the CLI dials back into the server; "stdio" is the supported stream-json
|
|
218
218
|
// transport where the server owns the child process and bridges over its
|
|
219
219
|
// stdin/stdout pipes. See claude-adapter stdio section below.
|
|
220
|
-
|
|
220
|
+
// "sdk" is set by SdkClaudeAdapter (claude-sdk-adapter.ts), which reuses
|
|
221
|
+
// this class's routing/queueing but replaces the byte transport entirely.
|
|
222
|
+
protected transportKind: "websocket" | "stdio" | "sdk" = "websocket";
|
|
221
223
|
|
|
222
224
|
// WebSocket to the Claude Code CLI process (transportKind === "websocket")
|
|
223
225
|
private cliSocket: ServerWebSocket<SocketData> | null = null;
|
|
@@ -234,7 +236,7 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
234
236
|
/** Guard so the disconnect callback fires at most once per transport, no
|
|
235
237
|
* matter which teardown path (stdout reader end, reader error, or process
|
|
236
238
|
* exit) trips first. Mirrors CodexAdapter.disconnectFired. */
|
|
237
|
-
|
|
239
|
+
protected disconnectFired = false;
|
|
238
240
|
/** Whether the most recent substantive CLI message was a terminal `result`
|
|
239
241
|
* (end-of-turn). Used by the stdout-EOF handler to distinguish a process
|
|
240
242
|
* that closed stdout while *finishing a turn cleanly* (exiting on its own —
|
|
@@ -267,10 +269,10 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
267
269
|
// Callbacks registered by the bridge via on*() methods
|
|
268
270
|
private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
|
|
269
271
|
private sessionMetaCb: ((meta: { cliSessionId?: string; model?: string; cwd?: string }) => void) | null = null;
|
|
270
|
-
|
|
272
|
+
protected disconnectCb: (() => void) | null = null;
|
|
271
273
|
|
|
272
274
|
// Pending NDJSON messages queued before CLI WebSocket connects
|
|
273
|
-
|
|
275
|
+
protected pendingMessages: string[] = [];
|
|
274
276
|
|
|
275
277
|
// Async control request/response pairs (e.g. MCP status queries)
|
|
276
278
|
private pendingControlRequests = new Map<string, PendingControlRequest>();
|
|
@@ -282,7 +284,7 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
282
284
|
};
|
|
283
285
|
|
|
284
286
|
// Optional recorder for raw protocol messages
|
|
285
|
-
|
|
287
|
+
protected recorder: RecorderManager | null;
|
|
286
288
|
|
|
287
289
|
// Session cwd used to stage non-inline attachments (anything other than
|
|
288
290
|
// image/*, i.e. PDFs and all other files) inside the working directory so the
|
|
@@ -1895,7 +1897,7 @@ export class ClaudeAdapter implements IBackendAdapter {
|
|
|
1895
1897
|
* delimiter and records the outgoing message. Assumes the transport is
|
|
1896
1898
|
* connected (callers gate on isConnected() / flush after attach).
|
|
1897
1899
|
*/
|
|
1898
|
-
|
|
1900
|
+
protected sendRaw(ndjson: string): boolean {
|
|
1899
1901
|
// Record raw outgoing CLI message
|
|
1900
1902
|
this.recorder?.record(
|
|
1901
1903
|
this.sessionId, "out", ndjson, "cli", "claude", "",
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Stub Bun.hash for vitest (runs under Node, not Bun) — the shared CLI-message
|
|
4
|
+
// dedup in ws-bridge-cli-ingest hashes raw lines with it. Same stub as
|
|
5
|
+
// ws-bridge-cli-ingest.test.ts.
|
|
6
|
+
if (typeof globalThis.Bun === "undefined") {
|
|
7
|
+
(globalThis as unknown as { Bun: { hash: (s: string) => bigint } }).Bun = {
|
|
8
|
+
hash: (input: string) => {
|
|
9
|
+
let h = 0n;
|
|
10
|
+
for (let i = 0; i < input.length; i++) {
|
|
11
|
+
h = (h * 31n + BigInt(input.charCodeAt(i))) & 0xffffffffffffn;
|
|
12
|
+
}
|
|
13
|
+
return h;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SdkClaudeAdapter tests — the switchable Agent-SDK transport.
|
|
20
|
+
*
|
|
21
|
+
* These verify the adapter's core contract: it must be wire-compatible with
|
|
22
|
+
* the stdio ClaudeAdapter from the bridge's point of view. The SDK itself is
|
|
23
|
+
* mocked with a controllable async generator + method spies, so every test
|
|
24
|
+
* exercises the real base-class routing (handleRawMessage / handleOutgoing*)
|
|
25
|
+
* end to end without spawning a CLI.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Controllable stand-in for the SDK's Query async generator.
|
|
29
|
+
class MockQuery {
|
|
30
|
+
private buffer: unknown[] = [];
|
|
31
|
+
private waiters: Array<{
|
|
32
|
+
resolve: (r: IteratorResult<unknown>) => void;
|
|
33
|
+
reject: (e: Error) => void;
|
|
34
|
+
}> = [];
|
|
35
|
+
private done = false;
|
|
36
|
+
private error: Error | null = null;
|
|
37
|
+
interrupt = vi.fn(async () => undefined);
|
|
38
|
+
setModel = vi.fn(async () => undefined);
|
|
39
|
+
setPermissionMode = vi.fn(async () => undefined);
|
|
40
|
+
mcpServerStatus = vi.fn(async () => [{ name: "test-mcp", status: "connected" }]);
|
|
41
|
+
|
|
42
|
+
emit(msg: unknown): void {
|
|
43
|
+
const w = this.waiters.shift();
|
|
44
|
+
if (w) w.resolve({ value: msg, done: false });
|
|
45
|
+
else this.buffer.push(msg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
end(): void {
|
|
49
|
+
this.done = true;
|
|
50
|
+
for (const w of this.waiters.splice(0)) w.resolve({ value: undefined, done: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Make the generator reject — how the live SDK surfaces an abort/crash. */
|
|
54
|
+
fail(err: Error): void {
|
|
55
|
+
this.error = err;
|
|
56
|
+
for (const w of this.waiters.splice(0)) w.reject(err);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[Symbol.asyncIterator]() {
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
next(): Promise<IteratorResult<unknown>> {
|
|
64
|
+
if (this.error) return Promise.reject(this.error);
|
|
65
|
+
if (this.buffer.length > 0) {
|
|
66
|
+
return Promise.resolve({ value: this.buffer.shift(), done: false });
|
|
67
|
+
}
|
|
68
|
+
if (this.done) return Promise.resolve({ value: undefined, done: true });
|
|
69
|
+
return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return(): Promise<IteratorResult<unknown>> {
|
|
73
|
+
this.end();
|
|
74
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw(e: unknown): Promise<IteratorResult<unknown>> {
|
|
78
|
+
return Promise.reject(e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const mockState = vi.hoisted(() => ({
|
|
83
|
+
lastQuery: null as unknown,
|
|
84
|
+
lastParams: null as { prompt: AsyncIterable<unknown>; options?: Record<string, unknown> } | null,
|
|
85
|
+
canUseTool: null as
|
|
86
|
+
| ((toolName: string, input: Record<string, unknown>, opts: Record<string, unknown>) => Promise<unknown>)
|
|
87
|
+
| null,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
91
|
+
query: vi.fn((params: { prompt: AsyncIterable<unknown>; options?: Record<string, unknown> }) => {
|
|
92
|
+
mockState.lastParams = params;
|
|
93
|
+
mockState.canUseTool = (params.options?.canUseTool as typeof mockState.canUseTool) ?? null;
|
|
94
|
+
return mockState.lastQuery;
|
|
95
|
+
}),
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
import { SdkClaudeAdapter } from "./claude-sdk-adapter.js";
|
|
99
|
+
|
|
100
|
+
async function drainInput(iterable: AsyncIterable<unknown>, count: number): Promise<unknown[]> {
|
|
101
|
+
const out: unknown[] = [];
|
|
102
|
+
const it = iterable[Symbol.asyncIterator]();
|
|
103
|
+
for (let i = 0; i < count; i++) {
|
|
104
|
+
const r = await it.next();
|
|
105
|
+
if (r.done) break;
|
|
106
|
+
out.push(r.value);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function makeAdapter() {
|
|
112
|
+
const q = new MockQuery();
|
|
113
|
+
mockState.lastQuery = q;
|
|
114
|
+
const adapter = new SdkClaudeAdapter("test-session");
|
|
115
|
+
const browserMessages: Array<Record<string, unknown>> = [];
|
|
116
|
+
adapter.onBrowserMessage((m) => browserMessages.push(m as unknown as Record<string, unknown>));
|
|
117
|
+
const disconnects: number[] = [];
|
|
118
|
+
adapter.onDisconnect(() => disconnects.push(Date.now()));
|
|
119
|
+
const exits: Array<number | null> = [];
|
|
120
|
+
adapter.onExit((code) => exits.push(code));
|
|
121
|
+
adapter.attachSdk({ model: "claude-opus-5", permissionMode: "bypassPermissions", cwd: "/tmp" });
|
|
122
|
+
return { adapter, q, browserMessages, disconnects, exits };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
mockState.lastQuery = null;
|
|
127
|
+
mockState.lastParams = null;
|
|
128
|
+
mockState.canUseTool = null;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("SdkClaudeAdapter", () => {
|
|
132
|
+
it("reports connected after attach and passes core options to the SDK", () => {
|
|
133
|
+
const { adapter } = makeAdapter();
|
|
134
|
+
expect(adapter.isConnected()).toBe(true);
|
|
135
|
+
const opts = mockState.lastParams?.options ?? {};
|
|
136
|
+
expect(opts.model).toBe("claude-opus-5");
|
|
137
|
+
expect(opts.permissionMode).toBe("bypassPermissions");
|
|
138
|
+
expect(opts.includePartialMessages).toBe(true);
|
|
139
|
+
// Env must inherit the process env (subscription auth resolution).
|
|
140
|
+
expect((opts.env as Record<string, string>).PATH).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("forwards user messages into the SDK input stream", async () => {
|
|
144
|
+
const { adapter } = makeAdapter();
|
|
145
|
+
adapter.send({
|
|
146
|
+
type: "user_message",
|
|
147
|
+
content: "hello there",
|
|
148
|
+
} as never);
|
|
149
|
+
const [first] = (await drainInput(mockState.lastParams!.prompt, 1)) as Array<{
|
|
150
|
+
type: string;
|
|
151
|
+
message: { role: string };
|
|
152
|
+
parent_tool_use_id: null;
|
|
153
|
+
}>;
|
|
154
|
+
expect(first.type).toBe("user");
|
|
155
|
+
expect(first.message.role).toBe("user");
|
|
156
|
+
expect(first.parent_tool_use_id).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("routes inbound SDK messages through the shared message brain", async () => {
|
|
160
|
+
const { q, browserMessages } = makeAdapter();
|
|
161
|
+
q.emit({
|
|
162
|
+
type: "assistant",
|
|
163
|
+
message: { id: "m1", role: "assistant", content: [{ type: "text", text: "hi" }] },
|
|
164
|
+
session_id: "cli-sess-1",
|
|
165
|
+
});
|
|
166
|
+
await vi.waitFor(() => {
|
|
167
|
+
expect(browserMessages.some((m) => m.type === "assistant")).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("maps interrupt to Query.interrupt()", async () => {
|
|
172
|
+
const { adapter, q } = makeAdapter();
|
|
173
|
+
adapter.send({ type: "interrupt" } as never);
|
|
174
|
+
await vi.waitFor(() => expect(q.interrupt).toHaveBeenCalledOnce());
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("maps set_model and set_permission_mode to the Query methods", async () => {
|
|
178
|
+
const { adapter, q } = makeAdapter();
|
|
179
|
+
adapter.send({ type: "set_model", model: "claude-sonnet-5" } as never);
|
|
180
|
+
adapter.send({ type: "set_permission_mode", mode: "acceptEdits" } as never);
|
|
181
|
+
await vi.waitFor(() => {
|
|
182
|
+
expect(q.setModel).toHaveBeenCalledWith("claude-sonnet-5");
|
|
183
|
+
expect(q.setPermissionMode).toHaveBeenCalledWith("acceptEdits");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("serves mcp_get_status from Query.mcpServerStatus()", async () => {
|
|
188
|
+
const { adapter, q, browserMessages } = makeAdapter();
|
|
189
|
+
adapter.send({ type: "mcp_get_status" } as never);
|
|
190
|
+
await vi.waitFor(() => {
|
|
191
|
+
expect(q.mcpServerStatus).toHaveBeenCalledOnce();
|
|
192
|
+
const status = browserMessages.find((m) => m.type === "mcp_status");
|
|
193
|
+
expect(status).toBeDefined();
|
|
194
|
+
expect((status?.servers as Array<{ name: string }>)[0]?.name).toBe("test-mcp");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("round-trips canUseTool through the bridge permission flow (allow)", async () => {
|
|
199
|
+
const { adapter, browserMessages } = makeAdapter();
|
|
200
|
+
expect(mockState.canUseTool).toBeTypeOf("function");
|
|
201
|
+
|
|
202
|
+
const resultPromise = mockState.canUseTool!(
|
|
203
|
+
"Bash",
|
|
204
|
+
{ command: "ls" },
|
|
205
|
+
{ signal: new AbortController().signal, suggestions: [] },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// The adapter must synthesize the CLI's can_use_tool frame for the bridge.
|
|
209
|
+
const req = await vi.waitFor(() => {
|
|
210
|
+
const r = browserMessages.find((m) => m.type === "permission_request");
|
|
211
|
+
expect(r).toBeDefined();
|
|
212
|
+
return r as { request: { request_id: string; tool_name: string } };
|
|
213
|
+
});
|
|
214
|
+
expect(req.request.tool_name).toBe("Bash");
|
|
215
|
+
|
|
216
|
+
// Bridge answers like it would for the stdio transport.
|
|
217
|
+
adapter.send({
|
|
218
|
+
type: "permission_response",
|
|
219
|
+
request_id: req.request.request_id,
|
|
220
|
+
behavior: "allow",
|
|
221
|
+
updated_input: { command: "ls -la" },
|
|
222
|
+
} as never);
|
|
223
|
+
|
|
224
|
+
const result = (await resultPromise) as { behavior: string; updatedInput: Record<string, unknown> };
|
|
225
|
+
expect(result.behavior).toBe("allow");
|
|
226
|
+
expect(result.updatedInput).toEqual({ command: "ls -la" });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("round-trips canUseTool deny with the user's message", async () => {
|
|
230
|
+
const { adapter, browserMessages } = makeAdapter();
|
|
231
|
+
const resultPromise = mockState.canUseTool!(
|
|
232
|
+
"Write",
|
|
233
|
+
{ file_path: "/etc/passwd" },
|
|
234
|
+
{ signal: new AbortController().signal },
|
|
235
|
+
);
|
|
236
|
+
const req = await vi.waitFor(() => {
|
|
237
|
+
const r = browserMessages.find((m) => m.type === "permission_request");
|
|
238
|
+
expect(r).toBeDefined();
|
|
239
|
+
return r as { request: { request_id: string } };
|
|
240
|
+
});
|
|
241
|
+
adapter.send({
|
|
242
|
+
type: "permission_response",
|
|
243
|
+
request_id: req.request.request_id,
|
|
244
|
+
behavior: "deny",
|
|
245
|
+
message: "nope",
|
|
246
|
+
} as never);
|
|
247
|
+
const result = (await resultPromise) as { behavior: string; message: string };
|
|
248
|
+
expect(result.behavior).toBe("deny");
|
|
249
|
+
expect(result.message).toBe("nope");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("cancelling via the SDK abort signal clears the pending permission in the bridge", async () => {
|
|
253
|
+
const { browserMessages } = makeAdapter();
|
|
254
|
+
const ac = new AbortController();
|
|
255
|
+
const resultPromise = mockState.canUseTool!("Bash", { command: "ls" }, { signal: ac.signal });
|
|
256
|
+
await vi.waitFor(() => {
|
|
257
|
+
expect(browserMessages.some((m) => m.type === "permission_request")).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
ac.abort();
|
|
260
|
+
const result = (await resultPromise) as { behavior: string };
|
|
261
|
+
expect(result.behavior).toBe("deny");
|
|
262
|
+
await vi.waitFor(() => {
|
|
263
|
+
expect(browserMessages.some((m) => m.type === "permission_cancelled")).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("fires disconnect + exit(0) when the SDK stream ends cleanly", async () => {
|
|
268
|
+
const { adapter, q, disconnects, exits } = makeAdapter();
|
|
269
|
+
q.end();
|
|
270
|
+
await vi.waitFor(() => {
|
|
271
|
+
expect(disconnects.length).toBe(1);
|
|
272
|
+
expect(exits).toEqual([0]);
|
|
273
|
+
});
|
|
274
|
+
expect(adapter.isConnected()).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("reports exit 143 when aborted (kill path), mirroring a SIGTERM'd child", async () => {
|
|
278
|
+
const { adapter, q, exits } = makeAdapter();
|
|
279
|
+
// Simulate the SDK generator rejecting after abort, as it does live.
|
|
280
|
+
adapter.abort();
|
|
281
|
+
q.fail(new Error("aborted"));
|
|
282
|
+
await vi.waitFor(() => expect(exits).toEqual([143]));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("queues messages sent before attach and flushes them on attach", async () => {
|
|
286
|
+
const q = new MockQuery();
|
|
287
|
+
mockState.lastQuery = q;
|
|
288
|
+
const adapter = new SdkClaudeAdapter("late-attach");
|
|
289
|
+
adapter.onBrowserMessage(() => {});
|
|
290
|
+
// Not attached yet — send queues instead of dropping.
|
|
291
|
+
adapter.send({ type: "user_message", content: "early" } as never);
|
|
292
|
+
adapter.attachSdk({});
|
|
293
|
+
const [first] = (await drainInput(mockState.lastParams!.prompt, 1)) as Array<{
|
|
294
|
+
message: { content: unknown };
|
|
295
|
+
}>;
|
|
296
|
+
expect(JSON.stringify(first.message.content)).toContain("early");
|
|
297
|
+
});
|
|
298
|
+
});
|