@interactive-inc/claude-funnel 0.18.0 → 0.19.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.
@@ -542,8 +542,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
542
542
  mv ${this.path} ${this.path}.bak`);if(x&&typeof x==="object"&&"version"in x&&x.version!==G2)throw Error(`unsupported settings.json version (${this.path}): expected ${G2}, got ${String(x.version)}`);let Y=vh.safeParse(x);if(!Y.success)throw Error(`invalid settings.json (${this.path}): ${Y.error.issues.map((X)=>`${X.path.join(".")}: ${X.message}`).join(", ")}`);return Y.data}looksLikeLegacy($){if(!$||typeof $!=="object")return!1;let x=$;if(Array.isArray(x.channels))for(let Y of x.channels){if(!Y||typeof Y!=="object")continue;let X=Y;if(Array.isArray(X.connectors)&&X.connectors.some((Q)=>typeof Q==="string"))return!0;if(!("id"in X)&&"name"in X)return!0}if(Array.isArray(x.connectors))return!0;if(Array.isArray(x.repositories))return!0;if(Array.isArray(x.profiles))for(let Y of x.profiles){if(!Y||typeof Y!=="object")continue;let X=Y;if("repository"in X||"envFiles"in X||"channel"in X&&!("channelId"in X))return!0}return!1}write($){this.fs.mkdirSync(om1(this.path),{recursive:!0});let x={...$,version:G2};this.fs.writeFileSync(this.path,`${JSON.stringify(x,null,2)}
543
543
  `)}}import{join as Ze0}from"path";class fY{}class xM{}class YM extends xM{constructor(){super();Object.freeze(this)}async fetch($){let x=await globalThis.fetch($.url,{method:$.method,headers:$.headers,body:$.body});return{status:x.status,ok:x.ok,text:()=>x.text(),json:()=>x.json()}}}var em1="https://discord.com/api/v10",$h1=new YM;class QM extends fY{token;http;constructor($){super();this.token=$.config.botToken,this.http=$.http??$h1,Object.freeze(this)}async call($){let x=($.method||"GET").toUpperCase(),Y=$.path.startsWith("/")?$.path:`/${$.path}`,X=$.body,Q=X!==null&&typeof X==="object"&&x!=="GET"&&Object.keys(X).length>0,z=await this.http.fetch({method:x,url:`${em1}${Y}`,headers:{Authorization:`Bot ${this.token}`,"Content-Type":"application/json"},body:Q?JSON.stringify($.body):void 0});if(!z.ok)throw Error(`Discord API failed (${z.status}): ${await z.text()}`);if(z.status===204)return null;return await z.json()}}var k2=xL(Hw1(),1);class f2{isAlive(){return!0}}class oA{ownUserId;constructor($){this.ownUserId=$.ownUserId}process($){if($.authorIsBot)return{skip:!0};let x=this.ownUserId?$.mentionedUserIds.includes(this.ownUserId):!1;return{skip:!1,content:JSON.stringify($.raw),meta:{event_type:"discord",channel_id:$.channelId,user_id:$.authorId,mentioned:String(x),guild_id:$.guildId??""}}}}import{appendFileSync as yJ0,mkdirSync as mJ0}from"fs";import{dirname as hJ0,join as gJ0}from"path";class NQ{}var pJ0=gJ0("/tmp/funnel","funnel.log");class I6 extends NQ{file;now;constructor($={}){super();this.file=$.file??pJ0,this.now=$.now??(()=>new Date),Object.freeze(this)}info($,x){this.write("info",$,x)}warn($,x){this.write("warn",$,x)}error($,x){this.write("error",$,x)}write($,x,Y){mJ0(hJ0(this.file),{recursive:!0});let X={time:this.now().toISOString(),level:$,message:x,...Y?{meta:Y}:{}};yJ0(this.file,`${JSON.stringify(X)}
544
544
  `)}}var iJ0=new I6;class rA extends f2{config;logger;client=null;constructor($){super();this.config=$.config,this.logger=$.logger??iJ0}async start($){let x=new k2.Client({intents:[k2.GatewayIntentBits.Guilds,k2.GatewayIntentBits.GuildMessages,k2.GatewayIntentBits.MessageContent,k2.GatewayIntentBits.DirectMessages],partials:[k2.Partials.Channel]});x.on("messageCreate",async(Y)=>{let X=x.user?.id??"",Q=[...Y.mentions.users.keys()];this.logger.info("discord messageCreate",{author:Y.author.id,authorIsBot:String(Y.author.bot),channelId:Y.channelId,guildId:Y.guildId??"",mentions:Q.join(","),ownUserId:X,mentioned:String(Q.includes(X))});let Z=new oA({ownUserId:X}).process({authorId:Y.author.id,authorIsBot:Y.author.bot,channelId:Y.channelId,guildId:Y.guildId,mentionedUserIds:Q,raw:Y.toJSON()});if(Z.skip){this.logger.info("discord skip",{reason:"bot author"});return}try{await $(Z.content,Z.meta)}catch(K){this.logger.error("discord notify error",{error:K instanceof Error?K.message:String(K)})}}),x.on("ready",(Y)=>{this.logger.info("discord ready",{userId:Y.user.id,tag:Y.user.tag,guilds:String(Y.guilds.cache.size)})}),x.on("error",(Y)=>{this.logger.error("discord client error",{error:Y instanceof Error?Y.message:String(Y)})}),await x.login(this.config.botToken),this.client=x}async stop(){if(!this.client)return;try{await this.client.destroy()}catch($){this.logger.error("discord stop error",{error:$ instanceof Error?$.message:String($)})}finally{this.client=null}}isAlive(){return this.client!==null}}class cW{}var tA=($)=>{if(!$)return;let x={};for(let[Y,X]of Object.entries(process.env))if(typeof X==="string")x[Y]=X;for(let[Y,X]of Object.entries($))x[Y]=X;return x};class L$ extends cW{constructor(){super();Object.freeze(this)}runSync($){let x=Bun.spawnSync($,{stdout:"pipe",stderr:"pipe"});return{exitCode:x.exitCode??0,stdout:x.stdout.toString(),stderr:x.stderr.toString()}}async run($,x={}){let Y=Bun.spawn($,{cwd:x.cwd,env:tA(x.env),stdin:x.input!==void 0?"pipe":"ignore",stdout:"pipe",stderr:"pipe"});if(x.input!==void 0&&Y.stdin)Y.stdin.write(x.input),Y.stdin.end();let X=await Y.exited,Q=await new Response(Y.stdout).text(),z=await new Response(Y.stderr).text();return{exitCode:X,stdout:Q,stderr:z}}async attach($,x={}){let Y=Bun.spawn($,{cwd:x.cwd,env:tA(x.env),stdio:["inherit","inherit","inherit"]});if(x.onSpawned)x.onSpawned(Y.pid);return await Y.exited}detach($,x={}){Bun.spawn($,{env:tA(x.env),stdio:["ignore","ignore","ignore"]}).unref()}kill($,x="SIGTERM"){try{process.kill($,x)}catch{}}}var lJ0=new L$;class eA extends fY{process;constructor($={}){super();this.process=$.process??lJ0,Object.freeze(this)}async call($){let x=["api",$.path];if($.method&&$.method.toLowerCase()!=="get")x.push("-X",$.method.toUpperCase());let Y=$.body&&typeof $.body==="object"&&Object.keys($.body).length>0;if(Y)x.push("--input","-");let X=await this.process.run(["gh",...x],{input:Y?JSON.stringify($.body):void 0});if(X.exitCode!==0)throw Error(`gh api failed: ${X.stderr.trim()||X.stdout.trim()}`);try{return JSON.parse(X.stdout)}catch{return X.stdout}}}var nJ0=m.object({id:m.string(),reason:m.string(),subject:m.object({type:m.string(),url:m.string(),title:m.string()}),repository:m.object({full_name:m.string()}),updated_at:m.string()}),dJ0=m.array(nJ0),aJ0=new L$,sJ0=new I6,oJ0=1e4,rJ0=5000;class $f extends f2{config;process;logger;now;seen=new Map;bootstrapped=!1;since;timer=null;constructor($){super();this.config=$.config,this.process=$.process??aJ0,this.logger=$.logger??sJ0,this.now=$.now??(()=>new Date),this.since=this.now().toISOString()}async start($){await this.pollOnce($);let x=this.config.pollInterval??60;this.timer=setInterval(()=>void this.pollOnce($),x*1000),this.timer.unref()}async stop(){if(!this.timer)return;clearInterval(this.timer),this.timer=null}isAlive(){return this.timer!==null}async pollOnce($){let x=this.now().toISOString(),Y=new URLSearchParams({since:this.since,all:"false"});try{let X=await this.process.run(["gh","api",`/notifications?${Y}`]);if(X.exitCode!==0){this.logger.error("gh poll failed",{stderr:X.stderr});return}let Q=dJ0.safeParse(JSON.parse(X.stdout));if(!Q.success){this.logger.warn("gh response did not match schema",{error:Q.error.message});return}let z=Q.data;for(let Z of z){if(this.seen.get(Z.id)===Z.updated_at)continue;if(this.seen.set(Z.id,Z.updated_at),!this.bootstrapped)continue;let K={event_type:"gh",reason:Z.reason,subject_type:Z.subject.type,subject_url:Z.subject.url,repository:Z.repository.full_name,thread_id:Z.id,updated_at:Z.updated_at};await $(JSON.stringify(Z),K)}if(this.seen.size>oJ0){let Z=this.seen.size-rJ0,K=0;for(let W of this.seen.keys()){if(K>=Z)break;this.seen.delete(W),K++}}this.since=x,this.bootstrapped=!0}catch(X){this.logger.error("gh poll error",{error:X instanceof Error?X.message:String(X)})}}}var SW=($,x,Y)=>{let X=new Set;for(let Q of $.split(",")){let[z,Z]=Q.split("/"),K=Z?Number(Z):1;if(!Number.isFinite(K)||K<=0)throw Error(`invalid cron step: "${Z}"`);let W=x,V=Y;if(z==="*"||z===void 0||z==="")W=x,V=Y;else if(z.includes("-")){let[w,U]=z.split("-"),H=Number(w),J=Number(U);if(!Number.isFinite(H)||!Number.isFinite(J))throw Error(`invalid cron range: "${z}"`);W=H,V=J}else{let w=Number(z);if(!Number.isFinite(w))throw Error(`invalid cron value: "${z}"`);W=w,V=Z?Y:w}if(W<x||V>Y||W>V)throw Error(`cron value out of range: ${z} (must be ${x}-${Y})`);for(let w=W;w<=V;w+=K)X.add(w)}return{min:x,max:Y,values:X}},$q=($,x)=>{let Y=$.trim().split(/\s+/);if(Y.length!==5)throw Error(`cron must have 5 fields (got ${Y.length}): "${$}"`);let[X,Q,z,Z,K]=Y;if(!X||!Q||!z||!Z||!K)throw Error(`cron has empty fields: "${$}"`);let W=[{field:SW(X,0,59),value:x.getMinutes()},{field:SW(Q,0,23),value:x.getHours()},{field:SW(z,1,31),value:x.getDate()},{field:SW(Z,1,12),value:x.getMonth()+1},{field:SW(K,0,6),value:x.getDay()}];for(let{field:V,value:w}of W)if(!V.values.has(w))return!1;return!0};var tJ0=new I6,Gw1=1440;class xf extends f2{config;lastFiredStore;logger;now;onFired;timer=null;stopped=!1;constructor($){super();this.config=$.config,this.lastFiredStore=$.lastFiredStore,this.logger=$.logger??tJ0,this.now=$.now??(()=>new Date),this.onFired=$.onFired??null}async start($){this.stopped=!1;let x=()=>{if(this.stopped)return;let Y=this.now(),X=60000-(Y.getSeconds()*1000+Y.getMilliseconds());this.timer=setTimeout(async()=>{if(this.stopped)return;await this.tick($),x()},X),this.timer.unref()};await this.tick($),x()}async stop(){if(this.stopped=!0,this.timer)clearTimeout(this.timer),this.timer=null}isAlive(){return!this.stopped&&this.timer!==null}async tick($){let x=this.truncateToMinute(this.now()),Y=this.lastFiredStore.load(),X=!1;for(let Q of this.config.entries){if(!Q.enabled)continue;if(await this.fireEntry(Q,x,Y,$))X=!0}if(X)this.lastFiredStore.save(Y)}async fireEntry($,x,Y,X){let Q=Y.get($.id),z=Q?new Date(Q.getTime()+60000):x;if(z.getTime()>x.getTime())return!1;if($.catchupPolicy==="skip"){try{if(!$q($.cron,x))return!1}catch(K){return this.logInvalidCron($,K),!1}return await this.notifyOne($,x,X,!1),Y.set($.id,x),!0}if($.catchupPolicy==="all"){let K=this.findAllMatches($.cron,z,x,$.id);if(K.length===0)return!1;for(let W of K)await this.notifyOne($,W,X,W.getTime()!==x.getTime());return Y.set($.id,K[K.length-1]??x),!0}let Z=this.findMostRecentMatch($.cron,z,x,$.id);if(!Z)return!1;return await this.notifyOne($,Z,X,Z.getTime()!==x.getTime()),Y.set($.id,Z),!0}async notifyOne($,x,Y,X){let Q={event_type:"schedule",schedule_id:$.id,cron:$.cron,fired_at:x.toISOString(),catchup_policy:$.catchupPolicy};if(X)Q.catchup="true";if(await Y($.prompt,Q),this.onFired)try{await this.onFired($,x)}catch(z){this.logger.error("schedule onFired callback failed",{connector:this.config.name,id:$.id,error:z instanceof Error?z.message:String(z)})}}findMostRecentMatch($,x,Y,X){let Q=Math.min(Gw1,Math.floor((Y.getTime()-x.getTime())/60000)+1);for(let z=0;z<Q;z++){let Z=new Date(Y.getTime()-z*60000);try{if($q($,Z))return Z}catch(K){return this.logInvalidCron({id:X,cron:$},K),null}}return null}findAllMatches($,x,Y,X){let Q=Math.min(Gw1,Math.floor((Y.getTime()-x.getTime())/60000)+1),z=[];for(let Z=0;Z<Q;Z++){let K=new Date(x.getTime()+Z*60000);if(K.getTime()>Y.getTime())break;try{if($q($,K))z.push(K)}catch(W){return this.logInvalidCron({id:X,cron:$},W),[]}}return z}logInvalidCron($,x){this.logger.error("invalid cron expression in schedule",{connector:this.config.name,id:$.id,cron:$.cron,error:x instanceof Error?x.message:String(x)})}truncateToMinute($){let x=new Date($.getTime());return x.setSeconds(0,0),x}}import{dirname as eJ0}from"path";var $q0=new M8;class Yf{path;fs;constructor($){this.path=$.path,this.fs=$.fs??$q0,Object.freeze(this)}load(){let $=new Map;if(!this.fs.existsSync(this.path))return $;let x=JSON.parse(this.fs.readFileSync(this.path));if(x===null||typeof x!=="object")return $;for(let[Y,X]of Object.entries(x))if(typeof X==="string")$.set(Y,new Date(X));return $}save($){let x={};for(let[Y,X]of $)x[Y]=X.toISOString();this.fs.mkdirSync(eJ0(this.path),{recursive:!0}),this.fs.writeFileSync(this.path,`${JSON.stringify(x,null,2)}
