@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.
- package/dist/bin.js +240 -240
- package/dist/gateway/daemon.js +2 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +116 -37
- package/package.json +1 -1
package/dist/gateway/daemon.js
CHANGED
|
@@ -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
|
|
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<
|
|
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 =
|
|
955
|
+
const touched = [];
|
|
956
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
950
957
|
for (const spec of channel.connectors) {
|
|
951
|
-
const
|
|
952
|
-
touched.
|
|
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
|
-
|
|
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)
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
1017
|
+
return {
|
|
1018
|
+
id: byToken.id,
|
|
1019
|
+
name: spec.name,
|
|
1020
|
+
changed: true
|
|
1021
|
+
};
|
|
993
1022
|
}
|
|
994
|
-
return
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
|
|
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)
|
|
1012
|
-
|
|
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
|
|
1062
|
+
return {
|
|
1063
|
+
id: byToken.id,
|
|
1064
|
+
name: spec.name,
|
|
1065
|
+
changed: true
|
|
1066
|
+
};
|
|
1019
1067
|
}
|
|
1020
|
-
return
|
|
1021
|
-
|
|
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
|
-
|
|
1024
|
-
}
|
|
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)
|
|
1031
|
-
|
|
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
|
|
1034
|
-
|
|
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
|
-
|
|
1037
|
-
}
|
|
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
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
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