@lovek4d/kickflip 0.11.0 → 0.11.1
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/CONTEXT.md +17 -14
- package/SKILL.md +2 -2
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +12 -3
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +8 -8
- package/board/public/app-cards.js +2 -0
- package/board/public/app-header.js +8 -0
- package/board/public/app-lanes.js +43 -19
- package/board/public/app-snapshot.js +1 -2
- package/board/public/app-threads.js +2 -2
- package/board/public/app.html +0 -1
- package/board/public/app.js +3 -25
- package/board/public/col-resize.js +25 -44
- package/board/public/prompts.js +1 -1
- package/board/public/prototype-kit.js +1 -1
- package/board/public/prototype-switcher.js +3 -8
- package/board/public/prototype.html +2 -6
- package/board/public/router.js +16 -22
- package/board/public/shell.js +3 -39
- package/board/public/style.css +42 -95
- package/board/public/ui-cards.js +27 -23
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-page-header.js +2 -2
- package/board/public/ui-prompts.js +1 -1
- package/board/public/ui-questions.js +2 -3
- package/board/public/ui.js +1 -1
- package/board/public/view-hub.js +1 -1
- package/board/public/view-questions.js +7 -26
- package/board/server.mjs +18 -18
- package/board/store.mjs +28 -28
- package/hub/hub.mjs +8 -8
- package/hub/prompts.mjs +2 -2
- package/package.json +1 -1
- package/board/public/app-needs.js +0 -248
package/hub/hub.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Bt=Object.defineProperty;var u=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Ut from"node:net";import{spawn as Fe,execFileSync as Ge}from"node:child_process";import b from"node:fs";import S from"node:path";import ze from"node:os";import{fileURLToPath as Ft}from"node:url";import Je from"node:vm";import Gt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as zt,addSubscription as Jt,removeSubscription as Kt,removeSubscriptionById as Wt,sendAll as Vt,flushQueue as Ke,queueStatus as We,saveSettings as Zt,readSettings as te,migrateSettings as Qt,setNotifications as Xt,readCodexShare as Ve,setCodexShare as Yt,readClaudeModel as qt,setClaudeModel as en,deliver as tn}from"./push.mjs";import{ownerTracker as nn}from"./presence.mjs";import{POLL_MS as Ze,answerOf as an,expired as sn,pending as rn,pendingFor as on,promptRecord as cn,pushOf as ln,updatePrompts as F,shown as un,pruneRemembered as ke,rememberRules as dn,rememberedAnswer as hn,rememberedFor as Qe}from"./prompts.mjs";import{hold as fn,heldUntil as pn,holdLine as mn,editQuiet as gn,checkEvery as wn,nextCheckAt as bn}from"./schedule.mjs";import{capital as yn,hubView as kn,settingsView as Sn,flushView as $n,SNOOZE_REFUSED as _n}from"./views.mjs";import{caffeinate as xn,setKeepAwake as vn,awakeOn as Tn,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{nodeBin as Pn}from"../bin/install.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as Se,listenState as Xe,RESUME_STATE as Cn,sendToBoards as Ye,checkListening as In,recordHubCode as An,hubCode as Hn,runningHubCode as Mn,boardCode as qe,recordLinkFaults as Nn,linksOf as Bn,hubLink as Un,hubCodeState as Fn,readJSON as et,migrateCellarNode as Gn}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as zn,applyRules as Jn,changed as nt,readHubRules as $e,writeHubRules as Kn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Vn,sidebarSites as Zn,startRoute as Qn,hubHealth as Xn,holdNeeds as Yn}from"./home.mjs";import{checkSessions as qn,compactFullContexts as ea,startingBoards as ta}from"./sessions.mjs";import{repoService as na,repoLookupRoute as aa}from"./repos.mjs";import{boardRepo as at,changeBoardRepo as sa,migrateBoardRepos as ra,migrateHubBoard as oa}from"./board-repo.mjs";import{requestBoard as ia,retryBoard as ca,dismissBoard as la,clonePercent as ua,settleStarts as da}from"./new-board.mjs";import{boardClosure as ha,closeBoard as fa,closedBoards as pa,reopenBoard as ma}from"./close-board.mjs";import{restartBoard as ga,restartable as wa}from"./restart-board.mjs";import{crossSite as ba,isJSON as _e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as xe,appFile as ya,colorsVersion as st,destOf as ka,fileVersion as Sa,pageOf as rt,shellHtml as $a}from"./shell.mjs";import{hubStream as _a}from"./stream.mjs";import{usageReader as xa,checkUsagePause as va,contextReading as Ta}from"./usage.mjs";import{readUsageGate as ne,setPauseAt as Oa,setKeepGoing as ja,setHubPause as Pa,boardPausedUntil as Ra}from"../shared/usage-gate.mjs";import{checkMachine as Ea}from"../shared/machine-gate.mjs";import{readiness as Da,readCodex as ae}from"./readiness.mjs";import{BuildError as La,startBuild as Ca,killBuild as Ia,stopTries as Aa,endUnsharedTries as Ha,tryTick as ve,initializeTries as Ma,fetchTryBranches as ot,pullNow as Na,pullMains as Ba}from"./try.mjs";import{serveFiles as Ua}from"./files.mjs";import{upgrader as Fa}from"./upgrade.mjs";import{restarter as Ga}from"./self-restart.mjs";import{fallBackFromLinear as za,migrateWaveNames as Ja,openBoardBuilders as Ka}from"../board/store.mjs";import{recordRefusal as Wa}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Va,refuse as Za,refuseUpgrade as Qa}from"./front-door.mjs";import{shareEntries as Xa,sharedRoutes as it,shareReach as Ya,routeOfPath as qa,sharedSidebar as es,shareTracker as ts}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as ns}from"../shared/service.mjs";import{cloneRoot as se}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as re,removeLinearKey as as,saveLinearKey as ss,savedClient as lt}from"./linear.mjs";import{SlackError as rs,slackStatus as os,removeSlackToken as is,saveSlackToken as cs}from"./slack.mjs";import{sendReviews as ls}from"./review-send.mjs";const G=S.dirname(Ft(import.meta.url)),us=S.join(G,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync(S.join(G,"..","package.json"),"utf8")).version,c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),z=S.join(ze.homedir(),".claude","kickflip"),dt=S.join(z,"manifest.json"),oe=S.join(z,"logs"),ht=S.join(z,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[z,oe,ht])b.mkdirSync(e,{recursive:!0});try{const e=An();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ra();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await oa();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}try{const e=await Ja();e.length&&c(`boards: lineups renamed to waves and wave files to item files on ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("wave name migration failed:",e.message)}try{const{routes:e,loginItem:t}=await Gn();e.length&&c(`manifest: ${e.map(n=>`/${n}`).join(", ")} spawn node by Homebrew's link`),t&&c("hub login item file: spawns node by Homebrew's link")}catch(e){c("Cellar node migration failed:",e.message)}const H=na(),ds=xa(),ie=xn({log:c});ie.apply();let ce={version:1,sites:{}};function g(){try{ce=JSON.parse(b.readFileSync(dt,"utf8")),ce.sites||={}}catch(e){e.code==="ENOENT"?ce={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return ce}u(g,"loadManifest");const j=new Map,M=new Set;let le=!1;const ft=5e3,ue=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),N=u(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function de(e){return S.join(ht,e+".pid")}u(de,"pidFile");function hs(e,t){try{const n=Number(b.readFileSync(de(e),"utf8")),a=n?Ge("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";a&&a.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(de(e))}catch{}}u(hs,"killStale");function fs(e,t){const n=j.get(e)||{restarts:0},a=b.openSync(S.join(oe,e+".log"),"a"),[o,...r]=t.cmd,f={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(a,`
|
|
3
3
|
--- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
|
|
4
|
-
`),n.code=Se(t)?
|
|
5
|
-
`),b.closeSync(a),Object.assign(n,{child:null,key:ue(t),state:"crashed",exitCode:null,nextStart:Date.now()+mt(n.restarts++)}),j.set(e,n);return}b.closeSync(a),b.writeFileSync(de(e),String(h.pid)),Object.assign(n,{child:h,key:ue(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&M.add(h);const l=u((_,k)=>{if(M.delete(h),n.child!==h)return;try{b.unlinkSync(de(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+mt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(hs,"start");const mt=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function he(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;fe(n,"SIGTERM"),setTimeout(()=>{M.has(n)&&fe(n,"SIGKILL")},pt)}u(he,"stop");const fe=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),a=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>a(!1)),n.on("connect",()=>a(!0)),n.on("error",()=>a(!1))})}u(Re,"probe");let gt=!0;const B=new Set;let Ee=!1;const K=new Set,fs=3e4;async function ps(e){const t=Date.now()+fs;for(;;){const n=P(g().sites,e),a=j.get(e);if(n&&a?.child&&a.up&&a.key===ue(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}u(ps,"serverAnswers");async function wt(){if(le)return;bt&&yt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),he(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),he(t),j.delete(t)));for(const t of[...J.keys()])N(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))N(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;gt&&ds(t,n);const a=j.get(t);if(a&&a.child&&a.key!==ue(n)){c(`${t} entry changed; restarting it`),he(t),a.restarts=0,a.nextStart=Date.now()+1500;continue}const o=bt&&a?.child&&a.code&&!a.stopping&&!K.has(t)&&!B.has(t)?et(n):null;if(o&&o!==a.code){if(!(await Ja(n.dir)).length){c(`${t} runs older code than its files; restarting it`),he(t),Object.assign(a,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}a.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),a.waitedOn=o}(!a||!a.child&&Date.now()>=(a.nextStart||0))&&hs(t,n)}gt=!1;for(const[t,n]of j){const a=P(e,t);a&&(n.up=n.child?await Re(a.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(wt,"reconcile");const bt=process.env.KICKFLIP_CODE_RESTARTS!=="off",yt=Fa({root:se(),files:An().files,restart:u(()=>lt("hub"),"restart"),log:c}),ms=setInterval(()=>wt().catch(e=>c("reconcile failed:",e.message)),2e3);wt().catch(e=>c("reconcile failed:",e.message));try{Xt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const kt=u(async()=>{try{for(const e of await Ga())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await kt();const gs=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+gs,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},W=an(),ws=u(()=>$.startedAt?wn({last:new Date($.startedAt),...W.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Le().catch(n=>c("poll failed:",n.message))}u(St,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=te();let a;try{a=await Pn({every:gn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){a={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=a.rate?.remaining??null,a.merged.length){try{await it({manifest:g(),log:c,routes:a.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${a.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:a.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await it({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Na({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{a.failed.push(...(await En({manifest:g()})).failed)}catch(o){a.failed.push(`incoming PRs: ${o.message}`)}a.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${a.failed.length} step${a.failed.length===1?"":"s"} failed, first: ${a.failed[0]} (logged at most hourly)`)),a.changes.length&&c(`github check: ${a.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${a.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of a.linear.moved)c(`linear: ${o} \u2192 ${r}`);a.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${a.linear.failed.length} step${a.linear.failed.length===1?"":"s"} failed, first: ${a.linear.failed[0]} (logged at most hourly)`));try{a.slack=await cs({boards:D(g())})}catch(o){a.slack={sent:[],failed:[o.message]}}for(const o of a.slack.sent)c(`slack: sent ${o}`);a.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${a.slack.failed.length} failed, first: ${a.slack.failed[0]} (logged at most hourly)`));try{a.deaf=await Cn({manifest:g()})}catch(o){a.deaf={pushed:[],failed:[o.message]}}(a.deaf.pushed.length||a.deaf.failed.length)&&c(`not listening: pushed ${a.deaf.pushed.join(", ")||"none"}${a.deaf.failed.length?`; ${a.deaf.failed.join("; ")}`:""}`);try{Mn(g())}catch(o){c("could not record link faults:",o.message)}if(a.flushed=null,a.held=fn(t,n)!==null,e&&!a.held)try{const o=await Ke();o&&(a.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=a,a})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let $t=null;function bs(){$t||=Rn({settings:te(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}u(bs,"runWakes");const Ce=new Set,I={running:null};function _t(){if(I.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),I.running=Yn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>qn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{I.running=null})}u(_t,"runSessionCheck"),await Ha(g());const ys=Date.now(),V=new Map;function Z(e){e?(X.delete(e),x.changed(e)):x.refresh()}u(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await F(n=>{const a=rn(n,{now:Date.now(),aliveSince:ys}),o=ke(n,Ie());for(const r of a)delete n.prompts[r.id];return{gone:a,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(xt,"prunePrompts");const Ie=u(()=>({running:nt(),boards:D(g())}),"promptContext");async function vt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ka(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(vt,"noteRefusal");let Q=null,Tt=null;async function pe(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,pe())):(Q=(async()=>xa({gate:ne(),resume:u(async e=>{const t=await qe(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}u(pe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),pe().catch(e=>c("usage gate failed:",e.message));try{Ra()}catch(e){c("machine check failed:",e.message)}W.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=ws().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ua({listening:u(e=>Dn(e)&&me().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),bs(),ie.apply(),Me.tick(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(le)return;le=!0,clearInterval(ms),ie.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)fe(n,"SIGTERM");let e=!1;Ia().finally(()=>{e=!0});const t=Date.now()+pt;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),fe(n,"SIGKILL");process.exit(0)}},50)}u(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,a;try{a=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of a){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=S.join(e,o.name);if(o.isDirectory())n=Math.max(n,Pt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(Pt,"newestMtime");function ks(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const a=Pt(t.dir);return jt.set(e,{at:a,checked:Date.now()}),a}u(ks,"changedAt");const Rt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function me(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const a=j.get(t)||{},o=N(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?J.has(t)?J.get(t)?"up":"down":"starting":a.child?a.up?"up":"starting":a.state==="crashed"?"down":a.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:a.exitCode??null,owner:n.owner||null,restart:ga(n),changedAt:ks(t,n)||null,created:n.created||null,log:S.join(oe,t+".log")}})}u(me,"status");const U=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),Ae=new Map,X=new Map,Et=2e4,Ss=1500;async function $s(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Ss)});return t.ok?Wn(await t.json()):null}catch{return null}}u($s,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=X.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&X.set(o.route,{at:Date.now(),needs:$s(o.port)})}const a=await Promise.all(n.map(o=>X.get(o.route).needs));return new Map(n.map((o,r)=>{const f=Xn(Ae.get(o.route)??null,a[r],X.get(o.route).at,Date.now());return f.good?Ae.set(o.route,f.good):Ae.delete(o.route),[o.route,f.needs]}))}u(Dt,"waitingCounts");const Lt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Qe(null).length,"badgeTotal"),He=new Map;function _s(e){const t=new Map,n=new Map,a={running:nt(),wakes:tt(Ln,{})};for(const o of e)try{const r=Ye(o,a);n.set(o.route,r);let f=He.get(o.route);(!f||f.listen!==r||Date.now()-f.at>=Et)&&(f={at:Date.now(),listen:r,links:Nn(o,{listen:r})},He.set(o.route,f)),t.set(o.route,f.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u(_s,"boardLinks");async function Y({fresh:e=!1}={}){const t=new Date,n=me(),a=await Dt(n,{fresh:e}),o=g(),r=D(o),{links:f,listens:h}=_s(r),l=Un(),_=new Map(r.map(i=>[i.route,va(i.owner?.session)]).filter(([,i])=>i)),k=ea(o,t.getTime(),la,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(k.map(i=>i.route)),m=new Map(r.filter(i=>Pa(i.dir)).map(i=>[i.route,ne({dir:i.dir})])),w=Vn(n,a,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=te(),C=hn(t,O);return{data:{code:l.running??null,version:dt,total:Lt(a),start:Zn(w),sites:w,hub:Qn(w,f,Bn(l)),home:b.realpathSync(ze.homedir()),starting:k,usage:(({start:i,...d})=>d)(us({codexShare:Ve(ae())})),pause:ne(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(C,t))}:{state:"on",text:"On"},away:!W.state().present},links:f,rows:n,code:l}}u(Y,"sidebarState");function Ct(e,t,n,a,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const f={...a,vary:[a.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),f["content-encoding"]="gzip"),f["content-length"]=r.length,t.writeHead(n,f),t.end(e.method==="HEAD"?void 0:r)}u(Ct,"sendText");async function xs(e,t=null){const n=t?t.sidebar((await Y()).data):(await Y()).data,a=Je.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Je.runInContext(b.readFileSync(S.join(xe,o),"utf8"),a);return Sa({sidebar:a.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(xs,"shellPage");const Me=Ua({root:se(),run:vs,log:c,pulled:u(()=>yt.restartNow(),"pulled"),awayFor:u(()=>{const{awaySince:e}=W.state();return e===null?0:Date.now()-e},"awayFor")});function vs({signal:e}){const t=S.join(oe,"kickflip-update.log");b.writeFileSync(t,`${new Date().toISOString()} kickflip update
|
|
6
|
-
`);const n=b.openSync(t,"a");return c("kickflip update: pulling the new kickflip"),new Promise(a=>{const o=Fe(
|
|
7
|
-
`).at(-1);c(`kickflip update exited ${r}: ${f}`),a(r===0?{ok:!0}:{ok:!1,error:f.replace(/^kickflip: /,"")})})})}u(
|
|
4
|
+
`),n.code=Se(t)?qe(t):null;let h;try{h=Fe(o,r,{cwd:t.cwd||t.dir,env:f,stdio:["ignore",a,a],detached:!0})}catch(_){b.writeSync(a,`hub could not start it: ${_.message}
|
|
5
|
+
`),b.closeSync(a),Object.assign(n,{child:null,key:ue(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(a),b.writeFileSync(de(e),String(h.pid)),Object.assign(n,{child:h,key:ue(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&M.add(h);const l=u((_,k)=>{if(M.delete(h),n.child!==h)return;try{b.unlinkSync(de(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||l(_.code,null)}),h.on("exit",l)}u(fs,"start");const pt=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function he(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;fe(n,"SIGTERM"),setTimeout(()=>{M.has(n)&&fe(n,"SIGKILL")},ft)}u(he,"stop");const fe=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Ut.connect({port:e,host:"127.0.0.1"}),a=u(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>a(!1)),n.on("connect",()=>a(!0)),n.on("error",()=>a(!1))})}u(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const K=new Set,ps=3e4;async function ms(e){const t=Date.now()+ps;for(;;){const n=P(g().sites,e),a=j.get(e);if(n&&a?.child&&a.up&&a.key===ue(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}u(ms,"serverAnswers");async function gt(){if(le)return;wt&&bt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(P(e,t)?Pe(P(e,t))||(c(`${t} no longer has a command; stopping it`),he(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),he(t),j.delete(t)));for(const t of[...J.keys()])N(P(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))N(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&hs(t,n);const a=j.get(t);if(a&&a.child&&a.key!==ue(n)){c(`${t} entry changed; restarting it`),he(t),a.restarts=0,a.nextStart=Date.now()+1500;continue}const o=wt&&a?.child&&a.code&&!a.stopping&&!K.has(t)&&!B.has(t)?qe(n):null;if(o&&o!==a.code){if(!(await Ka(n.dir)).length){c(`${t} runs older code than its files; restarting it`),he(t),Object.assign(a,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}a.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),a.waitedOn=o}(!a||!a.child&&Date.now()>=(a.nextStart||0))&&fs(t,n)}mt=!1;for(const[t,n]of j){const a=P(e,t);a&&(n.up=n.child?await Re(a.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(gt,"reconcile");const wt=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Ga({root:se(),files:Hn().files,restart:u(()=>ct("hub"),"restart"),log:c}),gs=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Qt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=u(async()=>{try{for(const e of await za())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const ws=6e4,De=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+ws,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},W=nn(),bs=u(()=>$.startedAt?bn({last:new Date($.startedAt),...W.state(),remaining:$.remaining}):new Date($.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&$.startedAt&&!$.running&&Le().catch(n=>c("poll failed:",n.message))}u(kt,"presenceChanged");function Le({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=te();let a;try{a=await Rn({every:wn({present:!1,remaining:$.remaining}),manifest:g()})}catch(o){a={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}if($.remaining=a.rate?.remaining??null,a.merged.length){try{await ot({manifest:g(),log:c,routes:a.merged})}catch(o){c("merge fetch failed:",o.message)}c(`merged on ${a.merged.join(", ")}: fetched and refreshing Main`),ve({manifest:g(),routes:a.merged}).catch(o=>c("merge refresh failed:",o.message))}try{await ot({manifest:g(),log:c})}catch(o){c("fetch failed:",o.message)}try{const o=await Ba({manifest:g()});o.length&&ve({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{a.failed.push(...(await Dn({manifest:g()})).failed)}catch(o){a.failed.push(`incoming PRs: ${o.message}`)}a.failed.length&&Date.now()-$.errorLoggedAt>De&&($.errorLoggedAt=Date.now(),c(`github check: ${a.failed.length} step${a.failed.length===1?"":"s"} failed, first: ${a.failed[0]} (logged at most hourly)`)),a.changes.length&&c(`github check: ${a.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${a.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of a.linear.moved)c(`linear: ${o} \u2192 ${r}`);a.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>De&&($.linearErrorLoggedAt=Date.now(),c(`linear check: ${a.linear.failed.length} step${a.linear.failed.length===1?"":"s"} failed, first: ${a.linear.failed[0]} (logged at most hourly)`));try{a.slack=await ls({boards:D(g())})}catch(o){a.slack={sent:[],failed:[o.message]}}for(const o of a.slack.sent)c(`slack: sent ${o}`);a.slack.failed.length&&Date.now()-$.slackErrorLoggedAt>De&&($.slackErrorLoggedAt=Date.now(),c(`slack sends: ${a.slack.failed.length} failed, first: ${a.slack.failed[0]} (logged at most hourly)`));try{a.deaf=await In({manifest:g()})}catch(o){a.deaf={pushed:[],failed:[o.message]}}(a.deaf.pushed.length||a.deaf.failed.length)&&c(`not listening: pushed ${a.deaf.pushed.join(", ")||"none"}${a.deaf.failed.length?`; ${a.deaf.failed.join("; ")}`:""}`);try{Nn(g())}catch(o){c("could not record link faults:",o.message)}if(a.flushed=null,a.held=pn(t,n)!==null,e&&!a.held)try{const o=await Ke();o&&(a.flushed={count:o.count,sent:o.sent,ok:o.ok},c(`push queue: sent ${o.sent} of ${o.count} queued`))}catch(o){c("push queue flush failed:",o.message)}return $.last=a,a})().finally(()=>{$.running=null}),$.running}u(Le,"runPoll");let St=null;function ys(){St||=En({settings:te(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{St=null})}u(ys,"runWakes");const Ce=new Set,I={running:null};function $t(){if(I.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),I.running=qn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>ea({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{I.running=null})}u($t,"runSessionCheck"),await Ma(g());const ks=Date.now(),V=new Map;function Z(e){e?(X.delete(e),x.changed(e)):x.refresh()}u(Z,"promptChanged");async function _t(){const{gone:e,rulesChanged:t}=await F(n=>{const a=sn(n,{now:Date.now(),aliveSince:ks}),o=ke(n,Ie());for(const r of a)delete n.prompts[r.id];return{gone:a,rulesChanged:o}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(_t,"prunePrompts");const Ie=u(()=>({running:tt(),boards:D(g())}),"promptContext");async function xt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Wa(at(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(xt,"noteRefusal");let Q=null,vt=null;async function pe(){return Q?vt??=Q.catch(()=>{}).then(()=>(vt=null,pe())):(Q=(async()=>va({gate:ne(),resume:u(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}u(pe,"checkUsageGate"),setInterval(()=>{ve({manifest:g()}).catch(e=>c("try tick failed:",e.message)),pe().catch(e=>c("usage gate failed:",e.message));try{Ea()}catch(e){c("machine check failed:",e.message)}W.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!$.running&&Date.now()>=bs().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),da({listening:u(e=>Ln(e)&&me().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ys(),ie.apply(),Me.tick(),_t().catch(e=>c("prompt expiry failed:",e.message)).finally($t)},15e3);function Tt(){if(le)return;le=!0,clearInterval(gs),ie.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of M)fe(n,"SIGTERM");let e=!1;Aa().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||M.size)&&Date.now()<t)){for(const n of M)c(`pid ${n.pid} ignored SIGTERM; killing it`),fe(n,"SIGKILL");process.exit(0)}},50)}u(Tt,"shutdown"),process.on("SIGTERM",Tt),process.on("SIGINT",Tt);const Ot=new Map;function jt(e,t={n:3e3}){let n=0,a;try{a=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of a){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=S.join(e,o.name);if(o.isDirectory())n=Math.max(n,jt(r,t));else try{n=Math.max(n,b.statSync(r).mtimeMs)}catch{}}return n}u(jt,"newestMtime");function Ss(e,t){const n=Ot.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const a=jt(t.dir);return Ot.set(e,{at:a,checked:Date.now()}),a}u(Ss,"changedAt");const Pt=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function me(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const a=j.get(t)||{},o=N(n),r=o==="files"?Pt(n.dir)?"up":"down":o==="port"?J.has(t)?J.get(t)?"up":"down":"starting":a.child?a.up?"up":"starting":a.state==="crashed"?"down":a.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:o,state:r,exitCode:a.exitCode??null,owner:n.owner||null,restart:wa(n),changedAt:Ss(t,n)||null,created:n.created||null,log:S.join(oe,t+".log")}})}u(me,"status");const U=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[t]),"esc"),Ae=new Map,X=new Map,Rt=2e4,$s=1500;async function _s(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout($s)});return t.ok?Vn(await t.json()):null}catch{return null}}u(_s,"fetchNeeds");async function Et(e,{fresh:t=!1}={}){const n=e.filter(o=>o.state==="up"&&o.owner&&o.port);for(const o of n){const r=X.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Rt)&&X.set(o.route,{at:Date.now(),needs:_s(o.port)})}const a=await Promise.all(n.map(o=>X.get(o.route).needs));return new Map(n.map((o,r)=>{const f=Yn(Ae.get(o.route)??null,a[r],X.get(o.route).at,Date.now());return f.good?Ae.set(o.route,f.good):Ae.delete(o.route),[o.route,f.needs]}))}u(Et,"waitingCounts");const Dt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+on(null).length,"badgeTotal"),He=new Map;function xs(e){const t=new Map,n=new Map,a={running:tt(),wakes:et(Cn,{})};for(const o of e)try{const r=Xe(o,a);n.set(o.route,r);let f=He.get(o.route);(!f||f.listen!==r||Date.now()-f.at>=Rt)&&(f={at:Date.now(),listen:r,links:Bn(o,{listen:r})},He.set(o.route,f)),t.set(o.route,f.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}u(xs,"boardLinks");async function Y({fresh:e=!1}={}){const t=new Date,n=me(),a=await Et(n,{fresh:e}),o=g(),r=D(o),{links:f,listens:h}=xs(r),l=Fn(),_=new Map(r.map(i=>[i.route,Ta(i.owner?.session)]).filter(([,i])=>i)),k=ta(o,t.getTime(),ua,i=>h.get(i)==="listening"&&n.some(d=>d.route===i&&d.state==="up")),y=new Set(k.map(i=>i.route)),m=new Map(r.filter(i=>Ra(i.dir)).map(i=>[i.route,ne({dir:i.dir})])),w=Zn(n,a,new Set(r.map(i=>i.route)),h,_).filter(i=>!y.has(i.route)).map(i=>m.has(i.route)?{...i,pause:m.get(i.route)}:i),O=te(),C=fn(t,O);return{data:{code:l.running??null,version:ut,total:Dt(a),start:Qn(w),sites:w,hub:Xn(w,f,Un(l)),home:b.realpathSync(ze.homedir()),starting:k,usage:(({start:i,...d})=>d)(ds({codexShare:Ve(ae())})),pause:ne(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(mn(C,t))}:{state:"on",text:"On"},away:!W.state().present},links:f,rows:n,code:l}}u(Y,"sidebarState");function Lt(e,t,n,a,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const f={...a,vary:[a.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=Gt.gzipSync(r),f["content-encoding"]="gzip"),f["content-length"]=r.length,t.writeHead(n,f),t.end(e.method==="HEAD"?void 0:r)}u(Lt,"sendText");async function vs(e,t=null){const n=t?t.sidebar((await Y()).data):(await Y()).data,a=Je.createContext({});for(const o of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Je.runInContext(b.readFileSync(S.join(xe,o),"utf8"),a);return $a({sidebar:a.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:ge(180)})}u(vs,"shellPage");const Me=Fa({root:se(),run:Ts,log:c,pulled:u(()=>bt.restartNow(),"pulled"),awayFor:u(()=>{const{awaySince:e}=W.state();return e===null?0:Date.now()-e},"awayFor")});function Ts({signal:e}){const t=S.join(oe,"kickflip-update.log");b.writeFileSync(t,`${new Date().toISOString()} kickflip update
|
|
6
|
+
`);const n=b.openSync(t,"a");return c("kickflip update: pulling the new kickflip"),new Promise(a=>{const o=Fe(Pn(),[S.join(se(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",n,n]});b.closeSync(n),o.unref(),e.addEventListener("abort",()=>{try{process.kill(-o.pid,"SIGKILL")}catch{}a({ok:!1,error:"killed after 10 minutes"})},{once:!0}),o.on("error",r=>a({ok:!1,error:r.message})),o.on("exit",r=>{const f=b.readFileSync(t,"utf8").trim().split(`
|
|
7
|
+
`).at(-1);c(`kickflip update exited ${r}: ${f}`),a(r===0?{ok:!0}:{ok:!1,error:f.replace(/^kickflip: /,"")})})})}u(Ts,"runUpdate"),Me.tick();const x=_a({targets:u(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await Y({fresh:e})).data,"sidebar"),log:c});function ge(e){let t="0";try{t=Math.floor(b.statSync(S.join(G,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(ge,"iconUrl");const Os=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:ge(192),sizes:"192x192",type:"image/png"},{src:ge(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const a of e)if(n+=a,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function v(e,t){if(!_e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(v,"readJSONBody");const Ct=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
8
8
|
<title>${U(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${ge(180)}">
|
|
9
|
-
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${rt()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Os=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await xt();const t=new Date,{data:n,links:a,rows:o,code:r}=await Y();return{...yn({now:t,st:We(t),sidebar:n,rows:o,links:a,rules:$e(),integrations:await Ea.rows(),code:r,version:dt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ve(ae()),claudeModel:en(),codex:ae(),awake:{on:vn(),unsupported:Tn()},remembered:new Map(D(g()).map(f=>[f.route,Xe(f.route)]))}),prompts:Qe(null).map(ln),remembered:Xe(null)}}u(we,"hubData");const A=u(async()=>{const e=new Date,t=ne({now:e}),n=t.manual==="hub"?On(t.resets_at,e):null;return{...kn({now:e,st:We(e),rules:$e(),linear:re(),slack:rs(),pausedUntil:n}),closed:fa()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function js(e,t,n,a=null){const o=u((s,i,d,p={})=>{t.writeHead(s,{"content-type":i,"cache-control":"no-store",...p}),t.end(d)},"send"),r=u((s,i)=>{const d=Hn();return o(s,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),f=u(({route:s,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${s} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:s})),"starting"),h=Os(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await da(_[1]))}catch(s){return r(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let s;try{const i=await v(e);if(s=i.route,l==="/_hub/reopen-board"){const T=await pa(s);return x.refresh(),r(200,T)}if(B.has(s))return r(409,{error:"That board is already closing."});B.add(s);const d=u(()=>B.delete(s),"release");t.once("finish",d),t.once("close",d),await I.running;const p=await ha(s,i.name);return x.refresh(),r(200,p)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await v(e)).route,K.has(s))return r(409,{error:"That board is already restarting."});K.add(s),await I.running;const i=await ma(s,{serverUp:ps,portUp:Re,onStarting:u(d=>c(`restart /${s}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${s}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return s&&c(`restart /${s} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{K.delete(s),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const s=lt("hub");s&&(Ee=!1,c(`hub restart failed: ${s}`))}),r(200,{restarting:!0,command:ts().restartCommand}))}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const s=await Me.check();return s.state==="clone"&&(s.commit=Ge("git",["-C",se(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,s)}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),St(W.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await v(e),i=D(g()).find(d=>d.route===s);return i?(Ce.add(s),await I.running,_t(),await I.running,He.clear(),x.changed(s),r(200,{route:s,listen:Ye(i)})):r(404,{error:"That board is not open."})}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return r(s.status||502,{error:s.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return r(s.status||502,{error:s.message})}if(l==="/_hub/repo"&&e.method==="GET")return na(n,{repos:H,manifest:g(),sendJSON:r});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(k&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?st(k[1]):await aa(k[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,s)}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return f(await oa(s,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return f(await ia(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(s){if(s.status)return o(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const s=await ca(m[1]);return x.refresh(),r(200,s)}catch(s){if(s.status)return o(s.status,"text/plain; charset=utf-8",s.message);throw s}if((l==="/_hub/build"||l==="/_hub/pull"||l==="/_hub/kill")&&e.method==="POST"){if(le)return r(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(p=>p==="forwarded"||p.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":l==="/_hub/kill"?"Serves":"Builds"} can only be ${l==="/_hub/kill"?"killed":"started"} from the host over loopback.`});let d;try{d=await v(e)}catch(p){return r(p.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Ma(d,{manifest:g()})):l==="/_hub/kill"?r(200,await Ca(d,{manifest:g()})):r(202,await La(d,{manifest:g(),log:c}))}catch(p){if(p instanceof Da)return r(p.status,{error:p.message,...p.ask?{ask:p.ask}:{}});throw p}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(S.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",Ts());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(S.join(G,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const s=n.searchParams.get("v")===rt()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ls),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(S.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(S.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:s}=await Y({fresh:n.searchParams.get("fresh")==="1"});return r(200,a?a.sidebar(s):s)}if(l==="/_hub/stream"&&e.method==="GET"){if(a){const s={login:a.login,routes:a.routes,res:t};be.add(s),e.on("close",()=>be.delete(s)),e.on("close",q.watching(a.login))}x.handle(e,t,a&&{routes:a.routes,view:a.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const s=P(g().sites,O[1]);if(!Se(s))return r(404,{error:"That board is not open."});const i=Qa(tt(S.join(s.dir,"board.json"),{})),d=i.map(L=>L.login),p=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=q.people(O[1],d).map((L,ee)=>({...L,level:i[ee].level}));return r(200,{people:T,refused:q.refusals(O[1],d),link:p?`https://${p}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const s=ba(l.slice(10));if(!s){t.writeHead(404).end();return}const i=S.extname(s),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],p=n.searchParams.get("v")===ka(s)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(s),{"cache-control":p}):Ct(e,t,200,{"content-type":d,"cache-control":p},b.readFileSync(s))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await A());if(l==="/_hub/push/key")return r(200,{key:Jt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Kt(await v(e),`https://${s}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const s=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:s.filter(i=>i.status>=200&&i.status<300).length,results:s})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),i=s.get("set"),d=i==="on"?{on:s.get("value")==="on"}:i==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,p="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:p}):o(400,"text/plain; charset=utf-8",p);let T;try{T=await Yt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await A()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{xn(s.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return ie.apply(),h?r(200,await A()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{ja(s.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await pe(),h?r(200,await A()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{qt(s.get("value"),ae({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{tn(s.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?Ta(s.get("value")):Oa(s.get("value")),await pe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(ns(),c("linear: key removed"),await kt()):c(`linear: key ${(await as(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await A()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(os(),c("slack: token removed")):c(`slack: token ${(await is(s.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof ss))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await A()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const s=re();if(!s.set||s.refused)return r(200,s);const i=ut();try{const d=await i.teams();return r(200,{...re(),teams:d.map(({key:p,name:T})=>({key:p,name:T})),projects:d.flatMap(p=>p.projects.map(T=>({id:T.id,name:T.name,team:p.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...re(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const s=ut();if(!s)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(p){return r(p.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await s.createProject(i.team,d))}catch(p){if(!(p instanceof Oe))throw p;return r(p.status,{error:p.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>s.has(d)).map(d=>[d,s.get(d).trim()]));s.get("set")==="hold"&&(i.hold=s.get("value")||"off");try{Qt(mn(te(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await A()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let s,i;try{s=await v(e,64*1024),i=zn($e(),s.rules,s.base??[]),at(i.changes)&&Jn(i.rules)}catch(p){if(!(p instanceof Gn)&&!(p instanceof SyntaxError))throw p;return r(400,{error:p.message})}if(!at(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await qe(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(p=>p.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const s=await Ke();return s&&c(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?r(200,Sn(s)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const s=await v(e);return r(200,await Le({flush:s.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await v(e,8*1024*1024)}catch(p){if(p.status)throw p;return r(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await F(p=>{const T=Ie(),L=T.boards.find(Be=>Be.owner?.session&&Be.owner.session===s.session_id)||null;ke(p,T)&&x.refresh();const ee=dn(p,s);if(ee)return{answer:ee};const ye=on(s,{board:L,live:T.running?.get(s.session_id)});return p.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await vt(s),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(p=>c(`prompt ${d.id}: push failed: ${p.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let s;try{s=await v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return s?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await vt(s)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?Ps(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(js,"hubRoute");async function Ps(e,t,{id:n,op:a,url:o,sendJSON:r}){if(a==="wait"&&e.method==="GET"){const f=u(()=>F(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await f();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ze,Ze));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await f();return k?k.answer?h(k):r(200,{state:"pending"}):r(404,{state:"gone"})}if(a==="answer"&&e.method==="POST"){const f=await v(e),h=await F(k=>{const y=P(k.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=sn(y,f);if(w)return{status:400,error:w};y.answer=m;const O=Ie();return ke(k,O),un(k,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(l.route),r(200,{ok:!0})}if(a===null&&e.method==="DELETE"){const f=await F(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return f&&(Z(f.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(Ps,"promptRoute");const Rs=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function At(e,t){const n=j.get(e)||{},a=N(t);return a==="port"?"stopped":a==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(At,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":At(t,n)}),e.end(Es(t,n))}u(Ht,"sendDown");function Es(e,t){const n=j.get(e)||{},a=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${a}`},r=At(e,t),f=r==="code"||r==="starting"?"wait":"down",h=N(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${It(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${st()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),js=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON");async function we(e){await _t();const t=new Date,{data:n,links:a,rows:o,code:r}=await Y();return{...kn({now:t,st:We(t),sidebar:n,rows:o,links:a,rules:$e(),integrations:await Da.rows(),code:r,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ve(ae()),claudeModel:qt(),codex:ae(),awake:{on:Tn(),unsupported:On()},remembered:new Map(D(g()).map(f=>[f.route,Qe(f.route)]))}),prompts:rn().map(un),remembered:Qe(null)}}u(we,"hubData");const A=u(async()=>{const e=new Date,t=ne({now:e}),n=t.manual==="hub"?jn(t.resets_at,e):null;return{...Sn({now:e,st:We(e),rules:$e(),linear:re(),slack:os(),pausedUntil:n}),closed:pa()}},"settingsData"),R=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ps(e,t,n,a=null){const o=u((s,i,d,p={})=>{t.writeHead(s,{"content-type":i,"cache-control":"no-store",...p}),t.end(d)},"send"),r=u((s,i)=>{const d=Mn();return o(s,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),f=u(({route:s,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${s} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:s})),"starting"),h=js(e),l=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(l);if(_&&e.method==="GET")try{return r(200,await ha(_[1]))}catch(s){return r(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(l)&&e.method==="POST"){let s;try{const i=await v(e);if(s=i.route,l==="/_hub/reopen-board"){const T=await ma(s);return x.refresh(),r(200,T)}if(B.has(s))return r(409,{error:"That board is already closing."});B.add(s);const d=u(()=>B.delete(s),"release");t.once("finish",d),t.once("close",d),await I.running;const p=await fa(s,i.name);return x.refresh(),r(200,p)}catch(i){return r(i.status||400,{error:i.message})}}if(l==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await v(e)).route,K.has(s))return r(409,{error:"That board is already restarting."});K.add(s),await I.running;const i=await ga(s,{serverUp:ms,portUp:Re,onStarting:u(d=>c(`restart /${s}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${s}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return s&&c(`restart /${s} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{K.delete(s),x.refresh()}}if(l==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?r(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const s=ct("hub");s&&(Ee=!1,c(`hub restart failed: ${s}`))}),r(200,{restarting:!0,command:ns().restartCommand}))}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/check-update"&&e.method==="POST")try{await v(e);const s=await Me.check();return s.state==="clone"&&(s.commit=Ge("git",["-C",se(),"rev-parse","--short","HEAD"],{encoding:"utf8"}).trim()),r(200,s)}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/presence"&&e.method==="POST")return await v(e),kt(W.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(l==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await v(e),i=D(g()).find(d=>d.route===s);return i?(Ce.add(s),await I.running,$t(),await I.running,He.clear(),x.changed(s),r(200,{route:s,listen:Xe(i)})):r(404,{error:"That board is not open."})}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/repos"&&e.method==="GET")try{return r(200,await H.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return r(s.status||502,{error:s.message})}if(l==="/_hub/branches"&&e.method==="GET")try{return r(200,await H.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return r(s.status||502,{error:s.message})}if(l==="/_hub/repo"&&e.method==="GET")return aa(n,{repos:H,manifest:g(),sendJSON:r});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(l);if(k&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?at(k[1]):await sa(k[1],await v(e),{repos:H});return e.method==="POST"&&x.refresh(),r(200,s)}catch(s){return r(s.status||400,{error:s.message})}if(l==="/_hub/new-board"&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await v(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return f(await ia(s,{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(l);if(y&&e.method==="POST"){if(!_e(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return f(await ca(y[1],{repos:H,changed:u(()=>x.refresh(),"changed")}))}catch(s){if(s.status)return o(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(l);if(m&&e.method==="DELETE")try{const s=await la(m[1]);return x.refresh(),r(200,s)}catch(s){if(s.status)return o(s.status,"text/plain; charset=utf-8",s.message);throw s}if((l==="/_hub/build"||l==="/_hub/pull"||l==="/_hub/kill")&&e.method==="POST"){if(le)return r(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(p=>p==="forwarded"||p.startsWith("x-forwarded-")))return r(403,{error:`${l==="/_hub/pull"?"Pulls":l==="/_hub/kill"?"Serves":"Builds"} can only be ${l==="/_hub/kill"?"killed":"started"} from the host over loopback.`});let d;try{d=await v(e)}catch(p){return r(p.status||400,{error:l==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return l==="/_hub/pull"?r(200,await Na(d,{manifest:g()})):l==="/_hub/kill"?r(200,await Ia(d,{manifest:g()})):r(202,await Ca(d,{manifest:g(),log:c}))}catch(p){if(p instanceof La)return r(p.status,{error:p.message,...p.ask?{ask:p.ask}:{}});throw p}}if(l==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(S.join(G,"sw.js")),{"service-worker-allowed":"/"});if(l==="/manifest.webmanifest")return o(200,"application/manifest+json",Os());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(l);if(w)try{return o(200,"image/png",b.readFileSync(S.join(G,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/colors.css"){const s=n.searchParams.get("v")===st()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(us),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(l==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(S.join(xe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(l==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(S.join(xe,"hub.css")));if(l==="/_hub/sidebar.json"){const{data:s}=await Y({fresh:n.searchParams.get("fresh")==="1"});return r(200,a?a.sidebar(s):s)}if(l==="/_hub/stream"&&e.method==="GET"){if(a){const s={login:a.login,routes:a.routes,res:t};be.add(s),e.on("close",()=>be.delete(s)),e.on("close",q.watching(a.login))}x.handle(e,t,a&&{routes:a.routes,view:a.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(l);if(O&&e.method==="GET"){const s=P(g().sites,O[1]);if(!Se(s))return r(404,{error:"That board is not open."});const i=Xa(et(S.join(s.dir,"board.json"),{})),d=i.map(L=>L.login),p=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=q.people(O[1],d).map((L,ee)=>({...L,level:i[ee].level}));return r(200,{people:T,refused:q.refusals(O[1],d),link:p?`https://${p}/${O[1]}/`:null})}if(l.startsWith("/_hub/app/")){const s=ya(l.slice(10));if(!s){t.writeHead(404).end();return}const i=S.extname(s),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],p=n.searchParams.get("v")===Sa(s)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(s),{"cache-control":p}):Lt(e,t,200,{"content-type":d,"cache-control":p},b.readFileSync(s))}if(l==="/_hub/api/hub")return r(200,await we(e));if(l==="/_hub/api/settings")return r(200,await A());if(l==="/_hub/push/key")return r(200,{key:zt()});if(l==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Jt(await v(e),`https://${s}/`,e.headers["user-agent"]))}if(l==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Wt((await v(e))?.id)});if(l==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Kt((await v(e))?.endpoint)});if(l==="/_hub/push/test"&&e.method==="POST"){const s=await Vt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:s.filter(i=>i.status>=200&&i.status<300).length,results:s})}if(l==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),i=s.get("set"),d=i==="on"?{on:s.get("value")==="on"}:i==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,p="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:p}):o(400,"text/plain; charset=utf-8",p);let T;try{T=await Xt(d)}catch(L){if(L.status!==400)throw L;return h?r(400,{error:_n}):R(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?r(200,await A()):R(t,"/_hub/#notifications")}if(l==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{vn(s.get("keep_awake")==="on")}catch(i){if(i.status!==409)throw i;return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message)}return ie.apply(),h?r(200,await A()):R(t,"/_hub/#keep-awake")}if(l==="/_hub/settings/hub-pause"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Pa(s.get("pause"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return await pe(),h?r(200,await A()):R(t,"/_hub/#pause")}if(l==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Yt(s.get("value"),ae({fresh:!0}))}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{en(s.get("value"))}catch(i){if(i.status!==400)throw i;return h?r(400,{error:i.message}):o(400,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(l)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{l.endsWith("/pause-at")?Oa(s.get("value")):ja(s.get("value")),await pe()}catch(i){if(i.status!==400&&i.status!==409)throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await we(e)):R(t,"/_hub/")}if(l==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(as(),c("linear: key removed"),await yt()):c(`linear: key ${(await ss(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Oe))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await A()):R(t,"/_hub/#linear")}if(l==="/_hub/settings/slack"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(is(),c("slack: token removed")):c(`slack: token ${(await cs(s.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof rs))throw i;return h?r(i.status,{error:i.message}):o(i.status,"text/plain; charset=utf-8",i.message)}return h?r(200,await A()):R(t,"/_hub/#slack")}if(l==="/_hub/api/linear"&&e.method==="GET"){const s=re();if(!s.set||s.refused)return r(200,s);const i=lt();try{const d=await i.teams();return r(200,{...re(),teams:d.map(({key:p,name:T})=>({key:p,name:T})),projects:d.flatMap(p=>p.projects.map(T=>({id:T.id,name:T.name,team:p.key})))})}catch(d){if(!(d instanceof Oe))throw d;return r(200,{...re(),error:d.message})}}if(l==="/_hub/linear/projects"&&e.method==="POST"){const s=lt();if(!s)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await v(e,4*1024)}catch(p){return r(p.status||400,{error:"Send the team and project name as JSON."})}const d=typeof i.name=="string"?i.name.trim():"";if(!d||d.length>80||typeof i.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(i.team))return r(400,{error:"Name the project and its team."});try{return r(200,await s.createProject(i.team,d))}catch(p){if(!(p instanceof Oe))throw p;return r(p.status,{error:p.message})}}if(l==="/_hub/settings/quiet"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),i=Object.fromEntries(["start","end"].filter(d=>s.has(d)).map(d=>[d,s.get(d).trim()]));s.get("set")==="hold"&&(i.hold=s.get("value")||"off");try{Zt(gn(te(),i))}catch(d){if(d.status!==400)throw d;return h?r(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?r(200,await A()):R(t,"/_hub/#quiet")}if(l==="/_hub/settings/rules"&&e.method==="POST"){let s,i;try{s=await v(e,64*1024),i=Jn($e(),s.rules,s.base??[]),nt(i.changes)&&Kn(i.rules)}catch(p){if(!(p instanceof zn)&&!(p instanceof SyntaxError))throw p;return r(400,{error:p.message})}if(!nt(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(p=>p.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(l==="/_hub/push/flush"&&e.method==="POST"){const s=await Ke();return s&&c(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?r(200,$n(s)):R(t,"/_hub/")}if(l==="/_hub/poll"&&e.method==="POST"){const s=await v(e);return r(200,await Le({flush:s.flush!==!1}))}if(l==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await v(e,8*1024*1024)}catch(p){if(p.status)throw p;return r(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await F(p=>{const T=Ie(),L=T.boards.find(Be=>Be.owner?.session&&Be.owner.session===s.session_id)||null;ke(p,T)&&x.refresh();const ee=hn(p,s);if(ee)return{answer:ee};const ye=cn(s,{board:L,live:T.running?.get(s.session_id)});return p.prompts[ye.id]=ye,{rec:ye}});return i?r(200,{state:"answered",answer:i}):(Z(d.route),await xt(s),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),tn(ln(d),{kind:"prompt",immediate:!0}).catch(p=>c(`prompt ${d.id}: push failed: ${p.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(l==="/_hub/permission-denied"&&e.method==="POST"){let s;try{s=await v(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return s?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await xt(s)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(l);return C?Rs(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:r}):!1}u(Ps,"hubRoute");async function Rs(e,t,{id:n,op:a,url:o,sendJSON:r}){if(a==="wait"&&e.method==="GET"){const f=u(()=>F(y=>{const m=P(y.prompts,n);return m?.answer?delete y.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=u(y=>(Z(y.route),r(200,{state:"answered",answer:y.answer})),"take"),l=await f();if(!l)return r(404,{state:"gone"});if(l.answer)return h(l);const _=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||Ze,Ze));if(await new Promise(y=>{const m=V.get(n)||new Set;V.set(n,m);const w=u(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),y()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await f();return k?k.answer?h(k):r(200,{state:"pending"}):r(404,{state:"gone"})}if(a==="answer"&&e.method==="POST"){const f=await v(e),h=await F(k=>{const y=P(k.prompts,n);if(!y)return{status:404,error:"That prompt has expired."};if(y.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=an(y,f);if(w)return{status:400,error:w};y.answer=m;const O=Ie();return ke(k,O),dn(k,y,O),{rec:y,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:l,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(l.route),r(200,{ok:!0})}if(a===null&&e.method==="DELETE"){const f=await F(h=>{const l=P(h.prompts,n);return delete h.prompts[n],l});return f&&(Z(f.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}u(Rs,"promptRoute");const Es=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},a=N(t);return a==="port"?"stopped":a==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}u(It,"downWhy");function At(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(Ds(t,n))}u(At,"sendDown");function Ds(e,t){const n=j.get(e)||{},a=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${a}`},r=It(e,t),f=r==="code"||r==="starting"?"wait":"down",h=N(t),l=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${Ct(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
|
|
10
10
|
<a class="back" href="/_hub/">\u2039 Hub</a>
|
|
11
11
|
<h1>${U(t.title||e)} isn't answering</h1>
|
|
12
12
|
<p class="lede">${U(l)}</p>
|
|
13
13
|
<h2 class="label">Site</h2>
|
|
14
|
-
<div class="group">${
|
|
14
|
+
<div class="group">${Es({icon:Wn(e,t),title:U(t.title||e),sub:`<span class="st ${f}">${U(o[r])}</span>`})}</div>
|
|
15
15
|
<p class="note">This page retries every 5 seconds.</p>
|
|
16
|
-
</main></body></html>`}u(
|
|
16
|
+
</main></body></html>`}u(Ds,"downPage");const Ne=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),q=ts({file:S.join(z,"share-seen.json")}),be=new Set;function Ht(){const e=g().sites;for(const t of be){const n=it(t.login,e);[...t.routes].some(([a,o])=>n.get(a)!==o)&&(be.delete(t),t.res.destroy())}Ha()}u(Ht,"endUnshared"),setInterval(Ht,2e3).unref();async function Mt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const a=e.headers["tailscale-user-name"]||null,{pathname:o}=Ne(e),r=it(n,g().sites),f=r.size?Ya({method:e.method,pathname:o,routes:r}):"other",h=qa(o);if(f==="other")return(!r.size||h)&&q.refuse(n,a,h),{verdict:"other"};if(f!=="pass")return{verdict:f};q.visit(n,a,h);const l=await Te.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:u(_=>es(_,r,l?.name||l?.login),"sidebar")}}}u(Mt,"access");const Nt=Ue.createServer((e,t)=>{Mt(e).then(({verdict:n,viewer:a})=>{n==="owner"||n==="shared"?Ls(e,t,a):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Za(e,t,n)})});function Ls(e,t,n=null){const a=Ne(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ba(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:a.pathname,search:a.search,dest:ka(e.headers),sites:o})==="shell"){const m=a.pathname==="/"?"":a.pathname.startsWith("/_hub/")?a.pathname.slice(1):decodeURIComponent(a.pathname.split("/")[1]);vs(m,n).then(w=>Lt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(a.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+a.search}).end();return}if(a.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:me()},null,2));return}if(a.pathname==="/_hub/needs.json"){const m=me();Et(m,{fresh:a.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Dt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(a.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(a.pathname==="/sw.js"||a.pathname==="/manifest.webmanifest"||a.pathname.startsWith("/_hub/")){Ps(e,t,a,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const r=a.pathname.split("/")[1],f=P(g().sites,r);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${Ct("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${U(r)}</h1></main></body></html>`);return}if(a.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+a.search}).end();return}const h="/"+r,l=n?a.pathname.slice(h.length)+a.search:e.url.slice(h.length)||"/",_=rt({pathname:a.pathname,dest:"document",sites:o})==="shell";if(N(f)==="files"){if(!Pt(f.dir)){At(t,r,f);return}Ua(e,t,{dir:f.dir,rest:l,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const y=Ue.request({host:"127.0.0.1",port:f.port,method:e.method,path:l,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),!n&&e.method==="POST"&&l.split("?")[0]==="/api/share"&&t.on("finish",Ht),y.on("error",()=>{if(t.headersSent){t.destroy();return}At(t,r,f)}),e.pipe(y)}u(Ls,"serve"),Nt.on("upgrade",async(e,t,n)=>{const{verdict:a}=await Mt(e);if(a!=="owner"){Qa(t,a==="shared"?"view-only":a==="home"?"other":a);return}const o=Ne(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const f=`/${o}`;Va({req:e,socket:t,head:n,port:r.port,path:e.url.slice(f.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${f}`]})}),Nt.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
|
package/hub/prompts.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var j=Object.defineProperty;var s=(e,t)=>j(e,"name",{value:t,configurable:!0});import S from"node:fs";import
|
|
2
|
-
`),S.renameSync(t,
|
|
1
|
+
var j=Object.defineProperty;var s=(e,t)=>j(e,"name",{value:t,configurable:!0});import S from"node:fs";import _ from"node:os";import O from"node:path";import E from"node:crypto";import{withFileLock as D}from"./locks.mjs";import{routeRecord as l}from"./routes.mjs";const d=O.join(_.homedir(),".claude","kickflip","prompts.json"),v=10*6e4,V=25e3,x=6e4,X="No answer from the owner; ask this as an open question on your board's card instead.",Y="The hub can't be reached, so the owner can't answer this prompt; ask this as an open question on your board's card instead.",Z="The hub dropped this prompt; ask this as an open question on your board's card instead.",k="The owner denied this from the hub.",F="The owner skipped this question; ask it as an open question on your board's card instead.",p="AskUserQuestion",h={Edit:"edit",MultiEdit:"edit",Write:"write",Read:"read",NotebookEdit:"edit"},w=s((e,t)=>{const r=String(e??"").replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r},"cut"),b=s(e=>e?.file_path||e?.notebook_path||"","fileOf"),N=s(e=>{try{return new URL(e).host}catch{return String(e||"")}},"hostOf");function W(e,t={}){if(e==="Bash")return`run ${w(t.command,60)}`;if(l(h,e))return`${l(h,e)} ${O.basename(b(t))||"a file"}`;if(e==="WebFetch")return`fetch ${N(t.url)}`;if(e==="WebSearch")return`search the web for "${w(t.query,50)}"`;const r=/^mcp__(.+?)__(.+)$/.exec(e||"");return r?`use ${r[1]} ${r[2]}`:`use ${e}`}s(W,"actionOf");function T(e,t={}){if(e==="Bash")return String(t.command??"");if(l(h,e))return b(t);if(e==="WebFetch")return String(t.url??"");if(e==="WebSearch")return String(t.query??"");const r=JSON.stringify(t);return r==="{}"?"":w(r,600)}s(T,"detailOf");function P(e){const t=(Array.isArray(e?.permission_suggestions)?e.permission_suggestions:[]).filter(n=>n?.type==="addRules"&&n.behavior==="allow"&&Array.isArray(n.rules)&&n.rules.length).map(n=>({type:"addRules",rules:n.rules,behavior:"allow",destination:"session"}));if(t.length)return t;const r=e?.tool_name,o=e?.tool_input||{},i=r==="Bash"&&o.command?o.command:l(h,r)&&b(o).startsWith("/")?`/${b(o)}`:r==="WebFetch"&&o.url?`domain:${N(o.url)}`:null;return[{type:"addRules",rules:[i?{toolName:r,ruleContent:i}:{toolName:r}],behavior:"allow",destination:"session"}]}s(P,"alwaysRules");const R=s(e=>e.flatMap(t=>t.rules).map(t=>t.ruleContent?`${t.toolName}(${t.ruleContent})`:t.toolName).join(", "),"ruleText"),g=s(e=>Number.isInteger(e?.pid)&&e.pid>0&&typeof e.procStart=="string"&&Number.isFinite(Date.parse(`${e.procStart.trim().replace(/\s+/g," ")} UTC`))?{pid:e.pid,procStart:e.procStart}:null,"runtimeOf"),$=s((e,t)=>!!(g(e)&&g(t)&&e.pid===t.pid&&e.procStart===t.procStart),"sameRuntime"),f=s(e=>typeof e?.owner?.since=="string"&&e.owner.since?e.owner.since:null,"ownerSince"),q=s((e,t,r)=>!!(t?.owner?.session===r&&e.ownerSince&&e.ownerSince===f(t)),"sameOwner");function B(e,t){const r=t?.tool_name;if(r===p||!e||e.toolName!==r||typeof r!="string")return!1;const o=e.ruleContent;if(o===void 0)return!0;if(typeof o!="string"||!o)return!1;const i=t.tool_input||{};if(r==="Bash")return typeof i.command=="string"&&o===i.command;if(l(h,r))return o.startsWith("//")&&!/[\*?\[\]{}]/.test(o)&&o.slice(1)===b(i);if(r==="WebFetch")try{const n=new URL(i.url);return["http:","https:"].includes(n.protocol)&&o===`domain:${n.host}`}catch{return!1}return!1}s(B,"matchesRule");function ee(e,{running:t,boards:r}){if(!t)return!1;let o=!1;for(const[i,n]of Object.entries(e.remembered||{})){const a=r.find(u=>n.route?u.route===n.route:u.owner?.session===i);!$(n.runtime,g(t.get(i)))||n.route&&!q(n,a,i)||a&&!f(a)?(delete e.remembered[i],o=!0):n.route===null&&a&&(n.route=a.route,n.ownerSince=f(a),o=!0)}return o}s(ee,"pruneRemembered");function te(e,t,r){if(t.answer?.choice!=="always"||t.tool===p||!t.session||!t.permissions||!$(t.runtime,g(r.running?.get(t.session))))return!1;const o=l(e.remembered,t.session),i=t.route||o?.route,n=i&&r.boards.find(c=>c.route===i)||!t.route&&r.boards.find(c=>c.owner?.session===t.session);if(t.route&&!q(t,n,t.session)||n&&(n.owner?.session!==t.session||!f(n)))return!1;e.remembered||={};const u=o&&$(o.runtime,t.runtime)&&(o.route?o.route===n?.route&&q(o,n,t.session):!0)?[...o.rules]:[];for(const c of t.permissions.flatMap(m=>m.rules))u.some(m=>m.toolName===c.toolName&&m.ruleContent===c.ruleContent)||u.push(c);return Object.defineProperty(e.remembered,t.session,{value:{route:n?.route||null,ownerSince:f(n),runtime:t.runtime,rules:u},enumerable:!0,writable:!0,configurable:!0}),!0}s(te,"rememberRules");function re(e,t){return!t.session_id||t.tool_name===p||!l(e.remembered,t.session_id)?null:l(e.remembered,t.session_id).rules.some(r=>B(r,t))?{choice:"allow"}:null}s(re,"rememberedAnswer");function oe(e,t=y()){return Object.entries(t.remembered||{}).filter(([,r])=>r.route===e).map(([r,o])=>({session:r,rules:o.rules.map(i=>R([{rules:[i]}]))}))}s(oe,"rememberedFor");function ne(e,t){return{hookSpecificOutput:{hookEventName:"PermissionRequest",decision:t?.choice==="deny"?{behavior:"deny",message:t.message||k}:t?.choice==="always"&&e?.tool_name!==p?{behavior:"allow",updatedPermissions:P(e)}:e?.tool_name===p?{behavior:"allow",updatedInput:{...e.tool_input,answers:t?.answers||{}}}:{behavior:"allow"}}}}s(ne,"hookOutput");const A=s(e=>e&&e.startsWith(_.homedir()+"/")?`~${e.slice(_.homedir().length)}`:e||"","tilde");function C(e){return(Array.isArray(e?.questions)?e.questions:[]).filter(r=>typeof r?.question=="string").map(r=>({question:r.question,header:typeof r.header=="string"?r.header:"",multiSelect:r.multiSelect===!0,options:(Array.isArray(r.options)?r.options:[]).filter(o=>typeof o?.label=="string").map(o=>({label:o.label,description:typeof o.description=="string"?o.description:""}))}))}s(C,"questionsOf");function se(e,{board:t,live:r,now:o=new Date}){const i=String(e.tool_name),n=i===p,a=n?null:P(e),u=o.toISOString();return{id:"p"+E.randomBytes(6).toString("hex"),session:String(e.session_id||""),route:t?.route||null,title:t?.title||A(e.cwd)||"A background session",cwd:String(e.cwd||""),tool:i,action:n?null:W(i,e.tool_input),detail:n?"":T(i,e.tool_input),rule:n?null:R(a),questions:n?C(e.tool_input):null,permissions:a,runtime:g(r),ownerSince:f(t),at:u,deadline:new Date(o.getTime()+v).toISOString(),seen:u,answer:null}}s(se,"promptRecord");function ie(e,t,r=new Date){const o=r.toISOString();if(e.questions){if(t?.choice==="deny")return{answer:{choice:"deny",message:F,at:o}};const i=t?.answers&&typeof t.answers=="object"?t.answers:{};for(const n of e.questions){const a=String(l(i,n.question)??"").split(n.multiSelect?", ":"\0").filter(Boolean);if(!a.length)return{error:`Pick an answer to "${n.question}".`};if(!a.every(u=>n.options.some(c=>c.label===u)))return{error:`"${l(i,n.question)}" isn't an option of "${n.question}".`}}return{answer:{choice:"allow",answers:Object.fromEntries(e.questions.map(n=>[n.question,String(l(i,n.question))])),at:o}}}return["allow","always","deny"].includes(t?.choice)?{answer:{choice:t.choice,...t.choice==="deny"?{message:k}:{},at:o}}:{error:"Pass choice allow, always or deny."}}s(ie,"answerOf");function ae(e,{now:t=Date.now(),aliveSince:r=0}={}){return Object.values(e.prompts||{}).filter(o=>t-Date.parse(o.deadline)>x?!0:o.answer?t-Date.parse(o.answer.at)>x:t-Math.max(Date.parse(o.seen),r)>x)}s(ae,"expired");function y(){try{const e=JSON.parse(S.readFileSync(d,"utf8"));return{prompts:e?.prompts&&typeof e.prompts=="object"?e.prompts:{},remembered:e?.remembered&&typeof e.remembered=="object"?e.remembered:{}}}catch{return{prompts:{},remembered:{}}}}s(y,"readPrompts");function I(e){const t=`${d}.${process.pid}.tmp`;S.mkdirSync(O.dirname(d),{recursive:!0}),S.writeFileSync(t,JSON.stringify(e,null,2)+`
|
|
2
|
+
`),S.renameSync(t,d)}s(I,"savePrompts");const L=s(e=>D(d,e),"withPromptsLock");function ue(e){return L(()=>{const t=y(),r=e(t);return I(t),r})}s(ue,"updatePrompts");function ce(e,t){const r=new Set(t.filter(Boolean));for(const o of r)delete e.remembered[o];for(const[o,i]of Object.entries(e.prompts))r.has(i.session)&&delete e.prompts[o]}s(ce,"dropSessionPrompts");function M(e=y()){return Object.values(e.prompts).filter(t=>!t.answer).sort((t,r)=>t.at.localeCompare(r.at))}s(M,"pending");const le=s((e,t=y())=>M(t).filter(r=>r.route===e),"pendingFor"),me=s(({id:e,route:t,title:r,cwd:o,tool:i,action:n,detail:a,rule:u,questions:c,at:m})=>({id:e,board:t?r:null,title:r,cwd:A(o),tool:i,action:n,detail:a,rule:u,questions:c,at:m}),"shown");function pe(e){const t=e.questions?.[0];return{title:t?`${e.title} asks: ${w(t.question,90)}`:`${e.title} wants to ${e.action}`,body:t?t.options.map(r=>r.label).join(" \xB7 ")+(e.questions.length>1?` (+${e.questions.length-1} more)`:""):w(e.detail,160)||"Answer on the hub page.",url:"/_hub/",tag:`prompt-${e.id}`}}s(pe,"pushOf");export{k as DENIED,Z as DROPPED,x as GONE_AFTER,X as NO_ANSWER,V as POLL_MS,F as SKIPPED,Y as UNREACHABLE,v as WAIT_MS,W as actionOf,P as alwaysRules,ie as answerOf,ce as dropSessionPrompts,ae as expired,ne as hookOutput,B as matchesRule,M as pending,le as pendingFor,se as promptRecord,ee as pruneRemembered,pe as pushOf,y as readPrompts,te as rememberRules,re as rememberedAnswer,oe as rememberedFor,I as savePrompts,me as shown,ue as updatePrompts,L as withPromptsLock};
|