545
- `)}}var AD1=xL(RD1(),1);var uj0=($)=>{let x={};for(let[Y,X]of Object.entries($))x[Y]=X;return x};class kk extends fY{client;constructor($){super();this.client=$.client??new AD1.WebClient($.config.botToken),Object.freeze(this)}async call($){let x=$.body!==null&&typeof $.body==="object"?uj0($.body):{};return await this.client.apiCall($.path,x)}}var ZF=xL(XS1(),1);var zr0=new Set(["message","app_mention"]),Zr0=new Set([void 0,"thread_broadcast","bot_message","file_share"]);var J7=($,x)=>{let Y=$[x];return typeof Y==="string"?Y:void 0};class SC{ownBotUserId;ownBotId;now;dedup=new Map;constructor($){this.ownBotUserId=$.ownBotUserId,this.ownBotId=$.ownBotId,this.now=$.now??(()=>Date.now())}process($){let x=J7($,"type");if(!x||!zr0.has(x))return{skip:!0};let Y=J7($,"subtype");if(!Zr0.has(Y))return{skip:!0};let X=J7($,"channel")??"",Q=J7($,"event_ts")??J7($,"ts")??"",z=`${X}:${Q}`,Z=this.now();if(this.dedup.has(z))return{skip:!0};this.dedup.set(z,Z);for(let H of this.dedup.keys())if((this.dedup.get(H)??0)<Z-1e4)this.dedup.delete(H);let K=J7($,"user"),W=J7($,"bot_id");if(K===this.ownBotUserId)return{skip:!0};if(W===this.ownBotId)return{skip:!0};let w=(J7($,"text")??"").includes(`<@${this.ownBotUserId}>`),U=J7($,"thread_ts")??J7($,"ts")??"";return{skip:!1,content:JSON.stringify($),meta:{event_type:"slack",channel_id:X,user_id:K??"",mentioned:String(w),thread_ts:U},shouldReact:w,channel:X,timestamp:J7($,"ts")??""}}}var Kr0=m.object({event:m.record(m.string(),m.unknown()).optional()}),Wr0=new I6;class EC extends f2{config;logger;onAppCreated;preprocessEvent;app=null;constructor($){super();this.config=$.config,this.logger=$.logger??Wr0,this.onAppCreated=$.onAppCreated??null,this.preprocessEvent=$.preprocessEvent??null}async start($){let x=new ZF.App({token:this.config.botToken,appToken:this.config.appToken,socketMode:!0,logLevel:ZF.LogLevel.ERROR}),Y=await x.client.auth.test({token:this.config.botToken}),X=new SC({ownBotUserId:Y.user_id??"",ownBotId:Y.bot_id??""}),Q=this.preprocessEvent;if(x.use(async(z)=>{let Z=Kr0.safeParse(z);if(!Z.success||!Z.data.event)return;let K=Z.data.event,W=Q?Q(K):K;if(W===null)return;let V=X.process(W);if(V.skip)return;if(V.shouldReact)try{await x.client.reactions.add({token:this.config.botToken,channel:V.channel,timestamp:V.timestamp,name:"eyes"})}catch{}await $(V.content,V.meta)}),x.error(async(z)=>{this.logger.error("Slack error",{error:z instanceof Error?z.message:String(z)})}),this.onAppCreated)await this.onAppCreated(x);await x.start(),this.app=x}async stop(){if(!this.app)return;try{await this.app.stop()}catch($){this.logger.error("Slack stop error",{error:$ instanceof Error?$.message:String($)})}finally{this.app=null}}isAlive(){return this.app!==null}}import{join as CC}from"path";var Vr0=new M8,wr0=new L$,Ur0=new I6;class uC{fs;process;logger;dir;slackListenerOptions;scheduleListenerOptions;constructor($={}){this.fs=$.fs??Vr0,this.process=$.process??wr0,this.logger=$.logger??Ur0,this.dir=$.dir??O8,this.slackListenerOptions=$.slackListenerOptions??{},this.scheduleListenerOptions=$.scheduleListenerOptions??{},Object.freeze(this)}createListener($,x){if(x.type==="slack")return new EC({config:x,logger:this.logger,onAppCreated:this.slackListenerOptions.onAppCreated,preprocessEvent:this.slackListenerOptions.preprocessEvent});if(x.type==="gh")return new $f({config:x,process:this.process,logger:this.logger});if(x.type==="discord")return new rA({config:x,logger:this.logger});let Y=new Yf({path:CC(this.connectorDir($,x.id),"state.json"),fs:this.fs});return new xf({config:x,lastFiredStore:Y,logger:this.logger,onFired:this.scheduleListenerOptions.onFired})}createAdapter($){if($.type==="slack")return new kk({config:$});if($.type==="gh")return new eA({process:this.process});if($.type==="discord")return new QM({config:$});return null}connectorDir($,x){return CC(this.dir,"channels",$,"connectors",x)}channelDir($){return CC(this.dir,"channels",$)}}function yC($){switch($.type){case"slack":return[$.botToken,$.appToken];case"discord":return[$.botToken];case"gh":case"schedule":return[]}}function Hr0($,x){return $.type===x}function YX($,x,Y){let X=$.connectors.find((Q)=>Q.name===x);if(!X)throw Error(`connector "${x}" not found in channel "${$.name}"`);if(!Hr0(X,Y))throw Error(`connector "${x}" is type "${X.type}", not "${Y}"`);return X}class EV{millis(){return this.now().getTime()}iso(){return this.now().toISOString()}}class QX extends EV{now(){return new Date}}class CV{}class uV extends CV{generate(){return crypto.randomUUID()}}var Gr0=new QX,Jr0=new uV;class mC{store;factory;profileChecker;clock;idGenerator;constructor($){this.store=$.store,this.factory=$.factory,this.profileChecker=$.profileChecker,this.clock=$.clock??Gr0,this.idGenerator=$.idGenerator??Jr0,Object.freeze(this)}list(){return this.store.read().channels}get($){return this.list().find((x)=>x.name===$)??null}getById($){return this.list().find((x)=>x.id===$)??null}add($){let x=this.store.read();if(x.channels.some((X)=>X.name===$.name))throw Error(`channel "${$.name}" already exists`);let Y={id:this.idGenerator.generate(),name:$.name,delivery:$.delivery??"fanout",options:$.options??[],env:$.env??{},connectors:[]};return x.channels.push(Y),this.store.write(x),Y}setDelivery($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);X.delivery=x,this.store.write(Y)}setOptions($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);X.options=x,this.store.write(Y)}setEnv($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);X.env=x,this.store.write(Y)}remove($){let x=this.store.read(),Y=x.channels.findIndex((Q)=>Q.name===$);if(Y<0)throw Error(`channel "${$}" not found`);let X=x.channels[Y];if(X&&this.profileChecker.hasChannelRef(X.id))throw Error(`channel "${$}" is referenced by a profile`);x.channels.splice(Y,1),this.store.write(x)}rename($,x){let Y=this.store.read(),X=Y.channels.find((Q)=>Q.name===$);if(!X)throw Error(`channel "${$}" not found`);if(Y.channels.some((Q)=>Q.name===x))throw Error(`channel "${x}" already exists`);X.name=x,this.store.write(Y)}listConnectors($){return this.requireChannel(this.store.read(),$).connectors}getConnector($,x){let Y=this.get($);if(!Y)return null;return Y.connectors.find((X)=>X.name===x)??null}listAllConnectors(){let $=[];for(let x of this.list())for(let Y of x.connectors)$.push({...Y,channelId:x.id,channelName:x.name});return $}addConnector($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);if(X.connectors.some((z)=>z.name===x.name))throw Error(`connector "${x.name}" already exists in channel "${$}"`);let Q=this.fromInput(x);return this.assertNoTokenCollision(Y,Q),X.connectors.push(Q),this.store.write(Y),Q}fromInput($){let x=this.idGenerator.generate(),Y=this.clock.iso(),X=Y,Q=Y;switch($.type){case"slack":return{id:x,type:"slack",name:$.name,botToken:$.botToken,appToken:$.appToken,createdAt:X,updatedAt:Q};case"gh":return{id:x,type:"gh",name:$.name,...$.pollInterval!==void 0?{pollInterval:$.pollInterval}:{},createdAt:X,updatedAt:Q};case"discord":return{id:x,type:"discord",name:$.name,botToken:$.botToken,createdAt:X,updatedAt:Q};case"schedule":return{id:x,type:"schedule",name:$.name,entries:$.entries??[],createdAt:X,updatedAt:Q}}}removeConnector($,x){let Y=this.store.read(),X=this.requireChannel(Y,$),Q=X.connectors.findIndex((z)=>z.name===x);if(Q<0)throw Error(`connector "${x}" not found in channel "${$}"`);X.connectors.splice(Q,1),this.store.write(Y)}renameConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=Q.connectors.find((Z)=>Z.name===x);if(!z)throw Error(`connector "${x}" not found in channel "${$}"`);if(Q.connectors.some((Z)=>Z.name===Y))throw Error(`connector "${Y}" already exists in channel "${$}"`);z.name=Y,z.updatedAt=this.clock.iso(),this.store.write(X)}updateSlackConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"slack"),Z={...z,botToken:Y.botToken??z.botToken,appToken:Y.appToken??z.appToken,updatedAt:this.clock.iso()};this.assertNoTokenCollision(X,Z),Object.assign(z,Z),this.store.write(X)}updateGhConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"gh");if(Y.pollInterval!==void 0)z.pollInterval=Y.pollInterval;z.updatedAt=this.clock.iso(),this.store.write(X)}updateDiscordConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"discord"),Z={...z,botToken:Y.botToken??z.botToken,updatedAt:this.clock.iso()};this.assertNoTokenCollision(X,Z),Object.assign(z,Z),this.store.write(X)}listScheduleEntries($,x){let Y=this.requireChannel(this.store.read(),$);return YX(Y,x,"schedule").entries}addScheduleEntry($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"schedule"),Z={id:Y.id??this.idGenerator.generate(),cron:Y.cron,prompt:Y.prompt,enabled:Y.enabled??!0,catchupPolicy:Y.catchupPolicy??"latest"};return z.entries.push(Z),z.updatedAt=this.clock.iso(),this.store.write(X),Z}removeScheduleEntry($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"schedule"),Z=z.entries.findIndex((K)=>K.id===Y);if(Z<0)throw Error(`schedule entry "${Y}" not found`);z.entries.splice(Z,1),z.updatedAt=this.clock.iso(),this.store.write(X)}async call($,x,Y){let X=this.getConnector($,x);if(!X)throw Error(`connector "${x}" not found in channel "${$}"`);let Q=this.factory.createAdapter(X);if(!Q)throw Error(`connector type "${X.type}" does not support outbound calls`);return await Q.call(Y)}createListener($,x){let Y=this.get($);if(!Y)return null;let X=Y.connectors.find((Q)=>Q.name===x);if(!X)return null;return{config:X,channelId:Y.id,listener:this.factory.createListener(Y.id,X)}}createAllListeners(){let $=[];for(let x of this.list())for(let Y of x.connectors)$.push({config:Y,channelId:x.id,channelName:x.name,listener:this.factory.createListener(x.id,Y)});return $}requireChannel($,x){let Y=$.channels.find((X)=>X.name===x);if(!Y)throw Error(`channel "${x}" not found`);return Y}assertNoTokenCollision($,x){let Y=yC(x);if(Y.length===0)return;for(let X of $.channels)for(let Q of X.connectors){if(Q.id===x.id)continue;for(let z of yC(Q))if(Y.includes(z))throw Error(`token already in use by connector "${Q.name}" in channel "${X.name}"`)}}}import{join as zS1}from"path";var qr0=new L$,Dr0=new M8,Br0=new I6;class hC{channels;mcp;gateway;process;fs;logger;pidDir;constructor($){this.channels=$.channels,this.mcp=$.mcp,this.gateway=$.gateway,this.process=$.process??qr0,this.fs=$.fs??Dr0,this.logger=$.logger??Br0,this.pidDir=zS1($.dir??O8,"claude"),Object.freeze(this)}async launch($){let x=this.channels.get($.channel)??this.channels.getById($.channel);if(!x)throw Error(`channel "${$.channel}" not found`);if($.profileName&&this.isRunning($.profileName))throw Error(`profile "${$.profileName}" is already running`);let Y=$.cwd??globalThis.process.cwd();if(!this.mcp.findInstalledName(Y))this.mcp.install(Y),this.logger.info("added funnel MCP to .mcp.json",{cwd:Y});if(!this.gateway.isRunning())this.logger.info("starting gateway automatically"),await this.gateway.start();if($.profileName)this.writePidFile($.profileName),this.installCleanup($.profileName);let X=this.buildArgs(x.options,$.userArgs??[],Y),Q=this.buildEnv(x.id,x.env);this.logger.info("claude launch",{channel:$.channel,channelId:x.id,cwd:Y});try{return await this.process.attach(["claude",...X],{cwd:Y,env:Q,onSpawned:$.onSpawned})}finally{if($.profileName)this.removePidFile($.profileName)}}isRunning($){let x=this.readPid($);if(!x)return!1;return this.isProcessAlive(x)}pidPath($){return zS1(this.pidDir,`${$}.pid`)}readPid($){let x=this.pidPath($);if(!this.fs.existsSync(x))return null;try{let Y=this.fs.readFileSync(x).trim(),X=Number(Y);if(!X||X<=0)return null;return X}catch{return null}}writePidFile($){this.fs.mkdirSync(this.pidDir,{recursive:!0}),this.fs.writeFileSync(this.pidPath($),String(globalThis.process.pid))}removePidFile($){let x=this.pidPath($);if(this.fs.existsSync(x))this.fs.unlink(x)}installCleanup($){globalThis.process.once("exit",()=>this.removePidFile($))}isProcessAlive($){let x=this.process.runSync(["ps","-p",String($),"-o","state="]);if(x.exitCode!==0)return!1;let Y=x.stdout.trim();if(!Y)return!1;return!Y.startsWith("Z")}buildArgs($,x,Y){let X=[...$,...x],Q=this.mcp.findInstalledName(Y);if(Q&&!X.includes("--dangerously-load-development-channels")&&!X.includes("--channels"))X.push("--dangerously-load-development-channels",`server:${Q}`);return X}buildEnv($,x){let Y={};for(let[X,Q]of Object.entries(x))Y[X]=Q;for(let[X,Q]of Object.entries(globalThis.process.env))if(typeof Q==="string")Y[X]=Q;return Y.FUNNEL_CHANNEL_ID=$,Y}}var Fr0=384;class gC extends HK{dirs;files;mtimes;modes;now;constructor($={}){super();this.dirs=new Set($.dirs??[]),this.files=new Map(Object.entries($.files??{})),this.mtimes=new Map(Object.entries($.mtimes??{})),this.modes=new Map(Object.entries($.modes??{})),this.now=$.now??(()=>Date.now())}existsSync($){return this.dirs.has($)||this.files.has($)}readFileSync($){return this.files.get($)??""}writeFileSync($,x){this.files.set($,x),this.touch($)}writeSecretFileSync($,x){this.files.set($,x),this.modes.set($,Fr0),this.touch($)}appendFileSync($,x){let Y=this.files.get($)??"";this.files.set($,Y+x),this.touch($)}unlink($){this.files.delete($),this.mtimes.delete($),this.modes.delete($)}mkdirSync($,x){this.dirs.add($)}readdirSync($){let x=$.endsWith("/")?$:`${$}/`,Y=[];for(let X of this.files.keys()){if(!X.startsWith(x))continue;let Q=X.slice(x.length);if(!Q.includes("/"))Y.push(Q)}return Y}statSync($){let x=this.mtimes.get($);if(x===void 0)throw Error(`not found: ${$}`);return{mtimeMs:x,mode:this.modes.get($)??null}}setMtime($,x){this.mtimes.set($,x)}setMode($,x){this.modes.set($,x)}touch($){if(!this.mtimes.has($))this.mtimes.set($,this.now());else this.mtimes.set($,this.now())}}class pC extends CV{counter=0;prefix;constructor($={}){super();this.prefix=$.prefix??"id"}generate(){return this.counter++,`${this.prefix}-${this.counter}`}}import{join as Or0}from"path";var Lr0=m.object({botToken:m.string().optional(),appToken:m.string().optional()}).optional(),Nr0=m.object({type:m.literal("slack"),name:m.string(),botToken:m.string().optional(),appToken:m.string().optional(),env:Lr0}),Pr0=m.object({botToken:m.string().optional()}).optional(),jr0=m.object({type:m.literal("discord"),name:m.string(),botToken:m.string().optional(),env:Pr0}),Ir0=m.object({type:m.literal("gh"),name:m.string(),pollInterval:m.number().int().positive().optional()}),vr0=m.object({type:m.literal("schedule"),name:m.string()}),Mr0=m.discriminatedUnion("type",[Nr0,jr0,Ir0,vr0]),_r0=m.object({name:m.string(),options:m.array(m.string()).optional(),env:m.record(m.string(),m.string()).optional(),connectors:m.array(Mr0).optional()}),ZS1=m.object({$schema:m.string().optional(),channels:m.array(_r0).min(1)}),KF="funnel.json",KS1=".env.local";var br0=/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/,Rr0=($)=>{if($.length<2)return $;let x=$[0],Y=$[$.length-1];if(x==='"'&&Y==='"')return $.slice(1,-1);if(x==="'"&&Y==="'")return $.slice(1,-1);return $};class iC{fs;constructor($){this.fs=$.fs,Object.freeze(this)}read($){let x=Or0($,KS1);if(!this.fs.existsSync(x))return{};let Y=this.fs.readFileSync(x),X={};for(let Q of Y.split(`
546
- `)){let z=Q.trim();if(z===""||z.startsWith("#"))continue;let Z=z.match(br0);if(!Z)continue;let K=Z[1],W=Z[2];if(!K)continue;X[K]=Rr0(W??"")}return X}}import{join as Ar0}from"path";class lC{fs;constructor($){this.fs=$.fs,Object.freeze(this)}read($){let x=Ar0($,KF);if(!this.fs.existsSync(x))return null;let Y=this.fs.readFileSync(x),X=(()=>{try{return JSON.parse(Y)}catch(z){let Z=z instanceof Error?z.message:String(z);throw Error(`${KF} is not valid JSON: ${Z}`)}})(),Q=ZS1.safeParse(X);if(!Q.success)throw Error(`${KF} is invalid: ${Q.error.message}`);return Q.data}}var fr0=($,x)=>{if($.length!==x.length)return!1;for(let Y=0;Y<$.length;Y++)if($[Y]!==x[Y])return!1;return!0},kr0=($,x)=>{let Y=Object.keys($);if(Y.length!==Object.keys(x).length)return!1;for(let X of Y)if($[X]!==x[X])return!1;return!0};class nC{channels;dotenv;prompter;env;constructor($){this.channels=$.channels,this.dotenv=$.dotenv,this.prompter=$.prompter,this.env=$.env??process.env,Object.freeze(this)}async ensure($,x){let Y=this.channels.get($.name);if(!Y)this.channels.add({name:$.name,options:$.options??[],env:$.env??{}});else{let z=$.options??[],Z=$.env??{};if(!fr0(Y.options,z))this.channels.setOptions($.name,z);if(!kr0(Y.env,Z))this.channels.setEnv($.name,Z)}if($.connectors===void 0)return;let X=this.dotenv.read(x),Q=new Set;for(let z of $.connectors){let Z=await this.ensureConnector($.name,z,X);Q.add(Z)}this.removeExtras($.name,Q)}async ensureConnector($,x,Y){if(x.type==="slack")return await this.ensureSlack($,x,Y);if(x.type==="discord")return await this.ensureDiscord($,x,Y);if(x.type==="gh")return this.ensureGh($,x);return this.ensureSchedule($,x)}async ensureSlack($,x,Y){let X=this.findExistingSlack($,x.name),Q=await this.resolveField({literal:x.botToken,envVar:x.env?.botToken,dotenv:Y,label:`${x.name}.botToken`,existing:X?.botToken}),z=await this.resolveField({literal:x.appToken,envVar:x.env?.appToken,dotenv:Y,label:`${x.name}.appToken`,existing:X?.appToken});if(X){if(X.botToken!==Q||X.appToken!==z)this.channels.updateSlackConnector($,x.name,{botToken:Q,appToken:z});return X.id}let Z=this.findSlackByToken($,[Q,z]);if(Z){if(this.channels.renameConnector($,Z.name,x.name),Z.botToken!==Q||Z.appToken!==z)this.channels.updateSlackConnector($,x.name,{botToken:Q,appToken:z});return Z.id}return this.channels.addConnector($,{type:"slack",name:x.name,botToken:Q,appToken:z}).id}async ensureDiscord($,x,Y){let X=this.findExistingDiscord($,x.name),Q=await this.resolveField({literal:x.botToken,envVar:x.env?.botToken,dotenv:Y,label:`${x.name}.botToken`,existing:X?.botToken});if(X){if(X.botToken!==Q)this.channels.updateDiscordConnector($,x.name,{botToken:Q});return X.id}let z=this.findDiscordByToken($,Q);if(z){if(this.channels.renameConnector($,z.name,x.name),z.botToken!==Q)this.channels.updateDiscordConnector($,x.name,{botToken:Q});return z.id}return this.channels.addConnector($,{type:"discord",name:x.name,botToken:Q}).id}ensureGh($,x){let Y=this.channels.getConnector($,x.name);if(Y&&Y.type!=="gh")throw Error(`connector "${x.name}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "gh"`);if(Y&&Y.type==="gh"){if(x.pollInterval!==void 0&&Y.pollInterval!==x.pollInterval)this.channels.updateGhConnector($,x.name,{pollInterval:x.pollInterval});return Y.id}return this.channels.addConnector($,{type:"gh",name:x.name,...x.pollInterval!==void 0?{pollInterval:x.pollInterval}:{}}).id}ensureSchedule($,x){let Y=this.channels.getConnector($,x.name);if(Y&&Y.type!=="schedule")throw Error(`connector "${x.name}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "schedule"`);if(Y&&Y.type==="schedule")return Y.id;return this.channels.addConnector($,{type:"schedule",name:x.name}).id}findExistingSlack($,x){let Y=this.channels.getConnector($,x);if(!Y)return null;if(Y.type!=="slack")throw Error(`connector "${x}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "slack"`);return Y}findExistingDiscord($,x){let Y=this.channels.getConnector($,x);if(!Y)return null;if(Y.type!=="discord")throw Error(`connector "${x}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "discord"`);return Y}findSlackByToken($,x){let Y=this.channels.get($);if(!Y)return null;for(let X of Y.connectors){if(X.type!=="slack")continue;if(x.includes(X.botToken)||x.includes(X.appToken))return X}return null}findDiscordByToken($,x){let Y=this.channels.get($);if(!Y)return null;for(let X of Y.connectors){if(X.type!=="discord")continue;if(X.botToken===x)return X}return null}removeExtras($,x){let Y=this.channels.get($);if(!Y)return;let X=Y.connectors.filter((Q)=>!x.has(Q.id));for(let Q of X)this.channels.removeConnector($,Q.name)}async resolveField($){if($.literal!==void 0&&$.envVar!==void 0)throw Error(`${$.label} is set both as a literal and as env.${$.label.split(".").pop()}; pick one`);if($.literal!==void 0&&$.literal!=="")return $.literal;if($.envVar!==void 0&&$.envVar!==""){let x=this.env[$.envVar];if(x)return x;let Y=$.dotenv[$.envVar];if(Y)return Y;throw Error(`${$.label} references env var "${$.envVar}" but it is not set in process env or .env.local`)}if($.existing)return $.existing;return await this.prompter.promptSecret($.label)}}class dC extends NQ{file=null;entries=[];info($,x){this.entries.push({level:"info",message:$,meta:x})}warn($,x){this.entries.push({level:"warn",message:$,meta:x})}error($,x){this.entries.push({level:"error",message:$,meta:x})}clear(){this.entries.length=0}}import{join as WS1}from"path";var VS1="funnel",Tr0="funnel",cr0=m.object({command:m.string().optional(),args:m.array(m.string()).optional()}),Sr0=m.object({mcpServers:m.record(m.string(),cr0).optional()}),Er0=new M8;class aC{fs;constructor($={}){this.fs=$.fs??Er0,Object.freeze(this)}install($){if(!this.fs.existsSync($))throw Error(`repository does not exist: ${$}`);let x=this.readConfig($),Y=x.mcpServers??{},Q=this.findServerName(Y)??Tr0;Y[Q]={command:VS1,args:["mcp"]},this.writeConfig($,{...x,mcpServers:Y})}uninstall($){if(!this.fs.existsSync($))return;let x=this.readConfig($),Y=x.mcpServers??{},X=this.findServerName(Y);if(!X)return;let Q={...Y};delete Q[X],this.writeConfig($,{...x,mcpServers:Q})}findInstalledName($){let x=this.readConfig($);return this.findServerName(x.mcpServers??{})}findServerName($){for(let x of Object.entries($)){let Y=x[0];if(x[1]?.command===VS1)return Y}return null}readConfig($){let x=WS1($,".mcp.json");if(!this.fs.existsSync(x))return{};let Y=this.fs.readFileSync(x).trim();if(!Y)return{};let X;try{X=JSON.parse(Y)}catch(z){throw Error(`invalid .mcp.json (${x}): ${z instanceof Error?z.message:String(z)}`)}let Q=Sr0.safeParse(X);if(!Q.success)throw Error(`invalid .mcp.json (${x}): ${Q.error.message}`);return Q.data}writeConfig($,x){let Y=WS1($,".mcp.json");this.fs.writeFileSync(Y,`${JSON.stringify(x,null,2)}
545
+ `)}}var AD1=xL(RD1(),1);var uj0=($)=>{let x={};for(let[Y,X]of Object.entries($))x[Y]=X;return x};class kk extends fY{client;constructor($){super();this.client=$.client??new AD1.WebClient($.config.botToken),Object.freeze(this)}async call($){let x=$.body!==null&&typeof $.body==="object"?uj0($.body):{};return await this.client.apiCall($.path,x)}}var ZF=xL(XS1(),1);var zr0=new Set(["message","app_mention"]),Zr0=new Set([void 0,"thread_broadcast","bot_message","file_share"]);var J7=($,x)=>{let Y=$[x];return typeof Y==="string"?Y:void 0};class SC{ownBotUserId;ownBotId;now;dedup=new Map;constructor($){this.ownBotUserId=$.ownBotUserId,this.ownBotId=$.ownBotId,this.now=$.now??(()=>Date.now())}process($){let x=J7($,"type");if(!x||!zr0.has(x))return{skip:!0};let Y=J7($,"subtype");if(!Zr0.has(Y))return{skip:!0};let X=J7($,"channel")??"",Q=J7($,"event_ts")??J7($,"ts")??"",z=`${X}:${Q}`,Z=this.now();if(this.dedup.has(z))return{skip:!0};this.dedup.set(z,Z);for(let H of this.dedup.keys())if((this.dedup.get(H)??0)<Z-1e4)this.dedup.delete(H);let K=J7($,"user"),W=J7($,"bot_id");if(K===this.ownBotUserId)return{skip:!0};if(W===this.ownBotId)return{skip:!0};let w=(J7($,"text")??"").includes(`<@${this.ownBotUserId}>`),U=J7($,"thread_ts")??J7($,"ts")??"";return{skip:!1,content:JSON.stringify($),meta:{event_type:"slack",channel_id:X,user_id:K??"",mentioned:String(w),thread_ts:U},shouldReact:w,channel:X,timestamp:J7($,"ts")??""}}}var Kr0=m.object({event:m.record(m.string(),m.unknown()).optional()}),Wr0=new I6;class EC extends f2{config;logger;onAppCreated;preprocessEvent;app=null;constructor($){super();this.config=$.config,this.logger=$.logger??Wr0,this.onAppCreated=$.onAppCreated??null,this.preprocessEvent=$.preprocessEvent??null}async start($){let x=new ZF.App({token:this.config.botToken,appToken:this.config.appToken,socketMode:!0,logLevel:ZF.LogLevel.ERROR}),Y=await x.client.auth.test({token:this.config.botToken}),X=new SC({ownBotUserId:Y.user_id??"",ownBotId:Y.bot_id??""}),Q=this.preprocessEvent;if(x.use(async(z)=>{let Z=Kr0.safeParse(z);if(!Z.success||!Z.data.event)return;let K=Z.data.event,W=Q?Q(K):K;if(W===null)return;let V=X.process(W);if(V.skip)return;if(V.shouldReact)try{await x.client.reactions.add({token:this.config.botToken,channel:V.channel,timestamp:V.timestamp,name:"eyes"})}catch{}await $(V.content,V.meta)}),x.error(async(z)=>{this.logger.error("Slack error",{error:z instanceof Error?z.message:String(z)})}),this.onAppCreated)await this.onAppCreated(x);await x.start(),this.app=x}async stop(){if(!this.app)return;try{await this.app.stop()}catch($){this.logger.error("Slack stop error",{error:$ instanceof Error?$.message:String($)})}finally{this.app=null}}isAlive(){return this.app!==null}}import{join as CC}from"path";var Vr0=new M8,wr0=new L$,Ur0=new I6;class uC{fs;process;logger;dir;slackListenerOptions;scheduleListenerOptions;constructor($={}){this.fs=$.fs??Vr0,this.process=$.process??wr0,this.logger=$.logger??Ur0,this.dir=$.dir??O8,this.slackListenerOptions=$.slackListenerOptions??{},this.scheduleListenerOptions=$.scheduleListenerOptions??{},Object.freeze(this)}createListener($,x){if(x.type==="slack")return new EC({config:x,logger:this.logger,onAppCreated:this.slackListenerOptions.onAppCreated,preprocessEvent:this.slackListenerOptions.preprocessEvent});if(x.type==="gh")return new $f({config:x,process:this.process,logger:this.logger});if(x.type==="discord")return new rA({config:x,logger:this.logger});let Y=new Yf({path:CC(this.connectorDir($,x.id),"state.json"),fs:this.fs});return new xf({config:x,lastFiredStore:Y,logger:this.logger,onFired:this.scheduleListenerOptions.onFired})}createAdapter($){if($.type==="slack")return new kk({config:$});if($.type==="gh")return new eA({process:this.process});if($.type==="discord")return new QM({config:$});return null}connectorDir($,x){return CC(this.dir,"channels",$,"connectors",x)}channelDir($){return CC(this.dir,"channels",$)}}function yC($){switch($.type){case"slack":return[$.botToken,$.appToken];case"discord":return[$.botToken];case"gh":case"schedule":return[]}}function Hr0($,x){return $.type===x}function YX($,x,Y){let X=$.connectors.find((Q)=>Q.name===x);if(!X)throw Error(`connector "${x}" not found in channel "${$.name}"`);if(!Hr0(X,Y))throw Error(`connector "${x}" is type "${X.type}", not "${Y}"`);return X}class EV{millis(){return this.now().getTime()}iso(){return this.now().toISOString()}}class QX extends EV{now(){return new Date}}class CV{}class uV extends CV{generate(){return crypto.randomUUID()}}var Gr0=new QX,Jr0=new uV;class mC{store;factory;profileChecker;clock;idGenerator;constructor($){this.store=$.store,this.factory=$.factory,this.profileChecker=$.profileChecker,this.clock=$.clock??Gr0,this.idGenerator=$.idGenerator??Jr0,Object.freeze(this)}list(){return this.store.read().channels}get($){return this.list().find((x)=>x.name===$)??null}getById($){return this.list().find((x)=>x.id===$)??null}add($){let x=this.store.read();if(x.channels.some((X)=>X.name===$.name))throw Error(`channel "${$.name}" already exists`);let Y={id:this.idGenerator.generate(),name:$.name,delivery:$.delivery??"fanout",options:$.options??[],env:$.env??{},connectors:[]};return x.channels.push(Y),this.store.write(x),Y}setDelivery($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);X.delivery=x,this.store.write(Y)}setOptions($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);X.options=x,this.store.write(Y)}setEnv($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);X.env=x,this.store.write(Y)}remove($){let x=this.store.read(),Y=x.channels.findIndex((Q)=>Q.name===$);if(Y<0)throw Error(`channel "${$}" not found`);let X=x.channels[Y];if(X&&this.profileChecker.hasChannelRef(X.id))throw Error(`channel "${$}" is referenced by a profile`);x.channels.splice(Y,1),this.store.write(x)}rename($,x){let Y=this.store.read(),X=Y.channels.find((Q)=>Q.name===$);if(!X)throw Error(`channel "${$}" not found`);if(Y.channels.some((Q)=>Q.name===x))throw Error(`channel "${x}" already exists`);X.name=x,this.store.write(Y)}listConnectors($){return this.requireChannel(this.store.read(),$).connectors}getConnector($,x){let Y=this.get($);if(!Y)return null;return Y.connectors.find((X)=>X.name===x)??null}listAllConnectors(){let $=[];for(let x of this.list())for(let Y of x.connectors)$.push({...Y,channelId:x.id,channelName:x.name});return $}addConnector($,x){let Y=this.store.read(),X=this.requireChannel(Y,$);if(X.connectors.some((z)=>z.name===x.name))throw Error(`connector "${x.name}" already exists in channel "${$}"`);let Q=this.fromInput(x);return this.assertNoTokenCollision(Y,Q),X.connectors.push(Q),this.store.write(Y),Q}fromInput($){let x=this.idGenerator.generate(),Y=this.clock.iso(),X=Y,Q=Y;switch($.type){case"slack":return{id:x,type:"slack",name:$.name,botToken:$.botToken,appToken:$.appToken,createdAt:X,updatedAt:Q};case"gh":return{id:x,type:"gh",name:$.name,...$.pollInterval!==void 0?{pollInterval:$.pollInterval}:{},createdAt:X,updatedAt:Q};case"discord":return{id:x,type:"discord",name:$.name,botToken:$.botToken,createdAt:X,updatedAt:Q};case"schedule":return{id:x,type:"schedule",name:$.name,entries:$.entries??[],createdAt:X,updatedAt:Q}}}removeConnector($,x){let Y=this.store.read(),X=this.requireChannel(Y,$),Q=X.connectors.findIndex((z)=>z.name===x);if(Q<0)throw Error(`connector "${x}" not found in channel "${$}"`);X.connectors.splice(Q,1),this.store.write(Y)}renameConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=Q.connectors.find((Z)=>Z.name===x);if(!z)throw Error(`connector "${x}" not found in channel "${$}"`);if(Q.connectors.some((Z)=>Z.name===Y))throw Error(`connector "${Y}" already exists in channel "${$}"`);z.name=Y,z.updatedAt=this.clock.iso(),this.store.write(X)}updateSlackConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"slack"),Z={...z,botToken:Y.botToken??z.botToken,appToken:Y.appToken??z.appToken,updatedAt:this.clock.iso()};this.assertNoTokenCollision(X,Z),Object.assign(z,Z),this.store.write(X)}updateGhConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"gh");if(Y.pollInterval!==void 0)z.pollInterval=Y.pollInterval;z.updatedAt=this.clock.iso(),this.store.write(X)}updateDiscordConnector($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"discord"),Z={...z,botToken:Y.botToken??z.botToken,updatedAt:this.clock.iso()};this.assertNoTokenCollision(X,Z),Object.assign(z,Z),this.store.write(X)}listScheduleEntries($,x){let Y=this.requireChannel(this.store.read(),$);return YX(Y,x,"schedule").entries}addScheduleEntry($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"schedule"),Z={id:Y.id??this.idGenerator.generate(),cron:Y.cron,prompt:Y.prompt,enabled:Y.enabled??!0,catchupPolicy:Y.catchupPolicy??"latest"};return z.entries.push(Z),z.updatedAt=this.clock.iso(),this.store.write(X),Z}removeScheduleEntry($,x,Y){let X=this.store.read(),Q=this.requireChannel(X,$),z=YX(Q,x,"schedule"),Z=z.entries.findIndex((K)=>K.id===Y);if(Z<0)throw Error(`schedule entry "${Y}" not found`);z.entries.splice(Z,1),z.updatedAt=this.clock.iso(),this.store.write(X)}async call($,x,Y){let X=this.getConnector($,x);if(!X)throw Error(`connector "${x}" not found in channel "${$}"`);let Q=this.factory.createAdapter(X);if(!Q)throw Error(`connector type "${X.type}" does not support outbound calls`);return await Q.call(Y)}createListener($,x){let Y=this.get($);if(!Y)return null;let X=Y.connectors.find((Q)=>Q.name===x);if(!X)return null;return{config:X,channelId:Y.id,listener:this.factory.createListener(Y.id,X)}}createAllListeners(){let $=[];for(let x of this.list())for(let Y of x.connectors)$.push({config:Y,channelId:x.id,channelName:x.name,listener:this.factory.createListener(x.id,Y)});return $}requireChannel($,x){let Y=$.channels.find((X)=>X.name===x);if(!Y)throw Error(`channel "${x}" not found`);return Y}assertNoTokenCollision($,x){let Y=yC(x);if(Y.length===0)return;for(let X of $.channels)for(let Q of X.connectors){if(Q.id===x.id)continue;for(let z of yC(Q))if(Y.includes(z))throw Error(`token already in use by connector "${Q.name}" in channel "${X.name}"`)}}}import{join as zS1}from"path";var qr0=new L$,Dr0=new M8,Br0=new I6;class hC{channels;mcp;gateway;process;fs;logger;pidDir;constructor($){this.channels=$.channels,this.mcp=$.mcp,this.gateway=$.gateway,this.process=$.process??qr0,this.fs=$.fs??Dr0,this.logger=$.logger??Br0,this.pidDir=zS1($.dir??O8,"claude"),Object.freeze(this)}async launch($){let x=this.channels.get($.channel)??this.channels.getById($.channel);if(!x)throw Error(`channel "${$.channel}" not found`);if($.profileName&&this.isRunning($.profileName))throw Error(`profile "${$.profileName}" is already running`);let Y=$.cwd??globalThis.process.cwd();if(($.installMcp??!0)&&!this.mcp.findInstalledName(Y))this.mcp.install(Y),this.logger.info("added funnel MCP to .mcp.json",{cwd:Y});if(!this.gateway.isRunning())this.logger.info("starting gateway automatically"),await this.gateway.start();if($.profileName)this.writePidFile($.profileName),this.installCleanup($.profileName);let Q=this.buildArgs(x.options,$.userArgs??[],Y),z=this.buildEnv(x.id,x.env);this.logger.info("claude launch",{channel:$.channel,channelId:x.id,cwd:Y});try{return await this.process.attach(["claude",...Q],{cwd:Y,env:z,onSpawned:$.onSpawned})}finally{if($.profileName)this.removePidFile($.profileName)}}isRunning($){let x=this.readPid($);if(!x)return!1;return this.isProcessAlive(x)}pidPath($){return zS1(this.pidDir,`${$}.pid`)}readPid($){let x=this.pidPath($);if(!this.fs.existsSync(x))return null;try{let Y=this.fs.readFileSync(x).trim(),X=Number(Y);if(!X||X<=0)return null;return X}catch{return null}}writePidFile($){this.fs.mkdirSync(this.pidDir,{recursive:!0}),this.fs.writeFileSync(this.pidPath($),String(globalThis.process.pid))}removePidFile($){let x=this.pidPath($);if(this.fs.existsSync(x))this.fs.unlink(x)}installCleanup($){globalThis.process.once("exit",()=>this.removePidFile($))}isProcessAlive($){let x=this.process.runSync(["ps","-p",String($),"-o","state="]);if(x.exitCode!==0)return!1;let Y=x.stdout.trim();if(!Y)return!1;return!Y.startsWith("Z")}buildArgs($,x,Y){let X=[...$,...x],Q=this.mcp.findInstalledName(Y);if(Q&&!X.includes("--dangerously-load-development-channels")&&!X.includes("--channels"))X.push("--dangerously-load-development-channels",`server:${Q}`);return X}buildEnv($,x){let Y={};for(let[X,Q]of Object.entries(x))Y[X]=Q;for(let[X,Q]of Object.entries(globalThis.process.env))if(typeof Q==="string")Y[X]=Q;return Y.FUNNEL_CHANNEL_ID=$,Y}}var Fr0=384;class gC extends HK{dirs;files;mtimes;modes;now;constructor($={}){super();this.dirs=new Set($.dirs??[]),this.files=new Map(Object.entries($.files??{})),this.mtimes=new Map(Object.entries($.mtimes??{})),this.modes=new Map(Object.entries($.modes??{})),this.now=$.now??(()=>Date.now())}existsSync($){return this.dirs.has($)||this.files.has($)}readFileSync($){return this.files.get($)??""}writeFileSync($,x){this.files.set($,x),this.touch($)}writeSecretFileSync($,x){this.files.set($,x),this.modes.set($,Fr0),this.touch($)}appendFileSync($,x){let Y=this.files.get($)??"";this.files.set($,Y+x),this.touch($)}unlink($){this.files.delete($),this.mtimes.delete($),this.modes.delete($)}mkdirSync($,x){this.dirs.add($)}readdirSync($){let x=$.endsWith("/")?$:`${$}/`,Y=[];for(let X of this.files.keys()){if(!X.startsWith(x))continue;let Q=X.slice(x.length);if(!Q.includes("/"))Y.push(Q)}return Y}statSync($){let x=this.mtimes.get($);if(x===void 0)throw Error(`not found: ${$}`);return{mtimeMs:x,mode:this.modes.get($)??null}}setMtime($,x){this.mtimes.set($,x)}setMode($,x){this.modes.set($,x)}touch($){if(!this.mtimes.has($))this.mtimes.set($,this.now());else this.mtimes.set($,this.now())}}class pC extends CV{counter=0;prefix;constructor($={}){super();this.prefix=$.prefix??"id"}generate(){return this.counter++,`${this.prefix}-${this.counter}`}}import{join as Or0}from"path";var Lr0=m.object({botToken:m.string().optional(),appToken:m.string().optional()}).optional(),Nr0=m.object({type:m.literal("slack"),name:m.string(),botToken:m.string().optional(),appToken:m.string().optional(),env:Lr0}),Pr0=m.object({botToken:m.string().optional()}).optional(),jr0=m.object({type:m.literal("discord"),name:m.string(),botToken:m.string().optional(),env:Pr0}),Ir0=m.object({type:m.literal("gh"),name:m.string(),pollInterval:m.number().int().positive().optional()}),vr0=m.object({type:m.literal("schedule"),name:m.string()}),Mr0=m.discriminatedUnion("type",[Nr0,jr0,Ir0,vr0]),_r0=m.object({name:m.string(),options:m.array(m.string()).optional(),env:m.record(m.string(),m.string()).optional(),connectors:m.array(Mr0).optional()}),ZS1=m.object({$schema:m.string().optional(),channels:m.array(_r0).min(1)}),KF="funnel.json",KS1=".env.local";var br0=/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/,Rr0=($)=>{if($.length<2)return $;let x=$[0],Y=$[$.length-1];if(x==='"'&&Y==='"')return $.slice(1,-1);if(x==="'"&&Y==="'")return $.slice(1,-1);return $};class iC{fs;constructor($){this.fs=$.fs,Object.freeze(this)}read($){let x=Or0($,KS1);if(!this.fs.existsSync(x))return{};let Y=this.fs.readFileSync(x),X={};for(let Q of Y.split(`
546
+ `)){let z=Q.trim();if(z===""||z.startsWith("#"))continue;let Z=z.match(br0);if(!Z)continue;let K=Z[1],W=Z[2];if(!K)continue;X[K]=Rr0(W??"")}return X}}import{join as Ar0}from"path";class lC{fs;constructor($){this.fs=$.fs,Object.freeze(this)}read($){let x=Ar0($,KF);if(!this.fs.existsSync(x))return null;let Y=this.fs.readFileSync(x),X=(()=>{try{return JSON.parse(Y)}catch(z){let Z=z instanceof Error?z.message:String(z);throw Error(`${KF} is not valid JSON: ${Z}`)}})(),Q=ZS1.safeParse(X);if(!Q.success)throw Error(`${KF} is invalid: ${Q.error.message}`);return Q.data}}var fr0=($,x)=>{if($.length!==x.length)return!1;for(let Y=0;Y<$.length;Y++)if($[Y]!==x[Y])return!1;return!0},kr0=($,x)=>{let Y=Object.keys($);if(Y.length!==Object.keys(x).length)return!1;for(let X of Y)if($[X]!==x[X])return!1;return!0};class nC{channels;dotenv;prompter;env;constructor($){this.channels=$.channels,this.dotenv=$.dotenv,this.prompter=$.prompter,this.env=$.env??process.env,Object.freeze(this)}async ensure($,x){let Y=this.channels.get($.name);if(!Y)this.channels.add({name:$.name,options:$.options??[],env:$.env??{}});else{let K=$.options??[],W=$.env??{};if(!fr0(Y.options,K))this.channels.setOptions($.name,K);if(!kr0(Y.env,W))this.channels.setEnv($.name,W)}if($.connectors===void 0)return{touched:[],removed:[]};let X=this.dotenv.read(x),Q=[],z=new Set;for(let K of $.connectors){let W=await this.ensureConnector($.name,K,X);Q.push({name:W.name,changed:W.changed}),z.add(W.id)}let Z=this.removeExtras($.name,z);return{touched:Q,removed:Z}}async ensureConnector($,x,Y){if(x.type==="slack")return await this.ensureSlack($,x,Y);if(x.type==="discord")return await this.ensureDiscord($,x,Y);if(x.type==="gh")return this.ensureGh($,x);return this.ensureSchedule($,x)}async ensureSlack($,x,Y){let X=this.findExistingSlack($,x.name),Q=await this.resolveField({literal:x.botToken,envVar:x.env?.botToken,dotenv:Y,label:`${x.name}.botToken`,existing:X?.botToken}),z=await this.resolveField({literal:x.appToken,envVar:x.env?.appToken,dotenv:Y,label:`${x.name}.appToken`,existing:X?.appToken});if(X){if(X.botToken!==Q||X.appToken!==z)return this.channels.updateSlackConnector($,x.name,{botToken:Q,appToken:z}),{id:X.id,name:x.name,changed:!0};return{id:X.id,name:x.name,changed:!1}}let Z=this.findSlackByToken($,[Q,z]);if(Z){if(this.channels.renameConnector($,Z.name,x.name),Z.botToken!==Q||Z.appToken!==z)this.channels.updateSlackConnector($,x.name,{botToken:Q,appToken:z});return{id:Z.id,name:x.name,changed:!0}}return{id:this.channels.addConnector($,{type:"slack",name:x.name,botToken:Q,appToken:z}).id,name:x.name,changed:!0}}async ensureDiscord($,x,Y){let X=this.findExistingDiscord($,x.name),Q=await this.resolveField({literal:x.botToken,envVar:x.env?.botToken,dotenv:Y,label:`${x.name}.botToken`,existing:X?.botToken});if(X){if(X.botToken!==Q)return this.channels.updateDiscordConnector($,x.name,{botToken:Q}),{id:X.id,name:x.name,changed:!0};return{id:X.id,name:x.name,changed:!1}}let z=this.findDiscordByToken($,Q);if(z){if(this.channels.renameConnector($,z.name,x.name),z.botToken!==Q)this.channels.updateDiscordConnector($,x.name,{botToken:Q});return{id:z.id,name:x.name,changed:!0}}return{id:this.channels.addConnector($,{type:"discord",name:x.name,botToken:Q}).id,name:x.name,changed:!0}}ensureGh($,x){let Y=this.channels.getConnector($,x.name);if(Y&&Y.type!=="gh")throw Error(`connector "${x.name}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "gh"`);if(Y&&Y.type==="gh"){if(x.pollInterval!==void 0&&Y.pollInterval!==x.pollInterval)return this.channels.updateGhConnector($,x.name,{pollInterval:x.pollInterval}),{id:Y.id,name:x.name,changed:!0};return{id:Y.id,name:x.name,changed:!1}}return{id:this.channels.addConnector($,{type:"gh",name:x.name,...x.pollInterval!==void 0?{pollInterval:x.pollInterval}:{}}).id,name:x.name,changed:!0}}ensureSchedule($,x){let Y=this.channels.getConnector($,x.name);if(Y&&Y.type!=="schedule")throw Error(`connector "${x.name}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "schedule"`);if(Y&&Y.type==="schedule")return{id:Y.id,name:x.name,changed:!1};return{id:this.channels.addConnector($,{type:"schedule",name:x.name}).id,name:x.name,changed:!0}}findExistingSlack($,x){let Y=this.channels.getConnector($,x);if(!Y)return null;if(Y.type!=="slack")throw Error(`connector "${x}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "slack"`);return Y}findExistingDiscord($,x){let Y=this.channels.getConnector($,x);if(!Y)return null;if(Y.type!=="discord")throw Error(`connector "${x}" exists in channel "${$}" with type "${Y.type}", funnel.json declares "discord"`);return Y}findSlackByToken($,x){let Y=this.channels.get($);if(!Y)return null;for(let X of Y.connectors){if(X.type!=="slack")continue;if(x.includes(X.botToken)||x.includes(X.appToken))return X}return null}findDiscordByToken($,x){let Y=this.channels.get($);if(!Y)return null;for(let X of Y.connectors){if(X.type!=="discord")continue;if(X.botToken===x)return X}return null}removeExtras($,x){let Y=this.channels.get($);if(!Y)return[];let X=Y.connectors.filter((Q)=>!x.has(Q.id));for(let Q of X)this.channels.removeConnector($,Q.name);return X.map((Q)=>Q.name)}async resolveField($){if($.literal!==void 0&&$.envVar!==void 0)throw Error(`${$.label} is set both as a literal and as env.${$.label.split(".").pop()}; pick one`);if($.literal!==void 0&&$.literal!=="")return $.literal;if($.envVar!==void 0&&$.envVar!==""){let x=this.env[$.envVar];if(x)return x;let Y=$.dotenv[$.envVar];if(Y)return Y;throw Error(`${$.label} references env var "${$.envVar}" but it is not set in process env or .env.local`)}if($.existing)return $.existing;return await this.prompter.promptSecret($.label)}}class dC extends NQ{file=null;entries=[];info($,x){this.entries.push({level:"info",message:$,meta:x})}warn($,x){this.entries.push({level:"warn",message:$,meta:x})}error($,x){this.entries.push({level:"error",message:$,meta:x})}clear(){this.entries.length=0}}import{join as WS1}from"path";var VS1="funnel",Tr0="funnel",cr0=m.object({command:m.string().optional(),args:m.array(m.string()).optional()}),Sr0=m.object({mcpServers:m.record(m.string(),cr0).optional()}),Er0=new M8;class aC{fs;constructor($={}){this.fs=$.fs??Er0,Object.freeze(this)}install($){if(!this.fs.existsSync($))throw Error(`repository does not exist: ${$}`);let x=this.readConfig($),Y=x.mcpServers??{},Q=this.findServerName(Y)??Tr0;Y[Q]={command:VS1,args:["mcp"]},this.writeConfig($,{...x,mcpServers:Y})}uninstall($){if(!this.fs.existsSync($))return;let x=this.readConfig($),Y=x.mcpServers??{},X=this.findServerName(Y);if(!X)return;let Q={...Y};delete Q[X],this.writeConfig($,{...x,mcpServers:Q})}findInstalledName($){let x=this.readConfig($);return this.findServerName(x.mcpServers??{})}findServerName($){for(let x of Object.entries($)){let Y=x[0];if(x[1]?.command===VS1)return Y}return null}readConfig($){let x=WS1($,".mcp.json");if(!this.fs.existsSync(x))return{};let Y=this.fs.readFileSync(x).trim();if(!Y)return{};let X;try{X=JSON.parse(Y)}catch(z){throw Error(`invalid .mcp.json (${x}): ${z instanceof Error?z.message:String(z)}`)}let Q=Sr0.safeParse(X);if(!Q.success)throw Error(`invalid .mcp.json (${x}): ${Q.error.message}`);return Q.data}writeConfig($,x){let Y=WS1($,".mcp.json");this.fs.writeFileSync(Y,`${JSON.stringify(x,null,2)}
547
547
  `)}}var wS1={exitCode:0,stdout:"",stderr:""};class sC extends cW{calls=[];killed=[];handler=()=>wS1;syncHandler=()=>wS1;on($){return this.handler=$,this}onSync($){return this.syncHandler=$,this}async run($,x={}){this.calls.push({kind:"run",command:$,options:x});let Y=await this.handler($);return{exitCode:Y.exitCode??0,stdout:Y.stdout??"",stderr:Y.stderr??""}}runSync($){this.calls.push({kind:"runSync",command:$});let x=this.syncHandler($);return{exitCode:x.exitCode??0,stdout:x.stdout??"",stderr:x.stderr??""}}async attach($,x={}){if(this.calls.push({kind:"attach",command:$,options:x}),x.onSpawned)x.onSpawned(1);return(await this.handler($)).exitCode??0}detach($,x={}){this.calls.push({kind:"detach",command:$,options:x})}kill($,x="SIGTERM"){this.calls.push({kind:"kill",command:[String($),x]}),this.killed.push({pid:$,signal:x})}}class oC{store;constructor($){this.store=$.store,Object.freeze(this)}list(){return this.store.read().profiles}get($){return this.list().find((x)=>x.name===$)??null}getDefault(){return this.list()[0]??null}add($){let x=this.store.read();if(x.profiles.some((Y)=>Y.name===$.name))throw Error(`profile "${$.name}" already exists`);if(!x.channels.some((Y)=>Y.id===$.channelId))throw Error(`channel id "${$.channelId}" not found`);x.profiles.push($),this.store.write(x)}remove($){let x=this.store.read(),Y=x.profiles.findIndex((X)=>X.name===$);if(Y<0)throw Error(`profile "${$}" not found`);x.profiles.splice(Y,1),this.store.write(x)}rename($,x){let Y=this.store.read(),X=Y.profiles.find((Q)=>Q.name===$);if(!X)throw Error(`profile "${$}" not found`);if(Y.profiles.some((Q)=>Q.name===x))throw Error(`profile "${x}" already exists`);X.name=x,this.store.write(Y)}asDefault($){let x=this.store.read(),Y=x.profiles.findIndex((Q)=>Q.name===$);if(Y<0)throw Error(`profile "${$}" not found`);if(Y===0)return;let[X]=x.profiles.splice(Y,1);if(!X)return;x.profiles.unshift(X),this.store.write(x)}hasChannelRef($){return this.store.read().profiles.some((x)=>x.channelId===$)}update($,x){let Y=this.store.read(),X=Y.profiles.find((Q)=>Q.name===$);if(!X)throw Error(`profile "${$}" not found`);if(x.channelId!==void 0){if(!Y.channels.some((Q)=>Q.id===x.channelId))throw Error(`channel id "${x.channelId}" not found`);X.channelId=x.channelId}if(x.path!==void 0)X.path=x.path;this.store.write(Y)}}import{stderr as WF,stdin as e7}from"process";class rC{}var Cr0="*",ur0="\r",US1=`
