@lovek4d/kickflip 0.5.3 → 0.6.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.
Files changed (91) hide show
  1. package/CONTEXT.md +82 -46
  2. package/SKILL.md +21 -15
  3. package/bin/doctor.mjs +5 -4
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +12 -12
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/agents.mjs +2 -2
  8. package/board/arch-review.mjs +4 -4
  9. package/board/bin/board.mjs +179 -92
  10. package/board/build-script.mjs +2 -1
  11. package/board/domain.mjs +9 -7
  12. package/board/finish.mjs +4 -4
  13. package/board/headless.mjs +9 -5
  14. package/board/onboarding.mjs +11 -0
  15. package/board/prototype-base.mjs +1 -0
  16. package/board/prototype-page.mjs +1 -0
  17. package/board/public/app-arch.js +1 -1
  18. package/board/public/app-cards.js +40 -47
  19. package/board/public/app-header.js +6 -7
  20. package/board/public/app-lanes.js +19 -45
  21. package/board/public/app-needs.js +36 -31
  22. package/board/public/app-snapshot.js +43 -70
  23. package/board/public/app-threads.js +42 -46
  24. package/board/public/app.html +9 -4
  25. package/board/public/app.js +78 -32
  26. package/board/public/hub-kit.js +62 -2
  27. package/board/public/hub.css +2 -0
  28. package/board/public/new-task.js +8 -6
  29. package/board/public/progress-review.html +46 -0
  30. package/board/public/progress-review.js +158 -0
  31. package/board/public/prototype-kit.js +109 -4
  32. package/board/public/prototype-switcher.js +130 -31
  33. package/board/public/prototype.html +57 -3
  34. package/board/public/router.js +7 -6
  35. package/board/public/shell.js +65 -24
  36. package/board/public/sidebar.css +6 -0
  37. package/board/public/sidebar.js +19 -69
  38. package/board/public/style.css +116 -73
  39. package/board/public/ui-cards.js +78 -57
  40. package/board/public/ui-enums.js +2 -2
  41. package/board/public/ui-header.js +23 -16
  42. package/board/public/ui-hub.js +19 -5
  43. package/board/public/ui-links.js +7 -7
  44. package/board/public/ui-order.js +1 -1
  45. package/board/public/ui-page-header.js +19 -7
  46. package/board/public/ui-patch.js +7 -33
  47. package/board/public/ui-progress-bar.js +28 -0
  48. package/board/public/ui-questions.js +25 -17
  49. package/board/public/ui-sidebar.js +31 -23
  50. package/board/public/ui-snapshot.js +44 -53
  51. package/board/public/ui-threads.js +57 -28
  52. package/board/public/ui.js +94 -18
  53. package/board/public/view-hub-settings.js +12 -58
  54. package/board/public/view-hub.js +5 -2
  55. package/board/public/view-notes.js +27 -7
  56. package/board/public/view-questions.js +90 -68
  57. package/board/public/view-settings.js +133 -15
  58. package/board/render-check.mjs +10 -0
  59. package/board/review.mjs +1 -1
  60. package/board/server.mjs +20 -19
  61. package/board/stop.mjs +1 -0
  62. package/board/store.mjs +29 -18
  63. package/board/sync.mjs +2 -0
  64. package/board/tickets.mjs +3 -3
  65. package/board/transcript.mjs +2 -2
  66. package/board/worktree.mjs +22 -21
  67. package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
  68. package/hooks-plugin/hooks/usage-gate.mjs +20 -3
  69. package/hub/board-repo.mjs +1 -1
  70. package/hub/boards.mjs +4 -4
  71. package/hub/front-door.mjs +2 -2
  72. package/hub/github.mjs +3 -3
  73. package/hub/hub.mjs +11 -11
  74. package/hub/incoming.mjs +2 -2
  75. package/hub/owner-checks.mjs +1 -1
  76. package/hub/pr-report.mjs +2 -2
  77. package/hub/review-send.mjs +1 -1
  78. package/hub/sessions.mjs +2 -2
  79. package/hub/share.mjs +1 -1
  80. package/hub/slack.mjs +2 -2
  81. package/hub/try.mjs +6 -6
  82. package/hub/upgrade.mjs +1 -1
  83. package/hub/usage.mjs +3 -3
  84. package/hub/views.mjs +1 -1
  85. package/package.json +1 -1
  86. package/shared/card-prs.mjs +1 -1
  87. package/shared/machine-gate.mjs +2 -0
  88. package/shared/prototype-frame.js +1 -0
  89. package/shared/tailscale.mjs +1 -1
  90. package/shared/usage-gate.mjs +1 -1
  91. package/board/public/ui-phase-bars.js +0 -17
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Bt=Object.defineProperty;var l=(e,t)=>Bt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Ft from"node:net";import{spawn as Be,execFileSync as Nt}from"node:child_process";import b from"node:fs";import k from"node:path";import Fe from"node:os";import{fileURLToPath as Gt}from"node:url";import Ne from"node:vm";import zt from"node:zlib";import{routeRecord as P}from"./routes.mjs";import{publicKey as Jt,addSubscription as Wt,removeSubscription as Kt,removeSubscriptionById as Vt,sendAll as Zt,flushQueue as Ge,queueStatus as ze,saveSettings as Qt,readSettings as q,migrateSettings as Xt,setNotifications as Yt,readCodexShare as Je,setCodexShare as qt,readClaudeModel as en,setClaudeModel as tn,deliver as nn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as We,answerOf as sn,expired as rn,pendingFor as Ke,promptRecord as on,pushOf as cn,updatePrompts as B,shown as ln,pruneRemembered as we,rememberRules as un,rememberedAnswer as dn,rememberedFor as Ve}from"./prompts.mjs";import{hold as hn,heldUntil as fn,holdLine as pn,editQuiet as mn,checkEvery as Ze,nextCheckAt as gn,LOW_POINTS as wn}from"./schedule.mjs";import{capital as bn,hubView as yn,settingsView as Sn,flushView as kn,SNOOZE_REFUSED as $n,AWAKE_REFUSED as _n}from"./views.mjs";import{caffeinate as xn,setKeepAwake as Tn,awakeUntil as Qe,awakeUnsupported as vn}from"./awake.mjs";import{checkGitHub as On,sendWakes as jn}from"./github.mjs";import{checkIncoming as Pn}from"./incoming.mjs";import{listBoards as E,isListening as Rn,isBoardSite as be,listenState as Xe,RESUME_STATE as En,sendToBoards as Ye,checkListening as Dn,recordHubCode as Ln,hubCode as Cn,runningHubCode as An,boardCode as qe,recordLinkFaults as In,linksOf as Hn,hubLink as Mn,hubCodeState as Un,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Bn,applyRules as Fn,changed as nt,readHubRules as ye,writeHubRules as Nn}from"./rules.mjs";import{siteIcon as Gn}from"./icons.mjs";import{readNeeds as zn,sidebarSites as Jn,startRoute as Wn,hubHealth as Kn}from"./home.mjs";import{checkSessions as Vn,compactFullContexts as Zn,startingBoards as Qn}from"./sessions.mjs";import{repoService as Xn,repoLookupRoute as Yn}from"./repos.mjs";import{boardRepo as at,changeBoardRepo as qn,migrateBoardRepos as ea,migrateHubBoard as ta}from"./board-repo.mjs";import{requestBoard as na,retryBoard as aa,dismissBoard as sa,clonePercent as ra,settleStarts as oa}from"./new-board.mjs";import{boardClosure as ia,closeBoard as ca,closedBoards as la,reopenBoard as ua}from"./close-board.mjs";import{restartBoard as da,restartable as ha}from"./restart-board.mjs";import{crossSite as fa,isJSON as Se}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as ke,appFile as pa,colorsVersion as st,destOf as ma,fileVersion as ga,pageOf as rt,shellHtml as wa}from"./shell.mjs";import{hubStream as ba}from"./stream.mjs";import{usageReader as ya,checkUsagePause as Sa,contextReading as ka}from"./usage.mjs";import{readUsageGate as ot,setPauseAt as $a,setKeepGoing as _a}from"../shared/usage-gate.mjs";import{readiness as xa,readCodex as ee}from"./readiness.mjs";import{BuildError as Ta,startBuild as va,stopTries as Oa,endUnsharedTries as ja,tryTick as $e,initializeTries as Pa,fetchTryBranches as it,pullNow as Ra,pullMains as Ea}from"./try.mjs";import{serveFiles as Da}from"./files.mjs";import{upgrader as La,CHECK_MS as Ca}from"./upgrade.mjs";import{restarter as Aa}from"./self-restart.mjs";import{fallBackFromLinear as Ia,openBoardBuilders as Ha}from"../board/store.mjs";import{recordRefusal as Ma}from"../board/permissions.mjs";import{hubGate as _e}from"../board/on-host.mjs";import{pipeUpgrade as Ua,refuse as Ba,refuseUpgrade as Fa}from"./front-door.mjs";import{shareOf as Na,sharedRoutes as ct,shareReach as Ga,routeOfPath as za,sharedSidebar as Ja,shareTracker as Wa}from"./share.mjs";import{restartRunner as lt}from"../board/worktree.mjs";import{hubService as Ka}from"../shared/service.mjs";import{cloneRoot as xe}from"../shared/repos.mjs";import{LinearError as Te,linearStatus as te,removeLinearKey as Va,saveLinearKey as Za,savedClient as ut}from"./linear.mjs";import{SlackError as Qa,slackStatus as Xa,removeSlackToken as Ya,saveSlackToken as qa}from"./slack.mjs";import{sendReviews as es}from"./review-send.mjs";const F=k.dirname(Gt(import.meta.url)),ts=k.join(F,"..","shared","colors.css"),dt=JSON.parse(b.readFileSync(k.join(F,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),N=k.join(Fe.homedir(),".claude","kickflip"),ht=k.join(N,"manifest.json"),ne=k.join(N,"logs"),ft=k.join(N,"run"),ve=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[N,ne,ft])b.mkdirSync(e,{recursive:!0});try{const e=Ln();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 ea();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 ta();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const C=Xn(),ns=ya(),ae=xn({log:c});ae.apply();let se={version:1,sites:{}};function g(){try{se=JSON.parse(b.readFileSync(ht,"utf8")),se.sites||={}}catch(e){e.code==="ENOENT"?se={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return se}l(g,"loadManifest");const j=new Map,A=new Set;let re=!1;const pt=5e3,oe=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Oe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),G=l(e=>Oe(e)?"cmd":e?.port?"port":"files","servesOf"),z=new Map;function ie(e){return k.join(ft,e+".pid")}l(ie,"pidFile");function as(e,t){try{const n=Number(b.readFileSync(ie(e),"utf8")),s=n?Nt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(ie(e))}catch{}}l(as,"killStale");function ss(e,t){const n=j.get(e)||{restarts:0},s=b.openSync(k.join(ne,e+".log"),"a"),[o,...r]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
2
+ var Ft=Object.defineProperty;var l=(e,t)=>Ft(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Nt from"node:net";import{spawn as Be,execFileSync as Gt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as zt}from"node:url";import Ne from"node:vm";import Wt from"node:zlib";import{routeRecord as R}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Vt,removeSubscriptionById as Zt,sendAll as Qt,flushQueue as Ge,queueStatus as ze,saveSettings as Xt,readSettings as ee,migrateSettings as Yt,setNotifications as qt,readCodexShare as We,setCodexShare as en,readClaudeModel as tn,setClaudeModel as nn,deliver as sn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as Je,answerOf as rn,expired as on,pendingFor as Ke,promptRecord as cn,pushOf as ln,updatePrompts as G,shown as un,pruneRemembered as ye,rememberRules as dn,rememberedAnswer as hn,rememberedFor as Ve}from"./prompts.mjs";import{hold as fn,heldUntil as pn,holdLine as mn,editQuiet as gn,checkEvery as Ze,nextCheckAt as wn,LOW_POINTS as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n,AWAKE_REFUSED as xn}from"./views.mjs";import{caffeinate as Tn,setKeepAwake as vn,awakeUntil as Qe,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as Se,listenState as Xe,RESUME_STATE as Ln,sendToBoards as Ye,checkListening as Cn,recordHubCode as An,hubCode as In,runningHubCode as Mn,boardCode as qe,recordLinkFaults as Hn,linksOf as Un,hubLink as Bn,hubCodeState as Fn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Nn,applyRules as Gn,changed as nt,readHubRules as ke,writeHubRules as zn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Kn,startRoute as Vn,hubHealth as Zn}from"./home.mjs";import{checkSessions as Qn,compactFullContexts as Xn,startingBoards as Yn}from"./sessions.mjs";import{repoService as qn,repoLookupRoute as es}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ts,migrateBoardRepos as ns,migrateHubBoard as ss}from"./board-repo.mjs";import{requestBoard as as,retryBoard as rs,dismissBoard as os,clonePercent as is,settleStarts as cs}from"./new-board.mjs";import{boardClosure as ls,closeBoard as us,closedBoards as ds,reopenBoard as hs}from"./close-board.mjs";import{restartBoard as fs,restartable as ps}from"./restart-board.mjs";import{crossSite as ms,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as gs,colorsVersion as at,destOf as ws,fileVersion as bs,pageOf as rt,shellHtml as ys}from"./shell.mjs";import{hubStream as Ss}from"./stream.mjs";import{usageReader as ks,checkUsagePause as $s,contextReading as _s}from"./usage.mjs";import{readUsageGate as te,setPauseAt as xs,setKeepGoing as Ts,setHubPause as vs,boardPausedUntil as Os}from"../shared/usage-gate.mjs";import{checkMachine as js}from"../shared/machine-gate.mjs";import{readiness as Ps,readCodex as ne}from"./readiness.mjs";import{BuildError as Rs,startBuild as Es,stopTries as Ds,endUnsharedTries as Ls,tryTick as xe,initializeTries as Cs,fetchTryBranches as ot,pullNow as As,pullMains as Is}from"./try.mjs";import{serveFiles as Ms}from"./files.mjs";import{upgrader as Hs,CHECK_MS as Us}from"./upgrade.mjs";import{restarter as Bs}from"./self-restart.mjs";import{fallBackFromLinear as Fs,openBoardBuilders as Ns}from"../board/store.mjs";import{recordRefusal as Gs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as zs,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Ks,sharedRoutes as it,shareReach as Vs,routeOfPath as Zs,sharedSidebar as Qs,shareTracker as Xs}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as Ys}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as qs,saveLinearKey as ea,savedClient as lt}from"./linear.mjs";import{SlackError as ta,slackStatus as na,removeSlackToken as sa,saveSlackToken as aa}from"./slack.mjs";import{sendReviews as ra}from"./review-send.mjs";const z=$.dirname(zt(import.meta.url)),oa=$.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync($.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),W=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(W,"manifest.json"),ae=$.join(W,"logs"),ht=$.join(W,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[W,ae,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 ns();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 ss();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const M=qn(),ia=ks(),re=Tn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}l(g,"loadManifest");const j=new Map,H=new Set;let ie=!1;const ft=5e3,ce=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function ca(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?Gt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}l(ca,"killStale");function la(e,t){const n=j.get(e)||{restarts:0},s=b.openSync($.join(ae,e+".log"),"a"),[i,...a]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `),n.code=be(t)?qe(t):null;let h;try{h=Be(o,r,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch($){b.writeSync(s,`hub could not start it: ${$.message}
5
- `),b.closeSync(s),Object.assign(n,{child:null,key:oe(t),state:"crashed",exitCode:null,nextStart:Date.now()+mt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(ie(e),String(h.pid)),Object.assign(n,{child:h,key:oe(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&A.add(h);const u=l(($,_)=>{if(A.delete(h),n.child!==h)return;try{b.unlinkSync(ie(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:$??_,nextStart:Date.now()+mt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${$??_})`)},"ended");h.on("error",$=>{c(`${e} spawn error: ${$.message}`),h.pid||u($.code,null)}),h.on("exit",u)}l(ss,"start");const mt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ce(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;le(n,"SIGTERM"),setTimeout(()=>{A.has(n)&&le(n,"SIGKILL")},pt)}l(ce,"stop");const le=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function je(e){return new Promise(t=>{const n=Ft.connect({port:e,host:"127.0.0.1"}),s=l(o=>{n.destroy(),t(o)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(je,"probe");let gt=!0;const I=new Set;let Pe=!1;const J=new Set,rs=3e4;async function os(e){const t=Date.now()+rs;for(;;){const n=P(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===oe(n))return!0;if(Date.now()>=t)return!1;await new Promise(o=>setTimeout(o,250))}}l(os,"serverAnswers");async function wt(){if(re)return;bt.check();const{sites:e}=g();for(const t of[...j.keys()])I.has(t)||(P(e,t)?Oe(P(e,t))||(c(`${t} no longer has a command; stopping it`),ce(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ce(t),j.delete(t)));for(const t of[...z.keys()])G(P(e,t))!=="port"&&z.delete(t);for(const[t,n]of Object.entries(e))G(n)==="port"&&z.set(t,await je(n.port));for(const[t,n]of Object.entries(e)){if(!Oe(n))continue;gt&&as(t,n);const s=j.get(t);if(s&&s.child&&s.key!==oe(n)){c(`${t} entry changed; restarting it`),ce(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const o=s?.child&&s.code&&!s.stopping&&!J.has(t)&&!I.has(t)?qe(n):null;if(o&&o!==s.code){if(!(await Ha(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ce(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==o&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=o}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ss(t,n)}gt=!1;for(const[t,n]of j){const s=P(e,t);s&&(n.up=n.child?await je(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(wt,"reconcile");const bt=Aa({root:xe(),files:Cn().files,restart:l(()=>lt("hub"),"restart"),log:c}),is=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 yt=l(async()=>{try{for(const e of await Ia())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const cs=6e4,Re=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+cs,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},H=an(),St=l(()=>y.startedAt?gn({last:new Date(y.startedAt),...H.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Ee().catch(n=>c("poll failed:",n.message))}l(kt,"presenceChanged");function Ee({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=q();let s;try{s=await On({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(o){s={at:t.toISOString(),watched:0,failed:[o.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await it({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),$e({manifest:g(),routes:s.merged}).catch(o=>c("merge refresh failed:",o.message))),await it({manifest:g(),log:c});try{const o=await Ea({manifest:g()});o.length&&$e({manifest:g(),routes:o}).catch(r=>c("pull refresh failed:",r.message))}catch(o){c("pull failed:",o.message)}try{s.failed.push(...(await Pn({manifest:g()})).failed)}catch(o){s.failed.push(`incoming PRs: ${o.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>Re&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(o=>`#${o.pr} ${o.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(o=>o.route).join(", ")||"no board"}`);for(const{id:o,state:r}of s.linear.moved)c(`linear: ${o} \u2192 ${r}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>Re&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await es({boards:E(g())})}catch(o){s.slack={sent:[],failed:[o.message]}}for(const o of s.slack.sent)c(`slack: sent ${o}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>Re&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Dn({manifest:g()})}catch(o){s.deaf={pushed:[],failed:[o.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{In(g())}catch(o){c("could not record link faults:",o.message)}if(s.flushed=null,s.held=fn(t,n)!==null,e&&!s.held)try{const o=await Ge();o&&(s.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 y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Ee,"runPoll");let $t=null;function ls(){$t||=jn({settings:q(),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})}l(ls,"runWakes");const De=new Set,L={running:null};function _t(){if(L.running||I.size||J.size)return;const e=new Set(De);De.clear(),L.running=Vn({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(()=>Zn({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(()=>{L.running=null})}l(_t,"runSessionCheck"),await Pa(g());const us=Date.now(),W=new Map;function K(e){e?(de.delete(e),x.changed(e)):x.refresh()}l(K,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await B(n=>{const s=rn(n,{now:Date.now(),aliveSince:us}),o=we(n,Le());for(const r of s)delete n.prompts[r.id];return{gone:s,rulesChanged:o}});for(const n of e)K(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(xt,"prunePrompts");const Le=l(()=>({running:tt(),boards:E(g())}),"promptContext");async function Tt(e){const t=E(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Ma(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}}l(Tt,"noteRefusal");let V=null,vt=null;async function Ce(){return V?vt??=V.catch(()=>{}).then(()=>(vt=null,Ce())):(V=(async()=>Sa({gate:ot(),resume:l(async e=>{const t=await Ye(E(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{V=null}),V)}l(Ce,"checkUsageGate"),setInterval(()=>{$e({manifest:g()}).catch(e=>c("try tick failed:",e.message)),Ce().catch(e=>c("usage gate failed:",e.message)),H.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=St().getTime()&&Ee().catch(e=>c("poll failed:",e.message))}),oa({listening:l(e=>Rn(e)&&ue().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ls(),ae.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(re)return;re=!0,clearInterval(is),ae.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of A)le(n,"SIGTERM");let e=!1;Oa().finally(()=>{e=!0});const t=Date.now()+pt;setInterval(()=>{if(!((!e||A.size)&&Date.now()<t)){for(const n of A)c(`pid ${n.pid} ignored SIGTERM; killing it`),le(n,"SIGKILL");process.exit(0)}},50)}l(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const o of s){if(t.n--<=0)break;if(o.name.startsWith(".")||o.name==="node_modules")continue;const r=k.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}l(Pt,"newestMtime");function ds(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}l(ds,"changedAt");const Rt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ue(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},o=G(n),r=o==="files"?Rt(n.dir)?"up":"down":o==="port"?z.has(t)?z.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.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:s.exitCode??null,owner:n.owner||null,restart:ha(n),changedAt:ds(t,n)||null,created:n.created||null,log:k.join(ne,t+".log")}})}l(ue,"status");const M=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),de=new Map,Et=2e4,hs=1500;async function fs(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(hs)});return t.ok?zn(await t.json()):null}catch{return null}}l(fs,"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=de.get(o.route);(t===!0||t?.has?.(o.route)||!r||Date.now()-r.at>=Et)&&de.set(o.route,{at:Date.now(),needs:fs(o.port)})}const s=await Promise.all(n.map(o=>de.get(o.route).needs));return new Map(n.map((o,r)=>[o.route,s[r]]))}l(Dt,"waitingCounts");const Lt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ae=new Map;function ps(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(En,{})};for(const o of e)try{const r=Xe(o,s);n.set(o.route,r);let p=Ae.get(o.route);(!p||p.listen!==r||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:r,links:Hn(o,{listen:r})},Ae.set(o.route,p)),t.set(o.route,p.links)}catch(r){c(`links of ${o.route} failed:`,r.message),t.set(o.route,[])}return{links:t,listens:n}}l(ps,"boardLinks");async function Z({fresh:e=!1}={}){const t=new Date,n=ue(),s=await Dt(n,{fresh:e}),o=g(),r=E(o),{links:p,listens:h}=ps(r),u=Un(),$=new Map(r.map(a=>[a.route,ka(a.owner?.session)]).filter(([,a])=>a)),_=Qn(o,t.getTime(),ra,a=>h.get(a)==="listening"&&n.some(i=>i.route===a&&i.state==="up")),S=new Set(_.map(a=>a.route)),m=Jn(n,s,new Set(r.map(a=>a.route)),h,$).filter(a=>!S.has(a.route)),w=q(),v=hn(t,w);return{data:{code:u.running??null,total:Lt(s),start:Wn(m),sites:m,hub:Kn(m,p,Mn(u)),home:b.realpathSync(Fe.homedir()),starting:_,usage:ns({codexShare:Je(ee())}),pause:ot(),upgrade:he.state(),notifications:v?v.why==="off"?{state:"off",text:"Off"}:{state:"held",text:bn(pn(v,t))}:{state:"on",text:"On"},away:!H.state().present},links:p,rows:n,code:u}}l(Z,"sidebarState");function Ct(e,t,n,s,o){let r=Buffer.isBuffer(o)?o:Buffer.from(o);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};r.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(r=zt.gzipSync(r),p["content-encoding"]="gzip"),p["content-length"]=r.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:r)}l(Ct,"sendText");async function ms(e,t=null){const n=t?t.sidebar((await Z()).data):(await Z()).data,s=Ne.createContext({});for(const o of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync(k.join(ke,o),"utf8"),s);return wa({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:fe(180)})}l(ms,"shellPage");const he=La({root:xe(),running:dt,run:gs,log:c,changed:l(()=>x.refresh(),"changed"),pulled:l(()=>bt.restartNow(),"pulled")});function gs(){const e=k.join(ne,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[k.join(xe(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",o=>n({ok:!1,error:o.message})),s.on("exit",o=>{const r=b.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${o}: ${r}`),n(o===0?{ok:!0}:{ok:!1,error:r.replace(/^kickflip: /,"")})})})}l(gs,"runUpdate"),he.check(),setInterval(()=>he.check(),Ca);const x=ba({targets:l(()=>Object.entries(g().sites).filter(([,e])=>be(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>E(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await Z({fresh:e})).data,"sidebar"),log:c});function fe(e){let t="0";try{t=Math.floor(b.statSync(k.join(F,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(fe,"iconUrl");const ws=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:fe(192),sizes:"192x192",type:"image/png"},{src:fe(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function D(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(D,"readBody");async function T(e,t){if(!Se(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await D(e,t);return n?JSON.parse(n):{}}l(T,"readJSONBody");const At=l((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
- <title>${M(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${fe(180)}">
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"),bs=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),ys=l(()=>({running:!!y.running,nextAt:St(),last:y.last,present:H.state().present,every:Ze({present:H.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<wn}),"pollState");async function pe(e){await xt();const t=new Date,{data:n,links:s,rows:o,code:r}=await Z();return{...yn({now:t,st:ze(t),poll:ys(),sidebar:n,rows:o,links:s,rules:ye(),integrations:await xa.rows(),code:r,version:dt,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Je(ee()),claudeModel:en(),codex:ee(),awake:Qe(t),remembered:new Map(E(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(ln),remembered:Ve(null)}}l(pe,"hubData");const U=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:vn()};return{...Sn({now:e,st:ze(e),rules:ye(),linear:te(),slack:Xa(),awake:t}),closed:la()}},"settingsData"),R=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ss(e,t,n,s=null){const o=l((a,i,d,f={})=>{t.writeHead(a,{"content-type":i,"cache-control":"no-store",...f}),t.end(d)},"send"),r=l((a,i)=>{const d=An();return o(a,"application/json",JSON.stringify(i),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:a,started:i})=>(i.then(({error:d})=>{d&&c(`new board /${a} didn't start: ${d}`),x.refresh()}),x.refresh(),r(200,{route:a})),"starting"),h=bs(e),u=n.pathname,$=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if($&&e.method==="GET")try{return r(200,await ia($[1]))}catch(a){return r(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let a;try{const i=await T(e);if(a=i.route,u==="/_hub/reopen-board"){const O=await ua(a);return x.refresh(),r(200,O)}if(I.has(a))return r(409,{error:"That board is already closing."});I.add(a);const d=l(()=>I.delete(a),"release");t.once("finish",d),t.once("close",d),await L.running;const f=await ca(a,i.name);return x.refresh(),r(200,f)}catch(i){return r(i.status||400,{error:i.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let a;try{if(a=(await T(e)).route,J.has(a))return r(409,{error:"That board is already restarting."});J.add(a),await L.running;const i=await da(a,{serverUp:os,portUp:je,onStarting:l(d=>c(`restart /${a}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${a}: server answers, session ${i.session} resumed`),r(200,i)}catch(i){return a&&c(`restart /${a} failed: ${i.message}`),r(i.status||400,{error:i.message})}finally{J.delete(a),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await T(e),Pe?r(409,{error:"The hub is already restarting."}):(Pe=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const a=lt("hub");a&&(Pe=!1,c(`hub restart failed: ${a}`))}),r(200,{restarting:!0,command:Ka().restartCommand}))}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/upgrade"&&e.method==="POST")try{return await T(e),r(200,{upgrade:await he.request()})}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/presence"&&e.method==="POST")return await T(e),kt(H.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:a}=await T(e),i=E(g()).find(d=>d.route===a);return i?(De.add(a),await L.running,_t(),await L.running,Ae.clear(),x.changed(a),r(200,{route:a,listen:Xe(i)})):r(404,{error:"That board is not open."})}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return r(200,await C.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return r(200,await C.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(a){return r(a.status||502,{error:a.message})}if(u==="/_hub/repo"&&e.method==="GET")return Yn(n,{repos:C,manifest:g(),sendJSON:r});const _=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(_&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?at(_[1]):await qn(_[1],await T(e),{repos:C});return e.method==="POST"&&x.refresh(),r(200,a)}catch(a){return r(a.status||400,{error:a.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!Se(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await T(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await na(a,{repos:C,changed:l(()=>x.refresh(),"changed")}))}catch(i){if(i.status)return o(i.status,"text/plain; charset=utf-8",i.message);throw i}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!Se(e.headers))return o(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await aa(S[1],{repos:C,changed:l(()=>x.refresh(),"changed")}))}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const a=await sa(m[1]);return x.refresh(),r(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(re)return r(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],i=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(i)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return r(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await T(e)}catch(f){return r(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?r(200,await Ra(d,{manifest:g()})):r(202,await va(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Ta)return r(f.status,{error:f.message});throw f}}if(u==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(k.join(F,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return o(200,"application/manifest+json",ws());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return o(200,"image/png",b.readFileSync(k.join(F,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const a=n.searchParams.get("v")===st()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(ts),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(k.join(ke,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(k.join(ke,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:a}=await Z({fresh:n.searchParams.get("fresh")==="1"});return r(200,s?s.sidebar(a):a)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const a={login:s.login,routes:s.routes,res:t};me.add(a),e.on("close",()=>me.delete(a)),e.on("close",Q.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const v=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(v&&e.method==="GET"){const a=P(g().sites,v[1]);if(!be(a))return r(404,{error:"That board is not open."});const i=Na(et(k.join(a.dir,"board.json"),{})),d=(await _e.node())?.dnsName||e.headers["x-forwarded-host"];return r(200,{people:Q.people(v[1],i),refused:Q.refusals(v[1],i),link:d?`https://${d}/${v[1]}/`:null})}if(u.startsWith("/_hub/app/")){const a=pa(u.slice(10));if(!a){t.writeHead(404).end();return}const i=k.extname(a),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[i],f=n.searchParams.get("v")===ga(a)?"public, max-age=31536000, immutable":"no-cache";return i===".png"?o(200,d,b.readFileSync(a),{"cache-control":f}):Ct(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(a))}if(u==="/_hub/api/hub")return r(200,await pe(e));if(u==="/_hub/api/settings")return r(200,await U());if(u==="/_hub/push/key")return r(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return r(200,Wt(await T(e),`https://${a}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return r(200,{count:Vt((await T(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return r(200,{count:Kt((await T(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const a=await Zt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return r(200,{sent:a.filter(i=>i.status>=200&&i.status<300).length,results:a})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await D(e)),i=a.get("set"),d=i==="on"?{on:a.get("value")==="on"}:i==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?r(400,{error:f}):o(400,"text/plain; charset=utf-8",f);let O;try{O=await Yt(d)}catch(Y){if(Y.status!==400)throw Y;return h?r(400,{error:$n}):R(t,"/_hub/?refused=snooze#notifications")}return O.dropped&&c(`notifications off: dropped ${O.dropped} queued push${O.dropped===1?"":"es"}`),h?r(200,await U()):R(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{Tn(a.get("keep_awake"))}catch(i){if(i.status===409)return h?r(409,{error:i.message}):o(409,"text/plain; charset=utf-8",i.message);if(i.status!==400)throw i;return h?r(400,{error:_n}):R(t,"/_hub/?refused=keep-awake#keep-awake")}return ae.apply(),h?r(200,await U()):R(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{qt(a.get("value"),ee({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 pe(e)):R(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{tn(a.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 pe(e)):R(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const a=new URLSearchParams(await D(e));try{u.endsWith("/pause-at")?$a(a.get("value")):_a(a.get("value")),await Ce()}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 pe(e)):R(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await D(e,4096));try{a.has("remove")?(Va(),c("linear: key removed"),await yt()):c(`linear: key ${(await Za(a.get("key"))).refused?"refused by Linear":"saved"}`)}catch(i){if(!(i instanceof Te))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 U()):R(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const a=new URLSearchParams(await D(e,4096));try{a.has("remove")?(Ya(),c("slack: token removed")):c(`slack: token ${(await qa(a.get("token"))).refused?"refused by Slack":"saved"}`)}catch(i){if(!(i instanceof Qa))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 U()):R(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const a=te();if(!a.set||a.refused)return r(200,a);const i=ut();try{const d=await i.teams();return r(200,{...te(),teams:d.map(({key:f,name:O})=>({key:f,name:O})),projects:d.flatMap(f=>f.projects.map(O=>({id:O.id,name:O.name,team:f.key})))})}catch(d){if(!(d instanceof Te))throw d;return r(200,{...te(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const a=ut();if(!a)return r(400,{error:"Add a Linear key on Hub first."});let i;try{i=await T(e,4*1024)}catch(f){return r(f.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 a.createProject(i.team,d))}catch(f){if(!(f instanceof Te))throw f;return r(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const a=new URLSearchParams(await D(e)),i=Object.fromEntries(["start","end"].filter(d=>a.has(d)).map(d=>[d,a.get(d).trim()]));a.get("set")==="hold"&&(i.hold=a.get("value")||"off");try{Qt(mn(q(),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 U()):R(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let a,i;try{a=await T(e,64*1024),i=Fn(ye(),a.rules,a.base??[]),nt(i.changes)&&Nn(i.rules)}catch(f){if(!(f instanceof Bn)&&!(f instanceof SyntaxError))throw f;return r(400,{error:f.message})}if(!nt(i.changes))return r(200,{rules:i.rules,changed:!1,events:[],failed:[]});const d=await Ye(E(g()),"rules",{scope:"hub",...i.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),r(200,{rules:i.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const a=await Ge();return a&&c(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),h?r(200,kn(a)):R(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const a=await T(e);return r(200,await Ee({flush:a.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(f){if(f.status)throw f;return r(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return r(400,{error:"Send a PermissionRequest hook input."});const{answer:i,rec:d}=await B(f=>{const O=Le(),Y=O.boards.find(Me=>Me.owner?.session&&Me.owner.session===a.session_id)||null;we(f,O)&&x.refresh();const He=dn(f,a);if(He)return{answer:He};const ge=on(a,{board:Y,live:O.running?.get(a.session_id)});return f.prompts[ge.id]=ge,{rec:ge}});return i?r(200,{state:"answered",answer:i}):(K(d.route),await Tt(a),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),nn(cn(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),r(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let a;try{a=await T(e,8*1024*1024)}catch(i){if(i.status)throw i;return r(400,{error:"Send the hook input as JSON."})}return a?.hook_event_name!=="PermissionDenied"?r(400,{error:"Send a PermissionDenied hook input."}):r(200,{noted:await Tt(a)})}const X=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return X?ks(e,t,{id:X[1],op:X[2]?.slice(1)||null,url:n,sendJSON:r}):!1}l(Ss,"hubRoute");async function ks(e,t,{id:n,op:s,url:o,sendJSON:r}){if(s==="wait"&&e.method==="GET"){const p=l(()=>B(S=>{const m=P(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(K(S.route),r(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return r(404,{state:"gone"});if(u.answer)return h(u);const $=Math.max(0,Math.min(Number(o.searchParams.get("ms"))||We,We));if(await new Promise(S=>{const m=W.get(n)||new Set;W.set(n,m);const w=l(()=>{clearTimeout(v),m.delete(w),!m.size&&W.get(n)===m&&W.delete(n),S()},"done"),v=setTimeout(w,$);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const _=await p();return _?_.answer?h(_):r(200,{state:"pending"}):r(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await T(e),h=await B(_=>{const S=P(_.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=sn(S,p);if(w)return{status:400,error:w};S.answer=m;const v=Le();return we(_,v),un(_,S,v),{rec:S,answer:m}});if(h.error)return r(h.status,{error:h.error});const{rec:u,answer:$}=h;c(`prompt ${n}: ${$.choice}`);for(const _ of[...W.get(n)||[]])_();return K(u.route),r(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await B(h=>{const u=P(h.prompts,n);return delete h.prompts[n],u});return p&&(K(p.route),c(`prompt ${n}: its hook gave up`)),r(200,{ok:!0})}return!1}l(ks,"promptRoute");const $s=l(({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)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",o=G(t),[r,p]=o==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:o==="files"?["down","down \xB7 its folder is missing"]:n.why==="code"?["wait","restarting \xB7 its code changed"]:n.child?["wait","starting \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${s}`],h=o==="cmd"?"The hub runs every site and restarts one that stops.":o==="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`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `),n.code=Se(t)?qe(t):null;let h;try{h=Be(i,a,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
+ `),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(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||u(_.code,null)}),h.on("exit",u)}l(la,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const K=new Set,ua=3e4;async function da(e){const t=Date.now()+ua;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(da,"serverAnswers");async function gt(){if(ie)return;wt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...J.keys()])U(R(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ca(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Ns(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&la(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=Bs({root:ve(),files:In().files,restart:l(()=>ct("hub"),"restart"),log:c}),ha=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Yt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const bt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const fa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=an(),yt=l(()=>y.startedAt?wn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(St,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Pn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Is({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ra({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Cn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Hn(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=pn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let kt=null;function pa(){kt||=Rn({settings:ee(),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(()=>{kt=null})}l(pa,"runWakes");const Ce=new Set,A={running:null};function $t(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.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(()=>Xn({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(()=>{A.running=null})}l($t,"runSessionCheck"),await Cs(g());const ma=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function _t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=on(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});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`)}l(_t,"prunePrompts");const Ae=l(()=>({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 Gs(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}}l(xt,"noteRefusal");let Q=null,Tt=null;async function he(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,he())):(Q=(async()=>$s({gate:te(),resume:l(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)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{js()}catch(e){c("machine check failed:",e.message)}F.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=yt().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),cs({listening:l(e=>Dn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),re.apply(),_t().catch(e=>c("prompt expiry failed:",e.message)).finally($t)},15e3);function vt(){if(ie)return;ie=!0,clearInterval(ha),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ds().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Ot=new Map;function jt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,jt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(jt,"newestMtime");function ga(e,t){const n=Ot.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=jt(t.dir);return Ot.set(e,{at:s,checked:Date.now()}),s}l(ga,"changedAt");const Pt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Pt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.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:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ps(n),changedAt:ga(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Rt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ba,"fetchNeeds");async function Et(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Rt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Et,"waitingCounts");const Dt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Ln,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Rt)&&(p={at:Date.now(),listen:a,links:Un(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(ya,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Et(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=ya(a),u=Fn(),_=new Map(a.map(o=>[o.route,_s(o.owner?.session)]).filter(([,o])=>o)),k=Yn(i,t.getTime(),is,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>Os(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Kn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=fn(t,O);return{data:{code:u.running??null,version:ut,total:Dt(s),start:Vn(w),sites:w,hub:Zn(w,p,Bn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ia({codexShare:We(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(mn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Lt(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Lt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return ys({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(Sa,"shellPage");const Ct=Hs({root:ve(),run:ka,log:c,pulled:l(()=>wt.restartNow(),"pulled")});function ka(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
+ `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[$.join(ve(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const a=b.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l(ka,"runUpdate"),Ct.check(),setInterval(()=>Ct.check(),Us);const x=Ss({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync($.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const $a=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(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):{}}l(v,"readJSONBody");const At=l((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
+ <title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),xa=l(()=>({running:!!y.running,nextAt:yt(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<bn}),"pollState");async function ge(e){await _t();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...Sn({now:t,st:ze(t),poll:xa(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Ps.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(ne()),claudeModel:tn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(un),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:On()},n=te({now:e}),s=n.manual==="hub"?jn(n.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:se(),slack:na(),awake:t,pausedUntil:s}),closed:ds()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ta(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Mn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=_a(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await ls(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await hs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await us(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,K.has(r))return a(409,{error:"That board is already restarting."});K.add(r),await A.running;const o=await fs(r,{serverUp:da,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{K.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ys().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),St(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,$t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return es(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ts(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await as(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await rs(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await os(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await As(d,{manifest:g()})):a(202,await Es(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Rs)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(oa),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Ks(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=gs(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===bs(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Lt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Kt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Vt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Qt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await qt(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:_n}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:xn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vs(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{en(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{nn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?xs(r.get("value")):Ts(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(qs(),c("linear: key removed"),await bt()):c(`linear: key ${(await ea(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(sa(),c("slack: token removed")):c(`slack: token ${(await aa(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ta))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Xt(gn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=Gn(ke(),r.rules,r.base??[]),nt(o.changes)&&zn(o.rules)}catch(f){if(!(f instanceof Nn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,$n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=hn(f,r);if(q)return{answer:q};const be=cn(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await xt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),sn(ln(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await xt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ta,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Je,Je));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=rn(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),dn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(va,"promptRoute");const Oa=l(({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)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(It,"downWhy");function Mt(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(ja(t,n))}l(Mt,"sendDown");function ja(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={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${s}`},a=It(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=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`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
- <h1>${M(t.title||e)} isn't answering</h1>
12
- <p class="lede">${M(h)}</p>
11
+ <h1>${N(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${N(u)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${$s({icon:Gn(e,t),title:M(t.title||e),sub:`<span class="st ${r}">${M(p)}</span>`})}</div>
14
+ <div class="group">${Oa({icon:Wn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(i[a])}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}l(It,"downPage");const Ie=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Q=Wa({file:k.join(N,"share-seen.json")}),me=new Set;function Ht(){const e=g().sites;for(const t of me){const n=ct(t.login,e);[...t.routes].some(s=>!n.has(s))&&(me.delete(t),t.res.destroy())}ja()}l(Ht,"endUnshared"),setInterval(Ht,2e3).unref();async function Mt(e){const t=await _e(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:o}=Ie(e),r=ct(n,g().sites),p=r.size?Ga({method:e.method,pathname:o,routes:r}):"other",h=za(o);if(p==="other")return(!r.size||h)&&Q.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Q.visit(n,s,h);const u=await _e.node();return{verdict:"shared",viewer:{login:n,routes:r,sidebar:l($=>Ja($,r,u?.name||u?.login),"sidebar")}}}l(Mt,"access");const Ut=Ue.createServer((e,t)=>{Mt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?_s(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ba(e,t,n)})});function _s(e,t,n=null){const s=Ie(e);if(e.method!=="GET"&&e.method!=="HEAD"&&fa(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:s.pathname,search:s.search,dest:ma(e.headers),sites:o})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);ms(m,n).then(w=>Ct(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(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ve,pid:process.pid},sites:ue()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=ue();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Lt(w),sites:Object.fromEntries(m.map(v=>[v.route,w.get(v.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Ss(e,t,s,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=s.pathname.split("/")[1],p=P(g().sites,r);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${M(r)}</h1></main></body></html>`);return}if(s.pathname==="/"+r){t.writeHead(302,{location:"/"+r+"/"+s.search}).end();return}const h="/"+r,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",$=rt({pathname:s.pathname,dest:"document",sites:o})==="shell";if(G(p)==="files"){if(!Rt(p.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(It(r,p));return}Da(e,t,{dir:p.dir,rest:u,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 _={...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"},S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:_},m=>{const w={...m.headers};$&&(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||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ht),S.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(It(r,p))}),e.pipe(S)}l(_s,"serve"),Ut.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Mt(e);if(s!=="owner"){Fa(t,s==="shared"?"view-only":s==="home"?"other":s);return}const o=Ie(e).pathname.split("/")[1],r=P(g().sites,o);if(!r?.port){t.destroy();return}const p=`/${o}`;Ua({req:e,socket:t,head:n,port:r.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ut.listen(ve,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${ve}/ (manifest ${ht})`));
16
+ </main></body></html>`}l(ja,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=Xs({file:$.join(W,"share-seen.json")}),we=new Set;function Ht(){const e=g().sites;for(const t of we){const n=it(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Ls()}l(Ht,"endUnshared"),setInterval(Ht,2e3).unref();async function Ut(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),a=it(n,g().sites),p=a.size?Vs({method:e.method,pathname:i,routes:a}):"other",h=Zs(i);if(p==="other")return(!a.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await Te.node();return{verdict:"shared",viewer:{login:n,routes:a,sidebar:l(_=>Qs(_,a,u?.name||u?.login),"sidebar")}}}l(Ut,"access");const Bt=Ue.createServer((e,t)=>{Ut(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Pa(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Pa(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ms(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 i=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:ws(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);Sa(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(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.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:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Et(m,{fresh:s.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(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Ta(e,t,s,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 a=s.pathname.split("/")[1],p=R(g().sites,a);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(a)}</h1></main></body></html>`);return}if(s.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+s.search}).end();return}const h="/"+a,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Pt(p.dir)){Mt(t,a,p);return}Ms(e,t,{dir:p.dir,rest:u,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 S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,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||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ht),S.on("error",()=>{if(t.headersSent){t.destroy();return}Mt(t,a,p)}),e.pipe(S)}l(Pa,"serve"),Bt.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ut(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],a=R(g().sites,i);if(!a?.port){t.destroy();return}const p=`/${i}`;zs({req:e,socket:t,head:n,port:a.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Bt.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
package/hub/incoming.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var N=Object.defineProperty;var c=(n,r)=>N(n,"name",{value:r,configurable:!0});import g from"node:fs";import S from"node:path";import{INCOMING_FILES as y,listBoards as v,readJSON as F,saveJSON as L}from"./boards.mjs";import{openPRs as O,prFiles as R,githubLogin as C}from"./pr-report.mjs";import{cardPRs as E}from"../shared/card-prs.mjs";const k=c((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(t=>t.pr));return n.filter(t=>!t.draft&&!k(t.author,i)&&!s.has(t.number)).map(t=>{const u=new Set(t.files),f=[...new Set(r.filter(o=>o.files.some(m=>u.has(m))).map(o=>o.key))],e=t.requested.some(o=>k(o,i));return f.length||e?{number:t.number,title:t.title,author:t.author,url:t.url,head:t.head,touches:f,asked:e}:null}).filter(Boolean)}c(J,"incomingFor");function M(n,r){const i=S.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;g.mkdirSync(S.dirname(i),{recursive:!0}),g.writeFileSync(s,JSON.stringify(r,null,2)+`
2
- `),g.renameSync(s,i)}c(M,"writeIncoming");async function _({manifest:n,gh:r={openPRs:O,prFiles:R},login:i=C}){const s=F(y,{}),t=[],u=[];let f;for(const e of v(n).filter(o=>o.auto_review&&o.github))try{const o=e.githubLogin||(f??=await i()),m=await r.openPRs(e.github),$=new Map(m.map(l=>[l.number,l])),d=[];for(const{slug:l,wave:I}of e.waves)for(const b of I.items.filter(a=>a?.status!=="dropped"))for(const{number:a}of E(b,e.github).filter(h=>h.repo===e.github)){const h=`${e.github}#${a}`;let p=$.get(a)?.files||s[h];if(!p){const w=await r.prFiles(e.github,a);p=w.files,w.state==="MERGED"&&(s[h]=p)}d.push({key:`${l}/${b.id}`,pr:a,files:p})}M(e.dir,{at:new Date().toISOString(),repo:e.github,prs:J({open:m,cards:d,me:o})}),t.push(e.route)}catch(o){u.push(`${e.route}: ${o.message}`)}return L(y,s),{checked:t,failed:u}}c(_,"checkIncoming");export{_ as checkIncoming,J as incomingFor};
1
+ var N=Object.defineProperty;var u=(n,r)=>N(n,"name",{value:r,configurable:!0});import d from"node:fs";import S from"node:path";import{INCOMING_FILES as y,listBoards as v,readJSON as F,saveJSON as L}from"./boards.mjs";import{openPRs as O,prFiles as R,githubLogin as P}from"./pr-report.mjs";import{cardPRs as C,storedPRs as E}from"../shared/card-prs.mjs";const k=u((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(e=>e.pr));return n.filter(e=>!e.draft&&!k(e.author,i)&&!s.has(e.number)).map(e=>{const f=new Set(e.files),m=[...new Set(r.filter(o=>o.files.some(l=>f.has(l))).map(o=>o.key))],t=e.requested.some(o=>k(o,i));return m.length||t?{number:e.number,title:e.title,author:e.author,url:e.url,head:e.head,touches:m,asked:t}:null}).filter(Boolean)}u(J,"incomingFor");function M(n,r){const i=S.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;d.mkdirSync(S.dirname(i),{recursive:!0}),d.writeFileSync(s,JSON.stringify(r,null,2)+`
2
+ `),d.renameSync(s,i)}u(M,"writeIncoming");async function q({manifest:n,gh:r={openPRs:O,prFiles:R},login:i=P}){const s=F(y,{}),e=[],f=[];let m;for(const t of v(n).filter(o=>o.auto_review&&o.github))try{const o=t.githubLogin||(m??=await i()),l=await r.openPRs(t.github),$=new Map(l.map(h=>[h.number,h])),p=[];for(const{slug:h,wave:I}of t.waves)for(const b of I.items.filter(a=>a&&(a.lane!=="done"||E(a).some(c=>c.merged_at))))for(const{number:a}of C(b,t.github).filter(c=>c.repo===t.github)){const c=`${t.github}#${a}`;let g=$.get(a)?.files||s[c];if(!g){const w=await r.prFiles(t.github,a);g=w.files,w.state==="MERGED"&&(s[c]=g)}p.push({key:`${h}/${b.id}`,pr:a,files:g})}M(t.dir,{at:new Date().toISOString(),repo:t.github,prs:J({open:l,cards:p,me:o})}),e.push(t.route)}catch(o){f.push(`${t.route}: ${o.message}`)}return L(y,s),{checked:e,failed:f}}u(q,"checkIncoming");export{q as checkIncoming,J as incomingFor};
@@ -1 +1 @@
1
- var g=Object.defineProperty;var w=(r,s)=>g(r,"name",{value:s,configurable:!0});import{WAVES as C}from"./boards.mjs";import{parseCheck as l,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const v=3;async function A(r,s,u){const m=s.ownerChecks||{},i={},d=new Set;for(const e of r)for(const{slug:f,wave:p}of e.waves)for(const n of p.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||!["done","merged"].includes(n.status))continue;const o=JSON.stringify([e.kind==="waves"?C:e.dir,f,n.id,c.check]);if(d.has(o))continue;d.add(o);const t=`${f}/${n.id}`;let a;try{a=await y(l(c.check),{since:c.since})}catch(k){const h=(m[o]||0)+1;if(h<v){i[o]=h;continue}try{await b(e.dir,t,c.check,k.message)}catch($){i[o]=h,u.push(`${e.route}: could not mark ${t}'s check broken (${$.message})`)}continue}if(a.passed)try{await O(e.dir,t,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${t} on its passing check (${k.message})`)}}s.ownerChecks=i}w(A,"runOwnerChecks");export{v as BROKEN_AFTER,A as runOwnerChecks};
1
+ var C=Object.defineProperty;var w=(r,t)=>C(r,"name",{value:t,configurable:!0});import{WAVES as g}from"./boards.mjs";import{parseCheck as l,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const v=3;async function A(r,t,u){const p=t.ownerChecks||{},i={},f=new Set;for(const e of r)for(const{slug:d,wave:m}of e.waves)for(const n of m.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||n.lane!=="done")continue;const o=JSON.stringify([e.kind==="waves"?g:e.dir,d,n.id,c.check]);if(f.has(o))continue;f.add(o);const s=`${d}/${n.id}`;let a;try{a=await y(l(c.check),{since:c.since})}catch(k){const h=(p[o]||0)+1;if(h<v){i[o]=h;continue}try{await b(e.dir,s,c.check,k.message)}catch($){i[o]=h,u.push(`${e.route}: could not mark ${s}'s check broken (${$.message})`)}continue}if(a.passed)try{await O(e.dir,s,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${s} on its passing check (${k.message})`)}}t.ownerChecks=i}w(A,"runOwnerChecks");export{v as BROKEN_AFTER,A as runOwnerChecks};
package/hub/pr-report.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var R=Object.defineProperty;var a=(t,n)=>R(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const E="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",g="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",w=a(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function d(t,n=process.env){return new Promise((s,r)=>{h("gh",t,{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(o,u,m)=>{if(o)return r(new Error(w(m||o.message)));try{s(JSON.parse(u))}catch(l){r(new Error(w(`unreadable output (${l.message})`)))}})})}a(d,"gh");const v="\u{1F916} ",b="<!-- claude-bridge -->";async function y(t){const n=process.env;if(!t)return n;const s=await new Promise(r=>h("gh",["auth","token","-u",t],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")}},(o,u)=>r(o?"":u.trim())));if(!s)throw new Error(`gh has no ${t} account; run gh auth login`);return{...n,GH_TOKEN:s}}a(y,"envFor");async function O({repo:t,pr:n,replyTo:s,words:r,login:o}){const u=await y(o),m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await d(["api",m,"-f",`body=${v}${r}
1
+ var _=Object.defineProperty;var i=(t,n)=>_(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const E="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",g="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",w=i(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function p(t,n=process.env){return new Promise((s,o)=>{h("gh",t,{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(r,u,m)=>{if(r)return o(new Error(w(m||r.message)));try{s(JSON.parse(u))}catch(l){o(new Error(w(`unreadable output (${l.message})`)))}})})}i(p,"gh");const v="\u{1F916} ",b="<!-- claude-bridge -->";async function y(t){const n=process.env;if(!t)return n;const s=await new Promise(o=>h("gh",["auth","token","-u",t],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")}},(r,u)=>o(r?"":u.trim())));if(!s)throw new Error(`gh has no ${t} account; run gh auth login`);return{...n,GH_TOKEN:s}}i(y,"envFor");async function O({repo:t,pr:n,replyTo:s,words:o,login:r}){const u=await y(r),m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await p(["api",m,"-f",`body=${v}${o}
2
2
 
3
- ${b}`],u)).html_url}catch(l){throw new Error(`${t}#${n}: ${l.message}`)}}a(O,"postToPR");async function A({repo:t,pr:n,head:s,body:r,login:o}){const u=await y(o);try{return(await d(["api",`repos/${t}/pulls/${n}/reviews`,"-f",`body=${r}`,"-f","event=COMMENT","-f",`commit_id=${s}`],u)).html_url}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}}a(A,"postReview");async function T(t){return(await d(["api","--paginate","--slurp",`repos/${t}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}a(T,"collaborators");async function I(t){try{return(await d(["api",`users/${encodeURIComponent(t)}`])).login}catch{return null}}a(I,"githubUser");async function H({repo:t,pr:n,since:s}){try{return(await d(["api","--paginate","--slurp",`repos/${t}/pulls/${n}/reviews`])).flat().filter(o=>Date.parse(o.submitted_at)>Date.parse(s)).map(o=>({login:o.user?.login??null,at:o.submitted_at}))}catch(r){throw new Error(`${t}#${n}: ${r.message}`)}}a(H,"reviewsSince");function L({repo:t,pr:n}){return new Promise((s,r)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,g].filter(Boolean).join(":")},timeout:9e4},(o,u,m)=>o?r(new Error(w(m||o.message))):s())})}a(L,"markReady");const N=a(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function q({repo:t,pr:n,since:s}){let r;try{r=await d(["pr","view",String(n),"--repo",t,"--json",E])}catch(c){throw new Error(`${t}#${n}: ${c.message}`)}const o=a(c=>d(["api","--paginate","--slurp",`repos/${t}/${c}`]).then(p=>p.flat()).catch(()=>[]),"list"),[u,m,l]=await Promise.all([o(`issues/${n}/comments?since=${encodeURIComponent(s)}`),o(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),o(`pulls/${n}/reviews`)]);return{view:r,issueComments:u,reviewComments:m,reviews:l}}a(q,"fetchPR");const $="author { __typename login }",S=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${$} createdAt body url } } reviews(last: 50) { nodes { databaseId ${$} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${$} createdAt body url path line replyTo { databaseId } } } } }`;function P(t,n=process.env){return new Promise((s,r)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(o,u,m)=>{let l=null;try{l=JSON.parse(u)}catch{}if(l?.data)return s(l);r(new Error(`GitHub query: ${w(o?m||o.message:"unreadable output")}`))})})}a(P,"graphql");const _=a(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user"),C="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function U(t,n=[]){const s=t.map(({repo:l,pr:c},p)=>{const[e,f]=l.split("/");return`pr${p}: repository(owner: ${JSON.stringify(e)}, name: ${JSON.stringify(f)}) { pullRequest(number: ${Number(c)}) { ${S} } }`});n.forEach((l,c)=>{const[p,e]=l.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(p)}, name: ${JSON.stringify(e)}) { ${C} }`)});const{data:r,errors:o=[]}=await P(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),u=Object.fromEntries(n.flatMap((l,c)=>r[`open${c}`]?.pullRequests?[[l,r[`open${c}`].pullRequests.nodes]]:[])),m=t.map(({repo:l,pr:c},p)=>{const e=r[`pr${p}`]?.pullRequest;if(!e)return{error:`${l}#${c}: ${o.find(i=>i.path?.[0]===`pr${p}`)?.message||"GitHub did not answer for it"}`};const f=a(i=>({id:i.databaseId??null,user:_(i.author),created_at:i.createdAt??null,body:i.body??null,html_url:i.url??null}),"comment");return{view:{state:e.state,isDraft:e.isDraft,reviewDecision:e.reviewDecision,mergeable:e.mergeable,mergeStateStatus:e.mergeStateStatus,headRefOid:e.headRefOid,statusCheckRollup:e.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[]},issueComments:(e.comments?.nodes??[]).map(f),reviewComments:(e.reviewThreads?.nodes??[]).flatMap(i=>i.comments?.nodes??[]).map(i=>({...f(i),path:i.path??null,line:i.line??null,in_reply_to_id:i.replyTo?.databaseId??null})),reviews:(e.reviews?.nodes??[]).map(i=>({id:i.databaseId??null,user:_(i.author),submitted_at:i.submittedAt??null,body:i.body??null,state:i.state??null,html_url:i.url??null}))}});return{login:r.viewer?.login??null,rate:r.rateLimit??null,results:m,open:u}}a(U,"fetchPRs");function k({view:t,issueComments:n,reviewComments:s,reviews:r},{pr:o,since:u,me:m}){const l=a((e,f)=>({kind:f,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),c=[...n.map(e=>({...l(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...l(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...r.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>u).map(e=>({...e,who:e.login===m?(e.body??"").includes(b)?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,f)=>e.at<f.at?-1:e.at>f.at?1:0),p=t.statusCheckRollup??[];return{pr:Number(o),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:p.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:p.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:c}}a(k,"reportOf");async function B(t){let n;try{n=await d(["pr","list","--repo",t,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${t} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(r=>r.path),requested:(s.reviewRequests||[]).map(r=>r.login).filter(Boolean)}))}a(B,"openPRs");async function j(t,n){let s;try{s=await d(["pr","view",String(n),"--repo",t,"--json","state,files"])}catch(r){throw new Error(`${t}#${n}: ${r.message}`)}return{state:s.state??null,files:(s.files||[]).map(r=>r.path)}}a(j,"prFiles");async function F({env:t}={}){const n=await d(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}a(F,"githubLogin");export{b as CLAUDE_MARK,v as CLAUDE_PREFIX,E as PR_VIEW_FIELDS,N as checksPassed,T as collaborators,q as fetchPR,U as fetchPRs,F as githubLogin,I as githubUser,L as markReady,B as openPRs,A as postReview,O as postToPR,j as prFiles,k as reportOf,H as reviewsSince};
3
+ ${b}`],u)).html_url}catch(l){throw new Error(`${t}#${n}: ${l.message}`)}}i(O,"postToPR");async function A({repo:t,pr:n,head:s,body:o,login:r}){const u=await y(r);try{return(await p(["api",`repos/${t}/pulls/${n}/reviews`,"-f",`body=${o}`,"-f","event=COMMENT","-f",`commit_id=${s}`],u)).html_url}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}}i(A,"postReview");async function T(t){return(await p(["api","--paginate","--slurp",`repos/${t}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}i(T,"collaborators");async function I(t){try{return(await p(["api",`users/${encodeURIComponent(t)}`])).login}catch{return null}}i(I,"githubUser");async function H({repo:t,pr:n,since:s}){try{return(await p(["api","--paginate","--slurp",`repos/${t}/pulls/${n}/reviews`])).flat().filter(r=>Date.parse(r.submitted_at)>Date.parse(s)).map(r=>({login:r.user?.login??null,at:r.submitted_at}))}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}}i(H,"reviewsSince");function L({repo:t,pr:n}){return new Promise((s,o)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,g].filter(Boolean).join(":")},timeout:9e4},(r,u,m)=>r?o(new Error(w(m||r.message))):s())})}i(L,"markReady");const N=i(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function q({repo:t,pr:n,since:s}){let o;try{o=await p(["pr","view",String(n),"--repo",t,"--json",E])}catch(c){throw new Error(`${t}#${n}: ${c.message}`)}const r=i(c=>p(["api","--paginate","--slurp",`repos/${t}/${c}`]).then(d=>d.flat()).catch(()=>[]),"list"),[u,m,l]=await Promise.all([r(`issues/${n}/comments?since=${encodeURIComponent(s)}`),r(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),r(`pulls/${n}/reviews`)]);return{view:o,issueComments:u,reviewComments:m,reviews:l}}i(q,"fetchPR");const $="author { __typename login }",S=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid title additions deletions changedFiles latestReviews(first: 50) { nodes { state } } recent: commits(last: 20) { nodes { commit { oid messageHeadline } } } commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${$} createdAt body url } } reviews(last: 50) { nodes { databaseId ${$} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${$} createdAt body url path line replyTo { databaseId } } } } }`;function P(t,n=process.env){return new Promise((s,o)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,g].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(r,u,m)=>{let l=null;try{l=JSON.parse(u)}catch{}if(l?.data)return s(l);o(new Error(`GitHub query: ${w(r?m||r.message:"unreadable output")}`))})})}i(P,"graphql");const R=i(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user"),C="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function U(t,n=[]){const s=t.map(({repo:l,pr:c},d)=>{const[e,f]=l.split("/");return`pr${d}: repository(owner: ${JSON.stringify(e)}, name: ${JSON.stringify(f)}) { pullRequest(number: ${Number(c)}) { ${S} } }`});n.forEach((l,c)=>{const[d,e]=l.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(d)}, name: ${JSON.stringify(e)}) { ${C} }`)});const{data:o,errors:r=[]}=await P(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),u=Object.fromEntries(n.flatMap((l,c)=>o[`open${c}`]?.pullRequests?[[l,o[`open${c}`].pullRequests.nodes]]:[])),m=t.map(({repo:l,pr:c},d)=>{const e=o[`pr${d}`]?.pullRequest;if(!e)return{error:`${l}#${c}: ${r.find(a=>a.path?.[0]===`pr${d}`)?.message||"GitHub did not answer for it"}`};const f=i(a=>({id:a.databaseId??null,user:R(a.author),created_at:a.createdAt??null,body:a.body??null,html_url:a.url??null}),"comment");return{view:{state:e.state,isDraft:e.isDraft,reviewDecision:e.reviewDecision,mergeable:e.mergeable,mergeStateStatus:e.mergeStateStatus,headRefOid:e.headRefOid,statusCheckRollup:e.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[],title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,changedFiles:e.changedFiles??null,approved:(e.latestReviews?.nodes??[]).some(a=>a.state==="APPROVED"),commits:(e.recent?.nodes??[]).map(a=>({oid:a.commit.oid.slice(0,10),subject:a.commit.messageHeadline}))},issueComments:(e.comments?.nodes??[]).map(f),reviewComments:(e.reviewThreads?.nodes??[]).flatMap(a=>a.comments?.nodes??[]).map(a=>({...f(a),path:a.path??null,line:a.line??null,in_reply_to_id:a.replyTo?.databaseId??null})),reviews:(e.reviews?.nodes??[]).map(a=>({id:a.databaseId??null,user:R(a.author),submitted_at:a.submittedAt??null,body:a.body??null,state:a.state??null,html_url:a.url??null}))}});return{login:o.viewer?.login??null,rate:o.rateLimit??null,results:m,open:u}}i(U,"fetchPRs");function k({view:t,issueComments:n,reviewComments:s,reviews:o},{pr:r,since:u,me:m}){const l=i((e,f)=>({kind:f,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),c=[...n.map(e=>({...l(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...l(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...o.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>u).map(e=>({...e,who:e.login===m?(e.body??"").includes(b)?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,f)=>e.at<f.at?-1:e.at>f.at?1:0),d=t.statusCheckRollup??[];return{pr:Number(r),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:d.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:d.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:c,title:t.title??null,additions:t.additions??null,deletions:t.deletions??null,files:t.changedFiles??null,approved:!!t.approved,commits:t.commits??[]}}i(k,"reportOf");async function F(t){let n;try{n=await p(["pr","list","--repo",t,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${t} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(o=>o.path),requested:(s.reviewRequests||[]).map(o=>o.login).filter(Boolean)}))}i(F,"openPRs");async function B(t,n){let s;try{s=await p(["pr","view",String(n),"--repo",t,"--json","state,files"])}catch(o){throw new Error(`${t}#${n}: ${o.message}`)}return{state:s.state??null,files:(s.files||[]).map(o=>o.path)}}i(B,"prFiles");async function j({env:t}={}){const n=await p(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}i(j,"githubLogin");export{b as CLAUDE_MARK,v as CLAUDE_PREFIX,E as PR_VIEW_FIELDS,N as checksPassed,T as collaborators,q as fetchPR,U as fetchPRs,j as githubLogin,I as githubUser,L as markReady,F as openPRs,A as postReview,O as postToPR,B as prFiles,k as reportOf,H as reviewsSince};
@@ -1 +1 @@
1
- var $=Object.defineProperty;var f=(t,s)=>$(t,"name",{value:s,configurable:!0});import{listBoards as p}from"./boards.mjs";import{savedSlack as l}from"./slack.mjs";import{reviewDue as y,reviewSent as h,reviewReact as v,reviewDigestSent as g}from"../board/store.mjs";const u=f(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function k(t,s,d){const a=await y(t.dir,{now:d});if(a.waiting)return{sent:[],failed:[]};const c=[],o=[],r=f(async(e,n)=>{try{await n(),c.push(e)}catch(i){if(u(i))throw i;o.push(`${t.route}: ${e}: ${i.message}`)}},"attempt");for(const e of a.dms)await r(`link to ${e.reviewer} on ${e.key}`,async()=>{const n=await s.openDM(e.slack_user),i=await s.post({channel:n,text:e.text});await h(t.dir,e.key,e.reviewer,{ts:i,channel:n})});for(const e of a.reactions)await r(`${e.emoji} for ${e.reviewer} on ${e.key}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await v(t.dir,e.key,e.reviewer,e.emoji)});for(const e of a.digest?.reviewers||[]){const n=e.items.length;await r(`digest to ${e.name}`,async()=>{const i=await s.openDM(e.slack_user),m=await s.post({channel:i,text:`\u{1F916} ${t.title} digest: ${n} PR${n===1?"":"s"} pending your review`});for(const w of e.items)await s.post({channel:i,thread_ts:m,text:`${w.url} \xB7 ${w.title}${w.days_old?` (${w.days_old} days old)`:""}`});await g(t.dir,e.name,{ts:m,channel:i,items:e.items.map(w=>w.key).join(",")},{now:d})})}return{sent:c,failed:o}}f(k,"sendBoard");async function S({boards:t=p(),fetch:s,now:d=new Date}={}){const a=l({fetch:s}),c=[],o=[];if(!a)return{sent:c,failed:o};for(const r of t)try{const e=await k(r,a,d);c.push(...e.sent.map(n=>`${r.route}: ${n}`)),o.push(...e.failed)}catch(e){if(o.push(`${r.route}: ${e.message}`),u(e))break}return{sent:c,failed:o}}f(S,"sendReviews");export{S as sendReviews};
1
+ var p=Object.defineProperty;var i=(t,s)=>p(t,"name",{value:s,configurable:!0});import l from"node:fs";import h from"node:path";import{listBoards as w}from"./boards.mjs";import{savedSlack as m,refusalUnpushed as k,markRefusalPushed as $}from"./slack.mjs";import{deliver as g}from"./push.mjs";import{reviewDue as y,queuePushes as v,postLink as R,postReply as S,recordReaction as x,postDigest as D}from"../board/store.mjs";const d=i(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function _(t,s,c){await v(t.dir);const n=await y(t.dir,{now:c});if(n.waiting)return{sent:[],failed:[]};const u=[],o=[],a=i(async(e,r)=>{try{await r(),u.push(e)}catch(f){if(d(f))throw f;o.push(`${t.route}: ${e}: ${f.message}`)}},"attempt");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{await R(t.dir,e,await s.openDM(e.slack_user),r=>s.post({channel:r,text:e.text}))});for(const e of n.replies)await a(`${e.id} to ${e.reviewer} on ${e.pr}`,()=>S(t.dir,e,()=>s.post({channel:e.channel,thread_ts:e.thread_ts,text:e.text})));for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await s.react({channel:e.channel,ts:e.ts,name:e.emoji}),await x(t.dir,e)});for(const e of(n.digest?.reviewers||[]).filter(r=>r.owed))await a(`digest to ${e.name}`,async()=>{await D(t.dir,n.digest.date,e,{channel:e.sent?.channel||await s.openDM(e.slack_user),header:i(r=>`${t.title} digest: ${r} PR${r===1?"":"s"} pending your review`,"header"),post:i(r=>s.post(r),"post")})});return{sent:u,failed:o}}i(_,"sendBoard");const j=i(t=>{try{return(JSON.parse(l.readFileSync(h.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function P(t,s){return!k()||!t.some(j)?[]:(await g({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub settings \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:s}),$(),["pushed that Slack refused the token"])}i(P,"pushRefusal");async function N({boards:t=w(),fetch:s,now:c=new Date}={}){const n=m({fetch:s}),u=[],o=[];if(!n)return{sent:u,failed:o};for(const a of t)try{const e=await _(a,n,c);u.push(...e.sent.map(r=>`${a.route}: ${r}`)),o.push(...e.failed)}catch(e){if(o.push(`${a.route}: ${e.message}`),d(e))break}try{u.push(...await P(t,c))}catch(a){o.push(`refused-token push: ${a.message}`)}return{sent:u,failed:o}}i(N,"sendReviews");export{N as sendReviews};
package/hub/sessions.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function Me(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Me,"startStep");function $s(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=Be(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Me(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a($s,"startingBoards");const Re=Oe/1e3+60,Fe=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-hook.mjs")}"`,timeout:Re}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ge(e,s){if(!fe(e))throw new m("Choose a board route.","none");return pe(g.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ve(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ge,"withSessionLock");function C(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=g.join(A.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}a(C,"claudeBin");const D=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Z(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>O.existsSync(e))||"tmux"}a(Z,"tmuxBin");function B(e){const s=de(Z(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!D(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(B,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),M=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");async function ks({manifest:e=_(),now:s=new Date,running:t=x(),wakes:r=N(P,{}),tmux:i=B}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),N(V,{})),o=[],f=[];let u=!1;const c=H(e)?.route;for(const p of U(e)){const l=Le(p.owner?.session);if(!De(we(p,{now:s.getTime(),running:t,wakes:r}),l)){h(n,p.route)&&(delete n[p.route],u=!0);continue}if(p.route!==c||h(n,p.route)||!M(i,p.route))continue;const d=`=${T(p.route)}:`,y=i(["send-keys","-t",d,"-l","/compact"]),b=y.status===0?i(["send-keys","-t",d,"Enter"]):y,w=b.status===0?null:(b.stderr||b.stdout).trim().slice(0,300)||`tmux exited ${b.status}`;n[p.route]={session:p.owner.session,percent:l.percent,at:s.toISOString(),...w?{error:w}:{}},Y(z(p.dir),{call:"compact",result:w?"failed":"typed /compact",reason:`Its session's context is ${l.percent}% full and it isn't listening.${w?` ${w}`:""}`,inputs:{session:p.owner.session,context:l}},{now:s}),u=!0,w?f.push(`${p.route}: could not send /compact to tmux session ${T(p.route)} (${w})`):o.push(p.route)}return u&&X(V,n),{compacted:o,failed:f}}a(ks,"compactFullContexts");function R(e,{cwd:s,timeout:t=6e4}){const r=C(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)));return i.CLAUDE_BG_ISOLATION="none",g.isAbsolute(r)&&(i.PATH=`${g.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",f="",u;try{u=J(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){n({status:1,stdout:o,stderr:l.message});return}const c=a(l=>{clearTimeout(p),u.stdout.destroy(),u.stderr.destroy(),n({status:l,stdout:o,stderr:f})},"done"),p=setTimeout(()=>{try{u.kill("SIGKILL")}catch{}c(1)},t);u.stdout.on("data",l=>{o+=l}),u.stderr.on("data",l=>{f+=l}),u.on("error",l=>{f+=l.message,c(1)}),u.on("close",l=>c(l??1)),u.on("exit",l=>setTimeout(()=>c(l??1),300)),u.unref()})}a(R,"runClaude");async function Ke(e,{cwd:s,run:t=R,starts:r=xe,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!je(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ke,"runSessionStop");async function ys(e,{live:s=x,stop:t=Ke}={}){if(!e?.session)return;const r=s();if(!r)throw new m(oe(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||A.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(f=>setTimeout(f,100))}}a(ys,"stopSession");function We(e){const s=`=${T(e)}`;return Je(Z(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(We,"attachTmux");function Je(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const f=J(e,s,{env:t,stdio:"inherit"});f.on("error",u=>{i(),o(new m(u.message,"failed"))}),f.on("close",u=>{i(),n({status:u??1})})})}a(Je,"runForeground");const F=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Fe,...s?["--model",s]:[]],"common"),He=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),L=a((e,s)=>`--name ${He(le(e,s))}`,"boardFlag"),Xe=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...F({name:s,model:t}),`/kickflip ${L(e,s)}${r?` ${r}`:""}`],"startArgv"),qe=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${g.join(E,"bin","kickflip.mjs")} join ${L(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),Qe=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:F({name:t,model:r}),qe(s,e,t)],"resumeArgv"),ee=a((e,s)=>`You are the hub session. Read \`${g.join(E,"docs","hub-session.md")}\`, then rejoin /${e} (join ${L(e,s)}) and relaunch its watcher.`,"hubPrompt"),se=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...F({name:s,model:r}),t?`${ee(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${L(e,s)} --hub
2
- ${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!M(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");K(o,i);const f=o?.repo||g.resolve(s||process.cwd());if(!O.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");te(n.route,se({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:f,tmux:t})}return r(n.route)}a(Is,"hubSession");function G(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(G,"sessionOf");async function ne(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let r=0;r<20;r++){const i=Te().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(ne,"startedId");async function Ye(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Ye,"isBackground");const oe=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy Mac. Try again in a moment.`,"unknownLiveness");function K(e,s){if(!e)return;const t=s();if(!t)throw new m(oe(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(K,"refuseLive");async function ie(e,{from:s,to:t,mark:r}){await ye(async()=>{const i=_(),n=U(i).find(f=>f.route===e);if(n?.owner?.session===s&&(r||t!==s)&&me(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await Ie(i)})}a(ie,"moveOwner");async function Ss(e,{manifest:s=_(),repo:t,model:r,prompt:i,run:n=R,live:o=x,clones:f=Ae()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const u=G(s,e);K(u,o);const c=g.resolve(t||u?.repo||process.cwd());if(!O.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(Ee(c,f))try{Ce(c)}catch(w){throw new m(`Couldn't trust this folder for Claude Code: ${w.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,l=Xe({route:e,name:p,model:r||u?.model,prompt:i}),d=await n(l,{cwd:c});if(d.status!==0){const w=(d.stderr||"").trim()||(d.stdout||"").trim();throw new m(w?`claude --bg failed (${d.status}): ${w.slice(0,300)}`:`claude exited ${d.status} with no output; check \`claude agents\``,"failed")}const{session:y,confirmed:b}=await ne(d.stdout);if(!y)throw new m(`claude --bg started a session but no session record names it yet; find it with claude agents (${d.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:y,confirmed:b,repo:Q(c),model:r||u?.model||null,name:p,argv:[C(),...l],output:d.stdout.trim()}}a(Ss,"startSession");function ze(e,s={}){return Ge(e,({resume:t})=>t(s))}a(ze,"resumeSession");async function Ve(e,{manifest:s=_(),run:t=R,tmux:r=B,live:i=x,mark:n=null}={}){if(h(_().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=G(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(K(o,i),h(s.sites,e)?.hub===!0){if(M(r,e))throw new m(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const d=te(e,se({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await ie(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:d,output:`in tmux session ${T(e)}`}}const f=await Ye(o.session,{run:t,cwd:o.repo}),u=Qe({id:o.session,route:e,name:o.title,model:o.model,background:f}),c=await t(u,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:l}=await ne(c.stdout,o.session);return await ie(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:l,repo:o.repo,argv:[C(),...u],output:c.stdout.trim()}}a(Ve,"resumeRecordedSession");function Ze(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!1,now:f}){if(s)return{action:"none",entry:null};const u=new Date(f).toISOString(),c=e?{...e}:{down_since:u};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||f-Date.parse(c.resumed_at)>=Ue?{action:"push",entry:{...c,pushed_at:u}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,d])=>d>(h(e.mark,l)??0)))||o||n&&f-Date.parse(c.down_since)>=Pe?{action:"resume",entry:{...c,resumed_at:u}}:{action:"none",entry:c}}a(Ze,"resumeStep");async function _s({manifest:e=_(),now:s=new Date,live:t=x,asked:r=new Set,listening:i=a((u,c)=>he(u,{now:s.getTime(),running:c}),"listening"),resume:n=a((u,c)=>ze(u,{manifest:e,live:t,mark:c}),"resume"),push:o=ve,gate:f=Se({now:s})}={}){const u=t();if(!u)return{resumed:[],pushed:[],failed:[]};const c=N(P,{}),p=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const d of U(e)){const y=G(e,d.route),b=!!(y&&u.has(y.session)),w=i(d,u);let{action:j,entry:$}=Ze(h(c,d.route),{listening:w,live:b,recorded:!!y?.repo,now:s.getTime(),asked:r.has(d.route),counts:w?null:be(d),waiting:!w&&!b&&ge(d)!==null});if(j==="resume"&&f.state==="paused"){const I=h(c,d.route)?.mark||ke(d);Object.entries($e(d)).flatMap(([S,ae])=>ae.slice(h(I,S)||0)).some(S=>!_e(S))||(j="none",delete $.resumed_at)}const W=h(c,d.route),re={listening:w,live:b,asked:r.has(d.route),downSince:W?.down_since??null,resumedAt:W?.resumed_at??null},v=a((I,k,S)=>Y(z(d.dir),{call:I,result:k,reason:S,inputs:re},{now:s}),"logged");if(j==="resume"){const I=r.has(d.route)?"The owner opened the board's page while its session wasn't listening.":"An event waited while the board's session wasn't listening.";try{await n(d.route,$.mark),l.resumed.push(d.route),v("session-resume","resumed",I)}catch(k){k.reason==="unknown"?delete $.resumed_at:$={...$,error:k.message},l.failed.push(`${d.route}: could not resume its session (${k.message})`),v("session-resume","failed",`${I} ${k.message}`)}}else if(j==="push"){const I=$.error?`The hub couldn't resume it: ${$.error}`:b?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${d.route}, or claude agents.`,k=`Its session didn't come back after the hub resumed it: ${I}`;try{await o({title:`${d.title}'s session didn't come back`,body:I,url:`/${d.route}/`,tag:`resume-${d.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(d.route),v("push","sent",k)}catch(S){$={...$,pushed_at:void 0},l.failed.push(`${d.route}: could not push that its session didn't come back (${S.message})`),v("push","not sent",`${k} Not sent: ${S.message}`)}}$&&(p[d.route]=$)}return JSON.stringify(p)!==JSON.stringify(c)&&X(P,p),l}a(_s,"checkSessions");export{Fe as BG_SETTINGS,Ue as COMEBACK_WAIT,Re as PROMPT_HOOK_TIMEOUT_S,Pe as RESUME_AFTER,Ne as STARTING_WAIT,m as SessionError,We as attachTmux,_s as checkSessions,C as claudeBin,ks as compactFullContexts,se as hubArgv,Is as hubSession,D as inherited,Qe as resumeArgv,qe as resumePrompt,ze as resumeSession,Ze as resumeStep,R as runClaude,Ke as runSessionStop,B as runTmux,G as sessionOf,Xe as startArgv,Be as startFailed,Ss as startSession,$s as startingBoards,ys as stopSession,Z as tmuxBin,T as tmuxName,Ge as withSessionLock};
1
+ var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function Re(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Re,"startStep");function $s(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=Be(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Re(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a($s,"startingBoards");const Me=Oe/1e3+60,Fe=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-hook.mjs")}"`,timeout:Me}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ge(e,s){if(!fe(e))throw new m("Choose a board route.","none");return pe(g.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ve(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ge,"withSessionLock");function C(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=g.join(A.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}a(C,"claudeBin");const D=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Z(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>O.existsSync(e))||"tmux"}a(Z,"tmuxBin");function B(e){const s=de(Z(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!D(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(B,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),R=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");async function ks({manifest:e=_(),now:s=new Date,running:t=x(),wakes:r=N(P,{}),tmux:i=B}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),N(V,{})),o=[],f=[];let d=!1;const c=H(e)?.route;for(const p of U(e)){const l=Le(p.owner?.session);if(!De(we(p,{now:s.getTime(),running:t,wakes:r}),l)){h(n,p.route)&&(delete n[p.route],d=!0);continue}if(p.route!==c||h(n,p.route)||!R(i,p.route))continue;const u=`=${T(p.route)}:`,y=i(["send-keys","-t",u,"-l","/compact"]),b=y.status===0?i(["send-keys","-t",u,"Enter"]):y,w=b.status===0?null:(b.stderr||b.stdout).trim().slice(0,300)||`tmux exited ${b.status}`;n[p.route]={session:p.owner.session,percent:l.percent,at:s.toISOString(),...w?{error:w}:{}},Y(z(p.dir),{call:"compact",result:w?"failed":"typed /compact",reason:`Its session's context is ${l.percent}% full and it isn't listening.${w?` ${w}`:""}`,inputs:{session:p.owner.session,context:l}},{now:s}),d=!0,w?f.push(`${p.route}: could not send /compact to tmux session ${T(p.route)} (${w})`):o.push(p.route)}return d&&X(V,n),{compacted:o,failed:f}}a(ks,"compactFullContexts");function M(e,{cwd:s,timeout:t=6e4}){const r=C(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)));return i.CLAUDE_BG_ISOLATION="none",g.isAbsolute(r)&&(i.PATH=`${g.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",f="",d;try{d=J(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){n({status:1,stdout:o,stderr:l.message});return}const c=a(l=>{clearTimeout(p),d.stdout.destroy(),d.stderr.destroy(),n({status:l,stdout:o,stderr:f})},"done"),p=setTimeout(()=>{try{d.kill("SIGKILL")}catch{}c(1)},t);d.stdout.on("data",l=>{o+=l}),d.stderr.on("data",l=>{f+=l}),d.on("error",l=>{f+=l.message,c(1)}),d.on("close",l=>c(l??1)),d.on("exit",l=>setTimeout(()=>c(l??1),300)),d.unref()})}a(M,"runClaude");async function Ke(e,{cwd:s,run:t=M,starts:r=xe,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!je(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ke,"runSessionStop");async function ys(e,{live:s=x,stop:t=Ke}={}){if(!e?.session)return;const r=s();if(!r)throw new m(oe(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||A.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(f=>setTimeout(f,100))}}a(ys,"stopSession");function We(e){const s=`=${T(e)}`;return Je(Z(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(We,"attachTmux");function Je(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const f=J(e,s,{env:t,stdio:"inherit"});f.on("error",d=>{i(),o(new m(d.message,"failed"))}),f.on("close",d=>{i(),n({status:d??1})})})}a(Je,"runForeground");const F=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Fe,...s?["--model",s]:[]],"common"),He=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),L=a((e,s)=>`--name ${He(le(e,s))}`,"boardFlag"),Xe=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...F({name:s,model:t}),`/kickflip ${L(e,s)}${r?` ${r}`:""}`],"startArgv"),qe=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${g.join(E,"bin","kickflip.mjs")} join ${L(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),Qe=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:F({name:t,model:r}),qe(s,e,t)],"resumeArgv"),ee=a((e,s)=>`You are the hub session. Read \`${g.join(E,"docs","hub-session.md")}\`, then rejoin /${e} (join ${L(e,s)}) and relaunch its watcher.`,"hubPrompt"),se=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...F({name:s,model:r}),t?`${ee(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${L(e,s)} --hub
2
+ ${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!R(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");K(o,i);const f=o?.repo||g.resolve(s||process.cwd());if(!O.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");te(n.route,se({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:f,tmux:t})}return r(n.route)}a(Is,"hubSession");function G(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(G,"sessionOf");async function ne(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let r=0;r<20;r++){const i=Te().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(ne,"startedId");async function Ye(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Ye,"isBackground");const oe=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy host. Try again in a moment.`,"unknownLiveness");function K(e,s){if(!e)return;const t=s();if(!t)throw new m(oe(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(K,"refuseLive");async function ie(e,{from:s,to:t,mark:r}){await ye(async()=>{const i=_(),n=U(i).find(f=>f.route===e);if(n?.owner?.session===s&&(r||t!==s)&&me(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await Ie(i)})}a(ie,"moveOwner");async function Ss(e,{manifest:s=_(),repo:t,model:r,prompt:i,run:n=M,live:o=x,clones:f=Ae()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=G(s,e);K(d,o);const c=g.resolve(t||d?.repo||process.cwd());if(!O.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(Ee(c,f))try{Ce(c)}catch(w){throw new m(`Couldn't trust this folder for Claude Code: ${w.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,l=Xe({route:e,name:p,model:r||d?.model,prompt:i}),u=await n(l,{cwd:c});if(u.status!==0){const w=(u.stderr||"").trim()||(u.stdout||"").trim();throw new m(w?`claude --bg failed (${u.status}): ${w.slice(0,300)}`:`claude exited ${u.status} with no output; check \`claude agents\``,"failed")}const{session:y,confirmed:b}=await ne(u.stdout);if(!y)throw new m(`claude --bg started a session but no session record names it yet; find it with claude agents (${u.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:y,confirmed:b,repo:Q(c),model:r||d?.model||null,name:p,argv:[C(),...l],output:u.stdout.trim()}}a(Ss,"startSession");function ze(e,s={}){return Ge(e,({resume:t})=>t(s))}a(ze,"resumeSession");async function Ve(e,{manifest:s=_(),run:t=M,tmux:r=B,live:i=x,mark:n=null}={}){if(h(_().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=G(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(K(o,i),h(s.sites,e)?.hub===!0){if(R(r,e))throw new m(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const u=te(e,se({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await ie(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:u,output:`in tmux session ${T(e)}`}}const f=await Ye(o.session,{run:t,cwd:o.repo}),d=Qe({id:o.session,route:e,name:o.title,model:o.model,background:f}),c=await t(d,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:l}=await ne(c.stdout,o.session);return await ie(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:l,repo:o.repo,argv:[C(),...d],output:c.stdout.trim()}}a(Ve,"resumeRecordedSession");function Ze(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!1,now:f}){if(s)return{action:"none",entry:null};const d=new Date(f).toISOString(),c=e?{...e}:{down_since:d};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||f-Date.parse(c.resumed_at)>=Ue?{action:"push",entry:{...c,pushed_at:d}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,u])=>u>(h(e.mark,l)??0)))||o||n&&f-Date.parse(c.down_since)>=Pe?{action:"resume",entry:{...c,resumed_at:d}}:{action:"none",entry:c}}a(Ze,"resumeStep");async function _s({manifest:e=_(),now:s=new Date,live:t=x,asked:r=new Set,listening:i=a((d,c)=>he(d,{now:s.getTime(),running:c}),"listening"),resume:n=a((d,c)=>ze(d,{manifest:e,live:t,mark:c}),"resume"),push:o=ve,gateOf:f=a(d=>Se({now:s,dir:d.dir}),"gateOf")}={}){const d=t();if(!d)return{resumed:[],pushed:[],failed:[]};const c=N(P,{}),p=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const u of U(e)){const y=G(e,u.route),b=!!(y&&d.has(y.session)),w=i(u,d);let{action:j,entry:$}=Ze(h(c,u.route),{listening:w,live:b,recorded:!!y?.repo,now:s.getTime(),asked:r.has(u.route),counts:w?null:be(u),waiting:!w&&!b&&ge(u)!==null});if(j==="resume"&&f(u).state==="paused"){const I=h(c,u.route)?.mark||ke(u);Object.entries($e(u)).flatMap(([S,ae])=>ae.slice(h(I,S)||0)).some(S=>!_e(S))||(j="none",delete $.resumed_at)}const W=h(c,u.route),re={listening:w,live:b,asked:r.has(u.route),downSince:W?.down_since??null,resumedAt:W?.resumed_at??null},v=a((I,k,S)=>Y(z(u.dir),{call:I,result:k,reason:S,inputs:re},{now:s}),"logged");if(j==="resume"){const I=r.has(u.route)?"The owner opened the board's page while its session wasn't listening.":"An event waited while the board's session wasn't listening.";try{await n(u.route,$.mark),l.resumed.push(u.route),v("session-resume","resumed",I)}catch(k){k.reason==="unknown"?delete $.resumed_at:$={...$,error:k.message},l.failed.push(`${u.route}: could not resume its session (${k.message})`),v("session-resume","failed",`${I} ${k.message}`)}}else if(j==="push"){const I=$.error?`The hub couldn't resume it: ${$.error}`:b?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${u.route}, or claude agents.`,k=`Its session didn't come back after the hub resumed it: ${I}`;try{await o({title:`${u.title}'s session didn't come back`,body:I,url:`/${u.route}/`,tag:`resume-${u.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(u.route),v("push","sent",k)}catch(S){$={...$,pushed_at:void 0},l.failed.push(`${u.route}: could not push that its session didn't come back (${S.message})`),v("push","not sent",`${k} Not sent: ${S.message}`)}}$&&(p[u.route]=$)}return JSON.stringify(p)!==JSON.stringify(c)&&X(P,p),l}a(_s,"checkSessions");export{Fe as BG_SETTINGS,Ue as COMEBACK_WAIT,Me as PROMPT_HOOK_TIMEOUT_S,Pe as RESUME_AFTER,Ne as STARTING_WAIT,m as SessionError,We as attachTmux,_s as checkSessions,C as claudeBin,ks as compactFullContexts,se as hubArgv,Is as hubSession,D as inherited,Qe as resumeArgv,qe as resumePrompt,ze as resumeSession,Ze as resumeStep,M as runClaude,Ke as runSessionStop,B as runTmux,G as sessionOf,Xe as startArgv,Be as startFailed,Ss as startSession,$s as startingBoards,ys as stopSession,Z as tmuxBin,T as tmuxName,Ge as withSessionLock};
package/hub/share.mjs CHANGED
@@ -1 +1 @@
1
- var m=Object.defineProperty;var u=(t,e)=>m(t,"name",{value:e,configurable:!0});import E from"node:path";import{isBoardSite as _,readJSON as w,saveJSON as b}from"./boards.mjs";const g=u(t=>typeof t=="string"&&t.length<=200&&/^[^\s@]+@[^\s@]+$/.test(t),"validLogin"),y=u(t=>Array.isArray(t?.share)?[...new Set(t.share.filter(g))]:[],"shareOf");function M(t,e){const i=new Set;for(const[c,s]of Object.entries(e||{}))_(s)&&y(w(E.join(s.dir,"board.json"),{})).includes(t)&&i.add(c);return i}u(M,"sharedRoutes");const x=new Set(["/","/sw.js","/manifest.webmanifest","/favicon.ico"]),R=/^\/_hub\/(?:app\/[\w.-]+|colors\.css|kickflip\.png|hub\.css|icon-(?:180|192|512)\.png|sidebar\.json|stream)$/,j=new Set(["/_hub","/_hub/","/_hub/settings","/_hub/settings/polling"]),D=new RegExp("^/(?:"+["","questions\\.html","notes\\.html","plan\\.html","index\\.html","prototype-[a-z0-9-]+\\.html","[\\w-][\\w.-]*\\.(?:js|mjs|css|png|svg|ico|webp|jpe?g|gif|woff2?)","prototypes/[a-z0-9-]+/[\\w-][\\w.-]*\\.js","uploads/[^/]+","api/(?:board|questions|done|links|working|thread|notes|plan|try/main/log)"].join("|")+")$");function U({method:t="GET",pathname:e,routes:i}){const c=t==="GET"||t==="HEAD";if(x.has(e)||R.test(e))return c?"pass":"view-only";if(j.has(e))return c?"home":"other";const s=/^\/([^/]+)(\/.*)?$/.exec(e);if(!s||!i.has(s[1]))return"other";const l=s[2]||"/";return l===`/${s[1]}`||l.startsWith(`/${s[1]}/`)?"view-only":c&&D.test(l)?"pass":"view-only"}u(U,"shareReach");function B(t){const e=String(t||"").split("/")[1]||"";if(!e||e==="_hub"||e.includes("."))return null;try{return decodeURIComponent(e)}catch{return null}}u(B,"routeOfPath");function F(t,e,i){const c=(t?.sites||[]).filter(s=>e.has(s.route)).map(({count:s,ctx:l,...p})=>p);return{code:t?.code??null,total:0,start:c[0]?.route??null,sites:c,starting:[],shared:{by:i||null}}}u(F,"sharedSidebar");const O=60*6e4,v=20,A=6e4;function H({file:t,now:e=Date.now}={}){const i=w(t,{}),c=new Map,s=new Map,l=new Map;let p=[];const S=u(()=>{try{b(t,i)}catch{}},"save");return{visit(n,o,r){const a=i[n]||={name:null,at:{}};let f=!1;if(o&&a.name!==o&&(a.name=o,f=!0),r){l.set(n,r);const h=new Date(e()).toISOString();a.at[r]=h;const d=`${n} ${r}`;e()-(c.get(d)??-1/0)<A||(c.set(d,e()),f=!0)}f&&S()},watching(n){return s.set(n,(s.get(n)||0)+1),()=>{const o=s.get(n)-1;o>0?s.set(n,o):s.delete(n)}},refuse(n,o,r){p=[{login:n,name:o||null,route:r||null,at:new Date(e()).toISOString()},...p.filter(a=>a.login!==n)].slice(0,v)},people(n,o){return o.map(r=>({login:r,name:i[r]?.name||null,now:s.has(r)&&l.get(r)===n,at:i[r]?.at?.[n]||null}))},refusals(n,o){const r=e()-O;return p.filter(a=>Date.parse(a.at)>=r&&(a.route===n||a.route===null)&&!o.includes(a.login)).map(({login:a,name:f,at:h})=>({login:a,name:f,at:h}))}}}u(H,"shareTracker");export{B as routeOfPath,y as shareOf,U as shareReach,H as shareTracker,M as sharedRoutes,F as sharedSidebar,g as validLogin};
1
+ var g=Object.defineProperty;var u=(e,t)=>g(e,"name",{value:t,configurable:!0});import y from"node:path";import{isBoardSite as E,readJSON as S,saveJSON as _}from"./boards.mjs";const b=u(e=>typeof e=="string"&&e.length<=200&&/^[^\s@]+@[^\s@]+$/.test(e),"validLogin");function m(e){const t=new Map;for(const n of Array.isArray(e?.share)?e.share:[]){const a=typeof n=="string"?n:n?.login;b(a)&&!t.has(a)&&t.set(a,{login:a,level:n?.level==="full"?"full":"view"})}return[...t.values()]}u(m,"shareEntries");const L=u(e=>m(e).map(t=>t.login),"shareOf");function T(e,t){const n=new Map;for(const[a,s]of Object.entries(t||{})){const r=E(s)&&m(S(y.join(s.dir,"board.json"),{})).find(f=>f.login===e);r&&n.set(a,r.level)}return n}u(T,"sharedRoutes");const v=new Set(["/","/sw.js","/manifest.webmanifest","/favicon.ico"]),x=/^\/_hub\/(?:app\/[\w.-]+|colors\.css|kickflip\.png|hub\.css|icon-(?:180|192|512)\.png|sidebar\.json|stream)$/,R=new Set(["/_hub","/_hub/","/_hub/settings","/_hub/settings/polling"]),j=new RegExp("^/(?:"+["","questions\\.html","notes\\.html","plan\\.html","index\\.html","prototype-[a-z0-9-]+\\.html","[\\w-][\\w.-]*\\.(?:js|mjs|css|png|svg|ico|webp|jpe?g|gif|woff2?)","prototypes/[a-z0-9-]+/[\\w-][\\w.-]*\\.js","uploads/[^/]+","api/(?:board|questions|done|links|working|thread|notes|plan|try/main/log)"].join("|")+")$"),O=new Set(["/_claude/events","/api/upload"]);function U({method:e="GET",pathname:t,routes:n}){const a=e==="GET"||e==="HEAD";if(v.has(t)||x.test(t))return a?"pass":"view-only";if(R.has(t))return a?"home":"other";const s=/^\/([^/]+)(\/.*)?$/.exec(t);if(!s||!n.has(s[1]))return"other";const r=s[2]||"/";return r===`/${s[1]}`||r.startsWith(`/${s[1]}/`)?"view-only":a?j.test(r)?"pass":"view-only":e==="POST"&&n.get(s[1])==="full"&&O.has(r)?"pass":"view-only"}u(U,"shareReach");function F(e){const t=String(e||"").split("/")[1]||"";if(!t||t==="_hub"||t.includes("."))return null;try{return decodeURIComponent(t)}catch{return null}}u(F,"routeOfPath");function B(e,t,n){const a=(e?.sites||[]).filter(r=>t.has(r.route)).map(({count:r,ctx:f,...h})=>h),s=[...t].filter(([,r])=>r==="full").map(([r])=>r);return{code:e?.code??null,total:0,start:a[0]?.route??null,sites:a,starting:[],shared:{by:n||null,full:s}}}u(B,"sharedSidebar");const D=60*6e4,A=20,$=6e4;function H({file:e,now:t=Date.now}={}){const n=S(e,{}),a=new Map,s=new Map,r=new Map;let f=[];const h=u(()=>{try{_(e,n)}catch{}},"save");return{visit(o,l,i){const c=n[o]||={name:null,at:{}};let p=!1;if(l&&c.name!==l&&(c.name=l,p=!0),i){r.set(o,i);const d=new Date(t()).toISOString();c.at[i]=d;const w=`${o} ${i}`;t()-(a.get(w)??-1/0)<$||(a.set(w,t()),p=!0)}p&&h()},watching(o){return s.set(o,(s.get(o)||0)+1),()=>{const l=s.get(o)-1;l>0?s.set(o,l):s.delete(o)}},refuse(o,l,i){f=[{login:o,name:l||null,route:i||null,at:new Date(t()).toISOString()},...f.filter(c=>c.login!==o)].slice(0,A)},people(o,l){return l.map(i=>({login:i,name:n[i]?.name||null,now:s.has(i)&&r.get(i)===o,at:n[i]?.at?.[o]||null}))},refusals(o,l){const i=t()-D;return f.filter(c=>Date.parse(c.at)>=i&&(c.route===o||c.route===null)&&!l.includes(c.login)).map(({login:c,name:p,at:d})=>({login:c,name:p,at:d}))}}}u(H,"shareTracker");export{F as routeOfPath,m as shareEntries,L as shareOf,U as shareReach,H as shareTracker,T as sharedRoutes,B as sharedSidebar,b as validLogin};
package/hub/slack.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var v=Object.defineProperty;var n=(r,e)=>v(r,"name",{value:e,configurable:!0});import u from"node:fs";import d from"node:path";import p from"node:os";import{withFileLock as x}from"./locks.mjs";import{readSecret as $,writeSecret as f}from"./secrets.mjs";const l=["self_dm","owner_slack_user","slack_domain","timezone"],k=n(()=>d.join(p.homedir(),".claude","kickflip","slack.json"),"fileOf"),w=n(r=>{try{return JSON.parse(u.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),S=n(r=>Object.fromEntries(l.map(e=>[e,typeof r?.[e]=="string"?r[e]:""])),"identityOf");function y(r){const e=k();u.mkdirSync(d.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;return u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
- `,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(y,"save");function _(){const r=S(w(k()));if(l.some(t=>r[t]))return r;const e=S(w(d.join(p.homedir(),".claude","slack-bridge","state.json")).config);return l.some(t=>e[t])?y(e):r}n(_,"readSlack");async function I(r){for(const[e,t]of Object.entries(r)){if(!l.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return x(k(),()=>y({..._(),...r}))}n(I,"setSlack");function N(r=new Date){const e=_().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:a})=>[c,a]));return{date:`${t.year}-${t.month}-${t.day}`,morning:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9}}n(N,"slackDay");class m extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(a,i={}){let o;try{const s=await e(`${E()}/${a}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await s.json().catch(()=>null),!o)throw new Error(`Slack answered ${s.status}`)}catch(s){throw new m(`Could not reach Slack: ${s.message}`)}if(O.includes(o.error))throw t(!1),new m("Slack refused the token.",400);if(t(!0),!o.ok)throw new m(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:a,team_id:i,user:o,user_id:s}=await c("auth.test");return{team:a,team_id:i,user:o,user_id:s}},async people(){const a=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const s of o.members)!s.deleted&&!s.is_bot&&s.id!=="USLACKBOT"&&a.push({id:s.id,name:s.profile?.real_name||s.real_name||s.name});i=o.response_metadata?.next_cursor||""}while(i);return a.sort((o,s)=>o.name.localeCompare(s.name))},openDM:n(async a=>(await c("conversations.open",{users:a})).channel.id,"openDM"),post:n(async({channel:a,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:a,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:a,ts:i,name:o}){try{await c("reactions.add",{channel:a,timestamp:i,name:o})}catch(s){if(s.message!=="Slack: already_reacted")throw s}}}}n(g,"slackClient");const h=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function b(r){const e=h(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(b,"slackStatus");const K="Slack needs Hub\u2019s Slack token; add it in Hub settings \u203A Slack first.";async function L(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new m("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let a=!1;const i=await g(t,{fetch:e,checked:n(o=>{a=!o},"checked")}).who().catch(o=>{if(!a)throw o;return null});return f("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),b()}n(L,"saveSlackToken");function U(){return f("slack",null),b()}n(U,"removeSlackToken");function D(r,e){const t=h();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,...a}=t;f("slack",e?a:{...a,refused_at:new Date().toISOString()})}n(D,"markChecked");function Z({fetch:r}={}){const e=h();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(Z,"savedSlack");export{K as SLACK_NEEDS_TOKEN,m as SlackError,_ as readSlack,U as removeSlackToken,L as saveSlackToken,Z as savedSlack,I as setSlack,g as slackClient,N as slackDay,b as slackStatus};
1
+ var b=Object.defineProperty;var n=(r,e)=>b(r,"name",{value:e,configurable:!0});import u from"node:fs";import h from"node:path";import k from"node:os";import{withFileLock as v}from"./locks.mjs";import{readSecret as $,writeSecret as d}from"./secrets.mjs";const m=["self_dm","owner_slack_user","slack_domain","timezone"],p=n(()=>h.join(k.homedir(),".claude","kickflip","slack.json"),"fileOf"),w=n(r=>{try{return JSON.parse(u.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),S=n(r=>Object.fromEntries(m.map(e=>[e,typeof r?.[e]=="string"?r[e]:""])),"identityOf");function _(r){const e=p();u.mkdirSync(h.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;return u.writeFileSync(t,JSON.stringify(r,null,2)+`
2
+ `,{mode:384}),u.chmodSync(t,384),u.renameSync(t,e),r}n(_,"save");function y(){const r=S(w(p()));if(m.some(t=>r[t]))return r;const e=S(w(h.join(k.homedir(),".claude","slack-bridge","state.json")).config);return m.some(t=>e[t])?_(e):r}n(y,"readSlack");async function F(r){for(const[e,t]of Object.entries(r)){if(!m.includes(e)||typeof t!="string")throw new Error(`Unknown Slack setting ${e}`);if(e==="self_dm"&&t&&!/^D[A-Z0-9]+$/.test(t))throw new Error("self-dm must be a DM channel id");if(e==="owner_slack_user"&&t&&!/^U[A-Z0-9]+$/.test(t))throw new Error("user must be a Slack user id");if(e==="slack_domain"&&t&&!/^[a-z0-9][a-z0-9-]*$/.test(t))throw new Error("domain must be a Slack workspace domain");if(e==="timezone"&&t)try{new Intl.DateTimeFormat("en",{timeZone:t})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return v(p(),()=>_({...y(),...r}))}n(F,"setSlack");function N(r=new Date){const e=y().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,t=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:c,value:s})=>[c,s]));return{date:`${t.year}-${t.month}-${t.day}`,open:!["Sat","Sun"].includes(t.weekday)&&Number(t.hour)>=9&&Number(t.hour)<17}}n(N,"slackDay");class f extends Error{static{n(this,"SlackError")}constructor(e,t=502){super(e),this.status=t}}const E=n(()=>process.env.KICKFLIP_SLACK_URL||"https://slack.com/api","api"),O=["invalid_auth","not_authed","token_revoked","token_expired","account_inactive"];function g(r,{fetch:e=globalThis.fetch,checked:t=n(()=>{},"checked")}={}){async function c(s,i={}){let o;try{const a=await e(`${E()}/${s}`,{method:"POST",headers:{"content-type":"application/json; charset=utf-8",authorization:`Bearer ${r}`},body:JSON.stringify(i),signal:AbortSignal.timeout(15e3)});if(o=await a.json().catch(()=>null),!o)throw new Error(`Slack answered ${a.status}`)}catch(a){throw new f(`Could not reach Slack: ${a.message}`)}if(O.includes(o.error))throw t(!1),new f("Slack refused the token.",400);if(t(!0),!o.ok)throw new f(`Slack: ${o.error}`);return o}return n(c,"call"),{async who(){const{team:s,team_id:i,user:o,user_id:a}=await c("auth.test");return{team:s,team_id:i,user:o,user_id:a}},async people(){const s=[];let i="";do{const o=await c("users.list",{limit:200,...i?{cursor:i}:{}});for(const a of o.members)!a.deleted&&!a.is_bot&&a.id!=="USLACKBOT"&&s.push({id:a.id,name:a.profile?.real_name||a.real_name||a.name});i=o.response_metadata?.next_cursor||""}while(i);return s.sort((o,a)=>o.name.localeCompare(a.name))},openDM:n(async s=>(await c("conversations.open",{users:s})).channel.id,"openDM"),post:n(async({channel:s,text:i,thread_ts:o})=>(await c("chat.postMessage",{channel:s,text:i,unfurl_links:!1,...o?{thread_ts:o}:{}})).ts,"post"),async react({channel:s,ts:i,name:o}){try{await c("reactions.add",{channel:s,timestamp:i,name:o})}catch(a){if(a.message!=="Slack: already_reacted")throw a}}}}n(g,"slackClient");const l=n(r=>{const e=$("slack",r);return typeof e?.token=="string"&&e.token?e:null},"savedToken");function x(r){const e=l(r);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.team?{team:e.team,user:e.user}:{}}:{set:!1}}n(x,"slackStatus");const U="Slack needs Hub\u2019s Slack token; add it in Hub settings \u203A Slack first.";async function K(r,{fetch:e}={}){const t=typeof r=="string"?r.trim():"";if(!/^xoxp-[\w-]{10,250}$/.test(t))throw new f("Paste a Slack user token, which starts with xoxp-.",400);const c=new Date().toISOString();let s=!1;const i=await g(t,{fetch:e,checked:n(o=>{s=!o},"checked")}).who().catch(o=>{if(!s)throw o;return null});return d("slack",i?{token:t,...i,saved_at:c}:{token:t,saved_at:c,refused_at:c}),x()}n(K,"saveSlackToken");function L(){return d("slack",null),x()}n(L,"removeSlackToken");function D(r,e){const t=l();if(t?.token!==r||!!t.refused_at==!e)return;const{refused_at:c,refusal_pushed_at:s,...i}=t;d("slack",e?i:{...i,refused_at:new Date().toISOString()})}n(D,"markChecked");const P=n(()=>{const r=l();return!!(r?.refused_at&&!r.refusal_pushed_at)},"refusalUnpushed");function Z(){const r=l();r?.refused_at&&d("slack",{...r,refusal_pushed_at:new Date().toISOString()})}n(Z,"markRefusalPushed");function B({fetch:r}={}){const e=l();return e?g(e.token,{fetch:r,checked:n(t=>D(e.token,t),"checked")}):null}n(B,"savedSlack");export{U as SLACK_NEEDS_TOKEN,f as SlackError,Z as markRefusalPushed,y as readSlack,P as refusalUnpushed,L as removeSlackToken,K as saveSlackToken,B as savedSlack,F as setSlack,g as slackClient,N as slackDay,x as slackStatus};