548
548
  `,yr0=String.fromCharCode(8),mr0=String.fromCharCode(127),hr0=String.fromCharCode(3),gr0=String.fromCharCode(4);class tC extends rC{async promptSecret($){if(!e7.isTTY)throw Error(`cannot prompt for "${$}": stdin is not a TTY. Set the matching env var or run \`fnl channels <ch> connectors add ...\` first.`);WF.write(`${$}: `);let x=e7.isRaw;e7.setRawMode(!0),e7.resume();try{return await this.readSecret()}finally{e7.setRawMode(x),e7.pause(),WF.write(US1)}}readSecret(){return new Promise(($,x)=>{let Y="",X=(Q)=>{for(let z of Q){let Z=String.fromCharCode(z);if(Z===US1||Z===ur0){e7.off("data",X),$(Y);return}if(Z===hr0){e7.off("data",X),x(Error("prompt cancelled"));return}if(Z===gr0){if(e7.off("data",X),Y.length===0)x(Error("prompt cancelled"));else $(Y);return}if(Z===yr0||Z===mr0){if(Y.length>0)Y=Y.slice(0,-1),WF.write("\b \b");continue}Y+=Z,WF.write(Cr0)}};e7.on("data",X)})}}var pr0=($={})=>({version:G2,channels:[],profiles:[],...$});class eC extends GK{state;constructor($){super();this.state=pr0($)}read(){return this.state}write($){this.state=$}}class $u extends EV{current;constructor($={}){super();this.current=$.start??new Date(0)}now(){return new Date(this.current.getTime())}set($){this.current=$}advance($){this.current=new Date(this.current.getTime()+$)}}var HS1=m.object({content:m.string().min(1),meta:m.record(m.string(),m.string()).optional(),connector:m.string().min(1).optional()}),GS1=m.object({ok:m.literal(!0),offset:m.number().int().nonnegative()});var ir0={state:"offline"};class xu{port;isDaemonRunning;getToken;constructor($){this.port=$.port,this.isDaemonRunning=$.isDaemonRunning,this.getToken=$.getToken??(()=>null),Object.freeze(this)}async publish($,x){if(!this.isDaemonRunning())return ir0;try{let Y=`http://localhost:${this.port}/channels/${encodeURIComponent($)}/publish`,X=await fetch(Y,{method:"POST",headers:{...this.authHeaders(),"content-type":"application/json"},body:JSON.stringify(x)});if(!X.ok)return{state:"error",reason:await X.text()||`HTTP ${X.status}`};let Q=GS1.safeParse(await X.json());if(!Q.success)return{state:"error",reason:"malformed daemon response"};return{state:"ok",offset:Q.data.offset}}catch(Y){return{state:"error",reason:Y instanceof Error?Y.message:String(Y)}}}authHeaders(){let $=this.getToken();return $?{authorization:`Bearer ${$}`}:{}}}import{join as Qu}from"path";import{existsSync as lr0}from"fs";import{dirname as nr0,resolve as Yu}from"path";import{fileURLToPath as dr0}from"url";var JS1=()=>{let $=nr0(dr0(import.meta.url)),x=[Yu($,"./daemon.ts"),Yu($,"./daemon.js"),Yu($,"./gateway/daemon.js")];for(let Y of x)if(lr0(Y))return Y;throw Error(`daemon script not found (looked in ${x.join(", ")})`)};var ar0=9742,sr0="/tmp/funnel",or0=5000,rr0=2000,qS1=100,tr0=200,er0=new L$,$t0=new M8,xt0=new QX,Yt0=($)=>new Promise((x)=>{setTimeout(x,$)});class Xu{process;fs;clock;dir;pidFile;logDir;gatewayLog;tmpDir;port;sleep;constructor($={}){this.process=$.process??er0,this.fs=$.fs??$t0,this.clock=$.clock??xt0,this.dir=$.dir??O8,this.tmpDir=$.tmpDir??sr0,this.pidFile=Qu(this.dir,"gateway.pid"),this.logDir=Qu(this.tmpDir,"events"),this.gatewayLog=Qu(this.tmpDir,"gateway.log"),this.port=$.port??ar0,this.sleep=$.sleep??Yt0,Object.freeze(this)}isRunning(){let $=this.readPid();if(!$)return!1;return this.isProcessAlive($)}getStatus(){let $=this.readPid(),x=$!==null&&this.isProcessAlive($);return{running:x,pid:x?$:null,port:this.port}}async start($={}){if(this.isRunning())return!0;this.fs.mkdirSync(this.tmpDir,{recursive:!0});let x=JS1(),Y=this.buildStartCommand(x,$);this.process.detach(["bash","-c",Y]);let X=this.clock.millis()+or0;while(this.clock.millis()<X){if(this.isRunning())return!0;await this.sleep(qS1)}return this.isRunning()}buildStartCommand($,x={}){let X=x.caffeinate!==!1&&globalThis.process.platform==="darwin"?"caffeinate -i ":"",Q=`funnel-gateway[${this.dir}]`;return`nohup ${X}bun ${$} ${Q} >> ${this.gatewayLog} 2>&1 &`}async stop(){let $=this.readPid();if(!$)return!0;if(!this.isProcessAlive($))return this.removePid(),!0;try{this.process.kill($,"SIGTERM")}catch{return!1}let x=this.clock.millis()+rr0;while(this.clock.millis()<x){if(!this.isProcessAlive($))return this.removePid(),!0;await this.sleep(qS1)}try{this.process.kill($,"SIGKILL")}catch{}return await this.sleep(tr0),this.removePid(),!this.isProcessAlive($)}async restart($={}){let x=this.isRunning();if($.onlyIfRunning&&!x)return{ok:!0,wasRunning:!1,stopped:!1,started:!1};let Y=x?await this.stop():!0;if(!Y)return{ok:!1,wasRunning:x,stopped:!1,started:!1};let X=await this.start({caffeinate:$.caffeinate});return{ok:X,wasRunning:x,stopped:Y,started:X}}getLogDir(){return this.logDir}getGatewayLog(){return this.gatewayLog}getPort(){return this.port}readPid(){if(!this.fs.existsSync(this.pidFile))return null;try{let $=this.fs.readFileSync(this.pidFile).trim(),x=Number($);if(!x||x<=0)return null;return x}catch{return null}}removePid(){this.fs.unlink(this.pidFile)}isProcessAlive($){let x=this.process.runSync(["ps","-p",String($),"-o","state="]);if(x.exitCode!==0)return!1;let Y=x.stdout.trim();if(!Y)return!1;return!Y.startsWith("Z")}}import{existsSync as dt0,mkdirSync as at0}from"fs";import{join as xE1}from"path";import{timingSafeEqual as Qt0}from"crypto";var VF=($)=>{return async(x,Y)=>{let z=(x.req.header("authorization")??"").match(/^Bearer\s+(.+)$/i)?.[1]??"";if(!wF(z,$.expected))return x.text("unauthorized",401);return await Y()}},wF=($,x)=>{let Y=Buffer.from($,"utf-8"),X=Buffer.from(x,"utf-8"),Q=Math.max(Y.length,X.length,1),z=Buffer.alloc(Q),Z=Buffer.alloc(Q);return Y.copy(z),X.copy(Z),Qt0(z,Z)&&Y.length===X.length};var zu=($,x,Y)=>{return(X,Q)=>{let z=-1;return Z(0);async function Z(K){if(K<=z)throw Error("next() called multiple times");z=K;let W,V=!1,w;if($[K])w=$[K][0][0],X.req.routeIndex=K;else w=K===$.length&&Q||void 0;if(w)try{W=await w(X,()=>Z(K+1))}catch(U){if(U instanceof Error&&x)X.error=U,W=await x(U,X),V=!0;else throw U}else if(X.finalized===!1&&Y)W=await Y(X);if(W&&(X.finalized===!1||V))X.res=W;return X}}};var QK=class extends Error{res;status;constructor($=500,x){super(x?.message,{cause:x?.cause});this.res=x?.res,this.status=$}getResponse(){if(this.res)return new Response(this.res.body,{status:this.status,headers:this.res.headers});return new Response(this.message,{status:this.status})}};var DS1=Symbol();var BS1=async($,x=Object.create(null))=>{let{all:Y=!1,dot:X=!1}=x,z=($ instanceof UF?$.raw.headers:$.headers).get("Content-Type");if(z?.startsWith("multipart/form-data")||z?.startsWith("application/x-www-form-urlencoded"))return Xt0($,{all:Y,dot:X});return{}};async function Xt0($,x){let Y=await $.formData();if(Y)return zt0(Y,x);return{}}function zt0($,x){let Y=Object.create(null);if($.forEach((X,Q)=>{if(!(x.all||Q.endsWith("[]")))Y[Q]=X;else Zt0(Y,Q,X)}),x.dot)Object.entries(Y).forEach(([X,Q])=>{if(X.includes("."))Kt0(Y,X,Q),delete Y[X]});return Y}var Zt0=($,x,Y)=>{if($[x]!==void 0)if(Array.isArray($[x]))$[x].push(Y);else $[x]=[$[x],Y];else if(!x.endsWith("[]"))$[x]=Y;else $[x]=[Y]},Kt0=($,x,Y)=>{if(/(?:^|\.)__proto__\./.test(x))return;let X=$,Q=x.split(".");Q.forEach((z,Z)=>{if(Z===Q.length-1)X[z]=Y;else{if(!X[z]||typeof X[z]!=="object"||Array.isArray(X[z])||X[z]instanceof File)X[z]=Object.create(null);X=X[z]}})};var Ku=($)=>{let x=$.split("/");if(x[0]==="")x.shift();return x},FS1=($)=>{let{groups:x,path:Y}=Wt0($),X=Ku(Y);return Vt0(X,x)},Wt0=($)=>{let x=[];return $=$.replace(/\{[^}]+\}/g,(Y,X)=>{let Q=`@${X}`;return x.push([Q,Y]),Q}),{groups:x,path:$}},Vt0=($,x)=>{for(let Y=x.length-1;Y>=0;Y--){let[X]=x[Y];for(let Q=$.length-1;Q>=0;Q--)if($[Q].includes(X)){$[Q]=$[Q].replace(X,x[Y][1]);break}}return $},HF={},LS1=($,x)=>{if($==="*")return"*";let Y=$.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);if(Y){let X=`${$}#${x}`;if(!HF[X])if(Y[2])HF[X]=x&&x[0]!==":"&&x[0]!=="*"?[X,Y[1],new RegExp(`^${Y[2]}(?=/${x})`)]:[$,Y[1],new RegExp(`^${Y[2]}$`)];else HF[X]=[$,Y[1],!0];return HF[X]}return null},XK=($,x)=>{try{return x($)}catch{return $.replace(/(?:%[0-9A-Fa-f]{2})+/g,(Y)=>{try{return x(Y)}catch{return Y}})}},wt0=($)=>XK($,decodeURI),Wu=($)=>{let x=$.url,Y=x.indexOf("/",x.indexOf(":")+4),X=Y;for(;X<x.length;X++){let Q=x.charCodeAt(X);if(Q===37){let z=x.indexOf("?",X),Z=x.indexOf("#",X),K=z===-1?Z===-1?void 0:Z:Z===-1?z:Math.min(z,Z),W=x.slice(Y,K);return wt0(W.includes("%25")?W.replace(/%25/g,"%2525"):W)}else if(Q===63||Q===35)break}return x.slice(Y,X)};var NS1=($)=>{let x=Wu($);return x.length>1&&x.at(-1)==="/"?x.slice(0,-1):x},XX=($,x,...Y)=>{if(Y.length)x=XX(x,...Y);return`${$?.[0]==="/"?"":"/"}${$}${x==="/"?"":`${$?.at(-1)==="/"?"":"/"}${x?.[0]==="/"?x.slice(1):x}`}`},GF=($)=>{if($.charCodeAt($.length-1)!==63||!$.includes(":"))return null;let x=$.split("/"),Y=[],X="";return x.forEach((Q)=>{if(Q!==""&&!/\:/.test(Q))X+="/"+Q;else if(/\:/.test(Q))if(/\?/.test(Q)){if(Y.length===0&&X==="")Y.push("/");else Y.push(X);let z=Q.replace("?","");X+="/"+z,Y.push(X)}else X+="/"+Q}),Y.filter((Q,z,Z)=>Z.indexOf(Q)===z)},Zu=($)=>{if(!/[%+]/.test($))return $;if($.indexOf("+")!==-1)$=$.replace(/\+/g," ");return $.indexOf("%")!==-1?XK($,yV):$},PS1=($,x,Y)=>{let X;if(!Y&&x&&!/[%+]/.test(x)){let Z=$.indexOf("?",8);if(Z===-1)return;if(!$.startsWith(x,Z+1))Z=$.indexOf(`&${x}`,Z+1);while(Z!==-1){let K=$.charCodeAt(Z+x.length+1);if(K===61){let W=Z+x.length+2,V=$.indexOf("&",W);return Zu($.slice(W,V===-1?void 0:V))}else if(K==38||isNaN(K))return"";Z=$.indexOf(`&${x}`,Z+1)}if(X=/[%+]/.test($),!X)return}let Q={};X??=/[%+]/.test($);let z=$.indexOf("?",8);while(z!==-1){let Z=$.indexOf("&",z+1),K=$.indexOf("=",z);if(K>Z&&Z!==-1)K=-1;let W=$.slice(z+1,K===-1?Z===-1?void 0:Z:K);if(X)W=Zu(W);if(z=Z,W==="")continue;let V;if(K===-1)V="";else if(V=$.slice(K+1,Z===-1?void 0:Z),X)V=Zu(V);if(Y){if(!(Q[W]&&Array.isArray(Q[W])))Q[W]=[];Q[W].push(V)}else Q[W]??=V}return x?Q[x]:Q},jS1=PS1,IS1=($,x)=>{return PS1($,x,!0)},yV=decodeURIComponent;var vS1=($)=>XK($,yV),UF=class{raw;#$;#x;routeIndex=0;path;bodyCache={};constructor($,x="/",Y=[[]]){this.raw=$,this.path=x,this.#x=Y,this.#$={}}param($){return $?this.#Y($):this.#z()}#Y($){let x=this.#x[0][this.routeIndex][1][$],Y=this.#X(x);return Y&&/\%/.test(Y)?vS1(Y):Y}#z(){let $={},x=Object.keys(this.#x[0][this.routeIndex][1]);for(let Y of x){let X=this.#X(this.#x[0][this.routeIndex][1][Y]);if(X!==void 0)$[Y]=/\%/.test(X)?vS1(X):X}return $}#X($){return this.#x[1]?this.#x[1][$]:$}query($){return jS1(this.url,$)}queries($){return IS1(this.url,$)}header($){if($)return this.raw.headers.get($)??void 0;let x={};return this.raw.headers.forEach((Y,X)=>{x[X]=Y}),x}async parseBody($){return BS1(this,$)}#Q=($)=>{let{bodyCache:x,raw:Y}=this,X=x[$];if(X)return X;let Q=Object.keys(x)[0];if(Q)return x[Q].then((z)=>{if(Q==="json")z=JSON.stringify(z);return new Response(z)[$]()});return x[$]=Y[$]()};json(){return this.#Q("text").then(($)=>JSON.parse($))}text(){return this.#Q("text")}arrayBuffer(){return this.#Q("arrayBuffer")}bytes(){return this.#Q("arrayBuffer").then(($)=>new Uint8Array($))}blob(){return this.#Q("blob")}formData(){return this.#Q("formData")}addValidatedData($,x){this.#$[$]=x}valid($){return this.#$[$]}get url(){return this.raw.url}get method(){return this.raw.method}get[DS1](){return this.#x}get matchedRoutes(){return this.#x[0].map(([[,$]])=>$)}get routePath(){return this.#x[0].map(([[,$]])=>$)[this.routeIndex].path}};var MS1={Stringify:1,BeforeStream:2,Stream:3},Ut0=($,x)=>{let Y=new String($);return Y.isEscaped=!0,Y.callbacks=x,Y};var Vu=async($,x,Y,X,Q)=>{if(typeof $==="object"&&!($ instanceof String)){if(!($ instanceof Promise))$=$.toString();if($ instanceof Promise)$=await $}let z=$.callbacks;if(!z?.length)return Promise.resolve($);if(Q)Q[0]+=$;else Q=[$];let Z=Promise.all(z.map((K)=>K({phase:x,buffer:Q,context:X}))).then((K)=>Promise.all(K.filter(Boolean).map((W)=>Vu(W,x,!1,X,Q))).then(()=>Q[0]));if(Y)return Ut0(await Z,z);else return Z};var Ht0="text/plain; charset=UTF-8",wu=($,x)=>{return{"Content-Type":$,...x}},mV=($,x)=>new Response($,x),_S1=class{#$;#x;env={};#Y;finalized=!1;error;#z;#X;#Q;#w;#W;#V;#K;#U;#H;constructor($,x){if(this.#$=$,x)this.#X=x.executionCtx,this.env=x.env,this.#V=x.notFoundHandler,this.#H=x.path,this.#U=x.matchResult}get req(){return this.#x??=new UF(this.#$,this.#H,this.#U),this.#x}get event(){if(this.#X&&"respondWith"in this.#X)return this.#X;else throw Error("This context has no FetchEvent")}get executionCtx(){if(this.#X)return this.#X;else throw Error("This context has no ExecutionContext")}get res(){return this.#Q||=mV(null,{headers:this.#K??=new Headers})}set res($){if(this.#Q&&$){$=mV($.body,$);for(let[x,Y]of this.#Q.headers.entries()){if(x==="content-type")continue;if(x==="set-cookie"){let X=this.#Q.headers.getSetCookie();$.headers.delete("set-cookie");for(let Q of X)$.headers.append("set-cookie",Q)}else $.headers.set(x,Y)}}this.#Q=$,this.finalized=!0}render=(...$)=>{return this.#W??=(x)=>this.html(x),this.#W(...$)};setLayout=($)=>this.#w=$;getLayout=()=>this.#w;setRenderer=($)=>{this.#W=$};header=($,x,Y)=>{if(this.finalized)this.#Q=mV(this.#Q.body,this.#Q);let X=this.#Q?this.#Q.headers:this.#K??=new Headers;if(x===void 0)X.delete($);else if(Y?.append)X.append($,x);else X.set($,x)};status=($)=>{this.#z=$};set=($,x)=>{this.#Y??=new Map,this.#Y.set($,x)};get=($)=>{return this.#Y?this.#Y.get($):void 0};get var(){if(!this.#Y)return{};return Object.fromEntries(this.#Y)}#Z($,x,Y){let X=this.#Q?new Headers(this.#Q.headers):this.#K??new Headers;if(typeof x==="object"&&"headers"in x){let z=x.headers instanceof Headers?x.headers:new Headers(x.headers);for(let[Z,K]of z)if(Z.toLowerCase()==="set-cookie")X.append(Z,K);else X.set(Z,K)}if(Y)for(let[z,Z]of Object.entries(Y))if(typeof Z==="string")X.set(z,Z);else{X.delete(z);for(let K of Z)X.append(z,K)}let Q=typeof x==="number"?x:x?.status??this.#z;return mV($,{status:Q,headers:X})}newResponse=(...$)=>this.#Z(...$);body=($,x,Y)=>this.#Z($,x,Y);text=($,x,Y)=>{return!this.#K&&!this.#z&&!x&&!Y&&!this.finalized?new Response($):this.#Z($,x,wu(Ht0,Y))};json=($,x,Y)=>{return this.#Z(JSON.stringify($),x,wu("application/json",Y))};html=($,x,Y)=>{let X=(Q)=>this.#Z(Q,x,wu("text/html; charset=UTF-8",Y));return typeof $==="object"?Vu($,MS1.Stringify,!1,{}).then(X):X($)};redirect=($,x)=>{let Y=String($);return this.header("Location",!/[^\x00-\xFF]/.test(Y)?Y:encodeURI(Y)),this.newResponse(null,x??302)};notFound=()=>{return this.#V??=()=>mV(),this.#V(this)}};var t0="ALL",OS1="all",bS1=["get","post","put","delete","options","patch"],JF="Can not add a route since the matcher is already built.",qF=class extends Error{};var RS1="__COMPOSED_HANDLER";var Gt0=($)=>{return $.text("404 Not Found",404)},AS1=($,x)=>{if("getResponse"in $){let Y=$.getResponse();return x.newResponse(Y.body,Y)}return console.error($),x.text("Internal Server Error",500)},fS1=class ${get;post;put;delete;options;patch;all;on;use;router;getPath;_basePath="/";#$="/";routes=[];constructor(x={}){[...bS1,OS1].forEach((z)=>{this[z]=(Z,...K)=>{if(typeof Z==="string")this.#$=Z;else this.#z(z,this.#$,Z);return K.forEach((W)=>{this.#z(z,this.#$,W)}),this}}),this.on=(z,Z,...K)=>{for(let W of[Z].flat()){this.#$=W;for(let V of[z].flat())K.map((w)=>{this.#z(V.toUpperCase(),this.#$,w)})}return this},this.use=(z,...Z)=>{if(typeof z==="string")this.#$=z;else this.#$="*",Z.unshift(z);return Z.forEach((K)=>{this.#z(t0,this.#$,K)}),this};let{strict:X,...Q}=x;Object.assign(this,Q),this.getPath=X??!0?x.getPath??Wu:NS1}#x(){let x=new $({router:this.router,getPath:this.getPath});return x.errorHandler=this.errorHandler,x.#Y=this.#Y,x.routes=this.routes,x}#Y=Gt0;errorHandler=AS1;route(x,Y){let X=this.basePath(x);return Y.routes.map((Q)=>{let z;if(Y.errorHandler===AS1)z=Q.handler;else z=async(Z,K)=>(await zu([],Y.errorHandler)(Z,()=>Q.handler(Z,K))).res,z[RS1]=Q.handler;X.#z(Q.method,Q.path,z)}),this}basePath(x){let Y=this.#x();return Y._basePath=XX(this._basePath,x),Y}onError=(x)=>{return this.errorHandler=x,this};notFound=(x)=>{return this.#Y=x,this};mount(x,Y,X){let Q,z;if(X)if(typeof X==="function")z=X;else if(z=X.optionHandler,X.replaceRequest===!1)Q=(W)=>W;else Q=X.replaceRequest;let Z=z?(W)=>{let V=z(W);return Array.isArray(V)?V:[V]}:(W)=>{let V=void 0;try{V=W.executionCtx}catch{}return[W.env,V]};Q||=(()=>{let W=XX(this._basePath,x),V=W==="/"?0:W.length;return(w)=>{let U=new URL(w.url);return U.pathname=U.pathname.slice(V)||"/",new Request(U,w)}})();let K=async(W,V)=>{let w=await Y(Q(W.req.raw),...Z(W));if(w)return w;await V()};return this.#z(t0,XX(x,"*"),K),this}#z(x,Y,X){x=x.toUpperCase(),Y=XX(this._basePath,Y);let Q={basePath:this._basePath,path:Y,method:x,handler:X};this.router.add(x,Y,[X,Q]),this.routes.push(Q)}#X(x,Y){if(x instanceof Error)return this.errorHandler(x,Y);throw x}#Q(x,Y,X,Q){if(Q==="HEAD")return(async()=>new Response(null,await this.#Q(x,Y,X,"GET")))();let z=this.getPath(x,{env:X}),Z=this.router.match(Q,z),K=new _S1(x,{path:z,matchResult:Z,env:X,executionCtx:Y,notFoundHandler:this.#Y});if(Z[0].length===1){let V;try{V=Z[0][0][0][0](K,async()=>{K.res=await this.#Y(K)})}catch(w){return this.#X(w,K)}return V instanceof Promise?V.then((w)=>w||(K.finalized?K.res:this.#Y(K))).catch((w)=>this.#X(w,K)):V??this.#Y(K)}let W=zu(Z[0],this.errorHandler,this.#Y);return(async()=>{try{let V=await W(K);if(!V.finalized)throw Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");return V.res}catch(V){return this.#X(V,K)}})()}fetch=(x,...Y)=>{return this.#Q(x,Y[1],Y[0],x.method)};request=(x,Y,X,Q)=>{if(x instanceof Request)return this.fetch(Y?new Request(x,Y):x,X,Q);return x=x.toString(),this.fetch(new Request(/^https?:\/\//.test(x)?x:`http://localhost${XX("/",x)}`,Y),X,Q)};fire=()=>{addEventListener("fetch",(x)=>{x.respondWith(this.#Q(x.request,x,void 0,x.request.method))})}};var hV=[];function DF($,x){let Y=this.buildAllMatchers(),X=(Q,z)=>{let Z=Y[Q]||Y[t0],K=Z[2][z];if(K)return K;let W=z.match(Z[0]);if(!W)return[[],hV];let V=W.indexOf("",1);return[Z[1][V],W]};return this.match=X,X($,x)}var BF="[^/]+",gV=".*",pV="(?:|/.*)",zX=Symbol(),Jt0=new Set(".\\+*[^]$()");function qt0($,x){if($.length===1)return x.length===1?$<x?-1:1:-1;if(x.length===1)return 1;if($===gV||$===pV)return 1;else if(x===gV||x===pV)return-1;if($===BF)return 1;else if(x===BF)return-1;return $.length===x.length?$<x?-1:1:x.length-$.length}var kS1=class ${#$;#x;#Y=Object.create(null);insert(x,Y,X,Q,z){if(x.length===0){if(this.#$!==void 0)throw zX;if(z)return;this.#$=Y;return}let[Z,...K]=x,W=Z==="*"?K.length===0?["","",gV]:["","",BF]:Z==="/*"?["","",pV]:Z.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/),V;if(W){let w=W[1],U=W[2]||BF;if(w&&W[2]){if(U===".*")throw zX;if(U=U.replace(/^\((?!\?:)(?=[^)]+\)$)/,"(?:"),/\((?!\?:)/.test(U))throw zX}if(V=this.#Y[U],!V){if(Object.keys(this.#Y).some((H)=>H!==gV&&H!==pV))throw zX;if(z)return;if(V=this.#Y[U]=new $,w!=="")V.#x=Q.varIndex++}if(!z&&w!=="")X.push([w,V.#x])}else if(V=this.#Y[Z],!V){if(Object.keys(this.#Y).some((w)=>w.length>1&&w!==gV&&w!==pV))throw zX;if(z)return;V=this.#Y[Z]=new $}V.insert(K,Y,X,Q,z)}buildRegExpStr(){let Y=Object.keys(this.#Y).sort(qt0).map((X)=>{let Q=this.#Y[X];return(typeof Q.#x==="number"?`(${X})@${Q.#x}`:Jt0.has(X)?`\\${X}`:X)+Q.buildRegExpStr()});if(typeof this.#$==="number")Y.unshift(`#${this.#$}`);if(Y.length===0)return"";if(Y.length===1)return Y[0];return"(?:"+Y.join("|")+")"}};var TS1=class{#$={varIndex:0};#x=new kS1;insert($,x,Y){let X=[],Q=[];for(let Z=0;;){let K=!1;if($=$.replace(/\{[^}]+\}/g,(W)=>{let V=`@\\${Z}`;return Q[Z]=[V,W],Z++,K=!0,V}),!K)break}let z=$.match(/(?::[^\/]+)|(?:\/\*$)|./g)||[];for(let Z=Q.length-1;Z>=0;Z--){let[K]=Q[Z];for(let W=z.length-1;W>=0;W--)if(z[W].indexOf(K)!==-1){z[W]=z[W].replace(K,Q[Z][1]);break}}return this.#x.insert(z,x,X,this.#$,Y),X}buildRegExp(){let $=this.#x.buildRegExpStr();if($==="")return[/^$/,[],[]];let x=0,Y=[],X=[];return $=$.replace(/#(\d+)|@(\d+)|\.\*\$/g,(Q,z,Z)=>{if(z!==void 0)return Y[++x]=Number(z),"$()";if(Z!==void 0)return X[Number(Z)]=++x,"";return""}),[new RegExp(`^${$}`),Y,X]}};var Dt0=[/^$/,[],Object.create(null)],cS1=Object.create(null);function SS1($){return cS1[$]??=new RegExp($==="*"?"":`^${$.replace(/\/\*$|([.\\+*[^\]$()])/g,(x,Y)=>Y?`\\${Y}`:"(?:|/.*)")}$`)}function Bt0(){cS1=Object.create(null)}function Ft0($){let x=new TS1,Y=[];if($.length===0)return Dt0;let X=$.map((V)=>[!/\*|\/:/.test(V[0]),...V]).sort(([V,w],[U,H])=>V?1:U?-1:w.length-H.length),Q=Object.create(null);for(let V=0,w=-1,U=X.length;V<U;V++){let[H,J,q]=X[V];if(H)Q[J]=[q.map(([D])=>[D,Object.create(null)]),hV];else w++;let B;try{B=x.insert(J,w,H)}catch(D){throw D===zX?new qF(J):D}if(H)continue;Y[w]=q.map(([D,F])=>{let P=Object.create(null);F-=1;for(;F>=0;F--){let[N,b]=B[F];P[N]=b}return[D,P]})}let[z,Z,K]=x.buildRegExp();for(let V=0,w=Y.length;V<w;V++)for(let U=0,H=Y[V].length;U<H;U++){let J=Y[V][U]?.[1];if(!J)continue;let q=Object.keys(J);for(let B=0,D=q.length;B<D;B++)J[q[B]]=K[J[q[B]]]}let W=[];for(let V in Z)W[V]=Y[Z[V]];return[z,W,Q]}function zK($,x){if(!$)return;for(let Y of Object.keys($).sort((X,Q)=>Q.length-X.length))if(SS1(Y).test(x))return[...$[Y]];return}var FF=class{name="RegExpRouter";#$;#x;constructor(){this.#$={[t0]:Object.create(null)},this.#x={[t0]:Object.create(null)}}add($,x,Y){let X=this.#$,Q=this.#x;if(!X||!Q)throw Error(JF);if(!X[$])[X,Q].forEach((K)=>{K[$]=Object.create(null),Object.keys(K[t0]).forEach((W)=>{K[$][W]=[...K[t0][W]]})});if(x==="/*")x="*";let z=(x.match(/\/:/g)||[]).length;if(/\*$/.test(x)){let K=SS1(x);if($===t0)Object.keys(X).forEach((W)=>{X[W][x]||=zK(X[W],x)||zK(X[t0],x)||[]});else X[$][x]||=zK(X[$],x)||zK(X[t0],x)||[];Object.keys(X).forEach((W)=>{if($===t0||$===W)Object.keys(X[W]).forEach((V)=>{K.test(V)&&X[W][V].push([Y,z])})}),Object.keys(Q).forEach((W)=>{if($===t0||$===W)Object.keys(Q[W]).forEach((V)=>K.test(V)&&Q[W][V].push([Y,z]))});return}let Z=GF(x)||[x];for(let K=0,W=Z.length;K<W;K++){let V=Z[K];Object.keys(Q).forEach((w)=>{if($===t0||$===w)Q[w][V]||=[...zK(X[w],V)||zK(X[t0],V)||[]],Q[w][V].push([Y,z-W+K+1])})}}match=DF;buildAllMatchers(){let $=Object.create(null);return Object.keys(this.#x).concat(Object.keys(this.#$)).forEach((x)=>{$[x]||=this.#Y(x)}),this.#$=this.#x=void 0,Bt0(),$}#Y($){let x=[],Y=$===t0;if([this.#$,this.#x].forEach((X)=>{let Q=X[$]?Object.keys(X[$]).map((z)=>[z,X[$][z]]):[];if(Q.length!==0)Y||=!0,x.push(...Q);else if($!==t0)x.push(...Object.keys(X[t0]).map((z)=>[z,X[t0][z]]))}),!Y)return null;else return Ft0(x)}};var Lt0=class{name="PreparedRegExpRouter";#$;#x;constructor($,x){this.#$=$,this.#x=x}#Y($,x){let Y=this.#$[$];Y[1].forEach((X)=>X&&X.push(x)),Object.values(Y[2]).forEach((X)=>X[0].push(x))}#z($,x,Y,X,Q){let z=this.#$[$];if(!Q)z[2][x][0].push([Y,{}]);else X.forEach((Z)=>{if(typeof Z==="number")z[1][Z].push([Y,Q]);else z[2][Z||x][0].push([Y,Q])})}add($,x,Y){if(!this.#$[$]){let Q=this.#$[t0],z={};for(let Z in Q[2])z[Z]=[Q[2][Z][0].slice(),hV];this.#$[$]=[Q[0],Q[1].map((Z)=>Array.isArray(Z)?Z.slice():0),z]}if(x==="/*"||x==="*"){let Q=[Y,{}];if($===t0)for(let z in this.#$)this.#Y(z,Q);else this.#Y($,Q);return}let X=this.#x[x];if(!X)throw Error(`Path ${x} is not registered`);for(let[Q,z]of X)if($===t0)for(let Z in this.#$)this.#z(Z,x,Y,Q,z);else this.#z($,x,Y,Q,z)}buildAllMatchers(){return this.#$}match=DF};var Uu=class{name="SmartRouter";#$=[];#x=[];constructor($){this.#$=$.routers}add($,x,Y){if(!this.#x)throw Error(JF);this.#x.push([$,x,Y])}match($,x){if(!this.#x)throw Error("Fatal error");let Y=this.#$,X=this.#x,Q=Y.length,z=0,Z;for(;z<Q;z++){let K=Y[z];try{for(let W=0,V=X.length;W<V;W++)K.add(...X[W]);Z=K.match($,x)}catch(W){if(W instanceof qF)continue;throw W}this.match=K.match.bind(K),this.#$=[K],this.#x=void 0;break}if(z===Q)throw Error("Fatal error");return this.name=`SmartRouter + ${this.activeRouter.name}`,Z}get activeRouter(){if(this.#x||this.#$.length!==1)throw Error("No active router has been determined yet.");return this.#$[0]}};var iV=Object.create(null),Nt0=($)=>{for(let x in $)return!0;return!1},ES1=class ${#$;#x;#Y;#z=0;#X=iV;constructor(x,Y,X){if(this.#x=X||Object.create(null),this.#$=[],x&&Y){let Q=Object.create(null);Q[x]={handler:Y,possibleKeys:[],score:0},this.#$=[Q]}this.#Y=[]}insert(x,Y,X){this.#z=++this.#z;let Q=this,z=FS1(Y),Z=[];for(let K=0,W=z.length;K<W;K++){let V=z[K],w=z[K+1],U=LS1(V,w),H=Array.isArray(U)?U[0]:V;if(H in Q.#x){if(Q=Q.#x[H],U)Z.push(U[1]);continue}if(Q.#x[H]=new $,U)Q.#Y.push(U),Z.push(U[1]);Q=Q.#x[H]}return Q.#$.push({[x]:{handler:X,possibleKeys:Z.filter((K,W,V)=>V.indexOf(K)===W),score:this.#z}}),Q}#Q(x,Y,X,Q,z){for(let Z=0,K=Y.#$.length;Z<K;Z++){let W=Y.#$[Z],V=W[X]||W[t0],w={};if(V!==void 0){if(V.params=Object.create(null),x.push(V),Q!==iV||z&&z!==iV)for(let U=0,H=V.possibleKeys.length;U<H;U++){let J=V.possibleKeys[U],q=w[V.score];V.params[J]=z?.[J]&&!q?z[J]:Q[J]??z?.[J],w[V.score]=!0}}}}search(x,Y){let X=[];this.#X=iV;let z=[this],Z=Ku(Y),K=[],W=Z.length,V=null;for(let w=0;w<W;w++){let U=Z[w],H=w===W-1,J=[];for(let B=0,D=z.length;B<D;B++){let F=z[B],P=F.#x[U];if(P)if(P.#X=F.#X,H){if(P.#x["*"])this.#Q(X,P.#x["*"],x,F.#X);this.#Q(X,P,x,F.#X)}else J.push(P);for(let N=0,b=F.#Y.length;N<b;N++){let C=F.#Y[N],l=F.#X===iV?{}:{...F.#X};if(C==="*"){let X1=F.#x["*"];if(X1)this.#Q(X,X1,x,F.#X),X1.#X=l,J.push(X1);continue}let[H1,F1,g]=C;if(!U&&!(g instanceof RegExp))continue;let a=F.#x[H1];if(g instanceof RegExp){if(V===null){V=Array(W);let _1=Y[0]==="/"?1:0;for(let N1=0;N1<W;N1++)V[N1]=_1,_1+=Z[N1].length+1}let X1=Y.substring(V[w]),V1=g.exec(X1);if(V1){if(l[F1]=V1[0],this.#Q(X,a,x,F.#X,l),Nt0(a.#x)){a.#X=l;let _1=V1[0].match(/\//)?.length??0;(K[_1]||=[]).push(a)}continue}}if(g===!0||g.test(U))if(l[F1]=U,H){if(this.#Q(X,a,x,l,F.#X),a.#x["*"])this.#Q(X,a.#x["*"],x,l,F.#X)}else a.#X=l,J.push(a)}}let q=K.shift();z=q?J.concat(q):J}if(X.length>1)X.sort((w,U)=>{return w.score-U.score});return[X.map(({handler:w,params:U})=>[w,U])]}};var Hu=class{name="TrieRouter";#$;constructor(){this.#$=new ES1}add($,x,Y){let X=GF(x);if(X){for(let Q=0,z=X.length;Q<z;Q++)this.#$.insert($,X[Q],Y);return}this.#$.insert($,x,Y)}match($,x){return this.#$.search($,x)}};var CS1=class extends fS1{constructor($={}){super($);this.router=$.router??new Uu({routers:[new FF,new Hu]})}};var Pt0=class{initApp;#$;constructor($){this.initApp=$?.initApp,this.#$=$?.defaultAppOptions}createApp=($)=>{let x=new CS1($&&this.#$?{...this.#$,...$}:$??this.#$);if(this.initApp)this.initApp(x);return x};createMiddleware=($)=>$;createHandlers=(...$)=>{return $.filter((x)=>x!==void 0)}},uS1=($)=>new Pt0($);var z8=uS1();class Gu extends NQ{file=null;info(){}warn(){}error(){}}var yS1=($)=>{let x=Buffer.byteLength($.content,"utf-8");if($.meta)for(let[Y,X]of Object.entries($.meta))x+=Buffer.byteLength(Y,"utf-8")+Buffer.byteLength(X,"utf-8");return x},jt0=1048576,It0=200,vt0=4194304,Mt0=new Gu;class Ju{clients=new Map;subscribers=new Set;logger;maxBufferedBytes;now;replayBufferSize;replayBufferMaxBytes;replayBuffer=[];persistentReplay;exclusiveCursor=new Map;replayBufferBytes=0;eventsBroadcast=0;droppedSlowClients=0;lastBroadcastAt=null;latestOffset=0;constructor($={}){this.logger=$.logger??Mt0,this.maxBufferedBytes=$.maxBufferedBytes??jt0,this.now=$.now??(()=>Date.now()),this.replayBufferSize=Math.max(0,$.replayBufferSize??It0),this.replayBufferMaxBytes=Math.max(0,$.replayBufferMaxBytes??vt0),this.persistentReplay=$.persistentReplay??null}getMetrics(){return{clients:this.clients.size,subscribers:this.subscribers.size,eventsBroadcast:this.eventsBroadcast,droppedSlowClients:this.droppedSlowClients,lastBroadcastAt:this.lastBroadcastAt?new Date(this.lastBroadcastAt).toISOString():null,latestOffset:this.latestOffset,oldestReplayableOffset:this.replayBuffer[0]?.offset??null}}replaySince($,x){let Y=this.replayBuffer[0]?.offset,X=this.persistentReplay&&(Y===void 0||$<Y-1),Q=this.replayBuffer.filter((W)=>W.offset>$&&this.matchesClient(W,x));if(!X)return Q;let z=this.persistentReplay?this.persistentReplay.loadSince($).filter((W)=>this.matchesClient(W,x)):[],Z=Y??Number.POSITIVE_INFINITY;return[...z.filter((W)=>W.offset<Z),...Q]}matchesClient($,x){if(x.tapAll)return!0;let Y=$.meta?.channelId;if(Y&&Y!==x.channel)return!1;let X=$.meta?.connector;if(!X)return!0;return x.connectors.includes(X)}pickRecipients($){let x=new Map,Y=[];for(let[X,Q]of this.clients){if(!this.matchesClient($,Q))continue;if(Q.tapAll){Y.push(X);continue}if(Q.delivery==="exclusive"){let z=x.get(Q.channel)??[];z.push(X),x.set(Q.channel,z);continue}Y.push(X)}for(let[X,Q]of x){if(Q.length===0)continue;let z=this.exclusiveCursor.get(X)??0,Z=Q[z%Q.length];if(Z)Y.push(Z);this.exclusiveCursor.set(X,z+1)}return Y}addClient($,x){this.clients.set($,x)}removeClient($){this.clients.delete($)}getClientCount(){return this.clients.size}listChannels(){return[...this.clients.values()].map(($)=>({...$}))}subscribe($){return this.subscribers.add($),()=>{this.subscribers.delete($)}}broadcast($,x){this.latestOffset+=1;let Y={content:$,meta:x,offset:this.latestOffset},X=JSON.stringify(Y),Q=x?.connector;if(this.eventsBroadcast+=1,this.lastBroadcastAt=this.now(),this.replayBufferSize>0){let Z=yS1(Y);this.replayBuffer.push(Y),this.replayBufferBytes+=Z;while((this.replayBuffer.length>this.replayBufferSize||this.replayBufferBytes>this.replayBufferMaxBytes)&&this.replayBuffer.length>0){let K=this.replayBuffer.shift();if(K)this.replayBufferBytes-=yS1(K)}}let z=this.pickRecipients(Y);for(let Z of z){let K=Z.getBufferedAmount();if(K>this.maxBufferedBytes){let W=this.clients.get(Z);this.logger.warn("dropping slow WS client (backpressure)",{channel:W?.channel,buffered:K,max:this.maxBufferedBytes});try{Z.close(1009,"backpressure")}catch{}this.clients.delete(Z),this.droppedSlowClients+=1;continue}Z.send(X)}for(let Z of this.subscribers)try{Z(Y)}catch(K){this.logger.error("broadcast subscriber threw",{error:K instanceof Error?K.message:String(K)})}return Y}seedLatestOffset($){if($>this.latestOffset)this.latestOffset=$}}import{Database as _t0}from"bun:sqlite";var Ot0=/^[a-z_][a-z0-9_]*$/,bt0=new Set(["seq","ts","type","event"]),mS1=[["CREATE TABLE IF NOT EXISTS leuco_log (seq INTEGER PRIMARY KEY, ts INTEGER NOT NULL, type TEXT, event TEXT NOT NULL)","CREATE INDEX IF NOT EXISTS idx_leuco_log_ts ON leuco_log (ts)","CREATE INDEX IF NOT EXISTS idx_leuco_log_type ON leuco_log (type)"]];class qu{db;maxRows;maxAgeMs;now;indexes;extractIndexes;insertStmt;insertWithSeqStmt;maxSeqStmt;countStmt;trimRowsStmt;trimAgeStmt;constructor($){if(this.db=new _t0($.path),this.db.run("PRAGMA journal_mode = WAL"),this.migrate(),this.maxRows=$.maxRows??null,this.maxAgeMs=$.maxAgeMs??null,this.now=$.now??(()=>Date.now()),this.indexes=$.indexes??[],this.indexes.length>0)Rt0(this.indexes),this.extractIndexes=$.extractIndexes??null,this.syncIndexColumns();else this.extractIndexes=null;let x=["ts","type","event",...this.indexes],Y=x.map(()=>"?").join(", ");this.insertStmt=this.db.prepare(`INSERT INTO leuco_log (${x.join(", ")}) VALUES (${Y})`);let X=["seq",...x],Q=X.map(()=>"?").join(", ");this.insertWithSeqStmt=this.db.prepare(`INSERT INTO leuco_log (${X.join(", ")}) VALUES (${Q})`),this.maxSeqStmt=this.db.prepare("SELECT COALESCE(MAX(seq), 0) AS max FROM leuco_log"),this.countStmt=this.db.prepare("SELECT COUNT(*) AS n FROM leuco_log"),this.trimRowsStmt=this.db.prepare("DELETE FROM leuco_log WHERE seq <= (SELECT seq FROM leuco_log ORDER BY seq DESC LIMIT 1 OFFSET ?)"),this.trimAgeStmt=this.db.prepare("DELETE FROM leuco_log WHERE ts < ?")}insert($){try{let x=this.buildInsertParams($.ts,$.event),Y=this.insertStmt.run(...x),X=Number(Y.lastInsertRowid);return this.trim(),{seq:X,ts:$.ts,event:$.event}}catch(x){return x instanceof Error?x:Error(String(x))}}insertMany($){if($.length===0)return[];try{let x=[];return this.db.transaction((X)=>{for(let Q of X){let z=this.buildInsertParams(Q.ts,Q.event),Z=this.insertStmt.run(...z);x.push({seq:Number(Z.lastInsertRowid),ts:Q.ts,event:Q.event})}})($),this.trim(),x}catch(x){return x instanceof Error?x:Error(String(x))}}write($){try{let x=[$.seq,...this.buildInsertParams($.ts,$.event)];this.insertWithSeqStmt.run(...x),this.trim()}catch(x){return x instanceof Error?x:Error(String(x))}}getMaxSeq(){let $=this.maxSeqStmt.get();return $?$.max:0}getRecords($={}){let x=["seq > ?"],Y=[$.sinceSeq??0];if(typeof $.type==="string")x.push("type = ?"),Y.push($.type);if($.where)this.appendWhereConditions($.where,x,Y);let X=$.limit??1000;Y.push(X);let Q=`SELECT seq, ts, type, event FROM leuco_log WHERE ${x.join(" AND ")} ORDER BY seq ASC LIMIT ?`;return this.db.prepare(Q).all(...Y).map(ft0)}getSchemaVersion(){return this.db.prepare("PRAGMA user_version").get()?.user_version??0}close(){this.db.close()}buildInsertParams($,x){let Y=At0(x),X=JSON.stringify(x);if(this.indexes.length===0)return[$,Y,X];let Q=this.extractIndexes?this.extractIndexes(x):null,z=this.indexes.map((Z)=>Q?.[Z]??null);return[$,Y,X,...z]}appendWhereConditions($,x,Y){let X=$;for(let Q of this.indexes){let z=X[Q];if(z===void 0)continue;if(z===null)x.push(`${Q} IS NULL`);else x.push(`${Q} = ?`),Y.push(z)}}trim(){if(this.maxRows!==null){let $=this.countStmt.get();if($&&$.n>this.maxRows)this.trimRowsStmt.run(this.maxRows)}if(this.maxAgeMs!==null)this.trimAgeStmt.run(this.now()-this.maxAgeMs)}syncIndexColumns(){let $=new Set(this.db.prepare("PRAGMA table_info(leuco_log)").all().map((x)=>x.name));for(let x of this.indexes){if(!$.has(x))this.db.run(`ALTER TABLE leuco_log ADD COLUMN ${x} TEXT`);this.db.run(`CREATE INDEX IF NOT EXISTS idx_leuco_log_${x} ON leuco_log (${x})`)}}migrate(){let x=this.db.prepare("PRAGMA user_version").get()?.user_version??0;if(x>=mS1.length)return;let Y=mS1.slice(x),X=x;for(let Q of Y)X+=1,this.db.transaction(()=>{for(let Z of Q)this.db.run(Z);this.db.run(`PRAGMA user_version = ${X}`)})()}}function Rt0($){for(let x of $){if(!Ot0.test(x))throw Error(`invalid index column name: ${x}`);if(bt0.has(x))throw Error(`reserved index column name: ${x}`)}}function At0($){if(typeof $!=="object"||$===null)return null;if(!("type"in $))return null;let x=$.type;return typeof x==="string"?x:null}function ft0($){return{seq:$.seq,ts:$.ts,event:JSON.parse($.event)}}var hS1=2000,HB6=m.object({type:m.string(),content:m.string(),channel_id:m.string().nullable(),connector_id:m.string().nullable(),meta:m.record(m.string(),m.string()).nullable()});class Du{sink;now;constructor($){this.now=$.now??(()=>Date.now()),this.sink=new qu({path:$.path,indexes:["channel_id","connector_id"],extractIndexes:(x)=>({channel_id:x.channel_id,connector_id:x.connector_id}),now:this.now,...$.maxRows!==void 0?{maxRows:$.maxRows}:{},...$.maxAgeMs!==void 0?{maxAgeMs:$.maxAgeMs}:{}})}record($){let x={type:$.meta?.event_type??"unknown",content:kt0($.content),channel_id:$.channelId,connector_id:$.connectorId,meta:$.meta};this.sink.write({seq:$.offset,ts:this.now(),event:x})}loadSince($){let x=this.sink.getRecords({sinceSeq:$}),Y=[];for(let X of x)Y.push({content:X.event.content,meta:X.event.meta??void 0,offset:X.seq});return Y}loadForChannel($){let x={channel_id:$.channelId};if($.connectorId!==void 0)x.connector_id=$.connectorId;let Y=this.sink.getRecords({where:x,...$.sinceSeq!==void 0?{sinceSeq:$.sinceSeq}:{},...$.limit!==void 0?{limit:$.limit}:{}}),X=[];for(let Q of Y)X.push({content:Q.event.content,meta:Q.event.meta??void 0,offset:Q.seq});return X}findMaxOffset(){return this.sink.getMaxSeq()}close(){this.sink.close()}}function kt0($){if($.length<=hS1)return $;return`${$.slice(0,hS1)}...`}var Tt0=new I6,ct0=30000,St0=60000,Et0=($)=>new Promise((x)=>{setTimeout(x,$)});class ZX{channels;notify;logger;running=new Map;failureCounts=new Map;stats=new Map;healthCheckIntervalMs;maxBackoffMs;sleep;now;healthCheckTimer=null;healthCheckInFlight=!1;constructor($){this.channels=$.channels,this.notify=$.notify,this.logger=$.logger??Tt0,this.healthCheckIntervalMs=$.healthCheckIntervalMs??ct0,this.maxBackoffMs=$.maxBackoffMs??St0,this.sleep=$.sleep??Et0,this.now=$.now??(()=>Date.now())}static keyOf($,x){return`${$}/${x}`}isRunning($,x){return this.running.has(ZX.keyOf($,x))}list(){return[...this.running.entries()].map(([$,x])=>{let Y=this.stats.get($);return{channelName:x.channelName,channelId:x.channelId,name:x.config.name,type:x.config.type,alive:x.listener.isAlive(),events:Y?.events??0,errors:Y?.errors??0,failureCount:this.failureCounts.get($)??0,lastEventAt:Y?.lastEventAt??null}})}async start($,x){let Y=ZX.keyOf($,x);if(this.running.has(Y))return{ok:!0,reason:"already running"};let X=this.channels.createListener($,x);if(!X)return{ok:!1,reason:`connector "${x}" not found in channel "${$}"`};let Q=async(z,Z)=>{try{await this.notify($,x,z,Z),this.recordEvent(Y)}catch(K){throw this.recordError(Y),K}};try{return await X.listener.start(Q),this.running.set(Y,{config:X.config,channelName:$,channelId:X.channelId,listener:X.listener}),this.ensureStats(Y),this.logger.info(`${X.config.type} listener started`,{channel:$,connector:x}),{ok:!0}}catch(z){return this.logger.error(`${X.config.type} listener failed to start`,{channel:$,connector:x,error:z instanceof Error?z.message:String(z)}),{ok:!1,reason:z instanceof Error?z.message:String(z)}}}async stop($,x){let Y=ZX.keyOf($,x),X=this.running.get(Y);if(!X)return{ok:!0,reason:"not running"};try{return await X.listener.stop(),this.running.delete(Y),this.failureCounts.delete(Y),this.logger.info(`${X.config.type} listener stopped`,{channel:$,connector:x}),{ok:!0}}catch(Q){return this.logger.error(`${X.config.type} listener failed to stop`,{channel:$,connector:x,error:Q instanceof Error?Q.message:String(Q)}),{ok:!1,reason:Q instanceof Error?Q.message:String(Q)}}}async restart($,x){let Y=await this.stop($,x);if(!Y.ok)return Y;return await this.start($,x)}async startAll(){let $=this.channels.listAllConnectors();for(let x of $)await this.start(x.channelName,x.name);this.startHealthCheck()}async stopAll(){this.stopHealthCheck();for(let[,$]of[...this.running.entries()])await this.stop($.channelName,$.config.name)}ensureStats($){let x=this.stats.get($);if(x)return x;let Y={events:0,errors:0,failureCount:0,lastEventAt:null};return this.stats.set($,Y),Y}recordEvent($){let x=this.ensureStats($);x.events+=1,x.lastEventAt=new Date(this.now()).toISOString()}recordError($){this.ensureStats($).errors+=1}startHealthCheck(){if(this.healthCheckTimer)return;this.healthCheckTimer=setInterval(()=>{this.runHealthCheck()},this.healthCheckIntervalMs),this.healthCheckTimer.unref()}stopHealthCheck(){if(!this.healthCheckTimer)return;clearInterval(this.healthCheckTimer),this.healthCheckTimer=null}async runHealthCheck(){if(this.healthCheckInFlight)return;this.healthCheckInFlight=!0;try{for(let[$,x]of[...this.running.entries()]){if(x.listener.isAlive()){this.failureCounts.delete($);continue}await this.recoverDead(x.channelName,x.config.name,x.config.type)}}finally{this.healthCheckInFlight=!1}}async recoverDead($,x,Y){let X=ZX.keyOf($,x),Q=this.failureCounts.get(X)??0,z=Math.min(1000*2**Q,this.maxBackoffMs);if(this.logger.warn(`${Y} listener unhealthy, restarting`,{channel:$,connector:x,attempt:Q+1,backoffMs:z}),await this.stop($,x),await this.sleep(z),(await this.start($,x)).ok)this.failureCounts.delete(X),this.logger.info(`${Y} listener recovered`,{channel:$,connector:x});else this.failureCounts.set(X,Q+1)}}var Ct0=new L$,ut0=new I6,yt0=($)=>`funnel-gateway[${$}]`,gS1=async($)=>{let x=$.process??Ct0,Y=$.logger??ut0,X=await x.run(["ps","-e","-o","pid=,args="]);if(X.exitCode!==0)return[];let Q=yt0($.dir),z=[];for(let Z of X.stdout.split(`
549
549
  `)){let K=Z.trim();if(!K)continue;let W=/^(\d+)\s+(.+)$/.exec(K);if(!W)continue;let V=Number(W[1]),w=W[2];if(!Number.isInteger(V)||V<=0)continue;if(V===$.selfPid)continue;if(!w.includes(Q))continue;x.kill(V,"SIGTERM"),z.push(V),Y.info("killed competing Slack gateway process",{pid:V,args:w.slice(0,160)})}return z};var mt0=/^[\w!#$%&'*.^`|~+-]+$/,ht0=/^[ !#-:<-[\]-~]*$/,pS1=($)=>{let x=0,Y=$.length;while(x<Y){let X=$.charCodeAt(x);if(X!==32&&X!==9)break;x++}while(Y>x){let X=$.charCodeAt(Y-1);if(X!==32&&X!==9)break;Y--}return x===0&&Y===$.length?$:$.slice(x,Y)},Bu=($,x)=>{if(x&&$.indexOf(x)===-1)return{};let Y=$.split(";"),X=Object.create(null);for(let Q of Y){let z=Q.indexOf("=");if(z===-1)continue;let Z=pS1(Q.substring(0,z));if(x&&x!==Z||!mt0.test(Z)||Z in X)continue;let K=pS1(Q.substring(z+1));if(K.startsWith('"')&&K.endsWith('"'))K=K.slice(1,-1);if(ht0.test(K)){if(X[Z]=K.indexOf("%")!==-1?XK(K,yV):K,x)break}}return X};var iS1=($,x,Y)=>{let X=$.req.raw.headers.get("Cookie");if(typeof x==="string"){if(!X)return;let z=x;if(Y==="secure")z="__Secure-"+x;else if(Y==="host")z="__Host-"+x;return Bu(X,z)[z]}if(!X)return{};return Bu(X)};var lS1=($,x)=>{return new Response($,{headers:{"Content-Type":x}}).formData()};var gt0=/^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/,pt0=/^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/,it0=/^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/,Fu=($,x)=>{return async(Y,X)=>{let Q={},z=Y.req.header("Content-Type");switch($){case"json":if(!z||!gt0.test(z))break;try{Q=await Y.req.json()}catch{throw new QK(400,{message:"Malformed JSON in request body"})}break;case"form":{if(!z||!(pt0.test(z)||it0.test(z)))break;let K;if(Y.req.bodyCache.formData)K=await Y.req.bodyCache.formData;else try{let V=await Y.req.arrayBuffer();K=await lS1(V,z),Y.req.bodyCache.formData=K}catch(V){let w="Malformed FormData request.";throw w+=V instanceof Error?` ${V.message}`:` ${String(V)}`,new QK(400,{message:w})}let W=Object.create(null);K.forEach((V,w)=>{if(w.endsWith("[]"))(W[w]??=[]).push(V);else if(Array.isArray(W[w]))W[w].push(V);else if(Object.hasOwn(W,w))W[w]=[W[w],V];else W[w]=V}),Q=W;break}case"query":Q=Object.fromEntries(Object.entries(Y.req.queries()).map(([K,W])=>{return W.length===1?[K,W[0]]:[K,W]}));break;case"param":Q=Y.req.param();break;case"header":Q=Y.req.header();break;case"cookie":Q=iS1(Y);break}let Z=await x(Q,Y);if(Z instanceof Response)return Z;return Y.req.addValidatedData($,Z),await X()}};function lt0($,x,Y,X){return Fu($,async(Q,z)=>{let Z=Q;if($==="header"&&"_def"in x||$==="header"&&"_zod"in x){let W=Object.keys("in"in x?x.in.shape:x.shape),V=Object.fromEntries(W.map((w)=>[w.toLowerCase(),w]));Z=Object.fromEntries(Object.entries(Q).map(([w,U])=>[V[w]||w,U]))}let K=X&&X.validationFunction?await X.validationFunction(x,Z):await x.safeParseAsync(Z);if(Y){let W=await Y({data:Z,...K,target:$},z);if(W){if(W instanceof Response)return W;if("response"in W)return W.response}}if(!K.success)return z.json(K,400);return K.data})}var LF=lt0;var $2=($)=>LF("param",$,(x,Y)=>{if(x.success)return;let X=x.error.issues[0],Q=X?`${X.path.join(".")}: ${X.message}`:"invalid request";return Y.json({ok:!1,reason:Q},400)});var nt0=m.object({method:m.string().min(1),path:m.string().min(1),body:m.unknown().optional()}),nS1=z8.createHandlers($2(m.object({channel:m.string().min(1),connector:m.string().min(1)})),async($)=>{let x=$.req.valid("param"),Y=await $.req.json().catch(()=>null),X=nt0.safeParse(Y);if(!X.success)throw new QK(400,{message:X.error.issues[0]?.message??"invalid body"});let Q=await $.var.deps.channels.call(x.channel,x.connector,{method:X.data.method,path:X.data.path,body:X.data.body??{}});return $.json({ok:!0,result:Q})});var dS1=z8.createHandlers($2(m.object({channel:m.string().min(1)})),LF("json",HS1,($,x)=>{if($.success)return;let Y=$.error.issues[0],X=Y?`${Y.path.join(".")}: ${Y.message}`:"invalid body";return x.json({ok:!1,reason:X},400)}),($)=>{let x=$.req.valid("param"),Y=$.req.valid("json"),Q={ok:!0,offset:$.var.deps.emit({channel:x.channel,connector:Y.connector,content:Y.content,meta:Y.meta}).offset};return $.json(Q)});var aS1=z8.createHandlers(($)=>{let x=$.var.deps;return $.json({ok:!0,pid:x.selfPid,clients:x.broadcaster.getClientCount(),listeners:x.supervisor.list()})});var sS1=z8.createHandlers(($)=>{return $.json({listeners:$.var.deps.supervisor.list()})});var oS1=z8.createHandlers($2(m.object({channel:m.string().min(1),connector:m.string().min(1)})),async($)=>{let x=$.req.valid("param"),Y=await $.var.deps.supervisor.restart(x.channel,x.connector);return $.json(Y,Y.ok?200:400)});var rS1=z8.createHandlers($2(m.object({channel:m.string().min(1),connector:m.string().min(1)})),async($)=>{let x=$.req.valid("param"),Y=await $.var.deps.supervisor.start(x.channel,x.connector);return $.json(Y,Y.ok?200:400)});var tS1=z8.createHandlers($2(m.object({channel:m.string().min(1),connector:m.string().min(1)})),async($)=>{let x=$.req.valid("param"),Y=await $.var.deps.supervisor.stop(x.channel,x.connector);return $.json(Y,Y.ok?200:400)});var eS1=z8.createHandlers(($)=>{let x=$.var.deps;return $.json({ok:!0,pid:x.selfPid,uptimeMs:x.uptimeMs(),clients:x.broadcaster.listChannels(),listeners:x.supervisor.list(),broadcaster:x.broadcaster.getMetrics()})});var $E1=z8.createApp().get("/health",...aS1).get("/status",...eS1).get("/listeners",...sS1).post("/listeners/:channel/:connector/start",...rS1).delete("/listeners/:channel/:connector",...tS1).post("/listeners/:channel/:connector/restart",...oS1).post("/channels/:channel/connectors/:connector/call",...nS1).post("/channels/:channel/publish",...dS1);var st0=9742,ot0="/tmp/funnel/events",YE1="events.db",rt0=new I6;class Lu{channels;settings;port;logDir;process;logger;selfPid;dir;killCompetingSlack;token;broadcaster;eventStore;supervisor;nowMs;extraRoutes;startedAt=null;server=null;constructor($){this.channels=$.channels,this.settings=$.settings,this.port=$.port??st0,this.logDir=$.logDir??ot0,this.process=$.process,this.logger=$.logger??rt0,this.selfPid=$.selfPid??globalThis.process.pid,this.dir=$.dir??O8,this.killCompetingSlack=$.killCompetingSlack??!0,this.token=$.token??"",this.extraRoutes=$.extraRoutes??null;let x=$.clock;if(this.nowMs=x?()=>x.millis():()=>Date.now(),!dt0(this.logDir))at0(this.logDir,{recursive:!0});this.eventStore=new Du({path:xE1(this.logDir,YE1),now:this.nowMs}),this.broadcaster=new Ju({logger:this.logger,now:this.nowMs,persistentReplay:this.eventStore}),this.broadcaster.seedLatestOffset(this.eventStore.findMaxOffset()),this.supervisor=new ZX({channels:this.channels,logger:this.logger,notify:async(Y,X,Q,z)=>{this.emit({channel:Y,connector:X,content:Q,meta:z})},now:this.nowMs})}async start(){if(this.server)return this.server;let $=this.buildApp();return this.startedAt=this.nowMs(),this.server=Bun.serve({port:this.port,development:!1,fetch:(x,Y)=>this.handleFetch(x,Y,$),websocket:{open:(x)=>this.handleWsOpen(x),close:(x)=>this.handleWsClose(x),message(){}}}),this.logServerStarted(),await this.bootListeners(),this.server}async stop(){if(await this.supervisor.stopAll(),this.server)this.server.stop(),this.server=null}getStatus(){return{clients:this.broadcaster.getClientCount(),channels:this.broadcaster.listChannels()}}getBroadcaster(){return this.broadcaster}getSupervisor(){return this.supervisor}getEventStore(){return this.eventStore}handleFetch($,x,Y){let X=new URL($.url);if(X.pathname==="/ws"&&$.headers.get("upgrade")==="websocket"){if(this.token&&!this.tokenMatchesUpgrade($))return new Response("unauthorized",{status:401});let Q=X.searchParams.get("tap")==="all",z=Q?"":X.searchParams.get("channel")??"",Z=!Q&&z?this.resolveChannel(z):null,K=Q?"":Z?.id??z,W=Q?null:Z?.name??null,V=Z?.connectors??[],w=Z?.delivery??"fanout",U=X.searchParams.get("since"),H=U===null?Number.NaN:Number.parseInt(U,10),J=Number.isFinite(H)&&H>=0?H:void 0;if(x.upgrade($,{data:{channel:K,channelName:W,connectors:V,tapAll:Q,delivery:w,since:J}}))return;return new Response("WebSocket upgrade failed",{status:400})}return Y.fetch($)}handleWsOpen($){if(typeof $.data.since==="number"){let x=this.broadcaster.replaySince($.data.since,$.data);for(let Y of x)$.send(JSON.stringify(Y))}if(this.broadcaster.addClient($,$.data),$.data.channelName){let x={event_type:"system",action:"channel_connect",channel:$.data.channelName,channelId:$.data.channel,connectors:$.data.connectors.join(","),total:String(this.broadcaster.getClientCount())};this.logger.info("channel connected",x)}else this.logger.info("tap-all client connected",{event_type:"system",action:"tap_connect",total:String(this.broadcaster.getClientCount())})}handleWsClose($){if(this.broadcaster.removeClient($),$.data.channelName)this.logger.info("channel disconnected",{event_type:"system",action:"channel_disconnect",channel:$.data.channelName,channelId:$.data.channel,total:String(this.broadcaster.getClientCount())});else this.logger.info("tap-all client disconnected",{event_type:"system",action:"tap_disconnect",total:String(this.broadcaster.getClientCount())})}logServerStarted(){this.logger.info("gateway started",{event_type:"system",action:"gateway_start",port:String(this.port),pid:String(this.selfPid)}),this.logger.info("funnel gateway listening",{url:`http://localhost:${this.port}`,websocket:`ws://localhost:${this.port}/ws`,health:`http://localhost:${this.port}/health`})}buildApp(){let $=z8.createApp();if($.use((Y,X)=>{return Y.set("deps",{selfPid:this.selfPid,broadcaster:this.broadcaster,supervisor:this.supervisor,channels:this.channels,uptimeMs:()=>this.startedAt?this.nowMs()-this.startedAt:0,emit:(Q)=>this.emit(Q)}),X()}),this.token)$.use("/listeners/*",VF({expected:this.token})),$.use("/status",VF({expected:this.token})),$.use("/channels/*",VF({expected:this.token}));return(this.extraRoutes?$.route("/",this.extraRoutes):$).route("/",$E1)}tokenMatchesUpgrade($){let x=($.headers.get("sec-websocket-protocol")??"").split(",").map((Q)=>Q.trim()).filter((Q)=>Q.length>0);for(let Q of x)if(Q.startsWith("funnel.token.")&&wF(Q.slice(13),this.token))return!0;let X=($.headers.get("authorization")??"").match(/^Bearer\s+(.+)$/i);if(X&&wF(X[1]??"",this.token))return!0;return!1}resolveChannel($){let Y=this.settings.read()?.channels.find((X)=>X.id===$||X.name===$);if(!Y)return null;return{id:Y.id,name:Y.name,connectors:Y.connectors.map((X)=>X.name),delivery:Y.delivery}}async bootListeners(){let $=this.channels.listAllConnectors();if(this.killCompetingSlack&&$.some((x)=>x.type==="slack")){let x=await gS1({selfPid:this.selfPid,dir:this.dir,process:this.process,logger:this.logger});if(x.length>0)this.logger.info("killed competing Slack gateway processes",{event_type:"system",action:"kill_competing",pids:x.join(",")})}await this.supervisor.startAll();for(let x of this.supervisor.list())this.logger.info(`${x.type} listener started: ${x.name}`,{event_type:"system",action:`${x.type}_connect`,channel:x.channelName,connector:x.name});this.logger.info(`event store: ${xE1(this.logDir,YE1)}`),this.logger.info("funnel gateway running")}emit($){let x=this.lookupChannelId($.channel),Y=x&&$.connector?this.lookupConnectorId(x,$.connector):null,X={...$.meta,channel:$.channel};if($.connector)X.connector=$.connector;if(x)X.channelId=x;if(Y)X.connectorId=Y;let Q=this.broadcaster.broadcast($.content,X);return this.eventStore.record({content:$.content,channelId:x??null,connectorId:Y??null,meta:X,offset:Q.offset}),{offset:Q.offset}}lookupChannelId($){return this.settings.read().channels.find((Y)=>Y.name===$)?.id??null}lookupConnectorId($,x){return this.settings.read().channels.find((Q)=>Q.id===$)?.connectors.find((Q)=>Q.name===x)?.id??null}}import{homedir as tt0}from"os";import{dirname as et0,join as QE1}from"path";var XE1="gateway.token",$e0=32,xe0=new M8,Ye0=()=>{let $=new Uint8Array($e0);return crypto.getRandomValues($),[...$].map((x)=>x.toString(16).padStart(2,"0")).join("")};class Nu{fs;path;generate;constructor($={}){this.fs=$.fs??xe0,this.path=QE1($.dir??O8,XE1),this.generate=$.generate??Ye0,Object.freeze(this)}read(){if(!this.fs.existsSync(this.path))return null;let $=this.fs.readFileSync(this.path).trim();return $.length>0?$:null}ensure(){let $=this.read();if($)return $;let x=this.generate();return this.fs.mkdirSync(et0(this.path),{recursive:!0}),this.fs.writeSecretFileSync(this.path,`${x}
package/dist/index.d.ts CHANGED
@@ -402,6 +402,10 @@ type LaunchOptions = {
402
402
  * Useful for hosts that need to register the spawned process before it exits
403
403
  * (e.g. multi-session registries that track per-claude liveness). */
404
404
  onSpawned?: (pid: number) => void;
405
+ /** Whether to install the funnel MCP entry into `.mcp.json` (default: true).
406
+ * Set to false when the host already provides its own MCP server entry and
407
+ * does not need the funnel binary as an MCP endpoint. */
408
+ installMcp?: boolean;
405
409
  };
406
410
  type Deps$12 = {
407
411
  channels: FunnelChannels;
@@ -581,6 +585,14 @@ type Deps$9 = {
581
585
  prompter: FunnelTokenPrompter;
582
586
  env?: NodeJS.ProcessEnv;
583
587
  };
588
+ type ConnectorSyncOutcome = {
589
+ name: string;
590
+ changed: boolean;
591
+ };
592
+ type LocalConfigSyncResult = {
593
+ touched: ConnectorSyncOutcome[];
594
+ removed: string[];
595
+ };
584
596
  /**
585
597
  * Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
586
598
  * The spec is the source of truth for the channel it declares:
@@ -596,6 +608,9 @@ type Deps$9 = {
596
608
  * absent field means "do not manage connectors from here" and leaves
597
609
  * everything in `~/.funnel` alone. Other channels in funnel.json (not
598
610
  * passed to this call) are untouched.
611
+ *
612
+ * Returns the per-connector change set so callers (e.g. the claude launcher)
613
+ * can drive listener hot-reload on the gateway after settings are written.
599
614
  */
600
615
  declare class FunnelLocalConfigSync {
601
616
  private readonly channels;
@@ -603,7 +618,7 @@ declare class FunnelLocalConfigSync {
603
618
  private readonly prompter;
604
619
  private readonly env;
605
620
  constructor(deps: Deps$9);
606
- ensure(channel: ChannelSpec, cwd: string): Promise<void>;
621
+ ensure(channel: ChannelSpec, cwd: string): Promise<LocalConfigSyncResult>;
607
622
  private ensureConnector;
608
623
  private ensureSlack;
609
624
  private ensureDiscord;
@@ -4195,4 +4210,4 @@ ${string}`;
4195
4210
  //#region lib/tui/tui.d.ts
4196
4211
  declare function launchTui(funnel: Funnel): Promise<void>;
4197
4212
  //#endregion
4198
- export { AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
4213
+ export { AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
package/dist/index.js CHANGED
@@ -570,7 +570,7 @@ var FunnelClaude = class {
570
570
  if (!channel) throw new Error(`channel "${options.channel}" not found`);
571
571
  if (options.profileName && this.isRunning(options.profileName)) throw new Error(`profile "${options.profileName}" is already running`);
572
572
  const cwd = options.cwd ?? globalThis.process.cwd();
573
- if (!this.mcp.findInstalledName(cwd)) {
573
+ if ((options.installMcp ?? true) && !this.mcp.findInstalledName(cwd)) {
574
574
  this.mcp.install(cwd);
575
575
  this.logger.info(`added funnel MCP to .mcp.json`, { cwd });
576
576
  }
@@ -918,6 +918,9 @@ const recordsEqual = (a, b) => {
918
918
  * absent field means "do not manage connectors from here" and leaves
919
919
  * everything in `~/.funnel` alone. Other channels in funnel.json (not
920
920
  * passed to this call) are untouched.
921
+ *
922
+ * Returns the per-connector change set so callers (e.g. the claude launcher)
923
+ * can drive listener hot-reload on the gateway after settings are written.
921
924
  */
922
925
  var FunnelLocalConfigSync = class {
923
926
  channels;
@@ -944,14 +947,25 @@ var FunnelLocalConfigSync = class {
944
947
  if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
945
948
  if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
946
949
  }
947
- if (channel.connectors === void 0) return;
950
+ if (channel.connectors === void 0) return {
951
+ touched: [],
952
+ removed: []
953
+ };
948
954
  const dotenv = this.dotenv.read(cwd);
949
- const touched = /* @__PURE__ */ new Set();
955
+ const touched = [];
956
+ const touchedIds = /* @__PURE__ */ new Set();
950
957
  for (const spec of channel.connectors) {
951
- const id = await this.ensureConnector(channel.name, spec, dotenv);
952
- touched.add(id);
958
+ const outcome = await this.ensureConnector(channel.name, spec, dotenv);
959
+ touched.push({
960
+ name: outcome.name,
961
+ changed: outcome.changed
962
+ });
963
+ touchedIds.add(outcome.id);
953
964
  }
954
- this.removeExtras(channel.name, touched);
965
+ return {
966
+ touched,
967
+ removed: this.removeExtras(channel.name, touchedIds)
968
+ };
955
969
  }
956
970
  async ensureConnector(channelName, spec, dotenv) {
957
971
  if (spec.type === "slack") return await this.ensureSlack(channelName, spec, dotenv);
@@ -976,11 +990,22 @@ var FunnelLocalConfigSync = class {
976
990
  existing: byName?.appToken
977
991
  });
978
992
  if (byName) {
979
- if (byName.botToken !== botToken || byName.appToken !== appToken) this.channels.updateSlackConnector(channelName, spec.name, {
980
- botToken,
981
- appToken
982
- });
983
- return byName.id;
993
+ if (byName.botToken !== botToken || byName.appToken !== appToken) {
994
+ this.channels.updateSlackConnector(channelName, spec.name, {
995
+ botToken,
996
+ appToken
997
+ });
998
+ return {
999
+ id: byName.id,
1000
+ name: spec.name,
1001
+ changed: true
1002
+ };
1003
+ }
1004
+ return {
1005
+ id: byName.id,
1006
+ name: spec.name,
1007
+ changed: false
1008
+ };
984
1009
  }
985
1010
  const byToken = this.findSlackByToken(channelName, [botToken, appToken]);
986
1011
  if (byToken) {
@@ -989,14 +1014,22 @@ var FunnelLocalConfigSync = class {
989
1014
  botToken,
990
1015
  appToken
991
1016
  });
992
- return byToken.id;
1017
+ return {
1018
+ id: byToken.id,
1019
+ name: spec.name,
1020
+ changed: true
1021
+ };
993
1022
  }
994
- return this.channels.addConnector(channelName, {
995
- type: "slack",
1023
+ return {
1024
+ id: this.channels.addConnector(channelName, {
1025
+ type: "slack",
1026
+ name: spec.name,
1027
+ botToken,
1028
+ appToken
1029
+ }).id,
996
1030
  name: spec.name,
997
- botToken,
998
- appToken
999
- }).id;
1031
+ changed: true
1032
+ };
1000
1033
  }
1001
1034
  async ensureDiscord(channelName, spec, dotenv) {
1002
1035
  const byName = this.findExistingDiscord(channelName, spec.name);
@@ -1008,42 +1041,84 @@ var FunnelLocalConfigSync = class {
1008
1041
  existing: byName?.botToken
1009
1042
  });
1010
1043
  if (byName) {
1011
- if (byName.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1012
- return byName.id;
1044
+ if (byName.botToken !== botToken) {
1045
+ this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1046
+ return {
1047
+ id: byName.id,
1048
+ name: spec.name,
1049
+ changed: true
1050
+ };
1051
+ }
1052
+ return {
1053
+ id: byName.id,
1054
+ name: spec.name,
1055
+ changed: false
1056
+ };
1013
1057
  }
1014
1058
  const byToken = this.findDiscordByToken(channelName, botToken);
1015
1059
  if (byToken) {
1016
1060
  this.channels.renameConnector(channelName, byToken.name, spec.name);
1017
1061
  if (byToken.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1018
- return byToken.id;
1062
+ return {
1063
+ id: byToken.id,
1064
+ name: spec.name,
1065
+ changed: true
1066
+ };
1019
1067
  }
1020
- return this.channels.addConnector(channelName, {
1021
- type: "discord",
1068
+ return {
1069
+ id: this.channels.addConnector(channelName, {
1070
+ type: "discord",
1071
+ name: spec.name,
1072
+ botToken
1073
+ }).id,
1022
1074
  name: spec.name,
1023
- botToken
1024
- }).id;
1075
+ changed: true
1076
+ };
1025
1077
  }
1026
1078
  ensureGh(channelName, spec) {
1027
1079
  const existing = this.channels.getConnector(channelName, spec.name);
1028
1080
  if (existing && existing.type !== "gh") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "gh"`);
1029
1081
  if (existing && existing.type === "gh") {
1030
- if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
1031
- return existing.id;
1082
+ if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) {
1083
+ this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
1084
+ return {
1085
+ id: existing.id,
1086
+ name: spec.name,
1087
+ changed: true
1088
+ };
1089
+ }
1090
+ return {
1091
+ id: existing.id,
1092
+ name: spec.name,
1093
+ changed: false
1094
+ };
1032
1095
  }
1033
- return this.channels.addConnector(channelName, {
1034
- type: "gh",
1096
+ return {
1097
+ id: this.channels.addConnector(channelName, {
1098
+ type: "gh",
1099
+ name: spec.name,
1100
+ ...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
1101
+ }).id,
1035
1102
  name: spec.name,
1036
- ...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
1037
- }).id;
1103
+ changed: true
1104
+ };
1038
1105
  }
1039
1106
  ensureSchedule(channelName, spec) {
1040
1107
  const existing = this.channels.getConnector(channelName, spec.name);
1041
1108
  if (existing && existing.type !== "schedule") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "schedule"`);
1042
- if (existing && existing.type === "schedule") return existing.id;
1043
- return this.channels.addConnector(channelName, {
1044
- type: "schedule",
1045
- name: spec.name
1046
- }).id;
1109
+ if (existing && existing.type === "schedule") return {
1110
+ id: existing.id,
1111
+ name: spec.name,
1112
+ changed: false
1113
+ };
1114
+ return {
1115
+ id: this.channels.addConnector(channelName, {
1116
+ type: "schedule",
1117
+ name: spec.name
1118
+ }).id,
1119
+ name: spec.name,
1120
+ changed: true
1121
+ };
1047
1122
  }
1048
1123
  findExistingSlack(channelName, connectorName) {
1049
1124
  const existing = this.channels.getConnector(channelName, connectorName);
@@ -1077,9 +1152,10 @@ var FunnelLocalConfigSync = class {
1077
1152
  }
1078
1153
  removeExtras(channelName, touched) {
1079
1154
  const channel = this.channels.get(channelName);
1080
- if (!channel) return;
1155
+ if (!channel) return [];
1081
1156
  const stale = channel.connectors.filter((c) => !touched.has(c.id));
1082
1157
  for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
1158
+ return stale.map((c) => c.name);
1083
1159
  }
1084
1160
  async resolveField(input) {
1085
1161
  if (input.literal !== void 0 && input.envVar !== void 0) throw new Error(`${input.label} is set both as a literal and as env.${input.label.split(".").pop()}; pick one`);
@@ -4165,7 +4241,10 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
4165
4241
  if (local) {
4166
4242
  const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
4167
4243
  if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
4168
- await funnel.localConfigSync.ensure(picked, cwd);
4244
+ const synced = await funnel.localConfigSync.ensure(picked, cwd);
4245
+ for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
4246
+ else await funnel.listeners.start(picked.name, outcome.name);
4247
+ for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
4169
4248
  const exitCode = await funnel.claude.launch({
4170
4249
  channel: picked.name,
4171
4250
  cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Hub CLI that routes external events (Slack / GitHub / Discord) to Claude Code agents through subscription channels over MCP.",
5
5
  "keywords": [
6
6
  "bun",